Merge remote-tracking branch 'cppcooper/unit-testing' into myk_ctest

develop
myk002 2022-11-28 17:25:10 -08:00
commit f921b02a1b
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
76 changed files with 5653 additions and 3329 deletions

@ -83,7 +83,7 @@ jobs:
-B build-ci \
-G Ninja \
-DDFHACK_BUILD_ARCH=64 \
-DBUILD_TESTS:BOOL=ON \
-DBUILD_TESTING:BOOL=ON \
-DBUILD_DEV_PLUGINS:BOOL=${{ matrix.plugins == 'all' }} \
-DBUILD_SIZECHECK:BOOL=${{ matrix.plugins == 'all' }} \
-DBUILD_SKELETON:BOOL=${{ matrix.plugins == 'all' }} \
@ -96,8 +96,15 @@ jobs:
run: |
ninja -C build-ci install
ccache --show-stats
- name: Run tests
id: run_tests
- name: Run unit tests
id: run_tests1
run: |
if build-ci/library/tests/test-library; then
exit 0
fi
exit 1
- name: Run lua tests
id: run_tests2
run: |
export TERM=dumb
status=0

3
.gitmodules vendored

@ -28,3 +28,6 @@
[submodule "depends/luacov"]
path = depends/luacov
url = ../../DFHack/luacov.git
[submodule "depends/googletest"]
path = depends/googletest
url = ../../google/googletest.git

@ -74,6 +74,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(MSVC)
# increase warning level and treat warnings as errors
add_definitions("/WX")
add_definitions("/W3")
# disable C4819 code-page warning
add_definitions("/wd4819")
@ -390,10 +394,11 @@ else()
endif()
endif()
find_package(ZLIB REQUIRED)
include_directories(depends/protobuf)
include_directories(depends/lua/include)
include_directories(depends/md5)
if(BUILD_TESTING)
message("BUILD TESTS: Core, Scripts")
set(BUILD_SCRIPT_TESTS ON FORCE)
set(BUILD_CORE_TESTS ON FORCE)
endif()
# Support linking against external tinyxml
# If we find an external tinyxml, set the DFHACK_TINYXML variable to "tinyxml"
@ -410,6 +415,11 @@ else()
set(DFHACK_TINYXML "dfhack-tinyxml")
endif()
find_package(ZLIB REQUIRED)
include_directories(depends/protobuf)
include_directories(depends/lua/include)
include_directories(depends/md5)
include_directories(depends/lodepng)
include_directories(depends/tthread)
include_directories(${ZLIB_INCLUDE_DIRS})
@ -417,6 +427,43 @@ include_directories(depends/clsocket/src)
include_directories(depends/xlsxio/include)
add_subdirectory(depends)
# Testing with CTest
if(BUILD_TESTING OR BUILD_CORE_TESTS)
macro(dfhack_test name files)
message("dfhack_test(${name}, ${files})")
add_executable(${name} ${files})
target_include_directories(${name} PUBLIC depends/googletest/googletest/include)
target_link_libraries(${name} dfhack gtest)
set_target_properties(${name} PROPERTIES COMPILE_FLAGS "-Wno-sign-compare")
add_test(NAME ${name} COMMAND ${name})
endmacro()
include(CTest)
endif()
include(CMakeDependentOption)
cmake_dependent_option(
BUILD_SCRIPT_TESTS "Install integration tests in hack/scripts/test" OFF
"BUILD_TESTING" OFF)
mark_as_advanced(FORCE BUILD_TESTS)
# Handle deprecated BUILD_TESTS option
option(BUILD_TESTS "Deprecated option; please use BUILD_SCRIPT_TESTS=ON" OFF)
if(BUILD_TESTING OR BUILD_SCRIPT_TESTS)
if(EXISTS "${dfhack_SOURCE_DIR}/test/scripts")
message(SEND_ERROR "test/scripts must not exist in the dfhack repo since it would conflict with the tests installed from the scripts repo.")
endif()
install(DIRECTORY ${dfhack_SOURCE_DIR}/test
DESTINATION ${DFHACK_DATA_DESTINATION}/scripts)
install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts)
else()
add_custom_target(test
COMMENT "Nothing to do: CMake option BUILD_TESTING is OFF"
# Portable NOOP; need to put something here or the comment isn't displayed
COMMAND cd
)
endif()
find_package(Git REQUIRED)
if(NOT GIT_FOUND)
message(SEND_ERROR "could not find git")

@ -97,7 +97,7 @@ class TrailingWhitespaceLinter(Linter):
msg = 'Contains trailing whitespace'
def check_line(self, line):
line = line.replace('\r', '').replace('\n', '')
return not line.strip() or line == line.rstrip('\t ')
return line == line.rstrip('\t ')
def fix_line(self, line):
return line.rstrip('\t ')

@ -256,6 +256,10 @@ html_favicon = 'docs/styles/dfhack-icon.ico'
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['docs/styles']
# A list of paths that contain extra files not directly related to the
# documentation.
html_extra_path = ['robots.txt']
# Custom sidebar templates, maps document names to template names.
html_sidebars = {
'**': [

@ -21,7 +21,7 @@ install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/
FILES_MATCHING PATTERN "*"
PATTERN blueprints/library/test EXCLUDE)
if(BUILD_TESTS)
if(BUILD_SCRIPT_TESTS)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/blueprints/library/test/
DESTINATION blueprints/library/test
)

@ -1,5 +1,5 @@
This folder contains blueprints that can be applied by the `quickfort` script. For more information, see:
* [Quickfort command reference](https://docs.dfhack.org/en/stable/docs/_auto/base.html#quickfort)
* [Quickfort command reference](https://docs.dfhack.org/en/stable/docs/tools/quickfort.html)
* [Quickfort blueprint guide](https://docs.dfhack.org/en/stable/docs/guides/quickfort-user-guide.html)
* [Quickfort library guide](https://docs.dfhack.org/en/stable/docs/guides/quickfort-library-guide.html)

@ -314,7 +314,7 @@ Here are some tips and procedures for handling seiges -- including how to clean
""
"After a siege, you can use the caged prisoners to safely train your military. Here's how:"
""
"- Once the prisoners are hauled to the ""prisoner quantum"" stockpile, run ""stripcaged all"" in the DFHack console."
"- Once the prisoners are hauled to the ""prisoner quantum"" stockpile, run ""unforbid all"" and ""stripcaged all"" in the DFHack console (or GUI launcher)."
""
"- After all the prisoners' items have been confiscated, bring your military dwarves to the barracks (if they aren't already there)."
""
@ -2292,7 +2292,7 @@ query_jail/services_query_jail
,`,`,`,`,`,`,`,`,`,`,`,,`,A,A,A,`,A,A,A,`,,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,r,`,`,`,`,,,,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,d,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,trackstopN,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,,`,,`,,b
,`,`,`,`,`,`,`,`,`,`,`,,,,,d,,d,,,,,`,,`,,`,,`
,`,`,`,`,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,t,`,`,`,`,R
@ -2327,9 +2327,9 @@ query_jail/services_query_jail
,`,`,`,`,`,`,`,`,`,`,`,,,,,,`,,,,,,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,,,`
,`,`,`,`,`,`,`,`,`,`,`,,`,z(7x3),,,`,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,c,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,,`,,`,,`
,`,`,`,`,`,`,`,`,`,`,`,,`,z(7x1),,,`,`,`,`,`,,`,,`,,`,,`
,`,`,`,`,`,`,`,`,`,`,`,,,,,`,,`,,,,,`,,`,,`,,`
,`,`,`,`,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`
@ -2409,7 +2409,7 @@ query_jail/services_query_jail
,,,,,,,,,,,,,,,`,`,`,`,`
,,,,,,,,,,,,,,,`,`,`,`,`
#query label(services_query_stockpiles) start(18; 18) hidden() message(Configure the training ammo stockpile to take from the metalworker quantum on the industry level.) configure stockpiles
"#query label(services_query_stockpiles) start(18; 18) hidden() message(Configure the training ammo stockpile to take from the metalworker quantum on the industry level. Assign a minecart to the training ammo quantum dump with ""assign-minecarts all"") configure stockpiles"
,`,`,`,,`,`,`,,`,`,`,,`,`,`,,`,,`,`,`
,`,`,`,,`,`,`,,`,`,`,,`,`,`,`,`,`,`,`,`
@ -2421,9 +2421,9 @@ query_jail/services_query_jail
,`,`,`,`,`,`,`,`,`,`,`,,,,,,`,,,,,,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,,,`
,`,`,`,`,`,`,`,`,`,`,`,,`,nocontainers,{bolts}{forbidmetalbolts}{forbidartifactammo},"{givename name=""training bolts""}",`,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,`,`,`,`,`,,`,,`,,`,,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,"{quantum name=""training quantum""}",`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,`,`,`,"{quantumstopfromsouth name=""Training quantum""}{givename name=""training dumper""}",`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,,`,nocontainers,{bolts}{forbidmetalbolts}{forbidartifactammo},"{givename name=""training bolts""}",`,`,`,`,`,,`,,`,,`,,`
,`,`,`,`,`,`,`,`,`,`,`,,,,,`,,`,,,,,`,,`,,`,,`
,`,`,`,`,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,`,`,`
,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`

Can't render this file because it has a wrong number of fields in line 56.

@ -5,73 +5,65 @@
# dfhack-config/init/dfhack.init
###################
# Global bindings #
# global bindings #
###################
# the GUI command launcher (two bindings since some keyboards don't have `)
keybinding add ` gui/launcher
keybinding add Ctrl-Shift-D gui/launcher
# show all current key bindings
keybinding add Ctrl-F1 hotkeys
keybinding add Alt-F1 hotkeys
# show hotkey popup menu
keybinding add Ctrl-Shift-C hotkeys
# a dfhack prompt in df. Sublime text like.
keybinding add Ctrl-Shift-P command-prompt
# on-screen keyboard
keybinding add Ctrl-Shift-K gui/cp437-table
##############################
# Generic dwarfmode bindings #
##############################
# an in-game init file editor
keybinding add Alt-S@title|dwarfmode/Default|dungeonmode gui/settings-manager
# toggle the display of water level as 1-7 tiles
keybinding add Ctrl-W twaterlvl
# with cursor:
######################
# dwarfmode bindings #
######################
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
# toggle the display of water level as 1-7 tiles
keybinding add Ctrl-W@dwarfmode|dungeonmode twaterlvl
# designate the whole vein for digging
keybinding add Ctrl-V digv
keybinding add Ctrl-Shift-V "digv x"
keybinding add Ctrl-V@dwarfmode digv
keybinding add Ctrl-Shift-V@dwarfmode "digv x"
# clean the selected tile of blood etc
keybinding add Ctrl-C spotclean
# destroy items designated for dump in the selected tile
keybinding add Ctrl-Shift-K autodump-destroy-here
# set the zone or cage under the cursor as the default
keybinding add Alt-Shift-I@dwarfmode/Zones "zone set"
# with an item selected:
# destroy the selected item
keybinding add Ctrl-K autodump-destroy-item
keybinding add Ctrl-K@dwarfmode autodump-destroy-item
# scripts:
# quicksave, only in main dwarfmode screen and menu page
keybinding add Ctrl-Alt-S@dwarfmode/Default quicksave
# destroy items designated for dump in the selected tile
keybinding add Ctrl-Shift-K@dwarfmode autodump-destroy-here
# apply blueprints to the map (Alt-F for compatibility with LNP Quickfort)
keybinding add Ctrl-Shift-Q@dwarfmode gui/quickfort
keybinding add Alt-F@dwarfmode gui/quickfort
# gui/rename script - rename units and buildings
keybinding add Ctrl-Shift-N gui/rename
keybinding add Ctrl-Shift-T "gui/rename unit-profession"
# a dfhack prompt in df. Sublime text like.
keybinding add Ctrl-Shift-P command-prompt
# show information collected by dwarfmonitor
keybinding add Alt-M@dwarfmode/Default "dwarfmonitor prefs"
keybinding add Ctrl-F@dwarfmode/Default "dwarfmonitor stats"
# export a Dwarf's preferences screen in BBCode to post to a forum
keybinding add Ctrl-Shift-F@dwarfmode forum-dwarves
# set the zone or cage under the cursor as the default
keybinding add Alt-Shift-I@dwarfmode/Zones "zone set"
# an in-game init file editor
keybinding add Alt-S@title gui/settings-manager
keybinding add Alt-S@dwarfmode/Default gui/settings-manager
# Stocks plugin
keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show"
# open an overview window summarising some stocks (dfstatus)
keybinding add Ctrl-Shift-I@dwarfmode/Default|dfhack/lua/dfstatus gui/dfstatus
# change quantity of manager orders
keybinding add Alt-Q@jobmanagement/Main gui/manager-quantity
@ -79,51 +71,23 @@ keybinding add Alt-Q@jobmanagement/Main gui/manager-quantity
# re-check manager orders
keybinding add Alt-R@jobmanagement/Main workorder-recheck
# workorder detail configuration
# set workorder item details (on workorder details screen press D again)
keybinding add D@workquota_details gui/workorder-details
# view combat reports for the selected unit/corpse/spatter
keybinding add Ctrl-Shift-R view-unit-reports
keybinding add Ctrl-Shift-R@dwarfmode|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|workshop_profile|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit|buildinglist|workshop_profile view-unit-reports
# view extra unit information
keybinding add Alt-I@dwarfmode/ViewUnits|unitlist gui/unit-info-viewer
# set workorder item details (on workorder details screen press D again)
keybinding add D@workquota_details gui/workorder-details
# boost priority of jobs related to the selected entity
keybinding add Alt-N do-job-now
##############################
# Generic adv mode bindings #
##############################
keybinding add Ctrl-B@dungeonmode adv-bodyswap
keybinding add Ctrl-Shift-B@dungeonmode "adv-bodyswap force"
keybinding add Shift-O@dungeonmode gui/companion-order
keybinding add Ctrl-T@dungeonmode gui/advfort
keybinding add Ctrl-A@dungeonmode/ConversationSpeak adv-rumors
##############################
# Generic legends bindings #
##############################
# export all information, or just the detailed maps (doesn't handle site maps)
keybinding add Ctrl-A@legends "exportlegends all"
#############################
# Context-specific bindings #
#############################
# Stocks plugin
keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show"
keybinding add Alt-N@dwarfmode|job|joblist|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit|buildinglist|textviewer|item|layer_assigntrade|tradegoods|store|assign_display_item|treasurelist do-job-now
# open an overview window summarising some stocks (dfstatus)
keybinding add Ctrl-Shift-I@dwarfmode/Default "gui/dfstatus"
keybinding add Ctrl-Shift-I@dfhack/lua/dfstatus "gui/dfstatus"
# export a Dwarf's preferences screen in BBCode to post to a forum
keybinding add Ctrl-Shift-F@textviewer forum-dwarves
# q->stockpile - copy & paste stockpiles
keybinding add Alt-P copystock
keybinding add Alt-P@dwarfmode/QueryBuilding/Some/Stockpile copystock
# q->stockpile - load and save stockpile settings out of game
keybinding add Alt-L@dwarfmode/QueryBuilding/Some/Stockpile "gui/stockpiles -load"
@ -183,7 +147,30 @@ keybinding add Alt-W@overallstatus "gui/workflow status"
keybinding add Alt-W@dfhack/lua/status_overlay "gui/workflow status"
# autobutcher front-end
keybinding add Shift-B@pet/List/Unit "gui/autobutcher"
keybinding add Shift-B@pet/List/Unit gui/autobutcher
# view pathable tiles from active cursor
keybinding add Alt-Shift-P@dwarfmode/LookAround gui/pathable
# gui/rename script - rename units and buildings
keybinding add Ctrl-Shift-N@dwarfmode|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|workshop_profile|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit|buildinglist gui/rename
keybinding add Ctrl-Shift-T@dwarfmode|unit|unitlist|joblist|dungeon_monsterstatus|layer_unit_relationship|item|workshop_profile|layer_noblelist|locations|pets|layer_overall_health|textviewer|reportlist|announcelist|layer_military|layer_unit_health|customize_unit "gui/rename unit-profession"
#####################
# adv mode bindings #
#####################
keybinding add Ctrl-A@dungeonmode/ConversationSpeak adv-rumors
keybinding add Ctrl-B@dungeonmode adv-bodyswap
keybinding add Ctrl-Shift-B@dungeonmode "adv-bodyswap force"
keybinding add Shift-O@dungeonmode gui/companion-order
keybinding add Ctrl-T@dungeonmode gui/advfort
#########################
# legends mode bindings #
#########################
# export all information, or just the detailed maps (doesn't handle site maps)
keybinding add Ctrl-A@legends "exportlegends all"

@ -89,6 +89,7 @@ enable automaterial
# Other interface improvement tools
enable \
overlay \
confirm \
dwarfmonitor \
mousequery \
@ -97,7 +98,6 @@ enable \
automelt \
autotrade \
buildingplan \
resume \
trackstop \
zone \
stocks \

@ -1004,7 +1004,8 @@
"value" : 2
}
],
"job" : "MakeCrafts",
"item_subtype" : "ITEM_PANTS_LEGGINGS",
"job" : "MakePants",
"material_category" :
[
"shell"

@ -3,6 +3,10 @@ add_subdirectory(lodepng)
add_subdirectory(lua)
add_subdirectory(md5)
add_subdirectory(protobuf)
if(BUILD_CORE_TESTS)
add_subdirectory(googletest EXCLUDE_FROM_ALL)
set_target_properties(gtest PROPERTIES COMPILE_FLAGS "-Wno-maybe-uninitialized -Wno-sign-compare")
endif()
# Don't build tinyxml if it's being externally linked against.
if(NOT TinyXML_FOUND)

@ -0,0 +1 @@
Subproject commit 2fe3bd994b3189899d93f1d5a881e725e046fdc2

@ -1,21 +1,3 @@
{
"widgets": [
{
"type": "weather",
"x": 22,
"y": -1
},
{
"type": "date",
"x": -30,
"y": 0,
"format": "Y-M-D"
},
{
"type": "misery",
"x": -2,
"y": -1,
"anchor": "right"
}
]
"date_format": "Y-M-D"
}

@ -0,0 +1,17 @@
{
"dwarfmonitor.date": {
"enabled": true
},
"dwarfmonitor.misery": {
"enabled": true
},
"dwarfmonitor.weather": {
"enabled": true
},
"hotkeys.menu": {
"enabled": true
},
"unsuspend.overlay": {
"enabled": true
}
}

@ -1019,17 +1019,26 @@ Fortress mode
Resets the fortress mode sidebar menus and cursors to their default state. If
``pause`` is true, also pauses the game.
* ``dfhack.gui.revealInDwarfmodeMap(pos)``
* ``dfhack.gui.pauseRecenter(pos[,pause])``
``dfhack.gui.pauseRecenter(x,y,z[,pause])``
Centers the view on the given position, which can be a ``df.coord`` instance
or a table assignable to a ``df.coord`` (see `lua-api-table-assignment`),
Same as ``resetDwarfmodeView``, but also recenter if position is valid. If ``pause`` is false, skip pausing. Respects
``RECENTER_INTERFACE_SHUTDOWN_MS`` in DF's ``init.txt`` (the delay before input is recognized when a recenter occurs.)
* ``dfhack.gui.revealInDwarfmodeMap(pos[,center])``
``dfhack.gui.revealInDwarfmodeMap(x,y,z[,center])``
Centers the view on the given coordinates. If ``center`` is true, make sure the
position is in the exact center of the view, else just bring it on screen.
``pos`` can be a ``df.coord`` instance or a table assignable to a ``df.coord`` (see `lua-api-table-assignment`),
e.g.::
{x = 5, y = 7, z = 11}
getSelectedUnit().pos
copyall(df.global.cursor)
Returns false if unsuccessful.
If the position is invalid, the function will simply ensure the current window position is clamped between valid values.
* ``dfhack.gui.refreshSidebar()``
@ -1088,6 +1097,19 @@ Announcements
Uses the type to look up options from announcements.txt, and calls the above
operations accordingly. The units are used to call ``addCombatReportAuto``.
* ``dfhack.gui.autoDFAnnouncement(report,text)``
``dfhack.gui.autoDFAnnouncement(type,pos,text,color[,is_bright,unit1,unit2,is_sparring])``
Takes a ``df.report_init`` (see: `structure definition <https://github.com/DFHack/df-structures/blob/master/df.announcements.xml>`_)
and a string and processes them just like DF does. Can also be built from parameters instead of a ``report_init``.
Setting ``is_sparring`` to *true* means the report will be added to sparring logs (if applicable) rather than hunting or combat.
The announcement will not display if units are involved and the player can't see them (or hear, for adventure mode sound announcement types.)
Text is parsed using ``&`` as an escape character, with ``&r`` adding a blank line (equivalent to ``\n \n``,)
``&&`` being just ``&``, and any other combination causing neither character to display.
If you want a guaranteed announcement without parsing, use ``dfhack.gui.showAutoAnnouncement`` instead.
* ``dfhack.gui.getMousePos()``
Returns the map coordinates of the map tile the mouse is over as a table of
@ -1188,6 +1210,187 @@ Job module
Units module
------------
* ``dfhack.units.isUnitInBox(unit,x1,y1,z1,x2,y2,z2)``
The unit is within the specified coordinates.
* ``dfhack.units.isActive(unit)``
The unit is active (alive and on the map).
* ``dfhack.units.isVisible(unit)``
The unit is visible on the map.
* ``dfhack.units.isCitizen(unit[,ignore_sanity])``
The unit is an alive sane citizen of the fortress; wraps the
same checks the game uses to decide game-over by extinction,
with an additional sanity check. You can identify citizens,
regardless of their sanity, by passing ``true`` as the optional
second parameter.
* ``dfhack.units.isFortControlled(unit)``
Similar to ``dfhack.units.isCitizen(unit)``, but is based on checks
for units hidden in ambush, and includes tame animals. Returns *false*
if not in fort mode.
* ``dfhack.units.isOwnCiv(unit)``
The unit belongs to the player's civilization.
* ``dfhack.units.isOwnGroup(unit)``
The unit belongs to the player's group.
* ``dfhack.units.isOwnRace(unit)``
The unit belongs to the player's race.
* ``dfhack.units.isAlive(unit)``
The unit isn't dead or undead.
* ``dfhack.units.isDead(unit)``
The unit is completely dead and passive, or a ghost. Equivalent to
``dfhack.units.isKilled(unit) or dfhack.units.isGhost(unit)``.
* ``dfhack.units.isKilled(unit)``
The unit has been killed.
* ``dfhack.units.isSane(unit)``
The unit is capable of rational action, i.e. not dead, insane, zombie, or active werewolf.
* ``dfhack.units.isCrazed``
The unit is berserk and will attack all other creatures except members of its own species
that are also crazed. (can be modified by curses)
* ``dfhack.units.isGhost(unit)``
The unit is a ghost.
* ``dfhack.units.isHidden(unit)``
The unit is hidden to the player, accounting for sneaking. Works for any game mode.
* ``dfhack.units.isHidingCurse(unit)``
The unit is hiding a curse.
* ``dfhack.units.isMale(unit)``
* ``dfhack.units.isFemale(unit)``
* ``dfhack.units.isBaby(unit)``
* ``dfhack.units.isChild(unit)``
* ``dfhack.units.isAdult(unit)``
* ``dfhack.units.isGay(unit)``
* ``dfhack.units.isNaked(unit)``
Simple unit property checks
* ``dfhack.units.isVisiting(unit)``
The unit is visiting. eg. Merchants, Diplomatics, travelers.
* ``dfhack.units.isTrainableHunting(unit)``
The unit is trainable for hunting.
* ``dfhack.units.isTrainableWar(unit)``
The unit is trainable for war.
* ``dfhack.units.isTrained(unit)``
The unit is trained.
* ``dfhack.units.isHunter(unit)``
The unit is a trained hunter.
* ``dfhack.units.isWar(unit)``
The unit is trained for war.
* ``dfhack.units.isTame(unit)``
* ``dfhack.units.isTamable(unit)``
* ``dfhack.units.isDomesticated(unit)``
* ``dfhack.units.isMarkedForSlaughter(unit)``
* ``dfhack.units.isGelded(unit)``
* ``dfhack.units.isEggLayer(unit)``
* ``dfhack.units.isGrazer(unit)``
* ``dfhack.units.isMilkable(unit)``
Simple unit property checks.
* ``dfhack.units.isForest(unit)``
The unit is of the forest.
* ``dfhack.units.isMischievous(unit)``
The unit is mischievous.
* ``dfhack.units.isAvailableForAdoption(unit)``
The unit is available for adoption.
* ``dfhack.units.isOpposedToLife(unit)``
* ``dfhack.units.hasExtravision(unit)``
* ``dfhack.units.isBloodsucker(unit)``
Simple checks of caste attributes that can be modified by curses.
* ``dfhack.units.isDwarf(unit)``
The unit is of the correct race for the fortress.
* ``dfhack.units.isAnimal(unit)``
* ``dfhack.units.isMerchant(unit)``
* ``dfhack.units.isDiplomat(unit)``
Simple unit type checks.
* ``dfhack.units.isVisitor(unit)``
The unit is a regular visitor with no special purpose (eg. merchant).
* ``dfhack.units.isInvader(unit)``
The unit is an active invader or marauder.
* ``dfhack.units.isUndead(unit[,include_vamps])``
The unit is undead. Pass ``true`` as the optional second parameter to
count vampires as undead.
* ``dfhack.units.isNightCreature(unit)``
* ``dfhack.units.isSemiMegabeast(unit)``
* ``dfhack.units.isMegabeast(unit)``
* ``dfhack.units.isTitan(unit)``
* ``dfhack.units.isDemon(unit)``
Simple enemy type checks.
* ``dfhack.units.isDanger(unit)``
The unit is dangerous, and probably hostile. This includes
Great Dangers (see below), semi-megabeasts, night creatures,
undead, invaders, and crazed units.
* ``dfhack.units.isGreatDanger(unit)``
The unit is of Great Danger. This include demons, titans, and megabeasts.
* ``dfhack.units.getPosition(unit)``
Returns true *x,y,z* of the unit, or *nil* if invalid; may be not equal to unit.pos if caged.
@ -1238,72 +1441,15 @@ Units module
Returns the nemesis record of the unit if it has one, or *nil*.
* ``dfhack.units.isHidingCurse(unit)``
Checks if the unit hides improved attributes from its curse.
* ``dfhack.units.getPhysicalAttrValue(unit, attr_type)``
* ``dfhack.units.getMentalAttrValue(unit, attr_type)``
Computes the effective attribute value, including curse effect.
* ``dfhack.units.isCrazed(unit)``
* ``dfhack.units.isOpposedToLife(unit)``
* ``dfhack.units.hasExtravision(unit)``
* ``dfhack.units.isBloodsucker(unit)``
Simple checks of caste attributes that can be modified by curses.
* ``dfhack.units.getMiscTrait(unit, type[, create])``
Finds (or creates if requested) a misc trait object with the given id.
* ``dfhack.units.isActive(unit)``
The unit is active (alive and on the map).
* ``dfhack.units.isAlive(unit)``
The unit isn't dead or undead.
* ``dfhack.units.isDead(unit)``
The unit is completely dead and passive, or a ghost. Equivalent to
``dfhack.units.isKilled(unit) or dfhack.units.isGhost(unit)``.
* ``dfhack.units.isKilled(unit)``
The unit has been killed.
* ``dfhack.units.isGhost(unit)``
The unit is a ghost.
* ``dfhack.units.isSane(unit)``
The unit is capable of rational action, i.e. not dead, insane, zombie, or active werewolf.
* ``dfhack.units.isDwarf(unit)``
The unit is of the correct race of the fortress.
* ``dfhack.units.isCitizen(unit)``
The unit is an alive sane citizen of the fortress; wraps the
same checks the game uses to decide game-over by extinction.
* ``dfhack.units.isFortControlled(unit)``
Similar to ``dfhack.units.isCitizen(unit)``, but is based on checks for units hidden in ambush, and includes tame animals. Returns *false* if not in fort mode.
* ``dfhack.units.isVisible(unit)``
The unit is visible on the map.
* ``dfhack.units.isHidden(unit)``
The unit is hidden to the player, accounting for sneaking. Works for any game mode.
* ``dfhack.units.getAge(unit[,true_age])``
Returns the age of the unit in years as a floating-point value.
@ -3898,6 +4044,8 @@ gui.widgets
This module implements some basic widgets based on the View infrastructure.
.. _widget:
Widget class
------------

@ -118,6 +118,14 @@ Tool that warned the user when the ``dfhack.init`` file did not exist. Now that
``dfhack.init`` is autogenerated in ``dfhack-config/init``, this warning is no
longer necessary.
.. _resume:
resume
======
Allowed you to resume suspended jobs and displayed an overlay indicating
suspended building construction jobs. Replaced by `unsuspend` script.
.. _warn-stuck-trees:
warn-stuck-trees

@ -7,10 +7,10 @@ keybinding
Like any other command, it can be used at any time from the console, but
bindings are not remembered between runs of the game unless re-created in
`dfhack.init`.
:file:`dfhack-config/init/dfhack.init`.
Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or
\` (the key below the :kbd:`Esc` key.
Hotkeys can be any combinations of Ctrl/Alt/Shift with A-Z, 0-9, F1-F12, or `
(the key below the :kbd:`Esc` key on most keyboards).
Usage
-----
@ -21,16 +21,17 @@ Usage
List bindings active for the key combination.
``keybinding clear <key> [<key>...]``
Remove bindings for the specified keys.
``keybinding add <key> "cmdline" ["cmdline"...]``
``keybinding add <key> "<cmdline>" ["<cmdline>" ...]``
Add bindings for the specified key.
``keybinding set <key> "cmdline" ["cmdline"...]``
``keybinding set <key> "<cmdline>" ["<cmdline>" ...]``
Clear, and then add bindings for the specified key.
The ``<key>`` parameter above has the following **case-sensitive** syntax::
[Ctrl-][Alt-][Shift-]KEY[@context[|context...]]
where the ``KEY`` part can be any recognized key and [] denote optional parts.
where the ``KEY`` part can be any recognized key and :kbd:`[`:kbd:`]` denote
optional parts.
When multiple commands are bound to the same key combination, DFHack selects
the first applicable one. Later ``add`` commands, and earlier entries within one
@ -49,13 +50,18 @@ Multiple contexts can be specified by separating them with a pipe (``|``) - for
example, ``@foo|bar|baz/foo`` would match anything under ``@foo``, ``@bar``, or
``@baz/foo``.
Interactive commands like `liquids` cannot be used as hotkeys.
Commands like `liquids` or `tiletypes` cannot be used as hotkeys since they
require the console for interactive input.
Examples
--------
``keybinding add Alt-F1 hotkeys``
Bind Alt-F1 to run the `hotkeys` command on any screen at any time.
``keybinding add Ctrl-Shift-C hotkeys``
Bind Ctrl-Shift-C to run the `hotkeys` command on any screen at any time.
``keybinding add Alt-F@dwarfmode gui/quickfort``
Bind Alt-F to run `gui/quickfort`, but only when on a screen that shows the
main map.
``keybinding add Ctrl-Shift-Z@dwarfmode/Default "stocks show"``
Bind Ctrl-Shift-Z to run `stocks show <stocks>`, but only when on the main
map in the default mode (that is, no special mode, like cursor look, is
enabled).

@ -34,6 +34,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
# Future
## New Plugins
- `overlay`: plugin is transformed from a single line of text that runs `gui/launcher` on click to a fully-featured overlay injection framework. See `overlay-dev-guide` for details.
## Fixes
- `automaterial`: fix the cursor jumping up a z level when clicking quickly after box select
@ -41,8 +42,11 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `mousequery`: fix the cursor jumping up z levels sometimes when using TWBT
- `tiletypes`: no longer resets dig priority to the default when updating other properties of a tile
- `automaterial`: fix rendering errors with box boundary markers
- `autolabor` & `autohauler`: properly handle jobs 241, 242, and 243
- `autofarm`: add missing output flushes
- Core: fix the segmentation fault with the REPORT event in EventManager
- Core: fix the new JOB_STARTED event only sending each event once, to the first handler listed
- Core: ensure ``foo.init`` always runs before ``foo.*.init`` (e.g. ``dfhack.init`` should always run before ``dfhack.something.init``)
## Misc Improvements
- `blueprint`: new ``--smooth`` option for recording all smoothed floors and walls instead of just the ones that require smoothing for later carving
@ -52,28 +56,53 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- `blueprint`: generate meta blueprints to reduce the number of blueprints you have to apply
- `blueprint`: support splitting the output file into phases grouped by when they can be applied
- `blueprint`: when splitting output files, number them so they sort into the order you should apply them in
- `dwarfmonitor`: widgets have been ported to the overlay framework and can be enabled and configured via the overlay command
- `ls`: indent tag listings and wrap them in the right column for better readability
- `ls`: new ``--exclude`` option for hiding matched scripts from the output. this can be especially useful for modders who don't want their mod scripts to be included in ``ls`` output.
- `hotkeys`: hotkey screen has been transformed into an interactive `overlay` widget that you can bring up by moving the mouse cursor over the hotspot (in the upper left corner of the screen by default)
- `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down
- UX: List widgets now have mouse-interactive scrollbars
- UX: You can now hold down the mouse button on a scrollbar to make it scroll multiple times.
- UX: You can now drag the scrollbar to scroll to a specific spot
- `overlay`: reduce the size of the "DFHack Launcher" button
- Constructions module: ``findAtTile`` now uses a binary search intead of a linear search.
- `orders`: replace shell craft orders with orders for shell leggings. they have a slightly higher trade price, and "shleggings" is hilarious.
- `spectate`: new ``auto-unpause`` option for auto-dismissal of announcement pause events (e.g. sieges).
- `spectate`: new ``auto-disengage`` option for auto-disengagement of plugin through player interaction whilst unpaused.
- `spectate`: new ``focus-jobs`` option for following a dwarf after their job has finished (when disabled).
- `spectate`: new ``tick-threshold``, option for specifying the change interval (maximum follow time when focus-jobs is enabled)
- `spectate`: new ``animals``, option for sometimes following animals
- `spectate`: new ``hostiles``, option for sometimes following hostiles
- `spectate`: new ``visiting``, option for sometimes following visiting merchants, diplomats or plain visitors
- `spectate`: added persistent configuration of the plugin settings
- `gui/cp437-table`: new global keybinding for the clickable on-screen keyboard for players with keyboard layouts that prevent them from using certain keys: Ctrl-Shift-K
- `quickfort-library-guide`: dreamfort blueprint improvements: added a quantum stockpile for training bolts
## Documentation
- `spectate`: improved documentation of features and functionality
- `overlay-dev-guide`: documentation and guide for injecting functionality into DF viewscreens from Lua scripts and creating overlay widgets
- ``dfhack.gui.revealInDwarfmodeMap``: document ``center`` bool for lua API
## API
- ``Gui::anywhere_hotkey``: for plugin commands bound to keybindings that can be invoked on any screen
- add functions reverse-engineered from announcement code: ``Gui::autoDFAnnouncement``, ``Gui::pauseRecenter``
- ``Gui::revealInDwarfmodeMap``: Now enforce valid view bounds when pos invalid, add variant accepting x, y, z
- ``Lua::PushInterfaceKeys()``: transforms viewscreen ``feed()`` keys into something that can be interpreted by lua-based widgets
- ``Lua::Push()``: now handles maps with otherwise supported keys and values
- Units module: added new checks
- ``isUnitInBox()``
- ``isAnimal()``
- ``isVisiting()`` any visiting unit (diplomat, merchant, visitor)
- ``isVisitor()`` ie. not merchants or diplomats
- ``isInvader()``
- ``isDemon()`` returns true for unique/regular demons
- ``isTitan()``
- ``isMegabeast()``
- ``isGreatDanger()`` returns true if unit is a demon, titan, or megabeast
- ``isSemiMegabeast()``
- ``isNightCreature()``
- ``isDanger()`` returns true if is a 'GreatDanger', semi-megabeast, night creature, undead, or invader
- Units module: modifies existing checks
- ``isUndead(df::unit* unit)`` => ``isUndead(df::unit* unit, bool ignore_vamps = true)`` isUndead used to always ignore vamps, now it does it by default and includes them when false is passed
- ``isCitizen(df::unit* unit)`` => ``isCitizen(df::unit* unit, bool ignore_sanity = false)`` isCitizen used to always check sanity, now it does it by default and ignores sanity when true is passed
- Constructions module: added ``insert()`` to insert constructions into the game's sorted list.
- MiscUtils: moved the following string transformation functions from ``uicommon.h``: ``int_to_string``, ``ltrim``, ``rtrim``, and ``trim``; added ``string_to_int``
@ -87,6 +116,13 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- ``widgets.EditField`` now allows other widgets to process characters that the ``on_char`` callback rejects.
- ``gui.Screen.show()`` now returns ``self`` as a convenience
- ``gui.View.getMousePos()`` now takes an optional ``ViewRect`` parameter in case the caller wants to get the mouse pos relative to a rect that is not the frame_body (such as the frame_rect)
- Lua mouse events now conform to documented behavior in `lua-api` -- ``_MOUSE_L_DOWN`` will be sent exactly once per mouse click and ``_MOUSE_L`` will be sent repeatedly as long as the button is held down. Similarly for right mouse button events.
## Internals
- MSVC warning level upped to /W3, and /WX added to make warnings cause compilations to fail.
## Removed
- `resume`: functionality (including suspended building overlay) has moved to `unsuspend`
# 0.47.05-r7

@ -8,6 +8,7 @@ These pages are detailed guides covering DFHack tools.
:maxdepth: 1
/docs/guides/examples-guide
/docs/guides/overlay-dev-guide
/docs/guides/modding-guide
/docs/guides/quickfort-library-guide
/docs/guides/quickfort-user-guide

@ -0,0 +1,302 @@
.. _overlay-dev-guide:
DFHack overlay dev guide
=========================
.. highlight:: lua
This guide walks you through how to build overlay widgets and register them with
the `overlay` framework for injection into Dwarf Fortress viewscreens.
Why would I want to create an overlay widget?
---------------------------------------------
There are both C++ and Lua APIs for creating viewscreens and drawing to the
screen. If you need very specific low-level control, those APIs might be the
right choice for you. However, here are some reasons you might want to implement
an overlay widget instead:
1. You can draw directly to an existing viewscreen instead of creating an
entirely new screen on the viewscreen stack. This allows the original
viewscreen to continue processing uninterrupted and keybindings bound to
that viewscreen will continue to function. This was previously only
achievable by C++ plugins.
1. You'll get a free UI for enabling/disabling your widget and repositioning it
on the screen. Widget state is saved for you and is automatically restored
when the game is restarted.
1. You don't have to manage the C++ interposing logic yourself and can focus on
the business logic, writing purely in Lua if desired.
In general, if you are writing a plugin or script and have anything you'd like
to add to an existing screen (including live updates of map tiles while the game
is unpaused), an overlay widget is probably your easiest path to get it done. If
your plugin or script doesn't otherwise need to be enabled to function, using
the overlay allows you to avoid writing any of the enable management code that
would normally be required for you to show info in the UI.
Overlay widget API
------------------
Overlay widgets are Lua classes that inherit from ``overlay.OverlayWidget``
(which itself inherits from `widgets.Widget <widget>`). The regular
``onInput(keys)``, ``onRenderFrame(dc, frame_rect)``, and ``onRenderBody(dc)``
functions work as normal, and they are called when the viewscreen that the
widget is associated with does its usual input and render processing. The widget
gets first dibs on input processing. If a widget returns ``true`` from its
``onInput()`` function, the viewscreen will not receive the input.
Overlay widgets can contain other Widgets and be as simple or complex as you
need them to be, just like you're building a regular UI element.
There are a few extra capabilities that overlay widgets have that take them
beyond your everyday ``Widget``:
- If an ``overlay_onupdate(viewscreen)`` function is defined, it will be called
just after the associated viewscreen's ``logic()`` function is called (i.e.
a "tick" or a (non-graphical) "frame"). For hotspot widgets, this function
will also get called after the top viewscreen's ``logic()`` function is
called, regardless of whether the widget is associated with that viewscreen.
If this function returns ``true``, then the widget's ``overlay_trigger()``
function is immediately called. Note that the ``viewscreen`` parameter will
be ``nil`` for hotspot widgets that are not also associated with the current
viewscreen.
- If an ``overlay_trigger()`` function is defined, will be called when the
widget's ``overlay_onupdate`` callback returns true or when the player uses
the CLI (or a keybinding calling the CLI) to trigger the widget. The
function must return either ``nil`` or the ``gui.Screen`` object that the
widget code has allocated, shown, and now owns. Hotspot widgets will receive
no callbacks from unassociated viewscreens until the returned screen is
dismissed. Unbound hotspot widgets **must** allocate a Screen with this
function if they want to react to the ``onInput()`` feed or be rendered. The
widgets owned by the overlay framework must not be attached to that new
screen, but the returned screen can instantiate and configure any new views
that it wants to.
If the widget can take up a variable amount of space on the screen, and you want
the widget to adjust its position according to the size of its contents, you can
modify ``self.frame.w`` and ``self.frame.h`` at any time -- in ``init()`` or in
any of the callbacks -- to indicate a new size. The overlay framework will
detect the size change and adjust the widget position and layout.
If you don't need to dynamically resize, just set ``self.frame.w`` and
``self.frame.h`` once in ``init()``.
Widget attributes
*****************
The ``overlay.OverlayWidget`` superclass defines the following class attributes:
- ``name``
This will be filled in with the display name of your widget, in case you
have multiple widgets with the same implementation but different
configurations.
- ``default_pos`` (default: ``{x=-2, y=-2}``)
Override this attribute with your desired default widget position. See
the `overlay` docs for information on what positive and negative numbers
mean for the position. Players can change the widget position at any time
via the `overlay position <overlay>` command, so don't assume that your
widget will always be at the default position.
- ``viewscreens`` (default: ``{}``)
The list of viewscreens that this widget should be associated with. When
one of these viewscreens is on top of the viewscreen stack, your widget's
callback functions for update, input, and render will be interposed into the
viewscreen's call path. The name of the viewscreen is the name of the DFHack
class that represents the viewscreen, minus the ``viewscreen_`` prefix and
``st`` suffix. For example, the fort mode main map viewscreen would be
``dwarfmode`` and the adventure mode map viewscreen would be
``dungeonmode``. If there is only one viewscreen that this widget is
associated with, it can be specified as a string instead of a list of
strings with a single element.
- ``hotspot`` (default: ``false``)
If set to ``true``, your widget's ``overlay_onupdate`` function will be
called whenever the `overlay` plugin's ``plugin_onupdate()`` function is
called (which corresponds to one call per call to the current top
viewscreen's ``logic()`` function). This call to ``overlay_onupdate`` is in
addition to any calls initiated from associated interposed viewscreens and
will come after calls from associated viewscreens.
- ``overlay_onupdate_max_freq_seconds`` (default: ``5``)
This throttles how often a widget's ``overlay_onupdate`` function can be
called (from any source). Set this to the largest amount of time (in
seconds) that your widget can take to react to changes in information and
not annoy the player. Set to 0 to be called at the maximum rate. Be aware
that running more often than you really need to will impact game FPS,
especially if your widget can run while the game is unpaused.
Registering a widget with the overlay framework
***********************************************
Anywhere in your code after the widget classes are declared, define a table
named ``OVERLAY_WIDGETS``. The keys are the display names for your widgets and
the values are the widget classes. For example, the `dwarfmonitor` widgets are
declared like this::
OVERLAY_WIDGETS = {
cursor=CursorWidget,
date=DateWidget,
misery=MiseryWidget,
weather=WeatherWidget,
}
When the `overlay` plugin is enabled, it scans all plugins and scripts for
this table and registers the widgets on your behalf. The widget is enabled if it
was enabled the last time the `overlay` plugin was loaded and the widget's
position is restored according to the state saved in the
:file:`dfhack-config/overlay.json` file.
The overlay framework will instantiate widgets from the named classes and own
the resulting objects. The instantiated widgets must not be added as subviews to
any other View, including the Screen views that can be returned from the
``overlay_trigger()`` function.
Widget example 1: adding text to a DF screen
--------------------------------------------
This is a simple widget that displays a message at its position. The message
text is retrieved from the host script or plugin every ~20 seconds or when
the :kbd:`Alt`:kbd:`Z` hotkey is hit::
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
MessageWidget = defclass(MessageWidget, overlay.OverlayWidget)
MessageWidget.ATTRS{
default_pos={x=5,y=-2},
viewscreens={'dwarfmode', 'dungeonmode'},
overlay_onupdate_max_freq_seconds=20,
}
function MessageWidget:init()
self.label = widgets.Label{text=''}
self:addviews{self.label}
end
function MessageWidget:overlay_onupdate()
local text = getImportantMessage() -- defined in the host script/plugin
self.label:setText(text)
self.frame.w = #text
end
function MessageWidget:onInput(keys)
if keys.CUSTOM_ALT_Z then
self:overlay_onupdate()
return true
end
end
OVERLAY_WIDGETS = {message=MessageWidget}
Widget example 2: highlighting artifacts on the live game map
-------------------------------------------------------------
This widget is not rendered at its "position" at all, but instead monitors the
map and overlays information about where artifacts are located. Scanning for
which artifacts are visible on the map can slow, so that is only done every 10
seconds to avoid slowing down the entire game on every frame.
::
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
ArtifactRadarWidget = defclass(ArtifactRadarWidget, overlay.OverlayWidget)
ArtifactRadarWidget.ATTRS{
viewscreens={'dwarfmode', 'dungeonmode'},
overlay_onupdate_max_freq_seconds=10,
}
function ArtifactRadarWidget:overlay_onupdate()
self.visible_artifacts_coords = getVisibleArtifactCoords()
end
function ArtifactRadarWidget:onRenderFrame()
for _,pos in ipairs(self.visible_artifacts_coords) do
-- highlight tile at given coordinates
end
end
OVERLAY_WIDGETS = {radar=ArtifactRadarWidget}
Widget example 3: corner hotspot
--------------------------------
This hotspot reacts to mouseover events and launches a screen that can react to
input events. The hotspot area is a 2x2 block near the lower right corner of the
screen (by default, but the player can move it wherever).
::
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget)
HotspotMenuWidget.ATTRS{
default_pos={x=-3,y=-3},
frame={w=2, h=2},
hotspot=true,
viewscreens='dwarfmode',
overlay_onupdate_max_freq_seconds=0, -- check for mouseover every tick
}
function HotspotMenuWidget:init()
-- note this label only gets rendered on the associated viewscreen
-- (dwarfmode), but the hotspot is active on all screens
self:addviews{widgets.Label{text={'!!', NEWLINE, '!!'}}}
self.mouseover = false
end
function HotspotMenuWidget:overlay_onupdate()
local hasMouse = self:getMousePos()
if hasMouse and not self.mouseover then -- only trigger on mouse entry
self.mouseover = true
return true
end
self.mouseover = hasMouse
end
function HotspotMenuWidget:overlay_trigger()
return MenuScreen{hotspot_frame=self.frame}:show()
end
OVERLAY_WIDGETS = {menu=HotspotMenuWidget}
MenuScreen = defclass(MenuScreen, gui.Screen)
MenuScreen.ATTRS{
focus_path='hotspot/menu',
hotspot_frame=DEFAULT_NIL,
}
function MenuScreen:init()
self.mouseover = false
-- derrive the menu frame from the hotspot frame so it
-- can appear in a nearby location
local frame = copyall(self.hotspot_frame)
-- ...
self:addviews{
widgets.ResizingPanel{
autoarrange_subviews=true,
frame=frame,
frame_style=gui.GREY_LINE_FRAME,
frame_background=gui.CLEAR_PEN,
subviews={
-- ...
},
},
},
}
end
function MenuScreen:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
return true
end
return self:inputToSubviews(keys)
end
function MenuScreen:onRenderFrame(dc, rect)
self:renderParent()
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@ -0,0 +1,76 @@
channel-safely
==============
.. dfhack-tool::
:summary: Auto-manage channel designations to keep dwarves safe.
:tags: fort auto
Multi-level channel projects can be dangerous, and managing the safety of your
dwarves throughout the completion of such projects can be difficult and time
consuming. This plugin keeps your dwarves safe (at least while channeling) so you don't
have to. Now you can focus on designing your dwarven cities with the deep chasms
they were meant to have.
Usage
-----
::
enable channel-safely
channel-safely set <setting> <value>
channel-safely enable|disable <feature>
channel-safely <command>
When enabled the map will be scanned for channel designations which will be grouped
together based on adjacency and z-level. These groups will then be analyzed for safety
and designations deemed unsafe will be put into :wiki:`Marker Mode <Designations_menu#Marker_Mode>`.
Each time a channel designation is completed its group status is checked, and if the group
is complete pending groups below are made active again.
Features and settings once set will persist until you change them, even if you save and reload your game.
Examples
--------
``channel-safely``
The plugin reports its configured status.
``channel-safely runonce``
Runs the safety procedures once. You can use this if you prefer initiating scans manually.
``channel-safely disable require-vision``
Allows the plugin to read all tiles, including the ones your dwarves know nothing about.
``channel-safely enable monitor``
Enables monitoring active channel digging jobs. Meaning that if another unit it present
or the tile below becomes open space the job will be paused or canceled (respectively).
``channel-safely set ignore-threshold 3``
Configures the plugin to ignore designations equal to or above priority 3 designations.
Commands
--------
:runonce: Run the safety procedures once to set the marker mode of designations.
:rebuild: Rebuild the designation group data. Intended for to be used in the event
the marker mode isn't being set correctly (mostly for debugging).
Features
--------
:require-vision: Toggle whether the dwarves need vision of a tile before channeling to it can be deemed unsafe. (default: enabled)
:monitor: Toggle whether to monitor the conditions of active digs. (default: disabled)
:resurrect: Toggle whether to resurrect units involved in cave-ins, and if monitor is enabled
units who die while digging. (default: disabled)
:insta-dig: Toggle whether to use insta-digging on unreachable designations.
Runs on the refresh cycles. (default: disabled)
Settings
--------
:refresh-freq: The rate at which full refreshes are performed.
This can be expensive if you're undertaking many mega projects. (default:600, twice a day)
:monitor-freq: The rate at which active jobs are monitored. (default:1)
:ignore-threshold: Sets the priority threshold below which designations are processed. You can set to 1 or 0 to
effectively disable the scanning. (default: 5)
:fall-threshold: Sets the fall threshold beyond which is considered unsafe. (default: 1)

@ -2,7 +2,7 @@ dwarfmonitor
============
.. dfhack-tool::
:summary: Measure fort happiness and efficiency.
:summary: Report on dwarf preferences and efficiency.
:tags: fort inspection jobs units
It can also show heads-up display widgets with live fort statistics.
@ -11,84 +11,39 @@ Usage
-----
``enable dwarfmonitor``
Enable the plugin.
``dwarfmonitor enable <mode>``
Start tracking a specific facet of fortress life. The ``mode`` can be
"work", "misery", "date", "weather", or "all". This will show the
corresponding on-screen widgets, if applicable.
``dwarfmonitor disable <mode>``
Stop monitoring ``mode`` and disable corresponding widgets.
Enable tracking of job efficiency for display on the ``dwarfmonitor stats``
screen.
``dwarfmonitor stats``
Show statistics summary.
Show statistics and efficiency summary.
``dwarfmonitor prefs``
Show summary of dwarf preferences.
``dwarfmonitor reload``
Reload the widget configuration file (``dfhack-config/dwarfmonitor.json``).
Show a summary of preferences for dwarves in your fort.
Widget configuration
--------------------
The following types of widgets (defined in
:file:`hack/lua/plugins/dwarfmonitor.lua`) can be displayed on the main fortress
mode screen:
The following widgets are registered for display on the main fortress mode
screen with the `overlay` framework:
``misery``
Show overall happiness levels of all dwarves.
``date``
``dwarfmonitor.cursor``
Show the current keyboard and mouse cursor positions.
``dwarfmonitor.date``
Show the in-game date.
``weather``
``dwarfmonitor.misery``
Show overall happiness levels of all dwarves.
``dwarfmonitor.weather``
Show current weather (e.g. rain/snow).
``cursor``
Show the current mouse cursor position.
The file :file:`dfhack-config/dwarfmonitor.json` can be edited to control the
positions and settings of all widgets. This file should contain a JSON object
with the key ``widgets`` containing an array of objects:
.. code-block:: lua
{
"widgets": [
{
"type": "widget type (weather, misery, etc.)",
"x": X coordinate,
"y": Y coordinate
<...additional options...>
}
]
}
X and Y coordinates begin at zero (in the upper left corner of the screen).
Negative coordinates will be treated as distances from the lower right corner,
beginning at 1 - e.g. an x coordinate of 0 is the leftmost column, while an x
coordinate of -1 is the rightmost column.
By default, the x and y coordinates given correspond to the leftmost tile of
the widget. Including an ``anchor`` option set to ``right`` will cause the
rightmost tile of the widget to be located at this position instead.
Some widgets support additional options:
* ``date`` widget:
* ``format``: specifies the format of the date. The following characters
are replaced (all others, such as punctuation, are not modified)
* ``Y`` or ``y``: The current year
* ``M``: The current month, zero-padded if necessary
* ``m``: The current month, *not* zero-padded
* ``D``: The current day, zero-padded if necessary
* ``d``: The current day, *not* zero-padded
They can be enabled or disable via the `overlay` command.
The default date format is ``Y-M-D``, per the ISO8601_ standard.
The :file:`dfhack-config/dwarfmonitor.json` file can be edited to specify the
format for the ``dwarfmonitor.date`` widget:
.. _ISO8601: https://en.wikipedia.org/wiki/ISO_8601
* ``Y`` or ``y``: The current year
* ``M``: The current month, zero-padded if necessary
* ``m``: The current month, *not* zero-padded
* ``D``: The current day, zero-padded if necessary
* ``d``: The current day, *not* zero-padded
* ``cursor`` widget:
The default date format is ``Y-M-D``, per the ISO8601_ standard.
* ``format``: Specifies the format. ``X``, ``x``, ``Y``, and ``y`` are
replaced with the corresponding cursor coordinates, while all other
characters are unmodified.
* ``show_invalid``: If set to ``true``, the mouse coordinates will both be
displayed as ``-1`` when the cursor is outside of the DF window; otherwise,
nothing will be displayed.
.. _ISO8601: https://en.wikipedia.org/wiki/ISO_8601

@ -2,7 +2,7 @@ hotkeys
=======
.. dfhack-tool::
:summary: Show all dfhack keybindings for the current context.
:summary: Show all DFHack keybindings for the current context.
:tags: dfhack
The command opens an in-game screen showing which DFHack keybindings are active
@ -11,8 +11,22 @@ in the current context. See also `hotkey-notes`.
Usage
-----
::
``hotkeys``
Show the list of keybindings for the current context in an in-game menu.
``hotkeys list``
List the keybindings to the console.
hotkeys
Menu overlay widget
-------------------
.. image:: ../images/hotkeys.png
The in-game hotkeys menu is registered with the `overlay` framework and can be
enabled as a hotspot in the upper-left corner of the screen. You can bring up
the menu by hovering the mouse cursor over the hotspot and can select a command
to run from the list by clicking on it with the mouse or by using the keyboard
to select a command with the arrow keys and hitting :kbd:`Enter`.
A short description of the command will appear in a nearby textbox. If you'd
like to see the full help text for the command or edit the command before
running, you can open it for editing in `gui/launcher` by right clicking on the
command, left clicking on the arrow to the left of the command, or by pressing
the right arrow key while the command is selected.

@ -55,7 +55,7 @@ This collection of orders handles basic fort necessities:
- thread/cloth/dye
- pots/jugs/buckets/mugs
- bags of leather, cloth, silk, and yarn
- crafts and totems from otherwise unusable by-products
- crafts, totems, and shleggings from otherwise unusable by-products
- mechanisms/cages
- splints/crutches
- lye/soap
@ -66,6 +66,8 @@ This collection of orders handles basic fort necessities:
You should import it as soon as you have enough dwarves to perform the tasks.
Right after the first migration wave is usually a good time.
Armok's note: shleggings? Yes, `shleggings <https://youtu.be/bLN8cOcTjdo>`__.
:source:`library/furnace <data/orders/furnace.json>`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@ -2,19 +2,66 @@ overlay
=======
.. dfhack-tool::
:summary: Provide an on-screen clickable DFHack launcher button.
:summary: Manage on-screen overlay widgets.
:tags: dfhack interface
This tool places a small button in the lower left corner of the screen that you
can click to run DFHack commands with `gui/launcher`.
If you would rather always run `gui/launcher` with the hotkeys, or just don't
want the DFHack button on-screen, just disable the plugin with
``disable overlay``.
The overlay framework manages the on-screen widgets that other tools (including
3rd party plugins and scripts) can register for display. If you are a developer
who wants to write an overlay widget, please see the `overlay-dev-guide`.
Usage
-----
::
``enable overlay``
Display enabled widgets.
``overlay enable|disable all|<name or list number> [<name or list number> ...]``
Enable/disable all or specified widgets. Widgets can be specified by either
their name or their number, as returned by ``overlay list``.
``overlay list [<filter>]``
Show a list of all the widgets that are registered with the overlay
framework, optionally filtered by the given filter string.
``overlay position <name or list number> [default|<x> <y>]``
Display configuration information for the given widget or change the
position where it is rendered. See the `Widget position`_ section below for
details.
``overlay trigger <name or list number>``
Intended to be used by keybindings for manually triggering a widget. For
example, you could use an ``overlay trigger`` keybinding to show a menu that
normally appears when you hover the mouse over a screen hotspot.
Examples
--------
``overlay enable all``
Enable all widgets. Note that they will only be displayed on the screens
that they are associated with. You can see which screens a widget will be
displayed on, along with whether the widget is a hotspot, by calling
``overlay position``.
``overlay position hotkeys.menu``
Show the current configuration of the `hotkeys` menu widget.
``overlay position dwarfmonitor.cursor -2 -3``
Display the `dwarfmonitor` cursor position reporting widget in the lower
right corner of the screen, 2 tiles from the left and 3 tiles from the
bottom.
``overlay position dwarfmonitor.cursor default``
Reset the `dwarfmonitor` cursor position to its default.
``overlay trigger hotkeys.menu``
Trigger the `hotkeys` menu widget so that it shows its popup menu. This is
what is run when you hit :kbd:`Ctrl`:kbd:`Shift`:kbd:`C`.
Widget position
---------------
Widgets can be positioned at any (``x``, ``y``) position on the screen, and can
be specified relative to any edge. Coordinates are 1-based, which means that
``1`` is the far left column (for ``x``) or the top row (for ``y``). Negative
numbers are measured from the right of the screen to the right edge of the
widget or from the bottom of the screen to the bottom of the widget,
respectively.
For easy reference, the corners can be found at the following coordinates:
enable overlay
:(1, 1): top left corner
:(-1, 1): top right corner
:(1, -1): lower left corner
:(-1, -1): lower right corner

@ -1,23 +0,0 @@
resume
======
.. dfhack-tool::
:summary: Color planned buildings based on their suspend status.
:tags: fort productivity interface jobs
:no-command:
.. dfhack-command:: resume
:summary: Resume all suspended building jobs.
When enabled, this plugin will display a colored 'X' over suspended buildings.
When run as a command, it can resume all suspended building jobs, allowing you
to quickly recover if a bunch of jobs were suspended due to the workers getting
scared off by wildlife or items temporarily blocking building sites.
Usage
-----
::
enable resume
resume all

@ -38,11 +38,12 @@ Examples
Features
--------
:focus-jobs: Toggle whether the plugin should always be following a job. (default: disabled)
:auto-unpause: Toggle auto-dismissal of game pause events. (default: disabled)
:auto-disengage: Toggle auto-disengagement of plugin through player intervention while unpaused. (default: disabled)
:animals: Toggle whether to sometimes follow animals. (default: disabled)
:hostiles: Toggle whether to sometimes follow hostiles (eg. undead, titan, invader, etc.) (default: disabled)
:visiting: Toggle whether to sometimes follow visiting units (eg. diplomat)
Settings
--------
:tick-threshold: Set the plugin's tick interval for changing the followed dwarf.
Acts as a maximum follow time when used with focus-jobs enabled. (default: 50)
:tick-threshold: Set the plugin's tick interval for changing the followed dwarf. (default: 1000)

@ -82,16 +82,8 @@ set(MAIN_SOURCES
RemoteTools.cpp
)
if(BUILD_TESTING)
# TODO Make a function or macro for this
add_executable(MiscUtils.test MiscUtils.test.cpp)
target_link_libraries(MiscUtils.test dfhack)
add_test(NAME MiscUtils.test COMMAND MiscUtils.test)
# How to get `test` to ensure everything is up to date before running
# tests? This add_dependencies() fails with:
# Cannot add target-level dependencies to non-existent target "test".
#add_dependencies(test MiscUtils.test)
if(BUILD_CORE_TESTS)
add_subdirectory(tests)
endif()
if(WIN32)

@ -2000,15 +2000,21 @@ void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& p
}
size_t loadScriptFiles(Core* core, color_ostream& out, const vector<std::string>& prefix, const std::string& folder) {
vector<std::string> scriptFiles;
static const string suffix = ".init";
vector<string> scriptFiles;
for ( size_t a = 0; a < prefix.size(); a++ ) {
getFilesWithPrefixAndSuffix(folder, prefix[a], ".init", scriptFiles);
}
std::sort(scriptFiles.begin(), scriptFiles.end());
std::sort(scriptFiles.begin(), scriptFiles.end(),
[&](const string &a, const string &b) {
string a_base = a.substr(0, a.size() - suffix.size());
string b_base = b.substr(0, b.size() - suffix.size());
return a_base < b_base;
});
size_t result = 0;
for ( size_t a = 0; a < scriptFiles.size(); a++ ) {
result++;
std::string path = "";
string path = "";
if (folder != ".")
path = folder + "/";
core->loadScriptFile(out, path + scriptFiles[a], false);

@ -98,6 +98,8 @@ distribution.
#include "df/specific_ref.h"
#include "df/specific_ref_type.h"
#include "df/vermin.h"
#include "df/report_init.h"
#include "df/report_zoom_type.h"
#include <lua.h>
#include <lauxlib.h>
@ -1481,15 +1483,136 @@ static const LuaWrapper::FunctionReg dfhack_gui_module[] = {
WRAPM(Gui, showPopupAnnouncement),
WRAPM(Gui, showAutoAnnouncement),
WRAPM(Gui, resetDwarfmodeView),
WRAPM(Gui, revealInDwarfmodeMap),
WRAPM(Gui, refreshSidebar),
WRAPM(Gui, inRenameBuilding),
WRAPM(Gui, getDepthAt),
{ NULL, NULL }
};
static int gui_autoDFAnnouncement(lua_State *state)
{
bool rv;
df::report_init *r = Lua::GetDFObject<df::report_init>(state, 1);
if (r)
{
std::string message = luaL_checkstring(state, 2);
rv = Gui::autoDFAnnouncement(*r, message);
}
else
{
df::coord pos;
int color = 0; // initialize these to prevent warning
bool bright = false, is_sparring = false;
df::unit *unit1 = NULL, *unit2 = NULL;
auto type = (df::announcement_type)lua_tointeger(state, 1);
Lua::CheckDFAssign(state, &pos, 2);
std::string message = luaL_checkstring(state, 3);
switch (lua_gettop(state))
{
default:
case 8:
is_sparring = lua_toboolean(state, 8);
case 7:
unit2 = Lua::CheckDFObject<df::unit>(state, 7);
case 6:
unit1 = Lua::CheckDFObject<df::unit>(state, 6);
case 5:
bright = lua_toboolean(state, 5);
case 4:
color = lua_tointeger(state, 4);
case 3:
break;
}
switch (lua_gettop(state))
{ // Use the defaults in Gui.h
default:
case 8:
rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2, is_sparring);
break;
case 7:
rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1, unit2);
break;
case 6:
rv = Gui::autoDFAnnouncement(type, pos, message, color, bright, unit1);
break;
case 5:
rv = Gui::autoDFAnnouncement(type, pos, message, color, bright);
break;
case 4:
rv = Gui::autoDFAnnouncement(type, pos, message, color);
break;
case 3:
rv = Gui::autoDFAnnouncement(type, pos, message);
}
}
lua_pushboolean(state, rv);
return 1;
}
static int gui_pauseRecenter(lua_State *state)
{
bool rv;
df::coord p;
switch (lua_gettop(state))
{
default:
case 4:
rv = Gui::pauseRecenter(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4));
break;
case 3:
rv = Gui::pauseRecenter(CheckCoordXYZ(state, 1, false));
break;
case 2:
Lua::CheckDFAssign(state, &p, 1);
rv = Gui::pauseRecenter(p, lua_toboolean(state, 2));
break;
case 1:
Lua::CheckDFAssign(state, &p, 1);
rv = Gui::pauseRecenter(p);
}
lua_pushboolean(state, rv);
return 1;
}
static int gui_revealInDwarfmodeMap(lua_State *state)
{
bool rv;
df::coord p;
switch (lua_gettop(state))
{
default:
case 4:
rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false), lua_toboolean(state, 4));
break;
case 3:
rv = Gui::revealInDwarfmodeMap(CheckCoordXYZ(state, 1, false));
break;
case 2:
Lua::CheckDFAssign(state, &p, 1);
rv = Gui::revealInDwarfmodeMap(p, lua_toboolean(state, 2));
break;
case 1:
Lua::CheckDFAssign(state, &p, 1);
rv = Gui::revealInDwarfmodeMap(p);
}
lua_pushboolean(state, rv);
return 1;
}
static const luaL_Reg dfhack_gui_funcs[] = {
{ "autoDFAnnouncement", gui_autoDFAnnouncement },
{ "getDwarfmodeViewDims", gui_getDwarfmodeViewDims },
{ "pauseRecenter", gui_pauseRecenter },
{ "revealInDwarfmodeMap", gui_revealInDwarfmodeMap },
{ "getMousePos", gui_getMousePos },
{ NULL, NULL }
};
@ -1559,6 +1682,63 @@ static const luaL_Reg dfhack_job_funcs[] = {
/***** Units module *****/
static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, isUnitInBox),
WRAPM(Units, isActive),
WRAPM(Units, isVisible),
WRAPM(Units, isCitizen),
WRAPM(Units, isFortControlled),
WRAPM(Units, isOwnCiv),
WRAPM(Units, isOwnGroup),
WRAPM(Units, isOwnRace),
WRAPM(Units, isAlive),
WRAPM(Units, isDead),
WRAPM(Units, isKilled),
WRAPM(Units, isSane),
WRAPM(Units, isCrazed),
WRAPM(Units, isGhost),
WRAPM(Units, isHidden),
WRAPM(Units, isHidingCurse),
WRAPM(Units, isMale),
WRAPM(Units, isFemale),
WRAPM(Units, isBaby),
WRAPM(Units, isChild),
WRAPM(Units, isAdult),
WRAPM(Units, isGay),
WRAPM(Units, isNaked),
WRAPM(Units, isVisiting),
WRAPM(Units, isTrainableHunting),
WRAPM(Units, isTrainableWar),
WRAPM(Units, isTrained),
WRAPM(Units, isHunter),
WRAPM(Units, isWar),
WRAPM(Units, isTame),
WRAPM(Units, isTamable),
WRAPM(Units, isDomesticated),
WRAPM(Units, isMarkedForSlaughter),
WRAPM(Units, isGelded),
WRAPM(Units, isEggLayer),
WRAPM(Units, isGrazer),
WRAPM(Units, isMilkable),
WRAPM(Units, isForest),
WRAPM(Units, isMischievous),
WRAPM(Units, isAvailableForAdoption),
WRAPM(Units, hasExtravision),
WRAPM(Units, isOpposedToLife),
WRAPM(Units, isBloodsucker),
WRAPM(Units, isDwarf),
WRAPM(Units, isAnimal),
WRAPM(Units, isMerchant),
WRAPM(Units, isDiplomat),
WRAPM(Units, isVisitor),
WRAPM(Units, isInvader),
WRAPM(Units, isUndead),
WRAPM(Units, isNightCreature),
WRAPM(Units, isSemiMegabeast),
WRAPM(Units, isMegabeast),
WRAPM(Units, isTitan),
WRAPM(Units, isDemon),
WRAPM(Units, isDanger),
WRAPM(Units, isGreatDanger),
WRAPM(Units, teleport),
WRAPM(Units, getGeneralRef),
WRAPM(Units, getSpecificRef),
@ -1567,23 +1747,9 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getVisibleName),
WRAPM(Units, getIdentity),
WRAPM(Units, getNemesis),
WRAPM(Units, isHidingCurse),
WRAPM(Units, getPhysicalAttrValue),
WRAPM(Units, getMentalAttrValue),
WRAPM(Units, isCrazed),
WRAPM(Units, isOpposedToLife),
WRAPM(Units, hasExtravision),
WRAPM(Units, isBloodsucker),
WRAPM(Units, isMischievous),
WRAPM(Units, getMiscTrait),
WRAPM(Units, isDead),
WRAPM(Units, isAlive),
WRAPM(Units, isSane),
WRAPM(Units, isDwarf),
WRAPM(Units, isCitizen),
WRAPM(Units, isFortControlled),
WRAPM(Units, isVisible),
WRAPM(Units, isHidden),
WRAPM(Units, getAge),
WRAPM(Units, getKillCount),
WRAPM(Units, getNominalSkill),
@ -1601,12 +1767,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getGoalName),
WRAPM(Units, isGoalAchieved),
WRAPM(Units, getSquadName),
WRAPM(Units, isWar),
WRAPM(Units, isHunter),
WRAPM(Units, isAvailableForAdoption),
WRAPM(Units, isOwnCiv),
WRAPM(Units, isOwnGroup),
WRAPM(Units, isOwnRace),
WRAPM(Units, getPhysicalDescription),
WRAPM(Units, getRaceName),
WRAPM(Units, getRaceNamePlural),
@ -1615,31 +1775,6 @@ static const LuaWrapper::FunctionReg dfhack_units_module[] = {
WRAPM(Units, getRaceBabyNameById),
WRAPM(Units, getRaceChildName),
WRAPM(Units, getRaceChildNameById),
WRAPM(Units, isBaby),
WRAPM(Units, isChild),
WRAPM(Units, isAdult),
WRAPM(Units, isEggLayer),
WRAPM(Units, isGrazer),
WRAPM(Units, isMilkable),
WRAPM(Units, isTrainableWar),
WRAPM(Units, isTrainableHunting),
WRAPM(Units, isTamable),
WRAPM(Units, isMale),
WRAPM(Units, isFemale),
WRAPM(Units, isMerchant),
WRAPM(Units, isDiplomat),
WRAPM(Units, isForest),
WRAPM(Units, isMarkedForSlaughter),
WRAPM(Units, isTame),
WRAPM(Units, isTrained),
WRAPM(Units, isGay),
WRAPM(Units, isNaked),
WRAPM(Units, isUndead),
WRAPM(Units, isGhost),
WRAPM(Units, isActive),
WRAPM(Units, isKilled),
WRAPM(Units, isGelded),
WRAPM(Units, isDomesticated),
WRAPM(Units, getMainSocialActivity),
WRAPM(Units, getMainSocialEvent),
WRAPM(Units, getStressCategory),
@ -1705,6 +1840,8 @@ static int units_getUnitsInBox(lua_State *state)
{
luaL_checktype(state, 7, LUA_TFUNCTION);
units.erase(std::remove_if(units.begin(), units.end(), [&state](df::unit *unit) -> bool {
// todo: merging this filter into the base function would be welcomed by plugins
// (it would also be faster, and less obfuscated than this [ie. erase(remove_if)])
lua_dup(state); // copy function
Lua::PushDFObject(state, unit);
lua_call(state, 1, 1);

@ -156,21 +156,21 @@ void DFHack::Lua::PushInterfaceKeys(lua_State *L,
if (df::global::enabler) {
if (df::global::enabler->mouse_lbut_down) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L");
lua_setfield(L, -2, "_MOUSE_L_DOWN");
}
if (df::global::enabler->mouse_rbut_down) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_R");
lua_setfield(L, -2, "_MOUSE_R_DOWN");
}
if (df::global::enabler->mouse_lbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L_DOWN");
df::global::enabler->mouse_lbut = 0;
lua_setfield(L, -2, "_MOUSE_L");
df::global::enabler->mouse_lbut_down = 0;
}
if (df::global::enabler->mouse_rbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_R_DOWN");
df::global::enabler->mouse_rbut = 0;
lua_setfield(L, -2, "_MOUSE_R");
df::global::enabler->mouse_rbut_down = 0;
}
}
}
@ -820,6 +820,29 @@ bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres
return ok;
}
bool DFHack::Lua::CallLuaModuleFunction(color_ostream &out, lua_State *L,
const char *module_name, const char *fn_name,
int nargs, int nres, LuaLambda && args_lambda, LuaLambda && res_lambda,
bool perr){
if (!lua_checkstack(L, 1 + nargs) ||
!Lua::PushModulePublic(out, L, module_name, fn_name)) {
if (perr)
out.printerr("Failed to load %s Lua code\n", module_name);
return false;
}
std::forward<LuaLambda&&>(args_lambda)(L);
if (!Lua::SafeCall(out, L, nargs, nres, perr)) {
if (perr)
out.printerr("Failed Lua call to '%s.%s'\n", module_name, fn_name);
return false;
}
std::forward<LuaLambda&&>(res_lambda)(L);
return true;
}
// Copied from lcorolib.c, with error handling modifications
static int resume_helper(lua_State *L, lua_State *co, int narg, int nres)
{

@ -24,6 +24,7 @@ distribution.
#pragma once
#include <functional>
#include <string>
#include <sstream>
#include <vector>
@ -218,6 +219,20 @@ namespace DFHack {namespace Lua {
*/
DFHACK_EXPORT bool SafeCall(color_ostream &out, lua_State *state, int nargs, int nres, bool perr = true);
/**
* Load named module and function and invoke it via SafeCall. Returns true
* on success. If an error is signalled, and perr is true, it is printed and
* popped from the stack.
*/
typedef std::function<void(lua_State *)> LuaLambda;
static auto DEFAULT_LUA_LAMBDA = [](lua_State *){};
DFHACK_EXPORT bool CallLuaModuleFunction(color_ostream &out,
lua_State *state, const char *module_name, const char *fn_name,
int nargs = 0, int nres = 0,
LuaLambda && args_lambda = DEFAULT_LUA_LAMBDA,
LuaLambda && res_lambda = DEFAULT_LUA_LAMBDA,
bool perr = true);
/**
* Pops a function from the top of the stack, and pushes a new coroutine.
*/

@ -36,6 +36,8 @@ distribution.
#include "df/ui.h"
#include "df/announcement_type.h"
#include "df/announcement_flags.h"
#include "df/report_init.h"
#include "df/report_zoom_type.h"
#include "df/unit_report_type.h"
#include "modules/GuiHooks.h"
@ -125,6 +127,11 @@ namespace DFHack
// Show an announcement with effects determined by announcements.txt
DFHACK_EXPORT void showAutoAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true, df::unit *unit1 = NULL, df::unit *unit2 = NULL);
// Process an announcement exactly like DF would, which might result in no announcement
DFHACK_EXPORT bool autoDFAnnouncement(df::report_init r, std::string message);
DFHACK_EXPORT bool autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color = 7, bool bright = true,
df::unit *unit1 = NULL, df::unit *unit2 = NULL, bool is_sparring = false);
/*
* Cursor and window map coords
*/
@ -148,7 +155,10 @@ namespace DFHack
DFHACK_EXPORT DwarfmodeDims getDwarfmodeViewDims();
DFHACK_EXPORT void resetDwarfmodeView(bool pause = false);
DFHACK_EXPORT bool revealInDwarfmodeMap(df::coord pos, bool center = false);
DFHACK_EXPORT bool revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center = false);
DFHACK_EXPORT inline bool revealInDwarfmodeMap(df::coord pos, bool center = false) { return revealInDwarfmodeMap(pos.x, pos.y, pos.z, center); };
DFHACK_EXPORT bool pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause = true);
DFHACK_EXPORT inline bool pauseRecenter(df::coord pos, bool pause = true) { return pauseRecenter(pos.x, pos.y, pos.z, pause); };
DFHACK_EXPORT bool refreshSidebar();
DFHACK_EXPORT bool inRenameBuilding();

@ -68,6 +68,73 @@ static const int MAX_COLORS = 15;
* The Units module - allows reading all non-vermin units and their properties
*/
DFHACK_EXPORT bool isUnitInBox(df::unit* u,
int16_t x1, int16_t y1, int16_t z1,
int16_t x2, int16_t y2, int16_t z2);
DFHACK_EXPORT bool isActive(df::unit *unit);
DFHACK_EXPORT bool isVisible(df::unit* unit);
DFHACK_EXPORT bool isCitizen(df::unit *unit, bool ignore_sanity = false);
DFHACK_EXPORT bool isFortControlled(df::unit *unit);
DFHACK_EXPORT bool isOwnCiv(df::unit* unit);
DFHACK_EXPORT bool isOwnGroup(df::unit* unit);
DFHACK_EXPORT bool isOwnRace(df::unit* unit);
DFHACK_EXPORT bool isAlive(df::unit *unit);
DFHACK_EXPORT bool isDead(df::unit *unit);
DFHACK_EXPORT bool isKilled(df::unit *unit);
DFHACK_EXPORT bool isSane(df::unit *unit);
DFHACK_EXPORT bool isCrazed(df::unit *unit);
DFHACK_EXPORT bool isGhost(df::unit *unit);
/// is unit hidden to the player? accounts for ambushing
DFHACK_EXPORT bool isHidden(df::unit *unit);
DFHACK_EXPORT bool isHidingCurse(df::unit *unit);
DFHACK_EXPORT bool isMale(df::unit* unit);
DFHACK_EXPORT bool isFemale(df::unit* unit);
DFHACK_EXPORT bool isBaby(df::unit* unit);
DFHACK_EXPORT bool isChild(df::unit* unit);
DFHACK_EXPORT bool isAdult(df::unit* unit);
DFHACK_EXPORT bool isGay(df::unit* unit);
DFHACK_EXPORT bool isNaked(df::unit* unit);
DFHACK_EXPORT bool isVisiting(df::unit* unit);
DFHACK_EXPORT bool isTrainableHunting(df::unit* unit);
DFHACK_EXPORT bool isTrainableWar(df::unit* unit);
DFHACK_EXPORT bool isTrained(df::unit* unit);
DFHACK_EXPORT bool isHunter(df::unit* unit);
DFHACK_EXPORT bool isWar(df::unit* unit);
DFHACK_EXPORT bool isTame(df::unit* unit);
DFHACK_EXPORT bool isTamable(df::unit* unit);
DFHACK_EXPORT bool isDomesticated(df::unit* unit);
DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit);
DFHACK_EXPORT bool isGelded(df::unit* unit);
DFHACK_EXPORT bool isEggLayer(df::unit* unit);
DFHACK_EXPORT bool isGrazer(df::unit* unit);
DFHACK_EXPORT bool isMilkable(df::unit* unit);
DFHACK_EXPORT bool isForest(df::unit* unit);
DFHACK_EXPORT bool isMischievous(df::unit *unit);
DFHACK_EXPORT bool isAvailableForAdoption(df::unit* unit);
DFHACK_EXPORT bool hasExtravision(df::unit *unit);
DFHACK_EXPORT bool isOpposedToLife(df::unit *unit);
DFHACK_EXPORT bool isBloodsucker(df::unit *unit);
DFHACK_EXPORT bool isDwarf(df::unit *unit);
DFHACK_EXPORT bool isAnimal(df::unit* unit);
DFHACK_EXPORT bool isMerchant(df::unit* unit);
DFHACK_EXPORT bool isDiplomat(df::unit* unit);
DFHACK_EXPORT bool isVisitor(df::unit* unit);
DFHACK_EXPORT bool isInvader(df::unit* unit);
DFHACK_EXPORT bool isUndead(df::unit* unit, bool include_vamps = false);
DFHACK_EXPORT bool isNightCreature(df::unit* unit);
DFHACK_EXPORT bool isSemiMegabeast(df::unit* unit);
DFHACK_EXPORT bool isMegabeast(df::unit* unit);
DFHACK_EXPORT bool isTitan(df::unit* unit);
DFHACK_EXPORT bool isDemon(df::unit* unit);
DFHACK_EXPORT bool isDanger(df::unit* unit);
DFHACK_EXPORT bool isGreatDanger(df::unit* unit);
/* Read Functions */
// Read units in a box, starting with index. Returns -1 if no more units
// found. Call repeatedly do get all units in a specified box (uses tile coords)
@ -99,35 +166,12 @@ DFHACK_EXPORT df::language_name *getVisibleName(df::unit *unit);
DFHACK_EXPORT df::identity *getIdentity(df::unit *unit);
DFHACK_EXPORT df::nemesis_record *getNemesis(df::unit *unit);
DFHACK_EXPORT bool isHidingCurse(df::unit *unit);
DFHACK_EXPORT int getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr);
DFHACK_EXPORT int getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr);
DFHACK_EXPORT bool casteFlagSet(int race, int caste, df::caste_raw_flags flag);
DFHACK_EXPORT bool isCrazed(df::unit *unit);
DFHACK_EXPORT bool isOpposedToLife(df::unit *unit);
DFHACK_EXPORT bool hasExtravision(df::unit *unit);
DFHACK_EXPORT bool isBloodsucker(df::unit *unit);
DFHACK_EXPORT bool isMischievous(df::unit *unit);
DFHACK_EXPORT df::unit_misc_trait *getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create = false);
DFHACK_EXPORT bool isDead(df::unit *unit);
DFHACK_EXPORT bool isAlive(df::unit *unit);
DFHACK_EXPORT bool isSane(df::unit *unit);
DFHACK_EXPORT bool isCitizen(df::unit *unit);
DFHACK_EXPORT bool isFortControlled(df::unit *unit);
DFHACK_EXPORT bool isDwarf(df::unit *unit);
DFHACK_EXPORT bool isWar(df::unit* unit);
DFHACK_EXPORT bool isHunter(df::unit* unit);
DFHACK_EXPORT bool isAvailableForAdoption(df::unit* unit);
DFHACK_EXPORT bool isOwnCiv(df::unit* unit);
DFHACK_EXPORT bool isOwnGroup(df::unit* unit);
DFHACK_EXPORT bool isOwnRace(df::unit* unit);
DFHACK_EXPORT bool isVisible(df::unit* unit);
/// is unit hidden to the player? accounts for ambushing
DFHACK_EXPORT bool isHidden(df::unit *unit);
DFHACK_EXPORT std::string getRaceNameById(int32_t race_id);
DFHACK_EXPORT std::string getRaceName(df::unit* unit);
DFHACK_EXPORT std::string getPhysicalDescription(df::unit* unit);
@ -138,31 +182,6 @@ DFHACK_EXPORT std::string getRaceBabyName(df::unit* unit);
DFHACK_EXPORT std::string getRaceChildNameById(int32_t race_id);
DFHACK_EXPORT std::string getRaceChildName(df::unit* unit);
DFHACK_EXPORT bool isBaby(df::unit* unit);
DFHACK_EXPORT bool isChild(df::unit* unit);
DFHACK_EXPORT bool isAdult(df::unit* unit);
DFHACK_EXPORT bool isEggLayer(df::unit* unit);
DFHACK_EXPORT bool isGrazer(df::unit* unit);
DFHACK_EXPORT bool isMilkable(df::unit* unit);
DFHACK_EXPORT bool isTrainableWar(df::unit* unit);
DFHACK_EXPORT bool isTrainableHunting(df::unit* unit);
DFHACK_EXPORT bool isTamable(df::unit* unit);
DFHACK_EXPORT bool isMale(df::unit* unit);
DFHACK_EXPORT bool isFemale(df::unit* unit);
DFHACK_EXPORT bool isMerchant(df::unit* unit);
DFHACK_EXPORT bool isDiplomat(df::unit* unit);
DFHACK_EXPORT bool isForest(df::unit* unit);
DFHACK_EXPORT bool isMarkedForSlaughter(df::unit* unit);
DFHACK_EXPORT bool isTame(df::unit* unit);
DFHACK_EXPORT bool isTrained(df::unit* unit);
DFHACK_EXPORT bool isGay(df::unit* unit);
DFHACK_EXPORT bool isNaked(df::unit* unit);
DFHACK_EXPORT bool isUndead(df::unit* unit);
DFHACK_EXPORT bool isGhost(df::unit *unit);
DFHACK_EXPORT bool isActive(df::unit *unit);
DFHACK_EXPORT bool isKilled(df::unit *unit);
DFHACK_EXPORT bool isGelded(df::unit* unit);
DFHACK_EXPORT bool isDomesticated(df::unit* unit);
DFHACK_EXPORT double getAge(df::unit *unit, bool true_age = false);
DFHACK_EXPORT int getKillCount(df::unit *unit);

@ -334,7 +334,7 @@ function EditField:onInput(keys)
elseif keys._MOUSE_L then
local mouse_x, mouse_y = self:getMousePos()
if mouse_x then
self:setCursor(self.start_pos + mouse_x)
self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0))
return true
end
elseif keys._STRING then
@ -496,7 +496,7 @@ function Scrollbar:onRenderBody(dc)
if self.is_dragging then
scrollbar_do_drag(self)
end
if df.global.enabler.mouse_lbut_down == 0 then
if df.global.enabler.mouse_lbut == 0 then
self.last_scroll_ms = 0
self.is_dragging = false
self.scroll_spec = nil
@ -928,7 +928,7 @@ end
function HotkeyLabel:onInput(keys)
if HotkeyLabel.super.onInput(self, keys) then
return true
elseif keys._MOUSE_L and self:getMousePos() then
elseif keys._MOUSE_L_DOWN and self:getMousePos() then
self.on_activate()
return true
end
@ -1009,7 +1009,7 @@ end
function CycleHotkeyLabel:onInput(keys)
if CycleHotkeyLabel.super.onInput(self, keys) then
return true
elseif keys._MOUSE_L and self:getMousePos() then
elseif keys._MOUSE_L_DOWN and self:getMousePos() then
self:cycle()
return true
end
@ -1274,7 +1274,7 @@ function List:onInput(keys)
elseif self.on_submit2 and keys.SEC_SELECT then
self:submit2()
return true
elseif keys._MOUSE_L then
elseif keys._MOUSE_L_DOWN then
local idx = self:getIdxUnderMouse()
if idx then
self:setSelected(idx)

@ -37,6 +37,7 @@ using namespace std;
#include "Error.h"
#include "ModuleFactory.h"
#include "Core.h"
#include "Debug.h"
#include "PluginManager.h"
#include "MiscUtils.h"
using namespace DFHack;
@ -44,6 +45,7 @@ using namespace DFHack;
#include "modules/Job.h"
#include "modules/Screen.h"
#include "modules/Maps.h"
#include "modules/Units.h"
#include "DataDefs.h"
@ -70,6 +72,7 @@ using namespace DFHack;
#include "df/plant.h"
#include "df/popup_message.h"
#include "df/report.h"
#include "df/report_zoom_type.h"
#include "df/route_stockpile_link.h"
#include "df/stop_depart_condition.h"
#include "df/ui_advmode.h"
@ -111,6 +114,16 @@ using namespace DFHack;
#include "df/viewscreen_workshop_profilest.h"
#include "df/world.h"
const size_t MAX_REPORTS_SIZE = 3000; // DF clears old reports to maintain this vector size
const int32_t RECENT_REPORT_TICKS = 500; // used by UNIT_COMBAT_REPORT_ALL_ACTIVE
const int32_t ANNOUNCE_LINE_DURATION = 100; // time to display each line in announcement bar; 3.3 sec at 30 GFPS
const int16_t ANNOUNCE_DISPLAY_TIME = 2000; // DF uses this value for most announcements; 66.6 sec at 30 GFPS
namespace DFHack
{
DBG_DECLARE(core, gui, DebugCategory::LINFO);
}
using namespace df::enums;
using df::global::gamemode;
@ -673,8 +686,6 @@ bool Gui::cursor_hotkey(df::viewscreen *top)
bool Gui::workshop_job_hotkey(df::viewscreen *top)
{
using namespace ui_sidebar_mode;
using df::global::ui;
using df::global::world;
using df::global::ui_workshop_in_add;
using df::global::ui_workshop_job_cursor;
@ -711,7 +722,6 @@ bool Gui::workshop_job_hotkey(df::viewscreen *top)
bool Gui::build_selector_hotkey(df::viewscreen *top)
{
using namespace ui_sidebar_mode;
using df::global::ui;
using df::global::ui_build_selector;
if (!dwarfmode_hotkey(top))
@ -738,8 +748,6 @@ bool Gui::build_selector_hotkey(df::viewscreen *top)
bool Gui::view_unit_hotkey(df::viewscreen *top)
{
using df::global::ui;
using df::global::world;
using df::global::ui_selected_unit;
if (!dwarfmode_hotkey(top))
@ -766,7 +774,6 @@ bool Gui::unit_inventory_hotkey(df::viewscreen *top)
df::job *Gui::getSelectedWorkshopJob(color_ostream &out, bool quiet)
{
using df::global::world;
using df::global::ui_workshop_job_cursor;
if (!workshop_job_hotkey(Core::getTopViewscreen())) {
@ -834,8 +841,6 @@ df::job *Gui::getSelectedJob(color_ostream &out, bool quiet)
df::unit *Gui::getAnyUnit(df::viewscreen *top)
{
using namespace ui_sidebar_mode;
using df::global::ui;
using df::global::world;
using df::global::ui_look_cursor;
using df::global::ui_look_list;
using df::global::ui_selected_unit;
@ -1116,13 +1121,10 @@ df::unit *Gui::getSelectedUnit(color_ostream &out, bool quiet)
df::item *Gui::getAnyItem(df::viewscreen *top)
{
using namespace ui_sidebar_mode;
using df::global::ui;
using df::global::world;
using df::global::ui_look_cursor;
using df::global::ui_look_list;
using df::global::ui_unit_view_mode;
using df::global::ui_building_item_cursor;
using df::global::ui_sidebar_menus;
if (VIRTUAL_CAST_VAR(screen, df::viewscreen_textviewerst, top))
{
@ -1257,11 +1259,8 @@ df::item *Gui::getSelectedItem(color_ostream &out, bool quiet)
df::building *Gui::getAnyBuilding(df::viewscreen *top)
{
using namespace ui_sidebar_mode;
using df::global::ui;
using df::global::ui_look_list;
using df::global::ui_look_cursor;
using df::global::world;
using df::global::ui_sidebar_menus;
if (VIRTUAL_CAST_VAR(screen, df::viewscreen_buildinglistst, top))
return vector_get(screen->buildings, screen->cursor);
@ -1324,8 +1323,6 @@ df::building *Gui::getSelectedBuilding(color_ostream &out, bool quiet)
df::plant *Gui::getAnyPlant(df::viewscreen *top)
{
using df::global::cursor;
using df::global::ui;
using df::global::world;
if (auto dfscreen = dfhack_viewscreen::try_cast(top))
return dfscreen->getSelectedPlant();
@ -1372,98 +1369,192 @@ DFHACK_EXPORT void Gui::writeToGamelog(std::string message)
fseed.close();
}
// Utility functions for reports
static bool parseReportString(std::vector<std::string> *out, const std::string &str, size_t line_length = 73)
{ // parse a string into output strings like DF does for reports
if (str.empty() || line_length == 0)
return false;
string parsed;
size_t i = 0;
do
{
if (str[i] == '&') // escape character
{
i++; // ignore the '&' itself
if (i >= str.length())
break;
if (str[i] == 'r') // "&r" adds a blank line
{
word_wrap(out, parsed, line_length, WSMODE_TRIM_LEADING);
out->push_back(" "); // DF adds a line with a space for some reason
parsed.clear();
}
else if (str[i] == '&') // "&&" is '&'
parsed.push_back('&');
// else next char is ignored
}
else
parsed.push_back(str[i]);
}
while (++i < str.length());
if (parsed.length())
word_wrap(out, parsed, line_length, WSMODE_TRIM_LEADING);
return true;
}
static bool recent_report(df::unit *unit, df::unit_report_type slot)
{
return unit && !unit->reports.log[slot].empty() &&
*df::global::cur_year == unit->reports.last_year[slot] &&
(*df::global::cur_year_tick - unit->reports.last_year_tick[slot]) <= RECENT_REPORT_TICKS;
}
static bool recent_report_any(df::unit *unit)
{
FOR_ENUM_ITEMS(unit_report_type, slot)
{
if (recent_report(unit, slot))
return true;
}
return false;
}
static void delete_old_reports()
{
auto &reports = world->status.reports;
if (reports.size() > MAX_REPORTS_SIZE)
{
size_t excess = reports.size() - MAX_REPORTS_SIZE;
for (size_t i = 0; i < excess; i++)
{
if (reports[i] != NULL)
{ // report destructor
if (reports[i]->flags.bits.announcement)
erase_from_vector(world->status.announcements, &df::report::id, reports[i]->id);
delete reports[i];
}
}
reports.erase(reports.begin(), reports.begin() + excess);
}
}
static int32_t check_repeat_report(vector<string> &results)
{ // returns the new repeat count, else 0
if (*gamemode == game_mode::DWARF && !results.empty() && world->status.reports.size() >= results.size())
{
auto &reports = world->status.reports;
size_t base = reports.size() - results.size(); // index where a repeat would start
size_t offset = 0;
while (reports[base + offset]->text == results[offset] && ++offset < results.size()); // match each report
if (offset == results.size()) // all lines matched
{
reports[base]->duration = ANNOUNCE_LINE_DURATION; // display the last line again
return ++(reports[base]->repeat_count);
}
}
return 0;
}
static bool add_proper_report(df::unit *unit, bool is_sparring, int report_index)
{ // add report to proper category based on is_sparring and unit current job
if (is_sparring)
return Gui::addCombatReport(unit, unit_report_type::Sparring, report_index);
else if (unit->job.current_job != NULL && unit->job.current_job->job_type == job_type::Hunt)
return Gui::addCombatReport(unit, unit_report_type::Hunting, report_index);
else
return Gui::addCombatReport(unit, unit_report_type::Combat, report_index);
}
// End of utility functions for reports
DFHACK_EXPORT int Gui::makeAnnouncement(df::announcement_type type, df::announcement_flags flags, df::coord pos, std::string message, int color, bool bright)
{
using df::global::world;
using df::global::cur_year;
using df::global::cur_year_tick;
if (message.empty())
return -1;
int year = 0, year_time = 0;
if (cur_year && cur_year_tick)
if (gamemode == NULL || cur_year == NULL || cur_year_tick == NULL)
{
year = *cur_year;
year_time = *cur_year_tick;
return -1;
}
else if (!world->status.reports.empty())
else if (message.empty())
{
// Fallback: copy from the last report
df::report *last = world->status.reports.back();
year = last->year;
year_time = last->time;
Core::printerr("Empty announcement %u\n", type); // DF would print this to errorlog.txt
return -1;
}
// Apply the requested effects
writeToGamelog(message);
if (flags.bits.DO_MEGA || flags.bits.PAUSE || flags.bits.RECENTER)
{
resetDwarfmodeView(flags.bits.DO_MEGA || flags.bits.PAUSE);
if (flags.bits.PAUSE || flags.bits.RECENTER)
pauseRecenter((flags.bits.RECENTER ? pos : df::coord()), flags.bits.PAUSE);
if (flags.bits.RECENTER && pos.isValid())
revealInDwarfmodeMap(pos, true);
bool adv_unconscious = (*gamemode == game_mode::ADVENTURE && !world->units.active.empty() && world->units.active[0]->counters.unconscious > 0);
if (flags.bits.DO_MEGA)
showPopupAnnouncement(message, color, bright);
}
if (flags.bits.DO_MEGA && !adv_unconscious)
showPopupAnnouncement(message, color, bright);
bool display = false;
vector<string> results;
word_wrap(&results, message, init->display.grid_x - 7);
if (gamemode == NULL)
display = flags.bits.A_DISPLAY || flags.bits.D_DISPLAY;
else if (*gamemode == game_mode::ADVENTURE)
display = flags.bits.A_DISPLAY;
else
display = flags.bits.D_DISPLAY;
// Check for repeat report
int32_t repeat_count = check_repeat_report(results); // Does nothing outside dwarf mode
if (repeat_count > 0)
{
if (flags.bits.D_DISPLAY)
{
world->status.display_timer = ANNOUNCE_DISPLAY_TIME;
Gui::writeToGamelog("x" + to_string(repeat_count + 1));
}
return -1;
}
// Not a repeat, write the message to gamelog.txt
writeToGamelog(message);
// Generate the report objects
int report_idx = world->status.reports.size();
bool continued = false;
bool display = ((*gamemode == game_mode::ADVENTURE && flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && flags.bits.D_DISPLAY));
while (!message.empty())
for (size_t i = 0; i < results.size(); i++)
{
df::report *new_rep = new df::report();
auto new_rep = new df::report();
new_rep->type = type;
new_rep->pos = pos;
new_rep->color = color;
new_rep->bright = bright;
new_rep->year = year;
new_rep->time = year_time;
new_rep->year = *cur_year;
new_rep->time = *cur_year_tick;
new_rep->flags.bits.continuation = continued;
int size = std::min(message.size(), (size_t)73);
new_rep->text = message.substr(0, size);
message = message.substr(size);
continued = true;
// Add the object to the lists
new_rep->text = results[i];
new_rep->id = world->status.next_report_id++;
world->status.reports.push_back(new_rep);
if (adv_unconscious)
new_rep->flags.bits.unconscious = true;
if (display)
{
insert_into_vector(world->status.announcements, &df::report::id, new_rep);
new_rep->flags.bits.announcement = true;
world->status.announcements.push_back(new_rep);
world->status.display_timer = 2000;
world->status.display_timer = ANNOUNCE_DISPLAY_TIME;
}
}
delete_old_reports();
return report_idx;
}
bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_index)
{
using df::global::world;
CHECK_INVALID_ARGUMENT(is_valid_enum_item(slot));
auto &vec = world->status.reports;
@ -1504,8 +1595,6 @@ bool Gui::addCombatReport(df::unit *unit, df::unit_report_type slot, int report_
bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int report_index)
{
using df::global::world;
auto &vec = world->status.reports;
auto report = vector_get(vec, report_index);
@ -1515,25 +1604,14 @@ bool Gui::addCombatReportAuto(df::unit *unit, df::announcement_flags mode, int r
bool ok = false;
if (mode.bits.UNIT_COMBAT_REPORT)
{
if (unit->flags2.bits.sparring)
ok |= addCombatReport(unit, unit_report_type::Sparring, report_index);
else if (unit->job.current_job && unit->job.current_job->job_type == job_type::Hunt)
ok |= addCombatReport(unit, unit_report_type::Hunting, report_index);
else
ok |= addCombatReport(unit, unit_report_type::Combat, report_index);
}
ok |= add_proper_report(unit, unit->flags2.bits.sparring, report_index);
if (mode.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE)
{
FOR_ENUM_ITEMS(unit_report_type, slot)
{
if (!unit->reports.log[slot].empty() &&
unit->reports.last_year[slot] == report->year &&
(report->time - unit->reports.last_year_tick[slot]) <= 500)
{
if (recent_report(unit, slot))
ok |= addCombatReport(unit, slot, report_index);
}
}
}
@ -1559,13 +1637,20 @@ void Gui::showZoomAnnouncement(
void Gui::showPopupAnnouncement(std::string message, int color, bool bright)
{
using df::global::world;
df::popup_message *popup = new df::popup_message();
popup->text = message;
popup->color = color;
popup->bright = bright;
world->status.popups.push_back(popup);
auto &popups = world->status.popups;
popups.push_back(popup);
while (popups.size() > MAX_REPORTS_SIZE)
{ // Delete old popups
if (popups[0] != NULL)
delete popups[0];
popups.erase(popups.begin());
}
}
void Gui::showAutoAnnouncement(
@ -1586,6 +1671,208 @@ void Gui::showAutoAnnouncement(
addCombatReportAuto(unit2, flags, id);
}
bool Gui::autoDFAnnouncement(df::report_init r, string message)
{ // Reverse-engineered from DF announcement code
if (!world->allow_announcements)
{
DEBUG(gui).print("Skipped announcement because world->allow_announcements is false:\n%s\n", message.c_str());
return false;
}
else if (!is_valid_enum_item(r.type))
{
WARN(gui).print("Invalid announcement type:\n%s\n", message.c_str());
return false;
}
else if (message.empty())
{
Core::printerr("Empty announcement %u\n", r.type); // DF would print this to errorlog.txt
return false;
}
df::announcement_flags a_flags = df::global::d_init->announcements.flags[r.type];
// Check if the announcement will actually be announced
if (*gamemode == game_mode::ADVENTURE)
{
if (r.pos.x != -30000 &&
r.type != announcement_type::CREATURE_SOUND &&
r.type != announcement_type::REGULAR_CONVERSATION &&
r.type != announcement_type::CONFLICT_CONVERSATION &&
r.type != announcement_type::MECHANISM_SOUND)
{ // If not sound, make sure we can see pos
if (world->units.active.empty() || (r.unit1 != world->units.active[0] && r.unit2 != world->units.active[0]))
{ // Adventure mode reuses a dwarf mode digging designation bit to determine current visibility
if (!Maps::isValidTilePos(r.pos) || (Maps::getTileDesignation(r.pos)->whole & 0x10) == 0x0)
{
DEBUG(gui).print("Adventure mode announcement not detected:\n%s\n", message.c_str());
return false;
}
}
}
}
else
{ // Dwarf mode
if ((r.unit1 != NULL || r.unit2 != NULL) && (r.unit1 == NULL || Units::isHidden(r.unit1)) && (r.unit2 == NULL || Units::isHidden(r.unit2)))
{
DEBUG(gui).print("Dwarf mode announcement not detected:\n%s\n", message.c_str());
return false;
}
if (!a_flags.bits.D_DISPLAY)
{
if (a_flags.bits.UNIT_COMBAT_REPORT)
{
if (r.unit1 == NULL && r.unit2 == NULL)
{
DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT because it has no units:\n%s\n", message.c_str());
return false;
}
}
else
{
if (!a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE)
{
DEBUG(gui).print("Skipped announcement not enabled for this game mode:\n%s\n", message.c_str());
return false;
}
else if (!recent_report_any(r.unit1) && !recent_report_any(r.unit2))
{
DEBUG(gui).print("Skipped UNIT_COMBAT_REPORT_ALL_ACTIVE because there's no active report:\n%s\n", message.c_str());
return false;
}
}
}
}
if (a_flags.bits.PAUSE || a_flags.bits.RECENTER)
pauseRecenter((a_flags.bits.RECENTER ? r.pos : df::coord()), a_flags.bits.PAUSE); // Does nothing outside dwarf mode
bool adv_unconscious = (*gamemode == game_mode::ADVENTURE && !world->units.active.empty() && world->units.active[0]->counters.unconscious > 0);
if (a_flags.bits.DO_MEGA && !adv_unconscious)
showPopupAnnouncement(message, r.color, r.bright);
vector<string> results;
size_t line_length = (r.speaker_id == -1) ? (init->display.grid_x - 7) : (init->display.grid_x - 10);
parseReportString(&results, message, line_length);
if (results.empty()) // DF doesn't do this check
{
DEBUG(gui).print("Skipped announcement because it was empty after parsing:\n%s\n", message.c_str());
return false;
}
// Check for repeat report
int32_t repeat_count = check_repeat_report(results); // always returns 0 outside dwarf mode
if (repeat_count > 0)
{
if (a_flags.bits.D_DISPLAY)
{
world->status.display_timer = r.display_timer;
Gui::writeToGamelog("x" + to_string(repeat_count + 1));
}
DEBUG(gui).print("Announcement succeeded as repeat:\n%s\n", message.c_str());
return true;
}
size_t new_report_index = world->status.reports.size(); // we need this for addCombatReport
bool success = false; // only print to gamelog if report was used
bool display = ((*gamemode == game_mode::ADVENTURE && a_flags.bits.A_DISPLAY) || (*gamemode == game_mode::DWARF && a_flags.bits.D_DISPLAY));
for (size_t i = 0; i < results.size(); i++)
{ // Generate report entries for each line
auto new_report = new df::report();
new_report->type = r.type;
new_report->text = results[i];
new_report->color = r.color;
new_report->bright = r.bright;
new_report->flags.whole = 0x0;
new_report->zoom_type = r.zoom_type;
new_report->pos = r.pos;
new_report->zoom_type2 = r.zoom_type2;
new_report->pos2 = r.pos2;
new_report->id = world->status.next_report_id++;
new_report->year = *df::global::cur_year;
new_report->time = *df::global::cur_year_tick;
new_report->unk_v40_1 = r.unk_v40_1;
new_report->unk_v40_2 = r.unk_v40_2;
new_report->speaker_id = r.speaker_id;
world->status.reports.push_back(new_report);
if (i > 0)
new_report->flags.bits.continuation = true;
if (adv_unconscious)
new_report->flags.bits.unconscious = true;
if (display)
{
insert_into_vector(world->status.announcements, &df::report::id, new_report);
new_report->flags.bits.announcement = true;
world->status.display_timer = r.display_timer;
success = true;
}
}
if (*gamemode == game_mode::DWARF) // DF does this inside the previous loop, but we're using addCombatReport instead
{
if (a_flags.bits.UNIT_COMBAT_REPORT)
{
if (r.unit1 != NULL)
success |= add_proper_report(r.unit1, !r.flags.bits.hostile_combat, new_report_index);
if (r.unit2 != NULL)
success |= add_proper_report(r.unit2, !r.flags.bits.hostile_combat, new_report_index);
}
if (a_flags.bits.UNIT_COMBAT_REPORT_ALL_ACTIVE)
{
FOR_ENUM_ITEMS(unit_report_type, slot)
{
if (recent_report(r.unit1, slot))
success |= addCombatReport(r.unit1, slot, new_report_index);
if (recent_report(r.unit2, slot))
success |= addCombatReport(r.unit2, slot, new_report_index);
}
}
}
delete_old_reports();
if (/*debug_gamelog &&*/ success) // TODO: Add debug_gamelog to globals?
{
DEBUG(gui).print("Announcement succeeded and printed to gamelog.txt:\n%s\n", message.c_str());
Gui::writeToGamelog(message);
}
/*else if (success)
{
DEBUG(gui).print("Announcement succeeded but skipped printing to gamelog.txt because debug_gamelog is false:\n%s\n", message.c_str());
}*/
else // not sure if this can actually happen; our results.empty() check handles the one edge case I can think of that would get this far
{
DEBUG(gui).print("Announcement succeeded internally but didn't qualify to be displayed anywhere:\n%s\n", message.c_str());
}
return true;
}
bool Gui::autoDFAnnouncement(df::announcement_type type, df::coord pos, std::string message, int color,
bool bright, df::unit *unit1, df::unit *unit2, bool is_sparring)
{
auto r = df::report_init();
r.type = type;
r.color = color;
r.bright = bright;
r.pos = pos;
r.display_timer = ANNOUNCE_DISPLAY_TIME;
r.unit1 = unit1;
r.unit2 = unit2;
r.flags.bits.hostile_combat = !is_sparring;
return autoDFAnnouncement(r, message);
}
df::viewscreen *Gui::getCurViewscreen(bool skip_dismissed)
{
if (!gview)
@ -1655,7 +1942,7 @@ Gui::DwarfmodeDims getDwarfmodeViewDims_default()
int menu_pos = (ui_menu_width ? (*ui_menu_width)[0] : 2);
int area_pos = (ui_menu_width ? (*ui_menu_width)[1] : 3);
if (ui && ui->main.mode && menu_pos >= area_pos)
if (ui && ui->main.mode != ui_sidebar_mode::Default && ui->main.mode != ui_sidebar_mode::ArenaWeather && menu_pos >= area_pos)
{
dims.menu_forced = true;
menu_pos = area_pos-1;
@ -1715,38 +2002,68 @@ void Gui::resetDwarfmodeView(bool pause)
*df::global::pause_state = true;
}
bool Gui::revealInDwarfmodeMap(df::coord pos, bool center)
{
bool Gui::revealInDwarfmodeMap(int32_t x, int32_t y, int32_t z, bool center)
{ // Reverse-engineered from DF announcement and scrolling code
using df::global::window_x;
using df::global::window_y;
using df::global::window_z;
if (!window_x || !window_y || !window_z || !world)
return false;
if (!Maps::isValidTilePos(pos))
return false;
auto dims = getDwarfmodeViewDims();
int w = dims.map_x2 - dims.map_x1 + 1;
int h = dims.map_y2 - dims.map_y1 + 1;
*window_z = pos.z;
int32_t w = dims.map_x2 - dims.map_x1 + 1;
int32_t h = dims.map_y2 - dims.map_y1 + 1;
int32_t new_win_x, new_win_y, new_win_z;
getViewCoords(new_win_x, new_win_y, new_win_z);
if (center)
if (Maps::isValidTilePos(x, y, z))
{
*window_x = pos.x - w/2;
*window_y = pos.y - h/2;
if (center)
{
new_win_x = x - w / 2;
new_win_y = y - h / 2;
}
else // just bring it on screen
{
if (new_win_x > (x - 5)) // equivalent to: "while (new_win_x > x - 5) new_win_x -= 10;"
new_win_x -= (new_win_x - (x - 5) - 1) / 10 * 10 + 10;
if (new_win_y > (y - 5))
new_win_y -= (new_win_y - (y - 5) - 1) / 10 * 10 + 10;
if (new_win_x < (x + 5 - w))
new_win_x += ((x + 5 - w) - new_win_x - 1) / 10 * 10 + 10;
if (new_win_y < (y + 5 - h))
new_win_y += ((y + 5 - h) - new_win_y - 1) / 10 * 10 + 10;
}
new_win_z = z;
}
else
*window_x = clip_range(new_win_x, 0, (world->map.x_count - w));
*window_y = clip_range(new_win_y, 0, (world->map.y_count - h));
*window_z = clip_range(new_win_z, 0, (world->map.z_count - 1));
ui_sidebar_menus->minimap.need_render = true;
ui_sidebar_menus->minimap.need_scan = true;
return true;
}
bool Gui::pauseRecenter(int32_t x, int32_t y, int32_t z, bool pause)
{ // Reverse-engineered from DF announcement code
if (*gamemode != game_mode::DWARF)
return false;
resetDwarfmodeView(pause);
if (Maps::isValidTilePos(x, y, z))
revealInDwarfmodeMap(x, y, z, false);
if (init->input.pause_zoom_no_interface_ms > 0)
{
while (*window_x + w < pos.x+5) *window_x += 10;
while (*window_y + h < pos.y+5) *window_y += 10;
while (*window_x + 5 > pos.x) *window_x -= 10;
while (*window_y + 5 > pos.y) *window_y -= 10;
gview->shutdown_interface_tickcount = Core::getInstance().p->getTickCount();
gview->shutdown_interface_for_ms = init->input.pause_zoom_no_interface_ms;
}
*window_x = std::max(0, std::min(*window_x, world->map.x_count-w));
*window_y = std::max(0, std::min(*window_y, world->map.y_count-h));
return true;
}
@ -1821,17 +2138,17 @@ bool Gui::setCursorCoords (const int32_t x, const int32_t y, const int32_t z)
bool Gui::getDesignationCoords (int32_t &x, int32_t &y, int32_t &z)
{
x = df::global::selection_rect->start_x;
y = df::global::selection_rect->start_y;
z = df::global::selection_rect->start_z;
x = selection_rect->start_x;
y = selection_rect->start_y;
z = selection_rect->start_z;
return (x == -30000) ? false : true;
}
bool Gui::setDesignationCoords (const int32_t x, const int32_t y, const int32_t z)
{
df::global::selection_rect->start_x = x;
df::global::selection_rect->start_y = y;
df::global::selection_rect->start_z = z;
selection_rect->start_x = x;
selection_rect->start_y = y;
selection_rect->start_z = z;
return true;
}
@ -1880,14 +2197,14 @@ bool Gui::getWindowSize (int32_t &width, int32_t &height)
bool Gui::getMenuWidth(uint8_t &menu_width, uint8_t &area_map_width)
{
menu_width = (*df::global::ui_menu_width)[0];
area_map_width = (*df::global::ui_menu_width)[1];
menu_width = (*ui_menu_width)[0];
area_map_width = (*ui_menu_width)[1];
return true;
}
bool Gui::setMenuWidth(const uint8_t menu_width, const uint8_t area_map_width)
{
(*df::global::ui_menu_width)[0] = menu_width;
(*df::global::ui_menu_width)[1] = area_map_width;
(*ui_menu_width)[0] = menu_width;
(*ui_menu_width)[1] = area_map_width;
return true;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,7 @@
file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false *test.cpp)
dfhack_test(test-library "${TEST_SOURCES}")
# How to get `test` to ensure everything is up to date before running
# tests? This add_dependencies() fails with:
# Cannot add target-level dependencies to non-existent target "test".
#add_dependencies(test MiscUtils.test)

@ -0,0 +1,19 @@
#include "MiscUtils.h"
#include <gtest/gtest.h>
#include <string>
TEST(MiscUtils, wordwrap) {
std::vector<std::string> result;
word_wrap(&result, "123", 3);
ASSERT_EQ(result.size(), 1);
result.clear();
word_wrap(&result, "12345", 3);
ASSERT_EQ(result.size(), 2);
result.clear();
word_wrap(&result, "1234567", 3);
ASSERT_EQ(result.size(), 3);
}

@ -0,0 +1,6 @@
#include <gtest/gtest.h>
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

@ -1 +1 @@
Subproject commit ea78ed8bf70c3e75b8fba90cdc61cab34788899e
Subproject commit 4c5697dcb060d645849327410b8ecce6880053d4

@ -88,8 +88,7 @@ if(BUILD_SUPPORTED)
dfhack_plugin(autodump autodump.cpp)
dfhack_plugin(autofarm autofarm.cpp)
dfhack_plugin(autogems autogems.cpp LINK_LIBRARIES jsoncpp_static)
dfhack_plugin(autohauler autohauler.cpp)
dfhack_plugin(autolabor autolabor.cpp)
add_subdirectory(autolabor)
dfhack_plugin(automaterial automaterial.cpp LINK_LIBRARIES lua)
dfhack_plugin(automelt automelt.cpp)
dfhack_plugin(autonestbox autonestbox.cpp LINK_LIBRARIES lua)
@ -101,6 +100,7 @@ if(BUILD_SUPPORTED)
dfhack_plugin(changeitem changeitem.cpp)
dfhack_plugin(changelayer changelayer.cpp)
dfhack_plugin(changevein changevein.cpp)
add_subdirectory(channel-safely)
dfhack_plugin(cleanconst cleanconst.cpp)
dfhack_plugin(cleaners cleaners.cpp)
dfhack_plugin(cleanowned cleanowned.cpp)
@ -133,7 +133,6 @@ if(BUILD_SUPPORTED)
dfhack_plugin(infiniteSky infiniteSky.cpp)
dfhack_plugin(isoworldremote isoworldremote.cpp PROTOBUFS isoworldremote)
dfhack_plugin(jobutils jobutils.cpp)
add_subdirectory(labormanager)
dfhack_plugin(lair lair.cpp)
dfhack_plugin(liquids liquids.cpp Brushes.h LINK_LIBRARIES lua)
dfhack_plugin(luasocket luasocket.cpp LINK_LIBRARIES clsocket lua dfhack-tinythread)
@ -144,7 +143,7 @@ if(BUILD_SUPPORTED)
dfhack_plugin(mousequery mousequery.cpp)
dfhack_plugin(nestboxes nestboxes.cpp)
dfhack_plugin(orders orders.cpp LINK_LIBRARIES jsoncpp_static)
dfhack_plugin(overlay overlay.cpp)
dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua)
dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua)
dfhack_plugin(petcapRemover petcapRemover.cpp)
dfhack_plugin(plants plants.cpp)
@ -155,7 +154,6 @@ if(BUILD_SUPPORTED)
add_subdirectory(remotefortressreader)
dfhack_plugin(rename rename.cpp LINK_LIBRARIES lua PROTOBUFS rename)
add_subdirectory(rendermax)
dfhack_plugin(resume resume.cpp LINK_LIBRARIES lua)
dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua)
dfhack_plugin(search search.cpp)
dfhack_plugin(seedwatch seedwatch.cpp)

@ -317,6 +317,8 @@ public:
{
set_farms(out, plants[ff.first], ff.second);
}
out << std::flush;
}
void status(color_ostream& out)
@ -336,6 +338,8 @@ public:
out << plant->id << " limit " << getThreshold(th.first) << " current 0" << '\n';
}
out << "Default: " << defaultThreshold << '\n';
out << std::flush;
}
};
@ -409,7 +413,7 @@ static command_result setThresholds(color_ostream& out, std::vector<std::string>
}
if (!ok)
{
out << "Cannot find plant with id " << id << '\n';
out << "Cannot find plant with id " << id << '\n' << std::flush;
return CR_WRONG_USAGE;
}
}

@ -0,0 +1,16 @@
project(autolahor)
# A list of source files
set(COMMON_SRCS
)
# A list of headers
set(COMMON_HDRS laborstatemap.h
)
set_source_files_properties(${COMMON_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE)
# mash them together (headers are marked as headers and nothing will try to compile them)
list(APPEND COMMON_SRCS ${COMMON_HDRS})
dfhack_plugin(labormanager labormanager.cpp joblabormapper.cpp ${COMMON_SRCS})
dfhack_plugin(autohauler autohauler.cpp ${COMMON_SRCS})
dfhack_plugin(autolabor autolabor.cpp ${COMMON_SRCS})

@ -43,6 +43,8 @@
#include "modules/Items.h"
#include "modules/Units.h"
#include "laborstatemap.h"
// Not sure what this does, but may have to figure it out later
#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0]))
@ -127,287 +129,7 @@ static void setOptionEnabled(ConfigFlags flag, bool on)
}
// This is a vector of states and number of dwarves in that state
static std::vector<int> state_count(5);
// Employment status of dwarves
enum dwarf_state {
// Ready for a new task
IDLE,
// Busy with a useful task
BUSY,
// In the military, can't work
MILITARY,
// Baby or Child, can't work
CHILD,
// Doing something that precludes working, may be busy for a while
OTHER
};
// I presume this is the number of states in the following enumeration.
static const int NUM_STATE = 5;
// This is a list of strings to be associated with aforementioned dwarf_state
// struct
static const char *state_names[] = {
"IDLE",
"BUSY",
"MILITARY",
"CHILD",
"OTHER"
};
// List of possible activites of a dwarf that will be further narrowed to states
// IDLE - Specifically waiting to be assigned a task (No Job)
// BUSY - Performing a toggleable labor, or a support action for that labor.
// OTHER - Doing something else
static const dwarf_state dwarf_states[] = {
BUSY /* CarveFortification */,
BUSY /* DetailWall */,
BUSY /* DetailFloor */,
BUSY /* Dig */,
BUSY /* CarveUpwardStaircase */,
BUSY /* CarveDownwardStaircase */,
BUSY /* CarveUpDownStaircase */,
BUSY /* CarveRamp */,
BUSY /* DigChannel */,
BUSY /* FellTree */,
BUSY /* GatherPlants */,
BUSY /* RemoveConstruction */,
BUSY /* CollectWebs */,
BUSY /* BringItemToDepot */,
BUSY /* BringItemToShop */,
OTHER /* Eat */,
OTHER /* GetProvisions */,
OTHER /* Drink */,
OTHER /* Drink2 */,
OTHER /* FillWaterskin */,
OTHER /* FillWaterskin2 */,
OTHER /* Sleep */,
BUSY /* CollectSand */,
BUSY /* Fish */,
BUSY /* Hunt */,
BUSY /* HuntVermin */,
OTHER /* Kidnap */,
OTHER /* BeatCriminal */,
OTHER /* StartingFistFight */,
OTHER /* CollectTaxes */,
OTHER /* GuardTaxCollector */,
BUSY /* CatchLiveLandAnimal */,
BUSY /* CatchLiveFish */,
OTHER /* ReturnKill */,
OTHER /* CheckChest */,
OTHER /* StoreOwnedItem */,
BUSY /* PlaceItemInTomb */,
BUSY /* StoreItemInStockpile */,
BUSY /* StoreItemInBag */,
BUSY /* StoreItemInHospital */,
BUSY /* StoreItemInChest */,
BUSY /* StoreItemInCabinet */,
BUSY /* StoreWeapon */,
BUSY /* StoreArmor */,
BUSY /* StoreItemInBarrel */,
BUSY /* StoreItemInBin */,
OTHER /* SeekArtifact */,
OTHER /* SeekInfant */,
OTHER /* AttendParty */,
OTHER /* GoShopping */,
OTHER /* GoShopping2 */,
OTHER /* Clean */,
OTHER /* Rest */,
BUSY /* PickupEquipment */,
BUSY /* DumpItem */,
OTHER /* StrangeMoodCrafter */,
OTHER /* StrangeMoodJeweller */,
OTHER /* StrangeMoodForge */,
OTHER /* StrangeMoodMagmaForge */,
OTHER /* StrangeMoodBrooding */,
OTHER /* StrangeMoodFell */,
OTHER /* StrangeMoodCarpenter */,
OTHER /* StrangeMoodMason */,
OTHER /* StrangeMoodBowyer */,
OTHER /* StrangeMoodTanner */,
OTHER /* StrangeMoodWeaver */,
OTHER /* StrangeMoodGlassmaker */,
OTHER /* StrangeMoodMechanics */,
BUSY /* ConstructBuilding */,
BUSY /* ConstructDoor */,
BUSY /* ConstructFloodgate */,
BUSY /* ConstructBed */,
BUSY /* ConstructThrone */,
BUSY /* ConstructCoffin */,
BUSY /* ConstructTable */,
BUSY /* ConstructChest */,
BUSY /* ConstructBin */,
BUSY /* ConstructArmorStand */,
BUSY /* ConstructWeaponRack */,
BUSY /* ConstructCabinet */,
BUSY /* ConstructStatue */,
BUSY /* ConstructBlocks */,
BUSY /* MakeRawGlass */,
BUSY /* MakeCrafts */,
BUSY /* MintCoins */,
BUSY /* CutGems */,
BUSY /* CutGlass */,
BUSY /* EncrustWithGems */,
BUSY /* EncrustWithGlass */,
BUSY /* DestroyBuilding */,
BUSY /* SmeltOre */,
BUSY /* MeltMetalObject */,
BUSY /* ExtractMetalStrands */,
BUSY /* PlantSeeds */,
BUSY /* HarvestPlants */,
BUSY /* TrainHuntingAnimal */,
BUSY /* TrainWarAnimal */,
BUSY /* MakeWeapon */,
BUSY /* ForgeAnvil */,
BUSY /* ConstructCatapultParts */,
BUSY /* ConstructBallistaParts */,
BUSY /* MakeArmor */,
BUSY /* MakeHelm */,
BUSY /* MakePants */,
BUSY /* StudWith */,
BUSY /* ButcherAnimal */,
BUSY /* PrepareRawFish */,
BUSY /* MillPlants */,
BUSY /* BaitTrap */,
BUSY /* MilkCreature */,
BUSY /* MakeCheese */,
BUSY /* ProcessPlants */,
BUSY /* ProcessPlantsBag */,
BUSY /* ProcessPlantsVial */,
BUSY /* ProcessPlantsBarrel */,
BUSY /* PrepareMeal */,
BUSY /* WeaveCloth */,
BUSY /* MakeGloves */,
BUSY /* MakeShoes */,
BUSY /* MakeShield */,
BUSY /* MakeCage */,
BUSY /* MakeChain */,
BUSY /* MakeFlask */,
BUSY /* MakeGoblet */,
BUSY /* MakeInstrument */,
BUSY /* MakeToy */,
BUSY /* MakeAnimalTrap */,
BUSY /* MakeBarrel */,
BUSY /* MakeBucket */,
BUSY /* MakeWindow */,
BUSY /* MakeTotem */,
BUSY /* MakeAmmo */,
BUSY /* DecorateWith */,
BUSY /* MakeBackpack */,
BUSY /* MakeQuiver */,
BUSY /* MakeBallistaArrowHead */,
BUSY /* AssembleSiegeAmmo */,
BUSY /* LoadCatapult */,
BUSY /* LoadBallista */,
BUSY /* FireCatapult */,
BUSY /* FireBallista */,
BUSY /* ConstructMechanisms */,
BUSY /* MakeTrapComponent */,
BUSY /* LoadCageTrap */,
BUSY /* LoadStoneTrap */,
BUSY /* LoadWeaponTrap */,
BUSY /* CleanTrap */,
OTHER /* CastSpell */,
BUSY /* LinkBuildingToTrigger */,
BUSY /* PullLever */,
BUSY /* BrewDrink */,
BUSY /* ExtractFromPlants */,
BUSY /* ExtractFromRawFish */,
BUSY /* ExtractFromLandAnimal */,
BUSY /* TameVermin */,
BUSY /* TameAnimal */,
BUSY /* ChainAnimal */,
BUSY /* UnchainAnimal */,
BUSY /* UnchainPet */,
BUSY /* ReleaseLargeCreature */,
BUSY /* ReleasePet */,
BUSY /* ReleaseSmallCreature */,
BUSY /* HandleSmallCreature */,
BUSY /* HandleLargeCreature */,
BUSY /* CageLargeCreature */,
BUSY /* CageSmallCreature */,
BUSY /* RecoverWounded */,
BUSY /* DiagnosePatient */,
BUSY /* ImmobilizeBreak */,
BUSY /* DressWound */,
BUSY /* CleanPatient */,
BUSY /* Surgery */,
BUSY /* Suture */,
BUSY /* SetBone */,
BUSY /* PlaceInTraction */,
BUSY /* DrainAquarium */,
BUSY /* FillAquarium */,
BUSY /* FillPond */,
BUSY /* GiveWater */,
BUSY /* GiveFood */,
BUSY /* GiveWater2 */,
BUSY /* GiveFood2 */,
BUSY /* RecoverPet */,
BUSY /* PitLargeAnimal */,
BUSY /* PitSmallAnimal */,
BUSY /* SlaughterAnimal */,
BUSY /* MakeCharcoal */,
BUSY /* MakeAsh */,
BUSY /* MakeLye */,
BUSY /* MakePotashFromLye */,
BUSY /* FertilizeField */,
BUSY /* MakePotashFromAsh */,
BUSY /* DyeThread */,
BUSY /* DyeCloth */,
BUSY /* SewImage */,
BUSY /* MakePipeSection */,
BUSY /* OperatePump */,
OTHER /* ManageWorkOrders */,
OTHER /* UpdateStockpileRecords */,
OTHER /* TradeAtDepot */,
BUSY /* ConstructHatchCover */,
BUSY /* ConstructGrate */,
BUSY /* RemoveStairs */,
BUSY /* ConstructQuern */,
BUSY /* ConstructMillstone */,
BUSY /* ConstructSplint */,
BUSY /* ConstructCrutch */,
BUSY /* ConstructTractionBench */,
OTHER /* CleanSelf */,
BUSY /* BringCrutch */,
BUSY /* ApplyCast */,
BUSY /* CustomReaction */,
BUSY /* ConstructSlab */,
BUSY /* EngraveSlab */,
BUSY /* ShearCreature */,
BUSY /* SpinThread */,
BUSY /* PenLargeAnimal */,
BUSY /* PenSmallAnimal */,
BUSY /* MakeTool */,
BUSY /* CollectClay */,
BUSY /* InstallColonyInHive */,
BUSY /* CollectHiveProducts */,
OTHER /* CauseTrouble */,
OTHER /* DrinkBlood */,
OTHER /* ReportCrime */,
OTHER /* ExecuteCriminal */,
BUSY /* TrainAnimal */,
BUSY /* CarveTrack */,
BUSY /* PushTrackVehicle */,
BUSY /* PlaceTrackVehicle */,
BUSY /* StoreItemInVehicle */,
BUSY /* GeldAnimal */,
BUSY /* MakeFigurine */,
BUSY /* MakeAmulet */,
BUSY /* MakeScepter */,
BUSY /* MakeCrown */,
BUSY /* MakeRing */,
BUSY /* MakeEarring */,
BUSY /* MakeBracelet */,
BUSY /* MakeGem */,
BUSY /* PutItemOnDisplay */,
};
static std::vector<int> state_count(NUM_STATE);
// Mode assigned to labors. Either it's a hauling job, or it's not.
enum labor_mode {

@ -40,6 +40,8 @@
#include "modules/Items.h"
#include "modules/Units.h"
#include "laborstatemap.h"
using std::string;
using std::endl;
using std::vector;
@ -102,280 +104,6 @@ enum labor_mode {
AUTOMATIC,
};
enum dwarf_state {
// Ready for a new task
IDLE,
// Busy with a useful task
BUSY,
// Busy with a useful task that requires a tool
EXCLUSIVE,
// In the military, can't work
MILITARY,
// Child or noble, can't work
CHILD,
// Doing something that precludes working, may be busy for a while
OTHER
};
const int NUM_STATE = 6;
static const char *state_names[] = {
"IDLE",
"BUSY",
"EXCLUSIVE",
"MILITARY",
"CHILD",
"OTHER",
};
static const dwarf_state dwarf_states[] = {
BUSY /* CarveFortification */,
BUSY /* DetailWall */,
BUSY /* DetailFloor */,
EXCLUSIVE /* Dig */,
EXCLUSIVE /* CarveUpwardStaircase */,
EXCLUSIVE /* CarveDownwardStaircase */,
EXCLUSIVE /* CarveUpDownStaircase */,
EXCLUSIVE /* CarveRamp */,
EXCLUSIVE /* DigChannel */,
EXCLUSIVE /* FellTree */,
BUSY /* GatherPlants */,
BUSY /* RemoveConstruction */,
BUSY /* CollectWebs */,
BUSY /* BringItemToDepot */,
BUSY /* BringItemToShop */,
OTHER /* Eat */,
OTHER /* GetProvisions */,
OTHER /* Drink */,
OTHER /* Drink2 */,
OTHER /* FillWaterskin */,
OTHER /* FillWaterskin2 */,
OTHER /* Sleep */,
BUSY /* CollectSand */,
BUSY /* Fish */,
EXCLUSIVE /* Hunt */,
OTHER /* HuntVermin */,
BUSY /* Kidnap */,
BUSY /* BeatCriminal */,
BUSY /* StartingFistFight */,
BUSY /* CollectTaxes */,
BUSY /* GuardTaxCollector */,
BUSY /* CatchLiveLandAnimal */,
BUSY /* CatchLiveFish */,
BUSY /* ReturnKill */,
BUSY /* CheckChest */,
BUSY /* StoreOwnedItem */,
BUSY /* PlaceItemInTomb */,
BUSY /* StoreItemInStockpile */,
BUSY /* StoreItemInBag */,
BUSY /* StoreItemInHospital */,
BUSY /* StoreItemInChest */,
BUSY /* StoreItemInCabinet */,
BUSY /* StoreWeapon */,
BUSY /* StoreArmor */,
BUSY /* StoreItemInBarrel */,
BUSY /* StoreItemInBin */,
BUSY /* SeekArtifact */,
BUSY /* SeekInfant */,
OTHER /* AttendParty */,
OTHER /* GoShopping */,
OTHER /* GoShopping2 */,
BUSY /* Clean */,
OTHER /* Rest */,
EXCLUSIVE /* PickupEquipment */,
BUSY /* DumpItem */,
OTHER /* StrangeMoodCrafter */,
OTHER /* StrangeMoodJeweller */,
OTHER /* StrangeMoodForge */,
OTHER /* StrangeMoodMagmaForge */,
OTHER /* StrangeMoodBrooding */,
OTHER /* StrangeMoodFell */,
OTHER /* StrangeMoodCarpenter */,
OTHER /* StrangeMoodMason */,
OTHER /* StrangeMoodBowyer */,
OTHER /* StrangeMoodTanner */,
OTHER /* StrangeMoodWeaver */,
OTHER /* StrangeMoodGlassmaker */,
OTHER /* StrangeMoodMechanics */,
BUSY /* ConstructBuilding */,
BUSY /* ConstructDoor */,
BUSY /* ConstructFloodgate */,
BUSY /* ConstructBed */,
BUSY /* ConstructThrone */,
BUSY /* ConstructCoffin */,
BUSY /* ConstructTable */,
BUSY /* ConstructChest */,
BUSY /* ConstructBin */,
BUSY /* ConstructArmorStand */,
BUSY /* ConstructWeaponRack */,
BUSY /* ConstructCabinet */,
BUSY /* ConstructStatue */,
BUSY /* ConstructBlocks */,
BUSY /* MakeRawGlass */,
BUSY /* MakeCrafts */,
BUSY /* MintCoins */,
BUSY /* CutGems */,
BUSY /* CutGlass */,
BUSY /* EncrustWithGems */,
BUSY /* EncrustWithGlass */,
BUSY /* DestroyBuilding */,
BUSY /* SmeltOre */,
BUSY /* MeltMetalObject */,
BUSY /* ExtractMetalStrands */,
BUSY /* PlantSeeds */,
BUSY /* HarvestPlants */,
BUSY /* TrainHuntingAnimal */,
BUSY /* TrainWarAnimal */,
BUSY /* MakeWeapon */,
BUSY /* ForgeAnvil */,
BUSY /* ConstructCatapultParts */,
BUSY /* ConstructBallistaParts */,
BUSY /* MakeArmor */,
BUSY /* MakeHelm */,
BUSY /* MakePants */,
BUSY /* StudWith */,
BUSY /* ButcherAnimal */,
BUSY /* PrepareRawFish */,
BUSY /* MillPlants */,
BUSY /* BaitTrap */,
BUSY /* MilkCreature */,
BUSY /* MakeCheese */,
BUSY /* ProcessPlants */,
BUSY /* ProcessPlantsBag */,
BUSY /* ProcessPlantsVial */,
BUSY /* ProcessPlantsBarrel */,
BUSY /* PrepareMeal */,
BUSY /* WeaveCloth */,
BUSY /* MakeGloves */,
BUSY /* MakeShoes */,
BUSY /* MakeShield */,
BUSY /* MakeCage */,
BUSY /* MakeChain */,
BUSY /* MakeFlask */,
BUSY /* MakeGoblet */,
BUSY /* MakeInstrument */,
BUSY /* MakeToy */,
BUSY /* MakeAnimalTrap */,
BUSY /* MakeBarrel */,
BUSY /* MakeBucket */,
BUSY /* MakeWindow */,
BUSY /* MakeTotem */,
BUSY /* MakeAmmo */,
BUSY /* DecorateWith */,
BUSY /* MakeBackpack */,
BUSY /* MakeQuiver */,
BUSY /* MakeBallistaArrowHead */,
BUSY /* AssembleSiegeAmmo */,
BUSY /* LoadCatapult */,
BUSY /* LoadBallista */,
BUSY /* FireCatapult */,
BUSY /* FireBallista */,
BUSY /* ConstructMechanisms */,
BUSY /* MakeTrapComponent */,
BUSY /* LoadCageTrap */,
BUSY /* LoadStoneTrap */,
BUSY /* LoadWeaponTrap */,
BUSY /* CleanTrap */,
BUSY /* CastSpell */,
BUSY /* LinkBuildingToTrigger */,
BUSY /* PullLever */,
BUSY /* BrewDrink */,
BUSY /* ExtractFromPlants */,
BUSY /* ExtractFromRawFish */,
BUSY /* ExtractFromLandAnimal */,
BUSY /* TameVermin */,
BUSY /* TameAnimal */,
BUSY /* ChainAnimal */,
BUSY /* UnchainAnimal */,
BUSY /* UnchainPet */,
BUSY /* ReleaseLargeCreature */,
BUSY /* ReleasePet */,
BUSY /* ReleaseSmallCreature */,
BUSY /* HandleSmallCreature */,
BUSY /* HandleLargeCreature */,
BUSY /* CageLargeCreature */,
BUSY /* CageSmallCreature */,
BUSY /* RecoverWounded */,
BUSY /* DiagnosePatient */,
BUSY /* ImmobilizeBreak */,
BUSY /* DressWound */,
BUSY /* CleanPatient */,
BUSY /* Surgery */,
BUSY /* Suture */,
BUSY /* SetBone */,
BUSY /* PlaceInTraction */,
BUSY /* DrainAquarium */,
BUSY /* FillAquarium */,
BUSY /* FillPond */,
BUSY /* GiveWater */,
BUSY /* GiveFood */,
BUSY /* GiveWater2 */,
BUSY /* GiveFood2 */,
BUSY /* RecoverPet */,
BUSY /* PitLargeAnimal */,
BUSY /* PitSmallAnimal */,
BUSY /* SlaughterAnimal */,
BUSY /* MakeCharcoal */,
BUSY /* MakeAsh */,
BUSY /* MakeLye */,
BUSY /* MakePotashFromLye */,
BUSY /* FertilizeField */,
BUSY /* MakePotashFromAsh */,
BUSY /* DyeThread */,
BUSY /* DyeCloth */,
BUSY /* SewImage */,
BUSY /* MakePipeSection */,
BUSY /* OperatePump */,
OTHER /* ManageWorkOrders */,
OTHER /* UpdateStockpileRecords */,
OTHER /* TradeAtDepot */,
BUSY /* ConstructHatchCover */,
BUSY /* ConstructGrate */,
BUSY /* RemoveStairs */,
BUSY /* ConstructQuern */,
BUSY /* ConstructMillstone */,
BUSY /* ConstructSplint */,
BUSY /* ConstructCrutch */,
BUSY /* ConstructTractionBench */,
BUSY /* CleanSelf */,
BUSY /* BringCrutch */,
BUSY /* ApplyCast */,
BUSY /* CustomReaction */,
BUSY /* ConstructSlab */,
BUSY /* EngraveSlab */,
BUSY /* ShearCreature */,
BUSY /* SpinThread */,
BUSY /* PenLargeAnimal */,
BUSY /* PenSmallAnimal */,
BUSY /* MakeTool */,
BUSY /* CollectClay */,
BUSY /* InstallColonyInHive */,
BUSY /* CollectHiveProducts */,
OTHER /* CauseTrouble */,
OTHER /* DrinkBlood */,
OTHER /* ReportCrime */,
OTHER /* ExecuteCriminal */,
BUSY /* TrainAnimal */,
BUSY /* CarveTrack */,
BUSY /* PushTrackVehicle */,
BUSY /* PlaceTrackVehicle */,
BUSY /* StoreItemInVehicle */,
BUSY /* GeldAnimal */,
BUSY /* MakeFigurine */,
BUSY /* MakeAmulet */,
BUSY /* MakeScepter */,
BUSY /* MakeCrown */,
BUSY /* MakeRing */,
BUSY /* MakeEarring */,
BUSY /* MakeBracelet */,
BUSY /* MakeGem */,
BUSY /* PutItemOnDisplay */,
};
struct labor_info
{

@ -75,6 +75,8 @@
#include "labormanager.h"
#include "joblabormapper.h"
#include "laborstatemap.h"
using namespace std;
using std::string;
using std::endl;
@ -116,280 +118,6 @@ DFHACK_PLUGIN("labormanager");
static void generate_labor_to_skill_map();
enum dwarf_state {
// Ready for a new task
IDLE,
// Busy with a useful task
BUSY,
// In the military, can't work
MILITARY,
// Child or noble, can't work
CHILD,
// Doing something that precludes working, may be busy for a while
OTHER
};
const int NUM_STATE = 5;
static const char *state_names[] = {
"IDLE",
"BUSY",
"MILITARY",
"CHILD",
"OTHER",
};
static const dwarf_state dwarf_states[] = {
BUSY /* CarveFortification */,
BUSY /* DetailWall */,
BUSY /* DetailFloor */,
BUSY /* Dig */,
BUSY /* CarveUpwardStaircase */,
BUSY /* CarveDownwardStaircase */,
BUSY /* CarveUpDownStaircase */,
BUSY /* CarveRamp */,
BUSY /* DigChannel */,
BUSY /* FellTree */,
BUSY /* GatherPlants */,
BUSY /* RemoveConstruction */,
BUSY /* CollectWebs */,
BUSY /* BringItemToDepot */,
BUSY /* BringItemToShop */,
OTHER /* Eat */,
OTHER /* GetProvisions */,
OTHER /* Drink */,
OTHER /* Drink2 */,
OTHER /* FillWaterskin */,
OTHER /* FillWaterskin2 */,
OTHER /* Sleep */,
BUSY /* CollectSand */,
BUSY /* Fish */,
BUSY /* Hunt */,
OTHER /* HuntVermin */,
BUSY /* Kidnap */,
BUSY /* BeatCriminal */,
BUSY /* StartingFistFight */,
BUSY /* CollectTaxes */,
BUSY /* GuardTaxCollector */,
BUSY /* CatchLiveLandAnimal */,
BUSY /* CatchLiveFish */,
BUSY /* ReturnKill */,
BUSY /* CheckChest */,
BUSY /* StoreOwnedItem */,
BUSY /* PlaceItemInTomb */,
BUSY /* StoreItemInStockpile */,
BUSY /* StoreItemInBag */,
BUSY /* StoreItemInHospital */,
BUSY /* StoreItemInChest */,
BUSY /* StoreItemInCabinet */,
BUSY /* StoreWeapon */,
BUSY /* StoreArmor */,
BUSY /* StoreItemInBarrel */,
BUSY /* StoreItemInBin */,
BUSY /* SeekArtifact */,
BUSY /* SeekInfant */,
OTHER /* AttendParty */,
OTHER /* GoShopping */,
OTHER /* GoShopping2 */,
BUSY /* Clean */,
OTHER /* Rest */,
OTHER /* PickupEquipment */,
BUSY /* DumpItem */,
OTHER /* StrangeMoodCrafter */,
OTHER /* StrangeMoodJeweller */,
OTHER /* StrangeMoodForge */,
OTHER /* StrangeMoodMagmaForge */,
OTHER /* StrangeMoodBrooding */,
OTHER /* StrangeMoodFell */,
OTHER /* StrangeMoodCarpenter */,
OTHER /* StrangeMoodMason */,
OTHER /* StrangeMoodBowyer */,
OTHER /* StrangeMoodTanner */,
OTHER /* StrangeMoodWeaver */,
OTHER /* StrangeMoodGlassmaker */,
OTHER /* StrangeMoodMechanics */,
BUSY /* ConstructBuilding */,
BUSY /* ConstructDoor */,
BUSY /* ConstructFloodgate */,
BUSY /* ConstructBed */,
BUSY /* ConstructThrone */,
BUSY /* ConstructCoffin */,
BUSY /* ConstructTable */,
BUSY /* ConstructChest */,
BUSY /* ConstructBin */,
BUSY /* ConstructArmorStand */,
BUSY /* ConstructWeaponRack */,
BUSY /* ConstructCabinet */,
BUSY /* ConstructStatue */,
BUSY /* ConstructBlocks */,
BUSY /* MakeRawGlass */,
BUSY /* MakeCrafts */,
BUSY /* MintCoins */,
BUSY /* CutGems */,
BUSY /* CutGlass */,
BUSY /* EncrustWithGems */,
BUSY /* EncrustWithGlass */,
BUSY /* DestroyBuilding */,
BUSY /* SmeltOre */,
BUSY /* MeltMetalObject */,
BUSY /* ExtractMetalStrands */,
BUSY /* PlantSeeds */,
BUSY /* HarvestPlants */,
BUSY /* TrainHuntingAnimal */,
BUSY /* TrainWarAnimal */,
BUSY /* MakeWeapon */,
BUSY /* ForgeAnvil */,
BUSY /* ConstructCatapultParts */,
BUSY /* ConstructBallistaParts */,
BUSY /* MakeArmor */,
BUSY /* MakeHelm */,
BUSY /* MakePants */,
BUSY /* StudWith */,
BUSY /* ButcherAnimal */,
BUSY /* PrepareRawFish */,
BUSY /* MillPlants */,
BUSY /* BaitTrap */,
BUSY /* MilkCreature */,
BUSY /* MakeCheese */,
BUSY /* ProcessPlants */,
BUSY /* ProcessPlantsBag */,
BUSY /* ProcessPlantsVial */,
BUSY /* ProcessPlantsBarrel */,
BUSY /* PrepareMeal */,
BUSY /* WeaveCloth */,
BUSY /* MakeGloves */,
BUSY /* MakeShoes */,
BUSY /* MakeShield */,
BUSY /* MakeCage */,
BUSY /* MakeChain */,
BUSY /* MakeFlask */,
BUSY /* MakeGoblet */,
BUSY /* MakeInstrument */,
BUSY /* MakeToy */,
BUSY /* MakeAnimalTrap */,
BUSY /* MakeBarrel */,
BUSY /* MakeBucket */,
BUSY /* MakeWindow */,
BUSY /* MakeTotem */,
BUSY /* MakeAmmo */,
BUSY /* DecorateWith */,
BUSY /* MakeBackpack */,
BUSY /* MakeQuiver */,
BUSY /* MakeBallistaArrowHead */,
BUSY /* AssembleSiegeAmmo */,
BUSY /* LoadCatapult */,
BUSY /* LoadBallista */,
BUSY /* FireCatapult */,
BUSY /* FireBallista */,
BUSY /* ConstructMechanisms */,
BUSY /* MakeTrapComponent */,
BUSY /* LoadCageTrap */,
BUSY /* LoadStoneTrap */,
BUSY /* LoadWeaponTrap */,
BUSY /* CleanTrap */,
BUSY /* CastSpell */,
BUSY /* LinkBuildingToTrigger */,
BUSY /* PullLever */,
BUSY /* BrewDrink */,
BUSY /* ExtractFromPlants */,
BUSY /* ExtractFromRawFish */,
BUSY /* ExtractFromLandAnimal */,
BUSY /* TameVermin */,
BUSY /* TameAnimal */,
BUSY /* ChainAnimal */,
BUSY /* UnchainAnimal */,
BUSY /* UnchainPet */,
BUSY /* ReleaseLargeCreature */,
BUSY /* ReleasePet */,
BUSY /* ReleaseSmallCreature */,
BUSY /* HandleSmallCreature */,
BUSY /* HandleLargeCreature */,
BUSY /* CageLargeCreature */,
BUSY /* CageSmallCreature */,
BUSY /* RecoverWounded */,
BUSY /* DiagnosePatient */,
BUSY /* ImmobilizeBreak */,
BUSY /* DressWound */,
BUSY /* CleanPatient */,
BUSY /* Surgery */,
BUSY /* Suture */,
BUSY /* SetBone */,
BUSY /* PlaceInTraction */,
BUSY /* DrainAquarium */,
BUSY /* FillAquarium */,
BUSY /* FillPond */,
BUSY /* GiveWater */,
BUSY /* GiveFood */,
BUSY /* GiveWater2 */,
BUSY /* GiveFood2 */,
BUSY /* RecoverPet */,
BUSY /* PitLargeAnimal */,
BUSY /* PitSmallAnimal */,
BUSY /* SlaughterAnimal */,
BUSY /* MakeCharcoal */,
BUSY /* MakeAsh */,
BUSY /* MakeLye */,
BUSY /* MakePotashFromLye */,
BUSY /* FertilizeField */,
BUSY /* MakePotashFromAsh */,
BUSY /* DyeThread */,
BUSY /* DyeCloth */,
BUSY /* SewImage */,
BUSY /* MakePipeSection */,
BUSY /* OperatePump */,
OTHER /* ManageWorkOrders */,
OTHER /* UpdateStockpileRecords */,
OTHER /* TradeAtDepot */,
BUSY /* ConstructHatchCover */,
BUSY /* ConstructGrate */,
BUSY /* RemoveStairs */,
BUSY /* ConstructQuern */,
BUSY /* ConstructMillstone */,
BUSY /* ConstructSplint */,
BUSY /* ConstructCrutch */,
BUSY /* ConstructTractionBench */,
BUSY /* CleanSelf */,
BUSY /* BringCrutch */,
BUSY /* ApplyCast */,
BUSY /* CustomReaction */,
BUSY /* ConstructSlab */,
BUSY /* EngraveSlab */,
BUSY /* ShearCreature */,
BUSY /* SpinThread */,
BUSY /* PenLargeAnimal */,
BUSY /* PenSmallAnimal */,
BUSY /* MakeTool */,
BUSY /* CollectClay */,
BUSY /* InstallColonyInHive */,
BUSY /* CollectHiveProducts */,
OTHER /* CauseTrouble */,
OTHER /* DrinkBlood */,
OTHER /* ReportCrime */,
OTHER /* ExecuteCriminal */,
BUSY /* TrainAnimal */,
BUSY /* CarveTrack */,
BUSY /* PushTrackVehicle */,
BUSY /* PlaceTrackVehicle */,
BUSY /* StoreItemInVehicle */,
BUSY /* GeldAnimal */,
BUSY /* MakeFigurine */,
BUSY /* MakeAmulet */,
BUSY /* MakeScepter */,
BUSY /* MakeCrown */,
BUSY /* MakeRing */,
BUSY /* MakeEarring */,
BUSY /* MakeBracelet */,
BUSY /* MakeGem */,
BUSY /* PutItemOnDisplay */,
OTHER /* unk_fake_no_job */,
OTHER /* InterrogateSubject */,
OTHER /* unk_fake_no_activity */,
};
struct labor_info
{
PersistentDataItem config;

@ -0,0 +1,294 @@
#pragma once
#include <Core.h>
#include "df/job.h"
#include "df/job_type.h"
#include "df/unit_labor.h"
using namespace DFHack;
using namespace df::enums;
enum dwarf_state : int {
// Ready for a new task
IDLE=0,
// Busy with a useful task
BUSY,
// Busy with a useful task that requires a tool
EXCLUSIVE,
// In the military, can't work
MILITARY,
// Child or noble, can't work
CHILD,
// Doing something that precludes working, may be busy for a while
OTHER
};
const int NUM_STATE = 6;
char const* state_names[] {
"IDLE",
"BUSY",
"EXCLUSIVE",
"MILITARY",
"CHILD",
"OTHER",
};
const dwarf_state dwarf_states[] = {
dwarf_state::BUSY /* CarveFortification */,
dwarf_state::BUSY /* DetailWall */,
dwarf_state::BUSY /* DetailFloor */,
dwarf_state::EXCLUSIVE /* Dig */,
dwarf_state::EXCLUSIVE /* CarveUpwardStaircase */,
dwarf_state::EXCLUSIVE /* CarveDownwardStaircase */,
dwarf_state::EXCLUSIVE /* CarveUpDownStaircase */,
dwarf_state::EXCLUSIVE /* CarveRamp */,
dwarf_state::EXCLUSIVE /* DigChannel */,
dwarf_state::EXCLUSIVE /* FellTree */,
dwarf_state::BUSY /* GatherPlants */,
dwarf_state::BUSY /* RemoveConstruction */,
dwarf_state::BUSY /* CollectWebs */,
dwarf_state::BUSY /* BringItemToDepot */,
dwarf_state::BUSY /* BringItemToShop */,
dwarf_state::OTHER /* Eat */,
dwarf_state::OTHER /* GetProvisions */,
dwarf_state::OTHER /* Drink */,
dwarf_state::OTHER /* Drink2 */,
dwarf_state::OTHER /* FillWaterskin */,
dwarf_state::OTHER /* FillWaterskin2 */,
dwarf_state::OTHER /* Sleep */,
dwarf_state::BUSY /* CollectSand */,
dwarf_state::BUSY /* Fish */,
dwarf_state::EXCLUSIVE /* Hunt */,
dwarf_state::OTHER /* HuntVermin */,
dwarf_state::BUSY /* Kidnap */,
dwarf_state::BUSY /* BeatCriminal */,
dwarf_state::BUSY /* StartingFistFight */,
dwarf_state::BUSY /* CollectTaxes */,
dwarf_state::BUSY /* GuardTaxCollector */,
dwarf_state::BUSY /* CatchLiveLandAnimal */,
dwarf_state::BUSY /* CatchLiveFish */,
dwarf_state::BUSY /* ReturnKill */,
dwarf_state::BUSY /* CheckChest */,
dwarf_state::BUSY /* StoreOwnedItem */,
dwarf_state::BUSY /* PlaceItemInTomb */,
dwarf_state::BUSY /* StoreItemInStockpile */,
dwarf_state::BUSY /* StoreItemInBag */,
dwarf_state::BUSY /* StoreItemInHospital */,
dwarf_state::BUSY /* StoreItemInChest */,
dwarf_state::BUSY /* StoreItemInCabinet */,
dwarf_state::BUSY /* StoreWeapon */,
dwarf_state::BUSY /* StoreArmor */,
dwarf_state::BUSY /* StoreItemInBarrel */,
dwarf_state::BUSY /* StoreItemInBin */,
dwarf_state::BUSY /* SeekArtifact */,
dwarf_state::BUSY /* SeekInfant */,
dwarf_state::OTHER /* AttendParty */,
dwarf_state::OTHER /* GoShopping */,
dwarf_state::OTHER /* GoShopping2 */,
dwarf_state::BUSY /* Clean */,
dwarf_state::OTHER /* Rest */,
dwarf_state::EXCLUSIVE /* PickupEquipment */,
dwarf_state::BUSY /* DumpItem */,
dwarf_state::OTHER /* StrangeMoodCrafter */,
dwarf_state::OTHER /* StrangeMoodJeweller */,
dwarf_state::OTHER /* StrangeMoodForge */,
dwarf_state::OTHER /* StrangeMoodMagmaForge */,
dwarf_state::OTHER /* StrangeMoodBrooding */,
dwarf_state::OTHER /* StrangeMoodFell */,
dwarf_state::OTHER /* StrangeMoodCarpenter */,
dwarf_state::OTHER /* StrangeMoodMason */,
dwarf_state::OTHER /* StrangeMoodBowyer */,
dwarf_state::OTHER /* StrangeMoodTanner */,
dwarf_state::OTHER /* StrangeMoodWeaver */,
dwarf_state::OTHER /* StrangeMoodGlassmaker */,
dwarf_state::OTHER /* StrangeMoodMechanics */,
dwarf_state::BUSY /* ConstructBuilding */,
dwarf_state::BUSY /* ConstructDoor */,
dwarf_state::BUSY /* ConstructFloodgate */,
dwarf_state::BUSY /* ConstructBed */,
dwarf_state::BUSY /* ConstructThrone */,
dwarf_state::BUSY /* ConstructCoffin */,
dwarf_state::BUSY /* ConstructTable */,
dwarf_state::BUSY /* ConstructChest */,
dwarf_state::BUSY /* ConstructBin */,
dwarf_state::BUSY /* ConstructArmorStand */,
dwarf_state::BUSY /* ConstructWeaponRack */,
dwarf_state::BUSY /* ConstructCabinet */,
dwarf_state::BUSY /* ConstructStatue */,
dwarf_state::BUSY /* ConstructBlocks */,
dwarf_state::BUSY /* MakeRawGlass */,
dwarf_state::BUSY /* MakeCrafts */,
dwarf_state::BUSY /* MintCoins */,
dwarf_state::BUSY /* CutGems */,
dwarf_state::BUSY /* CutGlass */,
dwarf_state::BUSY /* EncrustWithGems */,
dwarf_state::BUSY /* EncrustWithGlass */,
dwarf_state::BUSY /* DestroyBuilding */,
dwarf_state::BUSY /* SmeltOre */,
dwarf_state::BUSY /* MeltMetalObject */,
dwarf_state::BUSY /* ExtractMetalStrands */,
dwarf_state::BUSY /* PlantSeeds */,
dwarf_state::BUSY /* HarvestPlants */,
dwarf_state::BUSY /* TrainHuntingAnimal */,
dwarf_state::BUSY /* TrainWarAnimal */,
dwarf_state::BUSY /* MakeWeapon */,
dwarf_state::BUSY /* ForgeAnvil */,
dwarf_state::BUSY /* ConstructCatapultParts */,
dwarf_state::BUSY /* ConstructBallistaParts */,
dwarf_state::BUSY /* MakeArmor */,
dwarf_state::BUSY /* MakeHelm */,
dwarf_state::BUSY /* MakePants */,
dwarf_state::BUSY /* StudWith */,
dwarf_state::BUSY /* ButcherAnimal */,
dwarf_state::BUSY /* PrepareRawFish */,
dwarf_state::BUSY /* MillPlants */,
dwarf_state::BUSY /* BaitTrap */,
dwarf_state::BUSY /* MilkCreature */,
dwarf_state::BUSY /* MakeCheese */,
dwarf_state::BUSY /* ProcessPlants */,
dwarf_state::BUSY /* ProcessPlantsBag */,
dwarf_state::BUSY /* ProcessPlantsVial */,
dwarf_state::BUSY /* ProcessPlantsBarrel */,
dwarf_state::BUSY /* PrepareMeal */,
dwarf_state::BUSY /* WeaveCloth */,
dwarf_state::BUSY /* MakeGloves */,
dwarf_state::BUSY /* MakeShoes */,
dwarf_state::BUSY /* MakeShield */,
dwarf_state::BUSY /* MakeCage */,
dwarf_state::BUSY /* MakeChain */,
dwarf_state::BUSY /* MakeFlask */,
dwarf_state::BUSY /* MakeGoblet */,
dwarf_state::BUSY /* MakeInstrument */,
dwarf_state::BUSY /* MakeToy */,
dwarf_state::BUSY /* MakeAnimalTrap */,
dwarf_state::BUSY /* MakeBarrel */,
dwarf_state::BUSY /* MakeBucket */,
dwarf_state::BUSY /* MakeWindow */,
dwarf_state::BUSY /* MakeTotem */,
dwarf_state::BUSY /* MakeAmmo */,
dwarf_state::BUSY /* DecorateWith */,
dwarf_state::BUSY /* MakeBackpack */,
dwarf_state::BUSY /* MakeQuiver */,
dwarf_state::BUSY /* MakeBallistaArrowHead */,
dwarf_state::BUSY /* AssembleSiegeAmmo */,
dwarf_state::BUSY /* LoadCatapult */,
dwarf_state::BUSY /* LoadBallista */,
dwarf_state::BUSY /* FireCatapult */,
dwarf_state::BUSY /* FireBallista */,
dwarf_state::BUSY /* ConstructMechanisms */,
dwarf_state::BUSY /* MakeTrapComponent */,
dwarf_state::BUSY /* LoadCageTrap */,
dwarf_state::BUSY /* LoadStoneTrap */,
dwarf_state::BUSY /* LoadWeaponTrap */,
dwarf_state::BUSY /* CleanTrap */,
dwarf_state::BUSY /* CastSpell */,
dwarf_state::BUSY /* LinkBuildingToTrigger */,
dwarf_state::BUSY /* PullLever */,
dwarf_state::BUSY /* BrewDrink */,
dwarf_state::BUSY /* ExtractFromPlants */,
dwarf_state::BUSY /* ExtractFromRawFish */,
dwarf_state::BUSY /* ExtractFromLandAnimal */,
dwarf_state::BUSY /* TameVermin */,
dwarf_state::BUSY /* TameAnimal */,
dwarf_state::BUSY /* ChainAnimal */,
dwarf_state::BUSY /* UnchainAnimal */,
dwarf_state::BUSY /* UnchainPet */,
dwarf_state::BUSY /* ReleaseLargeCreature */,
dwarf_state::BUSY /* ReleasePet */,
dwarf_state::BUSY /* ReleaseSmallCreature */,
dwarf_state::BUSY /* HandleSmallCreature */,
dwarf_state::BUSY /* HandleLargeCreature */,
dwarf_state::BUSY /* CageLargeCreature */,
dwarf_state::BUSY /* CageSmallCreature */,
dwarf_state::BUSY /* RecoverWounded */,
dwarf_state::BUSY /* DiagnosePatient */,
dwarf_state::BUSY /* ImmobilizeBreak */,
dwarf_state::BUSY /* DressWound */,
dwarf_state::BUSY /* CleanPatient */,
dwarf_state::BUSY /* Surgery */,
dwarf_state::BUSY /* Suture */,
dwarf_state::BUSY /* SetBone */,
dwarf_state::BUSY /* PlaceInTraction */,
dwarf_state::BUSY /* DrainAquarium */,
dwarf_state::BUSY /* FillAquarium */,
dwarf_state::BUSY /* FillPond */,
dwarf_state::BUSY /* GiveWater */,
dwarf_state::BUSY /* GiveFood */,
dwarf_state::BUSY /* GiveWater2 */,
dwarf_state::BUSY /* GiveFood2 */,
dwarf_state::BUSY /* RecoverPet */,
dwarf_state::BUSY /* PitLargeAnimal */,
dwarf_state::BUSY /* PitSmallAnimal */,
dwarf_state::BUSY /* SlaughterAnimal */,
dwarf_state::BUSY /* MakeCharcoal */,
dwarf_state::BUSY /* MakeAsh */,
dwarf_state::BUSY /* MakeLye */,
dwarf_state::BUSY /* MakePotashFromLye */,
dwarf_state::BUSY /* FertilizeField */,
dwarf_state::BUSY /* MakePotashFromAsh */,
dwarf_state::BUSY /* DyeThread */,
dwarf_state::BUSY /* DyeCloth */,
dwarf_state::BUSY /* SewImage */,
dwarf_state::BUSY /* MakePipeSection */,
dwarf_state::BUSY /* OperatePump */,
dwarf_state::OTHER /* ManageWorkOrders */,
dwarf_state::OTHER /* UpdateStockpileRecords */,
dwarf_state::OTHER /* TradeAtDepot */,
dwarf_state::BUSY /* ConstructHatchCover */,
dwarf_state::BUSY /* ConstructGrate */,
dwarf_state::BUSY /* RemoveStairs */,
dwarf_state::BUSY /* ConstructQuern */,
dwarf_state::BUSY /* ConstructMillstone */,
dwarf_state::BUSY /* ConstructSplint */,
dwarf_state::BUSY /* ConstructCrutch */,
dwarf_state::BUSY /* ConstructTractionBench */,
dwarf_state::BUSY /* CleanSelf */,
dwarf_state::BUSY /* BringCrutch */,
dwarf_state::BUSY /* ApplyCast */,
dwarf_state::BUSY /* CustomReaction */,
dwarf_state::BUSY /* ConstructSlab */,
dwarf_state::BUSY /* EngraveSlab */,
dwarf_state::BUSY /* ShearCreature */,
dwarf_state::BUSY /* SpinThread */,
dwarf_state::BUSY /* PenLargeAnimal */,
dwarf_state::BUSY /* PenSmallAnimal */,
dwarf_state::BUSY /* MakeTool */,
dwarf_state::BUSY /* CollectClay */,
dwarf_state::BUSY /* InstallColonyInHive */,
dwarf_state::BUSY /* CollectHiveProducts */,
dwarf_state::OTHER /* CauseTrouble */,
dwarf_state::OTHER /* DrinkBlood */,
dwarf_state::OTHER /* ReportCrime */,
dwarf_state::OTHER /* ExecuteCriminal */,
dwarf_state::BUSY /* TrainAnimal */,
dwarf_state::BUSY /* CarveTrack */,
dwarf_state::BUSY /* PushTrackVehicle */,
dwarf_state::BUSY /* PlaceTrackVehicle */,
dwarf_state::BUSY /* StoreItemInVehicle */,
dwarf_state::BUSY /* GeldAnimal */,
dwarf_state::BUSY /* MakeFigurine */,
dwarf_state::BUSY /* MakeAmulet */,
dwarf_state::BUSY /* MakeScepter */,
dwarf_state::BUSY /* MakeCrown */,
dwarf_state::BUSY /* MakeRing */,
dwarf_state::BUSY /* MakeEarring */,
dwarf_state::BUSY /* MakeBracelet */,
dwarf_state::BUSY /* MakeGem */,
dwarf_state::BUSY /* PutItemOnDisplay */,
dwarf_state::OTHER /* unk_fake_no_job */,
dwarf_state::OTHER /* InterrogateSubject */,
dwarf_state::OTHER /* unk_fake_no_activity */,
};
#define ARRAY_COUNT(array) (sizeof(array)/sizeof((array)[0]))
const int dwarf_state_count = ARRAY_COUNT(dwarf_states);
#undef ARRAY_COUNT

@ -0,0 +1,9 @@
project(channel-safely)
include_directories(include)
SET(SOURCES
channel-groups.cpp
channel-manager.cpp
channel-safely-plugin.cpp)
dfhack_plugin(${PROJECT_NAME} ${SOURCES} LINK_LIBRARIES lua)

@ -0,0 +1,290 @@
#include <channel-groups.h>
#include <tile-cache.h>
#include <inlines.h>
#include <modules/Maps.h>
#include <df/block_square_event_designation_priorityst.h>
#include <random>
// iterates the DF job list and adds channel jobs to the `jobs` container
void ChannelJobs::load_channel_jobs() {
locations.clear();
df::job_list_link* node = df::global::world->jobs.list.next;
while (node) {
df::job* job = node->item;
node = node->next;
if (is_dig_job(job)) {
locations.emplace(job->pos);
}
}
}
// adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found
void ChannelGroups::add(const df::coord &map_pos) {
// if we've already added this, we don't need to do it again
if (groups_map.count(map_pos)) {
return;
}
/* We need to add map_pos to an existing group if possible...
* So what we do is we look at neighbours to see if they belong to one or more existing groups
* If there is more than one group, we'll be merging them
*/
df::coord neighbors[8];
get_neighbours(map_pos, neighbors);
Group* group = nullptr;
int group_index = -1;
DEBUG(groups).print(" add(" COORD ")\n", COORDARGS(map_pos));
// and so we begin iterating the neighbours
for (auto &neighbour: neighbors) {
// go to the next neighbour if this one doesn't have a group
if (!groups_map.count(neighbour)) {
TRACE(groups).print(" -> neighbour is not designated\n");
continue;
}
// get the group, since at least one exists... then merge any additional into that one
if (!group){
TRACE(groups).print(" -> group* has no valid state yet\n");
group_index = groups_map.find(neighbour)->second;
group = &groups.at(group_index);
} else {
TRACE(groups).print(" -> group* has an existing state\n");
// we don't do anything if the found group is the same as the existing group
auto index2 = groups_map.find(neighbour)->second;
if (group_index != index2) {
// we already have group "prime" if you will, so we're going to merge the new find into prime
Group &group2 = groups.at(index2);
// merge
TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(),
group2.size());
for (auto pos2: group2) {
group->emplace(pos2);
groups_map[pos2] = group_index;
}
group2.clear();
free_spots.emplace(index2);
TRACE(groups).print(" merged size: %zu\n", group->size());
}
}
}
// if we haven't found at least one group by now we need to create/get one
if (!group) {
TRACE(groups).print(" -> no merging took place\n");
// first we check if we can re-use a group that's been freed
if (!free_spots.empty()) {
TRACE(groups).print(" -> use recycled old group\n");
// first element in a set is always the lowest value, so we re-use from the front of the vector
group_index = *free_spots.begin();
group = &groups[group_index];
free_spots.erase(free_spots.begin());
} else {
TRACE(groups).print(" -> brand new group\n");
// we create a brand-new group to use
group_index = groups.size();
groups.push_back(Group());
group = &groups[group_index];
}
}
// puts the "add" in "ChannelGroups::add"
group->emplace(map_pos);
DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size());
// we may have performed a merge, so we update all the `coord -> group index` mappings
for (auto &wpos: *group) {
groups_map[wpos] = group_index;
}
DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size());
}
// scans a single tile for channel designations
void ChannelGroups::scan_one(const df::coord &map_pos) {
df::map_block* block = Maps::getTileBlock(map_pos);
int16_t lx = map_pos.x % 16;
int16_t ly = map_pos.y % 16;
if (is_dig_designation(block->designation[lx][ly])) {
for (df::block_square_event* event: block->block_events) {
if (auto evT = virtual_cast<df::block_square_event_designation_priorityst>(event)) {
// we want to let the user keep some designations free of being managed
if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) {
TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos));
add(map_pos);
}
}
}
} else if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) {
TileCache::Get().uncache(map_pos);
remove(map_pos);
}
}
// builds groupings of adjacent channel designations
void ChannelGroups::scan() {
// save current jobs, then clear and load the current jobs
std::set<df::coord> last_jobs;
for (auto &pos : jobs) {
last_jobs.emplace(pos);
}
jobs.load_channel_jobs();
// transpose channel jobs to
std::set<df::coord> new_jobs;
std::set<df::coord> gone_jobs;
set_difference(last_jobs, jobs, gone_jobs);
set_difference(jobs, last_jobs, new_jobs);
for (auto &pos : new_jobs) {
add(pos);
}
for (auto &pos : gone_jobs){
remove(pos);
}
static std::default_random_engine RNG(0);
static std::bernoulli_distribution optimizing(0.75); // fixing OpenSpace as designated
DEBUG(groups).print(" scan()\n");
// foreach block
for (int32_t z = mapz - 1; z >= 0; --z) {
for (int32_t by = 0; by < mapy; ++by) {
for (int32_t bx = 0; bx < mapx; ++bx) {
// the block
if (df::map_block* block = Maps::getBlock(bx, by, z)) {
// skip this block?
if (!block->flags.bits.designated && !group_blocks.count(block) && optimizing(RNG)) {
continue;
}
// foreach tile
bool empty_group = true;
for (int16_t lx = 0; lx < 16; ++lx) {
for (int16_t ly = 0; ly < 16; ++ly) {
// the tile, check if it has a channel designation
df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z);
if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) {
TileCache::Get().uncache(map_pos);
remove(map_pos);
if (jobs.count(map_pos)) {
jobs.erase(map_pos);
}
block->designation[lx][ly].bits.dig = df::tile_dig_designation::No;
} else if (is_dig_designation(block->designation[lx][ly])) {
for (df::block_square_event* event: block->block_events) {
if (auto evT = virtual_cast<df::block_square_event_designation_priorityst>(event)) {
// we want to let the user keep some designations free of being managed
TRACE(groups).print(" tile designation priority: %d\n", evT->priority[lx][ly]);
if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) {
if (empty_group) {
group_blocks.emplace(block);
empty_group = false;
}
TRACE(groups).print(" adding (" COORD ")\n", COORDARGS(map_pos));
add(map_pos);
} else if (groups_map.count(map_pos)) {
remove(map_pos);
}
}
}
}
}
}
// erase the block if we didn't find anything iterating through it
if (empty_group) {
group_blocks.erase(block);
}
}
}
}
}
INFO(groups).print("scan() exits\n");
}
// clears out the containers for unloading maps or disabling the plugin
void ChannelGroups::clear() {
debug_map();
WARN(groups).print(" <- clearing groups\n");
group_blocks.clear();
free_spots.clear();
groups_map.clear();
for(size_t i = 0; i < groups.size(); ++i) {
groups[i].clear();
free_spots.emplace(i);
}
}
// erases map_pos from its group, and deletes mappings IFF the group is empty
void ChannelGroups::remove(const df::coord &map_pos) {
// we don't need to do anything if the position isn't in a group (granted, that should never be the case)
INFO(groups).print(" remove()\n");
if (groups_map.count(map_pos)) {
INFO(groups).print(" -> found group\n");
// get the group, and map_pos' block*
int group_index = groups_map.find(map_pos)->second;
Group &group = groups[group_index];
// erase map_pos from the group
INFO(groups).print(" -> erase(" COORD ")\n", COORDARGS(map_pos));
group.erase(map_pos);
groups_map.erase(map_pos);
// clean up if the group is empty
if (group.empty()) {
WARN(groups).print(" -> group is empty\n");
// erase `coord -> group group_index` mappings
for (auto iter = groups_map.begin(); iter != groups_map.end();) {
if (group_index == iter->second) {
iter = groups_map.erase(iter);
continue;
}
++iter;
}
// flag the `groups` group_index as available
free_spots.insert(group_index);
}
}
INFO(groups).print(" remove() exits\n");
}
// finds a group corresponding to a map position if one exists
Groups::const_iterator ChannelGroups::find(const df::coord &map_pos) const {
const auto iter = groups_map.find(map_pos);
if (iter != groups_map.end()) {
return groups.begin() + iter->second;
}
return groups.end();
}
// returns an iterator to the first element stored
Groups::const_iterator ChannelGroups::begin() const {
return groups.begin();
}
// returns an iterator to after the last element stored
Groups::const_iterator ChannelGroups::end() const {
return groups.end();
}
// returns a count of 0 or 1 depending on whether map_pos is mapped to a group
size_t ChannelGroups::count(const df::coord &map_pos) const {
return groups_map.count(map_pos);
}
// prints debug info about the groups stored, and their members
void ChannelGroups::debug_groups() {
if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) {
int idx = 0;
TRACE(groups).print(" debugging group data\n");
for (auto &group: groups) {
TRACE(groups).print(" group %d (size: %zu)\n", idx, group.size());
for (auto &pos: group) {
TRACE(groups).print(" (%d,%d,%d)\n", pos.x, pos.y, pos.z);
}
idx++;
}
}
}
// prints debug info group mappings
void ChannelGroups::debug_map() {
if (DFHack::debug_groups.isEnabled(DebugCategory::LDEBUG)) {
INFO(groups).print("Group Mappings: %zu\n", groups_map.size());
for (auto &pair: groups_map) {
DEBUG(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second);
}
}
}

@ -0,0 +1,105 @@
#include <channel-manager.h>
#include <tile-cache.h>
#include <inlines.h>
#include <modules/EventManager.h> //hash function for df::coord
#include <df/block_square_event_designation_priorityst.h>
// sets mark flags as necessary, for all designations
void ChannelManager::manage_groups() {
INFO(manager).print("manage_groups()\n");
// make sure we've got a fort map to analyze
if (World::isFortressMode() && Maps::IsValid()) {
// iterate the groups we built/updated
for (const auto &group: groups) {
manage_group(group, true, has_any_groups_above(groups, group));
}
}
}
void ChannelManager::manage_group(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) {
INFO(manager).print("manage_group(" COORD ")\n ", COORDARGS(map_pos));
if (!groups.count(map_pos)) {
groups.scan_one(map_pos);
}
auto iter = groups.find(map_pos);
if (iter != groups.end()) {
manage_group(*iter, set_marker_mode, marker_mode);
}
INFO(manager).print("manage_group() is done\n");
}
void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool marker_mode) {
INFO(manager).print("manage_group()\n");
if (!set_marker_mode) {
if (has_any_groups_above(groups, group)) {
marker_mode = true;
} else {
marker_mode = false;
}
}
for (auto &designation: group) {
manage_one(group, designation, true, marker_mode);
}
INFO(manager).print("manage_group() is done\n");
}
bool ChannelManager::manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode, bool marker_mode) {
if (Maps::isValidTilePos(map_pos)) {
INFO(manager).print("manage_one(" COORD ")\n", COORDARGS(map_pos));
df::map_block* block = Maps::getTileBlock(map_pos);
// we calculate the position inside the block*
df::coord local(map_pos);
local.x = local.x % 16;
local.y = local.y % 16;
df::tile_occupancy &tile_occupancy = block->occupancy[Coord(local)];
// ensure that we aren't on the top-most layers
if (map_pos.z < mapz - 3) {
// do we already know whether to set marker mode?
if (set_marker_mode) {
DEBUG(manager).print(" -> marker_mode\n");
// if enabling marker mode, just do it
if (marker_mode) {
tile_occupancy.bits.dig_marked = marker_mode;
return true;
}
// if activating designation, check if it is safe to dig or not a channel designation
if (!is_channel_designation(block->designation[Coord(local)]) || is_safe_to_dig_down(map_pos)) {
if (!block->flags.bits.designated) {
block->flags.bits.designated = true;
}
tile_occupancy.bits.dig_marked = false;
TileCache::Get().cache(map_pos, block->tiletype[Coord(local)]);
}
return false;
} else {
// next search for the designation priority
DEBUG(manager).print(" if(has_groups_above())\n");
// check that the group has no incomplete groups directly above it
if (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos)) {
DEBUG(manager).print(" has_groups_above: setting marker mode\n");
tile_occupancy.bits.dig_marked = true;
if (jobs.count(map_pos)) {
jobs.erase(map_pos);
}
WARN(manager).print(" <- manage_one() exits normally\n");
return true;
}
}
} else {
// if we are though, it should be totally safe to dig
tile_occupancy.bits.dig_marked = false;
}
WARN(manager).print(" <- manage_one() exits normally\n");
}
return false;
}
void ChannelManager::mark_done(const df::coord &map_pos) {
groups.remove(map_pos);
jobs.erase(map_pos);
CSP::dignow_queue.erase(map_pos);
TileCache::Get().uncache(map_pos);
}

@ -0,0 +1,668 @@
/* Prevent channeling down into known open space.
Author: Josh Cooper
Created: Aug. 4 2020
Updated: Nov. 6 2022
Enable plugin:
-> build groups
-> manage designations
Unpause event:
-> build groups
-> manage designations
Manage Designation(s):
-> for each group in groups:
-> does any tile in group have a group above
-> Yes: set entire group to marker mode
-> No: activate entire group (still checks is_safe_to_dig_down before activating each designation)
Job started event:
-> validate job type (channel)
-> check pathing:
-> Can: add job/worker to tracking
-> Can: set tile to restricted
-> Cannot: remove worker
-> Cannot: insta-dig & delete job
-> Cannot: set designation to Marker Mode (no insta-digging)
OnUpdate:
-> check worker location:
-> CanFall: check if a fall would be safe:
-> Safe: do nothing
-> Unsafe: remove worker
-> Unsafe: insta-dig & delete job (presumes the job is only accessible from directly on the tile)
-> Unsafe: set designation to Marker Mode (no insta-digging)
-> check tile occupancy:
-> HasUnit: check if a fall would be safe:
-> Safe: do nothing, let them fall
-> Unsafe: remove worker for 1 tick (test if this "pauses" or cancels the job)
-> Unsafe: Add feature to teleport unit?
Job completed event:
-> validate job type (channel)
-> verify completion:
-> IsOpenSpace: mark done
-> IsOpenSpace: manage tile below
-> NotOpenSpace: check for designation
-> HasDesignation: do nothing
-> NoDesignation: mark done (erases from group)
-> NoDesignation: manage tile below
*/
#include <plugin.h>
#include <inlines.h>
#include <channel-manager.h>
#include <tile-cache.h>
#include <Debug.h>
#include <LuaTools.h>
#include <LuaWrapper.h>
#include <PluginManager.h>
#include <modules/EventManager.h>
#include <modules/Units.h>
#include <df/world.h>
#include <df/report.h>
#include <df/tile_traffic.h>
#include <df/block_square_event_designation_priorityst.h>
#include <cinttypes>
#include <unordered_map>
#include <unordered_set>
// Debugging
namespace DFHack {
DBG_DECLARE(channelsafely, plugin, DebugCategory::LINFO);
DBG_DECLARE(channelsafely, monitor, DebugCategory::LERROR);
DBG_DECLARE(channelsafely, manager, DebugCategory::LERROR);
DBG_DECLARE(channelsafely, groups, DebugCategory::LERROR);
DBG_DECLARE(channelsafely, jobs, DebugCategory::LERROR);
}
DFHACK_PLUGIN("channel-safely");
DFHACK_PLUGIN_IS_ENABLED(enabled);
REQUIRE_GLOBAL(world);
namespace EM = EventManager;
using namespace DFHack;
using namespace EM::EventType;
int32_t mapx, mapy, mapz;
Configuration config;
PersistentDataItem psetting;
PersistentDataItem pfeature;
const std::string FCONFIG_KEY = std::string(plugin_name) + "/feature";
const std::string SCONFIG_KEY = std::string(plugin_name) + "/setting";
enum FeatureConfigData {
VISION,
MONITOR,
RESURRECT,
INSTADIG
};
enum SettingConfigData {
REFRESH_RATE,
MONITOR_RATE,
IGNORE_THRESH,
FALL_THRESH
};
// dig-now.cpp
df::coord simulate_fall(const df::coord &pos) {
df::coord resting_pos(pos);
while (Maps::ensureTileBlock(resting_pos)) {
df::tiletype tt = *Maps::getTileType(resting_pos);
df::tiletype_shape_basic basic_shape = tileShapeBasic(tileShape(tt));
if (isWalkable(tt) && basic_shape != df::tiletype_shape_basic::Open)
break;
--resting_pos.z;
}
return resting_pos;
}
df::coord simulate_area_fall(const df::coord &pos) {
df::coord neighbours[8]{};
get_neighbours(pos, neighbours);
df::coord lowest = simulate_fall(pos);
for (auto p : neighbours) {
auto nlow = simulate_fall(p);
if (nlow.z < lowest.z) {
lowest = nlow;
}
}
return lowest;
}
// executes dig designations for the specified tile coordinates
inline bool dig_now(color_ostream &out, const df::coord &map_pos) {
bool ret = false;
lua_State* state = Lua::Core::State;
static const char* module_name = "plugins.dig-now";
static const char* fn_name = "dig_now_tile";
// the stack layout isn't likely to change, ever
static auto args_lambda = [&map_pos](lua_State* L) {
Lua::Push(L, map_pos);
};
static auto res_lambda = [&ret](lua_State* L) {
ret = lua_toboolean(L, -1);
};
Lua::StackUnwinder top(state);
Lua::CallLuaModuleFunction(out, state, module_name, fn_name, 1, 1, args_lambda, res_lambda);
return ret;
}
// fully heals the unit specified, resurrecting if need be
inline void resurrect(color_ostream &out, const int32_t &unit) {
std::vector<std::string> params{"-r", "--unit", std::to_string(unit)};
Core::getInstance().runCommand(out,"full-heal", params);
}
namespace CSP {
std::unordered_map<df::unit*, int32_t> endangered_units;
std::unordered_map<df::job*, int32_t> job_id_map;
std::unordered_map<int32_t, df::job*> active_jobs;
std::unordered_map<int32_t, df::unit*> active_workers;
std::unordered_map<int32_t, df::coord> last_safe;
std::unordered_set<df::coord> dignow_queue;
void ClearData() {
ChannelManager::Get().destroy_groups();
dignow_queue.clear();
last_safe.clear();
endangered_units.clear();
active_workers.clear();
active_jobs.clear();
job_id_map.clear();
}
void SaveSettings() {
if (pfeature.isValid() && psetting.isValid()) {
try {
pfeature.ival(MONITOR) = config.monitor_active;
pfeature.ival(VISION) = config.require_vision;
pfeature.ival(INSTADIG) = config.insta_dig;
pfeature.ival(RESURRECT) = config.resurrect;
psetting.ival(REFRESH_RATE) = config.refresh_freq;
psetting.ival(MONITOR_RATE) = config.monitor_freq;
psetting.ival(IGNORE_THRESH) = config.ignore_threshold;
psetting.ival(FALL_THRESH) = config.fall_threshold;
} catch (std::exception &e) {
ERR(plugin).print("%s\n", e.what());
}
}
}
void LoadSettings() {
pfeature = World::GetPersistentData(FCONFIG_KEY);
psetting = World::GetPersistentData(SCONFIG_KEY);
if (!pfeature.isValid() || !psetting.isValid()) {
pfeature = World::AddPersistentData(FCONFIG_KEY);
psetting = World::AddPersistentData(SCONFIG_KEY);
SaveSettings();
} else {
try {
config.monitor_active = pfeature.ival(MONITOR);
config.require_vision = pfeature.ival(VISION);
config.insta_dig = pfeature.ival(INSTADIG);
config.resurrect = pfeature.ival(RESURRECT);
config.ignore_threshold = psetting.ival(IGNORE_THRESH);
config.fall_threshold = psetting.ival(FALL_THRESH);
config.refresh_freq = psetting.ival(REFRESH_RATE);
config.monitor_freq = psetting.ival(MONITOR_RATE);
} catch (std::exception &e) {
ERR(plugin).print("%s\n", e.what());
}
}
active_workers.clear();
}
void UnpauseEvent(){
CoreSuspender suspend; // we need exclusive access to df memory and this call stack doesn't already have a lock
INFO(monitor).print("UnpauseEvent()\n");
ChannelManager::Get().build_groups();
ChannelManager::Get().manage_groups();
ChannelManager::Get().debug();
INFO(monitor).print("UnpauseEvent() exits\n");
}
void JobStartedEvent(color_ostream &out, void* j) {
if (enabled && World::isFortressMode() && Maps::IsValid()) {
INFO(jobs).print("JobStartedEvent()\n");
auto job = (df::job*) j;
// validate job type
if (ChannelManager::Get().exists(job->pos)) {
WARN(jobs).print(" valid channel job:\n");
df::unit* worker = Job::getWorker(job);
// there is a valid worker (living citizen) on the job? right..
if (worker && Units::isAlive(worker) && Units::isCitizen(worker)) {
DEBUG(jobs).print(" valid worker:\n");
// track workers on jobs
df::coord &pos = job->pos;
WARN(jobs).print(" -> Starting job at (" COORD ")\n", COORDARGS(pos));
if (config.monitor_active || config.resurrect) {
job_id_map.emplace(job, job->id);
active_jobs.emplace(job->id, job);
active_workers[job->id] = worker;
if (config.resurrect) {
// this is the only place we can be 100% sure of "safety"
// (excluding deadly enemies that will have arrived)
last_safe[worker->id] = worker->pos;
}
}
// set tile to restricted
TRACE(jobs).print(" setting job tile to restricted\n");
Maps::getTileDesignation(job->pos)->bits.traffic = df::tile_traffic::Restricted;
}
}
INFO(jobs).print(" <- JobStartedEvent() exits normally\n");
}
}
void JobCompletedEvent(color_ostream &out, void* j) {
if (enabled && World::isFortressMode() && Maps::IsValid()) {
INFO(jobs).print("JobCompletedEvent()\n");
auto job = (df::job*) j;
// we only care if the job is a channeling one
if (ChannelManager::Get().exists(job->pos)) {
// check job outcome
auto block = Maps::getTileBlock(job->pos);
df::coord local(job->pos);
local.x = local.x % 16;
local.y = local.y % 16;
// verify completion
if (TileCache::Get().hasChanged(job->pos, block->tiletype[Coord(local)])) {
// the job can be considered done
df::coord below(job->pos);
below.z--;
WARN(jobs).print(" -> (" COORD ") is marked done, managing group below.\n", COORDARGS(job->pos));
// mark done and manage below
block->designation[Coord(local)].bits.traffic = df::tile_traffic::Normal;
ChannelManager::Get().mark_done(job->pos);
ChannelManager::Get().manage_group(below);
ChannelManager::Get().debug();
if (config.resurrect) {
// this is the only place we can be 100% sure of "safety"
// (excluding deadly enemies that will have arrived)
if (active_workers.count(job->id)) {
df::unit* worker = active_workers[job->id];
last_safe[worker->id] = worker->pos;
}
}
}
// clean up
auto jp = active_jobs[job->id];
job_id_map.erase(jp);
active_workers.erase(job->id);
active_jobs.erase(job->id);
}
INFO(jobs).print("JobCompletedEvent() exits\n");
}
}
void NewReportEvent(color_ostream &out, void* r) {
int32_t tick = df::global::world->frame_counter;
auto report_id = (int32_t)(intptr_t(r));
if (df::global::world) {
std::vector<df::report*> &reports = df::global::world->status.reports;
size_t idx = -1;
idx = df::report::binsearch_index(reports, report_id);
df::report* report = reports.at(idx);
switch (report->type) {
case announcement_type::CANCEL_JOB:
if (config.insta_dig) {
if (report->text.find("cancels Dig") != std::string::npos) {
dignow_queue.emplace(report->pos);
} else if (report->text.find("path") != std::string::npos) {
dignow_queue.emplace(report->pos);
}
DEBUG(plugin).print("%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos),
COORDARGS(report->pos2), report->text.c_str());
}
break;
case announcement_type::CAVE_COLLAPSE:
if (config.resurrect) {
DEBUG(plugin).print("CAVE IN\n%d, pos: " COORD ", pos2: " COORD "\n%s\n", report_id, COORDARGS(report->pos),
COORDARGS(report->pos2), report->text.c_str());
df::coord below = report->pos;
below.z -= 1;
below = simulate_area_fall(below);
df::coord areaMin{report->pos};
df::coord areaMax{areaMin};
areaMin.x -= 15;
areaMin.y -= 15;
areaMax.x += 15;
areaMax.y += 15;
areaMin.z = below.z;
areaMax.z += 1;
std::vector<df::unit*> units;
Units::getUnitsInBox(units, COORDARGS(areaMin), COORDARGS(areaMax));
for (auto unit: units) {
endangered_units[unit] = tick;
DEBUG(plugin).print(" [id %d] was near a cave in.\n", unit->id);
}
for (auto unit : world->units.all) {
if (last_safe.count(unit->id)) {
endangered_units[unit] = tick;
DEBUG(plugin).print(" [id %d] is/was a worker, we'll track them too.\n", unit->id);
}
}
}
break;
default:
break;
}
}
}
void OnUpdate(color_ostream &out) {
static auto print_res_msg = [](df::unit* unit) {
WARN(plugin).print("Channel-Safely: Resurrecting..\n [id: %d]\n", unit->id);
};
if (enabled && World::isFortressMode() && Maps::IsValid() && !World::ReadPauseState()) {
static int32_t last_tick = df::global::world->frame_counter;
static int32_t last_monitor_tick = df::global::world->frame_counter;
static int32_t last_refresh_tick = df::global::world->frame_counter;
static int32_t last_resurrect_tick = df::global::world->frame_counter;
int32_t tick = df::global::world->frame_counter;
// Refreshing the group data with full scanning
if (tick - last_refresh_tick >= config.refresh_freq) {
last_refresh_tick = tick;
TRACE(monitor).print("OnUpdate() refreshing now\n");
if (config.insta_dig) {
TRACE(monitor).print(" -> evaluate dignow queue\n");
for (auto iter = dignow_queue.begin(); iter != dignow_queue.end();) {
dig_now(out, *iter); // teleports units to the bottom of a simulated fall
iter = dignow_queue.erase(iter);
DEBUG(plugin).print(">INSTA-DIGGING<\n");
}
}
UnpauseEvent();
TRACE(monitor).print("OnUpdate() refresh done\n");
}
// Clean up stale df::job*
if ((config.monitor_active || config.resurrect) && tick - last_tick >= 1) {
last_tick = tick;
// make note of valid jobs
std::unordered_map<int32_t, df::job*> valid_jobs;
for (df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next) {
df::job* job = link->item;
if (job && active_jobs.count(job->id)) {
valid_jobs.emplace(job->id, job);
}
}
// erase the active jobs that aren't valid
std::unordered_set<df::job*> erase;
map_value_difference(active_jobs, valid_jobs, erase);
for (auto j : erase) {
auto id = job_id_map[j];
job_id_map.erase(j);
active_jobs.erase(id);
active_workers.erase(id);
}
}
// Monitoring Active and Resurrecting Dead
if (config.monitor_active && tick - last_monitor_tick >= config.monitor_freq) {
last_monitor_tick = tick;
TRACE(monitor).print("OnUpdate() monitoring now\n");
// iterate active jobs
for (auto pair: active_jobs) {
df::job* job = pair.second;
df::unit* unit = active_workers[job->id];
if (!unit) continue;
if (!Maps::isValidTilePos(job->pos)) continue;
TRACE(monitor).print(" -> check for job in tracking\n");
if (Units::isAlive(unit)) {
if (!config.monitor_active) continue;
TRACE(monitor).print(" -> compare positions of worker and job\n");
// save position
if (unit->pos != job->pos && isFloorTerrain(*Maps::getTileType(unit->pos))) {
// worker is probably safe right now
continue;
}
// check for fall safety
if (unit->pos == job->pos && !is_safe_fall(job->pos)) {
// unsafe
WARN(monitor).print(" -> unsafe job\n");
Job::removeWorker(job);
// decide to insta-dig or marker mode
if (config.insta_dig) {
// delete the job
Job::removeJob(job);
// queue digging the job instantly
dignow_queue.emplace(job->pos);
DEBUG(monitor).print(" -> insta-dig\n");
} else if (config.resurrect) {
endangered_units.emplace(unit, tick);
} else {
// set marker mode
Maps::getTileOccupancy(job->pos)->bits.dig_marked = true;
// prevent algorithm from re-enabling designation
for (auto &be: Maps::getBlock(job->pos)->block_events) { ;
if (auto bsedp = virtual_cast<df::block_square_event_designation_priorityst>(
be)) {
df::coord local(job->pos);
local.x = local.x % 16;
local.y = local.y % 16;
bsedp->priority[Coord(local)] = config.ignore_threshold * 1000 + 1;
break;
}
}
DEBUG(monitor).print(" -> set marker mode\n");
}
}
} else if (config.resurrect) {
resurrect(out, unit->id);
if (last_safe.count(unit->id)) {
df::coord lowest = simulate_fall(last_safe[unit->id]);
Units::teleport(unit, lowest);
}
print_res_msg(unit);
}
}
TRACE(monitor).print("OnUpdate() monitoring done\n");
}
// Resurrect Dead Workers
if (config.resurrect && tick - last_resurrect_tick >= 1) {
last_resurrect_tick = tick;
// clean up any "endangered" workers that have been tracked 100 ticks or more
for (auto iter = endangered_units.begin(); iter != endangered_units.end();) {
if (tick - iter->second >= 1200) { //keep watch 1 day
DEBUG(plugin).print("It has been one day since [id %d]'s last incident.\n", iter->first->id);
iter = endangered_units.erase(iter);
continue;
}
++iter;
}
// resurrect any dead units
for (auto pair : endangered_units) {
auto unit = pair.first;
if (!Units::isAlive(unit)) {
resurrect(out, unit->id);
if (last_safe.count(unit->id)) {
df::coord lowest = simulate_fall(last_safe[unit->id]);
Units::teleport(unit, lowest);
}
print_res_msg(unit);
}
}
}
}
}
}
command_result channel_safely(color_ostream &out, std::vector<std::string> &parameters);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector<PluginCommand> &commands) {
commands.push_back(PluginCommand("channel-safely",
"Automatically manage channel designations.",
channel_safely,
false));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
EM::unregisterAll(plugin_self);
return CR_OK;
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
CSP::LoadSettings();
if (enabled) {
std::vector<std::string> params;
channel_safely(out, params);
}
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
if (enable && !enabled) {
// register events to check jobs / update tracking
EM::EventHandler jobStartHandler(CSP::JobStartedEvent, 0);
EM::EventHandler jobCompletionHandler(CSP::JobCompletedEvent, 0);
EM::EventHandler reportHandler(CSP::NewReportEvent, 0);
EM::registerListener(EventType::REPORT, reportHandler, plugin_self);
EM::registerListener(EventType::JOB_STARTED, jobStartHandler, plugin_self);
EM::registerListener(EventType::JOB_COMPLETED, jobCompletionHandler, plugin_self);
// manage designations to start off (first time building groups [very important])
out.print("channel-safely: enabled!\n");
CSP::UnpauseEvent();
} else if (!enable) {
// don't need the groups if the plugin isn't going to be enabled
EM::unregisterAll(plugin_self);
out.print("channel-safely: disabled!\n");
}
enabled = enable;
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
switch (event) {
case SC_UNPAUSED:
if (enabled && World::isFortressMode() && Maps::IsValid()) {
// manage all designations on unpause
CSP::UnpauseEvent();
}
break;
case SC_MAP_LOADED:
// cache the map size
Maps::getSize(mapx, mapy, mapz);
case SC_WORLD_LOADED:
case SC_WORLD_UNLOADED:
case SC_MAP_UNLOADED:
CSP::ClearData();
break;
default:
return DFHack::CR_OK;
}
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out, state_change_event event) {
CSP::OnUpdate(out);
return DFHack::CR_OK;
}
command_result channel_safely(color_ostream &out, std::vector<std::string> &parameters) {
if (!parameters.empty()) {
if (parameters[0] == "runonce") {
CSP::UnpauseEvent();
return DFHack::CR_OK;
} else if (parameters[0] == "rebuild") {
ChannelManager::Get().destroy_groups();
ChannelManager::Get().build_groups();
}
if (parameters.size() >= 2 && parameters.size() <= 3) {
bool state = false;
bool set = false;
if (parameters[0] == "enable") {
state = true;
} else if (parameters[0] == "disable") {
state = false;
} else if (parameters[0] == "set") {
set = true;
} else {
return DFHack::CR_WRONG_USAGE;
}
try {
if(parameters[1] == "monitor"){
if (state != config.monitor_active) {
config.monitor_active = state;
// if this is a fresh start
if (state && !config.resurrect) {
// we need a fresh start
CSP::active_workers.clear();
}
}
} else if (parameters[1] == "require-vision") {
config.require_vision = state;
} else if (parameters[1] == "insta-dig") {
config.insta_dig = state;
} else if (parameters[1] == "resurrect") {
if (state != config.resurrect) {
config.resurrect = state;
// if this is a fresh start
if (state && !config.monitor_active) {
// we need a fresh start
CSP::active_workers.clear();
}
}
} else if (parameters[1] == "refresh-freq" && set && parameters.size() == 3) {
config.refresh_freq = std::abs(std::stol(parameters[2]));
} else if (parameters[1] == "monitor-freq" && set && parameters.size() == 3) {
config.monitor_freq = std::abs(std::stol(parameters[2]));
} else if (parameters[1] == "ignore-threshold" && set && parameters.size() == 3) {
config.ignore_threshold = std::abs(std::stol(parameters[2]));
} else if (parameters[1] == "fall-threshold" && set && parameters.size() == 3) {
uint8_t t = std::abs(std::stol(parameters[2]));
if (t > 0) {
config.fall_threshold = t;
} else {
out.printerr("fall-threshold must have a value greater than 0 or the plugin does a lot of nothing.\n");
return DFHack::CR_FAILURE;
}
} else {
return DFHack::CR_WRONG_USAGE;
}
} catch (const std::exception &e) {
out.printerr("%s\n", e.what());
return DFHack::CR_FAILURE;
}
}
} else {
out.print("Channel-Safely is %s\n", enabled ? "ENABLED." : "DISABLED.");
out.print(" FEATURES:\n");
out.print(" %-20s\t%s\n", "monitor-active: ", config.monitor_active ? "on." : "off.");
out.print(" %-20s\t%s\n", "require-vision: ", config.require_vision ? "on." : "off.");
out.print(" %-20s\t%s\n", "insta-dig: ", config.insta_dig ? "on." : "off.");
out.print(" %-20s\t%s\n", "resurrect: ", config.resurrect ? "on." : "off.");
out.print(" SETTINGS:\n");
out.print(" %-20s\t%" PRIi32 "\n", "refresh-freq: ", config.refresh_freq);
out.print(" %-20s\t%" PRIi32 "\n", "monitor-freq: ", config.monitor_freq);
out.print(" %-20s\t%" PRIu8 "\n", "ignore-threshold: ", config.ignore_threshold);
out.print(" %-20s\t%" PRIu8 "\n", "fall-threshold: ", config.fall_threshold);
}
CSP::SaveSettings();
return DFHack::CR_OK;
}

@ -0,0 +1,51 @@
#pragma once
#include "plugin.h"
#include "channel-jobs.h"
#include <df/map_block.h>
#include <df/coord.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <vector>
#include <unordered_map>
#include <unordered_set>
using namespace DFHack;
using Group = std::unordered_set<df::coord>;
using Groups = std::vector<Group>;
/* Used to build groups of adjacent channel designations/jobs
* groups_map: maps coordinates to a group index in `groups`
* groups: list of Groups
* Group: used to track designations which are connected through adjacency to one another (a group cannot span Z)
* Note: a designation plan may become unsafe if the jobs aren't completed in a specific order;
* the easiest way to programmatically ensure safety is to..
* lock overlapping groups directly adjacent across Z until the above groups are complete, or no longer overlap
* groups may no longer overlap if the adjacent designations are completed, but requires a rebuild of groups
* jobs: list of coordinates with channel jobs associated to them
*/
class ChannelGroups {
private:
using GroupBlocks = std::unordered_set<df::map_block*>;
using GroupsMap = std::unordered_map<df::coord, int>;
GroupBlocks group_blocks;
GroupsMap groups_map;
Groups groups;
ChannelJobs &jobs;
std::set<int> free_spots;
protected:
void add(const df::coord &map_pos);
public:
explicit ChannelGroups(ChannelJobs &jobs) : jobs(jobs) { groups.reserve(200); }
void scan_one(const df::coord &map_pos);
void scan();
void clear();
void remove(const df::coord &map_pos);
Groups::const_iterator find(const df::coord &map_pos) const;
Groups::const_iterator begin() const;
Groups::const_iterator end() const;
size_t count(const df::coord &map_pos) const;
void debug_groups();
void debug_map();
};

@ -0,0 +1,43 @@
#pragma once
#include <PluginManager.h>
#include <modules/Job.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <df/world.h>
#include <df/job.h>
#include <unordered_set>
using namespace DFHack;
/* Used to read/store/iterate channel digging jobs
* jobs: list of coordinates with channel jobs associated to them
* load_channel_jobs: iterates world->jobs.list to find channel jobs and adds them into the `jobs` map
* clear: empties the container
* erase: finds a job corresponding to a coord, removes the mapping in jobs, and calls Job::removeJob, then returns an iterator following the element removed
* find: returns an iterator to a job if one exists for a map coordinate
* begin: returns jobs.begin()
* end: returns jobs.end()
*/
class ChannelJobs {
private:
friend class ChannelGroup;
using Jobs = std::unordered_set<df::coord>; // job* will exist until it is complete, and likely beyond
Jobs locations;
public:
void load_channel_jobs();
void clear() {
locations.clear();
}
int count(const df::coord &map_pos) const { return locations.count(map_pos); }
Jobs::iterator erase(const df::coord &map_pos) {
auto iter = locations.find(map_pos);
if (iter != locations.end()) {
return locations.erase(iter);
}
return iter;
}
Jobs::const_iterator find(const df::coord &map_pos) const { return locations.find(map_pos); }
Jobs::const_iterator begin() const { return locations.begin(); }
Jobs::const_iterator end() const { return locations.end(); }
};

@ -0,0 +1,39 @@
#pragma once
#include <PluginManager.h>
#include <modules/World.h>
#include <modules/Maps.h>
#include <modules/Job.h>
#include <df/map_block.h>
#include "channel-groups.h"
#include "plugin.h"
using namespace DFHack;
// Uses GroupData to detect an unsafe work environment
class ChannelManager {
private:
ChannelJobs jobs;
ChannelManager()= default;
protected:
public:
ChannelGroups groups = ChannelGroups(jobs);
static ChannelManager& Get(){
static ChannelManager instance;
return instance;
}
void build_groups() { groups.scan(); debug(); }
void destroy_groups() { groups.clear(); debug(); }
void manage_groups();
void manage_group(const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false);
void manage_group(const Group &group, bool set_marker_mode = false, bool marker_mode = false);
bool manage_one(const Group &group, const df::coord &map_pos, bool set_marker_mode = false, bool marker_mode = false);
void mark_done(const df::coord &map_pos);
bool exists(const df::coord &map_pos) const { return groups.count(map_pos); }
void debug() {
DEBUG(groups).print(" DEBUGGING GROUPS:\n");
groups.debug_groups();
groups.debug_map();
}
};

@ -0,0 +1,196 @@
#pragma once
#include "plugin.h"
#include "channel-manager.h"
#include <modules/Maps.h>
#include <df/job.h>
#include <TileTypes.h>
#include <cinttypes>
#include <unordered_set>
#define Coord(id) id.x][id.y
#define COORD "%" PRIi16 " %" PRIi16 " %" PRIi16
#define COORDARGS(id) id.x, id.y, id.z
namespace CSP {
extern std::unordered_set<df::coord> dignow_queue;
}
inline void get_neighbours(const df::coord &map_pos, df::coord(&neighbours)[8]) {
neighbours[0] = map_pos;
neighbours[1] = map_pos;
neighbours[2] = map_pos;
neighbours[3] = map_pos;
neighbours[4] = map_pos;
neighbours[5] = map_pos;
neighbours[6] = map_pos;
neighbours[7] = map_pos;
neighbours[0].x--; neighbours[0].y--;
neighbours[1].y--;
neighbours[2].x++; neighbours[2].y--;
neighbours[3].x--;
neighbours[4].x++;
neighbours[5].x--; neighbours[5].y++;
neighbours[6].y++;
neighbours[7].x++; neighbours[7].y++;
}
inline bool is_dig_job(const df::job* job) {
return job->job_type == df::job_type::Dig || job->job_type == df::job_type::DigChannel;
}
inline bool is_channel_job(const df::job* job) {
return job->job_type == df::job_type::DigChannel;
}
inline bool is_group_job(const ChannelGroups &groups, const df::job* job) {
return groups.count(job->pos);
}
inline bool is_dig_designation(const df::tile_designation &designation) {
return designation.bits.dig != df::tile_dig_designation::No;
}
inline bool is_channel_designation(const df::tile_designation &designation) {
return designation.bits.dig != df::tile_dig_designation::Channel;
}
inline bool is_safe_fall(const df::coord &map_pos) {
df::coord below(map_pos);
for (uint8_t zi = 0; zi < config.fall_threshold; ++zi) {
below.z--;
if (config.require_vision && Maps::getTileDesignation(below)->bits.hidden) {
return true; //we require vision, and we can't see below.. so we gotta assume it's safe
}
df::tiletype type = *Maps::getTileType(below);
if (!DFHack::isOpenTerrain(type)) {
return true;
}
}
return false;
}
inline bool is_safe_to_dig_down(const df::coord &map_pos) {
df::coord pos(map_pos);
for (uint8_t zi = 0; zi <= config.fall_threshold; ++zi) {
// assume safe if we can't see and need vision
if (config.require_vision && Maps::getTileDesignation(pos)->bits.hidden) {
return true;
}
df::tiletype type = *Maps::getTileType(pos);
if (zi == 0 && DFHack::isOpenTerrain(type)) {
// the starting tile is open space, that's obviously not safe
return false;
} else if (!DFHack::isOpenTerrain(type)) {
// a tile after the first one is not open space
return true;
}
pos.z--;
}
return false;
}
inline bool can_reach_designation(const df::coord &start, const df::coord &end) {
if (start != end) {
if (!Maps::canWalkBetween(start, end)) {
df::coord neighbours[8];
get_neighbours(end, neighbours);
for (auto &pos: neighbours) {
if (Maps::isValidTilePos(pos) && Maps::canWalkBetween(start, pos)) {
return true;
}
}
return false;
}
}
return true;
}
inline bool has_unit(const df::tile_occupancy* occupancy) {
return occupancy->bits.unit || occupancy->bits.unit_grounded;
}
inline bool has_group_above(const ChannelGroups &groups, const df::coord &map_pos) {
df::coord above(map_pos);
above.z++;
if (groups.count(above)) {
return true;
}
return false;
}
inline bool has_any_groups_above(const ChannelGroups &groups, const Group &group) {
// for each designation in the group
for (auto &pos : group) {
df::coord above(pos);
above.z++;
if (groups.count(above)) {
return true;
}
}
// if there are no incomplete groups above this group, then this group is ready
return false;
}
inline void cancel_job(df::job* job) {
if (job != nullptr) {
df::coord &pos = job->pos;
df::map_block* job_block = Maps::getTileBlock(pos);
uint16_t x, y;
x = pos.x % 16;
y = pos.y % 16;
df::tile_designation &designation = job_block->designation[x][y];
auto type = job->job_type;
Job::removeJob(job);
switch (type) {
case job_type::Dig:
designation.bits.dig = df::tile_dig_designation::Default;
break;
case job_type::CarveUpwardStaircase:
designation.bits.dig = df::tile_dig_designation::UpStair;
break;
case job_type::CarveDownwardStaircase:
designation.bits.dig = df::tile_dig_designation::DownStair;
break;
case job_type::CarveUpDownStaircase:
designation.bits.dig = df::tile_dig_designation::UpDownStair;
break;
case job_type::CarveRamp:
designation.bits.dig = df::tile_dig_designation::Ramp;
break;
case job_type::DigChannel:
designation.bits.dig = df::tile_dig_designation::Channel;
break;
default:
designation.bits.dig = df::tile_dig_designation::No;
break;
}
}
}
template<class Ctr1, class Ctr2, class Ctr3>
void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) {
for (const auto &a : c1) {
if (!c2.count(a)) {
c3.emplace(a);
}
}
}
template<class Ctr1, class Ctr2, class Ctr3>
void map_value_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) {
for (const auto &a : c1) {
bool matched = false;
for (const auto &b : c2) {
if (a.second == b.second) {
matched = true;
break;
}
}
if (!matched) {
c3.emplace(a.second);
}
}
}

@ -0,0 +1,23 @@
#pragma once
#include <Debug.h>
namespace DFHack {
DBG_EXTERN(channelsafely, monitor);
DBG_EXTERN(channelsafely, manager);
DBG_EXTERN(channelsafely, groups);
DBG_EXTERN(channelsafely, jobs);
}
struct Configuration {
bool monitor_active = false;
bool require_vision = true;
bool insta_dig = false;
bool resurrect = false;
int32_t refresh_freq = 600;
int32_t monitor_freq = 1;
uint8_t ignore_threshold = 5;
uint8_t fall_threshold = 1;
};
extern Configuration config;
extern int32_t mapx, mapy, mapz;

@ -0,0 +1,31 @@
#pragma once
#include <modules/Maps.h>
#include <df/coord.h>
#include <df/tiletype.h>
#include <modules/EventManager.h> //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway)
#include <unordered_map>
class TileCache {
private:
TileCache() = default;
std::unordered_map<df::coord, df::tiletype> locations;
public:
static TileCache& Get() {
static TileCache instance;
return instance;
}
void cache(const df::coord &pos, df::tiletype type) {
locations.emplace(pos, type);
}
void uncache(const df::coord &pos) {
locations.erase(pos);
}
bool hasChanged(const df::coord &pos, const df::tiletype &type) {
return locations.count(pos) && type != locations[pos];
}
};

@ -147,8 +147,8 @@ command_result df_cleanowned (color_ostream &out, vector <string> & parameters)
std::string description;
item->getItemDescription(&description, 0);
out.print(
"%p %s (wear %d)",
item,
"[%d] %s (wear level %d)",
item->id,
description.c_str(),
item->getWear()
);

@ -52,7 +52,6 @@ using std::deque;
DFHACK_PLUGIN("dwarfmonitor");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(current_weather);
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(ui);
@ -74,20 +73,8 @@ struct less_second {
}
};
struct dwarfmonitor_configst {
std::string date_format;
};
static dwarfmonitor_configst dwarfmonitor_config;
static bool monitor_jobs = false;
static bool monitor_misery = true;
static bool monitor_date = true;
static bool monitor_weather = true;
static map<df::unit *, deque<activity_type>> work_history;
static int misery[] = { 0, 0, 0, 0, 0, 0, 0 };
static bool misery_upto_date = false;
static color_value monitor_colors[] =
{
COLOR_LIGHTRED,
@ -151,102 +138,18 @@ static void move_cursor(df::coord &pos)
static void open_stats_screen();
namespace dm_lua {
static color_ostream_proxy *out;
static lua_State *state;
typedef int(*initializer)(lua_State*);
int no_args (lua_State *L) { return 0; }
void cleanup()
{
if (out)
{
delete out;
out = NULL;
}
}
bool init_call (const char *func)
{
if (!out)
out = new color_ostream_proxy(Core::getInstance().getConsole());
return Lua::PushModulePublic(*out, state, "plugins.dwarfmonitor", func);
}
bool safe_call (int nargs)
{
return Lua::SafeCall(*out, state, nargs, 0);
}
bool call (const char *func, initializer init = no_args)
{
Lua::StackUnwinder top(state);
if (!init_call(func))
return false;
int nargs = init(state);
return safe_call(nargs);
}
namespace api {
int monitor_state (lua_State *L)
{
std::string type = luaL_checkstring(L, 1);
if (type == "weather")
lua_pushboolean(L, monitor_weather);
else if (type == "misery")
lua_pushboolean(L, monitor_misery);
else if (type == "date")
lua_pushboolean(L, monitor_date);
else
lua_pushnil(L);
return 1;
}
int get_weather_counts (lua_State *L)
{
#define WEATHER_TYPES WTYPE(clear, None); WTYPE(rain, Rain); WTYPE(snow, Snow);
#define WTYPE(type, name) int type = 0;
WEATHER_TYPES
#undef WTYPE
int i, j;
for (i = 0; i < 5; ++i)
{
for (j = 0; j < 5; ++j)
{
switch ((*current_weather)[i][j])
{
#define WTYPE(type, name) case weather_type::name: type++; break;
WEATHER_TYPES
#undef WTYPE
}
}
}
lua_newtable(L);
#define WTYPE(type, name) Lua::TableInsert(L, #type, type);
WEATHER_TYPES
#undef WTYPE
#undef WEATHER_TYPES
return 1;
}
int get_misery_data (lua_State *L)
{
lua_newtable(L);
for (int i = 0; i < 7; i++)
{
Lua::Push(L, i);
lua_newtable(L);
Lua::TableInsert(L, "value", misery[i]);
Lua::TableInsert(L, "color", monitor_colors[i]);
Lua::TableInsert(L, "last", (i == 6));
lua_settable(L, -3);
}
return 1;
}
static int getStressCategoryColors(lua_State *L) {
const size_t n = sizeof(monitor_colors)/sizeof(color_value);
lua_createtable(L, n, 0);
for (size_t i = 0; i < n; ++i) {
Lua::Push(L, monitor_colors[i]);
lua_rawseti(L, -2, i+1);
}
return 1;
}
#define DM_LUA_FUNC(name) { #name, df::wrap_function(dm_lua::api::name, true) }
#define DM_LUA_CMD(name) { #name, dm_lua::api::name }
DFHACK_PLUGIN_LUA_COMMANDS {
DM_LUA_CMD(monitor_state),
DM_LUA_CMD(get_weather_counts),
DM_LUA_CMD(get_misery_data),
DFHACK_LUA_COMMAND(getStressCategoryColors),
DFHACK_LUA_END
};
@ -1648,8 +1551,7 @@ public:
return (selected_column == 1) ? dwarf_column.getFirstSelectedElem() : nullptr;
}
void feed(set<df::interface_key> *input)
{
void feed(set<df::interface_key> *input) override {
bool key_processed = false;
switch (selected_column)
{
@ -1723,8 +1625,7 @@ public:
}
}
void render()
{
void render() override {
using namespace df::enums::interface_key;
if (Screen::isDismissed(this))
@ -1751,7 +1652,7 @@ public:
getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY);
}
std::string getFocusString() { return "dwarfmonitor_preferences"; }
std::string getFocusString() override { return "dwarfmonitor_preferences"; }
private:
ListColumn<size_t> preferences_column;
@ -1762,13 +1663,11 @@ private:
vector<preference_map> preferences_store;
void validateColumn()
{
void validateColumn() {
set_to_limit(selected_column, 1);
}
void resize(int32_t x, int32_t y)
{
void resize(int32_t x, int32_t y) override {
dfhack_viewscreen::resize(x, y);
preferences_column.resize();
dwarf_column.resize();
@ -1776,15 +1675,12 @@ private:
};
static void open_stats_screen()
{
static void open_stats_screen() {
Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
}
static void add_work_history(df::unit *unit, activity_type type)
{
if (work_history.find(unit) == work_history.end())
{
static void add_work_history(df::unit *unit, activity_type type) {
if (work_history.find(unit) == work_history.end()) {
auto max_history = get_max_history();
for (int i = 0; i < max_history; i++)
work_history[unit].push_back(JOB_UNKNOWN);
@ -1794,8 +1690,7 @@ static void add_work_history(df::unit *unit, activity_type type)
work_history[unit].pop_front();
}
static bool is_at_leisure(df::unit *unit)
{
static bool is_at_leisure(df::unit *unit) {
if (Units::getMiscTrait(unit, misc_trait_type::Migrant))
return true;
@ -1805,32 +1700,17 @@ static bool is_at_leisure(df::unit *unit)
return false;
}
static void reset()
{
static void reset() {
work_history.clear();
for (int i = 0; i < 7; i++)
misery[i] = 0;
misery_upto_date = false;
}
static void update_dwarf_stats(bool is_paused)
{
if (monitor_misery)
{
for (int i = 0; i < 7; i++)
misery[i] = 0;
}
for (auto iter = world->units.active.begin(); iter != world->units.active.end(); iter++)
{
df::unit* unit = *iter;
for (auto unit : world->units.active) {
if (!Units::isCitizen(unit))
continue;
if (!DFHack::Units::isActive(unit))
{
if (!DFHack::Units::isActive(unit)) {
auto it = work_history.find(unit);
if (it != work_history.end())
work_history.erase(it);
@ -1838,35 +1718,25 @@ static void update_dwarf_stats(bool is_paused)
continue;
}
if (monitor_misery)
{
misery[get_happiness_cat(unit)]++;
}
if (!monitor_jobs || is_paused)
if (is_paused)
continue;
if (Units::isBaby(unit) ||
Units::isChild(unit) ||
unit->profession == profession::DRUNK)
{
Units::isChild(unit) ||
unit->profession == profession::DRUNK)
continue;
}
if (ENUM_ATTR(profession, military, unit->profession))
{
if (ENUM_ATTR(profession, military, unit->profession)) {
add_work_history(unit, JOB_MILITARY);
continue;
}
if (!unit->job.current_job)
{
if (!unit->job.current_job) {
add_work_history(unit, JOB_IDLE);
continue;
}
if (is_at_leisure(unit))
{
if (is_at_leisure(unit)) {
add_work_history(unit, JOB_LEISURE);
continue;
}
@ -1876,107 +1746,21 @@ static void update_dwarf_stats(bool is_paused)
}
DFhackCExport command_result plugin_onupdate (color_ostream &out)
{
if (!monitor_jobs && !monitor_misery)
return CR_OK;
if(!Maps::IsValid())
DFhackCExport command_result plugin_onupdate (color_ostream &out) {
if (!is_enabled | !Maps::IsValid())
return CR_OK;
bool is_paused = DFHack::World::ReadPauseState();
if (is_paused)
{
if (monitor_misery && !misery_upto_date)
misery_upto_date = true;
else
return CR_OK;
}
else
{
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
}
if (!is_paused && world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
update_dwarf_stats(is_paused);
return CR_OK;
}
struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest
{
typedef df::viewscreen_dwarfmodest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
CoreSuspendClaimer suspend;
if (Maps::IsValid())
{
dm_lua::call("render_all");
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, render);
static bool set_monitoring_mode(const string &mode, const bool &state)
{
bool mode_recognized = false;
if (!is_enabled)
return false;
/*
NOTE: although we are not touching DF directly but there might be
code running that uses these values. So this could use another mutex
or just suspend the core while we edit our values.
*/
CoreSuspender guard;
if (mode == "work" || mode == "all")
{
mode_recognized = true;
monitor_jobs = state;
if (!monitor_jobs)
reset();
}
if (mode == "misery" || mode == "all")
{
mode_recognized = true;
monitor_misery = state;
}
if (mode == "date" || mode == "all")
{
mode_recognized = true;
monitor_date = state;
}
if (mode == "weather" || mode == "all")
{
mode_recognized = true;
monitor_weather = state;
}
return mode_recognized;
}
static bool load_config()
{
return dm_lua::call("load_config");
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (enable)
{
CoreSuspender guard;
load_config();
}
if (is_enabled != enable)
{
if (!INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable))
return CR_FAILURE;
DFhackCExport command_result plugin_enable(color_ostream &, bool enable) {
if (is_enabled != enable) {
reset();
is_enabled = enable;
}
@ -1984,76 +1768,28 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
return CR_OK;
}
static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & parameters)
{
bool show_help = false;
static command_result dwarfmonitor_cmd(color_ostream &, vector <string> & parameters) {
if (parameters.empty())
{
show_help = true;
}
else
{
auto cmd = parameters[0][0];
string mode;
if (parameters.size() > 1)
mode = toLower(parameters[1]);
if (cmd == 'v' || cmd == 'V')
{
out << "DwarfMonitor" << endl << "Version: " << PLUGIN_VERSION << endl;
}
else if ((cmd == 'e' || cmd == 'E') && !mode.empty())
{
if (!is_enabled)
plugin_enable(out, true);
return CR_WRONG_USAGE;
if (set_monitoring_mode(mode, true))
{
out << "Monitoring enabled: " << mode << endl;
}
else
{
show_help = true;
}
}
else if ((cmd == 'd' || cmd == 'D') && !mode.empty())
{
if (set_monitoring_mode(mode, false))
out << "Monitoring disabled: " << mode << endl;
else
show_help = true;
}
else if (cmd == 's' || cmd == 'S')
{
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
}
else if (cmd == 'p' || cmd == 'P')
{
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self);
}
else if (cmd == 'r' || cmd == 'R')
{
CoreSuspender guard;
load_config();
}
else
{
show_help = true;
}
auto cmd = parameters[0][0];
if (cmd == 's' || cmd == 'S') {
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
}
if (show_help)
else if (cmd == 'p' || cmd == 'P') {
CoreSuspender guard;
if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self);
}
else
return CR_WRONG_USAGE;
return CR_OK;
}
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands)
DFhackCExport command_result plugin_init(color_ostream &, std::vector <PluginCommand> &commands)
{
activity_labels[JOB_IDLE] = "Idle";
activity_labels[JOB_MILITARY] = "Military Duty";
@ -2079,27 +1815,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
"Measure fort happiness and efficiency.",
dwarfmonitor_cmd));
dm_lua::state=Lua::Core::State;
if (dm_lua::state == NULL)
return CR_FAILURE;
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out)
{
dm_lua::cleanup();
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
DFhackCExport command_result plugin_onstatechange(color_ostream &, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
if (event == SC_MAP_LOADED)
reset();
break;
default:
break;
}
return CR_OK;
}

@ -1,94 +1,116 @@
#include "uicommon.h"
#include "listcolumn.h"
#include <map>
#include <string>
#include <vector>
#include "df/viewscreen_dwarfmodest.h"
#include "df/ui.h"
#include "modules/Maps.h"
#include "modules/World.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
#include "Debug.h"
#include "LuaTools.h"
#include "PluginManager.h"
DFHACK_PLUGIN("hotkeys");
#define PLUGIN_VERSION 0.1
namespace DFHack {
DBG_DECLARE(hotkeys, log, DebugCategory::LINFO);
}
using std::map;
using std::string;
using std::vector;
using namespace DFHack;
static const string INVOKE_MENU_COMMAND = "overlay trigger hotkeys.menu";
static const string INVOKE_HOTKEYS_COMMAND = "hotkeys";
static const std::string MENU_SCREEN_FOCUS_STRING = "dfhack/lua/hotkeys/menu";
static bool valid = false; // whether the following two vars contain valid data
static string current_focus;
static map<string, string> current_bindings;
static vector<string> sorted_keys;
static bool show_usage = false;
static bool can_invoke(string cmdline, df::viewscreen *screen)
{
static bool can_invoke(const string &cmdline, df::viewscreen *screen) {
vector<string> cmd_parts;
split_string(&cmd_parts, cmdline, " ");
if (toLower(cmd_parts[0]) == "hotkeys")
return false;
return Core::getInstance().getPluginManager()->CanInvokeHotkey(cmd_parts[0], screen);
}
static void add_binding_if_valid(string sym, string cmdline, df::viewscreen *screen)
{
static int cleanupHotkeys(lua_State *) {
DEBUG(log).print("cleaning up old stub keybindings for %s\n", current_focus.c_str());
std::for_each(sorted_keys.begin(), sorted_keys.end(), [](const string &sym) {
string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING;
DEBUG(log).print("clearing keybinding: %s\n", keyspec.c_str());
Core::getInstance().ClearKeyBindings(keyspec);
});
valid = false;
current_focus = "";
sorted_keys.clear();
current_bindings.clear();
return 0;
}
static void add_binding_if_valid(const string &sym, const string &cmdline, df::viewscreen *screen, bool filtermenu) {
if (!can_invoke(cmdline, screen))
return;
if (filtermenu && (cmdline == INVOKE_MENU_COMMAND ||
cmdline == INVOKE_HOTKEYS_COMMAND)) {
DEBUG(log).print("filtering out hotkey menu keybinding\n");
return;
}
current_bindings[sym] = cmdline;
sorted_keys.push_back(sym);
string keyspec = sym + "@dfhack/viewscreen_hotkeys";
Core::getInstance().AddKeyBinding(keyspec, "hotkeys invoke " + int_to_string(sorted_keys.size() - 1));
string keyspec = sym + "@" + MENU_SCREEN_FOCUS_STRING;
string binding = "hotkeys invoke " + int_to_string(sorted_keys.size() - 1);
DEBUG(log).print("adding keybinding: %s -> %s\n", keyspec.c_str(), binding.c_str());
Core::getInstance().AddKeyBinding(keyspec, binding);
}
static void find_active_keybindings(df::viewscreen *screen)
{
current_bindings.clear();
sorted_keys.clear();
static void find_active_keybindings(df::viewscreen *screen, bool filtermenu) {
DEBUG(log).print("scanning for active keybindings\n");
if (valid)
cleanupHotkeys(NULL);
vector<string> valid_keys;
for (char c = 'A'; c <= 'Z'; c++)
{
for (char c = 'A'; c <= 'Z'; c++) {
valid_keys.push_back(string(&c, 1));
}
for (int i = 1; i < 10; i++)
{
for (int i = 1; i < 10; i++) {
valid_keys.push_back("F" + int_to_string(i));
}
valid_keys.push_back("`");
auto current_focus = Gui::getFocusString(screen);
for (int shifted = 0; shifted < 2; shifted++)
{
for (int ctrl = 0; ctrl < 2; ctrl++)
{
for (int alt = 0; alt < 2; alt++)
{
for (auto it = valid_keys.begin(); it != valid_keys.end(); it++)
{
current_focus = Gui::getFocusString(screen);
for (int shifted = 0; shifted < 2; shifted++) {
for (int alt = 0; alt < 2; alt++) {
for (int ctrl = 0; ctrl < 2; ctrl++) {
for (auto it = valid_keys.begin(); it != valid_keys.end(); it++) {
string sym;
if (shifted) sym += "Shift-";
if (ctrl) sym += "Ctrl-";
if (alt) sym += "Alt-";
if (shifted) sym += "Shift-";
sym += *it;
auto list = Core::getInstance().ListKeyBindings(sym);
for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++)
{
if (invoke_cmd->find(":") == string::npos)
{
add_binding_if_valid(sym, *invoke_cmd, screen);
for (auto invoke_cmd = list.begin(); invoke_cmd != list.end(); invoke_cmd++) {
string::size_type colon_pos = invoke_cmd->find(":");
// colons at location 0 are for commands like ":lua"
if (colon_pos == string::npos || colon_pos == 0) {
add_binding_if_valid(sym, *invoke_cmd, screen, filtermenu);
}
else
{
else {
vector<string> tokens;
split_string(&tokens, *invoke_cmd, ":");
string focus = tokens[0].substr(1);
if (prefix_matches(focus, current_focus))
{
if (prefix_matches(focus, current_focus)) {
auto cmdline = trim(tokens[1]);
add_binding_if_valid(sym, cmdline, screen);
add_binding_if_valid(sym, cmdline, screen, filtermenu);
}
}
}
@ -96,288 +118,87 @@ static void find_active_keybindings(df::viewscreen *screen)
}
}
}
}
static bool close_hotkeys_screen()
{
auto screen = Core::getTopViewscreen();
if (Gui::getFocusString(screen) != "dfhack/viewscreen_hotkeys")
return false;
Screen::dismiss(Core::getTopViewscreen());
for_each_(sorted_keys, [] (const string &sym)
{ Core::getInstance().ClearKeyBindings(sym + "@dfhack/viewscreen_hotkeys"); });
sorted_keys.clear();
return true;
valid = true;
}
static void invoke_command(const size_t index)
{
if (sorted_keys.size() <= index)
return;
auto cmd = current_bindings[sorted_keys[index]];
if (close_hotkeys_screen())
{
Core::getInstance().setHotkeyCmd(cmd);
}
static int getHotkeys(lua_State *L) {
find_active_keybindings(Gui::getCurViewscreen(true), true);
Lua::PushVector(L, sorted_keys);
Lua::Push(L, current_bindings);
return 2;
}
static std::string get_help(const std::string &command, bool full_help)
{
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 2) ||
!Lua::PushModulePublic(out, L, "helpdb",
full_help ? "get_entry_long_help" : "get_entry_short_help"))
return "Help text unavailable.";
Lua::Push(L, command);
DFHACK_PLUGIN_LUA_COMMANDS {
DFHACK_LUA_COMMAND(getHotkeys),
DFHACK_LUA_COMMAND(cleanupHotkeys),
DFHACK_LUA_END
};
if (!Lua::SafeCall(out, L, 1, 1))
return "Help text unavailable.";
static void list(color_ostream &out) {
DEBUG(log).print("listing active hotkeys\n");
bool was_valid = valid;
if (!valid)
find_active_keybindings(Gui::getCurViewscreen(true), false);
const char *s = lua_tostring(L, -1);
if (!s)
return "Help text unavailable.";
out.print("Valid keybindings for the current screen (%s)\n",
current_focus.c_str());
std::for_each(sorted_keys.begin(), sorted_keys.end(), [&](const string &sym) {
out.print("%s: %s\n", sym.c_str(), current_bindings[sym].c_str());
});
return s;
if (!was_valid)
cleanupHotkeys(NULL);
}
class ViewscreenHotkeys : public dfhack_viewscreen
{
public:
ViewscreenHotkeys(df::viewscreen *top_screen) : top_screen(top_screen)
{
hotkeys_column.multiselect = false;
hotkeys_column.auto_select = true;
hotkeys_column.setTitle("Key Binding");
hotkeys_column.bottom_margin = 4;
hotkeys_column.allow_search = false;
focus = Gui::getFocusString(top_screen);
populateColumns();
}
void populateColumns()
{
hotkeys_column.clear();
size_t max_key_length = 0;
for_each_(sorted_keys, [&] (const string &sym)
{ if (sym.length() > max_key_length) { max_key_length = sym.length(); } });
int padding = max_key_length + 2;
for (size_t i = 0; i < sorted_keys.size(); i++)
{
string text = pad_string(sorted_keys[i], padding, false);
text += current_bindings[sorted_keys[i]];
hotkeys_column.add(text, i+1);
}
help_start = hotkeys_column.fixWidth() + 2;
hotkeys_column.filterDisplay();
}
void feed(set<df::interface_key> *input)
{
if (hotkeys_column.feed(input))
return;
if (input->count(interface_key::LEAVESCREEN))
{
close_hotkeys_screen();
}
else if (input->count(interface_key::SELECT))
{
invoke_command(hotkeys_column.highlighted_index);
}
else if (input->count(interface_key::CUSTOM_U))
{
show_usage = !show_usage;
}
}
void render()
{
if (Screen::isDismissed(this))
return;
dfhack_viewscreen::render();
Screen::clear();
Screen::drawBorder(" Hotkeys ");
hotkeys_column.display(true);
int32_t y = gps->dimy - 3;
int32_t x = 2;
OutputHotkeyString(x, y, "Leave", "Esc");
x += 3;
OutputHotkeyString(x, y, "Invoke", "Enter or Hotkey");
x += 3;
OutputToggleString(x, y, "Show Usage", "u", show_usage);
y = gps->dimy - 4;
x = 2;
OutputHotkeyString(x, y, focus.c_str(), "Context", false, help_start, COLOR_WHITE, COLOR_BROWN);
if (sorted_keys.size() == 0)
return;
y = 2;
x = help_start;
auto width = gps->dimx - help_start - 2;
vector <string> parts;
Core::cheap_tokenise(current_bindings[sorted_keys[hotkeys_column.highlighted_index]], parts);
if(parts.size() == 0)
return;
string first = parts[0];
parts.erase(parts.begin());
if (first[0] == '#')
return;
static bool invoke_command(color_ostream &out, const size_t index) {
auto screen = Core::getTopViewscreen();
if (sorted_keys.size() <= index ||
Gui::getFocusString(screen) != MENU_SCREEN_FOCUS_STRING)
return false;
OutputString(COLOR_BROWN, x, y, "Help", true, help_start);
string help_text = get_help(first, show_usage);
vector <string> lines;
split_string(&lines, help_text, "\n");
for (auto it = lines.begin(); it != lines.end() && y < gps->dimy - 4; it++)
{
auto wrapped_lines = wrapString(*it, width);
for (auto wit = wrapped_lines.begin(); wit != wrapped_lines.end() && y < gps->dimy - 4; wit++)
{
OutputString(COLOR_WHITE, x, y, *wit, true, help_start);
}
}
}
auto cmd = current_bindings[sorted_keys[index]];
DEBUG(log).print("invoking command: '%s'\n", cmd.c_str());
virtual std::string getFocusString()
{
return "viewscreen_hotkeys";
Screen::Hide hideGuard(screen, Screen::Hide::RESTORE_AT_TOP);
Core::getInstance().runCommand(out, cmd);
}
private:
ListColumn<int> hotkeys_column;
df::viewscreen *top_screen;
string focus;
int32_t help_start;
Screen::dismiss(screen);
return true;
}
void resize(int32_t x, int32_t y)
{
dfhack_viewscreen::resize(x, y);
hotkeys_column.resize();
static command_result hotkeys_cmd(color_ostream &out, vector <string> & parameters) {
if (!parameters.size()) {
DEBUG(log).print("invoking command: '%s'\n", INVOKE_MENU_COMMAND.c_str());
return Core::getInstance().runCommand(out, INVOKE_MENU_COMMAND );
}
static vector<string> wrapString(string str, int width)
{
vector<string> result;
string excess;
if (int(str.length()) > width)
{
auto cut_space = str.rfind(' ', width-1);
int excess_start;
if (cut_space == string::npos)
{
cut_space = width-1;
excess_start = cut_space;
}
else
{
excess_start = cut_space + 1;
}
string line = str.substr(0, cut_space);
excess = str.substr(excess_start);
result.push_back(line);
auto excess_lines = wrapString(excess, width);
result.insert(result.end(), excess_lines.begin(), excess_lines.end());
}
else
{
result.push_back(str);
}
return result;
if (parameters[0] == "list") {
list(out);
return CR_OK;
}
};
// internal command -- intentionally undocumented
if (parameters.size() != 2 || parameters[0] != "invoke")
return CR_WRONG_USAGE;
static command_result hotkeys_cmd(color_ostream &out, vector <string> & parameters)
{
if (parameters.empty())
{
if (Maps::IsValid())
{
auto top_screen = Core::getTopViewscreen();
if (Gui::getFocusString(top_screen) != "dfhack/viewscreen_hotkeys")
{
find_active_keybindings(top_screen);
Screen::show(dts::make_unique<ViewscreenHotkeys>(top_screen), plugin_self);
}
}
}
else
{
auto cmd = parameters[0][0];
if (cmd == 'v')
{
out << "Hotkeys" << endl << "Version: " << PLUGIN_VERSION << endl;
}
else if (cmd == 'i')
{
int index;
stringstream index_raw(parameters[1]);
index_raw >> index;
invoke_command(index);
}
else
{
return CR_WRONG_USAGE;
}
}
CoreSuspender guard;
return CR_OK;
int index = string_to_int(parameters[1], -1);
if (index < 0)
return CR_WRONG_USAGE;
return invoke_command(out, index) ? CR_OK : CR_WRONG_USAGE;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
if (!gps)
out.printerr("Could not insert hotkeys hooks!\n");
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands) {
commands.push_back(
PluginCommand(
"hotkeys",
"Show all dfhack keybindings in current context.",
hotkeys_cmd));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
sorted_keys.clear();
break;
default:
break;
}
"Invoke hotkeys from the interactive menu.",
hotkeys_cmd,
Gui::anywhere_hotkey));
return CR_OK;
}

@ -1,17 +0,0 @@
project(labormanager)
# A list of source files
set(PROJECT_SRCS
labormanager.cpp
joblabormapper.cpp
)
# A list of headers
set(PROJECT_HDRS
labormanager.h
joblabormapper.h
)
set_source_files_properties(${PROJECT_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE)
# mash them together (headers are marked as headers and nothing will try to compile them)
list(APPEND PROJECT_SRCS ${PROJECT_HDRS})
dfhack_plugin(labormanager ${PROJECT_SRCS})

@ -1,175 +1,186 @@
local _ENV = mkmodule('plugins.dwarfmonitor')
local gps = df.global.gps
local gui = require 'gui'
local json = require('json')
local guidm = require('gui.dwarfmode')
local overlay = require('plugins.overlay')
config = {}
widgets = {}
local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json'
function dmerror(desc)
qerror('dwarfmonitor: ' .. tostring(desc))
end
Widget = defclass(Widget)
function Widget:init(opts)
self.opts = opts
end
function Widget:get_pos()
local x = self.opts.x >= 0 and self.opts.x or gps.dimx + self.opts.x
local y = self.opts.y >= 0 and self.opts.y or gps.dimy + self.opts.y
if self.opts.anchor == 'right' then
x = x - (self:get_width() or 0) + 1
end
return x, y
end
function Widget:render()
if monitor_state(self.opts.type) == false then
return
end
self:update()
local x, y = self:get_pos()
local p = gui.Painter.new_xy(x, y, gps.dimx - 1, y)
self:render_body(p)
end
function Widget:update() end
function Widget:get_width() end
function Widget:render_body() end
-- ------------- --
-- WeatherWidget --
-- ------------- --
Widget_weather = defclass(Widget_weather, Widget)
WeatherWidget = defclass(WeatherWidget, overlay.OverlayWidget)
WeatherWidget.ATTRS{
default_pos={x=15,y=-1},
viewscreens={'dungeonmode', 'dwarfmode'},
}
function Widget_weather:update()
self.counts = get_weather_counts()
function WeatherWidget:init()
self.rain = false
self.snow = false
end
function Widget_weather:get_width()
if self.counts.rain > 0 then
if self.counts.snow > 0 then
return 9
function WeatherWidget:overlay_onupdate()
local rain, snow = false, false
local cw = df.global.current_weather
for i=0,4 do
for j=0,4 do
weather = cw[i][j]
if weather == df.weather_type.Rain then rain = true end
if weather == df.weather_type.Snow then snow = true end
end
return 4
elseif self.counts.snow > 0 then
return 4
end
return 0
self.frame.w = (rain and 4 or 0) + (snow and 4 or 0) +
((snow and rain) and 1 or 0)
self.rain, self.snow = rain, snow
end
function Widget_weather:render_body(p)
if self.counts.rain > 0 then
p:string('Rain', COLOR_LIGHTBLUE):advance(1)
end
if self.counts.snow > 0 then
p:string('Snow', COLOR_WHITE)
function WeatherWidget:onRenderBody(dc)
if self.rain then dc:string('Rain', COLOR_LIGHTBLUE):advance(1) end
if self.snow then dc:string('Snow', COLOR_WHITE) end
end
-- ---------- --
-- DateWidget --
-- ---------- --
local function get_date_format()
local ok, config = pcall(json.decode_file, DWARFMONITOR_CONFIG_FILE)
if not ok or not config.date_format then
return 'Y-M-D'
end
return config.date_format
end
Widget_date = defclass(Widget_date, Widget)
Widget_date.ATTRS = {
output = ''
DateWidget = defclass(DateWidget, overlay.OverlayWidget)
DateWidget.ATTRS{
default_pos={x=-16,y=1},
viewscreens={'dungeonmode', 'dwarfmode'},
}
function Widget_date:update()
if not self.opts.format then
self.opts.format = 'Y-M-D'
end
function DateWidget:init()
self.datestr = ''
self.fmt = get_date_format()
end
function DateWidget:overlay_onupdate()
local year = dfhack.world.ReadCurrentYear()
local month = dfhack.world.ReadCurrentMonth() + 1
local day = dfhack.world.ReadCurrentDay()
self.output = 'Date:'
for i = 1, #self.opts.format do
local c = self.opts.format:sub(i, i)
local fmt = self.fmt
local datestr = 'Date:'
for i=1,#fmt do
local c = fmt:sub(i, i)
if c == 'y' or c == 'Y' then
self.output = self.output .. year
datestr = datestr .. year
elseif c == 'm' or c == 'M' then
if c == 'M' and month < 10 then
self.output = self.output .. '0'
datestr = datestr .. '0'
end
self.output = self.output .. month
datestr = datestr .. month
elseif c == 'd' or c == 'D' then
if c == 'D' and day < 10 then
self.output = self.output .. '0'
datestr = datestr .. '0'
end
self.output = self.output .. day
datestr = datestr .. day
else
self.output = self.output .. c
datestr = datestr .. c
end
end
end
function Widget_date:get_width()
return #self.output
self.frame.w = #datestr
self.datestr = datestr
end
function Widget_date:render_body(p)
p:string(self.output, COLOR_GREY)
function DateWidget:onRenderBody(dc)
dc:string(self.datestr, COLOR_GREY)
end
Widget_misery = defclass(Widget_misery, Widget)
-- ------------ --
-- MiseryWidget --
-- ------------ --
MiseryWidget = defclass(MiseryWidget, overlay.OverlayWidget)
MiseryWidget.ATTRS{
default_pos={x=-2,y=-1},
viewscreens={'dwarfmode'},
}
function Widget_misery:update()
self.data = get_misery_data()
function MiseryWidget:init()
self.colors = getStressCategoryColors()
self.stress_category_counts = {}
end
function Widget_misery:get_width()
local w = 2 + 6
for k, v in pairs(self.data) do
w = w + #tostring(v.value)
function MiseryWidget:overlay_onupdate()
local counts, num_colors = {}, #self.colors
for _,unit in ipairs(df.global.world.units.active) do
if not dfhack.units.isCitizen(unit, true) then goto continue end
local stress_category = math.min(num_colors,
dfhack.units.getStressCategory(unit))
counts[stress_category] = (counts[stress_category] or 0) + 1
::continue::
end
return w
end
function Widget_misery:render_body(p)
p:string("H:", COLOR_WHITE)
for i = 0, 6 do
local v = self.data[i]
p:string(tostring(v.value), v.color)
if not v.last then
p:string("/", COLOR_WHITE)
end
local width = 2 + num_colors - 1 -- 'H:' plus the slashes
for i=1,num_colors do
width = width + #tostring(counts[i] or 0)
end
end
Widget_cursor = defclass(Widget_cursor, Widget)
self.stress_category_counts = counts
self.frame.w = width
end
function Widget_cursor:update()
if gps.mouse_x == -1 and not self.opts.show_invalid then
self.output = ''
return
function MiseryWidget:onRenderBody(dc)
dc:string('H:', COLOR_WHITE)
local counts = self.stress_category_counts
for i,color in ipairs(self.colors) do
dc:string(tostring(counts[i] or 0), color)
if i < #self.colors then dc:string('/', COLOR_WHITE) end
end
self.output = (self.opts.format or '(x,y)'):gsub('[xX]', gps.mouse_x):gsub('[yY]', gps.mouse_y)
end
function Widget_cursor:get_width()
return #self.output
end
-- ------------ --
-- CursorWidget --
-- ------------ --
function Widget_cursor:render_body(p)
p:string(self.output)
end
CursorWidget = defclass(CursorWidget, overlay.OverlayWidget)
CursorWidget.ATTRS{
default_pos={x=2,y=-4},
viewscreens={'dungeonmode', 'dwarfmode'},
}
function render_all()
for _, w in pairs(widgets) do
w:render()
end
end
function CursorWidget:onRenderBody(dc)
local screenx, screeny = dfhack.screen.getMousePos()
local mouse_map = dfhack.gui.getMousePos()
local keyboard_map = guidm.getCursorPos()
function load_config()
config = require('json').decode_file('dfhack-config/dwarfmonitor.json')
if not config.widgets then
dmerror('No widgets enabled')
local text = {}
table.insert(text, ('mouse UI grid (%d,%d)'):format(screenx, screeny))
if mouse_map then
table.insert(text, ('mouse map coord (%d,%d,%d)')
:format(mouse_map.x, mouse_map.y, mouse_map.z))
end
if type(config.widgets) ~= 'table' then
dmerror('"widgets" is not a table')
if keyboard_map then
table.insert(text, ('kbd cursor coord (%d,%d,%d)')
:format(keyboard_map.x, keyboard_map.y, keyboard_map.z))
end
widgets = {}
for _, opts in pairs(config.widgets) do
if type(opts) ~= 'table' then dmerror('"widgets" is not an array') end
if not opts.type then dmerror('Widget missing type field') end
local cls = _ENV['Widget_' .. opts.type]
if not cls then
dmerror('Invalid widget type: ' .. opts.type)
end
table.insert(widgets, cls(opts))
local width = 0
for i,line in ipairs(text) do
dc:seek(0, i-1):string(line)
width = math.max(width, #line)
end
self.frame.w = width
self.frame.h = #text
end
-- register our widgets with the overlay
OVERLAY_WIDGETS = {
cursor=CursorWidget,
date=DateWidget,
misery=MiseryWidget,
weather=WeatherWidget,
}
return _ENV

@ -0,0 +1,244 @@
local _ENV = mkmodule('plugins.hotkeys')
local gui = require('gui')
local helpdb = require('helpdb')
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
-- ----------------- --
-- HotspotMenuWidget --
-- ----------------- --
HotspotMenuWidget = defclass(HotspotMenuWidget, overlay.OverlayWidget)
HotspotMenuWidget.ATTRS{
default_pos={x=1,y=3},
hotspot=true,
viewscreens={'dwarfmode'},
overlay_onupdate_max_freq_seconds=0,
frame={w=2, h=1}
}
function HotspotMenuWidget:init()
self:addviews{widgets.Label{text='!!'}}
self.mouseover = false
end
function HotspotMenuWidget:overlay_onupdate()
local hasMouse = self:getMousePos()
if hasMouse and not self.mouseover then
self.mouseover = true
return true
end
self.mouseover = hasMouse
end
function HotspotMenuWidget:overlay_trigger()
local hotkeys, bindings = getHotkeys()
return MenuScreen{
hotspot_frame=self.frame,
hotkeys=hotkeys,
bindings=bindings}:show()
end
-- register the menu hotspot with the overlay
OVERLAY_WIDGETS = {menu=HotspotMenuWidget}
-- ---------- --
-- MenuScreen --
-- ---------- --
local ARROW = string.char(26)
local MAX_LIST_WIDTH = 45
local MAX_LIST_HEIGHT = 15
MenuScreen = defclass(MenuScreen, gui.Screen)
MenuScreen.ATTRS{
focus_path='hotkeys/menu',
hotspot_frame=DEFAULT_NIL,
hotkeys=DEFAULT_NIL,
bindings=DEFAULT_NIL,
}
-- get a map from the binding string to a list of hotkey strings that all
-- point to that binding
local function get_bindings_to_hotkeys(hotkeys, bindings)
local bindings_to_hotkeys = {}
for _,hotkey in ipairs(hotkeys) do
local binding = bindings[hotkey]
table.insert(ensure_key(bindings_to_hotkeys, binding), hotkey)
end
return bindings_to_hotkeys
end
-- number of non-text tiles: icon, space, space between cmd and hk, scrollbar
local LIST_BUFFER = 2 + 1 + 1
local function get_choices(hotkeys, bindings, is_inverted)
local choices, max_width, seen = {}, 0, {}
local bindings_to_hotkeys = get_bindings_to_hotkeys(hotkeys, bindings)
-- build list choices
for _,hotkey in ipairs(hotkeys) do
local command = bindings[hotkey]
if seen[command] then goto continue end
seen[command] = true
local hk_width, tokens = 0, {}
for _,hk in ipairs(bindings_to_hotkeys[command]) do
if hk_width ~= 0 then
table.insert(tokens, ', ')
hk_width = hk_width + 2
end
table.insert(tokens, {text=hk, pen=COLOR_LIGHTGREEN})
hk_width = hk_width + #hk
end
local command_str = command
if hk_width + #command + LIST_BUFFER > MAX_LIST_WIDTH then
local max_command_len = MAX_LIST_WIDTH - hk_width - LIST_BUFFER
command_str = command:sub(1, max_command_len - 3) .. '...'
end
table.insert(tokens, 1, {text=command_str})
local choice = {icon=ARROW, command=command, text=tokens,
hk_width=hk_width}
max_width = math.max(max_width, hk_width + #command_str + LIST_BUFFER)
table.insert(choices, is_inverted and 1 or #choices + 1, choice)
::continue::
end
-- adjust width of command fields so the hotkey tokens are right justified
for _,choice in ipairs(choices) do
local command_token = choice.text[1]
command_token.width = max_width - choice.hk_width - 3
end
return choices, max_width
end
function MenuScreen:init()
self.mouseover = false
local choices,list_width = get_choices(self.hotkeys, self.bindings,
self.hotspot_frame.b)
local list_frame = copyall(self.hotspot_frame)
list_frame.w = list_width + 2
list_frame.h = math.min(#choices, MAX_LIST_HEIGHT) + 2
if list_frame.t then
list_frame.t = math.max(0, list_frame.t - 1)
else
list_frame.b = math.max(0, list_frame.b - 1)
end
if list_frame.l then
list_frame.l = math.max(0, list_frame.l - 1)
else
list_frame.r = math.max(0, list_frame.r - 1)
end
local help_frame = {w=list_frame.w, l=list_frame.l, r=list_frame.r}
if list_frame.t then
help_frame.t = list_frame.t + list_frame.h + 1
else
help_frame.b = list_frame.b + list_frame.h + 1
end
self:addviews{
widgets.ResizingPanel{
view_id='list_panel',
autoarrange_subviews=true,
frame=list_frame,
frame_style=gui.GREY_LINE_FRAME,
frame_background=gui.CLEAR_PEN,
subviews={
widgets.List{
view_id='list',
choices=choices,
icon_width=2,
on_select=self:callback('onSelect'),
on_submit=self:callback('onSubmit'),
on_submit2=self:callback('onSubmit2'),
},
},
},
widgets.ResizingPanel{
view_id='help_panel',
autoarrange_subviews=true,
frame=help_frame,
frame_style=gui.GREY_LINE_FRAME,
frame_background=gui.CLEAR_PEN,
subviews={
widgets.WrappedLabel{
view_id='help',
text_to_wrap='',
scroll_keys={},
},
},
},
}
end
function MenuScreen:onDismiss()
cleanupHotkeys()
end
function MenuScreen:onSelect(_, choice)
if not choice or #self.subviews == 0 then return end
local first_word = choice.command:trim():split(' +')[1]
if first_word:startswith(':') then first_word = first_word:sub(2) end
self.subviews.help.text_to_wrap = helpdb.is_entry(first_word) and
helpdb.get_entry_short_help(first_word) or 'Command not found'
self.subviews.help_panel:updateLayout()
end
function MenuScreen:onSubmit(_, choice)
if not choice then return end
dfhack.screen.hideGuard(self, dfhack.run_command, choice.command)
self:dismiss()
end
function MenuScreen:onSubmit2(_, choice)
if not choice then return end
self:dismiss()
dfhack.run_script('gui/launcher', choice.command)
end
function MenuScreen:onInput(keys)
if keys.LEAVESCREEN then
self:dismiss()
return true
elseif keys.STANDARDSCROLL_RIGHT then
self:onSubmit2(self.subviews.list:getSelected())
return true
elseif keys._MOUSE_L_DOWN then
local list = self.subviews.list
local x = list:getMousePos()
if x == 0 then -- clicked on icon
self:onSubmit2(list:getSelected())
return true
end
end
return self:inputToSubviews(keys)
end
function MenuScreen:onRenderFrame(dc, rect)
self:renderParent()
end
function MenuScreen:onRenderBody(dc)
local panel = self.subviews.list_panel
local list = self.subviews.list
local idx = list:getIdxUnderMouse()
if idx and idx ~= self.last_mouse_idx then
-- focus follows mouse, but if cursor keys were used to change the
-- selection, don't override the selection until the mouse moves to
-- another item
list:setSelected(idx)
self.mouseover = true
self.last_mouse_idx = idx
elseif not panel:getMousePos(gui.ViewRect{rect=panel.frame_rect})
and self.mouseover then
-- once the mouse has entered the list area, leaving the frame should
-- close the menu screen
self:dismiss()
end
end
return _ENV

@ -0,0 +1,485 @@
local _ENV = mkmodule('plugins.overlay')
local gui = require('gui')
local json = require('json')
local utils = require('utils')
local widgets = require('gui.widgets')
local OVERLAY_CONFIG_FILE = 'dfhack-config/overlay.json'
local OVERLAY_WIDGETS_VAR = 'OVERLAY_WIDGETS'
local DEFAULT_X_POS, DEFAULT_Y_POS = -2, -2
-- ---------------- --
-- state and config --
-- ---------------- --
local active_triggered_widget = nil
local active_triggered_screen = nil -- if non-nil, hotspots will not get updates
local widget_db = {} -- map of widget name to ephermeral state
local widget_index = {} -- ordered list of widget names
local overlay_config = {} -- map of widget name to persisted state
local active_hotspot_widgets = {} -- map of widget names to the db entry
local active_viewscreen_widgets = {} -- map of vs_name to map of w.names -> db
local function reset()
if active_triggered_screen then
active_triggered_screen:dismiss()
end
active_triggered_widget = nil
active_triggered_screen = nil
widget_db = {}
widget_index = {}
local ok, config = pcall(json.decode_file, OVERLAY_CONFIG_FILE)
overlay_config = ok and config or {}
active_hotspot_widgets = {}
active_viewscreen_widgets = {}
end
local function save_config()
if not safecall(json.encode_file, overlay_config, OVERLAY_CONFIG_FILE) then
dfhack.printerr(('failed to save overlay config file: "%s"')
:format(path))
end
end
local function triggered_screen_has_lock()
if not active_triggered_screen then return false end
if active_triggered_screen:isActive() then return true end
active_triggered_widget = nil
active_triggered_screen = nil
return false
end
-- ----------- --
-- utility fns --
-- ----------- --
local function normalize_list(element_or_list)
if type(element_or_list) == 'table' then return element_or_list end
return {element_or_list}
end
-- normalize "short form" viewscreen names to "long form"
local function normalize_viewscreen_name(vs_name)
if vs_name:match('viewscreen_.*st') then return vs_name end
return 'viewscreen_' .. vs_name .. 'st'
end
-- reduce "long form" viewscreen names to "short form"
local function simplify_viewscreen_name(vs_name)
_,_,short_name = vs_name:find('^viewscreen_(.*)st$')
if short_name then return short_name end
return vs_name
end
local function is_empty(tbl)
for _ in pairs(tbl) do
return false
end
return true
end
local function sanitize_pos(pos)
local x = math.floor(tonumber(pos.x) or DEFAULT_X_POS)
local y = math.floor(tonumber(pos.y) or DEFAULT_Y_POS)
-- if someone accidentally uses 0-based instead of 1-based indexing, fix it
if x == 0 then x = 1 end
if y == 0 then y = 1 end
return {x=x, y=y}
end
local function make_frame(pos, old_frame)
old_frame = old_frame or {}
local frame = {w=old_frame.w, h=old_frame.h}
if pos.x < 0 then frame.r = math.abs(pos.x) - 1 else frame.l = pos.x - 1 end
if pos.y < 0 then frame.b = math.abs(pos.y) - 1 else frame.t = pos.y - 1 end
return frame
end
local function get_screen_rect()
local w, h = dfhack.screen.getWindowSize()
return gui.ViewRect{rect=gui.mkdims_wh(0, 0, w, h)}
end
-- ------------- --
-- CLI functions --
-- ------------- --
local function get_name(name_or_number)
local num = tonumber(name_or_number)
if num and widget_index[num] then
return widget_index[num]
end
return tostring(name_or_number)
end
local function do_by_names_or_numbers(args, fn)
local arglist = normalize_list(args)
if #arglist == 0 then
dfhack.printerr('please specify a widget name or list number')
return
end
for _,name_or_number in ipairs(arglist) do
local name = get_name(name_or_number)
local db_entry = widget_db[name]
if not db_entry then
dfhack.printerr(('widget not found: "%s"'):format(name))
else
fn(name, db_entry)
end
end
end
local function do_enable(args, quiet, skip_save)
local enable_fn = function(name, db_entry)
overlay_config[name].enabled = true
if db_entry.widget.hotspot then
active_hotspot_widgets[name] = db_entry
end
for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do
vs_name = normalize_viewscreen_name(vs_name)
ensure_key(active_viewscreen_widgets, vs_name)[name] = db_entry
end
if not quiet then
print(('enabled widget %s'):format(name))
end
end
if args[1] == 'all' then
for name,db_entry in pairs(widget_db) do
if not overlay_config[name].enabled then
enable_fn(name, db_entry)
end
end
else
do_by_names_or_numbers(args, enable_fn)
end
if not skip_save then
save_config()
end
end
local function do_disable(args)
local disable_fn = function(name, db_entry)
overlay_config[name].enabled = false
if db_entry.widget.hotspot then
active_hotspot_widgets[name] = nil
end
for _,vs_name in ipairs(normalize_list(db_entry.widget.viewscreens)) do
vs_name = normalize_viewscreen_name(vs_name)
ensure_key(active_viewscreen_widgets, vs_name)[name] = nil
if is_empty(active_viewscreen_widgets[vs_name]) then
active_viewscreen_widgets[vs_name] = nil
end
end
print(('disabled widget %s'):format(name))
end
if args[1] == 'all' then
for name,db_entry in pairs(widget_db) do
if overlay_config[name].enabled then
disable_fn(name, db_entry)
end
end
else
do_by_names_or_numbers(args, disable_fn)
end
save_config()
end
local function do_list(args)
local filter = args and #args > 0
local num_filtered = 0
for i,name in ipairs(widget_index) do
if filter then
local passes = false
for _,str in ipairs(args) do
if name:find(str) then
passes = true
break
end
end
if not passes then
num_filtered = num_filtered + 1
goto continue
end
end
local db_entry = widget_db[name]
local enabled = overlay_config[name].enabled
dfhack.color(enabled and COLOR_LIGHTGREEN or COLOR_YELLOW)
dfhack.print(enabled and '[enabled] ' or '[disabled]')
dfhack.color()
print((' %d) %s'):format(i, name))
::continue::
end
if num_filtered > 0 then
print(('(%d widgets filtered out)'):format(num_filtered))
end
end
local function load_widget(name, widget_class)
local widget = widget_class{name=name}
widget_db[name] = {
widget=widget,
next_update_ms=widget.overlay_onupdate and 0 or math.huge,
}
if not overlay_config[name] then overlay_config[name] = {} end
local config = overlay_config[name]
config.pos = sanitize_pos(config.pos or widget.default_pos)
widget.frame = make_frame(config.pos, widget.frame)
if config.enabled then
do_enable(name, true, true)
else
config.enabled = false
end
end
local function load_widgets(env_prefix, provider, env_fn)
local env_name = env_prefix .. provider
local ok, provider_env = pcall(env_fn, env_name)
if not ok or not provider_env[OVERLAY_WIDGETS_VAR] then return end
local overlay_widgets = provider_env[OVERLAY_WIDGETS_VAR]
if type(overlay_widgets) ~= 'table' then
dfhack.printerr(
('error loading overlay widgets from "%s": %s map is malformed')
:format(env_name, OVERLAY_WIDGETS_VAR))
return
end
for widget_name,widget_class in pairs(overlay_widgets) do
local name = provider .. '.' .. widget_name
if not safecall(load_widget, name, widget_class) then
dfhack.printerr(('error loading overlay widget "%s"'):format(name))
end
end
end
-- called directly from cpp on plugin enable
function reload()
reset()
for _,plugin in ipairs(dfhack.internal.listPlugins()) do
load_widgets('plugins.', plugin, require)
end
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
local files = dfhack.filesystem.listdir_recursive(
script_path, nil, false)
if not files then goto skip_path end
for _,f in ipairs(files) do
if not f.isdir and
f.path:endswith('.lua') and
not f.path:startswith('test/') and
not f.path:startswith('internal/') then
local script_name = f.path:sub(1, #f.path - 4) -- remove '.lua'
load_widgets('', script_name, reqscript)
end
end
::skip_path::
end
for name in pairs(widget_db) do
table.insert(widget_index, name)
end
table.sort(widget_index)
reposition_widgets()
end
local function dump_widget_config(name, widget)
local pos = overlay_config[name].pos
print(('widget %s is positioned at x=%d, y=%d'):format(name, pos.x, pos.y))
local viewscreens = normalize_list(widget.viewscreens)
if #viewscreens > 0 then
print(' it will be attached to the following viewscreens:')
for _,vs in ipairs(viewscreens) do
print((' %s'):format(simplify_viewscreen_name(vs)))
end
end
if widget.hotspot then
print(' it will act as a hotspot on all screens')
end
end
local function do_position(args)
local name_or_number, x, y = table.unpack(args)
local name = get_name(name_or_number)
if not widget_db[name] then
if not name_or_number then
dfhack.printerr('please specify a widget name or list number')
else
dfhack.printerr(('widget not found: "%s"'):format(name))
end
return
end
local widget = widget_db[name].widget
local pos
if x == 'default' then
pos = sanitize_pos(widget.default_pos)
else
x, y = tonumber(x), tonumber(y)
if not x or not y then
dump_widget_config(name, widget)
return
end
pos = sanitize_pos{x=x, y=y}
end
overlay_config[name].pos = pos
widget.frame = make_frame(pos, widget.frame)
widget:updateLayout(get_screen_rect())
save_config()
print(('repositioned widget %s to x=%d, y=%d'):format(name, pos.x, pos.y))
end
-- note that the widget does not have to be enabled to be triggered
local function do_trigger(args)
if triggered_screen_has_lock() then
dfhack.printerr(('cannot trigger widget; widget "%s" is already active')
:format(active_triggered_widget))
return
end
do_by_names_or_numbers(args[1], function(name, db_entry)
local widget = db_entry.widget
if widget.overlay_trigger then
active_triggered_screen = widget:overlay_trigger()
if active_triggered_screen then
active_triggered_widget = name
end
print(('triggered widget %s'):format(name))
end
end)
end
local command_fns = {
enable=do_enable,
disable=do_disable,
list=do_list,
position=do_position,
trigger=do_trigger,
}
local HELP_ARGS = utils.invert{'help', '--help', '-h'}
function overlay_command(args)
local command = table.remove(args, 1) or 'help'
if HELP_ARGS[command] or not command_fns[command] then return false end
command_fns[command](args)
return true
end
-- ---------------- --
-- event management --
-- ---------------- --
local function detect_frame_change(widget, fn)
local frame = widget.frame
local w, h = frame.w, frame.h
local ret = fn()
if w ~= frame.w or h ~= frame.h then
widget:updateLayout()
end
return ret
end
local function get_next_onupdate_timestamp(now_ms, widget)
local freq_s = widget.overlay_onupdate_max_freq_seconds
if freq_s == 0 then
return now_ms
end
local freq_ms = math.floor(freq_s * 1000)
local jitter = math.random(0, freq_ms // 8) -- up to ~12% jitter
return now_ms + freq_ms - jitter
end
-- reduces the next call by a small random amount to introduce jitter into the
-- widget processing timings
local function do_update(name, db_entry, now_ms, vs)
if db_entry.next_update_ms > now_ms then return end
local w = db_entry.widget
db_entry.next_update_ms = get_next_onupdate_timestamp(now_ms, w)
if detect_frame_change(w, function() return w:overlay_onupdate(vs) end) then
active_triggered_screen = w:overlay_trigger()
if active_triggered_screen then
active_triggered_widget = name
return true
end
end
end
function update_hotspot_widgets()
if triggered_screen_has_lock() then return end
local now_ms = dfhack.getTickCount()
for name,db_entry in pairs(active_hotspot_widgets) do
if do_update(name, db_entry, now_ms) then return end
end
end
-- not subject to trigger lock since these widgets are already filtered by
-- viewscreen
function update_viewscreen_widgets(vs_name, vs)
local vs_widgets = active_viewscreen_widgets[vs_name]
if not vs_widgets then return end
local now_ms = dfhack.getTickCount()
for name,db_entry in pairs(vs_widgets) do
if do_update(name, db_entry, now_ms, vs) then return end
end
end
function feed_viewscreen_widgets(vs_name, keys)
local vs_widgets = active_viewscreen_widgets[vs_name]
if not vs_widgets then return false end
for _,db_entry in pairs(vs_widgets) do
local w = db_entry.widget
if detect_frame_change(w, function() return w:onInput(keys) end) then
return true
end
end
return false
end
function render_viewscreen_widgets(vs_name)
local vs_widgets = active_viewscreen_widgets[vs_name]
if not vs_widgets then return false end
local dc = gui.Painter.new()
for _,db_entry in pairs(vs_widgets) do
local w = db_entry.widget
detect_frame_change(w, function() w:render(dc) end)
end
end
-- called when the DF window is resized
function reposition_widgets()
local sr = get_screen_rect()
for _,db_entry in pairs(widget_db) do
db_entry.widget:updateLayout(sr)
end
end
-- ------------------------------------------------- --
-- OverlayWidget (base class of all overlay widgets) --
-- ------------------------------------------------- --
OverlayWidget = defclass(OverlayWidget, widgets.Widget)
OverlayWidget.ATTRS{
name=DEFAULT_NIL, -- this is set by the framework to the widget name
default_pos={x=DEFAULT_X_POS, y=DEFAULT_Y_POS}, -- 1-based widget screen pos
hotspot=false, -- whether to call overlay_onupdate on all screens
viewscreens={}, -- override with associated viewscreen or list of viewscrens
overlay_onupdate_max_freq_seconds=5, -- throttle calls to overlay_onupdate
}
function OverlayWidget:init()
if self.overlay_onupdate_max_freq_seconds < 0 then
error(('overlay_onupdate_max_freq_seconds must be >= 0: %s')
:format(tostring(self.overlay_onupdate_max_freq_seconds)))
end
-- set defaults for frame. the widget is expected to keep these up to date
-- when display contents change so the widget position can shift if the
-- frame is relative to the right or bottom edges.
self.frame = self.frame or {}
self.frame.w = self.frame.w or 5
self.frame.h = self.frame.h or 1
end
return _ENV

@ -88,6 +88,9 @@
#include "PluginManager.h"
#include "VTableInterpose.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
using namespace DFHack;
DFHACK_PLUGIN("overlay");
@ -98,18 +101,58 @@ namespace DFHack {
DBG_DECLARE(overlay, event, DebugCategory::LINFO);
}
static df::coord2d screenSize;
static void call_overlay_lua(color_ostream *out, const char *fn_name,
int nargs = 0, int nres = 0,
Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA,
Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) {
DEBUG(event).print("calling overlay lua function: '%s'\n", fn_name);
CoreSuspender guard;
auto L = Lua::Core::State;
Lua::StackUnwinder top(L);
if (!out)
out = &Core::getInstance().getConsole();
Lua::CallLuaModuleFunction(*out, L, "plugins.overlay", fn_name, nargs, nres,
std::forward<Lua::LuaLambda&&>(args_lambda),
std::forward<Lua::LuaLambda&&>(res_lambda));
}
template<class T>
struct viewscreen_overlay : T {
typedef T interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, logic, ()) {
INTERPOSE_NEXT(logic)();
call_overlay_lua(NULL, "update_viewscreen_widgets", 2, 0,
[&](lua_State *L) {
Lua::Push(L, T::_identity.getName());
Lua::Push(L, this);
});
}
DEFINE_VMETHOD_INTERPOSE(void, feed, (std::set<df::interface_key> *input)) {
INTERPOSE_NEXT(feed)(input);
bool input_is_handled = false;
call_overlay_lua(NULL, "feed_viewscreen_widgets", 2, 1,
[&](lua_State *L) {
Lua::Push(L, T::_identity.getName());
Lua::PushInterfaceKeys(L, *input);
}, [&](lua_State *L) {
input_is_handled = lua_toboolean(L, -1);
});
if (!input_is_handled)
INTERPOSE_NEXT(feed)(input);
}
DEFINE_VMETHOD_INTERPOSE(void, render, ()) {
INTERPOSE_NEXT(render)();
call_overlay_lua(NULL, "render_viewscreen_widgets", 2, 0,
[&](lua_State *L) {
Lua::Push(L, T::_identity.getName());
Lua::Push(L, this);
});
}
};
@ -207,10 +250,15 @@ IMPLEMENT_HOOKS(workshop_profile)
!INTERPOSE_HOOK(screen##_overlay, feed).apply(enable) || \
!INTERPOSE_HOOK(screen##_overlay, render).apply(enable)
DFhackCExport command_result plugin_enable(color_ostream &, bool enable) {
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
if (is_enabled == enable)
return CR_OK;
if (enable) {
screenSize = Screen::getWindowSize();
call_overlay_lua(&out, "reload");
}
DEBUG(control).print("%sing interpose hooks\n", enable ? "enabl" : "disabl");
if (INTERPOSE_HOOKS_FAILED(adopt_region) ||
@ -302,15 +350,14 @@ DFhackCExport command_result plugin_enable(color_ostream &, bool enable) {
#undef INTERPOSE_HOOKS_FAILED
static command_result overlay_cmd(color_ostream &out, std::vector <std::string> & parameters) {
if (DBG_NAME(control).isEnabled(DebugCategory::LDEBUG)) {
DEBUG(control).print("interpreting command with %zu parameters:\n",
parameters.size());
for (auto &param : parameters) {
DEBUG(control).print(" %s\n", param.c_str());
}
}
bool show_help = false;
call_overlay_lua(&out, "overlay_command", 1, 1, [&](lua_State *L) {
Lua::PushVector(L, parameters);
}, [&](lua_State *L) {
show_help = !lua_toboolean(L, -1);
});
return CR_OK;
return show_help ? CR_WRONG_USAGE : CR_OK;
}
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
@ -318,9 +365,10 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
PluginCommand(
"overlay",
"Manage onscreen widgets.",
overlay_cmd));
overlay_cmd,
Gui::anywhere_hotkey));
return plugin_enable(out, true);
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
@ -328,5 +376,11 @@ DFhackCExport command_result plugin_shutdown(color_ostream &out) {
}
DFhackCExport command_result plugin_onupdate (color_ostream &out) {
df::coord2d newScreenSize = Screen::getWindowSize();
if (newScreenSize != screenSize) {
call_overlay_lua(&out, "reposition_widgets");
screenSize = newScreenSize;
}
call_overlay_lua(&out, "update_hotspot_widgets");
return CR_OK;
}

@ -1,325 +0,0 @@
#include <string>
#include <vector>
#include <algorithm>
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <VTableInterpose.h>
// DF data structure definition headers
#include "DataDefs.h"
#include "LuaTools.h"
#include "MiscUtils.h"
#include "Types.h"
#include "df/viewscreen_dwarfmodest.h"
#include "df/world.h"
#include "df/building_constructionst.h"
#include "df/building.h"
#include "df/job.h"
#include "df/job_item.h"
#include "modules/Gui.h"
#include "modules/Screen.h"
#include "modules/Buildings.h"
#include "modules/Maps.h"
#include "modules/World.h"
#include "uicommon.h"
using std::map;
using std::string;
using std::vector;
using namespace DFHack;
using namespace df::enums;
DFHACK_PLUGIN("resume");
#define PLUGIN_VERSION 0.2
REQUIRE_GLOBAL(gps);
REQUIRE_GLOBAL(process_jobs);
REQUIRE_GLOBAL(ui);
REQUIRE_GLOBAL(world);
#ifndef HAVE_NULLPTR
#define nullptr 0L
#endif
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
df::job *get_suspended_job(df::building *bld)
{
if (bld->getBuildStage() != 0)
return nullptr;
if (bld->jobs.size() == 0)
return nullptr;
auto job = bld->jobs[0];
if (job->flags.bits.suspend)
return job;
return nullptr;
}
struct SuspendedBuilding
{
df::building *bld;
df::coord pos;
bool was_resumed;
bool is_planned;
SuspendedBuilding(df::building *bld_) : bld(bld_), was_resumed(false), is_planned(false)
{
pos = df::coord(bld->centerx, bld->centery, bld->z);
}
bool isValid()
{
return bld && Buildings::findAtTile(pos) == bld && get_suspended_job(bld);
}
};
static bool is_planned_building(df::building *bld)
{
auto L = Lua::Core::State;
color_ostream_proxy out(Core::getInstance().getConsole());
Lua::StackUnwinder top(L);
if (!lua_checkstack(L, 2) ||
!Lua::PushModulePublic(
out, L, "plugins.buildingplan", "isPlannedBuilding"))
return false;
Lua::Push(L, bld);
if (!Lua::SafeCall(out, L, 1, 1))
return false;
return lua_toboolean(L, -1);
}
DFHACK_PLUGIN_IS_ENABLED(enabled);
static bool buildings_scanned = false;
static vector<SuspendedBuilding> suspended_buildings, resumed_buildings;
void scan_for_suspended_buildings()
{
if (buildings_scanned)
return;
for (auto b = world->buildings.all.begin(); b != world->buildings.all.end(); b++)
{
auto bld = *b;
auto job = get_suspended_job(bld);
if (job)
{
SuspendedBuilding sb(bld);
sb.is_planned = is_planned_building(bld);
auto it = resumed_buildings.begin();
for (; it != resumed_buildings.end(); ++it)
if (it->bld == bld) break;
sb.was_resumed = it != resumed_buildings.end();
suspended_buildings.push_back(sb);
}
}
buildings_scanned = true;
}
void show_suspended_buildings()
{
int32_t vx, vy, vz;
if (!Gui::getViewCoords(vx, vy, vz))
return;
auto dims = Gui::getDwarfmodeViewDims();
int left_margin = vx + dims.map_x2;
int bottom_margin = vy + dims.map_y2 - 1;
for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end();)
{
if (!sb->isValid())
{
sb = suspended_buildings.erase(sb);
continue;
}
if (sb->bld->z == vz && sb->bld->centerx >= vx && sb->bld->centerx <= left_margin &&
sb->bld->centery >= vy && sb->bld->centery <= bottom_margin)
{
int x = sb->bld->centerx - vx + 1;
int y = sb->bld->centery - vy + 1;
auto color = COLOR_YELLOW;
if (sb->is_planned)
color = COLOR_GREEN;
else if (sb->was_resumed)
color = COLOR_RED;
OutputString(color, x, y, "X", false, 0, 0, true /* map */);
}
sb++;
}
}
void clear_scanned()
{
buildings_scanned = false;
suspended_buildings.clear();
}
void resume_suspended_buildings(color_ostream &out)
{
out << "Resuming all buildings." << endl;
for (auto isb = resumed_buildings.begin(); isb != resumed_buildings.end();)
{
if (isb->isValid())
{
isb++;
continue;
}
isb = resumed_buildings.erase(isb);
}
scan_for_suspended_buildings();
for (auto sb = suspended_buildings.begin(); sb != suspended_buildings.end(); sb++)
{
if (sb->is_planned)
continue;
resumed_buildings.push_back(*sb);
sb->bld->jobs[0]->flags.bits.suspend = false;
}
clear_scanned();
out << resumed_buildings.size() << " buildings resumed" << endl;
}
//START Viewscreen Hook
struct resume_hook : public df::viewscreen_dwarfmodest
{
//START UI Methods
typedef df::viewscreen_dwarfmodest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
if (enabled && DFHack::World::ReadPauseState() && ui->main.mode == ui_sidebar_mode::Default)
{
if (*process_jobs)
{
// something just created some buildings. rescan.
clear_scanned();
}
scan_for_suspended_buildings();
show_suspended_buildings();
}
else
{
clear_scanned();
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(resume_hook, render);
DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable)
{
if (!gps)
return CR_FAILURE;
if (enabled != enable)
{
clear_scanned();
if (!INTERPOSE_HOOK(resume_hook, render).apply(enable))
return CR_FAILURE;
enabled = enable;
}
return CR_OK;
}
static command_result resume_cmd(color_ostream &out, vector <string> & parameters)
{
bool show_help = false;
if (parameters.empty())
{
show_help = true;
}
else
{
auto cmd = parameters[0][0];
if (cmd == 'v')
{
out << "Resume" << endl << "Version: " << PLUGIN_VERSION << endl;
}
else if (cmd == 's')
{
plugin_enable(out, true);
out << "Overlay enabled" << endl;
}
else if (cmd == 'h')
{
plugin_enable(out, false);
out << "Overlay disabled" << endl;
}
else if (cmd == 'a')
{
resume_suspended_buildings(out);
}
else
{
show_help = true;
}
}
if (show_help)
return CR_WRONG_USAGE;
return CR_OK;
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{
commands.push_back(
PluginCommand(
"resume",
"Mark suspended constructions on the map and easily resume them.",
resume_cmd));
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
suspended_buildings.clear();
resumed_buildings.clear();
break;
default:
break;
}
return CR_OK;
}

@ -1,16 +1,19 @@
//
// Created by josh on 7/28/21.
// Last updated: 11//10/22
//
#include "pause.h"
#include "Core.h"
#include <modules/Gui.h>
#include <Console.h>
#include <Debug.h>
#include <Core.h>
#include <Export.h>
#include <PluginManager.h>
#include <modules/World.h>
#include <modules/EventManager.h>
#include <modules/World.h>
#include <modules/Maps.h>
#include <modules/Gui.h>
#include <modules/Job.h>
#include <modules/Units.h>
#include <df/job.h>
@ -19,11 +22,17 @@
#include <df/global_objects.h>
#include <df/world.h>
#include <df/viewscreen.h>
#include <df/creature_raw.h>
#include <map>
#include <set>
#include <array>
#include <random>
#include <cinttypes>
#include <functional>
// Debugging
namespace DFHack {
DBG_DECLARE(spectate, plugin, DebugCategory::LINFO);
}
DFHACK_PLUGIN("spectate");
DFHACK_PLUGIN_IS_ENABLED(enabled);
@ -36,51 +45,336 @@ using namespace DFHack;
using namespace Pausing;
using namespace df::enums;
void onTick(color_ostream& out, void* tick);
void onJobStart(color_ostream &out, void* job);
void onJobCompletion(color_ostream &out, void* job);
struct Configuration {
bool unpause = false;
bool disengage = false;
bool animals = false;
bool hostiles = true;
bool visitors = false;
int32_t tick_threshold = 1000;
} config;
uint64_t tick_threshold = 1000;
bool focus_jobs_enabled = false;
bool disengage_enabled = false;
bool unpause_enabled = false;
Pausing::AnnouncementLock* pause_lock = nullptr;
bool lock_collision = false;
bool announcements_disabled = false;
bool following_dwarf = false;
df::unit* our_dorf = nullptr;
df::job* job_watched = nullptr;
int32_t timestamp = -1;
std::set<int32_t> job_tracker;
std::map<uint16_t,int16_t> freq;
std::default_random_engine RNG;
void enable_auto_unpause(color_ostream &out, bool state);
#define base 0.99
static const std::string CONFIG_KEY = std::string(plugin_name) + "/config";
enum ConfigData {
UNPAUSE,
DISENGAGE,
JOB_FOCUS,
TICK_THRESHOLD
TICK_THRESHOLD,
ANIMALS,
HOSTILES,
VISITORS
};
static PersistentDataItem config;
inline void saveConfig() {
if (config.isValid()) {
config.ival(UNPAUSE) = unpause_enabled;
config.ival(DISENGAGE) = disengage_enabled;
config.ival(JOB_FOCUS) = focus_jobs_enabled;
config.ival(TICK_THRESHOLD) = tick_threshold;
}
}
static PersistentDataItem pconfig;
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable);
command_result spectate (color_ostream &out, std::vector <std::string> & parameters);
#define COORDARGS(id) id.x, id.y, id.z
namespace SP {
bool following_dwarf = false;
df::unit* our_dorf = nullptr;
int32_t timestamp = -1;
std::default_random_engine RNG;
void DebugUnitVector(std::vector<df::unit*> units) {
if (debug_plugin.isEnabled(DFHack::DebugCategory::LDEBUG)) {
for (auto unit: units) {
DEBUG(plugin).print("[id: %d]\n animal: %d\n hostile: %d\n visiting: %d\n",
unit->id,
Units::isAnimal(unit),
Units::isDanger(unit),
Units::isVisiting(unit));
}
}
}
void PrintStatus(color_ostream &out) {
out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED.");
out.print(" FEATURES:\n");
out.print(" %-20s\t%s\n", "auto-unpause: ", config.unpause ? "on." : "off.");
out.print(" %-20s\t%s\n", "auto-disengage: ", config.disengage ? "on." : "off.");
out.print(" %-20s\t%s\n", "animals: ", config.animals ? "on." : "off.");
out.print(" %-20s\t%s\n", "hostiles: ", config.hostiles ? "on." : "off.");
out.print(" %-20s\t%s\n", "visiting: ", config.visitors ? "on." : "off.");
out.print(" SETTINGS:\n");
out.print(" %-20s\t%" PRIi32 "\n", "tick-threshold: ", config.tick_threshold);
if (following_dwarf)
out.print(" %-21s\t%s[id: %d]\n","FOLLOWING:", our_dorf ? our_dorf->name.first_name.c_str() : "nullptr", df::global::ui->follow_unit);
}
void SetUnpauseState(bool state) {
// we don't need to do any of this yet if the plugin isn't enabled
if (enabled) {
// todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings]
// lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete
// The onupdate function above ensure the procedure properly completes, thus we only care about
// state reversal here ergo `enabled != state`
if (lock_collision && config.unpause != state) {
WARN(plugin).print("Spectate auto-unpause: Not enabled yet, there was a lock collision. When the other lock holder releases, auto-unpause will engage on its own.\n");
// if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings,
// therefore nothing to revert and the lock won't even be engaged (nothing to unlock)
lock_collision = false;
config.unpause = state;
if (config.unpause) {
// a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock
pause_lock->lock();
}
return;
}
// update the announcement settings if we can
if (state) {
if (World::SaveAnnouncementSettings()) {
World::DisableAnnouncementPausing();
announcements_disabled = true;
pause_lock->lock();
} else {
WARN(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n");
lock_collision = true;
}
} else {
pause_lock->unlock();
if (announcements_disabled) {
if (!World::RestoreAnnouncementSettings()) {
// this in theory shouldn't happen, if others use the lock like we do in spectate
WARN(plugin).print("Spectate auto-unpause: Could not fully disable. There was a lock collision, when the other lock holder releases, auto-unpause will disengage on its own.\n");
lock_collision = true;
} else {
announcements_disabled = false;
}
}
}
if (lock_collision) {
ERR(plugin).print("Spectate auto-unpause: Could not fully enable. There was a lock collision, when the other lock holder releases, auto-unpause will engage on its own.\n");
WARN(plugin).print(
" auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n"
" The action you were attempting will complete when the following lock or locks lift.\n");
pause_lock->reportLocks(Core::getInstance().getConsole());
}
}
config.unpause = state;
}
void SaveSettings() {
if (pconfig.isValid()) {
pconfig.ival(UNPAUSE) = config.unpause;
pconfig.ival(DISENGAGE) = config.disengage;
pconfig.ival(TICK_THRESHOLD) = config.tick_threshold;
pconfig.ival(ANIMALS) = config.animals;
pconfig.ival(HOSTILES) = config.hostiles;
pconfig.ival(VISITORS) = config.visitors;
}
}
void LoadSettings() {
pconfig = World::GetPersistentData(CONFIG_KEY);
if (!pconfig.isValid()) {
pconfig = World::AddPersistentData(CONFIG_KEY);
SaveSettings();
} else {
config.unpause = pconfig.ival(UNPAUSE);
config.disengage = pconfig.ival(DISENGAGE);
config.tick_threshold = pconfig.ival(TICK_THRESHOLD);
config.animals = pconfig.ival(ANIMALS);
config.hostiles = pconfig.ival(HOSTILES);
config.visitors = pconfig.ival(VISITORS);
pause_lock->unlock();
SetUnpauseState(config.unpause);
}
}
bool FollowADwarf() {
if (enabled && !World::ReadPauseState()) {
df::coord viewMin = Gui::getViewportPos();
df::coord viewMax{viewMin};
const auto &dims = Gui::getDwarfmodeViewDims().map().second;
viewMax.x += dims.x - 1;
viewMax.y += dims.y - 1;
viewMax.z = viewMin.z;
std::vector<df::unit*> units;
static auto add_if = [&](std::function<bool(df::unit*)> check) {
for (auto unit : world->units.active) {
if (check(unit)) {
units.push_back(unit);
}
}
};
static auto valid = [](df::unit* unit) {
if (Units::isAnimal(unit)) {
return config.animals;
}
if (Units::isVisiting(unit)) {
return config.visitors;
}
if (Units::isDanger(unit)) {
return config.hostiles;
}
return true;
};
static auto calc_extra_weight = [](size_t idx, double r1, double r2) {
switch(idx) {
case 0:
return r2;
case 1:
return (r2-r1)/1.3;
case 2:
return (r2-r1)/2;
default:
return 0.0;
}
};
/// Collecting our choice pool
///////////////////////////////
std::array<int32_t, 10> ranges{};
std::array<bool, 5> range_exists{};
static auto build_range = [&](size_t idx){
size_t first = idx * 2;
size_t second = idx * 2 + 1;
size_t previous = first - 1;
// first we get the end of the range
ranges[second] = units.size() - 1;
// then we calculate whether the range exists, and set the first index appropriately
if (idx == 0) {
range_exists[idx] = ranges[second] >= 0;
ranges[first] = 0;
} else {
range_exists[idx] = ranges[second] > ranges[previous];
ranges[first] = ranges[previous] + (range_exists[idx] ? 1 : 0);
}
};
/// RANGE 0 (in view + working)
// grab valid working units
add_if([&](df::unit* unit) {
return valid(unit) &&
Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax)) &&
Units::isCitizen(unit, true) &&
unit->job.current_job;
});
build_range(0);
/// RANGE 1 (in view)
add_if([&](df::unit* unit) {
return valid(unit) && Units::isUnitInBox(unit, COORDARGS(viewMin), COORDARGS(viewMax));
});
build_range(1);
/// RANGE 2 (working citizens)
add_if([](df::unit* unit) {
return valid(unit) && Units::isCitizen(unit, true) && unit->job.current_job;
});
build_range(2);
/// RANGE 3 (citizens)
add_if([](df::unit* unit) {
return valid(unit) && Units::isCitizen(unit, true);
});
build_range(3);
/// RANGE 4 (any valid)
add_if(valid);
build_range(4);
// selecting from our choice pool
if (!units.empty()) {
std::array<double, 5> bw{23,17,13,7,1}; // probability weights for each range
std::vector<double> i;
std::vector<double> w;
bool at_least_one = false;
// in one word, elegance
for(size_t idx = 0; idx < range_exists.size(); ++idx) {
if (range_exists[idx]) {
at_least_one = true;
const auto &r1 = ranges[idx*2];
const auto &r2 = ranges[idx*2+1];
double extra = calc_extra_weight(idx, r1, r2);
i.push_back(r1);
w.push_back(bw[idx] + extra);
if (r1 != r2) {
i.push_back(r2);
w.push_back(bw[idx] + extra);
}
}
}
if (!at_least_one) {
return false;
}
DebugUnitVector(units);
std::piecewise_linear_distribution<> follow_any(i.begin(), i.end(), w.begin());
// if you're looking at a warning about a local address escaping, it means the unit* from units (which aren't local)
size_t idx = follow_any(RNG);
our_dorf = units[idx];
df::global::ui->follow_unit = our_dorf->id;
timestamp = df::global::world->frame_counter;
return true;
} else {
WARN(plugin).print("units vector is empty!\n");
}
}
return false;
}
void onUpdate(color_ostream &out) {
if (!World::isFortressMode() || !Maps::IsValid())
return;
// keeps announcement pause settings locked
World::Update(); // from pause.h
// Plugin Management
if (lock_collision) {
if (config.unpause) {
// player asked for auto-unpause enabled
World::SaveAnnouncementSettings();
if (World::DisableAnnouncementPausing()) {
// now that we've got what we want, we can lock it down
lock_collision = false;
}
} else {
if (World::RestoreAnnouncementSettings()) {
lock_collision = false;
}
}
}
int failsafe = 0;
while (config.unpause && !world->status.popups.empty() && ++failsafe <= 10) {
// dismiss announcement popup(s)
Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT);
if (World::ReadPauseState()) {
// WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is
World::SetPauseState(false);
}
}
if (failsafe >= 10) {
out.printerr("spectate encountered a problem dismissing a popup!\n");
}
// plugin logic
static int32_t last_tick = -1;
int32_t tick = world->frame_counter;
if (!World::ReadPauseState() && tick - last_tick >= 1) {
last_tick = tick;
// validate follow state
if (!following_dwarf || !our_dorf || df::global::ui->follow_unit < 0 || tick - timestamp >= config.tick_threshold) {
// we're not following anyone
following_dwarf = false;
if (!config.disengage) {
// try to
following_dwarf = FollowADwarf();
} else if (!World::ReadPauseState()) {
plugin_enable(out, false);
}
}
}
}
};
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands) {
commands.push_back(PluginCommand("spectate",
@ -97,45 +391,24 @@ DFhackCExport command_result plugin_shutdown (color_ostream &out) {
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
config = World::GetPersistentData(CONFIG_KEY);
if (!config.isValid()) {
config = World::AddPersistentData(CONFIG_KEY);
saveConfig();
} else {
unpause_enabled = config.ival(UNPAUSE);
disengage_enabled = config.ival(DISENGAGE);
focus_jobs_enabled = config.ival(JOB_FOCUS);
tick_threshold = config.ival(TICK_THRESHOLD);
pause_lock->unlock();
enable_auto_unpause(out, unpause_enabled);
}
SP::LoadSettings();
SP::following_dwarf = SP::FollowADwarf();
SP::PrintStatus(out);
return DFHack::CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
namespace EM = EventManager;
if (enable && !enabled) {
out.print("Spectate mode enabled!\n");
using namespace EM::EventType;
EM::EventHandler ticking(onTick, 15);
EM::EventHandler start(onJobStart, 0);
EM::EventHandler complete(onJobCompletion, 0);
EM::registerListener(EventType::TICK, ticking, plugin_self);
EM::registerListener(EventType::JOB_STARTED, start, plugin_self);
EM::registerListener(EventType::JOB_COMPLETED, complete, plugin_self);
enabled = true; // enable_auto_unpause won't do anything without this set now
enable_auto_unpause(out, unpause_enabled);
SP::SetUnpauseState(config.unpause);
} else if (!enable && enabled) {
// warp 8, engage!
out.print("Spectate mode disabled!\n");
EM::unregisterAll(plugin_self);
// we need to retain whether auto-unpause is enabled, but we also need to disable its effect
bool temp = unpause_enabled;
enable_auto_unpause(out, false);
unpause_enabled = temp;
job_tracker.clear();
freq.clear();
bool temp = config.unpause;
SP::SetUnpauseState(false);
config.unpause = temp;
}
enabled = enable;
return DFHack::CR_OK;
@ -147,9 +420,8 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
case SC_MAP_UNLOADED:
case SC_BEGIN_UNLOAD:
case SC_WORLD_UNLOADED:
our_dorf = nullptr;
job_watched = nullptr;
following_dwarf = false;
SP::our_dorf = nullptr;
SP::following_dwarf = false;
default:
break;
}
@ -158,93 +430,10 @@ DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_chan
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
// keeps announcement pause settings locked
World::Update(); // from pause.h
if (lock_collision) {
if (unpause_enabled) {
// player asked for auto-unpause enabled
World::SaveAnnouncementSettings();
if (World::DisableAnnouncementPausing()) {
// now that we've got what we want, we can lock it down
lock_collision = false;
}
} else {
if (World::RestoreAnnouncementSettings()) {
lock_collision = false;
}
}
}
int failsafe = 0;
while (unpause_enabled && !world->status.popups.empty() && ++failsafe <= 10) {
// dismiss announcement popup(s)
Gui::getCurViewscreen(true)->feed_key(interface_key::CLOSE_MEGA_ANNOUNCEMENT);
if (World::ReadPauseState()) {
// WARNING: This has a possibility of conflicting with `reveal hell` - if Hermes himself runs `reveal hell` on precisely the right moment that is
World::SetPauseState(false);
}
}
if (failsafe >= 10) {
out.printerr("spectate encountered a problem dismissing a popup!\n");
}
if (disengage_enabled && !World::ReadPauseState()) {
if (our_dorf && our_dorf->id != df::global::ui->follow_unit) {
plugin_enable(out, false);
}
}
SP::onUpdate(out);
return DFHack::CR_OK;
}
void enable_auto_unpause(color_ostream &out, bool state) {
// we don't need to do any of this yet if the plugin isn't enabled
if (enabled) {
// todo: R.E. UNDEAD_ATTACK event [still pausing regardless of announcement settings]
// lock_collision == true means: enable_auto_unpause() was already invoked and didn't complete
// The onupdate function above ensure the procedure properly completes, thus we only care about
// state reversal here ergo `enabled != state`
if (lock_collision && unpause_enabled != state) {
out.print("handling collision\n");
// if unpaused_enabled is true, then a lock collision means: we couldn't save/disable the pause settings,
// therefore nothing to revert and the lock won't even be engaged (nothing to unlock)
lock_collision = false;
unpause_enabled = state;
if (unpause_enabled) {
// a collision means we couldn't restore the pause settings, therefore we only need re-engage the lock
pause_lock->lock();
}
return;
}
// update the announcement settings if we can
if (state) {
if (World::SaveAnnouncementSettings()) {
World::DisableAnnouncementPausing();
announcements_disabled = true;
pause_lock->lock();
} else {
out.printerr("lock collision enabling auto-unpause\n");
lock_collision = true;
}
} else {
pause_lock->unlock();
if (announcements_disabled) {
if (!World::RestoreAnnouncementSettings()) {
// this in theory shouldn't happen, if others use the lock like we do in spectate
out.printerr("lock collision disabling auto-unpause\n");
lock_collision = true;
} else {
announcements_disabled = false;
}
}
}
if (lock_collision) {
out.printerr(
"auto-unpause: must wait for another Pausing::AnnouncementLock to be lifted.\n"
"The action you were attempting will complete when the following lock or locks lift.\n");
pause_lock->reportLocks(out);
}
}
unpause_enabled = state;
}
command_result spectate (color_ostream &out, std::vector <std::string> & parameters) {
if (!parameters.empty()) {
if (parameters.size() >= 2 && parameters.size() <= 3) {
@ -260,14 +449,18 @@ command_result spectate (color_ostream &out, std::vector <std::string> & paramet
return DFHack::CR_WRONG_USAGE;
}
if(parameters[1] == "auto-unpause"){
enable_auto_unpause(out, state);
SP::SetUnpauseState(state);
} else if (parameters[1] == "auto-disengage") {
disengage_enabled = state;
} else if (parameters[1] == "focus-jobs") {
focus_jobs_enabled = state;
config.disengage = state;
} else if (parameters[1] == "animals") {
config.animals = state;
} else if (parameters[1] == "hostiles") {
config.hostiles = state;
} else if (parameters[1] == "visiting") {
config.visitors = state;
} else if (parameters[1] == "tick-threshold" && set && parameters.size() == 3) {
try {
tick_threshold = std::abs(std::stol(parameters[2]));
config.tick_threshold = std::abs(std::stol(parameters[2]));
} catch (const std::exception &e) {
out.printerr("%s\n", e.what());
}
@ -276,91 +469,8 @@ command_result spectate (color_ostream &out, std::vector <std::string> & paramet
}
}
} else {
out.print("Spectate is %s\n", enabled ? "ENABLED." : "DISABLED.");
out.print("tick-threshold: %" PRIu64 "\n", tick_threshold);
out.print("focus-jobs: %s\n", focus_jobs_enabled ? "on." : "off.");
out.print("auto-unpause: %s\n", unpause_enabled ? "on." : "off.");
out.print("auto-disengage: %s\n", disengage_enabled ? "on." : "off.");
SP::PrintStatus(out);
}
saveConfig();
SP::SaveSettings();
return DFHack::CR_OK;
}
// every tick check whether to decide to follow a dwarf
void onTick(color_ostream& out, void* ptr) {
int32_t tick = df::global::world->frame_counter;
if (our_dorf) {
if (!Units::isAlive(our_dorf)) {
following_dwarf = false;
df::global::ui->follow_unit = -1;
}
}
if (!following_dwarf || (focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) {
std::vector<df::unit*> dwarves;
for (auto unit: df::global::world->units.active) {
if (!Units::isCitizen(unit)) {
continue;
}
dwarves.push_back(unit);
}
std::uniform_int_distribution<uint64_t> follow_any(0, dwarves.size() - 1);
// if you're looking at a warning about a local address escaping, it means the unit* from dwarves (which aren't local)
our_dorf = dwarves[follow_any(RNG)];
df::global::ui->follow_unit = our_dorf->id;
job_watched = our_dorf->job.current_job;
following_dwarf = true;
if (!job_watched) {
timestamp = tick;
}
}
}
// every new worked job needs to be considered
void onJobStart(color_ostream& out, void* job_ptr) {
// todo: detect mood jobs
int32_t tick = df::global::world->frame_counter;
auto job = (df::job*) job_ptr;
// don't forget about it
int zcount = ++freq[job->pos.z];
job_tracker.emplace(job->id);
// if we're not doing anything~ then let's pick something
if ((focus_jobs_enabled && !job_watched) || (tick - timestamp) > (int32_t) tick_threshold) {
following_dwarf = true;
// todo: allow the user to configure b, and also revise the math
const double b = base;
double p = b * ((double) zcount / job_tracker.size());
std::bernoulli_distribution follow_job(p);
if (!job->flags.bits.special && follow_job(RNG)) {
job_watched = job;
if (df::unit* unit = Job::getWorker(job)) {
our_dorf = unit;
df::global::ui->follow_unit = unit->id;
}
} else {
timestamp = tick;
std::vector<df::unit*> nonworkers;
for (auto unit: df::global::world->units.active) {
if (!Units::isCitizen(unit) || unit->job.current_job) {
continue;
}
nonworkers.push_back(unit);
}
std::uniform_int_distribution<> follow_drunk(0, nonworkers.size() - 1);
df::global::ui->follow_unit = nonworkers[follow_drunk(RNG)]->id;
}
}
}
// every job completed can be forgotten about
void onJobCompletion(color_ostream &out, void* job_ptr) {
auto job = (df::job*) job_ptr;
// forget about it
freq[job->pos.z]--;
freq[job->pos.z] = freq[job->pos.z] < 0 ? 0 : freq[job->pos.z];
// the job doesn't exist, so we definitely need to get rid of that
job_tracker.erase(job->id);
// the event manager clones jobs and returns those clones for completed jobs. So the pointers won't match without a refactor of EM passing clones to both events
if (job_watched && job_watched->id == job->id) {
job_watched = nullptr;
}
}

@ -0,0 +1,5 @@
User-agent: *
Allow: /en/stable/
Sitemap: https://docs.dfhack.org/sitemap.xml

@ -1 +1 @@
Subproject commit 020f2466bc4462e59c1c16c036881907cad9718e
Subproject commit 727e4921c00e260d7c8d1112daf77115ce3960ee

@ -5,7 +5,7 @@ function test.hotkeylabel_click()
local l = widgets.HotkeyLabel{key='SELECT', on_activate=func}
mock.patch(l, 'getMousePos', mock.func(0), function()
l:onInput{_MOUSE_L=true}
l:onInput{_MOUSE_L_DOWN=true}
expect.eq(1, func.call_count)
end)
end
@ -31,7 +31,7 @@ function test.togglehotkeylabel_click()
local l = widgets.ToggleHotkeyLabel{}
expect.true_(l:getOptionValue())
mock.patch(l, 'getMousePos', mock.func(0), function()
l:onInput{_MOUSE_L=true}
l:onInput{_MOUSE_L_DOWN=true}
expect.false_(l:getOptionValue())
end)
end