[Scummvm-git-logs] scummvm master -> 70c0a4ef397fe8b1de4a5828efa2dcf6881b11f6

bluegr noreply at scummvm.org
Wed Apr 29 06:24:21 UTC 2026


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

Summary:
00ab16b7c4 NANCY: Handle additional fields added to the BSUM chunk since Nancy12
c8233b4eaa NANCY: Implement the new UI (taskbar), used in Nancy10+ games
93a0af84a1 NANCY: Initial implementation of the inventory and notebook popups
01f016cc4f NANCY: Add new fields used in the TBOX chunk in Nancy10+ games
fac3c45628 NANCY: Add stubs for some Nancy10+ ARs
e21a82ba41 NANCY: Add handling for disabling taskbar buttons in Nancy10+
7b833ce9b2 NANCY: Properly handle saved slots when changing page in Nancy8+
70c0a4ef39 NANCY: Properly handle whitespace while entering a save game


Commit: 00ab16b7c4c2deb36b4229780e6c3ade83f48dd2
    https://github.com/scummvm/scummvm/commit/00ab16b7c4c2deb36b4229780e6c3ade83f48dd2
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:33+03:00

Commit Message:
NANCY: Handle additional fields added to the BSUM chunk since Nancy12

It's now possible to start a new game in Nancy12 (although the
difficulty selection buttons are not working)

Changed paths:
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index d7beb0a3a53..5258fdbd018 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -47,6 +47,8 @@ BSUM::BSUM(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 
 	readFilename(s, conversationTextsFilename, kGameTypeNancy6);
 	readFilename(s, autotextFilename, kGameTypeNancy6);
+	readFilename(s, fontFilename, kGameTypeNancy12);
+	readFilename(s, flagsFilename, kGameTypeNancy12);
 
 	s.syncAsUint16LE(firstScene.sceneID);
 	s.skip(0xC, kGameTypeVampire, kGameTypeVampire); // Palette name + unknown 2 bytes
@@ -62,7 +64,7 @@ BSUM::BSUM(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	s.skip(0xA4, kGameTypeVampire, kGameTypeNancy2);
 	s.skip(3); // Number of object, frame, and logo images
 	if (g_nancy->getEngineData("PLG0")) {
-		// Parner logos were introduced with nancy4, but at least one nancy3 release
+		// Partner logos were introduced with nancy4, but at least one nancy3 release
 		// had one as well. For some reason they didn't port over the code from the
 		// later games, but implemented it the same way the other BSUM images work.
 		// Hence, we skip an extra byte indicating the number of partner logos.
@@ -112,16 +114,6 @@ BSUM::BSUM(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	s.syncAsByte(overrideMovementTimeDeltas);
 	s.syncAsSint16LE(slowMovementTimeDelta);
 	s.syncAsSint16LE(fastMovementTimeDelta);
-
-	// FIXME: Data fixup/HACK for Nancy12. The BSUM data format changed
-	// significantly, but we want to be able to start the game without
-	// crashing, even if the data is wrong.
-	if (g_nancy->getGameType() >= kGameTypeNancy12) {
-		numFonts = 18;
-		rTrans = 0;
-		gTrans = 31;
-		bTrans = 0;
-	}
 }
 
 VIEW::VIEW(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index e42db07bcdf..16aca54bd5f 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -46,6 +46,10 @@ struct BSUM : public EngineData {
 	Common::Path conversationTextsFilename;
 	Common::Path autotextFilename;
 
+	// Nancy12+
+	Common::Path fontFilename;
+	Common::Path flagsFilename;
+
 	// Game start section
 	SceneChangeDescription firstScene;
 	uint16 startTimeHours;


Commit: c8233b4eaa7f91a183b0aae54f5658b45d352f84
    https://github.com/scummvm/scummvm/commit/c8233b4eaa7f91a183b0aae54f5658b45d352f84
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:36+03:00

Commit Message:
NANCY: Implement the new UI (taskbar), used in Nancy10+ games

Changed paths:
  A engines/nancy/ui/taskbar.cpp
  A engines/nancy/ui/taskbar.h
    engines/nancy/module.mk
    engines/nancy/state/scene.cpp
    engines/nancy/state/scene.h


diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 5449fadc7c5..a006cbb1a1f 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -60,6 +60,7 @@ MODULE_OBJS = \
   ui/inventorybox.o \
   ui/ornaments.o \
   ui/scrollbar.o \
+  ui/taskbar.o \
   ui/textbox.o \
   ui/viewport.o \
   state/credits.o \
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 80e0e30aa05..2ad61df88a8 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -39,6 +39,7 @@
 #include "engines/nancy/ui/button.h"
 #include "engines/nancy/ui/ornaments.h"
 #include "engines/nancy/ui/clock.h"
+#include "engines/nancy/ui/taskbar.h"
 
 #include "engines/nancy/misc/lightning.h"
 #include "engines/nancy/misc/specialeffect.h"
@@ -121,6 +122,7 @@ Scene::Scene() :
 		_inventoryBox(),
 		_menuButton(nullptr),
 		_helpButton(nullptr),
+		_taskbar(nullptr),
 		_viewportOrnaments(nullptr),
 		_textboxOrnaments(nullptr),
 		_inventoryBoxOrnaments(nullptr),
@@ -137,6 +139,7 @@ Scene::Scene() :
 Scene::~Scene() {
 	delete _helpButton;
 	delete _menuButton;
+	delete _taskbar;
 	delete _viewportOrnaments;
 	delete _textboxOrnaments;
 	delete _inventoryBoxOrnaments;
@@ -344,7 +347,11 @@ void Scene::addItemToInventory(int16 id) {
 
 		g_nancy->_sound->playSound("BUOK");
 
-		_inventoryBox.addItem(id);
+		if (g_nancy->getGameType() <= kGameTypeNancy9) {
+			_inventoryBox.addItem(id);
+		} else if (_inventoryPopup.isOpen()) {
+			//_inventoryPopup.refreshGrid();
+		}
 	}
 }
 
@@ -364,7 +371,11 @@ void Scene::removeItemFromInventory(int16 id, bool pickUp) {
 
 		g_nancy->_sound->playSound("BUOK");
 
-		_inventoryBox.removeItem(id);
+		if (g_nancy->getGameType() <= kGameTypeNancy9) {
+			_inventoryBox.removeItem(id);
+		} else if (_inventoryPopup.isOpen()) {
+			//_inventoryPopup.refreshGrid();
+		}
 	}
 }
 
@@ -587,10 +598,24 @@ void Scene::useHint(uint16 characterID, uint16 hintID) {
 void Scene::registerGraphics() {
 	_frame.registerGraphics();
 	_viewport.registerGraphics();
-	_textbox.registerGraphics();
 
-	if (g_nancy->getGameType() <= kGameTypeNancy9)
+	// Pre-Nancy 10: legacy textbox is the on-screen subtitle/conversation
+	// strip. Nancy 10+: that role moves to the UICO popup; the legacy
+	// textbox is kept around in memory (other code still calls clear() /
+	// addTextLine() on it) but stays unregistered so its surface is
+	// never blitted to the screen.
+	if (g_nancy->getGameType() <= kGameTypeNancy9) {
+		_textbox.registerGraphics();
+	}
+
+	// Pre-Nancy 10: inventory box is always-on-screen.
+	// Nancy 10+: a separate popup widget driven by UIIV (initially hidden).
+	if (g_nancy->getGameType() <= kGameTypeNancy9) {
 		_inventoryBox.registerGraphics();
+	} else {
+		//_inventoryPopup.registerGraphics();
+		//_notebookPopup.registerGraphics();
+	}
 
 	_hotspotDebug.registerGraphics();
 
@@ -604,6 +629,10 @@ void Scene::registerGraphics() {
 		_helpButton->setVisible(false);
 	}
 
+	if (_taskbar) {
+		_taskbar->registerGraphics();
+	}
+
 	if (_viewportOrnaments) {
 		_viewportOrnaments->registerGraphics();
 		_viewportOrnaments->setVisible(true);
@@ -1111,7 +1140,12 @@ void Scene::handleInput() {
 	// We handle the textbox and inventory box first because of their scrollbars, which
 	// need to take highest priority
 	_textbox.handleInput(input);
-	_inventoryBox.handleInput(input);
+	if (g_nancy->getGameType() <= kGameTypeNancy9) {
+		_inventoryBox.handleInput(input);
+	} else {
+		//_inventoryPopup.handleInput(input);
+		//_notebookPopup.handleInput(input);
+	}
 
 	// Handle invisible map button
 	// We do this before the viewport since TVD's map button overlaps the viewport's right hotspot
@@ -1153,6 +1187,31 @@ void Scene::handleInput() {
 
 	// Menu/help are disabled when a movie is active
 	if (!_activeMovie) {
+		if (_taskbar) {
+			_taskbar->handleInput(input);
+
+			int clicked = _taskbar->getClickedButton();
+			switch (clicked) {
+			case kTaskButtonMenu:
+				requestStateChange(NancyState::kMainMenu);
+				break;
+			case kTaskButtonInventory:
+				//_inventoryPopup.toggle();
+				break;
+			case kTaskButtonNotebook:
+				//_notebookPopup.toggle();
+				break;
+			case kTaskButtonCellphone:
+				// TODO: open cell-phone popup (UICL)
+				break;
+			case kTaskButtonHelp:
+				requestStateChange(NancyState::kHelp);
+				break;
+			default:
+				break;
+			}
+		}
+
 		if (_menuButton) {
 			_menuButton->handleInput(input);
 
@@ -1209,8 +1268,12 @@ void Scene::initStaticData() {
 	_viewport.init();
 	_textbox.init();
 
-	if (g_nancy->getGameType() <= kGameTypeNancy9)
+	if (g_nancy->getGameType() <= kGameTypeNancy9) {
 		_inventoryBox.init();
+	} else {
+		//_inventoryPopup.init();
+		//_notebookPopup.init();
+	}
 
 	// Init buttons
 	if (g_nancy->getGameType() == kGameTypeVampire) {
@@ -1219,8 +1282,18 @@ void Scene::initStaticData() {
 		_mapHotspot = mapData->buttonDest;
 	}
 
-	_menuButton = new UI::Button(5, g_nancy->_graphics->_object0, bootSummary->menuButtonSrc, bootSummary->menuButtonDest, bootSummary->menuButtonHighlightSrc);
-	_helpButton = new UI::Button(5, g_nancy->_graphics->_object0, bootSummary->helpButtonSrc, bootSummary->helpButtonDest, bootSummary->helpButtonHighlightSrc);
+	if (g_nancy->getGameType() <= kGameTypeNancy9) {
+		// Pre-Nancy 10: free-floating MENU and HELP buttons whose
+		// rects come from BSUM. Replaced in Nancy 10+ by the taskbar.
+		_menuButton = new UI::Button(5, g_nancy->_graphics->_object0, bootSummary->menuButtonSrc, bootSummary->menuButtonDest, bootSummary->menuButtonHighlightSrc);
+		_helpButton = new UI::Button(5, g_nancy->_graphics->_object0, bootSummary->helpButtonSrc, bootSummary->helpButtonDest, bootSummary->helpButtonHighlightSrc);
+	} else {
+		// Nancy 10+: bottom-of-screen taskbar holds MENU / inventory /
+		// notebook / cellphone / HELP buttons. Built from the TASK chunk.
+		_taskbar = new UI::Taskbar();
+		_taskbar->init();
+	}
+
 	g_nancy->setMouseEnabled(true);
 
 	// Init ornaments and clock (TVD only)
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index bc2fb5ecd11..20c4322acdc 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -60,6 +60,7 @@ class SpecialEffect;
 
 namespace UI {
 class Button;
+class Taskbar;
 class ViewportOrnaments;
 class TextboxOrnaments;
 class InventoryBoxOrnaments;
@@ -263,6 +264,7 @@ private:
 
 	UI::Button *_menuButton;
 	UI::Button *_helpButton;
+	UI::Taskbar *_taskbar;
 	Time _buttonPressActivationTime;
 
 	UI::ViewportOrnaments *_viewportOrnaments;
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
new file mode 100644
index 00000000000..00608cc9e5e
--- /dev/null
+++ b/engines/nancy/ui/taskbar.cpp
@@ -0,0 +1,156 @@
+/* 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 "engines/nancy/nancy.h"
+#include "engines/nancy/cursor.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+
+#include "engines/nancy/ui/taskbar.h"
+
+namespace Nancy {
+namespace UI {
+
+Taskbar::Taskbar() :
+		RenderObject(7),
+		_hoveredButton(-1),
+		_clickedButton(-1) {
+	for (uint i = 0; i < TASK::kNumButtons; ++i) {
+		_buttonStates[i] = kButtonIdle;
+	}
+}
+
+void Taskbar::init() {
+	auto *taskData = GetEngineData(TASK);
+	assert(taskData);
+
+	g_nancy->_resource->loadImage(taskData->imageName, _backgroundImage);
+	g_nancy->_resource->loadImage(taskData->buttons[0].button.primaryImageName, _buttonImage);
+
+	// Draw the taskbar strip itself (background) into the draw surface.
+	moveTo(taskData->dstRect);
+
+	_drawSurface.create(_screenPosition.width(), _screenPosition.height(), g_nancy->_graphics->getInputPixelFormat());
+	_drawSurface.blitFrom(_backgroundImage, taskData->srcRect, Common::Point(0, 0));
+
+	// Draw each button in its idle state on top of the strip
+	for (uint i = 0; i < TASK::kNumButtons; ++i) {
+		drawButton(i, kButtonIdle);
+	}
+
+	setTransparent(false);
+
+	RenderObject::init();
+}
+
+void Taskbar::registerGraphics() {
+	RenderObject::registerGraphics();
+	setVisible(true);
+}
+
+void Taskbar::drawButton(uint index, ButtonState state) {
+	auto *taskData = GetEngineData(TASK);
+	assert(taskData);
+
+	const UIButtonRecord &btn = taskData->buttons[index].button;
+
+	Common::Rect src = btn.sourceRects[state];
+	if (src.isEmpty())
+		src = btn.sourceRects[kButtonIdle];
+	if (src.isEmpty())
+		return;
+
+	// Convert the on-screen DEST rect to taskbar-local coordinates.
+	Common::Rect dst = btn.destRect;
+	dst.translate(-_screenPosition.left, -_screenPosition.top);
+
+	// Restore the background under the button before drawing the new state,
+	// so a hover/pressed sprite blends correctly (and idle erases any
+	// previous hover/pressed sprite). srcBackgroundRestore is in absolute
+	// screen coords, same as destRect — they match in shipped data.
+	if (!btn.srcBackgroundRestore.isEmpty())
+		_drawSurface.blitFrom(_backgroundImage, btn.srcBackgroundRestore, Common::Point(dst.left, dst.top));
+
+	_drawSurface.blitFrom(_buttonImage, src, Common::Point(dst.left, dst.top));
+	_buttonStates[index] = state;
+	_needsRedraw = true;
+}
+
+void Taskbar::handleInput(NancyInput &input) {
+	auto *taskData = GetEngineData(TASK);
+	assert(taskData);
+
+	_clickedButton = -1;
+
+	int newHovered = -1;
+	for (uint i = 0; i < TASK::kNumButtons; ++i) {
+		if (taskData->buttons[i].button.destRect.contains(input.mousePos)) {
+			newHovered = i;
+			break;
+		}
+	}
+
+	// Update hover graphic on enter/exit. Always revert the previously-
+	// hovered button (even from kButtonPressed) so it never gets stuck
+	// after the cursor leaves.
+	if (newHovered != _hoveredButton) {
+		if (_hoveredButton != -1) {
+			drawButton(_hoveredButton, kButtonIdle);
+		}
+		if (newHovered != -1) {
+			drawButton(newHovered, kButtonHover);
+		}
+		_hoveredButton = newHovered;
+	}
+
+	if (newHovered == -1) {
+		return;
+	}
+
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+	if (input.input & NancyInput::kLeftMouseButtonDown) {
+		// Mouse pressed: show the pressed sprite for the duration of the hold.
+		if (_buttonStates[newHovered] != kButtonPressed) {
+			drawButton(newHovered, kButtonPressed);
+		}
+	} else if (input.input & NancyInput::kLeftMouseButtonUp) {
+		// Mouse released over the button: trigger the click action and
+		// snap the sprite back to hover (the cursor is still over it).
+		drawButton(newHovered, kButtonHover);
+		_clickedButton = newHovered;
+
+		// Play the first available click sound for this button. The TASK
+		// chunk stores up to three alternates per button; if the slot is
+		// empty we silently skip.
+		const Common::String &snd = taskData->buttons[newHovered].clickSoundName[0];
+		if (!snd.empty()) {
+			g_nancy->_sound->playSound(snd);
+		}
+
+		input.eatMouseInput();
+	}
+}
+
+} // End of namespace UI
+} // End of namespace Nancy
diff --git a/engines/nancy/ui/taskbar.h b/engines/nancy/ui/taskbar.h
new file mode 100644
index 00000000000..b365daaf620
--- /dev/null
+++ b/engines/nancy/ui/taskbar.h
@@ -0,0 +1,68 @@
+/* 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 NANCY_UI_TASKBAR_H
+#define NANCY_UI_TASKBAR_H
+
+#include "engines/nancy/renderobject.h"
+
+namespace Nancy {
+
+struct NancyInput;
+
+namespace UI {
+
+// Always-on bottom-of-screen strip introduced in Nancy 10.
+// Holds 5 buttons (MENU / inventory / notebook / cellphone / HELP) that
+// open the various popup UIs handled by Scene.
+class Taskbar : public RenderObject {
+public:
+	Taskbar();
+	virtual ~Taskbar() = default;
+
+	void init() override;
+	void registerGraphics() override;
+	void handleInput(NancyInput &input);
+
+	// Returns the index of the button that was clicked this frame, or -1
+	// if none. Cleared on the next call to handleInput().
+	int getClickedButton() const { return _clickedButton; }
+
+private:
+	enum ButtonState {
+		kButtonIdle    = 0,
+		kButtonHover   = 1,
+		kButtonPressed = 2
+	};
+
+	void drawButton(uint index, ButtonState state);
+
+	Graphics::ManagedSurface _backgroundImage; // TASK::imageName (e.g. "Frame")
+	Graphics::ManagedSurface _buttonImage;     // buttons' primaryImageName (e.g. "UIShared_OVL")
+	int _hoveredButton;
+	int _clickedButton;
+	ButtonState _buttonStates[5];
+};
+
+} // End of namespace UI
+} // End of namespace Nancy
+
+#endif // NANCY_UI_TASKBAR_H


Commit: 93a0af84a16b45aeb5326a4f9f5aa9f9b7e7a574
    https://github.com/scummvm/scummvm/commit/93a0af84a16b45aeb5326a4f9f5aa9f9b7e7a574
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:39+03:00

Commit Message:
NANCY: Initial implementation of the inventory and notebook popups

These are used in Nancy10 and newer games

The inventory popup is functional, except from the specialized filters.
The notebook popup is still a stub, as the actual text of each notebook
page is not yet rendered

Changed paths:
  A engines/nancy/ui/inventorypopup.cpp
  A engines/nancy/ui/inventorypopup.h
  A engines/nancy/ui/notebookpopup.cpp
  A engines/nancy/ui/notebookpopup.h
    engines/nancy/module.mk
    engines/nancy/state/scene.cpp
    engines/nancy/state/scene.h


diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index a006cbb1a1f..b6f5a1fe60d 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -58,6 +58,8 @@ MODULE_OBJS = \
   ui/button.o \
   ui/clock.o \
   ui/inventorybox.o \
+  ui/inventorypopup.o \
+  ui/notebookpopup.o \
   ui/ornaments.o \
   ui/scrollbar.o \
   ui/taskbar.o \
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 2ad61df88a8..bc0ed7aa336 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -350,7 +350,7 @@ void Scene::addItemToInventory(int16 id) {
 		if (g_nancy->getGameType() <= kGameTypeNancy9) {
 			_inventoryBox.addItem(id);
 		} else if (_inventoryPopup.isOpen()) {
-			//_inventoryPopup.refreshGrid();
+			_inventoryPopup.refreshGrid();
 		}
 	}
 }
@@ -374,7 +374,7 @@ void Scene::removeItemFromInventory(int16 id, bool pickUp) {
 		if (g_nancy->getGameType() <= kGameTypeNancy9) {
 			_inventoryBox.removeItem(id);
 		} else if (_inventoryPopup.isOpen()) {
-			//_inventoryPopup.refreshGrid();
+			_inventoryPopup.refreshGrid();
 		}
 	}
 }
@@ -613,8 +613,8 @@ void Scene::registerGraphics() {
 	if (g_nancy->getGameType() <= kGameTypeNancy9) {
 		_inventoryBox.registerGraphics();
 	} else {
-		//_inventoryPopup.registerGraphics();
-		//_notebookPopup.registerGraphics();
+		_inventoryPopup.registerGraphics();
+		_notebookPopup.registerGraphics();
 	}
 
 	_hotspotDebug.registerGraphics();
@@ -1143,8 +1143,8 @@ void Scene::handleInput() {
 	if (g_nancy->getGameType() <= kGameTypeNancy9) {
 		_inventoryBox.handleInput(input);
 	} else {
-		//_inventoryPopup.handleInput(input);
-		//_notebookPopup.handleInput(input);
+		_inventoryPopup.handleInput(input);
+		_notebookPopup.handleInput(input);
 	}
 
 	// Handle invisible map button
@@ -1196,10 +1196,10 @@ void Scene::handleInput() {
 				requestStateChange(NancyState::kMainMenu);
 				break;
 			case kTaskButtonInventory:
-				//_inventoryPopup.toggle();
+				_inventoryPopup.toggle();
 				break;
 			case kTaskButtonNotebook:
-				//_notebookPopup.toggle();
+				_notebookPopup.toggle();
 				break;
 			case kTaskButtonCellphone:
 				// TODO: open cell-phone popup (UICL)
@@ -1271,8 +1271,8 @@ void Scene::initStaticData() {
 	if (g_nancy->getGameType() <= kGameTypeNancy9) {
 		_inventoryBox.init();
 	} else {
-		//_inventoryPopup.init();
-		//_notebookPopup.init();
+		_inventoryPopup.init();
+		_notebookPopup.init();
 	}
 
 	// Init buttons
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 20c4322acdc..ab59a2557b5 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -36,6 +36,8 @@
 #include "engines/nancy/ui/viewport.h"
 #include "engines/nancy/ui/textbox.h"
 #include "engines/nancy/ui/inventorybox.h"
+#include "engines/nancy/ui/inventorypopup.h"
+#include "engines/nancy/ui/notebookpopup.h"
 
 namespace Common {
 class SeekableReadStream;
@@ -261,6 +263,8 @@ private:
 	UI::Viewport _viewport;
 	UI::Textbox _textbox;
 	UI::InventoryBox _inventoryBox;
+	UI::InventoryPopup _inventoryPopup;
+	UI::NotebookPopup _notebookPopup;
 
 	UI::Button *_menuButton;
 	UI::Button *_helpButton;
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
new file mode 100644
index 00000000000..6d6295f0260
--- /dev/null
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -0,0 +1,278 @@
+/* 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 "engines/nancy/cursor.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+
+#include "engines/nancy/state/scene.h"
+
+#include "engines/nancy/ui/inventorypopup.h"
+
+namespace Nancy {
+namespace UI {
+
+InventoryPopup::InventoryPopup() :
+		RenderObject(6),
+		_uiivData(nullptr),
+		_invData(nullptr),
+		_isOpen(false),
+		_activeFilter(kFilterAll),
+		_currentPage(0) {
+	for (uint i = 0; i < kSlotsPerPage; ++i) {
+		_slotItemIDs[i] = -1;
+	}
+}
+
+void InventoryPopup::init() {
+	_uiivData = GetEngineData(UIIV);
+	assert(_uiivData);
+
+	_invData = GetEngineData(INV);
+	assert(_invData);
+
+	// Popup background image (e.g. "UIInv_OVL").
+	g_nancy->_resource->loadImage(_uiivData->header.imageName, _overlayImage);
+
+	// Per-item icon sheet shared with the legacy InventoryBox.
+	g_nancy->_resource->loadImage(_invData->inventoryBoxIconsImageName, _itemIcons);
+
+	// Position the popup using the "normal-size" rects from the popup
+	// header (popup state 2).
+	moveTo(_uiivData->header.normalDestRect);
+	Common::Rect bounds = _screenPosition;
+	bounds.moveTo(0, 0);
+	_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
+
+	drawBackground();
+	drawFilterTabs();
+
+	setTransparent(false);
+	setVisible(false);
+
+	RenderObject::init();
+}
+
+void InventoryPopup::registerGraphics() {
+	RenderObject::registerGraphics();
+}
+
+void InventoryPopup::open() {
+	if (_isOpen) {
+		return;
+	}
+	_isOpen = true;
+	_currentPage = 0;
+
+	rebuildVisibleList();
+	refreshGrid();
+
+	setVisible(true);
+
+	if (!_uiivData->header.sounds[0].name.empty()) {
+		g_nancy->_sound->loadSound(_uiivData->header.sounds[0]);
+		g_nancy->_sound->playSound(_uiivData->header.sounds[0]);
+	}
+}
+
+void InventoryPopup::close() {
+	if (!_isOpen) {
+		return;
+	}
+	_isOpen = false;
+
+	setVisible(false);
+
+	if (!_uiivData->header.sounds[1].name.empty()) {
+		g_nancy->_sound->loadSound(_uiivData->header.sounds[1]);
+		g_nancy->_sound->playSound(_uiivData->header.sounds[1]);
+	}
+}
+
+void InventoryPopup::refreshGrid() {
+	rebuildVisibleList();
+
+	// Re-blit the popup background; this also wipes any previous slot icons.
+	drawBackground();
+	drawFilterTabs();
+
+	for (uint i = 0; i < kSlotsPerPage; ++i) {
+		const uint listIndex = _currentPage * kSlotsPerPage + i;
+		const int16 itemID = (listIndex < _visibleItems.size()) ? _visibleItems[listIndex] : -1;
+		_slotItemIDs[i] = itemID;
+		drawSlot(i, itemID);
+	}
+
+	_needsRedraw = true;
+}
+
+void InventoryPopup::rebuildVisibleList() {
+	_visibleItems.clear();
+
+	const uint16 numItems = MIN<uint16>(g_nancy->getStaticData().numItems,
+										_invData->itemDescriptions.size());
+
+	for (uint16 id = 0; id < numItems; ++id) {
+		if (NancySceneState.hasItem(id) != g_nancy->_true) {
+			continue;
+		}
+
+		const INV::ItemDescription &desc = _invData->itemDescriptions[id];
+		bool include;
+
+		switch (_activeFilter) {
+		case kFilterDocuments:
+			include = (desc.keepItem == 3);
+			break;
+		case kFilterUsable:
+			include = (desc.keepItem <= 2);
+			break;
+		case kFilterSpecial1:
+		case kFilterSpecial2:
+		case kFilterSpecial3:
+			// Specialty filters
+			// TODO: Show nothing, for now
+			include = false;
+			break;
+		case kFilterAll:
+		default:
+			include = true;
+			break;
+		}
+
+		if (include) {
+			_visibleItems.push_back(id);
+		}
+	}
+}
+
+void InventoryPopup::drawBackground() {
+	Common::Rect src = _uiivData->header.normalSrcRect;
+	_drawSurface.blitFrom(_overlayImage, src, Common::Point(0, 0));
+}
+
+void InventoryPopup::drawFilterTabs() {
+	for (const auto &filter : _uiivData->filters) {
+		if (!filter.enabled || filter.button.sourceRects[0].isEmpty()) {
+			continue;
+		}
+
+		Common::Rect dst = filter.button.destRect;
+		dst.translate(-_screenPosition.left, -_screenPosition.top);
+
+		// Draw the idle sprite from the button's primary image; fall back
+		// to the overlay image if no primary image is loaded for the slot.
+		_drawSurface.blitFrom(_overlayImage, filter.button.sourceRects[0], Common::Point(dst.left, dst.top));
+	}
+}
+
+void InventoryPopup::drawSlot(uint slotIndex, int16 itemID) {
+	if (slotIndex >= _uiivData->slotDestRects.size()) {
+		return;
+	}
+
+	Common::Rect dst = _uiivData->slotDestRects[slotIndex];
+	dst.translate(-_screenPosition.left, -_screenPosition.top);
+
+	if (itemID < 0 || itemID >= (int16)_invData->itemDescriptions.size()) {
+		// Empty slot — leave the popup-background pixels in place.
+		return;
+	}
+
+	const INV::ItemDescription &desc = _invData->itemDescriptions[itemID];
+	if (desc.sourceRect.isEmpty()) {
+		return;
+	}
+
+	_drawSurface.blitFrom(_itemIcons, desc.sourceRect, Common::Point(dst.left, dst.top));
+}
+
+void InventoryPopup::handleInput(NancyInput &input) {
+	if (!_isOpen) {
+		return;
+	}
+
+	// Hit-test slots against the on-screen DEST rects (UIIV stores them in
+	// absolute screen coords, same as button rects).
+	int newHovered = -1;
+	for (uint i = 0; i < kSlotsPerPage; ++i) {
+		if (i >= _uiivData->slotDestRects.size()) {
+			break;
+		}
+		if (_slotItemIDs[i] < 0) {
+			continue;
+		}
+		if (_uiivData->slotDestRects[i].contains(input.mousePos)) {
+			newHovered = (int)i;
+			break;
+		}
+	}
+
+	if (newHovered != -1) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			const int16 itemID = _slotItemIDs[newHovered];
+			if (itemID >= 0) {
+				// Pick the item up: it leaves the inventory and becomes
+				// the held item (cursor sprite)
+				NancySceneState.removeItemFromInventory(itemID, true);
+				refreshGrid();
+				close();
+				input.eatMouseInput();
+				return;
+			}
+		}
+	}
+
+	// Filter tabs: clicking switches the active filter.
+	for (const auto &filter : _uiivData->filters) {
+		if (!filter.enabled) {
+			continue;
+		}
+		if (!filter.button.destRect.contains(input.mousePos)) {
+			continue;
+		}
+
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_activeFilter = static_cast<FilterID>(filter.id);
+			_currentPage = 0;
+			refreshGrid();
+			input.eatMouseInput();
+			return;
+		}
+		break;
+	}
+
+	// While the popup is open, swallow clicks that fall on the popup so
+	// the underlying scene/viewport doesn't react.
+	if (_screenPosition.contains(input.mousePos)) {
+		input.eatMouseInput();
+	}
+}
+
+} // End of namespace UI
+} // End of namespace Nancy
diff --git a/engines/nancy/ui/inventorypopup.h b/engines/nancy/ui/inventorypopup.h
new file mode 100644
index 00000000000..a2b340640aa
--- /dev/null
+++ b/engines/nancy/ui/inventorypopup.h
@@ -0,0 +1,104 @@
+/* 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 NANCY_UI_INVENTORYPOPUP_H
+#define NANCY_UI_INVENTORYPOPUP_H
+
+#include "engines/nancy/renderobject.h"
+
+namespace Nancy {
+
+struct NancyInput;
+struct INV;
+struct UIIV;
+
+namespace State {
+class Scene;
+}
+
+namespace UI {
+
+// Nancy 10+ inventory popup (replaces the always-visible InventoryBox of
+// pre-Nancy-10 games). Driven by the UIIV chunk (popup graphics + slot
+// rects + filter buttons) and the INV chunk (per-item descriptions/icons).
+class InventoryPopup : public RenderObject {
+public:
+	InventoryPopup();
+	~InventoryPopup() override = default;
+
+	void init() override;
+	void registerGraphics() override;
+	void handleInput(NancyInput &input);
+
+	bool isOpen() const { return _isOpen; }
+	void open();
+	void close();
+
+	// The taskbar's inventory button toggles the popup; convenience helper.
+	void toggle() { if (_isOpen) close(); else open(); }
+
+	// Re-render the slot grid. Called by Scene whenever the inventory
+	// contents change while the popup is open.
+	void refreshGrid();
+
+	enum FilterID {
+		kFilterAll       = 0x64, // default branch — every owned item
+		kFilterDocuments = 0x65, // keepItem == 3
+		kFilterUsable    = 0x66, // keepItem == 0/1/2
+		kFilterSpecial1  = 0x67,
+		kFilterSpecial2  = 0x68,
+		kFilterSpecial3  = 0x69
+	};
+
+private:
+	static const uint kSlotsPerPage = 16;
+	static const uint kNumFilters = 6;
+
+	void drawBackground();
+	void drawSlot(uint slotIndex, int16 itemID);
+	void drawFilterTabs();
+	void rebuildVisibleList();
+
+	const UIIV *_uiivData;
+	const INV *_invData;
+
+	Graphics::ManagedSurface _overlayImage;  // popup background image
+	Graphics::ManagedSurface _itemIcons;     // per-item icon sheet
+
+	bool _isOpen;
+
+	FilterID _activeFilter;
+
+	// Page index within the active filter (0-based).
+	uint _currentPage;
+
+	// Items the player owns that match the active filter, in inventory
+	// order. The on-screen grid is a 16-item window into this array.
+	Common::Array<int16> _visibleItems;
+
+	// Item ID currently shown in each of the 16 slots (-1 if empty).
+	int16 _slotItemIDs[kSlotsPerPage];
+};
+
+} // End of namespace UI
+} // End of namespace Nancy
+
+#endif // NANCY_UI_INVENTORYPOPUP_H
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
new file mode 100644
index 00000000000..6b5b7444648
--- /dev/null
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -0,0 +1,174 @@
+/* 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 "engines/nancy/cursor.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+
+#include "engines/nancy/ui/notebookpopup.h"
+
+namespace Nancy {
+namespace UI {
+
+NotebookPopup::NotebookPopup() :
+		RenderObject(6),
+		_uinbData(nullptr),
+		_isOpen(false),
+		_activeTab(0) {}
+
+void NotebookPopup::init() {
+	_uinbData = GetEngineData(UINB);
+	assert(_uinbData);
+
+	g_nancy->_resource->loadImage(_uinbData->header.imageName, _overlayImage);
+
+	moveTo(_uinbData->header.normalDestRect);
+	Common::Rect bounds = _screenPosition;
+	bounds.moveTo(0, 0);
+	_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
+
+	// Pick the first enabled tab as the initially active one
+	_activeTab = 0;
+	for (uint i = 0; i < kNumTabs; ++i) {
+		if (_uinbData->tabs[i].enabled) {
+			_activeTab = (int)i;
+			break;
+		}
+	}
+
+	drawBackground();
+	drawTabs();
+
+	// TODO: Draw the actual notebook page contents
+
+	setTransparent(false);
+	setVisible(false);
+
+	RenderObject::init();
+}
+
+void NotebookPopup::registerGraphics() {
+	RenderObject::registerGraphics();
+}
+
+void NotebookPopup::open() {
+	if (_isOpen) {
+		return;
+	}
+	_isOpen = true;
+	setVisible(true);
+
+	if (!_uinbData->header.sounds[0].name.empty()) {
+		g_nancy->_sound->loadSound(_uinbData->header.sounds[0]);
+		g_nancy->_sound->playSound(_uinbData->header.sounds[0]);
+	}
+}
+
+void NotebookPopup::close() {
+	if (!_isOpen) {
+		return;
+	}
+	_isOpen = false;
+	setVisible(false);
+
+	if (!_uinbData->header.sounds[1].name.empty()) {
+		g_nancy->_sound->loadSound(_uinbData->header.sounds[1]);
+		g_nancy->_sound->playSound(_uinbData->header.sounds[1]);
+	}
+}
+
+void NotebookPopup::drawBackground() {
+	_drawSurface.blitFrom(_overlayImage, _uinbData->header.normalSrcRect, Common::Point(0, 0));
+}
+
+void NotebookPopup::drawTabs() {
+	for (uint i = 0; i < kNumTabs; ++i) {
+		const UIButtonSlot &tab = _uinbData->tabs[i];
+		if (!tab.enabled) {
+			continue;
+		}
+
+		// Use the active state's sprite for the selected tab, idle for
+		// the rest.
+		const Common::Rect src = ((int)i == _activeTab && !tab.button.sourceRects[2].isEmpty())
+									? tab.button.sourceRects[2]
+									: tab.button.sourceRects[0];
+		if (src.isEmpty()) {
+			continue;
+		}
+
+		Common::Rect dst = tab.button.destRect;
+		dst.translate(-_screenPosition.left, -_screenPosition.top);
+
+		_drawSurface.blitFrom(_overlayImage, src, Common::Point(dst.left, dst.top));
+	}
+
+	_needsRedraw = true;
+}
+
+void NotebookPopup::handleInput(NancyInput &input) {
+	if (!_isOpen) {
+		return;
+	}
+
+	// Tab clicks
+	for (uint i = 0; i < kNumTabs; ++i) {
+		const UIButtonSlot &tab = _uinbData->tabs[i];
+		if (!tab.enabled) {
+			continue;
+		}
+		if (!tab.button.destRect.contains(input.mousePos)) {
+			continue;
+		}
+
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			if (_activeTab != (int)i) {
+				_activeTab = (int)i;
+
+				// Play the page-flip sound (first slot of either
+				// actionable or no-action set; both have 3 alternates).
+				const Common::Path &soundName = _uinbData->noActionClickSounds[0];
+				if (!soundName.empty()) {
+					g_nancy->_sound->playSound(soundName.toString());
+				}
+
+				drawBackground();
+				drawTabs();
+			}
+			input.eatMouseInput();
+			return;
+		}
+		break;
+	}
+
+	// Swallow clicks on the popup itself so they don't fall through.
+	if (_screenPosition.contains(input.mousePos)) {
+		input.eatMouseInput();
+	}
+}
+
+} // End of namespace UI
+} // End of namespace Nancy
diff --git a/engines/nancy/ui/notebookpopup.h b/engines/nancy/ui/notebookpopup.h
new file mode 100644
index 00000000000..cd2158a8c14
--- /dev/null
+++ b/engines/nancy/ui/notebookpopup.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/>.
+ *
+ */
+
+#ifndef NANCY_UI_NOTEBOOKPOPUP_H
+#define NANCY_UI_NOTEBOOKPOPUP_H
+
+#include "engines/nancy/renderobject.h"
+
+namespace Nancy {
+
+struct NancyInput;
+struct UINB;
+
+namespace UI {
+
+// Nancy 10+ notebook popup. Driven by the UINB chunk: overlay image + two
+// tab buttons.
+class NotebookPopup : public RenderObject {
+public:
+	NotebookPopup();
+	~NotebookPopup() override = default;
+
+	void init() override;
+	void registerGraphics() override;
+	void handleInput(NancyInput &input);
+
+	bool isOpen() const { return _isOpen; }
+	void open();
+	void close();
+	void toggle() { if (_isOpen) close(); else open(); }
+
+private:
+	static const uint kNumTabs = 2;
+
+	void drawBackground();
+	void drawTabs();
+
+	const UINB *_uinbData;
+
+	Graphics::ManagedSurface _overlayImage; // popup background image
+
+	bool _isOpen;
+	int _activeTab; // 0..1, matching UINB::tabs index
+};
+
+} // End of namespace UI
+} // End of namespace Nancy
+
+#endif // NANCY_UI_NOTEBOOKPOPUP_H


Commit: 01f016cc4f6ee15209d48dc41561f74657009e63
    https://github.com/scummvm/scummvm/commit/01f016cc4f6ee15209d48dc41561f74657009e63
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:41+03:00

Commit Message:
NANCY: Add new fields used in the TBOX chunk in Nancy10+ games

Changed paths:
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index 5258fdbd018..a98e841c3f4 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -231,7 +231,7 @@ INV::INV(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 }
 
 TBOX::TBOX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
-	bool isVampire = g_nancy->getGameType() == Nancy::GameType::kGameTypeVampire;
+	bool isVampire = g_nancy->getGameType() == kGameTypeVampire;
 
 	readRect(*chunkStream, scrollbarSrcBounds);
 
@@ -254,6 +254,17 @@ TBOX::TBOX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 		readRectArray(*chunkStream, ornamentDests, 14);
 	}
 
+	if (g_nancy->getGameType() >= kGameTypeNancy10) {
+		chunkStream->skip(2);
+
+		maxScrollWidth = chunkStream->readSint32LE();
+		firstLineY = chunkStream->readSint32LE();
+		unknown1 = chunkStream->readSint32LE();
+		unknown2 = chunkStream->readSint32LE();
+		contentWidth = chunkStream->readSint32LE();
+		contentHeight = chunkStream->readSint32LE();
+	}
+
 	defaultFontID = chunkStream->readUint16LE();
 	defaultTextColor = chunkStream->readUint16LE();
 
@@ -279,6 +290,9 @@ TBOX::TBOX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 									(g << format.gShift) |
 									(b << format.bShift);
 
+		if (g_nancy->getGameType() >= kGameTypeNancy10)
+			chunkStream->skip(1);
+
 		r = chunkStream->readByte();
 		g = chunkStream->readByte();
 		b = chunkStream->readByte();
@@ -289,13 +303,6 @@ TBOX::TBOX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	} else {
 		textBackground = highlightTextBackground = 0;
 	}
-
-	// FIXME: Data fixup/HACK for Nancy10 and later
-	if (g_nancy->getGameType() >= kGameTypeNancy10) {
-		highlightConversationFontID = conversationFontID;
-		tabWidth = 4;
-		leftOffset = rightOffset = 0;
-	}
 }
 
 MAP::MAP(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 16aca54bd5f..5c4dc7146e1 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -177,6 +177,14 @@ struct TBOX : public EngineData {
 
 	uint32 textBackground;
 	uint32 highlightTextBackground;
+
+	// Nancy 10+ extra layout variables.
+	int32 maxScrollWidth = 0;
+	int32 firstLineY = 0; // added to the y-cursor when starting a new line
+	int32 unknown1 = 0;
+	int32 unknown2 = 0;
+	int32 contentWidth = 0;
+	int32 contentHeight = 0;
 };
 
 // Contains data about the map state. Only used in TVD and nancy1


Commit: fac3c456287d14c2a7ca4f0d5b375221c7ec4b55
    https://github.com/scummvm/scummvm/commit/fac3c456287d14c2a7ca4f0d5b375221c7ec4b55
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:45+03:00

Commit Message:
NANCY: Add stubs for some Nancy10+ ARs

- ControlUIItems
- UIPopupPrepScene
- FrameTextBox
- AddSearchLink

Changed paths:
    engines/nancy/action/arfactory.cpp
    engines/nancy/action/miscrecords.cpp
    engines/nancy/action/miscrecords.h


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 8380b66b9e1..ea8ecb96d46 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -174,12 +174,10 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 			return new InteractiveVideo();	// Moved from 26 to 28 in Nancy10
 	case 29:
 		// Nancy 10+
-		warning("ControlUIItems - not implemented yet");
-		return nullptr;
+		return new ControlUIItems();
 	case 32:
 		// Nancy 10+
-		warning("UIPopupPrepScene - not implemented yet");
-		return nullptr;
+		return new UIPopupPrepScene();
 	case 40:
 		if (g_nancy->getGameType() <= kGameTypeNancy1)
 			return new LightningOn();	// Only used in TVD
@@ -258,8 +256,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		if (g_nancy->getGameType() <= kGameTypeNancy9 && type == 75) {
 			return new TextBoxWrite();
 		} else {
-			warning("FrameTextBox - not implemented yet");
-			return nullptr;
+			return new FrameTextBox(static_cast<FrameTextBox::Variant>(type));
 		}
 	case 76:
 		return new TextboxClear();
@@ -342,8 +339,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return nullptr;
 	case 131:
 		// Nancy 10+
-		warning("AddSearchLink - not implemented yet");
-		return nullptr;
+		return new AddSearchLink();
 	case 140:
 		return new SetVolume();
 	case 148:
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index a30695a517b..488bde65de6 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -30,6 +30,7 @@
 
 #include "common/events.h"
 #include "common/config-manager.h"
+#include "nancy/ui/taskbar.h"
 
 namespace Nancy {
 namespace Action {
@@ -144,6 +145,102 @@ void TextboxClear::execute() {
 	finishExecution();
 }
 
+void FrameTextBox::readData(Common::SeekableReadStream &stream) {
+	const int16 size = stream.readSint16LE();
+
+	if (size > 10000)
+		error("FrameTextBox: too many text characters: %d", size);
+
+	if (size == -1) {
+		// CVTX-keyed lookup, same as the Nancy 6+ TextBoxWrite path.
+		Common::String stringID;
+		readFilename(stream, stringID);
+
+		const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
+		assert(autotext);
+
+		// TODO: we probably ought to be doing something more robust here
+		// to detect missing keys, but for now just return an empty string
+		// if the key isn't found.
+		_text = autotext->texts.getValOrDefault(stringID, "");
+	} else if (size > 0) {
+		char *buf = new char[size];
+		stream.read(buf, size);
+		buf[size - 1] = '\0';
+
+		assembleTextLine(buf, _text, size);
+
+		delete[] buf;
+	}
+
+	// Trailing two int16 fields: meaning differs slightly between the
+	// three opcodes that reuse this layout, but are safe to capture as a
+	// pair until UICO conversation rendering is wired up.
+	_flags = stream.readSint16LE();
+	_slot  = stream.readSint16LE();
+}
+
+void FrameTextBox::execute() {
+	// TODO: UICO-driven conversation rendering isn't ready yet; route the
+	// line into the legacy textbox so subtitles still surface (the textbox
+	// is kept off-screen on Nancy 10+ but addTextLine is harmless).
+	auto &tb = NancySceneState.getTextbox();
+	tb.clear();
+	if (!_text.empty()) {
+		tb.addTextLine(_text);
+	}
+	finishExecution();
+}
+
+void ControlUIItems::readData(Common::SeekableReadStream &stream) {
+	_uiButton = stream.readUint16LE();
+	_flagA = stream.readByte();
+	_flagB  = stream.readByte();
+	_scene1 = stream.readSint16LE();
+	_scene2 = stream.readSint16LE();
+}
+
+void ControlUIItems::execute() {
+	// TODO: finish this
+
+	NancySceneState.getTaskbar()->toggleButton(_uiButton, _flagA != 0);
+	debug("ControlUIItems: UIButton=%d, flagA=%d, flagB=%d, scene1=%d, scene2=%d", _uiButton, _flagA, _flagB, _scene1, _scene2);
+
+	finishExecution();
+}
+
+void UIPopupPrepScene::readData(Common::SeekableReadStream &stream) {
+	_uiType      = stream.readSint32LE();
+	_signalValue = stream.readSint32LE();
+}
+
+void UIPopupPrepScene::execute() {
+	// TODO: finish this
+
+	debug("UIPopupPrepScene: UIType=%d, signalValue=%d", _uiType, _signalValue);
+
+	finishExecution();
+}
+
+void AddSearchLink::readData(Common::SeekableReadStream &stream) {
+	_mode = stream.readSint16LE();
+
+	readFilename(stream, _key);
+	readFilename(stream, _value);
+
+	_extra  = stream.readSint16LE();
+	_scene1 = stream.readSint16LE();
+	_scene2 = stream.readSint16LE();
+}
+
+void AddSearchLink::execute() {
+	// TODO: finish this
+
+	debug("AddSearchLink: mode=%d, key=%s, value=%s, extra=%d, scene1=%d, scene2=%d", _mode, _key.c_str(), _value.c_str(), _extra, _scene1, _scene2);
+
+	finishExecution();
+}
+
 void BumpPlayerClock::readData(Common::SeekableReadStream &stream) {
 	_relative = stream.readByte();
 	_hours = stream.readUint16LE();
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 39afe0e0ad3..38f6ce69234 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -109,6 +109,84 @@ protected:
 	Common::String getRecordTypeName() const override { return "TextboxClear"; }
 };
 
+// Nancy 10+ replacement for TextBoxWrite. Pushes a line of conversation
+// text into the new (UICO-driven) textbox
+class FrameTextBox : public ActionRecord {
+public:
+	enum Variant {
+		kVariant74 = 74,
+		kVariant75 = 75,
+		kVariant81 = 81
+	};
+
+	FrameTextBox(Variant variant) : _variant(variant), _flags(0), _slot(0) {}
+
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	Variant _variant;
+	Common::String _text;
+	int16 _flags;
+	int16 _slot;
+
+protected:
+	Common::String getRecordTypeName() const override { return "FrameTextBox"; }
+};
+
+// Nancy 10+ opcode 29. Toggles whether one of the taskbar popups
+// (inventory / notebook / cellphone) is enabled.
+class ControlUIItems : public ActionRecord {
+public:
+	enum UIType {
+		kUITypeInventory = 1,
+		kUITypeNotebook  = 2,
+		kUITypeCellphone = 3
+	};
+
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	uint16 _uiButton = 0;
+	byte _flagA = 0;    // 0, 1 or 10
+	byte _flagB = 0;    // 0 = clear, 1 = enable+remember scene
+	int16 _scene1 = 0;  // start scene id (9999 = none)
+	int16 _scene2 = 0;  // end scene id (9999 = none)
+
+protected:
+	Common::String getRecordTypeName() const override { return "ControlUIItems"; }
+};
+
+// Nancy 10+ opcode 32. Prepares a UI popup
+class UIPopupPrepScene : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	int32 _uiType = 0;
+	int32 _signalValue = 0;
+
+protected:
+	Common::String getRecordTypeName() const override { return "UIPopupPrepScene"; }
+};
+
+// Nancy 10+ opcode 131. Pushes a new entry into either the cellphone
+// search-results list (mode 0) or the URL link list (mode 1).
+class AddSearchLink : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	int16 _mode = 0;
+	Common::String _key;
+	Common::String _value;
+	int16 _extra = 0;
+	int16 _flag = 0;
+	int16 _scene = 0;
+
+protected:
+	Common::String getRecordTypeName() const override { return "AddSearchLink"; }
+};
+
 // Changes the in-game time. Used prior to the introduction of SetPlayerClock.
 class BumpPlayerClock : public ActionRecord {
 public:


Commit: e21a82ba4170a3d8a9a977d3a42882b8cba729b7
    https://github.com/scummvm/scummvm/commit/e21a82ba4170a3d8a9a977d3a42882b8cba729b7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:46+03:00

Commit Message:
NANCY: Add handling for disabling taskbar buttons in Nancy10+

Changed paths:
    engines/nancy/action/miscrecords.cpp
    engines/nancy/state/scene.h
    engines/nancy/ui/taskbar.cpp
    engines/nancy/ui/taskbar.h


diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index 488bde65de6..4963bd7a109 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -229,14 +229,14 @@ void AddSearchLink::readData(Common::SeekableReadStream &stream) {
 	readFilename(stream, _value);
 
 	_extra  = stream.readSint16LE();
-	_scene1 = stream.readSint16LE();
-	_scene2 = stream.readSint16LE();
+	_flag = stream.readSint16LE();
+	_scene = stream.readSint16LE();
 }
 
 void AddSearchLink::execute() {
 	// TODO: finish this
 
-	debug("AddSearchLink: mode=%d, key=%s, value=%s, extra=%d, scene1=%d, scene2=%d", _mode, _key.c_str(), _value.c_str(), _extra, _scene1, _scene2);
+	debug("AddSearchLink: mode=%d, key=%s, value=%s, extra=%d, scene1=%d, scene2=%d", _mode, _key.c_str(), _value.c_str(), _extra, _flag, _scene);
 
 	finishExecution();
 }
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index ab59a2557b5..13e01bc05aa 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -173,6 +173,7 @@ public:
 	UI::Textbox &getTextbox() { return _textbox; }
 	UI::InventoryBox &getInventoryBox() { return _inventoryBox; }
 	UI::Clock *getClock();
+	UI::Taskbar *getTaskbar() { return _taskbar; }
 
 	Action::ActionManager &getActionManager() { return _actionManager; }
 
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index 00608cc9e5e..79b9a759630 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -96,6 +96,10 @@ void Taskbar::drawButton(uint index, ButtonState state) {
 	_needsRedraw = true;
 }
 
+void Taskbar::toggleButton(uint index, bool enabled) {
+	drawButton(index, enabled ? kButtonIdle : kButtonDisabled);
+}
+
 void Taskbar::handleInput(NancyInput &input) {
 	auto *taskData = GetEngineData(TASK);
 	assert(taskData);
diff --git a/engines/nancy/ui/taskbar.h b/engines/nancy/ui/taskbar.h
index b365daaf620..d3cfcf7cb65 100644
--- a/engines/nancy/ui/taskbar.h
+++ b/engines/nancy/ui/taskbar.h
@@ -42,15 +42,18 @@ public:
 	void registerGraphics() override;
 	void handleInput(NancyInput &input);
 
+	void toggleButton(uint index, bool enabled);
+
 	// Returns the index of the button that was clicked this frame, or -1
 	// if none. Cleared on the next call to handleInput().
 	int getClickedButton() const { return _clickedButton; }
 
 private:
 	enum ButtonState {
-		kButtonIdle    = 0,
-		kButtonHover   = 1,
-		kButtonPressed = 2
+		kButtonIdle     = 0,
+		kButtonHover    = 1,
+		kButtonPressed  = 2,
+		kButtonDisabled = 3
 	};
 
 	void drawButton(uint index, ButtonState state);


Commit: 7b833ce9b2deb078a01ac886ee50ee2639d281a7
    https://github.com/scummvm/scummvm/commit/7b833ce9b2deb078a01ac886ee50ee2639d281a7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:48+03:00

Commit Message:
NANCY: Properly handle saved slots when changing page in Nancy8+

Fix #16683

Changed paths:
    engines/nancy/state/loadsave.cpp


diff --git a/engines/nancy/state/loadsave.cpp b/engines/nancy/state/loadsave.cpp
index 59bd2269f5a..6e1be9d2b19 100644
--- a/engines/nancy/state/loadsave.cpp
+++ b/engines/nancy/state/loadsave.cpp
@@ -1219,7 +1219,7 @@ bool LoadSaveMenu_V2::save() {
 		_enteringNewState = true;
 	}
 
-	if ((int)_saveExists.size() < _selectedSave) {
+	if ((int)_saveExists.size() <= _selectedSave) {
 		_saveExists.resize(_selectedSave + 1, false);
 	}
 


Commit: 70c0a4ef397fe8b1de4a5828efa2dcf6881b11f6
    https://github.com/scummvm/scummvm/commit/70c0a4ef397fe8b1de4a5828efa2dcf6881b11f6
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-04-29T09:23:49+03:00

Commit Message:
NANCY: Properly handle whitespace while entering a save game

Fix #16682

Changed paths:
    engines/nancy/state/loadsave.cpp


diff --git a/engines/nancy/state/loadsave.cpp b/engines/nancy/state/loadsave.cpp
index 6e1be9d2b19..1dadf1221fe 100644
--- a/engines/nancy/state/loadsave.cpp
+++ b/engines/nancy/state/loadsave.cpp
@@ -185,7 +185,7 @@ void LoadSaveMenu::enterFilename() {
 			if (_enteredString.size()) {
 				_enteredString.deleteLastChar();
 			}
-		} else if (Common::isAlnum(key.ascii) || Common::isSpace(key.ascii)) {
+		} else if (Common::isAlnum(key.ascii) || key.ascii == ' ') {
 			_enteredString += key.ascii;
 		}
 	}




More information about the Scummvm-git-logs mailing list