Merge branch 'develop' into reenable-spectatte

develop
Myk 2023-10-03 10:50:24 -07:00 committed by GitHub
commit 726828af2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1050 additions and 492 deletions

@ -12,45 +12,45 @@ This release is compatible with all distributions of Dwarf Fortress: [Steam](htt
Please report any issues (or feature requests) on the DFHack [GitHub issue tracker](https://github.com/DFHack/dfhack/issues). When reporting issues, please upload a zip file of your savegame and a zip file of your `mods` directory to the cloud and add links to the GitHub issue. Make sure your files are downloadable by "everyone with the link". We need your savegame to reproduce the problem and test the fix, and we need your active mods so we can load your savegame. Issues with savegames and mods attached get fixed first!
Announcements
Highlights
----------------------------------
<details>
<summary>Annc 1, PSAs</summary>
### Annc 1
<summary>Highlight 1, Highlight 2</summary>
Text
### Highlight 1
### PSAs
Demo screenshot/vidcap
As always, remember that, just like the vanilla DF game, DFHack tools can also have bugs. It is a good idea to **save often and keep backups** of the forts that you care about.
Text
Many DFHack tools that worked in previous (pre-Steam) versions of DF have not been updated yet and are marked with the "unavailable" tag in their docs. If you try to run them, they will show a warning and exit immediately. You can run the command again to override the warning (though of course the tools may not work). We make no guarantees of reliability for the tools that are marked as "unavailable".
### Highlight 2
The in-game interface for running DFHack commands (`gui/launcher`) will not show "unavailable" tools by default. You can still run them if you know their names, or you can turn on dev mode by hitting Ctrl-D while in `gui/launcher` and they will be added to the autocomplete list. Some tools do not compile yet and are not available at all, even when in dev mode.
Demo screenshot/vidcap
If you see a tool complaining about the lack of a cursor, know that it's referring to the **keyboard** cursor (which used to be the only real option in Dwarf Fortress). You can enable the keyboard cursor by entering mining mode or selecting the dump/forbid tool and hitting Alt-K (the DFHack keybinding for `toggle-kbd-cursor`. We're working on making DFHack tools more mouse-aware and accessible so this step isn't necessary in the future.
Text
</details>
Highlights
Announcements
----------------------------------
<details>
<summary>Highlight 1, Highlight 2</summary>
### Highlight 1
<summary>Annc 1, PSAs</summary>
Demo screenshot/vidcap
### Annc 1
Text
### Highlight 2
### PSAs
Demo screenshot/vidcap
As always, remember that, just like the vanilla DF game, DFHack tools can also have bugs. It is a good idea to **save often and keep backups** of the forts that you care about.
Text
Many DFHack tools that worked in previous (pre-Steam) versions of DF have not been updated yet and are marked with the "unavailable" tag in their docs. If you try to run them, they will show a warning and exit immediately. You can run the command again to override the warning (though of course the tools may not work). We make no guarantees of reliability for the tools that are marked as "unavailable".
The in-game interface for running DFHack commands (`gui/launcher`) will not show "unavailable" tools by default. You can still run them if you know their names, or you can turn on dev mode by hitting Ctrl-D while in `gui/launcher` and they will be added to the autocomplete list. Some tools do not compile yet and are not available at all, even when in dev mode.
If you see a tool complaining about the lack of a cursor, know that it's referring to the **keyboard** cursor (which used to be the only real option in Dwarf Fortress). You can enable the keyboard cursor by entering mining mode or selecting the dump/forbid tool and hitting Alt-K (the DFHack keybinding for `toggle-kbd-cursor`. We're working on making DFHack tools more mouse-aware and accessible so this step isn't necessary in the future.
</details>
@ -61,5 +61,4 @@ Generated release notes
<summary>New tools, fixes, and improvements</summary>
%RELEASE_NOTES%
</details>

@ -67,19 +67,25 @@ jobs:
compiler: msvc
plugins: "default"
config: "empty"
# TODO: uncomment once we have a linux build we can download from bay12
# - os: ubuntu
# compiler: gcc-10
# plugins: "default"
# config: "default"
# - os: ubuntu
# compiler: gcc-12
# plugins: "all"
# config: "default"
- os: ubuntu
compiler: gcc-10
plugins: "default"
config: "default"
- os: ubuntu
compiler: gcc-12
plugins: "all"
config: "default"
steps:
- name: Set env
shell: bash
run: echo "DF_FOLDER=DF" >> $GITHUB_ENV
- name: Install dependencies
if: matrix.os == 'ubuntu'
run: |
sudo apt-get update
sudo apt-get install \
libsdl2-2.0-0 \
libsdl2-image-2.0-0
- name: Clone DFHack
uses: actions/checkout@v3
with:
@ -117,8 +123,14 @@ jobs:
- name: Install DFHack
shell: bash
run: tar xjf test-${{ matrix.compiler }}.tar.bz2 -C ${{ env.DF_FOLDER }}
- name: Start X server
if: matrix.os == 'ubuntu'
run: Xvfb :0 -screen 0 1600x1200x24 &
- name: Run lua tests
timeout-minutes: 10
env:
DISPLAY: :0
TERM: xterm-256color
run: python ci/run-tests.py --keep-status "${{ env.DF_FOLDER }}"
- name: Check RPC interface
run: python ci/check-rpc.py "${{ env.DF_FOLDER }}/dfhack-rpc.txt"

@ -20,7 +20,7 @@ repos:
args: ['--fix=lf']
- id: trailing-whitespace
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.26.3
rev: 0.27.0
hooks:
- id: check-github-workflows
- repo: https://github.com/Lucas-C/pre-commit-hooks

@ -7,7 +7,7 @@ cmake_policy(SET CMP0074 NEW)
project(dfhack)
# set up versioning.
set(DF_VERSION "50.10")
set(DF_VERSION "50.11")
set(DFHACK_RELEASE "r1")
set(DFHACK_PRERELEASE FALSE)

@ -18,7 +18,7 @@ elif test "$OS_TARGET" = "ubuntu"; then
WGET=wget
df_url="${df_url}_linux.tar.bz2"
df_archive_name="df.tar.bz2"
df_extract_cmd="tar -x -j --strip-components=1 -f"
df_extract_cmd="tar -x -j -C ${DF_FOLDER} -f"
else
echo "Unhandled OS target: ${OS_TARGET}"
exit 1
@ -29,22 +29,25 @@ if ! $WGET -v "$df_url" -O "$df_archive_name"; then
exit 1
fi
md5sum "$df_archive_name"
save_url="https://dffd.bay12games.com/download.php?id=15434&f=dreamfort.7z"
save_archive_name="test_save.7z"
save_extract_cmd="7z x -oDF/save"
save_extract_cmd="7z x -o${DF_FOLDER}/save"
if ! $WGET -v "$save_url" -O "$save_archive_name"; then
echo "Failed to download test save from $save_url"
exit 1
fi
md5sum "$save_archive_name"
echo Extracting
mkdir -p ${DF_FOLDER}
$df_extract_cmd "$df_archive_name"
$save_extract_cmd "$save_archive_name"
mv DF/save/* DF/save/region1
mv ${DF_FOLDER}/save/* ${DF_FOLDER}/save/region1
echo Done
ls -l
md5sum "$df_archive_name" "$save_archive_name"

@ -65,14 +65,12 @@ if not os.path.exists(init_txt_path):
shutil.copyfile(init_txt_path, init_txt_path + '.orig')
with open(init_txt_path) as f:
init_contents = f.read()
init_contents = change_setting(init_contents, 'INTRO', 'NO')
init_contents = change_setting(init_contents, 'SOUND', 'NO')
init_contents = change_setting(init_contents, 'WINDOWED', 'YES')
init_contents = change_setting(init_contents, 'WINDOWEDX', '80')
init_contents = change_setting(init_contents, 'WINDOWEDY', '25')
init_contents = change_setting(init_contents, 'FPS', 'YES')
if args.headless:
init_contents = change_setting(init_contents, 'PRINT_MODE', 'TEXT')
init_contents = change_setting(init_contents, 'WINDOWEDX', '1200')
init_contents = change_setting(init_contents, 'WINDOWEDY', '800')
#if args.headless:
# init_contents = change_setting(init_contents, 'PRINT_MODE', 'TEXT')
init_path = 'dfhack-config/init'
if not os.path.isdir('hack/init'):

@ -2,6 +2,7 @@
--@ module = true
local expect = require('test_util.expect')
local gui = require('gui')
local helpdb = require('helpdb')
local json = require('json')
local mock = require('test_util.mock')
@ -151,33 +152,37 @@ end
test_envvars.require = clean_require
test_envvars.reqscript = clean_reqscript
local function is_title_screen(scr)
scr = scr or dfhack.gui.getCurViewscreen()
return df.viewscreen_titlest:is_instance(scr)
local function is_title_screen()
return dfhack.gui.matchFocusString('title/Default')
end
-- This only handles pre-fortress-load screens. It will time out if the player
-- has already loaded a fortress or is in any screen that can't get to the title
-- screen by sending ESC keys.
local function ensure_title_screen()
local function wait_for(ms, desc, predicate)
local start_ms = dfhack.getTickCount()
local prev_ms = start_ms
while df.viewscreen_initial_prepst:is_instance(dfhack.gui.getCurViewscreen()) do
while not predicate() do
delay(10)
-- wait up to 1 minute for the game to load and show the title screen
local now_ms = dfhack.getTickCount()
if now_ms - start_ms > 60000 then
qerror(('Could not find title screen (timed out at %s)'):format(
dfhack.gui.getCurFocus(true)[1]))
if now_ms - start_ms > ms then
qerror(('%s took too long (timed out at %s)'):format(
desc, dfhack.gui.getCurFocus(true)[1]))
end
if now_ms - prev_ms > 1000 then
print('Waiting for game to load and show title screen...')
print(('Waiting for %s...'):format(desc))
prev_ms = now_ms
end
end
end
-- This only handles pre-fortress-load screens. It will time out if the player
-- has already loaded a fortress or is in any screen that can't get to the title
-- screen by sending ESC keys.
local function ensure_title_screen()
if df.viewscreen_dwarfmodest:is_instance(dfhack.gui.getDFViewscreen(true)) then
qerror('Cannot reach title screen from loaded fort')
end
for i = 1, 100 do
local scr = dfhack.gui.getCurViewscreen()
if is_title_screen(scr) then
if is_title_screen() then
print('Found title screen')
return
end
@ -189,54 +194,91 @@ local function ensure_title_screen()
dfhack.gui.getCurFocus(true)[1]))
end
local function is_fortress(focus_string)
focus_string = focus_string or dfhack.gui.getCurFocus(true)
return focus_string == 'dwarfmode/Default'
local function is_fortress()
return dfhack.gui.matchFocusString('dwarfmode/Default')
end
-- error out if we're not running in a CI environment
-- the tests may corrupt saves, and we don't want to unexpectedly ruin a real player save
-- this heuristic is not perfect, but it should be able to detect most cases
local function ensure_ci_save(scr)
if #scr.savegame_header ~= 1
or #scr.savegame_header_world ~= 1
or not string.find(scr.savegame_header[0].fort_name, 'Dream')
then
qerror('Unexpected test save in slot 0; please manually load a fort for ' ..
'running fortress mode tests. note that tests may alter or corrupt the ' ..
'fort! Do not save after running tests.')
end
end
local function click_top_title_button(scr)
local sw, sh = dfhack.screen.getWindowSize()
df.global.gps.mouse_x = sw // 2
df.global.gps.precise_mouse_x = df.global.gps.mouse_x * df.global.gps.tile_pixel_x
if sh < 60 then
df.global.gps.mouse_y = 25
else
df.global.gps.mouse_y = (sh // 2) + 3
end
df.global.gps.precise_mouse_y = df.global.gps.mouse_y * df.global.gps.tile_pixel_y
gui.simulateInput(scr, '_MOUSE_L')
end
local function load_first_save(scr)
if #scr.savegame_header == 0 then
qerror('no savegames available to load')
end
click_top_title_button(scr)
wait_for(1000, 'world list', function()
return scr.mode == 2
end)
click_top_title_button(scr)
wait_for(1000, 'savegame list', function()
return scr.mode == 3
end)
click_top_title_button(scr)
wait_for(1000, 'loadgame progress bar', function()
return dfhack.gui.matchFocusString('loadgame')
end)
end
-- Requires that a fortress game is already loaded or is ready to be loaded via
-- the "Continue Playing" option in the title screen. Otherwise the function
-- the "Continue active game" option in the title screen. Otherwise the function
-- will time out and/or exit with error.
local function ensure_fortress(config)
local focus_string = dfhack.gui.getCurFocus(true)
for screen_timeout = 1,10 do
if is_fortress(focus_string) then
print('Loaded fortress map')
if is_fortress() then
print('Fortress map is loaded')
-- pause the game (if it's not already paused)
dfhack.gui.resetDwarfmodeView(true)
return
end
local scr = dfhack.gui.getCurViewscreen(true)
if focus_string == 'title' or
focus_string == 'dfhack/lua/load_screen' then
local scr = dfhack.gui.getCurViewscreen()
if dfhack.gui.matchFocusString('title/Default', scr) then
print('Attempting to load the test fortress')
-- TODO: reinstate loading of a specified save dir; for now
-- just load the first possible save, which will at least let us
-- run fortress tests in CI
-- qerror()'s on falure
dfhack.run_script('load-save', config.save_dir)
elseif focus_string ~= 'loadgame' then
-- dfhack.run_script('load-save', config.save_dir)
ensure_ci_save(scr)
load_first_save(scr)
elseif not dfhack.gui.matchFocusString('loadgame', scr) then
-- if we're not actively loading a game, hope we're in
-- a screen where hitting ESC will get us to the game map
-- or the title screen
scr:feed_key(df.interface_key.LEAVESCREEN)
end
-- wait for current screen to change
local prev_focus_string = focus_string
for frame_timeout = 1,100 do
delay(10)
focus_string = dfhack.gui.getCurFocus(true)
if focus_string ~= prev_focus_string then
goto next_screen
end
if frame_timeout % 10 == 0 then
print(string.format(
'Loading fortress (currently at screen: %s)',
focus_string))
end
end
print('Timed out waiting for screen to change')
break
::next_screen::
local prev_focus_string = dfhack.gui.getCurFocus()[1]
wait_for(60000, 'screen change', function()
return dfhack.gui.getCurFocus()[1] ~= prev_focus_string
end)
end
qerror(string.format('Could not load fortress (timed out at %s)',
focus_string))
table.concat(dfhack.gui.getCurFocus(), ' ')))
end
local MODES = {
@ -578,6 +620,10 @@ local function filter_tests(tests, config)
end
local function run_tests(tests, status, counts, config)
wait_for(60000, 'game load', function()
local scr = dfhack.gui.getDFViewscreen()
return not df.viewscreen_initial_prepst:is_instance(scr)
end)
print(('Running %d tests'):format(#tests))
local start_ms = dfhack.getTickCount()
local num_skipped = 0
@ -596,12 +642,13 @@ local function run_tests(tests, status, counts, config)
goto skip
end
end
-- pre-emptively mark the test as failed in case we induce a crash
status[test.full_name] = TestStatus.FAILED
save_test_status(status)
if run_test(test, status, counts) then
status[test.full_name] = TestStatus.PASSED
else
status[test.full_name] = TestStatus.FAILED
end
save_test_status(status)
end
::skip::
end
local elapsed_ms = dfhack.getTickCount() - start_ms

@ -139,6 +139,7 @@ moversti moversti
mrrho mrrho
Murad Beybalaev Erquint
Myk Taylor myk002
Najeeb Al-Shabibi master-spike
napagokc napagokc
Neil Little nmlittle
Nick Rart nickrart comestible

@ -10,6 +10,13 @@ work (e.g. links from the `changelog`).
:local:
:depth: 1
.. _workorder-recheck:
workorder-recheck
=================
Tool to set 'Checking' status of the selected work order, allowing conditions to be
reevaluated. Merged into `orders`.
.. _autohauler:
autohauler

@ -68,12 +68,36 @@ Template for new versions:
## Removed
# 50.11-r1
## New Tools
- `tubefill`: (reinstated) replenishes mined-out adamantine
## Fixes
- `autolabor`: ensure vanilla work details are reinstated when the fort or the plugin is unloaded
- ``dfhack.TranslateName()``: fixed crash on certain invalid names, which affected `warn-starving`
- EventManager: Unit death event no longer misfires on units leaving the map
## Misc Improvements
- `digtype`: designate only visible tiles by default, and use "auto" dig mode for following veins
- `digtype`: added options for designating only current z-level, this z-level and above, and this z-level and below
- `hotkeys`: make the DFHack logo brighten on hover in ascii mode to indicate that it is clickable
- `hotkeys`: use vertical bars instead of "!" symbols for the DFHack logo in ascii mode to make it easier to read
- EventManager: guard against potential iterator invalidation if one of the event listeners were to modify the global data structure being iterated over
- EventManager: for ``onBuildingCreatedDestroyed`` events, changed firing order of events so destroyed events come before created events
## Lua
- mouse key events are now aligned with internal DF semantics: ``_MOUSE_L`` indicates that the left mouse button has just been pressed and ``_MOUSE_L_DOWN`` indicates that the left mouse button is being held down. similarly for ``_MOUSE_R`` and ``_MOUSE_M``. 3rd party scripts may have to adjust.
# 50.10-r1
## Fixes
- Linux launcher: allow Steam Overlay and game streaming to function
- `autobutcher`: don't ignore semi-wild units when marking units for slaughter
## Misc Improvements
- 'sort': Improve combat skill scale thresholds
# 50.09-r4
## New Features

@ -2528,10 +2528,10 @@ Supported callbacks and fields are:
Maps to an integer in range 0-255. Duplicates a separate "STRING_A???" code for convenience.
``_MOUSE_L, _MOUSE_R, _MOUSE_M``
If the left, right, and/or middle mouse button is being pressed.
If the left, right, and/or middle mouse button was just pressed.
``_MOUSE_L_DOWN, _MOUSE_R_DOWN, _MOUSE_M_DOWN``
If the left, right, and/or middle mouse button was just pressed.
If the left, right, and/or middle mouse button is being held down.
If this method is omitted, the screen is dismissed on reception of the ``LEAVESCREEN`` key.
@ -2594,11 +2594,18 @@ invalidates the ``texpos`` value that used to point to that texture.
The ``textures`` module solves this problem by providing a stable handle instead of a
raw ``texpos``. When we need to draw a particular tile, we can look up the current
``texpos`` value via the handle.
Texture module can register textures in two ways: to reserved and dynamic ranges.
Reserved range is a limit buffer in a game texture vector, that will never be wiped.
It is good for static assets, which need to be loaded at the very beginning and will be used during the process running.
In other cases, it is better to use dynamic range.
If reserved range buffer limit has been reached, dynamic range will be used by default.
* ``loadTileset(file, tile_px_w, tile_px_h)``
* ``loadTileset(file, tile_px_w, tile_px_h[, reserved])``
Loads a tileset from the image ``file`` with give tile dimensions in pixels. The
image will be sliced in row major order. Returns an array of ``TexposHandle``.
``reserved`` is optional boolean argument, which indicates texpos range.
``true`` - reserved, ``false`` - dynamic (default).
Example usage::
@ -2611,18 +2618,22 @@ raw ``texpos``. When we need to draw a particular tile, we can look up the curre
get the ``texpos`` for your texture. ``texpos`` can change when game textures are
reset, but the handle will be the same.
* ``createTile(pixels, tile_px_w, tile_px_h)``
* ``createTile(pixels, tile_px_w, tile_px_h[, reserved])``
Create and register a new texture with the given tile dimensions and an array of
``pixels`` in row major order. Each pixel is an integer representing color in packed
RBGA format (for example, #0022FF11). Returns a ``TexposHandle``.
``reserved`` is optional boolean argument, which indicates texpos range.
``true`` - reserved, ``false`` - dynamic (default).
* ``createTileset(pixels, texture_px_w, texture_px_h, tile_px_w, tile_px_h)``
* ``createTileset(pixels, texture_px_w, texture_px_h, tile_px_w, tile_px_h[, reserved])``
Create and register a new texture with the given texture dimensions and an array of
``pixels`` in row major order. Then slice it into tiles with the given tile
dimensions. Each pixel is an integer representing color in packed RBGA format (for
example #0022FF11). Returns an array of ``TexposHandle``.
``reserved`` is optional boolean argument, which indicates texpos range.
``true`` - reserved, ``false`` - dynamic (default).
* ``deleteHandle(handle)``
@ -3941,6 +3952,14 @@ Misc
of keycodes to *true* or *false*. For instance, it is possible to use the
table passed as argument to ``onInput``.
You can send mouse clicks as will by setting the ``_MOUSE_L`` key or other
mouse-related pseudo-keys documented with the ``screen:onInput(keys)``
function above. Note that if you are simulating a click at a specific spot on
the screen, you must set ``df.global.gps.mouse_x`` and
``df.global.gps.mouse_y`` if you are clicking on the interface layer or
``df.global.gps.precise_mouse_x`` and ``df.global.gps.precise_mouse_y`` if
you are clicking on the map.
* ``mkdims_xy(x1,y1,x2,y2)``
Returns a table containing the arguments as fields, and also ``width`` and

@ -50,7 +50,7 @@ Usage
Designate circles. The diameter is the number of tiles across the center of
the circle that you want to dig. See the `digcircle`_ section below for
options.
``digtype [<designation>] [-p<number>] [-z]``
``digtype [<designation>] [-p<number>] [--zup|-u] [--zdown|-zu] [--cur-zlevel|-z] [--hidden|-h] [--no-auto|-a]``
Designate all vein tiles of the same type as the selected tile. See the
`digtype`_ section below for options.
``digexp [<pattern>] [<filter>] [-p<number>]``
@ -119,9 +119,11 @@ the last selected parameters.
digtype
-------
For every tile on the map of the same vein type as the selected tile, this
command designates it to have the same designation as the selected tile. If the
selected tile has no designation, they will be dig designated.
For every tile on the map of the same vein type as the selected tile, this command
designates it to have the same designation as the selected tile. If the selected
tile has no designation, they will be dig designated. By default, only designates
visible tiles, and in the case of dig designation, applies automatic mining to them
(designates uncovered neighbouring tiles of the same type to be dug).
If an argument is given, the designation of the selected tile is ignored, and
all appropriate tiles are set to the specified designation.
@ -143,9 +145,18 @@ Designation options:
``clear``
Clear any designations.
You can also pass a ``-z`` option, which restricts designations to the current
z-level and down. This is useful when you don't want to designate tiles on the
same z-levels as your carefully dug fort above.
Other options:
``--zdown``, ``-d``
Only designates tiles on the cursor's z-level and below.
``--zup``, ``-u``
Only designates tiles on the cursor's z-level and above.
``--cur-zlevel``, ``-z``
Only designates tiles on the same z-level as the cursor.
``--hidden``, ``-h``
Allows designation of hidden tiles, and picking a hidden tile as the target type.
``--no-auto``, ``-a``
No automatic mining mode designation - useful if you want to avoid dwarves digging where you don't want them.
digexp
------

@ -3,7 +3,7 @@ embark-tools
.. dfhack-tool::
:summary: Extend the embark screen functionality.
:tags: unavailable embark fort interface
Usage
-----

@ -17,6 +17,13 @@ Usage
manager orders. It will not clear the orders that already exist.
``orders clear``
Deletes all manager orders in the current embark.
``orders recheck [this]``
Sets the status to ``Checking`` (from ``Active``) for all work orders. if the
"this" option is passed, only sets the status for the workorder whose condition
details page is open. This makes the manager reevaluate its conditions.
This is especially useful for an order that had its conditions met when it
was started, but the requisite items have since disappeared and the workorder
is now generating job cancellation spam.
``orders sort``
Sorts current manager orders by repeat frequency so repeating orders don't
prevent one-time orders from ever being completed. The sorting order is:

@ -3,9 +3,13 @@ tubefill
.. dfhack-tool::
:summary: Replenishes mined-out adamantine.
:tags: unavailable fort armok map
:tags: fort armok map
Veins that were originally hollow will be left alone.
This tool replaces mined-out tiles of adamantine spires with fresh, undug
adamantine walls, ready to be re-harvested. Empty tiles within the spire that
used to contain special gemstones, obsidian, water, or magma will also be
replaced with fresh adamantine. Adamantine spires that were originally hollow
will be left hollow. See below for more details.
Usage
-----

@ -1760,7 +1760,8 @@ static int textures_loadTileset(lua_State *state)
std::string file = luaL_checkstring(state, 1);
auto tile_w = luaL_checkint(state, 2);
auto tile_h = luaL_checkint(state, 3);
auto handles = Textures::loadTileset(file, tile_w, tile_h);
bool reserved = lua_isboolean(state, 4) ? lua_toboolean(state, 4) : false;
auto handles = Textures::loadTileset(file, tile_w, tile_h, reserved);
Lua::PushVector(state, handles);
return 1;
}
@ -1798,7 +1799,8 @@ static int textures_createTile(lua_State *state)
Lua::GetVector(state, pixels);
auto tile_w = luaL_checkint(state, 2);
auto tile_h = luaL_checkint(state, 3);
auto handle = Textures::createTile(pixels, tile_w, tile_h);
bool reserved = lua_isboolean(state, 4) ? lua_toboolean(state, 4) : false;
auto handle = Textures::createTile(pixels, tile_w, tile_h, reserved);
Lua::Push(state, handle);
return 1;
}
@ -1811,7 +1813,8 @@ static int textures_createTileset(lua_State *state)
auto texture_h = luaL_checkint(state, 3);
auto tile_w = luaL_checkint(state, 4);
auto tile_h = luaL_checkint(state, 5);
auto handles = Textures::createTileset(pixels, texture_w, texture_h, tile_w, tile_h);
bool reserved = lua_isboolean(state, 6) ? lua_toboolean(state, 6) : false;
auto handles = Textures::createTileset(pixels, texture_w, texture_h, tile_w, tile_h, reserved);
Lua::PushVector(state, handles);
return 1;
}

@ -131,12 +131,12 @@ void DFHack::Lua::GetVector(lua_State *state, std::vector<std::string> &pvec, in
}
}
static bool trigger_inhibit_l_down = false;
static bool trigger_inhibit_r_down = false;
static bool trigger_inhibit_m_down = false;
static bool inhibit_l_down = false;
static bool inhibit_r_down = false;
static bool inhibit_m_down = false;
static bool trigger_inhibit_l = false;
static bool trigger_inhibit_r = false;
static bool trigger_inhibit_m = false;
static bool inhibit_l = false;
static bool inhibit_r = false;
static bool inhibit_m = false;
void DFHack::Lua::PushInterfaceKeys(lua_State *L,
const std::set<df::interface_key> &keys) {
@ -161,32 +161,32 @@ void DFHack::Lua::PushInterfaceKeys(lua_State *L,
}
if (df::global::enabler) {
if (!inhibit_l_down && df::global::enabler->mouse_lbut_down) {
if (!inhibit_l && df::global::enabler->mouse_lbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_L_DOWN");
trigger_inhibit_l_down = true;
lua_setfield(L, -2, "_MOUSE_L");
trigger_inhibit_l = true;
}
if (!inhibit_r_down && df::global::enabler->mouse_rbut_down) {
if (!inhibit_r && df::global::enabler->mouse_rbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_R_DOWN");
trigger_inhibit_r_down = true;
lua_setfield(L, -2, "_MOUSE_R");
trigger_inhibit_r = true;
}
if (!inhibit_m_down && df::global::enabler->mouse_mbut_down) {
if (!inhibit_m && df::global::enabler->mouse_mbut) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_M_DOWN");
trigger_inhibit_m_down = true;
lua_setfield(L, -2, "_MOUSE_M");
trigger_inhibit_m = true;
}
if (df::global::enabler->mouse_lbut) {
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) {
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_mbut) {
if (df::global::enabler->mouse_mbut_down) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "_MOUSE_M");
lua_setfield(L, -2, "_MOUSE_M_DOWN");
}
}
}
@ -2159,25 +2159,25 @@ void DFHack::Lua::Core::Reset(color_ostream &out, const char *where)
lua_settop(State, 0);
}
if (trigger_inhibit_l_down) {
trigger_inhibit_l_down = false;
inhibit_l_down = true;
if (trigger_inhibit_l) {
trigger_inhibit_l = false;
inhibit_l = true;
}
if (trigger_inhibit_r_down) {
trigger_inhibit_r_down = false;
inhibit_r_down = true;
if (trigger_inhibit_r) {
trigger_inhibit_r = false;
inhibit_r = true;
}
if (trigger_inhibit_m_down) {
trigger_inhibit_m_down = false;
inhibit_m_down = true;
if (trigger_inhibit_m) {
trigger_inhibit_m = false;
inhibit_m = true;
}
if (df::global::enabler) {
if (!df::global::enabler->mouse_lbut)
inhibit_l_down = false;
if (!df::global::enabler->mouse_rbut)
inhibit_r_down = false;
if (!df::global::enabler->mouse_mbut)
inhibit_m_down = false;
if (!df::global::enabler->mouse_lbut_down)
inhibit_l = false;
if (!df::global::enabler->mouse_rbut_down)
inhibit_r = false;
if (!df::global::enabler->mouse_mbut_down)
inhibit_m = false;
}
}

@ -391,6 +391,7 @@ namespace DFHack
virtual ~dfhack_lua_viewscreen();
static df::viewscreen *get_pointer(lua_State *L, int idx, bool make);
static void markInputAsHandled();
virtual bool is_lua_screen() { return true; }
virtual bool isFocused() { return !defocused; }

@ -26,7 +26,7 @@ const uint32_t TILE_HEIGHT_PX = 12;
* Load texture and get handle.
* Keep it to obtain valid texpos.
*/
DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface);
DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface, bool reserved = false);
/**
* Load tileset from image file.
@ -34,7 +34,8 @@ DFHACK_EXPORT TexposHandle loadTexture(SDL_Surface* surface);
*/
DFHACK_EXPORT std::vector<TexposHandle> loadTileset(const std::string& file,
int tile_px_w = TILE_WIDTH_PX,
int tile_px_h = TILE_HEIGHT_PX);
int tile_px_h = TILE_HEIGHT_PX,
bool reserved = false);
/**
* Get texpos by handle.
@ -53,7 +54,7 @@ DFHACK_EXPORT void deleteHandle(TexposHandle handle);
* Register this texture and return TexposHandle.
*/
DFHACK_EXPORT TexposHandle createTile(std::vector<uint32_t>& pixels, int tile_px_w = TILE_WIDTH_PX,
int tile_px_h = TILE_HEIGHT_PX);
int tile_px_h = TILE_HEIGHT_PX, bool reserved = false);
/**
* Create new textures as tileset with RGBA32 format and pixels as data in row major order.
@ -62,7 +63,8 @@ DFHACK_EXPORT TexposHandle createTile(std::vector<uint32_t>& pixels, int tile_px
DFHACK_EXPORT std::vector<TexposHandle> createTileset(std::vector<uint32_t>& pixels,
int texture_px_w, int texture_px_h,
int tile_px_w = TILE_WIDTH_PX,
int tile_px_h = TILE_HEIGHT_PX);
int tile_px_h = TILE_HEIGHT_PX,
bool reserved = false);
/**
* Call this on DFHack init just once to setup interposed handlers and

@ -15,8 +15,8 @@ TRANSPARENT_PEN = to_pen{tile=0, ch=0}
KEEP_LOWER_PEN = to_pen{ch=32, fg=0, bg=0, keep_lower=true}
local MOUSE_KEYS = {
_MOUSE_L = true,
_MOUSE_R = true,
_MOUSE_L = function(is_set) df.global.enabler.mouse_lbut = is_set and 1 or 0 end,
_MOUSE_R = function(is_set) df.global.enabler.mouse_rbut = is_set and 1 or 0 end,
_MOUSE_M = true,
_MOUSE_L_DOWN = true,
_MOUSE_R_DOWN = true,
@ -27,7 +27,7 @@ local FAKE_INPUT_KEYS = copyall(MOUSE_KEYS)
FAKE_INPUT_KEYS._STRING = true
function simulateInput(screen,...)
local keys = {}
local keys, enabled_mouse_keys = {}, {}
local function push_key(arg)
local kv = arg
if type(arg) == 'string' then
@ -35,6 +35,10 @@ function simulateInput(screen,...)
if kv == nil and not FAKE_INPUT_KEYS[arg] then
error('Invalid keycode: '..arg)
end
if MOUSE_KEYS[arg] then
df.global.enabler.tracking_on = 1
enabled_mouse_keys[arg] = true
end
end
if type(kv) == 'number' then
keys[#keys+1] = kv
@ -57,6 +61,11 @@ function simulateInput(screen,...)
end
end
end
for mk, fn in pairs(MOUSE_KEYS) do
if type(fn) == 'function' then
fn(enabled_mouse_keys[mk])
end
end
dscreen._doSimulateInput(screen, keys)
end
@ -696,21 +705,6 @@ end
DEFAULT_INITIAL_PAUSE = true
local zscreen_inhibit_mouse_l = false
-- ensure underlying DF screens don't also react to handled clicks
function markMouseClicksHandled(keys)
if keys._MOUSE_L_DOWN then
-- note we can't clear mouse_lbut here. otherwise we break dragging,
df.global.enabler.mouse_lbut_down = 0
zscreen_inhibit_mouse_l = true
end
if keys._MOUSE_R_DOWN then
df.global.enabler.mouse_rbut_down = 0
df.global.enabler.mouse_rbut = 0
end
end
ZScreen = defclass(ZScreen, Screen)
ZScreen.ATTRS{
defocusable=true,
@ -789,37 +783,24 @@ function ZScreen:onInput(keys)
local has_mouse = self:isMouseOver()
if not self:hasFocus() then
if has_mouse and
(keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or
(keys._MOUSE_L or keys._MOUSE_R or
keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or
keys.CONTEXT_SCROLL_PAGEUP or keys.CONTEXT_SCROLL_PAGEDOWN) then
self:raise()
else
self:sendInputToParent(keys)
return
return true
end
end
if ZScreen.super.onInput(self, keys) then
markMouseClicksHandled(keys)
return
end
if self.pass_mouse_clicks and keys._MOUSE_L_DOWN and not has_mouse then
-- noop
elseif self.pass_mouse_clicks and keys._MOUSE_L and not has_mouse then
self.defocused = self.defocusable
self:sendInputToParent(keys)
return
elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
elseif keys.LEAVESCREEN or keys._MOUSE_R then
self:dismiss()
markMouseClicksHandled(keys)
return
else
if zscreen_inhibit_mouse_l then
if keys._MOUSE_L then
return
else
zscreen_inhibit_mouse_l = false
end
end
local passit = self.pass_pause and keys.D_PAUSE
if not passit and self.pass_mouse_clicks then
if keys.CONTEXT_SCROLL_UP or keys.CONTEXT_SCROLL_DOWN or
@ -840,8 +821,8 @@ function ZScreen:onInput(keys)
if passit then
self:sendInputToParent(keys)
end
return
end
return true
end
function ZScreen:raise()

@ -4,8 +4,6 @@ local _ENV = mkmodule('gui.buildings')
local gui = require('gui')
local widgets = require('gui.widgets')
local dlg = require('gui.dialogs')
local utils = require('utils')
ARROW = string.char(26)

@ -57,14 +57,13 @@ function MessageBox:onDestroy()
end
function MessageBox:onInput(keys)
if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R then
self:dismiss()
if keys.SELECT and self.on_accept then
self.on_accept()
elseif (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) and self.on_cancel then
elseif (keys.LEAVESCREEN or keys._MOUSE_R) and self.on_cancel then
self.on_cancel()
end
gui.markMouseClicksHandled(keys)
return true
end
return self:inputToSubviews(keys)
@ -129,12 +128,11 @@ function InputBox:onInput(keys)
self.on_input(self.subviews.edit.text)
end
return true
elseif keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
elseif keys.LEAVESCREEN or keys._MOUSE_R then
self:dismiss()
if self.on_cancel then
self.on_cancel()
end
gui.markMouseClicksHandled(keys)
return true
end
return self:inputToSubviews(keys)
@ -231,12 +229,11 @@ function ListBox:getWantedFrameSize()
end
function ListBox:onInput(keys)
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if keys.LEAVESCREEN or keys._MOUSE_R then
self:dismiss()
if self.on_cancel then
self.on_cancel()
end
gui.markMouseClicksHandled(keys)
return true
end
return self:inputToSubviews(keys)

@ -5,7 +5,6 @@ local _ENV = mkmodule('gui.materials')
local gui = require('gui')
local widgets = require('gui.widgets')
local dlg = require('gui.dialogs')
local utils = require('utils')
ARROW = string.char(26)

@ -6,18 +6,18 @@ local _ENV = mkmodule('gui.textures')
-- Preloaded DFHack Assets.
-- Use this handles if you need to get dfhack standard textures.
---@type table<string, TexposHandle>
---@type table<string, TexposHandle[]>
local texpos_handles = {
green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12),
red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12),
icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12),
on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12),
control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12),
border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12),
border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12),
border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12),
border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12),
border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12),
green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12, true),
red_pin = dfhack.textures.loadTileset('hack/data/art/red-pin.png', 8, 12, true),
icons = dfhack.textures.loadTileset('hack/data/art/icons.png', 8, 12, true),
on_off = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true),
control_panel = dfhack.textures.loadTileset('hack/data/art/control-panel.png', 8, 12, true),
border_thin = dfhack.textures.loadTileset('hack/data/art/border-thin.png', 8, 12, true),
border_medium = dfhack.textures.loadTileset('hack/data/art/border-medium.png', 8, 12, true),
border_bold = dfhack.textures.loadTileset('hack/data/art/border-bold.png', 8, 12, true),
border_panel = dfhack.textures.loadTileset('hack/data/art/border-panel.png', 8, 12, true),
border_window = dfhack.textures.loadTileset('hack/data/art/border-window.png', 8, 12, true),
}
-- Get valid texpos for preloaded texture in tileset

@ -273,7 +273,7 @@ end
function Panel:onInput(keys)
if self.kbd_get_pos then
if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if keys.SELECT or keys.LEAVESCREEN or keys._MOUSE_R then
Panel_end_drag(self, not keys.SELECT and self.saved_frame or nil,
not not keys.SELECT)
return true
@ -281,7 +281,6 @@ function Panel:onInput(keys)
for code in pairs(keys) do
local dx, dy = guidm.get_movement_delta(code, 1, 10)
if dx then
local frame_rect = self.frame_rect
local kbd_pos = self.kbd_get_pos()
kbd_pos.x = kbd_pos.x + dx
kbd_pos.y = kbd_pos.y + dy
@ -292,9 +291,9 @@ function Panel:onInput(keys)
return
end
if self.drag_offset then
if keys._MOUSE_R_DOWN then
if keys._MOUSE_R then
Panel_end_drag(self, self.saved_frame)
elseif keys._MOUSE_L then
elseif keys._MOUSE_L_DOWN then
Panel_update_frame(self, Panel_make_frame(self))
end
return true
@ -302,7 +301,7 @@ function Panel:onInput(keys)
if Panel.super.onInput(self, keys) then
return true
end
if not keys._MOUSE_L_DOWN then return end
if not keys._MOUSE_L then return end
local x,y = self:getMouseFramePos()
if not x then return end
@ -489,7 +488,7 @@ function Panel:onRenderFrame(dc, rect)
dc:seek(pos.x, pos.y):pen(pen):char(string.char(0xDB))
end
if self.drag_offset and not self.kbd_get_pos
and df.global.enabler.mouse_lbut == 0 then
and df.global.enabler.mouse_lbut_down == 0 then
Panel_end_drag(self, nil, true)
end
end
@ -718,7 +717,7 @@ function EditField:onInput(keys)
end
end
if self.key and (keys.LEAVESCREEN or keys._MOUSE_R_DOWN) then
if self.key and (keys.LEAVESCREEN or keys._MOUSE_R) then
self:setText(self.saved_text)
self:setFocus(false)
return true
@ -740,8 +739,8 @@ function EditField:onInput(keys)
end
end
return not not self.key
elseif keys._MOUSE_L then
local mouse_x, mouse_y = self:getMousePos()
elseif keys._MOUSE_L_DOWN then
local mouse_x = self:getMousePos()
if mouse_x then
self:setCursor(self.start_pos + mouse_x - (self.text_offset or 0))
return true
@ -986,7 +985,7 @@ function Scrollbar:onRenderBody(dc)
if self.is_dragging then
scrollbar_do_drag(self)
end
if df.global.enabler.mouse_lbut == 0 then
if df.global.enabler.mouse_lbut_down == 0 then
self.last_scroll_ms = 0
self.is_dragging = false
self.scroll_spec = nil
@ -1023,7 +1022,7 @@ function Scrollbar:onInput(keys)
return true
end
end
if not keys._MOUSE_L_DOWN then return false end
if not keys._MOUSE_L then return false end
local _,y = self:getMousePos()
if not y then return false end
local scroll_spec = nil
@ -1386,11 +1385,11 @@ function Label:onInput(keys)
if self:inputToSubviews(keys) then
return true
end
if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then
if keys._MOUSE_L and self:getMousePos() and self.on_click then
self.on_click()
return true
end
if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then
if keys._MOUSE_R and self:getMousePos() and self.on_rclick then
self.on_rclick()
return true
end
@ -1498,7 +1497,7 @@ end
function HotkeyLabel:onInput(keys)
if HotkeyLabel.super.onInput(self, keys) then
return true
elseif keys._MOUSE_L_DOWN and self:getMousePos() and self.on_activate
elseif keys._MOUSE_L and self:getMousePos() and self.on_activate
and not is_disabled(self) then
self.on_activate()
return true
@ -1658,7 +1657,7 @@ end
function CycleHotkeyLabel:onInput(keys)
if CycleHotkeyLabel.super.onInput(self, keys) then
return true
elseif keys._MOUSE_L_DOWN and self:getMousePos() and not is_disabled(self) then
elseif keys._MOUSE_L and self:getMousePos() and not is_disabled(self) then
self:cycle()
return true
end
@ -1962,7 +1961,7 @@ function List:onInput(keys)
return self:submit()
elseif keys.CUSTOM_SHIFT_ENTER then
return self:submit2()
elseif keys._MOUSE_L_DOWN then
elseif keys._MOUSE_L then
local idx = self:getIdxUnderMouse()
if idx then
local now_ms = dfhack.getTickCount()
@ -2317,7 +2316,7 @@ end
function Tab:onInput(keys)
if Tab.super.onInput(self, keys) then return true end
if keys._MOUSE_L_DOWN and self:getMousePos() then
if keys._MOUSE_L and self:getMousePos() then
self.on_select(self.id)
return true
end
@ -2419,7 +2418,7 @@ local function rangeslider_get_width_per_idx(self)
end
function RangeSlider:onInput(keys)
if not keys._MOUSE_L_DOWN then return false end
if not keys._MOUSE_L then return false end
local x = self:getMousePos()
if not x then return false end
local left_idx, right_idx = self.get_left_idx_fn(), self.get_right_idx_fn()
@ -2527,7 +2526,7 @@ function RangeSlider:onRenderBody(dc, rect)
if self.is_dragging_target then
rangeslider_do_drag(self, width_per_idx)
end
if df.global.enabler.mouse_lbut == 0 then
if df.global.enabler.mouse_lbut_down == 0 then
self.is_dragging_target = nil
self.is_dragging_idx = nil
end

@ -43,6 +43,7 @@
#include <unordered_map>
#include <unordered_set>
#include <array>
#include <utility>
namespace DFHack {
DBG_DECLARE(eventmanager, log, DebugCategory::LINFO);
@ -246,6 +247,15 @@ static int32_t reportToRelevantUnitsTime = -1;
//interaction
static int32_t lastReportInteraction;
struct hash_pair {
template<typename A, typename B>
size_t operator()(const std::pair<A,B>& p) const {
auto h1 = std::hash<A>{}(p.first);
auto h2 = std::hash<B>{}(p.second);
return h1 ^ (h2 << 1);
}
};
void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event event) {
static bool doOnce = false;
// const string eventNames[] = {"world loaded", "world unloaded", "map loaded", "map unloaded", "viewscreen changed", "core initialized", "begin unload", "paused", "unpaused"};
@ -276,9 +286,9 @@ void DFHack::EventManager::onStateChange(color_ostream& out, state_change_event
gameLoaded = false;
multimap<Plugin*,EventHandler> copy(handlers[EventType::UNLOAD].begin(), handlers[EventType::UNLOAD].end());
for (auto &key_value : copy) {
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for map unloaded state change event\n");
key_value.second.eventHandler(out, nullptr);
handle.eventHandler(out, nullptr);
}
} else if ( event == DFHack::SC_MAP_LOADED ) {
/*
@ -375,8 +385,7 @@ void DFHack::EventManager::manageEvents(color_ostream& out) {
continue;
int32_t eventFrequency = -100;
if ( a != EventType::TICK )
for (auto &key_value : handlers[a]) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : handlers[a]) {
if (handle.freq < eventFrequency || eventFrequency == -100 )
eventFrequency = handle.freq;
}
@ -439,8 +448,7 @@ static void manageJobInitiatedEvent(color_ostream& out) {
continue;
if ( link->item->id <= lastJobId )
continue;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for job initiated event\n");
handle.eventHandler(out, (void*)link->item);
}
@ -455,17 +463,24 @@ static void manageJobStartedEvent(color_ostream& out) {
static unordered_set<int32_t> startedJobs;
vector<df::job*> new_started_jobs;
// iterate event handler callbacks
multimap<Plugin*, EventHandler> copy(handlers[EventType::JOB_STARTED].begin(), handlers[EventType::JOB_STARTED].end());
for (df::job_list_link* link = df::global::world->jobs.list.next; link != nullptr; link = link->next) {
df::job* job = link->item;
for (df::job_list_link* link = &df::global::world->jobs.list; link->next != nullptr; link = link->next) {
df::job* job = link->next->item;
int32_t j_id = job->id;
if (job && Job::getWorker(job) && !startedJobs.count(job->id)) {
startedJobs.emplace(job->id);
for (auto &key_value : copy) {
auto &handler = key_value.second;
for (auto &[_,handle] : copy) {
// the jobs must have a worker to start
DEBUG(log,out).print("calling handler for job started event\n");
handler.eventHandler(out, job);
handle.eventHandler(out, job);
}
}
if (link->next == nullptr || link->next->item->id != j_id) {
if ( Once::doOnce("EventManager jobstarted job removed") ) {
out.print("%s,%d: job %u removed from jobs linked list\n", __FILE__, __LINE__, j_id);
}
}
}
@ -487,11 +502,11 @@ static void manageJobCompletedEvent(color_ostream& out) {
int32_t tick1 = df::global::world->frame_counter;
multimap<Plugin*,EventHandler> copy(handlers[EventType::JOB_COMPLETED].begin(), handlers[EventType::JOB_COMPLETED].end());
map<int32_t, df::job*> nowJobs;
unordered_map<int32_t, df::job*> nowJobs;
for ( df::job_list_link* link = &df::global::world->jobs.list; link != nullptr; link = link->next ) {
if ( link->item == nullptr )
continue;
nowJobs[link->item->id] = link->item;
nowJobs.emplace(link->item->id, link->item);
}
#if 0
@ -573,8 +588,7 @@ static void manageJobCompletedEvent(color_ostream& out) {
continue;
//still false positive if cancelled at EXACTLY the right time, but experiments show this doesn't happen
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for repeated job completed event\n");
handle.eventHandler(out, (void*) &job0);
}
@ -586,28 +600,27 @@ static void manageJobCompletedEvent(color_ostream& out) {
if ( job0.flags.bits.repeat || job0.completion_timer != 0 )
continue;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for job completed event\n");
handle.eventHandler(out, (void*) &job0);
}
}
//erase old jobs, copy over possibly altered jobs
for (auto &prevJob : prevJobs) {
Job::deleteJobStruct(prevJob.second, true);
for (auto &[_,prev_job] : prevJobs) {
Job::deleteJobStruct(prev_job, true);
}
prevJobs.clear();
//create new jobs
for (auto &nowJob : nowJobs) {
for (auto &[_,now_job] : nowJobs) {
/*map<int32_t, df::job*>::iterator i = prevJobs.find((*j).first);
if ( i != prevJobs.end() ) {
continue;
}*/
df::job* newJob = Job::cloneJobStruct(nowJob.second, true);
prevJobs[newJob->id] = newJob;
df::job* newJob = Job::cloneJobStruct(now_job, true);
prevJobs.emplace(newJob->id, newJob);
}
}
@ -617,16 +630,18 @@ static void manageNewUnitActiveEvent(color_ostream& out) {
multimap<Plugin*,EventHandler> copy(handlers[EventType::UNIT_NEW_ACTIVE].begin(), handlers[EventType::UNIT_NEW_ACTIVE].end());
// iterate event handler callbacks
for (auto &key_value : copy) {
auto &handler = key_value.second;
vector<int32_t> new_active_unit_ids;
for (df::unit* unit : df::global::world->units.active) {
int32_t id = unit->id;
if (!activeUnits.count(id)) {
activeUnits.emplace(id);
DEBUG(log,out).print("calling handler for new unit event\n");
handler.eventHandler(out, (void*) intptr_t(id)); // intptr_t() avoids cast from smaller type warning
if (!activeUnits.count(unit->id)) {
activeUnits.emplace(unit->id);
new_active_unit_ids.emplace_back(unit->id);
}
}
for (int32_t unit_id : new_active_unit_ids) {
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for new unit event\n");
handle.eventHandler(out, (void*) intptr_t(unit_id)); // intptr_t() avoids cast from smaller type warning
}
}
}
@ -635,22 +650,26 @@ static void manageUnitDeathEvent(color_ostream& out) {
if (!df::global::world)
return;
multimap<Plugin*,EventHandler> copy(handlers[EventType::UNIT_DEATH].begin(), handlers[EventType::UNIT_DEATH].end());
vector<int32_t> dead_unit_ids;
for (auto unit : df::global::world->units.all) {
//if ( unit->counters.death_id == -1 ) {
if ( Units::isActive(unit) ) {
livingUnits.insert(unit->id);
continue;
}
if (!Units::isDead(unit)) continue; // for units that have left the map but aren't dead
//dead: if dead since last check, trigger events
if ( livingUnits.find(unit->id) == livingUnits.end() )
continue;
livingUnits.erase(unit->id);
dead_unit_ids.emplace_back(unit->id);
}
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (int32_t unit_id : dead_unit_ids) {
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for unit death event\n");
handle.eventHandler(out, (void*)intptr_t(unit->id));
handle.eventHandler(out, (void*)intptr_t(unit_id));
}
livingUnits.erase(unit->id);
}
}
@ -666,6 +685,8 @@ static void manageItemCreationEvent(color_ostream& out) {
multimap<Plugin*,EventHandler> copy(handlers[EventType::ITEM_CREATED].begin(), handlers[EventType::ITEM_CREATED].end());
size_t index = df::item::binsearch_index(df::global::world->items.all, nextItem, false);
if ( index != 0 ) index--;
std::vector<int32_t> created_items;
for ( size_t a = index; a < df::global::world->items.all.size(); a++ ) {
df::item* item = df::global::world->items.all[a];
//already processed
@ -683,12 +704,17 @@ static void manageItemCreationEvent(color_ostream& out) {
//spider webs don't count
if ( item->flags.bits.spider_web )
continue;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
created_items.push_back(item->id);
}
// handle all created items
for (int32_t item_id : created_items) {
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for item created event\n");
handle.eventHandler(out, (void*)intptr_t(item->id));
handle.eventHandler(out, (void*)intptr_t(item_id));
}
}
nextItem = *df::global::item_next_id;
}
@ -703,6 +729,7 @@ static void manageBuildingEvent(color_ostream& out) {
**/
multimap<Plugin*,EventHandler> copy(handlers[EventType::BUILDING].begin(), handlers[EventType::BUILDING].end());
//first alert people about new buildings
vector<int32_t> new_buildings;
for ( int32_t a = nextBuilding; a < *df::global::building_next_id; a++ ) {
int32_t index = df::building::binsearch_index(df::global::world->buildings.all, a);
if ( index == -1 ) {
@ -711,30 +738,34 @@ static void manageBuildingEvent(color_ostream& out) {
continue;
}
buildings.insert(a);
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
DEBUG(log,out).print("calling handler for created building event\n");
handle.eventHandler(out, (void*)intptr_t(a));
}
new_buildings.emplace_back(a);
}
nextBuilding = *df::global::building_next_id;
//now alert people about destroyed buildings
for ( auto a = buildings.begin(); a != buildings.end(); ) {
int32_t id = *a;
for ( auto it = buildings.begin(); it != buildings.end(); ) {
int32_t id = *it;
int32_t index = df::building::binsearch_index(df::global::world->buildings.all,id);
if ( index != -1 ) {
a++;
++it;
continue;
}
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for destroyed building event\n");
handle.eventHandler(out, (void*)intptr_t(id));
}
a = buildings.erase(a);
it = buildings.erase(it);
}
//alert people about newly created buildings
std::for_each(new_buildings.begin(), new_buildings.end(), [&](int32_t building){
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for created building event\n");
handle.eventHandler(out, (void*)intptr_t(building));
}
});
}
static void manageConstructionEvent(color_ostream& out) {
@ -743,44 +774,52 @@ static void manageConstructionEvent(color_ostream& out) {
//unordered_set<df::construction*> constructionsNow(df::global::world->constructions.begin(), df::global::world->constructions.end());
multimap<Plugin*, EventHandler> copy(handlers[EventType::CONSTRUCTION].begin(), handlers[EventType::CONSTRUCTION].end());
// find & send construction removals
for (auto iter = constructions.begin(); iter != constructions.end();) {
auto &construction = *iter;
// if we can't find it, it was removed
if (df::construction::find(construction.pos) != nullptr) {
++iter;
continue;
unordered_set<df::construction> next_construction_set; // will be swapped with constructions
next_construction_set.reserve(constructions.bucket_count());
vector<df::construction> new_constructions;
// find new constructions - swapping found constructions over from constructions to next_construction_set
for (auto c : df::global::world->constructions) {
auto &construction = *c;
auto it = constructions.find(construction);
if (it == constructions.end()) {
// handle new construction event later
new_constructions.emplace_back(construction);
}
else {
constructions.erase(it);
}
// send construction to handlers, because it was removed
for (const auto &key_value: copy) {
EventHandler handle = key_value.second;
next_construction_set.emplace(construction);
}
constructions.swap(next_construction_set);
// now next_construction_set contains all the constructions that were removed (not found in df::global::world->constructions)
for (auto& construction : next_construction_set) {
// handle construction removed event
for (const auto &[_,handle]: copy) {
DEBUG(log,out).print("calling handler for destroyed construction event\n");
handle.eventHandler(out, (void*) &construction);
}
// erase from existent constructions
iter = constructions.erase(iter);
}
// find & send construction additions
for (auto c: df::global::world->constructions) {
auto &construction = *c;
// add construction to constructions, if it isn't already present
if (constructions.emplace(construction).second) {
// send construction to handlers, because it is new
for (const auto &key_value: copy) {
EventHandler handle = key_value.second;
// now handle all the new constructions
for (auto& construction : new_constructions) {
for (const auto &[_,handle]: copy) {
DEBUG(log,out).print("calling handler for created construction event\n");
handle.eventHandler(out, (void*) &construction);
}
}
}
}
static void manageSyndromeEvent(color_ostream& out) {
if (!df::global::world)
return;
multimap<Plugin*,EventHandler> copy(handlers[EventType::SYNDROME].begin(), handlers[EventType::SYNDROME].end());
int32_t highestTime = -1;
std::vector<SyndromeData> new_syndrome_data;
for (auto unit : df::global::world->units.all) {
/*
@ -795,14 +834,16 @@ static void manageSyndromeEvent(color_ostream& out) {
if ( startTime <= lastSyndromeTime )
continue;
SyndromeData data(unit->id, b);
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
new_syndrome_data.emplace_back(unit->id, b);
}
}
for (auto& data : new_syndrome_data) {
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for syndrome event\n");
handle.eventHandler(out, (void*)&data);
}
}
}
lastSyndromeTime = highestTime;
}
@ -815,8 +856,7 @@ static void manageInvasionEvent(color_ostream& out) {
return;
nextInvasion = df::global::plotinfo->invasions.next_id;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for invasion event\n");
handle.eventHandler(out, (void*)intptr_t(nextInvasion-1));
}
@ -829,6 +869,18 @@ static void manageEquipmentEvent(color_ostream& out) {
unordered_map<int32_t, InventoryItem> itemIdToInventoryItem;
unordered_set<int32_t> currentlyEquipped;
vector<InventoryChangeData> equipment_pickups;
vector<InventoryChangeData> equipment_drops;
vector<InventoryChangeData> equipment_changes;
// This vector stores the pointers to newly created changed items
// needed as the stack allocated temporary (in the loop) is lost when we go to
// handle the event calls, so we move that data to the heap if its needed,
// and then once we are done we delete everything.
vector<InventoryItem*> changed_items;
for (auto unit : df::global::world->units.all) {
itemIdToInventoryItem.clear();
currentlyEquipped.clear();
@ -856,40 +908,30 @@ static void manageEquipmentEvent(color_ostream& out) {
auto c = itemIdToInventoryItem.find(dfitem_new->item->id);
if ( c == itemIdToInventoryItem.end() ) {
//new item equipped (probably just picked up)
InventoryChangeData data(unit->id, nullptr, &item_new);
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
DEBUG(log,out).print("calling handler for new item equipped inventory change event\n");
handle.eventHandler(out, (void*)&data);
}
changed_items.emplace_back(new InventoryItem(item_new));
equipment_pickups.emplace_back(unit->id, nullptr, changed_items.back());
continue;
}
InventoryItem item_old = (*c).second;
InventoryItem item_old = c->second;
df::unit_inventory_item& item0 = item_old.item;
df::unit_inventory_item& item1 = item_new.item;
if ( item0.mode == item1.mode && item0.body_part_id == item1.body_part_id && item0.wound_id == item1.wound_id )
continue;
//some sort of change in how it's equipped
InventoryChangeData data(unit->id, &item_old, &item_new);
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
DEBUG(log,out).print("calling handler for inventory change event\n");
handle.eventHandler(out, (void*)&data);
}
changed_items.emplace_back(new InventoryItem(item_new));
InventoryItem* item_new_ptr = changed_items.back();
changed_items.emplace_back(new InventoryItem(item_old));
InventoryItem* item_old_ptr = changed_items.back();
equipment_changes.emplace_back(unit->id, item_old_ptr, item_new_ptr);
}
//check for dropped items
for (auto i : v) {
if ( currentlyEquipped.find(i.itemId) != currentlyEquipped.end() )
continue;
//TODO: delete ptr if invalid
InventoryChangeData data(unit->id, &i, nullptr);
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
DEBUG(log,out).print("calling handler for dropped item inventory change event\n");
handle.eventHandler(out, (void*)&data);
}
changed_items.emplace_back(new InventoryItem(i));
equipment_drops.emplace_back(unit->id, changed_items.back(), nullptr);
}
if ( !hadEquipment )
delete temp;
@ -902,6 +944,31 @@ static void manageEquipmentEvent(color_ostream& out) {
equipment.push_back(item);
}
}
// now handle events
std::for_each(equipment_pickups.begin(), equipment_pickups.end(), [&](InventoryChangeData& data) {
for (auto &[_, handle] : copy) {
DEBUG(log,out).print("calling handler for new item equipped inventory change event\n");
handle.eventHandler(out, (void*) &data);
}
});
std::for_each(equipment_drops.begin(), equipment_drops.end(), [&](InventoryChangeData& data) {
for (auto &[_, handle] : copy) {
DEBUG(log,out).print("calling handler for dropped item inventory change event\n");
handle.eventHandler(out, (void*) &data);
}
});
std::for_each(equipment_changes.begin(), equipment_changes.end(), [&](InventoryChangeData& data) {
for (auto &[_, handle] : copy) {
DEBUG(log,out).print("calling handler for inventory change event\n");
handle.eventHandler(out, (void*) &data);
}
});
// clean up changed items list
std::for_each(changed_items.begin(), changed_items.end(), [](InventoryItem* p){
delete p;
});
}
static void updateReportToRelevantUnits() {
@ -939,8 +1006,7 @@ static void manageReportEvent(color_ostream& out) {
for ( ; idx < reports.size(); idx++ ) {
df::report* report = reports[idx];
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for report event\n");
handle.eventHandler(out, (void*)intptr_t(report->id));
}
@ -981,7 +1047,7 @@ static void manageUnitAttackEvent(color_ostream& out) {
if ( strikeReports.empty() )
return;
updateReportToRelevantUnits();
map<int32_t, map<int32_t, int32_t> > alreadyDone;
unordered_set<std::pair<int32_t, int32_t>, hash_pair> already_done;
for (int reportId : strikeReports) {
df::report* report = df::report::find(reportId);
if ( !report )
@ -1011,27 +1077,25 @@ static void manageUnitAttackEvent(color_ostream& out) {
UnitAttackData data{};
data.report_id = report->id;
if ( wound1 && !alreadyDone[unit1->id][unit2->id] ) {
if ( wound1 && already_done.find(std::make_pair(unit1->id,unit2->id)) == already_done.end() ) {
data.attacker = unit1->id;
data.defender = unit2->id;
data.wound = wound1->id;
alreadyDone[data.attacker][data.defender] = 1;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
already_done.emplace(unit1->id, unit2->id);
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for unit1 attack unit attack event\n");
handle.eventHandler(out, (void*)&data);
}
}
if ( wound2 && !alreadyDone[unit1->id][unit2->id] ) {
if ( wound2 && already_done.find(std::make_pair(unit1->id,unit2->id)) == already_done.end() ) {
data.attacker = unit2->id;
data.defender = unit1->id;
data.wound = wound2->id;
alreadyDone[data.attacker][data.defender] = 1;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
already_done.emplace(unit1->id, unit2->id);
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for unit2 attack unit attack event\n");
handle.eventHandler(out, (void*)&data);
}
@ -1041,9 +1105,9 @@ static void manageUnitAttackEvent(color_ostream& out) {
data.attacker = unit2->id;
data.defender = unit1->id;
data.wound = -1;
alreadyDone[data.attacker][data.defender] = 1;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
already_done.emplace(unit1->id, unit2->id);
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for unit1 killed unit attack event\n");
handle.eventHandler(out, (void*)&data);
}
@ -1053,9 +1117,9 @@ static void manageUnitAttackEvent(color_ostream& out) {
data.attacker = unit1->id;
data.defender = unit2->id;
data.wound = -1;
alreadyDone[data.attacker][data.defender] = 1;
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
already_done.emplace(unit1->id, unit2->id);
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for unit2 killed unit attack event\n");
handle.eventHandler(out, (void*)&data);
}
@ -1313,8 +1377,7 @@ static void manageInteractionEvent(color_ostream& out) {
lastAttacker = df::unit::find(data.attacker);
//lastDefender = df::unit::find(data.defender);
//fire event
for (auto &key_value : copy) {
EventHandler &handle = key_value.second;
for (auto &[_,handle] : copy) {
DEBUG(log,out).print("calling handler for interaction event\n");
handle.eventHandler(out, (void*)&data);
}

@ -87,6 +87,7 @@ using namespace DFHack;
#include "df/viewscreen_dwarfmodest.h"
#include "df/viewscreen_legendsst.h"
#include "df/viewscreen_new_regionst.h"
#include "df/viewscreen_setupdwarfgamest.h"
#include "df/viewscreen_titlest.h"
#include "df/world.h"
@ -174,6 +175,45 @@ DEFINE_GET_FOCUS_STRING_HANDLER(new_region)
focusStrings.push_back(baseFocus);
}
DEFINE_GET_FOCUS_STRING_HANDLER(setupdwarfgame)
{
if (screen->doing_custom_settings)
focusStrings.push_back(baseFocus + "/CustomSettings");
else if (game->main_interface.options.open)
focusStrings.push_back(baseFocus + "/Abort");
else if (screen->initial_selection == 1)
focusStrings.push_back(baseFocus + "/Default");
else if (game->main_interface.name_creator.open) {
switch (game->main_interface.name_creator.context) {
case df::name_creator_context_type::EMBARK_FORT_NAME:
focusStrings.push_back(baseFocus + "/FortName");
break;
case df::name_creator_context_type::EMBARK_GROUP_NAME:
focusStrings.push_back(baseFocus + "/GroupName");
break;
default:
break;
}
}
else if (game->main_interface.image_creator.open) {
focusStrings.push_back(baseFocus + "/GroupSymbol");
}
else if (screen->viewing_objections != 0)
focusStrings.push_back(baseFocus + "/Objections");
else {
switch (screen->mode) {
case 0: focusStrings.push_back(baseFocus + "/Dwarves"); break;
case 1: focusStrings.push_back(baseFocus + "/Items"); break;
case 2: focusStrings.push_back(baseFocus + "/Animals"); break;
default:
break;
}
}
if (focusStrings.empty())
focusStrings.push_back(baseFocus + "/Default");
}
DEFINE_GET_FOCUS_STRING_HANDLER(legends)
{
if (screen->init_stage != -1)

@ -956,6 +956,18 @@ int dfhack_lua_viewscreen::do_notify(lua_State *L)
return 1;
}
void dfhack_lua_viewscreen::markInputAsHandled() {
if (!enabler)
return;
// clear text buffer
enabler->last_text_input[0] = '\0';
// mark clicked mouse buttons as handled
enabler->mouse_lbut = 0;
enabler->mouse_rbut = 0;
}
int dfhack_lua_viewscreen::do_input(lua_State *L)
{
auto self = get_self(L);
@ -977,7 +989,11 @@ int dfhack_lua_viewscreen::do_input(lua_State *L)
lua_pushvalue(L, -2);
Lua::PushInterfaceKeys(L, Screen::normalize_text_keys(*keys));
lua_call(L, 2, 0);
lua_call(L, 2, 1);
if (lua_toboolean(L, -1))
markInputAsHandled();
lua_pop(L, 1);
self->update_focus(L, -1);
return 0;
}
@ -1004,6 +1020,8 @@ dfhack_lua_viewscreen::~dfhack_lua_viewscreen()
void dfhack_lua_viewscreen::render()
{
using df::global::enabler;
if (Screen::isDismissed(this))
{
if (parent)
@ -1011,6 +1029,14 @@ void dfhack_lua_viewscreen::render()
return;
}
if (enabler &&
(enabler->mouse_lbut_down || enabler->mouse_rbut_down || enabler->mouse_mbut_down))
{
// synthesize feed events for held mouse buttons
std::set<df::interface_key> keys;
feed(&keys);
}
dfhack_viewscreen::render();
safe_call_lua(do_render, 0, 0);

@ -1,3 +1,5 @@
#include <algorithm>
#include <atomic>
#include <mutex>
#include <numeric>
#include <unordered_map>
@ -28,9 +30,35 @@ namespace DFHack {
DBG_DECLARE(core, textures, DebugCategory::LINFO);
}
struct ReservedRange {
void init(int32_t start) {
this->start = start;
this->end = start + ReservedRange::size;
this->current = start;
this->is_installed = true;
}
long get_new_texpos() {
if (this->current == this->end)
return -1;
return this->current++;
}
static const int32_t size = 10000; // size of reserved texpos buffer
int32_t start = -1;
int32_t end = -1;
long current = -1;
bool is_installed = false;
};
static ReservedRange reserved_range{};
static std::unordered_map<TexposHandle, long> g_handle_to_texpos;
static std::unordered_map<TexposHandle, long> g_handle_to_reserved_texpos;
static std::unordered_map<TexposHandle, SDL_Surface*> g_handle_to_surface;
static std::unordered_map<std::string, std::vector<TexposHandle>> g_tileset_to_handles;
static std::vector<TexposHandle> g_delayed_regs;
static std::mutex g_adding_mutex;
static std::atomic<bool> loading_state = false;
static SDL_Surface* dummy_surface = NULL;
// Converts an arbitrary Surface to something like the display format
// (32-bit RGBA), and converts magenta to transparency if convert_magenta is set
@ -71,6 +99,12 @@ static long add_texture(SDL_Surface* surface) {
return texpos;
}
// register surface in texture raws to specific texpos
static void insert_texture(SDL_Surface* surface, long texpos) {
std::lock_guard<std::mutex> lg_add_texture(g_adding_mutex);
enabler->textures.raws[texpos] = surface;
}
// delete surface from texture raws
static void delete_texture(long texpos) {
std::lock_guard<std::mutex> lg_add_texture(g_adding_mutex);
@ -94,7 +128,8 @@ SDL_Surface* create_texture(std::vector<uint32_t>& pixels, int texture_px_w, int
// convert single surface into tiles according w/h
// register tiles in texture raws and return handles
std::vector<TexposHandle> slice_tileset(SDL_Surface* surface, int tile_px_w, int tile_px_h) {
std::vector<TexposHandle> slice_tileset(SDL_Surface* surface, int tile_px_w, int tile_px_h,
bool reserved) {
std::vector<TexposHandle> handles{};
if (!surface)
return handles;
@ -102,6 +137,12 @@ std::vector<TexposHandle> slice_tileset(SDL_Surface* surface, int tile_px_w, int
int dimx = surface->w / tile_px_w;
int dimy = surface->h / tile_px_h;
if (reserved && (dimx * dimy > reserved_range.end - reserved_range.current)) {
WARN(textures).print(
"there is not enough space in reserved range for whole tileset, using dynamic range\n");
reserved = false;
}
for (int y = 0; y < dimy; y++) {
for (int x = 0; x < dimx; x++) {
SDL_Surface* tile = DFSDL_CreateRGBSurface(
@ -109,7 +150,7 @@ std::vector<TexposHandle> slice_tileset(SDL_Surface* surface, int tile_px_w, int
surface->format->Bmask, surface->format->Amask);
SDL_Rect vp{tile_px_w * x, tile_px_h * y, tile_px_w, tile_px_h};
DFSDL_UpperBlit(surface, &vp, tile, NULL);
auto handle = Textures::loadTexture(tile);
auto handle = Textures::loadTexture(tile, reserved);
handles.push_back(handle);
}
}
@ -118,20 +159,46 @@ std::vector<TexposHandle> slice_tileset(SDL_Surface* surface, int tile_px_w, int
return handles;
}
TexposHandle Textures::loadTexture(SDL_Surface* surface) {
TexposHandle Textures::loadTexture(SDL_Surface* surface, bool reserved) {
if (!surface || !enabler)
return 0; // should be some error, i guess
if (loading_state)
reserved = true; // use reserved range during loading for all textures
auto handle = reinterpret_cast<uintptr_t>(surface);
g_handle_to_surface.emplace(handle, surface);
surface->refcount++; // prevent destruct on next FreeSurface by game
if (reserved && reserved_range.is_installed) {
auto texpos = reserved_range.get_new_texpos();
if (texpos != -1) {
insert_texture(surface, texpos);
g_handle_to_reserved_texpos.emplace(handle, texpos);
dummy_surface->refcount--;
return handle;
}
if (loading_state) { // if we in loading state and reserved range is full -> error
ERR(textures).printerr("reserved range limit has been reached, use dynamic range\n");
return 0;
}
}
// if we here in loading state = true, then it should be dynamic range -> delay reg
if (loading_state) {
g_delayed_regs.push_back(handle);
} else {
auto texpos = add_texture(surface);
g_handle_to_texpos.emplace(handle, texpos);
}
return handle;
}
std::vector<TexposHandle> Textures::loadTileset(const std::string& file, int tile_px_w,
int tile_px_h) {
int tile_px_h, bool reserved) {
if (g_tileset_to_handles.contains(file))
return g_tileset_to_handles[file];
if (!enabler)
return std::vector<TexposHandle>{};
@ -142,9 +209,10 @@ std::vector<TexposHandle> Textures::loadTileset(const std::string& file, int til
}
surface = canonicalize_format(surface);
auto handles = slice_tileset(surface, tile_px_w, tile_px_h);
auto handles = slice_tileset(surface, tile_px_w, tile_px_h, reserved);
DEBUG(textures).print("loaded %zd textures from '%s'\n", handles.size(), file.c_str());
g_tileset_to_handles[file] = handles;
return handles;
}
@ -153,11 +221,18 @@ long Textures::getTexposByHandle(TexposHandle handle) {
if (!handle || !enabler)
return -1;
if (g_handle_to_reserved_texpos.contains(handle))
return g_handle_to_reserved_texpos[handle];
if (g_handle_to_texpos.contains(handle))
return g_handle_to_texpos[handle];
if (std::find(g_delayed_regs.begin(), g_delayed_regs.end(), handle) != g_delayed_regs.end())
return 0;
if (g_handle_to_surface.contains(handle)) {
g_handle_to_surface[handle]->refcount++; // prevent destruct on next FreeSurface by game
if (loading_state) { // reinit dor dynamic range during loading -> delayed
g_delayed_regs.push_back(handle);
return 0;
}
auto texpos = add_texture(g_handle_to_surface[handle]);
g_handle_to_texpos.emplace(handle, texpos);
return texpos;
@ -166,22 +241,24 @@ long Textures::getTexposByHandle(TexposHandle handle) {
return -1;
}
TexposHandle Textures::createTile(std::vector<uint32_t>& pixels, int tile_px_w, int tile_px_h) {
TexposHandle Textures::createTile(std::vector<uint32_t>& pixels, int tile_px_w, int tile_px_h,
bool reserved) {
if (!enabler)
return 0;
auto texture = create_texture(pixels, tile_px_w, tile_px_h);
auto handle = Textures::loadTexture(texture);
auto handle = Textures::loadTexture(texture, reserved);
return handle;
}
std::vector<TexposHandle> Textures::createTileset(std::vector<uint32_t>& pixels, int texture_px_w,
int texture_px_h, int tile_px_w, int tile_px_h) {
int texture_px_h, int tile_px_w, int tile_px_h,
bool reserved) {
if (!enabler)
return std::vector<TexposHandle>{};
auto texture = create_texture(pixels, texture_px_w, texture_px_h);
auto handles = slice_tileset(texture, tile_px_w, tile_px_h);
auto handles = slice_tileset(texture, tile_px_w, tile_px_h, reserved);
return handles;
}
@ -192,8 +269,13 @@ void Textures::deleteHandle(TexposHandle handle) {
auto texpos = Textures::getTexposByHandle(handle);
if (texpos > 0)
delete_texture(texpos);
if (g_handle_to_reserved_texpos.contains(handle))
g_handle_to_reserved_texpos.erase(handle);
if (g_handle_to_texpos.contains(handle))
g_handle_to_texpos.erase(handle);
if (auto it = std::find(g_delayed_regs.begin(), g_delayed_regs.end(), handle);
it != g_delayed_regs.end())
g_delayed_regs.erase(it);
if (g_handle_to_surface.contains(handle)) {
auto surface = g_handle_to_surface[handle];
while (surface->refcount)
@ -207,20 +289,45 @@ static void reset_texpos() {
g_handle_to_texpos.clear();
}
static void reset_reserved_texpos() {
DEBUG(textures).print("resetting reserved texture mappings\n");
g_handle_to_reserved_texpos.clear();
}
static void reset_tilesets() {
DEBUG(textures).print("resetting tileset to handle mappings\n");
g_tileset_to_handles.clear();
}
static void reset_surface() {
DEBUG(textures).print("deleting cached surfaces\n");
for (auto& entry : g_handle_to_surface) {
DFSDL_FreeSurface(entry.second);
}
g_handle_to_surface.clear();
}
static void register_delayed_handles() {
DEBUG(textures).print("register delayed handles, size %zd\n", g_delayed_regs.size());
for (auto& handle : g_delayed_regs) {
auto texpos = add_texture(g_handle_to_surface[handle]);
g_handle_to_texpos.emplace(handle, texpos);
}
g_delayed_regs.clear();
}
// reset point on New Game
struct tracking_stage_new_region : df::viewscreen_new_regionst {
typedef df::viewscreen_new_regionst interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, logic, ()) {
if (this->m_raw_load_stage != this->raw_load_stage) {
TRACE(textures).print("raw_load_stage %d -> %d\n", this->m_raw_load_stage, this->raw_load_stage);
TRACE(textures).print("raw_load_stage %d -> %d\n", this->m_raw_load_stage,
this->raw_load_stage);
bool tmp_state = loading_state;
loading_state = this->raw_load_stage >= 0 && this->raw_load_stage < 3 ? true : false;
if (tmp_state != loading_state && !loading_state)
register_delayed_handles();
this->m_raw_load_stage = this->raw_load_stage;
if (this->m_raw_load_stage == 1)
reset_texpos();
@ -240,6 +347,10 @@ struct tracking_stage_adopt_region : df::viewscreen_adopt_regionst {
DEFINE_VMETHOD_INTERPOSE(void, logic, ()) {
if (this->m_cur_step != this->cur_step) {
TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step);
bool tmp_state = loading_state;
loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false;
if (tmp_state != loading_state && !loading_state)
register_delayed_handles();
this->m_cur_step = this->cur_step;
if (this->m_cur_step == 1)
reset_texpos();
@ -259,6 +370,10 @@ struct tracking_stage_load_region : df::viewscreen_loadgamest {
DEFINE_VMETHOD_INTERPOSE(void, logic, ()) {
if (this->m_cur_step != this->cur_step) {
TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step);
bool tmp_state = loading_state;
loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false;
if (tmp_state != loading_state && !loading_state)
register_delayed_handles();
this->m_cur_step = this->cur_step;
if (this->m_cur_step == 1)
reset_texpos();
@ -278,6 +393,10 @@ struct tracking_stage_new_arena : df::viewscreen_new_arenast {
DEFINE_VMETHOD_INTERPOSE(void, logic, ()) {
if (this->m_cur_step != this->cur_step) {
TRACE(textures).print("step %d -> %d\n", this->m_cur_step, this->cur_step);
bool tmp_state = loading_state;
loading_state = this->cur_step >= 0 && this->cur_step < 3 ? true : false;
if (tmp_state != loading_state && !loading_state)
register_delayed_handles();
this->m_cur_step = this->cur_step;
if (this->m_cur_step == 0)
reset_texpos();
@ -304,12 +423,31 @@ static void uninstall_reset_point() {
INTERPOSE_HOOK(tracking_stage_new_arena, logic).remove();
}
static void reserve_static_range() {
if (static_cast<size_t>(enabler->textures.init_texture_size) != enabler->textures.raws.size()) {
WARN(textures).print(
"reserved range can't be installed! all textures will be loaded to dynamic range!");
return;
}
reserved_range.init(enabler->textures.init_texture_size);
dummy_surface =
DFSDL_CreateRGBSurfaceWithFormat(0, 0, 0, 32, SDL_PixelFormatEnum::SDL_PIXELFORMAT_RGBA32);
dummy_surface->refcount += ReservedRange::size;
for (int32_t i = 0; i < ReservedRange::size; i++) {
add_texture(dummy_surface);
}
enabler->textures.init_texture_size += ReservedRange::size;
}
void Textures::init(color_ostream& out) {
if (!enabler)
return;
reserve_static_range();
install_reset_point();
DEBUG(textures, out).print("dynamic texture loading ready");
DEBUG(textures, out)
.print("dynamic texture loading ready, reserved range %d-%d\n", reserved_range.start,
reserved_range.end);
}
void Textures::cleanup() {
@ -317,6 +455,8 @@ void Textures::cleanup() {
return;
reset_texpos();
reset_reserved_texpos();
reset_tilesets();
reset_surface();
uninstall_reset_point();
}

@ -131,6 +131,37 @@ void Translation::setNickname(df::language_name *name, std::string nick)
}
}
static string translate_word(const df::language_name * name, size_t word_idx) {
CHECK_NULL_POINTER(name);
auto translation = vector_get(world->raws.language.translations, name->language);
if (!translation)
return "";
auto word = vector_get(translation->words, word_idx);
if (!word)
return "";
return *word;
}
static string translate_english_word(const df::language_name * name, size_t part_idx) {
CHECK_NULL_POINTER(name);
if (part_idx >= 7)
return "";
auto words = vector_get(world->raws.language.words, name->words[part_idx]);
if (!words)
return "";
df::part_of_speech part = name->parts_of_speech[part_idx];
if (part < df::part_of_speech::Noun || part > df::part_of_speech::VerbGerund)
return "";
return words->forms[part];
}
string Translation::TranslateName(const df::language_name * name, bool inEnglish, bool onlyLastPart)
{
CHECK_NULL_POINTER(name);
@ -166,20 +197,20 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish
{
word.clear();
if (name->words[0] >= 0)
word.append(*world->raws.language.translations[name->language]->words[name->words[0]]);
word.append(translate_word(name, name->words[0]));
if (name->words[1] >= 0)
word.append(*world->raws.language.translations[name->language]->words[name->words[1]]);
word.append(translate_word(name, name->words[1]));
addNameWord(out, word);
}
word.clear();
for (int i = 2; i <= 5; i++)
if (name->words[i] >= 0)
word.append(*world->raws.language.translations[name->language]->words[name->words[i]]);
word.append(translate_word(name, name->words[i]));
addNameWord(out, word);
if (name->words[6] >= 0)
{
word.clear();
word.append(*world->raws.language.translations[name->language]->words[name->words[6]]);
word.append(translate_word(name, name->words[6]));
addNameWord(out, word);
}
}
@ -189,9 +220,9 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish
{
word.clear();
if (name->words[0] >= 0)
word.append(world->raws.language.words[name->words[0]]->forms[name->parts_of_speech[0]]);
word.append(translate_english_word(name, 0));
if (name->words[1] >= 0)
word.append(world->raws.language.words[name->words[1]]->forms[name->parts_of_speech[1]]);
word.append(translate_english_word(name, 1));
addNameWord(out, word);
}
if (name->words[2] >= 0 || name->words[3] >= 0 || name->words[4] >= 0 || name->words[5] >= 0)
@ -201,10 +232,10 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish
else
out.append("The");
}
for (int i = 2; i <= 5; i++)
for (size_t i = 2; i <= 5; i++)
{
if (name->words[i] >= 0)
addNameWord(out, world->raws.language.words[name->words[i]]->forms[name->parts_of_speech[i]]);
addNameWord(out, translate_english_word(name, i));
}
if (name->words[6] >= 0)
{
@ -213,7 +244,7 @@ string Translation::TranslateName(const df::language_name * name, bool inEnglish
else
out.append("Of");
addNameWord(out, world->raws.language.words[name->words[6]]->forms[name->parts_of_speech[6]]);
addNameWord(out, translate_english_word(name, 6));
}
}

@ -1 +1 @@
Subproject commit 041493b221e0799c106abeac1f86df4535ab80d3
Subproject commit f38f3c4955d604f2b5a8e0d952e676a0ab05c053

@ -167,7 +167,7 @@ if(BUILD_SUPPORTED)
dfhack_plugin(tiletypes tiletypes.cpp Brushes.h LINK_LIBRARIES lua)
#dfhack_plugin(title-folder title-folder.cpp)
#dfhack_plugin(trackstop trackstop.cpp)
#dfhack_plugin(tubefill tubefill.cpp)
dfhack_plugin(tubefill tubefill.cpp)
#add_subdirectory(tweak)
dfhack_plugin(workflow workflow.cpp LINK_LIBRARIES lua)
dfhack_plugin(work-now work-now.cpp)

@ -305,6 +305,7 @@ static void cleanup_state()
{
enable_autolabor = false;
labor_infos.clear();
game->external_flag &= ~1; // reinstate DF's work detail system
}
static void reset_labor(df::unit_labor labor)
@ -326,6 +327,8 @@ static void init_state()
if (!enable_autolabor)
return;
game->external_flag |= 1; // bypass DF's work detail system
auto cfg_haulpct = World::GetPersistentData("autolabor/haulpct");
if (cfg_haulpct.isValid())
{
@ -413,8 +416,17 @@ static void enable_plugin(color_ostream &out)
cleanup_state();
init_state();
}
static void disable_plugin(color_ostream& out)
{
if (config.isValid())
setOptionEnabled(CF_ENABLED, false);
enable_autolabor = false;
out << "Disabling autolabor." << std::endl;
game->external_flag |= 1; // shut down DF's work detail system
cleanup_state();
}
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
@ -1081,12 +1093,7 @@ DFhackCExport command_result plugin_enable ( color_ostream &out, bool enable )
}
else if(!enable && enable_autolabor)
{
enable_autolabor = false;
setOptionEnabled(CF_ENABLED, false);
game->external_flag &= ~1; // reenable DF's work detail system
out << "Autolabor is disabled." << std::endl;
disable_plugin(out);
}
return CR_OK;

@ -151,24 +151,6 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out)
return CR_OK;
}
// Name functions taken from manipulator.cpp
static std::string get_first_name(df::unit *unit)
{
return Translation::capitalize(unit->name.first_name);
}
static std::string get_last_name(df::unit *unit)
{
df::language_name name = unit->name;
std::string ret = "";
for (int i = 0; i < 2; i++)
{
if (name.words[i] >= 0)
ret += *world->raws.language.translations[name.language]->words[name.words[i]];
}
return Translation::capitalize(ret);
}
// Queue up a single order to engrave the slab for the given unit
static void createSlabJob(df::unit *unit)
{
@ -212,7 +194,7 @@ static void checkslabs(color_ostream &out)
)
{
createSlabJob(ghost);
auto fullName = get_first_name(ghost) + " " + get_last_name(ghost);
auto fullName = Translation::TranslateName(&ghost->name, false);
out.print("Added slab order for ghost %s\n", fullName.c_str());
}
}

@ -4,6 +4,7 @@
#include <stack>
#include <string>
#include <cmath>
#include <memory>
#include "Core.h"
#include "Console.h"
@ -1072,14 +1073,13 @@ command_result digv (color_ostream &out, vector <string> & parameters)
con.printerr("I won't dig the borders. That would be cheating!\n");
return CR_FAILURE;
}
MapExtras::MapCache * MCache = new MapExtras::MapCache;
std::unique_ptr<MapExtras::MapCache> MCache = std::make_unique<MapExtras::MapCache>();
df::tile_designation des = MCache->designationAt(xy);
df::tiletype tt = MCache->tiletypeAt(xy);
int16_t veinmat = MCache->veinMaterialAt(xy);
if( veinmat == -1 )
{
con.printerr("This tile is not a vein.\n");
delete MCache;
return CR_FAILURE;
}
con.print("%d/%d/%d tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, des.whole);
@ -1192,7 +1192,6 @@ command_result digv (color_ostream &out, vector <string> & parameters)
}
}
MCache->WriteAll();
delete MCache;
return CR_OK;
}
@ -1259,7 +1258,7 @@ command_result digl (color_ostream &out, vector <string> & parameters)
con.printerr("I won't dig the borders. That would be cheating!\n");
return CR_FAILURE;
}
MapExtras::MapCache * MCache = new MapExtras::MapCache;
std::unique_ptr<MapExtras::MapCache> MCache = std::make_unique<MapExtras::MapCache>();
df::tile_designation des = MCache->designationAt(xy);
df::tiletype tt = MCache->tiletypeAt(xy);
int16_t veinmat = MCache->veinMaterialAt(xy);
@ -1267,7 +1266,6 @@ command_result digl (color_ostream &out, vector <string> & parameters)
if( veinmat != -1 )
{
con.printerr("This is a vein. Use digv instead!\n");
delete MCache;
return CR_FAILURE;
}
con.print("%d/%d/%d tiletype: %d, basemat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, basemat, des.whole);
@ -1408,7 +1406,6 @@ command_result digl (color_ostream &out, vector <string> & parameters)
}
}
MCache->WriteAll();
delete MCache;
return CR_OK;
}
@ -1424,9 +1421,13 @@ command_result digtype (color_ostream &out, vector <string> & parameters)
return CR_FAILURE;
}
uint32_t zMin = 0;
uint32_t xMax,yMax,zMax;
Maps::getSize(xMax,yMax,zMax);
bool hidden = false;
bool automine = true;
int32_t targetDigType = -1;
for (string parameter : parameters) {
if ( parameter == "clear" )
@ -1443,8 +1444,16 @@ command_result digtype (color_ostream &out, vector <string> & parameters)
targetDigType = tile_dig_designation::DownStair;
else if ( parameter == "up" )
targetDigType = tile_dig_designation::UpStair;
else if ( parameter == "-z" )
else if ( parameter == "-z" || parameter == "--cur-zlevel" )
{zMax = *window_z + 1; zMin = *window_z;}
else if ( parameter == "--zdown" || parameter == "-d")
zMax = *window_z + 1;
else if ( parameter == "--zup" || parameter == "-u")
zMin = *window_z;
else if ( parameter == "--hidden" || parameter == "-h")
hidden = true;
else if ( parameter == "--no-auto" || parameter == "-a" )
automine = false;
else
{
out.printerr("Invalid parameter: '%s'.\n", parameter.c_str());
@ -1462,14 +1471,21 @@ command_result digtype (color_ostream &out, vector <string> & parameters)
return CR_FAILURE;
}
DFHack::DFCoord xy ((uint32_t)cx,(uint32_t)cy,cz);
MapExtras::MapCache * mCache = new MapExtras::MapCache;
std::unique_ptr<MapExtras::MapCache> mCache = std::make_unique<MapExtras::MapCache>();
df::tile_designation baseDes = mCache->designationAt(xy);
if (baseDes.bits.hidden && !hidden) {
out.printerr("Cursor is pointing at a hidden tile. Point the cursor at a visible tile when using the --hidden option.\n");
return CR_FAILURE;
}
df::tile_occupancy baseOcc = mCache->occupancyAt(xy);
df::tiletype tt = mCache->tiletypeAt(xy);
int16_t veinmat = mCache->veinMaterialAt(xy);
if( veinmat == -1 )
{
out.printerr("This tile is not a vein.\n");
delete mCache;
return CR_FAILURE;
}
out.print("(%d,%d,%d) tiletype: %d, veinmat: %d, designation: 0x%x ... DIGGING!\n", cx,cy,cz, tt, veinmat, baseDes.whole);
@ -1485,8 +1501,12 @@ command_result digtype (color_ostream &out, vector <string> & parameters)
baseDes.bits.dig = tile_dig_designation::Default;
}
}
// Auto dig only works on default dig designation. Setting dig_auto for any other designation
// prevents dwarves from digging that tile at all.
if (baseDes.bits.dig == tile_dig_designation::Default && automine) baseOcc.bits.dig_auto = true;
else baseOcc.bits.dig_auto = false;
for( uint32_t z = 0; z < zMax; z++ )
for( uint32_t z = zMin; z < zMax; z++ )
{
for( uint32_t x = 1; x < tileXMax-1; x++ )
{
@ -1506,18 +1526,22 @@ command_result digtype (color_ostream &out, vector <string> & parameters)
if ( !mCache->testCoord(current) )
{
out.printerr("testCoord failed at (%d,%d,%d)\n", x, y, z);
delete mCache;
return CR_FAILURE;
}
df::tile_designation designation = mCache->designationAt(current);
if (designation.bits.hidden && !hidden) continue;
df::tile_occupancy occupancy = mCache->occupancyAt(current);
designation.bits.dig = baseDes.bits.dig;
occupancy.bits.dig_auto = baseOcc.bits.dig_auto;
mCache->setDesignationAt(current, designation, priority);
mCache->setOccupancyAt(current, occupancy);
}
}
}
mCache->WriteAll();
delete mCache;
return CR_OK;
}

@ -127,9 +127,9 @@ function InspectorOverlay:onInput(keys)
if not require('plugins.buildingplan').isPlannedBuilding(dfhack.gui.getSelectedBuilding(true)) then
return false
end
if keys._MOUSE_L_DOWN and mouse_is_over_resume_button(self.frame_parent_rect) then
if keys._MOUSE_L and mouse_is_over_resume_button(self.frame_parent_rect) then
return true
elseif keys._MOUSE_L_DOWN or keys._MOUSE_R_DOWN or keys.LEAVESCREEN then
elseif keys._MOUSE_L or keys._MOUSE_R or keys.LEAVESCREEN then
self:reset()
end
return InspectorOverlay.super.onInput(self, keys)

@ -366,10 +366,10 @@ function ItemSelection:submit(choices)
end
function ItemSelection:onInput(keys)
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if keys.LEAVESCREEN or keys._MOUSE_R then
self.on_cancel()
return true
elseif keys._MOUSE_L_DOWN then
elseif keys._MOUSE_L then
local list = self.subviews.flist.list
local idx = list:getIdxUnderMouse()
if idx then

@ -272,7 +272,7 @@ function ItemLine:reset()
end
function ItemLine:onInput(keys)
if keys._MOUSE_L_DOWN and self:getMousePos() then
if keys._MOUSE_L and self:getMousePos() then
self.on_select(self.idx)
end
return ItemLine.super.onInput(self, keys)
@ -739,7 +739,7 @@ end
function PlannerOverlay:onInput(keys)
if not is_plannable() then return false end
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if keys.LEAVESCREEN or keys._MOUSE_R then
if uibs.selection_pos:isValid() then
uibs.selection_pos:clear()
return true
@ -758,7 +758,7 @@ function PlannerOverlay:onInput(keys)
return true
end
if self:is_minimized() then return false end
if keys._MOUSE_L_DOWN then
if keys._MOUSE_L then
if is_over_options_panel() then return false end
local detect_rect = copyall(self.frame_rect)
detect_rect.height = self.subviews.main.frame_rect.height +
@ -828,7 +828,7 @@ function PlannerOverlay:onInput(keys)
end
end
end
return keys._MOUSE_L or keys.SELECT
return keys._MOUSE_L_DOWN or keys.SELECT
end
function PlannerOverlay:render(dc)

@ -5,8 +5,8 @@ local helpdb = require('helpdb')
local overlay = require('plugins.overlay')
local widgets = require('gui.widgets')
local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12)
local logo_hovered_textures = dfhack.textures.loadTileset('hack/data/art/logo_hovered.png', 8, 12)
local logo_textures = dfhack.textures.loadTileset('hack/data/art/logo.png', 8, 12, true)
local logo_hovered_textures = dfhack.textures.loadTileset('hack/data/art/logo_hovered.png', 8, 12, true)
local function get_command(cmdline)
local first_word = cmdline:trim():split(' +')[1]
@ -65,6 +65,7 @@ function HotspotMenuWidget:init()
tile=function() return dfhack.textures.getTexposByHandle(logo_hovered_textures[idx]) end,
ch=ch,
fg=COLOR_WHITE,
bold=true,
}
end
local function get_tile_token(idx, ch)
@ -78,9 +79,9 @@ function HotspotMenuWidget:init()
self:addviews{
widgets.Label{
text={
get_tile_token(1, '!'), get_tile_token(2, 'D'), get_tile_token(3, 'F'), get_tile_token(4, '!'), NEWLINE,
get_tile_token(5, '!'), get_tile_token(6, 'H'), get_tile_token(7, 'a'), get_tile_token(8, '!'), NEWLINE,
get_tile_token(9, '!'), get_tile_token(10, 'c'), get_tile_token(11, 'k'), get_tile_token(12, '!'),
get_tile_token(1, 179), get_tile_token(2, 'D'), get_tile_token(3, 'F'), get_tile_token(4, 179), NEWLINE,
get_tile_token(5, 179), get_tile_token(6, 'H'), get_tile_token(7, 'a'), get_tile_token(8, 179), NEWLINE,
get_tile_token(9, 179), get_tile_token(10, 'c'), get_tile_token(11, 'k'), get_tile_token(12, 179),
},
on_click=function() dfhack.run_command('hotkeys') end,
},
@ -269,24 +270,22 @@ function Menu:onSubmit2(_, choice)
end
function Menu:onInput(keys)
if keys.LEAVESCREEN or keys._MOUSE_R_DOWN then
if keys.LEAVESCREEN or keys._MOUSE_R then
return false
elseif keys.KEYBOARD_CURSOR_RIGHT then
self:onSubmit2(self.subviews.list:getSelected())
return true
elseif keys._MOUSE_L_DOWN then
elseif keys._MOUSE_L then
local list = self.subviews.list
local x = list:getMousePos()
if x == 0 then -- clicked on icon
self:onSubmit2(list:getSelected())
df.global.enabler.mouse_lbut = 0
return true
end
if not self:getMouseFramePos() then
self.parent_view:dismiss()
return true
end
df.global.enabler.mouse_lbut = 0
end
self:inputToSubviews(keys)
return true -- we're modal

@ -62,12 +62,16 @@ local function do_export()
}:show()
end
local function do_recheck()
dfhack.run_command('orders', 'recheck')
end
OrdersOverlay = defclass(OrdersOverlay, overlay.OverlayWidget)
OrdersOverlay.ATTRS{
default_pos={x=53,y=-6},
default_enabled=true,
viewscreens='dwarfmode/Info/WORK_ORDERS/Default',
frame={w=30, h=4},
frame={w=46, h=4},
}
function OrdersOverlay:init()
@ -95,13 +99,20 @@ function OrdersOverlay:init()
},
widgets.HotkeyLabel{
frame={t=0, l=15},
label='recheck',
key='CUSTOM_CTRL_K',
auto_width=true,
on_activate=do_recheck,
},
widgets.HotkeyLabel{
frame={t=1, l=15},
label='sort',
key='CUSTOM_CTRL_O',
auto_width=true,
on_activate=do_sort,
},
widgets.HotkeyLabel{
frame={t=1, l=15},
frame={t=0, l=31},
label='clear',
key='CUSTOM_CTRL_C',
auto_width=true,
@ -157,7 +168,91 @@ function OrdersOverlay:render(dc)
OrdersOverlay.super.render(self, dc)
end
-- Resets the selected work order to the `Checking` state
local function set_current_inactive()
local scrConditions = df.global.game.main_interface.info.work_orders.conditions
if scrConditions.open then
dfhack.run_command('orders', 'recheck', 'this')
else
qerror("Order conditions is not open")
end
end
local function is_current_active()
local scrConditions = df.global.game.main_interface.info.work_orders.conditions
local order = scrConditions.wq
return order.status.active
end
-- -------------------
-- RecheckOverlay
--
local focusString = 'dwarfmode/Info/WORK_ORDERS/Conditions'
RecheckOverlay = defclass(RecheckOverlay, overlay.OverlayWidget)
RecheckOverlay.ATTRS{
default_pos={x=6,y=8},
default_enabled=true,
viewscreens=focusString,
-- width is the sum of lengths of `[` + `Ctrl+A` + `: ` + button.label + `]`
frame={w=1 + 6 + 2 + 16 + 1, h=3},
}
local function areTabsInTwoRows()
-- get the tile above the order status icon
local pen = dfhack.screen.readTile(7, 7, false)
-- in graphics mode, `0` when one row, something else when two (`67` aka 'C' from "Creatures")
-- in ASCII mode, `32` aka ' ' when one row, something else when two (`196` aka '-' from tab frame's top)
return (pen.ch ~= 0 and pen.ch ~= 32)
end
function RecheckOverlay:updateTextButtonFrame()
local twoRows = areTabsInTwoRows()
if (self._twoRows == twoRows) then return false end
self._twoRows = twoRows
local frame = twoRows
and {b=0, l=0, r=0, h=1}
or {t=0, l=0, r=0, h=1}
self.subviews.button.frame = frame
return true
end
function RecheckOverlay:init()
self:addviews{
widgets.TextButton{
view_id = 'button',
-- frame={t=0, l=0, r=0, h=1}, -- is set in `updateTextButtonFrame()`
label='request re-check',
key='CUSTOM_CTRL_A',
on_activate=set_current_inactive,
enabled=is_current_active,
},
}
self:updateTextButtonFrame()
end
function RecheckOverlay:onRenderBody(dc)
if (self.frame_rect.y1 == 7) then
-- only apply this logic if the overlay is on the same row as
-- originally thought: just above the order status icon
if self:updateTextButtonFrame() then
self:updateLayout()
end
end
RecheckOverlay.super.onRenderBody(self, dc)
end
-- -------------------
OVERLAY_WIDGETS = {
recheck=RecheckOverlay,
overlay=OrdersOverlay,
}

@ -506,10 +506,6 @@ function feed_viewscreen_widgets(vs_name, vs, keys)
not _feed_viewscreen_widgets('all', nil, keys) then
return false
end
gui.markMouseClicksHandled(keys)
if keys._MOUSE_L_DOWN then
df.global.enabler.mouse_lbut = 0
end
return true
end

@ -212,7 +212,7 @@ local function melee_skill_effectiveness(unit)
end
local function get_melee_skill_effectiveness_rating(unit)
return get_rating(melee_skill_effectiveness(unit), 350000, 2350000, 78, 64, 49, 35)
return get_rating(melee_skill_effectiveness(unit), 350000, 2750000, 64, 52, 40, 28)
end
local function make_sort_by_melee_skill_effectiveness_desc()
@ -272,7 +272,7 @@ local function ranged_skill_effectiveness(unit)
end
local function get_ranged_skill_effectiveness_rating(unit)
return get_rating(ranged_skill_effectiveness(unit), 0, 500000, 90, 62, 44, 27)
return get_rating(ranged_skill_effectiveness(unit), 0, 800000, 72, 52, 31, 11)
end
local function make_sort_by_ranged_skill_effectiveness_desc(list)
@ -345,41 +345,41 @@ end
-- Statistical rating that is higher for dwarves that are mentally stable
local function get_mental_stability(unit)
local ALTRUISM = unit.status.current_soul.personality.traits.ALTRUISM
local ANXIETY_PROPENSITY = unit.status.current_soul.personality.traits.ANXIETY_PROPENSITY
local BRAVERY = unit.status.current_soul.personality.traits.BRAVERY
local CHEER_PROPENSITY = unit.status.current_soul.personality.traits.CHEER_PROPENSITY
local CURIOUS = unit.status.current_soul.personality.traits.CURIOUS
local DISCORD = unit.status.current_soul.personality.traits.DISCORD
local DUTIFULNESS = unit.status.current_soul.personality.traits.DUTIFULNESS
local EMOTIONALLY_OBSESSIVE = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE
local HUMOR = unit.status.current_soul.personality.traits.HUMOR
local LOVE_PROPENSITY = unit.status.current_soul.personality.traits.LOVE_PROPENSITY
local PERSEVERENCE = unit.status.current_soul.personality.traits.PERSEVERENCE
local POLITENESS = unit.status.current_soul.personality.traits.POLITENESS
local PRIVACY = unit.status.current_soul.personality.traits.PRIVACY
local STRESS_VULNERABILITY = unit.status.current_soul.personality.traits.STRESS_VULNERABILITY
local TOLERANT = unit.status.current_soul.personality.traits.TOLERANT
local CRAFTSMANSHIP = setbelief.getUnitBelief(unit, df.value_type['CRAFTSMANSHIP'])
local FAMILY = setbelief.getUnitBelief(unit, df.value_type['FAMILY'])
local HARMONY = setbelief.getUnitBelief(unit, df.value_type['HARMONY'])
local INDEPENDENCE = setbelief.getUnitBelief(unit, df.value_type['INDEPENDENCE'])
local KNOWLEDGE = setbelief.getUnitBelief(unit, df.value_type['KNOWLEDGE'])
local LEISURE_TIME = setbelief.getUnitBelief(unit, df.value_type['LEISURE_TIME'])
local NATURE = setbelief.getUnitBelief(unit, df.value_type['NATURE'])
local SKILL = setbelief.getUnitBelief(unit, df.value_type['SKILL'])
-- Calculate the rating using the defined variables
local rating = (CRAFTSMANSHIP * -0.01) + (FAMILY * -0.09) + (HARMONY * 0.05)
+ (INDEPENDENCE * 0.06) + (KNOWLEDGE * -0.30) + (LEISURE_TIME * 0.24)
+ (NATURE * 0.27) + (SKILL * -0.21) + (ALTRUISM * 0.13)
+ (ANXIETY_PROPENSITY * -0.06) + (BRAVERY * 0.06)
+ (CHEER_PROPENSITY * 0.41) + (CURIOUS * -0.06) + (DISCORD * 0.14)
+ (DUTIFULNESS * -0.03) + (EMOTIONALLY_OBSESSIVE * -0.13)
+ (HUMOR * -0.05) + (LOVE_PROPENSITY * 0.15) + (PERSEVERENCE * -0.07)
+ (POLITENESS * -0.14) + (PRIVACY * 0.03) + (STRESS_VULNERABILITY * -0.20)
+ (TOLERANT * -0.11)
local altruism = unit.status.current_soul.personality.traits.ALTRUISM
local anxiety_propensity = unit.status.current_soul.personality.traits.ANXIETY_PROPENSITY
local bravery = unit.status.current_soul.personality.traits.BRAVERY
local cheer_propensity = unit.status.current_soul.personality.traits.CHEER_PROPENSITY
local curious = unit.status.current_soul.personality.traits.CURIOUS
local discord = unit.status.current_soul.personality.traits.DISCORD
local dutifulness = unit.status.current_soul.personality.traits.DUTIFULNESS
local emotionally_obsessive = unit.status.current_soul.personality.traits.EMOTIONALLY_OBSESSIVE
local humor = unit.status.current_soul.personality.traits.HUMOR
local love_propensity = unit.status.current_soul.personality.traits.LOVE_PROPENSITY
local perseverence = unit.status.current_soul.personality.traits.PERSEVERENCE
local politeness = unit.status.current_soul.personality.traits.POLITENESS
local privacy = unit.status.current_soul.personality.traits.PRIVACY
local stress_vulnerability = unit.status.current_soul.personality.traits.STRESS_VULNERABILITY
local tolerant = unit.status.current_soul.personality.traits.TOLERANT
local craftsmanship = setbelief.getUnitBelief(unit, df.value_type['CRAFTSMANSHIP'])
local family = setbelief.getUnitBelief(unit, df.value_type['FAMILY'])
local harmony = setbelief.getUnitBelief(unit, df.value_type['HARMONY'])
local independence = setbelief.getUnitBelief(unit, df.value_type['INDEPENDENCE'])
local knowledge = setbelief.getUnitBelief(unit, df.value_type['KNOWLEDGE'])
local leisure_time = setbelief.getUnitBelief(unit, df.value_type['LEISURE_TIME'])
local nature = setbelief.getUnitBelief(unit, df.value_type['NATURE'])
local skill = setbelief.getUnitBelief(unit, df.value_type['SKILL'])
-- calculate the rating using the defined variables
local rating = (craftsmanship * -0.01) + (family * -0.09) + (harmony * 0.05)
+ (independence * 0.06) + (knowledge * -0.30) + (leisure_time * 0.24)
+ (nature * 0.27) + (skill * -0.21) + (altruism * 0.13)
+ (anxiety_propensity * -0.06) + (bravery * 0.06)
+ (cheer_propensity * 0.41) + (curious * -0.06) + (discord * 0.14)
+ (dutifulness * -0.03) + (emotionally_obsessive * -0.13)
+ (humor * -0.05) + (love_propensity * 0.15) + (perseverence * -0.07)
+ (politeness * -0.14) + (privacy * 0.03) + (stress_vulnerability * -0.20)
+ (tolerant * -0.11)
return rating
end
@ -1148,9 +1148,7 @@ function SquadAssignmentOverlay:refresh_list(sort_widget, sort_fn)
end
function SquadAssignmentOverlay:onInput(keys)
if keys._MOUSE_R_DOWN or
keys._MOUSE_L_DOWN and not self:getMouseFramePos()
then
if keys._MOUSE_R or (keys._MOUSE_L and not self:getMouseFramePos()) then
-- if any click is made outside of our window, we may need to refresh our list
self.dirty = true
end

@ -801,10 +801,12 @@ end
function AssignAnimalScreen:onInput(keys)
local handled = AssignAnimalScreen.super.onInput(self, keys)
if not self.is_valid_ui_state() then
if view then
view:dismiss()
end
return
end
if keys._MOUSE_L_DOWN then
if keys._MOUSE_L then
-- if any click is made outside of our window, we need to recheck unit properties
local window = self.subviews[1]
if not window:getMouseFramePos() then
@ -818,7 +820,7 @@ function AssignAnimalScreen:onInput(keys)
end
function AssignAnimalScreen:onRenderFrame()
if not self.is_valid_ui_state() then
if view and not self.is_valid_ui_state() then
view:dismiss()
end
end
@ -1072,6 +1074,7 @@ function CageChainOverlay:init()
frame={t=0, l=0, r=0, h=1},
label='DFHack assign',
key='CUSTOM_CTRL_T',
visible=is_valid_building,
on_activate=function() view = view and view:raise() or show_cage_chain_screen() end,
},
}

@ -10,6 +10,7 @@
#include "json/json.h"
#include "df/building.h"
#include "df/gamest.h"
#include "df/historical_figure.h"
#include "df/itemdef_ammost.h"
#include "df/itemdef_armorst.h"
@ -36,6 +37,8 @@
using namespace DFHack;
using namespace df::enums;
using df::global::game;
DFHACK_PLUGIN("orders");
REQUIRE_GLOBAL(world);
@ -64,6 +67,8 @@ static command_result orders_export_command(color_ostream & out, const std::stri
static command_result orders_import_command(color_ostream & out, const std::string & name);
static command_result orders_clear_command(color_ostream & out);
static command_result orders_sort_command(color_ostream & out);
static command_result orders_recheck_command(color_ostream & out);
static command_result orders_recheck_current_command(color_ostream & out);
static command_result orders_command(color_ostream & out, std::vector<std::string> & parameters)
{
@ -111,6 +116,19 @@ static command_result orders_command(color_ostream & out, std::vector<std::strin
return orders_sort_command(out);
}
if (parameters[0] == "recheck" && parameters.size() == 1)
{
return orders_recheck_command(out);
}
if (parameters[0] == "recheck" && parameters.size() == 2)
{
if (parameters[1] == "this")
{
return orders_recheck_current_command(out);
}
}
return CR_WRONG_USAGE;
}
@ -1015,3 +1033,27 @@ static command_result orders_sort_command(color_ostream & out)
return CR_OK;
}
static command_result orders_recheck_command(color_ostream & out)
{
for (auto it : world->manager_orders)
{
it->status.bits.active = false;
it->status.bits.validated = false;
}
return CR_OK;
}
static command_result orders_recheck_current_command(color_ostream & out)
{
if (game->main_interface.info.work_orders.conditions.open)
{
game->main_interface.info.work_orders.conditions.wq->status.bits.active = false;
}
else
{
out << COLOR_LIGHTRED << "Order conditions is not open" << std::endl;
return CR_FAILURE;
}
return CR_OK;
}

@ -86,7 +86,7 @@ struct viewscreen_overlay : T {
if (!input_is_handled)
INTERPOSE_NEXT(feed)(input);
else
enabler->last_text_input[0] = '\0';
dfhack_lua_viewscreen::markInputAsHandled();
}
DEFINE_VMETHOD_INTERPOSE(void, render, ()) {
INTERPOSE_NEXT(render)();

@ -36,7 +36,7 @@ namespace DFHack {
static std::vector<TexposHandle> textures;
DFhackCExport command_result plugin_init(color_ostream &out, std::vector<PluginCommand> &commands) {
textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32);
textures = Textures::loadTileset("hack/data/art/pathable.png", 32, 32, true);
return CR_OK;
}

@ -1 +1 @@
Subproject commit e0591830b72cdfaec5c9bdb1bf713a74fe744788
Subproject commit d2ad86165e89dc3b0f262eea00db8e2347cc4421

@ -42,17 +42,17 @@ function test.editfield_click()
expect.eq(5, e.cursor)
mock.patch(e, 'getMousePos', mock.func(0), function()
e:onInput{_MOUSE_L=true}
e:onInput{_MOUSE_L_DOWN=true}
expect.eq(1, e.cursor)
end)
mock.patch(e, 'getMousePos', mock.func(20), function()
e:onInput{_MOUSE_L=true}
e:onInput{_MOUSE_L_DOWN=true}
expect.eq(5, e.cursor, 'should only seek to end of text')
end)
mock.patch(e, 'getMousePos', mock.func(2), function()
e:onInput{_MOUSE_L=true}
e:onInput{_MOUSE_L_DOWN=true}
expect.eq(3, e.cursor)
end)
end

@ -59,37 +59,37 @@ function test.onInput()
s:update(23, 10, 50)
expect.false_(s:onInput{}, 'no mouse down')
expect.false_(s:onInput{_MOUSE_L_DOWN=true}, 'no y coord')
expect.false_(s:onInput{_MOUSE_L=true}, 'no y coord')
spec, y = nil, 0
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.eq('up_small', spec, 'on up arrow')
spec, y = nil, 1
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.eq('up_large', spec, 'on body above bar')
spec, y = nil, 44
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.eq('up_large', spec, 'on body just above bar')
spec, y = nil, 45
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.nil_(spec, 'on top of bar')
spec, y = nil, 63
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.nil_(spec, 'on bottom of bar')
spec, y = nil, 64
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.eq('down_large', spec, 'on body just below bar')
spec, y = nil, 98
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.eq('down_large', spec, 'on body below bar')
spec, y = nil, 99
expect.true_(s:onInput{_MOUSE_L_DOWN=true})
expect.true_(s:onInput{_MOUSE_L=true})
expect.eq('down_small', spec, 'on down arrow')
end

@ -7,7 +7,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_DOWN=true}
l:onInput{_MOUSE_L=true}
expect.eq(1, func.call_count)
end)
end
@ -33,7 +33,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_DOWN=true}
l:onInput{_MOUSE_L=true}
expect.false_(l:getOptionValue())
end)
end

@ -1,5 +1,5 @@
config.mode = 'fortress'
--config.target = 'orders'
config.target = 'orders'
local FILE_PATH_PATTERN = 'dfhack-config/orders/%s.json'