diff --git a/docs/changelog.txt b/docs/changelog.txt index a94bd44ac4..4ebb60bd67 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -63,6 +63,7 @@ Template for new versions: ## Fixes - Fix broken weather lookup in ``World::ReadCurrentWeather`` +- Fixed a possible hang or assertion failure when pressing a hotkey while a DFHack GUI window was open but unfocused - `3dveins`: fix failure on embarks that cross midmap tiles - `aquifer`: make ``--skip-top`` and top-relative ``--levels`` take effect for the ``drain``, ``convert``, and ``add`` actions instead of being silently ignored - `autodump`: ``destroy`` no longer leaves the contents of destroyed containers in limbo or crashes when destroying unit-held items diff --git a/library/include/Core.h b/library/include/Core.h index 2957a1347a..0245d79df7 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -163,6 +163,9 @@ namespace DFHack /// check if the activity lock is owned by this thread bool isSuspended(void); + /// check if this is DF's render thread, which can never acquire the + /// activity lock and therefore can never call into Lua + bool isRenderThread() { return df_render_thread == std::this_thread::get_id(); } /// Is everything OK? bool isValid(void) { return !errorstate; } diff --git a/library/modules/Gui.cpp b/library/modules/Gui.cpp index e7c8233ba1..96a7ab5472 100644 --- a/library/modules/Gui.cpp +++ b/library/modules/Gui.cpp @@ -99,6 +99,7 @@ distribution. #include #include #include +#include using std::string; using std::vector; @@ -1065,6 +1066,33 @@ bool Gui::any_job_hotkey(df::viewscreen *top) || workshop_job_hotkey(top); } +// Resolves the selection reported by a DFHack viewscreen, if it can be +// queried safely on this thread. Returns the selection (possibly null) when +// the question is answered, or std::nullopt when the caller should continue +// to the native UI checks. +// +// getSelected*() on a lua-backed screen enters Lua through CoreSuspender, +// which asserts or deadlocks on the render thread, where hotkey guards run. +// A focused lua screen owns the selection, so we must report none; a +// defocused lua screen is transparent to the UI below, so the native checks +// still apply. +template +static std::optional dfscreen_selection(df::viewscreen *top, + R (dfhack_viewscreen::*method)()) +{ + auto dfscreen = dfhack_viewscreen::try_cast(top); + if (!dfscreen) + return std::nullopt; + + if (!dfscreen->is_lua_screen() || !Core::getInstance().isRenderThread()) + return (dfscreen->*method)(); + + if (dfscreen->isFocused()) + return nullptr; + + return std::nullopt; +} + df::job *Gui::getAnyWorkshopJob(df::viewscreen *top) { auto bld = getAnyBuilding(top); if (!bld) @@ -1084,8 +1112,8 @@ df::job *Gui::getSelectedWorkshopJob(color_ostream &out, bool quiet) { } df::job *Gui::getAnyJob(df::viewscreen *top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedJob(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedJob)) + return *sel; if (matchFocusString("dwarfmode/Info/JOBS")) { auto &cri_job = game->main_interface.info.jobs.cri_job; @@ -1124,8 +1152,8 @@ df::job *Gui::getSelectedJob(color_ostream &out, bool quiet) df::unit *Gui::getAnyUnit(df::viewscreen *top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedUnit(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedUnit)) + return *sel; if (game->main_interface.view_sheets.open && game->main_interface.view_sheets.active_sheet == view_sheet_type::UNIT) @@ -1417,8 +1445,8 @@ df::item *Gui::getAnyItem(df::viewscreen *top) { using df::global::game; - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedItem(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedItem)) + return *sel; if (game->main_interface.view_sheets.open && game->main_interface.view_sheets.active_sheet == view_sheet_type::ITEM) @@ -1568,8 +1596,8 @@ bool Gui::any_stockpile_hotkey(df::viewscreen* top) } df::building_stockpilest* Gui::getAnyStockpile(df::viewscreen* top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedStockpile(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedStockpile)) + return *sel; if (game->main_interface.bottom_mode_selected == main_bottom_mode_type::STOCKPILE) return game->main_interface.stockpile.cur_bld; @@ -1591,8 +1619,8 @@ bool Gui::any_civzone_hotkey(df::viewscreen* top) { } df::building_civzonest *Gui::getAnyCivZone(df::viewscreen* top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedCivZone(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedCivZone)) + return *sel; if (game->main_interface.bottom_mode_selected == main_bottom_mode_type::ZONE) return game->main_interface.civzone.cur_bld; @@ -1611,8 +1639,8 @@ df::building_civzonest *Gui::getSelectedCivZone(color_ostream &out, bool quiet) df::building *Gui::getAnyBuilding(df::viewscreen *top) { - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedBuilding(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedBuilding)) + return *sel; if (game->main_interface.view_sheets.open && game->main_interface.view_sheets.active_sheet == view_sheet_type::BUILDING) @@ -1686,8 +1714,8 @@ df::plant *Gui::getAnyPlant(df::viewscreen *top) { using df::global::cursor; - if (auto dfscreen = dfhack_viewscreen::try_cast(top)) - return dfscreen->getSelectedPlant(); + if (auto sel = dfscreen_selection(top, &dfhack_viewscreen::getSelectedPlant)) + return *sel; if (Gui::dwarfmode_hotkey(top) && has_cursor()) {