diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83f3490da..f8631bc4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitmodules b/.gitmodules index 9c5ac2d51..c349de288 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "depends/luacov"] path = depends/luacov url = ../../DFHack/luacov.git +[submodule "depends/googletest"] + path = depends/googletest + url = ../../google/googletest.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 6808c3785..f6a3bec8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/ci/lint.py b/ci/lint.py index b2fb8e647..f2c01cd9c 100755 --- a/ci/lint.py +++ b/ci/lint.py @@ -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 ') diff --git a/conf.py b/conf.py index 661b0daea..60c3be579 100644 --- a/conf.py +++ b/conf.py @@ -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 = { '**': [ diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 412ffa347..8cf6bb2ca 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -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 ) diff --git a/data/blueprints/README.md b/data/blueprints/README.md index 2facb3f4e..5d0e41a24 100644 --- a/data/blueprints/README.md +++ b/data/blueprints/README.md @@ -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) diff --git a/data/blueprints/library/dreamfort.csv b/data/blueprints/library/dreamfort.csv index e26650943..c5566920b 100644 --- a/data/blueprints/library/dreamfort.csv +++ b/data/blueprints/library/dreamfort.csv @@ -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""}",`,`,`,`,`,,`,,`,,`,,` ,`,`,`,`,`,`,`,`,`,`,`,,,,,`,,`,,,,,`,,`,,`,,` ,`,`,`,`,`,`,`,`,`,`,`,,,,`,`,`,`,`,,,,`,`,`,`,`,`,`,`,` ,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,`,` diff --git a/data/init/dfhack.keybindings.init b/data/init/dfhack.keybindings.init index fd02635e2..f8a35f874 100644 --- a/data/init/dfhack.keybindings.init +++ b/data/init/dfhack.keybindings.init @@ -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" diff --git a/data/init/dfhack.tools.init b/data/init/dfhack.tools.init index aab17ebbe..70da533b3 100644 --- a/data/init/dfhack.tools.init +++ b/data/init/dfhack.tools.init @@ -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 \ diff --git a/data/orders/basic.json b/data/orders/basic.json index 4c785e7ab..b3fae8dcd 100644 --- a/data/orders/basic.json +++ b/data/orders/basic.json @@ -1004,7 +1004,8 @@ "value" : 2 } ], - "job" : "MakeCrafts", + "item_subtype" : "ITEM_PANTS_LEGGINGS", + "job" : "MakePants", "material_category" : [ "shell" diff --git a/depends/CMakeLists.txt b/depends/CMakeLists.txt index 405a9555e..f5e77ad8a 100644 --- a/depends/CMakeLists.txt +++ b/depends/CMakeLists.txt @@ -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) diff --git a/depends/googletest b/depends/googletest new file mode 160000 index 000000000..2fe3bd994 --- /dev/null +++ b/depends/googletest @@ -0,0 +1 @@ +Subproject commit 2fe3bd994b3189899d93f1d5a881e725e046fdc2 diff --git a/dfhack-config/dwarfmonitor.json b/dfhack-config/dwarfmonitor.json index 007dad020..9bd3b1f76 100644 --- a/dfhack-config/dwarfmonitor.json +++ b/dfhack-config/dwarfmonitor.json @@ -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" } diff --git a/dfhack-config/overlay.json b/dfhack-config/overlay.json new file mode 100644 index 000000000..8b90fc4a4 --- /dev/null +++ b/dfhack-config/overlay.json @@ -0,0 +1,17 @@ +{ + "dwarfmonitor.date": { + "enabled": true + }, + "dwarfmonitor.misery": { + "enabled": true + }, + "dwarfmonitor.weather": { + "enabled": true + }, + "hotkeys.menu": { + "enabled": true + }, + "unsuspend.overlay": { + "enabled": true + } +} diff --git a/docs/Lua API.rst b/docs/Lua API.rst index 5b6a30d93..8ba7429fa 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -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 `_) + 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 ------------ diff --git a/docs/Removed.rst b/docs/Removed.rst index f9bf1c62e..a5fa29a1d 100644 --- a/docs/Removed.rst +++ b/docs/Removed.rst @@ -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 diff --git a/docs/builtins/keybinding.rst b/docs/builtins/keybinding.rst index 6d1565509..c9665a048 100644 --- a/docs/builtins/keybinding.rst +++ b/docs/builtins/keybinding.rst @@ -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 [...]`` Remove bindings for the specified keys. -``keybinding add "cmdline" ["cmdline"...]`` +``keybinding add "" ["" ...]`` Add bindings for the specified key. -``keybinding set "cmdline" ["cmdline"...]`` +``keybinding set "" ["" ...]`` Clear, and then add bindings for the specified key. The ```` 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 `, but only when on the main + map in the default mode (that is, no special mode, like cursor look, is + enabled). diff --git a/docs/changelog.txt b/docs/changelog.txt index 9eb067d50..75bec1368 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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 diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 96c5688d2..a3d9a0482 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -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 diff --git a/docs/guides/overlay-dev-guide.rst b/docs/guides/overlay-dev-guide.rst new file mode 100644 index 000000000..8c84f27c7 --- /dev/null +++ b/docs/guides/overlay-dev-guide.rst @@ -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 `). 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 ` 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 diff --git a/docs/images/hotkeys.png b/docs/images/hotkeys.png deleted file mode 100644 index 524ce9a52..000000000 Binary files a/docs/images/hotkeys.png and /dev/null differ diff --git a/docs/plugins/channel-safely.rst b/docs/plugins/channel-safely.rst new file mode 100644 index 000000000..1b72c93df --- /dev/null +++ b/docs/plugins/channel-safely.rst @@ -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 + channel-safely enable|disable + channel-safely + +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 `. +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) diff --git a/docs/plugins/dwarfmonitor.rst b/docs/plugins/dwarfmonitor.rst index 79472ae58..75e261171 100644 --- a/docs/plugins/dwarfmonitor.rst +++ b/docs/plugins/dwarfmonitor.rst @@ -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 `` - 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 `` - 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 diff --git a/docs/plugins/hotkeys.rst b/docs/plugins/hotkeys.rst index 6780efca1..6b2ef3f0c 100644 --- a/docs/plugins/hotkeys.rst +++ b/docs/plugins/hotkeys.rst @@ -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. diff --git a/docs/plugins/orders.rst b/docs/plugins/orders.rst index 298800c96..bc15fd175 100644 --- a/docs/plugins/orders.rst +++ b/docs/plugins/orders.rst @@ -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 `__. + :source:`library/furnace ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/plugins/overlay.rst b/docs/plugins/overlay.rst index 9416fba31..4d2a1be7f 100644 --- a/docs/plugins/overlay.rst +++ b/docs/plugins/overlay.rst @@ -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| [ ...]`` + Enable/disable all or specified widgets. Widgets can be specified by either + their name or their number, as returned by ``overlay list``. +``overlay list []`` + Show a list of all the widgets that are registered with the overlay + framework, optionally filtered by the given filter string. +``overlay position [default| ]`` + 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 `` + 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 diff --git a/docs/plugins/resume.rst b/docs/plugins/resume.rst deleted file mode 100644 index af3fe161d..000000000 --- a/docs/plugins/resume.rst +++ /dev/null @@ -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 diff --git a/docs/plugins/spectate.rst b/docs/plugins/spectate.rst index 1ca3efeb8..f54d68142 100644 --- a/docs/plugins/spectate.rst +++ b/docs/plugins/spectate.rst @@ -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) diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index a62eb2fe0..5e7488027 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -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) diff --git a/library/Core.cpp b/library/Core.cpp index 083a6223f..836772c04 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2000,15 +2000,21 @@ void getFilesWithPrefixAndSuffix(const std::string& folder, const std::string& p } size_t loadScriptFiles(Core* core, color_ostream& out, const vector& prefix, const std::string& folder) { - vector scriptFiles; + static const string suffix = ".init"; + vector 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); diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 9f713244d..36d132124 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -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 #include @@ -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(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(state, 7); + case 6: + unit1 = Lua::CheckDFObject(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); diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 3cd6a1f7d..5b080ce43 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -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(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(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) { diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index e4245f09a..9e1901f03 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -24,6 +24,7 @@ distribution. #pragma once +#include #include #include #include @@ -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 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. */ diff --git a/library/include/modules/Gui.h b/library/include/modules/Gui.h index a0ae27889..d0b6f4d15 100644 --- a/library/include/modules/Gui.h +++ b/library/include/modules/Gui.h @@ -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(); diff --git a/library/include/modules/Units.h b/library/include/modules/Units.h index 4dfa9f937..be630b802 100644 --- a/library/include/modules/Units.h +++ b/library/include/modules/Units.h @@ -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); diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 2fab12f70..e8093345f 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -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) diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index 7f2097f43..e548b4246 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -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 *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 &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 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 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; } diff --git a/library/modules/Units.cpp b/library/modules/Units.cpp index 32b9f28e5..0395044ce 100644 --- a/library/modules/Units.cpp +++ b/library/modules/Units.cpp @@ -88,239 +88,233 @@ using df::global::ui; using df::global::gamemode; using df::global::gametype; -int32_t Units::getNumUnits() -{ - return world->units.all.size(); -} - -df::unit *Units::getUnit (const int32_t index) -{ - return vector_get(world->units.all, index); -} - -// returns index of creature actually read or -1 if no creature can be found -bool Units::getUnitsInBox (std::vector &units, - int16_t x1, int16_t y1, int16_t z1, - int16_t x2, int16_t y2, int16_t z2) -{ - if (!world) - return false; +bool Units::isUnitInBox(df::unit* u, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) { if (x1 > x2) swap(x1, x2); if (y1 > y2) swap(y1, y2); if (z1 > z2) swap(z1, z2); - - units.clear(); - for (df::unit *u : world->units.all) - { - if (u->pos.x >= x1 && u->pos.x <= x2) - { - if (u->pos.y >= y1 && u->pos.y <= y2) - { - if (u->pos.z >= z1 && u->pos.z <= z2) - { - units.push_back(u); - } + if (u->pos.x >= x1 && u->pos.x <= x2) { + if (u->pos.y >= y1 && u->pos.y <= y2) { + if (u->pos.z >= z1 && u->pos.z <= z2) { + return true; } } } - return true; + return false; } -int32_t Units::findIndexById(int32_t creature_id) +bool Units::isActive(df::unit *unit) { - return df::unit::binsearch_index(world->units.all, creature_id); + CHECK_NULL_POINTER(unit); + + return !unit->flags1.bits.inactive; } -df::coord Units::getPosition(df::unit *unit) +bool Units::isVisible(df::unit* unit) { CHECK_NULL_POINTER(unit); + return Maps::isTileVisible(unit->pos); +} - if (unit->flags1.bits.caged) - { - auto cage = getContainer(unit); - if (cage) - return Items::getPosition(cage); - } +bool Units::isCitizen(df::unit *unit, bool ignore_sanity) +{ + CHECK_NULL_POINTER(unit); - return unit->pos; + // Copied from the conditions used to decide game over, + // except that the game appears to let melancholy/raving + // dwarves count as citizens. + + if (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader || + unit->flags1.bits.forest || + unit->flags1.bits.merchant || + unit->flags1.bits.diplomat || + unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited || + unit->flags2.bits.underworld || + unit->flags2.bits.resident) + return false; + + if (!ignore_sanity && !isSane(unit)) + return false; + + return isOwnGroup(unit); } -bool Units::teleport(df::unit *unit, df::coord target_pos) -{ - // make sure source and dest map blocks are valid - auto old_occ = Maps::getTileOccupancy(unit->pos); - auto new_occ = Maps::getTileOccupancy(target_pos); - if (!old_occ || !new_occ) +bool Units::isFortControlled(df::unit *unit) +{ // Reverse-engineered from ambushing unit code + CHECK_NULL_POINTER(unit); + + if (*gamemode != game_mode::DWARF) return false; - // clear appropriate occupancy flags at old tile - if (unit->flags1.bits.on_ground) - // this is potentially wrong, but the game will recompute this as needed - old_occ->bits.unit_grounded = 0; - else - old_occ->bits.unit = 0; + if (unit->mood == mood_type::Berserk || + Units::isCrazed(unit) || + Units::isOpposedToLife(unit) || + unit->enemy.undead || + unit->flags3.bits.ghostly) + return false; - // if there's already somebody standing at the destination, then force the - // unit to lay down - if (new_occ->bits.unit) - unit->flags1.bits.on_ground = 1; + if (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader || + unit->flags1.bits.forest || + unit->flags1.bits.merchant || + unit->flags1.bits.diplomat) + return false; - // set appropriate occupancy flags at new tile - if (unit->flags1.bits.on_ground) - new_occ->bits.unit_grounded = 1; - else - new_occ->bits.unit = 1; + if (unit->flags1.bits.tame) + return true; - // move unit to destination - unit->pos = target_pos; - unit->idle_area = target_pos; + if (unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited || + unit->flags2.bits.underworld || + unit->flags2.bits.resident) + return false; - // move unit's riders (including babies) to destination - if (unit->flags1.bits.ridden) + return unit->civ_id != -1 && unit->civ_id == ui->civ_id; +} + +// check if creature belongs to the player's civilization +// (don't try to pasture/slaughter random untame animals) +bool Units::isOwnCiv(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->civ_id == ui->civ_id; +} + +// check if creature belongs to the player's group +bool Units::isOwnGroup(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto histfig = df::historical_figure::find(unit->hist_figure_id); + if (!histfig) + return false; + for (size_t i = 0; i < histfig->entity_links.size(); i++) { - for (size_t j = 0; j < world->units.other[units_other_id::ANY_RIDER].size(); j++) - { - df::unit *rider = world->units.other[units_other_id::ANY_RIDER][j]; - if (rider->relationship_ids[df::unit_relationship_type::RiderMount] == unit->id) - rider->pos = unit->pos; - } + auto link = histfig->entity_links[i]; + if (link->entity_id == ui->group_id && link->getType() == df::histfig_entity_link_type::MEMBER) + return true; } + return false; +} - return true; +// check if creature belongs to the player's race +// (in combination with check for civ helps to filter out own dwarves) +bool Units::isOwnRace(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->race == ui->race_id; } -df::general_ref *Units::getGeneralRef(df::unit *unit, df::general_ref_type type) + +bool Units::isAlive(df::unit *unit) { CHECK_NULL_POINTER(unit); - return findRef(unit->general_refs, type); + return !unit->flags2.bits.killed && + !unit->flags3.bits.ghostly && + !unit->curse.add_tags1.bits.NOT_LIVING; } -df::specific_ref *Units::getSpecificRef(df::unit *unit, df::specific_ref_type type) +bool Units::isDead(df::unit *unit) { CHECK_NULL_POINTER(unit); - return findRef(unit->specific_refs, type); + return unit->flags2.bits.killed || + unit->flags3.bits.ghostly; } -df::item *Units::getContainer(df::unit *unit) +bool Units::isKilled(df::unit *unit) { CHECK_NULL_POINTER(unit); - return findItemRef(unit->general_refs, general_ref_type::CONTAINED_IN_ITEM); + return unit->flags2.bits.killed; } -void Units::getOuterContainerRef(df::specific_ref &spec_ref, df::unit *unit, bool init_ref) +bool Units::isSane(df::unit *unit) { CHECK_NULL_POINTER(unit); - // Reverse-engineered from ambushing unit code - if (init_ref) - { - spec_ref.type = specific_ref_type::UNIT; - spec_ref.data.unit = unit; - } + if (isDead(unit) || + isOpposedToLife(unit) || + unit->enemy.undead) + return false; - if (unit->flags1.bits.caged) + if (unit->enemy.normal_race == unit->enemy.were_race && isCrazed(unit)) + return false; + + switch (unit->mood) { - df::item *cage = getContainer(unit); - if (cage) - return Items::getOuterContainerRef(spec_ref, cage); + case mood_type::Melancholy: + case mood_type::Raving: + case mood_type::Berserk: + return false; + default: + break; } - return; + + return true; } -static df::identity *getFigureIdentity(df::historical_figure *figure) +bool Units::isCrazed(df::unit *unit) { - if (figure && figure->info && figure->info->reputation) - return df::identity::find(figure->info->reputation->cur_identity); - - return NULL; + CHECK_NULL_POINTER(unit); + if (unit->flags3.bits.scuttle) + return false; + if (unit->curse.rem_tags1.bits.CRAZED) + return false; + if (unit->curse.add_tags1.bits.CRAZED) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); } -df::identity *Units::getIdentity(df::unit *unit) +bool Units::isGhost(df::unit *unit) { CHECK_NULL_POINTER(unit); - df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); - - return getFigureIdentity(figure); + return unit->flags3.bits.ghostly; } -void Units::setNickname(df::unit *unit, std::string nick) +bool Units::isHidden(df::unit *unit) { CHECK_NULL_POINTER(unit); + // Reverse-engineered from ambushing unit code - // There are >=3 copies of the name, and the one - // in the unit is not the authoritative one. - // This is the reason why military units often - // lose nicknames set from Dwarf Therapist. - Translation::setNickname(&unit->name, nick); - - if (unit->status.current_soul) - Translation::setNickname(&unit->status.current_soul->name, nick); + if (*df::global::debug_showambush) + return false; - df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); - if (figure) + if (*gamemode == game_mode::ADVENTURE) { - Translation::setNickname(&figure->name, nick); + if (unit == world->units.active[0]) + return false; + else if (unit->flags1.bits.hidden_in_ambush) + return true; + } + else + { + if (*gametype == game_type::DWARF_ARENA) + return false; + else if (unit->flags1.bits.hidden_in_ambush && !isFortControlled(unit)) + return true; + } - if (auto identity = getFigureIdentity(figure)) - { - df::historical_figure *id_hfig = NULL; + if (unit->flags1.bits.caged) + { + auto spec_ref = getOuterContainerRef(unit); + if (spec_ref.type == specific_ref_type::UNIT) + return isHidden(spec_ref.data.unit); + } - switch (identity->type) { - case df::identity_type::None: - case df::identity_type::HidingCurse: - case df::identity_type::FalseIdentity: - case df::identity_type::InfiltrationIdentity: - case df::identity_type::Identity: - break; // We want the nickname to end up in the identity - - case df::identity_type::Impersonating: - case df::identity_type::TrueName: - id_hfig = df::historical_figure::find(identity->histfig_id); - break; - } - - if (id_hfig) - { - Translation::setNickname(&id_hfig->name, nick); - } - else - Translation::setNickname(&identity->name, nick); - } - } -} - -df::language_name *Units::getVisibleName(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - // as of 0.44.11, identity names take precedence over associated histfig names - if (auto identity = getIdentity(unit)) - return &identity->name; - - return &unit->name; -} - -df::nemesis_record *Units::getNemesis(df::unit *unit) -{ - if (!unit) - return NULL; - - for (unsigned i = 0; i < unit->general_refs.size(); i++) - { - df::nemesis_record *rv = unit->general_refs[i]->getNemesis(); - if (rv && rv->unit == unit) - return rv; - } - - return NULL; + if (*gamemode == game_mode::ADVENTURE || isFortControlled(unit)) + return false; + else + return !Maps::isTileVisible(Units::getPosition(unit)); } - bool Units::isHidingCurse(df::unit *unit) { if (!unit->job.hunt_target) @@ -333,232 +327,294 @@ bool Units::isHidingCurse(df::unit *unit) return false; } -int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) -{ - auto &aobj = unit->body.physical_attrs[attr]; - int value = std::max(0, aobj.value - aobj.soft_demotion); - - if (auto mod = unit->curse.attr_change) - { - int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; - - if (isHidingCurse(unit)) - value = std::min(value, mvalue); - else - value = mvalue; - } - return std::max(0, value); +bool Units::isMale(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->sex == 1; } -int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +bool Units::isFemale(df::unit* unit) { - auto soul = unit->status.current_soul; - if (!soul) return 0; - - auto &aobj = soul->mental_attrs[attr]; - int value = std::max(0, aobj.value - aobj.soft_demotion); - - if (auto mod = unit->curse.attr_change) - { - int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; - - if (isHidingCurse(unit)) - value = std::min(value, mvalue); - else - value = mvalue; - } - - return std::max(0, value); + CHECK_NULL_POINTER(unit); + return unit->sex == 0; } -bool Units::casteFlagSet(int race, int caste, df::caste_raw_flags flag) +bool Units::isBaby(df::unit* unit) { - auto creature = df::creature_raw::find(race); - if (!creature) - return false; - - auto craw = vector_get(creature->caste, caste); - if (!craw) - return false; - - return craw->flags.is_set(flag); + CHECK_NULL_POINTER(unit); + return unit->profession == df::profession::BABY; } -bool Units::isCrazed(df::unit *unit) +bool Units::isChild(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->flags3.bits.scuttle) - return false; - if (unit->curse.rem_tags1.bits.CRAZED) - return false; - if (unit->curse.add_tags1.bits.CRAZED) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::CRAZED); + return unit->profession == df::profession::CHILD; } -bool Units::isOpposedToLife(df::unit *unit) +bool Units::isAdult(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) - return false; - if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); + return !isBaby(unit) && !isChild(unit); } -bool Units::hasExtravision(df::unit *unit) +bool Units::isGay(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.EXTRAVISION) + if (!unit->status.current_soul) return false; - if (unit->curse.add_tags1.bits.EXTRAVISION) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); + df::orientation_flags orientation = unit->status.current_soul->orientation_flags; + return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) + && (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); } -bool Units::isBloodsucker(df::unit *unit) +bool Units::isNaked(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.BLOODSUCKER) - return false; - if (unit->curse.add_tags1.bits.BLOODSUCKER) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); + // TODO(kazimuth): is this correct? + return (unit->inventory.empty()); } -bool Units::isMischievous(df::unit *unit) +bool Units::isVisiting(df::unit* unit) { + CHECK_NULL_POINTER(unit); + + return unit->flags1.bits.merchant || + unit->flags1.bits.diplomat || + unit->flags2.bits.visitor || + unit->flags2.bits.visitor_uninvited; +} + + +bool Units::isTrainableHunting(df::unit* unit) { CHECK_NULL_POINTER(unit); - if (unit->curse.rem_tags1.bits.MISCHIEVOUS) - return false; - if (unit->curse.add_tags1.bits.MISCHIEVOUS) - return true; - return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::TRAINABLE_HUNTING); } -df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) +bool Units::isTrainableWar(df::unit* unit) { CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::TRAINABLE_WAR); +} - auto &vec = unit->status.misc_traits; - for (size_t i = 0; i < vec.size(); i++) - if (vec[i]->id == type) - return vec[i]; +bool Units::isTrained(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + // case a: trained for war/hunting (those don't have a training level, strangely) + if(Units::isWar(unit) || Units::isHunter(unit)) + return true; - if (create) + // case b: tamed and trained wild creature, gets a training level + bool trained = false; + switch (unit->training_level) { - auto obj = new df::unit_misc_trait(); - obj->id = type; - vec.push_back(obj); - return obj; + case df::animal_training_level::Trained: + case df::animal_training_level::WellTrained: + case df::animal_training_level::SkilfullyTrained: + case df::animal_training_level::ExpertlyTrained: + case df::animal_training_level::ExceptionallyTrained: + case df::animal_training_level::MasterfullyTrained: + //case df::animal_training_level::Domesticated: + trained = true; + break; + default: + break; } + return trained; +} - return NULL; +// check for profession "hunting creature" +bool Units::isHunter(df::unit* unit) +{ + CHECK_NULL_POINTER(unit) + return unit->profession == df::profession::TRAINED_HUNTER + || unit->profession2 == df::profession::TRAINED_HUNTER; } -bool Units::isDead(df::unit *unit) +// check for profession "war creature" +bool Units::isWar(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return unit->flags2.bits.killed || - unit->flags3.bits.ghostly; + return unit->profession == df::profession::TRAINED_WAR + || unit->profession2 == df::profession::TRAINED_WAR; } -bool Units::isAlive(df::unit *unit) +bool Units::isTame(df::unit* unit) { CHECK_NULL_POINTER(unit); - - return !unit->flags2.bits.killed && - !unit->flags3.bits.ghostly && - !unit->curse.add_tags1.bits.NOT_LIVING; + bool tame = false; + if(unit->flags1.bits.tame) + { + switch (unit->training_level) + { + case df::animal_training_level::SemiWild: //?? + case df::animal_training_level::Trained: + case df::animal_training_level::WellTrained: + case df::animal_training_level::SkilfullyTrained: + case df::animal_training_level::ExpertlyTrained: + case df::animal_training_level::ExceptionallyTrained: + case df::animal_training_level::MasterfullyTrained: + case df::animal_training_level::Domesticated: + tame=true; + break; + case df::animal_training_level::Unk8: //?? + case df::animal_training_level::WildUntamed: + default: + tame=false; + break; + } + } + return tame; } -bool Units::isSane(df::unit *unit) +bool Units::isTamable(df::unit* unit) { CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::PET) + || caste->flags.is_set(caste_raw_flags::PET_EXOTIC); +} - if (isDead(unit) || - isOpposedToLife(unit) || - unit->enemy.undead) - return false; - - if (unit->enemy.normal_race == unit->enemy.were_race && isCrazed(unit)) - return false; - - switch (unit->mood) +// check if creature is domesticated +// seems to be the only way to really tell if it's completely safe to autonestbox it (training can revert) +bool Units::isDomesticated(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + bool tame = false; + if(unit->flags1.bits.tame) { - case mood_type::Melancholy: - case mood_type::Raving: - case mood_type::Berserk: - return false; - default: - break; + switch (unit->training_level) + { + case df::animal_training_level::Domesticated: + tame=true; + break; + default: + tame=false; + break; + } } - - return true; + return tame; } -bool Units::isCitizen(df::unit *unit) +bool Units::isMarkedForSlaughter(df::unit* unit) { CHECK_NULL_POINTER(unit); + return unit->flags2.bits.slaughter == 1; +} - // Copied from the conditions used to decide game over, - // except that the game appears to let melancholy/raving - // dwarves count as citizens. +bool Units::isGelded(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto wounds = unit->body.wounds; + for(auto wound = wounds.begin(); wound != wounds.end(); ++wound) + { + auto parts = (*wound)->parts; + for (auto part = parts.begin(); part != parts.end(); ++part) + { + if ((*part)->flags2.bits.gelded) + return true; + } + } + return false; +} - if (unit->flags1.bits.marauder || - unit->flags1.bits.invader_origin || - unit->flags1.bits.active_invader || - unit->flags1.bits.forest || - unit->flags1.bits.merchant || - unit->flags1.bits.diplomat || - unit->flags2.bits.visitor || - unit->flags2.bits.visitor_uninvited || - unit->flags2.bits.underworld || - unit->flags2.bits.resident) - return false; +bool Units::isEggLayer(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::LAYS_EGGS) + || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS); +} - if (!isSane(unit)) - return false; +bool Units::isGrazer(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::GRAZER); +} - return isOwnGroup(unit); +bool Units::isMilkable(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + df::creature_raw *raw = world->raws.creatures.all[unit->race]; + df::caste_raw *caste = raw->caste.at(unit->caste); + return caste->flags.is_set(caste_raw_flags::MILKABLE); } -bool Units::isFortControlled(df::unit *unit) -{ // Reverse-engineered from ambushing unit code +bool Units::isForest(df::unit* unit) +{ CHECK_NULL_POINTER(unit); + return unit->flags1.bits.forest == 1; +} - if (*gamemode != game_mode::DWARF) +bool Units::isMischievous(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.MISCHIEVOUS) return false; + if (unit->curse.add_tags1.bits.MISCHIEVOUS) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::MISCHIEVOUS); +} - if (unit->mood == mood_type::Berserk || - Units::isCrazed(unit) || - Units::isOpposedToLife(unit) || - unit->enemy.undead || - unit->flags3.bits.ghostly) - return false; +// check if unit is marked as available for adoption +bool Units::isAvailableForAdoption(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + auto refs = unit->specific_refs; + for(size_t i=0; itype; + if( reftype == df::specific_ref_type::PETINFO_PET ) + { + //df::pet_info* pet = ref->pet; + return true; + } + } - if (unit->flags1.bits.marauder || - unit->flags1.bits.invader_origin || - unit->flags1.bits.active_invader || - unit->flags1.bits.forest || - unit->flags1.bits.merchant || - unit->flags1.bits.diplomat) - return false; + return false; +} - if (unit->flags1.bits.tame) + +bool Units::hasExtravision(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.EXTRAVISION) + return false; + if (unit->curse.add_tags1.bits.EXTRAVISION) return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::EXTRAVISION); +} - if (unit->flags2.bits.visitor || - unit->flags2.bits.visitor_uninvited || - unit->flags2.bits.underworld || - unit->flags2.bits.resident) +bool Units::isOpposedToLife(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.OPPOSED_TO_LIFE) return false; + if (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::OPPOSED_TO_LIFE); +} - return unit->civ_id != -1 && unit->civ_id == ui->civ_id; +bool Units::isBloodsucker(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + if (unit->curse.rem_tags1.bits.BLOODSUCKER) + return false; + if (unit->curse.add_tags1.bits.BLOODSUCKER) + return true; + return casteFlagSet(unit->race, unit->caste, caste_raw_flags::BLOODSUCKER); } + bool Units::isDwarf(df::unit *unit) { CHECK_NULL_POINTER(unit); @@ -567,113 +623,393 @@ bool Units::isDwarf(df::unit *unit) unit->enemy.normal_race == ui->race_id; } -// check for profession "war creature" -bool Units::isWar(df::unit* unit) +bool Units::isAnimal(df::unit* unit) +{ + CHECK_NULL_POINTER(unit) + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NATURAL_ANIMAL); +} + +bool Units::isMerchant(df::unit* unit) { CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::TRAINED_WAR - || unit->profession2 == df::profession::TRAINED_WAR; + + return unit->flags1.bits.merchant == 1; } -// check for profession "hunting creature" -bool Units::isHunter(df::unit* unit) +bool Units::isDiplomat(df::unit* unit) { - CHECK_NULL_POINTER(unit) - return unit->profession == df::profession::TRAINED_HUNTER - || unit->profession2 == df::profession::TRAINED_HUNTER; + CHECK_NULL_POINTER(unit); + + return unit->flags1.bits.diplomat == 1; } -// check if unit is marked as available for adoption -bool Units::isAvailableForAdoption(df::unit* unit) +bool Units::isVisitor(df::unit* unit) { CHECK_NULL_POINTER(unit); - auto refs = unit->specific_refs; - for(size_t i=0; iflags2.bits.visitor || unit->flags2.bits.visitor_uninvited; +} + +bool Units::isInvader(df::unit* unit) { + CHECK_NULL_POINTER(unit); + + return !isOwnGroup(unit) && + (unit->flags1.bits.marauder || + unit->flags1.bits.invader_origin || + unit->flags1.bits.active_invader); +} + +bool Units::isUndead(df::unit* unit, bool include_vamps) +{ + CHECK_NULL_POINTER(unit); + + const auto &cb = unit->curse.add_tags1.bits; + return unit->flags3.bits.ghostly || + ((cb.OPPOSED_TO_LIFE || cb.NOT_LIVING) && (include_vamps || !cb.BLOODSUCKER)); +} + +bool Units::isNightCreature(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::NIGHT_CREATURE); +} + +bool Units::isSemiMegabeast(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::SEMIMEGABEAST); +} + +bool Units::isMegabeast(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::MEGABEAST); +} + +bool Units::isTitan(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return unit->enemy.caste_flags.is_set(df::enums::caste_raw_flags::TITAN); +} + +bool Units::isDemon(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + using namespace df::enums::caste_raw_flags; + const auto &cf = unit->enemy.caste_flags; + return cf.is_set(DEMON) || cf.is_set(UNIQUE_DEMON); +} + +bool Units::isDanger(df::unit* unit) { + CHECK_NULL_POINTER(unit); + return isCrazed(unit) || + isInvader(unit) || + isUndead(unit, true) || + isSemiMegabeast(unit) || + isNightCreature(unit) || + isGreatDanger(unit); +} + +bool Units::isGreatDanger(df::unit* unit) +{ + CHECK_NULL_POINTER(unit); + return isDemon(unit) || isTitan(unit) || isMegabeast(unit); +} + + + +int32_t Units::getNumUnits() +{ + return world->units.all.size(); +} + +df::unit *Units::getUnit (const int32_t index) +{ + return vector_get(world->units.all, index); +} + +// returns index of creature actually read or -1 if no creature can be found +bool Units::getUnitsInBox (std::vector &units, + int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) +{ + if (!world) + return false; + + units.clear(); + for (df::unit *u : world->units.all) { - auto ref = refs[i]; - auto reftype = ref->type; - if( reftype == df::specific_ref_type::PETINFO_PET ) + if (isUnitInBox(u, x1, y1, z1, x2, y2, z2)) { - //df::pet_info* pet = ref->pet; - return true; + units.push_back(u); } } + return true; +} - return false; +int32_t Units::findIndexById(int32_t creature_id) +{ + return df::unit::binsearch_index(world->units.all, creature_id); } -// check if creature belongs to the player's civilization -// (don't try to pasture/slaughter random untame animals) -bool Units::isOwnCiv(df::unit* unit) +df::coord Units::getPosition(df::unit *unit) { CHECK_NULL_POINTER(unit); - return unit->civ_id == ui->civ_id; + + if (unit->flags1.bits.caged) + { + auto cage = getContainer(unit); + if (cage) + return Items::getPosition(cage); + } + + return unit->pos; } -// check if creature belongs to the player's group -bool Units::isOwnGroup(df::unit* unit) +bool Units::teleport(df::unit *unit, df::coord target_pos) { - CHECK_NULL_POINTER(unit); - auto histfig = df::historical_figure::find(unit->hist_figure_id); - if (!histfig) + // make sure source and dest map blocks are valid + auto old_occ = Maps::getTileOccupancy(unit->pos); + auto new_occ = Maps::getTileOccupancy(target_pos); + if (!old_occ || !new_occ) return false; - for (size_t i = 0; i < histfig->entity_links.size(); i++) + + // clear appropriate occupancy flags at old tile + if (unit->flags1.bits.on_ground) + // this is potentially wrong, but the game will recompute this as needed + old_occ->bits.unit_grounded = 0; + else + old_occ->bits.unit = 0; + + // if there's already somebody standing at the destination, then force the + // unit to lay down + if (new_occ->bits.unit) + unit->flags1.bits.on_ground = 1; + + // set appropriate occupancy flags at new tile + if (unit->flags1.bits.on_ground) + new_occ->bits.unit_grounded = 1; + else + new_occ->bits.unit = 1; + + // move unit to destination + unit->pos = target_pos; + unit->idle_area = target_pos; + + // move unit's riders (including babies) to destination + if (unit->flags1.bits.ridden) { - auto link = histfig->entity_links[i]; - if (link->entity_id == ui->group_id && link->getType() == df::histfig_entity_link_type::MEMBER) - return true; + for (size_t j = 0; j < world->units.other[units_other_id::ANY_RIDER].size(); j++) + { + df::unit *rider = world->units.other[units_other_id::ANY_RIDER][j]; + if (rider->relationship_ids[df::unit_relationship_type::RiderMount] == unit->id) + rider->pos = unit->pos; + } } - return false; + + return true; } -// check if creature belongs to the player's race -// (in combination with check for civ helps to filter out own dwarves) -bool Units::isOwnRace(df::unit* unit) +df::general_ref *Units::getGeneralRef(df::unit *unit, df::general_ref_type type) { CHECK_NULL_POINTER(unit); - return unit->race == ui->race_id; + + return findRef(unit->general_refs, type); } -bool Units::isVisible(df::unit* unit) +df::specific_ref *Units::getSpecificRef(df::unit *unit, df::specific_ref_type type) { CHECK_NULL_POINTER(unit); - return Maps::isTileVisible(unit->pos); + + return findRef(unit->specific_refs, type); +} + +df::item *Units::getContainer(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + return findItemRef(unit->general_refs, general_ref_type::CONTAINED_IN_ITEM); +} + +void Units::getOuterContainerRef(df::specific_ref &spec_ref, df::unit *unit, bool init_ref) +{ + CHECK_NULL_POINTER(unit); + // Reverse-engineered from ambushing unit code + + if (init_ref) + { + spec_ref.type = specific_ref_type::UNIT; + spec_ref.data.unit = unit; + } + + if (unit->flags1.bits.caged) + { + df::item *cage = getContainer(unit); + if (cage) + return Items::getOuterContainerRef(spec_ref, cage); + } + return; +} + +static df::identity *getFigureIdentity(df::historical_figure *figure) +{ + if (figure && figure->info && figure->info->reputation) + return df::identity::find(figure->info->reputation->cur_identity); + + return NULL; +} + +df::identity *Units::getIdentity(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); + + return getFigureIdentity(figure); +} + +void Units::setNickname(df::unit *unit, std::string nick) +{ + CHECK_NULL_POINTER(unit); + + // There are >=3 copies of the name, and the one + // in the unit is not the authoritative one. + // This is the reason why military units often + // lose nicknames set from Dwarf Therapist. + Translation::setNickname(&unit->name, nick); + + if (unit->status.current_soul) + Translation::setNickname(&unit->status.current_soul->name, nick); + + df::historical_figure *figure = df::historical_figure::find(unit->hist_figure_id); + if (figure) + { + Translation::setNickname(&figure->name, nick); + + if (auto identity = getFigureIdentity(figure)) + { + df::historical_figure *id_hfig = NULL; + + switch (identity->type) { + case df::identity_type::None: + case df::identity_type::HidingCurse: + case df::identity_type::FalseIdentity: + case df::identity_type::InfiltrationIdentity: + case df::identity_type::Identity: + break; // We want the nickname to end up in the identity + + case df::identity_type::Impersonating: + case df::identity_type::TrueName: + id_hfig = df::historical_figure::find(identity->histfig_id); + break; + } + + if (id_hfig) + { + Translation::setNickname(&id_hfig->name, nick); + } + else + Translation::setNickname(&identity->name, nick); + } + } +} + +df::language_name *Units::getVisibleName(df::unit *unit) +{ + CHECK_NULL_POINTER(unit); + + // as of 0.44.11, identity names take precedence over associated histfig names + if (auto identity = getIdentity(unit)) + return &identity->name; + + return &unit->name; +} + +df::nemesis_record *Units::getNemesis(df::unit *unit) +{ + if (!unit) + return NULL; + + for (unsigned i = 0; i < unit->general_refs.size(); i++) + { + df::nemesis_record *rv = unit->general_refs[i]->getNemesis(); + if (rv && rv->unit == unit) + return rv; + } + + return NULL; +} + + +int Units::getPhysicalAttrValue(df::unit *unit, df::physical_attribute_type attr) +{ + auto &aobj = unit->body.physical_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->phys_att_perc[attr] / 100) + mod->phys_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +int Units::getMentalAttrValue(df::unit *unit, df::mental_attribute_type attr) +{ + auto soul = unit->status.current_soul; + if (!soul) return 0; + + auto &aobj = soul->mental_attrs[attr]; + int value = std::max(0, aobj.value - aobj.soft_demotion); + + if (auto mod = unit->curse.attr_change) + { + int mvalue = (value * mod->ment_att_perc[attr] / 100) + mod->ment_att_add[attr]; + + if (isHidingCurse(unit)) + value = std::min(value, mvalue); + else + value = mvalue; + } + + return std::max(0, value); +} + +bool Units::casteFlagSet(int race, int caste, df::caste_raw_flags flag) +{ + auto creature = df::creature_raw::find(race); + if (!creature) + return false; + + auto craw = vector_get(creature->caste, caste); + if (!craw) + return false; + + return craw->flags.is_set(flag); } -bool Units::isHidden(df::unit *unit) +df::unit_misc_trait *Units::getMiscTrait(df::unit *unit, df::misc_trait_type type, bool create) { CHECK_NULL_POINTER(unit); - // Reverse-engineered from ambushing unit code - - if (*df::global::debug_showambush) - return false; - if (*gamemode == game_mode::ADVENTURE) - { - if (unit == world->units.active[0]) - return false; - else if (unit->flags1.bits.hidden_in_ambush) - return true; - } - else - { - if (*gametype == game_type::DWARF_ARENA) - return false; - else if (unit->flags1.bits.hidden_in_ambush && !isFortControlled(unit)) - return true; - } + auto &vec = unit->status.misc_traits; + for (size_t i = 0; i < vec.size(); i++) + if (vec[i]->id == type) + return vec[i]; - if (unit->flags1.bits.caged) + if (create) { - auto spec_ref = getOuterContainerRef(unit); - if (spec_ref.type == specific_ref_type::UNIT) - return isHidden(spec_ref.data.unit); + auto obj = new df::unit_misc_trait(); + obj->id = type; + vec.push_back(obj); + return obj; } - if (*gamemode == game_mode::ADVENTURE || isFortControlled(unit)) - return false; - else - return !Maps::isTileVisible(Units::getPosition(unit)); + return NULL; } // get race name by id or unit pointer @@ -752,86 +1088,6 @@ string Units::getRaceChildName(df::unit* unit) return getRaceChildNameById(unit->race); } -bool Units::isBaby(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::BABY; -} - -bool Units::isChild(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->profession == df::profession::CHILD; -} - -bool Units::isAdult(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return !isBaby(unit) && !isChild(unit); -} - -bool Units::isEggLayer(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::LAYS_EGGS) - || caste->flags.is_set(caste_raw_flags::LAYS_UNUSUAL_EGGS); -} - -bool Units::isGrazer(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::GRAZER); -} - -bool Units::isMilkable(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::MILKABLE); -} - -bool Units::isTrainableWar(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::TRAINABLE_WAR); -} - -bool Units::isTrainableHunting(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::TRAINABLE_HUNTING); -} - -bool Units::isTamable(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - df::creature_raw *raw = world->raws.creatures.all[unit->race]; - df::caste_raw *caste = raw->caste.at(unit->caste); - return caste->flags.is_set(caste_raw_flags::PET) - || caste->flags.is_set(caste_raw_flags::PET_EXOTIC); -} - -bool Units::isMale(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->sex == 1; -} - -bool Units::isFemale(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->sex == 0; -} - double Units::getAge(df::unit *unit, bool true_age) { @@ -1660,170 +1916,6 @@ df::activity_event *Units::getMainSocialEvent(df::unit *unit) return entry->events[entry->events.size() - 1]; } -bool Units::isMerchant(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags1.bits.merchant == 1; -} - -bool Units::isDiplomat(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags1.bits.diplomat == 1; -} - -bool Units::isForest(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->flags1.bits.forest == 1; -} - -bool Units::isMarkedForSlaughter(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - return unit->flags2.bits.slaughter == 1; -} - -bool Units::isTame(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - bool tame = false; - if(unit->flags1.bits.tame) - { - switch (unit->training_level) - { - case df::animal_training_level::SemiWild: //?? - case df::animal_training_level::Trained: - case df::animal_training_level::WellTrained: - case df::animal_training_level::SkilfullyTrained: - case df::animal_training_level::ExpertlyTrained: - case df::animal_training_level::ExceptionallyTrained: - case df::animal_training_level::MasterfullyTrained: - case df::animal_training_level::Domesticated: - tame=true; - break; - case df::animal_training_level::Unk8: //?? - case df::animal_training_level::WildUntamed: - default: - tame=false; - break; - } - } - return tame; -} - -bool Units::isTrained(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // case a: trained for war/hunting (those don't have a training level, strangely) - if(Units::isWar(unit) || Units::isHunter(unit)) - return true; - - // case b: tamed and trained wild creature, gets a training level - bool trained = false; - switch (unit->training_level) - { - case df::animal_training_level::Trained: - case df::animal_training_level::WellTrained: - case df::animal_training_level::SkilfullyTrained: - case df::animal_training_level::ExpertlyTrained: - case df::animal_training_level::ExceptionallyTrained: - case df::animal_training_level::MasterfullyTrained: - //case df::animal_training_level::Domesticated: - trained = true; - break; - default: - break; - } - return trained; -} - -bool Units::isGay(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - if (!unit->status.current_soul) - return false; - df::orientation_flags orientation = unit->status.current_soul->orientation_flags; - return (!Units::isFemale(unit) || !(orientation.whole & (orientation.mask_marry_male | orientation.mask_romance_male))) - && (!Units::isMale(unit) || !(orientation.whole & (orientation.mask_marry_female | orientation.mask_romance_female))); -} - -bool Units::isNaked(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // TODO(kazimuth): is this correct? - return (unit->inventory.empty()); -} - -bool Units::isUndead(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - // ignore vampires, they should be treated like normal dwarves - return (unit->flags3.bits.ghostly || - ( (unit->curse.add_tags1.bits.OPPOSED_TO_LIFE || unit->curse.add_tags1.bits.NOT_LIVING) - && !unit->curse.add_tags1.bits.BLOODSUCKER )); -} - -bool Units::isGhost(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags3.bits.ghostly; -} - -bool Units::isActive(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return !unit->flags1.bits.inactive; -} - -bool Units::isKilled(df::unit *unit) -{ - CHECK_NULL_POINTER(unit); - - return unit->flags2.bits.killed; -} - -bool Units::isGelded(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - auto wounds = unit->body.wounds; - for(auto wound = wounds.begin(); wound != wounds.end(); ++wound) - { - auto parts = (*wound)->parts; - for (auto part = parts.begin(); part != parts.end(); ++part) - { - if ((*part)->flags2.bits.gelded) - return true; - } - } - return false; -} - -// check if creature is domesticated -// seems to be the only way to really tell if it's completely safe to autonestbox it (training can revert) -bool Units::isDomesticated(df::unit* unit) -{ - CHECK_NULL_POINTER(unit); - bool tame = false; - if(unit->flags1.bits.tame) - { - switch (unit->training_level) - { - case df::animal_training_level::Domesticated: - tame=true; - break; - default: - tame=false; - break; - } - } - return tame; -} - // 50000 and up is level 0, 25000 and up is level 1, etc. const vector Units::stress_cutoffs {50000, 25000, 10000, -10000, -25000, -50000, -100000}; diff --git a/library/tests/CMakeLists.txt b/library/tests/CMakeLists.txt new file mode 100644 index 000000000..3e5a2b3ca --- /dev/null +++ b/library/tests/CMakeLists.txt @@ -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) diff --git a/library/tests/MiscUtils.test.cpp b/library/tests/MiscUtils.test.cpp new file mode 100644 index 000000000..033b226a7 --- /dev/null +++ b/library/tests/MiscUtils.test.cpp @@ -0,0 +1,19 @@ + +#include "MiscUtils.h" +#include +#include + +TEST(MiscUtils, wordwrap) { + std::vector 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); +} diff --git a/library/tests/test.cpp b/library/tests/test.cpp new file mode 100644 index 000000000..76f841f1b --- /dev/null +++ b/library/tests/test.cpp @@ -0,0 +1,6 @@ +#include + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/library/xml b/library/xml index ea78ed8bf..4c5697dcb 160000 --- a/library/xml +++ b/library/xml @@ -1 +1 @@ -Subproject commit ea78ed8bf70c3e75b8fba90cdc61cab34788899e +Subproject commit 4c5697dcb060d645849327410b8ecce6880053d4 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index c9438e04e..7ed7a46ed 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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) diff --git a/plugins/autofarm.cpp b/plugins/autofarm.cpp index 1a02b6a06..2f8762425 100644 --- a/plugins/autofarm.cpp +++ b/plugins/autofarm.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 } if (!ok) { - out << "Cannot find plant with id " << id << '\n'; + out << "Cannot find plant with id " << id << '\n' << std::flush; return CR_WRONG_USAGE; } } diff --git a/plugins/autolabor/CMakeLists.txt b/plugins/autolabor/CMakeLists.txt new file mode 100644 index 000000000..96f3026b4 --- /dev/null +++ b/plugins/autolabor/CMakeLists.txt @@ -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}) diff --git a/plugins/autohauler.cpp b/plugins/autolabor/autohauler.cpp similarity index 77% rename from plugins/autohauler.cpp rename to plugins/autolabor/autohauler.cpp index b52e9cfb5..691dc2e8e 100644 --- a/plugins/autohauler.cpp +++ b/plugins/autolabor/autohauler.cpp @@ -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 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 state_count(NUM_STATE); // Mode assigned to labors. Either it's a hauling job, or it's not. enum labor_mode { diff --git a/plugins/autolabor.cpp b/plugins/autolabor/autolabor.cpp similarity index 84% rename from plugins/autolabor.cpp rename to plugins/autolabor/autolabor.cpp index 15c6903b4..198a588cc 100644 --- a/plugins/autolabor.cpp +++ b/plugins/autolabor/autolabor.cpp @@ -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 { diff --git a/plugins/labormanager/joblabormapper.cpp b/plugins/autolabor/joblabormapper.cpp similarity index 100% rename from plugins/labormanager/joblabormapper.cpp rename to plugins/autolabor/joblabormapper.cpp diff --git a/plugins/labormanager/joblabormapper.h b/plugins/autolabor/joblabormapper.h similarity index 100% rename from plugins/labormanager/joblabormapper.h rename to plugins/autolabor/joblabormapper.h diff --git a/plugins/labormanager/labormanager.cpp b/plugins/autolabor/labormanager.cpp similarity index 91% rename from plugins/labormanager/labormanager.cpp rename to plugins/autolabor/labormanager.cpp index f508c9797..44817e405 100644 --- a/plugins/labormanager/labormanager.cpp +++ b/plugins/autolabor/labormanager.cpp @@ -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; diff --git a/plugins/labormanager/labormanager.h b/plugins/autolabor/labormanager.h similarity index 100% rename from plugins/labormanager/labormanager.h rename to plugins/autolabor/labormanager.h diff --git a/plugins/autolabor/laborstatemap.h b/plugins/autolabor/laborstatemap.h new file mode 100644 index 000000000..789685930 --- /dev/null +++ b/plugins/autolabor/laborstatemap.h @@ -0,0 +1,294 @@ +#pragma once + +#include + +#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 diff --git a/plugins/channel-safely/CMakeLists.txt b/plugins/channel-safely/CMakeLists.txt new file mode 100644 index 000000000..36c7307e4 --- /dev/null +++ b/plugins/channel-safely/CMakeLists.txt @@ -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) diff --git a/plugins/channel-safely/channel-groups.cpp b/plugins/channel-safely/channel-groups.cpp new file mode 100644 index 000000000..52f7e6c40 --- /dev/null +++ b/plugins/channel-safely/channel-groups.cpp @@ -0,0 +1,290 @@ +#include +#include +#include +#include +#include + +#include + +// 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(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 last_jobs; + for (auto &pos : jobs) { + last_jobs.emplace(pos); + } + jobs.load_channel_jobs(); + // transpose channel jobs to + std::set new_jobs; + std::set 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(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); + } + } +} diff --git a/plugins/channel-safely/channel-manager.cpp b/plugins/channel-safely/channel-manager.cpp new file mode 100644 index 000000000..e905f2cfb --- /dev/null +++ b/plugins/channel-safely/channel-manager.cpp @@ -0,0 +1,105 @@ +#include +#include +#include + +#include //hash function for df::coord +#include + + +// 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); +} diff --git a/plugins/channel-safely/channel-safely-plugin.cpp b/plugins/channel-safely/channel-safely-plugin.cpp new file mode 100644 index 000000000..d291c0efc --- /dev/null +++ b/plugins/channel-safely/channel-safely-plugin.cpp @@ -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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +// 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 params{"-r", "--unit", std::to_string(unit)}; + Core::getInstance().runCommand(out,"full-heal", params); +} + +namespace CSP { + std::unordered_map endangered_units; + std::unordered_map job_id_map; + std::unordered_map active_jobs; + std::unordered_map active_workers; + + std::unordered_map last_safe; + std::unordered_set 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 &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 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 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 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( + 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 ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &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 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 ¶meters) { + 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; +} diff --git a/plugins/channel-safely/include/channel-groups.h b/plugins/channel-safely/include/channel-groups.h new file mode 100644 index 000000000..7547e2564 --- /dev/null +++ b/plugins/channel-safely/include/channel-groups.h @@ -0,0 +1,51 @@ +#pragma once +#include "plugin.h" +#include "channel-jobs.h" + +#include +#include +#include //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway) + +#include +#include +#include + +using namespace DFHack; + +using Group = std::unordered_set; +using Groups = std::vector; + +/* 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; + using GroupsMap = std::unordered_map; + GroupBlocks group_blocks; + GroupsMap groups_map; + Groups groups; + ChannelJobs &jobs; + std::set 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(); +}; diff --git a/plugins/channel-safely/include/channel-jobs.h b/plugins/channel-safely/include/channel-jobs.h new file mode 100644 index 000000000..3be704aeb --- /dev/null +++ b/plugins/channel-safely/include/channel-jobs.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway) +#include +#include + +#include + +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; // 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(); } +}; diff --git a/plugins/channel-safely/include/channel-manager.h b/plugins/channel-safely/include/channel-manager.h new file mode 100644 index 000000000..0cd3abfac --- /dev/null +++ b/plugins/channel-safely/include/channel-manager.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include +#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(); + } +}; diff --git a/plugins/channel-safely/include/inlines.h b/plugins/channel-safely/include/inlines.h new file mode 100644 index 000000000..8bd1de44d --- /dev/null +++ b/plugins/channel-safely/include/inlines.h @@ -0,0 +1,196 @@ +#pragma once +#include "plugin.h" +#include "channel-manager.h" + +#include +#include +#include + +#include +#include + +#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 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 +void set_difference(const Ctr1 &c1, const Ctr2 &c2, Ctr3 &c3) { + for (const auto &a : c1) { + if (!c2.count(a)) { + c3.emplace(a); + } + } +} + +template +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); + } + } +} diff --git a/plugins/channel-safely/include/plugin.h b/plugins/channel-safely/include/plugin.h new file mode 100644 index 000000000..23b2f8441 --- /dev/null +++ b/plugins/channel-safely/include/plugin.h @@ -0,0 +1,23 @@ +#pragma once +#include + +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; diff --git a/plugins/channel-safely/include/tile-cache.h b/plugins/channel-safely/include/tile-cache.h new file mode 100644 index 000000000..10e91cd46 --- /dev/null +++ b/plugins/channel-safely/include/tile-cache.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include //hash functions (they should probably get moved at this point, the ones that aren't specifically for EM anyway) + +#include + +class TileCache { +private: + TileCache() = default; + std::unordered_map 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]; + } +}; diff --git a/plugins/cleanowned.cpp b/plugins/cleanowned.cpp index 387dfdd92..86ef0a2e1 100644 --- a/plugins/cleanowned.cpp +++ b/plugins/cleanowned.cpp @@ -147,8 +147,8 @@ command_result df_cleanowned (color_ostream &out, vector & 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() ); diff --git a/plugins/dwarfmonitor.cpp b/plugins/dwarfmonitor.cpp index 1a453e0d5..4355104a7 100644 --- a/plugins/dwarfmonitor.cpp +++ b/plugins/dwarfmonitor.cpp @@ -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> 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 *input) - { + void feed(set *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 preferences_column; @@ -1762,13 +1663,11 @@ private: vector 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(), 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 & parameters) -{ - bool show_help = false; +static command_result dwarfmonitor_cmd(color_ostream &, vector & 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(), plugin_self); - } - else if (cmd == 'p' || cmd == 'P') - { - CoreSuspender guard; - if(Maps::IsValid()) - Screen::show(dts::make_unique(), 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(), plugin_self); } - - if (show_help) + else if (cmd == 'p' || cmd == 'P') { + CoreSuspender guard; + if(Maps::IsValid()) + Screen::show(dts::make_unique(), plugin_self); + } + else return CR_WRONG_USAGE; return CR_OK; } -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_init(color_ostream &, std::vector &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 +#include +#include -#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 current_bindings; static vector 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 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 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 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 *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 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 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 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 & 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 wrapString(string str, int width) - { - vector 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 & 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(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 &commands) -{ - if (!gps) - out.printerr("Could not insert hotkeys hooks!\n"); - +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &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; } diff --git a/plugins/labormanager/CMakeLists.txt b/plugins/labormanager/CMakeLists.txt deleted file mode 100644 index 787028452..000000000 --- a/plugins/labormanager/CMakeLists.txt +++ /dev/null @@ -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}) diff --git a/plugins/lua/dwarfmonitor.lua b/plugins/lua/dwarfmonitor.lua index 7637c50f7..5358e4289 100644 --- a/plugins/lua/dwarfmonitor.lua +++ b/plugins/lua/dwarfmonitor.lua @@ -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 diff --git a/plugins/lua/hotkeys.lua b/plugins/lua/hotkeys.lua new file mode 100644 index 000000000..e3ad26e68 --- /dev/null +++ b/plugins/lua/hotkeys.lua @@ -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 diff --git a/plugins/lua/overlay.lua b/plugins/lua/overlay.lua new file mode 100644 index 000000000..ad20c2605 --- /dev/null +++ b/plugins/lua/overlay.lua @@ -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 diff --git a/plugins/overlay.cpp b/plugins/overlay.cpp index 0c63e53e8..c2a04ac8b 100644 --- a/plugins/overlay.cpp +++ b/plugins/overlay.cpp @@ -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(args_lambda), + std::forward(res_lambda)); +} + template 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 *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 & parameters) { - if (DBG_NAME(control).isEnabled(DebugCategory::LDEBUG)) { - DEBUG(control).print("interpreting command with %zu parameters:\n", - parameters.size()); - for (auto ¶m : 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 &commands) { @@ -318,9 +365,10 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector -#include -#include - -#include "Core.h" -#include -#include -#include -#include - - -// 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 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 & 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 &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; -} diff --git a/plugins/spectate/spectate.cpp b/plugins/spectate/spectate.cpp index 5f720806c..f6a5ad8af 100644 --- a/plugins/spectate/spectate.cpp +++ b/plugins/spectate/spectate.cpp @@ -1,16 +1,19 @@ // // Created by josh on 7/28/21. +// Last updated: 11//10/22 // #include "pause.h" -#include "Core.h" -#include -#include +#include +#include #include #include -#include + #include +#include +#include +#include #include #include #include @@ -19,11 +22,17 @@ #include #include #include +#include -#include -#include +#include #include #include +#include + +// 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 job_tracker; -std::map 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 & 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 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 units; + static auto add_if = [&](std::function 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 ranges{}; + std::array 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 bw{23,17,13,7,1}; // probability weights for each range + std::vector i; + std::vector 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 &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 & parameters) { if (!parameters.empty()) { if (parameters.size() >= 2 && parameters.size() <= 3) { @@ -260,14 +449,18 @@ command_result spectate (color_ostream &out, std::vector & 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 & 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 dwarves; - for (auto unit: df::global::world->units.active) { - if (!Units::isCitizen(unit)) { - continue; - } - dwarves.push_back(unit); - } - std::uniform_int_distribution 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 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; - } -} diff --git a/robots.txt b/robots.txt new file mode 100644 index 000000000..15dc7919e --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * + +Allow: /en/stable/ + +Sitemap: https://docs.dfhack.org/sitemap.xml diff --git a/scripts b/scripts index 020f2466b..727e4921c 160000 --- a/scripts +++ b/scripts @@ -1 +1 @@ -Subproject commit 020f2466bc4462e59c1c16c036881907cad9718e +Subproject commit 727e4921c00e260d7c8d1112daf77115ce3960ee diff --git a/test/library/gui/widgets.lua b/test/library/gui/widgets.lua index 95dbd34f1..51622e691 100644 --- a/test/library/gui/widgets.lua +++ b/test/library/gui/widgets.lua @@ -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