[Scummvm-git-logs] scummvm master -> 50184ef27df59e562824c99c0b1abf7a35c700d1

sev- noreply at scummvm.org
Mon Jul 13 11:35:44 UTC 2026


This automated email contains information about 12 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
fa93cdd093 COMMON: Add toggle-hotspots event and standard action
61db0bf14d BACKENDS: Handle toggle-hotspots keymapper action
d0f6830e64 GRAPHICS: Add hotspot overlay renderer
e2532bdc15 ENGINES: Add hotspot display infrastructure
c8c6bcf85d GUI: Add hotspot options to Game Options
496a9a65f5 GUI: Add hotspot theme layout and regenerate themes
62a9809ead TEST: Add hotspot unit tests
0ac8afb5b5 DOCS: Document hotspot display feature
258a839452 TOUCHE: Add hotspot display support
0379edd771 GRAPHICS: Scale hotspot overlay and support 32-bit backends
a1bba7b319 ENGINES: Hide mouse cursor while hotspots overlay is shown
50184ef27d COMMON: Drive hotspot display with a start/end event pair


Commit: fa93cdd093cf594f429b1dd817fe4d4962eccfca
    https://github.com/scummvm/scummvm/commit/fa93cdd093cf594f429b1dd817fe4d4962eccfca
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
COMMON: Add toggle-hotspots event and standard action

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    common/events.h


diff --git a/common/events.h b/common/events.h
index 60b9d03c774..6e69117d176 100644
--- a/common/events.h
+++ b/common/events.h
@@ -117,6 +117,9 @@ enum EventType {
 	EVENT_FOCUS_GAINED = 36,
 	EVENT_FOCUS_LOST = 37,
 
+	/** Toggle hotspot display. */
+	EVENT_TOGGLE_HOTSPOTS = 38,
+
 	/**
 	 * We reserve some event ids for custom events.
 	 * 
@@ -129,6 +132,15 @@ enum EventType {
 	EVENT_USER_LAST_AVAILABLE = 9999
 };
 
+/**
+ * Custom engine action types for EVENT_CUSTOM_ENGINE_ACTION_START/END.
+ * These are used by cross-engine features. Values start at 20000 to avoid
+ * clashing with engine-specific custom action ids, which start from 0.
+ */
+enum EngineAction {
+	kEngineActionHotspotToggle = 20000
+};
+
 const int16 JOYAXIS_MIN = -32768;
 const int16 JOYAXIS_MAX = 32767;
 


Commit: 61db0bf14dc0b521b58bf457264f81de8c287e4d
    https://github.com/scummvm/scummvm/commit/61db0bf14dc0b521b58bf457264f81de8c287e4d
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
BACKENDS: Handle toggle-hotspots keymapper action

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    backends/events/default/default-events.cpp
    backends/keymapper/standard-actions.cpp
    backends/keymapper/standard-actions.h


diff --git a/backends/events/default/default-events.cpp b/backends/events/default/default-events.cpp
index 4a02bb270c2..056aef9124a 100644
--- a/backends/events/default/default-events.cpp
+++ b/backends/events/default/default-events.cpp
@@ -120,6 +120,22 @@ bool DefaultEventManager::pollEvent(Common::Event &event) {
 		_modifierState = event.kbd.flags;
 		break;
 
+	case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
+		if (event.customType == Common::kEngineActionHotspotToggle && g_engine) {
+			if (ConfMan.getBool("enable_hotspots")) {
+				g_engine->showHotspots(true);
+			}
+		}
+		break;
+
+	case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
+		if (event.customType == Common::kEngineActionHotspotToggle && g_engine) {
+			if (ConfMan.getBool("enable_hotspots")) {
+				g_engine->showHotspots(false);
+			}
+		}
+		break;
+
 	case Common::EVENT_MOUSEMOVE:
 		_mousePos = event.mouse;
 		break;
diff --git a/backends/keymapper/standard-actions.cpp b/backends/keymapper/standard-actions.cpp
index 138a7794288..ca92b3e6109 100644
--- a/backends/keymapper/standard-actions.cpp
+++ b/backends/keymapper/standard-actions.cpp
@@ -41,5 +41,6 @@ const char *const kStandardActionEE            = "EEKY";
 const char *const kStandardActionCut           = "CUT";
 const char *const kStandardActionCopy          = "COPY";
 const char *const kStandardActionPaste         = "PASTE";
+const char *const kStandardActionToggleHotspots = "HOTS";
 
 } //namespace Common
diff --git a/backends/keymapper/standard-actions.h b/backends/keymapper/standard-actions.h
index 23555d29e57..e7dd7ca1b1b 100644
--- a/backends/keymapper/standard-actions.h
+++ b/backends/keymapper/standard-actions.h
@@ -55,6 +55,7 @@ extern const char *const kStandardActionEE;
 extern const char *const kStandardActionCut;
 extern const char *const kStandardActionCopy;
 extern const char *const kStandardActionPaste;
+extern const char *const kStandardActionToggleHotspots;
 
 } //namespace Common
 


Commit: d0f6830e64d9f96e3a7c0de15b72bc40f287f754
    https://github.com/scummvm/scummvm/commit/d0f6830e64d9f96e3a7c0de15b72bc40f287f754
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
GRAPHICS: Add hotspot overlay renderer

Assisted-by: Claude:claude-opus-4.8

Changed paths:
  A graphics/hotspot_renderer.cpp
  A graphics/hotspot_renderer.h
    graphics/module.mk


diff --git a/graphics/hotspot_renderer.cpp b/graphics/hotspot_renderer.cpp
new file mode 100644
index 00000000000..b8d42a16cb5
--- /dev/null
+++ b/graphics/hotspot_renderer.cpp
@@ -0,0 +1,259 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "graphics/hotspot_renderer.h"
+#include "common/util.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
+#include "graphics/pixelformat.h"
+#include "graphics/surface.h"
+
+namespace Graphics {
+
+HotspotRenderer::HotspotRenderer() {
+}
+
+HotspotRenderer::~HotspotRenderer() {
+}
+
+void HotspotRenderer::render(Surface *surface,
+		const Common::Array<HotspotInfo> &hotspots,
+		int gameWidth, int gameHeight,
+		int overlayWidth, int overlayHeight,
+		const PixelFormat &format,
+		MarkerShape markerShape,
+		bool showText) {
+	if (!surface || hotspots.empty())
+		return;
+
+	const Font *font = nullptr;
+	if (showText) {
+		font = FontMan.getFontByUsage(FontManager::kGUIFont);
+	}
+
+	float scaleX = (float)overlayWidth / (float)gameWidth;
+	float scaleY = (float)overlayHeight / (float)gameHeight;
+
+	for (uint i = 0; i < hotspots.size(); i++) {
+		int gameX = hotspots[i].position.x;
+		int gameY = hotspots[i].position.y;
+
+		int overlayX = (int)(gameX * scaleX);
+		int overlayY = (int)(gameY * scaleY);
+
+		drawMarker(surface, overlayX, overlayY, format, markerShape);
+
+		if (font && !hotspots[i].name.empty()) {
+			drawHotspotLabel(surface, overlayX, overlayY, hotspots[i].name,
+				overlayWidth, overlayHeight, format, font);
+		}
+	}
+}
+
+void HotspotRenderer::drawMarker(Surface *surface, int x, int y, const PixelFormat &format, MarkerShape markerShape) {
+	int width = surface->w;
+	int height = surface->h;
+
+	switch (markerShape) {
+	case kMarkerCrosshair:
+		drawCrosshairMarker(surface, x, y, width, height, format);
+		break;
+	case kMarkerSquare:
+		drawSquareMarker(surface, x, y, width, height, format);
+		break;
+	case kMarkerPoint:
+		drawPointMarker(surface, x, y, width, height, format);
+		break;
+	}
+}
+
+void HotspotRenderer::drawHotspotLabel(Surface *surface, int overlayX, int overlayY,
+		const Common::U32String &label, int overlayWidth, int overlayHeight,
+		const PixelFormat &format, const Font *font) {
+	int textX = overlayX + kMarkerSize / 2 + 8;
+	int textY = overlayY - font->getFontHeight() / 2;
+
+	int textWidth = font->getStringWidth(label);
+	int textHeight = font->getFontHeight();
+
+	int bgX = textX - 4;
+	int bgY = textY - 2;
+	int bgW = textWidth + 8;
+	int bgH = textHeight + 4;
+
+	drawLabelBox(surface, bgX, bgY, bgW, bgH,
+		overlayWidth, overlayHeight, format);
+
+	uint32 textColor = format.RGBToColor(255, 255, 255);
+	font->drawString(surface, label, textX, textY,
+		overlayWidth - textX, textColor, kTextAlignLeft);
+}
+
+void HotspotRenderer::drawCrosshairMarker(Surface *surface, int x, int y,
+		int width, int height, const PixelFormat &format) {
+	if (format.bytesPerPixel != 2)
+		return;
+
+	const int lineLength = (kMarkerSize / 2) - 1;
+
+	drawLineWithGlow(surface, x - lineLength, y, x + lineLength, y, width, height, format, kLineThickness);
+	drawLineWithGlow(surface, x, y - lineLength, x, y + lineLength, width, height, format, kLineThickness);
+}
+
+void HotspotRenderer::drawSquareMarker(Surface *surface, int x, int y,
+		int width, int height, const PixelFormat &format) {
+	if (format.bytesPerPixel != 2)
+		return;
+
+	int size = kMarkerSize;
+	int rectX = x - kMarkerSize / 2;
+	int rectY = y - kMarkerSize / 2;
+
+	drawRectWithGlow(surface, rectX, rectY, size, size, width, height, format);
+}
+
+void HotspotRenderer::drawPointMarker(Surface *surface, int x, int y,
+		int width, int height, const PixelFormat &format) {
+	if (format.bytesPerPixel != 2)
+		return;
+
+	int maxRadius = kPointRadius + kGlowSize;
+
+	for (int dy = -maxRadius; dy <= maxRadius; dy++) {
+		for (int dx = -maxRadius; dx <= maxRadius; dx++) {
+			int px = x + dx;
+			int py = y + dy;
+
+			if (px < 0 || px >= width || py < 0 || py >= height)
+				continue;
+
+			int distSq = dx * dx + dy * dy;
+			int dist = (int)(sqrtf((float)distSq) + 0.5f);
+
+			blendPixelWithGlow(surface, px, py, format, dist, kPointRadius);
+		}
+	}
+}
+
+void HotspotRenderer::blendPixelWithGlow(Surface *surface, int px, int py,
+		const PixelFormat &format, int distance, int solidSize) {
+	uint16 *destPixel = (uint16 *)surface->getBasePtr(px, py);
+	byte bgR, bgG, bgB, bgA;
+	format.colorToARGB(*destPixel, bgA, bgR, bgG, bgB);
+
+	// Convert solidSize to maximum solid distance (solidSize=1 means center pixel only, distance=0)
+	int maxSolidDistance = solidSize - 1;
+
+	if (distance <= maxSolidDistance) {
+		const int solidBeta = 180;
+		const int solidAlpha = 255 - solidBeta;
+		byte r = ((255 * solidBeta) + (bgR * solidAlpha)) / 255;
+		byte g = ((255 * solidBeta) + (bgG * solidAlpha)) / 255;
+		byte b = ((255 * solidBeta) + (bgB * solidAlpha)) / 255;
+		*destPixel = format.RGBToColor(r, g, b);
+	} else if (distance <= maxSolidDistance + kGlowSize) {
+		int glowDist = distance - maxSolidDistance;
+		int alpha = ((kGlowSize - glowDist) * 80) / kGlowSize;
+		byte r = ((255 * alpha) + (bgR * (255 - alpha))) / 255;
+		byte g = ((255 * alpha) + (bgG * (255 - alpha))) / 255;
+		byte b = ((255 * alpha) + (bgB * (255 - alpha))) / 255;
+		*destPixel = format.RGBToColor(r, g, b);
+	}
+}
+
+void HotspotRenderer::drawLineWithGlow(Surface *surface, int x1, int y1, int x2, int y2,
+		int width, int height, const PixelFormat &format, int lineThickness) {
+	if (format.bytesPerPixel != 2)
+		return;
+
+	bool isHorizontal = (y1 == y2);
+	bool isVertical = (x1 == x2);
+
+	if (!isHorizontal && !isVertical)
+		return;
+
+	int alongMin, alongMax, acrossFixed;
+
+	if (isHorizontal) {
+		alongMin = MIN(x1, x2);
+		alongMax = MAX(x1, x2);
+		acrossFixed = y1;
+	} else {
+		alongMin = MIN(y1, y2);
+		alongMax = MAX(y1, y2);
+		acrossFixed = x1;
+	}
+
+	for (int along = alongMin; along <= alongMax; along++) {
+		for (int thickness = -lineThickness - kGlowSize; thickness <= lineThickness + kGlowSize; thickness++) {
+			int px, py;
+			if (isHorizontal) {
+				px = along;
+				py = acrossFixed + thickness;
+			} else {
+				px = acrossFixed + thickness;
+				py = along;
+			}
+
+			if (px < 0 || px >= width || py < 0 || py >= height)
+				continue;
+
+			int distFromCenter = ABS(thickness);
+			blendPixelWithGlow(surface, px, py, format, distFromCenter, lineThickness);
+		}
+	}
+}
+
+void HotspotRenderer::drawRectWithGlow(Surface *surface, int x, int y, int w, int h,
+		int overlayWidth, int overlayHeight, const PixelFormat &format) {
+	drawLineWithGlow(surface, x, y, x + w - 1, y, overlayWidth, overlayHeight, format, kLineThickness);
+	drawLineWithGlow(surface, x, y + h - 1, x + w - 1, y + h - 1,
+			overlayWidth, overlayHeight, format, kLineThickness);
+	drawLineWithGlow(surface, x, y + 1, x, y + h - 2, overlayWidth, overlayHeight, format, kLineThickness);
+	drawLineWithGlow(surface, x + w - 1, y + 1, x + w - 1, y + h - 2,
+			overlayWidth, overlayHeight, format, kLineThickness);
+}
+
+void HotspotRenderer::drawLabelBox(Surface *surface, int x, int y, int w, int h,
+		int overlayWidth, int overlayHeight, const PixelFormat &format) {
+	if (format.bytesPerPixel != 2)
+		return;
+
+	for (int py = y + 1; py < y + h - 1; py++) {
+		for (int px = x + 1; px < x + w - 1; px++) {
+			if (px < 0 || px >= overlayWidth || py < 0 || py >= overlayHeight)
+				continue;
+
+			uint16 *destPixel = (uint16 *)surface->getBasePtr(px, py);
+			byte bgR, bgG, bgB, bgA;
+			format.colorToARGB(*destPixel, bgA, bgR, bgG, bgB);
+
+			byte r = (bgR * 40) / 100;
+			byte g = (bgG * 40) / 100;
+			byte b = (bgB * 40) / 100;
+			*destPixel = format.RGBToColor(r, g, b);
+		}
+	}
+
+	drawRectWithGlow(surface, x, y, w, h, overlayWidth, overlayHeight, format);
+}
+
+} // End of namespace Graphics
diff --git a/graphics/hotspot_renderer.h b/graphics/hotspot_renderer.h
new file mode 100644
index 00000000000..adb5b038b20
--- /dev/null
+++ b/graphics/hotspot_renderer.h
@@ -0,0 +1,125 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef GRAPHICS_HOTSPOT_RENDERER_H
+#define GRAPHICS_HOTSPOT_RENDERER_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/ustr.h"
+
+namespace Graphics {
+
+class Font;
+class Surface;
+struct PixelFormat;
+
+/**
+ * Classification of a hotspot by its role in the game world.
+ */
+enum HotspotType {
+	kHotspotDefault, ///< No specific type (generic / unclassified)
+	kHotspotObject,  ///< Interactable object
+	kHotspotExit,    ///< Exit or door leading to another area
+	kHotspotNPC      ///< Non-player character
+};
+
+/**
+ * Information about a single hotspot to display.
+ */
+struct HotspotInfo {
+	Common::Point position; ///< Position in game coordinates
+	Common::U32String name; ///< Display name
+	HotspotType type;       ///< Classification of the hotspot
+
+	HotspotInfo() : type(kHotspotDefault) {}
+	HotspotInfo(const Common::Point &pos, const Common::U32String &n,
+		HotspotType t = kHotspotDefault)
+		: position(pos), name(n), type(t) {}
+};
+
+/**
+ * Marker shape types.
+ */
+enum MarkerShape {
+	kMarkerCrosshair,
+	kMarkerSquare,
+	kMarkerPoint
+};
+
+/**
+ * Renderer for drawing hotspot markers on overlay surface.
+ */
+class HotspotRenderer {
+public:
+	HotspotRenderer();
+	~HotspotRenderer();
+
+	/**
+	 * Render hotspot markers on an overlay surface.
+	 *
+	 * @param surface       The overlay surface to draw on
+	 * @param hotspots      Array of hotspot information
+	 * @param gameWidth     Width of game screen
+	 * @param gameHeight    Height of game screen
+	 * @param overlayWidth  Width of overlay
+	 * @param overlayHeight Height of overlay
+	 * @param format        Pixel format of overlay
+	 * @param markerShape   Type of marker to display
+	 * @param showText      Whether to display text labels
+	 */
+	void render(Surface *surface,
+		const Common::Array<HotspotInfo> &hotspots,
+		int gameWidth, int gameHeight,
+		int overlayWidth, int overlayHeight,
+		const PixelFormat &format,
+		MarkerShape markerShape,
+		bool showText);
+
+private:
+	enum {
+		kMarkerSize = 10,  ///< Size of square and crosshair markers
+		kGlowSize = 3,     ///< Size of glow effect around markers and text
+		kPointRadius = 3,  ///< Radius of point marker
+		kLineThickness = 1 ///< Thickness of lines for markers and text boxes
+	};
+
+	void drawMarker(Surface *surface, int x, int y, const PixelFormat &format, MarkerShape markerShape);
+	void drawCrosshairMarker(Surface *surface, int x, int y, int width, int height, const PixelFormat &format);
+	void drawSquareMarker(Surface *surface, int x, int y, int width, int height, const PixelFormat &format);
+	void drawPointMarker(Surface *surface, int x, int y, int width, int height, const PixelFormat &format);
+	void drawHotspotLabel(Surface *surface, int overlayX, int overlayY, const Common::U32String &label,
+		int overlayWidth, int overlayHeight, const PixelFormat &format, const Font *font);
+	void drawLabelBox(Surface *surface, int x, int y, int w, int h,
+		int overlayWidth, int overlayHeight, const PixelFormat &format);
+	void drawRectWithGlow(Surface *surface, int x, int y, int w, int h,
+		int overlayWidth, int overlayHeight, const PixelFormat &format);
+
+	void drawLineWithGlow(Surface *surface, int x1, int y1, int x2, int y2,
+		int width, int height, const PixelFormat &format, int lineThickness);
+
+	void blendPixelWithGlow(Surface *surface, int px, int py, const PixelFormat &format,
+		int distance, int solidSize);
+};
+
+} // End of namespace Graphics
+
+#endif // GRAPHICS_HOTSPOT_RENDERER_H
diff --git a/graphics/module.mk b/graphics/module.mk
index 04725fcaaa3..d96f720c89c 100644
--- a/graphics/module.mk
+++ b/graphics/module.mk
@@ -24,6 +24,7 @@ MODULE_OBJS := \
 	fonts/ttf.o \
 	fonts/winfont.o \
 	framelimiter.o \
+	hotspot_renderer.o \
 	image-archive.o \
 	korfont.o \
 	larryScale.o \


Commit: e2532bdc150563869e38caf3625ac8b0a2d11b53
    https://github.com/scummvm/scummvm/commit/e2532bdc150563869e38caf3625ac8b0a2d11b53
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
ENGINES: Add hotspot display infrastructure

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/engine.cpp
    engines/engine.h
    engines/metaengine.cpp


diff --git a/engines/engine.cpp b/engines/engine.cpp
index 5ab92309d10..aaa7a7870e6 100644
--- a/engines/engine.cpp
+++ b/engines/engine.cpp
@@ -60,6 +60,8 @@
 #include "graphics/fontman.h"
 #include "graphics/paletteman.h"
 #include "graphics/pixelformat.h"
+#include "graphics/font.h"
+#include "graphics/hotspot_renderer.h"
 #include "image/bmp.h"
 
 #include "common/text-to-speech.h"
@@ -154,7 +156,9 @@ Engine::Engine(OSystem *syst)
 		_mainMenuDialog(NULL),
 		_debugger(NULL),
 		_autosaveInterval(ConfMan.getInt("autosave_period")),
-		_lastAutosaveTime(_system->getMillis()) {
+		_lastAutosaveTime(_system->getMillis()),
+		_showHotspots(false),
+		_hotspotForceRedraw(false) {
 
 	g_engine = this;
 	_quitRequested = false;
@@ -742,6 +746,66 @@ void Engine::pauseEngineIntern(bool pause) {
 	_mixer->pauseAll(pause);
 }
 
+void Engine::showHotspots(bool show) {
+	_showHotspots = show;
+
+	if (show)
+		_hotspotForceRedraw = true;
+	else if (g_system->isOverlayVisible())
+		g_system->hideOverlay();
+}
+
+void Engine::getHotspotPositions(Common::Array<Graphics::HotspotInfo> &hotspots) {
+}
+
+bool Engine::hotspotDirty() const {
+	return true;
+}
+
+void Engine::drawHotspots() {
+	if (!_showHotspots)
+		return;
+
+	if (!hotspotDirty() && !_hotspotForceRedraw)
+		return;
+	_hotspotForceRedraw = false;
+
+	Common::Array<Graphics::HotspotInfo> hotspots;
+	getHotspotPositions(hotspots);
+
+	if (hotspots.empty())
+		return;
+
+	int16 gameWidth = g_system->getWidth();
+	int16 gameHeight = g_system->getHeight();
+	int16 overlayWidth = g_system->getOverlayWidth();
+	int16 overlayHeight = g_system->getOverlayHeight();
+	Graphics::PixelFormat overlayFormat = g_system->getOverlayFormat();
+
+	if (!g_system->isOverlayVisible())
+		g_system->showOverlay(false);
+
+	g_system->clearOverlay();
+
+	Graphics::Surface overlayBuffer;
+	overlayBuffer.create(overlayWidth, overlayHeight, overlayFormat);
+	g_system->grabOverlay(overlayBuffer);
+
+	bool showText = ConfMan.getBool("show_hotspot_text");
+	if (!ConfMan.hasKey("show_hotspot_text"))
+		showText = true;
+
+	int markerType = ConfMan.getInt("hotspot_marker");
+
+	Graphics::HotspotRenderer renderer;
+	renderer.render(&overlayBuffer, hotspots, gameWidth, gameHeight,
+		overlayWidth, overlayHeight, overlayFormat,
+		(Graphics::MarkerShape)markerType, showText);
+
+	g_system->copyRectToOverlay(overlayBuffer.getPixels(), overlayBuffer.pitch, 0, 0, overlayWidth, overlayHeight);
+	overlayBuffer.free();
+}
+
 void Engine::openMainMenuDialog() {
 	if (!_mainMenuDialog)
 		_mainMenuDialog = new MainMenuDialog(this);
diff --git a/engines/engine.h b/engines/engine.h
index fccc2efff5d..73508b474d7 100644
--- a/engines/engine.h
+++ b/engines/engine.h
@@ -22,7 +22,9 @@
 #ifndef ENGINES_ENGINE_H
 #define ENGINES_ENGINE_H
 
+#include "common/array.h"
 #include "common/error.h"
+#include "common/rect.h"
 #include "common/scummsys.h"
 #include "common/str.h"
 #include "common/language.h"
@@ -52,6 +54,9 @@ namespace GUI {
 class Debugger;
 class Dialog;
 }
+namespace Graphics {
+struct HotspotInfo;
+}
 
 /**
  * @defgroup engines_engine Engine
@@ -182,6 +187,18 @@ protected:
 
 	int32 _activeEnhancements = kEnhGameBreakingBugFixes;
 
+	/**
+	 * Flag for whether hotspots should be displayed
+	 */
+	bool _showHotspots;
+
+	/**
+	 * Flag to force one redraw after hotspots are shown (e.g. after button press
+	 * re-enables the overlay), so engines that cache dirty state don't skip the
+	 * first render after the overlay was hidden.
+	 */
+	bool _hotspotForceRedraw;
+
 private:
 	/**
 	 * The associated metaengine
@@ -509,11 +526,48 @@ protected:
 	 */
 	virtual void pauseEngineIntern(bool pause);
 
+	/**
+	 * Get information about all hotspots that should be displayed.
+	 *
+	 * Engines should override this method to populate the hotspots array
+	 * with hotspot information in game screen coordinates.
+	 *
+	 * @param hotspots Array to be filled with hotspot information
+	 */
+	virtual void getHotspotPositions(Common::Array<Graphics::HotspotInfo> &hotspots);
+
+	/**
+	 * Draw hotspot markers on the screen using the overlay.
+	 *
+	 * Default implementation calls getHotspotPositions() and draws semi-transparent
+	 * circles, automatically converting from game coordinates to overlay coordinates.
+	 */
+	virtual void drawHotspots();
+
+	/**
+	 * Returns whether hotspot markers need to be re-rendered this frame.
+	 *
+	 * The default implementation always returns true. Engines may override
+	 * this to return false when hotspot positions have not changed (e.g. no
+	 * object state change, no screen pan), avoiding an unnecessary overlay
+	 * redraw.
+	 *
+	 * @return true if hotspots should be re-rendered, false to skip
+	 */
+	virtual bool hotspotDirty() const;
+
 	 /** @} */
 
 
 public:
 
+	/**
+	 * Set whether hotspots should be displayed.
+	 *
+	 * @param show true to show hotspots, false to hide them
+	 */
+	virtual void showHotspots(bool show);
+
 	/**
 	 * Request the engine to quit.
 	 *
diff --git a/engines/metaengine.cpp b/engines/metaengine.cpp
index 5a2bfb0f09b..f98ae29bdcb 100644
--- a/engines/metaengine.cpp
+++ b/engines/metaengine.cpp
@@ -29,6 +29,7 @@
 #include "common/savefile.h"
 #include "common/system.h"
 #include "common/translation.h"
+#include "common/events.h"
 
 #include "engines/dialogs.h"
 
@@ -158,6 +159,11 @@ Common::KeymapArray MetaEngine::initKeymaps(const char *target) const {
 	act->allowKbdRepeats();
 	engineKeyMap->addAction(act);
 
+	act = new Action(kStandardActionToggleHotspots, _("Show hotspots"));
+	act->setCustomEngineActionEvent(Common::kEngineActionHotspotToggle);
+	act->addDefaultInputMapping("h");
+	engineKeyMap->addAction(act);
+
 	return Keymap::arrayOf(engineKeyMap);
 }
 


Commit: c8c6bcf85da1a13a52a8e12f0006b72b46307d5c
    https://github.com/scummvm/scummvm/commit/c8c6bcf85da1a13a52a8e12f0006b72b46307d5c
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
GUI: Add hotspot options to Game Options

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    gui/editgamedialog.cpp
    gui/editgamedialog.h


diff --git a/gui/editgamedialog.cpp b/gui/editgamedialog.cpp
index 79bb1d30a35..d21ccc7e184 100644
--- a/gui/editgamedialog.cpp
+++ b/gui/editgamedialog.cpp
@@ -22,6 +22,7 @@
 #include "gui/editgamedialog.h"
 
 #include "backends/keymapper/keymapper.h"
+#include "graphics/hotspot_renderer.h"
 
 #include "common/config-manager.h"
 #include "common/gui_options.h"
@@ -79,7 +80,8 @@ enum {
 	kCmdSavePathClear = 'PSAC',
 	kCmdCheckIntegrity = 'PCHI',
 
-	kGraphicsTabContainerReflowCmd = 'gtcr'
+	kGraphicsTabContainerReflowCmd = 'gtcr',
+	kEnableHotspotsCmd = 'enhs',
 };
 
 class DomainEditTextWidget : public EditTextWidget {
@@ -317,7 +319,46 @@ EditGameDialog::EditGameDialog(const Common::String &domain)
 	_savePathClearButton = addClearButton(tab, "GameOptions_Paths.SavePathClearButton", kCmdSavePathClear);
 
 	//
-	// 9) The Achievements & The Statistics tabs
+	// 9) The Misc tab
+	//
+	tab->addTab(g_gui.useLowResGUI() ? _c("Misc", "lowres") : _("Misc"), "GameOptions_Misc");
+
+	ScrollContainerWidget *miscContainer = new ScrollContainerWidget(tab,
+			"GameOptions_Misc.Container", "GameOptions_Misc_Container");
+	miscContainer->setBackgroundType(ThemeEngine::kWidgetBackgroundNo);
+	miscContainer->setTarget(this);
+
+	bool hotspotsEnabled = ConfMan.getBool("enable_hotspots", _domain);
+	_enableHotspotsCheckbox = new CheckboxWidget(miscContainer, "GameOptions_Misc_Container.EnableHotspots",
+		_("Enable hotspot display"),
+		_("Enable visual markers and labels for interactive objects"),
+		kEnableHotspotsCmd
+	);
+
+	_hotspotMarkerPopUpDesc = new StaticTextWidget(miscContainer,
+			"GameOptions_Misc_Container.HotspotMarkerPopupDesc", _("Hotspot marker:"));
+	_hotspotMarkerPopUp = new PopUpWidget(miscContainer, "GameOptions_Misc_Container.HotspotMarkerPopup");
+	_hotspotMarkerPopUp->appendEntry(_("Point"), Graphics::kMarkerPoint);
+	_hotspotMarkerPopUp->appendEntry(_("Square"), Graphics::kMarkerSquare);
+	_hotspotMarkerPopUp->appendEntry(_("Crosshair"), Graphics::kMarkerCrosshair);
+	_hotspotMarkerPopUp->setSelectedTag(ConfMan.getInt("hotspot_marker", _domain));
+	_hotspotMarkerPopUp->setEnabled(hotspotsEnabled);
+	_hotspotMarkerPopUpDesc->setEnabled(hotspotsEnabled);
+
+	bool showHotspotText = ConfMan.getBool("show_hotspot_text", _domain);
+	if (!ConfMan.hasKey("show_hotspot_text", _domain))
+		showHotspotText = true;
+	_showHotspotTextCheckbox = new CheckboxWidget(miscContainer, "GameOptions_Misc_Container.ShowHotspotText",
+		_("Show hotspot labels"),
+		_("Display text labels next to hotspot markers")
+	);
+	_showHotspotTextCheckbox->setState(showHotspotText);
+	_showHotspotTextCheckbox->setEnabled(hotspotsEnabled);
+
+	_enableHotspotsCheckbox->setState(hotspotsEnabled);
+
+	//
+	// 10) The Achievements & The Statistics tabs
 	//
 	if (enginePlugin) {
 		const MetaEngine &metaEngine = enginePlugin->get<MetaEngine>();
@@ -529,11 +570,23 @@ void EditGameDialog::apply() {
 		_engineOptions->save();
 	}
 
+	ConfMan.setBool("enable_hotspots", _enableHotspotsCheckbox->getState(), _domain);
+	ConfMan.setInt("hotspot_marker", _hotspotMarkerPopUp->getSelectedTag(), _domain);
+	ConfMan.setBool("show_hotspot_text", _showHotspotTextCheckbox->getState(), _domain);
+
 	OptionsDialog::apply();
 }
 
 void EditGameDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
 	switch (cmd) {
+	case kEnableHotspotsCmd: {
+		bool enabled = _enableHotspotsCheckbox->getState();
+		_hotspotMarkerPopUpDesc->setEnabled(enabled);
+		_hotspotMarkerPopUp->setEnabled(enabled);
+		_showHotspotTextCheckbox->setEnabled(enabled);
+		g_gui.scheduleTopDialogRedraw();
+		break;
+	}
 	case kCmdGlobalGraphicsOverride:
 		setGraphicSettingsState(data != 0);
 		g_gui.scheduleTopDialogRedraw();
diff --git a/gui/editgamedialog.h b/gui/editgamedialog.h
index 0dc95c8a838..aec6ba4ddd0 100644
--- a/gui/editgamedialog.h
+++ b/gui/editgamedialog.h
@@ -91,6 +91,11 @@ protected:
 	CheckboxWidget *_globalMT32Override;
 	CheckboxWidget *_globalVolumeOverride;
 
+	CheckboxWidget *_enableHotspotsCheckbox;
+	StaticTextWidget *_hotspotMarkerPopUpDesc;
+	PopUpWidget *_hotspotMarkerPopUp;
+	CheckboxWidget *_showHotspotTextCheckbox;
+
 	ScrollContainerWidget *_gameContainer;
 	OptionsContainerWidget *_engineOptions;
 };


Commit: 496a9a65f5c896736044f900538b736d9696242c
    https://github.com/scummvm/scummvm/commit/496a9a65f5c896736044f900538b736d9696242c
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
GUI: Add hotspot theme layout and regenerate themes

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    gui/ThemeEngine.h
    gui/themes/common/highres_layout.stx
    gui/themes/common/lowres_layout.stx
    gui/themes/default.inc
    gui/themes/residualvm.zip
    gui/themes/residualvm/THEMERC
    gui/themes/scummclassic.zip
    gui/themes/scummclassic/THEMERC
    gui/themes/scummclassic/classic_layout.stx
    gui/themes/scummclassic/classic_layout_lowres.stx
    gui/themes/scummmodern.zip
    gui/themes/scummmodern/THEMERC
    gui/themes/scummremastered.zip
    gui/themes/scummremastered/THEMERC


diff --git a/gui/ThemeEngine.h b/gui/ThemeEngine.h
index 48d47c5d176..75a61fd1f4a 100644
--- a/gui/ThemeEngine.h
+++ b/gui/ThemeEngine.h
@@ -36,7 +36,7 @@
 #include "graphics/pixelformat.h"
 
 
-#define SCUMMVM_THEME_VERSION_STR "SCUMMVM_STX0.9.23"
+#define SCUMMVM_THEME_VERSION_STR "SCUMMVM_STX0.9.24"
 
 class OSystem;
 
diff --git a/gui/themes/common/highres_layout.stx b/gui/themes/common/highres_layout.stx
index c8f8d872fd2..96623302e63 100644
--- a/gui/themes/common/highres_layout.stx
+++ b/gui/themes/common/highres_layout.stx
@@ -1876,6 +1876,37 @@
 		</layout>
 	</dialog>
 
+	<dialog name = 'GameOptions_Misc' overlays = 'Dialog.GameOptions.TabWidget'>
+		<layout type = 'vertical' padding = '0, 0, 0, 0'>
+			<widget name = 'Container'
+					type = 'ScrollContainerWidget'
+			/>
+		</layout>
+	</dialog>
+
+	<dialog name = 'GameOptions_Misc_Container' overlays = 'GameOptions_Misc.Container' shading = 'dim'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'EnableHotspots'
+						type = 'Checkbox'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'HotspotMarkerPopupDesc'
+						type = 'OptionsLabel'
+				/>
+				<widget name = 'HotspotMarkerPopup'
+						type = 'PopUp'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'ShowHotspotText'
+						type = 'Checkbox'
+				/>
+			</layout>
+		</layout>
+	</dialog>
+
 	<dialog name = 'GlobalMenu' overlays = 'screen_center'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' align = 'center'>
 			<widget name = 'Logo'
diff --git a/gui/themes/common/lowres_layout.stx b/gui/themes/common/lowres_layout.stx
index ae6311cebe3..1ec690bcca1 100644
--- a/gui/themes/common/lowres_layout.stx
+++ b/gui/themes/common/lowres_layout.stx
@@ -1703,6 +1703,37 @@
 		</layout>
 	</dialog>
 
+	<dialog name = 'GameOptions_Misc' overlays = 'Dialog.GameOptions.TabWidget'>
+		<layout type = 'vertical' padding = '0, 0, 0, 0'>
+			<widget name = 'Container'
+					type = 'ScrollContainerWidget'
+			/>
+		</layout>
+	</dialog>
+
+	<dialog name = 'GameOptions_Misc_Container' overlays = 'GameOptions_Misc.Container' shading = 'dim'>
+		<layout type = 'vertical' padding = '8, 8, 8, 8' spacing = '6'>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'EnableHotspots'
+						type = 'Checkbox'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'HotspotMarkerPopupDesc'
+						type = 'OptionsLabel'
+				/>
+				<widget name = 'HotspotMarkerPopup'
+						type = 'PopUp'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'ShowHotspotText'
+						type = 'Checkbox'
+				/>
+			</layout>
+		</layout>
+	</dialog>
+
 	<dialog name = 'GlobalMenu' overlays = 'screen_center'>
 		<layout type = 'vertical' padding = '4, 4, 4, 4' align = 'center' spacing='2'>
 			<widget name = 'Title'
diff --git a/gui/themes/default.inc b/gui/themes/default.inc
index 49ef03e6c50..2cbdbe96dd8 100644
--- a/gui/themes/default.inc
+++ b/gui/themes/default.inc
@@ -2842,6 +2842,35 @@ const char *defaultXML1 = "<?xml version = '1.0'?>"
 "/>"
 "</layout>"
 "</dialog>"
+"<dialog name='GameOptions_Misc' overlays='Dialog.GameOptions.TabWidget'>"
+"<layout type='vertical' padding='0,0,0,0'>"
+"<widget name='Container' "
+"type='ScrollContainerWidget' "
+"/>"
+"</layout>"
+"</dialog>"
+"<dialog name='GameOptions_Misc_Container' overlays='GameOptions_Misc.Container' shading='dim'>"
+"<layout type='vertical' padding='16,16,16,16' spacing='8'>"
+"<layout type='horizontal' padding='0,0,0,0' spacing='10' align='center'>"
+"<widget name='EnableHotspots' "
+"type='Checkbox' "
+"/>"
+"</layout>"
+"<layout type='horizontal' padding='0,0,0,0' spacing='10' align='center'>"
+"<widget name='HotspotMarkerPopupDesc' "
+"type='OptionsLabel' "
+"/>"
+"<widget name='HotspotMarkerPopup' "
+"type='PopUp' "
+"/>"
+"</layout>"
+"<layout type='horizontal' padding='0,0,0,0' spacing='10' align='center'>"
+"<widget name='ShowHotspotText' "
+"type='Checkbox' "
+"/>"
+"</layout>"
+"</layout>"
+"</dialog>"
 "<dialog name='GlobalMenu' overlays='screen_center'>"
 "<layout type='vertical' padding='16,16,16,16' align='center'>"
 "<widget name='Title' "
@@ -5321,6 +5350,35 @@ const char *defaultXML1 = "<?xml version = '1.0'?>"
 "/>"
 "</layout>"
 "</dialog>"
+"<dialog name='GameOptions_Misc' overlays='Dialog.GameOptions.TabWidget'>"
+"<layout type='vertical' padding='0,0,0,0'>"
+"<widget name='Container' "
+"type='ScrollContainerWidget' "
+"/>"
+"</layout>"
+"</dialog>"
+"<dialog name='GameOptions_Misc_Container' overlays='GameOptions_Misc.Container' shading='dim'>"
+"<layout type='vertical' padding='8,8,8,8' spacing='6'>"
+"<layout type='horizontal' padding='0,0,0,0' spacing='10' align='center'>"
+"<widget name='EnableHotspots' "
+"type='Checkbox' "
+"/>"
+"</layout>"
+"<layout type='horizontal' padding='0,0,0,0' spacing='10' align='center'>"
+"<widget name='HotspotMarkerPopupDesc' "
+"type='OptionsLabel' "
+"/>"
+"<widget name='HotspotMarkerPopup' "
+"type='PopUp' "
+"/>"
+"</layout>"
+"<layout type='horizontal' padding='0,0,0,0' spacing='10' align='center'>"
+"<widget name='ShowHotspotText' "
+"type='Checkbox' "
+"/>"
+"</layout>"
+"</layout>"
+"</dialog>"
 "<dialog name='GlobalMenu' overlays='screen_center'>"
 "<layout type='vertical' padding='2,2,2,6' align='center' spacing='0'>"
 "<widget name='Title' "
diff --git a/gui/themes/residualvm.zip b/gui/themes/residualvm.zip
index 0d57f6e477a..40cb2b5bb42 100644
Binary files a/gui/themes/residualvm.zip and b/gui/themes/residualvm.zip differ
diff --git a/gui/themes/residualvm/THEMERC b/gui/themes/residualvm/THEMERC
index 442f37e083e..e3792fee684 100644
--- a/gui/themes/residualvm/THEMERC
+++ b/gui/themes/residualvm/THEMERC
@@ -1,3 +1,3 @@
-[SCUMMVM_STX0.9.23:ResidualVM Modern Theme Remastered:No Author]
+[SCUMMVM_STX0.9.24:ResidualVM Modern Theme Remastered:No Author]
 %using ../common
 %using ../common-svg
diff --git a/gui/themes/scummclassic.zip b/gui/themes/scummclassic.zip
index ca2607b382f..a539796554c 100644
Binary files a/gui/themes/scummclassic.zip and b/gui/themes/scummclassic.zip differ
diff --git a/gui/themes/scummclassic/THEMERC b/gui/themes/scummclassic/THEMERC
index d3a8ae3af49..09accb5ad96 100644
--- a/gui/themes/scummclassic/THEMERC
+++ b/gui/themes/scummclassic/THEMERC
@@ -1 +1 @@
-[SCUMMVM_STX0.9.23:ScummVM Classic Theme:No Author]
+[SCUMMVM_STX0.9.24:ScummVM Classic Theme:No Author]
diff --git a/gui/themes/scummclassic/classic_layout.stx b/gui/themes/scummclassic/classic_layout.stx
index ceafccb9459..984c467ae4e 100644
--- a/gui/themes/scummclassic/classic_layout.stx
+++ b/gui/themes/scummclassic/classic_layout.stx
@@ -1511,6 +1511,37 @@
 		</layout>
 	</dialog>
 
+	<dialog name = 'GameOptions_Misc' overlays = 'Dialog.GameOptions.TabWidget'>
+		<layout type = 'vertical' padding = '0, 0, 0, 0'>
+			<widget name = 'Container'
+					type = 'ScrollContainerWidget'
+			/>
+		</layout>
+	</dialog>
+
+	<dialog name = 'GameOptions_Misc_Container' overlays = 'GameOptions_Misc.Container' shading = 'dim'>
+		<layout type = 'vertical' padding = '16, 16, 16, 16' spacing = '8'>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'EnableHotspots'
+						type = 'Checkbox'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'HotspotMarkerPopupDesc'
+						type = 'OptionsLabel'
+				/>
+				<widget name = 'HotspotMarkerPopup'
+						type = 'PopUp'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'ShowHotspotText'
+						type = 'Checkbox'
+				/>
+			</layout>
+		</layout>
+	</dialog>
+
 	<dialog name = 'GlobalMenu' overlays = 'screen_center'>
 		<layout type = 'vertical' padding = '16, 16, 16, 16' align = 'center'>
 			<widget name = 'Title'
diff --git a/gui/themes/scummclassic/classic_layout_lowres.stx b/gui/themes/scummclassic/classic_layout_lowres.stx
index 0fe3d149386..4f7b03026c4 100644
--- a/gui/themes/scummclassic/classic_layout_lowres.stx
+++ b/gui/themes/scummclassic/classic_layout_lowres.stx
@@ -1532,6 +1532,37 @@
 		</layout>
 	</dialog>
 
+	<dialog name = 'GameOptions_Misc' overlays = 'Dialog.GameOptions.TabWidget'>
+		<layout type = 'vertical' padding = '0, 0, 0, 0'>
+			<widget name = 'Container'
+					type = 'ScrollContainerWidget'
+			/>
+		</layout>
+	</dialog>
+
+	<dialog name = 'GameOptions_Misc_Container' overlays = 'GameOptions_Misc.Container' shading = 'dim'>
+		<layout type = 'vertical' padding = '8, 8, 8, 8' spacing = '6'>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'EnableHotspots'
+						type = 'Checkbox'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'HotspotMarkerPopupDesc'
+						type = 'OptionsLabel'
+				/>
+				<widget name = 'HotspotMarkerPopup'
+						type = 'PopUp'
+				/>
+			</layout>
+			<layout type = 'horizontal' padding = '0, 0, 0, 0' spacing = '10' align = 'center'>
+				<widget name = 'ShowHotspotText'
+						type = 'Checkbox'
+				/>
+			</layout>
+		</layout>
+	</dialog>
+
 	<dialog name = 'GlobalMenu' overlays = 'screen_center'>
 		<layout type = 'vertical' padding = '2, 2, 2, 6' align = 'center' spacing='0'>
 			<widget name = 'Title'
diff --git a/gui/themes/scummmodern.zip b/gui/themes/scummmodern.zip
index 756addce923..ec6dcb3cc71 100644
Binary files a/gui/themes/scummmodern.zip and b/gui/themes/scummmodern.zip differ
diff --git a/gui/themes/scummmodern/THEMERC b/gui/themes/scummmodern/THEMERC
index f56e57a3da9..514707afb8d 100644
--- a/gui/themes/scummmodern/THEMERC
+++ b/gui/themes/scummmodern/THEMERC
@@ -1,2 +1,2 @@
-[SCUMMVM_STX0.9.23:ScummVM Modern Theme:No Author]
+[SCUMMVM_STX0.9.24:ScummVM Modern Theme:No Author]
 %using ../common
diff --git a/gui/themes/scummremastered.zip b/gui/themes/scummremastered.zip
index 6abf823e5d5..e7352e4e615 100644
Binary files a/gui/themes/scummremastered.zip and b/gui/themes/scummremastered.zip differ
diff --git a/gui/themes/scummremastered/THEMERC b/gui/themes/scummremastered/THEMERC
index 2fffe8f54ca..6ce0aa09246 100644
--- a/gui/themes/scummremastered/THEMERC
+++ b/gui/themes/scummremastered/THEMERC
@@ -1,3 +1,3 @@
-[SCUMMVM_STX0.9.23:ScummVM Modern Theme Remastered:No Author]
+[SCUMMVM_STX0.9.24:ScummVM Modern Theme Remastered:No Author]
 %using ../common
 %using ../common-svg


Commit: 62a9809eadc27d97cdf86f047e3a792fa433a8da
    https://github.com/scummvm/scummvm/commit/62a9809eadc27d97cdf86f047e3a792fa433a8da
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
TEST: Add hotspot unit tests

Assisted-by: Claude:claude-opus-4.8

Changed paths:
  A test/common/events.h
  A test/graphics/hotspot_info.h


diff --git a/test/common/events.h b/test/common/events.h
new file mode 100644
index 00000000000..183ecf186e4
--- /dev/null
+++ b/test/common/events.h
@@ -0,0 +1,48 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <cxxtest/TestSuite.h>
+#include "common/events.h"
+
+class EventsTestSuite : public CxxTest::TestSuite {
+public:
+	void test_engine_action_hotspot_toggle() {
+		TS_ASSERT_EQUALS(Common::kEngineActionHotspotToggle, 20000);
+	}
+
+	void test_custom_engine_action_event() {
+		Common::Event event;
+		event.type = Common::EVENT_CUSTOM_ENGINE_ACTION_START;
+		event.customType = Common::kEngineActionHotspotToggle;
+
+		TS_ASSERT_EQUALS(event.type, Common::EVENT_CUSTOM_ENGINE_ACTION_START);
+		TS_ASSERT_EQUALS(event.customType, Common::kEngineActionHotspotToggle);
+	}
+
+	void test_custom_engine_action_end_event() {
+		Common::Event event;
+		event.type = Common::EVENT_CUSTOM_ENGINE_ACTION_END;
+		event.customType = Common::kEngineActionHotspotToggle;
+
+		TS_ASSERT_EQUALS(event.type, Common::EVENT_CUSTOM_ENGINE_ACTION_END);
+		TS_ASSERT_EQUALS(event.customType, Common::kEngineActionHotspotToggle);
+	}
+};
diff --git a/test/graphics/hotspot_info.h b/test/graphics/hotspot_info.h
new file mode 100644
index 00000000000..d71734244e2
--- /dev/null
+++ b/test/graphics/hotspot_info.h
@@ -0,0 +1,67 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <cxxtest/TestSuite.h>
+#include "graphics/hotspot_renderer.h"
+
+class HotspotInfoTestSuite : public CxxTest::TestSuite {
+public:
+	void test_constructor_with_values() {
+		Graphics::HotspotInfo info(Common::Point(10, 20), "Test");
+		TS_ASSERT_EQUALS(info.position.x, 10);
+		TS_ASSERT_EQUALS(info.position.y, 20);
+		TS_ASSERT_EQUALS(info.name, "Test");
+	}
+
+	void test_default_constructor() {
+		Graphics::HotspotInfo info;
+		TS_ASSERT_EQUALS(info.position.x, 0);
+		TS_ASSERT_EQUALS(info.position.y, 0);
+		TS_ASSERT(info.name.empty());
+	}
+
+	void test_empty_name() {
+		Graphics::HotspotInfo info(Common::Point(100, 200), "");
+		TS_ASSERT_EQUALS(info.position.x, 100);
+		TS_ASSERT_EQUALS(info.position.y, 200);
+		TS_ASSERT(info.name.empty());
+	}
+
+	void test_negative_coordinates() {
+		Graphics::HotspotInfo info(Common::Point(-5, -10), "Negative");
+		TS_ASSERT_EQUALS(info.position.x, -5);
+		TS_ASSERT_EQUALS(info.position.y, -10);
+		TS_ASSERT_EQUALS(info.name, "Negative");
+	}
+
+	void test_default_type() {
+		Graphics::HotspotInfo info(Common::Point(0, 0), "");
+		TS_ASSERT_EQUALS(info.type, Graphics::kHotspotDefault);
+	}
+
+	void test_explicit_type() {
+		Graphics::HotspotInfo npc(Common::Point(10, 20), "Guard", Graphics::kHotspotNPC);
+		TS_ASSERT_EQUALS(npc.type, Graphics::kHotspotNPC);
+
+		Graphics::HotspotInfo obj(Common::Point(30, 40), "Door", Graphics::kHotspotExit);
+		TS_ASSERT_EQUALS(obj.type, Graphics::kHotspotExit);
+	}
+};


Commit: 0ac8afb5b57d58010914c3f8c03b3bb9780024d2
    https://github.com/scummvm/scummvm/commit/0ac8afb5b57d58010914c3f8c03b3bb9780024d2
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
DOCS: Document hotspot display feature

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    doc/docportal/settings/keymaps.rst
    doc/docportal/settings/misc.rst


diff --git a/doc/docportal/settings/keymaps.rst b/doc/docportal/settings/keymaps.rst
index f116e376e47..625b5f633d6 100644
--- a/doc/docportal/settings/keymaps.rst
+++ b/doc/docportal/settings/keymaps.rst
@@ -248,3 +248,8 @@ Left
 
 Right
 	*keymap_engine-default_RIGHT*
+
+.. _hotspots:
+
+Show hotspots
+	*keymap_engine-default_HOTS*
diff --git a/doc/docportal/settings/misc.rst b/doc/docportal/settings/misc.rst
index c3a6b88956f..f6d67f72a50 100644
--- a/doc/docportal/settings/misc.rst
+++ b/doc/docportal/settings/misc.rst
@@ -24,10 +24,31 @@ Autosave
 .. _seed:
 
 Random seed
-	Every time you start ScummVM, a random seed is generated. Use this option to set a fixed seed, which ensures that random events in games play out the same way every time. 
+	Every time you start ScummVM, a random seed is generated. Use this option to set a fixed seed, which ensures that random events in games play out the same way every time.
 
 	*random_seed*
 
+.. _enablehotspots:
+
+Enable hotspot display
+	Enables visual markers and labels for interactive objects in supported games. When enabled, pressing the hotspot key (default 'h') displays markers at clickable locations.
+
+	*enable_hotspots*
+
+.. _hotspotmarker:
+
+Hotspot marker
+	Selects the visual style for hotspot markers. Options include Point (circular marker), Square (rectangular outline), or Crosshair (crosshair symbol).
+
+	*hotspot_marker*
+
+.. _showhotspottext:
+
+Show hotspot labels
+	When enabled, displays text labels next to hotspot markers showing the name of the interactive object.
+
+	*show_hotspot_text*
+
 .. _discord:
 
 Enable Discord Integration


Commit: 258a839452458900375cfc636f811f62d3966362
    https://github.com/scummvm/scummvm/commit/258a839452458900375cfc636f811f62d3966362
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
TOUCHE: Add hotspot display support

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/touche/metaengine.cpp
    engines/touche/touche.cpp
    engines/touche/touche.h


diff --git a/engines/touche/metaengine.cpp b/engines/touche/metaengine.cpp
index 9cb6c836863..0c0b0099db3 100644
--- a/engines/touche/metaengine.cpp
+++ b/engines/touche/metaengine.cpp
@@ -24,6 +24,7 @@
 #include "common/savefile.h"
 #include "common/system.h"
 #include "common/translation.h"
+#include "common/events.h"
 
 #include "backends/keymapper/action.h"
 #include "backends/keymapper/keymapper.h"
@@ -131,6 +132,11 @@ Common::KeymapArray ToucheMetaEngine::initKeymaps(const char *target) const {
 	act->addDefaultInputMapping("JOY_B");
 	engineKeyMap->addAction(act);
 
+	act = new Action(kStandardActionToggleHotspots, _("Show hotspots"));
+	act->setCustomEngineActionEvent(Common::kEngineActionHotspotToggle);
+	act->addDefaultInputMapping("h");
+	gameKeyMap->addAction(act);
+
 	{
 		act = new Action("SKIPORQUIT", _("Skip sequence / open quit dialog"));
 		act->setCustomEngineActionEvent(kToucheActionSkipOrQuit);
diff --git a/engines/touche/touche.cpp b/engines/touche/touche.cpp
index 31f1f7b9835..62ee0b42d0a 100644
--- a/engines/touche/touche.cpp
+++ b/engines/touche/touche.cpp
@@ -36,6 +36,7 @@
 
 #include "engines/util.h"
 #include "graphics/cursorman.h"
+#include "graphics/hotspot_renderer.h"
 #include "graphics/paletteman.h"
 #include "gui/debugger.h"
 
@@ -180,6 +181,7 @@ ToucheEngine::ToucheEngine(OSystem *system, Common::Language language)
 	_inventoryVar2 = nullptr;
 	_currentCursorObject = 0;
 	_talkTextMode = 0;
+	_hotspotSnapshot.currentEpisodeNum = -1;
 }
 
 ToucheEngine::~ToucheEngine() {
@@ -517,6 +519,7 @@ void ToucheEngine::runCycle() {
 	processAnimationTable();
 	updateKeyCharTalk(0);
 	updateDirtyScreenAreas();
+	drawHotspots();
 	++_flagsTable[295];
 	++_flagsTable[296];
 	++_flagsTable[297];
@@ -1505,6 +1508,145 @@ void ToucheEngine::drawHitBoxes() {
 	}
 }
 
+void ToucheEngine::getHotspotPositions(Common::Array< ::Graphics::HotspotInfo> &hotspots) {
+	if (_flagsTable[618] != 0)
+		return;
+
+	int scrollX = _flagsTable[614];
+	int scrollY = _flagsTable[615];
+
+	for (uint i = 0; i < _programHitBoxTable.size(); ++i) {
+		const ProgramHitBoxData &hitBox = _programHitBoxTable[i];
+
+		if (hitBox.item & 0x1000)
+			break;
+
+		if (hitBox.item & 0x2000)
+			continue;
+
+		if (hitBox.hitBoxes[0].top & 0x4000)
+			continue;
+
+		int16 str = hitBox.str;
+		int screenX, screenY;
+		::Graphics::HotspotType hotspotType;
+
+		if (hitBox.item & 0x4000) {
+			const KeyChar *keyChar = &_keyCharsTable[hitBox.item & ~0x4000];
+			if (keyChar->num == 0 || (keyChar->flags & 0x4000))
+				continue;
+			if (keyChar->strNum != 0)
+				str = hitBox.defaultStr;
+
+			const Common::Rect &charRect = keyChar->prevBoundingRect;
+			if (!charRect.isValidRect())
+				continue;
+
+			screenX = (charRect.left + charRect.right) / 2;
+			screenY = (charRect.top + charRect.bottom) / 2;
+			hotspotType = ::Graphics::kHotspotNPC;
+		} else {
+			const Common::Rect &objRect = hitBox.hitBoxes[0];
+			if (!objRect.isValidRect())
+				continue;
+
+			int centerX = (objRect.left + objRect.right) / 2;
+			int centerY = (objRect.top + objRect.bottom) / 2;
+
+			screenX = centerX - scrollX;
+			screenY = centerY - scrollY;
+			hotspotType = ::Graphics::kHotspotObject;
+		}
+
+		if (screenX < 0 || screenX >= kScreenWidth || screenY < 0 || screenY >= kRoomHeight)
+			continue;
+
+		Common::String name;
+		if (str > 0) {
+			const char *strData = getString(str);
+			if (strData)
+				name = Common::String(strData);
+		}
+
+		hotspots.push_back(::Graphics::HotspotInfo(Common::Point(screenX, screenY), name, hotspotType));
+	}
+}
+
+void ToucheEngine::rebuildHotspotSnapshot() const {
+	_hotspotSnapshot.flag618 = _flagsTable[618];
+	_hotspotSnapshot.scrollX = _flagsTable[614];
+	_hotspotSnapshot.scrollY = _flagsTable[615];
+	_hotspotSnapshot.currentEpisodeNum = _currentEpisodeNum;
+	_hotspotSnapshot.hitBoxes.clear();
+	for (uint i = 0; i < _programHitBoxTable.size(); ++i) {
+		const ProgramHitBoxData &hb = _programHitBoxTable[i];
+		HotspotSnapshot::HitBoxEntry e;
+		e.item = hb.item;
+		e.lockedTop = hb.hitBoxes[0].top;
+		e.str = hb.str;
+		e.defaultStr = hb.defaultStr;
+		e.hitBox0 = hb.hitBoxes[0];
+		_hotspotSnapshot.hitBoxes.push_back(e);
+	}
+	for (int i = 0; i < NUM_KEYCHARS; ++i) {
+		_hotspotSnapshot.chars[i].num = _keyCharsTable[i].num;
+		_hotspotSnapshot.chars[i].flags = _keyCharsTable[i].flags;
+		_hotspotSnapshot.chars[i].prevBoundingRect = _keyCharsTable[i].prevBoundingRect;
+	}
+}
+
+bool ToucheEngine::hotspotDirty() const {
+	const int16 flag618 = _flagsTable[618];
+	const int16 scrollX = _flagsTable[614];
+	const int16 scrollY = _flagsTable[615];
+
+	// Fast path: any scalar change forces a snapshot rebuild.
+	if (flag618 != _hotspotSnapshot.flag618 ||
+		scrollX != _hotspotSnapshot.scrollX ||
+		scrollY != _hotspotSnapshot.scrollY ||
+		_currentEpisodeNum != _hotspotSnapshot.currentEpisodeNum ||
+		_programHitBoxTable.size() != _hotspotSnapshot.hitBoxes.size()) {
+		rebuildHotspotSnapshot();
+		return true;
+	}
+
+	// When globally suppressed there are no hotspots to redraw.
+	if (flag618 != 0)
+		return false;
+
+	// Per-hitbox and per-character checks.
+	for (uint i = 0; i < _programHitBoxTable.size(); ++i) {
+		const ProgramHitBoxData &hb = _programHitBoxTable[i];
+		const HotspotSnapshot::HitBoxEntry &snap = _hotspotSnapshot.hitBoxes[i];
+
+		if (hb.item != snap.item ||
+			hb.hitBoxes[0].top != snap.lockedTop ||
+			hb.str != snap.str ||
+			hb.defaultStr != snap.defaultStr ||
+			hb.hitBoxes[0] != snap.hitBox0) {
+			rebuildHotspotSnapshot();
+			return true;
+		}
+
+		if (hb.item & 0x4000) {
+			const int charIdx = hb.item & ~0x4000;
+			const KeyChar &kc = _keyCharsTable[charIdx];
+			const HotspotSnapshot::CharEntry &cs = _hotspotSnapshot.chars[charIdx];
+			if (kc.num != cs.num ||
+				kc.flags != cs.flags ||
+				kc.prevBoundingRect != cs.prevBoundingRect) {
+				rebuildHotspotSnapshot();
+				return true;
+			}
+		}
+
+		if (hb.item & 0x1000)
+			break;
+	}
+
+	return false;
+}
+
 void ToucheEngine::showCursor(bool show) {
 	debugC(9, kDebugEngine, "ToucheEngine::showCursor()");
 	CursorMan.showMouse(show);
diff --git a/engines/touche/touche.h b/engines/touche/touche.h
index 927b45a4cf0..c11675756d1 100644
--- a/engines/touche/touche.h
+++ b/engines/touche/touche.h
@@ -637,6 +637,10 @@ protected:
 		return Common::String::format("%s.%d", _targetName.c_str(), slot);
 	}
 
+	void getHotspotPositions(Common::Array< ::Graphics::HotspotInfo> &hotspots) override;
+	bool hotspotDirty() const override;
+	void rebuildHotspotSnapshot() const;
+
 	void setupOpcodes();
 	void op_nop();
 	void op_jnz();
@@ -911,6 +915,28 @@ protected:
 	Common::Rect _dirtyRectsTable[NUM_DIRTY_RECTS];
 	int _dirtyRectsTableCount;
 
+	struct HotspotSnapshot {
+		struct HitBoxEntry {
+			int16 item;
+			int16 lockedTop;
+			int16 str;
+			int16 defaultStr;
+			Common::Rect hitBox0;
+		};
+		struct CharEntry {
+			uint16 num;
+			uint16 flags;
+			Common::Rect prevBoundingRect;
+		};
+		int16 flag618;
+		int16 scrollX;
+		int16 scrollY;
+		int currentEpisodeNum;
+		Common::Array<HitBoxEntry> hitBoxes;
+		CharEntry chars[NUM_KEYCHARS];
+	};
+	mutable HotspotSnapshot _hotspotSnapshot;
+
 	static const uint8 _directionsTable[NUM_DIRECTIONS];
 };
 


Commit: 0379edd7713a7e47345a0f6de1530cc630e900e8
    https://github.com/scummvm/scummvm/commit/0379edd7713a7e47345a0f6de1530cc630e900e8
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
GRAPHICS: Scale hotspot overlay and support 32-bit backends

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    graphics/hotspot_renderer.cpp
    graphics/hotspot_renderer.h


diff --git a/graphics/hotspot_renderer.cpp b/graphics/hotspot_renderer.cpp
index b8d42a16cb5..a262d6a9780 100644
--- a/graphics/hotspot_renderer.cpp
+++ b/graphics/hotspot_renderer.cpp
@@ -28,7 +28,9 @@
 
 namespace Graphics {
 
-HotspotRenderer::HotspotRenderer() {
+HotspotRenderer::HotspotRenderer() :
+		_sizeScale(1.0f), _markerSize(kBaseMarkerSize), _glowSize(kBaseGlowSize),
+		_pointRadius(kBasePointRadius), _lineThickness(kBaseLineThickness) {
 }
 
 HotspotRenderer::~HotspotRenderer() {
@@ -44,14 +46,23 @@ void HotspotRenderer::render(Surface *surface,
 	if (!surface || hotspots.empty())
 		return;
 
+	if (format.bytesPerPixel < 2)
+		return;
+
+	float scaleX = (float)overlayWidth / (float)gameWidth;
+	float scaleY = (float)overlayHeight / (float)gameHeight;
+
+	_sizeScale = scaleX;
+	_markerSize = MAX(2, (int)(kBaseMarkerSize * _sizeScale));
+	_glowSize = MAX(1, (int)(kBaseGlowSize * _sizeScale));
+	_pointRadius = MAX(1, (int)(kBasePointRadius * _sizeScale));
+	_lineThickness = MAX(1, (int)(kBaseLineThickness * _sizeScale));
+
 	const Font *font = nullptr;
 	if (showText) {
 		font = FontMan.getFontByUsage(FontManager::kGUIFont);
 	}
 
-	float scaleX = (float)overlayWidth / (float)gameWidth;
-	float scaleY = (float)overlayHeight / (float)gameHeight;
-
 	for (uint i = 0; i < hotspots.size(); i++) {
 		int gameX = hotspots[i].position.x;
 		int gameY = hotspots[i].position.y;
@@ -88,54 +99,79 @@ void HotspotRenderer::drawMarker(Surface *surface, int x, int y, const PixelForm
 void HotspotRenderer::drawHotspotLabel(Surface *surface, int overlayX, int overlayY,
 		const Common::U32String &label, int overlayWidth, int overlayHeight,
 		const PixelFormat &format, const Font *font) {
-	int textX = overlayX + kMarkerSize / 2 + 8;
-	int textY = overlayY - font->getFontHeight() / 2;
+	int baseTextWidth = font->getStringWidth(label);
+	int baseTextHeight = font->getFontHeight();
+	if (baseTextWidth <= 0 || baseTextHeight <= 0)
+		return;
 
-	int textWidth = font->getStringWidth(label);
-	int textHeight = font->getFontHeight();
+	int textWidth = MAX(1, (int)(baseTextWidth * _sizeScale));
+	int textHeight = MAX(1, (int)(baseTextHeight * _sizeScale));
 
-	int bgX = textX - 4;
-	int bgY = textY - 2;
-	int bgW = textWidth + 8;
-	int bgH = textHeight + 4;
+	int gap = MAX(1, (int)(8 * _sizeScale));
+	int textX = overlayX + _markerSize / 2 + gap;
+	int textY = overlayY - textHeight / 2;
+
+	int padX = MAX(1, (int)(4 * _sizeScale));
+	int padY = MAX(1, (int)(2 * _sizeScale));
+	int bgX = textX - padX;
+	int bgY = textY - padY;
+	int bgW = textWidth + padX * 2;
+	int bgH = textHeight + padY * 2;
 
 	drawLabelBox(surface, bgX, bgY, bgW, bgH,
 		overlayWidth, overlayHeight, format);
 
+	Surface textSurface;
+	textSurface.create(baseTextWidth, baseTextHeight, format);
+	textSurface.fillRect(Common::Rect(0, 0, baseTextWidth, baseTextHeight), 0);
+
 	uint32 textColor = format.RGBToColor(255, 255, 255);
-	font->drawString(surface, label, textX, textY,
-		overlayWidth - textX, textColor, kTextAlignLeft);
+	font->drawString(&textSurface, label, 0, 0, baseTextWidth, textColor, kTextAlignLeft);
+
+	Surface *scaledText = textSurface.scale((int16)textWidth, (int16)textHeight, false);
+	textSurface.free();
+	if (!scaledText)
+		return;
+
+	for (int sy = 0; sy < textHeight; sy++) {
+		int py = textY + sy;
+		if (py < 0 || py >= overlayHeight)
+			continue;
+		for (int sx = 0; sx < textWidth; sx++) {
+			int px = textX + sx;
+			if (px < 0 || px >= overlayWidth)
+				continue;
+			uint32 pixel = scaledText->getPixel(sx, sy);
+			if (pixel == 0)
+				continue;
+			surface->setPixel(px, py, pixel);
+		}
+	}
+
+	scaledText->free();
+	delete scaledText;
 }
 
 void HotspotRenderer::drawCrosshairMarker(Surface *surface, int x, int y,
 		int width, int height, const PixelFormat &format) {
-	if (format.bytesPerPixel != 2)
-		return;
+	const int lineLength = (_markerSize / 2) - 1;
 
-	const int lineLength = (kMarkerSize / 2) - 1;
-
-	drawLineWithGlow(surface, x - lineLength, y, x + lineLength, y, width, height, format, kLineThickness);
-	drawLineWithGlow(surface, x, y - lineLength, x, y + lineLength, width, height, format, kLineThickness);
+	drawLineWithGlow(surface, x - lineLength, y, x + lineLength, y, width, height, format, _lineThickness);
+	drawLineWithGlow(surface, x, y - lineLength, x, y + lineLength, width, height, format, _lineThickness);
 }
 
 void HotspotRenderer::drawSquareMarker(Surface *surface, int x, int y,
 		int width, int height, const PixelFormat &format) {
-	if (format.bytesPerPixel != 2)
-		return;
-
-	int size = kMarkerSize;
-	int rectX = x - kMarkerSize / 2;
-	int rectY = y - kMarkerSize / 2;
+	int size = _markerSize;
+	int rectX = x - _markerSize / 2;
+	int rectY = y - _markerSize / 2;
 
 	drawRectWithGlow(surface, rectX, rectY, size, size, width, height, format);
 }
 
 void HotspotRenderer::drawPointMarker(Surface *surface, int x, int y,
 		int width, int height, const PixelFormat &format) {
-	if (format.bytesPerPixel != 2)
-		return;
-
-	int maxRadius = kPointRadius + kGlowSize;
+	int maxRadius = _pointRadius + _glowSize;
 
 	for (int dy = -maxRadius; dy <= maxRadius; dy++) {
 		for (int dx = -maxRadius; dx <= maxRadius; dx++) {
@@ -148,16 +184,15 @@ void HotspotRenderer::drawPointMarker(Surface *surface, int x, int y,
 			int distSq = dx * dx + dy * dy;
 			int dist = (int)(sqrtf((float)distSq) + 0.5f);
 
-			blendPixelWithGlow(surface, px, py, format, dist, kPointRadius);
+			blendPixelWithGlow(surface, px, py, format, dist, _pointRadius);
 		}
 	}
 }
 
 void HotspotRenderer::blendPixelWithGlow(Surface *surface, int px, int py,
 		const PixelFormat &format, int distance, int solidSize) {
-	uint16 *destPixel = (uint16 *)surface->getBasePtr(px, py);
 	byte bgR, bgG, bgB, bgA;
-	format.colorToARGB(*destPixel, bgA, bgR, bgG, bgB);
+	format.colorToARGB(surface->getPixel(px, py), bgA, bgR, bgG, bgB);
 
 	// Convert solidSize to maximum solid distance (solidSize=1 means center pixel only, distance=0)
 	int maxSolidDistance = solidSize - 1;
@@ -168,22 +203,19 @@ void HotspotRenderer::blendPixelWithGlow(Surface *surface, int px, int py,
 		byte r = ((255 * solidBeta) + (bgR * solidAlpha)) / 255;
 		byte g = ((255 * solidBeta) + (bgG * solidAlpha)) / 255;
 		byte b = ((255 * solidBeta) + (bgB * solidAlpha)) / 255;
-		*destPixel = format.RGBToColor(r, g, b);
-	} else if (distance <= maxSolidDistance + kGlowSize) {
+		surface->setPixel(px, py, format.RGBToColor(r, g, b));
+	} else if (distance <= maxSolidDistance + _glowSize) {
 		int glowDist = distance - maxSolidDistance;
-		int alpha = ((kGlowSize - glowDist) * 80) / kGlowSize;
+		int alpha = ((_glowSize - glowDist) * 80) / _glowSize;
 		byte r = ((255 * alpha) + (bgR * (255 - alpha))) / 255;
 		byte g = ((255 * alpha) + (bgG * (255 - alpha))) / 255;
 		byte b = ((255 * alpha) + (bgB * (255 - alpha))) / 255;
-		*destPixel = format.RGBToColor(r, g, b);
+		surface->setPixel(px, py, format.RGBToColor(r, g, b));
 	}
 }
 
 void HotspotRenderer::drawLineWithGlow(Surface *surface, int x1, int y1, int x2, int y2,
 		int width, int height, const PixelFormat &format, int lineThickness) {
-	if (format.bytesPerPixel != 2)
-		return;
-
 	bool isHorizontal = (y1 == y2);
 	bool isVertical = (x1 == x2);
 
@@ -203,7 +235,7 @@ void HotspotRenderer::drawLineWithGlow(Surface *surface, int x1, int y1, int x2,
 	}
 
 	for (int along = alongMin; along <= alongMax; along++) {
-		for (int thickness = -lineThickness - kGlowSize; thickness <= lineThickness + kGlowSize; thickness++) {
+		for (int thickness = -lineThickness - _glowSize; thickness <= lineThickness + _glowSize; thickness++) {
 			int px, py;
 			if (isHorizontal) {
 				px = along;
@@ -224,32 +256,28 @@ void HotspotRenderer::drawLineWithGlow(Surface *surface, int x1, int y1, int x2,
 
 void HotspotRenderer::drawRectWithGlow(Surface *surface, int x, int y, int w, int h,
 		int overlayWidth, int overlayHeight, const PixelFormat &format) {
-	drawLineWithGlow(surface, x, y, x + w - 1, y, overlayWidth, overlayHeight, format, kLineThickness);
+	drawLineWithGlow(surface, x, y, x + w - 1, y, overlayWidth, overlayHeight, format, _lineThickness);
 	drawLineWithGlow(surface, x, y + h - 1, x + w - 1, y + h - 1,
-			overlayWidth, overlayHeight, format, kLineThickness);
-	drawLineWithGlow(surface, x, y + 1, x, y + h - 2, overlayWidth, overlayHeight, format, kLineThickness);
+			overlayWidth, overlayHeight, format, _lineThickness);
+	drawLineWithGlow(surface, x, y + 1, x, y + h - 2, overlayWidth, overlayHeight, format, _lineThickness);
 	drawLineWithGlow(surface, x + w - 1, y + 1, x + w - 1, y + h - 2,
-			overlayWidth, overlayHeight, format, kLineThickness);
+			overlayWidth, overlayHeight, format, _lineThickness);
 }
 
 void HotspotRenderer::drawLabelBox(Surface *surface, int x, int y, int w, int h,
 		int overlayWidth, int overlayHeight, const PixelFormat &format) {
-	if (format.bytesPerPixel != 2)
-		return;
-
 	for (int py = y + 1; py < y + h - 1; py++) {
 		for (int px = x + 1; px < x + w - 1; px++) {
 			if (px < 0 || px >= overlayWidth || py < 0 || py >= overlayHeight)
 				continue;
 
-			uint16 *destPixel = (uint16 *)surface->getBasePtr(px, py);
 			byte bgR, bgG, bgB, bgA;
-			format.colorToARGB(*destPixel, bgA, bgR, bgG, bgB);
+			format.colorToARGB(surface->getPixel(px, py), bgA, bgR, bgG, bgB);
 
 			byte r = (bgR * 40) / 100;
 			byte g = (bgG * 40) / 100;
 			byte b = (bgB * 40) / 100;
-			*destPixel = format.RGBToColor(r, g, b);
+			surface->setPixel(px, py, format.RGBToColor(r, g, b));
 		}
 	}
 
diff --git a/graphics/hotspot_renderer.h b/graphics/hotspot_renderer.h
index adb5b038b20..c64dfddbe03 100644
--- a/graphics/hotspot_renderer.h
+++ b/graphics/hotspot_renderer.h
@@ -96,12 +96,18 @@ public:
 
 private:
 	enum {
-		kMarkerSize = 10,  ///< Size of square and crosshair markers
-		kGlowSize = 3,     ///< Size of glow effect around markers and text
-		kPointRadius = 3,  ///< Radius of point marker
-		kLineThickness = 1 ///< Thickness of lines for markers and text boxes
+		kBaseMarkerSize = 10,  ///< Size of square and crosshair markers at 1x scale
+		kBaseGlowSize = 3,     ///< Size of glow effect around markers and text at 1x scale
+		kBasePointRadius = 3,  ///< Radius of point marker at 1x scale
+		kBaseLineThickness = 1 ///< Thickness of lines for markers and text boxes at 1x scale
 	};
 
+	float _sizeScale;   ///< Overlay/game scale factor applied to marker geometry
+	int _markerSize;    ///< Scaled size of square and crosshair markers
+	int _glowSize;      ///< Scaled size of glow effect around markers and text
+	int _pointRadius;   ///< Scaled radius of point marker
+	int _lineThickness; ///< Scaled thickness of lines for markers and text boxes
+
 	void drawMarker(Surface *surface, int x, int y, const PixelFormat &format, MarkerShape markerShape);
 	void drawCrosshairMarker(Surface *surface, int x, int y, int width, int height, const PixelFormat &format);
 	void drawSquareMarker(Surface *surface, int x, int y, int width, int height, const PixelFormat &format);


Commit: a1bba7b319e96e38ff8fe5235acdfa4d5865dd1a
    https://github.com/scummvm/scummvm/commit/a1bba7b319e96e38ff8fe5235acdfa4d5865dd1a
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
ENGINES: Hide mouse cursor while hotspots overlay is shown

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    engines/engine.cpp
    engines/engine.h


diff --git a/engines/engine.cpp b/engines/engine.cpp
index aaa7a7870e6..a2e92e1a241 100644
--- a/engines/engine.cpp
+++ b/engines/engine.cpp
@@ -158,7 +158,8 @@ Engine::Engine(OSystem *syst)
 		_autosaveInterval(ConfMan.getInt("autosave_period")),
 		_lastAutosaveTime(_system->getMillis()),
 		_showHotspots(false),
-		_hotspotForceRedraw(false) {
+		_hotspotForceRedraw(false),
+		_hotspotPrevCursorVisible(false) {
 
 	g_engine = this;
 	_quitRequested = false;
@@ -747,12 +748,19 @@ void Engine::pauseEngineIntern(bool pause) {
 }
 
 void Engine::showHotspots(bool show) {
+	bool wasShowing = _showHotspots;
 	_showHotspots = show;
 
-	if (show)
+	if (show) {
 		_hotspotForceRedraw = true;
-	else if (g_system->isOverlayVisible())
-		g_system->hideOverlay();
+		if (!wasShowing)
+			_hotspotPrevCursorVisible = CursorMan.showMouse(false);
+	} else {
+		if (wasShowing)
+			CursorMan.showMouse(_hotspotPrevCursorVisible);
+		if (g_system->isOverlayVisible())
+			g_system->hideOverlay();
+	}
 }
 
 void Engine::getHotspotPositions(Common::Array<Graphics::HotspotInfo> &hotspots) {
diff --git a/engines/engine.h b/engines/engine.h
index 73508b474d7..daaec3cdf5a 100644
--- a/engines/engine.h
+++ b/engines/engine.h
@@ -199,6 +199,13 @@ protected:
 	 */
 	bool _hotspotForceRedraw;
 
+	/**
+	 * Mouse cursor visibility from before hotspots were shown. While hotspots
+	 * are displayed the cursor is hidden (otherwise it leaves trails over the
+	 * overlay), and this remembers the state to restore afterwards.
+	 */
+	bool _hotspotPrevCursorVisible;
+
 private:
 	/**
 	 * The associated metaengine


Commit: 50184ef27df59e562824c99c0b1abf7a35c700d1
    https://github.com/scummvm/scummvm/commit/50184ef27df59e562824c99c0b1abf7a35c700d1
Author: Mariusz Górski (takashivip at gmail.com)
Date: 2026-07-13T13:35:35+02:00

Commit Message:
COMMON: Drive hotspot display with a start/end event pair

Replace the EVENT_TOGGLE_HOTSPOTS event and the kEngineActionHotspotToggle
custom engine action with an EVENT_HOTSPOTS_SHOW / EVENT_HOTSPOTS_HIDE pair
registered in Keymapper::convertStartToEnd(). The keymap action now sends the
event directly via Action::setEvent(), so the reserved global EngineAction
enum is no longer needed.

Because the pair is a keymapper start/end event, SHOW is emitted while the
hotspot key is held and HIDE when it is released, preserving the hold-to-show
behavior that a single plain event (which only fires on key press) could not
provide.

Assisted-by: Claude:claude-opus-4.8

Changed paths:
    backends/events/default/default-events.cpp
    backends/keymapper/keymapper.cpp
    common/events.h
    engines/metaengine.cpp
    engines/touche/metaengine.cpp
    test/common/events.h


diff --git a/backends/events/default/default-events.cpp b/backends/events/default/default-events.cpp
index 056aef9124a..1179c47877a 100644
--- a/backends/events/default/default-events.cpp
+++ b/backends/events/default/default-events.cpp
@@ -120,20 +120,18 @@ bool DefaultEventManager::pollEvent(Common::Event &event) {
 		_modifierState = event.kbd.flags;
 		break;
 
-	case Common::EVENT_CUSTOM_ENGINE_ACTION_START:
-		if (event.customType == Common::kEngineActionHotspotToggle && g_engine) {
-			if (ConfMan.getBool("enable_hotspots")) {
-				g_engine->showHotspots(true);
-			}
+	case Common::EVENT_HOTSPOTS_SHOW:
+		if (g_engine && ConfMan.getBool("enable_hotspots")) {
+			g_engine->showHotspots(true);
 		}
+		forwardEvent = false;
 		break;
 
-	case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
-		if (event.customType == Common::kEngineActionHotspotToggle && g_engine) {
-			if (ConfMan.getBool("enable_hotspots")) {
-				g_engine->showHotspots(false);
-			}
+	case Common::EVENT_HOTSPOTS_HIDE:
+		if (g_engine && ConfMan.getBool("enable_hotspots")) {
+			g_engine->showHotspots(false);
 		}
+		forwardEvent = false;
 		break;
 
 	case Common::EVENT_MOUSEMOVE:
diff --git a/backends/keymapper/keymapper.cpp b/backends/keymapper/keymapper.cpp
index 303cde16552..33c6338c778 100644
--- a/backends/keymapper/keymapper.cpp
+++ b/backends/keymapper/keymapper.cpp
@@ -377,6 +377,9 @@ EventType Keymapper::convertStartToEnd(EventType type) {
 	case EVENT_CUSTOM_ENGINE_ACTION_START:
 		result = EVENT_CUSTOM_ENGINE_ACTION_END;
 		break;
+	case EVENT_HOTSPOTS_SHOW:
+		result = EVENT_HOTSPOTS_HIDE;
+		break;
 	default:
 		break;
 	}
diff --git a/common/events.h b/common/events.h
index 6e69117d176..702080247f8 100644
--- a/common/events.h
+++ b/common/events.h
@@ -117,8 +117,13 @@ enum EventType {
 	EVENT_FOCUS_GAINED = 36,
 	EVENT_FOCUS_LOST = 37,
 
-	/** Toggle hotspot display. */
-	EVENT_TOGGLE_HOTSPOTS = 38,
+	/**
+	 * Hotspot display, driven by a hold-to-show key. Bound as a keymapper
+	 * start/end pair (see Keymapper::convertStartToEnd), so SHOW is sent while
+	 * the key is held and HIDE when it is released.
+	 */
+	EVENT_HOTSPOTS_SHOW = 38,
+	EVENT_HOTSPOTS_HIDE = 39,
 
 	/**
 	 * We reserve some event ids for custom events.
@@ -132,15 +137,6 @@ enum EventType {
 	EVENT_USER_LAST_AVAILABLE = 9999
 };
 
-/**
- * Custom engine action types for EVENT_CUSTOM_ENGINE_ACTION_START/END.
- * These are used by cross-engine features. Values start at 20000 to avoid
- * clashing with engine-specific custom action ids, which start from 0.
- */
-enum EngineAction {
-	kEngineActionHotspotToggle = 20000
-};
-
 const int16 JOYAXIS_MIN = -32768;
 const int16 JOYAXIS_MAX = 32767;
 
diff --git a/engines/metaengine.cpp b/engines/metaengine.cpp
index f98ae29bdcb..23f10114393 100644
--- a/engines/metaengine.cpp
+++ b/engines/metaengine.cpp
@@ -160,7 +160,7 @@ Common::KeymapArray MetaEngine::initKeymaps(const char *target) const {
 	engineKeyMap->addAction(act);
 
 	act = new Action(kStandardActionToggleHotspots, _("Show hotspots"));
-	act->setCustomEngineActionEvent(Common::kEngineActionHotspotToggle);
+	act->setEvent(Common::EVENT_HOTSPOTS_SHOW);
 	act->addDefaultInputMapping("h");
 	engineKeyMap->addAction(act);
 
diff --git a/engines/touche/metaengine.cpp b/engines/touche/metaengine.cpp
index 0c0b0099db3..215c69623d5 100644
--- a/engines/touche/metaengine.cpp
+++ b/engines/touche/metaengine.cpp
@@ -133,7 +133,7 @@ Common::KeymapArray ToucheMetaEngine::initKeymaps(const char *target) const {
 	engineKeyMap->addAction(act);
 
 	act = new Action(kStandardActionToggleHotspots, _("Show hotspots"));
-	act->setCustomEngineActionEvent(Common::kEngineActionHotspotToggle);
+	act->setEvent(Common::EVENT_HOTSPOTS_SHOW);
 	act->addDefaultInputMapping("h");
 	gameKeyMap->addAction(act);
 
diff --git a/test/common/events.h b/test/common/events.h
index 183ecf186e4..88a21c1fbb0 100644
--- a/test/common/events.h
+++ b/test/common/events.h
@@ -24,25 +24,17 @@
 
 class EventsTestSuite : public CxxTest::TestSuite {
 public:
-	void test_engine_action_hotspot_toggle() {
-		TS_ASSERT_EQUALS(Common::kEngineActionHotspotToggle, 20000);
-	}
-
-	void test_custom_engine_action_event() {
+	void test_hotspots_show_event() {
 		Common::Event event;
-		event.type = Common::EVENT_CUSTOM_ENGINE_ACTION_START;
-		event.customType = Common::kEngineActionHotspotToggle;
+		event.type = Common::EVENT_HOTSPOTS_SHOW;
 
-		TS_ASSERT_EQUALS(event.type, Common::EVENT_CUSTOM_ENGINE_ACTION_START);
-		TS_ASSERT_EQUALS(event.customType, Common::kEngineActionHotspotToggle);
+		TS_ASSERT_EQUALS(event.type, Common::EVENT_HOTSPOTS_SHOW);
 	}
 
-	void test_custom_engine_action_end_event() {
+	void test_hotspots_hide_event() {
 		Common::Event event;
-		event.type = Common::EVENT_CUSTOM_ENGINE_ACTION_END;
-		event.customType = Common::kEngineActionHotspotToggle;
+		event.type = Common::EVENT_HOTSPOTS_HIDE;
 
-		TS_ASSERT_EQUALS(event.type, Common::EVENT_CUSTOM_ENGINE_ACTION_END);
-		TS_ASSERT_EQUALS(event.customType, Common::kEngineActionHotspotToggle);
+		TS_ASSERT_EQUALS(event.type, Common::EVENT_HOTSPOTS_HIDE);
 	}
 };




More information about the Scummvm-git-logs mailing list