From fa453a003246eae06728d4a01dc1662d6d83552e Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Sun, 14 Jun 2026 18:24:13 +0200 Subject: [PATCH 01/24] Radius overlay & tooltips --- libs/s25main/controls/ctrlBuildingIcon.cpp | 11 +++++++ libs/s25main/controls/ctrlBuildingIcon.h | 8 +++++ libs/s25main/gameData/BuildingConsts.cpp | 32 +++++++++++++++++++ libs/s25main/gameData/BuildingConsts.h | 10 ++++++ libs/s25main/ingameWindows/iwAction.cpp | 27 +++++++++++++++- libs/s25main/ingameWindows/iwAction.h | 6 ++++ libs/s25main/world/GameWorldView.cpp | 36 ++++++++++++++++++++++ libs/s25main/world/GameWorldView.h | 10 ++++++ 8 files changed, 139 insertions(+), 1 deletion(-) diff --git a/libs/s25main/controls/ctrlBuildingIcon.cpp b/libs/s25main/controls/ctrlBuildingIcon.cpp index e43493f752..51ba42099c 100644 --- a/libs/s25main/controls/ctrlBuildingIcon.cpp +++ b/libs/s25main/controls/ctrlBuildingIcon.cpp @@ -4,6 +4,7 @@ #include "ctrlBuildingIcon.h" #include "Loader.h" +#include "driver/MouseCoords.h" #include "files.h" #include "ogl/glArchivItem_Bitmap.h" #include "gameTypes/BuildingType.h" @@ -28,3 +29,13 @@ void ctrlBuildingIcon::Draw_() } void ctrlBuildingIcon::DrawContent() const {} + +bool ctrlBuildingIcon::Msg_MouseMove(const MouseCoords& mc) +{ + const bool wasHovered = (state == ButtonState::Hover); + const bool result = ctrlButton::Msg_MouseMove(mc); + const bool nowHovered = (state == ButtonState::Hover); + if(wasHovered != nowHovered && onHoverChanged_) + onHoverChanged_(nowHovered); + return result; +} diff --git a/libs/s25main/controls/ctrlBuildingIcon.h b/libs/s25main/controls/ctrlBuildingIcon.h index 393d597c18..2c4d07b85d 100644 --- a/libs/s25main/controls/ctrlBuildingIcon.h +++ b/libs/s25main/controls/ctrlBuildingIcon.h @@ -5,6 +5,7 @@ #pragma once #include "ctrlButton.h" +#include class Window; class ctrlBuildingIcon : public ctrlButton @@ -15,11 +16,18 @@ class ctrlBuildingIcon : public ctrlButton /// liefert den GebäudeTyp des Icons. BuildingType GetType() const { return type; } + /// Set callback for hover-state changes (called with true when hovered, false when not) + void SetOnHoverChanged(std::function cb) { onHoverChanged_ = std::move(cb); } + protected: /// zeichnet das Fenster. void Draw_() override; void DrawContent() const override; + bool Msg_MouseMove(const MouseCoords& mc) override; const BuildingType type; /// der GebäudeType des Icons. const Nation nation; /// Volk + +private: + std::function onHoverChanged_; }; diff --git a/libs/s25main/gameData/BuildingConsts.cpp b/libs/s25main/gameData/BuildingConsts.cpp index 105ed23d7c..38180fa3fe 100644 --- a/libs/s25main/gameData/BuildingConsts.cpp +++ b/libs/s25main/gameData/BuildingConsts.cpp @@ -4,6 +4,10 @@ #include "BuildingConsts.h" #include "mygettext/mygettext.h" +#include "gameData/MilitaryConsts.h" +#include "addons/AddonWoodcutterReachRadius.h" +#include "addons/AddonForesterReachRadius.h" +#include "addons/AddonStonemasonReachRadius.h" #include const helpers::EnumArray BUILDING_NAMES = { @@ -409,3 +413,31 @@ const helpers::MultiEnumArray BUILDING_ARMOR_SI babylonians[BuildingType::Fortress] = DrawPoint(20, -34); return result; }(); + +unsigned GetBuildingRadius(BuildingType bld) +{ + switch(bld) + { + // Military buildings (territory influence radius) + case BuildingType::Barracks: return MILITARY_RADIUS[0]; + case BuildingType::Guardhouse: return MILITARY_RADIUS[1]; + case BuildingType::Watchtower: return MILITARY_RADIUS[2]; + case BuildingType::Fortress: return MILITARY_RADIUS[3]; + // Headquarters + case BuildingType::Headquarters: return HQ_RADIUS; + // Harbor building + case BuildingType::HarborBuilding: return HARBOR_RADIUS; + // Production buildings with reach radius + case BuildingType::Woodcutter: return woodcutterRadiusValues[0]; // Default radius + case BuildingType::Forester: return foresterRadiusValues[0]; // Default radius + case BuildingType::Fishery: return FISHERY_RADIUS; + case BuildingType::Quarry: return stonemasonRadiusValues[0]; // Default radius + case BuildingType::Catapult: return CATAPULT_RANGE; + case BuildingType::LookoutTower: return VISUALRANGE_LOOKOUTTOWER; // Scouting range + case BuildingType::Hunter: return HUNTER_RADIUS; + case BuildingType::Farm: return FARMER_RADIUS; + case BuildingType::Charburner: return CHARBURNER_RADIUS; + // Buildings that don't have a notable radius + default: return 0; + } +} diff --git a/libs/s25main/gameData/BuildingConsts.h b/libs/s25main/gameData/BuildingConsts.h index 8b07e0cdcb..efe3e02b8a 100644 --- a/libs/s25main/gameData/BuildingConsts.h +++ b/libs/s25main/gameData/BuildingConsts.h @@ -119,3 +119,13 @@ constexpr std::array SUPPRESS_UNUSED NUBIAN_MINE_FIRE = {{ /// Hilfetexte für Gebäude extern const helpers::EnumArray BUILDING_HELP_STRINGS; + +/// Reach radius for various production buildings (in tiles) +constexpr unsigned FISHERY_RADIUS = 7; ///< Fisher searches for fish within this radius +constexpr unsigned HUNTER_RADIUS = 2; ///< Hunter hunts animals within this radius +constexpr unsigned FARMER_RADIUS = 2; ///< Farmer plants and harvests crops within this radius +constexpr unsigned CHARBURNER_RADIUS = 3; ///< Charburner places and harvests charcoal piles within this radius +constexpr unsigned CATAPULT_RANGE = 12; ///< Catapult attack range + +/// Get the radius in tiles for a building type (influence radius for military, working radius for workers, attack range for catapult) +unsigned GetBuildingRadius(BuildingType bld); diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index ec810eac1f..9245d83cc5 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -175,6 +175,11 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP std::stringstream tooltip; tooltip << _(BUILDING_NAMES[bld]); + // Radius anzeigen falls vorhanden + const unsigned radius = GetBuildingRadius(bld); + if(radius > 0) + tooltip << _("\nRange: ") << radius << _(" tiles"); + tooltip << _("\nCosts: "); if(BUILDING_COSTS[bld].boards > 0) tooltip << (int)BUILDING_COSTS[bld].boards << _(" boards"); @@ -186,9 +191,27 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP } DrawPoint iconPos((k % 5) * 36, (k / 5) * 36 + 45); - build_tab->GetGroup(static_cast(bt)) + ctrlBuildingIcon* icon = build_tab->GetGroup(static_cast(bt)) ->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str()); + // Set hover callback to show radius preview on the game world + const unsigned bldRadius = GetBuildingRadius(bld); + if(bldRadius > 0) + { + icon->SetOnHoverChanged([this, icon, bldRadius](bool hovered) noexcept { + if(hovered) + { + hoveredBldIcon_ = icon; + this->gwv.SetRadiusPreview(std::make_pair(this->selectedPt, bldRadius)); + } else if(hoveredBldIcon_ == icon) + { + // Only clear if no other icon took over hover + hoveredBldIcon_ = nullptr; + this->gwv.SetRadiusPreview(boost::none); + } + }); + } + ++k; } @@ -407,6 +430,8 @@ void iwAction::Close() { if(ShouldBeClosed()) return; + // Clear radius preview on the game world + gwv.SetRadiusPreview(boost::none); IngameWindow::Close(); if(mousePosAtOpen_.isValid()) VIDEODRIVER.SetMousePos(mousePosAtOpen_); diff --git a/libs/s25main/ingameWindows/iwAction.h b/libs/s25main/ingameWindows/iwAction.h index 3007f4df09..dd74998691 100644 --- a/libs/s25main/ingameWindows/iwAction.h +++ b/libs/s25main/ingameWindows/iwAction.h @@ -7,6 +7,9 @@ #include "IngameWindow.h" #include "variant.h" #include "gameTypes/MapCoordinates.h" +#include + +class ctrlBuildingIcon; #include class GameInterface; @@ -69,6 +72,9 @@ class iwAction : public IngameWindow /// Die einzelnen Höhen für die einzelnen Tabs im Bautab std::array building_tab_heights; + /// The building icon currently hovered in the build tabs (used to show radius preview) + const ctrlBuildingIcon* hoveredBldIcon_ = nullptr; + public: iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapPoint selectedPt, const DrawPoint& mousePos, Params params, bool military_buildings); diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index b2f59093b7..de553b98a4 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -6,6 +6,8 @@ #include "CatapultStone.h" #include "Cheats.h" #include "FOWObjects.h" +#include "ReturnMapPointWithRadius.h" +#include "Window.h" #include "GameInterface.h" #include "GamePlayer.h" #include "GlobalGameSettings.h" @@ -220,6 +222,23 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(show_names || show_productivity) DrawNameProductivityOverlay(terrainRenderer); + // Draw radius preview outline (if set via action window hover) + if(radiusPreview_) + DrawRadiusOutline(radiusPreview_->first, radiusPreview_->second); + + // Draw radius outline for the building under the mouse cursor + if(!radiusPreview_ && mousePos.x >= 0 && mousePos.x < static_cast(size_.x) && mousePos.y >= 0 + && mousePos.y < static_cast(size_.y)) + { + const auto* bldUnderMouse = GetWorld().GetSpecObj(selPt); + if(bldUnderMouse) + { + const unsigned bldRadius = GetBuildingRadius(bldUnderMouse->GetBuildingType()); + if(bldRadius > 0) + DrawRadiusOutline(bldUnderMouse->GetPos(), bldRadius); + } + } + DrawGUI(rb, terrainRenderer, selected, drawMouse); // Draw catapult stones @@ -713,6 +732,23 @@ void GameWorldView::RemoveDrawNodeCallback(IDrawNodeCallback* callbackToRemove) drawNodeCallbacks.erase(itPos); } +void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) +{ + const auto& world = GetWorld(); + // Get all border points at the exact radius + auto pts = world.GetPointsInRadius(center, radius, ReturnMapPointWithRadius{}); + for(const auto& ptWithRadius : pts) + { + if(ptWithRadius.second != radius) + continue; + // Convert map point to screen position + const DrawPoint screenPos = Position(world.GetNodePos(ptWithRadius.first)) - offset; + // Draw a small colored rectangle (border indicator) + constexpr unsigned BORDER_COLOR = 0xFFFF0000; // Red with full alpha + Window::DrawRectangle(Rect(screenPos - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + } +} + void GameWorldView::CalcFxLx() { // Calc first and last point in map units (with 1 extra for incomplete triangles) diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index c405ec803e..762196f278 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -8,6 +8,7 @@ #include "DrawPoint.h" #include "gameTypes/MapCoordinates.h" #include "gameTypes/MapTypes.h" +#include #include #include @@ -46,6 +47,9 @@ class GameWorldView /// Show productivities bool show_productivity; + /// Optional radius preview (center position, radius) drawn as outline on the map + boost::optional> radiusPreview_; + /// Offset from world origin in screen units (not map units): "scroll position" DrawPoint offset; /// Last scroll position (before jump) @@ -117,6 +121,9 @@ class GameWorldView void AddDrawNodeCallback(IDrawNodeCallback* newCallback); void RemoveDrawNodeCallback(IDrawNodeCallback* callbackToRemove); + /// Set/show a radius preview outline on the map (or clear with boost::none) + void SetRadiusPreview(const boost::optional>& preview) { radiusPreview_ = preview; } + /// Gibt selektierten Punkt zurück MapPoint GetSelectedPt() const { return selPt; } @@ -146,6 +153,9 @@ class GameWorldView void DrawGUI(const RoadBuildState& rb, const TerrainRenderer& terrainRenderer, const MapPoint& selectedPt, bool drawMouse); + /// Draw a radius outline ring around a center point with the given radius + void DrawRadiusOutline(const MapPoint& center, unsigned radius); + void SaveIngameSettingsValues() const; void updateEffectiveZoomFactor(); }; From 3175b05b33246460cef08a9e249b2a3e782d12cb Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Sun, 14 Jun 2026 19:52:02 +0200 Subject: [PATCH 02/24] Fix clipping, mine radius --- libs/s25main/gameData/BuildingConsts.cpp | 33 +++++++++++++++---- libs/s25main/gameData/BuildingConsts.h | 7 +++-- libs/s25main/ingameWindows/iwAction.cpp | 12 +++++-- libs/s25main/world/GameWorldView.cpp | 40 ++++++++++++++++++++---- 4 files changed, 76 insertions(+), 16 deletions(-) diff --git a/libs/s25main/gameData/BuildingConsts.cpp b/libs/s25main/gameData/BuildingConsts.cpp index 38180fa3fe..73cb75e7c2 100644 --- a/libs/s25main/gameData/BuildingConsts.cpp +++ b/libs/s25main/gameData/BuildingConsts.cpp @@ -5,9 +5,12 @@ #include "BuildingConsts.h" #include "mygettext/mygettext.h" #include "gameData/MilitaryConsts.h" +#include "gameData/GameConsts.h" +#include "GlobalGameSettings.h" #include "addons/AddonWoodcutterReachRadius.h" #include "addons/AddonForesterReachRadius.h" #include "addons/AddonStonemasonReachRadius.h" +#include "addons/const_addons.h" #include const helpers::EnumArray BUILDING_NAMES = { @@ -414,7 +417,7 @@ const helpers::MultiEnumArray BUILDING_ARMOR_SI return result; }(); -unsigned GetBuildingRadius(BuildingType bld) +unsigned GetBuildingRadius(BuildingType bld, const GlobalGameSettings& ggs) { switch(bld) { @@ -427,16 +430,34 @@ unsigned GetBuildingRadius(BuildingType bld) case BuildingType::Headquarters: return HQ_RADIUS; // Harbor building case BuildingType::HarborBuilding: return HARBOR_RADIUS; - // Production buildings with reach radius - case BuildingType::Woodcutter: return woodcutterRadiusValues[0]; // Default radius - case BuildingType::Forester: return foresterRadiusValues[0]; // Default radius + // Production buildings with (adjustable) reach radius + case BuildingType::Woodcutter: + { + const unsigned sel = ggs.getSelection(AddonId::WOODCUTTER_REACH_RADIUS); + return woodcutterRadiusValues[sel]; + } + case BuildingType::Forester: + { + const unsigned sel = ggs.getSelection(AddonId::FORESTER_REACH_RADIUS); + return foresterRadiusValues[sel]; + } + case BuildingType::Quarry: + { + const unsigned sel = ggs.getSelection(AddonId::STONEMASON_REACH_RADIUS); + return stonemasonRadiusValues[sel]; + } case BuildingType::Fishery: return FISHERY_RADIUS; - case BuildingType::Quarry: return stonemasonRadiusValues[0]; // Default radius case BuildingType::Catapult: return CATAPULT_RANGE; case BuildingType::LookoutTower: return VISUALRANGE_LOOKOUTTOWER; // Scouting range - case BuildingType::Hunter: return HUNTER_RADIUS; + // Farm, Charburner, and Hunter have fixed working/search radii case BuildingType::Farm: return FARMER_RADIUS; case BuildingType::Charburner: return CHARBURNER_RADIUS; + case BuildingType::Hunter: return HUNTER_SEARCH_RADIUS; // Search radius (square half-side 19) + // Mines extract resources from within a 2-tile radius (miner stays inside building) + case BuildingType::GraniteMine: + case BuildingType::CoalMine: + case BuildingType::IronMine: + case BuildingType::GoldMine: return MINER_RADIUS; // Buildings that don't have a notable radius default: return 0; } diff --git a/libs/s25main/gameData/BuildingConsts.h b/libs/s25main/gameData/BuildingConsts.h index efe3e02b8a..b96b0c4af0 100644 --- a/libs/s25main/gameData/BuildingConsts.h +++ b/libs/s25main/gameData/BuildingConsts.h @@ -122,10 +122,13 @@ extern const helpers::EnumArray BUILDING_HELP_STRINGS /// Reach radius for various production buildings (in tiles) constexpr unsigned FISHERY_RADIUS = 7; ///< Fisher searches for fish within this radius -constexpr unsigned HUNTER_RADIUS = 2; ///< Hunter hunts animals within this radius +constexpr unsigned HUNTER_SEARCH_RADIUS = 19; ///< Hunter searches for animals in a square of this half-side length constexpr unsigned FARMER_RADIUS = 2; ///< Farmer plants and harvests crops within this radius constexpr unsigned CHARBURNER_RADIUS = 3; ///< Charburner places and harvests charcoal piles within this radius constexpr unsigned CATAPULT_RANGE = 12; ///< Catapult attack range +class GlobalGameSettings; + /// Get the radius in tiles for a building type (influence radius for military, working radius for workers, attack range for catapult) -unsigned GetBuildingRadius(BuildingType bld); +/// Uses the actual game settings to get the current addon selection for adjustable ranges (woodcutter, forester, stonemason). +unsigned GetBuildingRadius(BuildingType bld, const GlobalGameSettings& ggs); diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index 9245d83cc5..9faef9ae47 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -176,7 +176,7 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP tooltip << _(BUILDING_NAMES[bld]); // Radius anzeigen falls vorhanden - const unsigned radius = GetBuildingRadius(bld); + const unsigned radius = GetBuildingRadius(bld, gwv.GetWorld().GetGGS()); if(radius > 0) tooltip << _("\nRange: ") << radius << _(" tiles"); @@ -195,7 +195,7 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP ->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str()); // Set hover callback to show radius preview on the game world - const unsigned bldRadius = GetBuildingRadius(bld); + const unsigned bldRadius = GetBuildingRadius(bld, gwv.GetWorld().GetGGS()); if(bldRadius > 0) { icon->SetOnHoverChanged([this, icon, bldRadius](bool hovered) noexcept { @@ -494,6 +494,10 @@ void iwAction::Msg_Group_ButtonClick(const unsigned /*group_id*/, const unsigned void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id) { + // Clear radius preview when switching tabs — old icons won't trigger mouse-leave + hoveredBldIcon_ = nullptr; + gwv.SetRadiusPreview(boost::none); + switch(ctrl_id) { case 0: // Haupttabs @@ -538,6 +542,10 @@ void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id void iwAction::Msg_Group_TabChange(const unsigned /*group_id*/, const unsigned ctrl_id, const unsigned short tab_id) { + // Clear radius preview when switching build subtabs, same reason as above + hoveredBldIcon_ = nullptr; + gwv.SetRadiusPreview(boost::none); + switch(ctrl_id) { case 1: // Gebäudetabs diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index de553b98a4..ef9f29cce9 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -233,7 +233,7 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool const auto* bldUnderMouse = GetWorld().GetSpecObj(selPt); if(bldUnderMouse) { - const unsigned bldRadius = GetBuildingRadius(bldUnderMouse->GetBuildingType()); + const unsigned bldRadius = GetBuildingRadius(bldUnderMouse->GetBuildingType(), GetWorld().GetGGS()); if(bldRadius > 0) DrawRadiusOutline(bldUnderMouse->GetPos(), bldRadius); } @@ -737,15 +737,43 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) const auto& world = GetWorld(); // Get all border points at the exact radius auto pts = world.GetPointsInRadius(center, radius, ReturnMapPointWithRadius{}); + + const MapExtent mapSize = world.GetSize(); + constexpr unsigned BORDER_COLOR = 0xFFFF0000; // Red with full alpha + + const int mapPixelW = mapSize.x * TR_W; + const int mapPixelH = mapSize.y * TR_H; + for(const auto& ptWithRadius : pts) { if(ptWithRadius.second != radius) continue; - // Convert map point to screen position - const DrawPoint screenPos = Position(world.GetNodePos(ptWithRadius.first)) - offset; - // Draw a small colored rectangle (border indicator) - constexpr unsigned BORDER_COLOR = 0xFFFF0000; // Red with full alpha - Window::DrawRectangle(Rect(screenPos - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + + const MapPoint pt = ptWithRadius.first; + const DrawPoint base = Position(world.GetNodePos(pt)) - offset; + + // Canonical copy (always drawn) + Window::DrawRectangle(Rect(base - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + + // Toroidal wraparound copies — draw shifted by ±1 map dimension so the + // ring is continuous across the seam. We unconditionally draw all four + // potential copies; the renderer clips anything off-screen. + // +x (wrap rightwards) + Window::DrawRectangle(Rect(base + DrawPoint(mapPixelW, 0) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + // -x (wrap leftwards) + Window::DrawRectangle(Rect(base + DrawPoint(-mapPixelW, 0) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + // +y (wrap downwards) + Window::DrawRectangle(Rect(base + DrawPoint(0, mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + // -y (wrap upwards) + Window::DrawRectangle(Rect(base + DrawPoint(0, -mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + // +x+y (diagonal) + Window::DrawRectangle(Rect(base + DrawPoint(mapPixelW, mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + // +x-y + Window::DrawRectangle(Rect(base + DrawPoint(mapPixelW, -mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + // -x+y + Window::DrawRectangle(Rect(base + DrawPoint(-mapPixelW, mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + // -x-y + Window::DrawRectangle(Rect(base + DrawPoint(-mapPixelW, -mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); } } From d489b4cc6fd52c5c24d9c90cb874269fecd94b63 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Sun, 14 Jun 2026 19:58:41 +0200 Subject: [PATCH 03/24] Cleaner DrawRadiusOutline --- libs/s25main/world/GameWorldView.cpp | 47 ++++++++++++---------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index ef9f29cce9..d477a04bef 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -741,39 +741,32 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) const MapExtent mapSize = world.GetSize(); constexpr unsigned BORDER_COLOR = 0xFFFF0000; // Red with full alpha - const int mapPixelW = mapSize.x * TR_W; - const int mapPixelH = mapSize.y * TR_H; + // Height-adjusted screen position for a map point, accounting for viewport offset. + auto getScreenPos = [&](Position p) -> DrawPoint { + return Position(world.GetNodePos(MapPoint(MakeMapPoint(p, mapSize)))) - offset + + (p - Position(MakeMapPoint(p, mapSize))) * Extent(TR_W, TR_H); + }; for(const auto& ptWithRadius : pts) { if(ptWithRadius.second != radius) continue; - const MapPoint pt = ptWithRadius.first; - const DrawPoint base = Position(world.GetNodePos(pt)) - offset; - - // Canonical copy (always drawn) - Window::DrawRectangle(Rect(base - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - - // Toroidal wraparound copies — draw shifted by ±1 map dimension so the - // ring is continuous across the seam. We unconditionally draw all four - // potential copies; the renderer clips anything off-screen. - // +x (wrap rightwards) - Window::DrawRectangle(Rect(base + DrawPoint(mapPixelW, 0) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - // -x (wrap leftwards) - Window::DrawRectangle(Rect(base + DrawPoint(-mapPixelW, 0) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - // +y (wrap downwards) - Window::DrawRectangle(Rect(base + DrawPoint(0, mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - // -y (wrap upwards) - Window::DrawRectangle(Rect(base + DrawPoint(0, -mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - // +x+y (diagonal) - Window::DrawRectangle(Rect(base + DrawPoint(mapPixelW, mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - // +x-y - Window::DrawRectangle(Rect(base + DrawPoint(mapPixelW, -mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - // -x+y - Window::DrawRectangle(Rect(base + DrawPoint(-mapPixelW, mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - // -x-y - Window::DrawRectangle(Rect(base + DrawPoint(-mapPixelW, -mapPixelH) - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + const Position pt(ptWithRadius.first); + const int w = mapSize.x; + const int h = mapSize.y; + + // Draw at all 9 toroidal copies (canonical ± 1 map dimension). + // getScreenPos uses MakeMapPoint + offset correction which handles + // the half-tile parity shift correctly. + for(int dw : {-w, 0, w}) + { + for(int dh : {-h, 0, h}) + { + const DrawPoint scr = getScreenPos(pt + Position(dw, dh)); + Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + } + } } } From 57286367f2ea62e3be703651e276e7e8d700506f Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Sun, 14 Jun 2026 20:42:12 +0200 Subject: [PATCH 04/24] FoW and cleaner DrawRadiusOutline --- libs/s25main/FOWObjects.h | 1 + libs/s25main/world/GameWorldView.cpp | 39 ++++++++++++++++++---------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/libs/s25main/FOWObjects.h b/libs/s25main/FOWObjects.h index e50a9b8d8d..a206f94cce 100644 --- a/libs/s25main/FOWObjects.h +++ b/libs/s25main/FOWObjects.h @@ -32,6 +32,7 @@ class fowBuilding : public FOWObject void Serialize(SerializedGameData& sgd) const override; void Draw(DrawPoint drawPt) const override; FoW_Type GetType() const override { return FoW_Type::Building; } + BuildingType GetBuildingType() const { return type; } }; /// Baustelle diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index d477a04bef..ef589dc811 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "world/GameWorldView.h" +#include #include "CatapultStone.h" #include "Cheats.h" #include "FOWObjects.h" @@ -230,12 +231,25 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(!radiusPreview_ && mousePos.x >= 0 && mousePos.x < static_cast(size_.x) && mousePos.y >= 0 && mousePos.y < static_cast(size_.y)) { - const auto* bldUnderMouse = GetWorld().GetSpecObj(selPt); - if(bldUnderMouse) + boost::optional bldType; + const Visibility vis = gwv.GetVisibility(selPt); + if(vis == Visibility::Visible) { - const unsigned bldRadius = GetBuildingRadius(bldUnderMouse->GetBuildingType(), GetWorld().GetGGS()); + const auto* bld = GetWorld().GetSpecObj(selPt); + if(bld) + bldType = bld->GetBuildingType(); + } else if(vis == Visibility::FogOfWar) + { + const FOWObject* fow = gwv.GetYoungestFOWObject(selPt); + if(fow && fow->GetType() == FoW_Type::Building) + bldType = static_cast(*fow).GetBuildingType(); + } + + if(bldType) + { + const unsigned bldRadius = GetBuildingRadius(*bldType, GetWorld().GetGGS()); if(bldRadius > 0) - DrawRadiusOutline(bldUnderMouse->GetPos(), bldRadius); + DrawRadiusOutline(selPt, bldRadius); } } @@ -741,11 +755,8 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) const MapExtent mapSize = world.GetSize(); constexpr unsigned BORDER_COLOR = 0xFFFF0000; // Red with full alpha - // Height-adjusted screen position for a map point, accounting for viewport offset. - auto getScreenPos = [&](Position p) -> DrawPoint { - return Position(world.GetNodePos(MapPoint(MakeMapPoint(p, mapSize)))) - offset - + (p - Position(MakeMapPoint(p, mapSize))) * Extent(TR_W, TR_H); - }; + const int w = mapSize.x; + const int h = mapSize.y; for(const auto& ptWithRadius : pts) { @@ -753,17 +764,17 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) continue; const Position pt(ptWithRadius.first); - const int w = mapSize.x; - const int h = mapSize.y; // Draw at all 9 toroidal copies (canonical ± 1 map dimension). - // getScreenPos uses MakeMapPoint + offset correction which handles - // the half-tile parity shift correctly. + // Using all copies guarantees the ring is continuous across the seam + // regardless of viewport position — the renderer clips off-screen pixels. for(int dw : {-w, 0, w}) { for(int dh : {-h, 0, h}) { - const DrawPoint scr = getScreenPos(pt + Position(dw, dh)); + const Position copyPos = pt + Position(dw, dh); + const auto alt = world.GetNode(MakeMapPoint(copyPos, mapSize)).altitude; + const DrawPoint scr = Position(GetNodePos(copyPos) - Position(0, HEIGHT_FACTOR * alt)) - offset; Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); } } From d6c53b81c879fa6abc86d90c07d96f5fe1865096 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Mon, 15 Jun 2026 10:50:41 +0200 Subject: [PATCH 05/24] Tidy --- libs/s25main/ingameWindows/iwAction.cpp | 11 ++++++----- libs/s25main/world/GameWorldView.cpp | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index 9faef9ae47..887d23530d 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -29,6 +29,7 @@ #include "gameData/BuildingConsts.h" #include "gameData/const_gui_ids.h" #include +#include // Tab - Flags enum TabID @@ -178,7 +179,7 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP // Radius anzeigen falls vorhanden const unsigned radius = GetBuildingRadius(bld, gwv.GetWorld().GetGGS()); if(radius > 0) - tooltip << _("\nRange: ") << radius << _(" tiles"); + tooltip << boost::format(_("\nRange: %1% tiles")) % radius; tooltip << _("\nCosts: "); if(BUILDING_COSTS[bld].boards > 0) @@ -195,14 +196,13 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP ->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str()); // Set hover callback to show radius preview on the game world - const unsigned bldRadius = GetBuildingRadius(bld, gwv.GetWorld().GetGGS()); - if(bldRadius > 0) + if(radius > 0) { - icon->SetOnHoverChanged([this, icon, bldRadius](bool hovered) noexcept { + icon->SetOnHoverChanged([this, icon, radius](bool hovered) noexcept { if(hovered) { hoveredBldIcon_ = icon; - this->gwv.SetRadiusPreview(std::make_pair(this->selectedPt, bldRadius)); + this->gwv.SetRadiusPreview(std::make_pair(this->selectedPt, radius)); } else if(hoveredBldIcon_ == icon) { // Only clear if no other icon took over hover @@ -431,6 +431,7 @@ void iwAction::Close() if(ShouldBeClosed()) return; // Clear radius preview on the game world + hoveredBldIcon_ = nullptr; gwv.SetRadiusPreview(boost::none); IngameWindow::Close(); if(mousePosAtOpen_.isValid()) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index ef589dc811..82f86974fc 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -763,7 +763,7 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) if(ptWithRadius.second != radius) continue; - const Position pt(ptWithRadius.first); + const MapPoint& basePt = ptWithRadius.first; // Draw at all 9 toroidal copies (canonical ± 1 map dimension). // Using all copies guarantees the ring is continuous across the seam @@ -772,9 +772,9 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) { for(int dh : {-h, 0, h}) { - const Position copyPos = pt + Position(dw, dh); - const auto alt = world.GetNode(MakeMapPoint(copyPos, mapSize)).altitude; - const DrawPoint scr = Position(GetNodePos(copyPos) - Position(0, HEIGHT_FACTOR * alt)) - offset; + const MapPoint copyPt = MakeMapPoint(Position(basePt.x + dw, basePt.y + dh), mapSize); + const auto alt = world.GetNode(copyPt).altitude; + const DrawPoint scr = GetNodePos(copyPt) - DrawPoint(0, HEIGHT_FACTOR * alt) - offset; Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); } } From 11df35af3329cd21b8fa0311dcda08ff9cb2814f Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Mon, 15 Jun 2026 16:25:55 +0200 Subject: [PATCH 06/24] Improve radius constants --- libs/s25main/figures/nofCatapultMan.cpp | 2 +- libs/s25main/figures/nofCatapultMan.h | 3 ++ libs/s25main/figures/nofHunter.cpp | 6 +-- libs/s25main/figures/nofHunter.h | 3 ++ libs/s25main/gameData/BuildingConsts.cpp | 56 ++++++++++-------------- libs/s25main/gameData/BuildingConsts.h | 14 +----- libs/s25main/ingameWindows/iwAction.cpp | 2 +- libs/s25main/world/GameWorldView.cpp | 2 +- 8 files changed, 36 insertions(+), 52 deletions(-) diff --git a/libs/s25main/figures/nofCatapultMan.cpp b/libs/s25main/figures/nofCatapultMan.cpp index 80b11c5a5e..065d3fd536 100644 --- a/libs/s25main/figures/nofCatapultMan.cpp +++ b/libs/s25main/figures/nofCatapultMan.cpp @@ -121,7 +121,7 @@ void nofCatapultMan::HandleDerivedEvent(const unsigned /*id*/) unsigned distance = world->CalcDistance(pos, building->GetPos()); // Entfernung nicht zu hoch? - if(distance < 14) + if(distance <= CATAPULT_MAX_TARGET_RANGE) { // Mit in die Liste aufnehmen possibleTargets.push_back(PossibleTarget(building->GetPos(), distance)); diff --git a/libs/s25main/figures/nofCatapultMan.h b/libs/s25main/figures/nofCatapultMan.h index 737cf833d4..4514f5e03b 100644 --- a/libs/s25main/figures/nofCatapultMan.h +++ b/libs/s25main/figures/nofCatapultMan.h @@ -8,6 +8,9 @@ class SerializedGameData; class nobUsual; +/// Maximum distance to a target the catapult can attack (distance < 14 -> max 13) +constexpr unsigned CATAPULT_MAX_TARGET_RANGE = 13; + /// Arbeiter im Katapult class nofCatapultMan : public nofBuildingWorker { diff --git a/libs/s25main/figures/nofHunter.cpp b/libs/s25main/figures/nofHunter.cpp index 80a57c8a79..3ef9038afe 100644 --- a/libs/s25main/figures/nofHunter.cpp +++ b/libs/s25main/figures/nofHunter.cpp @@ -132,16 +132,14 @@ void nofHunter::HandleDerivedEvent(unsigned /*id*/) void nofHunter::TryStartHunting() { // Find animals in a square around building (actually should be circle, but animals are moving anyway) - const int SQUARE_SIZE = 19; - // Liste mit den gefundenen Tieren std::vector available_animals; // Durchgehen und nach Tieren suchen Position curPos; - for(curPos.y = pos.y - SQUARE_SIZE; curPos.y <= pos.y + SQUARE_SIZE; ++curPos.y) + for(curPos.y = pos.y - HUNTER_SEARCH_HALFSIDE; curPos.y <= pos.y + HUNTER_SEARCH_HALFSIDE; ++curPos.y) { - for(curPos.x = pos.x - SQUARE_SIZE; curPos.x <= pos.x + SQUARE_SIZE; ++curPos.x) + for(curPos.x = pos.x - HUNTER_SEARCH_HALFSIDE; curPos.x <= pos.x + HUNTER_SEARCH_HALFSIDE; ++curPos.x) { MapPoint curMapPos = world->MakeMapPoint(curPos); diff --git a/libs/s25main/figures/nofHunter.h b/libs/s25main/figures/nofHunter.h index 1f780ebd5d..6e137eda6b 100644 --- a/libs/s25main/figures/nofHunter.h +++ b/libs/s25main/figures/nofHunter.h @@ -7,6 +7,9 @@ #include "nofBuildingWorker.h" #include "gameTypes/Direction.h" +/// Half-side length of the square the hunter scans for animals (centered on the building) +constexpr int HUNTER_SEARCH_HALFSIDE = 19; + class noAnimal; class SerializedGameData; class nobUsual; diff --git a/libs/s25main/gameData/BuildingConsts.cpp b/libs/s25main/gameData/BuildingConsts.cpp index 73cb75e7c2..8475b51047 100644 --- a/libs/s25main/gameData/BuildingConsts.cpp +++ b/libs/s25main/gameData/BuildingConsts.cpp @@ -6,11 +6,10 @@ #include "mygettext/mygettext.h" #include "gameData/MilitaryConsts.h" #include "gameData/GameConsts.h" -#include "GlobalGameSettings.h" -#include "addons/AddonWoodcutterReachRadius.h" -#include "addons/AddonForesterReachRadius.h" -#include "addons/AddonStonemasonReachRadius.h" -#include "addons/const_addons.h" +#include "figures/nofFarmhand.h" +#include "figures/nofCatapultMan.h" +#include "figures/nofHunter.h" +#include "gameTypes/BuildingTypes.h" #include const helpers::EnumArray BUILDING_NAMES = { @@ -417,11 +416,11 @@ const helpers::MultiEnumArray BUILDING_ARMOR_SI return result; }(); -unsigned GetBuildingRadius(BuildingType bld, const GlobalGameSettings& ggs) +unsigned GetBuildingRadius(BuildingType bld) { switch(bld) { - // Military buildings (territory influence radius) + // Military buildings (territory influence radius) — from MilitaryConsts.h case BuildingType::Barracks: return MILITARY_RADIUS[0]; case BuildingType::Guardhouse: return MILITARY_RADIUS[1]; case BuildingType::Watchtower: return MILITARY_RADIUS[2]; @@ -430,35 +429,26 @@ unsigned GetBuildingRadius(BuildingType bld, const GlobalGameSettings& ggs) case BuildingType::Headquarters: return HQ_RADIUS; // Harbor building case BuildingType::HarborBuilding: return HARBOR_RADIUS; - // Production buildings with (adjustable) reach radius - case BuildingType::Woodcutter: - { - const unsigned sel = ggs.getSelection(AddonId::WOODCUTTER_REACH_RADIUS); - return woodcutterRadiusValues[sel]; - } - case BuildingType::Forester: - { - const unsigned sel = ggs.getSelection(AddonId::FORESTER_REACH_RADIUS); - return foresterRadiusValues[sel]; - } - case BuildingType::Quarry: - { - const unsigned sel = ggs.getSelection(AddonId::STONEMASON_REACH_RADIUS); - return stonemasonRadiusValues[sel]; - } - case BuildingType::Fishery: return FISHERY_RADIUS; - case BuildingType::Catapult: return CATAPULT_RANGE; - case BuildingType::LookoutTower: return VISUALRANGE_LOOKOUTTOWER; // Scouting range - // Farm, Charburner, and Hunter have fixed working/search radii - case BuildingType::Farm: return FARMER_RADIUS; - case BuildingType::Charburner: return CHARBURNER_RADIUS; - case BuildingType::Hunter: return HUNTER_SEARCH_RADIUS; // Search radius (square half-side 19) - // Mines extract resources from within a 2-tile radius (miner stays inside building) + // Lookout tower — scouting visibility range + case BuildingType::LookoutTower: return VISUALRANGE_LOOKOUTTOWER; + // Catapult attack range + case BuildingType::Catapult: return CATAPULT_MAX_TARGET_RANGE; + // Hunter searches for animals in a square of this half-side length + case BuildingType::Hunter: return HUNTER_SEARCH_HALFSIDE; + // Mines — miner stays inside and extracts from adjacent tiles case BuildingType::GraniteMine: case BuildingType::CoalMine: case BuildingType::IronMine: case BuildingType::GoldMine: return MINER_RADIUS; - // Buildings that don't have a notable radius - default: return 0; + // For farmhand-based buildings, delegate to nofFarmhand::GetWorkRadius via the BLD_WORK_DESC + // job mapping. This is the single source of truth for worker reach radii, including + // addon-adjustable ranges (woodcutter, forester, stonemason). + default: + { + const auto job = BLD_WORK_DESC[bld].job; + if(job) + return nofFarmhand::GetWorkRadius(*job); + return 0; + } } } diff --git a/libs/s25main/gameData/BuildingConsts.h b/libs/s25main/gameData/BuildingConsts.h index b96b0c4af0..c94a8d7aa6 100644 --- a/libs/s25main/gameData/BuildingConsts.h +++ b/libs/s25main/gameData/BuildingConsts.h @@ -120,15 +120,5 @@ constexpr std::array SUPPRESS_UNUSED NUBIAN_MINE_FIRE = {{ /// Hilfetexte für Gebäude extern const helpers::EnumArray BUILDING_HELP_STRINGS; -/// Reach radius for various production buildings (in tiles) -constexpr unsigned FISHERY_RADIUS = 7; ///< Fisher searches for fish within this radius -constexpr unsigned HUNTER_SEARCH_RADIUS = 19; ///< Hunter searches for animals in a square of this half-side length -constexpr unsigned FARMER_RADIUS = 2; ///< Farmer plants and harvests crops within this radius -constexpr unsigned CHARBURNER_RADIUS = 3; ///< Charburner places and harvests charcoal piles within this radius -constexpr unsigned CATAPULT_RANGE = 12; ///< Catapult attack range - -class GlobalGameSettings; - -/// Get the radius in tiles for a building type (influence radius for military, working radius for workers, attack range for catapult) -/// Uses the actual game settings to get the current addon selection for adjustable ranges (woodcutter, forester, stonemason). -unsigned GetBuildingRadius(BuildingType bld, const GlobalGameSettings& ggs); +/// Get the radius in tiles for a building type (worker reach, territory influence, attack range, etc.) +unsigned GetBuildingRadius(BuildingType bld); diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index 887d23530d..d08ee71059 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -177,7 +177,7 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP tooltip << _(BUILDING_NAMES[bld]); // Radius anzeigen falls vorhanden - const unsigned radius = GetBuildingRadius(bld, gwv.GetWorld().GetGGS()); + const unsigned radius = GetBuildingRadius(bld); if(radius > 0) tooltip << boost::format(_("\nRange: %1% tiles")) % radius; diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 82f86974fc..bd00168296 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -247,7 +247,7 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(bldType) { - const unsigned bldRadius = GetBuildingRadius(*bldType, GetWorld().GetGGS()); + const unsigned bldRadius = GetBuildingRadius(*bldType); if(bldRadius > 0) DrawRadiusOutline(selPt, bldRadius); } From 0572598cf2210142362372620e0dbfa3e6053ef1 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Mon, 15 Jun 2026 16:44:16 +0200 Subject: [PATCH 07/24] Replace boost::optional and boost::none --- libs/s25main/ingameWindows/iwAction.cpp | 8 ++++---- libs/s25main/ingameWindows/iwAction.h | 2 -- libs/s25main/world/GameWorldView.cpp | 4 ++-- libs/s25main/world/GameWorldView.h | 8 ++++---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index d08ee71059..1e9e06abc3 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -207,7 +207,7 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP { // Only clear if no other icon took over hover hoveredBldIcon_ = nullptr; - this->gwv.SetRadiusPreview(boost::none); + this->gwv.SetRadiusPreview(std::nullopt); } }); } @@ -432,7 +432,7 @@ void iwAction::Close() return; // Clear radius preview on the game world hoveredBldIcon_ = nullptr; - gwv.SetRadiusPreview(boost::none); + gwv.SetRadiusPreview(std::nullopt); IngameWindow::Close(); if(mousePosAtOpen_.isValid()) VIDEODRIVER.SetMousePos(mousePosAtOpen_); @@ -497,7 +497,7 @@ void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id { // Clear radius preview when switching tabs — old icons won't trigger mouse-leave hoveredBldIcon_ = nullptr; - gwv.SetRadiusPreview(boost::none); + gwv.SetRadiusPreview(std::nullopt); switch(ctrl_id) { @@ -545,7 +545,7 @@ void iwAction::Msg_Group_TabChange(const unsigned /*group_id*/, const unsigned c { // Clear radius preview when switching build subtabs, same reason as above hoveredBldIcon_ = nullptr; - gwv.SetRadiusPreview(boost::none); + gwv.SetRadiusPreview(std::nullopt); switch(ctrl_id) { diff --git a/libs/s25main/ingameWindows/iwAction.h b/libs/s25main/ingameWindows/iwAction.h index dd74998691..d41b9a3eb6 100644 --- a/libs/s25main/ingameWindows/iwAction.h +++ b/libs/s25main/ingameWindows/iwAction.h @@ -7,8 +7,6 @@ #include "IngameWindow.h" #include "variant.h" #include "gameTypes/MapCoordinates.h" -#include - class ctrlBuildingIcon; #include diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index bd00168296..32a4165a70 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -3,7 +3,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "world/GameWorldView.h" -#include +#include #include "CatapultStone.h" #include "Cheats.h" #include "FOWObjects.h" @@ -231,7 +231,7 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(!radiusPreview_ && mousePos.x >= 0 && mousePos.x < static_cast(size_.x) && mousePos.y >= 0 && mousePos.y < static_cast(size_.y)) { - boost::optional bldType; + std::optional bldType; const Visibility vis = gwv.GetVisibility(selPt); if(vis == Visibility::Visible) { diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index 762196f278..cbd0191725 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -8,7 +8,7 @@ #include "DrawPoint.h" #include "gameTypes/MapCoordinates.h" #include "gameTypes/MapTypes.h" -#include +#include #include #include @@ -48,7 +48,7 @@ class GameWorldView bool show_productivity; /// Optional radius preview (center position, radius) drawn as outline on the map - boost::optional> radiusPreview_; + std::optional> radiusPreview_; /// Offset from world origin in screen units (not map units): "scroll position" DrawPoint offset; @@ -121,8 +121,8 @@ class GameWorldView void AddDrawNodeCallback(IDrawNodeCallback* newCallback); void RemoveDrawNodeCallback(IDrawNodeCallback* callbackToRemove); - /// Set/show a radius preview outline on the map (or clear with boost::none) - void SetRadiusPreview(const boost::optional>& preview) { radiusPreview_ = preview; } + /// Set/show a radius preview outline on the map (or clear with std::nullopt) + void SetRadiusPreview(const std::optional>& preview) { radiusPreview_ = preview; } /// Gibt selektierten Punkt zurück MapPoint GetSelectedPt() const { return selPt; } From cfebdce5a431d73ff8f7c96e4ddf2bca81133b7c Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Mon, 15 Jun 2026 17:01:35 +0200 Subject: [PATCH 08/24] Simpify UI/mouse changes --- libs/s25main/ingameWindows/iwAction.cpp | 20 ++------------------ libs/s25main/ingameWindows/iwAction.h | 4 ---- libs/s25main/world/GameWorldView.cpp | 7 ++----- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index 1e9e06abc3..27984c3ef6 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -198,17 +198,11 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP // Set hover callback to show radius preview on the game world if(radius > 0) { - icon->SetOnHoverChanged([this, icon, radius](bool hovered) noexcept { + icon->SetOnHoverChanged([this, radius](bool hovered) noexcept { if(hovered) - { - hoveredBldIcon_ = icon; this->gwv.SetRadiusPreview(std::make_pair(this->selectedPt, radius)); - } else if(hoveredBldIcon_ == icon) - { - // Only clear if no other icon took over hover - hoveredBldIcon_ = nullptr; + else this->gwv.SetRadiusPreview(std::nullopt); - } }); } @@ -430,8 +424,6 @@ void iwAction::Close() { if(ShouldBeClosed()) return; - // Clear radius preview on the game world - hoveredBldIcon_ = nullptr; gwv.SetRadiusPreview(std::nullopt); IngameWindow::Close(); if(mousePosAtOpen_.isValid()) @@ -495,10 +487,6 @@ void iwAction::Msg_Group_ButtonClick(const unsigned /*group_id*/, const unsigned void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id) { - // Clear radius preview when switching tabs — old icons won't trigger mouse-leave - hoveredBldIcon_ = nullptr; - gwv.SetRadiusPreview(std::nullopt); - switch(ctrl_id) { case 0: // Haupttabs @@ -543,10 +531,6 @@ void iwAction::Msg_TabChange(const unsigned ctrl_id, const unsigned short tab_id void iwAction::Msg_Group_TabChange(const unsigned /*group_id*/, const unsigned ctrl_id, const unsigned short tab_id) { - // Clear radius preview when switching build subtabs, same reason as above - hoveredBldIcon_ = nullptr; - gwv.SetRadiusPreview(std::nullopt); - switch(ctrl_id) { case 1: // Gebäudetabs diff --git a/libs/s25main/ingameWindows/iwAction.h b/libs/s25main/ingameWindows/iwAction.h index d41b9a3eb6..3007f4df09 100644 --- a/libs/s25main/ingameWindows/iwAction.h +++ b/libs/s25main/ingameWindows/iwAction.h @@ -7,7 +7,6 @@ #include "IngameWindow.h" #include "variant.h" #include "gameTypes/MapCoordinates.h" -class ctrlBuildingIcon; #include class GameInterface; @@ -70,9 +69,6 @@ class iwAction : public IngameWindow /// Die einzelnen Höhen für die einzelnen Tabs im Bautab std::array building_tab_heights; - /// The building icon currently hovered in the build tabs (used to show radius preview) - const ctrlBuildingIcon* hoveredBldIcon_ = nullptr; - public: iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapPoint selectedPt, const DrawPoint& mousePos, Params params, bool military_buildings); diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 32a4165a70..b4b8715c16 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -223,13 +223,10 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(show_names || show_productivity) DrawNameProductivityOverlay(terrainRenderer); - // Draw radius preview outline (if set via action window hover) + // Draw radius preview outline — either from build-menu hover or from the building under the cursor if(radiusPreview_) DrawRadiusOutline(radiusPreview_->first, radiusPreview_->second); - - // Draw radius outline for the building under the mouse cursor - if(!radiusPreview_ && mousePos.x >= 0 && mousePos.x < static_cast(size_.x) && mousePos.y >= 0 - && mousePos.y < static_cast(size_.y)) + else { std::optional bldType; const Visibility vis = gwv.GetVisibility(selPt); From 795a1106613b70adfe85bdfd815db35f8fee1525 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Mon, 15 Jun 2026 17:09:07 +0200 Subject: [PATCH 09/24] Fix map wrap clipping again --- libs/s25main/world/GameWorldView.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index b4b8715c16..dd08a6ef3a 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -761,17 +761,17 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) continue; const MapPoint& basePt = ptWithRadius.first; + const auto alt = world.GetNode(basePt).altitude; - // Draw at all 9 toroidal copies (canonical ± 1 map dimension). - // Using all copies guarantees the ring is continuous across the seam - // regardless of viewport position — the renderer clips off-screen pixels. + // Draw at the canonical position and its 8 toroidal copies (shifted by ± map dimensions). + // We use Position arithmetic (no modulo) so each shifted copy produces a different screen + // position. Copies outside the viewport are clipped by the renderer. for(int dw : {-w, 0, w}) { for(int dh : {-h, 0, h}) { - const MapPoint copyPt = MakeMapPoint(Position(basePt.x + dw, basePt.y + dh), mapSize); - const auto alt = world.GetNode(copyPt).altitude; - const DrawPoint scr = GetNodePos(copyPt) - DrawPoint(0, HEIGHT_FACTOR * alt) - offset; + const DrawPoint scr = GetNodePos(Position(basePt.x + dw, basePt.y + dh)) + - DrawPoint(0, HEIGHT_FACTOR * alt) - offset; Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); } } From bdfd9b14a62cacd8a28aa40903f13f583335eb0d Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 16 Jun 2026 11:35:28 +0200 Subject: [PATCH 10/24] Fix "Invalid job id" from GetWorkRadius() --- libs/s25main/gameData/BuildingConsts.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/s25main/gameData/BuildingConsts.cpp b/libs/s25main/gameData/BuildingConsts.cpp index 8475b51047..add9adff5c 100644 --- a/libs/s25main/gameData/BuildingConsts.cpp +++ b/libs/s25main/gameData/BuildingConsts.cpp @@ -440,15 +440,23 @@ unsigned GetBuildingRadius(BuildingType bld) case BuildingType::CoalMine: case BuildingType::IronMine: case BuildingType::GoldMine: return MINER_RADIUS; - // For farmhand-based buildings, delegate to nofFarmhand::GetWorkRadius via the BLD_WORK_DESC - // job mapping. This is the single source of truth for worker reach radii, including - // addon-adjustable ranges (woodcutter, forester, stonemason). - default: + // Farmhand-based buildings — worker goes out to gather resources from the map. + // Map each building type to its job via BLD_WORK_DESC, then query the work + // radius from nofFarmhand::GetWorkRadius. + case BuildingType::Woodcutter: + case BuildingType::Forester: + case BuildingType::Fishery: + case BuildingType::Quarry: + case BuildingType::Farm: + case BuildingType::Vineyard: + case BuildingType::Charburner: { const auto job = BLD_WORK_DESC[bld].job; if(job) return nofFarmhand::GetWorkRadius(*job); return 0; } + // All other building types have no relevant radius overlay + default: return 0; } } From 7957d831f45862379350b5197b55965fa7343771 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 16 Jun 2026 12:30:04 +0200 Subject: [PATCH 11/24] Toggle via addon --- libs/s25main/GlobalGameSettings.cpp | 3 ++- libs/s25main/addons/AddonBuildingRadius.h | 21 +++++++++++++++++++++ libs/s25main/addons/Addons.h | 2 ++ libs/s25main/addons/const_addons.h | 5 ++++- libs/s25main/ingameWindows/iwAction.cpp | 5 ++++- libs/s25main/world/GameWorldView.cpp | 6 ++++-- 6 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 libs/s25main/addons/AddonBuildingRadius.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index f5047f9893..bb1475b858 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -108,7 +108,8 @@ void GlobalGameSettings::registerAllAddons() AddonForesterFarmFieldAvoidance, AddonForesterReachRadius, AddonWoodcutterReachRadius, - AddonStonemasonReachRadius + AddonStonemasonReachRadius, + AddonBuildingRadius >; // clang-format on using namespace boost::mp11; diff --git a/libs/s25main/addons/AddonBuildingRadius.h b/libs/s25main/addons/AddonBuildingRadius.h new file mode 100644 index 0000000000..de9d493872 --- /dev/null +++ b/libs/s25main/addons/AddonBuildingRadius.h @@ -0,0 +1,21 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "AddonBool.h" +#include "mygettext/mygettext.h" + +/** + * Show building radius information in tooltips and as map overlay + */ +class AddonBuildingRadius : public AddonBool +{ +public: + AddonBuildingRadius() + : AddonBool(AddonId::BUILDING_RADIUS, AddonGroup::GamePlay, _("Show building radius"), + _("Shows the working radius of buildings in the build menu tooltip and as an overlay on the map " + "when hovering over a building icon or selecting a building.")) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 96b3203ff7..fec8a57ee6 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -70,3 +70,5 @@ #include "addons/AddonForesterReachRadius.h" #include "addons/AddonStonemasonReachRadius.h" #include "addons/AddonWoodcutterReachRadius.h" + +#include "addons/AddonBuildingRadius.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index f7ddf54f08..414bd96eca 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -28,6 +28,7 @@ // 010 aztimh // 011 DevOpsOfChaos // 012 MichalLabuda +// 013 Morgan // Do not forget to add your Addon to GlobalGameSettings::registerAllAddons @ GlobalGameSettings.cpp! // Never use a number twice! @@ -82,7 +83,9 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000, FORESTER_REACH_RADIUS = 0x01200000, WOODCUTTER_REACH_RADIUS = 0x01200001, - STONEMASON_REACH_RADIUS = 0x01200002) + STONEMASON_REACH_RADIUS = 0x01200002, + + BUILDING_RADIUS = 0x01300000) //-V:AddonId:801 enum class AddonGroup : unsigned diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index 27984c3ef6..be174e9f00 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -160,6 +160,7 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP building_available[BuildingType::LeatherWorks] = false; } + const bool showBuildingRadius = gwv.GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS); constexpr helpers::EnumArray NUM_TABS = {1, 2, 3, 1, 3}; for(unsigned char i = 0; i < NUM_TABS[tabs.build_tabs]; ++i) @@ -177,7 +178,9 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP tooltip << _(BUILDING_NAMES[bld]); // Radius anzeigen falls vorhanden - const unsigned radius = GetBuildingRadius(bld); + unsigned radius = 0; + if(showBuildingRadius) + radius = GetBuildingRadius(bld); if(radius > 0) tooltip << boost::format(_("\nRange: %1% tiles")) % radius; diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index dd08a6ef3a..5bccab0398 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -223,10 +223,12 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(show_names || show_productivity) DrawNameProductivityOverlay(terrainRenderer); - // Draw radius preview outline — either from build-menu hover or from the building under the cursor + // Draw radius preview outline if(radiusPreview_) DrawRadiusOutline(radiusPreview_->first, radiusPreview_->second); - else + + // Auto-detect radius for the building under the cursor + if(!radiusPreview_ && GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS)) { std::optional bldType; const Visibility vis = gwv.GetVisibility(selPt); From 1f701929e3d18e901610961bc65726d8ffd98fc4 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 16 Jun 2026 13:12:07 +0200 Subject: [PATCH 12/24] Ensure hover fires after leave --- libs/s25main/controls/ctrlBuildingIcon.cpp | 11 ------- libs/s25main/controls/ctrlBuildingIcon.h | 8 ----- libs/s25main/ingameWindows/iwAction.cpp | 38 +++++++++++++++------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/libs/s25main/controls/ctrlBuildingIcon.cpp b/libs/s25main/controls/ctrlBuildingIcon.cpp index 51ba42099c..e43493f752 100644 --- a/libs/s25main/controls/ctrlBuildingIcon.cpp +++ b/libs/s25main/controls/ctrlBuildingIcon.cpp @@ -4,7 +4,6 @@ #include "ctrlBuildingIcon.h" #include "Loader.h" -#include "driver/MouseCoords.h" #include "files.h" #include "ogl/glArchivItem_Bitmap.h" #include "gameTypes/BuildingType.h" @@ -29,13 +28,3 @@ void ctrlBuildingIcon::Draw_() } void ctrlBuildingIcon::DrawContent() const {} - -bool ctrlBuildingIcon::Msg_MouseMove(const MouseCoords& mc) -{ - const bool wasHovered = (state == ButtonState::Hover); - const bool result = ctrlButton::Msg_MouseMove(mc); - const bool nowHovered = (state == ButtonState::Hover); - if(wasHovered != nowHovered && onHoverChanged_) - onHoverChanged_(nowHovered); - return result; -} diff --git a/libs/s25main/controls/ctrlBuildingIcon.h b/libs/s25main/controls/ctrlBuildingIcon.h index 2c4d07b85d..393d597c18 100644 --- a/libs/s25main/controls/ctrlBuildingIcon.h +++ b/libs/s25main/controls/ctrlBuildingIcon.h @@ -5,7 +5,6 @@ #pragma once #include "ctrlButton.h" -#include class Window; class ctrlBuildingIcon : public ctrlButton @@ -16,18 +15,11 @@ class ctrlBuildingIcon : public ctrlButton /// liefert den GebäudeTyp des Icons. BuildingType GetType() const { return type; } - /// Set callback for hover-state changes (called with true when hovered, false when not) - void SetOnHoverChanged(std::function cb) { onHoverChanged_ = std::move(cb); } - protected: /// zeichnet das Fenster. void Draw_() override; void DrawContent() const override; - bool Msg_MouseMove(const MouseCoords& mc) override; const BuildingType type; /// der GebäudeType des Icons. const Nation nation; /// Volk - -private: - std::function onHoverChanged_; }; diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index be174e9f00..910be34812 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -195,20 +195,9 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP } DrawPoint iconPos((k % 5) * 36, (k / 5) * 36 + 45); - ctrlBuildingIcon* icon = build_tab->GetGroup(static_cast(bt)) + build_tab->GetGroup(static_cast(bt)) ->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str()); - // Set hover callback to show radius preview on the game world - if(radius > 0) - { - icon->SetOnHoverChanged([this, radius](bool hovered) noexcept { - if(hovered) - this->gwv.SetRadiusPreview(std::make_pair(this->selectedPt, radius)); - else - this->gwv.SetRadiusPreview(std::nullopt); - }); - } - ++k; } @@ -547,6 +536,31 @@ void iwAction::Msg_Group_TabChange(const unsigned /*group_id*/, const unsigned c void iwAction::Msg_PaintAfter() { IngameWindow::Msg_PaintAfter(); + + // Resolve building icon hover preview after all mouse events are processed + auto* mainTab = GetCtrl(0); + auto* buildTabCtrl = (mainTab && mainTab->GetCurrentTab() == TAB_BUILD) + ? mainTab->GetGroup(TAB_BUILD)->GetCtrl(1) + : nullptr; + auto* bldGroup = buildTabCtrl ? buildTabCtrl->GetGroup(buildTabCtrl->GetCurrentTab()) : nullptr; + + bool hasHoveredIcon = false; + if(bldGroup && gwv.GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS)) + { + for(auto* icon : bldGroup->GetCtrls()) + { + const unsigned radius = GetBuildingRadius(icon->GetType()); + if(icon->IsMouseOver() && radius > 0) + { + gwv.SetRadiusPreview(std::make_pair(selectedPt, radius)); + hasHoveredIcon = true; + break; + } + } + } + if(!hasHoveredIcon) + gwv.SetRadiusPreview(std::nullopt); + auto* tab = GetCtrl(0); if(tab) { From 4f4fa6e37f02553bd158c73d6513b73aea95f367 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 16 Jun 2026 13:29:35 +0200 Subject: [PATCH 13/24] Fix formatting --- libs/s25main/gameData/BuildingConsts.cpp | 8 ++++---- libs/s25main/ingameWindows/iwAction.cpp | 7 +++---- libs/s25main/world/GameWorldView.cpp | 10 +++++----- libs/s25main/world/GameWorldView.h | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/libs/s25main/gameData/BuildingConsts.cpp b/libs/s25main/gameData/BuildingConsts.cpp index add9adff5c..a4b7ac3dca 100644 --- a/libs/s25main/gameData/BuildingConsts.cpp +++ b/libs/s25main/gameData/BuildingConsts.cpp @@ -3,13 +3,13 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "BuildingConsts.h" -#include "mygettext/mygettext.h" -#include "gameData/MilitaryConsts.h" -#include "gameData/GameConsts.h" -#include "figures/nofFarmhand.h" #include "figures/nofCatapultMan.h" +#include "figures/nofFarmhand.h" #include "figures/nofHunter.h" +#include "mygettext/mygettext.h" #include "gameTypes/BuildingTypes.h" +#include "gameData/GameConsts.h" +#include "gameData/MilitaryConsts.h" #include const helpers::EnumArray BUILDING_NAMES = { diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index 910be34812..b1a2070d7d 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -28,8 +28,8 @@ #include "nodeObjs/noFlag.h" #include "gameData/BuildingConsts.h" #include "gameData/const_gui_ids.h" -#include #include +#include // Tab - Flags enum TabID @@ -539,9 +539,8 @@ void iwAction::Msg_PaintAfter() // Resolve building icon hover preview after all mouse events are processed auto* mainTab = GetCtrl(0); - auto* buildTabCtrl = (mainTab && mainTab->GetCurrentTab() == TAB_BUILD) - ? mainTab->GetGroup(TAB_BUILD)->GetCtrl(1) - : nullptr; + auto* buildTabCtrl = + (mainTab && mainTab->GetCurrentTab() == TAB_BUILD) ? mainTab->GetGroup(TAB_BUILD)->GetCtrl(1) : nullptr; auto* bldGroup = buildTabCtrl ? buildTabCtrl->GetGroup(buildTabCtrl->GetCurrentTab()) : nullptr; bool hasHoveredIcon = false; diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 5bccab0398..cb3daf4ef9 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -3,18 +3,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "world/GameWorldView.h" -#include #include "CatapultStone.h" #include "Cheats.h" #include "FOWObjects.h" -#include "ReturnMapPointWithRadius.h" -#include "Window.h" #include "GameInterface.h" #include "GamePlayer.h" #include "GlobalGameSettings.h" #include "Loader.h" #include "MapGeometry.h" +#include "ReturnMapPointWithRadius.h" #include "Settings.h" +#include "Window.h" #include "addons/AddonMaxWaterwayLength.h" #include "buildings/noBuildingSite.h" #include "buildings/nobMilitary.h" @@ -39,6 +38,7 @@ #include #include #include +#include GameWorldView::GameWorldView(const GameWorldViewer& gwv, const Position& pos, const Extent& size) : selPt(0, 0), show_bq(SETTINGS.ingame.showBQ), show_names(SETTINGS.ingame.showNames), @@ -772,8 +772,8 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) { for(int dh : {-h, 0, h}) { - const DrawPoint scr = GetNodePos(Position(basePt.x + dw, basePt.y + dh)) - - DrawPoint(0, HEIGHT_FACTOR * alt) - offset; + const DrawPoint scr = + GetNodePos(Position(basePt.x + dw, basePt.y + dh)) - DrawPoint(0, HEIGHT_FACTOR * alt) - offset; Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); } } diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index cbd0191725..d2f8cde22f 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -8,8 +8,8 @@ #include "DrawPoint.h" #include "gameTypes/MapCoordinates.h" #include "gameTypes/MapTypes.h" -#include #include +#include #include class GameWorldBase; From 7ec4ce5b701db7da81c6c4d6e35ecb8752e69dea Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Thu, 18 Jun 2026 11:11:53 +0200 Subject: [PATCH 14/24] Better radius ouline drawing --- libs/s25main/world/GameWorldView.cpp | 59 ++++++++++++++++++---------- libs/s25main/world/GameWorldView.h | 3 ++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index cb3daf4ef9..498e375f23 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -745,38 +745,57 @@ void GameWorldView::RemoveDrawNodeCallback(IDrawNodeCallback* callbackToRemove) drawNodeCallbacks.erase(itPos); } +// ----------------------------------------------------------------------------- +// Snap a point to the nearest toroidal copy relative to a reference position. +// Same formula as s25edit's correctMouseBlit(): +// k = round((reference - vertex) / mapSize) +// vertex += k * mapSize +// +// Reference: s25edit/external/s25edit/CMap.cpp :: correctMouseBlit() (line 1088) +// ----------------------------------------------------------------------------- +DrawPoint GameWorldView::SnapToNearestCopy(DrawPoint pt, const DrawPoint& ref, const DrawPoint& mapPxSize) +{ + if(mapPxSize.x > 0) + { + int kx = static_cast(std::floor((ref.x - pt.x) / static_cast(mapPxSize.x) + 0.5)); + pt.x += kx * mapPxSize.x; + } + if(mapPxSize.y > 0) + { + int ky = static_cast(std::floor((ref.y - pt.y) / static_cast(mapPxSize.y) + 0.5)); + pt.y += ky * mapPxSize.y; + } + return pt; +} + +// ----------------------------------------------------------------------------- +// Draw radius overlay using CorrectMouseBlit for toroidal wrapping. +// Reference: s25edit/external/s25edit/CMap.cpp :: render() (lines 1159, 1230) +// - Computes brush blit positions via correctMouseBlit() +// - Draws overlay sprites at those positions +// ----------------------------------------------------------------------------- void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) { const auto& world = GetWorld(); - // Get all border points at the exact radius auto pts = world.GetPointsInRadius(center, radius, ReturnMapPointWithRadius{}); const MapExtent mapSize = world.GetSize(); - constexpr unsigned BORDER_COLOR = 0xFFFF0000; // Red with full alpha + constexpr unsigned BORDER_COLOR = 0xFFFF0000; + const DrawPoint mapPxSize(mapSize.x * TR_W, mapSize.y * TR_H); - const int w = mapSize.x; - const int h = mapSize.y; + // Reference: screen position of the center vertex (with seam offset). + const auto centerAlt = world.GetNode(center).altitude; + const DrawPoint ref = GetNodePos(center) - DrawPoint(0, HEIGHT_FACTOR * centerAlt) + selPtOffset; - for(const auto& ptWithRadius : pts) + for(const auto& [basePt, dist] : pts) { - if(ptWithRadius.second != radius) + if(dist != radius) continue; - const MapPoint& basePt = ptWithRadius.first; - const auto alt = world.GetNode(basePt).altitude; + DrawPoint scr = GetNodePos(basePt, world.GetNode(basePt).altitude); + scr = SnapToNearestCopy(scr, ref, mapPxSize) - offset; - // Draw at the canonical position and its 8 toroidal copies (shifted by ± map dimensions). - // We use Position arithmetic (no modulo) so each shifted copy produces a different screen - // position. Copies outside the viewport are clipped by the renderer. - for(int dw : {-w, 0, w}) - { - for(int dh : {-h, 0, h}) - { - const DrawPoint scr = - GetNodePos(Position(basePt.x + dw, basePt.y + dh)) - DrawPoint(0, HEIGHT_FACTOR * alt) - offset; - Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); - } - } + Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); } } diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index d2f8cde22f..63a28b862d 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -156,6 +156,9 @@ class GameWorldView /// Draw a radius outline ring around a center point with the given radius void DrawRadiusOutline(const MapPoint& center, unsigned radius); + /// Snap a point to the nearest toroidal copy relative to a reference position + static DrawPoint SnapToNearestCopy(DrawPoint pt, const DrawPoint& ref, const DrawPoint& mapPxSize); + void SaveIngameSettingsValues() const; void updateEffectiveZoomFactor(); }; From ab91acd68e6ed512abf592722b4fa6fd0f88e13f Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Thu, 18 Jun 2026 22:19:30 +0200 Subject: [PATCH 15/24] Enable addon by default --- libs/s25main/GlobalGameSettings.cpp | 2 +- libs/s25main/addons/AddonBool.cpp | 4 ++-- libs/s25main/addons/AddonBool.h | 2 +- libs/s25main/addons/AddonBuildingRadius.h | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index bb1475b858..c2d24e3ae7 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -156,7 +156,7 @@ const GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId i bool GlobalGameSettings::isEnabled(AddonId id) const { const auto* addon = getAddon(id); - return addon && addon->status != addon->addon->getDefaultStatus(); + return addon && addon->status != 0; } unsigned GlobalGameSettings::getSelection(AddonId id) const diff --git a/libs/s25main/addons/AddonBool.cpp b/libs/s25main/addons/AddonBool.cpp index 3286fedf3e..5fb403f0ee 100644 --- a/libs/s25main/addons/AddonBool.cpp +++ b/libs/s25main/addons/AddonBool.cpp @@ -7,8 +7,8 @@ #include "Window.h" #include "controls/ctrlCheck.h" -AddonBool::AddonBool(const AddonId id, AddonGroup groups, const std::string& name, const std::string& description) - : Addon(id, groups, name, description, 0) +AddonBool::AddonBool(const AddonId id, AddonGroup groups, const std::string& name, const std::string& description, unsigned defaultStatus) + : Addon(id, groups, name, description, defaultStatus) {} std::unique_ptr AddonBool::createGui(Window& window, bool readonly) const diff --git a/libs/s25main/addons/AddonBool.h b/libs/s25main/addons/AddonBool.h index a20e928fb8..92fd47fe86 100644 --- a/libs/s25main/addons/AddonBool.h +++ b/libs/s25main/addons/AddonBool.h @@ -20,7 +20,7 @@ class AddonBool : public Addon }; public: - AddonBool(AddonId id, AddonGroup groups, const std::string& name, const std::string& description); + AddonBool(AddonId id, AddonGroup groups, const std::string& name, const std::string& description, unsigned defaultStatus = 0); unsigned getNumOptions() const override; diff --git a/libs/s25main/addons/AddonBuildingRadius.h b/libs/s25main/addons/AddonBuildingRadius.h index de9d493872..7744ba6b05 100644 --- a/libs/s25main/addons/AddonBuildingRadius.h +++ b/libs/s25main/addons/AddonBuildingRadius.h @@ -16,6 +16,7 @@ class AddonBuildingRadius : public AddonBool AddonBuildingRadius() : AddonBool(AddonId::BUILDING_RADIUS, AddonGroup::GamePlay, _("Show building radius"), _("Shows the working radius of buildings in the build menu tooltip and as an overlay on the map " - "when hovering over a building icon or selecting a building.")) + "when hovering over a building icon or selecting a building."), + 1) // Enabled by default {} }; From 16840a8ef0c2982154bf4aadf80d3be61bb9f72b Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Sat, 20 Jun 2026 15:41:48 +0200 Subject: [PATCH 16/24] Ignore game buildings when mouse over window --- libs/s25main/world/GameWorldView.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 498e375f23..4c5d58fcf0 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -14,6 +14,7 @@ #include "ReturnMapPointWithRadius.h" #include "Settings.h" #include "Window.h" +#include "WindowManager.h" #include "addons/AddonMaxWaterwayLength.h" #include "buildings/noBuildingSite.h" #include "buildings/nobMilitary.h" @@ -227,8 +228,10 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(radiusPreview_) DrawRadiusOutline(radiusPreview_->first, radiusPreview_->second); - // Auto-detect radius for the building under the cursor - if(!radiusPreview_ && GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS)) + // Auto-detect radius for the building under the cursor. + // Do not trigger hover when the mouse is over an ingame window. + if(!radiusPreview_ && GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS) + && !WINDOWMANAGER.FindWindowAtPos(VIDEODRIVER.GetMousePos())) { std::optional bldType; const Visibility vis = gwv.GetVisibility(selPt); From 0ed0c34730c0baad45cd385f7b75b6479e881e15 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 12:58:29 +0000 Subject: [PATCH 17/24] Revert isEnabled change: restore original semantics The original logic checks whether status differs from the addon's default (status != getDefaultStatus()). This is the convention used throughout the codebase: an addon is considered 'enabled' when its status has been changed from the default (S2-like) behavior. The previous change to 'status != 0' would break addons with default != 0. --- libs/s25main/GlobalGameSettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index c2d24e3ae7..bb1475b858 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -156,7 +156,7 @@ const GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId i bool GlobalGameSettings::isEnabled(AddonId id) const { const auto* addon = getAddon(id); - return addon && addon->status != 0; + return addon && addon->status != addon->addon->getDefaultStatus(); } unsigned GlobalGameSettings::getSelection(AddonId id) const From e4df373a4d9b403fb776b317a2a7919309ed1d85 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 15:16:37 +0200 Subject: [PATCH 18/24] Revert "Revert isEnabled change: restore original semantics" This reverts commit 0ed0c34730c0baad45cd385f7b75b6479e881e15. --- libs/s25main/GlobalGameSettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index bb1475b858..c2d24e3ae7 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -156,7 +156,7 @@ const GlobalGameSettings::AddonWithState* GlobalGameSettings::getAddon(AddonId i bool GlobalGameSettings::isEnabled(AddonId id) const { const auto* addon = getAddon(id); - return addon && addon->status != addon->addon->getDefaultStatus(); + return addon && addon->status != 0; } unsigned GlobalGameSettings::getSelection(AddonId id) const From 675e474df47741aff1cf8c1c912ffca07569e1d1 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 16:10:23 +0200 Subject: [PATCH 19/24] Move OnHover() callsite to Msg_MouseMove --- libs/s25main/controls/ctrlBuildingIcon.cpp | 11 ++++++ libs/s25main/controls/ctrlBuildingIcon.h | 10 +++++ libs/s25main/ingameWindows/iwAction.cpp | 46 ++++++++++------------ libs/s25main/ingameWindows/iwAction.h | 4 ++ 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/libs/s25main/controls/ctrlBuildingIcon.cpp b/libs/s25main/controls/ctrlBuildingIcon.cpp index e43493f752..51ba42099c 100644 --- a/libs/s25main/controls/ctrlBuildingIcon.cpp +++ b/libs/s25main/controls/ctrlBuildingIcon.cpp @@ -4,6 +4,7 @@ #include "ctrlBuildingIcon.h" #include "Loader.h" +#include "driver/MouseCoords.h" #include "files.h" #include "ogl/glArchivItem_Bitmap.h" #include "gameTypes/BuildingType.h" @@ -28,3 +29,13 @@ void ctrlBuildingIcon::Draw_() } void ctrlBuildingIcon::DrawContent() const {} + +bool ctrlBuildingIcon::Msg_MouseMove(const MouseCoords& mc) +{ + const bool wasHovered = (state == ButtonState::Hover); + const bool result = ctrlButton::Msg_MouseMove(mc); + const bool nowHovered = (state == ButtonState::Hover); + if(wasHovered != nowHovered && onHoverChanged_) + onHoverChanged_(nowHovered); + return result; +} diff --git a/libs/s25main/controls/ctrlBuildingIcon.h b/libs/s25main/controls/ctrlBuildingIcon.h index 393d597c18..060cc70648 100644 --- a/libs/s25main/controls/ctrlBuildingIcon.h +++ b/libs/s25main/controls/ctrlBuildingIcon.h @@ -5,6 +5,9 @@ #pragma once #include "ctrlButton.h" +#include + +struct MouseCoords; class Window; class ctrlBuildingIcon : public ctrlButton @@ -15,11 +18,18 @@ class ctrlBuildingIcon : public ctrlButton /// liefert den GebäudeTyp des Icons. BuildingType GetType() const { return type; } + /// Set callback for hover-state changes (called with true when hovered, false when not) + void SetOnHoverChanged(std::function cb) { onHoverChanged_ = std::move(cb); } + protected: /// zeichnet das Fenster. void Draw_() override; void DrawContent() const override; + bool Msg_MouseMove(const MouseCoords& mc) override; const BuildingType type; /// der GebäudeType des Icons. const Nation nation; /// Volk + +private: + std::function onHoverChanged_; }; diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index b1a2070d7d..0c1449d5c1 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -47,7 +47,7 @@ enum TabID iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapPoint selectedPt, const DrawPoint& mousePos, Params params, bool military_buildings) : IngameWindow(CGI_ACTION, mousePos, Extent(200, 254), _("Activity window"), LOADER.GetImageN("io", 1)), gi(gi), - gwv(gwv), selectedPt(selectedPt), mousePosAtOpen_(mousePos) + gwv(gwv), selectedPt(selectedPt), mousePosAtOpen_(mousePos), activeHoveredIcon_(nullptr) { /* TAB_FLAG 1 = Land road @@ -195,9 +195,27 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP } DrawPoint iconPos((k % 5) * 36, (k / 5) * 36 + 45); - build_tab->GetGroup(static_cast(bt)) + ctrlBuildingIcon* icon = build_tab->GetGroup(static_cast(bt)) ->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str()); + // Store hover callback; activeHoveredIcon_ guards against stale leaves from + // reversed child iteration order in Msg_MouseMove dispatch + if(radius > 0) + { + icon->SetOnHoverChanged([this, icon, radius](bool hovered) noexcept { + if(hovered) + { + this->activeHoveredIcon_ = icon; + this->gwv.SetRadiusPreview(std::make_pair(this->selectedPt, radius)); + } else if(this->activeHoveredIcon_ == icon) + { + this->activeHoveredIcon_ = nullptr; + this->gwv.SetRadiusPreview(std::nullopt); + } + // else: stale leave from a previously-hovered icon, ignore + }); + } + ++k; } @@ -416,6 +434,7 @@ void iwAction::Close() { if(ShouldBeClosed()) return; + activeHoveredIcon_ = nullptr; gwv.SetRadiusPreview(std::nullopt); IngameWindow::Close(); if(mousePosAtOpen_.isValid()) @@ -537,29 +556,6 @@ void iwAction::Msg_PaintAfter() { IngameWindow::Msg_PaintAfter(); - // Resolve building icon hover preview after all mouse events are processed - auto* mainTab = GetCtrl(0); - auto* buildTabCtrl = - (mainTab && mainTab->GetCurrentTab() == TAB_BUILD) ? mainTab->GetGroup(TAB_BUILD)->GetCtrl(1) : nullptr; - auto* bldGroup = buildTabCtrl ? buildTabCtrl->GetGroup(buildTabCtrl->GetCurrentTab()) : nullptr; - - bool hasHoveredIcon = false; - if(bldGroup && gwv.GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS)) - { - for(auto* icon : bldGroup->GetCtrls()) - { - const unsigned radius = GetBuildingRadius(icon->GetType()); - if(icon->IsMouseOver() && radius > 0) - { - gwv.SetRadiusPreview(std::make_pair(selectedPt, radius)); - hasHoveredIcon = true; - break; - } - } - } - if(!hasHoveredIcon) - gwv.SetRadiusPreview(std::nullopt); - auto* tab = GetCtrl(0); if(tab) { diff --git a/libs/s25main/ingameWindows/iwAction.h b/libs/s25main/ingameWindows/iwAction.h index 3007f4df09..2384f64189 100644 --- a/libs/s25main/ingameWindows/iwAction.h +++ b/libs/s25main/ingameWindows/iwAction.h @@ -11,6 +11,7 @@ class GameInterface; class GameWorldView; +class ctrlBuildingIcon; class ctrlGroup; class iwAction : public IngameWindow @@ -69,6 +70,9 @@ class iwAction : public IngameWindow /// Die einzelnen Höhen für die einzelnen Tabs im Bautab std::array building_tab_heights; + /// Track which icon currently owns the radius preview (to ignore stale leaves) + const ctrlBuildingIcon* activeHoveredIcon_; + public: iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapPoint selectedPt, const DrawPoint& mousePos, Params params, bool military_buildings); From 0f408f0169dcc239d9de7d6935de84d548e8ee7e Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 16:46:59 +0200 Subject: [PATCH 20/24] UpdateRadiusPreviewForMousePos --- libs/s25main/desktops/dskGameInterface.cpp | 3 + libs/s25main/world/GameWorldView.cpp | 68 +++++++++++++--------- libs/s25main/world/GameWorldView.h | 6 ++ 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/libs/s25main/desktops/dskGameInterface.cpp b/libs/s25main/desktops/dskGameInterface.cpp index 14038ecb1e..cba9069548 100644 --- a/libs/s25main/desktops/dskGameInterface.cpp +++ b/libs/s25main/desktops/dskGameInterface.cpp @@ -750,6 +750,9 @@ bool dskGameInterface::Msg_LeftUp(const MouseCoords& mc) bool dskGameInterface::Msg_MouseMove(const MouseCoords& mc) { + // Update radius preview for buildings under the cursor (event-driven, not in draw loop) + gwv.UpdateRadiusPreviewForMousePos(mc.pos); + if(!isScrolling) { if(mc.num_tfingers == 1) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 4c5d58fcf0..37e3f8272c 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -44,7 +44,8 @@ GameWorldView::GameWorldView(const GameWorldViewer& gwv, const Position& pos, const Extent& size) : selPt(0, 0), show_bq(SETTINGS.ingame.showBQ), show_names(SETTINGS.ingame.showNames), show_productivity(SETTINGS.ingame.showProductivity), offset(0, 0), lastOffset(0, 0), gwv(gwv), origin_(pos), - size_(size), zoomFactor_(1.f), targetZoomFactor_(1.f), zoomSpeed_(0.f) + size_(size), zoomFactor_(1.f), targetZoomFactor_(1.f), zoomSpeed_(0.f), + isBuildingRadiusEnabled_(GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS)) { updateEffectiveZoomFactor(); MoveBy({0, 0}); @@ -224,37 +225,10 @@ void GameWorldView::Draw(const RoadBuildState& rb, const MapPoint selected, bool if(show_names || show_productivity) DrawNameProductivityOverlay(terrainRenderer); - // Draw radius preview outline + // Draw radius preview outline (set by icon-hover or map-hover) if(radiusPreview_) DrawRadiusOutline(radiusPreview_->first, radiusPreview_->second); - // Auto-detect radius for the building under the cursor. - // Do not trigger hover when the mouse is over an ingame window. - if(!radiusPreview_ && GetWorld().GetGGS().isEnabled(AddonId::BUILDING_RADIUS) - && !WINDOWMANAGER.FindWindowAtPos(VIDEODRIVER.GetMousePos())) - { - std::optional bldType; - const Visibility vis = gwv.GetVisibility(selPt); - if(vis == Visibility::Visible) - { - const auto* bld = GetWorld().GetSpecObj(selPt); - if(bld) - bldType = bld->GetBuildingType(); - } else if(vis == Visibility::FogOfWar) - { - const FOWObject* fow = gwv.GetYoungestFOWObject(selPt); - if(fow && fow->GetType() == FoW_Type::Building) - bldType = static_cast(*fow).GetBuildingType(); - } - - if(bldType) - { - const unsigned bldRadius = GetBuildingRadius(*bldType); - if(bldRadius > 0) - DrawRadiusOutline(selPt, bldRadius); - } - } - DrawGUI(rb, terrainRenderer, selected, drawMouse); // Draw catapult stones @@ -802,6 +776,42 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) } } +void GameWorldView::UpdateRadiusPreviewForMousePos(const Position& mousePos) +{ + if(!isBuildingRadiusEnabled_ || WINDOWMANAGER.FindWindowAtPos(mousePos)) + return; + + const auto& world = GetWorld(); + std::optional bldType; + switch(gwv.GetVisibility(selPt)) + { + case Visibility::Visible: + { + const auto* bld = world.GetSpecObj(selPt); + if(bld) + bldType = bld->GetBuildingType(); + } + break; + case Visibility::FogOfWar: + { + const FOWObject* fow = gwv.GetYoungestFOWObject(selPt); + if(fow && fow->GetType() == FoW_Type::Building) + bldType = static_cast(*fow).GetBuildingType(); + } + break; + } + + if(bldType) + { + const unsigned bldRadius = GetBuildingRadius(*bldType); + if(bldRadius > 0) + radiusPreview_ = std::make_pair(selPt, bldRadius); + else + radiusPreview_ = std::nullopt; + } else + radiusPreview_ = std::nullopt; +} + void GameWorldView::CalcFxLx() { // Calc first and last point in map units (with 1 extra for incomplete triangles) diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index 63a28b862d..5a789559c2 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -49,6 +49,8 @@ class GameWorldView /// Optional radius preview (center position, radius) drawn as outline on the map std::optional> radiusPreview_; + /// Cached addon status: whether building radius overlay is enabled + bool isBuildingRadiusEnabled_; /// Offset from world origin in screen units (not map units): "scroll position" DrawPoint offset; @@ -124,6 +126,10 @@ class GameWorldView /// Set/show a radius preview outline on the map (or clear with std::nullopt) void SetRadiusPreview(const std::optional>& preview) { radiusPreview_ = preview; } + /// Update radius preview based on which map node is under the mouse cursor + /// Called from mouse-move handler, not from draw loop + void UpdateRadiusPreviewForMousePos(const Position& mousePos); + /// Gibt selektierten Punkt zurück MapPoint GetSelectedPt() const { return selPt; } From caef896c64cee563dd06562a8afffee681d856d6 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 17:03:30 +0200 Subject: [PATCH 21/24] SnapToNearestCopy --- libs/s25main/world/GameWorldView.cpp | 33 ++++++++-------------------- libs/s25main/world/GameWorldView.h | 2 +- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 37e3f8272c..0cea5892e5 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -722,34 +722,19 @@ void GameWorldView::RemoveDrawNodeCallback(IDrawNodeCallback* callbackToRemove) drawNodeCallbacks.erase(itPos); } -// ----------------------------------------------------------------------------- -// Snap a point to the nearest toroidal copy relative to a reference position. -// Same formula as s25edit's correctMouseBlit(): -// k = round((reference - vertex) / mapSize) -// vertex += k * mapSize -// -// Reference: s25edit/external/s25edit/CMap.cpp :: correctMouseBlit() (line 1088) -// ----------------------------------------------------------------------------- -DrawPoint GameWorldView::SnapToNearestCopy(DrawPoint pt, const DrawPoint& ref, const DrawPoint& mapPxSize) -{ - if(mapPxSize.x > 0) - { - int kx = static_cast(std::floor((ref.x - pt.x) / static_cast(mapPxSize.x) + 0.5)); - pt.x += kx * mapPxSize.x; - } - if(mapPxSize.y > 0) - { - int ky = static_cast(std::floor((ref.y - pt.y) / static_cast(mapPxSize.y) + 0.5)); - pt.y += ky * mapPxSize.y; - } +// Snap a point to the nearest toroidal copy (map wrapping) relative to a reference. +// k = round((reference - pt) / mapSize), pt += k * mapSize. +DrawPoint GameWorldView::SnapToNearestCopy(DrawPoint pt, DrawPoint ref, DrawPoint mapPxSize) +{ + const double kx = std::floor(static_cast(ref.x - pt.x) / mapPxSize.x + 0.5); + pt.x += static_cast(kx) * mapPxSize.x; + const double ky = std::floor(static_cast(ref.y - pt.y) / mapPxSize.y + 0.5); + pt.y += static_cast(ky) * mapPxSize.y; return pt; } // ----------------------------------------------------------------------------- -// Draw radius overlay using CorrectMouseBlit for toroidal wrapping. -// Reference: s25edit/external/s25edit/CMap.cpp :: render() (lines 1159, 1230) -// - Computes brush blit positions via correctMouseBlit() -// - Draws overlay sprites at those positions +// Draw radius overlay for a building's working range, handling map wrapping. // ----------------------------------------------------------------------------- void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) { diff --git a/libs/s25main/world/GameWorldView.h b/libs/s25main/world/GameWorldView.h index 5a789559c2..4238f3809b 100644 --- a/libs/s25main/world/GameWorldView.h +++ b/libs/s25main/world/GameWorldView.h @@ -163,7 +163,7 @@ class GameWorldView void DrawRadiusOutline(const MapPoint& center, unsigned radius); /// Snap a point to the nearest toroidal copy relative to a reference position - static DrawPoint SnapToNearestCopy(DrawPoint pt, const DrawPoint& ref, const DrawPoint& mapPxSize); + static DrawPoint SnapToNearestCopy(DrawPoint pt, DrawPoint ref, DrawPoint mapPxSize); void SaveIngameSettingsValues() const; void updateEffectiveZoomFactor(); From ae81945ec60afdf6ac7bc129440de9cfcae6579c Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 17:21:49 +0200 Subject: [PATCH 22/24] DrawRadiusOutline --- libs/s25main/world/GameWorldView.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libs/s25main/world/GameWorldView.cpp b/libs/s25main/world/GameWorldView.cpp index 0cea5892e5..4b4b2a790c 100644 --- a/libs/s25main/world/GameWorldView.cpp +++ b/libs/s25main/world/GameWorldView.cpp @@ -745,19 +745,19 @@ void GameWorldView::DrawRadiusOutline(const MapPoint& center, unsigned radius) constexpr unsigned BORDER_COLOR = 0xFFFF0000; const DrawPoint mapPxSize(mapSize.x * TR_W, mapSize.y * TR_H); - // Reference: screen position of the center vertex (with seam offset). - const auto centerAlt = world.GetNode(center).altitude; - const DrawPoint ref = GetNodePos(center) - DrawPoint(0, HEIGHT_FACTOR * centerAlt) + selPtOffset; + // Screen position of the center vertex. + // selPtOffset accounts for the pixel shift when the map wraps at the seam. + const DrawPoint centerScr = GetNodePos(center, world.GetNode(center).altitude) + selPtOffset; - for(const auto& [basePt, dist] : pts) + for(const auto& [mapPt, dist] : pts) { if(dist != radius) continue; - DrawPoint scr = GetNodePos(basePt, world.GetNode(basePt).altitude); - scr = SnapToNearestCopy(scr, ref, mapPxSize) - offset; + DrawPoint screenPt = GetNodePos(mapPt, world.GetNode(mapPt).altitude); + screenPt = SnapToNearestCopy(screenPt, centerScr, mapPxSize) - offset; - Window::DrawRectangle(Rect(scr - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); + Window::DrawRectangle(Rect(screenPt - DrawPoint(2, 2), Extent(5, 5)), BORDER_COLOR); } } From 93246c43153410b7299bc9e060d0f11c5fe87c9d Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 17:24:38 +0200 Subject: [PATCH 23/24] GetBuildingRadius comment wording --- libs/s25main/gameData/BuildingConsts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/s25main/gameData/BuildingConsts.cpp b/libs/s25main/gameData/BuildingConsts.cpp index a4b7ac3dca..6a6708823d 100644 --- a/libs/s25main/gameData/BuildingConsts.cpp +++ b/libs/s25main/gameData/BuildingConsts.cpp @@ -456,7 +456,7 @@ unsigned GetBuildingRadius(BuildingType bld) return nofFarmhand::GetWorkRadius(*job); return 0; } - // All other building types have no relevant radius overlay + // Remaining building types have no relevant radius default: return 0; } } From 83752d25e40cd3c3cf43a29d1016c8bc5fccb357 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 30 Jun 2026 17:25:56 +0200 Subject: [PATCH 24/24] clang-format --- libs/s25main/addons/AddonBool.cpp | 3 ++- libs/s25main/addons/AddonBool.h | 3 ++- libs/s25main/ingameWindows/iwAction.cpp | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/s25main/addons/AddonBool.cpp b/libs/s25main/addons/AddonBool.cpp index 5fb403f0ee..f72f6e2332 100644 --- a/libs/s25main/addons/AddonBool.cpp +++ b/libs/s25main/addons/AddonBool.cpp @@ -7,7 +7,8 @@ #include "Window.h" #include "controls/ctrlCheck.h" -AddonBool::AddonBool(const AddonId id, AddonGroup groups, const std::string& name, const std::string& description, unsigned defaultStatus) +AddonBool::AddonBool(const AddonId id, AddonGroup groups, const std::string& name, const std::string& description, + unsigned defaultStatus) : Addon(id, groups, name, description, defaultStatus) {} diff --git a/libs/s25main/addons/AddonBool.h b/libs/s25main/addons/AddonBool.h index 92fd47fe86..6895e3f7c8 100644 --- a/libs/s25main/addons/AddonBool.h +++ b/libs/s25main/addons/AddonBool.h @@ -20,7 +20,8 @@ class AddonBool : public Addon }; public: - AddonBool(AddonId id, AddonGroup groups, const std::string& name, const std::string& description, unsigned defaultStatus = 0); + AddonBool(AddonId id, AddonGroup groups, const std::string& name, const std::string& description, + unsigned defaultStatus = 0); unsigned getNumOptions() const override; diff --git a/libs/s25main/ingameWindows/iwAction.cpp b/libs/s25main/ingameWindows/iwAction.cpp index 0c1449d5c1..13426cb2c5 100644 --- a/libs/s25main/ingameWindows/iwAction.cpp +++ b/libs/s25main/ingameWindows/iwAction.cpp @@ -196,7 +196,7 @@ iwAction::iwAction(GameInterface& gi, GameWorldView& gwv, const Tabs& tabs, MapP DrawPoint iconPos((k % 5) * 36, (k / 5) * 36 + 45); ctrlBuildingIcon* icon = build_tab->GetGroup(static_cast(bt)) - ->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str()); + ->AddBuildingIcon(k, iconPos, bld, player.nation, 36, tooltip.str()); // Store hover callback; activeHoveredIcon_ guards against stale leaves from // reversed child iteration order in Msg_MouseMove dispatch