[Scummvm-git-logs] scummvm master -> 8400536d8ceeac57bfbfcb570f8f6b99a4799e22

alexbevi noreply at scummvm.org
Fri Sep 4 10:24:56 UTC 2026


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

Summary:
322f049413 HARVESTER: Add support for cheat codes
3ddc831b4d HARVESTER: Detect European release
7734bc481c HARVESTER: Support embedded  menu text for European release
8400536d8c HARVESTER: Match dialogue text mode input handling


Commit: 322f0494139c92d85e1f5b87046cd8def14fb48c
    https://github.com/scummvm/scummvm/commit/322f0494139c92d85e1f5b87046cd8def14fb48c
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-04T06:24:43-04:00

Commit Message:
HARVESTER: Add support for cheat codes

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
  A engines/harvester/cheats.cpp
  A engines/harvester/cheats.h
    engines/harvester/flow.cpp
    engines/harvester/flow.h
    engines/harvester/module.mk
    engines/harvester/room.cpp
    engines/harvester/room.h
    engines/harvester/script.cpp
    engines/harvester/script.h


diff --git a/engines/harvester/cheats.cpp b/engines/harvester/cheats.cpp
new file mode 100644
index 00000000000..e58bebe9158
--- /dev/null
+++ b/engines/harvester/cheats.cpp
@@ -0,0 +1,290 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "harvester/cheats.h"
+
+#include "common/keyboard.h"
+#include "common/util.h"
+#include "harvester/detection.h"
+#include "harvester/harvester.h"
+#include "harvester/script.h"
+
+namespace Harvester {
+
+static const char *const kCheatSoundPath = "1:/sound/effects/shotgun2.wav";
+static const char *const kHarvestBladeObjectName = "HARVEST_BLADE";
+
+static const char *const kTownWeaponObjects[] = {
+	"9GUN",
+	"WRENCH",
+	"PITCHFORK",
+	"SHOVEL",
+	"FIREAXE",
+	"BAT"
+};
+
+static const char *const kLodgeWeaponObjects[] = {
+	"CLEAVER",
+	"NAILGUN",
+	"SHOTGUN",
+	"FLAIL",
+	"WRENCH",
+	"SCYTHE",
+	"SWORD",
+	"CHAINSAW",
+	"BAT",
+	"POOLSTICK"
+};
+
+static const char *const kTownItemObjects[] = {
+	"BROOMKEY",
+	"GOOJF_CARD",
+	"PC_PEN",
+	"NEWSPAPER",
+	"FLATHEAD",
+	"ST_ASPRIN",
+	"ST_COUGHMED",
+	"ST_VITAMN",
+	"SHOVEL"
+};
+
+static const char *const kLodgeItemObjects[] = {
+	"WATERGLASFUL",
+	"BARCASHFIVE",
+	"CHUNK_O_MEAT",
+	"CHUNK_O_MEAT2",
+	"BARKEY",
+	"FOUNTKEY",
+	"WEEDKILLER",
+	"ARTKEY",
+	"KEWPIE_KEY",
+	"KEWPIE",
+	"CHESSKEY",
+	"DRFIREWOOD",
+	"SUPLY2_BOILKEY",
+	"STAG_PLANK1",
+	"STAG_PLANK2",
+	"STAG_PLANK3",
+	"FLAGA",
+	"FLAGB",
+	"BARCASH200",
+	"CLUE"
+};
+
+static bool addInventoryObjects(Script &script, const char *const *objectNames, uint count) {
+	bool changed = false;
+	for (uint i = 0; i < count; ++i)
+		changed = script.addRuntimeObjectToInventory(objectNames[i]) || changed;
+
+	return changed;
+}
+
+const char *CheatSystem::describe(CheatId cheatId) {
+	switch (cheatId) {
+	case kCheatMaxHealth:
+		return "max_health";
+	case kCheatAllWeapons:
+		return "all_weapons";
+	case kCheatAllItems:
+		return "all_items";
+	case kCheatLodgeLevel2:
+		return "lodge_level_2";
+	case kCheatLodgeLevel3:
+		return "lodge_level_3";
+	case kCheatLodgeFinalLevel:
+		return "lodge_final_level";
+	case kCheatInvincibility:
+		return "invincibility";
+	case kCheatLodgeLevel1:
+		return "lodge_level_1";
+	default:
+		return "unknown";
+	}
+}
+
+CheatSystem::CheatSystem(HarvesterEngine &engine) : _engine(engine) {
+}
+
+CheatInputResult CheatSystem::matchKey(uint16 ascii, CheatId &cheatId) {
+	struct CheatDefinition {
+		const char *phrase;
+		CheatId id;
+	};
+	static const CheatDefinition cheatDefinitions[] = {
+		{ "NICK", kCheatMaxHealth },
+		{ "MURDERER", kCheatAllWeapons },
+		{ "SON OF SAM", kCheatAllItems },
+		{ "BOSTON STRANGLER", kCheatLodgeLevel2 },
+		{ "HELTER SKELTER", kCheatLodgeLevel3 },
+		{ "CHARLES MANSON", kCheatLodgeFinalLevel },
+		{ "BRUCE", kCheatInvincibility },
+		{ "DUSTIN", kCheatLodgeLevel1 }
+	};
+
+	char character = 0;
+	if (ascii >= 'a' && ascii <= 'z')
+		character = (char)(ascii - 'a' + 'A');
+	else if ((ascii >= 'A' && ascii <= 'Z') || ascii == ' ')
+		character = (char)ascii;
+	else {
+		if (ascii >= 0x20 && ascii <= 0x7e)
+			_inputBuffer.clear();
+		return kCheatInputIgnored;
+	}
+
+	if (character != ' ' || (!_inputBuffer.empty() && _inputBuffer.lastChar() != ' '))
+		_inputBuffer += character;
+
+	for (uint i = 0; i < ARRAYSIZE(cheatDefinitions); ++i) {
+		if (_inputBuffer == cheatDefinitions[i].phrase) {
+			cheatId = cheatDefinitions[i].id;
+			debugC(3, kDebugGeneral, "Harvester: cheat input matched phrase='%s'",
+				cheatDefinitions[i].phrase);
+			_inputBuffer.clear();
+			return kCheatInputComplete;
+		}
+	}
+
+	for (uint start = 0; start < _inputBuffer.size(); ++start) {
+		const Common::String suffix = _inputBuffer.substr(start);
+		for (uint i = 0; i < ARRAYSIZE(cheatDefinitions); ++i) {
+			if (Common::String(cheatDefinitions[i].phrase).hasPrefix(suffix)) {
+				_inputBuffer = suffix;
+				debugC(3, kDebugGeneral, "Harvester: cheat input partial='%s'",
+					_inputBuffer.c_str());
+				return kCheatInputPartial;
+			}
+		}
+	}
+
+	_inputBuffer.clear();
+	return kCheatInputIgnored;
+}
+
+CheatInputResult CheatSystem::processKey(const Common::KeyState &key, Script &script,
+		const Common::String &roomName, InteractionResult &interaction,
+		CheatActivation &activation) {
+	interaction = InteractionResult();
+	activation = CheatActivation();
+
+	CheatId cheatId = kCheatMaxHealth;
+	const CheatInputResult inputResult = matchKey(key.ascii, cheatId);
+	if (inputResult == kCheatInputComplete)
+		execute(cheatId, script, roomName, interaction, activation);
+
+	return inputResult;
+}
+
+void CheatSystem::execute(CheatId cheatId, Script &script, const Common::String &roomName,
+		InteractionResult &interaction, CheatActivation &activation) {
+	const bool hasHarvestBlade = script.isObjectInInventory(kHarvestBladeObjectName);
+	const bool requiresHarvestBlade = cheatId == kCheatLodgeLevel2 ||
+		cheatId == kCheatLodgeLevel3 || cheatId == kCheatLodgeFinalLevel;
+	const bool requiresTown = cheatId == kCheatLodgeLevel1;
+	if ((requiresHarvestBlade && !hasHarvestBlade) || (requiresTown && hasHarvestBlade)) {
+		debugC(2, kDebugGeneral,
+			"Harvester: cheat unavailable id='%s' harvest_blade=%d",
+			describe(cheatId), hasHarvestBlade ? 1 : 0);
+		return;
+	}
+
+	activation.activated = true;
+	if (!_engine.playSound(kCheatSoundPath)) {
+		debugC(1, kDebugGeneral, "Harvester: cheat sound failed path='%s'",
+			kCheatSoundPath);
+	}
+
+	switch (cheatId) {
+	case kCheatMaxHealth:
+		activation.playerStateChanged =
+			script.setPlayerCurrentHitPoints(Script::kDefaultPlayerHitPoints);
+		break;
+	case kCheatAllWeapons:
+		if (hasHarvestBlade) {
+			activation.playerStateChanged =
+				script.setPlayerCombatResourceCount(2, 16) || activation.playerStateChanged;
+			activation.playerStateChanged =
+				script.setPlayerCombatResourceCount(3, 16) || activation.playerStateChanged;
+			activation.playerStateChanged =
+				script.setPlayerCombatResourceCount(14, 16) || activation.playerStateChanged;
+			activation.inventoryChanged = addInventoryObjects(
+				script, kLodgeWeaponObjects, ARRAYSIZE(kLodgeWeaponObjects));
+		} else {
+			activation.playerStateChanged =
+				script.setPlayerCombatResourceCount(4, 8) || activation.playerStateChanged;
+			activation.playerStateChanged =
+				script.setPlayerCombatResourceCount(5, 6) || activation.playerStateChanged;
+			activation.inventoryChanged = addInventoryObjects(
+				script, kTownWeaponObjects, ARRAYSIZE(kTownWeaponObjects));
+		}
+		break;
+	case kCheatAllItems:
+		activation.inventoryChanged = hasHarvestBlade
+			? addInventoryObjects(script, kLodgeItemObjects, ARRAYSIZE(kLodgeItemObjects))
+			: addInventoryObjects(script, kTownItemObjects, ARRAYSIZE(kTownItemObjects));
+		break;
+	case kCheatLodgeLevel2:
+		activation.hasInteraction = script.executeActionTag(
+			"FOUNTAIN_ART1", interaction, true, roomName);
+		break;
+	case kCheatLodgeLevel3:
+		activation.clearInteractionState = true;
+		activation.hasInteraction = script.executeActionTag(
+			"3RD_2_3RDNTRYR", interaction, true, roomName);
+		break;
+	case kCheatLodgeFinalLevel:
+		activation.clearInteractionState = true;
+		activation.hasInteraction = script.executeActionTag(
+			"L2_M1", interaction, true, roomName);
+		break;
+	case kCheatInvincibility:
+		activation.clearInteractionState = true;
+		_playerDamageDisabled = !_playerDamageDisabled;
+		activation.playerStateChanged = true;
+		break;
+	case kCheatLodgeLevel1:
+		activation.clearInteractionState = true;
+		activation.hasInteraction = script.executeActionTag(
+			"WARP_TO_LODGE", interaction, true, roomName);
+		break;
+	}
+
+	if ((cheatId == kCheatLodgeLevel1 || cheatId == kCheatLodgeLevel2 ||
+			cheatId == kCheatLodgeLevel3 || cheatId == kCheatLodgeFinalLevel) &&
+			!activation.hasInteraction) {
+		debugC(1, kDebugGeneral,
+			"Harvester: cheat action tag produced no interaction id='%s' room='%s'",
+			describe(cheatId), roomName.c_str());
+	}
+
+	debugC(2, kDebugGeneral,
+		"Harvester: cheat activated id='%s' inventory_changed=%d player_state_changed=%d interaction=%d damage_disabled=%d",
+		describe(cheatId), activation.inventoryChanged ? 1 : 0,
+		activation.playerStateChanged ? 1 : 0, activation.hasInteraction ? 1 : 0,
+		_playerDamageDisabled ? 1 : 0);
+}
+
+void CheatSystem::reset() {
+	_inputBuffer.clear();
+	_playerDamageDisabled = false;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/cheats.h b/engines/harvester/cheats.h
new file mode 100644
index 00000000000..fd6ab757a74
--- /dev/null
+++ b/engines/harvester/cheats.h
@@ -0,0 +1,85 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef HARVESTER_CHEATS_H
+#define HARVESTER_CHEATS_H
+
+#include "common/str.h"
+
+namespace Common {
+struct KeyState;
+}
+
+namespace Harvester {
+
+class HarvesterEngine;
+class Script;
+struct InteractionResult;
+
+enum CheatInputResult {
+	kCheatInputIgnored = 0,
+	kCheatInputPartial,
+	kCheatInputComplete
+};
+
+struct CheatActivation {
+	bool activated = false;
+	bool inventoryChanged = false;
+	bool playerStateChanged = false;
+	bool clearInteractionState = false;
+	bool hasInteraction = false;
+};
+
+class CheatSystem {
+public:
+	explicit CheatSystem(HarvesterEngine &engine);
+
+	CheatInputResult processKey(const Common::KeyState &key, Script &script,
+		const Common::String &roomName, InteractionResult &interaction,
+		CheatActivation &activation);
+	void reset();
+	bool isPlayerDamageDisabled() const { return _playerDamageDisabled; }
+
+private:
+	enum CheatId {
+		kCheatMaxHealth = 0,
+		kCheatAllWeapons,
+		kCheatAllItems,
+		kCheatLodgeLevel2,
+		kCheatLodgeLevel3,
+		kCheatLodgeFinalLevel,
+		kCheatInvincibility,
+		kCheatLodgeLevel1
+	};
+
+	CheatInputResult matchKey(uint16 ascii, CheatId &cheatId);
+	static const char *describe(CheatId cheatId);
+	void execute(CheatId cheatId, Script &script, const Common::String &roomName,
+		InteractionResult &interaction, CheatActivation &activation);
+
+	HarvesterEngine &_engine;
+	Common::String _inputBuffer;
+	bool _playerDamageDisabled = false;
+};
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_CHEATS_H
diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 4e00c58e22b..1e43bdbd38b 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -1401,7 +1401,8 @@ static void renderQuickTipsScreen(HarvesterEngine &engine, const RoomSceneResour
 
 Flow::Flow(HarvesterEngine &engine)
 	: _engine(engine), _mousePos(320, 200), _dialogue(engine, _mousePos), _inventory(engine),
-	  _menu(engine, _mousePos, _menuItems), _room(engine, _mousePos, _inventory) {
+	  _menu(engine, _mousePos, _menuItems), _cheats(engine),
+	  _room(engine, _mousePos, _inventory, _cheats) {
 }
 
 bool Flow::load() {
@@ -2022,6 +2023,7 @@ void Flow::prepareForNewGame() {
 	_engine.clearCurrentSaveRoomState();
 	if (_engine.getScript())
 		_engine.getScript()->resetRuntimeState();
+	_cheats.reset();
 	resetRoomNpcDialogueState();
 }
 
diff --git a/engines/harvester/flow.h b/engines/harvester/flow.h
index be6038e9523..bbf255d7328 100644
--- a/engines/harvester/flow.h
+++ b/engines/harvester/flow.h
@@ -26,6 +26,7 @@
 #include "common/error.h"
 #include "common/rect.h"
 #include "common/str.h"
+#include "harvester/cheats.h"
 #include "harvester/dialogue.h"
 #include "harvester/inventory.h"
 #include "harvester/menu.h"
@@ -122,6 +123,7 @@ private:
 	DialogueSystem _dialogue;
 	InventorySystem _inventory;
 	MenuSystem _menu;
+	CheatSystem _cheats;
 	RoomSystem _room;
 	InteractionResult _queuedDialogueInteraction;
 	bool _hasQueuedDialogueInteraction = false;
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 0778bb3eba3..7cbfefba482 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -3,6 +3,7 @@ MODULE := engines/harvester
 MODULE_OBJS = \
 	art.o \
 	cft_font.o \
+	cheats.o \
 	console.o \
 	dialogue.o \
 	flow.o \
diff --git a/engines/harvester/room.cpp b/engines/harvester/room.cpp
index 7852a62a8aa..cf99d81703c 100644
--- a/engines/harvester/room.cpp
+++ b/engines/harvester/room.cpp
@@ -31,6 +31,7 @@
 #include "graphics/fontman.h"
 #include "graphics/framelimiter.h"
 #include "harvester/cft_font.h"
+#include "harvester/cheats.h"
 #include "harvester/detection.h"
 #include "harvester/fst_player.h"
 #include "harvester/harvester.h"
@@ -322,8 +323,8 @@ static void setScaledRoomPalette(Graphics::Screen &screen, const byte *palette,
 }
 
 RoomSystem::RoomSystem(HarvesterEngine &engine, Common::Point &mousePos,
-		InventorySystem &inventory)
-	: _engine(engine), _mousePos(mousePos), _inventory(inventory) {
+		InventorySystem &inventory, CheatSystem &cheats)
+	: _engine(engine), _mousePos(mousePos), _inventory(inventory), _cheats(cheats) {
 }
 
 Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetName,
@@ -3043,14 +3044,18 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 							monster.monsterName.c_str(), currentFrame, script.getPlayerCurrentHitPoints());
 					} else {
 						const int playerHitPointsBefore = script.getPlayerCurrentHitPoints();
-						const bool changed = script.adjustPlayerCurrentHitPoints(-monster.damageAmount);
+						const bool damageDisabled = _cheats.isPlayerDamageDisabled();
+						const bool changed = damageDisabled
+							? false
+							: script.adjustPlayerCurrentHitPoints(-monster.damageAmount);
 						const int playerHitPointsAfter = script.getPlayerCurrentHitPoints();
 						const int damageLanded = playerHitPointsBefore - playerHitPointsAfter;
 						debugC(1, kDebugCombat,
-							"Harvester: combat monster attack hit monster='%s' frame=%d damage=%d damage_type='%s' player_hp=%d->%d changed=%d",
+							"Harvester: combat monster attack hit monster='%s' frame=%d damage=%d damage_type='%s' player_hp=%d->%d changed=%d damage_disabled=%d",
 							monster.monsterName.c_str(), currentFrame, monster.damageAmount,
 							Player::describeCombatDamageType(monster.damageType),
-							playerHitPointsBefore, playerHitPointsAfter, changed);
+							playerHitPointsBefore, playerHitPointsAfter, changed,
+							damageDisabled ? 1 : 0);
 						if (damageLanded > 0 && playerState.entity) {
 							spawnCombatDamagePopup(*playerState.entity, playerState.entity->getName(), damageLanded);
 							if (idleState.entity) {
@@ -4177,6 +4182,37 @@ Common::Error RoomSystem::runRoomLoop(Flow &flow, const Common::String &targetNa
 					break;
 				}
 
+				if (Script *script = _engine.getScript()) {
+					InteractionResult cheatInteraction;
+					CheatActivation cheatActivation;
+					const CheatInputResult cheatInputResult = _cheats.processKey(
+						event.kbd, *script, scene.state.roomName,
+						cheatInteraction, cheatActivation);
+					if (cheatInputResult != kCheatInputIgnored) {
+						if (cheatInputResult == kCheatInputComplete && cheatActivation.activated) {
+							if (cheatActivation.clearInteractionState && _inventory.clearSelection())
+								needsRedraw = true;
+							if (cheatActivation.inventoryChanged && _inventory.isOpen() &&
+									!_inventory.refresh()) {
+								return Common::kReadingFailed;
+							}
+							if (cheatActivation.hasInteraction) {
+								if (_inventory.isOpen())
+									(void)_inventory.close();
+								bool didTransition = false;
+								Common::Error cheatError = interactionProcessor.handleInteractionResult(
+									cheatInteraction, didTransition, Common::String());
+								if (cheatError.getCode() != Common::kNoError)
+									return cheatError;
+								if (flow.hasPendingMainMenuReturn())
+									return Common::kNoError;
+							}
+							needsRedraw = true;
+						}
+						break;
+					}
+				}
+
 				if (_inventory.isOpen()) {
 					if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
 						const bool clearedSelection = _inventory.clearSelection();
diff --git a/engines/harvester/room.h b/engines/harvester/room.h
index 3585a04f4e2..0f3cab9fc48 100644
--- a/engines/harvester/room.h
+++ b/engines/harvester/room.h
@@ -29,12 +29,14 @@
 namespace Harvester {
 
 class HarvesterEngine;
+class CheatSystem;
 class Flow;
 class InventorySystem;
 
 class RoomSystem {
 public:
-	RoomSystem(HarvesterEngine &engine, Common::Point &mousePos, InventorySystem &inventory);
+	RoomSystem(HarvesterEngine &engine, Common::Point &mousePos, InventorySystem &inventory,
+		CheatSystem &cheats);
 
 	Common::Error runRoomLoop(Flow &flow, const Common::String &targetName, bool targetIsRoomName);
 
@@ -42,6 +44,7 @@ private:
 	HarvesterEngine &_engine;
 	Common::Point &_mousePos;
 	InventorySystem &_inventory;
+	CheatSystem &_cheats;
 };
 
 } // End of namespace Harvester
diff --git a/engines/harvester/script.cpp b/engines/harvester/script.cpp
index 98318004164..ea889b4ce03 100644
--- a/engines/harvester/script.cpp
+++ b/engines/harvester/script.cpp
@@ -2422,6 +2422,18 @@ bool Script::consumePlayerCombatResourceUnit(int loadout) {
 	return adjustPlayerCombatResourceCount(loadout, -1, maxCount, "PLAYER_ATTACK");
 }
 
+bool Script::setPlayerCombatResourceCount(int loadout, int count) {
+	int *currentCount = getPlayerCombatResourceCountPtr(loadout);
+	const int maxCount = resolveCombatResourceDisplayMax(loadout);
+	if (!currentCount || maxCount <= 0)
+		return false;
+
+	const int clampedCount = CLIP<int>(count, 0, maxCount);
+	const bool changed = *currentCount != clampedCount;
+	*currentCount = clampedCount;
+	return changed;
+}
+
 int *Script::getPlayerCombatResourceCountPtr(int loadout) {
 	switch (loadout) {
 	case 2:
diff --git a/engines/harvester/script.h b/engines/harvester/script.h
index 0d253f7375f..1d721ec50dd 100644
--- a/engines/harvester/script.h
+++ b/engines/harvester/script.h
@@ -422,7 +422,9 @@ public:
 	int getPlayerCombatResourceCount(int loadout) const;
 	bool consumePlayerCombatResourceUnit(int loadout);
 	bool isPlayerControlPaused() const { return _playerControlPaused; }
+	bool setPlayerCurrentHitPoints(int hitPoints);
 	bool adjustPlayerCurrentHitPoints(int delta);
+	bool setPlayerCombatResourceCount(int loadout, int count);
 	bool setPlayerCombatLoadout(int loadout);
 	bool setPlayerControlPaused(bool paused);
 	bool syncRuntimeAnimState(const Common::String &animName, bool active, bool visible, int currentFrame);
@@ -476,7 +478,6 @@ private:
 	bool probePickupBlockingCommandChain(const Common::String &initialTag,
 		const Common::String &contextName, InteractionResult &result, uint recursionDepth) const;
 	bool hasActionableCommandChain(const Common::String &initialTag) const;
-	bool setPlayerCurrentHitPoints(int hitPoints);
 	int *getPlayerCombatResourceCountPtr(int loadout);
 	const int *getPlayerCombatResourceCountPtr(int loadout) const;
 	bool adjustPlayerCombatResourceCount(int loadout, int delta, int maxCount,


Commit: 3ddc831b4d4ba0226d49041ff224cffc9d1dd867
    https://github.com/scummvm/scummvm/commit/3ddc831b4d4ba0226d49041ff224cffc9d1dd867
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-04T06:24:43-04:00

Commit Message:
HARVESTER: Detect European release

Changed paths:
    engines/harvester/detection_tables.h


diff --git a/engines/harvester/detection_tables.h b/engines/harvester/detection_tables.h
index 615de0ba07c..0c436a519f5 100644
--- a/engines/harvester/detection_tables.h
+++ b/engines/harvester/detection_tables.h
@@ -36,6 +36,7 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO2(GAMEOPTION_GORE, GAMEOPTION_SHOW_CD_CHANGE_PROMPTS)
 	},
+	// French version
 	{
 		"harvester",
 		nullptr,
@@ -45,6 +46,16 @@ const ADGameDescription gameDescriptions[] = {
 		ADGF_UNSTABLE,
 		GUIO2(GAMEOPTION_GORE, GAMEOPTION_SHOW_CD_CHANGE_PROMPTS)
 	},
+	// European version
+	{
+		"harvester",
+		"European Release",
+		AD_ENTRY1s("harvest.exe", "787e43b868ebfaca614010af3ab66b6d", 1166887),
+		Common::EN_ANY,
+		Common::kPlatformDOS,
+		ADGF_UNSTABLE,
+		GUIO2(GAMEOPTION_GORE, GAMEOPTION_SHOW_CD_CHANGE_PROMPTS)
+	},
 	{
 		"harvester",
 		"Demo",


Commit: 7734bc481cd57721b5db76d9b69be36c8ffa94e7
    https://github.com/scummvm/scummvm/commit/7734bc481cd57721b5db76d9b69be36c8ffa94e7
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-04T06:24:43-04:00

Commit Message:
HARVESTER: Support embedded  menu text for European release

Assisted-by: Codex:gpt-5.6-sol

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


diff --git a/engines/harvester/flow.cpp b/engines/harvester/flow.cpp
index 1e43bdbd38b..00bf6597208 100644
--- a/engines/harvester/flow.cpp
+++ b/engines/harvester/flow.cpp
@@ -56,7 +56,7 @@ namespace {
 static const char *const kQuickTipsPath = "ADJHEAD.RCS";
 static const char *const kMenuPath = "MENU.INI";
 static const char *const kMenuSectionName = "menu";
-static const char *const kDemoMenuItems[] = {
+static const char *const kBuiltInMenuItems[] = {
 	"NEW GAME",
 	"SAVE GAME",
 	"LOAD GAME",
@@ -1518,16 +1518,11 @@ bool Flow::loadMenuItems() {
 
 	Common::Array<byte> data;
 	if (!_engine.getResources()->loadFile(kMenuPath, data)) {
-		if (_engine.isDemo()) {
-			for (const char *item : kDemoMenuItems)
-				_menuItems.push_back(item);
-			debugC(1, kDebugGeneral,
-				"Harvester: using %u built-in DOS demo menu items",
-				(uint)_menuItems.size());
-			return true;
-		}
-
-		warning("Harvester: unable to load startup menu '%s'", kMenuPath);
+		for (const char *item : kBuiltInMenuItems)
+			_menuItems.push_back(item);
+		debugC(2, kDebugGeneral,
+			"Harvester: using %u built-in startup menu items because '%s' is unavailable",
+			(uint)_menuItems.size(), kMenuPath);
 		return true;
 	}
 
diff --git a/engines/harvester/menu.cpp b/engines/harvester/menu.cpp
index 20c5cfece8b..1fd4fc5c7a1 100644
--- a/engines/harvester/menu.cpp
+++ b/engines/harvester/menu.cpp
@@ -102,6 +102,18 @@ static const int kOptionsSliderHeight = 0x1e;
 static const int kOptionsItemCount = 7;
 static const int kStartupOptionMaxLevel = 9;
 
+// The first three strings retain the padding from the European executable's
+// fixed-width table so their centered text sits underneath the volume bars.
+static const char *const kEuropeanBuiltInOptionItems[kOptionsItemCount] = {
+	" SOUND FX              ",
+	" MUSIC               ",
+	" GAMMA               ",
+	"TEXT",
+	"GORE",
+	"QUICK TIPS",
+	"PASSWORD"
+};
+
 static const int kQuickTipsOverlayX = 167;
 static const int kQuickTipsOverlayY = 200;
 static const int kQuickTipsHeaderY = 202;
@@ -269,6 +281,16 @@ static void loadMenuDisplayValue(Common::INIFile &menu, const char *key, Common:
 	dest = Common::move(value);
 }
 
+static void applyEuropeanBuiltInMenuText(MenuTextConfig &config) {
+	config.yesLabel = "Yes";
+	config.noLabel = "No";
+	config.clickLabel = "Click";
+	config.newGamePrompt = "START A NEW GAME?";
+	config.quitGamePrompt = " QUIT HARVESTER?";
+	for (uint i = 0; i < config.optionItems.size(); ++i)
+		config.optionItems[i] = kEuropeanBuiltInOptionItems[i];
+}
+
 } // End of anonymous namespace
 
 bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
@@ -295,8 +317,13 @@ bool loadMenuTextConfig(HarvesterEngine &engine, MenuTextConfig &config) {
 		return false;
 
 	Common::Array<byte> data;
-	if (!resources->loadFile(kMenuPath, data))
-		return false;
+	if (!resources->loadFile(kMenuPath, data)) {
+		if (!engine.isDemo())
+			applyEuropeanBuiltInMenuText(config);
+		debugC(2, kDebugGeneral,
+			"Harvester: using built-in menu text because '%s' is unavailable", kMenuPath);
+		return true;
+	}
 
 	Common::MemoryReadStream stream(data.data(), data.size());
 	Common::INIFile menu;


Commit: 8400536d8ceeac57bfbfcb570f8f6b99a4799e22
    https://github.com/scummvm/scummvm/commit/8400536d8ceeac57bfbfcb570f8f6b99a4799e22
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-09-04T06:24:43-04:00

Commit Message:
HARVESTER: Match dialogue text mode input handling

Keep CLICK mode waiting for input even when no subtitle resolves or
Escape interrupts the voice.

Assisted-by: Codex:gpt-5.6-sol

Changed paths:
    engines/harvester/dialogue.cpp


diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue.cpp
index d9a51e66ca4..0ce30c30ea9 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue.cpp
@@ -153,6 +153,20 @@ static const byte kTextColorHover = 251;
 static const byte kShadowColor = 0;
 static const byte kTransparentPaletteIndex = 0;
 
+static bool isDialogueVoiceInterruptEvent(const Common::Event &event) {
+	return event.type == Common::EVENT_LBUTTONDOWN ||
+		event.type == Common::EVENT_RBUTTONDOWN ||
+		(event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE);
+}
+
+static bool isDialoguePointerPressEvent(const Common::Event &event) {
+	return event.type == Common::EVENT_LBUTTONDOWN || event.type == Common::EVENT_RBUTTONDOWN;
+}
+
+static bool isDialogueContinueEvent(const Common::Event &event) {
+	return isDialoguePointerPressEvent(event) || event.type == Common::EVENT_KEYDOWN;
+}
+
 static void syncDialogueSharedState(Common::Serializer &s, DialogueSharedState &state) {
 	syncDialogueBool(s, state.boyleGascanApplicationState);
 	syncDialogueBool(s, state.dialogueStateD2e98);
@@ -523,8 +537,9 @@ public:
 			int headVariant) override {
 		setActiveSpeakerPortrait(speakerId, headVariant);
 
+		const StartupDialogueTextMode textMode = _script->getDialogueTextMode();
 		Common::String subtitleText;
-		const bool textEnabled = _script->getDialogueTextMode() != kStartupDialogueTextNone &&
+		const bool textEnabled = textMode != kStartupDialogueTextNone &&
 			_text->resolveDialogueSubtitle(wavId, subtitleText);
 		Common::Array<Common::String> subtitleLines;
 		const IndexedBitmap *textboxBitmap = nullptr;
@@ -537,8 +552,8 @@ public:
 		const Common::String voicePath = buildDialogueVoicePath(*_script, wavId);
 		const bool voiceStarted = !voicePath.empty() && _engine.playSpeech(voicePath);
 		debugC(2, kDebugDialogue,
-			"Harvester: dialogue line wav=0x%x speaker='%s' headVariant=%d voice='%s' subtitle='%s'",
-			wavId, speakerId.c_str(), headVariant, voicePath.c_str(),
+			"Harvester: dialogue line wav=0x%x speaker='%s' headVariant=%d voice='%s' textMode=%d subtitle='%s'",
+			wavId, speakerId.c_str(), headVariant, voicePath.c_str(), (int)textMode,
 			textEnabled ? subtitleText.c_str() : "");
 		Common::Error releaseError = waitForPointerRelease();
 		if (releaseError.getCode() != Common::kNoError) {
@@ -546,7 +561,8 @@ public:
 			return releaseError;
 		}
 
-		bool interrupted = false;
+		bool voiceInterrupted = false;
+		bool pointerInterrupted = false;
 		Graphics::FrameLimiter limiter(g_system, 60);
 		for (;;) {
 			drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr, nullptr, -1, false, nullptr);
@@ -559,27 +575,16 @@ public:
 					return result;
 				}
 
-				switch (event.type) {
-				case Common::EVENT_LBUTTONDOWN:
-				case Common::EVENT_RBUTTONDOWN:
-					interrupted = true;
-					break;
-				case Common::EVENT_KEYDOWN:
-					if (event.kbd.keycode == Common::KEYCODE_ESCAPE ||
-							event.kbd.keycode == Common::KEYCODE_RETURN ||
-							event.kbd.keycode == Common::KEYCODE_KP_ENTER ||
-							event.kbd.keycode == Common::KEYCODE_SPACE) {
-						interrupted = true;
-					}
-					break;
-				default:
-					break;
+				if (isDialogueVoiceInterruptEvent(event)) {
+					voiceInterrupted = true;
+					if (isDialoguePointerPressEvent(event))
+						pointerInterrupted = true;
 				}
 			}
 
 			if (_entityManager)
 				(void)_entityManager->syncCursorEntityPosition(_mousePos);
-			if (interrupted || (!voiceStarted || !_engine.isSpeechPlaying()))
+			if (voiceInterrupted || (!voiceStarted || !_engine.isSpeechPlaying()))
 				break;
 
 			limiter.delayBeforeSwap();
@@ -587,14 +592,15 @@ public:
 		}
 
 		_engine.stopSpeech();
-		if (interrupted) {
+		if (pointerInterrupted) {
 			Common::Error releaseResult = waitForPointerRelease();
 			if (releaseResult.getCode() != Common::kNoError)
 				return releaseResult;
-		} else if (textEnabled && _script->getDialogueTextMode() == kStartupDialogueTextClick) {
+		} else if (textMode == kStartupDialogueTextClick) {
 			Graphics::FrameLimiter clickLimiter(g_system, 60);
 			for (;;) {
-				drawDialogueOverlay(textboxBitmap, &subtitleLines, nullptr, -1, false, nullptr);
+				drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr,
+					nullptr, -1, false, nullptr);
 
 				bool continuePressed = false;
 				Common::Event event;
@@ -603,22 +609,8 @@ public:
 					if (DialogueFlowAccess::handleSystemEvent(_flow, event, result))
 						return result;
 
-					switch (event.type) {
-					case Common::EVENT_LBUTTONDOWN:
-					case Common::EVENT_RBUTTONDOWN:
+					if (isDialogueContinueEvent(event))
 						continuePressed = true;
-						break;
-					case Common::EVENT_KEYDOWN:
-						if (event.kbd.keycode == Common::KEYCODE_ESCAPE ||
-								event.kbd.keycode == Common::KEYCODE_RETURN ||
-								event.kbd.keycode == Common::KEYCODE_KP_ENTER ||
-								event.kbd.keycode == Common::KEYCODE_SPACE) {
-							continuePressed = true;
-						}
-						break;
-					default:
-						break;
-					}
 				}
 
 				if (_entityManager)




More information about the Scummvm-git-logs mailing list