[Scummvm-git-logs] scummvm master -> ec24d8369ad09c5740492be6ee72a0fee8b98c7b

alexbevi noreply at scummvm.org
Tue Sep 1 09:34:09 UTC 2026


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

Summary:
a931c4c914 HARVESTER: Add French detection entry
8a5b11049f HARVESTER: Preserve embedded TownScript quotes
4f459fd79c HARVESTER: localize quick tips from MENU.INI
377b9dc3f0 HARVESTER: localize room interaction prompts
c655b9c2e9 HARVESTER: localize dialogue menus
1c7d13ddd5 HARVESTER: localize inventory weekdays
18c388065f HARVESTER: reimplement original IDENT text presentation
465bd4a40d HARVESTER: localize main menu
ec24d8369a HARVESTER: add parental password lock


Commit: a931c4c91443f5dc9c9c8772c1cbf8a0cb6053c0
    https://github.com/scummvm/scummvm/commit/a931c4c91443f5dc9c9c8772c1cbf8a0cb6053c0
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:25:29-04:00

Commit Message:
HARVESTER: Add French detection entry

Changed paths:
    engines/harvester/detection_tables.h


diff --git a/engines/harvester/detection_tables.h b/engines/harvester/detection_tables.h
index 6cde08153c3..615de0ba07c 100644
--- a/engines/harvester/detection_tables.h
+++ b/engines/harvester/detection_tables.h
@@ -36,6 +36,15 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO2(GAMEOPTION_GORE, GAMEOPTION_SHOW_CD_CHANGE_PROMPTS)
 	},
+	{
+		"harvester",
+		nullptr,
+		AD_ENTRY1s("harvest.exe", "787e43b868ebfaca614010af3ab66b6d", 1180151),
+		Common::FR_FRA,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GAMEOPTION_GORE, GAMEOPTION_SHOW_CD_CHANGE_PROMPTS)
+	},
 	{
 		"harvester",
 		"Demo",


Commit: 8a5b11049f83fa777d240c4eb8bca18f1793ee5e
    https://github.com/scummvm/scummvm/commit/8a5b11049f83fa777d240c4eb8bca18f1793ee5e
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:25:29-04:00

Commit Message:
HARVESTER: Preserve embedded TownScript quotes

French HARVEST.SCR uses double quotes inside outer-quoted TEXT values.

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/script.cpp


diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index f97f99f92a4..98318004164 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -548,10 +548,24 @@ static void tokenizeTownScriptLine(const Common::String &line, Common::Array<Com
 		if (line[i] == '"') {
 			++i;
 			Common::String token;
-			while (i < line.size() && line[i] != '"')
+			bool hasEmbeddedQuote = false;
+			while (i < line.size()) {
+				const bool isClosingQuote = line[i] == '"' &&
+					(i + 1 == line.size() || line[i + 1] == ' ' || line[i + 1] == '\t');
+				if (isClosingQuote)
+					break;
+
+				// French TEXT records contain quotes inside underscore-separated values.
+				// The native word reader keeps them and strips only the outer quote pair.
+				if (line[i] == '"')
+					hasEmbeddedQuote = true;
 				token += line[i++];
+			}
 			if (i < line.size() && line[i] == '"')
 				++i;
+			if (hasEmbeddedQuote)
+				debugC(3, kDebugGeneral, "Harvester: TownScript token contains embedded quote value='%s'",
+					token.c_str());
 			tokens.push_back(Common::move(token));
 			continue;
 		}


Commit: 4f459fd79c95dbfa79d75f493e5bf1d9d8974895
    https://github.com/scummvm/scummvm/commit/4f459fd79c95dbfa79d75f493e5bf1d9d8974895
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:32:17-04:00

Commit Message:
HARVESTER: localize quick tips from MENU.INI

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/art.cpp
    engines/harvester/art.h
    engines/harvester/flow.cpp
    engines/harvester/flow.h
    engines/harvester/harvester.cpp
    engines/harvester/media_manager.cpp
    engines/harvester/media_manager.h
    engines/harvester/menu.cpp
    engines/harvester/menu.h


diff --git a/engines/harvester/art.cpp b/engines/harvester/art.cpp
index bd7c8006c32..776264f9fc6 100644
--- a/engines/harvester/art.cpp
+++ b/engines/harvester/art.cpp
@@ -73,6 +73,7 @@ static void logPaletteSummary(const char *label, const Common::String &path, con
 static const int kWaitX = 250;
 static const int kWaitY = 160;
 static const byte kTransparentPaletteIndex = 0;
+static const uint kQuickTipsTextboxIndex = 5;
 
 static const char *const kTextboxPaths[] = {
 	"1:/GRAPHIC/OTHER/TEXTBOX1.BM",
@@ -108,7 +109,7 @@ bool Art::load(ResourceManager &resources) {
 	       loadBitmap(resources, "1:/GRAPHIC/OTHER/HARVLOGO.BM", _logoBitmap);
 }
 
-bool Art::loadQuickTipsResources(ResourceManager &resources) {
+bool Art::loadQuickTipsResources(ResourceManager &resources, bool useTextboxPanel) {
 	_textboxes.resize(ARRAYSIZE(kTextboxPaths));
 	for (uint i = 0; i < _textboxes.size(); ++i) {
 		if (!loadBitmap(resources, kTextboxPaths[i], _textboxes[i]))
@@ -121,9 +122,21 @@ bool Art::loadQuickTipsResources(ResourceManager &resources) {
 			return false;
 	}
 
+	if (useTextboxPanel) {
+		_tipsBitmap = IndexedBitmap();
+		debugC(2, kDebugResources, "Harvester: quick tips panel '%s'", kTextboxPaths[kQuickTipsTextboxIndex]);
+		const IndexedBitmap *textbox = getQuickTipsTextboxBitmap();
+		return textbox && textbox->isValid();
+	}
+
+	debugC(2, kDebugResources, "Harvester: quick tips panel '1:/GRAPHIC/OTHER/TIPS.BM'");
 	return loadBitmap(resources, "1:/GRAPHIC/OTHER/TIPS.BM", _tipsBitmap);
 }
 
+const IndexedBitmap *Art::getQuickTipsTextboxBitmap() const {
+	return getTextboxBitmap(kQuickTipsTextboxIndex);
+}
+
 void Art::drawWaitFrame(Graphics::Screen &screen) const {
 	if (_waitFrames.empty() || !_waitFrames[0].isValid())
 		return;
diff --git a/engines/harvester/art.h b/engines/harvester/art.h
index 807ddb8dd55..d3460b09447 100644
--- a/engines/harvester/art.h
+++ b/engines/harvester/art.h
@@ -50,7 +50,7 @@ struct AbmFrame : IndexedBitmap {
 class Art {
 public:
 	bool load(ResourceManager &resources);
-	bool loadQuickTipsResources(ResourceManager &resources);
+	bool loadQuickTipsResources(ResourceManager &resources, bool useTextboxPanel);
 	void drawWaitFrame(Graphics::Screen &screen) const;
 
 	const byte *getWaitPalette() const { return _waitPalette; }
@@ -58,6 +58,7 @@ public:
 	const IndexedBitmap &getInventoryBitmap() const { return _inventoryBitmap; }
 	const IndexedBitmap &getLogoBitmap() const { return _logoBitmap; }
 	const IndexedBitmap &getTipsBitmap() const { return _tipsBitmap; }
+	const IndexedBitmap *getQuickTipsTextboxBitmap() const;
 	const Common::Array<IndexedBitmap> &getAmmoIcons() const { return _ammoIcons; }
 	const IndexedBitmap *getTextboxBitmap(uint index) const {
 		return index < _textboxes.size() ? &_textboxes[index] : nullptr;
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 5f5c5543ce0..caf3b5a08d6 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -73,13 +73,6 @@ static const char *const kTownMapBitmapPaths[] = {
 	"1:/GRAPHIC/TOWN/HARVMAP4.BM"
 };
 
-static const int kQuickTipsOverlayX = 167;
-static const int kQuickTipsOverlayY = 200;
-
-static const int kQuickTipTextX = 180;
-static const int kQuickTipTextY = 228;
-static const int kQuickTipTextWidth = 280;
-
 static const int kCursorSequence7 = 7;
 static const int kIdentTextboxX = 177;
 static const int kIdentTextboxY = 85;
@@ -112,9 +105,7 @@ static const int kTownMapEdgeThreshold = 9;
 static const int kTownMapCursorHitExtent = 5;
 
 static const byte kIdentTextColor = 0xd3;
-static const byte kTextColorNormal = 255;
 static const byte kShadowColor = 0;
-static const byte kQuickTipActionColor = 0xc3;
 static const byte kTownMapLabelColor = 0x28;
 static const int kRoomDebugLabelPaddingX = 3;
 static const int kRoomDebugLabelPaddingY = 2;
@@ -601,18 +592,6 @@ void logScenePaletteSummary(const char *label, const RoomSceneResources &scene,
 		palette[255 * 3], palette[255 * 3 + 1], palette[255 * 3 + 2]);
 }
 
-static Common::Rect quickTipsExitRect() {
-	return Common::Rect(180, 280, 238, 291);
-}
-
-static Common::Rect quickTipsNextRect() {
-	return Common::Rect(420, 280, 492, 291);
-}
-
-static Common::Rect quickTipsToggleRect() {
-	return Common::Rect(258, 280, 366, 291);
-}
-
 static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
 	if (!bitmap.isValid())
 		return;
@@ -1397,29 +1376,14 @@ static bool loadQuickTipsScene(HarvesterEngine &engine, RoomSceneResources &scen
 }
 
 static void renderQuickTipsScreen(HarvesterEngine &engine, const RoomSceneResources &scene,
-		const Common::Point &mousePos, const Common::String &tipText) {
+		const MenuTextConfig &config, const QuickTipsLayout &layout,
+		const Common::String &tipText) {
 	Graphics::Screen *screen = engine.getScreen();
-	const Art *art = engine.getArt();
-	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
-	Script *script = engine.getScript();
-	if (!screen || !art || !font || !script)
+	if (!screen)
 		return;
 
 	drawRoomScene(engine, *screen, scene, scene.targetPaletteBrightness);
-	blitBitmap(*screen, art->getTipsBitmap(), kQuickTipsOverlayX, kQuickTipsOverlayY);
-
-	drawWrappedShadowedText(*screen, *font, tipText, kQuickTipTextX, kQuickTipTextY, kQuickTipTextWidth, kTextColorNormal);
-	const Common::Rect exitRect = quickTipsExitRect();
-	const Common::Rect nextRect = quickTipsNextRect();
-	const Common::Rect toggleRect = quickTipsToggleRect();
-	const Common::String toggleLabel = script->resolveTextValue(
-		script->isQuickTipsEnabled() ? "Show_Tips_ON" : "Show_Tips_OFF");
-	drawShadowedString(*screen, *font, "Exit", exitRect.left, exitRect.top, exitRect.width(),
-		kQuickTipActionColor);
-	drawShadowedString(*screen, *font, "Next", nextRect.left, nextRect.top, nextRect.width(),
-		kQuickTipActionColor);
-	drawShadowedString(*screen, *font, toggleLabel,
-		toggleRect.left, toggleRect.top, toggleRect.width(), kQuickTipActionColor);
+	drawQuickTipsPanel(engine, config, layout, tipText);
 
 	if (engine.getRuntimeEntities())
 		engine.getRuntimeEntities()->drawCursor(*screen);
@@ -1433,6 +1397,7 @@ Flow::Flow(HarvesterEngine &engine)
 }
 
 bool Flow::load() {
+	(void)loadMenuTextConfig(_engine, _menuTextConfig);
 	return loadQuickTips() && loadMenuItems();
 }
 
@@ -1609,10 +1574,16 @@ Common::Error Flow::runQuickTips() {
 
 	resetCursorAnimationSequence();
 
+	QuickTipsLayout quickTipsLayout;
+	if (!resolveQuickTipsLayout(_engine, _menuTextConfig, quickTipsLayout))
+		return Common::kReadingFailed;
+	const Common::String &toggleLabel = _engine.getScript()->isQuickTipsEnabled()
+		? _menuTextConfig.quickTipsOnLabel : _menuTextConfig.quickTipsOffLabel;
 	debugC(1, kDebugGeneral,
-		"Harvester: quick tips labels exit='Exit' next='Next' toggle='%s'",
-		_engine.getScript()->resolveTextValue(
-			_engine.getScript()->isQuickTipsEnabled() ? "Show_Tips_ON" : "Show_Tips_OFF").c_str());
+		"Harvester: quick tips panel='%s' header='%s' exit='%s' next='%s' toggle='%s'",
+		_menuTextConfig.hasQuickTipsHeader() ? "TEXTBOX6.BM" : "TIPS.BM",
+		_menuTextConfig.quickTipsHeader.c_str(), _menuTextConfig.quickTipsExitLabel.c_str(),
+		_menuTextConfig.quickTipsNextLabel.c_str(), toggleLabel.c_str());
 
 	uint tipIndex = _engine.getRandomNumber(_quickTips.size() - 1);
 	bool needsRedraw = true;
@@ -1620,7 +1591,7 @@ Common::Error Flow::runQuickTips() {
 
 	while (!_engine.shouldQuit()) {
 		if (needsRedraw) {
-			renderQuickTipsScreen(_engine, scene, _mousePos, _quickTips[tipIndex]);
+			renderQuickTipsScreen(_engine, scene, _menuTextConfig, quickTipsLayout, _quickTips[tipIndex]);
 			needsRedraw = false;
 		}
 
@@ -1635,14 +1606,16 @@ Common::Error Flow::runQuickTips() {
 				needsRedraw = true;
 				break;
 			case Common::EVENT_LBUTTONDOWN:
-				if (quickTipsExitRect().contains(_mousePos))
+				if (quickTipsLayout.exitRect.contains(_mousePos))
 					return Common::kNoError;
 
-				if (quickTipsNextRect().contains(_mousePos)) {
+				if (quickTipsLayout.nextRect.contains(_mousePos)) {
 					tipIndex = (tipIndex + 1) % _quickTips.size();
 					needsRedraw = true;
-				} else if (quickTipsToggleRect().contains(_mousePos)) {
+				} else if (quickTipsLayout.toggleRect.contains(_mousePos)) {
 					_engine.getScript()->setQuickTipsEnabled(!_engine.getScript()->isQuickTipsEnabled());
+					if (!resolveQuickTipsLayout(_engine, _menuTextConfig, quickTipsLayout))
+						return Common::kReadingFailed;
 					needsRedraw = true;
 				}
 				break;
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index d2d62d189d1..be6038e9523 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -117,6 +117,7 @@ private:
 	HarvesterEngine &_engine;
 	Common::Array<Common::String> _quickTips;
 	Common::Array<Common::String> _menuItems;
+	MenuTextConfig _menuTextConfig;
 	Common::Point _mousePos;
 	DialogueSystem _dialogue;
 	InventorySystem _inventory;
diff --git a/engines/harvester/harvester.cpp b/engines/harvester/harvester.cpp
index 004cc839266..67fc022ac91 100644
--- a/engines/harvester/harvester.cpp
+++ b/engines/harvester/harvester.cpp
@@ -347,7 +347,11 @@ Common::Error HarvesterEngine::run() {
 	if (!_media->loadText())
 		return Common::kReadingFailed;
 
-	if (!_media->loadQuickTipsResources())
+	Flow flow(*this);
+	if (!flow.load())
+		return Common::kReadingFailed;
+
+	if (!_media->loadQuickTipsResources(flow._menuTextConfig.hasQuickTipsHeader()))
 		return Common::kReadingFailed;
 
 	// If a savegame was selected from the launcher, load it
@@ -355,13 +359,7 @@ Common::Error HarvesterEngine::run() {
 	if (saveSlot != -1)
 		(void)loadGameState(saveSlot);
 
-	Flow flow(*this);
 	_activeFlow = &flow;
-	if (!flow.load()) {
-		_activeFlow = nullptr;
-		return Common::kReadingFailed;
-	}
-
 	const Common::Error error = flow.run();
 	_activeFlow = nullptr;
 	return error;
diff --git a/engines/harvester/media_manager.cpp b/engines/harvester/media_manager.cpp
index 643034f469d..02e4a522859 100644
--- a/engines/harvester/media_manager.cpp
+++ b/engines/harvester/media_manager.cpp
@@ -261,8 +261,8 @@ bool MediaManager::loadText() {
 	return true;
 }
 
-bool MediaManager::loadQuickTipsResources() {
-	return _art && _art->loadQuickTipsResources(_resources);
+bool MediaManager::loadQuickTipsResources(bool useTextboxPanel) {
+	return _art && _art->loadQuickTipsResources(_resources, useTextboxPanel);
 }
 
 void MediaManager::drawWaitFrame() const {
diff --git a/engines/harvester/media_manager.h b/engines/harvester/media_manager.h
index 552ad72ff00..3ae159ec156 100644
--- a/engines/harvester/media_manager.h
+++ b/engines/harvester/media_manager.h
@@ -56,7 +56,7 @@ public:
 	void resetScreen(int width, int height);
 	bool loadArt();
 	bool loadText();
-	bool loadQuickTipsResources();
+	bool loadQuickTipsResources(bool useTextboxPanel);
 	void drawWaitFrame() const;
 	void applyMixerLevels(int fxLevel, int musicLevel);
 	bool isMusicPlaying() const;
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 29cfd3fd7c9..6158fcddf97 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -88,9 +88,15 @@ static const int kStartupOptionMaxLevel = 9;
 
 static const int kQuickTipsOverlayX = 167;
 static const int kQuickTipsOverlayY = 200;
+static const int kQuickTipsHeaderY = 202;
 static const int kQuickTipTextX = 180;
 static const int kQuickTipTextY = 228;
-static const int kQuickTipTextWidth = 280;
+static const int kEnglishQuickTipTextWidth = 280;
+static const int kLocalizedQuickTipTextWidth = 300;
+static const int kLocalizedQuickTipActionY = 308;
+static const int kQuickTipExitX = 180;
+static const int kQuickTipToggleX = 258;
+static const int kQuickTipNextRightInset = 8;
 static const int kConfirmDialogX = 167;
 static const int kConfirmDialogY = 200;
 static const int kConfirmPromptTextX = 0xea;
@@ -114,15 +120,6 @@ static const uint32 kPaletteFadeTickMs = 4;
 static const float kPaletteFadeStep = 0.1f;
 static const float kPaletteBrightnessBlack = 0.0f;
 
-struct RoomMenuTextConfig {
-	Common::Array<Common::String> optionItems;
-	Common::String yesLabel = "YES";
-	Common::String noLabel = "NO";
-	Common::String clickLabel = "CLICK";
-	Common::String newGamePrompt = "NEW GAME";
-	Common::String quitGamePrompt = "QUIT GAME";
-};
-
 class ScopedSceneTimerPause {
 public:
 	explicit ScopedSceneTimerPause(HarvesterEngine &engine) : _engine(engine) {
@@ -180,18 +177,6 @@ static int clampStartupOptionLevel(int level) {
 	return level;
 }
 
-static Common::Rect quickTipsExitRect() {
-	return Common::Rect(180, 280, 238, 291);
-}
-
-static Common::Rect quickTipsNextRect() {
-	return Common::Rect(420, 280, 492, 291);
-}
-
-static Common::Rect quickTipsToggleRect() {
-	return Common::Rect(258, 280, 366, 291);
-}
-
 static Common::Rect saveSlotRect(int slotIndex) {
 	const int top = kSaveSlotStartY + slotIndex * kSaveSlotStride;
 	return Common::Rect(8, top, 0x27b + 1, top + kSaveSlotStride);
@@ -255,8 +240,22 @@ static bool loadRawMenuValue(const Common::Array<byte> &data, const char *key, C
 	return false;
 }
 
-static bool loadMenuTextConfig(HarvesterEngine &engine, RoomMenuTextConfig &config) {
-	config = RoomMenuTextConfig();
+static void loadQuickTipsMenuValue(Common::INIFile &menu, const char *key, Common::String &dest) {
+	Common::String value;
+	if (!menu.getKey(key, kMenuSectionName, value) || value.empty())
+		return;
+
+	for (uint i = 0; i < value.size(); ++i) {
+		if (value[i] == '_')
+			value.setChar(' ', i);
+	}
+	dest = Common::move(value);
+}
+
+} // End of anonymous namespace
+
+bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
+	config = MenuTextConfig();
 	if (engine.isDemo()) {
 		config.clickLabel = "Click";
 		config.newGamePrompt = "START A NEW GAME?";
@@ -307,10 +306,17 @@ static bool loadMenuTextConfig(HarvesterEngine &engine, RoomMenuTextConfig &conf
 		config.newGamePrompt = Common::move(value);
 	if (loadRawMenuValue(data, "quitgame", value) && !value.empty())
 		config.quitGamePrompt = Common::move(value);
+	loadQuickTipsMenuValue(menu, "Exit", config.quickTipsExitLabel);
+	loadQuickTipsMenuValue(menu, "next", config.quickTipsNextLabel);
+	loadQuickTipsMenuValue(menu, "show_tips_on", config.quickTipsOnLabel);
+	loadQuickTipsMenuValue(menu, "show_tips_off", config.quickTipsOffLabel);
+	loadQuickTipsMenuValue(menu, "quick_tips_header", config.quickTipsHeader);
 
 	return true;
 }
 
+namespace {
+
 static void buildDisplayMainMenuItems(const Common::Array<Common::String> &source,
 		bool canSaveGame, bool canLoadGame, Common::Array<Common::String> &dest) {
 	static const char *const kBlankMenuSlot = " ";
@@ -371,7 +377,7 @@ static void renderHelpScreen(HarvesterEngine &engine, const IndexedBitmap &bitma
 	screen->update();
 }
 
-static Common::String buildTextModeSuffix(const Script &script, const RoomMenuTextConfig &config) {
+static Common::String buildTextModeSuffix(const Script &script, const MenuTextConfig &config) {
 	switch (script.getDialogueTextMode()) {
 	case kStartupDialogueTextNone:
 		return Common::String::format(" - %s", config.noLabel.c_str());
@@ -384,7 +390,7 @@ static Common::String buildTextModeSuffix(const Script &script, const RoomMenuTe
 }
 
 static Common::String buildOptionsMenuItemLabel(const Script &script,
-		const RoomMenuTextConfig &config, int index) {
+		const MenuTextConfig &config, int index) {
 	if (index < 0 || index >= (int)config.optionItems.size())
 		return Common::String();
 
@@ -502,7 +508,7 @@ static void splitMenuConfigLines(const Common::String &text, Common::Array<Commo
 static void renderOptionsMenuScreen(HarvesterEngine &engine, const IndexedBitmap &backdrop,
 		const byte *palette, float paletteBrightness,
 		const Graphics::Font &selectedFont, const Graphics::Font &unselectedFont,
-		const Art &art, const RoomMenuTextConfig &config,
+		const Art &art, const MenuTextConfig &config,
 		const IndexedBitmap &volumeBar, const IndexedBitmap &indicator, int selectedItem,
 		bool drawCursor = true) {
 	Graphics::Screen *screen = engine.getScreen();
@@ -548,29 +554,17 @@ static void renderOptionsMenuScreen(HarvesterEngine &engine, const IndexedBitmap
 
 static void renderQuickTipsOverlay(HarvesterEngine &engine, const IndexedBitmap &backdrop,
 		const byte *palette, float paletteBrightness,
+		const MenuTextConfig &config, const QuickTipsLayout &layout,
 		const Common::String &tipText) {
 	Graphics::Screen *screen = engine.getScreen();
 	const Art *art = engine.getArt();
-	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
-	Script *script = engine.getScript();
-	if (!screen || !art || !font || !script)
+	if (!screen || !art)
 		return;
 
 	applyMenuPalette(*screen, engine, palette, paletteBrightness);
 	blitBitmap(*screen, backdrop, 0, 0);
 	blitTransparentBitmap(*screen, art->getLogoBitmap(), kLogoX, kLogoY);
-	blitBitmap(*screen, art->getTipsBitmap(), kQuickTipsOverlayX, kQuickTipsOverlayY);
-
-	drawWrappedShadowedText(*screen, *font, tipText, kQuickTipTextX, kQuickTipTextY, kQuickTipTextWidth,
-		kTextColorNormal);
-	const Common::String toggleLabel = script->resolveTextValue(
-		script->isQuickTipsEnabled() ? "Show_Tips_ON" : "Show_Tips_OFF");
-	drawShadowedString(*screen, *font, "Exit", quickTipsExitRect().left, quickTipsExitRect().top,
-		quickTipsExitRect().width(), kQuickTipActionColor);
-	drawShadowedString(*screen, *font, "Next", quickTipsNextRect().left, quickTipsNextRect().top,
-		quickTipsNextRect().width(), kQuickTipActionColor);
-	drawShadowedString(*screen, *font, toggleLabel, quickTipsToggleRect().left, quickTipsToggleRect().top,
-		quickTipsToggleRect().width(), kQuickTipActionColor);
+	drawQuickTipsPanel(engine, config, layout, tipText);
 
 	if (engine.getRuntimeEntities())
 		engine.getRuntimeEntities()->drawCursor(*screen);
@@ -619,7 +613,7 @@ static void renderConfirmPromptScreen(HarvesterEngine &engine, const IndexedBitm
 		const byte *palette, float paletteBrightness, const Graphics::Font &promptFont,
 		const Graphics::Font &yesFont, const Graphics::Font &noFont,
 		const IndexedBitmap &textbox, const Common::String &promptText,
-		const RoomMenuTextConfig &config) {
+		const MenuTextConfig &config) {
 	Graphics::Screen *screen = engine.getScreen();
 	const Art *art = engine.getArt();
 	if (!screen || !art)
@@ -666,6 +660,77 @@ static int getNativeRoomMenuSelectionFromMouse(const Graphics::Font &selectedFon
 
 } // End of anonymous namespace
 
+bool resolveQuickTipsLayout(HarvesterEngine &engine, const MenuTextConfig &config,
+		QuickTipsLayout &layout) {
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	const Art *art = engine.getArt();
+	const Script *script = engine.getScript();
+	if (!font || !art || !script)
+		return false;
+
+	if (!config.hasQuickTipsHeader()) {
+		layout.exitRect = Common::Rect(180, 280, 238, 291);
+		layout.nextRect = Common::Rect(420, 280, 492, 291);
+		layout.toggleRect = Common::Rect(258, 280, 366, 291);
+		return true;
+	}
+
+	const IndexedBitmap *panel = art->getQuickTipsTextboxBitmap();
+	if (!panel || !panel->isValid())
+		return false;
+
+	const Common::String &toggleLabel = script->isQuickTipsEnabled()
+		? config.quickTipsOnLabel : config.quickTipsOffLabel;
+	auto makeLabelRect = [font](const Common::String &label, int x) {
+		const int width = MAX<int>(1, font->getStringWidth(label));
+		const int height = MAX<int>(1, font->getFontHeight());
+		return Common::Rect(x, kLocalizedQuickTipActionY,
+			x + width, kLocalizedQuickTipActionY + height);
+	};
+
+	layout.exitRect = makeLabelRect(config.quickTipsExitLabel, kQuickTipExitX);
+	layout.toggleRect = makeLabelRect(toggleLabel, kQuickTipToggleX);
+	const int nextX = kQuickTipsOverlayX + (int)panel->width
+		- font->getStringWidth(config.quickTipsNextLabel) - kQuickTipNextRightInset;
+	layout.nextRect = makeLabelRect(config.quickTipsNextLabel, nextX);
+	return true;
+}
+
+void drawQuickTipsPanel(HarvesterEngine &engine, const MenuTextConfig &config,
+		const QuickTipsLayout &layout, const Common::String &tipText) {
+	Graphics::Screen *screen = engine.getScreen();
+	const Art *art = engine.getArt();
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	const Script *script = engine.getScript();
+	if (!screen || !art || !font || !script)
+		return;
+
+	const IndexedBitmap *panel = config.hasQuickTipsHeader()
+		? art->getQuickTipsTextboxBitmap() : &art->getTipsBitmap();
+	if (!panel || !panel->isValid())
+		return;
+
+	blitBitmap(*screen, *panel, kQuickTipsOverlayX, kQuickTipsOverlayY);
+	if (config.hasQuickTipsHeader()) {
+		drawShadowedString(*screen, *font, config.quickTipsHeader,
+			kQuickTipsOverlayX, kQuickTipsHeaderY, panel->width,
+			kQuickTipActionColor, Graphics::kTextAlignCenter);
+	}
+
+	const int tipTextWidth = config.hasQuickTipsHeader()
+		? kLocalizedQuickTipTextWidth : kEnglishQuickTipTextWidth;
+	drawWrappedShadowedText(*screen, *font, tipText,
+		kQuickTipTextX, kQuickTipTextY, tipTextWidth, kTextColorNormal);
+	const Common::String &toggleLabel = script->isQuickTipsEnabled()
+		? config.quickTipsOnLabel : config.quickTipsOffLabel;
+	drawShadowedString(*screen, *font, config.quickTipsExitLabel,
+		layout.exitRect.left, layout.exitRect.top, layout.exitRect.width(), kQuickTipActionColor);
+	drawShadowedString(*screen, *font, config.quickTipsNextLabel,
+		layout.nextRect.left, layout.nextRect.top, layout.nextRect.width(), kQuickTipActionColor);
+	drawShadowedString(*screen, *font, toggleLabel,
+		layout.toggleRect.left, layout.toggleRect.top, layout.toggleRect.width(), kQuickTipActionColor);
+}
+
 MenuSystem::MenuSystem(HarvesterEngine &engine, Common::Point &mousePos,
 		const Common::Array<Common::String> &menuItems)
 	: _engine(engine), _mousePos(mousePos), _menuItems(menuItems) {
@@ -741,8 +806,7 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
 		statusMessage.clear();
 
 		if (item.equalsIgnoreCase("NEW GAME")) {
-			RoomMenuTextConfig config;
-			(void)loadMenuTextConfig(_engine, config);
+			const MenuTextConfig &config = flow._menuTextConfig;
 
 			IndexedBitmap menuBackdrop;
 			if (!captureMenuBackdrop(menuBackdrop))
@@ -986,8 +1050,7 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 
 		const Common::String &item = roomMenuItems[selectedItem];
 		if (item.equalsIgnoreCase("NEW GAME")) {
-			RoomMenuTextConfig config;
-			(void)loadMenuTextConfig(_engine, config);
+			const MenuTextConfig &config = flow._menuTextConfig;
 
 			bool confirmed = false;
 			Common::Error confirmError = runConfirmPrompt(
@@ -1589,8 +1652,7 @@ Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const
 	if (!promptFont.isValid() || !choiceFont.isValid())
 		return Common::kReadingFailed;
 
-	RoomMenuTextConfig config;
-	(void)loadMenuTextConfig(_engine, config);
+	const MenuTextConfig &config = flow._menuTextConfig;
 	const IndexedBitmap *textbox = art->getTextboxBitmap(3);
 	if (!textbox || !textbox->isValid())
 		return Common::kReadingFailed;
@@ -1660,8 +1722,7 @@ Common::Error MenuSystem::runConfirmPrompt(const IndexedBitmap &backdrop, const
 
 Common::Error MenuSystem::runQuitGameConfirm(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, Flow &flow) {
-	RoomMenuTextConfig config;
-	(void)loadMenuTextConfig(_engine, config);
+	const MenuTextConfig &config = flow._menuTextConfig;
 
 	bool confirmed = false;
 	Common::Error confirmError = runConfirmPrompt(
@@ -1692,8 +1753,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 	if (!selectedFont.isValid() || !unselectedFont.isValid())
 		return Common::kReadingFailed;
 
-	RoomMenuTextConfig config;
-	(void)loadMenuTextConfig(_engine, config);
+	const MenuTextConfig &config = flow._menuTextConfig;
 
 	IndexedBitmap volumeBar;
 	IndexedBitmap indicator;
@@ -1708,6 +1768,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 	bool showingQuickTips = false;
 	bool needsRedraw = true;
 	uint quickTipIndex = flow._quickTips.empty() ? 0 : _engine.getRandomNumber(flow._quickTips.size() - 1);
+	QuickTipsLayout quickTipsLayout;
 
 	auto persistConfig = [&]() {
 		(void)script->saveConfig();
@@ -1868,6 +1929,8 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 			break;
 		case 5:
 			if (!flow._quickTips.empty()) {
+				if (!resolveQuickTipsLayout(_engine, config, quickTipsLayout))
+					return Common::kReadingFailed;
 				showingQuickTips = true;
 				needsRedraw = true;
 			}
@@ -1887,7 +1950,7 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 		if (needsRedraw) {
 			if (showingQuickTips) {
 				renderQuickTipsOverlay(_engine, backdrop, palette, paletteBrightness,
-					flow._quickTips[quickTipIndex]);
+					config, quickTipsLayout, flow._quickTips[quickTipIndex]);
 			} else {
 				renderOptionsMenuScreen(_engine, backdrop, palette, paletteBrightness,
 					selectedFont, unselectedFont, *art, config, volumeBar, indicator, selectedItem);
@@ -1907,14 +1970,16 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 					needsRedraw = true;
 					break;
 				case Common::EVENT_LBUTTONDOWN:
-					if (quickTipsExitRect().contains(_mousePos)) {
+					if (quickTipsLayout.exitRect.contains(_mousePos)) {
 						showingQuickTips = false;
 						needsRedraw = true;
-					} else if (quickTipsNextRect().contains(_mousePos)) {
+					} else if (quickTipsLayout.nextRect.contains(_mousePos)) {
 						quickTipIndex = (quickTipIndex + 1) % flow._quickTips.size();
 						needsRedraw = true;
-					} else if (quickTipsToggleRect().contains(_mousePos)) {
+					} else if (quickTipsLayout.toggleRect.contains(_mousePos)) {
 						script->setQuickTipsEnabled(!script->isQuickTipsEnabled());
+						if (!resolveQuickTipsLayout(_engine, config, quickTipsLayout))
+							return Common::kReadingFailed;
 						persistConfig();
 						needsRedraw = true;
 					}
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index 2f4239f9290..28f321ace8e 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -37,6 +37,34 @@ namespace Harvester {
 class HarvesterEngine;
 class Flow;
 
+struct MenuTextConfig {
+	Common::Array<Common::String> optionItems;
+	Common::String yesLabel = "YES";
+	Common::String noLabel = "NO";
+	Common::String clickLabel = "CLICK";
+	Common::String newGamePrompt = "NEW GAME";
+	Common::String quitGamePrompt = "QUIT GAME";
+	Common::String quickTipsExitLabel = "Exit";
+	Common::String quickTipsNextLabel = "Next";
+	Common::String quickTipsOnLabel = "Show Tips ON";
+	Common::String quickTipsOffLabel = "Show Tips OFF";
+	Common::String quickTipsHeader;
+
+	bool hasQuickTipsHeader() const { return !quickTipsHeader.empty(); }
+};
+
+struct QuickTipsLayout {
+	Common::Rect exitRect;
+	Common::Rect nextRect;
+	Common::Rect toggleRect;
+};
+
+bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config);
+bool resolveQuickTipsLayout(HarvesterEngine &engine, const MenuTextConfig &config,
+	QuickTipsLayout &layout);
+void drawQuickTipsPanel(HarvesterEngine &engine, const MenuTextConfig &config,
+	const QuickTipsLayout &layout, const Common::String &tipText);
+
 class MenuSystem {
 public:
 	MenuSystem(HarvesterEngine &engine, Common::Point &mousePos,


Commit: 377b9dc3f0ac338c5de09c28cc3e1445cf602e14
    https://github.com/scummvm/scummvm/commit/377b9dc3f0ac338c5de09c28cc3e1445cf602e14
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:32:17-04:00

Commit Message:
HARVESTER: localize room interaction prompts

Read the French room prompt verbs from MENU.INI and keep the clicked prompt while an IDENT textbox is displayed.

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/flow.cpp
    engines/harvester/inventory.cpp
    engines/harvester/inventory.h
    engines/harvester/menu.cpp
    engines/harvester/menu.h
    engines/harvester/room.cpp
    engines/harvester/room_support.h


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index caf3b5a08d6..411cb3a9226 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1085,7 +1085,7 @@ static int resolveRoomObjectCursorSequence(const ObjectRecord &object, Script &s
 }
 
 static Common::String buildRoomObjectPrompt(const ObjectRecord &object, Script &script,
-		int cursorSequence) {
+		int cursorSequence, const MenuTextConfig &menuTextConfig) {
 	// Native room prompts come from explicit interaction metadata. Neutral scene sprites
 	// should not surface synthetic "Examine <object id>" prompts or steal hover/click focus.
 	if (cursorSequence == kCursorSequenceNeutral)
@@ -1098,19 +1098,20 @@ static Common::String buildRoomObjectPrompt(const ObjectRecord &object, Script &
 	if (cursorSequence == kCursorSequenceOperate) {
 		if (usesBareOperatePrompt(object))
 			return label;
-		return Common::String::format("Operate the %s", label.c_str());
+		return Common::String::format("%s %s", menuTextConfig.operateVerb.c_str(), label.c_str());
 	}
 	if (cursorSequence == kCursorSequencePickup)
-		return Common::String::format("Pick up the %s", label.c_str());
+		return Common::String::format("%s %s", menuTextConfig.pickUpVerb.c_str(), label.c_str());
 	if (cursorSequence == kCursorSequenceTalk)
-		return Common::String::format("Talk to %s", label.c_str());
+		return Common::String::format("%s %s", menuTextConfig.talkToVerb.c_str(), label.c_str());
 	if (cursorSequence == kCursorSequenceTransition)
 		return Common::String::format("Go to %s", label.c_str());
 
-	return Common::String::format("Examine %s", label.c_str());
+	return Common::String::format("%s %s", menuTextConfig.examineTheVerb.c_str(), label.c_str());
 }
 
-static Common::String buildRoomNpcPrompt(const NpcRecord &npc) {
+static Common::String buildRoomNpcPrompt(const NpcRecord &npc,
+		const MenuTextConfig &menuTextConfig) {
 	Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
 	for (uint i = 0; i < label.size(); ++i) {
 		if (label[i] == '_')
@@ -1119,7 +1120,7 @@ static Common::String buildRoomNpcPrompt(const NpcRecord &npc) {
 
 	if (label.empty())
 		return Common::String();
-	return Common::String::format("Talk to %s", label.c_str());
+	return Common::String::format("%s %s", menuTextConfig.talkToVerb.c_str(), label.c_str());
 }
 
 bool doesPlayerFacingMatchRegion(int playerFacing, const RegionRecord &region) {
@@ -1151,7 +1152,7 @@ RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupSta
 		const Common::Array<ObjectRecord> &sceneObjects,
 		const Common::Array<NpcRecord> &sceneNpcs,
 		const Common::Array<RegionRecord> &sceneRegions, const Common::Point &mousePos,
-		const DialogueSystem *dialogue) {
+		const MenuTextConfig &menuTextConfig, const DialogueSystem *dialogue) {
 	RoomHoverState hoverState;
 	EntityManager *entityManager = engine.getRuntimeEntities();
 	if (const Entity *playerEntity = findRoomPlayerAtPoint(engine, mousePos)) {
@@ -1162,7 +1163,7 @@ RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupSta
 	if (const NpcRecord *npc = findRoomNpcAtPoint(engine, sceneNpcs, mousePos, dialogue)) {
 		hoverState.npc = npc;
 		hoverState.cursorSequence = kCursorSequenceTalk;
-		hoverState.promptText = buildRoomNpcPrompt(*npc);
+		hoverState.promptText = buildRoomNpcPrompt(*npc, menuTextConfig);
 		return hoverState;
 	}
 
@@ -1181,7 +1182,8 @@ RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupSta
 	}
 	if (hoverState.object) {
 		hoverState.cursorSequence = resolveRoomObjectCursorSequence(*hoverState.object, *script);
-		hoverState.promptText = buildRoomObjectPrompt(*hoverState.object, *script, hoverState.cursorSequence);
+		hoverState.promptText = buildRoomObjectPrompt(
+			*hoverState.object, *script, hoverState.cursorSequence, menuTextConfig);
 		if (hoverState.cursorSequence != kCursorSequenceNeutral || !hoverState.promptText.empty())
 			return hoverState;
 		hoverState.object = nullptr;
@@ -1241,7 +1243,8 @@ static bool findRoomObjectProbePoint(HarvesterEngine &engine, const Common::Arra
 }
 
 void logStartupRoomProbe(HarvesterEngine &engine, const RoomSceneResources &scene,
-		const Common::String &entranceName, Common::Point &mousePos) {
+		const Common::String &entranceName, Common::Point &mousePos,
+		const MenuTextConfig &menuTextConfig) {
 	EntityManager *entityManager = engine.getRuntimeEntities();
 	Script *script = engine.getScript();
 	if (!entityManager || !script)
@@ -1288,7 +1291,8 @@ void logStartupRoomProbe(HarvesterEngine &engine, const RoomSceneResources &scen
 		const Common::String objectLabel = script->resolveObjectLabel(*hoveredObject);
 		ResolvedText inspectText;
 		const RoomHoverState hoverState = resolveRoomHoverState(
-			engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions, probePoint);
+			engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
+			probePoint, menuTextConfig);
 		const bool hasInteraction = script->hasObjectInteraction(*hoveredObject);
 		const bool hasInspectText = script->resolveObjectInspectText(*hoveredObject, inspectText);
 		debugC(1, kDebugRoom,
@@ -1305,7 +1309,7 @@ void logStartupRoomProbe(HarvesterEngine &engine, const RoomSceneResources &scen
 				for (int x = 48; x < 592; x += 16) {
 					const RoomHoverState candidateHover = resolveRoomHoverState(
 						engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
-						Common::Point(x, y));
+						Common::Point(x, y), menuTextConfig);
 					if (candidateHover.cursorSequence == kCursorSequenceWalk) {
 						floorProbe = Common::Point(x, y);
 						foundFloorProbe = true;
@@ -1316,7 +1320,7 @@ void logStartupRoomProbe(HarvesterEngine &engine, const RoomSceneResources &scen
 
 			const RoomHoverState floorHover = foundFloorProbe
 				? resolveRoomHoverState(engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
-					scene.sceneRegions, floorProbe)
+					scene.sceneRegions, floorProbe, menuTextConfig)
 				: RoomHoverState();
 			debugC(1, kDebugRoom,
 				"Harvester: startup probe floor room='%s' point=(%d,%d) found=%d cursor_sequence=%d prompt='%s'",
diff --git a/engines/harvester/inventory.cpp b/engines/harvester/inventory.cpp
index 5066f40a624..a3313bbb325 100644
--- a/engines/harvester/inventory.cpp
+++ b/engines/harvester/inventory.cpp
@@ -29,6 +29,7 @@
 #include "graphics/screen.h"
 #include "harvester/detection.h"
 #include "harvester/harvester.h"
+#include "harvester/menu.h"
 #include "harvester/player.h"
 #include "harvester/resources.h"
 #include "harvester/art.h"
@@ -148,15 +149,6 @@ static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, in
 		screen.format.bytesPerPixel, kTransparentPaletteIndex);
 }
 
-static Common::String buildUseItemPrompt(const Common::String &itemLabel, const Common::String &targetLabel) {
-	if (itemLabel.empty())
-		return Common::String();
-	if (targetLabel.empty())
-		return Common::String::format("Use %s on ...", itemLabel.c_str());
-
-	return Common::String::format("Use %s on %s", itemLabel.c_str(), targetLabel.c_str());
-}
-
 static Common::Rect getHotspotBounds(const ObjectRecord &object) {
 	if (object.boundsX2 > object.currentX && object.boundsY2 > object.currentY)
 		return Common::Rect(object.currentX, object.currentY, object.boundsX2 + 1, object.boundsY2 + 1);
@@ -436,8 +428,9 @@ Common::String InventorySystem::resolveSelectedLabel() const {
 	return normalizeHarvesterResourcePath(_selectedItemName);
 }
 
-Common::String InventorySystem::buildSelectedPrompt(const Common::String &targetLabel) const {
-	return buildUseItemPrompt(resolveSelectedLabel(), targetLabel);
+Common::String InventorySystem::buildSelectedPrompt(const Common::String &targetLabel,
+		const MenuTextConfig &menuTextConfig) const {
+	return buildUseItemPrompt(menuTextConfig, resolveSelectedLabel(), targetLabel);
 }
 
 void InventorySystem::selectItem(const Common::String &objectName) {
diff --git a/engines/harvester/inventory.h b/engines/harvester/inventory.h
index 70fba736bf6..ff66d8fb424 100644
--- a/engines/harvester/inventory.h
+++ b/engines/harvester/inventory.h
@@ -37,6 +37,7 @@ class Screen;
 namespace Harvester {
 
 class HarvesterEngine;
+struct MenuTextConfig;
 
 struct InventoryVisual {
 	ObjectRecord object;
@@ -63,7 +64,8 @@ public:
 	bool hasSelection() const;
 	const Common::String &getSelectedItemName() const;
 	Common::String resolveSelectedLabel() const;
-	Common::String buildSelectedPrompt(const Common::String &targetLabel) const;
+	Common::String buildSelectedPrompt(const Common::String &targetLabel,
+		const MenuTextConfig &menuTextConfig) const;
 	void selectItem(const Common::String &objectName);
 	bool toggleCombatLoadout(const ObjectRecord &object, int currentLoadout, bool &changed);
 	bool resolveSecondaryAction(const ObjectRecord &object, InventorySecondaryAction &action) const;
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 6158fcddf97..7328683d503 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -240,7 +240,7 @@ static bool loadRawMenuValue(const Common::Array<byte> &data, const char *key, C
 	return false;
 }
 
-static void loadQuickTipsMenuValue(Common::INIFile &menu, const char *key, Common::String &dest) {
+static void loadMenuDisplayValue(Common::INIFile &menu, const char *key, Common::String &dest) {
 	Common::String value;
 	if (!menu.getKey(key, kMenuSectionName, value) || value.empty())
 		return;
@@ -306,15 +306,34 @@ bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
 		config.newGamePrompt = Common::move(value);
 	if (loadRawMenuValue(data, "quitgame", value) && !value.empty())
 		config.quitGamePrompt = Common::move(value);
-	loadQuickTipsMenuValue(menu, "Exit", config.quickTipsExitLabel);
-	loadQuickTipsMenuValue(menu, "next", config.quickTipsNextLabel);
-	loadQuickTipsMenuValue(menu, "show_tips_on", config.quickTipsOnLabel);
-	loadQuickTipsMenuValue(menu, "show_tips_off", config.quickTipsOffLabel);
-	loadQuickTipsMenuValue(menu, "quick_tips_header", config.quickTipsHeader);
+	loadMenuDisplayValue(menu, "talk_to", config.talkToVerb);
+	loadMenuDisplayValue(menu, "examine", config.examineVerb);
+	loadMenuDisplayValue(menu, "examine_the", config.examineTheVerb);
+	loadMenuDisplayValue(menu, "operate", config.operateVerb);
+	loadMenuDisplayValue(menu, "pick_up", config.pickUpVerb);
+	loadMenuDisplayValue(menu, "use", config.useVerb);
+	loadMenuDisplayValue(menu, "use_on", config.useOnPreposition);
+	loadMenuDisplayValue(menu, "Exit", config.quickTipsExitLabel);
+	loadMenuDisplayValue(menu, "next", config.quickTipsNextLabel);
+	loadMenuDisplayValue(menu, "show_tips_on", config.quickTipsOnLabel);
+	loadMenuDisplayValue(menu, "show_tips_off", config.quickTipsOffLabel);
+	loadMenuDisplayValue(menu, "quick_tips_header", config.quickTipsHeader);
 
 	return true;
 }
 
+Common::String buildUseItemPrompt(const MenuTextConfig &config,
+		const Common::String &itemLabel, const Common::String &targetLabel) {
+	if (itemLabel.empty())
+		return Common::String();
+	if (targetLabel.empty())
+		return Common::String::format("%s %s %s",
+			config.useVerb.c_str(), itemLabel.c_str(), config.useOnPreposition.c_str());
+
+	return Common::String::format("%s %s %s %s", config.useVerb.c_str(), itemLabel.c_str(),
+		config.useOnPreposition.c_str(), targetLabel.c_str());
+}
+
 namespace {
 
 static void buildDisplayMainMenuItems(const Common::Array<Common::String> &source,
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index 28f321ace8e..b989b949ef9 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -44,6 +44,13 @@ struct MenuTextConfig {
 	Common::String clickLabel = "CLICK";
 	Common::String newGamePrompt = "NEW GAME";
 	Common::String quitGamePrompt = "QUIT GAME";
+	Common::String talkToVerb = "Talk to";
+	Common::String examineVerb = "Examine";
+	Common::String examineTheVerb = "Examine the";
+	Common::String operateVerb = "Operate the";
+	Common::String pickUpVerb = "Pick up the";
+	Common::String useVerb = "Use";
+	Common::String useOnPreposition = "on";
 	Common::String quickTipsExitLabel = "Exit";
 	Common::String quickTipsNextLabel = "Next";
 	Common::String quickTipsOnLabel = "Show Tips ON";
@@ -60,6 +67,8 @@ struct QuickTipsLayout {
 };
 
 bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config);
+Common::String buildUseItemPrompt(const MenuTextConfig &config,
+	const Common::String &itemLabel, const Common::String &targetLabel);
 bool resolveQuickTipsLayout(HarvesterEngine &engine, const MenuTextConfig &config,
 	QuickTipsLayout &layout);
 void drawQuickTipsPanel(HarvesterEngine &engine, const MenuTextConfig &config,
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index cadde58a66e..4518e0331cb 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -148,15 +148,6 @@ static const CftFontResource *findStartupFontByName(const HarvesterEngine &engin
 	return nullptr;
 }
 
-static Common::String buildUseItemPrompt(const Common::String &itemLabel, const Common::String &targetLabel) {
-	if (itemLabel.empty())
-		return Common::String();
-	if (targetLabel.empty())
-		return Common::String::format("Use %s on ...", itemLabel.c_str());
-
-	return Common::String::format("Use %s on %s", itemLabel.c_str(), targetLabel.c_str());
-}
-
 static Common::String resolveStartupNpcLabel(const NpcRecord &npc) {
 	Common::String label = !npc.entityInitArg.empty() ? npc.entityInitArg : npc.npcName;
 	for (uint i = 0; i < label.size(); ++i) {
@@ -485,6 +476,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 		Common::Array<RoomCombatDamagePopupState> damagePopupStates;
 		uint nextCombatEffectId = 0;
 		ResolvedText inspectText;
+		Common::String inspectPromptText;
 		bool showingInspectText = false;
 		bool inspectCanDismiss = false;
 		ResolvedText combatLoadoutStatusText;
@@ -2520,7 +2512,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 
 			const RoomHoverState hoverState = resolveRoomHoverState(
 				_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
-				_mousePos, &flow._dialogue);
+				_mousePos, flow._menuTextConfig, &flow._dialogue);
 			if (hoverState.npc) {
 				playerState.attackTargetName = hoverState.npc->npcName;
 				playerState.attackTargetClassId = kRuntimeEntityClassNpc;
@@ -3505,7 +3497,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 	captureCurrentSaveState();
 
 	if (shouldRunStartupRoomProbe())
-		logStartupRoomProbe(_engine, scene, currentRoomTarget, _mousePos);
+		logStartupRoomProbe(_engine, scene, currentRoomTarget, _mousePos, flow._menuTextConfig);
 
 	while (!_engine.shouldQuit()) {
 		if (flow.hasPendingMainMenuReturn())
@@ -3593,7 +3585,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 			RoomHoverState hoverState = suppressHover
 				? RoomHoverState()
 				: resolveRoomHoverState(_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs,
-					scene.sceneRegions, _mousePos, &flow._dialogue);
+					scene.sceneRegions, _mousePos, flow._menuTextConfig, &flow._dialogue);
 			if (!suppressHover && inventorySelectionActive && !hoverState.npc) {
 				if (ObjectRecord *selectedTarget = findSelectedInventoryRoomTarget(_mousePos))
 					hoverState.object = selectedTarget;
@@ -3624,10 +3616,11 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 					targetLabel = resolveCarryTargetLabel();
 				}
 				if (inventorySelectionActive) {
-					promptText = _inventory.buildSelectedPrompt(targetLabel);
+					promptText = _inventory.buildSelectedPrompt(targetLabel, flow._menuTextConfig);
 					_inventory.setPromptText(promptText);
 				} else {
-					promptText = buildUseItemPrompt(carriedRoomItemLabel, targetLabel);
+					promptText = buildUseItemPrompt(
+						flow._menuTextConfig, carriedRoomItemLabel, targetLabel);
 				}
 				hoverState.cursorSequence = 7;
 			} else if (_inventory.isOpen()) {
@@ -3671,6 +3664,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 
 			if (showingInspectText) {
 				drawRoomInspectText(*activeScreen, *art, *inspectFont, inspectText, useNativeInspectFont);
+				if (!inspectPromptText.empty())
+					drawRoomPrompt(*activeScreen, *promptFont, inspectPromptText, useNativePromptFont);
 			} else if (!combatLoadoutStatusText.value.empty()) {
 				drawRoomInspectText(*activeScreen, *art, *inspectFont, combatLoadoutStatusText,
 					useNativeInspectFont);
@@ -3878,6 +3873,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 						showingInspectText = false;
 						inspectCanDismiss = false;
 						inspectText = ResolvedText();
+						inspectPromptText.clear();
 						needsRedraw = true;
 					}
 					break;
@@ -3906,7 +3902,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 								"Harvester: inventory left click selecting object='%s'",
 								inventoryHover->object.objectName.c_str());
 							_inventory.selectItem(inventoryHover->object.objectName);
-							_inventory.setPromptText(_inventory.buildSelectedPrompt(Common::String()));
+							_inventory.setPromptText(_inventory.buildSelectedPrompt(
+								Common::String(), flow._menuTextConfig));
 							needsRedraw = true;
 							break;
 						}
@@ -3937,7 +3934,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 
 				const RoomHoverState hoverState = resolveRoomHoverState(
 					_engine, scene.state, scene.sceneObjects, scene.state.roomNpcs, scene.sceneRegions,
-					_mousePos, &flow._dialogue);
+					_mousePos, flow._menuTextConfig, &flow._dialogue);
 				ObjectRecord *selectedRoomTarget = nullptr;
 				if (_inventory.hasSelection() && !hoverState.npc)
 					selectedRoomTarget = findSelectedInventoryRoomTarget(_mousePos);
@@ -4098,6 +4095,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 					_engine.getScript()->markObjectIdentShown(*clickedObject);
 					if (canShowInspectText) {
 						inspectText = resolvedInspectText;
+						inspectPromptText = clickHoverState.promptText;
 						showingInspectText = true;
 						inspectCanDismiss = false;
 					} else if (hasInspectText) {
@@ -4137,6 +4135,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 						*clickedObject, interaction, scene.state.roomName)) {
 					if (canShowInspectText) {
 						inspectText = resolvedInspectText;
+						inspectPromptText = clickHoverState.promptText;
 						showingInspectText = true;
 						inspectCanDismiss = false;
 					} else if (hasInspectText) {
@@ -4164,6 +4163,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 						showingInspectText = false;
 						inspectCanDismiss = false;
 						inspectText = ResolvedText();
+						inspectPromptText.clear();
 						needsRedraw = true;
 					}
 					break;
diff --git a/engines/harvester/room_support.h b/engines/harvester/room_support.h
index 7c52d9df7f7..72793c1a92f 100644
--- a/engines/harvester/room_support.h
+++ b/engines/harvester/room_support.h
@@ -41,6 +41,7 @@ class Entity;
 class Art;
 class DialogueSystem;
 struct IndexedBitmap;
+struct MenuTextConfig;
 
 struct RoomSceneResources {
 	RoomSetupState state;
@@ -151,9 +152,11 @@ RoomHoverState resolveRoomHoverState(HarvesterEngine &engine, const RoomSetupSta
 	const Common::Array<ObjectRecord> &sceneObjects,
 	const Common::Array<NpcRecord> &npcs,
 	const Common::Array<RegionRecord> &regions,
-	const Common::Point &mousePos, const DialogueSystem *dialogue = nullptr);
+	const Common::Point &mousePos, const MenuTextConfig &menuTextConfig,
+	const DialogueSystem *dialogue = nullptr);
 void logStartupRoomProbe(HarvesterEngine &engine, const RoomSceneResources &scene,
-	const Common::String &entranceName, Common::Point &mousePos);
+	const Common::String &entranceName, Common::Point &mousePos,
+	const MenuTextConfig &menuTextConfig);
 
 } // End of namespace Harvester
 


Commit: c655b9c2e9dfc0e9206985398d8dcb797d4dfea1
    https://github.com/scummvm/scummvm/commit/c655b9c2e9dfc0e9206985398d8dcb797d4dfea1
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:32:17-04:00

Commit Message:
HARVESTER: localize dialogue menus

Bare paths in the English executable select installed files, while numbered paths select XFILE archive members, allowing the French version's KEYWORD.BM and fonts containing accented glyphs to be rendered correctly.

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/dialogue.cpp
    engines/harvester/menu.cpp
    engines/harvester/menu.h
    engines/harvester/resources.cpp


diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 2ce94627fbd..09731663070 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -111,6 +111,10 @@ public:
 	static void requestMainMenuReturn(Flow &flow) {
 		flow.requestMainMenuReturn();
 	}
+
+	static const MenuTextConfig &getMenuTextConfig(const Flow &flow) {
+		return flow._menuTextConfig;
+	}
 };
 
 namespace {
@@ -135,9 +139,11 @@ static const int kDialogueTopicStartX = 168;
 static const int kDialogueTopicEndX = 482;
 static const int kDialogueOtherStartY = 170;
 static const int kDialogueOtherEndY = 193;
+static const int kDialogueKeywordTitleYOffset = 4;
 static const int kDialogueGenericByeResponseIndex = 13;
 static const char *const kCdChangePromptPalettePath = "1:/GRAPHIC/PAL/CD1.PAL";
 static const char *const kDialogueKeywordBitmapPath = "1:/GRAPHIC/OTHER/KEYWORD.BM";
+static const char *const kDialogueLocalizedKeywordBitmapPath = "GRAPHIC/OTHER/KEYWORD.BM";
 static const char *const kDialogueGameOverBitmapPath = "1:/GRAPHIC/OTHER/GAMEOVER.BM";
 static const char *const kDialogueGameOverPalettePath = "1:/GRAPHIC/PAL/GAMEOVER.PAL";
 static const char *const kDialogueGameOverMusicPath = "SOUND/MUSIC/ANXIETY.CMP";
@@ -492,7 +498,8 @@ public:
 	RoomNpcDialogueSession(HarvesterEngine &engine, Common::Point &mousePos, Flow &flow,
 			const IndexedBitmap &backdrop, const byte *palette, float paletteBrightness,
 			const NpcRecord &npc)
-		: _engine(engine), _mousePos(mousePos), _flow(flow), _backdrop(backdrop),
+		: _engine(engine), _mousePos(mousePos), _flow(flow),
+		  _menuTextConfig(DialogueFlowAccess::getMenuTextConfig(flow)), _backdrop(backdrop),
 		  _palette(palette), _paletteBrightness(paletteBrightness), _npc(npc),
 		  _script(engine.getScript()), _text(engine.getText()), _art(engine.getArt()),
 		  _entityManager(engine.getRuntimeEntities()),
@@ -529,8 +536,15 @@ public:
 		_menuFontUsesCft = _menuCftFont.get() != nullptr;
 		_highlightFontUsesCft = _subtitleFontUsesCft;
 
-		if (!loadBitmapResource(*resources, kDialogueKeywordBitmapPath, _keywordBitmap))
+		const char *keywordBitmapPath = _menuTextConfig.hasDialogueKeywordLabel()
+			? kDialogueLocalizedKeywordBitmapPath : kDialogueKeywordBitmapPath;
+		if (!loadBitmapResource(*resources, keywordBitmapPath, _keywordBitmap))
 			return;
+		debugC(2, kDebugDialogue,
+			"Harvester: dialogue keyword panel='%s' title='%s' other='%s' responses='%s'",
+			keywordBitmapPath, _menuTextConfig.dialogueKeywordLabel.c_str(),
+			_menuTextConfig.dialogueOtherLabel.c_str(),
+			_menuTextConfig.dialogueResponsesLabel.c_str());
 
 		_genericByeTopic = _text->getDialogueResponseLine(kDialogueGenericByeResponseIndex);
 		if (_genericByeTopic.empty())
@@ -1144,6 +1158,16 @@ private:
 		}
 
 		if (topics) {
+			if (_menuTextConfig.hasDialogueKeywordLabel()) {
+				const Common::String &title = _menuTextConfig.dialogueKeywordLabel;
+				const int titleWidth = _highlightFont->getStringWidth(title);
+				const int titleX = kDialogueOverlayX +
+					MAX<int>(0, ((int)_keywordBitmap.width - titleWidth) / 2);
+				drawFontString(*_highlightFont, _highlightFontUsesCft, title, titleX,
+					kDialogueOverlayY + kDialogueKeywordTitleYOffset, titleWidth,
+					kTextColorNormal);
+			}
+
 			const int lineHeight = getDialogueTextLineHeight(*_menuFont);
 			for (uint i = 0; i < topics->size(); ++i) {
 				const bool highlighted = (int)i == hoveredTopicIndex;
@@ -1156,7 +1180,8 @@ private:
 
 			const Graphics::Font &otherFont = hoverOther ? *_highlightFont : *_menuFont;
 			const bool otherUsesCft = hoverOther ? _highlightFontUsesCft : _menuFontUsesCft;
-			drawFontString(otherFont, otherUsesCft, "Other", kDialogueOtherX, kDialogueOtherY,
+			drawFontString(otherFont, otherUsesCft, _menuTextConfig.dialogueOtherLabel,
+				kDialogueOtherX, kDialogueOtherY,
 				kDialogueOtherWidth, hoverOther ? kTextColorNormal : kTextColorHover);
 		}
 
@@ -1285,7 +1310,7 @@ private:
 		if (textboxBitmap && textboxBitmap->isValid())
 			blitTransparentBitmap(*activeScreen, *textboxBitmap, kDialogueOverlayX, kDialogueOverlayY);
 
-		const Common::String title = "Responses";
+		const Common::String &title = _menuTextConfig.dialogueResponsesLabel;
 		const Graphics::Font &titleFont = *_highlightFont;
 		const bool titleUsesCft = _highlightFontUsesCft;
 		const int titleWidth = titleFont.getStringWidth(title);
@@ -1414,6 +1439,7 @@ private:
 	HarvesterEngine &_engine;
 	Common::Point &_mousePos;
 	Flow &_flow;
+	const MenuTextConfig &_menuTextConfig;
 	const IndexedBitmap &_backdrop;
 	const byte *_palette;
 	const float _paletteBrightness;
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 7328683d503..38963ab1d00 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -313,6 +313,9 @@ bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
 	loadMenuDisplayValue(menu, "pick_up", config.pickUpVerb);
 	loadMenuDisplayValue(menu, "use", config.useVerb);
 	loadMenuDisplayValue(menu, "use_on", config.useOnPreposition);
+	loadMenuDisplayValue(menu, "other", config.dialogueOtherLabel);
+	loadMenuDisplayValue(menu, "responses", config.dialogueResponsesLabel);
+	loadMenuDisplayValue(menu, "keyword", config.dialogueKeywordLabel);
 	loadMenuDisplayValue(menu, "Exit", config.quickTipsExitLabel);
 	loadMenuDisplayValue(menu, "next", config.quickTipsNextLabel);
 	loadMenuDisplayValue(menu, "show_tips_on", config.quickTipsOnLabel);
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index b989b949ef9..cdadee4c6ee 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -51,12 +51,16 @@ struct MenuTextConfig {
 	Common::String pickUpVerb = "Pick up the";
 	Common::String useVerb = "Use";
 	Common::String useOnPreposition = "on";
+	Common::String dialogueOtherLabel = "Other";
+	Common::String dialogueResponsesLabel = "Responses";
+	Common::String dialogueKeywordLabel;
 	Common::String quickTipsExitLabel = "Exit";
 	Common::String quickTipsNextLabel = "Next";
 	Common::String quickTipsOnLabel = "Show Tips ON";
 	Common::String quickTipsOffLabel = "Show Tips OFF";
 	Common::String quickTipsHeader;
 
+	bool hasDialogueKeywordLabel() const { return !dialogueKeywordLabel.empty(); }
 	bool hasQuickTipsHeader() const { return !quickTipsHeader.empty(); }
 };
 
diff --git a/engines/harvester/resources.cpp b/engines/harvester/resources.cpp
index a87178e8923..4d97b57fe82 100644
--- a/engines/harvester/resources.cpp
+++ b/engines/harvester/resources.cpp
@@ -319,12 +319,11 @@ Common::SeekableReadStream *ResourceManager::openFile(const Common::String &path
 		}
 	} else {
 		const Common::Path memberPath(normalized, '/');
-		stream = openFromMountedArchives(memberPath);
-		if (!stream) {
-			const Common::String loosePath = resolveDiscLooseResourcePath(_currentDisc, normalized);
-			if (!loosePath.empty())
-				stream = SearchMan.createReadStreamForMember(Common::Path(loosePath, '/'));
-		}
+		const Common::String loosePath = resolveDiscLooseResourcePath(_currentDisc, normalized);
+		if (!loosePath.empty())
+			stream = SearchMan.createReadStreamForMember(Common::Path(loosePath, '/'));
+		if (!stream)
+			stream = openFromMountedArchives(memberPath);
 	}
 
 	debugC(3, kDebugResources, "Harvester: openFile(disc=%d, '%s' -> '%s') %s",


Commit: 1c7d13ddd539a173dcc8d1ddae3a6718e6a27404
    https://github.com/scummvm/scummvm/commit/1c7d13ddd539a173dcc8d1ddae3a6718e6a27404
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:32:18-04:00

Commit Message:
HARVESTER: localize inventory weekdays

The French version loads the weekday table from MENU.INI before the inventory screen indexes it by the current story day.

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/inventory.cpp
    engines/harvester/inventory.h
    engines/harvester/menu.cpp
    engines/harvester/menu.h
    engines/harvester/room.cpp


diff --git a/engines/harvester/inventory.cpp b/engines/harvester/inventory.cpp
index a3313bbb325..4c60875122e 100644
--- a/engines/harvester/inventory.cpp
+++ b/engines/harvester/inventory.cpp
@@ -229,25 +229,6 @@ static bool usesObjectActionForInventorySecondaryClick(const Common::String &obj
 	return false;
 }
 
-static Common::String resolveInventoryWeekdayLabel(int storyDayIndex) {
-	switch (storyDayIndex) {
-	case 1:
-		return "Monday";
-	case 2:
-		return "Tuesday";
-	case 3:
-		return "Wednesday";
-	case 4:
-		return "Thursday";
-	case 5:
-		return "Friday";
-	case 6:
-		return "Saturday";
-	default:
-		return Common::String();
-	}
-}
-
 static void debugLogInventoryVisual(const InventoryVisual &visual, const Common::String &spritePath) {
 	debugC(1, kDebugInventory,
 		"Harvester: inventory visual object='%s' sprite='%s' alt='%s' chosen='%s' bounds=(%d,%d)-(%d,%d) action='%s' owner='%s' text='%s'",
@@ -502,12 +483,16 @@ const Common::String &InventorySystem::getPromptText() const {
 	return _promptText;
 }
 
-Common::String InventorySystem::resolveWeekdayLabel() const {
+Common::String InventorySystem::resolveWeekdayLabel(const MenuTextConfig &menuTextConfig) const {
 	Script *script = _engine.getScript();
 	if (!script || script->isObjectInInventory(kHarvestBladeObjectName))
 		return Common::String();
 
-	return resolveInventoryWeekdayLabel(script->getCurrentStoryDayIndex());
+	const int storyDayIndex = script->getCurrentStoryDayIndex();
+	if (storyDayIndex < 1 || storyDayIndex > (int)menuTextConfig.weekdayLabels.size())
+		return Common::String();
+
+	return menuTextConfig.weekdayLabels[storyDayIndex - 1];
 }
 
 const InventoryVisual *InventorySystem::findItemAtPoint(const Common::Point &point) const {
diff --git a/engines/harvester/inventory.h b/engines/harvester/inventory.h
index ff66d8fb424..b15d33db022 100644
--- a/engines/harvester/inventory.h
+++ b/engines/harvester/inventory.h
@@ -71,7 +71,7 @@ public:
 	bool resolveSecondaryAction(const ObjectRecord &object, InventorySecondaryAction &action) const;
 	void setPromptText(const Common::String &promptText);
 	const Common::String &getPromptText() const;
-	Common::String resolveWeekdayLabel() const;
+	Common::String resolveWeekdayLabel(const MenuTextConfig &menuTextConfig) const;
 	const InventoryVisual *findItemAtPoint(const Common::Point &point) const;
 	Common::Rect getPanelBounds() const;
 	void drawOverlay(Graphics::Screen &screen) const;
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 38963ab1d00..db41b6b857f 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -34,6 +34,7 @@
 #include "graphics/fontman.h"
 #include "graphics/framelimiter.h"
 #include "harvester/cft_font.h"
+#include "harvester/detection.h"
 #include "harvester/harvester.h"
 #include "harvester/palette_utils.h"
 #include "harvester/resources.h"
@@ -65,6 +66,21 @@ static const byte kQuickTipActionColor = 0xc3;
 
 static const char *const kMenuPath = "MENU.INI";
 static const char *const kMenuSectionName = "menu";
+
+struct MenuWeekdayEntry {
+	const char *key;
+	const char *englishFallback;
+};
+
+static const MenuWeekdayEntry kMenuWeekdays[] = {
+	{ "monday", "Monday" },
+	{ "tuesday", "Tuesday" },
+	{ "wednesday", "Wednesday" },
+	{ "thursday", "Thursday" },
+	{ "friday", "Friday" },
+	{ "saturday", "Saturday" }
+};
+
 static const char *const kOptionsVolumeBitmapPath = "1:/GRAPHIC/OTHER/VOLUME.BM";
 static const char *const kOptionsIndicatorBitmapPath = "1:/GRAPHIC/OTHER/INDICATR.BM";
 static const char *const kOptionsPreviewSoundPath = "1:/SOUND/EFFECTS/WHIP2.WAV";
@@ -269,6 +285,9 @@ bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
 	config.optionItems[4] = "GORE";
 	config.optionItems[5] = "QUICK TIPS";
 	config.optionItems[6] = "PASSWORD";
+	config.weekdayLabels.resize(ARRAYSIZE(kMenuWeekdays));
+	for (uint i = 0; i < config.weekdayLabels.size(); ++i)
+		config.weekdayLabels[i] = kMenuWeekdays[i].englishFallback;
 
 	ResourceManager *resources = engine.getResources();
 	if (!resources)
@@ -316,6 +335,13 @@ bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
 	loadMenuDisplayValue(menu, "other", config.dialogueOtherLabel);
 	loadMenuDisplayValue(menu, "responses", config.dialogueResponsesLabel);
 	loadMenuDisplayValue(menu, "keyword", config.dialogueKeywordLabel);
+	for (uint i = 0; i < config.weekdayLabels.size(); ++i)
+		loadMenuDisplayValue(menu, kMenuWeekdays[i].key, config.weekdayLabels[i]);
+	debugC(2, kDebugGeneral,
+		"Harvester: inventory weekdays monday='%s' tuesday='%s' wednesday='%s' thursday='%s' friday='%s' saturday='%s'",
+		config.weekdayLabels[0].c_str(), config.weekdayLabels[1].c_str(),
+		config.weekdayLabels[2].c_str(), config.weekdayLabels[3].c_str(),
+		config.weekdayLabels[4].c_str(), config.weekdayLabels[5].c_str());
 	loadMenuDisplayValue(menu, "Exit", config.quickTipsExitLabel);
 	loadMenuDisplayValue(menu, "next", config.quickTipsNextLabel);
 	loadMenuDisplayValue(menu, "show_tips_on", config.quickTipsOnLabel);
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index cdadee4c6ee..90cb591ef75 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -54,6 +54,7 @@ struct MenuTextConfig {
 	Common::String dialogueOtherLabel = "Other";
 	Common::String dialogueResponsesLabel = "Responses";
 	Common::String dialogueKeywordLabel;
+	Common::Array<Common::String> weekdayLabels;
 	Common::String quickTipsExitLabel = "Exit";
 	Common::String quickTipsNextLabel = "Next";
 	Common::String quickTipsOnLabel = "Show Tips ON";
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 4518e0331cb..5336673910c 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -3652,7 +3652,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 			}
 			if (_inventory.isOpen()) {
 				_inventory.drawOverlay(*activeScreen);
-				drawInventoryWeekday(*activeScreen, *inventoryTooltipFont, _inventory.resolveWeekdayLabel());
+				drawInventoryWeekday(*activeScreen, *inventoryTooltipFont,
+					_inventory.resolveWeekdayLabel(flow._menuTextConfig));
 			}
 			if (inventorySelectionActive) {
 				_inventory.drawSelectedDragItem(*activeScreen, _mousePos);


Commit: 18c388065fe668924a27a7df63b32bfa326fc3f4
    https://github.com/scummvm/scummvm/commit/18c388065fe668924a27a7df63b32bfa326fc3f4
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:32:18-04:00

Commit Message:
HARVESTER: reimplement original IDENT text presentation

The CFT renderer wraps against a character count derived from the
space glyph and keeps the current interaction prompt while IDENT text is
displayed.

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/cft_font.cpp
    engines/harvester/cft_font.h
    engines/harvester/dialogue.cpp
    engines/harvester/flow.cpp
    engines/harvester/room.cpp


diff --git a/engines/harvester/cft_font.cpp b/engines/harvester/cft_font.cpp
index 41a04c08aad..7b3630aef2d 100644
--- a/engines/harvester/cft_font.cpp
+++ b/engines/harvester/cft_font.cpp
@@ -21,6 +21,7 @@
 
 #include "harvester/cft_font.h"
 
+#include "common/algorithm.h"
 #include "common/endian.h"
 #include "graphics/surface.h"
 
@@ -36,6 +37,62 @@ static const uint32 kCftGlyphCount = 256;
 
 } // End of anonymous namespace
 
+void wrapCftTextByCharacterCount(const Graphics::Font &font, const Common::String &text,
+		int width, Common::Array<Common::String> &lines) {
+	lines.clear();
+	if (text.empty())
+		return;
+
+	Common::String wrappedText;
+	for (uint i = 0; i < text.size(); ++i) {
+		if (text[i] != '\r')
+			wrappedText += text[i];
+	}
+
+	const int wrapCharsPerLine = width / MAX<int>(1, font.getCharWidth(' ') - 1);
+	if (wrapCharsPerLine <= 0) {
+		lines.push_back(Common::move(wrappedText));
+		return;
+	}
+
+	uint lineStart = 0;
+	while (lineStart < wrappedText.size()) {
+		uint lineEnd = lineStart;
+		while (lineEnd < wrappedText.size() && wrappedText[lineEnd] != '\n')
+			++lineEnd;
+
+		if (lineEnd - lineStart > (uint)wrapCharsPerLine) {
+			uint breakPos = MIN<uint>(lineStart + (uint)wrapCharsPerLine, lineEnd - 1);
+			while (breakPos > lineStart && wrappedText[breakPos] != ' ')
+				--breakPos;
+
+			if (breakPos > lineStart && wrappedText[breakPos] == ' ') {
+				wrappedText.setChar('\n', breakPos);
+				while (breakPos + 1 < wrappedText.size() && wrappedText[breakPos + 1] == ' ')
+					wrappedText.deleteChar(breakPos + 1);
+				lineStart = breakPos + 1;
+				continue;
+			}
+		}
+
+		lineStart = lineEnd + 1;
+	}
+
+	Common::String line;
+	for (uint i = 0; i < wrappedText.size(); ++i) {
+		if (wrappedText[i] == '\n') {
+			lines.push_back(Common::move(line));
+			line.clear();
+			continue;
+		}
+
+		line += wrappedText[i];
+	}
+
+	if (!line.empty() || lines.empty())
+		lines.push_back(Common::move(line));
+}
+
 HarvesterCftFont::HarvesterCftFont(const CftFontResource &resource) : _resource(resource) {
 	if (_resource.header.size() < kCftWidthTableOffset + kCftGlyphCount * 2 || _resource.atlasWidth == 0 || _resource.atlasHeight == 0)
 		return;
diff --git a/engines/harvester/cft_font.h b/engines/harvester/cft_font.h
index 63535fe8a4d..a7cd0d34241 100644
--- a/engines/harvester/cft_font.h
+++ b/engines/harvester/cft_font.h
@@ -27,6 +27,9 @@
 
 namespace Harvester {
 
+void wrapCftTextByCharacterCount(const Graphics::Font &font, const Common::String &text,
+	int width, Common::Array<Common::String> &lines);
+
 class HarvesterCftFont : public Graphics::Font {
 public:
 	explicit HarvesterCftFont(const CftFontResource &resource);
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index 09731663070..d9a51e66ca4 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -354,55 +354,7 @@ static void wrapDialogueTextLikeNative(const Graphics::Font &font, bool usesCft,
 		return;
 	}
 
-	Common::String wrappedText;
-	for (uint i = 0; i < text.size(); ++i) {
-		const char c = text[i];
-		if (c != '\r')
-			wrappedText += c;
-	}
-
-	const int wrapCharsPerLine = width / MAX<int>(1, font.getCharWidth(' ') - 1);
-	if (wrapCharsPerLine <= 0) {
-		lines.push_back(Common::move(wrappedText));
-		return;
-	}
-
-	uint lineStart = 0;
-	while (lineStart < wrappedText.size()) {
-		uint lineEnd = lineStart;
-		while (lineEnd < wrappedText.size() && wrappedText[lineEnd] != '\n')
-			++lineEnd;
-
-		if (lineEnd - lineStart > (uint)wrapCharsPerLine) {
-			uint breakPos = MIN<uint>(lineStart + (uint)wrapCharsPerLine, lineEnd - 1);
-			while (breakPos > lineStart && wrappedText[breakPos] != ' ')
-				--breakPos;
-
-			if (breakPos > lineStart && wrappedText[breakPos] == ' ') {
-				wrappedText.setChar('\n', breakPos);
-				while (breakPos + 1 < wrappedText.size() && wrappedText[breakPos + 1] == ' ')
-					wrappedText.deleteChar(breakPos + 1);
-				lineStart = breakPos + 1;
-				continue;
-			}
-		}
-
-		lineStart = lineEnd + 1;
-	}
-
-	Common::String line;
-	for (uint i = 0; i < wrappedText.size(); ++i) {
-		if (wrappedText[i] == '\n') {
-			lines.push_back(Common::move(line));
-			line.clear();
-			continue;
-		}
-
-		line += wrappedText[i];
-	}
-
-	if (!line.empty() || lines.empty())
-		lines.push_back(Common::move(line));
+	wrapCftTextByCharacterCount(font, text, width, lines);
 }
 
 static void splitDialogueMenuLine(const Common::String &line, Common::Array<Common::String> &parts) {
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 411cb3a9226..fed883ef79e 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -649,9 +649,12 @@ static void drawWrappedShadowedText(Graphics::Screen &screen, const Graphics::Fo
 }
 
 static void drawWrappedText(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
-		int x, int y, int width, byte color, int lineSpacing) {
+		int x, int y, int width, byte color, int lineSpacing, bool useCftCharacterWrapping = false) {
 	Common::Array<Common::String> lines;
-	font.wordWrapText(text, width, lines);
+	if (useCftCharacterWrapping)
+		wrapCftTextByCharacterCount(font, text, width, lines);
+	else
+		font.wordWrapText(text, width, lines);
 
 	const int lineHeight = font.getFontHeight() + lineSpacing;
 	for (uint i = 0; i < lines.size(); ++i)
@@ -1031,7 +1034,8 @@ void drawRoomInspectText(Graphics::Screen &screen, const Art &art, const Graphic
 			kIdentTextboxY + kIdentTextboxTextInsetY,
 			MAX<int>(0, (int)textbox->width - 2),
 			0,
-			kNativeIdentTextLineSpacing);
+			kNativeIdentTextLineSpacing,
+			true);
 		return;
 	}
 
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 5336673910c..65814b5bcaf 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -476,9 +476,18 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 		Common::Array<RoomCombatDamagePopupState> damagePopupStates;
 		uint nextCombatEffectId = 0;
 		ResolvedText inspectText;
-		Common::String inspectPromptText;
+		Common::String currentInteractionPromptText;
 		bool showingInspectText = false;
 		bool inspectCanDismiss = false;
+		auto beginInspectText = [&](const ObjectRecord &object, const ResolvedText &text) {
+			inspectText = text;
+			showingInspectText = true;
+			inspectCanDismiss = false;
+			debugC(2, kDebugRoom,
+				"Harvester: showing IDENT object='%s' prompt='%s' box='%s' text='%s'",
+				object.objectName.c_str(), currentInteractionPromptText.c_str(),
+				inspectText.boxName.c_str(), inspectText.value.c_str());
+		};
 		ResolvedText combatLoadoutStatusText;
 		uint32 combatLoadoutStatusHideTick = 0;
 		bool closeInventoryAfterCombatLoadoutStatus = false;
@@ -3635,6 +3644,8 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 			} else {
 				promptText = hoverState.promptText;
 			}
+			if (!showingInspectText)
+				currentInteractionPromptText = promptText;
 			if (Entity *cursor = entityManager ? entityManager->getCursorEntity() : nullptr) {
 				cursor->setAnimationSequence(
 					(showingInspectText || idleState.active || idleState.exiting ||
@@ -3665,8 +3676,9 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 
 			if (showingInspectText) {
 				drawRoomInspectText(*activeScreen, *art, *inspectFont, inspectText, useNativeInspectFont);
-				if (!inspectPromptText.empty())
-					drawRoomPrompt(*activeScreen, *promptFont, inspectPromptText, useNativePromptFont);
+				if (!currentInteractionPromptText.empty())
+					drawRoomPrompt(*activeScreen, *promptFont,
+						currentInteractionPromptText, useNativePromptFont);
 			} else if (!combatLoadoutStatusText.value.empty()) {
 				drawRoomInspectText(*activeScreen, *art, *inspectFont, combatLoadoutStatusText,
 					useNativeInspectFont);
@@ -3874,7 +3886,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 						showingInspectText = false;
 						inspectCanDismiss = false;
 						inspectText = ResolvedText();
-						inspectPromptText.clear();
 						needsRedraw = true;
 					}
 					break;
@@ -4095,10 +4106,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 					clickedObject->identShown = true;
 					_engine.getScript()->markObjectIdentShown(*clickedObject);
 					if (canShowInspectText) {
-						inspectText = resolvedInspectText;
-						inspectPromptText = clickHoverState.promptText;
-						showingInspectText = true;
-						inspectCanDismiss = false;
+						beginInspectText(*clickedObject, resolvedInspectText);
 					} else if (hasInspectText) {
 						debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
 							resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
@@ -4135,10 +4143,7 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 				if (!_engine.getScript()->resolveObjectInteraction(
 						*clickedObject, interaction, scene.state.roomName)) {
 					if (canShowInspectText) {
-						inspectText = resolvedInspectText;
-						inspectPromptText = clickHoverState.promptText;
-						showingInspectText = true;
-						inspectCanDismiss = false;
+						beginInspectText(*clickedObject, resolvedInspectText);
 					} else if (hasInspectText) {
 						debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
 							resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
@@ -4164,7 +4169,6 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 						showingInspectText = false;
 						inspectCanDismiss = false;
 						inspectText = ResolvedText();
-						inspectPromptText.clear();
 						needsRedraw = true;
 					}
 					break;


Commit: 465bd4a40d50c1a19229a8dcf0458667a0b6694c
    https://github.com/scummvm/scummvm/commit/465bd4a40d50c1a19229a8dcf0458667a0b6694c
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:32:18-04:00

Commit Message:
HARVESTER: localize main menu

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/menu.cpp


diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index db41b6b857f..6d53be55218 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -365,10 +365,19 @@ Common::String buildUseItemPrompt(const MenuTextConfig &config,
 
 namespace {
 
+enum MainMenuItem {
+	kMainMenuItemNewGame = 0,
+	kMainMenuItemSaveGame,
+	kMainMenuItemLoadGame,
+	kMainMenuItemOptions,
+	kMainMenuItemHelp,
+	kMainMenuItemQuitGame
+};
+
+static const char *const kBlankMenuSlot = " ";
+
 static void buildDisplayMainMenuItems(const Common::Array<Common::String> &source,
 		bool canSaveGame, bool canLoadGame, Common::Array<Common::String> &dest) {
-	static const char *const kBlankMenuSlot = " ";
-
 	dest = source;
 	if (dest.size() > 1 && !canSaveGame)
 		dest[1] = kBlankMenuSlot;
@@ -852,8 +861,12 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
 		const Common::String &item = mainMenuItems[selectedItem];
 		const byte *menuPalette = _hasMainMenuBackdrop ? _mainMenuBackdropPalette : art->getWaitPalette();
 		statusMessage.clear();
+		if (item.empty() || item == kBlankMenuSlot)
+			return Common::kNoError;
+		debugC(2, kDebugGeneral, "Harvester: main menu selected index=%d label='%s'",
+			selectedItem, item.c_str());
 
-		if (item.equalsIgnoreCase("NEW GAME")) {
+		if (selectedItem == kMainMenuItemNewGame) {
 			const MenuTextConfig &config = flow._menuTextConfig;
 
 			IndexedBitmap menuBackdrop;
@@ -873,7 +886,7 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
 			return runSelectedRoomLoop("START");
 		}
 
-		if (item.equalsIgnoreCase("LOAD GAME")) {
+		if (selectedItem == kMainMenuItemLoadGame) {
 			bool loadedGame = false;
 			Common::Error loadError = runLoadGameMenu(menuPalette, 1.0f, flow, loadedGame);
 			if (loadError.getCode() != Common::kNoError)
@@ -893,7 +906,7 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
 			return runSelectedRoomLoop(targetName);
 		}
 
-		if (item.equalsIgnoreCase("SAVE GAME")) {
+		if (selectedItem == kMainMenuItemSaveGame) {
 			bool savedGame = false;
 			Common::Error saveError = runSaveGameMenu(menuPalette, 1.0f, flow, savedGame);
 			(void)savedGame;
@@ -901,7 +914,7 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
 			return saveError;
 		}
 
-		if (item.equalsIgnoreCase("OPTIONS")) {
+		if (selectedItem == kMainMenuItemOptions) {
 			IndexedBitmap menuBackdrop;
 			if (!captureMenuBackdrop(menuBackdrop))
 				return Common::kReadingFailed;
@@ -910,13 +923,13 @@ Common::Error MenuSystem::runMainMenuStub(Flow &flow) {
 			return optionsError;
 		}
 
-		if (item.equalsIgnoreCase("HELP")) {
+		if (selectedItem == kMainMenuItemHelp) {
 			Common::Error helpError = runHelpScreen(menuPalette, 1.0f, flow);
 			needsRedraw = true;
 			return helpError;
 		}
 
-		if (item.equalsIgnoreCase("QUIT GAME")) {
+		if (selectedItem == kMainMenuItemQuitGame) {
 			IndexedBitmap menuBackdrop;
 			if (!captureMenuBackdrop(menuBackdrop))
 				return Common::kReadingFailed;
@@ -1097,7 +1110,12 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 			return RoomMenuActivationResult(Common::kNoError, false);
 
 		const Common::String &item = roomMenuItems[selectedItem];
-		if (item.equalsIgnoreCase("NEW GAME")) {
+		if (item.empty() || item == kBlankMenuSlot)
+			return RoomMenuActivationResult(Common::kNoError, false);
+		debugC(2, kDebugGeneral, "Harvester: room menu selected index=%d label='%s'",
+			selectedItem, item.c_str());
+
+		if (selectedItem == kMainMenuItemNewGame) {
 			const MenuTextConfig &config = flow._menuTextConfig;
 
 			bool confirmed = false;
@@ -1113,7 +1131,7 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 			return RoomMenuActivationResult(Common::kNoError, false);
 		}
 
-		if (item.equalsIgnoreCase("LOAD GAME")) {
+		if (selectedItem == kMainMenuItemLoadGame) {
 			bool loadedGame = false;
 			Common::Error loadError = runLoadGameMenu(palette, paletteBrightness, flow, loadedGame);
 			if (loadError.getCode() != Common::kNoError)
@@ -1122,7 +1140,7 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 			return RoomMenuActivationResult(Common::kNoError, loadedGame);
 		}
 
-		if (item.equalsIgnoreCase("OPTIONS")) {
+		if (selectedItem == kMainMenuItemOptions) {
 			Common::Error optionsError = runOptionsMenu(backdrop, palette, paletteBrightness, flow);
 			if (optionsError.getCode() != Common::kNoError)
 				return RoomMenuActivationResult(optionsError, false);
@@ -1130,7 +1148,7 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 			return RoomMenuActivationResult(Common::kNoError, false);
 		}
 
-		if (item.equalsIgnoreCase("HELP")) {
+		if (selectedItem == kMainMenuItemHelp) {
 			Common::Error helpError = runHelpScreen(palette, paletteBrightness, flow);
 			if (helpError.getCode() != Common::kNoError)
 				return RoomMenuActivationResult(helpError, false);
@@ -1138,7 +1156,7 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 			return RoomMenuActivationResult(Common::kNoError, false);
 		}
 
-		if (item.equalsIgnoreCase("SAVE GAME")) {
+		if (selectedItem == kMainMenuItemSaveGame) {
 			bool savedGame = false;
 			Common::Error saveError = runSaveGameMenu(palette, paletteBrightness, flow, savedGame);
 			if (saveError.getCode() != Common::kNoError)
@@ -1155,7 +1173,7 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 			return RoomMenuActivationResult(Common::kNoError, false);
 		}
 
-		if (item.equalsIgnoreCase("QUIT GAME")) {
+		if (selectedItem == kMainMenuItemQuitGame) {
 			Common::Error quitError = runQuitGameConfirm(backdrop, palette, paletteBrightness, flow);
 			if (quitError.getCode() != Common::kNoError)
 				return RoomMenuActivationResult(quitError, false);
@@ -1163,7 +1181,9 @@ Common::Error MenuSystem::runRoomMenuStub(const IndexedBitmap &backdrop, const b
 			return RoomMenuActivationResult(Common::kNoError, false);
 		}
 
-		debug(1, "Harvester: room menu item '%s' selected but not implemented", item.c_str());
+		debugC(1, kDebugGeneral,
+			"Harvester: room menu index=%d label='%s' selected but not implemented",
+			selectedItem, item.c_str());
 		return RoomMenuActivationResult(Common::kNoError, false);
 	};
 


Commit: ec24d8369ad09c5740492be6ee72a0fee8b98c7b
    https://github.com/scummvm/scummvm/commit/ec24d8369ad09c5740492be6ee72a0fee8b98c7b
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-01T05:32:19-04:00

Commit Message:
HARVESTER: add parental password lock

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/flow.cpp
    engines/harvester/menu.cpp
    engines/harvester/menu.h


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index fed883ef79e..ee4bc9c7d54 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1437,6 +1437,9 @@ bool Flow::loadDialogueSaveStateBlob(const Common::Array<byte> &blob, uint32 sav
 Common::Error Flow::run() {
 	if (!ensureCursorEntity())
 		return Common::kReadingFailed;
+	Common::Error passwordError = _menu.validateParentalPassword(*this);
+	if (passwordError.getCode() != Common::kNoError || _engine.shouldQuit())
+		return passwordError;
 
 	clearPendingMainMenuReturn();
 	clearPendingGameOverReturn();
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 6d53be55218..20c5cfece8b 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -132,6 +132,7 @@ static const int kPasswordEntryX = 0xdc;
 static const int kPasswordEntryY = 0xdc;
 static const int kPasswordEntryWidth = 0x226;
 static const int kPasswordMaxCharacters = 8;
+static const uint32 kPasswordCursorBlinkMs = 480;
 static const uint32 kPaletteFadeTickMs = 4;
 static const float kPaletteFadeStep = 0.1f;
 static const float kPaletteBrightnessBlack = 0.0f;
@@ -321,6 +322,9 @@ bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
 		config.noLabel = Common::move(value);
 	if (menu.getKey("click", kMenuSectionName, value) && !value.empty())
 		config.clickLabel = Common::move(value);
+	loadMenuDisplayValue(menu, "on", config.onLabel);
+	loadMenuDisplayValue(menu, "off", config.offLabel);
+	loadMenuDisplayValue(menu, "enter_password", config.enterPasswordLabel);
 	if (loadRawMenuValue(data, "newgame", value) && !value.empty())
 		config.newGamePrompt = Common::move(value);
 	if (loadRawMenuValue(data, "quitgame", value) && !value.empty())
@@ -455,9 +459,11 @@ static Common::String buildOptionsMenuItemLabel(const Script &script,
 	case 3:
 		return config.optionItems[index] + buildTextModeSuffix(script, config);
 	case 4:
-		return config.optionItems[index] + (script.isGoreEnabled() ? " - On" : " - Off");
+		return Common::String::format("%s - %s", config.optionItems[index].c_str(),
+			(script.isGoreEnabled() ? config.onLabel : config.offLabel).c_str());
 	case 6:
-		return config.optionItems[index] + (script.getParentalPassword().empty() ? " - Off" : " - On");
+		return Common::String::format("%s - %s", config.optionItems[index].c_str(),
+			(script.getParentalPassword().empty() ? config.offLabel : config.onLabel).c_str());
 	default:
 		return config.optionItems[index];
 	}
@@ -609,6 +615,34 @@ static void renderOptionsMenuScreen(HarvesterEngine &engine, const IndexedBitmap
 	screen->update();
 }
 
+static void renderPasswordPromptScreen(HarvesterEngine &engine, const IndexedBitmap &backdrop,
+		const byte *palette, float paletteBrightness, const Graphics::Font &titleFont,
+		const Graphics::Font &entryFont, const Art &art, const Common::String &title,
+		const Common::String &text, bool cursorVisible, bool drawLogo) {
+	Graphics::Screen *screen = engine.getScreen();
+	if (!screen)
+		return;
+
+	if (palette)
+		applyMenuPalette(*screen, engine, palette, paletteBrightness);
+	blitBitmap(*screen, backdrop, 0, 0);
+	if (drawLogo)
+		blitTransparentBitmap(*screen, art.getLogoBitmap(), kLogoX, kLogoY);
+
+	const int titleWidth = titleFont.getStringWidth(title);
+	const int titleX = (screen->w - titleWidth) / 2;
+	titleFont.drawString(screen, title, titleX, 0xa0, titleWidth, 0);
+
+	Common::String displayText = text;
+	if (cursorVisible)
+		displayText += '~';
+	entryFont.drawString(screen, displayText, kPasswordEntryX, kPasswordEntryY,
+		kPasswordEntryWidth, 0);
+
+	screen->makeAllDirty();
+	screen->update();
+}
+
 static void renderQuickTipsOverlay(HarvesterEngine &engine, const IndexedBitmap &backdrop,
 		const byte *palette, float paletteBrightness,
 		const MenuTextConfig &config, const QuickTipsLayout &layout,
@@ -1806,6 +1840,127 @@ Common::Error MenuSystem::runQuitGameConfirm(const IndexedBitmap &backdrop, cons
 	return Common::kNoError;
 }
 
+Common::Error MenuSystem::runPasswordPrompt(const IndexedBitmap &backdrop, const byte *palette,
+		float paletteBrightness, Flow &flow, bool drawLogo, Common::String &password,
+		bool &accepted) const {
+	const Art *art = _engine.getArt();
+	const CftFontResource *titleFontResource = findStartupFontByName(_engine, "HARVFONT");
+	const CftFontResource *entryFontResource = findStartupFontByName(_engine, "HARVFNT2");
+	if (!art || !titleFontResource || !entryFontResource)
+		return Common::kReadingFailed;
+
+	HarvesterCftFont titleFont(*titleFontResource);
+	HarvesterCftFont entryFont(*entryFontResource);
+	if (!titleFont.isValid() || !entryFont.isValid())
+		return Common::kReadingFailed;
+
+	password.clear();
+	accepted = false;
+	bool needsRedraw = true;
+	bool cursorVisible = true;
+	uint32 cursorToggleTicks = g_system->getMillis() + kPasswordCursorBlinkMs;
+	Graphics::FrameLimiter limiter(g_system, 60);
+
+	while (!_engine.shouldQuit()) {
+		if (needsRedraw) {
+			renderPasswordPromptScreen(_engine, backdrop, palette, paletteBrightness,
+				titleFont, entryFont, *art, flow._menuTextConfig.enterPasswordLabel,
+				password, cursorVisible, drawLogo);
+			needsRedraw = false;
+		}
+
+		Common::Event event;
+		while (g_system->getEventManager()->pollEvent(event)) {
+			Common::Error result = Common::kNoError;
+			if (flow.handleSystemEvent(event, result))
+				return result;
+
+			switch (event.type) {
+			case Common::EVENT_RBUTTONDOWN:
+				return Common::kNoError;
+			case Common::EVENT_KEYDOWN:
+				if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+					return Common::kNoError;
+				if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+						event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+					accepted = !password.empty();
+					return Common::kNoError;
+				}
+				if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
+						event.kbd.keycode == Common::KEYCODE_LEFT) {
+					if (!password.empty()) {
+						password.deleteLastChar();
+						needsRedraw = true;
+					}
+					break;
+				}
+				if (event.kbd.keycode == Common::KEYCODE_HOME) {
+					if (!password.empty()) {
+						password.clear();
+						needsRedraw = true;
+					}
+					break;
+				}
+				if (appendBoundedTextEntryCharacter(password, entryFont, event.kbd.ascii,
+						kPasswordMaxCharacters, kPasswordEntryWidth)) {
+					needsRedraw = true;
+				}
+				break;
+			default:
+				break;
+			}
+		}
+
+		const uint32 now = g_system->getMillis();
+		if ((int32)(now - cursorToggleTicks) >= 0) {
+			cursorVisible = !cursorVisible;
+			cursorToggleTicks = now + kPasswordCursorBlinkMs;
+			needsRedraw = true;
+		}
+
+		limiter.delayBeforeSwap();
+		limiter.startFrame();
+	}
+
+	return Common::kNoError;
+}
+
+Common::Error MenuSystem::validateParentalPassword(Flow &flow) {
+	Script *script = _engine.getScript();
+	Graphics::Screen *screen = _engine.getScreen();
+	if (!script || !screen)
+		return Common::kReadingFailed;
+
+	const Common::String &configuredPassword = script->getParentalPassword();
+	if (configuredPassword.empty()) {
+		debugC(2, kDebugGeneral, "Harvester: parental password startup gate bypassed state=disabled");
+		return Common::kNoError;
+	}
+
+	IndexedBitmap backdrop;
+	if (!captureScreenBackdrop(*screen, backdrop))
+		return Common::kReadingFailed;
+
+	debugC(1, kDebugGeneral, "Harvester: parental password startup gate active");
+	Common::String enteredPassword;
+	bool accepted = false;
+	Common::Error promptError = runPasswordPrompt(
+		backdrop, nullptr, 1.0f, flow, false, enteredPassword, accepted);
+	if (promptError.getCode() != Common::kNoError || _engine.shouldQuit())
+		return promptError;
+
+	if (!accepted || !configuredPassword.equalsIgnoreCase(enteredPassword)) {
+		debugC(1, kDebugGeneral,
+			"Harvester: parental password blocked startup reason=%s",
+			accepted ? "mismatch" : "empty-or-cancelled");
+		_engine.quitGame();
+		return Common::kNoError;
+	}
+
+	debugC(1, kDebugGeneral, "Harvester: parental password accepted; startup permitted");
+	return Common::kNoError;
+}
+
 Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, Flow &flow) {
 	const Art *art = _engine.getArt();
@@ -1838,8 +1993,8 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 	uint quickTipIndex = flow._quickTips.empty() ? 0 : _engine.getRandomNumber(flow._quickTips.size() - 1);
 	QuickTipsLayout quickTipsLayout;
 
-	auto persistConfig = [&]() {
-		(void)script->saveConfig();
+	auto persistConfig = [&]() -> bool {
+		return script->saveConfig();
 	};
 
 	auto updateSlider = [&](int sliderIndex) {
@@ -1896,93 +2051,45 @@ Common::Error MenuSystem::runOptionsMenu(const IndexedBitmap &backdrop, const by
 		needsRedraw = true;
 	};
 
-	auto runPasswordPrompt = [&]() -> Common::String {
-		Graphics::FrameLimiter promptLimiter(g_system, 60);
-		Common::String text;
-		bool needsPromptRedraw = true;
-		bool cursorVisible = true;
-		uint32 cursorToggleTicks = g_system->getMillis() + 400;
-
-		while (!_engine.shouldQuit()) {
-			if (needsPromptRedraw) {
-				renderOptionsMenuScreen(_engine, backdrop, palette, paletteBrightness,
-					selectedFont, unselectedFont, *art, config, volumeBar, indicator, selectedItem, false);
-
-				Graphics::Screen *screen = _engine.getScreen();
-				if (screen) {
-					const int titleWidth = selectedFont.getStringWidth("ENTER PASSWORD");
-					const int titleX = (screen->w - titleWidth) / 2;
-					selectedFont.drawString(screen, "ENTER PASSWORD", titleX, 0xa0, titleWidth, 0);
-
-					Common::String displayText = text;
-					if (cursorVisible)
-						displayText += "_";
-					unselectedFont.drawString(screen, displayText, kPasswordEntryX, kPasswordEntryY,
-						kPasswordEntryWidth, 0);
-					screen->makeAllDirty();
-					screen->update();
-				}
-
-				needsPromptRedraw = false;
-			}
-
-			Common::Event event;
-			while (g_system->getEventManager()->pollEvent(event)) {
-				Common::Error result = Common::kNoError;
-				if (flow.handleSystemEvent(event, result))
-					return Common::String();
-
-				switch (event.type) {
-				case Common::EVENT_RBUTTONDOWN:
-					return Common::String();
-				case Common::EVENT_KEYDOWN:
-					if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
-						return Common::String();
-					if (event.kbd.keycode == Common::KEYCODE_RETURN ||
-							event.kbd.keycode == Common::KEYCODE_KP_ENTER)
-						return text;
-					if (event.kbd.keycode == Common::KEYCODE_BACKSPACE) {
-						if (!text.empty()) {
-							text.deleteLastChar();
-							needsPromptRedraw = true;
-						}
-						break;
-					}
-					if (appendBoundedTextEntryCharacter(text, unselectedFont, event.kbd.ascii,
-							kPasswordMaxCharacters, kPasswordEntryWidth)) {
-						needsPromptRedraw = true;
-					}
-					break;
-				default:
-					break;
-				}
-			}
-
-			const uint32 now = g_system->getMillis();
-			if ((int32)(now - cursorToggleTicks) >= 0) {
-				cursorVisible = !cursorVisible;
-				cursorToggleTicks = now + 400;
-				needsPromptRedraw = true;
-			}
-
-			promptLimiter.delayBeforeSwap();
-			promptLimiter.startFrame();
-		}
-
-		return Common::String();
-	};
-
 	auto togglePassword = [&]() {
 		if (script->getParentalPassword().empty()) {
-			const Common::String password = runPasswordPrompt();
-			if (!password.empty()) {
+			debugC(2, kDebugGeneral, "Harvester: parental password set prompt opened");
+			Common::String password;
+			bool accepted = false;
+			Common::Error promptError = runPasswordPrompt(
+				backdrop, palette, paletteBrightness, flow, true, password, accepted);
+			if (promptError.getCode() != Common::kNoError) {
+				debugC(1, kDebugGeneral,
+					"Harvester: parental password set prompt failed error=%d",
+					(int)promptError.getCode());
+				return;
+			}
+			if (accepted) {
 				script->setParentalPassword(password);
-				persistConfig();
+				if (!persistConfig()) {
+					script->setParentalPassword(Common::String());
+					debugC(1, kDebugGeneral,
+						"Harvester: parental password enable failed");
+				} else {
+					debugC(1, kDebugGeneral,
+						"Harvester: parental password enabled");
+				}
 				needsRedraw = true;
+			} else {
+				debugC(2, kDebugGeneral,
+					"Harvester: parental password set prompt cancelled state=disabled");
 			}
 		} else {
+			const Common::String previousPassword = script->getParentalPassword();
 			script->setParentalPassword(Common::String());
-			persistConfig();
+			if (!persistConfig()) {
+				script->setParentalPassword(previousPassword);
+				debugC(1, kDebugGeneral,
+					"Harvester: parental password disable failed");
+			} else {
+				debugC(1, kDebugGeneral,
+					"Harvester: parental password disabled");
+			}
 			needsRedraw = true;
 		}
 	};
diff --git a/engines/harvester/menu.h b/engines/harvester/menu.h
index 90cb591ef75..e077f4af1c7 100644
--- a/engines/harvester/menu.h
+++ b/engines/harvester/menu.h
@@ -42,6 +42,9 @@ struct MenuTextConfig {
 	Common::String yesLabel = "YES";
 	Common::String noLabel = "NO";
 	Common::String clickLabel = "CLICK";
+	Common::String onLabel = "On";
+	Common::String offLabel = "Off";
+	Common::String enterPasswordLabel = "ENTER PASSWORD";
 	Common::String newGamePrompt = "NEW GAME";
 	Common::String quitGamePrompt = "QUIT GAME";
 	Common::String talkToVerb = "Talk to";
@@ -87,6 +90,7 @@ public:
 	Common::Error runMainMenuStub(Flow &flow);
 	Common::Error runRoomMenuStub(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, Flow &flow, bool canSaveGame);
+	Common::Error validateParentalPassword(Flow &flow);
 
 private:
 	Common::Error runLoadGameMenu(const byte *palette, float paletteBrightness,
@@ -103,6 +107,9 @@ private:
 		bool &confirmed);
 	Common::Error runQuitGameConfirm(const IndexedBitmap &backdrop, const byte *palette,
 		float paletteBrightness, Flow &flow);
+	Common::Error runPasswordPrompt(const IndexedBitmap &backdrop, const byte *palette,
+		float paletteBrightness, Flow &flow, bool drawLogo, Common::String &password,
+		bool &accepted) const;
 	Common::Error showGameOverBackdrop(Flow &flow);
 	void clearMainMenuBackdrop();
 	void renderMainMenuStub(const Common::Array<Common::String> &menuItems, int selectedItem,




More information about the Scummvm-git-logs mailing list