From 60cd9b713f2f7980c8ce7133003019b11b6e7248 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 14:47:39 -0500 Subject: [PATCH 1/5] Add new plugin edgescroll to automate panning of game/region maps --- docs/changelog.txt | 1 + docs/plugins/edgescroll.rst | 13 +++ plugins/CMakeLists.txt | 1 + plugins/edgescroll.cpp | 197 ++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+) create mode 100644 docs/plugins/edgescroll.rst create mode 100644 plugins/edgescroll.cpp diff --git a/docs/changelog.txt b/docs/changelog.txt index 91c50db23a..45cbc95137 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: # Future ## New Tools +- ``edgescroll``: Introduced plugin to pan the view automatically when the mouse reaches the screen border. ## New Features diff --git a/docs/plugins/edgescroll.rst b/docs/plugins/edgescroll.rst new file mode 100644 index 0000000000..4522ff36bf --- /dev/null +++ b/docs/plugins/edgescroll.rst @@ -0,0 +1,13 @@ +edgescroll +========== + +.. dfhack-tool:: + :summary: Scroll the game world and region maps when the mouse reaches the window border. + :tags: interface + +Usage +----- + +:: + + enable edgescroll diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index a49fdf7a9f..0ac4fe9b57 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -75,6 +75,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(dwarfvet dwarfvet.cpp LINK_LIBRARIES lua) #dfhack_plugin(dwarfmonitor dwarfmonitor.cpp LINK_LIBRARIES lua) #add_subdirectory(embark-assistant) + dfhack_plugin(edgescroll edgescroll.cpp) dfhack_plugin(eventful eventful.cpp LINK_LIBRARIES lua) dfhack_plugin(fastdwarf fastdwarf.cpp) dfhack_plugin(filltraffic filltraffic.cpp) diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp new file mode 100644 index 0000000000..7c25287c82 --- /dev/null +++ b/plugins/edgescroll.cpp @@ -0,0 +1,197 @@ +#include "ColorText.h" +#include "PluginManager.h" +#include "MemAccess.h" + +#include "df/world_generatorst.h" +#include "modules/Gui.h" + +#include "df/enabler.h" +#include "df/gamest.h" +#include "df/graphic.h" +#include "df/graphic_viewportst.h" +#include "df/renderer_2d.h" +#include "df/viewscreen_choose_start_sitest.h" +#include "df/viewscreen_worldst.h" +#include "df/viewscreen_new_regionst.h" +#include "df/world.h" +#include "df/world_data.h" + +#include +#include + +using namespace DFHack; + +DFHACK_PLUGIN("edgescroll"); +DFHACK_PLUGIN_IS_ENABLED(is_enabled); + +REQUIRE_GLOBAL(enabler); +REQUIRE_GLOBAL(game); +REQUIRE_GLOBAL(world); +REQUIRE_GLOBAL(gps); + +// Cooldown between edge scroll actions +constexpr uint32_t cooldown_ms = 100; +// Number of pixels from border to trigger edgescroll +constexpr int border_range = 5; + +// Controls how much edge scroll moves +constexpr int map_scroll_pixels = 100; +constexpr int world_scroll_tiles = 3; +constexpr int world_scroll_tiles_zoomed = 6; + +DFhackCExport command_result plugin_init([[maybe_unused]]color_ostream &out, [[maybe_unused]] std::vector &commands) { + return CR_OK; +} + +DFhackCExport command_result plugin_enable([[maybe_unused]]color_ostream &out, bool enable) { + is_enabled = enable; + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out) { + return CR_OK; +} + +template +static void apply_scroll(T* out, T diff, T min, T max) { + *out = std::min(std::max(*out + diff, min), max); +} + +static void scroll_dwarfmode(int xdiff, int ydiff) { + using df::global::window_x; + using df::global::window_y; + using df::global::game; + // Scale the movement by pixels, to keep scroll speeds visually consistent + int tilesize = gps->viewport_zoom_factor / 4; + int width = gps->main_viewport->dim_x; + int height = gps->main_viewport->dim_y; + + // Ensure the map doesn't go fully off-screen + int min_x = -width / 2; + int min_y = -height / 2; + int max_x = world->map.x_count - (width / 2); + int max_y = world->map.y_count - (height / 2); + apply_scroll(window_x, xdiff * std::max(1, map_scroll_pixels / tilesize), min_x, max_x); + apply_scroll(window_y, ydiff * std::max(1, map_scroll_pixels / tilesize), min_y, max_y); + + // Force a minimap update + game->minimap.update = 1; + game->minimap.mustmake = 1; +} + +template +static void scroll_world(T* screen, int xdiff, int ydiff) { + if constexpr(std::is_same_v) { + if (screen->zoomed_in) { + int max_x = (world->world_data->world_width * 16)-1; + int max_y = (world->world_data->world_height * 16)-1; + apply_scroll(&screen->zoom_cent_x, xdiff * world_scroll_tiles_zoomed, 0, max_x); + apply_scroll(&screen->zoom_cent_y, ydiff * world_scroll_tiles_zoomed, 0, max_y); + return; + } + } + + int32_t *x, *y; + if constexpr(std::is_same_v) { + x = &world->worldgen_status.cursor_x; + y = &world->worldgen_status.cursor_y; + } else { + x = &screen->region_cent_x; + y = &screen->region_cent_y; + } + int max_x = world->world_data->world_width-1; + int max_y = world->world_data->world_height-1; + apply_scroll(x, xdiff * world_scroll_tiles, 0, max_x); + apply_scroll(y, ydiff * world_scroll_tiles, 0, max_y); +} + +template +struct overloads : Ts... { using Ts::operator()...; }; + +using world_map = std::variant; +static std::optional get_map() { + df::viewscreen* screen = Gui::getCurViewscreen(true); + screen = Gui::getDFViewscreen(true, screen); // Get the first non-dfhack viewscreen + if (auto start_site = virtual_cast(screen)) + return start_site; + if (auto world_map = virtual_cast(screen)) + return world_map; + if (auto worldgen_map = virtual_cast(screen)) { + if (!world || world->worldgen_status.state <= df::world_generatorst::Initializing) + return {}; // Map isn't displayed yet + return worldgen_map; + } + return {}; +} + +static void scroll_world(world_map screen, int xdiff, int ydiff) { + const auto visitor = overloads { + [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world(s, xdiff, ydiff);}, + }; + std::visit(visitor, screen); +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + + // Ensure either a map viewscreen or the main viewport are visible + auto worldmap = get_map(); + if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) + return CR_OK; + + // FIXME: Once dfhooks_sdl_loop is hooked up in Core, use SDL_GetMouseState + // to determine the correct mouse position without forcing the screen to + // render slightly un-centered. + // origin_x/y are already zero if not fitting the interface to the grid + + // Force the origin_x/y values to zero to workaround df marking any + // mouse position within the margin register as invalid + auto renderer = virtual_cast(enabler->renderer); + if (renderer && (renderer->origin_x != 0 || renderer->origin_y != 0)) { + renderer->origin_x = 0; + renderer->origin_y = 0; + } + + auto dim_x = gps->screen_pixel_x; + auto dim_y = gps->screen_pixel_y; + auto x = gps->precise_mouse_x; + auto y = gps->precise_mouse_y; + if (x == -1 || y == -1) + return CR_OK; // Invalid mouse position + + // Apply a cooldown to any potential edgescrolls + auto& core = Core::getInstance(); + static uint32_t last_action = 0; + uint32_t now = core.p->getTickCount(); + if (now < last_action + cooldown_ms) + return CR_OK; + + + int xdiff = 0; + int ydiff = 0; + if (x <= border_range) { + xdiff--; + } else if (x >= dim_x - border_range) { + xdiff++; + } + if (y <= border_range) { + ydiff--; + } else if (y >= dim_y - border_range) { + ydiff++; + } + + if (xdiff == 0 && ydiff == 0) + return CR_OK; // No work to do + + // Dispatch scrolling to active scrollables + if (worldmap.has_value()) + scroll_world(worldmap.value(), xdiff, ydiff); + else if (gps->main_viewport->flag.bits.active) + scroll_dwarfmode(xdiff, ydiff); + + // Update cooldown + last_action = now; + + return CR_OK; +} From 04da99d53970be81bdcc6ec434eb8facd6be67fb Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 21:27:23 -0500 Subject: [PATCH 2/5] Setup dfhooks_sdl_loop and use it to read exact mouse position for edgescroll --- library/Core.cpp | 4 ++ library/Hooks.cpp | 1 + library/include/Core.h | 2 + library/include/modules/DFSDL.h | 10 +++ library/modules/DFSDL.cpp | 36 +++++++++++ plugins/edgescroll.cpp | 108 ++++++++++++++++++++------------ 6 files changed, 120 insertions(+), 41 deletions(-) diff --git a/library/Core.cpp b/library/Core.cpp index bb631414c6..3be676644e 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -2508,6 +2508,10 @@ bool Core::DFH_SDL_Event(SDL_Event* ev) { return ret; } +void Core::DFH_SDL_Loop() { + DFHack::runRenderThreadCallbacks(); +} + bool Core::doSdlInputEvent(SDL_Event* ev) { // this should only ever be called from the render thread diff --git a/library/Hooks.cpp b/library/Hooks.cpp index 951472eba6..0f957fea06 100644 --- a/library/Hooks.cpp +++ b/library/Hooks.cpp @@ -68,6 +68,7 @@ DFhackCExport void dfhooks_sdl_loop() { if (disabled) return; // TODO: wire this up to the new SDL-based console once it is merged + DFHack::Core::getInstance().DFH_SDL_Loop(); } // called from the main thread for each utf-8 char read from the ncurses input diff --git a/library/include/Core.h b/library/include/Core.h index 3ea8f68ef1..9791464c28 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -151,6 +151,7 @@ namespace DFHack friend void ::dfhooks_update(); friend void ::dfhooks_prerender(); friend bool ::dfhooks_sdl_event(SDL_Event* event); + friend void ::dfhooks_sdl_loop(); friend bool ::dfhooks_ncurses_key(int key); public: /// Get the single Core instance or make one. @@ -238,6 +239,7 @@ namespace DFHack int Update (void); int Shutdown (void); bool DFH_SDL_Event(SDL_Event* event); + void DFH_SDL_Loop(); bool ncurses_wgetch(int in, int & out); bool DFH_ncurses_key(int key); diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index ff8e81ab2c..3d371d3ea5 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -3,10 +3,13 @@ #include "Export.h" #include "ColorText.h" +#include +#include #include struct SDL_Surface; struct SDL_Rect; +struct SDL_Renderer; struct SDL_PixelFormat; struct SDL_Window; union SDL_Event; @@ -55,6 +58,10 @@ namespace DFHack::DFSDL DFHACK_EXPORT SDL_Surface* DFSDL_CreateRGBSurfaceWithFormat(uint32_t flags, int width, int height, int depth, uint32_t format); DFHACK_EXPORT int DFSDL_ShowSimpleMessageBox(uint32_t flags, const char* title, const char* message, SDL_Window* window); + DFHACK_EXPORT uint32_t DFSDL_GetMouseState(int* x, int* y); + DFHACK_EXPORT void DFSDL_RenderWindowToLogical(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); + DFHACK_EXPORT void DFSDL_RenderLogicalToWindow(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); + // submitted and returned text is UTF-8 // see wrapper functions below for cp-437 variants DFHACK_EXPORT char* DFSDL_GetClipboardText(); @@ -76,4 +83,7 @@ namespace DFHack DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); + // Queue a cb to be run on the render thread, with optional userdata + DFHACK_EXPORT void runOnRenderThread(std::function cb, void* userdata); + DFHACK_EXPORT void runRenderThreadCallbacks(); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 9da2bd8057..666844f6cb 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -8,6 +8,8 @@ #include +#include + #ifdef WIN32 # include #endif @@ -61,6 +63,9 @@ SDL_Surface* (*g_SDL_CreateRGBSurfaceWithFormat)(uint32_t flags, int width, int int (*g_SDL_ShowSimpleMessageBox)(uint32_t flags, const char *title, const char *message, SDL_Window *window) = nullptr; char* (*g_SDL_GetPrefPath)(const char* org, const char* app) = nullptr; char* (*g_SDL_GetBasePath)() = nullptr; +uint32_t (*g_SDL_GetMouseState)(int* x, int* y) = nullptr; +void (*g_SDL_RenderWindowToLogical)(SDL_Renderer* renderer, int windowX, int windowY, float* logicalX, float* logicalY); +void (*g_SDL_RenderLogicalToWindow)(SDL_Renderer* renderer, float logicalX, float logicalY, int* windowX, int* windowY); bool DFSDL::init(color_ostream &out) { for (auto &lib_str : SDL_LIBS) { @@ -106,6 +111,9 @@ bool DFSDL::init(color_ostream &out) { bind(g_sdl_handle, SDL_ShowSimpleMessageBox); bind(g_sdl_handle, SDL_GetPrefPath); bind(g_sdl_handle, SDL_GetBasePath); + bind(g_sdl_handle, SDL_GetMouseState); + bind(g_sdl_handle, SDL_RenderWindowToLogical); + bind(g_sdl_handle, SDL_RenderLogicalToWindow); #undef bind DEBUG(dfsdl,out).print("sdl successfully loaded\n"); @@ -190,6 +198,18 @@ char* DFSDL::DFSDL_GetBasePath() return g_SDL_GetBasePath(); } +uint32_t DFSDL::DFSDL_GetMouseState(int* x, int* y) { + return g_SDL_GetMouseState(x, y); +} + +void DFSDL::DFSDL_RenderWindowToLogical(SDL_Renderer *renderer, int windowX, int windowY, float *logicalX, float *logicalY) { + g_SDL_RenderWindowToLogical(renderer, windowX, windowY, logicalX, logicalY); +} + +void DFSDL::DFSDL_RenderLogicalToWindow(SDL_Renderer *renderer, float logicalX, float logicalY, int *windowX, int *windowY) { + g_SDL_RenderLogicalToWindow(renderer, logicalX, logicalY, windowX, windowY); +} + int DFSDL::DFSDL_ShowSimpleMessageBox(uint32_t flags, const char *title, const char *message, SDL_Window *window) { if (!g_SDL_ShowSimpleMessageBox) return -1; @@ -266,3 +286,19 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { } return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(str.str().c_str()); } + +static std::recursive_mutex render_cb_lock; +static std::vector, void*>> render_cb_queue; + +DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { + std::lock_guard l(render_cb_lock); + render_cb_queue.push_back({cb, userdata}); +} + +DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { + std::lock_guard l(render_cb_lock); + for (auto& cb : render_cb_queue) { + std::get<0>(cb)(std::get<1>(cb)); + } + render_cb_queue.clear(); +} diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 7c25287c82..2a26797281 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -4,6 +4,7 @@ #include "df/world_generatorst.h" #include "modules/Gui.h" +#include "modules/DFSDL.h" #include "df/enabler.h" #include "df/gamest.h" @@ -52,6 +53,62 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out return CR_OK; } +static std::atomic_bool request_queued = false; + +struct scroll_state { + int8_t xdiff; + int8_t ydiff; +}; + +static const scroll_state state_default(0, 0); + +static scroll_state state = state_default; +static scroll_state queued = state_default; + +static void render_thread_cb([[maybe_unused]] void* _) { + queued = state_default; + // Ignore the mouse if outside the window + if (!enabler->mouse_focus) { + request_queued.store(false); + return; + } + + // Determine window border location in window coordinates + auto* renderer = virtual_cast(enabler->renderer); + int origin_x, origin_y = 0; + int end_x, end_y; + DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->origin_x, renderer->origin_y, &origin_x, &origin_y); + DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->cur_w - renderer->origin_x, renderer->cur_h - renderer->origin_y, &end_x, &end_y); + + int mx, my; + DFSDL::DFSDL_GetMouseState(&mx, &my); + + if (mx <= origin_x + border_range) { + queued.xdiff--; + } else if (mx >= end_x - border_range) { + queued.xdiff++; + } + if (my <= origin_y + border_range) { + queued.ydiff--; + } else if (my >= end_y - border_range) { + queued.ydiff++; + } + + request_queued.store(false); +} + +static bool update_mouse_pos() { + if (request_queued.load()) + return false; // No new inputs, and a request for more is already placed + + state = queued; + queued = state_default; + DFHack::runOnRenderThread(render_thread_cb, nullptr); + request_queued.store(true); + return true; +} + +// Scrolling behavior template static void apply_scroll(T* out, T diff, T min, T max) { *out = std::min(std::max(*out + diff, min), max); @@ -134,32 +191,6 @@ static void scroll_world(world_map screen, int xdiff, int ydiff) { } DFhackCExport command_result plugin_onupdate(color_ostream &out) { - - // Ensure either a map viewscreen or the main viewport are visible - auto worldmap = get_map(); - if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) - return CR_OK; - - // FIXME: Once dfhooks_sdl_loop is hooked up in Core, use SDL_GetMouseState - // to determine the correct mouse position without forcing the screen to - // render slightly un-centered. - // origin_x/y are already zero if not fitting the interface to the grid - - // Force the origin_x/y values to zero to workaround df marking any - // mouse position within the margin register as invalid - auto renderer = virtual_cast(enabler->renderer); - if (renderer && (renderer->origin_x != 0 || renderer->origin_y != 0)) { - renderer->origin_x = 0; - renderer->origin_y = 0; - } - - auto dim_x = gps->screen_pixel_x; - auto dim_y = gps->screen_pixel_y; - auto x = gps->precise_mouse_x; - auto y = gps->precise_mouse_y; - if (x == -1 || y == -1) - return CR_OK; // Invalid mouse position - // Apply a cooldown to any potential edgescrolls auto& core = Core::getInstance(); static uint32_t last_action = 0; @@ -167,28 +198,23 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (now < last_action + cooldown_ms) return CR_OK; + // Update mouse_x/y to values from the render thread + if (!update_mouse_pos()) + return CR_OK; - int xdiff = 0; - int ydiff = 0; - if (x <= border_range) { - xdiff--; - } else if (x >= dim_x - border_range) { - xdiff++; - } - if (y <= border_range) { - ydiff--; - } else if (y >= dim_y - border_range) { - ydiff++; - } + // Ensure either a map viewscreen or the main viewport are visible + auto worldmap = get_map(); + if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) + return CR_OK; - if (xdiff == 0 && ydiff == 0) + if (state.xdiff == 0 && state.ydiff == 0) return CR_OK; // No work to do // Dispatch scrolling to active scrollables if (worldmap.has_value()) - scroll_world(worldmap.value(), xdiff, ydiff); + scroll_world(worldmap.value(), state.xdiff, state.ydiff); else if (gps->main_viewport->flag.bits.active) - scroll_dwarfmode(xdiff, ydiff); + scroll_dwarfmode(state.xdiff, state.ydiff); // Update cooldown last_action = now; From bfd9bc4a46a27ddda326292dfa679722c734f5c3 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 27 Nov 2025 23:52:02 -0500 Subject: [PATCH 3/5] Release render_cb_lock earlier --- library/modules/DFSDL.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 666844f6cb..2a67351d14 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -287,18 +287,24 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { return 0 == DFHack::DFSDL::DFSDL_SetClipboardText(str.str().c_str()); } +// Queue to run callbacks on the render thread. +// Semantics loosely based on SDL3's SDL_RunOnMainThread static std::recursive_mutex render_cb_lock; static std::vector, void*>> render_cb_queue; DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { std::lock_guard l(render_cb_lock); - render_cb_queue.push_back({cb, userdata}); + render_cb_queue.push_back({std::move(cb), userdata}); } DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { - std::lock_guard l(render_cb_lock); - for (auto& cb : render_cb_queue) { + static decltype(render_cb_queue) local_queue; + { + std::lock_guard l(render_cb_lock); + std::swap(local_queue, render_cb_queue); + } + for (auto& cb : local_queue) { std::get<0>(cb)(std::get<1>(cb)); } - render_cb_queue.clear(); + local_queue.clear(); } From 8f9688b439b4a5a7cd636aa979763feb699933ab Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 28 Nov 2025 22:54:07 -0500 Subject: [PATCH 4/5] Code cleanup and implement review suggestions --- library/include/modules/DFSDL.h | 4 +- library/modules/DFSDL.cpp | 8 ++-- plugins/edgescroll.cpp | 71 +++++++++++++++++++-------------- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/library/include/modules/DFSDL.h b/library/include/modules/DFSDL.h index 3d371d3ea5..be40eff2c5 100644 --- a/library/include/modules/DFSDL.h +++ b/library/include/modules/DFSDL.h @@ -83,7 +83,7 @@ namespace DFHack DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector * lines); DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text); - // Queue a cb to be run on the render thread, with optional userdata - DFHACK_EXPORT void runOnRenderThread(std::function cb, void* userdata); + // Queue a cb to be run on the render thread + DFHACK_EXPORT void runOnRenderThread(std::function cb); DFHACK_EXPORT void runRenderThreadCallbacks(); } diff --git a/library/modules/DFSDL.cpp b/library/modules/DFSDL.cpp index 2a67351d14..9543f81387 100644 --- a/library/modules/DFSDL.cpp +++ b/library/modules/DFSDL.cpp @@ -290,11 +290,11 @@ DFHACK_EXPORT bool DFHack::setClipboardTextCp437Multiline(string text) { // Queue to run callbacks on the render thread. // Semantics loosely based on SDL3's SDL_RunOnMainThread static std::recursive_mutex render_cb_lock; -static std::vector, void*>> render_cb_queue; +static std::vector> render_cb_queue; -DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb, void *userdata) { +DFHACK_EXPORT void DFHack::runOnRenderThread(std::function cb) { std::lock_guard l(render_cb_lock); - render_cb_queue.push_back({std::move(cb), userdata}); + render_cb_queue.push_back(std::move(cb)); } DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { @@ -304,7 +304,7 @@ DFHACK_EXPORT void DFHack::runRenderThreadCallbacks() { std::swap(local_queue, render_cb_queue); } for (auto& cb : local_queue) { - std::get<0>(cb)(std::get<1>(cb)); + cb(); } local_queue.clear(); } diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 2a26797281..08a50430f8 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -1,8 +1,7 @@ #include "ColorText.h" -#include "PluginManager.h" #include "MemAccess.h" +#include "PluginManager.h" -#include "df/world_generatorst.h" #include "modules/Gui.h" #include "modules/DFSDL.h" @@ -16,6 +15,7 @@ #include "df/viewscreen_new_regionst.h" #include "df/world.h" #include "df/world_data.h" +#include "df/world_generatorst.h" #include #include @@ -53,33 +53,37 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out return CR_OK; } -static std::atomic_bool request_queued = false; +static std::atomic_bool callback_queued = false; struct scroll_state { int8_t xdiff; int8_t ydiff; }; -static const scroll_state state_default(0, 0); +static scroll_state state; +static scroll_state queued; -static scroll_state state = state_default; -static scroll_state queued = state_default; - -static void render_thread_cb([[maybe_unused]] void* _) { - queued = state_default; +static void render_thread_cb() { + queued = {0}; // Ignore the mouse if outside the window if (!enabler->mouse_focus) { - request_queued.store(false); + callback_queued.store(false); return; } - // Determine window border location in window coordinates + // Calculate the render rect in window coordinates auto* renderer = virtual_cast(enabler->renderer); - int origin_x, origin_y = 0; + int origin_x, origin_y; int end_x, end_y; - DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->origin_x, renderer->origin_y, &origin_x, &origin_y); - DFSDL::DFSDL_RenderLogicalToWindow((SDL_Renderer*)renderer->sdl_renderer, renderer->cur_w - renderer->origin_x, renderer->cur_h - renderer->origin_y, &end_x, &end_y); - + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, (float)renderer->origin_x, + (float)renderer->origin_y, &origin_x, &origin_y); + DFSDL::DFSDL_RenderLogicalToWindow( + (SDL_Renderer*)renderer->sdl_renderer, + (float)renderer->cur_w - (float)renderer->origin_x, + (float)(renderer->cur_h - renderer->origin_y), &end_x, &end_y); + + // Get the mouse location in window coordinates int mx, my; DFSDL::DFSDL_GetMouseState(&mx, &my); @@ -94,26 +98,28 @@ static void render_thread_cb([[maybe_unused]] void* _) { queued.ydiff++; } - request_queued.store(false); + callback_queued.store(false); } static bool update_mouse_pos() { - if (request_queued.load()) - return false; // No new inputs, and a request for more is already placed + if (callback_queued.load()) + return false; // Queued callback not complete, check back later + // Queued callback complete, save the results and enqueue again state = queued; - queued = state_default; - DFHack::runOnRenderThread(render_thread_cb, nullptr); - request_queued.store(true); + queued = {0}; + DFHack::runOnRenderThread(render_thread_cb); + callback_queued.store(true); return true; } -// Scrolling behavior +// Apply scroll whilst maintaining boundaries template static void apply_scroll(T* out, T diff, T min, T max) { *out = std::min(std::max(*out + diff, min), max); } +// Scroll main fortress/adventure world views static void scroll_dwarfmode(int xdiff, int ydiff) { using df::global::window_x; using df::global::window_y; @@ -137,7 +143,7 @@ static void scroll_dwarfmode(int xdiff, int ydiff) { } template -static void scroll_world(T* screen, int xdiff, int ydiff) { +static void scroll_world_internal(T* screen, int xdiff, int ydiff) { if constexpr(std::is_same_v) { if (screen->zoomed_in) { int max_x = (world->world_data->world_width * 16)-1; @@ -169,6 +175,9 @@ using world_map = std::variant get_map() { df::viewscreen* screen = Gui::getCurViewscreen(true); screen = Gui::getDFViewscreen(true, screen); // Get the first non-dfhack viewscreen + if(!screen) + return {}; + if (auto start_site = virtual_cast(screen)) return start_site; if (auto world_map = virtual_cast(screen)) @@ -183,9 +192,9 @@ static std::optional get_map() { static void scroll_world(world_map screen, int xdiff, int ydiff) { const auto visitor = overloads { - [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world(s, xdiff, ydiff);}, - [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world(s, xdiff, ydiff);}, - [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_choose_start_sitest* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_worldst* s) {scroll_world_internal(s, xdiff, ydiff);}, + [xdiff, ydiff](df::viewscreen_new_regionst* s) {scroll_world_internal(s, xdiff, ydiff);}, }; std::visit(visitor, screen); } @@ -198,18 +207,18 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) { if (now < last_action + cooldown_ms) return CR_OK; - // Update mouse_x/y to values from the render thread + // Update mouse_x/y from values read in render thread callback if (!update_mouse_pos()) - return CR_OK; + return CR_OK; // No new input to process + + if (state.xdiff == 0 && state.ydiff == 0) + return CR_OK; // No work to do // Ensure either a map viewscreen or the main viewport are visible auto worldmap = get_map(); if (!worldmap.has_value() && (!gps->main_viewport || !gps->main_viewport->flag.bits.active)) return CR_OK; - if (state.xdiff == 0 && state.ydiff == 0) - return CR_OK; // No work to do - // Dispatch scrolling to active scrollables if (worldmap.has_value()) scroll_world(worldmap.value(), state.xdiff, state.ydiff); From cd825e0f0b672ef997ffe1ae460fd4d969a82015 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 4 Dec 2025 15:27:01 -0500 Subject: [PATCH 5/5] Default initialize scroll_state properly --- plugins/edgescroll.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/edgescroll.cpp b/plugins/edgescroll.cpp index 08a50430f8..cdc0c349d2 100644 --- a/plugins/edgescroll.cpp +++ b/plugins/edgescroll.cpp @@ -56,15 +56,15 @@ DFhackCExport command_result plugin_shutdown([[maybe_unused]] color_ostream &out static std::atomic_bool callback_queued = false; struct scroll_state { - int8_t xdiff; - int8_t ydiff; + int8_t xdiff = 0; + int8_t ydiff = 0; }; static scroll_state state; static scroll_state queued; static void render_thread_cb() { - queued = {0}; + queued = {}; // Ignore the mouse if outside the window if (!enabler->mouse_focus) { callback_queued.store(false); @@ -107,7 +107,7 @@ static bool update_mouse_pos() { // Queued callback complete, save the results and enqueue again state = queued; - queued = {0}; + queued = {}; DFHack::runOnRenderThread(render_thread_cb); callback_queued.store(true); return true;