[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