[Scummvm-git-logs] scummvm master -> 5e44c1d40fb49bd5db39e1a439c2e5d9081b9049
alexbevi
noreply at scummvm.org
Wed Oct 21 01:18:17 UTC 2026
This automated email contains information about 15 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
22d4ac18b0 HARVESTER: Separate dialogue layout and shared drawing helpers
0d9611d093 HARVESTER: Separate dialogue sessions from handler coordination
4c6a23d829 HARVESTER: Separate dialogue speech and modal playback
41294db54f HARVESTER: Separate dialogue menus and text entry
2c315a9616 HARVESTER: Separate audio decoding and scope input ownership
eef97d3eb7 HARVESTER: Separate media playback and resource lifecycle
0e7664836f HARVESTER: Separate interaction interruption and terminal outcomes
36999a576f HARVESTER: Separate interaction transitions and restarts
dba8b8b2fd HARVESTER: Separate same-scene effects and continuations
5e92cc9095 HARVESTER: Scope entity and archive setup ownership
8c364fa324 HARVESTER: Scope FST audio queue ownership
4d488b07f2 HARVESTER: Separate inventory, keyboard and scene input handling
762eaeed53 HARVESTER: Extract Sergeant dialogue branches into methods
35463df436 HARVESTER: Extract Jimmy dialogue routines into methods
5e44c1d40f HARVESTER: Extract Buster fatal dialogue sequence
Commit: 22d4ac18b04f94d2a00cc9cb6002988ca0e0936c
https://github.com/scummvm/scummvm/commit/22d4ac18b04f94d2a00cc9cb6002988ca0e0936c
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:52-04:00
Commit Message:
HARVESTER: Separate dialogue layout and shared drawing helpers
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/dialogue/dialogue.cpp
A engines/harvester/dialogue/dialogue.h
A engines/harvester/dialogue/dialogue_render.cpp
A engines/harvester/dialogue/dialogue_render.h
R engines/harvester/dialogue.cpp
R engines/harvester/dialogue.h
engines/harvester/flow/flow.h
engines/harvester/module.mk
engines/harvester/scene/input.cpp
diff --git a/engines/harvester/dialogue.cpp b/engines/harvester/dialogue/dialogue.cpp
similarity index 90%
rename from engines/harvester/dialogue.cpp
rename to engines/harvester/dialogue/dialogue.cpp
index 2a687180268..01c6c3cae7c 100644
--- a/engines/harvester/dialogue.cpp
+++ b/engines/harvester/dialogue/dialogue.cpp
@@ -19,7 +19,8 @@
*
*/
-#include "harvester/dialogue.h"
+#include "harvester/dialogue/dialogue.h"
+#include "harvester/dialogue/dialogue_render.h"
#include "common/algorithm.h"
#include "common/debug.h"
@@ -75,7 +76,8 @@
#include "harvester/npc/ryder_dialogue.h"
#include "harvester/npc/sergeant_dialogue.h"
#include "harvester/npc/nude_man_dialogue.h"
-#include "harvester/palette_utils.h"
+#include "harvester/flow/flow_support.h"
+#include "harvester/scene/room_presentation.h"
#include "harvester/npc/stephanie_dialogue.h"
#include "harvester/npc/stub_dialogue.h"
#include "harvester/npc/sparky_dialogue.h"
@@ -103,7 +105,6 @@ static const int kDialogueSubtitleTextX = 178;
static const int kDialogueSubtitleTextY = 12;
static const int kDialogueSubtitleTextWidth = 316;
static const int kDialogueKeywordTextX = 178;
-static const int kDialogueKeywordTextY = 34;
static const int kDialogueKeywordTextWidth = 300;
static const int kDialogueOtherX = 171;
static const int kDialogueOtherY = 183;
@@ -113,10 +114,6 @@ static const uint kDialogueTextEntryMaxLength = 26;
static const int kDialogueTextEntryMaxWidth = 550;
static const uint32 kDialogueTextEntryBlinkTicks = 0x30;
static const int kDialogueOtherWidth = 120;
-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";
@@ -125,7 +122,6 @@ static const char *const kDialogueLocalizedKeywordBitmapPath = "GRAPHIC/OTHER/KE
static const byte kTextColorNormal = 255;
static const byte kTextColorHover = 251;
-static const byte kShadowColor = 0;
static bool isDialogueVoiceInterruptEvent(const Common::Event &event) {
return event.type == Common::EVENT_LBUTTONDOWN ||
@@ -184,35 +180,6 @@ static void syncDialogueSharedState(Common::Serializer &s, DialogueSharedState &
syncDialogueInt(s, state.dialogueStateD2f30);
}
-static void blitBitmap(Graphics::Screen &screen, const IndexedBitmap &bitmap, int x, int y) {
- if (!bitmap.isValid())
- return;
-
- screen.copyRectToSurface(bitmap.pixels.data(), bitmap.width, x, y, bitmap.width, bitmap.height);
-}
-
-static void drawShadowedString(Graphics::Screen &screen, const Graphics::Font &font, const Common::String &text,
- int x, int y, int width, byte color, Graphics::TextAlign align = Graphics::kTextAlignLeft) {
- font.drawString(&screen, text, x + 1, y + 1, width, kShadowColor, align);
- font.drawString(&screen, text, x, y, width, color, align);
-}
-
-static void setScaledPalette(Graphics::Screen &screen, const byte *palette, float brightness) {
- byte scaledPalette[256 * 3];
- const float gammaBrightness = g_engine ? g_engine->getGammaBrightnessScale() : 1.0f;
- buildHarvesterDisplayPalette(palette, brightness * gammaBrightness, scaledPalette);
- screen.setPalette(scaledPalette);
-}
-
-static bool loadBitmapResource(ResourceManager &resources, const Common::String &path, IndexedBitmap &bitmap) {
- Common::Array<byte> data;
- if (!resources.loadFile(path, data) || data.size() < 12)
- return false;
-
- bitmap = IndexedBitmap();
- return decodeBitmap(data, bitmap);
-}
-
static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
Common::Array<byte> data;
if (!resources.loadFile(path, data) || data.size() < 256 * 3)
@@ -232,63 +199,6 @@ static bool isSecondaryMouseDown() {
((g_system->getEventManager()->getButtonState() & 2) != 0);
}
-static int getDialogueTextLineHeight(const Graphics::Font &font) {
- return font.getFontHeight() + 2;
-}
-
-static int resolveDialogueTextboxIndex(uint lineCount) {
- if (lineCount <= 1)
- return 0;
- if (lineCount < 3)
- return 1;
- if (lineCount < 4)
- return 2;
- if (lineCount < 6)
- return 3;
- if (lineCount < 7)
- return 4;
- if (lineCount < 9)
- return 5;
- if (lineCount < 11)
- return 6;
- return 7;
-}
-
-static int resolveDialogueResponseTextboxIndex(uint responseRowCount) {
- if (responseRowCount == 0)
- return 0;
- if (responseRowCount < 3)
- return 3;
- if (responseRowCount < 4)
- return 4;
- if (responseRowCount < 6)
- return 5;
- return 6;
-}
-
-static void drawDialogueTextLines(Graphics::Screen &screen, const Graphics::Font &font,
- const Common::Array<Common::String> &lines, int x, int y, int width) {
- const int lineHeight = getDialogueTextLineHeight(font);
- for (uint i = 0; i < lines.size(); ++i)
- font.drawString(&screen, lines[i], x, y + (int)i * lineHeight, width, 0);
-}
-
-static void wrapDialogueTextLikeNative(const Graphics::Font &font, bool usesCft,
- const Common::String &text, int width, Common::Array<Common::String> &lines) {
- lines.clear();
- if (text.empty())
- return;
-
- if (!usesCft || width <= 0) {
- font.wordWrapText(text, width, lines);
- if (lines.empty())
- lines.push_back(text);
- return;
- }
-
- wrapCftTextByCharacterCount(font, text, width, lines);
-}
-
static void splitDialogueMenuLine(const Common::String &line, Common::Array<Common::String> &parts) {
parts.clear();
if (line.empty())
@@ -352,24 +262,6 @@ static Common::String buildDialogueVoicePath(const Script &script, int wavId) {
return Common::String::format("%s%d.CMP", script.getVoicePath().c_str(), wavId);
}
-static int getDialogueMenuItemAt(const Graphics::Font &font, uint itemCount, const Common::Point &mousePos) {
- if (itemCount == 0)
- return -1;
- if (mousePos.x < kDialogueTopicStartX || mousePos.x > kDialogueTopicEndX)
- return -1;
- if (mousePos.y < kDialogueKeywordTextY)
- return -1;
-
- const int lineHeight = getDialogueTextLineHeight(font);
- const int selection = (mousePos.y - kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
- return selection >= 0 && selection < (int)itemCount ? selection : -1;
-}
-
-static bool isDialogueOtherHit(const Common::Point &mousePos) {
- return mousePos.x >= kDialogueTopicStartX && mousePos.x <= kDialogueTopicEndX &&
- mousePos.y >= kDialogueOtherStartY && mousePos.y <= kDialogueOtherEndY;
-}
-
class RoomNpcDialogueSession : public DialogueRuntimeCallbacks {
public:
struct DialogueResponseOptionLayout {
@@ -463,9 +355,9 @@ public:
Common::Array<Common::String> subtitleLines;
const IndexedBitmap *textboxBitmap = nullptr;
if (textEnabled) {
- wrapDialogueTextLikeNative(*_subtitleFont, _subtitleFontUsesCft,
+ DialogueRendering::wrapDialogueTextLikeNative(*_subtitleFont, _subtitleFontUsesCft,
subtitleText, kDialogueSubtitleTextWidth, subtitleLines);
- textboxBitmap = _art->getTextboxBitmap(resolveDialogueTextboxIndex(subtitleLines.size()));
+ textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueTextboxIndex(subtitleLines.size()));
}
const Common::String voicePath = buildDialogueVoicePath(*_script, wavId);
@@ -629,7 +521,7 @@ public:
while (!_engine.shouldQuit() && !activateRequestedDisc) {
if (needsRedraw) {
screen->fillRect(screen->getBounds(), 0);
- blitBitmap(*screen, promptBitmap, 0, 0);
+ RoomPresentation::blitBitmap(*screen, promptBitmap, 0, 0);
setScaledPalette(*screen, promptPalette, 1.0f);
if (_entityManager)
_entityManager->drawCursor(*screen);
@@ -710,8 +602,8 @@ public:
textEntryCursorVisible = !textEntryCursorVisible;
textEntryBlinkTimestamp = now;
}
- const int hoveredTopicIndex = editingOther ? -1 : getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
- const bool hoverOther = editingOther || isDialogueOtherHit(_mousePos);
+ const int hoveredTopicIndex = editingOther ? -1 : DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
+ const bool hoverOther = editingOther || DialogueRendering::isDialogueOtherHit(_mousePos);
drawDialogueOverlay(&_keywordBitmap, nullptr, &topics, hoveredTopicIndex, hoverOther,
editingOther ? &typedTopic : nullptr, textEntryCursorVisible);
@@ -780,7 +672,7 @@ public:
}
break;
case Common::EVENT_LBUTTONDOWN: {
- const int clickedTopic = getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
+ const int clickedTopic = DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
if (clickedTopic >= 0) {
selectedTopic = topics[(uint)clickedTopic];
selection.fromGenericBye = clickedTopic == (int)topics.size() - 1;
@@ -788,7 +680,7 @@ public:
clickedTopic + 1, selectedTopic.c_str());
return Common::kNoError;
}
- if (isDialogueOtherHit(_mousePos)) {
+ if (DialogueRendering::isDialogueOtherHit(_mousePos)) {
typedTopic.clear();
editingOther = true;
textEntryCursorVisible = false;
@@ -929,7 +821,7 @@ private:
if (usesCft)
font.drawString(activeScreen, text, x, y, width, 0);
else
- drawShadowedString(*activeScreen, font, text, x, y, width, color);
+ RoomPresentation::drawShadowedString(*activeScreen, font, text, x, y, width, color);
}
void ensureSpeakerPortrait(const Common::String &speakerId, int headVariant) {
@@ -960,7 +852,7 @@ private:
return;
setScaledPalette(*activeScreen, _palette, _paletteBrightness);
- blitBitmap(*activeScreen, _backdrop, 0, 0);
+ RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
if (_leftHeadVisible && _leftHeadBitmap.isValid())
blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
if (_rightHeadVisible && _rightHeadBitmap.isValid())
@@ -970,12 +862,12 @@ private:
if (subtitleLines) {
if (_subtitleFontUsesCft) {
- drawDialogueTextLines(*activeScreen, *_subtitleFont, *subtitleLines,
+ DialogueRendering::drawDialogueTextLines(*activeScreen, *_subtitleFont, *subtitleLines,
kDialogueSubtitleTextX, kDialogueSubtitleTextY, kDialogueSubtitleTextWidth);
} else {
- const int lineHeight = getDialogueTextLineHeight(*_subtitleFont);
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_subtitleFont);
for (uint i = 0; i < subtitleLines->size(); ++i) {
- drawShadowedString(*activeScreen, *_subtitleFont, (*subtitleLines)[i],
+ RoomPresentation::drawShadowedString(*activeScreen, *_subtitleFont, (*subtitleLines)[i],
kDialogueSubtitleTextX,
kDialogueSubtitleTextY + (int)i * lineHeight,
kDialogueSubtitleTextWidth, kTextColorNormal);
@@ -994,13 +886,13 @@ private:
kTextColorNormal);
}
- const int lineHeight = getDialogueTextLineHeight(*_menuFont);
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
for (uint i = 0; i < topics->size(); ++i) {
const bool highlighted = (int)i == hoveredTopicIndex;
const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
drawFontString(font, usesCft, (*topics)[i], kDialogueKeywordTextX,
- kDialogueKeywordTextY + (int)i * lineHeight, kDialogueKeywordTextWidth,
+ DialogueRendering::kDialogueKeywordTextY + (int)i * lineHeight, kDialogueKeywordTextWidth,
highlighted ? kTextColorNormal : kTextColorHover);
}
@@ -1047,7 +939,7 @@ private:
option.rowStart = (int)totalRows;
for (const Common::String &explicitLine : explicitLines) {
Common::Array<Common::String> wrappedExplicitLines;
- wrapDialogueTextLikeNative(*_menuFont, _menuFontUsesCft,
+ DialogueRendering::wrapDialogueTextLikeNative(*_menuFont, _menuFontUsesCft,
explicitLine, kDialogueSubtitleTextWidth, wrappedExplicitLines);
if (wrappedExplicitLines.empty())
wrappedExplicitLines.push_back(explicitLine);
@@ -1098,13 +990,13 @@ private:
int getResponseMenuItemAt(const Common::Array<DialogueResponseOptionLayout> &options,
uint totalRows, const Common::Point &mousePos) {
- if (mousePos.x < kDialogueTopicStartX || mousePos.x > kDialogueTopicEndX)
+ if (mousePos.x < DialogueRendering::kDialogueTopicStartX || mousePos.x > DialogueRendering::kDialogueTopicEndX)
return -1;
- if (mousePos.y < kDialogueKeywordTextY)
+ if (mousePos.y < DialogueRendering::kDialogueKeywordTextY)
return -1;
- const int lineHeight = getDialogueTextLineHeight(*_menuFont);
- const int row = (mousePos.y - kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ const int row = (mousePos.y - DialogueRendering::kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
if (row < 0 || row >= (int)totalRows)
return -1;
@@ -1124,7 +1016,7 @@ private:
return;
setScaledPalette(*activeScreen, _palette, _paletteBrightness);
- blitBitmap(*activeScreen, _backdrop, 0, 0);
+ RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
if (_leftHeadVisible && _leftHeadBitmap.isValid())
blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
if (_rightHeadVisible && _rightHeadBitmap.isValid())
@@ -1139,8 +1031,8 @@ private:
const int titleX = kDialogueSubtitleTextX + MAX<int>(0, (kDialogueSubtitleTextWidth - titleWidth) / 2);
drawFontString(titleFont, titleUsesCft, title, titleX, 11, titleWidth, kTextColorNormal);
- const int lineHeight = getDialogueTextLineHeight(*_menuFont);
- int drawY = kDialogueKeywordTextY;
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ int drawY = DialogueRendering::kDialogueKeywordTextY;
for (uint i = 0; i < options.size(); ++i) {
const bool highlighted = (int)i == hoveredOptionIndex;
const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
@@ -1195,7 +1087,7 @@ private:
}
logDialogueMenuItems("Response menu", responseLineIndex, responseLine, optionTexts);
- const IndexedBitmap *textboxBitmap = _art->getTextboxBitmap(resolveDialogueResponseTextboxIndex(totalRows));
+ const IndexedBitmap *textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueResponseTextboxIndex(totalRows));
Common::Error releaseError = waitForPointerRelease();
if (releaseError.getCode() != Common::kNoError)
return releaseError;
diff --git a/engines/harvester/dialogue.h b/engines/harvester/dialogue/dialogue.h
similarity index 100%
rename from engines/harvester/dialogue.h
rename to engines/harvester/dialogue/dialogue.h
diff --git a/engines/harvester/dialogue/dialogue_render.cpp b/engines/harvester/dialogue/dialogue_render.cpp
new file mode 100644
index 00000000000..feee8b6ca5d
--- /dev/null
+++ b/engines/harvester/dialogue/dialogue_render.cpp
@@ -0,0 +1,108 @@
+/* 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/dialogue/dialogue_render.h"
+
+#include "common/util.h"
+#include "graphics/font.h"
+#include "graphics/screen.h"
+#include "harvester/cft_font.h"
+
+namespace Harvester {
+namespace DialogueRendering {
+
+int getDialogueTextLineHeight(const Graphics::Font &font) {
+ return font.getFontHeight() + 2;
+}
+
+int resolveDialogueTextboxIndex(uint lineCount) {
+ if (lineCount <= 1)
+ return 0;
+ if (lineCount < 3)
+ return 1;
+ if (lineCount < 4)
+ return 2;
+ if (lineCount < 6)
+ return 3;
+ if (lineCount < 7)
+ return 4;
+ if (lineCount < 9)
+ return 5;
+ if (lineCount < 11)
+ return 6;
+ return 7;
+}
+
+int resolveDialogueResponseTextboxIndex(uint responseRowCount) {
+ if (responseRowCount == 0)
+ return 0;
+ if (responseRowCount < 3)
+ return 3;
+ if (responseRowCount < 4)
+ return 4;
+ if (responseRowCount < 6)
+ return 5;
+ return 6;
+}
+
+void drawDialogueTextLines(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::Array<Common::String> &lines, int x, int y, int width) {
+ const int lineHeight = getDialogueTextLineHeight(font);
+ for (uint i = 0; i < lines.size(); ++i)
+ font.drawString(&screen, lines[i], x, y + (int)i * lineHeight, width, 0);
+}
+
+void wrapDialogueTextLikeNative(const Graphics::Font &font, bool usesCft,
+ const Common::String &text, int width, Common::Array<Common::String> &lines) {
+ lines.clear();
+ if (text.empty())
+ return;
+
+ if (!usesCft || width <= 0) {
+ font.wordWrapText(text, width, lines);
+ if (lines.empty())
+ lines.push_back(text);
+ return;
+ }
+
+ wrapCftTextByCharacterCount(font, text, width, lines);
+}
+
+int getDialogueMenuItemAt(const Graphics::Font &font, uint itemCount, const Common::Point &mousePos) {
+ if (itemCount == 0)
+ return -1;
+ if (mousePos.x < kDialogueTopicStartX || mousePos.x > kDialogueTopicEndX)
+ return -1;
+ if (mousePos.y < kDialogueKeywordTextY)
+ return -1;
+
+ const int lineHeight = getDialogueTextLineHeight(font);
+ const int selection = (mousePos.y - kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
+ return selection >= 0 && selection < (int)itemCount ? selection : -1;
+}
+
+bool isDialogueOtherHit(const Common::Point &mousePos) {
+ return mousePos.x >= kDialogueTopicStartX && mousePos.x <= kDialogueTopicEndX &&
+ mousePos.y >= kDialogueOtherStartY && mousePos.y <= kDialogueOtherEndY;
+}
+
+} // End of namespace DialogueRendering
+} // End of namespace Harvester
diff --git a/engines/harvester/dialogue/dialogue_render.h b/engines/harvester/dialogue/dialogue_render.h
new file mode 100644
index 00000000000..44bec954daa
--- /dev/null
+++ b/engines/harvester/dialogue/dialogue_render.h
@@ -0,0 +1,56 @@
+/* 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_DIALOGUE_RENDER_H
+#define HARVESTER_DIALOGUE_RENDER_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/str.h"
+
+namespace Graphics {
+class Font;
+class Screen;
+}
+
+namespace Harvester {
+namespace DialogueRendering {
+
+static const int kDialogueKeywordTextY = 34;
+static const int kDialogueTopicStartX = 168;
+static const int kDialogueTopicEndX = 482;
+static const int kDialogueOtherStartY = 170;
+static const int kDialogueOtherEndY = 193;
+
+int getDialogueTextLineHeight(const Graphics::Font &font);
+int resolveDialogueTextboxIndex(uint lineCount);
+int resolveDialogueResponseTextboxIndex(uint responseRowCount);
+void drawDialogueTextLines(Graphics::Screen &screen, const Graphics::Font &font,
+ const Common::Array<Common::String> &lines, int x, int y, int width);
+void wrapDialogueTextLikeNative(const Graphics::Font &font, bool usesCft,
+ const Common::String &text, int width, Common::Array<Common::String> &lines);
+int getDialogueMenuItemAt(const Graphics::Font &font, uint itemCount, const Common::Point &mousePos);
+bool isDialogueOtherHit(const Common::Point &mousePos);
+
+} // End of namespace DialogueRendering
+} // End of namespace Harvester
+
+#endif // HARVESTER_DIALOGUE_RENDER_H
diff --git a/engines/harvester/flow/flow.h b/engines/harvester/flow/flow.h
index b056fe15f08..2d4a310d734 100644
--- a/engines/harvester/flow/flow.h
+++ b/engines/harvester/flow/flow.h
@@ -27,7 +27,7 @@
#include "common/rect.h"
#include "common/str.h"
#include "harvester/cheats.h"
-#include "harvester/dialogue.h"
+#include "harvester/dialogue/dialogue.h"
#include "harvester/inventory.h"
#include "harvester/menu/menu.h"
#include "harvester/scene/scene.h"
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 3faf628a78a..3c28b6bae8b 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -6,7 +6,8 @@ MODULE_OBJS = \
cft_font.o \
cheats.o \
console.o \
- dialogue.o \
+ dialogue/dialogue.o \
+ dialogue/dialogue_render.o \
flow/flow.o \
flow/flow_debug.o \
flow/flow_map.o \
diff --git a/engines/harvester/scene/input.cpp b/engines/harvester/scene/input.cpp
index 2402be6c3ad..39fd88352a2 100644
--- a/engines/harvester/scene/input.cpp
+++ b/engines/harvester/scene/input.cpp
@@ -26,7 +26,7 @@
#include "harvester/art.h"
#include "harvester/cheats.h"
#include "harvester/detection.h"
-#include "harvester/dialogue.h"
+#include "harvester/dialogue/dialogue.h"
#include "harvester/flow/flow.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
Commit: 0d9611d093bad086517c39ffb19533822ca38958
https://github.com/scummvm/scummvm/commit/0d9611d093bad086517c39ffb19533822ca38958
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:52-04:00
Commit Message:
HARVESTER: Separate dialogue sessions from handler coordination
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/dialogue/dialogue_session.cpp
A engines/harvester/dialogue/dialogue_session.h
engines/harvester/dialogue/dialogue.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/dialogue/dialogue.cpp b/engines/harvester/dialogue/dialogue.cpp
index 01c6c3cae7c..627ad1c03d8 100644
--- a/engines/harvester/dialogue/dialogue.cpp
+++ b/engines/harvester/dialogue/dialogue.cpp
@@ -20,25 +20,16 @@
*/
#include "harvester/dialogue/dialogue.h"
-#include "harvester/dialogue/dialogue_render.h"
+#include "harvester/dialogue/dialogue_session.h"
-#include "common/algorithm.h"
#include "common/debug.h"
-#include "common/events.h"
-#include "common/ptr.h"
-#include "common/system.h"
-#include "graphics/font.h"
-#include "graphics/fontman.h"
-#include "graphics/framelimiter.h"
+#include "harvester/detection.h"
+#include "harvester/harvester.h"
#include "harvester/npc/authority_dialogue.h"
#include "harvester/npc/beggar_dialogue.h"
#include "harvester/npc/boyle_dialogue.h"
#include "harvester/npc/buster_dialogue.h"
#include "harvester/npc/butcher_dialogue.h"
-#include "harvester/cft_font.h"
-#include "harvester/detection.h"
-#include "harvester/fst_player.h"
-#include "harvester/harvester.h"
#include "harvester/npc/chessmaster_dialogue.h"
#include "harvester/npc/cloak_atnd_dialogue.h"
#include "harvester/npc/curator_dialogue.h"
@@ -76,8 +67,6 @@
#include "harvester/npc/ryder_dialogue.h"
#include "harvester/npc/sergeant_dialogue.h"
#include "harvester/npc/nude_man_dialogue.h"
-#include "harvester/flow/flow_support.h"
-#include "harvester/scene/room_presentation.h"
#include "harvester/npc/stephanie_dialogue.h"
#include "harvester/npc/stub_dialogue.h"
#include "harvester/npc/sparky_dialogue.h"
@@ -86,57 +75,11 @@
#include "harvester/npc/vet_dialogue.h"
#include "harvester/npc/whaley_dialogue.h"
#include "harvester/npc/wasp_woman_dialogue.h"
-#include "harvester/resources.h"
-#include "harvester/entity.h"
-#include "harvester/art.h"
-#include "harvester/flow/flow.h"
-#include "harvester/text.h"
namespace Harvester {
namespace {
-static const int kDialogueOverlayX = 167;
-static const int kDialogueOverlayY = 6;
-static const int kDialogueLeftHeadX = 6;
-static const int kDialogueRightHeadX = 474;
-static const int kDialogueHeadY = 6;
-static const int kDialogueSubtitleTextX = 178;
-static const int kDialogueSubtitleTextY = 12;
-static const int kDialogueSubtitleTextWidth = 316;
-static const int kDialogueKeywordTextX = 178;
-static const int kDialogueKeywordTextWidth = 300;
-static const int kDialogueOtherX = 171;
-static const int kDialogueOtherY = 183;
-static const int kDialogueTextEntryX = 208;
-static const int kDialogueTextEntryY = 184;
-static const uint kDialogueTextEntryMaxLength = 26;
-static const int kDialogueTextEntryMaxWidth = 550;
-static const uint32 kDialogueTextEntryBlinkTicks = 0x30;
-static const int kDialogueOtherWidth = 120;
-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 byte kTextColorNormal = 255;
-static const byte kTextColorHover = 251;
-
-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);
@@ -180,1011 +123,6 @@ static void syncDialogueSharedState(Common::Serializer &s, DialogueSharedState &
syncDialogueInt(s, state.dialogueStateD2f30);
}
-static bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
- Common::Array<byte> data;
- if (!resources.loadFile(path, data) || data.size() < 256 * 3)
- return false;
-
- memcpy(dest, data.data(), 256 * 3);
- return true;
-}
-
-static bool isPrimaryMouseDown() {
- return g_system && g_system->getEventManager() &&
- ((g_system->getEventManager()->getButtonState() & 1) != 0);
-}
-
-static bool isSecondaryMouseDown() {
- return g_system && g_system->getEventManager() &&
- ((g_system->getEventManager()->getButtonState() & 2) != 0);
-}
-
-static void splitDialogueMenuLine(const Common::String &line, Common::Array<Common::String> &parts) {
- parts.clear();
- if (line.empty())
- return;
-
- Common::String token;
- for (uint i = 0; i < line.size(); ++i) {
- if (line[i] == '/') {
- parts.push_back(Common::move(token));
- token.clear();
- continue;
- }
-
- token += line[i];
- }
-
- parts.push_back(Common::move(token));
-}
-
-static void logDialogueMenuItems(const char *label, int sourceLineIndex,
- const Common::String &rawLine, const Common::Array<Common::String> &items) {
- if (sourceLineIndex >= 0) {
- debugC(3, kDebugDialogue, "Harvester: %s source line 0x%x (%d) raw='%s'",
- label, sourceLineIndex, sourceLineIndex + 1, rawLine.c_str());
- } else {
- debugC(3, kDebugDialogue, "Harvester: %s source raw='%s'", label, rawLine.c_str());
- }
-
- if (items.empty()) {
- debugC(1, kDebugDialogue, "Harvester: %s has no explicit items", label);
- return;
- }
-
- for (uint i = 0; i < items.size(); ++i)
- debugC(3, kDebugDialogue, "Harvester: %s item[%u]='%s'", label, i + 1, items[i].c_str());
-}
-
-static Common::String buildDialogueHeadId(const Common::String &speakerId, int headVariant) {
- return Common::String::format("%s%d", speakerId.c_str(), headVariant);
-}
-
-static bool loadDialogueHeadBitmap(HarvesterEngine &engine, const Common::String &speakerId, int headVariant,
- IndexedBitmap &bitmap) {
- Script *script = engine.getScript();
- ResourceManager *resources = engine.getResources();
- if (!script || !resources || speakerId.empty())
- return false;
-
- const Common::String headId = buildDialogueHeadId(speakerId, headVariant);
- const HeadRecord *head = script->findHeadRecord(headId);
- if (!head)
- return false;
-
- return loadBitmapResource(*resources, head->portraitPath + ".BM", bitmap);
-}
-
-static Common::String buildDialogueVoicePath(const Script &script, int wavId) {
- if (wavId <= 0)
- return Common::String();
-
- return Common::String::format("%s%d.CMP", script.getVoicePath().c_str(), wavId);
-}
-
-class RoomNpcDialogueSession : public DialogueRuntimeCallbacks {
-public:
- struct DialogueResponseOptionLayout {
- Common::String text;
- Common::Array<Common::String> wrappedLines;
- char hotkey = 0;
- int rowStart = 0;
- int rowCount = 0;
- };
-
- 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),
- _menuTextConfig(flow.getMenuTextConfig()), _backdrop(backdrop),
- _palette(palette), _paletteBrightness(paletteBrightness), _npc(npc),
- _script(engine.getScript()), _text(engine.getText()), _art(engine.getArt()),
- _entityManager(engine.getRuntimeEntities()),
- _fallbackFont(FontMan.getFontByUsage(Graphics::FontManager::kGUIFont)),
- _subtitleFont(nullptr), _menuFont(nullptr), _highlightFont(nullptr),
- _subtitleFontUsesCft(false), _menuFontUsesCft(false),
- _highlightFontUsesCft(false), _leftHeadVisible(false),
- _rightHeadVisible(false) {
- ResourceManager *resources = _engine.getResources();
- if (!_script || !_text || !_art || !_fallbackFont || !_backdrop.isValid() ||
- !_palette || !resources) {
- return;
- }
-
- if (const CftFontResource *subtitleFontResource = _text->findFont("TEXTFONT")) {
- _subtitleCftFont.reset(new HarvesterCftFont(*subtitleFontResource));
- if (!_subtitleCftFont->isValid())
- _subtitleCftFont.reset();
- }
- if (const CftFontResource *menuFontResource = _text->findFont("TEXTFNT2")) {
- _menuCftFont.reset(new HarvesterCftFont(*menuFontResource));
- if (!_menuCftFont->isValid())
- _menuCftFont.reset();
- }
-
- _subtitleFont = _subtitleCftFont
- ? static_cast<const Graphics::Font *>(_subtitleCftFont.get())
- : _fallbackFont;
- _menuFont = _menuCftFont
- ? static_cast<const Graphics::Font *>(_menuCftFont.get())
- : _subtitleFont;
- _highlightFont = _subtitleFont;
- _subtitleFontUsesCft = _subtitleCftFont.get() != nullptr;
- _menuFontUsesCft = _menuCftFont.get() != nullptr;
- _highlightFontUsesCft = _subtitleFontUsesCft;
-
- 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())
- _genericByeTopic = "BYE";
-
- _leftHeadSpeakerId = buildDialogueHeadId(_npc.npcName, 0);
- _rightHeadSpeakerId = buildDialogueHeadId("PC", 0);
- (void)loadDialogueHeadBitmap(_engine, _npc.npcName, 0, _leftHeadBitmap);
- (void)loadDialogueHeadBitmap(_engine, "PC", 0, _rightHeadBitmap);
- _leftHeadVisible = _leftHeadBitmap.isValid();
- _rightHeadVisible = _rightHeadBitmap.isValid();
- }
-
- bool isValid() const {
- return _script && _text && _art && _fallbackFont && _backdrop.isValid() &&
- _palette && _keywordBitmap.isValid();
- }
-
- const Common::String &getGenericByeTopic() const {
- return _genericByeTopic;
- }
-
- Common::Error playDialogueLineWithVariant(int wavId, const Common::String &speakerId,
- int headVariant) override {
- setActiveSpeakerPortrait(speakerId, headVariant);
-
- const StartupDialogueTextMode textMode = _script->getDialogueTextMode();
- Common::String subtitleText;
- const bool textEnabled = textMode != kStartupDialogueTextNone &&
- _text->resolveDialogueSubtitle(wavId, subtitleText);
- Common::Array<Common::String> subtitleLines;
- const IndexedBitmap *textboxBitmap = nullptr;
- if (textEnabled) {
- DialogueRendering::wrapDialogueTextLikeNative(*_subtitleFont, _subtitleFontUsesCft,
- subtitleText, kDialogueSubtitleTextWidth, subtitleLines);
- textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueTextboxIndex(subtitleLines.size()));
- }
-
- 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' 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) {
- _engine.stopSpeech();
- return releaseError;
- }
-
- bool voiceInterrupted = false;
- bool pointerInterrupted = false;
- Graphics::FrameLimiter limiter(g_system, 60);
- for (;;) {
- drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr, nullptr, -1, false, nullptr);
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result)) {
- _engine.stopSpeech();
- return result;
- }
-
- if (isDialogueVoiceInterruptEvent(event)) {
- voiceInterrupted = true;
- if (isDialoguePointerPressEvent(event))
- pointerInterrupted = true;
- }
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- if (voiceInterrupted || (!voiceStarted || !_engine.isSpeechPlaying()))
- break;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- _engine.stopSpeech();
- if (pointerInterrupted) {
- Common::Error releaseResult = waitForPointerRelease();
- if (releaseResult.getCode() != Common::kNoError)
- return releaseResult;
- } else if (textMode == kStartupDialogueTextClick) {
- Graphics::FrameLimiter clickLimiter(g_system, 60);
- for (;;) {
- drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr,
- nullptr, -1, false, nullptr);
-
- bool continuePressed = false;
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
-
- if (isDialogueContinueEvent(event))
- continuePressed = true;
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- if (continuePressed)
- break;
-
- clickLimiter.delayBeforeSwap();
- clickLimiter.startFrame();
- }
-
- Common::Error releaseResult = waitForPointerRelease();
- if (releaseResult.getCode() != Common::kNoError)
- return releaseResult;
- }
-
- return Common::kNoError;
- }
-
- Common::Error playDialogueLine(int wavId, const Common::String &speakerId) override {
- return playDialogueLineWithVariant(wavId, speakerId, 0);
- }
-
- Common::Error playDialogueEntrySequence(const DialogueLineEntry *lines, uint count) override {
- for (uint i = 0; i < count; ++i) {
- Common::Error lineError = playDialogueLineWithVariant(
- lines[i].wavId, lines[i].speakerId, lines[i].headVariant);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
-
- return Common::kNoError;
- }
-
- Common::Error playDialogueFst(const Common::String &path) override {
- FstPlayer fstPlayer(_engine);
- if (!fstPlayer.play(path))
- return Common::kReadingFailed;
-
- return Common::kNoError;
- }
-
- Common::Error clearScreenToBlack() override {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen)
- return Common::kReadingFailed;
-
- byte blackPalette[256 * 3];
- memset(blackPalette, 0, sizeof(blackPalette));
- screen->fillRect(screen->getBounds(), 0);
- setScaledPalette(*screen, blackPalette, 1.0f);
- screen->makeAllDirty();
- screen->update();
- return Common::kNoError;
- }
-
- Common::Error showCdChangePrompt(int discNumber) override {
- if (discNumber <= 0)
- return Common::kNoError;
-
- ResourceManager *resources = _engine.getResources();
- if (!resources)
- return Common::kReadingFailed;
- const int previousDisc = resources->getCurrentDisc();
-
- if (_engine.shouldShowCdChangePrompts()) {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen)
- return Common::kReadingFailed;
-
- const Common::String bitmapPath =
- Common::String::format("1:/GRAPHIC/OTHER/CD%d.BM", discNumber);
- IndexedBitmap promptBitmap;
- byte promptPalette[256 * 3];
- if (!loadBitmapResource(*resources, bitmapPath, promptBitmap) ||
- !loadPaletteResource(*resources, kCdChangePromptPalettePath, promptPalette)) {
- warning("Harvester: unable to load CD change prompt assets for disc %d", discNumber);
- return Common::kReadingFailed;
- }
-
- _engine.stopMusic();
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
- if (event.type == Common::EVENT_MOUSEMOVE)
- _mousePos = event.mouse;
- }
-
- bool waitingForRelease = false;
- bool needsRedraw = true;
- bool activateRequestedDisc = false;
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit() && !activateRequestedDisc) {
- if (needsRedraw) {
- screen->fillRect(screen->getBounds(), 0);
- RoomPresentation::blitBitmap(*screen, promptBitmap, 0, 0);
- setScaledPalette(*screen, promptPalette, 1.0f);
- if (_entityManager)
- _entityManager->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
- needsRedraw = false;
- }
-
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- _mousePos = event.mouse;
- needsRedraw = true;
- break;
- case Common::EVENT_LBUTTONDOWN:
- waitingForRelease = true;
- break;
- case Common::EVENT_LBUTTONUP:
- if (waitingForRelease)
- activateRequestedDisc = true;
- break;
- default:
- break;
- }
- }
-
- if (_entityManager && _entityManager->syncCursorEntityPosition(_mousePos))
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
- }
-
- if (!_engine.activateDisc(discNumber)) {
- warning("Harvester: unable to activate disc %d resources", discNumber);
- return Common::kReadingFailed;
- }
- if ((discNumber == 1 || discNumber == 3) &&
- previousDisc > 0 && previousDisc != resources->getCurrentDisc()) {
- Script *script = _engine.getScript();
- if (!script || !script->reloadTownWorld(*resources)) {
- warning("Harvester: unable to reload town script after disc prompt %d -> %d",
- previousDisc, discNumber);
- return Common::kReadingFailed;
- }
- }
-
- return Common::kNoError;
- }
-
- Common::Error runKeywordMenu(const Common::String &topicBuffer, int topicBufferLineIndex,
- Common::String &selectedTopic,
- DialogueKeywordMenuSelectionState &selection) override {
- selectedTopic.clear();
- selection = DialogueKeywordMenuSelectionState();
- Common::Array<Common::String> topics;
- splitDialogueMenuLine(topicBuffer, topics);
- logDialogueMenuItems("Keyword menu", topicBufferLineIndex, topicBuffer, topics);
- topics.push_back(_genericByeTopic);
- debugC(1, kDebugDialogue, "Harvester: keyword menu default topic='%s'", _genericByeTopic.c_str());
- bool editingOther = false;
- bool textEntryCursorVisible = false;
- uint32 textEntryBlinkTimestamp = 0;
- Common::String typedTopic;
- Common::Error releaseError = waitForPointerRelease();
- if (releaseError.getCode() != Common::kNoError)
- return releaseError;
-
- Graphics::FrameLimiter limiter(g_system, 60);
- for (;;) {
- const uint32 now = getRuntimeClockTicks();
- if (editingOther && now - textEntryBlinkTimestamp > kDialogueTextEntryBlinkTicks) {
- textEntryCursorVisible = !textEntryCursorVisible;
- textEntryBlinkTimestamp = now;
- }
- const int hoveredTopicIndex = editingOther ? -1 : DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
- const bool hoverOther = editingOther || DialogueRendering::isDialogueOtherHit(_mousePos);
- drawDialogueOverlay(&_keywordBitmap, nullptr, &topics, hoveredTopicIndex, hoverOther,
- editingOther ? &typedTopic : nullptr, textEntryCursorVisible);
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result)) {
- if (editingOther && _entityManager)
- _entityManager->showCursor();
- return result;
- }
-
- if (editingOther) {
- bool acceptInput = false;
- bool cancelInput = false;
- if (event.type == Common::EVENT_KEYDOWN) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- cancelInput = true;
- } else if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
- event.kbd.keycode == Common::KEYCODE_LEFT) {
- deleteTextEntryCharacter(typedTopic);
- } else if (event.kbd.keycode == Common::KEYCODE_HOME) {
- typedTopic.clear();
- } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
- event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
- acceptInput = true;
- } else if (event.kbd.ascii >= 0x1f && event.kbd.ascii <= 0x80 &&
- event.kbd.ascii != '~' &&
- (!_subtitleCftFont || _subtitleCftFont->hasGlyph(event.kbd.ascii))) {
- appendTextEntryCharacter(typedTopic, *_highlightFont, event.kbd.ascii,
- kDialogueTextEntryMaxLength, kDialogueTextEntryMaxWidth);
- }
- } else if (event.type == Common::EVENT_LBUTTONDOWN) {
- acceptInput = Common::Rect(3, 460, 318, 477).contains(event.mouse);
- cancelInput = Common::Rect(321, 460, 636, 477).contains(event.mouse);
- if (cancelInput)
- g_system->delayMillis(400);
- }
- if (acceptInput || cancelInput) {
- editingOther = false;
- if (_entityManager)
- _entityManager->showCursor();
- if (acceptInput && !typedTopic.empty()) {
- selectedTopic = typedTopic;
- selection.fromTypedInput = true;
- debugC(1, kDebugDialogue, "Harvester: keyword menu typed topic='%s'",
- selectedTopic.c_str());
- return Common::kNoError;
- }
- typedTopic.clear();
- debugC(1, kDebugDialogue, "Harvester: keyword menu cancelled Other input");
- }
- continue;
- }
-
- switch (event.type) {
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- selectedTopic = _genericByeTopic;
- selection.fromEscape = true;
- selection.fromGenericBye = true;
- debugC(1, kDebugDialogue,
- "Harvester: keyword menu selected default topic via escape '%s'",
- selectedTopic.c_str());
- return Common::kNoError;
- }
- break;
- case Common::EVENT_LBUTTONDOWN: {
- const int clickedTopic = DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
- if (clickedTopic >= 0) {
- selectedTopic = topics[(uint)clickedTopic];
- selection.fromGenericBye = clickedTopic == (int)topics.size() - 1;
- debugC(2, kDebugDialogue, "Harvester: keyword menu selected topic[%d]='%s' via click",
- clickedTopic + 1, selectedTopic.c_str());
- return Common::kNoError;
- }
- if (DialogueRendering::isDialogueOtherHit(_mousePos)) {
- typedTopic.clear();
- editingOther = true;
- textEntryCursorVisible = false;
- textEntryBlinkTimestamp = getRuntimeClockTicks();
- debugC(1, kDebugDialogue, "Harvester: keyword menu entered Other input");
- if (_entityManager)
- _entityManager->hideCursor();
- }
- break;
- }
- default:
- break;
- }
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
- }
-
- Common::Error runResponseMenu(int responseLineIndex, int &selectedIndex) override {
- const Common::String responseLine = _text->getDialogueResponseLine(responseLineIndex);
- return runResponseMenuTextInternal(responseLine, responseLineIndex, selectedIndex);
- }
-
- Common::Error runResponseMenuText(const Common::String &responseLine, int &selectedIndex) override {
- return runResponseMenuTextInternal(responseLine, -1, selectedIndex);
- }
-
- Common::Error runGameOverScreen() override {
- // GOG run_game_over_screen (0x7c540) enters run_main_menu (0x67390)
- // with GAMEOVER.BM and GAMEOVER.PAL retained as the menu backdrop.
- debugC(1, kDebugDialogue, "Harvester: dialogue death requesting game-over menu");
- _flow.requestGameOverReturn();
- return Common::kNoError;
- }
-
- void assignTopicBuffer(Common::String &topicBuffer, int &topicBufferLineIndex,
- int responseLineIndex, const char *) override {
- topicBuffer = _text->getDialogueResponseLine(responseLineIndex);
- topicBufferLineIndex = responseLineIndex;
- }
-
- bool matchesResponseLine(const Common::String &selectedTopic, int responseLineIndex) override {
- const Common::String topicText = _text->getDialogueResponseLine(responseLineIndex);
- return !topicText.empty() && selectedTopic.equalsIgnoreCase(topicText);
- }
-
- bool matchesAnyResponseLine(const Common::String &selectedTopic,
- const int *responseLineIndices, uint responseLineCount) override {
- for (uint i = 0; i < responseLineCount; ++i) {
- if (matchesResponseLine(selectedTopic, responseLineIndices[i]))
- return true;
- }
-
- return false;
- }
-
- void queueDialogueInteractionIfNeeded(const InteractionResult &interaction) override {
- if (interaction.abortRemainingCommandChain || interaction.mutatedRuntimeState ||
- !interaction.musicPath.empty() || !interaction.nextRoomName.empty() ||
- !interaction.cutscenePath.empty() ||
- !interaction.deathFlicPath.empty() || interaction.requestMainMenu ||
- interaction.requestDemoEnding ||
- interaction.requestRoomRestart ||
- interaction.requestCloseupExit ||
- interaction.cdChangeDisc > 0 ||
- !interaction.dialogueNpcName.empty() ||
- !interaction.dialogueContinuationTag.empty() ||
- !interaction.continuationTag.empty() ||
- !interaction.modalText.value.empty() ||
- interaction.lightingCommand != kStartupLightingCommandNone ||
- interaction.requestPlayerGotoXZ ||
- !interaction.audioCommands.empty()) {
- _flow.queueDialogueInteraction(interaction);
- }
- }
-
- void applyImmediateDialogueInteractionEffects(InteractionResult &interaction) override {
- if (!interaction.musicPath.empty())
- (void)_engine.playMusic(interaction.musicPath);
- if (!interaction.audioCommands.empty())
- _flow.executeStartupAudioCommands(interaction.audioCommands);
- interaction.musicPath.clear();
- interaction.audioCommands.clear();
- }
-
- int getRandomNumber(int maxValue) override {
- return _engine.getRandomNumber(maxValue);
- }
-
- void setActiveSpeakerPortrait(const Common::String &speakerId, int headVariant) override {
- if (speakerId.empty() || speakerId.equalsIgnoreCase("SPEECH"))
- return;
-
- ensureSpeakerPortrait(speakerId, headVariant);
- if (speakerId.equalsIgnoreCase("PC")) {
- _leftHeadVisible = false;
- _rightHeadVisible = _rightHeadBitmap.isValid();
- } else {
- _leftHeadVisible = _leftHeadBitmap.isValid();
- _rightHeadVisible = false;
- }
- }
-
-private:
- Graphics::Screen *getActiveScreen() const {
- return _engine.getScreen();
- }
-
- Common::Error waitForPointerRelease() {
- Graphics::FrameLimiter limiter(g_system, 60);
- while (!_engine.shouldQuit() && (isPrimaryMouseDown() || isSecondaryMouseDown())) {
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
- }
-
- void drawFontString(const Graphics::Font &font, bool usesCft, const Common::String &text,
- int x, int y, int width, byte color) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return;
-
- if (usesCft)
- font.drawString(activeScreen, text, x, y, width, 0);
- else
- RoomPresentation::drawShadowedString(*activeScreen, font, text, x, y, width, color);
- }
-
- void ensureSpeakerPortrait(const Common::String &speakerId, int headVariant) {
- if (speakerId.empty())
- return;
-
- const Common::String headId = buildDialogueHeadId(speakerId, headVariant);
- IndexedBitmap *targetBitmap = speakerId.equalsIgnoreCase("PC") ? &_rightHeadBitmap : &_leftHeadBitmap;
- Common::String *targetSpeakerId = speakerId.equalsIgnoreCase("PC") ? &_rightHeadSpeakerId : &_leftHeadSpeakerId;
- if (targetBitmap->isValid() && targetSpeakerId->equalsIgnoreCase(headId))
- return;
-
- IndexedBitmap updatedBitmap;
- if (loadDialogueHeadBitmap(_engine, speakerId, headVariant, updatedBitmap)) {
- *targetBitmap = Common::move(updatedBitmap);
- *targetSpeakerId = headId;
- } else {
- warning("Harvester: unable to load dialogue head for '%s'", headId.c_str());
- }
- }
-
- void drawDialogueOverlay(const IndexedBitmap *overlayBitmap,
- const Common::Array<Common::String> *subtitleLines, const Common::Array<Common::String> *topics,
- int hoveredTopicIndex, bool hoverOther, const Common::String *textEntryValue,
- bool textEntryCursorVisible = false) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return;
-
- setScaledPalette(*activeScreen, _palette, _paletteBrightness);
- RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
- if (_leftHeadVisible && _leftHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
- if (_rightHeadVisible && _rightHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
- if (overlayBitmap && overlayBitmap->isValid())
- blitTransparentBitmap(*activeScreen, *overlayBitmap, kDialogueOverlayX, kDialogueOverlayY);
-
- if (subtitleLines) {
- if (_subtitleFontUsesCft) {
- DialogueRendering::drawDialogueTextLines(*activeScreen, *_subtitleFont, *subtitleLines,
- kDialogueSubtitleTextX, kDialogueSubtitleTextY, kDialogueSubtitleTextWidth);
- } else {
- const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_subtitleFont);
- for (uint i = 0; i < subtitleLines->size(); ++i) {
- RoomPresentation::drawShadowedString(*activeScreen, *_subtitleFont, (*subtitleLines)[i],
- kDialogueSubtitleTextX,
- kDialogueSubtitleTextY + (int)i * lineHeight,
- kDialogueSubtitleTextWidth, kTextColorNormal);
- }
- }
- }
-
- 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 = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
- for (uint i = 0; i < topics->size(); ++i) {
- const bool highlighted = (int)i == hoveredTopicIndex;
- const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
- const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
- drawFontString(font, usesCft, (*topics)[i], kDialogueKeywordTextX,
- DialogueRendering::kDialogueKeywordTextY + (int)i * lineHeight, kDialogueKeywordTextWidth,
- highlighted ? kTextColorNormal : kTextColorHover);
- }
-
- const Graphics::Font &otherFont = hoverOther ? *_highlightFont : *_menuFont;
- const bool otherUsesCft = hoverOther ? _highlightFontUsesCft : _menuFontUsesCft;
- drawFontString(otherFont, otherUsesCft, _menuTextConfig.dialogueOtherLabel,
- kDialogueOtherX, kDialogueOtherY,
- kDialogueOtherWidth, hoverOther ? kTextColorNormal : kTextColorHover);
- }
-
- if (textEntryValue) {
- drawFontString(*_highlightFont, _highlightFontUsesCft,
- *textEntryValue + (textEntryCursorVisible ? "~" : ""),
- kDialogueTextEntryX, kDialogueTextEntryY, kDialogueTextEntryMaxWidth,
- kTextColorNormal);
- }
-
- if (_entityManager)
- _entityManager->drawCursor(*activeScreen);
- activeScreen->makeAllDirty();
- activeScreen->update();
- }
-
- void parseResponseMenuLine(const Common::String &responseLine,
- Common::Array<DialogueResponseOptionLayout> &options, uint &totalRows) {
- options.clear();
- totalRows = 0;
- if (responseLine.empty())
- return;
-
- Common::String segment;
- Common::Array<Common::String> explicitLines;
- Common::String optionText;
-
- auto flushOption = [&]() {
- if (explicitLines.empty())
- return;
-
- DialogueResponseOptionLayout option;
- option.text = optionText;
- if (optionText.size() > 1 && optionText[0] >= '1' && optionText[0] <= '9' &&
- optionText[1] == '.')
- option.hotkey = optionText[0];
- option.rowStart = (int)totalRows;
- for (const Common::String &explicitLine : explicitLines) {
- Common::Array<Common::String> wrappedExplicitLines;
- DialogueRendering::wrapDialogueTextLikeNative(*_menuFont, _menuFontUsesCft,
- explicitLine, kDialogueSubtitleTextWidth, wrappedExplicitLines);
- if (wrappedExplicitLines.empty())
- wrappedExplicitLines.push_back(explicitLine);
- for (const Common::String &wrappedExplicitLine : wrappedExplicitLines)
- option.wrappedLines.push_back(wrappedExplicitLine);
- }
- option.rowCount = MAX<int>(1, option.wrappedLines.size());
- totalRows += option.rowCount;
- options.push_back(Common::move(option));
-
- explicitLines.clear();
- optionText.clear();
- };
-
- auto consumeSegment = [&](const Common::String &rawSegment) {
- Common::String segmentText = rawSegment;
- if (segmentText.empty())
- return;
-
- const bool startsNewOption = segmentText.size() > 1 && segmentText[0] >= '1' &&
- segmentText[0] <= '9' && segmentText[1] == '.';
- if (startsNewOption) {
- flushOption();
- optionText = segmentText;
- explicitLines.push_back(Common::move(segmentText));
- return;
- }
-
- if (!optionText.empty())
- optionText += ' ';
- optionText += segmentText;
- explicitLines.push_back(Common::move(segmentText));
- };
-
- for (uint i = 0; i < responseLine.size(); ++i) {
- if (responseLine[i] == '/') {
- consumeSegment(segment);
- segment.clear();
- continue;
- }
-
- segment += responseLine[i];
- }
-
- consumeSegment(segment);
- flushOption();
- }
-
- int getResponseMenuItemAt(const Common::Array<DialogueResponseOptionLayout> &options,
- uint totalRows, const Common::Point &mousePos) {
- if (mousePos.x < DialogueRendering::kDialogueTopicStartX || mousePos.x > DialogueRendering::kDialogueTopicEndX)
- return -1;
- if (mousePos.y < DialogueRendering::kDialogueKeywordTextY)
- return -1;
-
- const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
- const int row = (mousePos.y - DialogueRendering::kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
- if (row < 0 || row >= (int)totalRows)
- return -1;
-
- for (uint i = 0; i < options.size(); ++i) {
- const DialogueResponseOptionLayout &option = options[i];
- if (row >= option.rowStart && row < option.rowStart + option.rowCount)
- return (int)i;
- }
-
- return -1;
- }
-
- void drawDialogueResponseMenu(const IndexedBitmap *textboxBitmap,
- const Common::Array<DialogueResponseOptionLayout> &options, int hoveredOptionIndex) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return;
-
- setScaledPalette(*activeScreen, _palette, _paletteBrightness);
- RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
- if (_leftHeadVisible && _leftHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
- if (_rightHeadVisible && _rightHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
- if (textboxBitmap && textboxBitmap->isValid())
- blitTransparentBitmap(*activeScreen, *textboxBitmap, kDialogueOverlayX, kDialogueOverlayY);
-
- const Common::String &title = _menuTextConfig.dialogueResponsesLabel;
- const Graphics::Font &titleFont = *_highlightFont;
- const bool titleUsesCft = _highlightFontUsesCft;
- const int titleWidth = titleFont.getStringWidth(title);
- const int titleX = kDialogueSubtitleTextX + MAX<int>(0, (kDialogueSubtitleTextWidth - titleWidth) / 2);
- drawFontString(titleFont, titleUsesCft, title, titleX, 11, titleWidth, kTextColorNormal);
-
- const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
- int drawY = DialogueRendering::kDialogueKeywordTextY;
- for (uint i = 0; i < options.size(); ++i) {
- const bool highlighted = (int)i == hoveredOptionIndex;
- const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
- const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
- const byte color = highlighted ? kTextColorNormal : kTextColorHover;
-
- for (const Common::String &wrappedLine : options[i].wrappedLines) {
- drawFontString(font, usesCft, wrappedLine, kDialogueSubtitleTextX, drawY,
- kDialogueSubtitleTextWidth, color);
- drawY += lineHeight;
- }
- }
-
- if (_entityManager)
- _entityManager->drawCursor(*activeScreen);
- activeScreen->makeAllDirty();
- activeScreen->update();
- }
-
- Common::Error runResponseMenuTextInternal(const Common::String &responseLine, int responseLineIndex,
- int &selectedIndex) {
- selectedIndex = 0;
-
- if (responseLine.empty()) {
- if (responseLineIndex >= 0) {
- debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) is empty",
- responseLineIndex, responseLineIndex + 1);
- } else {
- debugC(1, kDebugDialogue, "Harvester: response menu text is empty");
- }
- return Common::kNoError;
- }
-
- Common::Array<DialogueResponseOptionLayout> options;
- uint totalRows = 0;
- parseResponseMenuLine(responseLine, options, totalRows);
- if (options.empty()) {
- if (responseLineIndex >= 0) {
- debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) produced no options",
- responseLineIndex, responseLineIndex + 1);
- } else {
- debugC(1, kDebugDialogue, "Harvester: response menu text produced no options");
- }
- return Common::kNoError;
- }
-
- Common::Array<Common::String> optionTexts;
- for (const DialogueResponseOptionLayout &option : options) {
- optionTexts.push_back(option.text);
- debugC(3, kDebugDialogue, "Harvester: response menu option[%u] hotkey=%c rowStart=%d rowCount=%d",
- optionTexts.size(), option.hotkey ? option.hotkey : '-', option.rowStart, option.rowCount);
- }
- logDialogueMenuItems("Response menu", responseLineIndex, responseLine, optionTexts);
-
- const IndexedBitmap *textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueResponseTextboxIndex(totalRows));
- Common::Error releaseError = waitForPointerRelease();
- if (releaseError.getCode() != Common::kNoError)
- return releaseError;
-
- Graphics::FrameLimiter limiter(g_system, 60);
- for (;;) {
- const int hoveredOptionIndex = getResponseMenuItemAt(options, totalRows, _mousePos);
- drawDialogueResponseMenu(textboxBitmap, options, hoveredOptionIndex);
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
-
- if (event.type == Common::EVENT_KEYDOWN) {
- debugC(3, kDebugDialogue, "Harvester: response menu keycode=%d ascii=%u",
- (int)event.kbd.keycode, event.kbd.ascii);
- if (event.kbd.keycode >= Common::KEYCODE_1 && event.kbd.keycode <= Common::KEYCODE_9) {
- const char hotkey = '1' + event.kbd.keycode - Common::KEYCODE_1;
- for (int optionIndex = (int)options.size() - 1; optionIndex >= 0; --optionIndex) {
- const DialogueResponseOptionLayout &option = options[(uint)optionIndex];
- if (option.hotkey != hotkey)
- continue;
- selectedIndex = optionIndex + 1;
- if (responseLineIndex >= 0) {
- debugC(2, kDebugDialogue,
- "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via hotkey=%c result=%d",
- responseLineIndex, responseLineIndex + 1,
- optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
- } else {
- debugC(2, kDebugDialogue,
- "Harvester: response menu text selected option[%d]='%s' via hotkey=%c result=%d",
- optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
- }
- return waitForPointerRelease();
- }
- }
- } else if (event.type == Common::EVENT_LBUTTONDOWN) {
- const int clickedOptionIndex = getResponseMenuItemAt(options, totalRows, event.mouse);
- if (clickedOptionIndex < 0)
- continue;
- selectedIndex = clickedOptionIndex + 1;
- if (responseLineIndex >= 0) {
- debugC(2, kDebugDialogue,
- "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via click x=%d y=%d result=%d",
- responseLineIndex, responseLineIndex + 1,
- selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
- event.mouse.x, event.mouse.y, selectedIndex);
- } else {
- debugC(2, kDebugDialogue,
- "Harvester: response menu text selected option[%d]='%s' via click x=%d y=%d result=%d",
- selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
- event.mouse.x, event.mouse.y, selectedIndex);
- }
- return waitForPointerRelease();
- }
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
- }
-
- HarvesterEngine &_engine;
- Common::Point &_mousePos;
- Flow &_flow;
- const MenuTextConfig &_menuTextConfig;
- const IndexedBitmap &_backdrop;
- const byte *_palette;
- const float _paletteBrightness;
- const NpcRecord &_npc;
- Script *_script;
- Text *_text;
- Art *_art;
- EntityManager *_entityManager;
- const Graphics::Font *_fallbackFont;
- Common::ScopedPtr<HarvesterCftFont> _subtitleCftFont;
- Common::ScopedPtr<HarvesterCftFont> _menuCftFont;
- const Graphics::Font *_subtitleFont;
- const Graphics::Font *_menuFont;
- const Graphics::Font *_highlightFont;
- bool _subtitleFontUsesCft;
- bool _menuFontUsesCft;
- bool _highlightFontUsesCft;
- IndexedBitmap _keywordBitmap;
- Common::String _genericByeTopic;
- IndexedBitmap _leftHeadBitmap;
- IndexedBitmap _rightHeadBitmap;
- Common::String _leftHeadSpeakerId;
- Common::String _rightHeadSpeakerId;
- bool _leftHeadVisible;
- bool _rightHeadVisible;
-};
-
} // End of anonymous namespace
DialogueSystem::DialogueSystem(HarvesterEngine &engine, Common::Point &mousePos)
diff --git a/engines/harvester/dialogue/dialogue_session.cpp b/engines/harvester/dialogue/dialogue_session.cpp
new file mode 100644
index 00000000000..551c0bfbf6a
--- /dev/null
+++ b/engines/harvester/dialogue/dialogue_session.cpp
@@ -0,0 +1,1052 @@
+/* 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/dialogue/dialogue_session.h"
+#include "harvester/dialogue/dialogue_render.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "graphics/fontman.h"
+#include "graphics/framelimiter.h"
+#include "harvester/art.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/flow/flow.h"
+#include "harvester/flow/flow_support.h"
+#include "harvester/fst_player.h"
+#include "harvester/harvester.h"
+#include "harvester/resources.h"
+#include "harvester/scene/room_presentation.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+namespace {
+
+static const int kDialogueOverlayX = 167;
+static const int kDialogueOverlayY = 6;
+static const int kDialogueLeftHeadX = 6;
+static const int kDialogueRightHeadX = 474;
+static const int kDialogueHeadY = 6;
+static const int kDialogueSubtitleTextX = 178;
+static const int kDialogueSubtitleTextY = 12;
+static const int kDialogueSubtitleTextWidth = 316;
+static const int kDialogueKeywordTextX = 178;
+static const int kDialogueKeywordTextWidth = 300;
+static const int kDialogueOtherX = 171;
+static const int kDialogueOtherY = 183;
+static const int kDialogueTextEntryX = 208;
+static const int kDialogueTextEntryY = 184;
+static const uint kDialogueTextEntryMaxLength = 26;
+static const int kDialogueTextEntryMaxWidth = 550;
+static const uint32 kDialogueTextEntryBlinkTicks = 0x30;
+static const int kDialogueOtherWidth = 120;
+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 byte kTextColorNormal = 255;
+static const byte kTextColorHover = 251;
+
+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 bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
+ Common::Array<byte> data;
+ if (!resources.loadFile(path, data) || data.size() < 256 * 3)
+ return false;
+
+ memcpy(dest, data.data(), 256 * 3);
+ return true;
+}
+
+static bool isPrimaryMouseDown() {
+ return g_system && g_system->getEventManager() &&
+ ((g_system->getEventManager()->getButtonState() & 1) != 0);
+}
+
+static bool isSecondaryMouseDown() {
+ return g_system && g_system->getEventManager() &&
+ ((g_system->getEventManager()->getButtonState() & 2) != 0);
+}
+
+static void splitDialogueMenuLine(const Common::String &line, Common::Array<Common::String> &parts) {
+ parts.clear();
+ if (line.empty())
+ return;
+
+ Common::String token;
+ for (uint i = 0; i < line.size(); ++i) {
+ if (line[i] == '/') {
+ parts.push_back(Common::move(token));
+ token.clear();
+ continue;
+ }
+
+ token += line[i];
+ }
+
+ parts.push_back(Common::move(token));
+}
+
+static void logDialogueMenuItems(const char *label, int sourceLineIndex,
+ const Common::String &rawLine, const Common::Array<Common::String> &items) {
+ if (sourceLineIndex >= 0) {
+ debugC(3, kDebugDialogue, "Harvester: %s source line 0x%x (%d) raw='%s'",
+ label, sourceLineIndex, sourceLineIndex + 1, rawLine.c_str());
+ } else {
+ debugC(3, kDebugDialogue, "Harvester: %s source raw='%s'", label, rawLine.c_str());
+ }
+
+ if (items.empty()) {
+ debugC(1, kDebugDialogue, "Harvester: %s has no explicit items", label);
+ return;
+ }
+
+ for (uint i = 0; i < items.size(); ++i)
+ debugC(3, kDebugDialogue, "Harvester: %s item[%u]='%s'", label, i + 1, items[i].c_str());
+}
+
+static Common::String buildDialogueHeadId(const Common::String &speakerId, int headVariant) {
+ return Common::String::format("%s%d", speakerId.c_str(), headVariant);
+}
+
+static bool loadDialogueHeadBitmap(HarvesterEngine &engine, const Common::String &speakerId, int headVariant,
+ IndexedBitmap &bitmap) {
+ Script *script = engine.getScript();
+ ResourceManager *resources = engine.getResources();
+ if (!script || !resources || speakerId.empty())
+ return false;
+
+ const Common::String headId = buildDialogueHeadId(speakerId, headVariant);
+ const HeadRecord *head = script->findHeadRecord(headId);
+ if (!head)
+ return false;
+
+ return loadBitmapResource(*resources, head->portraitPath + ".BM", bitmap);
+}
+
+static Common::String buildDialogueVoicePath(const Script &script, int wavId) {
+ if (wavId <= 0)
+ return Common::String();
+
+ return Common::String::format("%s%d.CMP", script.getVoicePath().c_str(), wavId);
+}
+
+} // End of anonymous namespace
+
+RoomNpcDialogueSession::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),
+ _menuTextConfig(flow.getMenuTextConfig()), _backdrop(backdrop),
+ _palette(palette), _paletteBrightness(paletteBrightness), _npc(npc),
+ _script(engine.getScript()), _text(engine.getText()), _art(engine.getArt()),
+ _entityManager(engine.getRuntimeEntities()),
+ _fallbackFont(FontMan.getFontByUsage(Graphics::FontManager::kGUIFont)),
+ _subtitleFont(nullptr), _menuFont(nullptr), _highlightFont(nullptr),
+ _subtitleFontUsesCft(false), _menuFontUsesCft(false),
+ _highlightFontUsesCft(false), _leftHeadVisible(false),
+ _rightHeadVisible(false) {
+ ResourceManager *resources = _engine.getResources();
+ if (!_script || !_text || !_art || !_fallbackFont || !_backdrop.isValid() ||
+ !_palette || !resources) {
+ return;
+ }
+
+ if (const CftFontResource *subtitleFontResource = _text->findFont("TEXTFONT")) {
+ _subtitleCftFont.reset(new HarvesterCftFont(*subtitleFontResource));
+ if (!_subtitleCftFont->isValid())
+ _subtitleCftFont.reset();
+ }
+ if (const CftFontResource *menuFontResource = _text->findFont("TEXTFNT2")) {
+ _menuCftFont.reset(new HarvesterCftFont(*menuFontResource));
+ if (!_menuCftFont->isValid())
+ _menuCftFont.reset();
+ }
+
+ _subtitleFont = _subtitleCftFont
+ ? static_cast<const Graphics::Font *>(_subtitleCftFont.get())
+ : _fallbackFont;
+ _menuFont = _menuCftFont
+ ? static_cast<const Graphics::Font *>(_menuCftFont.get())
+ : _subtitleFont;
+ _highlightFont = _subtitleFont;
+ _subtitleFontUsesCft = _subtitleCftFont.get() != nullptr;
+ _menuFontUsesCft = _menuCftFont.get() != nullptr;
+ _highlightFontUsesCft = _subtitleFontUsesCft;
+
+ 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())
+ _genericByeTopic = "BYE";
+
+ _leftHeadSpeakerId = buildDialogueHeadId(_npc.npcName, 0);
+ _rightHeadSpeakerId = buildDialogueHeadId("PC", 0);
+ (void)loadDialogueHeadBitmap(_engine, _npc.npcName, 0, _leftHeadBitmap);
+ (void)loadDialogueHeadBitmap(_engine, "PC", 0, _rightHeadBitmap);
+ _leftHeadVisible = _leftHeadBitmap.isValid();
+ _rightHeadVisible = _rightHeadBitmap.isValid();
+}
+
+bool RoomNpcDialogueSession::isValid() const {
+ return _script && _text && _art && _fallbackFont && _backdrop.isValid() &&
+ _palette && _keywordBitmap.isValid();
+}
+
+const Common::String &RoomNpcDialogueSession::getGenericByeTopic() const {
+ return _genericByeTopic;
+}
+
+Common::Error RoomNpcDialogueSession::playDialogueLineWithVariant(int wavId, const Common::String &speakerId,
+ int headVariant) {
+ setActiveSpeakerPortrait(speakerId, headVariant);
+
+ const StartupDialogueTextMode textMode = _script->getDialogueTextMode();
+ Common::String subtitleText;
+ const bool textEnabled = textMode != kStartupDialogueTextNone &&
+ _text->resolveDialogueSubtitle(wavId, subtitleText);
+ Common::Array<Common::String> subtitleLines;
+ const IndexedBitmap *textboxBitmap = nullptr;
+ if (textEnabled) {
+ DialogueRendering::wrapDialogueTextLikeNative(*_subtitleFont, _subtitleFontUsesCft,
+ subtitleText, kDialogueSubtitleTextWidth, subtitleLines);
+ textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueTextboxIndex(subtitleLines.size()));
+ }
+
+ 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' 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) {
+ _engine.stopSpeech();
+ return releaseError;
+ }
+
+ bool voiceInterrupted = false;
+ bool pointerInterrupted = false;
+ Graphics::FrameLimiter limiter(g_system, 60);
+ for (;;) {
+ drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr, nullptr, -1, false, nullptr);
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result)) {
+ _engine.stopSpeech();
+ return result;
+ }
+
+ if (isDialogueVoiceInterruptEvent(event)) {
+ voiceInterrupted = true;
+ if (isDialoguePointerPressEvent(event))
+ pointerInterrupted = true;
+ }
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ if (voiceInterrupted || (!voiceStarted || !_engine.isSpeechPlaying()))
+ break;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ _engine.stopSpeech();
+ if (pointerInterrupted) {
+ Common::Error releaseResult = waitForPointerRelease();
+ if (releaseResult.getCode() != Common::kNoError)
+ return releaseResult;
+ } else if (textMode == kStartupDialogueTextClick) {
+ Graphics::FrameLimiter clickLimiter(g_system, 60);
+ for (;;) {
+ drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr,
+ nullptr, -1, false, nullptr);
+
+ bool continuePressed = false;
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+
+ if (isDialogueContinueEvent(event))
+ continuePressed = true;
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ if (continuePressed)
+ break;
+
+ clickLimiter.delayBeforeSwap();
+ clickLimiter.startFrame();
+ }
+
+ Common::Error releaseResult = waitForPointerRelease();
+ if (releaseResult.getCode() != Common::kNoError)
+ return releaseResult;
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::playDialogueLine(int wavId, const Common::String &speakerId) {
+ return playDialogueLineWithVariant(wavId, speakerId, 0);
+}
+
+Common::Error RoomNpcDialogueSession::playDialogueEntrySequence(const DialogueLineEntry *lines, uint count) {
+ for (uint i = 0; i < count; ++i) {
+ Common::Error lineError = playDialogueLineWithVariant(
+ lines[i].wavId, lines[i].speakerId, lines[i].headVariant);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::playDialogueFst(const Common::String &path) {
+ FstPlayer fstPlayer(_engine);
+ if (!fstPlayer.play(path))
+ return Common::kReadingFailed;
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::clearScreenToBlack() {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen)
+ return Common::kReadingFailed;
+
+ byte blackPalette[256 * 3];
+ memset(blackPalette, 0, sizeof(blackPalette));
+ screen->fillRect(screen->getBounds(), 0);
+ setScaledPalette(*screen, blackPalette, 1.0f);
+ screen->makeAllDirty();
+ screen->update();
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::showCdChangePrompt(int discNumber) {
+ if (discNumber <= 0)
+ return Common::kNoError;
+
+ ResourceManager *resources = _engine.getResources();
+ if (!resources)
+ return Common::kReadingFailed;
+ const int previousDisc = resources->getCurrentDisc();
+
+ if (_engine.shouldShowCdChangePrompts()) {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen)
+ return Common::kReadingFailed;
+
+ const Common::String bitmapPath =
+ Common::String::format("1:/GRAPHIC/OTHER/CD%d.BM", discNumber);
+ IndexedBitmap promptBitmap;
+ byte promptPalette[256 * 3];
+ if (!loadBitmapResource(*resources, bitmapPath, promptBitmap) ||
+ !loadPaletteResource(*resources, kCdChangePromptPalettePath, promptPalette)) {
+ warning("Harvester: unable to load CD change prompt assets for disc %d", discNumber);
+ return Common::kReadingFailed;
+ }
+
+ _engine.stopMusic();
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+ if (event.type == Common::EVENT_MOUSEMOVE)
+ _mousePos = event.mouse;
+ }
+
+ bool waitingForRelease = false;
+ bool needsRedraw = true;
+ bool activateRequestedDisc = false;
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit() && !activateRequestedDisc) {
+ if (needsRedraw) {
+ screen->fillRect(screen->getBounds(), 0);
+ RoomPresentation::blitBitmap(*screen, promptBitmap, 0, 0);
+ setScaledPalette(*screen, promptPalette, 1.0f);
+ if (_entityManager)
+ _entityManager->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+ needsRedraw = false;
+ }
+
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ _mousePos = event.mouse;
+ needsRedraw = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ waitingForRelease = true;
+ break;
+ case Common::EVENT_LBUTTONUP:
+ if (waitingForRelease)
+ activateRequestedDisc = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (_entityManager && _entityManager->syncCursorEntityPosition(_mousePos))
+ needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+ }
+
+ if (!_engine.activateDisc(discNumber)) {
+ warning("Harvester: unable to activate disc %d resources", discNumber);
+ return Common::kReadingFailed;
+ }
+ if ((discNumber == 1 || discNumber == 3) &&
+ previousDisc > 0 && previousDisc != resources->getCurrentDisc()) {
+ Script *script = _engine.getScript();
+ if (!script || !script->reloadTownWorld(*resources)) {
+ warning("Harvester: unable to reload town script after disc prompt %d -> %d",
+ previousDisc, discNumber);
+ return Common::kReadingFailed;
+ }
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::runKeywordMenu(const Common::String &topicBuffer, int topicBufferLineIndex,
+ Common::String &selectedTopic,
+ DialogueKeywordMenuSelectionState &selection) {
+ selectedTopic.clear();
+ selection = DialogueKeywordMenuSelectionState();
+ Common::Array<Common::String> topics;
+ splitDialogueMenuLine(topicBuffer, topics);
+ logDialogueMenuItems("Keyword menu", topicBufferLineIndex, topicBuffer, topics);
+ topics.push_back(_genericByeTopic);
+ debugC(1, kDebugDialogue, "Harvester: keyword menu default topic='%s'", _genericByeTopic.c_str());
+ bool editingOther = false;
+ bool textEntryCursorVisible = false;
+ uint32 textEntryBlinkTimestamp = 0;
+ Common::String typedTopic;
+ Common::Error releaseError = waitForPointerRelease();
+ if (releaseError.getCode() != Common::kNoError)
+ return releaseError;
+
+ Graphics::FrameLimiter limiter(g_system, 60);
+ for (;;) {
+ const uint32 now = getRuntimeClockTicks();
+ if (editingOther && now - textEntryBlinkTimestamp > kDialogueTextEntryBlinkTicks) {
+ textEntryCursorVisible = !textEntryCursorVisible;
+ textEntryBlinkTimestamp = now;
+ }
+ const int hoveredTopicIndex = editingOther ? -1 : DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
+ const bool hoverOther = editingOther || DialogueRendering::isDialogueOtherHit(_mousePos);
+ drawDialogueOverlay(&_keywordBitmap, nullptr, &topics, hoveredTopicIndex, hoverOther,
+ editingOther ? &typedTopic : nullptr, textEntryCursorVisible);
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result)) {
+ if (editingOther && _entityManager)
+ _entityManager->showCursor();
+ return result;
+ }
+
+ if (editingOther) {
+ bool acceptInput = false;
+ bool cancelInput = false;
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ cancelInput = true;
+ } else if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
+ event.kbd.keycode == Common::KEYCODE_LEFT) {
+ deleteTextEntryCharacter(typedTopic);
+ } else if (event.kbd.keycode == Common::KEYCODE_HOME) {
+ typedTopic.clear();
+ } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+ event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+ acceptInput = true;
+ } else if (event.kbd.ascii >= 0x1f && event.kbd.ascii <= 0x80 &&
+ event.kbd.ascii != '~' &&
+ (!_subtitleCftFont || _subtitleCftFont->hasGlyph(event.kbd.ascii))) {
+ appendTextEntryCharacter(typedTopic, *_highlightFont, event.kbd.ascii,
+ kDialogueTextEntryMaxLength, kDialogueTextEntryMaxWidth);
+ }
+ } else if (event.type == Common::EVENT_LBUTTONDOWN) {
+ acceptInput = Common::Rect(3, 460, 318, 477).contains(event.mouse);
+ cancelInput = Common::Rect(321, 460, 636, 477).contains(event.mouse);
+ if (cancelInput)
+ g_system->delayMillis(400);
+ }
+ if (acceptInput || cancelInput) {
+ editingOther = false;
+ if (_entityManager)
+ _entityManager->showCursor();
+ if (acceptInput && !typedTopic.empty()) {
+ selectedTopic = typedTopic;
+ selection.fromTypedInput = true;
+ debugC(1, kDebugDialogue, "Harvester: keyword menu typed topic='%s'",
+ selectedTopic.c_str());
+ return Common::kNoError;
+ }
+ typedTopic.clear();
+ debugC(1, kDebugDialogue, "Harvester: keyword menu cancelled Other input");
+ }
+ continue;
+ }
+
+ switch (event.type) {
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ selectedTopic = _genericByeTopic;
+ selection.fromEscape = true;
+ selection.fromGenericBye = true;
+ debugC(1, kDebugDialogue,
+ "Harvester: keyword menu selected default topic via escape '%s'",
+ selectedTopic.c_str());
+ return Common::kNoError;
+ }
+ break;
+ case Common::EVENT_LBUTTONDOWN: {
+ const int clickedTopic = DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
+ if (clickedTopic >= 0) {
+ selectedTopic = topics[(uint)clickedTopic];
+ selection.fromGenericBye = clickedTopic == (int)topics.size() - 1;
+ debugC(2, kDebugDialogue, "Harvester: keyword menu selected topic[%d]='%s' via click",
+ clickedTopic + 1, selectedTopic.c_str());
+ return Common::kNoError;
+ }
+ if (DialogueRendering::isDialogueOtherHit(_mousePos)) {
+ typedTopic.clear();
+ editingOther = true;
+ textEntryCursorVisible = false;
+ textEntryBlinkTimestamp = getRuntimeClockTicks();
+ debugC(1, kDebugDialogue, "Harvester: keyword menu entered Other input");
+ if (_entityManager)
+ _entityManager->hideCursor();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+}
+
+Common::Error RoomNpcDialogueSession::runResponseMenu(int responseLineIndex, int &selectedIndex) {
+ const Common::String responseLine = _text->getDialogueResponseLine(responseLineIndex);
+ return runResponseMenuTextInternal(responseLine, responseLineIndex, selectedIndex);
+}
+
+Common::Error RoomNpcDialogueSession::runResponseMenuText(const Common::String &responseLine, int &selectedIndex) {
+ return runResponseMenuTextInternal(responseLine, -1, selectedIndex);
+}
+
+Common::Error RoomNpcDialogueSession::runGameOverScreen() {
+ // GOG run_game_over_screen (0x7c540) enters run_main_menu (0x67390)
+ // with GAMEOVER.BM and GAMEOVER.PAL retained as the menu backdrop.
+ debugC(1, kDebugDialogue, "Harvester: dialogue death requesting game-over menu");
+ _flow.requestGameOverReturn();
+ return Common::kNoError;
+}
+
+void RoomNpcDialogueSession::assignTopicBuffer(Common::String &topicBuffer, int &topicBufferLineIndex,
+ int responseLineIndex, const char *) {
+ topicBuffer = _text->getDialogueResponseLine(responseLineIndex);
+ topicBufferLineIndex = responseLineIndex;
+}
+
+bool RoomNpcDialogueSession::matchesResponseLine(const Common::String &selectedTopic, int responseLineIndex) {
+ const Common::String topicText = _text->getDialogueResponseLine(responseLineIndex);
+ return !topicText.empty() && selectedTopic.equalsIgnoreCase(topicText);
+}
+
+bool RoomNpcDialogueSession::matchesAnyResponseLine(const Common::String &selectedTopic,
+ const int *responseLineIndices, uint responseLineCount) {
+ for (uint i = 0; i < responseLineCount; ++i) {
+ if (matchesResponseLine(selectedTopic, responseLineIndices[i]))
+ return true;
+ }
+
+ return false;
+}
+
+void RoomNpcDialogueSession::queueDialogueInteractionIfNeeded(const InteractionResult &interaction) {
+ if (interaction.abortRemainingCommandChain || interaction.mutatedRuntimeState ||
+ !interaction.musicPath.empty() || !interaction.nextRoomName.empty() ||
+ !interaction.cutscenePath.empty() ||
+ !interaction.deathFlicPath.empty() || interaction.requestMainMenu ||
+ interaction.requestDemoEnding ||
+ interaction.requestRoomRestart ||
+ interaction.requestCloseupExit ||
+ interaction.cdChangeDisc > 0 ||
+ !interaction.dialogueNpcName.empty() ||
+ !interaction.dialogueContinuationTag.empty() ||
+ !interaction.continuationTag.empty() ||
+ !interaction.modalText.value.empty() ||
+ interaction.lightingCommand != kStartupLightingCommandNone ||
+ interaction.requestPlayerGotoXZ ||
+ !interaction.audioCommands.empty()) {
+ _flow.queueDialogueInteraction(interaction);
+ }
+}
+
+void RoomNpcDialogueSession::applyImmediateDialogueInteractionEffects(InteractionResult &interaction) {
+ if (!interaction.musicPath.empty())
+ (void)_engine.playMusic(interaction.musicPath);
+ if (!interaction.audioCommands.empty())
+ _flow.executeStartupAudioCommands(interaction.audioCommands);
+ interaction.musicPath.clear();
+ interaction.audioCommands.clear();
+}
+
+int RoomNpcDialogueSession::getRandomNumber(int maxValue) {
+ return _engine.getRandomNumber(maxValue);
+}
+
+void RoomNpcDialogueSession::setActiveSpeakerPortrait(const Common::String &speakerId, int headVariant) {
+ if (speakerId.empty() || speakerId.equalsIgnoreCase("SPEECH"))
+ return;
+
+ ensureSpeakerPortrait(speakerId, headVariant);
+ if (speakerId.equalsIgnoreCase("PC")) {
+ _leftHeadVisible = false;
+ _rightHeadVisible = _rightHeadBitmap.isValid();
+ } else {
+ _leftHeadVisible = _leftHeadBitmap.isValid();
+ _rightHeadVisible = false;
+ }
+}
+
+Graphics::Screen *RoomNpcDialogueSession::getActiveScreen() const {
+ return _engine.getScreen();
+}
+
+Common::Error RoomNpcDialogueSession::waitForPointerRelease() {
+ Graphics::FrameLimiter limiter(g_system, 60);
+ while (!_engine.shouldQuit() && (isPrimaryMouseDown() || isSecondaryMouseDown())) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+void RoomNpcDialogueSession::drawFontString(const Graphics::Font &font, bool usesCft, const Common::String &text,
+ int x, int y, int width, byte color) {
+ Graphics::Screen *activeScreen = getActiveScreen();
+ if (!activeScreen)
+ return;
+
+ if (usesCft)
+ font.drawString(activeScreen, text, x, y, width, 0);
+ else
+ RoomPresentation::drawShadowedString(*activeScreen, font, text, x, y, width, color);
+}
+
+void RoomNpcDialogueSession::ensureSpeakerPortrait(const Common::String &speakerId, int headVariant) {
+ if (speakerId.empty())
+ return;
+
+ const Common::String headId = buildDialogueHeadId(speakerId, headVariant);
+ IndexedBitmap *targetBitmap = speakerId.equalsIgnoreCase("PC") ? &_rightHeadBitmap : &_leftHeadBitmap;
+ Common::String *targetSpeakerId = speakerId.equalsIgnoreCase("PC") ? &_rightHeadSpeakerId : &_leftHeadSpeakerId;
+ if (targetBitmap->isValid() && targetSpeakerId->equalsIgnoreCase(headId))
+ return;
+
+ IndexedBitmap updatedBitmap;
+ if (loadDialogueHeadBitmap(_engine, speakerId, headVariant, updatedBitmap)) {
+ *targetBitmap = Common::move(updatedBitmap);
+ *targetSpeakerId = headId;
+ } else {
+ warning("Harvester: unable to load dialogue head for '%s'", headId.c_str());
+ }
+}
+
+void RoomNpcDialogueSession::drawDialogueOverlay(const IndexedBitmap *overlayBitmap,
+ const Common::Array<Common::String> *subtitleLines, const Common::Array<Common::String> *topics,
+ int hoveredTopicIndex, bool hoverOther, const Common::String *textEntryValue,
+ bool textEntryCursorVisible) {
+ Graphics::Screen *activeScreen = getActiveScreen();
+ if (!activeScreen)
+ return;
+
+ setScaledPalette(*activeScreen, _palette, _paletteBrightness);
+ RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
+ if (_leftHeadVisible && _leftHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
+ if (_rightHeadVisible && _rightHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
+ if (overlayBitmap && overlayBitmap->isValid())
+ blitTransparentBitmap(*activeScreen, *overlayBitmap, kDialogueOverlayX, kDialogueOverlayY);
+
+ if (subtitleLines) {
+ if (_subtitleFontUsesCft) {
+ DialogueRendering::drawDialogueTextLines(*activeScreen, *_subtitleFont, *subtitleLines,
+ kDialogueSubtitleTextX, kDialogueSubtitleTextY, kDialogueSubtitleTextWidth);
+ } else {
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_subtitleFont);
+ for (uint i = 0; i < subtitleLines->size(); ++i) {
+ RoomPresentation::drawShadowedString(*activeScreen, *_subtitleFont, (*subtitleLines)[i],
+ kDialogueSubtitleTextX,
+ kDialogueSubtitleTextY + (int)i * lineHeight,
+ kDialogueSubtitleTextWidth, kTextColorNormal);
+ }
+ }
+ }
+
+ 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 = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ for (uint i = 0; i < topics->size(); ++i) {
+ const bool highlighted = (int)i == hoveredTopicIndex;
+ const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
+ const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
+ drawFontString(font, usesCft, (*topics)[i], kDialogueKeywordTextX,
+ DialogueRendering::kDialogueKeywordTextY + (int)i * lineHeight, kDialogueKeywordTextWidth,
+ highlighted ? kTextColorNormal : kTextColorHover);
+ }
+
+ const Graphics::Font &otherFont = hoverOther ? *_highlightFont : *_menuFont;
+ const bool otherUsesCft = hoverOther ? _highlightFontUsesCft : _menuFontUsesCft;
+ drawFontString(otherFont, otherUsesCft, _menuTextConfig.dialogueOtherLabel,
+ kDialogueOtherX, kDialogueOtherY,
+ kDialogueOtherWidth, hoverOther ? kTextColorNormal : kTextColorHover);
+ }
+
+ if (textEntryValue) {
+ drawFontString(*_highlightFont, _highlightFontUsesCft,
+ *textEntryValue + (textEntryCursorVisible ? "~" : ""),
+ kDialogueTextEntryX, kDialogueTextEntryY, kDialogueTextEntryMaxWidth,
+ kTextColorNormal);
+ }
+
+ if (_entityManager)
+ _entityManager->drawCursor(*activeScreen);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+}
+
+void RoomNpcDialogueSession::parseResponseMenuLine(const Common::String &responseLine,
+ Common::Array<DialogueResponseOptionLayout> &options, uint &totalRows) {
+ options.clear();
+ totalRows = 0;
+ if (responseLine.empty())
+ return;
+
+ Common::String segment;
+ Common::Array<Common::String> explicitLines;
+ Common::String optionText;
+
+ auto flushOption = [&]() {
+ if (explicitLines.empty())
+ return;
+
+ DialogueResponseOptionLayout option;
+ option.text = optionText;
+ if (optionText.size() > 1 && optionText[0] >= '1' && optionText[0] <= '9' &&
+ optionText[1] == '.')
+ option.hotkey = optionText[0];
+ option.rowStart = (int)totalRows;
+ for (const Common::String &explicitLine : explicitLines) {
+ Common::Array<Common::String> wrappedExplicitLines;
+ DialogueRendering::wrapDialogueTextLikeNative(*_menuFont, _menuFontUsesCft,
+ explicitLine, kDialogueSubtitleTextWidth, wrappedExplicitLines);
+ if (wrappedExplicitLines.empty())
+ wrappedExplicitLines.push_back(explicitLine);
+ for (const Common::String &wrappedExplicitLine : wrappedExplicitLines)
+ option.wrappedLines.push_back(wrappedExplicitLine);
+ }
+ option.rowCount = MAX<int>(1, option.wrappedLines.size());
+ totalRows += option.rowCount;
+ options.push_back(Common::move(option));
+
+ explicitLines.clear();
+ optionText.clear();
+ };
+
+ auto consumeSegment = [&](const Common::String &rawSegment) {
+ Common::String segmentText = rawSegment;
+ if (segmentText.empty())
+ return;
+
+ const bool startsNewOption = segmentText.size() > 1 && segmentText[0] >= '1' &&
+ segmentText[0] <= '9' && segmentText[1] == '.';
+ if (startsNewOption) {
+ flushOption();
+ optionText = segmentText;
+ explicitLines.push_back(Common::move(segmentText));
+ return;
+ }
+
+ if (!optionText.empty())
+ optionText += ' ';
+ optionText += segmentText;
+ explicitLines.push_back(Common::move(segmentText));
+ };
+
+ for (uint i = 0; i < responseLine.size(); ++i) {
+ if (responseLine[i] == '/') {
+ consumeSegment(segment);
+ segment.clear();
+ continue;
+ }
+
+ segment += responseLine[i];
+ }
+
+ consumeSegment(segment);
+ flushOption();
+}
+
+int RoomNpcDialogueSession::getResponseMenuItemAt(const Common::Array<DialogueResponseOptionLayout> &options,
+ uint totalRows, const Common::Point &mousePos) {
+ if (mousePos.x < DialogueRendering::kDialogueTopicStartX || mousePos.x > DialogueRendering::kDialogueTopicEndX)
+ return -1;
+ if (mousePos.y < DialogueRendering::kDialogueKeywordTextY)
+ return -1;
+
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ const int row = (mousePos.y - DialogueRendering::kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
+ if (row < 0 || row >= (int)totalRows)
+ return -1;
+
+ for (uint i = 0; i < options.size(); ++i) {
+ const DialogueResponseOptionLayout &option = options[i];
+ if (row >= option.rowStart && row < option.rowStart + option.rowCount)
+ return (int)i;
+ }
+
+ return -1;
+}
+
+void RoomNpcDialogueSession::drawDialogueResponseMenu(const IndexedBitmap *textboxBitmap,
+ const Common::Array<DialogueResponseOptionLayout> &options, int hoveredOptionIndex) {
+ Graphics::Screen *activeScreen = getActiveScreen();
+ if (!activeScreen)
+ return;
+
+ setScaledPalette(*activeScreen, _palette, _paletteBrightness);
+ RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
+ if (_leftHeadVisible && _leftHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
+ if (_rightHeadVisible && _rightHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
+ if (textboxBitmap && textboxBitmap->isValid())
+ blitTransparentBitmap(*activeScreen, *textboxBitmap, kDialogueOverlayX, kDialogueOverlayY);
+
+ const Common::String &title = _menuTextConfig.dialogueResponsesLabel;
+ const Graphics::Font &titleFont = *_highlightFont;
+ const bool titleUsesCft = _highlightFontUsesCft;
+ const int titleWidth = titleFont.getStringWidth(title);
+ const int titleX = kDialogueSubtitleTextX + MAX<int>(0, (kDialogueSubtitleTextWidth - titleWidth) / 2);
+ drawFontString(titleFont, titleUsesCft, title, titleX, 11, titleWidth, kTextColorNormal);
+
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ int drawY = DialogueRendering::kDialogueKeywordTextY;
+ for (uint i = 0; i < options.size(); ++i) {
+ const bool highlighted = (int)i == hoveredOptionIndex;
+ const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
+ const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
+ const byte color = highlighted ? kTextColorNormal : kTextColorHover;
+
+ for (const Common::String &wrappedLine : options[i].wrappedLines) {
+ drawFontString(font, usesCft, wrappedLine, kDialogueSubtitleTextX, drawY,
+ kDialogueSubtitleTextWidth, color);
+ drawY += lineHeight;
+ }
+ }
+
+ if (_entityManager)
+ _entityManager->drawCursor(*activeScreen);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+}
+
+Common::Error RoomNpcDialogueSession::runResponseMenuTextInternal(const Common::String &responseLine, int responseLineIndex,
+ int &selectedIndex) {
+ selectedIndex = 0;
+
+ if (responseLine.empty()) {
+ if (responseLineIndex >= 0) {
+ debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) is empty",
+ responseLineIndex, responseLineIndex + 1);
+ } else {
+ debugC(1, kDebugDialogue, "Harvester: response menu text is empty");
+ }
+ return Common::kNoError;
+ }
+
+ Common::Array<DialogueResponseOptionLayout> options;
+ uint totalRows = 0;
+ parseResponseMenuLine(responseLine, options, totalRows);
+ if (options.empty()) {
+ if (responseLineIndex >= 0) {
+ debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) produced no options",
+ responseLineIndex, responseLineIndex + 1);
+ } else {
+ debugC(1, kDebugDialogue, "Harvester: response menu text produced no options");
+ }
+ return Common::kNoError;
+ }
+
+ Common::Array<Common::String> optionTexts;
+ for (const DialogueResponseOptionLayout &option : options) {
+ optionTexts.push_back(option.text);
+ debugC(3, kDebugDialogue, "Harvester: response menu option[%u] hotkey=%c rowStart=%d rowCount=%d",
+ optionTexts.size(), option.hotkey ? option.hotkey : '-', option.rowStart, option.rowCount);
+ }
+ logDialogueMenuItems("Response menu", responseLineIndex, responseLine, optionTexts);
+
+ const IndexedBitmap *textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueResponseTextboxIndex(totalRows));
+ Common::Error releaseError = waitForPointerRelease();
+ if (releaseError.getCode() != Common::kNoError)
+ return releaseError;
+
+ Graphics::FrameLimiter limiter(g_system, 60);
+ for (;;) {
+ const int hoveredOptionIndex = getResponseMenuItemAt(options, totalRows, _mousePos);
+ drawDialogueResponseMenu(textboxBitmap, options, hoveredOptionIndex);
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+
+ if (event.type == Common::EVENT_KEYDOWN) {
+ debugC(3, kDebugDialogue, "Harvester: response menu keycode=%d ascii=%u",
+ (int)event.kbd.keycode, event.kbd.ascii);
+ if (event.kbd.keycode >= Common::KEYCODE_1 && event.kbd.keycode <= Common::KEYCODE_9) {
+ const char hotkey = '1' + event.kbd.keycode - Common::KEYCODE_1;
+ for (int optionIndex = (int)options.size() - 1; optionIndex >= 0; --optionIndex) {
+ const DialogueResponseOptionLayout &option = options[(uint)optionIndex];
+ if (option.hotkey != hotkey)
+ continue;
+ selectedIndex = optionIndex + 1;
+ if (responseLineIndex >= 0) {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via hotkey=%c result=%d",
+ responseLineIndex, responseLineIndex + 1,
+ optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
+ } else {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu text selected option[%d]='%s' via hotkey=%c result=%d",
+ optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
+ }
+ return waitForPointerRelease();
+ }
+ }
+ } else if (event.type == Common::EVENT_LBUTTONDOWN) {
+ const int clickedOptionIndex = getResponseMenuItemAt(options, totalRows, event.mouse);
+ if (clickedOptionIndex < 0)
+ continue;
+ selectedIndex = clickedOptionIndex + 1;
+ if (responseLineIndex >= 0) {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via click x=%d y=%d result=%d",
+ responseLineIndex, responseLineIndex + 1,
+ selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
+ event.mouse.x, event.mouse.y, selectedIndex);
+ } else {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu text selected option[%d]='%s' via click x=%d y=%d result=%d",
+ selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
+ event.mouse.x, event.mouse.y, selectedIndex);
+ }
+ return waitForPointerRelease();
+ }
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/dialogue/dialogue_session.h b/engines/harvester/dialogue/dialogue_session.h
new file mode 100644
index 00000000000..7886a55f79f
--- /dev/null
+++ b/engines/harvester/dialogue/dialogue_session.h
@@ -0,0 +1,131 @@
+/* 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_DIALOGUE_SESSION_H
+#define HARVESTER_DIALOGUE_SESSION_H
+
+#include "common/ptr.h"
+#include "common/rect.h"
+#include "harvester/bitmap.h"
+#include "harvester/cft_font.h"
+#include "harvester/npc/dialogue_runtime.h"
+
+namespace Graphics {
+class Screen;
+}
+
+namespace Harvester {
+
+class Art;
+class EntityManager;
+struct MenuTextConfig;
+
+class RoomNpcDialogueSession : public DialogueRuntimeCallbacks {
+public:
+ struct DialogueResponseOptionLayout {
+ Common::String text;
+ Common::Array<Common::String> wrappedLines;
+ char hotkey = 0;
+ int rowStart = 0;
+ int rowCount = 0;
+ };
+
+ RoomNpcDialogueSession(HarvesterEngine &engine, Common::Point &mousePos, Flow &flow,
+ const IndexedBitmap &backdrop, const byte *palette, float paletteBrightness,
+ const NpcRecord &npc);
+ bool isValid() const;
+ const Common::String &getGenericByeTopic() const;
+ Common::Error playDialogueLineWithVariant(int wavId, const Common::String &speakerId,
+ int headVariant) override;
+ Common::Error playDialogueLine(int wavId, const Common::String &speakerId) override;
+ Common::Error playDialogueEntrySequence(const DialogueLineEntry *lines, uint count) override;
+ Common::Error playDialogueFst(const Common::String &path) override;
+ Common::Error clearScreenToBlack() override;
+ Common::Error showCdChangePrompt(int discNumber) override;
+ Common::Error runKeywordMenu(const Common::String &topicBuffer, int topicBufferLineIndex,
+ Common::String &selectedTopic,
+ DialogueKeywordMenuSelectionState &selection) override;
+ Common::Error runResponseMenu(int responseLineIndex, int &selectedIndex) override;
+ Common::Error runResponseMenuText(const Common::String &responseLine, int &selectedIndex) override;
+ Common::Error runGameOverScreen() override;
+ void assignTopicBuffer(Common::String &topicBuffer, int &topicBufferLineIndex,
+ int responseLineIndex, const char *) override;
+ bool matchesResponseLine(const Common::String &selectedTopic, int responseLineIndex) override;
+ bool matchesAnyResponseLine(const Common::String &selectedTopic,
+ const int *responseLineIndices, uint responseLineCount) override;
+ void queueDialogueInteractionIfNeeded(const InteractionResult &interaction) override;
+ void applyImmediateDialogueInteractionEffects(InteractionResult &interaction) override;
+ int getRandomNumber(int maxValue) override;
+ void setActiveSpeakerPortrait(const Common::String &speakerId, int headVariant) override;
+
+private:
+ Graphics::Screen *getActiveScreen() const;
+ Common::Error waitForPointerRelease();
+ void drawFontString(const Graphics::Font &font, bool usesCft, const Common::String &text,
+ int x, int y, int width, byte color);
+ void ensureSpeakerPortrait(const Common::String &speakerId, int headVariant);
+ void drawDialogueOverlay(const IndexedBitmap *overlayBitmap,
+ const Common::Array<Common::String> *subtitleLines, const Common::Array<Common::String> *topics,
+ int hoveredTopicIndex, bool hoverOther, const Common::String *textEntryValue,
+ bool textEntryCursorVisible = false);
+ void parseResponseMenuLine(const Common::String &responseLine,
+ Common::Array<DialogueResponseOptionLayout> &options, uint &totalRows);
+ int getResponseMenuItemAt(const Common::Array<DialogueResponseOptionLayout> &options,
+ uint totalRows, const Common::Point &mousePos);
+ void drawDialogueResponseMenu(const IndexedBitmap *textboxBitmap,
+ const Common::Array<DialogueResponseOptionLayout> &options, int hoveredOptionIndex);
+ Common::Error runResponseMenuTextInternal(const Common::String &responseLine, int responseLineIndex,
+ int &selectedIndex);
+
+ HarvesterEngine &_engine;
+ Common::Point &_mousePos;
+ Flow &_flow;
+ const MenuTextConfig &_menuTextConfig;
+ const IndexedBitmap &_backdrop;
+ const byte *_palette;
+ const float _paletteBrightness;
+ const NpcRecord &_npc;
+ Script *_script;
+ Text *_text;
+ Art *_art;
+ EntityManager *_entityManager;
+ const Graphics::Font *_fallbackFont;
+ Common::ScopedPtr<HarvesterCftFont> _subtitleCftFont;
+ Common::ScopedPtr<HarvesterCftFont> _menuCftFont;
+ const Graphics::Font *_subtitleFont;
+ const Graphics::Font *_menuFont;
+ const Graphics::Font *_highlightFont;
+ bool _subtitleFontUsesCft;
+ bool _menuFontUsesCft;
+ bool _highlightFontUsesCft;
+ IndexedBitmap _keywordBitmap;
+ Common::String _genericByeTopic;
+ IndexedBitmap _leftHeadBitmap;
+ IndexedBitmap _rightHeadBitmap;
+ Common::String _leftHeadSpeakerId;
+ Common::String _rightHeadSpeakerId;
+ bool _leftHeadVisible;
+ bool _rightHeadVisible;
+};
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_DIALOGUE_SESSION_H
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 3c28b6bae8b..8d2438f8499 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -8,6 +8,7 @@ MODULE_OBJS = \
console.o \
dialogue/dialogue.o \
dialogue/dialogue_render.o \
+ dialogue/dialogue_session.o \
flow/flow.o \
flow/flow_debug.o \
flow/flow_map.o \
Commit: 4c6a23d829f57ed703275848b079b140649f8164
https://github.com/scummvm/scummvm/commit/4c6a23d829f57ed703275848b079b140649f8164
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:52-04:00
Commit Message:
HARVESTER: Separate dialogue speech and modal playback
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/dialogue/dialogue_playback.cpp
engines/harvester/dialogue/dialogue_render.h
engines/harvester/dialogue/dialogue_session.cpp
engines/harvester/dialogue/dialogue_session.h
engines/harvester/module.mk
diff --git a/engines/harvester/dialogue/dialogue_playback.cpp b/engines/harvester/dialogue/dialogue_playback.cpp
new file mode 100644
index 00000000000..5c69c8d3db5
--- /dev/null
+++ b/engines/harvester/dialogue/dialogue_playback.cpp
@@ -0,0 +1,358 @@
+/* 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/dialogue/dialogue_session.h"
+#include "harvester/dialogue/dialogue_render.h"
+
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "graphics/framelimiter.h"
+#include "harvester/art.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/flow/flow.h"
+#include "harvester/flow/flow_support.h"
+#include "harvester/fst_player.h"
+#include "harvester/harvester.h"
+#include "harvester/resources.h"
+#include "harvester/scene/room_presentation.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+namespace {
+
+static const char *const kCdChangePromptPalettePath = "1:/GRAPHIC/PAL/CD1.PAL";
+
+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 bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
+ Common::Array<byte> data;
+ if (!resources.loadFile(path, data) || data.size() < 256 * 3)
+ return false;
+
+ memcpy(dest, data.data(), 256 * 3);
+ return true;
+}
+
+static bool isPrimaryMouseDown() {
+ return g_system && g_system->getEventManager() &&
+ ((g_system->getEventManager()->getButtonState() & 1) != 0);
+}
+
+static bool isSecondaryMouseDown() {
+ return g_system && g_system->getEventManager() &&
+ ((g_system->getEventManager()->getButtonState() & 2) != 0);
+}
+
+static Common::String buildDialogueVoicePath(const Script &script, int wavId) {
+ if (wavId <= 0)
+ return Common::String();
+
+ return Common::String::format("%s%d.CMP", script.getVoicePath().c_str(), wavId);
+}
+
+} // End of anonymous namespace
+
+Common::Error RoomNpcDialogueSession::playDialogueLineWithVariant(int wavId, const Common::String &speakerId,
+ int headVariant) {
+ setActiveSpeakerPortrait(speakerId, headVariant);
+
+ const StartupDialogueTextMode textMode = _script->getDialogueTextMode();
+ Common::String subtitleText;
+ const bool textEnabled = textMode != kStartupDialogueTextNone &&
+ _text->resolveDialogueSubtitle(wavId, subtitleText);
+ Common::Array<Common::String> subtitleLines;
+ const IndexedBitmap *textboxBitmap = nullptr;
+ if (textEnabled) {
+ DialogueRendering::wrapDialogueTextLikeNative(*_subtitleFont, _subtitleFontUsesCft,
+ subtitleText, DialogueRendering::kDialogueSubtitleTextWidth, subtitleLines);
+ textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueTextboxIndex(subtitleLines.size()));
+ }
+
+ 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' 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) {
+ _engine.stopSpeech();
+ return releaseError;
+ }
+
+ bool voiceInterrupted = false;
+ bool pointerInterrupted = false;
+ Graphics::FrameLimiter limiter(g_system, 60);
+ for (;;) {
+ drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr, nullptr, -1, false, nullptr);
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result)) {
+ _engine.stopSpeech();
+ return result;
+ }
+
+ if (isDialogueVoiceInterruptEvent(event)) {
+ voiceInterrupted = true;
+ if (isDialoguePointerPressEvent(event))
+ pointerInterrupted = true;
+ }
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ if (voiceInterrupted || (!voiceStarted || !_engine.isSpeechPlaying()))
+ break;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ _engine.stopSpeech();
+ if (pointerInterrupted) {
+ Common::Error releaseResult = waitForPointerRelease();
+ if (releaseResult.getCode() != Common::kNoError)
+ return releaseResult;
+ } else if (textMode == kStartupDialogueTextClick) {
+ return waitForDialogueContinue(textboxBitmap, textEnabled ? &subtitleLines : nullptr);
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::waitForDialogueContinue(const IndexedBitmap *textboxBitmap,
+ const Common::Array<Common::String> *subtitleLines) {
+ Graphics::FrameLimiter clickLimiter(g_system, 60);
+ for (;;) {
+ drawDialogueOverlay(textboxBitmap, subtitleLines,
+ nullptr, -1, false, nullptr);
+
+ bool continuePressed = false;
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+
+ if (isDialogueContinueEvent(event))
+ continuePressed = true;
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ if (continuePressed)
+ break;
+
+ clickLimiter.delayBeforeSwap();
+ clickLimiter.startFrame();
+ }
+
+ Common::Error releaseResult = waitForPointerRelease();
+ if (releaseResult.getCode() != Common::kNoError)
+ return releaseResult;
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::playDialogueLine(int wavId, const Common::String &speakerId) {
+ return playDialogueLineWithVariant(wavId, speakerId, 0);
+}
+
+Common::Error RoomNpcDialogueSession::playDialogueEntrySequence(const DialogueLineEntry *lines, uint count) {
+ for (uint i = 0; i < count; ++i) {
+ Common::Error lineError = playDialogueLineWithVariant(
+ lines[i].wavId, lines[i].speakerId, lines[i].headVariant);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::playDialogueFst(const Common::String &path) {
+ FstPlayer fstPlayer(_engine);
+ if (!fstPlayer.play(path))
+ return Common::kReadingFailed;
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::clearScreenToBlack() {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen)
+ return Common::kReadingFailed;
+
+ byte blackPalette[256 * 3];
+ memset(blackPalette, 0, sizeof(blackPalette));
+ screen->fillRect(screen->getBounds(), 0);
+ setScaledPalette(*screen, blackPalette, 1.0f);
+ screen->makeAllDirty();
+ screen->update();
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::showCdChangePrompt(int discNumber) {
+ if (discNumber <= 0)
+ return Common::kNoError;
+
+ ResourceManager *resources = _engine.getResources();
+ if (!resources)
+ return Common::kReadingFailed;
+ const int previousDisc = resources->getCurrentDisc();
+
+ if (_engine.shouldShowCdChangePrompts()) {
+ Graphics::Screen *screen = _engine.getScreen();
+ if (!screen)
+ return Common::kReadingFailed;
+
+ const Common::String bitmapPath =
+ Common::String::format("1:/GRAPHIC/OTHER/CD%d.BM", discNumber);
+ IndexedBitmap promptBitmap;
+ byte promptPalette[256 * 3];
+ if (!loadBitmapResource(*resources, bitmapPath, promptBitmap) ||
+ !loadPaletteResource(*resources, kCdChangePromptPalettePath, promptPalette)) {
+ warning("Harvester: unable to load CD change prompt assets for disc %d", discNumber);
+ return Common::kReadingFailed;
+ }
+
+ _engine.stopMusic();
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+ if (event.type == Common::EVENT_MOUSEMOVE)
+ _mousePos = event.mouse;
+ }
+
+ bool waitingForRelease = false;
+ bool needsRedraw = true;
+ bool activateRequestedDisc = false;
+ Graphics::FrameLimiter limiter(g_system, 60);
+
+ while (!_engine.shouldQuit() && !activateRequestedDisc) {
+ if (needsRedraw) {
+ screen->fillRect(screen->getBounds(), 0);
+ RoomPresentation::blitBitmap(*screen, promptBitmap, 0, 0);
+ setScaledPalette(*screen, promptPalette, 1.0f);
+ if (_entityManager)
+ _entityManager->drawCursor(*screen);
+ screen->makeAllDirty();
+ screen->update();
+ needsRedraw = false;
+ }
+
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+
+ switch (event.type) {
+ case Common::EVENT_MOUSEMOVE:
+ _mousePos = event.mouse;
+ needsRedraw = true;
+ break;
+ case Common::EVENT_LBUTTONDOWN:
+ waitingForRelease = true;
+ break;
+ case Common::EVENT_LBUTTONUP:
+ if (waitingForRelease)
+ activateRequestedDisc = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (_entityManager && _entityManager->syncCursorEntityPosition(_mousePos))
+ needsRedraw = true;
+
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+ }
+
+ return activateDialogueDisc(*resources, discNumber, previousDisc);
+}
+
+Common::Error RoomNpcDialogueSession::activateDialogueDisc(ResourceManager &resources,
+ int discNumber, int previousDisc) {
+ if (!_engine.activateDisc(discNumber)) {
+ warning("Harvester: unable to activate disc %d resources", discNumber);
+ return Common::kReadingFailed;
+ }
+ if ((discNumber == 1 || discNumber == 3) &&
+ previousDisc > 0 && previousDisc != resources.getCurrentDisc()) {
+ Script *script = _engine.getScript();
+ if (!script || !script->reloadTownWorld(resources)) {
+ warning("Harvester: unable to reload town script after disc prompt %d -> %d",
+ previousDisc, discNumber);
+ return Common::kReadingFailed;
+ }
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::runGameOverScreen() {
+ debugC(1, kDebugDialogue, "Harvester: dialogue death requesting game-over menu");
+ _flow.requestGameOverReturn();
+ return Common::kNoError;
+}
+
+Common::Error RoomNpcDialogueSession::waitForPointerRelease() {
+ Graphics::FrameLimiter limiter(g_system, 60);
+ while (!_engine.shouldQuit() && (isPrimaryMouseDown() || isSecondaryMouseDown())) {
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/dialogue/dialogue_render.h b/engines/harvester/dialogue/dialogue_render.h
index 44bec954daa..99989403609 100644
--- a/engines/harvester/dialogue/dialogue_render.h
+++ b/engines/harvester/dialogue/dialogue_render.h
@@ -34,6 +34,7 @@ class Screen;
namespace Harvester {
namespace DialogueRendering {
+static const int kDialogueSubtitleTextWidth = 316;
static const int kDialogueKeywordTextY = 34;
static const int kDialogueTopicStartX = 168;
static const int kDialogueTopicEndX = 482;
diff --git a/engines/harvester/dialogue/dialogue_session.cpp b/engines/harvester/dialogue/dialogue_session.cpp
index 551c0bfbf6a..74ffa79964d 100644
--- a/engines/harvester/dialogue/dialogue_session.cpp
+++ b/engines/harvester/dialogue/dialogue_session.cpp
@@ -33,7 +33,6 @@
#include "harvester/entity.h"
#include "harvester/flow/flow.h"
#include "harvester/flow/flow_support.h"
-#include "harvester/fst_player.h"
#include "harvester/harvester.h"
#include "harvester/resources.h"
#include "harvester/scene/room_presentation.h"
@@ -50,7 +49,6 @@ static const int kDialogueRightHeadX = 474;
static const int kDialogueHeadY = 6;
static const int kDialogueSubtitleTextX = 178;
static const int kDialogueSubtitleTextY = 12;
-static const int kDialogueSubtitleTextWidth = 316;
static const int kDialogueKeywordTextX = 178;
static const int kDialogueKeywordTextWidth = 300;
static const int kDialogueOtherX = 171;
@@ -63,46 +61,12 @@ static const uint32 kDialogueTextEntryBlinkTicks = 0x30;
static const int kDialogueOtherWidth = 120;
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 byte kTextColorNormal = 255;
static const byte kTextColorHover = 251;
-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 bool loadPaletteResource(ResourceManager &resources, const Common::String &path, byte *dest) {
- Common::Array<byte> data;
- if (!resources.loadFile(path, data) || data.size() < 256 * 3)
- return false;
-
- memcpy(dest, data.data(), 256 * 3);
- return true;
-}
-
-static bool isPrimaryMouseDown() {
- return g_system && g_system->getEventManager() &&
- ((g_system->getEventManager()->getButtonState() & 1) != 0);
-}
-
-static bool isSecondaryMouseDown() {
- return g_system && g_system->getEventManager() &&
- ((g_system->getEventManager()->getButtonState() & 2) != 0);
-}
-
static void splitDialogueMenuLine(const Common::String &line, Common::Array<Common::String> &parts) {
parts.clear();
if (line.empty())
@@ -159,13 +123,6 @@ static bool loadDialogueHeadBitmap(HarvesterEngine &engine, const Common::String
return loadBitmapResource(*resources, head->portraitPath + ".BM", bitmap);
}
-static Common::String buildDialogueVoicePath(const Script &script, int wavId) {
- if (wavId <= 0)
- return Common::String();
-
- return Common::String::format("%s%d.CMP", script.getVoicePath().c_str(), wavId);
-}
-
} // End of anonymous namespace
RoomNpcDialogueSession::RoomNpcDialogueSession(HarvesterEngine &engine, Common::Point &mousePos, Flow &flow,
@@ -240,239 +197,6 @@ const Common::String &RoomNpcDialogueSession::getGenericByeTopic() const {
return _genericByeTopic;
}
-Common::Error RoomNpcDialogueSession::playDialogueLineWithVariant(int wavId, const Common::String &speakerId,
- int headVariant) {
- setActiveSpeakerPortrait(speakerId, headVariant);
-
- const StartupDialogueTextMode textMode = _script->getDialogueTextMode();
- Common::String subtitleText;
- const bool textEnabled = textMode != kStartupDialogueTextNone &&
- _text->resolveDialogueSubtitle(wavId, subtitleText);
- Common::Array<Common::String> subtitleLines;
- const IndexedBitmap *textboxBitmap = nullptr;
- if (textEnabled) {
- DialogueRendering::wrapDialogueTextLikeNative(*_subtitleFont, _subtitleFontUsesCft,
- subtitleText, kDialogueSubtitleTextWidth, subtitleLines);
- textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueTextboxIndex(subtitleLines.size()));
- }
-
- 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' 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) {
- _engine.stopSpeech();
- return releaseError;
- }
-
- bool voiceInterrupted = false;
- bool pointerInterrupted = false;
- Graphics::FrameLimiter limiter(g_system, 60);
- for (;;) {
- drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr, nullptr, -1, false, nullptr);
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result)) {
- _engine.stopSpeech();
- return result;
- }
-
- if (isDialogueVoiceInterruptEvent(event)) {
- voiceInterrupted = true;
- if (isDialoguePointerPressEvent(event))
- pointerInterrupted = true;
- }
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- if (voiceInterrupted || (!voiceStarted || !_engine.isSpeechPlaying()))
- break;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- _engine.stopSpeech();
- if (pointerInterrupted) {
- Common::Error releaseResult = waitForPointerRelease();
- if (releaseResult.getCode() != Common::kNoError)
- return releaseResult;
- } else if (textMode == kStartupDialogueTextClick) {
- Graphics::FrameLimiter clickLimiter(g_system, 60);
- for (;;) {
- drawDialogueOverlay(textboxBitmap, textEnabled ? &subtitleLines : nullptr,
- nullptr, -1, false, nullptr);
-
- bool continuePressed = false;
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
-
- if (isDialogueContinueEvent(event))
- continuePressed = true;
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- if (continuePressed)
- break;
-
- clickLimiter.delayBeforeSwap();
- clickLimiter.startFrame();
- }
-
- Common::Error releaseResult = waitForPointerRelease();
- if (releaseResult.getCode() != Common::kNoError)
- return releaseResult;
- }
-
- return Common::kNoError;
-}
-
-Common::Error RoomNpcDialogueSession::playDialogueLine(int wavId, const Common::String &speakerId) {
- return playDialogueLineWithVariant(wavId, speakerId, 0);
-}
-
-Common::Error RoomNpcDialogueSession::playDialogueEntrySequence(const DialogueLineEntry *lines, uint count) {
- for (uint i = 0; i < count; ++i) {
- Common::Error lineError = playDialogueLineWithVariant(
- lines[i].wavId, lines[i].speakerId, lines[i].headVariant);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
-
- return Common::kNoError;
-}
-
-Common::Error RoomNpcDialogueSession::playDialogueFst(const Common::String &path) {
- FstPlayer fstPlayer(_engine);
- if (!fstPlayer.play(path))
- return Common::kReadingFailed;
-
- return Common::kNoError;
-}
-
-Common::Error RoomNpcDialogueSession::clearScreenToBlack() {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen)
- return Common::kReadingFailed;
-
- byte blackPalette[256 * 3];
- memset(blackPalette, 0, sizeof(blackPalette));
- screen->fillRect(screen->getBounds(), 0);
- setScaledPalette(*screen, blackPalette, 1.0f);
- screen->makeAllDirty();
- screen->update();
- return Common::kNoError;
-}
-
-Common::Error RoomNpcDialogueSession::showCdChangePrompt(int discNumber) {
- if (discNumber <= 0)
- return Common::kNoError;
-
- ResourceManager *resources = _engine.getResources();
- if (!resources)
- return Common::kReadingFailed;
- const int previousDisc = resources->getCurrentDisc();
-
- if (_engine.shouldShowCdChangePrompts()) {
- Graphics::Screen *screen = _engine.getScreen();
- if (!screen)
- return Common::kReadingFailed;
-
- const Common::String bitmapPath =
- Common::String::format("1:/GRAPHIC/OTHER/CD%d.BM", discNumber);
- IndexedBitmap promptBitmap;
- byte promptPalette[256 * 3];
- if (!loadBitmapResource(*resources, bitmapPath, promptBitmap) ||
- !loadPaletteResource(*resources, kCdChangePromptPalettePath, promptPalette)) {
- warning("Harvester: unable to load CD change prompt assets for disc %d", discNumber);
- return Common::kReadingFailed;
- }
-
- _engine.stopMusic();
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
- if (event.type == Common::EVENT_MOUSEMOVE)
- _mousePos = event.mouse;
- }
-
- bool waitingForRelease = false;
- bool needsRedraw = true;
- bool activateRequestedDisc = false;
- Graphics::FrameLimiter limiter(g_system, 60);
-
- while (!_engine.shouldQuit() && !activateRequestedDisc) {
- if (needsRedraw) {
- screen->fillRect(screen->getBounds(), 0);
- RoomPresentation::blitBitmap(*screen, promptBitmap, 0, 0);
- setScaledPalette(*screen, promptPalette, 1.0f);
- if (_entityManager)
- _entityManager->drawCursor(*screen);
- screen->makeAllDirty();
- screen->update();
- needsRedraw = false;
- }
-
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
-
- switch (event.type) {
- case Common::EVENT_MOUSEMOVE:
- _mousePos = event.mouse;
- needsRedraw = true;
- break;
- case Common::EVENT_LBUTTONDOWN:
- waitingForRelease = true;
- break;
- case Common::EVENT_LBUTTONUP:
- if (waitingForRelease)
- activateRequestedDisc = true;
- break;
- default:
- break;
- }
- }
-
- if (_entityManager && _entityManager->syncCursorEntityPosition(_mousePos))
- needsRedraw = true;
-
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
- }
-
- if (!_engine.activateDisc(discNumber)) {
- warning("Harvester: unable to activate disc %d resources", discNumber);
- return Common::kReadingFailed;
- }
- if ((discNumber == 1 || discNumber == 3) &&
- previousDisc > 0 && previousDisc != resources->getCurrentDisc()) {
- Script *script = _engine.getScript();
- if (!script || !script->reloadTownWorld(*resources)) {
- warning("Harvester: unable to reload town script after disc prompt %d -> %d",
- previousDisc, discNumber);
- return Common::kReadingFailed;
- }
- }
-
- return Common::kNoError;
-}
-
Common::Error RoomNpcDialogueSession::runKeywordMenu(const Common::String &topicBuffer, int topicBufferLineIndex,
Common::String &selectedTopic,
DialogueKeywordMenuSelectionState &selection) {
@@ -608,14 +332,6 @@ Common::Error RoomNpcDialogueSession::runResponseMenuText(const Common::String &
return runResponseMenuTextInternal(responseLine, -1, selectedIndex);
}
-Common::Error RoomNpcDialogueSession::runGameOverScreen() {
- // GOG run_game_over_screen (0x7c540) enters run_main_menu (0x67390)
- // with GAMEOVER.BM and GAMEOVER.PAL retained as the menu backdrop.
- debugC(1, kDebugDialogue, "Harvester: dialogue death requesting game-over menu");
- _flow.requestGameOverReturn();
- return Common::kNoError;
-}
-
void RoomNpcDialogueSession::assignTopicBuffer(Common::String &topicBuffer, int &topicBufferLineIndex,
int responseLineIndex, const char *) {
topicBuffer = _text->getDialogueResponseLine(responseLineIndex);
@@ -688,25 +404,6 @@ Graphics::Screen *RoomNpcDialogueSession::getActiveScreen() const {
return _engine.getScreen();
}
-Common::Error RoomNpcDialogueSession::waitForPointerRelease() {
- Graphics::FrameLimiter limiter(g_system, 60);
- while (!_engine.shouldQuit() && (isPrimaryMouseDown() || isSecondaryMouseDown())) {
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-
- return Common::kNoError;
-}
-
void RoomNpcDialogueSession::drawFontString(const Graphics::Font &font, bool usesCft, const Common::String &text,
int x, int y, int width, byte color) {
Graphics::Screen *activeScreen = getActiveScreen();
@@ -758,14 +455,14 @@ void RoomNpcDialogueSession::drawDialogueOverlay(const IndexedBitmap *overlayBit
if (subtitleLines) {
if (_subtitleFontUsesCft) {
DialogueRendering::drawDialogueTextLines(*activeScreen, *_subtitleFont, *subtitleLines,
- kDialogueSubtitleTextX, kDialogueSubtitleTextY, kDialogueSubtitleTextWidth);
+ kDialogueSubtitleTextX, kDialogueSubtitleTextY, DialogueRendering::kDialogueSubtitleTextWidth);
} else {
const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_subtitleFont);
for (uint i = 0; i < subtitleLines->size(); ++i) {
RoomPresentation::drawShadowedString(*activeScreen, *_subtitleFont, (*subtitleLines)[i],
kDialogueSubtitleTextX,
kDialogueSubtitleTextY + (int)i * lineHeight,
- kDialogueSubtitleTextWidth, kTextColorNormal);
+ DialogueRendering::kDialogueSubtitleTextWidth, kTextColorNormal);
}
}
}
@@ -835,7 +532,7 @@ void RoomNpcDialogueSession::parseResponseMenuLine(const Common::String &respons
for (const Common::String &explicitLine : explicitLines) {
Common::Array<Common::String> wrappedExplicitLines;
DialogueRendering::wrapDialogueTextLikeNative(*_menuFont, _menuFontUsesCft,
- explicitLine, kDialogueSubtitleTextWidth, wrappedExplicitLines);
+ explicitLine, DialogueRendering::kDialogueSubtitleTextWidth, wrappedExplicitLines);
if (wrappedExplicitLines.empty())
wrappedExplicitLines.push_back(explicitLine);
for (const Common::String &wrappedExplicitLine : wrappedExplicitLines)
@@ -923,7 +620,7 @@ void RoomNpcDialogueSession::drawDialogueResponseMenu(const IndexedBitmap *textb
const Graphics::Font &titleFont = *_highlightFont;
const bool titleUsesCft = _highlightFontUsesCft;
const int titleWidth = titleFont.getStringWidth(title);
- const int titleX = kDialogueSubtitleTextX + MAX<int>(0, (kDialogueSubtitleTextWidth - titleWidth) / 2);
+ const int titleX = kDialogueSubtitleTextX + MAX<int>(0, (DialogueRendering::kDialogueSubtitleTextWidth - titleWidth) / 2);
drawFontString(titleFont, titleUsesCft, title, titleX, 11, titleWidth, kTextColorNormal);
const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
@@ -936,7 +633,7 @@ void RoomNpcDialogueSession::drawDialogueResponseMenu(const IndexedBitmap *textb
for (const Common::String &wrappedLine : options[i].wrappedLines) {
drawFontString(font, usesCft, wrappedLine, kDialogueSubtitleTextX, drawY,
- kDialogueSubtitleTextWidth, color);
+ DialogueRendering::kDialogueSubtitleTextWidth, color);
drawY += lineHeight;
}
}
diff --git a/engines/harvester/dialogue/dialogue_session.h b/engines/harvester/dialogue/dialogue_session.h
index 7886a55f79f..9e76dca33fc 100644
--- a/engines/harvester/dialogue/dialogue_session.h
+++ b/engines/harvester/dialogue/dialogue_session.h
@@ -36,6 +36,7 @@ namespace Harvester {
class Art;
class EntityManager;
+class ResourceManager;
struct MenuTextConfig;
class RoomNpcDialogueSession : public DialogueRuntimeCallbacks {
@@ -79,6 +80,9 @@ public:
private:
Graphics::Screen *getActiveScreen() const;
Common::Error waitForPointerRelease();
+ Common::Error waitForDialogueContinue(const IndexedBitmap *textboxBitmap,
+ const Common::Array<Common::String> *subtitleLines);
+ Common::Error activateDialogueDisc(ResourceManager &resources, int discNumber, int previousDisc);
void drawFontString(const Graphics::Font &font, bool usesCft, const Common::String &text,
int x, int y, int width, byte color);
void ensureSpeakerPortrait(const Common::String &speakerId, int headVariant);
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 8d2438f8499..cd7c62e0d4a 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -7,6 +7,7 @@ MODULE_OBJS = \
cheats.o \
console.o \
dialogue/dialogue.o \
+ dialogue/dialogue_playback.o \
dialogue/dialogue_render.o \
dialogue/dialogue_session.o \
flow/flow.o \
Commit: 41294db54f5a2a09e59cc84dcc0a9da4cf7504d9
https://github.com/scummvm/scummvm/commit/41294db54f5a2a09e59cc84dcc0a9da4cf7504d9
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:52-04:00
Commit Message:
HARVESTER: Separate dialogue menus and text entry
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/dialogue/dialogue_menu.cpp
engines/harvester/dialogue/dialogue_render.cpp
engines/harvester/dialogue/dialogue_render.h
engines/harvester/dialogue/dialogue_session.cpp
engines/harvester/dialogue/dialogue_session.h
engines/harvester/module.mk
diff --git a/engines/harvester/dialogue/dialogue_menu.cpp b/engines/harvester/dialogue/dialogue_menu.cpp
new file mode 100644
index 00000000000..8018bae86c1
--- /dev/null
+++ b/engines/harvester/dialogue/dialogue_menu.cpp
@@ -0,0 +1,451 @@
+/* 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/dialogue/dialogue_session.h"
+#include "harvester/dialogue/dialogue_render.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "graphics/framelimiter.h"
+#include "harvester/art.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/flow/flow.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+namespace {
+
+static const uint kDialogueTextEntryMaxLength = 26;
+static const uint32 kDialogueTextEntryBlinkTicks = 0x30;
+
+static void splitDialogueMenuLine(const Common::String &line, Common::Array<Common::String> &parts) {
+ parts.clear();
+ if (line.empty())
+ return;
+
+ Common::String token;
+ for (uint i = 0; i < line.size(); ++i) {
+ if (line[i] == '/') {
+ parts.push_back(Common::move(token));
+ token.clear();
+ continue;
+ }
+
+ token += line[i];
+ }
+
+ parts.push_back(Common::move(token));
+}
+
+static void logDialogueMenuItems(const char *label, int sourceLineIndex,
+ const Common::String &rawLine, const Common::Array<Common::String> &items) {
+ if (sourceLineIndex >= 0) {
+ debugC(3, kDebugDialogue, "Harvester: %s source line 0x%x (%d) raw='%s'",
+ label, sourceLineIndex, sourceLineIndex + 1, rawLine.c_str());
+ } else {
+ debugC(3, kDebugDialogue, "Harvester: %s source raw='%s'", label, rawLine.c_str());
+ }
+
+ if (items.empty()) {
+ debugC(1, kDebugDialogue, "Harvester: %s has no explicit items", label);
+ return;
+ }
+
+ for (uint i = 0; i < items.size(); ++i)
+ debugC(3, kDebugDialogue, "Harvester: %s item[%u]='%s'", label, i + 1, items[i].c_str());
+}
+
+} // End of anonymous namespace
+
+Common::Error RoomNpcDialogueSession::runKeywordMenu(const Common::String &topicBuffer, int topicBufferLineIndex,
+ Common::String &selectedTopic,
+ DialogueKeywordMenuSelectionState &selection) {
+ selectedTopic.clear();
+ selection = DialogueKeywordMenuSelectionState();
+ Common::Array<Common::String> topics;
+ splitDialogueMenuLine(topicBuffer, topics);
+ logDialogueMenuItems("Keyword menu", topicBufferLineIndex, topicBuffer, topics);
+ topics.push_back(_genericByeTopic);
+ debugC(1, kDebugDialogue, "Harvester: keyword menu default topic='%s'", _genericByeTopic.c_str());
+ bool editingOther = false;
+ bool textEntryCursorVisible = false;
+ uint32 textEntryBlinkTimestamp = 0;
+ Common::String typedTopic;
+ Common::Error releaseError = waitForPointerRelease();
+ if (releaseError.getCode() != Common::kNoError)
+ return releaseError;
+
+ Graphics::FrameLimiter limiter(g_system, 60);
+ for (;;) {
+ const uint32 now = getRuntimeClockTicks();
+ if (editingOther && now - textEntryBlinkTimestamp > kDialogueTextEntryBlinkTicks) {
+ textEntryCursorVisible = !textEntryCursorVisible;
+ textEntryBlinkTimestamp = now;
+ }
+ const int hoveredTopicIndex = editingOther ? -1 : DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
+ const bool hoverOther = editingOther || DialogueRendering::isDialogueOtherHit(_mousePos);
+ drawDialogueOverlay(&_keywordBitmap, nullptr, &topics, hoveredTopicIndex, hoverOther,
+ editingOther ? &typedTopic : nullptr, textEntryCursorVisible);
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result)) {
+ if (editingOther && _entityManager)
+ _entityManager->showCursor();
+ return result;
+ }
+
+ if (editingOther) {
+ if (handleKeywordTextEntry(event, typedTopic, editingOther, selectedTopic, selection))
+ return Common::kNoError;
+ continue;
+ }
+
+ switch (event.type) {
+ case Common::EVENT_KEYDOWN:
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ selectedTopic = _genericByeTopic;
+ selection.fromEscape = true;
+ selection.fromGenericBye = true;
+ debugC(1, kDebugDialogue,
+ "Harvester: keyword menu selected default topic via escape '%s'",
+ selectedTopic.c_str());
+ return Common::kNoError;
+ }
+ break;
+ case Common::EVENT_LBUTTONDOWN: {
+ const int clickedTopic = DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
+ if (clickedTopic >= 0) {
+ selectedTopic = topics[(uint)clickedTopic];
+ selection.fromGenericBye = clickedTopic == (int)topics.size() - 1;
+ debugC(2, kDebugDialogue, "Harvester: keyword menu selected topic[%d]='%s' via click",
+ clickedTopic + 1, selectedTopic.c_str());
+ return Common::kNoError;
+ }
+ if (DialogueRendering::isDialogueOtherHit(_mousePos)) {
+ typedTopic.clear();
+ editingOther = true;
+ textEntryCursorVisible = false;
+ textEntryBlinkTimestamp = getRuntimeClockTicks();
+ debugC(1, kDebugDialogue, "Harvester: keyword menu entered Other input");
+ if (_entityManager)
+ _entityManager->hideCursor();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+}
+
+bool RoomNpcDialogueSession::handleKeywordTextEntry(const Common::Event &event,
+ Common::String &typedTopic, bool &editingOther, Common::String &selectedTopic,
+ DialogueKeywordMenuSelectionState &selection) {
+ bool acceptInput = false;
+ bool cancelInput = false;
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
+ cancelInput = true;
+ } else if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
+ event.kbd.keycode == Common::KEYCODE_LEFT) {
+ deleteTextEntryCharacter(typedTopic);
+ } else if (event.kbd.keycode == Common::KEYCODE_HOME) {
+ typedTopic.clear();
+ } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
+ event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
+ acceptInput = true;
+ } else if (event.kbd.ascii >= 0x1f && event.kbd.ascii <= 0x80 &&
+ event.kbd.ascii != '~' &&
+ (!_subtitleCftFont || _subtitleCftFont->hasGlyph(event.kbd.ascii))) {
+ appendTextEntryCharacter(typedTopic, *_highlightFont, event.kbd.ascii,
+ kDialogueTextEntryMaxLength, DialogueRendering::kDialogueTextEntryMaxWidth);
+ }
+ } else if (event.type == Common::EVENT_LBUTTONDOWN) {
+ acceptInput = Common::Rect(3, 460, 318, 477).contains(event.mouse);
+ cancelInput = Common::Rect(321, 460, 636, 477).contains(event.mouse);
+ if (cancelInput)
+ g_system->delayMillis(400);
+ }
+ if (acceptInput || cancelInput) {
+ editingOther = false;
+ if (_entityManager)
+ _entityManager->showCursor();
+ if (acceptInput && !typedTopic.empty()) {
+ selectedTopic = typedTopic;
+ selection.fromTypedInput = true;
+ debugC(1, kDebugDialogue, "Harvester: keyword menu typed topic='%s'",
+ selectedTopic.c_str());
+ return true;
+ }
+ typedTopic.clear();
+ debugC(1, kDebugDialogue, "Harvester: keyword menu cancelled Other input");
+ }
+
+ return false;
+}
+
+Common::Error RoomNpcDialogueSession::runResponseMenu(int responseLineIndex, int &selectedIndex) {
+ const Common::String responseLine = _text->getDialogueResponseLine(responseLineIndex);
+ return runResponseMenuTextInternal(responseLine, responseLineIndex, selectedIndex);
+}
+
+Common::Error RoomNpcDialogueSession::runResponseMenuText(const Common::String &responseLine, int &selectedIndex) {
+ return runResponseMenuTextInternal(responseLine, -1, selectedIndex);
+}
+
+void RoomNpcDialogueSession::assignTopicBuffer(Common::String &topicBuffer, int &topicBufferLineIndex,
+ int responseLineIndex, const char *) {
+ topicBuffer = _text->getDialogueResponseLine(responseLineIndex);
+ topicBufferLineIndex = responseLineIndex;
+}
+
+bool RoomNpcDialogueSession::matchesResponseLine(const Common::String &selectedTopic, int responseLineIndex) {
+ const Common::String topicText = _text->getDialogueResponseLine(responseLineIndex);
+ return !topicText.empty() && selectedTopic.equalsIgnoreCase(topicText);
+}
+
+bool RoomNpcDialogueSession::matchesAnyResponseLine(const Common::String &selectedTopic,
+ const int *responseLineIndices, uint responseLineCount) {
+ for (uint i = 0; i < responseLineCount; ++i) {
+ if (matchesResponseLine(selectedTopic, responseLineIndices[i]))
+ return true;
+ }
+
+ return false;
+}
+
+void RoomNpcDialogueSession::parseResponseMenuLine(const Common::String &responseLine,
+ Common::Array<DialogueResponseOptionLayout> &options, uint &totalRows) {
+ options.clear();
+ totalRows = 0;
+ if (responseLine.empty())
+ return;
+
+ Common::String segment;
+ Common::Array<Common::String> explicitLines;
+ Common::String optionText;
+
+ auto flushOption = [&]() {
+ if (explicitLines.empty())
+ return;
+
+ DialogueResponseOptionLayout option;
+ option.text = optionText;
+ if (optionText.size() > 1 && optionText[0] >= '1' && optionText[0] <= '9' &&
+ optionText[1] == '.')
+ option.hotkey = optionText[0];
+ option.rowStart = (int)totalRows;
+ for (const Common::String &explicitLine : explicitLines) {
+ Common::Array<Common::String> wrappedExplicitLines;
+ DialogueRendering::wrapDialogueTextLikeNative(*_menuFont, _menuFontUsesCft,
+ explicitLine, DialogueRendering::kDialogueSubtitleTextWidth, wrappedExplicitLines);
+ if (wrappedExplicitLines.empty())
+ wrappedExplicitLines.push_back(explicitLine);
+ for (const Common::String &wrappedExplicitLine : wrappedExplicitLines)
+ option.wrappedLines.push_back(wrappedExplicitLine);
+ }
+ option.rowCount = MAX<int>(1, option.wrappedLines.size());
+ totalRows += option.rowCount;
+ options.push_back(Common::move(option));
+
+ explicitLines.clear();
+ optionText.clear();
+ };
+
+ auto consumeSegment = [&](const Common::String &rawSegment) {
+ Common::String segmentText = rawSegment;
+ if (segmentText.empty())
+ return;
+
+ const bool startsNewOption = segmentText.size() > 1 && segmentText[0] >= '1' &&
+ segmentText[0] <= '9' && segmentText[1] == '.';
+ if (startsNewOption) {
+ flushOption();
+ optionText = segmentText;
+ explicitLines.push_back(Common::move(segmentText));
+ return;
+ }
+
+ if (!optionText.empty())
+ optionText += ' ';
+ optionText += segmentText;
+ explicitLines.push_back(Common::move(segmentText));
+ };
+
+ for (uint i = 0; i < responseLine.size(); ++i) {
+ if (responseLine[i] == '/') {
+ consumeSegment(segment);
+ segment.clear();
+ continue;
+ }
+
+ segment += responseLine[i];
+ }
+
+ consumeSegment(segment);
+ flushOption();
+}
+
+int RoomNpcDialogueSession::getResponseMenuItemAt(const Common::Array<DialogueResponseOptionLayout> &options,
+ uint totalRows, const Common::Point &mousePos) {
+ if (mousePos.x < DialogueRendering::kDialogueTopicStartX || mousePos.x > DialogueRendering::kDialogueTopicEndX)
+ return -1;
+ if (mousePos.y < DialogueRendering::kDialogueKeywordTextY)
+ return -1;
+
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ const int row = (mousePos.y - DialogueRendering::kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
+ if (row < 0 || row >= (int)totalRows)
+ return -1;
+
+ for (uint i = 0; i < options.size(); ++i) {
+ const DialogueResponseOptionLayout &option = options[i];
+ if (row >= option.rowStart && row < option.rowStart + option.rowCount)
+ return (int)i;
+ }
+
+ return -1;
+}
+
+Common::Error RoomNpcDialogueSession::runResponseMenuTextInternal(const Common::String &responseLine, int responseLineIndex,
+ int &selectedIndex) {
+ selectedIndex = 0;
+
+ if (responseLine.empty()) {
+ if (responseLineIndex >= 0) {
+ debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) is empty",
+ responseLineIndex, responseLineIndex + 1);
+ } else {
+ debugC(1, kDebugDialogue, "Harvester: response menu text is empty");
+ }
+ return Common::kNoError;
+ }
+
+ Common::Array<DialogueResponseOptionLayout> options;
+ uint totalRows = 0;
+ parseResponseMenuLine(responseLine, options, totalRows);
+ if (options.empty()) {
+ if (responseLineIndex >= 0) {
+ debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) produced no options",
+ responseLineIndex, responseLineIndex + 1);
+ } else {
+ debugC(1, kDebugDialogue, "Harvester: response menu text produced no options");
+ }
+ return Common::kNoError;
+ }
+
+ Common::Array<Common::String> optionTexts;
+ for (const DialogueResponseOptionLayout &option : options) {
+ optionTexts.push_back(option.text);
+ debugC(3, kDebugDialogue, "Harvester: response menu option[%u] hotkey=%c rowStart=%d rowCount=%d",
+ optionTexts.size(), option.hotkey ? option.hotkey : '-', option.rowStart, option.rowCount);
+ }
+ logDialogueMenuItems("Response menu", responseLineIndex, responseLine, optionTexts);
+
+ const IndexedBitmap *textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueResponseTextboxIndex(totalRows));
+ Common::Error releaseError = waitForPointerRelease();
+ if (releaseError.getCode() != Common::kNoError)
+ return releaseError;
+
+ Graphics::FrameLimiter limiter(g_system, 60);
+ for (;;) {
+ const int hoveredOptionIndex = getResponseMenuItemAt(options, totalRows, _mousePos);
+ drawDialogueResponseMenu(textboxBitmap, options, hoveredOptionIndex);
+
+ Common::Event event;
+ while (g_system->getEventManager()->pollEvent(event)) {
+ Common::Error result = Common::kNoError;
+ if (_flow.handleSystemEvent(event, result))
+ return result;
+
+ if (event.type == Common::EVENT_KEYDOWN) {
+ if (selectResponseMenuHotkey(event, options, responseLineIndex, selectedIndex))
+ return waitForPointerRelease();
+ } else if (event.type == Common::EVENT_LBUTTONDOWN) {
+ const int clickedOptionIndex = getResponseMenuItemAt(options, totalRows, event.mouse);
+ if (clickedOptionIndex < 0)
+ continue;
+ selectedIndex = clickedOptionIndex + 1;
+ if (responseLineIndex >= 0) {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via click x=%d y=%d result=%d",
+ responseLineIndex, responseLineIndex + 1,
+ selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
+ event.mouse.x, event.mouse.y, selectedIndex);
+ } else {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu text selected option[%d]='%s' via click x=%d y=%d result=%d",
+ selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
+ event.mouse.x, event.mouse.y, selectedIndex);
+ }
+ return waitForPointerRelease();
+ }
+ }
+
+ if (_entityManager)
+ (void)_entityManager->syncCursorEntityPosition(_mousePos);
+ limiter.delayBeforeSwap();
+ limiter.startFrame();
+ }
+}
+
+bool RoomNpcDialogueSession::selectResponseMenuHotkey(const Common::Event &event,
+ const Common::Array<DialogueResponseOptionLayout> &options, int responseLineIndex,
+ int &selectedIndex) {
+ debugC(3, kDebugDialogue, "Harvester: response menu keycode=%d ascii=%u",
+ (int)event.kbd.keycode, event.kbd.ascii);
+ if (event.kbd.keycode >= Common::KEYCODE_1 && event.kbd.keycode <= Common::KEYCODE_9) {
+ const char hotkey = '1' + event.kbd.keycode - Common::KEYCODE_1;
+ for (int optionIndex = (int)options.size() - 1; optionIndex >= 0; --optionIndex) {
+ const DialogueResponseOptionLayout &option = options[(uint)optionIndex];
+ if (option.hotkey != hotkey)
+ continue;
+ selectedIndex = optionIndex + 1;
+ if (responseLineIndex >= 0) {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via hotkey=%c result=%d",
+ responseLineIndex, responseLineIndex + 1,
+ optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
+ } else {
+ debugC(2, kDebugDialogue,
+ "Harvester: response menu text selected option[%d]='%s' via hotkey=%c result=%d",
+ optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
+ }
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/dialogue/dialogue_render.cpp b/engines/harvester/dialogue/dialogue_render.cpp
index feee8b6ca5d..5d195b88ee9 100644
--- a/engines/harvester/dialogue/dialogue_render.cpp
+++ b/engines/harvester/dialogue/dialogue_render.cpp
@@ -20,11 +20,17 @@
*/
#include "harvester/dialogue/dialogue_render.h"
+#include "harvester/dialogue/dialogue_session.h"
#include "common/util.h"
#include "graphics/font.h"
#include "graphics/screen.h"
#include "harvester/cft_font.h"
+#include "harvester/entity.h"
+#include "harvester/flow/flow.h"
+#include "harvester/flow/flow_support.h"
+#include "harvester/harvester.h"
+#include "harvester/scene/room_presentation.h"
namespace Harvester {
namespace DialogueRendering {
@@ -105,4 +111,159 @@ bool isDialogueOtherHit(const Common::Point &mousePos) {
}
} // End of namespace DialogueRendering
+
+namespace {
+
+static const int kDialogueOverlayX = 167;
+static const int kDialogueOverlayY = 6;
+static const int kDialogueLeftHeadX = 6;
+static const int kDialogueRightHeadX = 474;
+static const int kDialogueHeadY = 6;
+static const int kDialogueSubtitleTextX = 178;
+static const int kDialogueSubtitleTextY = 12;
+static const int kDialogueKeywordTextX = 178;
+static const int kDialogueKeywordTextWidth = 300;
+static const int kDialogueOtherX = 171;
+static const int kDialogueOtherY = 183;
+static const int kDialogueTextEntryX = 208;
+static const int kDialogueTextEntryY = 184;
+static const int kDialogueOtherWidth = 120;
+static const int kDialogueKeywordTitleYOffset = 4;
+static const byte kTextColorNormal = 255;
+static const byte kTextColorHover = 251;
+
+} // End of anonymous namespace
+
+Graphics::Screen *RoomNpcDialogueSession::getActiveScreen() const {
+ return _engine.getScreen();
+}
+
+void RoomNpcDialogueSession::drawFontString(const Graphics::Font &font, bool usesCft, const Common::String &text,
+ int x, int y, int width, byte color) {
+ Graphics::Screen *activeScreen = getActiveScreen();
+ if (!activeScreen)
+ return;
+
+ if (usesCft)
+ font.drawString(activeScreen, text, x, y, width, 0);
+ else
+ RoomPresentation::drawShadowedString(*activeScreen, font, text, x, y, width, color);
+}
+
+void RoomNpcDialogueSession::drawDialogueOverlay(const IndexedBitmap *overlayBitmap,
+ const Common::Array<Common::String> *subtitleLines, const Common::Array<Common::String> *topics,
+ int hoveredTopicIndex, bool hoverOther, const Common::String *textEntryValue,
+ bool textEntryCursorVisible) {
+ Graphics::Screen *activeScreen = getActiveScreen();
+ if (!activeScreen)
+ return;
+
+ setScaledPalette(*activeScreen, _palette, _paletteBrightness);
+ RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
+ if (_leftHeadVisible && _leftHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
+ if (_rightHeadVisible && _rightHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
+ if (overlayBitmap && overlayBitmap->isValid())
+ blitTransparentBitmap(*activeScreen, *overlayBitmap, kDialogueOverlayX, kDialogueOverlayY);
+
+ if (subtitleLines) {
+ if (_subtitleFontUsesCft) {
+ DialogueRendering::drawDialogueTextLines(*activeScreen, *_subtitleFont, *subtitleLines,
+ kDialogueSubtitleTextX, kDialogueSubtitleTextY, DialogueRendering::kDialogueSubtitleTextWidth);
+ } else {
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_subtitleFont);
+ for (uint i = 0; i < subtitleLines->size(); ++i) {
+ RoomPresentation::drawShadowedString(*activeScreen, *_subtitleFont, (*subtitleLines)[i],
+ kDialogueSubtitleTextX,
+ kDialogueSubtitleTextY + (int)i * lineHeight,
+ DialogueRendering::kDialogueSubtitleTextWidth, kTextColorNormal);
+ }
+ }
+ }
+
+ 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 = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ for (uint i = 0; i < topics->size(); ++i) {
+ const bool highlighted = (int)i == hoveredTopicIndex;
+ const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
+ const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
+ drawFontString(font, usesCft, (*topics)[i], kDialogueKeywordTextX,
+ DialogueRendering::kDialogueKeywordTextY + (int)i * lineHeight, kDialogueKeywordTextWidth,
+ highlighted ? kTextColorNormal : kTextColorHover);
+ }
+
+ const Graphics::Font &otherFont = hoverOther ? *_highlightFont : *_menuFont;
+ const bool otherUsesCft = hoverOther ? _highlightFontUsesCft : _menuFontUsesCft;
+ drawFontString(otherFont, otherUsesCft, _menuTextConfig.dialogueOtherLabel,
+ kDialogueOtherX, kDialogueOtherY,
+ kDialogueOtherWidth, hoverOther ? kTextColorNormal : kTextColorHover);
+ }
+
+ if (textEntryValue) {
+ drawFontString(*_highlightFont, _highlightFontUsesCft,
+ *textEntryValue + (textEntryCursorVisible ? "~" : ""),
+ kDialogueTextEntryX, kDialogueTextEntryY, DialogueRendering::kDialogueTextEntryMaxWidth,
+ kTextColorNormal);
+ }
+
+ if (_entityManager)
+ _entityManager->drawCursor(*activeScreen);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+}
+
+void RoomNpcDialogueSession::drawDialogueResponseMenu(const IndexedBitmap *textboxBitmap,
+ const Common::Array<DialogueResponseOptionLayout> &options, int hoveredOptionIndex) {
+ Graphics::Screen *activeScreen = getActiveScreen();
+ if (!activeScreen)
+ return;
+
+ setScaledPalette(*activeScreen, _palette, _paletteBrightness);
+ RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
+ if (_leftHeadVisible && _leftHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
+ if (_rightHeadVisible && _rightHeadBitmap.isValid())
+ blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
+ if (textboxBitmap && textboxBitmap->isValid())
+ blitTransparentBitmap(*activeScreen, *textboxBitmap, kDialogueOverlayX, kDialogueOverlayY);
+
+ const Common::String &title = _menuTextConfig.dialogueResponsesLabel;
+ const Graphics::Font &titleFont = *_highlightFont;
+ const bool titleUsesCft = _highlightFontUsesCft;
+ const int titleWidth = titleFont.getStringWidth(title);
+ const int titleX = kDialogueSubtitleTextX + MAX<int>(0, (DialogueRendering::kDialogueSubtitleTextWidth - titleWidth) / 2);
+ drawFontString(titleFont, titleUsesCft, title, titleX, 11, titleWidth, kTextColorNormal);
+
+ const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
+ int drawY = DialogueRendering::kDialogueKeywordTextY;
+ for (uint i = 0; i < options.size(); ++i) {
+ const bool highlighted = (int)i == hoveredOptionIndex;
+ const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
+ const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
+ const byte color = highlighted ? kTextColorNormal : kTextColorHover;
+
+ for (const Common::String &wrappedLine : options[i].wrappedLines) {
+ drawFontString(font, usesCft, wrappedLine, kDialogueSubtitleTextX, drawY,
+ DialogueRendering::kDialogueSubtitleTextWidth, color);
+ drawY += lineHeight;
+ }
+ }
+
+ if (_entityManager)
+ _entityManager->drawCursor(*activeScreen);
+ activeScreen->makeAllDirty();
+ activeScreen->update();
+}
+
} // End of namespace Harvester
diff --git a/engines/harvester/dialogue/dialogue_render.h b/engines/harvester/dialogue/dialogue_render.h
index 99989403609..d979e6b3d3d 100644
--- a/engines/harvester/dialogue/dialogue_render.h
+++ b/engines/harvester/dialogue/dialogue_render.h
@@ -35,6 +35,7 @@ namespace Harvester {
namespace DialogueRendering {
static const int kDialogueSubtitleTextWidth = 316;
+static const int kDialogueTextEntryMaxWidth = 550;
static const int kDialogueKeywordTextY = 34;
static const int kDialogueTopicStartX = 168;
static const int kDialogueTopicEndX = 482;
diff --git a/engines/harvester/dialogue/dialogue_session.cpp b/engines/harvester/dialogue/dialogue_session.cpp
index 74ffa79964d..ef0a47f70ae 100644
--- a/engines/harvester/dialogue/dialogue_session.cpp
+++ b/engines/harvester/dialogue/dialogue_session.cpp
@@ -20,90 +20,25 @@
*/
#include "harvester/dialogue/dialogue_session.h"
-#include "harvester/dialogue/dialogue_render.h"
#include "common/algorithm.h"
#include "common/debug.h"
-#include "common/events.h"
-#include "common/system.h"
#include "graphics/fontman.h"
-#include "graphics/framelimiter.h"
-#include "harvester/art.h"
#include "harvester/detection.h"
-#include "harvester/entity.h"
#include "harvester/flow/flow.h"
#include "harvester/flow/flow_support.h"
#include "harvester/harvester.h"
#include "harvester/resources.h"
-#include "harvester/scene/room_presentation.h"
#include "harvester/text.h"
namespace Harvester {
namespace {
-static const int kDialogueOverlayX = 167;
-static const int kDialogueOverlayY = 6;
-static const int kDialogueLeftHeadX = 6;
-static const int kDialogueRightHeadX = 474;
-static const int kDialogueHeadY = 6;
-static const int kDialogueSubtitleTextX = 178;
-static const int kDialogueSubtitleTextY = 12;
-static const int kDialogueKeywordTextX = 178;
-static const int kDialogueKeywordTextWidth = 300;
-static const int kDialogueOtherX = 171;
-static const int kDialogueOtherY = 183;
-static const int kDialogueTextEntryX = 208;
-static const int kDialogueTextEntryY = 184;
-static const uint kDialogueTextEntryMaxLength = 26;
-static const int kDialogueTextEntryMaxWidth = 550;
-static const uint32 kDialogueTextEntryBlinkTicks = 0x30;
-static const int kDialogueOtherWidth = 120;
-static const int kDialogueKeywordTitleYOffset = 4;
static const int kDialogueGenericByeResponseIndex = 13;
static const char *const kDialogueKeywordBitmapPath = "1:/GRAPHIC/OTHER/KEYWORD.BM";
static const char *const kDialogueLocalizedKeywordBitmapPath = "GRAPHIC/OTHER/KEYWORD.BM";
-static const byte kTextColorNormal = 255;
-static const byte kTextColorHover = 251;
-
-static void splitDialogueMenuLine(const Common::String &line, Common::Array<Common::String> &parts) {
- parts.clear();
- if (line.empty())
- return;
-
- Common::String token;
- for (uint i = 0; i < line.size(); ++i) {
- if (line[i] == '/') {
- parts.push_back(Common::move(token));
- token.clear();
- continue;
- }
-
- token += line[i];
- }
-
- parts.push_back(Common::move(token));
-}
-
-static void logDialogueMenuItems(const char *label, int sourceLineIndex,
- const Common::String &rawLine, const Common::Array<Common::String> &items) {
- if (sourceLineIndex >= 0) {
- debugC(3, kDebugDialogue, "Harvester: %s source line 0x%x (%d) raw='%s'",
- label, sourceLineIndex, sourceLineIndex + 1, rawLine.c_str());
- } else {
- debugC(3, kDebugDialogue, "Harvester: %s source raw='%s'", label, rawLine.c_str());
- }
-
- if (items.empty()) {
- debugC(1, kDebugDialogue, "Harvester: %s has no explicit items", label);
- return;
- }
-
- for (uint i = 0; i < items.size(); ++i)
- debugC(3, kDebugDialogue, "Harvester: %s item[%u]='%s'", label, i + 1, items[i].c_str());
-}
-
static Common::String buildDialogueHeadId(const Common::String &speakerId, int headVariant) {
return Common::String::format("%s%d", speakerId.c_str(), headVariant);
}
@@ -197,162 +132,6 @@ const Common::String &RoomNpcDialogueSession::getGenericByeTopic() const {
return _genericByeTopic;
}
-Common::Error RoomNpcDialogueSession::runKeywordMenu(const Common::String &topicBuffer, int topicBufferLineIndex,
- Common::String &selectedTopic,
- DialogueKeywordMenuSelectionState &selection) {
- selectedTopic.clear();
- selection = DialogueKeywordMenuSelectionState();
- Common::Array<Common::String> topics;
- splitDialogueMenuLine(topicBuffer, topics);
- logDialogueMenuItems("Keyword menu", topicBufferLineIndex, topicBuffer, topics);
- topics.push_back(_genericByeTopic);
- debugC(1, kDebugDialogue, "Harvester: keyword menu default topic='%s'", _genericByeTopic.c_str());
- bool editingOther = false;
- bool textEntryCursorVisible = false;
- uint32 textEntryBlinkTimestamp = 0;
- Common::String typedTopic;
- Common::Error releaseError = waitForPointerRelease();
- if (releaseError.getCode() != Common::kNoError)
- return releaseError;
-
- Graphics::FrameLimiter limiter(g_system, 60);
- for (;;) {
- const uint32 now = getRuntimeClockTicks();
- if (editingOther && now - textEntryBlinkTimestamp > kDialogueTextEntryBlinkTicks) {
- textEntryCursorVisible = !textEntryCursorVisible;
- textEntryBlinkTimestamp = now;
- }
- const int hoveredTopicIndex = editingOther ? -1 : DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
- const bool hoverOther = editingOther || DialogueRendering::isDialogueOtherHit(_mousePos);
- drawDialogueOverlay(&_keywordBitmap, nullptr, &topics, hoveredTopicIndex, hoverOther,
- editingOther ? &typedTopic : nullptr, textEntryCursorVisible);
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result)) {
- if (editingOther && _entityManager)
- _entityManager->showCursor();
- return result;
- }
-
- if (editingOther) {
- bool acceptInput = false;
- bool cancelInput = false;
- if (event.type == Common::EVENT_KEYDOWN) {
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- cancelInput = true;
- } else if (event.kbd.keycode == Common::KEYCODE_BACKSPACE ||
- event.kbd.keycode == Common::KEYCODE_LEFT) {
- deleteTextEntryCharacter(typedTopic);
- } else if (event.kbd.keycode == Common::KEYCODE_HOME) {
- typedTopic.clear();
- } else if (event.kbd.keycode == Common::KEYCODE_RETURN ||
- event.kbd.keycode == Common::KEYCODE_KP_ENTER) {
- acceptInput = true;
- } else if (event.kbd.ascii >= 0x1f && event.kbd.ascii <= 0x80 &&
- event.kbd.ascii != '~' &&
- (!_subtitleCftFont || _subtitleCftFont->hasGlyph(event.kbd.ascii))) {
- appendTextEntryCharacter(typedTopic, *_highlightFont, event.kbd.ascii,
- kDialogueTextEntryMaxLength, kDialogueTextEntryMaxWidth);
- }
- } else if (event.type == Common::EVENT_LBUTTONDOWN) {
- acceptInput = Common::Rect(3, 460, 318, 477).contains(event.mouse);
- cancelInput = Common::Rect(321, 460, 636, 477).contains(event.mouse);
- if (cancelInput)
- g_system->delayMillis(400);
- }
- if (acceptInput || cancelInput) {
- editingOther = false;
- if (_entityManager)
- _entityManager->showCursor();
- if (acceptInput && !typedTopic.empty()) {
- selectedTopic = typedTopic;
- selection.fromTypedInput = true;
- debugC(1, kDebugDialogue, "Harvester: keyword menu typed topic='%s'",
- selectedTopic.c_str());
- return Common::kNoError;
- }
- typedTopic.clear();
- debugC(1, kDebugDialogue, "Harvester: keyword menu cancelled Other input");
- }
- continue;
- }
-
- switch (event.type) {
- case Common::EVENT_KEYDOWN:
- if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
- selectedTopic = _genericByeTopic;
- selection.fromEscape = true;
- selection.fromGenericBye = true;
- debugC(1, kDebugDialogue,
- "Harvester: keyword menu selected default topic via escape '%s'",
- selectedTopic.c_str());
- return Common::kNoError;
- }
- break;
- case Common::EVENT_LBUTTONDOWN: {
- const int clickedTopic = DialogueRendering::getDialogueMenuItemAt(*_menuFont, topics.size(), _mousePos);
- if (clickedTopic >= 0) {
- selectedTopic = topics[(uint)clickedTopic];
- selection.fromGenericBye = clickedTopic == (int)topics.size() - 1;
- debugC(2, kDebugDialogue, "Harvester: keyword menu selected topic[%d]='%s' via click",
- clickedTopic + 1, selectedTopic.c_str());
- return Common::kNoError;
- }
- if (DialogueRendering::isDialogueOtherHit(_mousePos)) {
- typedTopic.clear();
- editingOther = true;
- textEntryCursorVisible = false;
- textEntryBlinkTimestamp = getRuntimeClockTicks();
- debugC(1, kDebugDialogue, "Harvester: keyword menu entered Other input");
- if (_entityManager)
- _entityManager->hideCursor();
- }
- break;
- }
- default:
- break;
- }
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-}
-
-Common::Error RoomNpcDialogueSession::runResponseMenu(int responseLineIndex, int &selectedIndex) {
- const Common::String responseLine = _text->getDialogueResponseLine(responseLineIndex);
- return runResponseMenuTextInternal(responseLine, responseLineIndex, selectedIndex);
-}
-
-Common::Error RoomNpcDialogueSession::runResponseMenuText(const Common::String &responseLine, int &selectedIndex) {
- return runResponseMenuTextInternal(responseLine, -1, selectedIndex);
-}
-
-void RoomNpcDialogueSession::assignTopicBuffer(Common::String &topicBuffer, int &topicBufferLineIndex,
- int responseLineIndex, const char *) {
- topicBuffer = _text->getDialogueResponseLine(responseLineIndex);
- topicBufferLineIndex = responseLineIndex;
-}
-
-bool RoomNpcDialogueSession::matchesResponseLine(const Common::String &selectedTopic, int responseLineIndex) {
- const Common::String topicText = _text->getDialogueResponseLine(responseLineIndex);
- return !topicText.empty() && selectedTopic.equalsIgnoreCase(topicText);
-}
-
-bool RoomNpcDialogueSession::matchesAnyResponseLine(const Common::String &selectedTopic,
- const int *responseLineIndices, uint responseLineCount) {
- for (uint i = 0; i < responseLineCount; ++i) {
- if (matchesResponseLine(selectedTopic, responseLineIndices[i]))
- return true;
- }
-
- return false;
-}
-
void RoomNpcDialogueSession::queueDialogueInteractionIfNeeded(const InteractionResult &interaction) {
if (interaction.abortRemainingCommandChain || interaction.mutatedRuntimeState ||
!interaction.musicPath.empty() || !interaction.nextRoomName.empty() ||
@@ -400,22 +179,6 @@ void RoomNpcDialogueSession::setActiveSpeakerPortrait(const Common::String &spea
}
}
-Graphics::Screen *RoomNpcDialogueSession::getActiveScreen() const {
- return _engine.getScreen();
-}
-
-void RoomNpcDialogueSession::drawFontString(const Graphics::Font &font, bool usesCft, const Common::String &text,
- int x, int y, int width, byte color) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return;
-
- if (usesCft)
- font.drawString(activeScreen, text, x, y, width, 0);
- else
- RoomPresentation::drawShadowedString(*activeScreen, font, text, x, y, width, color);
-}
-
void RoomNpcDialogueSession::ensureSpeakerPortrait(const Common::String &speakerId, int headVariant) {
if (speakerId.empty())
return;
@@ -435,315 +198,4 @@ void RoomNpcDialogueSession::ensureSpeakerPortrait(const Common::String &speaker
}
}
-void RoomNpcDialogueSession::drawDialogueOverlay(const IndexedBitmap *overlayBitmap,
- const Common::Array<Common::String> *subtitleLines, const Common::Array<Common::String> *topics,
- int hoveredTopicIndex, bool hoverOther, const Common::String *textEntryValue,
- bool textEntryCursorVisible) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return;
-
- setScaledPalette(*activeScreen, _palette, _paletteBrightness);
- RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
- if (_leftHeadVisible && _leftHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
- if (_rightHeadVisible && _rightHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
- if (overlayBitmap && overlayBitmap->isValid())
- blitTransparentBitmap(*activeScreen, *overlayBitmap, kDialogueOverlayX, kDialogueOverlayY);
-
- if (subtitleLines) {
- if (_subtitleFontUsesCft) {
- DialogueRendering::drawDialogueTextLines(*activeScreen, *_subtitleFont, *subtitleLines,
- kDialogueSubtitleTextX, kDialogueSubtitleTextY, DialogueRendering::kDialogueSubtitleTextWidth);
- } else {
- const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_subtitleFont);
- for (uint i = 0; i < subtitleLines->size(); ++i) {
- RoomPresentation::drawShadowedString(*activeScreen, *_subtitleFont, (*subtitleLines)[i],
- kDialogueSubtitleTextX,
- kDialogueSubtitleTextY + (int)i * lineHeight,
- DialogueRendering::kDialogueSubtitleTextWidth, kTextColorNormal);
- }
- }
- }
-
- 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 = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
- for (uint i = 0; i < topics->size(); ++i) {
- const bool highlighted = (int)i == hoveredTopicIndex;
- const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
- const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
- drawFontString(font, usesCft, (*topics)[i], kDialogueKeywordTextX,
- DialogueRendering::kDialogueKeywordTextY + (int)i * lineHeight, kDialogueKeywordTextWidth,
- highlighted ? kTextColorNormal : kTextColorHover);
- }
-
- const Graphics::Font &otherFont = hoverOther ? *_highlightFont : *_menuFont;
- const bool otherUsesCft = hoverOther ? _highlightFontUsesCft : _menuFontUsesCft;
- drawFontString(otherFont, otherUsesCft, _menuTextConfig.dialogueOtherLabel,
- kDialogueOtherX, kDialogueOtherY,
- kDialogueOtherWidth, hoverOther ? kTextColorNormal : kTextColorHover);
- }
-
- if (textEntryValue) {
- drawFontString(*_highlightFont, _highlightFontUsesCft,
- *textEntryValue + (textEntryCursorVisible ? "~" : ""),
- kDialogueTextEntryX, kDialogueTextEntryY, kDialogueTextEntryMaxWidth,
- kTextColorNormal);
- }
-
- if (_entityManager)
- _entityManager->drawCursor(*activeScreen);
- activeScreen->makeAllDirty();
- activeScreen->update();
-}
-
-void RoomNpcDialogueSession::parseResponseMenuLine(const Common::String &responseLine,
- Common::Array<DialogueResponseOptionLayout> &options, uint &totalRows) {
- options.clear();
- totalRows = 0;
- if (responseLine.empty())
- return;
-
- Common::String segment;
- Common::Array<Common::String> explicitLines;
- Common::String optionText;
-
- auto flushOption = [&]() {
- if (explicitLines.empty())
- return;
-
- DialogueResponseOptionLayout option;
- option.text = optionText;
- if (optionText.size() > 1 && optionText[0] >= '1' && optionText[0] <= '9' &&
- optionText[1] == '.')
- option.hotkey = optionText[0];
- option.rowStart = (int)totalRows;
- for (const Common::String &explicitLine : explicitLines) {
- Common::Array<Common::String> wrappedExplicitLines;
- DialogueRendering::wrapDialogueTextLikeNative(*_menuFont, _menuFontUsesCft,
- explicitLine, DialogueRendering::kDialogueSubtitleTextWidth, wrappedExplicitLines);
- if (wrappedExplicitLines.empty())
- wrappedExplicitLines.push_back(explicitLine);
- for (const Common::String &wrappedExplicitLine : wrappedExplicitLines)
- option.wrappedLines.push_back(wrappedExplicitLine);
- }
- option.rowCount = MAX<int>(1, option.wrappedLines.size());
- totalRows += option.rowCount;
- options.push_back(Common::move(option));
-
- explicitLines.clear();
- optionText.clear();
- };
-
- auto consumeSegment = [&](const Common::String &rawSegment) {
- Common::String segmentText = rawSegment;
- if (segmentText.empty())
- return;
-
- const bool startsNewOption = segmentText.size() > 1 && segmentText[0] >= '1' &&
- segmentText[0] <= '9' && segmentText[1] == '.';
- if (startsNewOption) {
- flushOption();
- optionText = segmentText;
- explicitLines.push_back(Common::move(segmentText));
- return;
- }
-
- if (!optionText.empty())
- optionText += ' ';
- optionText += segmentText;
- explicitLines.push_back(Common::move(segmentText));
- };
-
- for (uint i = 0; i < responseLine.size(); ++i) {
- if (responseLine[i] == '/') {
- consumeSegment(segment);
- segment.clear();
- continue;
- }
-
- segment += responseLine[i];
- }
-
- consumeSegment(segment);
- flushOption();
-}
-
-int RoomNpcDialogueSession::getResponseMenuItemAt(const Common::Array<DialogueResponseOptionLayout> &options,
- uint totalRows, const Common::Point &mousePos) {
- if (mousePos.x < DialogueRendering::kDialogueTopicStartX || mousePos.x > DialogueRendering::kDialogueTopicEndX)
- return -1;
- if (mousePos.y < DialogueRendering::kDialogueKeywordTextY)
- return -1;
-
- const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
- const int row = (mousePos.y - DialogueRendering::kDialogueKeywordTextY) / MAX<int>(1, lineHeight);
- if (row < 0 || row >= (int)totalRows)
- return -1;
-
- for (uint i = 0; i < options.size(); ++i) {
- const DialogueResponseOptionLayout &option = options[i];
- if (row >= option.rowStart && row < option.rowStart + option.rowCount)
- return (int)i;
- }
-
- return -1;
-}
-
-void RoomNpcDialogueSession::drawDialogueResponseMenu(const IndexedBitmap *textboxBitmap,
- const Common::Array<DialogueResponseOptionLayout> &options, int hoveredOptionIndex) {
- Graphics::Screen *activeScreen = getActiveScreen();
- if (!activeScreen)
- return;
-
- setScaledPalette(*activeScreen, _palette, _paletteBrightness);
- RoomPresentation::blitBitmap(*activeScreen, _backdrop, 0, 0);
- if (_leftHeadVisible && _leftHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _leftHeadBitmap, kDialogueLeftHeadX, kDialogueHeadY);
- if (_rightHeadVisible && _rightHeadBitmap.isValid())
- blitTransparentBitmap(*activeScreen, _rightHeadBitmap, kDialogueRightHeadX, kDialogueHeadY);
- if (textboxBitmap && textboxBitmap->isValid())
- blitTransparentBitmap(*activeScreen, *textboxBitmap, kDialogueOverlayX, kDialogueOverlayY);
-
- const Common::String &title = _menuTextConfig.dialogueResponsesLabel;
- const Graphics::Font &titleFont = *_highlightFont;
- const bool titleUsesCft = _highlightFontUsesCft;
- const int titleWidth = titleFont.getStringWidth(title);
- const int titleX = kDialogueSubtitleTextX + MAX<int>(0, (DialogueRendering::kDialogueSubtitleTextWidth - titleWidth) / 2);
- drawFontString(titleFont, titleUsesCft, title, titleX, 11, titleWidth, kTextColorNormal);
-
- const int lineHeight = DialogueRendering::getDialogueTextLineHeight(*_menuFont);
- int drawY = DialogueRendering::kDialogueKeywordTextY;
- for (uint i = 0; i < options.size(); ++i) {
- const bool highlighted = (int)i == hoveredOptionIndex;
- const Graphics::Font &font = highlighted ? *_highlightFont : *_menuFont;
- const bool usesCft = highlighted ? _highlightFontUsesCft : _menuFontUsesCft;
- const byte color = highlighted ? kTextColorNormal : kTextColorHover;
-
- for (const Common::String &wrappedLine : options[i].wrappedLines) {
- drawFontString(font, usesCft, wrappedLine, kDialogueSubtitleTextX, drawY,
- DialogueRendering::kDialogueSubtitleTextWidth, color);
- drawY += lineHeight;
- }
- }
-
- if (_entityManager)
- _entityManager->drawCursor(*activeScreen);
- activeScreen->makeAllDirty();
- activeScreen->update();
-}
-
-Common::Error RoomNpcDialogueSession::runResponseMenuTextInternal(const Common::String &responseLine, int responseLineIndex,
- int &selectedIndex) {
- selectedIndex = 0;
-
- if (responseLine.empty()) {
- if (responseLineIndex >= 0) {
- debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) is empty",
- responseLineIndex, responseLineIndex + 1);
- } else {
- debugC(1, kDebugDialogue, "Harvester: response menu text is empty");
- }
- return Common::kNoError;
- }
-
- Common::Array<DialogueResponseOptionLayout> options;
- uint totalRows = 0;
- parseResponseMenuLine(responseLine, options, totalRows);
- if (options.empty()) {
- if (responseLineIndex >= 0) {
- debugC(1, kDebugDialogue, "Harvester: response menu line 0x%x (%d) produced no options",
- responseLineIndex, responseLineIndex + 1);
- } else {
- debugC(1, kDebugDialogue, "Harvester: response menu text produced no options");
- }
- return Common::kNoError;
- }
-
- Common::Array<Common::String> optionTexts;
- for (const DialogueResponseOptionLayout &option : options) {
- optionTexts.push_back(option.text);
- debugC(3, kDebugDialogue, "Harvester: response menu option[%u] hotkey=%c rowStart=%d rowCount=%d",
- optionTexts.size(), option.hotkey ? option.hotkey : '-', option.rowStart, option.rowCount);
- }
- logDialogueMenuItems("Response menu", responseLineIndex, responseLine, optionTexts);
-
- const IndexedBitmap *textboxBitmap = _art->getTextboxBitmap(DialogueRendering::resolveDialogueResponseTextboxIndex(totalRows));
- Common::Error releaseError = waitForPointerRelease();
- if (releaseError.getCode() != Common::kNoError)
- return releaseError;
-
- Graphics::FrameLimiter limiter(g_system, 60);
- for (;;) {
- const int hoveredOptionIndex = getResponseMenuItemAt(options, totalRows, _mousePos);
- drawDialogueResponseMenu(textboxBitmap, options, hoveredOptionIndex);
-
- Common::Event event;
- while (g_system->getEventManager()->pollEvent(event)) {
- Common::Error result = Common::kNoError;
- if (_flow.handleSystemEvent(event, result))
- return result;
-
- if (event.type == Common::EVENT_KEYDOWN) {
- debugC(3, kDebugDialogue, "Harvester: response menu keycode=%d ascii=%u",
- (int)event.kbd.keycode, event.kbd.ascii);
- if (event.kbd.keycode >= Common::KEYCODE_1 && event.kbd.keycode <= Common::KEYCODE_9) {
- const char hotkey = '1' + event.kbd.keycode - Common::KEYCODE_1;
- for (int optionIndex = (int)options.size() - 1; optionIndex >= 0; --optionIndex) {
- const DialogueResponseOptionLayout &option = options[(uint)optionIndex];
- if (option.hotkey != hotkey)
- continue;
- selectedIndex = optionIndex + 1;
- if (responseLineIndex >= 0) {
- debugC(2, kDebugDialogue,
- "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via hotkey=%c result=%d",
- responseLineIndex, responseLineIndex + 1,
- optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
- } else {
- debugC(2, kDebugDialogue,
- "Harvester: response menu text selected option[%d]='%s' via hotkey=%c result=%d",
- optionIndex + 1, option.text.c_str(), hotkey, selectedIndex);
- }
- return waitForPointerRelease();
- }
- }
- } else if (event.type == Common::EVENT_LBUTTONDOWN) {
- const int clickedOptionIndex = getResponseMenuItemAt(options, totalRows, event.mouse);
- if (clickedOptionIndex < 0)
- continue;
- selectedIndex = clickedOptionIndex + 1;
- if (responseLineIndex >= 0) {
- debugC(2, kDebugDialogue,
- "Harvester: response menu line 0x%x (%d) selected option[%d]='%s' via click x=%d y=%d result=%d",
- responseLineIndex, responseLineIndex + 1,
- selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
- event.mouse.x, event.mouse.y, selectedIndex);
- } else {
- debugC(2, kDebugDialogue,
- "Harvester: response menu text selected option[%d]='%s' via click x=%d y=%d result=%d",
- selectedIndex, options[(uint)clickedOptionIndex].text.c_str(),
- event.mouse.x, event.mouse.y, selectedIndex);
- }
- return waitForPointerRelease();
- }
- }
-
- if (_entityManager)
- (void)_entityManager->syncCursorEntityPosition(_mousePos);
- limiter.delayBeforeSwap();
- limiter.startFrame();
- }
-}
-
} // End of namespace Harvester
diff --git a/engines/harvester/dialogue/dialogue_session.h b/engines/harvester/dialogue/dialogue_session.h
index 9e76dca33fc..a496374d7cd 100644
--- a/engines/harvester/dialogue/dialogue_session.h
+++ b/engines/harvester/dialogue/dialogue_session.h
@@ -28,6 +28,10 @@
#include "harvester/cft_font.h"
#include "harvester/npc/dialogue_runtime.h"
+namespace Common {
+struct Event;
+}
+
namespace Graphics {
class Screen;
}
@@ -90,6 +94,11 @@ private:
const Common::Array<Common::String> *subtitleLines, const Common::Array<Common::String> *topics,
int hoveredTopicIndex, bool hoverOther, const Common::String *textEntryValue,
bool textEntryCursorVisible = false);
+ bool handleKeywordTextEntry(const Common::Event &event, Common::String &typedTopic,
+ bool &editingOther, Common::String &selectedTopic, DialogueKeywordMenuSelectionState &selection);
+ bool selectResponseMenuHotkey(const Common::Event &event,
+ const Common::Array<DialogueResponseOptionLayout> &options, int responseLineIndex,
+ int &selectedIndex);
void parseResponseMenuLine(const Common::String &responseLine,
Common::Array<DialogueResponseOptionLayout> &options, uint &totalRows);
int getResponseMenuItemAt(const Common::Array<DialogueResponseOptionLayout> &options,
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index cd7c62e0d4a..39f8009be96 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -7,6 +7,7 @@ MODULE_OBJS = \
cheats.o \
console.o \
dialogue/dialogue.o \
+ dialogue/dialogue_menu.o \
dialogue/dialogue_playback.o \
dialogue/dialogue_render.o \
dialogue/dialogue_session.o \
Commit: 2c315a9616abed2740757dacb4ed3752f5be767b
https://github.com/scummvm/scummvm/commit/2c315a9616abed2740757dacb4ed3752f5be767b
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Separate audio decoding and scope input ownership
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/media/audio_decoder.cpp
A engines/harvester/media/audio_decoder.h
engines/harvester/media_manager.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/media/audio_decoder.cpp b/engines/harvester/media/audio_decoder.cpp
new file mode 100644
index 00000000000..338661e1ac3
--- /dev/null
+++ b/engines/harvester/media/audio_decoder.cpp
@@ -0,0 +1,184 @@
+/* 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/media/audio_decoder.h"
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/decoders/wave.h"
+#include "common/endian.h"
+#include "common/ptr.h"
+#include "common/stream.h"
+#include "common/util.h"
+#include "harvester/resources.h"
+
+namespace Harvester {
+
+namespace {
+
+static const int8 kHarvesterImaIndexAdjustTable[16] = {
+ -1, -1, -1, -1, 2, 4, 6, 8,
+ -1, -1, -1, -1, 2, 4, 6, 8
+};
+
+static const uint16 kHarvesterImaStepTable[89] = {
+ 7, 8, 9, 10, 11, 12, 13, 14, 16, 17,
+ 19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
+ 50, 55, 60, 66, 73, 80, 88, 97, 107, 118,
+ 130, 143, 157, 173, 190, 209, 230, 253, 279, 307,
+ 337, 371, 408, 449, 494, 544, 598, 658, 724, 796,
+ 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
+ 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358,
+ 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
+ 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
+};
+
+static uint32 decodeHarvesterFcmp(byte *dest, const byte *src, uint32 srcSize, uint16 bitsPerSample) {
+ const uint32 decodedBytesPerInputByte = bitsPerSample >> 2;
+ const uint32 decodedSize = srcSize * decodedBytesPerInputByte;
+ const uint32 sampleCount = (bitsPerSample == 16) ? (decodedSize >> 1) : decodedSize;
+ int predictor = 0;
+ int stepIndex = 0;
+ int stepSize = 7;
+ byte cachedByte = 0;
+ uint32 srcPos = 0;
+
+ for (uint32 sample = 0; sample < sampleCount; ++sample) {
+ const byte nibble = ((sample & 1) == 0)
+ ? ((cachedByte = src[srcPos++]) & 0x0f)
+ : ((cachedByte >> 4) & 0x0f);
+ int delta = stepSize >> 3;
+ if (nibble & 4)
+ delta += stepSize;
+ if (nibble & 2)
+ delta += stepSize >> 1;
+ if (nibble & 1)
+ delta += stepSize >> 2;
+ if (nibble & 8)
+ delta = -delta;
+
+ predictor += delta;
+ if (predictor > 0x7fff)
+ predictor = 0x7fff;
+ else if (predictor < -0x8000)
+ predictor = -0x8000;
+
+ if (bitsPerSample == 16) {
+ WRITE_LE_INT16(dest, predictor);
+ dest += 2;
+ } else {
+ *dest++ = (byte)(((predictor >> 8) & 0xff) ^ 0x80);
+ }
+
+ stepIndex += kHarvesterImaIndexAdjustTable[nibble];
+ if (stepIndex < 0)
+ stepIndex = 0;
+ else if (stepIndex > 88)
+ stepIndex = 88;
+ stepSize = kHarvesterImaStepTable[stepIndex];
+ }
+
+ return decodedSize;
+}
+
+static void applyStartupFcmpWarmup(byte *decodedPcm, uint32 &decodedSize, FcmpWarmupMode warmupMode) {
+ if (!decodedPcm || decodedSize == 0)
+ return;
+
+ if (warmupMode == FcmpWarmupMode::kMusic) {
+ // Native start_music_stream zeros the first 0x64 decoded bytes of the first FCMP chunk.
+ memset(decodedPcm, 0, MIN<uint32>(decodedSize, 100));
+ return;
+ }
+
+ // Native load_sound_sample/load_dialogue_voice_sample queue from decoded_pcm + 0x1f4.
+ const uint32 trimBytes = MIN<uint32>(decodedSize, 500);
+ if (trimBytes == decodedSize) {
+ memset(decodedPcm, 0, decodedSize);
+ return;
+ }
+
+ memmove(decodedPcm, decodedPcm + trimBytes, decodedSize - trimBytes);
+ decodedSize -= trimBytes;
+}
+
+} // End of anonymous namespace
+
+Audio::SeekableAudioStream *decodeStartupAudioStream(Common::SeekableReadStream *stream,
+ FcmpWarmupMode warmupMode) {
+ if (!stream)
+ return nullptr;
+
+ Common::ScopedPtr<Common::SeekableReadStream> input(stream);
+
+ if (input->size() >= 14) {
+ char magic[4];
+ input->read(magic, sizeof(magic));
+ if (!memcmp(magic, "FCMP", sizeof(magic))) {
+ const uint32 payloadSizeFromHeader = input->readUint32LE();
+ const uint32 sampleRate = input->readUint32LE();
+ const uint16 bitsPerSample = input->readUint16LE();
+ const uint32 availablePayloadSize = input->size() - 14;
+ const uint32 payloadSize = payloadSizeFromHeader != 0
+ ? MIN<uint32>(payloadSizeFromHeader, availablePayloadSize)
+ : availablePayloadSize;
+
+ if (sampleRate == 0 || (bitsPerSample != 8 && bitsPerSample != 16) || payloadSize == 0)
+ return nullptr;
+
+ if (!input->seek(14))
+ return nullptr;
+
+ byte *compressedPayload = (byte *)malloc(payloadSize);
+ if (!compressedPayload)
+ return nullptr;
+ if (input->read(compressedPayload, payloadSize) != payloadSize) {
+ free(compressedPayload);
+ return nullptr;
+ }
+
+ uint32 decodedSize = payloadSize * (bitsPerSample >> 2);
+ byte *decodedPcm = (byte *)malloc(decodedSize);
+ if (!decodedPcm) {
+ free(compressedPayload);
+ return nullptr;
+ }
+
+ decodeHarvesterFcmp(decodedPcm, compressedPayload, payloadSize, bitsPerSample);
+ applyStartupFcmpWarmup(decodedPcm, decodedSize, warmupMode);
+ free(compressedPayload);
+ input.reset();
+ return Audio::makeRawStream(decodedPcm, decodedSize, sampleRate,
+ Audio::FLAG_LITTLE_ENDIAN | ((bitsPerSample == 16) ? Audio::FLAG_16BITS : 0),
+ DisposeAfterUse::YES);
+ }
+ }
+
+ input->seek(0);
+ return Audio::makeWAVStream(input.release(), DisposeAfterUse::YES);
+}
+
+Audio::SeekableAudioStream *openStartupAudioStream(ResourceManager &resources,
+ const Common::String &path, FcmpWarmupMode warmupMode) {
+ return decodeStartupAudioStream(resources.openFile(path), warmupMode);
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/media/audio_decoder.h b/engines/harvester/media/audio_decoder.h
new file mode 100644
index 00000000000..cabd9337771
--- /dev/null
+++ b/engines/harvester/media/audio_decoder.h
@@ -0,0 +1,51 @@
+/* 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_AUDIO_DECODER_H
+#define HARVESTER_AUDIO_DECODER_H
+
+#include "common/str.h"
+
+namespace Audio {
+class SeekableAudioStream;
+}
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace Harvester {
+
+class ResourceManager;
+
+enum class FcmpWarmupMode {
+ kMusic,
+ kSample
+};
+
+Audio::SeekableAudioStream *decodeStartupAudioStream(Common::SeekableReadStream *stream,
+ FcmpWarmupMode warmupMode);
+Audio::SeekableAudioStream *openStartupAudioStream(ResourceManager &resources,
+ const Common::String &path, FcmpWarmupMode warmupMode);
+
+} // End of namespace Harvester
+
+#endif // HARVESTER_AUDIO_DECODER_H
diff --git a/engines/harvester/media_manager.cpp b/engines/harvester/media_manager.cpp
index e7d2b2c8a77..842ac070f64 100644
--- a/engines/harvester/media_manager.cpp
+++ b/engines/harvester/media_manager.cpp
@@ -20,12 +20,10 @@
*/
#include "harvester/media_manager.h"
+#include "harvester/media/audio_decoder.h"
#include "audio/audiostream.h"
-#include "audio/decoders/raw.h"
-#include "audio/decoders/wave.h"
#include "common/algorithm.h"
-#include "common/endian.h"
#include "common/memstream.h"
#include "common/system.h"
#include "graphics/screen.h"
@@ -39,28 +37,6 @@ namespace Harvester {
namespace {
-enum class FcmpWarmupMode {
- kMusic,
- kSample
-};
-
-static const int8 kHarvesterImaIndexAdjustTable[16] = {
- -1, -1, -1, -1, 2, 4, 6, 8,
- -1, -1, -1, -1, 2, 4, 6, 8
-};
-
-static const uint16 kHarvesterImaStepTable[89] = {
- 7, 8, 9, 10, 11, 12, 13, 14, 16, 17,
- 19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
- 50, 55, 60, 66, 73, 80, 88, 97, 107, 118,
- 130, 143, 157, 173, 190, 209, 230, 253, 279, 307,
- 337, 371, 408, 449, 494, 544, 598, 658, 724, 796,
- 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
- 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358,
- 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
- 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
-};
-
static int clampStartupOptionLevel(int level) {
if (level < 0)
return 0;
@@ -74,140 +50,6 @@ static int mapStartupVolumeLevelToMixerVolume(int level) {
return (clampStartupOptionLevel(level) * Audio::Mixer::kMaxMixerVolume + 4) / 9;
}
-static uint32 decodeHarvesterFcmp(byte *dest, const byte *src, uint32 srcSize, uint16 bitsPerSample) {
- const uint32 decodedBytesPerInputByte = bitsPerSample >> 2;
- const uint32 decodedSize = srcSize * decodedBytesPerInputByte;
- const uint32 sampleCount = (bitsPerSample == 16) ? (decodedSize >> 1) : decodedSize;
- int predictor = 0;
- int stepIndex = 0;
- int stepSize = 7;
- byte cachedByte = 0;
- uint32 srcPos = 0;
-
- for (uint32 sample = 0; sample < sampleCount; ++sample) {
- const byte nibble = ((sample & 1) == 0)
- ? ((cachedByte = src[srcPos++]) & 0x0f)
- : ((cachedByte >> 4) & 0x0f);
- int delta = stepSize >> 3;
- if (nibble & 4)
- delta += stepSize;
- if (nibble & 2)
- delta += stepSize >> 1;
- if (nibble & 1)
- delta += stepSize >> 2;
- if (nibble & 8)
- delta = -delta;
-
- predictor += delta;
- if (predictor > 0x7fff)
- predictor = 0x7fff;
- else if (predictor < -0x8000)
- predictor = -0x8000;
-
- if (bitsPerSample == 16) {
- WRITE_LE_INT16(dest, predictor);
- dest += 2;
- } else {
- *dest++ = (byte)(((predictor >> 8) & 0xff) ^ 0x80);
- }
-
- stepIndex += kHarvesterImaIndexAdjustTable[nibble];
- if (stepIndex < 0)
- stepIndex = 0;
- else if (stepIndex > 88)
- stepIndex = 88;
- stepSize = kHarvesterImaStepTable[stepIndex];
- }
-
- return decodedSize;
-}
-
-static void applyStartupFcmpWarmup(byte *decodedPcm, uint32 &decodedSize, FcmpWarmupMode warmupMode) {
- if (!decodedPcm || decodedSize == 0)
- return;
-
- if (warmupMode == FcmpWarmupMode::kMusic) {
- // Native start_music_stream zeros the first 0x64 decoded bytes of the first FCMP chunk.
- memset(decodedPcm, 0, MIN<uint32>(decodedSize, 100));
- return;
- }
-
- // Native load_sound_sample/load_dialogue_voice_sample queue from decoded_pcm + 0x1f4.
- const uint32 trimBytes = MIN<uint32>(decodedSize, 500);
- if (trimBytes == decodedSize) {
- memset(decodedPcm, 0, decodedSize);
- return;
- }
-
- memmove(decodedPcm, decodedPcm + trimBytes, decodedSize - trimBytes);
- decodedSize -= trimBytes;
-}
-
-static Audio::SeekableAudioStream *decodeStartupAudioStream(Common::SeekableReadStream *stream,
- FcmpWarmupMode warmupMode) {
- if (!stream)
- return nullptr;
-
- if (stream->size() >= 14) {
- char magic[4];
- stream->read(magic, sizeof(magic));
- if (!memcmp(magic, "FCMP", sizeof(magic))) {
- const uint32 payloadSizeFromHeader = stream->readUint32LE();
- const uint32 sampleRate = stream->readUint32LE();
- const uint16 bitsPerSample = stream->readUint16LE();
- const uint32 availablePayloadSize = stream->size() - 14;
- const uint32 payloadSize = payloadSizeFromHeader != 0
- ? MIN<uint32>(payloadSizeFromHeader, availablePayloadSize)
- : availablePayloadSize;
-
- if (sampleRate == 0 || (bitsPerSample != 8 && bitsPerSample != 16) || payloadSize == 0) {
- delete stream;
- return nullptr;
- }
-
- if (!stream->seek(14)) {
- delete stream;
- return nullptr;
- }
-
- byte *compressedPayload = (byte *)malloc(payloadSize);
- if (!compressedPayload) {
- delete stream;
- return nullptr;
- }
- if (stream->read(compressedPayload, payloadSize) != payloadSize) {
- free(compressedPayload);
- delete stream;
- return nullptr;
- }
-
- uint32 decodedSize = payloadSize * (bitsPerSample >> 2);
- byte *decodedPcm = (byte *)malloc(decodedSize);
- if (!decodedPcm) {
- free(compressedPayload);
- delete stream;
- return nullptr;
- }
-
- decodeHarvesterFcmp(decodedPcm, compressedPayload, payloadSize, bitsPerSample);
- applyStartupFcmpWarmup(decodedPcm, decodedSize, warmupMode);
- free(compressedPayload);
- delete stream;
- return Audio::makeRawStream(decodedPcm, decodedSize, sampleRate,
- Audio::FLAG_LITTLE_ENDIAN | ((bitsPerSample == 16) ? Audio::FLAG_16BITS : 0),
- DisposeAfterUse::YES);
- }
- }
-
- stream->seek(0);
- return Audio::makeWAVStream(stream, DisposeAfterUse::YES);
-}
-
-static Audio::SeekableAudioStream *openStartupAudioStream(ResourceManager &resources,
- const Common::String &path, FcmpWarmupMode warmupMode) {
- return decodeStartupAudioStream(resources.openFile(path), warmupMode);
-}
-
} // End of anonymous namespace
MediaManager::MediaManager(ResourceManager &resources) : _resources(resources) {
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 39f8009be96..ad054bc3968 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -20,6 +20,7 @@ MODULE_OBJS = \
fst_player.o \
harvester.o \
inventory.o \
+ media/audio_decoder.o \
media_manager.o \
menu/menu.o \
menu/menu_config.o \
Commit: eef97d3eb757b22c116657a0648b0c014f742b1e
https://github.com/scummvm/scummvm/commit/eef97d3eb757b22c116657a0648b0c014f742b1e
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Separate media playback and resource lifecycle
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/media/media_manager.cpp
A engines/harvester/media/media_manager.h
A engines/harvester/media/media_playback.cpp
R engines/harvester/media_manager.cpp
R engines/harvester/media_manager.h
engines/harvester/harvester.cpp
engines/harvester/module.mk
diff --git a/engines/harvester/harvester.cpp b/engines/harvester/harvester.cpp
index 8969ce3153c..408667a9bff 100644
--- a/engines/harvester/harvester.cpp
+++ b/engines/harvester/harvester.cpp
@@ -26,7 +26,7 @@
#include "harvester/console.h"
#include "harvester/detection.h"
#include "harvester/fst_player.h"
-#include "harvester/media_manager.h"
+#include "harvester/media/media_manager.h"
#include "harvester/resources.h"
#include "harvester/flow/flow.h"
#include "harvester/script/script.h"
diff --git a/engines/harvester/media/media_manager.cpp b/engines/harvester/media/media_manager.cpp
new file mode 100644
index 00000000000..f6cc9be5cdb
--- /dev/null
+++ b/engines/harvester/media/media_manager.cpp
@@ -0,0 +1,117 @@
+/* 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/media/media_manager.h"
+
+#include "common/ptr.h"
+#include "common/system.h"
+#include "graphics/screen.h"
+#include "harvester/art.h"
+#include "harvester/resources.h"
+#include "harvester/entity.h"
+#include "harvester/text.h"
+
+namespace Harvester {
+
+namespace {
+
+static int clampStartupOptionLevel(int level) {
+ if (level < 0)
+ return 0;
+ if (level > 9)
+ return 9;
+
+ return level;
+}
+
+static int mapStartupVolumeLevelToMixerVolume(int level) {
+ return (clampStartupOptionLevel(level) * Audio::Mixer::kMaxMixerVolume + 4) / 9;
+}
+
+} // End of anonymous namespace
+
+MediaManager::MediaManager(ResourceManager &resources) : _resources(resources) {
+ _entityManager = new EntityManager(_resources);
+}
+
+MediaManager::~MediaManager() {
+ stopMusic();
+ stopSound();
+ delete _text;
+ delete _art;
+ delete _screen;
+ delete _entityManager;
+}
+
+float MediaManager::mapGammaLevelToBrightnessScale(int level) {
+ return 1.0f + 0.1f * clampStartupOptionLevel(level);
+}
+
+void MediaManager::resetScreen(int width, int height) {
+ delete _screen;
+ _screen = new Graphics::Screen();
+ _displayWidth = width;
+ _displayHeight = height;
+}
+
+bool MediaManager::loadArt() {
+ Common::ScopedPtr<Art> art(new Art());
+ if (!art->load(_resources))
+ return false;
+
+ delete _art;
+ _art = art.release();
+ return true;
+}
+
+bool MediaManager::loadText() {
+ Common::ScopedPtr<Text> text(new Text());
+ if (!text->load(_resources))
+ return false;
+
+ delete _text;
+ _text = text.release();
+ return true;
+}
+
+bool MediaManager::loadQuickTipsResources(bool useTextboxPanel) {
+ return _art && _art->loadQuickTipsResources(_resources, useTextboxPanel);
+}
+
+void MediaManager::drawWaitFrame() const {
+ if (_art && _screen)
+ _art->drawWaitFrame(*_screen);
+}
+
+void MediaManager::applyMixerLevels(int fxLevel, int musicLevel) {
+ if (!g_system || !g_system->getMixer())
+ return;
+
+ g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kSFXSoundType,
+ mapStartupVolumeLevelToMixerVolume(fxLevel));
+ g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kMusicSoundType,
+ mapStartupVolumeLevelToMixerVolume(musicLevel));
+
+ if (g_system->getMixer()->isSoundHandleActive(_musicHandle))
+ g_system->getMixer()->pauseHandle(_musicHandle, musicLevel == 0);
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/media_manager.h b/engines/harvester/media/media_manager.h
similarity index 100%
rename from engines/harvester/media_manager.h
rename to engines/harvester/media/media_manager.h
diff --git a/engines/harvester/media_manager.cpp b/engines/harvester/media/media_playback.cpp
similarity index 77%
rename from engines/harvester/media_manager.cpp
rename to engines/harvester/media/media_playback.cpp
index 842ac070f64..3e6489a11c4 100644
--- a/engines/harvester/media_manager.cpp
+++ b/engines/harvester/media/media_playback.cpp
@@ -19,109 +19,18 @@
*
*/
-#include "harvester/media_manager.h"
+#include "harvester/media/media_manager.h"
#include "harvester/media/audio_decoder.h"
#include "audio/audiostream.h"
#include "common/algorithm.h"
#include "common/memstream.h"
#include "common/system.h"
-#include "graphics/screen.h"
-#include "harvester/art.h"
#include "harvester/resources.h"
-#include "harvester/entity.h"
#include "harvester/script/script.h"
-#include "harvester/text.h"
namespace Harvester {
-namespace {
-
-static int clampStartupOptionLevel(int level) {
- if (level < 0)
- return 0;
- if (level > 9)
- return 9;
-
- return level;
-}
-
-static int mapStartupVolumeLevelToMixerVolume(int level) {
- return (clampStartupOptionLevel(level) * Audio::Mixer::kMaxMixerVolume + 4) / 9;
-}
-
-} // End of anonymous namespace
-
-MediaManager::MediaManager(ResourceManager &resources) : _resources(resources) {
- _entityManager = new EntityManager(_resources);
-}
-
-MediaManager::~MediaManager() {
- stopMusic();
- stopSound();
- delete _text;
- delete _art;
- delete _screen;
- delete _entityManager;
-}
-
-float MediaManager::mapGammaLevelToBrightnessScale(int level) {
- return 1.0f + 0.1f * clampStartupOptionLevel(level);
-}
-
-void MediaManager::resetScreen(int width, int height) {
- delete _screen;
- _screen = new Graphics::Screen();
- _displayWidth = width;
- _displayHeight = height;
-}
-
-bool MediaManager::loadArt() {
- Art *art = new Art();
- if (!art->load(_resources)) {
- delete art;
- return false;
- }
-
- delete _art;
- _art = art;
- return true;
-}
-
-bool MediaManager::loadText() {
- Text *text = new Text();
- if (!text->load(_resources)) {
- delete text;
- return false;
- }
-
- delete _text;
- _text = text;
- return true;
-}
-
-bool MediaManager::loadQuickTipsResources(bool useTextboxPanel) {
- return _art && _art->loadQuickTipsResources(_resources, useTextboxPanel);
-}
-
-void MediaManager::drawWaitFrame() const {
- if (_art && _screen)
- _art->drawWaitFrame(*_screen);
-}
-
-void MediaManager::applyMixerLevels(int fxLevel, int musicLevel) {
- if (!g_system || !g_system->getMixer())
- return;
-
- g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kSFXSoundType,
- mapStartupVolumeLevelToMixerVolume(fxLevel));
- g_system->getMixer()->setVolumeForSoundType(Audio::Mixer::kMusicSoundType,
- mapStartupVolumeLevelToMixerVolume(musicLevel));
-
- if (g_system->getMixer()->isSoundHandleActive(_musicHandle))
- g_system->getMixer()->pauseHandle(_musicHandle, musicLevel == 0);
-}
-
bool MediaManager::isMusicPlaying() const {
return g_system && g_system->getMixer() &&
g_system->getMixer()->isSoundHandleActive(_musicHandle);
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index ad054bc3968..b3cf08abc52 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -21,7 +21,8 @@ MODULE_OBJS = \
harvester.o \
inventory.o \
media/audio_decoder.o \
- media_manager.o \
+ media/media_manager.o \
+ media/media_playback.o \
menu/menu.o \
menu/menu_config.o \
menu/menu_navigation.o \
Commit: 0e7664836f815e5d8af661749e24285820d93046
https://github.com/scummvm/scummvm/commit/0e7664836f815e5d8af661749e24285820d93046
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Separate interaction interruption and terminal outcomes
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/scene/interaction_outcomes.cpp
engines/harvester/module.mk
engines/harvester/scene/interaction.cpp
engines/harvester/scene/interaction.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index b3cf08abc52..0edac6ad7f7 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -50,6 +50,7 @@ MODULE_OBJS = \
combat/combat_reactions.o \
combat/combat_targeting.o \
scene/interaction.o \
+ scene/interaction_outcomes.o \
scene/input.o \
scene/room_presentation.o \
scene/scene_entities.o \
diff --git a/engines/harvester/scene/interaction.cpp b/engines/harvester/scene/interaction.cpp
index 37156a5d23f..6d6cac06bac 100644
--- a/engines/harvester/scene/interaction.cpp
+++ b/engines/harvester/scene/interaction.cpp
@@ -468,65 +468,17 @@ int Interaction::getPendingRegionFacing() const {
return pendingRegion && pendingRegion->startEnabled ? pendingRegion->desiredFacing : -1;
}
-bool Interaction::hasPendingMainMenuReturn() const {
- return _flow.hasPendingMainMenuReturn();
-}
-
-Common::Error Interaction::handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw) {
- bool didTransition = false;
- Common::Error interactionError =
- handleInteractionResult(interaction, didTransition, Common::String());
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (!_flow.hasPendingMainMenuReturn() &&
- _engine.getScript() &&
- _engine.getScript()->getPlayerCurrentHitPoints() <= 0) {
- _callbacks.stopPlayerRegionInteraction();
- _callbacks.startPlayerDefeatSequence("combat_interaction", Common::String(), 1);
- }
-
- needsRedraw = true;
- return Common::kNoError;
-}
-
Common::Error Interaction::handleInteractionResult(const InteractionResult &interaction,
bool &didTransition, const Common::String &usedItemName, bool interruptPlayerMovement) {
const RoomPlayerState &playerState = _player.getState();
didTransition = false;
- if (interruptPlayerMovement) {
- _player.cancelMovement();
- _pendingRegionName.clear();
- } else if (playerState.hasMoveTarget || playerState.turnActive) {
- debugC(2, kDebugPlayer,
- "Harvester: background action preserving player movement room='%s' pos=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f) region='%s' turning=%d",
- _scene.state.roomName.c_str(), playerState.centerX, playerState.bottomY,
- playerState.z, playerState.targetX, playerState.targetBottomY,
- playerState.targetZ, _pendingRegionName.c_str(), playerState.turnActive);
- }
- if (interaction.requestDemoEnding) {
- Common::Error endingError = _flow.runDemoEnding();
- if (endingError.getCode() != Common::kNoError)
- return endingError;
- didTransition = true;
- return Common::kNoError;
- }
+ preparePlayerForInteraction(playerState, interruptPlayerMovement);
+ if (interaction.requestDemoEnding)
+ return handleDemoEnding(didTransition);
- if (interaction.requestMainMenu) {
- _engine.stopMusic();
- _engine.stopSound();
- if (!interaction.deathFlicPath.empty()) {
- FstPlayer fstPlayer(_engine);
- if (!fstPlayer.play(interaction.deathFlicPath))
- return Common::kReadingFailed;
- }
-
- if (!interaction.deathFlicPath.empty())
- _flow.requestGameOverReturn();
- else
- _flow.requestMainMenuReturn();
- return Common::kNoError;
- }
+ if (interaction.requestMainMenu)
+ return handleMainMenuReturn(interaction.deathFlicPath);
Common::String restoreMusicPath = _engine.getMusicPath();
bool discChanged = false;
@@ -680,13 +632,7 @@ Common::Error Interaction::handleInteractionResult(const InteractionResult &inte
if (didTransition)
return Common::kNoError;
- if (interaction.requestPlayerDeath &&
- _engine.getScript() &&
- _engine.getScript()->getPlayerCurrentHitPoints() <= 0) {
- _callbacks.stopPlayerRegionInteraction();
- _callbacks.startPlayerDefeatSequence("script_kill_pc", Common::String(),
- interaction.playerDeathDamageType);
- }
+ applyPlayerDeathRequest(interaction);
if (interaction.requestPlayerGotoXZ)
_callbacks.applyPlayerGotoXZ(interaction.playerGotoX, interaction.playerGotoZ);
diff --git a/engines/harvester/scene/interaction.h b/engines/harvester/scene/interaction.h
index 16c1b125500..19f2e18e0db 100644
--- a/engines/harvester/scene/interaction.h
+++ b/engines/harvester/scene/interaction.h
@@ -36,6 +36,7 @@ class Interaction;
class SceneEntities;
class Player;
struct IndexedBitmap;
+struct RoomPlayerState;
extern const char *const kExitCloseupPendingRoomChange;
@@ -123,6 +124,11 @@ public:
const Common::String &continuationTag, bool &didTransition);
private:
+ void preparePlayerForInteraction(const RoomPlayerState &playerState, bool interruptPlayerMovement);
+ Common::Error handleDemoEnding(bool &didTransition);
+ Common::Error handleMainMenuReturn(const Common::String &deathFlicPath);
+ void applyPlayerDeathRequest(const InteractionResult &interaction);
+
HarvesterEngine &_engine;
Flow &_flow;
SceneState &_scene;
diff --git a/engines/harvester/scene/interaction_outcomes.cpp b/engines/harvester/scene/interaction_outcomes.cpp
new file mode 100644
index 00000000000..45c03f1a59b
--- /dev/null
+++ b/engines/harvester/scene/interaction_outcomes.cpp
@@ -0,0 +1,102 @@
+/* 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/scene/interaction.h"
+
+#include "common/debug.h"
+#include "harvester/detection.h"
+#include "harvester/flow/flow.h"
+#include "harvester/fst_player.h"
+#include "harvester/harvester.h"
+#include "harvester/player/player.h"
+
+namespace Harvester {
+
+void Interaction::preparePlayerForInteraction(const RoomPlayerState &playerState,
+ bool interruptPlayerMovement) {
+ if (interruptPlayerMovement) {
+ _player.cancelMovement();
+ _pendingRegionName.clear();
+ } else if (playerState.hasMoveTarget || playerState.turnActive) {
+ debugC(2, kDebugPlayer,
+ "Harvester: background action preserving player movement room='%s' pos=(%d,%d,z=%.2f) target=(%d,%d,z=%.2f) region='%s' turning=%d",
+ _scene.state.roomName.c_str(), playerState.centerX, playerState.bottomY,
+ playerState.z, playerState.targetX, playerState.targetBottomY,
+ playerState.targetZ, _pendingRegionName.c_str(), playerState.turnActive);
+ }
+}
+
+Common::Error Interaction::handleDemoEnding(bool &didTransition) {
+ Common::Error endingError = _flow.runDemoEnding();
+ if (endingError.getCode() != Common::kNoError)
+ return endingError;
+ didTransition = true;
+ return Common::kNoError;
+}
+
+Common::Error Interaction::handleMainMenuReturn(const Common::String &deathFlicPath) {
+ _engine.stopMusic();
+ _engine.stopSound();
+ if (!deathFlicPath.empty()) {
+ FstPlayer fstPlayer(_engine);
+ if (!fstPlayer.play(deathFlicPath))
+ return Common::kReadingFailed;
+ }
+
+ if (!deathFlicPath.empty())
+ _flow.requestGameOverReturn();
+ else
+ _flow.requestMainMenuReturn();
+ return Common::kNoError;
+}
+
+void Interaction::applyPlayerDeathRequest(const InteractionResult &interaction) {
+ if (interaction.requestPlayerDeath &&
+ _engine.getScript() &&
+ _engine.getScript()->getPlayerCurrentHitPoints() <= 0) {
+ _callbacks.stopPlayerRegionInteraction();
+ _callbacks.startPlayerDefeatSequence("script_kill_pc", Common::String(),
+ interaction.playerDeathDamageType);
+ }
+}
+
+bool Interaction::hasPendingMainMenuReturn() const {
+ return _flow.hasPendingMainMenuReturn();
+}
+
+Common::Error Interaction::handleCombatInteraction(const InteractionResult &interaction, bool &needsRedraw) {
+ bool didTransition = false;
+ Common::Error interactionError =
+ handleInteractionResult(interaction, didTransition, Common::String());
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ if (!_flow.hasPendingMainMenuReturn() &&
+ _engine.getScript() &&
+ _engine.getScript()->getPlayerCurrentHitPoints() <= 0) {
+ _callbacks.stopPlayerRegionInteraction();
+ _callbacks.startPlayerDefeatSequence("combat_interaction", Common::String(), 1);
+ }
+
+ needsRedraw = true;
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
Commit: 36999a576f4c59ee7b72b722f7ae66efd314bcfa
https://github.com/scummvm/scummvm/commit/36999a576f4c59ee7b72b722f7ae66efd314bcfa
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Separate interaction transitions and restarts
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/scene/interaction_transitions.cpp
engines/harvester/module.mk
engines/harvester/scene/interaction.cpp
engines/harvester/scene/interaction.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 0edac6ad7f7..603d3f67afa 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -51,6 +51,7 @@ MODULE_OBJS = \
combat/combat_targeting.o \
scene/interaction.o \
scene/interaction_outcomes.o \
+ scene/interaction_transitions.o \
scene/input.o \
scene/room_presentation.o \
scene/scene_entities.o \
diff --git a/engines/harvester/scene/interaction.cpp b/engines/harvester/scene/interaction.cpp
index 6d6cac06bac..989a3b73b83 100644
--- a/engines/harvester/scene/interaction.cpp
+++ b/engines/harvester/scene/interaction.cpp
@@ -499,63 +499,10 @@ Common::Error Interaction::handleInteractionResult(const InteractionResult &inte
if (interaction.mutatedRuntimeState)
_callbacks.syncGlobalTimerEntities(interaction.previousTimerRecords);
- auto queueImplicitRoomRestart = [&]() -> Common::Error {
- _pendingRoomChangeIsRoomName = false;
- _pendingRoomChangeUsesSavedRoomState = false;
- _pendingRoomChangeSavedRoomState.clear();
- if (!interaction.roomRestartTargetName.empty()) {
- _pendingRoomChange = interaction.roomRestartTargetName;
- } else if (discChanged && interaction.cdChangeDisc == 3) {
- // Native disc-3 prompt reloads re-enter room_setup through the startup target
- // buffer, which remains "START" during ordinary play and resolves to CD3 RECEPTION.
- _pendingRoomChange = "START";
- } else if (!_scene.state.entranceName.empty()) {
- _pendingRoomChange = _scene.state.entranceName;
- } else {
- _pendingRoomChange = _scene.state.roomName;
- _pendingRoomChangeIsRoomName = true;
- }
- didTransition = !_pendingRoomChange.empty();
- debugC(1, kDebugRoom,
- "Harvester: implicit room restart target='%s' explicit='%s' discChanged=%d cdChangeDisc=%d sceneEntrance='%s' sceneRoom='%s'",
- _pendingRoomChange.c_str(), interaction.roomRestartTargetName.c_str(),
- discChanged ? 1 : 0, interaction.cdChangeDisc,
- _scene.state.entranceName.c_str(), _scene.state.roomName.c_str());
- return didTransition ? Common::kNoError : Common::kReadingFailed;
- };
- auto queueSavedRoomRestart = [&](const SaveRoomState &savedState) -> Common::Error {
- _pendingRoomChange.clear();
- _pendingRoomChangeIsRoomName = false;
- _pendingRoomChangeUsesSavedRoomState = false;
- _pendingRoomChangeSavedRoomState.clear();
- _pendingRoomChange = !savedState.entranceName.empty()
- ? savedState.entranceName
- : savedState.roomName;
- _pendingRoomChangeIsRoomName = savedState.entranceName.empty();
- _pendingRoomChangeUsesSavedRoomState = !_pendingRoomChange.empty();
- _pendingRoomChangeSavedRoomState = savedState;
- debugC(1, kDebugRoom,
- "Harvester: queued saved room restart target='%s' room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d music='%s'",
- _pendingRoomChange.c_str(), savedState.roomName.c_str(),
- savedState.entranceName.c_str(), savedState.playerX, savedState.playerY,
- savedState.playerZ, savedState.playerFacing, savedState.musicPath.c_str());
- didTransition = !_pendingRoomChange.empty();
- return didTransition ? Common::kNoError : Common::kReadingFailed;
- };
if (interaction.requestRoomRestart)
- return queueImplicitRoomRestart();
- if (interaction.requestCloseupExit) {
- if (!_canExitCloseupToParent) {
- debugC(1, kDebugRoom,
- "Harvester: EXIT_CLOSEUP ignored for room='%s' without parent room loop",
- _scene.state.roomName.c_str());
- return Common::kNoError;
- }
-
- _pendingRoomChange = kExitCloseupPendingRoomChange;
- didTransition = true;
- return Common::kNoError;
- }
+ return queueImplicitRoomRestart(interaction, discChanged, didTransition);
+ if (interaction.requestCloseupExit)
+ return queueCloseupExit(didTransition);
StartupRoomTransitionKind roomTransition = interaction.roomTransition;
if (roomTransition == kStartupRoomTransitionNone && !interaction.nextRoomName.empty())
@@ -568,60 +515,13 @@ Common::Error Interaction::handleInteractionResult(const InteractionResult &inte
interaction.nextRoomName.empty() &&
!interaction.mutatedRuntimeState;
if (needsTownScriptDiscReload)
- return queueImplicitRoomRestart();
+ return queueImplicitRoomRestart(interaction, discChanged, didTransition);
if (!interaction.nextRoomName.empty() &&
roomTransition == kStartupRoomTransitionChangeRoom) {
- Common::String resolvedTransitionTarget = interaction.nextRoomName;
- Common::Error resolveError = _flow.resolveRoomTransitionTarget(
- interaction.nextRoomName, resolvedTransitionTarget);
- if (resolveError.getCode() != Common::kNoError)
- return resolveError;
- if (resolvedTransitionTarget.empty())
- return Common::kNoError;
-
- Common::Error exitError = _callbacks.runRoomExitCommands();
- if (exitError.getCode() != Common::kNoError)
- return exitError;
-
- // Native CHANGE_ROOM queues a room handoff for the live loop instead of nesting.
- _pendingRoomChange = Common::move(resolvedTransitionTarget);
- didTransition = true;
+ return queueRoomChange(interaction.nextRoomName, didTransition);
} else if (!interaction.nextRoomName.empty()) {
- Common::Error exitError = _callbacks.runRoomExitCommands();
- if (exitError.getCode() != Common::kNoError)
- return exitError;
-
- SaveRoomState parentRoomState;
- const bool hasParentRoomState = _engine.hasCurrentSaveRoomState();
- if (hasParentRoomState)
- parentRoomState = _engine.getCurrentSaveRoomState();
- Common::Error roomError = _flow.runRoomLoop(interaction.nextRoomName);
- if (_flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- if (_flow.takePendingCloseupParentRestart()) {
- if (hasParentRoomState && parentRoomState.valid)
- return queueSavedRoomRestart(parentRoomState);
- return queueImplicitRoomRestart();
- }
- if (_flow.hasPendingDebugRoomChange()) {
- didTransition = true;
- return Common::kNoError;
- }
- if (roomError.getCode() != Common::kReadingFailed &&
- roomError.getCode() != Common::kNoError) {
- return roomError;
- }
-
- if (!_callbacks.refreshCurrentScene(true))
- return Common::kReadingFailed;
-
- _flow.executeStartupAudioCommands(_scene.state.audioCommands);
- if (!restoreMusicPath.empty())
- (void)_engine.playMusic(restoreMusicPath);
- else
- _engine.stopMusic();
- didTransition = true;
+ return runCloseupInteraction(interaction, discChanged, restoreMusicPath, didTransition);
} else if (interaction.visualRuntimeStateChanged) {
if (!_callbacks.applyCurrentRoomRuntimeMutationsInPlace(true) &&
!_callbacks.refreshCurrentScene(true)) {
diff --git a/engines/harvester/scene/interaction.h b/engines/harvester/scene/interaction.h
index 19f2e18e0db..fa011d3edcc 100644
--- a/engines/harvester/scene/interaction.h
+++ b/engines/harvester/scene/interaction.h
@@ -128,6 +128,13 @@ private:
Common::Error handleDemoEnding(bool &didTransition);
Common::Error handleMainMenuReturn(const Common::String &deathFlicPath);
void applyPlayerDeathRequest(const InteractionResult &interaction);
+ Common::Error queueImplicitRoomRestart(const InteractionResult &interaction,
+ bool discChanged, bool &didTransition);
+ Common::Error queueSavedRoomRestart(const SaveRoomState &savedState, bool &didTransition);
+ Common::Error queueRoomChange(const Common::String &nextRoomName, bool &didTransition);
+ Common::Error runCloseupInteraction(const InteractionResult &interaction, bool discChanged,
+ const Common::String &restoreMusicPath, bool &didTransition);
+ Common::Error queueCloseupExit(bool &didTransition);
HarvesterEngine &_engine;
Flow &_flow;
diff --git a/engines/harvester/scene/interaction_transitions.cpp b/engines/harvester/scene/interaction_transitions.cpp
new file mode 100644
index 00000000000..29d8d7a925c
--- /dev/null
+++ b/engines/harvester/scene/interaction_transitions.cpp
@@ -0,0 +1,150 @@
+/* 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/scene/interaction.h"
+
+#include "common/algorithm.h"
+#include "common/debug.h"
+#include "harvester/detection.h"
+#include "harvester/flow/flow.h"
+#include "harvester/harvester.h"
+
+namespace Harvester {
+
+Common::Error Interaction::queueImplicitRoomRestart(const InteractionResult &interaction,
+ bool discChanged, bool &didTransition) {
+ _pendingRoomChangeIsRoomName = false;
+ _pendingRoomChangeUsesSavedRoomState = false;
+ _pendingRoomChangeSavedRoomState.clear();
+ if (!interaction.roomRestartTargetName.empty()) {
+ _pendingRoomChange = interaction.roomRestartTargetName;
+ } else if (discChanged && interaction.cdChangeDisc == 3) {
+ // Disc-3 prompt reloads re-enter room_setup through the startup target
+ // buffer, which remains "START" during ordinary play and resolves to CD3 RECEPTION.
+ _pendingRoomChange = "START";
+ } else if (!_scene.state.entranceName.empty()) {
+ _pendingRoomChange = _scene.state.entranceName;
+ } else {
+ _pendingRoomChange = _scene.state.roomName;
+ _pendingRoomChangeIsRoomName = true;
+ }
+ didTransition = !_pendingRoomChange.empty();
+ debugC(1, kDebugRoom,
+ "Harvester: implicit room restart target='%s' explicit='%s' discChanged=%d cdChangeDisc=%d sceneEntrance='%s' sceneRoom='%s'",
+ _pendingRoomChange.c_str(), interaction.roomRestartTargetName.c_str(),
+ discChanged ? 1 : 0, interaction.cdChangeDisc,
+ _scene.state.entranceName.c_str(), _scene.state.roomName.c_str());
+ return didTransition ? Common::kNoError : Common::kReadingFailed;
+}
+
+Common::Error Interaction::queueSavedRoomRestart(const SaveRoomState &savedState, bool &didTransition) {
+ _pendingRoomChange.clear();
+ _pendingRoomChangeIsRoomName = false;
+ _pendingRoomChangeUsesSavedRoomState = false;
+ _pendingRoomChangeSavedRoomState.clear();
+ _pendingRoomChange = !savedState.entranceName.empty()
+ ? savedState.entranceName
+ : savedState.roomName;
+ _pendingRoomChangeIsRoomName = savedState.entranceName.empty();
+ _pendingRoomChangeUsesSavedRoomState = !_pendingRoomChange.empty();
+ _pendingRoomChangeSavedRoomState = savedState;
+ debugC(1, kDebugRoom,
+ "Harvester: queued saved room restart target='%s' room='%s' entrance='%s' spawn=(%d,%d,%d) facing=%d music='%s'",
+ _pendingRoomChange.c_str(), savedState.roomName.c_str(),
+ savedState.entranceName.c_str(), savedState.playerX, savedState.playerY,
+ savedState.playerZ, savedState.playerFacing, savedState.musicPath.c_str());
+ didTransition = !_pendingRoomChange.empty();
+ return didTransition ? Common::kNoError : Common::kReadingFailed;
+}
+
+Common::Error Interaction::queueRoomChange(const Common::String &nextRoomName,
+ bool &didTransition) {
+ Common::String resolvedTransitionTarget = nextRoomName;
+ Common::Error resolveError = _flow.resolveRoomTransitionTarget(
+ nextRoomName, resolvedTransitionTarget);
+ if (resolveError.getCode() != Common::kNoError)
+ return resolveError;
+ if (resolvedTransitionTarget.empty())
+ return Common::kNoError;
+
+ Common::Error exitError = _callbacks.runRoomExitCommands();
+ if (exitError.getCode() != Common::kNoError)
+ return exitError;
+
+ // CHANGE_ROOM queues a room handoff for the live loop instead of nesting.
+ _pendingRoomChange = Common::move(resolvedTransitionTarget);
+ didTransition = true;
+ return Common::kNoError;
+}
+
+Common::Error Interaction::runCloseupInteraction(const InteractionResult &interaction,
+ bool discChanged, const Common::String &restoreMusicPath, bool &didTransition) {
+ Common::Error exitError = _callbacks.runRoomExitCommands();
+ if (exitError.getCode() != Common::kNoError)
+ return exitError;
+
+ SaveRoomState parentRoomState;
+ const bool hasParentRoomState = _engine.hasCurrentSaveRoomState();
+ if (hasParentRoomState)
+ parentRoomState = _engine.getCurrentSaveRoomState();
+ Common::Error roomError = _flow.runRoomLoop(interaction.nextRoomName);
+ if (_flow.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ if (_flow.takePendingCloseupParentRestart()) {
+ if (hasParentRoomState && parentRoomState.valid)
+ return queueSavedRoomRestart(parentRoomState, didTransition);
+ return queueImplicitRoomRestart(interaction, discChanged, didTransition);
+ }
+ if (_flow.hasPendingDebugRoomChange()) {
+ didTransition = true;
+ return Common::kNoError;
+ }
+ if (roomError.getCode() != Common::kReadingFailed &&
+ roomError.getCode() != Common::kNoError) {
+ return roomError;
+ }
+
+ if (!_callbacks.refreshCurrentScene(true))
+ return Common::kReadingFailed;
+
+ _flow.executeStartupAudioCommands(_scene.state.audioCommands);
+ if (!restoreMusicPath.empty())
+ (void)_engine.playMusic(restoreMusicPath);
+ else
+ _engine.stopMusic();
+ didTransition = true;
+ return Common::kNoError;
+}
+
+Common::Error Interaction::queueCloseupExit(bool &didTransition) {
+ if (!_canExitCloseupToParent) {
+ debugC(1, kDebugRoom,
+ "Harvester: EXIT_CLOSEUP ignored for room='%s' without parent room loop",
+ _scene.state.roomName.c_str());
+ return Common::kNoError;
+ }
+
+ _pendingRoomChange = kExitCloseupPendingRoomChange;
+ didTransition = true;
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
Commit: dba8b8b2fd87e06b0b97fd6f038827555c505e48
https://github.com/scummvm/scummvm/commit/dba8b8b2fd87e06b0b97fd6f038827555c505e48
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Separate same-scene effects and continuations
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/scene/interaction_effects.cpp
engines/harvester/module.mk
engines/harvester/scene/interaction.cpp
engines/harvester/scene/interaction.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 603d3f67afa..19bff864123 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -50,6 +50,7 @@ MODULE_OBJS = \
combat/combat_reactions.o \
combat/combat_targeting.o \
scene/interaction.o \
+ scene/interaction_effects.o \
scene/interaction_outcomes.o \
scene/interaction_transitions.o \
scene/input.o \
diff --git a/engines/harvester/scene/interaction.cpp b/engines/harvester/scene/interaction.cpp
index 989a3b73b83..16bc9514bc7 100644
--- a/engines/harvester/scene/interaction.cpp
+++ b/engines/harvester/scene/interaction.cpp
@@ -27,7 +27,6 @@
#include "harvester/art.h"
#include "harvester/detection.h"
#include "harvester/flow/flow.h"
-#include "harvester/fst_player.h"
#include "harvester/harvester.h"
#include "harvester/inventory.h"
#include "harvester/player/player.h"
@@ -532,118 +531,8 @@ Common::Error Interaction::handleInteractionResult(const InteractionResult &inte
if (didTransition)
return Common::kNoError;
- applyPlayerDeathRequest(interaction);
-
- if (interaction.requestPlayerGotoXZ)
- _callbacks.applyPlayerGotoXZ(interaction.playerGotoX, interaction.playerGotoZ);
- else if (interaction.requestPlayerGotoZ)
- _callbacks.applyPlayerGotoXZ(playerState.centerX, interaction.playerGotoZ);
-
- if (!interaction.moveEntityToPlayerZName.empty()) {
- EntityManager *entityManager = _engine.getRuntimeEntities();
- Entity *entity = entityManager
- ? entityManager->findSceneEntityByName(interaction.moveEntityToPlayerZName)
- : nullptr;
- if (entity && playerState.entity) {
- entity->setPosition(entity->getX(), entity->getY(), playerState.entity->getZ());
- entityManager->reinsertSceneEntity(entity);
- debugC(1, kDebugPlayer, "Harvester: MOVE_BM2PCZ entity='%s' z=%.2f",
- interaction.moveEntityToPlayerZName.c_str(), entity->getZ());
- }
- }
-
- if (interaction.lightingCommand != kStartupLightingCommandNone) {
- Common::Error lightingError = _callbacks.applyLightingCommand(interaction.lightingCommand);
- if (lightingError.getCode() != Common::kNoError)
- return lightingError;
- }
-
- if (!interaction.cutscenePath.empty()) {
- FstPlayer fstPlayer(_engine);
- if (!fstPlayer.play(interaction.cutscenePath))
- return Common::kReadingFailed;
- }
-
- if (!interaction.modalText.value.empty()) {
- Common::Error modalError = _callbacks.runModalShowText(interaction.modalText);
- if (modalError.getCode() != Common::kNoError)
- return modalError;
- }
-
- if (!interaction.dialogueNpcName.empty()) {
- Common::Error dialogueError = runScriptedDialogue(
- interaction.dialogueNpcName, usedItemName, interaction.dialogueContinuationTag,
- didTransition);
- if (dialogueError.getCode() != Common::kNoError)
- return dialogueError;
- }
-
- if (didTransition)
- return Common::kNoError;
-
- if (!interaction.continuationTag.empty()) {
- InteractionResult continuationInteraction;
- if (_engine.getScript()->executeActionTag(
- interaction.continuationTag, continuationInteraction, true,
- _scene.executionContext())) {
- Common::Error interactionError =
- handleInteractionResult(continuationInteraction, didTransition, usedItemName, interruptPlayerMovement);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- }
- }
-
- return Common::kNoError;
-}
-
-Common::Error Interaction::runScriptedDialogue(const Common::String &npcName,
- const Common::String &usedItemName, const Common::String &continuationTag,
- bool &didTransition) {
- didTransition = false;
-
- const NpcRecord *dialogueNpc = _engine.getScript()->findRuntimeNpcRecord(npcName);
- if (dialogueNpc) {
- IndexedBitmap dialogueBackdrop;
- if (!_callbacks.captureDialogueBackdrop(dialogueBackdrop))
- return Common::kReadingFailed;
-
- Common::Error dialogueError = _flow.runRoomNpcDialogue(
- dialogueBackdrop, _scene.palette, _scene.targetPaletteBrightness,
- _scene.executionContext(), *dialogueNpc, usedItemName);
- if (dialogueError.getCode() != Common::kNoError)
- return dialogueError;
- } else {
- warning("Harvester: unresolved startup dialogue npc '%s' while processing room dialogue",
- npcName.c_str());
- }
-
- InteractionResult dialogueInteraction;
- bool abortRemainingCommandChain = false;
- if (_flow.takeQueuedDialogueInteraction(dialogueInteraction)) {
- abortRemainingCommandChain = dialogueInteraction.abortRemainingCommandChain;
- Common::Error interactionError =
- handleInteractionResult(dialogueInteraction, didTransition, usedItemName);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (_flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- }
- if (!didTransition && !abortRemainingCommandChain && !continuationTag.empty()) {
- InteractionResult continuationInteraction;
- if (_engine.getScript()->executeActionTag(
- continuationTag, continuationInteraction, true, _scene.executionContext())) {
- Common::Error interactionError =
- handleInteractionResult(continuationInteraction, didTransition, usedItemName);
- if (interactionError.getCode() != Common::kNoError)
- return interactionError;
- if (_flow.hasPendingMainMenuReturn())
- return Common::kNoError;
- }
- }
-
- _flow.resetCursorAnimationSequence();
- _callbacks.resetIdleState();
- return Common::kNoError;
+ return applySameSceneEffects(interaction, playerState, didTransition, usedItemName,
+ interruptPlayerMovement);
}
bool hasRoomEntryInteraction(const InteractionResult &interaction) {
diff --git a/engines/harvester/scene/interaction.h b/engines/harvester/scene/interaction.h
index fa011d3edcc..33f63b30c53 100644
--- a/engines/harvester/scene/interaction.h
+++ b/engines/harvester/scene/interaction.h
@@ -135,6 +135,9 @@ private:
Common::Error runCloseupInteraction(const InteractionResult &interaction, bool discChanged,
const Common::String &restoreMusicPath, bool &didTransition);
Common::Error queueCloseupExit(bool &didTransition);
+ Common::Error applySameSceneEffects(const InteractionResult &interaction,
+ const RoomPlayerState &playerState, bool &didTransition, const Common::String &usedItemName,
+ bool interruptPlayerMovement);
HarvesterEngine &_engine;
Flow &_flow;
diff --git a/engines/harvester/scene/interaction_effects.cpp b/engines/harvester/scene/interaction_effects.cpp
new file mode 100644
index 00000000000..7c564e4099a
--- /dev/null
+++ b/engines/harvester/scene/interaction_effects.cpp
@@ -0,0 +1,152 @@
+/* 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/scene/interaction.h"
+
+#include "common/debug.h"
+#include "common/textconsole.h"
+#include "harvester/detection.h"
+#include "harvester/entity.h"
+#include "harvester/flow/flow.h"
+#include "harvester/fst_player.h"
+#include "harvester/harvester.h"
+#include "harvester/player/player.h"
+
+namespace Harvester {
+
+Common::Error Interaction::applySameSceneEffects(const InteractionResult &interaction,
+ const RoomPlayerState &playerState, bool &didTransition, const Common::String &usedItemName,
+ bool interruptPlayerMovement) {
+ applyPlayerDeathRequest(interaction);
+
+ if (interaction.requestPlayerGotoXZ)
+ _callbacks.applyPlayerGotoXZ(interaction.playerGotoX, interaction.playerGotoZ);
+ else if (interaction.requestPlayerGotoZ)
+ _callbacks.applyPlayerGotoXZ(playerState.centerX, interaction.playerGotoZ);
+
+ if (!interaction.moveEntityToPlayerZName.empty()) {
+ EntityManager *entityManager = _engine.getRuntimeEntities();
+ Entity *entity = entityManager
+ ? entityManager->findSceneEntityByName(interaction.moveEntityToPlayerZName)
+ : nullptr;
+ if (entity && playerState.entity) {
+ entity->setPosition(entity->getX(), entity->getY(), playerState.entity->getZ());
+ entityManager->reinsertSceneEntity(entity);
+ debugC(1, kDebugPlayer, "Harvester: MOVE_BM2PCZ entity='%s' z=%.2f",
+ interaction.moveEntityToPlayerZName.c_str(), entity->getZ());
+ }
+ }
+
+ if (interaction.lightingCommand != kStartupLightingCommandNone) {
+ Common::Error lightingError = _callbacks.applyLightingCommand(interaction.lightingCommand);
+ if (lightingError.getCode() != Common::kNoError)
+ return lightingError;
+ }
+
+ if (!interaction.cutscenePath.empty()) {
+ FstPlayer fstPlayer(_engine);
+ if (!fstPlayer.play(interaction.cutscenePath))
+ return Common::kReadingFailed;
+ }
+
+ if (!interaction.modalText.value.empty()) {
+ Common::Error modalError = _callbacks.runModalShowText(interaction.modalText);
+ if (modalError.getCode() != Common::kNoError)
+ return modalError;
+ }
+
+ if (!interaction.dialogueNpcName.empty()) {
+ Common::Error dialogueError = runScriptedDialogue(
+ interaction.dialogueNpcName, usedItemName, interaction.dialogueContinuationTag,
+ didTransition);
+ if (dialogueError.getCode() != Common::kNoError)
+ return dialogueError;
+ }
+
+ if (didTransition)
+ return Common::kNoError;
+
+ if (!interaction.continuationTag.empty()) {
+ InteractionResult continuationInteraction;
+ if (_engine.getScript()->executeActionTag(
+ interaction.continuationTag, continuationInteraction, true,
+ _scene.executionContext())) {
+ Common::Error interactionError =
+ handleInteractionResult(continuationInteraction, didTransition, usedItemName, interruptPlayerMovement);
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ }
+ }
+
+ return Common::kNoError;
+}
+
+Common::Error Interaction::runScriptedDialogue(const Common::String &npcName,
+ const Common::String &usedItemName, const Common::String &continuationTag,
+ bool &didTransition) {
+ didTransition = false;
+
+ const NpcRecord *dialogueNpc = _engine.getScript()->findRuntimeNpcRecord(npcName);
+ if (dialogueNpc) {
+ IndexedBitmap dialogueBackdrop;
+ if (!_callbacks.captureDialogueBackdrop(dialogueBackdrop))
+ return Common::kReadingFailed;
+
+ Common::Error dialogueError = _flow.runRoomNpcDialogue(
+ dialogueBackdrop, _scene.palette, _scene.targetPaletteBrightness,
+ _scene.executionContext(), *dialogueNpc, usedItemName);
+ if (dialogueError.getCode() != Common::kNoError)
+ return dialogueError;
+ } else {
+ warning("Harvester: unresolved startup dialogue npc '%s' while processing room dialogue",
+ npcName.c_str());
+ }
+
+ InteractionResult dialogueInteraction;
+ bool abortRemainingCommandChain = false;
+ if (_flow.takeQueuedDialogueInteraction(dialogueInteraction)) {
+ abortRemainingCommandChain = dialogueInteraction.abortRemainingCommandChain;
+ Common::Error interactionError =
+ handleInteractionResult(dialogueInteraction, didTransition, usedItemName);
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ if (_flow.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ }
+ if (!didTransition && !abortRemainingCommandChain && !continuationTag.empty()) {
+ InteractionResult continuationInteraction;
+ if (_engine.getScript()->executeActionTag(
+ continuationTag, continuationInteraction, true, _scene.executionContext())) {
+ Common::Error interactionError =
+ handleInteractionResult(continuationInteraction, didTransition, usedItemName);
+ if (interactionError.getCode() != Common::kNoError)
+ return interactionError;
+ if (_flow.hasPendingMainMenuReturn())
+ return Common::kNoError;
+ }
+ }
+
+ _flow.resetCursorAnimationSequence();
+ _callbacks.resetIdleState();
+ return Common::kNoError;
+}
+
+} // End of namespace Harvester
Commit: 5e92cc9095ab546555932ebd9d61a24b1d7bb8a0
https://github.com/scummvm/scummvm/commit/5e92cc9095ab546555932ebd9d61a24b1d7bb8a0
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Scope entity and archive setup ownership
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/entity.cpp
engines/harvester/resources.cpp
diff --git a/engines/harvester/entity.cpp b/engines/harvester/entity.cpp
index 671cf7e3fd8..4fea5b5db22 100644
--- a/engines/harvester/entity.cpp
+++ b/engines/harvester/entity.cpp
@@ -23,6 +23,7 @@
#include "common/algorithm.h"
#include "common/debug.h"
+#include "common/ptr.h"
#include "common/system.h"
#include "graphics/blit.h"
#include "graphics/screen.h"
@@ -788,11 +789,9 @@ void EntityManager::clearSceneEntities(bool preserveGlobalTimers) {
Entity *EntityManager::spawnAbmEntityFromResource(const Common::String &name,
const Common::String &resourcePath, int classId, const Common::Point &position, float z,
int animationRate, bool looping, bool pingPong) {
- Entity *entity = new Entity();
- if (!entity->loadAbmResource(_resources, resourcePath)) {
- delete entity;
+ Common::ScopedPtr<Entity> entity(new Entity());
+ if (!entity->loadAbmResource(_resources, resourcePath))
return nullptr;
- }
entity->setName(name);
entity->setClassId(classId);
@@ -800,21 +799,19 @@ Entity *EntityManager::spawnAbmEntityFromResource(const Common::String &name,
entity->setLooping(looping);
entity->setPingPong(pingPong);
entity->setAnimationRate(animationRate);
- return entity;
+ return entity.release();
}
Entity *EntityManager::spawnBitmapEntityFromResource(const Common::String &name,
const Common::String &resourcePath, int classId, const Common::Point &position, float z) {
- Entity *entity = new Entity();
- if (!entity->loadBitmapResource(_resources, resourcePath)) {
- delete entity;
+ Common::ScopedPtr<Entity> entity(new Entity());
+ if (!entity->loadBitmapResource(_resources, resourcePath))
return nullptr;
- }
entity->setName(name);
entity->setClassId(classId);
entity->setPosition(position.x, position.y, z);
- return entity;
+ return entity.release();
}
Entity *EntityManager::spawnCursorEntity(const Common::Point &position) {
diff --git a/engines/harvester/resources.cpp b/engines/harvester/resources.cpp
index 4d97b57fe82..840be9576ae 100644
--- a/engines/harvester/resources.cpp
+++ b/engines/harvester/resources.cpp
@@ -22,6 +22,7 @@
#include "harvester/resources.h"
#include "common/debug.h"
+#include "common/ptr.h"
#include "common/stream.h"
#include "harvester/detection.h"
#include "harvester/xfile_archive.h"
@@ -204,16 +205,15 @@ bool ResourceManager::ensureDiscMounted(int discNumber) {
continue;
}
- XFileArchive *archive = new XFileArchive();
+ Common::ScopedPtr<XFileArchive> archive(new XFileArchive());
if (!archive->open(indexPath, dataPath)) {
debugC(1, kDebugResources,
"Harvester: failed to mount disc %d set %c from %s + %s",
discNumber, spec.archiveSetId, indexPath.c_str(), dataPath.c_str());
- delete archive;
continue;
}
- mountArchive(buildDiscArchiveName(discNumber, spec.archiveSetId), archive, spec.priority, true);
+ mountArchive(buildDiscArchiveName(discNumber, spec.archiveSetId), archive.release(), spec.priority, true);
debugC(1, kDebugResources,
"Harvester: mounted disc %d set %c from %s + %s",
discNumber, spec.archiveSetId, indexPath.c_str(), dataPath.c_str());
Commit: 8c364fa3247e1ecf3c669be8be39c2246a84e535
https://github.com/scummvm/scummvm/commit/8c364fa3247e1ecf3c669be8be39c2246a84e535
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Scope FST audio queue ownership
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/fst_player.cpp
diff --git a/engines/harvester/fst_player.cpp b/engines/harvester/fst_player.cpp
index e9406badb7b..22f26d01167 100644
--- a/engines/harvester/fst_player.cpp
+++ b/engines/harvester/fst_player.cpp
@@ -451,11 +451,11 @@ bool FstPlayer::play(const Common::String &path) {
uint nextCensorshipToggleIndex = 0;
Audio::SoundHandle audioHandle;
- Audio::QueuingAudioStream *audioStream = nullptr;
+ Common::ScopedPtr<Audio::QueuingAudioStream> audioStream;
const bool hasPcmAudio = header.sampleRate != 0 && header.bitsPerSample == 16;
const uint32 trailingAudioFrameSkip = hasPcmAudio ? getTrailingFstAudioFrameSkip(frames) : 0;
if (hasPcmAudio)
- audioStream = Audio::makeQueuingAudioStream(header.sampleRate, false);
+ audioStream.reset(Audio::makeQueuingAudioStream(header.sampleRate, false));
else if (header.sampleRate != 0 && header.bitsPerSample != 0)
warning("Harvester: unsupported FST audio format %u-bit in '%s'", header.bitsPerSample, path.c_str());
@@ -492,7 +492,7 @@ bool FstPlayer::play(const Common::String &path) {
Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN);
if (!audioStarted) {
- mixer->playStream(Audio::Mixer::kSFXSoundType, &audioHandle, audioStream, -1,
+ mixer->playStream(Audio::Mixer::kSFXSoundType, &audioHandle, audioStream.get(), -1,
Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
audioStarted = true;
}
@@ -577,7 +577,7 @@ bool FstPlayer::play(const Common::String &path) {
if (audioStarted)
mixer->stopHandle(audioHandle);
- delete audioStream;
+ audioStream.reset();
if (resumeMusicAfterPlayback)
_vm.pauseMusic(false);
if (shouldSwitchToMovieDisplay)
Commit: 4d488b07f27c306f18014079a5131c7a901ea224
https://github.com/scummvm/scummvm/commit/4d488b07f27c306f18014079a5131c7a901ea224
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Separate inventory, keyboard and scene input handling
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
A engines/harvester/scene/input_inventory.cpp
A engines/harvester/scene/input_keyboard.cpp
engines/harvester/module.mk
engines/harvester/scene/input.cpp
engines/harvester/scene/input.h
diff --git a/engines/harvester/module.mk b/engines/harvester/module.mk
index 19bff864123..d978a7fb68e 100644
--- a/engines/harvester/module.mk
+++ b/engines/harvester/module.mk
@@ -54,6 +54,8 @@ MODULE_OBJS = \
scene/interaction_outcomes.o \
scene/interaction_transitions.o \
scene/input.o \
+ scene/input_inventory.o \
+ scene/input_keyboard.o \
scene/room_presentation.o \
scene/scene_entities.o \
scene/scene.o \
diff --git a/engines/harvester/scene/input.cpp b/engines/harvester/scene/input.cpp
index 39fd88352a2..6893301f7f1 100644
--- a/engines/harvester/scene/input.cpp
+++ b/engines/harvester/scene/input.cpp
@@ -22,9 +22,7 @@
#include "harvester/scene/input.h"
#include "common/debug.h"
-#include "graphics/screen.h"
#include "harvester/art.h"
-#include "harvester/cheats.h"
#include "harvester/detection.h"
#include "harvester/dialogue/dialogue.h"
#include "harvester/flow/flow.h"
@@ -48,43 +46,12 @@ static const int kCursorSequenceTransition = 6;
static const int kCursorSequenceNeutral = 7;
static const uint32 kRoomExitFastClickWindowTicks = 20;
-static const int kNativeInventoryDragCloseLeft = 0x45;
-static const int kNativeInventoryDragCloseTop = 0x4b;
-static const int kNativeInventoryDragCloseRight = 0x239;
-static const int kNativeInventoryDragCloseBottom = 0x1b4;
-static const uint32 kCombatLoadoutStatusDisplayTicks = 75;
static bool roomAllowsImmediateExitClick(const Common::String &roomName) {
return !roomName.equalsIgnoreCase("LAVAPIT") &&
!roomName.equalsIgnoreCase("BOWLSNTRY1");
}
-static bool isOutsideNativeInventoryDragCloseBounds(const Common::Point &point) {
- return point.x < kNativeInventoryDragCloseLeft ||
- point.x >= kNativeInventoryDragCloseRight ||
- point.y < kNativeInventoryDragCloseTop ||
- point.y >= kNativeInventoryDragCloseBottom;
-}
-
-static void debugLogInventoryClick(const char *buttonLabel, const Common::Point &point,
- const InventoryVisual *inventoryHover) {
- if (!inventoryHover) {
- debugC(1, kDebugInventory,
- "Harvester: inventory %s click at (%d,%d) hit no item", buttonLabel, point.x, point.y);
- return;
- }
-
- debugC(1, kDebugInventory,
- "Harvester: inventory %s click at (%d,%d) object='%s' sprite='%s' alt='%s' action='%s' owner='%s' text='%s' bounds=(%d,%d)-(%d,%d) exit=%d status=%d",
- buttonLabel, point.x, point.y, inventoryHover->object.objectName.c_str(),
- inventoryHover->object.spritePath.c_str(), inventoryHover->object.altSpritePath.c_str(),
- inventoryHover->object.actionTag.c_str(), inventoryHover->object.currentOwnerOrRoom.c_str(),
- inventoryHover->object.inventoryTextKey.c_str(), inventoryHover->bounds.left,
- inventoryHover->bounds.top, inventoryHover->bounds.right, inventoryHover->bounds.bottom,
- InventorySystem::isExitObject(inventoryHover->object),
- InventorySystem::isStatusObject(inventoryHover->object));
-}
-
static bool canTalkToRoomNpc(HarvesterEngine &engine, const NpcRecord &npc,
const DialogueSystem *dialogue) {
Script *script = engine.getScript();
@@ -384,36 +351,6 @@ InputResult Input::applyObjectInteraction(InteractionResult &interaction, bool &
return InputResult();
}
-void Input::queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw) {
- if (message.empty()) {
- const bool hadMessage = !_presentationState.combatLoadoutStatusText.value.empty();
- _presentationState.combatLoadoutStatusText = ResolvedText();
- _presentationState.combatLoadoutStatusHideTick = 0;
- if (hadMessage)
- needsRedraw = true;
- return;
- }
-
- _presentationState.combatLoadoutStatusText.boxName = "BOX1";
- _presentationState.combatLoadoutStatusText.value = message;
- _presentationState.combatLoadoutStatusHideTick =
- Player::getRuntimeClockTicks() + kCombatLoadoutStatusDisplayTicks;
- needsRedraw = true;
-}
-
-bool Input::openInventoryOverlay() {
- Player &player = _session.getPlayer();
- InputState &inputState = _session.getInputState();
-
- inputState.clearHeldActions();
- _session.clearPendingRegionInteraction();
- (void)_session.cancelPlayerAttackAnimation();
- player.cancelMovement();
- if (!_roomItems.stowCarriedRoomItemToInventory())
- return false;
- return _inventory.open();
-}
-
void Input::capturePlayerAttackTarget() {
Player &player = _session.getPlayer();
@@ -423,19 +360,6 @@ void Input::capturePlayerAttackTarget() {
_session.capturePlayerAttackTarget(_flow.getMousePos(), hoverState.npc);
}
-void Input::handleMouseMove(bool &needsRedraw) {
- if (_inventory.isOpen() && _inventory.hasSelection()) {
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
- // Native inventory handoff is driven by the latched selected item, not by
- // a second click after the selection has already been made.
- if (!inventoryHover && isOutsideNativeInventoryDragCloseBounds(_flow.getMousePos())) {
- if (_inventory.close())
- needsRedraw = true;
- }
- }
- needsRedraw = true;
-}
-
InputResult Input::handleRightButtonDown(bool &needsRedraw) {
Player &player = _session.getPlayer();
const RoomPlayerState &playerState = player.getState();
@@ -450,99 +374,9 @@ InputResult Input::handleRightButtonDown(bool &needsRedraw) {
if (_session.isPlayerCombatLocked())
return InputResult();
_session.clearPendingRegionInteraction();
- if (_inventory.isOpen()) {
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
- debugLogInventoryClick("right", _flow.getMousePos(), inventoryHover);
- if (_inventory.hasSelection()) {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click cleared active selection='%s'",
- _inventory.getSelectedItemName().c_str());
- if (_inventory.clearSelection())
- needsRedraw = true;
- return InputResult();
- }
- if (inventoryHover &&
- !InventorySystem::isExitObject(inventoryHover->object) &&
- !InventorySystem::isStatusObject(inventoryHover->object)) {
- Script *script = _engine.getScript();
- const int previousLoadout = playerState.combatLoadout >= 0
- ? playerState.combatLoadout
- : (script ? script->getPlayerCombatLoadout() : 0);
- bool loadoutChanged = false;
- if (_inventory.toggleCombatLoadout(inventoryHover->object,
- playerState.combatLoadout, loadoutChanged)) {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click handled as combat toggle object='%s' changed=%d",
- inventoryHover->object.objectName.c_str(), loadoutChanged);
- if (loadoutChanged) {
- if (script) {
- const Common::String statusMessage = buildCombatLoadoutStatusMessage(
- *script, inventoryHover->object, previousLoadout,
- script->getPlayerCombatLoadout());
- queueCombatLoadoutStatusMessage(statusMessage, needsRedraw);
- _presentationState.closeInventoryAfterCombatLoadoutStatus = true;
- }
- _session.captureCurrentSaveState();
- player.syncCombatLoadout(_engine, _scene.state);
- if (!_inventory.refresh())
- return InputResult::leaveRoom(Common::kReadingFailed);
- }
- needsRedraw = true;
- return InputResult();
- }
+ if (_inventory.isOpen())
+ return handleInventoryRightClick(needsRedraw);
- InventorySecondaryAction secondaryAction;
- if (_inventory.resolveSecondaryAction(inventoryHover->object, secondaryAction)) {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click executing secondary action object='%s' action='%s' closeInventory=%d",
- inventoryHover->object.objectName.c_str(),
- secondaryAction.actionTag.c_str(), secondaryAction.closeInventory);
- if (secondaryAction.closeInventory)
- (void)_inventory.close();
-
- InteractionResult interaction;
- bool didTransition = false;
- const bool executedActionTag = _engine.getScript()->executeActionTag(
- secondaryAction.actionTag, interaction, true, _scene.executionContext());
- debugC(1, kDebugInventory,
- "Harvester: inventory right click executeActionTag('%s') -> %d music='%s' cutscene='%s' nextRoom='%s'",
- secondaryAction.actionTag.c_str(), executedActionTag,
- interaction.musicPath.c_str(), interaction.cutscenePath.c_str(),
- interaction.nextRoomName.c_str());
- if (executedActionTag) {
- Common::Error interactionError =
- _interactionProcessor.handleInteractionResult(
- interaction, didTransition, Common::String());
- debugC(1, kDebugInventory,
- "Harvester: inventory right click action='%s' interaction result error=%d transitioned=%d pendingMainMenu=%d",
- secondaryAction.actionTag.c_str(), interactionError.getCode(),
- didTransition, _flow.hasPendingMainMenuReturn());
- if (interactionError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(interactionError);
- if (_flow.hasPendingMainMenuReturn())
- return InputResult::leaveRoom(Common::kNoError);
- }
-
- if (_inventory.isOpen()) {
- if (!_inventory.refresh())
- return InputResult::leaveRoom(Common::kReadingFailed);
- } else {
- debugC(1, kDebugInventory,
- "Harvester: inventory right click skipped overlay refresh after action='%s' transitioned=%d closeInventory=%d",
- secondaryAction.actionTag.c_str(), didTransition, secondaryAction.closeInventory);
- }
- needsRedraw = true;
- return InputResult();
- }
- debugC(1, kDebugInventory,
- "Harvester: inventory right click object='%s' produced no combat toggle or secondary action",
- inventoryHover->object.objectName.c_str());
- }
- debugC(1, kDebugInventory, "Harvester: inventory right click closing overlay");
- if (_inventory.close())
- needsRedraw = true;
- return InputResult();
- }
if (_inventory.hasSelection()) {
if (_inventory.clearSelection())
needsRedraw = true;
@@ -587,7 +421,6 @@ void Input::handleLeftButtonUp() {
InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
Player &player = _session.getPlayer();
- const RoomPlayerState &playerState = player.getState();
const RoomIdleAnimationState &idleState = player.getIdleState();
InputState &inputState = _session.getInputState();
@@ -610,56 +443,14 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
if (_session.isPlayerCombatLocked())
return InputResult();
- const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
- if (_inventory.isOpen()) {
- const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
- debugLogInventoryClick("left", _flow.getMousePos(), inventoryHover);
- if (inventoryHover) {
- if (InventorySystem::isExitObject(inventoryHover->object)) {
- debugC(1, kDebugInventory, "Harvester: inventory left click exit button");
- const bool clearedSelection = _inventory.clearSelection();
- const bool closedInventory = _inventory.close();
- if (clearedSelection || closedInventory)
- needsRedraw = true;
- return InputResult();
- }
- if (InventorySystem::isStatusObject(inventoryHover->object))
- return InputResult();
-
- if (!_inventory.hasSelection()) {
- debugC(1, kDebugInventory,
- "Harvester: inventory left click selecting object='%s'",
- inventoryHover->object.objectName.c_str());
- _inventory.selectItem(inventoryHover->object.objectName);
- _inventory.setPromptText(_inventory.buildSelectedPrompt(
- Common::String(), _flow.getMenuTextConfig()));
- needsRedraw = true;
- return InputResult();
- }
+ if (_inventory.isOpen())
+ return handleInventoryLeftClick(needsRedraw);
- if (!inventoryHover->object.objectName.equalsIgnoreCase(_inventory.getSelectedItemName())) {
- debugC(1, kDebugInventory,
- "Harvester: inventory left click using selected='%s' on target='%s'",
- _inventory.getSelectedItemName().c_str(),
- inventoryHover->object.objectName.c_str());
- Common::Error interactionError =
- _roomItems.handleSelectedItemTargetInteraction(inventoryHover->object, _interactionProcessor, needsRedraw);
- if (interactionError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(interactionError);
- }
- needsRedraw = true;
- return InputResult();
- }
+ return handleSceneClick(isFastExitClick, needsRedraw);
+}
- if (_inventory.hasSelection()) {
- if (!inventoryPanelBounds.contains(_flow.getMousePos()) && _inventory.close())
- needsRedraw = true;
- return InputResult();
- }
- if (!inventoryPanelBounds.contains(_flow.getMousePos()) && _inventory.close())
- needsRedraw = true;
- return InputResult();
- }
+InputResult Input::handleSceneClick(bool isFastExitClick, bool &needsRedraw) {
+ const RoomPlayerState &playerState = _session.getPlayer().getState();
const RoomHoverState hoverState = _flow.getRoomHoverState(_scene);
RoomHoverState clickHoverState = hoverState;
@@ -699,38 +490,54 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
return InputResult();
}
}
- if (_roomItems.hasSelectedItem()) {
- const Common::String selectedItemName = _roomItems.getSelectedItemName();
- if (clickHoverState.npc) {
- inputState.clearHeldActions();
- _roomItems.clearSelectedItem();
- bool didTransition = false;
- Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
- clickHoverState.npc->npcName, selectedItemName, Common::String(), didTransition);
- if (dialogueError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(dialogueError);
- if (_flow.hasPendingMainMenuReturn())
- return InputResult::leaveRoom(Common::kNoError);
- needsRedraw = true;
- return InputResult();
- }
+ if (_roomItems.hasSelectedItem())
+ return handleSelectedItemClick(clickHoverState, itemTarget, needsRedraw);
+ return handleSceneTargetClick(hoverState, isFastExitClick, needsRedraw);
+}
- ObjectRecord *roomTarget = clickHoverState.object
- ? SceneEntities::findSceneObjectByName(_scene.sceneObjects, clickHoverState.object->objectName)
- : nullptr;
- if (roomTarget) {
- Common::Error interactionError =
- _roomItems.handleSelectedItemTargetInteraction(*roomTarget, _interactionProcessor, needsRedraw);
- if (interactionError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(interactionError);
- needsRedraw = true;
- } else if (itemTarget) {
- Common::Error interactionError = _roomItems.handleSelectedItemMiss(_interactionProcessor, needsRedraw);
- if (interactionError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(interactionError);
- }
+InputResult Input::handleSelectedItemClick(const RoomHoverState &clickHoverState,
+ const Entity *itemTarget, bool &needsRedraw) {
+ InputState &inputState = _session.getInputState();
+
+ const Common::String selectedItemName = _roomItems.getSelectedItemName();
+ if (clickHoverState.npc) {
+ inputState.clearHeldActions();
+ _roomItems.clearSelectedItem();
+ bool didTransition = false;
+ Common::Error dialogueError = _interactionProcessor.runScriptedDialogue(
+ clickHoverState.npc->npcName, selectedItemName, Common::String(), didTransition);
+ if (dialogueError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(dialogueError);
+ if (_flow.hasPendingMainMenuReturn())
+ return InputResult::leaveRoom(Common::kNoError);
+ needsRedraw = true;
return InputResult();
}
+
+ ObjectRecord *roomTarget = clickHoverState.object
+ ? SceneEntities::findSceneObjectByName(_scene.sceneObjects, clickHoverState.object->objectName)
+ : nullptr;
+ if (roomTarget) {
+ Common::Error interactionError =
+ _roomItems.handleSelectedItemTargetInteraction(*roomTarget, _interactionProcessor, needsRedraw);
+ if (interactionError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(interactionError);
+ needsRedraw = true;
+ } else if (itemTarget) {
+ Common::Error interactionError = _roomItems.handleSelectedItemMiss(_interactionProcessor, needsRedraw);
+ if (interactionError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(interactionError);
+ }
+ return InputResult();
+}
+
+InputResult Input::handleSceneTargetClick(const RoomHoverState &hoverState,
+ bool isFastExitClick, bool &needsRedraw) {
+ Player &player = _session.getPlayer();
+ const RoomPlayerState &playerState = player.getState();
+ const RoomIdleAnimationState &idleState = player.getIdleState();
+ InputState &inputState = _session.getInputState();
+
if (hoverState.playerEntity) {
if (!playerState.entity || hoverState.playerEntity != playerState.entity ||
idleState.active || idleState.exiting ||
@@ -783,33 +590,46 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
ObjectRecord *clickedObject = hoverState.object
? SceneEntities::findSceneObjectByName(_scene.sceneObjects, hoverState.object->objectName)
: nullptr;
- if (!clickedObject) {
- _session.clearPendingRegionInteraction();
- if (hoverState.cursorSequence == 0 && playerState.entity) {
- if (_engine.isPathfindingDebugEnabled()) {
- const Common::Rect playerRect = playerState.entity->getScreenRect();
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding click accepted room='%s' mouse=(%d,%d) movement_band=(y=%d..%d,z=%d..%d) player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d)",
- _scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
- _scene.state.roomMaxZScreenY, _scene.state.roomMinZScreenY,
- _scene.state.roomMinZ, _scene.state.roomMaxZ,
- playerState.centerX, playerState.bottomY, static_cast<double>(playerState.z),
- playerRect.left, playerRect.top, playerRect.right, playerRect.bottom);
- }
- player.setMoveTargetFromScreenPoint(_scene.state, _flow.getMousePos().x, _flow.getMousePos().y);
- if (_engine.isPathfindingDebugEnabled()) {
- debugC(1, kDebugPathfinding,
- "Harvester: pathfinding click target room='%s' mouse=(%d,%d) target=(%d,%d,z=%.2f)",
- _scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
- playerState.targetX, playerState.targetBottomY, static_cast<double>(playerState.targetZ));
- }
- needsRedraw = true;
+ if (!clickedObject)
+ return handleWalkClick(hoverState, needsRedraw);
+ return handleObjectClick(*clickedObject, needsRedraw);
+}
+
+InputResult Input::handleWalkClick(const RoomHoverState &hoverState, bool &needsRedraw) {
+ Player &player = _session.getPlayer();
+ const RoomPlayerState &playerState = player.getState();
+
+ _session.clearPendingRegionInteraction();
+ if (hoverState.cursorSequence == 0 && playerState.entity) {
+ if (_engine.isPathfindingDebugEnabled()) {
+ const Common::Rect playerRect = playerState.entity->getScreenRect();
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding click accepted room='%s' mouse=(%d,%d) movement_band=(y=%d..%d,z=%d..%d) player=(%d,%d,z=%.2f) player_rect=(%d,%d)-(%d,%d)",
+ _scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
+ _scene.state.roomMaxZScreenY, _scene.state.roomMinZScreenY,
+ _scene.state.roomMinZ, _scene.state.roomMaxZ,
+ playerState.centerX, playerState.bottomY, static_cast<double>(playerState.z),
+ playerRect.left, playerRect.top, playerRect.right, playerRect.bottom);
}
- return InputResult();
+ player.setMoveTargetFromScreenPoint(_scene.state, _flow.getMousePos().x, _flow.getMousePos().y);
+ if (_engine.isPathfindingDebugEnabled()) {
+ debugC(1, kDebugPathfinding,
+ "Harvester: pathfinding click target room='%s' mouse=(%d,%d) target=(%d,%d,z=%.2f)",
+ _scene.state.roomName.c_str(), _flow.getMousePos().x, _flow.getMousePos().y,
+ playerState.targetX, playerState.targetBottomY, static_cast<double>(playerState.targetZ));
+ }
+ needsRedraw = true;
}
+ return InputResult();
+}
+
+InputResult Input::handleObjectClick(ObjectRecord &object, bool &needsRedraw) {
+ Player &player = _session.getPlayer();
+ const RoomPlayerState &playerState = player.getState();
+
_session.clearPendingRegionInteraction();
- if (clickedObject->objectName.equalsIgnoreCase("EXIT_BM") ||
- clickedObject->objectName.equalsIgnoreCase("EXIT_HS")) {
+ if (object.objectName.equalsIgnoreCase("EXIT_BM") ||
+ object.objectName.equalsIgnoreCase("EXIT_HS")) {
Common::Error exitError = _session.runRoomExitCommands();
if (exitError.getCode() != Common::kNoError)
return InputResult::leaveRoom(exitError);
@@ -821,36 +641,36 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
ResolvedText resolvedInspectText;
const bool hasInspectText =
- _engine.getScript()->resolveObjectInspectText(*clickedObject, resolvedInspectText);
+ _engine.getScript()->resolveObjectInspectText(object, resolvedInspectText);
const bool unlocksAfterInitialExamine =
- unlocksRoomObjectInteractionAfterInitialExamine(*clickedObject, *_engine.getScript());
+ unlocksRoomObjectInteractionAfterInitialExamine(object, *_engine.getScript());
const bool canShowInspectText =
hasInspectText && RoomPresentation::resolveInspectTextboxBitmap(_art, resolvedInspectText);
- if (!clickedObject->identShown && unlocksAfterInitialExamine) {
- clickedObject->identShown = true;
- _engine.getScript()->markObjectIdentShown(*clickedObject);
+ if (!object.identShown && unlocksAfterInitialExamine) {
+ object.identShown = true;
+ _engine.getScript()->markObjectIdentShown(object);
if (canShowInspectText) {
- beginInspectText(*clickedObject, resolvedInspectText);
+ beginInspectText(object, resolvedInspectText);
} else if (hasInspectText) {
debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
- resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
+ resolvedInspectText.boxName.c_str(), object.objectName.c_str());
}
player.cancelMovement();
needsRedraw = true;
return InputResult();
}
// An IDENT can be shown before a non-operatable object's action, without setting identShown. Dispatch after dismissal.
- if (!clickedObject->identShown && !suppressesInitialObjectInspectGate(*clickedObject) &&
+ if (!object.identShown && !suppressesInitialObjectInspectGate(object) &&
!unlocksAfterInitialExamine && canShowInspectText) {
- beginInspectText(*clickedObject, resolvedInspectText);
- if (!clickedObject->actionTag.empty())
- _pendingInspectObjectName = clickedObject->objectName;
+ beginInspectText(object, resolvedInspectText);
+ if (!object.actionTag.empty())
+ _pendingInspectObjectName = object.objectName;
player.cancelMovement();
needsRedraw = true;
return InputResult();
}
- if (_engine.getScript()->isPickupObject(*clickedObject)) {
- Common::Error pickupError = _roomItems.handleRoomItemPickup(*clickedObject, playerState.entity != nullptr,
+ if (_engine.getScript()->isPickupObject(object)) {
+ Common::Error pickupError = _roomItems.handleRoomItemPickup(object, playerState.entity != nullptr,
_interactionProcessor, _session, needsRedraw);
if (pickupError.getCode() != Common::kNoError)
return InputResult::leaveRoom(pickupError);
@@ -859,12 +679,12 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
InteractionResult interaction;
if (!_engine.getScript()->resolveObjectInteraction(
- *clickedObject, interaction, _scene.executionContext())) {
+ object, interaction, _scene.executionContext())) {
if (canShowInspectText) {
- beginInspectText(*clickedObject, resolvedInspectText);
+ beginInspectText(object, resolvedInspectText);
} else if (hasInspectText) {
debug(1, "Harvester: unsupported IDENT textbox '%s' for object '%s'",
- resolvedInspectText.boxName.c_str(), clickedObject->objectName.c_str());
+ resolvedInspectText.boxName.c_str(), object.objectName.c_str());
}
needsRedraw = true;
return InputResult();
@@ -873,228 +693,6 @@ InputResult Input::handleLeftButtonDown(bool &needsRedraw) {
return applyObjectInteraction(interaction, needsRedraw);
}
-InputResult Input::handleGameAction(GameAction action, bool pressed, bool &needsRedraw) {
- InputState &inputState = _session.getInputState();
- bool *heldAction = nullptr;
- switch (action) {
- case kActionHighAttack:
- heldAction = &inputState.highAttackHeld;
- break;
- case kActionMediumAttack:
- heldAction = &inputState.mediumAttackHeld;
- break;
- case kActionLowAttack:
- heldAction = &inputState.lowAttackHeld;
- break;
- case kActionDuck:
- heldAction = &inputState.duckHeld;
- break;
- case kActionJump:
- heldAction = &inputState.jumpHeld;
- break;
- default:
- break;
- }
- if (heldAction) {
- inputState.speedUpActive = false;
- if (!pressed) {
- *heldAction = false;
- return InputResult();
- }
- _session.notePlayerActivity();
- if (_presentationState.showingInspectText || _inventory.isOpen())
- return InputResult();
- Player &player = _session.getPlayer();
- const RoomIdleAnimationState &idleState = player.getIdleState();
- if (idleState.active || idleState.exiting) {
- if (player.requestIdleAnimationExit(_scene.state))
- needsRedraw = true;
- return InputResult();
- }
- *heldAction = true;
- return InputResult();
- }
- static const Common::KeyCode keys[] = {
- Common::KEYCODE_LEFT, Common::KEYCODE_RIGHT, Common::KEYCODE_UP, Common::KEYCODE_DOWN,
- Common::KEYCODE_LCTRL, Common::KEYCODE_RSHIFT, Common::KEYCODE_LALT,
- Common::KEYCODE_i, Common::KEYCODE_ESCAPE
- };
- if ((uint)action >= ARRAYSIZE(keys))
- return InputResult();
- const Common::KeyState key(keys[action]);
- if (pressed)
- return handleKeyDown(key, needsRedraw, true);
- handleKeyUp(key, true);
- return InputResult();
-}
-
-InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw, bool gameAction) {
- Player &player = _session.getPlayer();
- const RoomIdleAnimationState &idleState = player.getIdleState();
- InputState &inputState = _session.getInputState();
- // GOG update_actor_runtime_state (0x4d750) tests the last scan code,
- // not the held Alt slot. Any other key press or release clears fast speed.
- inputState.speedUpActive = gameAction && key.keycode == Common::KEYCODE_LALT;
-
- if (_presentationState.showingInspectText) {
- if (key.keycode == Common::KEYCODE_ESCAPE)
- _inspectDismissPressed = true;
- return InputResult();
- }
-
- _session.notePlayerActivity();
- if (idleState.active || idleState.exiting) {
- if (player.requestIdleAnimationExit(_scene.state))
- needsRedraw = true;
- return InputResult();
- }
-
- if (Script *script = _engine.getScript()) {
- InteractionResult cheatInteraction;
- CheatActivation cheatActivation;
- const CheatInputResult cheatInputResult = _cheats.processKey(
- key, *script, _scene.executionContext(),
- 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 InputResult::leaveRoom(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 InputResult::leaveRoom(cheatError);
- if (_flow.hasPendingMainMenuReturn())
- return InputResult::leaveRoom(Common::kNoError);
- }
- needsRedraw = true;
- }
- return InputResult();
- }
- }
-
- if (_inventory.isOpen()) {
- if (key.keycode == Common::KEYCODE_ESCAPE) {
- const bool clearedSelection = _inventory.clearSelection();
- const bool closedInventory = _inventory.close();
- if (clearedSelection || closedInventory)
- needsRedraw = true;
- } else if (key.keycode == Common::KEYCODE_RETURN ||
- key.keycode == Common::KEYCODE_KP_ENTER ||
- key.keycode == Common::KEYCODE_i) {
- if (_inventory.close())
- needsRedraw = true;
- }
- return InputResult();
- }
- if (_inventory.hasSelection()) {
- if (key.keycode == Common::KEYCODE_ESCAPE) {
- if (_inventory.clearSelection())
- needsRedraw = true;
- return InputResult();
- }
- if (key.keycode == Common::KEYCODE_i) {
- if (_inventory.clearSelection())
- needsRedraw = true;
- if (!openInventoryOverlay())
- return InputResult::leaveRoom(Common::kReadingFailed);
- needsRedraw = true;
- return InputResult();
- }
- }
- if (gameAction && key.keycode == Common::KEYCODE_LCTRL) {
- inputState.attackModifierHeld = true;
- } else if (gameAction && key.keycode == Common::KEYCODE_RSHIFT) {
- inputState.evasiveModifierHeld = true;
- } else if (gameAction && key.keycode == Common::KEYCODE_LEFT)
- inputState.moveLeft = true;
- else if (gameAction && key.keycode == Common::KEYCODE_RIGHT)
- inputState.moveRight = true;
- else if (gameAction && key.keycode == Common::KEYCODE_UP)
- inputState.moveUp = true;
- else if (gameAction && key.keycode == Common::KEYCODE_DOWN)
- inputState.moveDown = true;
-
- if (_session.isPlayerCombatLocked())
- return InputResult();
-
- if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
- Common::Error debugKillError = _session.killDebugActiveMonster(_flow.getMousePos(), needsRedraw);
- if (debugKillError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(debugKillError);
- return InputResult();
- } else if (gameAction && key.keycode == Common::KEYCODE_i) {
- if (!openInventoryOverlay())
- return InputResult::leaveRoom(Common::kReadingFailed);
- needsRedraw = true;
- return InputResult();
- } else if (gameAction && key.keycode == Common::KEYCODE_ESCAPE) {
- if (_session.cancelPlayerAttackAnimation())
- needsRedraw = true;
- if (!_roomItems.stowCarriedRoomItemToInventory())
- return InputResult::leaveRoom(Common::kReadingFailed);
- inputState.clearHeldActions();
- _session.clearPendingRegionInteraction();
- player.cancelMovement();
- _session.captureCurrentSaveState();
- Graphics::Screen *activeScreen = _engine.getScreen();
- if (!activeScreen)
- return InputResult::leaveRoom(Common::kNoError);
- IndexedBitmap roomMenuBackdrop;
- RoomPresentation::drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
- if (!captureScreenBackdrop(*activeScreen, roomMenuBackdrop))
- return InputResult::leaveRoom(Common::kReadingFailed);
- Common::Error menuError = _flow.runRoomMenu(
- roomMenuBackdrop, _scene.palette, _scene.targetPaletteBrightness,
- _scene.state.hasEntrance);
- if (menuError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(menuError);
- _flow.resetCursorAnimationSequence();
- _session.resetIdleState();
- needsRedraw = true;
- return InputResult();
- }
-
- if (key.keycode == Common::KEYCODE_RETURN ||
- key.keycode == Common::KEYCODE_KP_ENTER) {
- if (!_roomItems.stowCarriedRoomItemToInventory())
- return InputResult::leaveRoom(Common::kReadingFailed);
- Common::Error exitError = _session.runRoomExitCommands();
- if (exitError.getCode() != Common::kNoError)
- return InputResult::leaveRoom(exitError);
- return InputResult::leaveRoom(Common::kNoError);
- }
- return InputResult();
-}
-
-void Input::handleKeyUp(const Common::KeyState &key, bool gameAction) {
- InputState &inputState = _session.getInputState();
- inputState.speedUpActive = false;
- if (!gameAction)
- return;
-
- if (key.keycode == Common::KEYCODE_LCTRL ||
- key.keycode == Common::KEYCODE_RCTRL)
- inputState.attackModifierHeld = false;
- else if (key.keycode == Common::KEYCODE_RSHIFT)
- inputState.evasiveModifierHeld = false;
- else if (key.keycode == Common::KEYCODE_LEFT)
- inputState.moveLeft = false;
- else if (key.keycode == Common::KEYCODE_RIGHT)
- inputState.moveRight = false;
- else if (key.keycode == Common::KEYCODE_UP)
- inputState.moveUp = false;
- else if (key.keycode == Common::KEYCODE_DOWN)
- inputState.moveDown = false;
-}
-
InputResult Input::handleEvent(const Common::Event &event, bool &needsRedraw) {
Common::Error result = Common::kNoError;
if (_flow.handleSystemEvent(event, result))
diff --git a/engines/harvester/scene/input.h b/engines/harvester/scene/input.h
index 136a47eae03..d3f58d24b83 100644
--- a/engines/harvester/scene/input.h
+++ b/engines/harvester/scene/input.h
@@ -33,13 +33,16 @@ namespace Harvester {
class Art;
class CheatSystem;
class DialogueSystem;
+class Entity;
class Flow;
class HarvesterEngine;
class InventorySystem;
class Interaction;
class ItemInteractions;
class Scene;
+struct CheatActivation;
struct InteractionResult;
+struct InventorySecondaryAction;
struct MenuTextConfig;
struct NpcRecord;
struct ObjectRecord;
@@ -127,9 +130,25 @@ private:
InputResult handleRightButtonDown(bool &needsRedraw);
void handleLeftButtonUp();
InputResult handleLeftButtonDown(bool &needsRedraw);
+ InputResult handleSceneClick(bool isFastExitClick, bool &needsRedraw);
+ InputResult handleSelectedItemClick(const RoomHoverState &clickHoverState,
+ const Entity *itemTarget, bool &needsRedraw);
+ InputResult handleSceneTargetClick(const RoomHoverState &hoverState,
+ bool isFastExitClick, bool &needsRedraw);
+ InputResult handleWalkClick(const RoomHoverState &hoverState, bool &needsRedraw);
+ InputResult handleObjectClick(ObjectRecord &object, bool &needsRedraw);
+ InputResult handleInventoryLeftClick(bool &needsRedraw);
+ InputResult handleInventoryRightClick(bool &needsRedraw);
+ InputResult applyInventorySecondaryAction(const ObjectRecord &object,
+ const InventorySecondaryAction &secondaryAction, bool &needsRedraw);
+ InputResult handleInventoryKeyDown(const Common::KeyState &key, bool &needsRedraw);
InputResult handleKeyDown(const Common::KeyState &key, bool &needsRedraw, bool gameAction = false);
+ InputResult handleGameplayKeyDown(const Common::KeyState &key, bool &needsRedraw, bool gameAction);
void handleKeyUp(const Common::KeyState &key, bool gameAction = false);
InputResult handleGameAction(GameAction action, bool pressed, bool &needsRedraw);
+ InputResult applyCheatActivation(InteractionResult &cheatInteraction,
+ const CheatActivation &cheatActivation, bool &needsRedraw);
+ InputResult openRoomMenu(bool &needsRedraw);
void beginInspectText(const ObjectRecord &object, const ResolvedText &text);
InputResult finishInspectText(bool &needsRedraw);
InputResult applyObjectInteraction(InteractionResult &interaction, bool &needsRedraw);
diff --git a/engines/harvester/scene/input_inventory.cpp b/engines/harvester/scene/input_inventory.cpp
new file mode 100644
index 00000000000..522f7b15e81
--- /dev/null
+++ b/engines/harvester/scene/input_inventory.cpp
@@ -0,0 +1,273 @@
+/* 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/scene/input.h"
+
+#include "common/debug.h"
+#include "harvester/detection.h"
+#include "harvester/flow/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/inventory.h"
+#include "harvester/player/player.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/scene/room_support.h"
+#include "harvester/scene/scene.h"
+
+namespace Harvester {
+
+static const int kNativeInventoryDragCloseLeft = 0x45;
+static const int kNativeInventoryDragCloseTop = 0x4b;
+static const int kNativeInventoryDragCloseRight = 0x239;
+static const int kNativeInventoryDragCloseBottom = 0x1b4;
+static const uint32 kCombatLoadoutStatusDisplayTicks = 75;
+
+static bool isOutsideNativeInventoryDragCloseBounds(const Common::Point &point) {
+ return point.x < kNativeInventoryDragCloseLeft ||
+ point.x >= kNativeInventoryDragCloseRight ||
+ point.y < kNativeInventoryDragCloseTop ||
+ point.y >= kNativeInventoryDragCloseBottom;
+}
+
+static void debugLogInventoryClick(const char *buttonLabel, const Common::Point &point,
+ const InventoryVisual *inventoryHover) {
+ if (!inventoryHover) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory %s click at (%d,%d) hit no item", buttonLabel, point.x, point.y);
+ return;
+ }
+
+ debugC(1, kDebugInventory,
+ "Harvester: inventory %s click at (%d,%d) object='%s' sprite='%s' alt='%s' action='%s' owner='%s' text='%s' bounds=(%d,%d)-(%d,%d) exit=%d status=%d",
+ buttonLabel, point.x, point.y, inventoryHover->object.objectName.c_str(),
+ inventoryHover->object.spritePath.c_str(), inventoryHover->object.altSpritePath.c_str(),
+ inventoryHover->object.actionTag.c_str(), inventoryHover->object.currentOwnerOrRoom.c_str(),
+ inventoryHover->object.inventoryTextKey.c_str(), inventoryHover->bounds.left,
+ inventoryHover->bounds.top, inventoryHover->bounds.right, inventoryHover->bounds.bottom,
+ InventorySystem::isExitObject(inventoryHover->object),
+ InventorySystem::isStatusObject(inventoryHover->object));
+}
+
+void Input::queueCombatLoadoutStatusMessage(const Common::String &message, bool &needsRedraw) {
+ if (message.empty()) {
+ const bool hadMessage = !_presentationState.combatLoadoutStatusText.value.empty();
+ _presentationState.combatLoadoutStatusText = ResolvedText();
+ _presentationState.combatLoadoutStatusHideTick = 0;
+ if (hadMessage)
+ needsRedraw = true;
+ return;
+ }
+
+ _presentationState.combatLoadoutStatusText.boxName = "BOX1";
+ _presentationState.combatLoadoutStatusText.value = message;
+ _presentationState.combatLoadoutStatusHideTick =
+ Player::getRuntimeClockTicks() + kCombatLoadoutStatusDisplayTicks;
+ needsRedraw = true;
+}
+
+bool Input::openInventoryOverlay() {
+ Player &player = _session.getPlayer();
+ InputState &inputState = _session.getInputState();
+
+ inputState.clearHeldActions();
+ _session.clearPendingRegionInteraction();
+ (void)_session.cancelPlayerAttackAnimation();
+ player.cancelMovement();
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return false;
+ return _inventory.open();
+}
+
+void Input::handleMouseMove(bool &needsRedraw) {
+ if (_inventory.isOpen() && _inventory.hasSelection()) {
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
+ // Native inventory handoff is driven by the latched selected item, not by
+ // a second click after the selection has already been made.
+ if (!inventoryHover && isOutsideNativeInventoryDragCloseBounds(_flow.getMousePos())) {
+ if (_inventory.close())
+ needsRedraw = true;
+ }
+ }
+ needsRedraw = true;
+}
+
+InputResult Input::handleInventoryLeftClick(bool &needsRedraw) {
+ const Common::Rect inventoryPanelBounds = _inventory.getPanelBounds();
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
+ debugLogInventoryClick("left", _flow.getMousePos(), inventoryHover);
+ if (inventoryHover) {
+ if (InventorySystem::isExitObject(inventoryHover->object)) {
+ debugC(1, kDebugInventory, "Harvester: inventory left click exit button");
+ const bool clearedSelection = _inventory.clearSelection();
+ const bool closedInventory = _inventory.close();
+ if (clearedSelection || closedInventory)
+ needsRedraw = true;
+ return InputResult();
+ }
+ if (InventorySystem::isStatusObject(inventoryHover->object))
+ return InputResult();
+
+ if (!_inventory.hasSelection()) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory left click selecting object='%s'",
+ inventoryHover->object.objectName.c_str());
+ _inventory.selectItem(inventoryHover->object.objectName);
+ _inventory.setPromptText(_inventory.buildSelectedPrompt(
+ Common::String(), _flow.getMenuTextConfig()));
+ needsRedraw = true;
+ return InputResult();
+ }
+
+ if (!inventoryHover->object.objectName.equalsIgnoreCase(_inventory.getSelectedItemName())) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory left click using selected='%s' on target='%s'",
+ _inventory.getSelectedItemName().c_str(),
+ inventoryHover->object.objectName.c_str());
+ Common::Error interactionError =
+ _roomItems.handleSelectedItemTargetInteraction(inventoryHover->object, _interactionProcessor, needsRedraw);
+ if (interactionError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(interactionError);
+ }
+ needsRedraw = true;
+ return InputResult();
+ }
+
+ if (!inventoryPanelBounds.contains(_flow.getMousePos()) && _inventory.close())
+ needsRedraw = true;
+ return InputResult();
+}
+
+InputResult Input::handleInventoryRightClick(bool &needsRedraw) {
+ Player &player = _session.getPlayer();
+ const RoomPlayerState &playerState = player.getState();
+
+ const InventoryVisual *inventoryHover = _inventory.findItemAtPoint(_flow.getMousePos());
+ debugLogInventoryClick("right", _flow.getMousePos(), inventoryHover);
+ if (_inventory.hasSelection()) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click cleared active selection='%s'",
+ _inventory.getSelectedItemName().c_str());
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ return InputResult();
+ }
+ if (inventoryHover &&
+ !InventorySystem::isExitObject(inventoryHover->object) &&
+ !InventorySystem::isStatusObject(inventoryHover->object)) {
+ Script *script = _engine.getScript();
+ const int previousLoadout = playerState.combatLoadout >= 0
+ ? playerState.combatLoadout
+ : (script ? script->getPlayerCombatLoadout() : 0);
+ bool loadoutChanged = false;
+ if (_inventory.toggleCombatLoadout(inventoryHover->object,
+ playerState.combatLoadout, loadoutChanged)) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click handled as combat toggle object='%s' changed=%d",
+ inventoryHover->object.objectName.c_str(), loadoutChanged);
+ if (loadoutChanged) {
+ if (script) {
+ const Common::String statusMessage = buildCombatLoadoutStatusMessage(
+ *script, inventoryHover->object, previousLoadout,
+ script->getPlayerCombatLoadout());
+ queueCombatLoadoutStatusMessage(statusMessage, needsRedraw);
+ _presentationState.closeInventoryAfterCombatLoadoutStatus = true;
+ }
+ _session.captureCurrentSaveState();
+ player.syncCombatLoadout(_engine, _scene.state);
+ if (!_inventory.refresh())
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ }
+ needsRedraw = true;
+ return InputResult();
+ }
+
+ InventorySecondaryAction secondaryAction;
+ if (_inventory.resolveSecondaryAction(inventoryHover->object, secondaryAction))
+ return applyInventorySecondaryAction(inventoryHover->object, secondaryAction, needsRedraw);
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click object='%s' produced no combat toggle or secondary action",
+ inventoryHover->object.objectName.c_str());
+ }
+ debugC(1, kDebugInventory, "Harvester: inventory right click closing overlay");
+ if (_inventory.close())
+ needsRedraw = true;
+ return InputResult();
+}
+
+InputResult Input::applyInventorySecondaryAction(const ObjectRecord &object,
+ const InventorySecondaryAction &secondaryAction, bool &needsRedraw) {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click executing secondary action object='%s' action='%s' closeInventory=%d",
+ object.objectName.c_str(),
+ secondaryAction.actionTag.c_str(), secondaryAction.closeInventory);
+ if (secondaryAction.closeInventory)
+ (void)_inventory.close();
+
+ InteractionResult interaction;
+ bool didTransition = false;
+ const bool executedActionTag = _engine.getScript()->executeActionTag(
+ secondaryAction.actionTag, interaction, true, _scene.executionContext());
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click executeActionTag('%s') -> %d music='%s' cutscene='%s' nextRoom='%s'",
+ secondaryAction.actionTag.c_str(), executedActionTag,
+ interaction.musicPath.c_str(), interaction.cutscenePath.c_str(),
+ interaction.nextRoomName.c_str());
+ if (executedActionTag) {
+ Common::Error interactionError =
+ _interactionProcessor.handleInteractionResult(
+ interaction, didTransition, Common::String());
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click action='%s' interaction result error=%d transitioned=%d pendingMainMenu=%d",
+ secondaryAction.actionTag.c_str(), interactionError.getCode(),
+ didTransition, _flow.hasPendingMainMenuReturn());
+ if (interactionError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(interactionError);
+ if (_flow.hasPendingMainMenuReturn())
+ return InputResult::leaveRoom(Common::kNoError);
+ }
+
+ if (_inventory.isOpen()) {
+ if (!_inventory.refresh())
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ } else {
+ debugC(1, kDebugInventory,
+ "Harvester: inventory right click skipped overlay refresh after action='%s' transitioned=%d closeInventory=%d",
+ secondaryAction.actionTag.c_str(), didTransition, secondaryAction.closeInventory);
+ }
+ needsRedraw = true;
+ return InputResult();
+}
+
+InputResult Input::handleInventoryKeyDown(const Common::KeyState &key, bool &needsRedraw) {
+ if (key.keycode == Common::KEYCODE_ESCAPE) {
+ const bool clearedSelection = _inventory.clearSelection();
+ const bool closedInventory = _inventory.close();
+ if (clearedSelection || closedInventory)
+ needsRedraw = true;
+ } else if (key.keycode == Common::KEYCODE_RETURN ||
+ key.keycode == Common::KEYCODE_KP_ENTER ||
+ key.keycode == Common::KEYCODE_i) {
+ if (_inventory.close())
+ needsRedraw = true;
+ }
+ return InputResult();
+}
+
+} // End of namespace Harvester
diff --git a/engines/harvester/scene/input_keyboard.cpp b/engines/harvester/scene/input_keyboard.cpp
new file mode 100644
index 00000000000..0c2fb74ccde
--- /dev/null
+++ b/engines/harvester/scene/input_keyboard.cpp
@@ -0,0 +1,267 @@
+/* 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/scene/input.h"
+
+#include "graphics/screen.h"
+#include "harvester/cheats.h"
+#include "harvester/flow/flow.h"
+#include "harvester/harvester.h"
+#include "harvester/inventory.h"
+#include "harvester/player/player.h"
+#include "harvester/scene/interaction.h"
+#include "harvester/scene/room_presentation.h"
+#include "harvester/scene/room_support.h"
+#include "harvester/scene/scene.h"
+
+namespace Harvester {
+
+InputResult Input::handleGameAction(GameAction action, bool pressed, bool &needsRedraw) {
+ InputState &inputState = _session.getInputState();
+ bool *heldAction = nullptr;
+ switch (action) {
+ case kActionHighAttack:
+ heldAction = &inputState.highAttackHeld;
+ break;
+ case kActionMediumAttack:
+ heldAction = &inputState.mediumAttackHeld;
+ break;
+ case kActionLowAttack:
+ heldAction = &inputState.lowAttackHeld;
+ break;
+ case kActionDuck:
+ heldAction = &inputState.duckHeld;
+ break;
+ case kActionJump:
+ heldAction = &inputState.jumpHeld;
+ break;
+ default:
+ break;
+ }
+ if (heldAction) {
+ inputState.speedUpActive = false;
+ if (!pressed) {
+ *heldAction = false;
+ return InputResult();
+ }
+ _session.notePlayerActivity();
+ if (_presentationState.showingInspectText || _inventory.isOpen())
+ return InputResult();
+ Player &player = _session.getPlayer();
+ const RoomIdleAnimationState &idleState = player.getIdleState();
+ if (idleState.active || idleState.exiting) {
+ if (player.requestIdleAnimationExit(_scene.state))
+ needsRedraw = true;
+ return InputResult();
+ }
+ *heldAction = true;
+ return InputResult();
+ }
+ static const Common::KeyCode keys[] = {
+ Common::KEYCODE_LEFT, Common::KEYCODE_RIGHT, Common::KEYCODE_UP, Common::KEYCODE_DOWN,
+ Common::KEYCODE_LCTRL, Common::KEYCODE_RSHIFT, Common::KEYCODE_LALT,
+ Common::KEYCODE_i, Common::KEYCODE_ESCAPE
+ };
+ if ((uint)action >= ARRAYSIZE(keys))
+ return InputResult();
+ const Common::KeyState key(keys[action]);
+ if (pressed)
+ return handleKeyDown(key, needsRedraw, true);
+ handleKeyUp(key, true);
+ return InputResult();
+}
+
+InputResult Input::handleKeyDown(const Common::KeyState &key, bool &needsRedraw, bool gameAction) {
+ Player &player = _session.getPlayer();
+ const RoomIdleAnimationState &idleState = player.getIdleState();
+ InputState &inputState = _session.getInputState();
+ // GOG update_actor_runtime_state (0x4d750) tests the last scan code,
+ // not the held Alt slot. Any other key press or release clears fast speed.
+ inputState.speedUpActive = gameAction && key.keycode == Common::KEYCODE_LALT;
+
+ if (_presentationState.showingInspectText) {
+ if (key.keycode == Common::KEYCODE_ESCAPE)
+ _inspectDismissPressed = true;
+ return InputResult();
+ }
+
+ _session.notePlayerActivity();
+ if (idleState.active || idleState.exiting) {
+ if (player.requestIdleAnimationExit(_scene.state))
+ needsRedraw = true;
+ return InputResult();
+ }
+
+ if (Script *script = _engine.getScript()) {
+ InteractionResult cheatInteraction;
+ CheatActivation cheatActivation;
+ const CheatInputResult cheatInputResult = _cheats.processKey(
+ key, *script, _scene.executionContext(),
+ cheatInteraction, cheatActivation);
+ if (cheatInputResult != kCheatInputIgnored) {
+ if (cheatInputResult == kCheatInputComplete && cheatActivation.activated)
+ return applyCheatActivation(cheatInteraction, cheatActivation, needsRedraw);
+ return InputResult();
+ }
+ }
+
+ if (_inventory.isOpen())
+ return handleInventoryKeyDown(key, needsRedraw);
+
+ if (_inventory.hasSelection()) {
+ if (key.keycode == Common::KEYCODE_ESCAPE) {
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ return InputResult();
+ }
+ if (key.keycode == Common::KEYCODE_i) {
+ if (_inventory.clearSelection())
+ needsRedraw = true;
+ if (!openInventoryOverlay())
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ needsRedraw = true;
+ return InputResult();
+ }
+ }
+ return handleGameplayKeyDown(key, needsRedraw, gameAction);
+}
+
+InputResult Input::handleGameplayKeyDown(const Common::KeyState &key,
+ bool &needsRedraw, bool gameAction) {
+ InputState &inputState = _session.getInputState();
+
+ if (gameAction && key.keycode == Common::KEYCODE_LCTRL) {
+ inputState.attackModifierHeld = true;
+ } else if (gameAction && key.keycode == Common::KEYCODE_RSHIFT) {
+ inputState.evasiveModifierHeld = true;
+ } else if (gameAction && key.keycode == Common::KEYCODE_LEFT)
+ inputState.moveLeft = true;
+ else if (gameAction && key.keycode == Common::KEYCODE_RIGHT)
+ inputState.moveRight = true;
+ else if (gameAction && key.keycode == Common::KEYCODE_UP)
+ inputState.moveUp = true;
+ else if (gameAction && key.keycode == Common::KEYCODE_DOWN)
+ inputState.moveDown = true;
+
+ if (_session.isPlayerCombatLocked())
+ return InputResult();
+
+ if (key.keycode == Common::KEYCODE_k && _engine.isCombatDebugEnabled()) {
+ Common::Error debugKillError = _session.killDebugActiveMonster(_flow.getMousePos(), needsRedraw);
+ if (debugKillError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(debugKillError);
+ return InputResult();
+ } else if (gameAction && key.keycode == Common::KEYCODE_i) {
+ if (!openInventoryOverlay())
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ needsRedraw = true;
+ return InputResult();
+ } else if (gameAction && key.keycode == Common::KEYCODE_ESCAPE) {
+ return openRoomMenu(needsRedraw);
+ }
+
+ if (key.keycode == Common::KEYCODE_RETURN ||
+ key.keycode == Common::KEYCODE_KP_ENTER) {
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ Common::Error exitError = _session.runRoomExitCommands();
+ if (exitError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(exitError);
+ return InputResult::leaveRoom(Common::kNoError);
+ }
+ return InputResult();
+}
+
+void Input::handleKeyUp(const Common::KeyState &key, bool gameAction) {
+ InputState &inputState = _session.getInputState();
+ inputState.speedUpActive = false;
+ if (!gameAction)
+ return;
+
+ if (key.keycode == Common::KEYCODE_LCTRL ||
+ key.keycode == Common::KEYCODE_RCTRL)
+ inputState.attackModifierHeld = false;
+ else if (key.keycode == Common::KEYCODE_RSHIFT)
+ inputState.evasiveModifierHeld = false;
+ else if (key.keycode == Common::KEYCODE_LEFT)
+ inputState.moveLeft = false;
+ else if (key.keycode == Common::KEYCODE_RIGHT)
+ inputState.moveRight = false;
+ else if (key.keycode == Common::KEYCODE_UP)
+ inputState.moveUp = false;
+ else if (key.keycode == Common::KEYCODE_DOWN)
+ inputState.moveDown = false;
+}
+
+InputResult Input::applyCheatActivation(InteractionResult &cheatInteraction,
+ const CheatActivation &cheatActivation, bool &needsRedraw) {
+ if (cheatActivation.clearInteractionState && _inventory.clearSelection())
+ needsRedraw = true;
+ if (cheatActivation.inventoryChanged && _inventory.isOpen() &&
+ !_inventory.refresh()) {
+ return InputResult::leaveRoom(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 InputResult::leaveRoom(cheatError);
+ if (_flow.hasPendingMainMenuReturn())
+ return InputResult::leaveRoom(Common::kNoError);
+ }
+ needsRedraw = true;
+ return InputResult();
+}
+
+InputResult Input::openRoomMenu(bool &needsRedraw) {
+ Player &player = _session.getPlayer();
+ InputState &inputState = _session.getInputState();
+
+ if (_session.cancelPlayerAttackAnimation())
+ needsRedraw = true;
+ if (!_roomItems.stowCarriedRoomItemToInventory())
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ inputState.clearHeldActions();
+ _session.clearPendingRegionInteraction();
+ player.cancelMovement();
+ _session.captureCurrentSaveState();
+ Graphics::Screen *activeScreen = _engine.getScreen();
+ if (!activeScreen)
+ return InputResult::leaveRoom(Common::kNoError);
+ IndexedBitmap roomMenuBackdrop;
+ RoomPresentation::drawRoomScene(_engine, *activeScreen, _scene, _scene.targetPaletteBrightness);
+ if (!captureScreenBackdrop(*activeScreen, roomMenuBackdrop))
+ return InputResult::leaveRoom(Common::kReadingFailed);
+ Common::Error menuError = _flow.runRoomMenu(
+ roomMenuBackdrop, _scene.palette, _scene.targetPaletteBrightness,
+ _scene.state.hasEntrance);
+ if (menuError.getCode() != Common::kNoError)
+ return InputResult::leaveRoom(menuError);
+ _flow.resetCursorAnimationSequence();
+ _session.resetIdleState();
+ needsRedraw = true;
+ return InputResult();
+}
+
+} // End of namespace Harvester
Commit: 762eaeed53ea8aadaf35e6c93e074d8413128fd3
https://github.com/scummvm/scummvm/commit/762eaeed53ea8aadaf35e6c93e074d8413128fd3
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Extract Sergeant dialogue branches into methods
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/npc/sergeant_dialogue.cpp
engines/harvester/npc/sergeant_dialogue.h
diff --git a/engines/harvester/npc/sergeant_dialogue.cpp b/engines/harvester/npc/sergeant_dialogue.cpp
index e84889529c5..6c4afa21c31 100644
--- a/engines/harvester/npc/sergeant_dialogue.cpp
+++ b/engines/harvester/npc/sergeant_dialogue.cpp
@@ -50,515 +50,535 @@ static const int kSergeantApplicationResponseLine = 0x2b8;
static const int kSergeantAmnesiaResponseLine = 0x2b9;
static const int kSergeantReminderResponseLine = 0x2bb;
-} // End of namespace
+static void resetAndSetObjectVisible(DialogueRuntime &runtime, const char *ownerOrRoom,
+ const char *objectName, bool visible) {
+ (void)runtime.startupScript().resetRuntimeObjectToInitialState(objectName);
+ (void)runtime.startupScript().setRuntimeObjectVisible(ownerOrRoom, objectName, visible);
+}
-Common::Error SergeantDialogueHandler::handleDialogue(DialogueRuntime &runtime,
- const Common::String &usedItemName, DialogueSharedState &sharedState) {
- SergeantRoomDialogueState &state = _state;
+static void returnItemToRah(DialogueRuntime &runtime, const char *objectName, bool visible) {
+ resetAndSetObjectVisible(runtime, kRahRoomName, objectName, visible);
+}
- auto hasInventoryItem = [&](const char *objectName) {
- Common::Array<ObjectRecord> inventoryObjects;
- runtime.startupScript().getVisibleInventoryObjects(inventoryObjects);
- for (const ObjectRecord &inventoryObject : inventoryObjects) {
- if (inventoryObject.objectName.equalsIgnoreCase(objectName))
- return true;
- }
+static Common::Error executeActionTagIfSet(DialogueRuntime &runtime, const char *actionTag) {
+ runtime.executeAndQueueAction(actionTag);
+ return Common::kNoError;
+}
- return false;
- };
- auto resetAndSetObjectVisible = [&](const char *ownerOrRoom, const char *objectName, bool visible) {
- (void)runtime.startupScript().resetRuntimeObjectToInitialState(objectName);
- (void)runtime.startupScript().setRuntimeObjectVisible(ownerOrRoom, objectName, visible);
- };
- auto returnItemToRah = [&](const char *objectName, bool visible) {
- resetAndSetObjectVisible(kRahRoomName, objectName, visible);
- };
- auto executeActionTagIfSet = [&](const char *actionTag) -> Common::Error {
- runtime.executeAndQueueAction(actionTag);
+static void queueNpcSlashTransition(DialogueRuntime &runtime, const char *npcName) {
+ InteractionResult interaction;
+ if (runtime.startupScript().finalizeRuntimeNpcDeathOrMonsterfy(
+ npcName, kSlashDeathDamageType)) {
+ interaction.mutatedRuntimeState = true;
+ }
+ runtime.queueDialogueInteractionIfNeeded(interaction);
+}
+
+} // End of namespace
+
+Common::Error SergeantDialogueHandler::runDay5ExitIfNeeded(DialogueRuntime &runtime) {
+ if (!runtime.startupScript().getFlagValue("DAY_5") || _state.day5ExitLinePlayed)
return Common::kNoError;
+
+ _state.day5ExitLinePlayed = true;
+ return runtime.playDialogueLineWithVariant(0x4324, "SERGEANT", 1);
+}
+
+Common::Error SergeantDialogueHandler::handleRemainsBranch(DialogueRuntime &runtime,
+ DialogueSharedState &sharedState) {
+ sharedState.dialogueStateD2f08 = true;
+ sharedState.dialogueStateD2f00 = true;
+ _state.talkStateBlock = true;
+ _state.dialogueStateD2d50 = true;
+ _state.postApplicationBriefingCompleted = true;
+ _state.dialogueStateD2d58 = true;
+ _state.dialogueStateD2d5c = true;
+ _state.dialogueStateD2d60 = true;
+ _state.dialogueStateD2d64 = true;
+ _state.dialogueStateD2d68 = true;
+ _state.dialogueStateD2d6c = true;
+ returnItemToRah(runtime, "REMAINS", false);
+
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x4333, "SERGEANT", 1);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ lineError = runtime.playDialogueFst(kDialogueC150FstPath);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ const DialogueLineEntry remainsIntroLines[] = {
+ { 0x433f, "SERGEANT", 0 },
+ { 0x4344, "PC", 2 },
+ { 0x4348, "SERGEANT", 0 },
+ { 0x434f, "PC", 0 },
+ { 0x4353, "SERGEANT", 1 }
};
- auto queueNpcSlashTransition = [&](const char *npcName) {
- InteractionResult interaction;
- if (runtime.startupScript().finalizeRuntimeNpcDeathOrMonsterfy(
- npcName, kSlashDeathDamageType)) {
- interaction.mutatedRuntimeState = true;
- }
- runtime.queueDialogueInteractionIfNeeded(interaction);
+ lineError = runtime.playDialogueEntrySequence(remainsIntroLines, ARRAYSIZE(remainsIntroLines));
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(
+ kSergeantRemainsResponseLine, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ lineError = runtime.playDialogueLineWithVariant(0x435f, "SERGEANT", 3);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ } else if (responseIndex == 2) {
+ lineError = runtime.playDialogueLineWithVariant(0x4364, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ }
+
+ const DialogueLineEntry remainsExitLines[] = {
+ { 0x4368, "SERGEANT", 0 },
+ { 0x4372, "PC", 2 },
+ { 0x4376, "SERGEANT", 0 },
+ { 0x4381, "PC", 2 },
+ { 0x4385, "SERGEANT", 0 }
};
- auto runDay5ExitIfNeeded = [&]() -> Common::Error {
- if (!runtime.startupScript().getFlagValue("DAY_5") || state.day5ExitLinePlayed)
- return Common::kNoError;
+ lineError = runtime.playDialogueEntrySequence(remainsExitLines, ARRAYSIZE(remainsExitLines));
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ lineError = runtime.clearScreenToBlack();
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ lineError = runtime.playDialogueFst(kDialogueC076FstPath);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ lineError = runtime.showCdChangePrompt(3);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ // Native prompt_for_cdrom_disc_change(3, 0) latches a main-loop room rebuild.
+ InteractionResult interaction;
+ interaction.requestRoomRestart = true;
+ interaction.roomRestartTargetName = "START";
+ runtime.queueDialogueInteractionIfNeeded(interaction);
+ return Common::kNoError;
+}
- state.day5ExitLinePlayed = true;
- return runtime.playDialogueLineWithVariant(0x4324, "SERGEANT", 1);
+Common::Error SergeantDialogueHandler::handleInviteBranch(DialogueRuntime &runtime,
+ DialogueSharedState &sharedState) {
+ sharedState.dialogueStateD2f08 = true;
+ sharedState.dialogueStateD2f00 = true;
+ _state.talkStateBlock = true;
+ _state.dialogueStateD2d50 = true;
+ _state.postApplicationBriefingCompleted = true;
+ _state.dialogueStateD2d58 = true;
+ _state.dialogueStateD2d5c = true;
+ _state.dialogueStateD2d60 = true;
+ _state.dialogueStateD2d64 = true;
+ _state.dialogueStateD2d68 = true;
+ resetAndSetObjectVisible(runtime, "ST_BEDRM", "STEFSKULL", true);
+ returnItemToRah(runtime, "INVITE", false);
+ (void)runtime.startupScript().setRuntimeFlagValue("TAKEN_INVITE_TO_SERGEANT", true);
+ (void)runtime.startupScript().setRuntimeFlagValue("NEED_REMAINS_FOR_LODGE", true);
+ (void)runtime.startupScript().setRuntimeNpcState("DWAYNE_ST_BEDRM", false, false);
+ (void)runtime.startupScript().setRuntimeNpcState("MRS_POTTS_ST_BEDRM", false, false);
+ (void)runtime.startupScript().setRuntimeNpcState("MRS_POTTS", true, true);
+
+ Common::Error lineError = runtime.playDialogueFst(kDialogueC149FstPath);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ const DialogueLineEntry inviteLines[] = {
+ { 0x4305, "PC", 0 },
+ { 0x4309, "SERGEANT", 2 }
};
- auto handleRemainsBranch = [&]() -> Common::Error {
- sharedState.dialogueStateD2f08 = true;
- sharedState.dialogueStateD2f00 = true;
- state.talkStateBlock = true;
- state.dialogueStateD2d50 = true;
- state.postApplicationBriefingCompleted = true;
- state.dialogueStateD2d58 = true;
- state.dialogueStateD2d5c = true;
- state.dialogueStateD2d60 = true;
- state.dialogueStateD2d64 = true;
- state.dialogueStateD2d68 = true;
- state.dialogueStateD2d6c = true;
- returnItemToRah("REMAINS", false);
-
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x4333, "SERGEANT", 1);
+ lineError = runtime.playDialogueEntrySequence(inviteLines, ARRAYSIZE(inviteLines));
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(
+ kSergeantInviteResponseLine, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ lineError = runtime.playDialogueLineWithVariant(0x4314, "SERGEANT", 2);
if (lineError.getCode() != Common::kNoError)
return lineError;
- lineError = runtime.playDialogueFst(kDialogueC150FstPath);
+ } else if (responseIndex == 2) {
+ lineError = runtime.playDialogueLineWithVariant(0x4319, "SERGEANT", 2);
if (lineError.getCode() != Common::kNoError)
return lineError;
- const DialogueLineEntry remainsIntroLines[] = {
- { 0x433f, "SERGEANT", 0 },
- { 0x4344, "PC", 2 },
- { 0x4348, "SERGEANT", 0 },
- { 0x434f, "PC", 0 },
- { 0x4353, "SERGEANT", 1 }
- };
- lineError = runtime.playDialogueEntrySequence(remainsIntroLines, ARRAYSIZE(remainsIntroLines));
+ }
+
+ lineError = runtime.playDialogueLineWithVariant(0x431d, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ return runDay5ExitIfNeeded(runtime);
+}
+
+Common::Error SergeantDialogueHandler::handleBarberPoleBranch(DialogueRuntime &runtime,
+ DialogueSharedState &sharedState) {
+ sharedState.dialogueStateD2f08 = true;
+ sharedState.dialogueStateD2f00 = true;
+ _state.talkStateBlock = true;
+ _state.dialogueStateD2d50 = true;
+ _state.postApplicationBriefingCompleted = true;
+ _state.dialogueStateD2d58 = true;
+ _state.dialogueStateD2d5c = true;
+ _state.dialogueStateD2d60 = true;
+ returnItemToRah(runtime, "BARBER_POLE", false);
+ (void)runtime.startupScript().setRuntimeFlagValue("ASSIGNED_DNA_TASK", true);
+
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x426f, "SERGEANT", 1);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ lineError = runtime.playDialogueLineWithVariant(0x4274, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(
+ kSergeantBarberPoleResponseLine, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ lineError = runtime.playDialogueLineWithVariant(0x4281, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
+ } else if (responseIndex == 2) {
+ lineError = runtime.playDialogueLineWithVariant(0x4289, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ }
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(
- kSergeantRemainsResponseLine, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
-
- if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x435f, "SERGEANT", 3);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- } else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x4364, "SERGEANT", 0);
+ lineError = runtime.playDialogueLineWithVariant(0x428d, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ lineError = runtime.playDialogueLineWithVariant(0x4299, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ responseIndex = 0;
+ responseError = runtime.runResponseMenu(
+ kSergeantBarberPoleFollowupResponseLine, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ lineError = runtime.playDialogueLineWithVariant(0x42a4, "SERGEANT", 3);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ if (runtime.startupScript().getFlagValue("PC_KILLED_ANYONE")) {
+ lineError = runtime.playDialogueLineWithVariant(0x42a8, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
}
-
- const DialogueLineEntry remainsExitLines[] = {
- { 0x4368, "SERGEANT", 0 },
- { 0x4372, "PC", 2 },
- { 0x4376, "SERGEANT", 0 },
- { 0x4381, "PC", 2 },
- { 0x4385, "SERGEANT", 0 }
- };
- lineError = runtime.playDialogueEntrySequence(remainsExitLines, ARRAYSIZE(remainsExitLines));
+ } else if (responseIndex == 2) {
+ lineError = runtime.playDialogueLineWithVariant(0x42ad, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
+ }
- lineError = runtime.clearScreenToBlack();
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- lineError = runtime.playDialogueFst(kDialogueC076FstPath);
+ return runDay5ExitIfNeeded(runtime);
+}
+
+Common::Error SergeantDialogueHandler::handleBoltClothBranch(DialogueRuntime &runtime,
+ DialogueSharedState &sharedState) {
+ sharedState.dialogueStateD2f08 = true;
+ sharedState.dialogueStateD2f00 = true;
+ _state.talkStateBlock = true;
+ _state.dialogueStateD2d50 = true;
+ _state.postApplicationBriefingCompleted = true;
+ _state.dialogueStateD2d58 = true;
+ _state.dialogueStateD2d5c = true;
+
+ const DialogueLineEntry boltClothLines[] = {
+ { 0x4241, "SERGEANT", 1 },
+ { 0x4245, "SERGEANT", 1 },
+ { 0x4252, "SERGEANT", 0 }
+ };
+ Common::Error lineError = runtime.playDialogueEntrySequence(boltClothLines, ARRAYSIZE(boltClothLines));
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(
+ kSergeantBoltClothResponseLine, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ lineError = runtime.playDialogueLineWithVariant(0x425e, "SERGEANT", 1);
if (lineError.getCode() != Common::kNoError)
return lineError;
- lineError = runtime.showCdChangePrompt(3);
+ } else if (responseIndex == 2) {
+ lineError = runtime.playDialogueLineWithVariant(0x4266, "SERGEANT", 1);
if (lineError.getCode() != Common::kNoError)
return lineError;
+ }
- // Native prompt_for_cdrom_disc_change(3, 0) latches a main-loop room rebuild.
- InteractionResult interaction;
- interaction.requestRoomRestart = true;
- interaction.roomRestartTargetName = "START";
- runtime.queueDialogueInteractionIfNeeded(interaction);
- return Common::kNoError;
- };
- auto handleInviteBranch = [&]() -> Common::Error {
- sharedState.dialogueStateD2f08 = true;
- sharedState.dialogueStateD2f00 = true;
- state.talkStateBlock = true;
- state.dialogueStateD2d50 = true;
- state.postApplicationBriefingCompleted = true;
- state.dialogueStateD2d58 = true;
- state.dialogueStateD2d5c = true;
- state.dialogueStateD2d60 = true;
- state.dialogueStateD2d64 = true;
- state.dialogueStateD2d68 = true;
- resetAndSetObjectVisible("ST_BEDRM", "STEFSKULL", true);
- returnItemToRah("INVITE", false);
- (void)runtime.startupScript().setRuntimeFlagValue("TAKEN_INVITE_TO_SERGEANT", true);
- (void)runtime.startupScript().setRuntimeFlagValue("NEED_REMAINS_FOR_LODGE", true);
- (void)runtime.startupScript().setRuntimeNpcState("DWAYNE_ST_BEDRM", false, false);
- (void)runtime.startupScript().setRuntimeNpcState("MRS_POTTS_ST_BEDRM", false, false);
- (void)runtime.startupScript().setRuntimeNpcState("MRS_POTTS", true, true);
-
- Common::Error lineError = runtime.playDialogueFst(kDialogueC149FstPath);
+ returnItemToRah(runtime, "BOLTCLTH", false);
+ (void)runtime.startupScript().setRuntimeFlagValue("FIREMEN_SUICIDE", true);
+ (void)runtime.startupScript().setRuntimeFlagValue("ASSIGNED_POLE_TASK", true);
+ queueNpcSlashTransition(runtime, "SPARKY");
+ queueNpcSlashTransition(runtime, "NUDE_MAN");
+ queueNpcSlashTransition(runtime, "FIREMAN2");
+ return runDay5ExitIfNeeded(runtime);
+}
+
+Common::Error SergeantDialogueHandler::handleCompletedFirstTaskBranch(DialogueRuntime &runtime,
+ DialogueSharedState &sharedState) {
+ sharedState.dialogueStateD2f08 = true;
+ sharedState.dialogueStateD2f00 = true;
+ sharedState.sergeantCompletedFirstTaskState = 1;
+ _state.talkStateBlock = true;
+ _state.dialogueStateD2d50 = true;
+ _state.postApplicationBriefingCompleted = true;
+ _state.completedFirstTaskState = true;
+
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x420d, "SERGEANT", 1);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ lineError = runtime.playDialogueLineWithVariant(0x421e, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(
+ kSergeantSecondTaskResponseLine, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ lineError = runtime.playDialogueLineWithVariant(0x422a, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
- const DialogueLineEntry inviteLines[] = {
- { 0x4305, "PC", 0 },
- { 0x4309, "SERGEANT", 2 }
+ } else if (responseIndex == 2) {
+ const DialogueLineEntry reluctantTaskLines[] = {
+ { 0x422f, "SERGEANT", 2 },
+ { 0x4239, "SERGEANT", 3 }
};
- lineError = runtime.playDialogueEntrySequence(inviteLines, ARRAYSIZE(inviteLines));
+ lineError = runtime.playDialogueEntrySequence(reluctantTaskLines, ARRAYSIZE(reluctantTaskLines));
if (lineError.getCode() != Common::kNoError)
return lineError;
+ }
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(
- kSergeantInviteResponseLine, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
-
- if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x4314, "SERGEANT", 2);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- } else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x4319, "SERGEANT", 2);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
+ if (!_state.dialogueStateD2d58) {
+ _state.dialogueStateD2d58 = true;
+ Common::Error actionError = executeActionTagIfSet(runtime, kGoDay3ActionTag);
+ if (actionError.getCode() != Common::kNoError)
+ return actionError;
+ }
+ return runDay5ExitIfNeeded(runtime);
+}
- lineError = runtime.playDialogueLineWithVariant(0x431d, "SERGEANT", 0);
+Common::Error SergeantDialogueHandler::handleCompletedApplicationBranch(DialogueRuntime &runtime,
+ DialogueSharedState &sharedState) {
+ sharedState.dialogueStateD2f08 = true;
+ sharedState.dialogueStateD2f00 = true;
+ _state.talkStateBlock = true;
+ _state.dialogueStateD2d50 = true;
+ returnItemToRah(runtime, "COMPLETED_LODGE_APPLICATION", false);
+ if (!runtime.startupScript().getFlagValue("QUEST_1")) {
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x41cd, "SERGEANT", 1);
if (lineError.getCode() != Common::kNoError)
return lineError;
- return runDay5ExitIfNeeded();
- };
- auto handleBarberPoleBranch = [&]() -> Common::Error {
- sharedState.dialogueStateD2f08 = true;
- sharedState.dialogueStateD2f00 = true;
- state.talkStateBlock = true;
- state.dialogueStateD2d50 = true;
- state.postApplicationBriefingCompleted = true;
- state.dialogueStateD2d58 = true;
- state.dialogueStateD2d5c = true;
- state.dialogueStateD2d60 = true;
- returnItemToRah("BARBER_POLE", false);
- (void)runtime.startupScript().setRuntimeFlagValue("ASSIGNED_DNA_TASK", true);
-
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x426f, "SERGEANT", 1);
+ lineError = runtime.playDialogueLineWithVariant(0x4292, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
- lineError = runtime.playDialogueLineWithVariant(0x4274, "SERGEANT", 0);
+ (void)runtime.startupScript().setRuntimeFlagValue("QUEST_1", true);
+ return executeActionTagIfSet(runtime, kGoDay2ActionTag);
+ }
+ if (!_state.postApplicationBriefingCompleted) {
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x41d2, "SERGEANT", 1);
if (lineError.getCode() != Common::kNoError)
return lineError;
int responseIndex = 0;
Common::Error responseError = runtime.runResponseMenu(
- kSergeantBarberPoleResponseLine, responseIndex);
+ kSergeantCompletedApplicationResponseLine, responseIndex);
if (responseError.getCode() != Common::kNoError)
return responseError;
if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x4281, "SERGEANT", 0);
+ const DialogueLineEntry acceptedTaskLines[] = {
+ { 0x41db, "SERGEANT", 0 },
+ { 0x41e2, "PC", 0 },
+ { 0x41e6, "SERGEANT", 0 },
+ { 0x41ec, "PC", 2 },
+ { 0x41f0, "SERGEANT", 0 },
+ { 0x41f6, "PC", 0 },
+ { 0x41fa, "SERGEANT", 0 }
+ };
+ lineError = runtime.playDialogueEntrySequence(
+ acceptedTaskLines, ARRAYSIZE(acceptedTaskLines));
if (lineError.getCode() != Common::kNoError)
return lineError;
+ _state.postApplicationBriefingCompleted = true;
} else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x4289, "SERGEANT", 0);
+ const DialogueLineEntry declinedTaskLines[] = {
+ { 0x4201, "SERGEANT", 3 },
+ { 0x4202, "SERGEANT", 3 },
+ { 0x4203, "SERGEANT", 2 }
+ };
+ lineError = runtime.playDialogueEntrySequence(
+ declinedTaskLines, ARRAYSIZE(declinedTaskLines));
if (lineError.getCode() != Common::kNoError)
return lineError;
+ _state.postApplicationBriefingCompleted = false;
}
- lineError = runtime.playDialogueLineWithVariant(0x428d, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- lineError = runtime.playDialogueLineWithVariant(0x4299, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
-
- responseIndex = 0;
- responseError = runtime.runResponseMenu(
- kSergeantBarberPoleFollowupResponseLine, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
-
- if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x42a4, "SERGEANT", 3);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- if (runtime.startupScript().getFlagValue("PC_KILLED_ANYONE")) {
- lineError = runtime.playDialogueLineWithVariant(0x42a8, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
- } else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x42ad, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
+ return runDay5ExitIfNeeded(runtime);
+ }
- return runDay5ExitIfNeeded();
+ const DialogueLineEntry repeatTaskLines[] = {
+ { 0x41f0, "SERGEANT", 0 },
+ { 0x41f6, "PC", 0 },
+ { 0x41fa, "SERGEANT", 0 }
};
- auto handleBoltClothBranch = [&]() -> Common::Error {
+ Common::Error lineError = runtime.playDialogueEntrySequence(
+ repeatTaskLines, ARRAYSIZE(repeatTaskLines));
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ return runDay5ExitIfNeeded(runtime);
+}
+
+Common::Error SergeantDialogueHandler::runIntroOrReminderBranch(DialogueRuntime &runtime,
+ DialogueSharedState &sharedState) {
+ if (!_state.talkStateBlock) {
sharedState.dialogueStateD2f08 = true;
- sharedState.dialogueStateD2f00 = true;
- state.talkStateBlock = true;
- state.dialogueStateD2d50 = true;
- state.postApplicationBriefingCompleted = true;
- state.dialogueStateD2d58 = true;
- state.dialogueStateD2d5c = true;
-
- const DialogueLineEntry boltClothLines[] = {
- { 0x4241, "SERGEANT", 1 },
- { 0x4245, "SERGEANT", 1 },
- { 0x4252, "SERGEANT", 0 }
+ _state.talkStateBlock = true;
+
+ const DialogueLineEntry introLines[] = {
+ { 0x413f, "SERGEANT", 2 },
+ { 0x4144, "PC", 0 },
+ { 0x4145, "SERGEANT", 0 },
+ { 0x4146, "PC", 0 },
+ { 0x4148, "SERGEANT", 2 }
};
- Common::Error lineError = runtime.playDialogueEntrySequence(boltClothLines, ARRAYSIZE(boltClothLines));
+ Common::Error lineError = runtime.playDialogueEntrySequence(introLines, ARRAYSIZE(introLines));
if (lineError.getCode() != Common::kNoError)
return lineError;
int responseIndex = 0;
Common::Error responseError = runtime.runResponseMenu(
- kSergeantBoltClothResponseLine, responseIndex);
+ kSergeantIntroResponseLine, responseIndex);
if (responseError.getCode() != Common::kNoError)
return responseError;
if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x425e, "SERGEANT", 1);
+ lineError = runtime.playDialogueLineWithVariant(0x4153, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
} else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x4266, "SERGEANT", 1);
+ lineError = runtime.playDialogueLineWithVariant(0x4157, "SERGEANT", 1);
if (lineError.getCode() != Common::kNoError)
return lineError;
}
- returnItemToRah("BOLTCLTH", false);
- (void)runtime.startupScript().setRuntimeFlagValue("FIREMEN_SUICIDE", true);
- (void)runtime.startupScript().setRuntimeFlagValue("ASSIGNED_POLE_TASK", true);
- queueNpcSlashTransition("SPARKY");
- queueNpcSlashTransition("NUDE_MAN");
- queueNpcSlashTransition("FIREMAN2");
- return runDay5ExitIfNeeded();
- };
- auto handleCompletedFirstTaskBranch = [&]() -> Common::Error {
- sharedState.dialogueStateD2f08 = true;
- sharedState.dialogueStateD2f00 = true;
- sharedState.sergeantCompletedFirstTaskState = 1;
- state.talkStateBlock = true;
- state.dialogueStateD2d50 = true;
- state.postApplicationBriefingCompleted = true;
- state.completedFirstTaskState = true;
-
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x420d, "SERGEANT", 1);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- lineError = runtime.playDialogueLineWithVariant(0x421e, "SERGEANT", 0);
+ lineError = runtime.playDialogueLineWithVariant(0x415d, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(
- kSergeantSecondTaskResponseLine, responseIndex);
+ responseIndex = 0;
+ responseError = runtime.runResponseMenu(
+ kSergeantApplicationResponseLine, responseIndex);
if (responseError.getCode() != Common::kNoError)
return responseError;
if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x422a, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- } else if (responseIndex == 2) {
- const DialogueLineEntry reluctantTaskLines[] = {
- { 0x422f, "SERGEANT", 2 },
- { 0x4239, "SERGEANT", 3 }
+ const DialogueLineEntry applicationLines[] = {
+ { 0x4168, "SERGEANT", 1 },
+ { 0x416c, "PC", 0 },
+ { 0x4170, "SERGEANT", 1 }
};
- lineError = runtime.playDialogueEntrySequence(reluctantTaskLines, ARRAYSIZE(reluctantTaskLines));
+ lineError = runtime.playDialogueEntrySequence(applicationLines, ARRAYSIZE(applicationLines));
if (lineError.getCode() != Common::kNoError)
return lineError;
- }
- if (!state.dialogueStateD2d58) {
- state.dialogueStateD2d58 = true;
- Common::Error actionError = executeActionTagIfSet(kGoDay3ActionTag);
- if (actionError.getCode() != Common::kNoError)
- return actionError;
- }
- return runDay5ExitIfNeeded();
- };
- auto handleCompletedApplicationBranch = [&]() -> Common::Error {
- sharedState.dialogueStateD2f08 = true;
- sharedState.dialogueStateD2f00 = true;
- state.talkStateBlock = true;
- state.dialogueStateD2d50 = true;
- returnItemToRah("COMPLETED_LODGE_APPLICATION", false);
- if (!runtime.startupScript().getFlagValue("QUEST_1")) {
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x41cd, "SERGEANT", 1);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- lineError = runtime.playDialogueLineWithVariant(0x4292, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- (void)runtime.startupScript().setRuntimeFlagValue("QUEST_1", true);
- return executeActionTagIfSet(kGoDay2ActionTag);
- }
- if (!state.postApplicationBriefingCompleted) {
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x41d2, "SERGEANT", 1);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
-
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(
- kSergeantCompletedApplicationResponseLine, responseIndex);
+ responseIndex = 0;
+ responseError = runtime.runResponseMenu(
+ kSergeantAmnesiaResponseLine, responseIndex);
if (responseError.getCode() != Common::kNoError)
return responseError;
if (responseIndex == 1) {
- const DialogueLineEntry acceptedTaskLines[] = {
- { 0x41db, "SERGEANT", 0 },
- { 0x41e2, "PC", 0 },
- { 0x41e6, "SERGEANT", 0 },
- { 0x41ec, "PC", 2 },
- { 0x41f0, "SERGEANT", 0 },
- { 0x41f6, "PC", 0 },
- { 0x41fa, "SERGEANT", 0 }
- };
- lineError = runtime.playDialogueEntrySequence(
- acceptedTaskLines, ARRAYSIZE(acceptedTaskLines));
+ lineError = runtime.playDialogueLineWithVariant(0x417c, "SERGEANT", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
- state.postApplicationBriefingCompleted = true;
} else if (responseIndex == 2) {
- const DialogueLineEntry declinedTaskLines[] = {
- { 0x4201, "SERGEANT", 3 },
- { 0x4202, "SERGEANT", 3 },
- { 0x4203, "SERGEANT", 2 }
- };
- lineError = runtime.playDialogueEntrySequence(
- declinedTaskLines, ARRAYSIZE(declinedTaskLines));
+ lineError = runtime.playDialogueLineWithVariant(0x4182, "SERGEANT", 2);
if (lineError.getCode() != Common::kNoError)
return lineError;
- state.postApplicationBriefingCompleted = false;
}
-
- return runDay5ExitIfNeeded();
- }
-
- const DialogueLineEntry repeatTaskLines[] = {
- { 0x41f0, "SERGEANT", 0 },
- { 0x41f6, "PC", 0 },
- { 0x41fa, "SERGEANT", 0 }
- };
- Common::Error lineError = runtime.playDialogueEntrySequence(
- repeatTaskLines, ARRAYSIZE(repeatTaskLines));
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- return runDay5ExitIfNeeded();
- };
- auto runIntroOrReminderBranch = [&]() -> Common::Error {
- if (!state.talkStateBlock) {
- sharedState.dialogueStateD2f08 = true;
- state.talkStateBlock = true;
-
- const DialogueLineEntry introLines[] = {
- { 0x413f, "SERGEANT", 2 },
- { 0x4144, "PC", 0 },
- { 0x4145, "SERGEANT", 0 },
- { 0x4146, "PC", 0 },
- { 0x4148, "SERGEANT", 2 }
+ } else {
+ const DialogueLineEntry applicationLines[] = {
+ { 0x4188, "SERGEANT", 0 },
+ { 0x418e, "PC", 0 },
+ { 0x4192, "SERGEANT", 0 }
};
- Common::Error lineError = runtime.playDialogueEntrySequence(introLines, ARRAYSIZE(introLines));
+ lineError = runtime.playDialogueEntrySequence(applicationLines, ARRAYSIZE(applicationLines));
if (lineError.getCode() != Common::kNoError)
return lineError;
-
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(
- kSergeantIntroResponseLine, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
-
- if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x4153, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- } else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x4157, "SERGEANT", 1);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
-
- lineError = runtime.playDialogueLineWithVariant(0x415d, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
-
- responseIndex = 0;
- responseError = runtime.runResponseMenu(
- kSergeantApplicationResponseLine, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
-
- if (responseIndex == 1) {
- const DialogueLineEntry applicationLines[] = {
- { 0x4168, "SERGEANT", 1 },
- { 0x416c, "PC", 0 },
- { 0x4170, "SERGEANT", 1 }
- };
- lineError = runtime.playDialogueEntrySequence(applicationLines, ARRAYSIZE(applicationLines));
- if (lineError.getCode() != Common::kNoError)
- return lineError;
-
- responseIndex = 0;
- responseError = runtime.runResponseMenu(
- kSergeantAmnesiaResponseLine, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
-
- if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x417c, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- } else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x4182, "SERGEANT", 2);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
- } else {
- const DialogueLineEntry applicationLines[] = {
- { 0x4188, "SERGEANT", 0 },
- { 0x418e, "PC", 0 },
- { 0x4192, "SERGEANT", 0 }
- };
- lineError = runtime.playDialogueEntrySequence(applicationLines, ARRAYSIZE(applicationLines));
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
-
- return runDay5ExitIfNeeded();
}
- state.talkStateBlock = true;
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x41ac, "SERGEANT", 0);
+ return runDay5ExitIfNeeded(runtime);
+ }
+
+ _state.talkStateBlock = true;
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x41ac, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ if (!runtime.startupScript().getFlagValue("HAVE_LODGE_APP")) {
+ lineError = runtime.playDialogueLineWithVariant(0x41b0, "PC", 0);
if (lineError.getCode() != Common::kNoError)
return lineError;
- if (!runtime.startupScript().getFlagValue("HAVE_LODGE_APP")) {
- lineError = runtime.playDialogueLineWithVariant(0x41b0, "PC", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- }
- lineError = runtime.playDialogueLineWithVariant(0x41b4, "SERGEANT", 3);
+ }
+ lineError = runtime.playDialogueLineWithVariant(0x41b4, "SERGEANT", 3);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(
+ kSergeantReminderResponseLine, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ lineError = runtime.playDialogueLineWithVariant(0x41bf, "SERGEANT", 2);
if (lineError.getCode() != Common::kNoError)
return lineError;
+ _state.dialogueStateD2d50 = true;
+ } else if (responseIndex == 2) {
+ lineError = runtime.playDialogueLineWithVariant(0x41c4, "SERGEANT", 0);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+ }
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(
- kSergeantReminderResponseLine, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
+ return runDay5ExitIfNeeded(runtime);
+}
- if (responseIndex == 1) {
- lineError = runtime.playDialogueLineWithVariant(0x41bf, "SERGEANT", 2);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
- state.dialogueStateD2d50 = true;
- } else if (responseIndex == 2) {
- lineError = runtime.playDialogueLineWithVariant(0x41c4, "SERGEANT", 0);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
+Common::Error SergeantDialogueHandler::handleDialogue(DialogueRuntime &runtime,
+ const Common::String &usedItemName, DialogueSharedState &sharedState) {
+ SergeantRoomDialogueState &state = _state;
+
+ auto hasInventoryItem = [&](const char *objectName) {
+ Common::Array<ObjectRecord> inventoryObjects;
+ runtime.startupScript().getVisibleInventoryObjects(inventoryObjects);
+ for (const ObjectRecord &inventoryObject : inventoryObjects) {
+ if (inventoryObject.objectName.equalsIgnoreCase(objectName))
+ return true;
}
- return runDay5ExitIfNeeded();
+ return false;
};
if (usedItemName.empty()) {
if (runtime.startupScript().getFlagValue("GOT_REMAINS_FOR_LODGE"))
- return handleRemainsBranch();
+ return handleRemainsBranch(runtime, sharedState);
if (runtime.startupScript().getFlagValue("STEPHANIE_IS_DEAD") &&
(hasInventoryItem("INVITE") ||
runtime.startupScript().getFlagValue("TAKEN_INVITE_TO_SERGEANT"))) {
- return handleInviteBranch();
+ return handleInviteBranch(runtime, sharedState);
}
if (runtime.startupScript().getFlagValue("DINER_BURNED")) {
sharedState.dialogueStateD2f08 = true;
@@ -623,36 +643,36 @@ Common::Error SergeantDialogueHandler::handleDialogue(DialogueRuntime &runtime,
if (lineError.getCode() != Common::kNoError)
return lineError;
- resetAndSetObjectVisible("ST_BEDRM", "INVITE", true);
- resetAndSetObjectVisible("ST_BEDRM", "STEFSKULL", true);
+ resetAndSetObjectVisible(runtime, "ST_BEDRM", "INVITE", true);
+ resetAndSetObjectVisible(runtime, "ST_BEDRM", "STEFSKULL", true);
(void)runtime.startupScript().setRuntimeNpcState("DWAYNE_ST_BEDRM", true, true);
(void)runtime.startupScript().setRuntimeFlagValue("STEPHANIE_IS_DEAD", true);
(void)runtime.startupScript().setRuntimeFlagValue(
"STEPHANIE_DEAD_IN_HALL_JUST_OUTSIDE", true);
- return runDay5ExitIfNeeded();
+ return runDay5ExitIfNeeded(runtime);
}
if (runtime.startupScript().getFlagValue("BARBER_POLE_STOLEN"))
- return handleBarberPoleBranch();
+ return handleBarberPoleBranch(runtime, sharedState);
if (runtime.startupScript().getFlagValue("BOLT_OF_CLOTH_TAKEN"))
- return handleBoltClothBranch();
+ return handleBoltClothBranch(runtime, sharedState);
if (runtime.startupScript().getFlagValue("SCRATCHED_TUCKER")) {
- return handleCompletedFirstTaskBranch();
+ return handleCompletedFirstTaskBranch(runtime, sharedState);
}
if (runtime.startupScript().getFlagValue("HAVE_COMPLETED_LODGE_APP"))
- return handleCompletedApplicationBranch();
- return runIntroOrReminderBranch();
+ return handleCompletedApplicationBranch(runtime, sharedState);
+ return runIntroOrReminderBranch(runtime, sharedState);
}
if (usedItemName.equalsIgnoreCase("BARBER_POLE"))
- return handleBarberPoleBranch();
+ return handleBarberPoleBranch(runtime, sharedState);
if (usedItemName.equalsIgnoreCase("BOLTCLTH"))
- return handleBoltClothBranch();
+ return handleBoltClothBranch(runtime, sharedState);
if (usedItemName.equalsIgnoreCase("COMPLETED_LODGE_APPLICATION"))
- return handleCompletedApplicationBranch();
+ return handleCompletedApplicationBranch(runtime, sharedState);
if (usedItemName.equalsIgnoreCase("INVITE"))
- return handleInviteBranch();
+ return handleInviteBranch(runtime, sharedState);
if (usedItemName.equalsIgnoreCase("REMAINS"))
- return handleRemainsBranch();
+ return handleRemainsBranch(runtime, sharedState);
return runtime.playDialogueLineWithVariant(0x42fa, "SERGEANT", 0);
}
diff --git a/engines/harvester/npc/sergeant_dialogue.h b/engines/harvester/npc/sergeant_dialogue.h
index 41e95cd92b9..a4292564551 100644
--- a/engines/harvester/npc/sergeant_dialogue.h
+++ b/engines/harvester/npc/sergeant_dialogue.h
@@ -64,6 +64,15 @@ public:
const Common::String &usedItemName, DialogueSharedState &sharedState) override;
private:
+ Common::Error runDay5ExitIfNeeded(DialogueRuntime &runtime);
+ Common::Error handleRemainsBranch(DialogueRuntime &runtime, DialogueSharedState &sharedState);
+ Common::Error handleInviteBranch(DialogueRuntime &runtime, DialogueSharedState &sharedState);
+ Common::Error handleBarberPoleBranch(DialogueRuntime &runtime, DialogueSharedState &sharedState);
+ Common::Error handleBoltClothBranch(DialogueRuntime &runtime, DialogueSharedState &sharedState);
+ Common::Error handleCompletedFirstTaskBranch(DialogueRuntime &runtime, DialogueSharedState &sharedState);
+ Common::Error handleCompletedApplicationBranch(DialogueRuntime &runtime, DialogueSharedState &sharedState);
+ Common::Error runIntroOrReminderBranch(DialogueRuntime &runtime, DialogueSharedState &sharedState);
+
SergeantRoomDialogueState _state;
};
Commit: 35463df4363ab4807eea1fa4e0b00823bd453d6e
https://github.com/scummvm/scummvm/commit/35463df4363ab4807eea1fa4e0b00823bd453d6e
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Extract Jimmy dialogue routines into methods
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/npc/jimmy_dialogue.cpp
engines/harvester/npc/jimmy_dialogue.h
diff --git a/engines/harvester/npc/jimmy_dialogue.cpp b/engines/harvester/npc/jimmy_dialogue.cpp
index 37f74137387..9c40930e56b 100644
--- a/engines/harvester/npc/jimmy_dialogue.cpp
+++ b/engines/harvester/npc/jimmy_dialogue.cpp
@@ -78,110 +78,112 @@ static const DialogueLineEntry kJimmyBlackmailEvidenceLines[] = {
{ 0x4b31, "JIMMY", 1 }
};
-} // End of namespace
-
-Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
- const Common::String &usedItemName, DialogueSharedState &sharedState) {
- JimmyRoomDialogueState &state = _state;
- Common::Error lineError = Common::kNoError;
+static void restoreItemToRah(DialogueRuntime &runtime, const char *objectName) {
+ (void)runtime.startupScript().resetRuntimeObjectToInitialState(objectName);
+ (void)runtime.startupScript().setRuntimeObjectVisible(kRahRoomName, objectName, true);
+}
- auto hasInventoryItem = [&](const char *objectName) {
- return runtime.startupScript().isObjectInInventory(objectName);
- };
-
- auto restoreItemToRah = [&](const char *objectName) {
- (void)runtime.startupScript().resetRuntimeObjectToInitialState(objectName);
- (void)runtime.startupScript().setRuntimeObjectVisible(kRahRoomName, objectName, true);
- };
- auto completeSneakersTrade = [&]() {
- restoreItemToRah("SNEAKERS");
- (void)runtime.startupScript().addRuntimeObjectToInventory("BROOMKEY");
- };
- auto playPaperHandoffPreludeIfNeeded = [&]() -> Common::Error {
- if (state.paperHandoffStateSet)
- return Common::kNoError;
+static void completeSneakersTrade(DialogueRuntime &runtime) {
+ restoreItemToRah(runtime, "SNEAKERS");
+ (void)runtime.startupScript().addRuntimeObjectToInventory("BROOMKEY");
+}
- Common::Error preludeError = runtime.playDialogueEntrySequence(kJimmyPaperHandoffLines,
- ARRAYSIZE(kJimmyPaperHandoffLines));
- if (preludeError.getCode() != Common::kNoError)
- return preludeError;
+} // End of namespace
- state.paperHandoffStateSet = true;
+Common::Error JimmyDialogueHandler::playPaperHandoffPreludeIfNeeded(DialogueRuntime &runtime) {
+ if (_state.paperHandoffStateSet)
return Common::kNoError;
- };
- auto runSneakersOfferMenu = [&]() -> Common::Error {
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(kJimmyThirdResponseLineIndex, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
- if (responseIndex == 1) {
- if (hasInventoryItem("SNEAKERS")) {
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x4a9e, "JIMMY", 1);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
-
- completeSneakersTrade();
- return Common::kNoError;
- }
+ Common::Error preludeError = runtime.playDialogueEntrySequence(kJimmyPaperHandoffLines,
+ ARRAYSIZE(kJimmyPaperHandoffLines));
+ if (preludeError.getCode() != Common::kNoError)
+ return preludeError;
- return runtime.playDialogueEntrySequence(kJimmyNoSneakersLines, ARRAYSIZE(kJimmyNoSneakersLines));
- }
- if (responseIndex == 2)
- return runtime.playDialogueLineWithVariant(0x4ab4, "JIMMY", 1);
-
- return Common::kNoError;
- };
- auto runJimmySneakersPrompt = [&](int responseLineIndex) -> Common::Error {
- int responseIndex = 0;
- Common::Error responseError = runtime.runResponseMenu(responseLineIndex, responseIndex);
- if (responseError.getCode() != Common::kNoError)
- return responseError;
+ _state.paperHandoffStateSet = true;
+ return Common::kNoError;
+}
- if (responseIndex == 1) {
- Common::Error lineError = runtime.playDialogueLineWithVariant(0x4a86, "JIMMY", 1);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
+Common::Error JimmyDialogueHandler::runSneakersOfferMenu(DialogueRuntime &runtime) {
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(kJimmyThirdResponseLineIndex, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
- lineError = runtime.playDialogueEntrySequence(kJimmySecondResponsePreludeLines,
- ARRAYSIZE(kJimmySecondResponsePreludeLines));
+ if (responseIndex == 1) {
+ if (runtime.startupScript().isObjectInInventory("SNEAKERS")) {
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x4a9e, "JIMMY", 1);
if (lineError.getCode() != Common::kNoError)
return lineError;
- return runSneakersOfferMenu();
+ completeSneakersTrade(runtime);
+ return Common::kNoError;
}
- if (responseIndex == 2)
- return runtime.playDialogueLineWithVariant(0x4abc, "JIMMY", 0);
- return Common::kNoError;
- };
- auto playJimmyNoItemFallback = [&]() -> Common::Error {
- if (hasInventoryItem("SNEAKERS") && !hasInventoryItem("BROOMKEY")) {
- Common::Error sneakersLineError = runtime.playDialogueLineWithVariant(0x4ac3, "JIMMY", 0);
- if (sneakersLineError.getCode() != Common::kNoError)
- return sneakersLineError;
-
- Common::Error sneakersPromptError = runJimmySneakersPrompt(kJimmySneakersResponseLineIndex);
- if (sneakersPromptError.getCode() != Common::kNoError)
- return sneakersPromptError;
- }
- if (runtime.startupScript().getFlagValue("PAPER_CHK_4"))
- return runtime.playDialogueLineWithVariant(0x4ae2, "JIMMY", 2);
- if (runtime.startupScript().getFlagValue("PAPER_CHK_3"))
- return runtime.playDialogueLineWithVariant(0x4adb, "JIMMY", 2);
- if (runtime.startupScript().getFlagValue("PAPER_CHK_2"))
- return runtime.playDialogueLineWithVariant(0x4ad4, "JIMMY", 2);
+ return runtime.playDialogueEntrySequence(kJimmyNoSneakersLines, ARRAYSIZE(kJimmyNoSneakersLines));
+ }
+ if (responseIndex == 2)
+ return runtime.playDialogueLineWithVariant(0x4ab4, "JIMMY", 1);
+
+ return Common::kNoError;
+}
+
+Common::Error JimmyDialogueHandler::runJimmySneakersPrompt(DialogueRuntime &runtime, int responseLineIndex) {
+ int responseIndex = 0;
+ Common::Error responseError = runtime.runResponseMenu(responseLineIndex, responseIndex);
+ if (responseError.getCode() != Common::kNoError)
+ return responseError;
+
+ if (responseIndex == 1) {
+ Common::Error lineError = runtime.playDialogueLineWithVariant(0x4a86, "JIMMY", 1);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ lineError = runtime.playDialogueEntrySequence(kJimmySecondResponsePreludeLines,
+ ARRAYSIZE(kJimmySecondResponsePreludeLines));
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
- return runtime.playDialogueLineWithVariant(0x4b38, "JIMMY", 0);
- };
+ return runSneakersOfferMenu(runtime);
+ }
+ if (responseIndex == 2)
+ return runtime.playDialogueLineWithVariant(0x4abc, "JIMMY", 0);
+
+ return Common::kNoError;
+}
+
+Common::Error JimmyDialogueHandler::playJimmyNoItemFallback(DialogueRuntime &runtime) {
+ if (runtime.startupScript().isObjectInInventory("SNEAKERS") &&
+ !runtime.startupScript().isObjectInInventory("BROOMKEY")) {
+ Common::Error sneakersLineError = runtime.playDialogueLineWithVariant(0x4ac3, "JIMMY", 0);
+ if (sneakersLineError.getCode() != Common::kNoError)
+ return sneakersLineError;
+
+ Common::Error sneakersPromptError = runJimmySneakersPrompt(runtime, kJimmySneakersResponseLineIndex);
+ if (sneakersPromptError.getCode() != Common::kNoError)
+ return sneakersPromptError;
+ }
+ if (runtime.startupScript().getFlagValue("PAPER_CHK_4"))
+ return runtime.playDialogueLineWithVariant(0x4ae2, "JIMMY", 2);
+ if (runtime.startupScript().getFlagValue("PAPER_CHK_3"))
+ return runtime.playDialogueLineWithVariant(0x4adb, "JIMMY", 2);
+ if (runtime.startupScript().getFlagValue("PAPER_CHK_2"))
+ return runtime.playDialogueLineWithVariant(0x4ad4, "JIMMY", 2);
+
+ return runtime.playDialogueLineWithVariant(0x4b38, "JIMMY", 0);
+}
+
+Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
+ const Common::String &usedItemName, DialogueSharedState &sharedState) {
+ JimmyRoomDialogueState &state = _state;
+ Common::Error lineError = Common::kNoError;
if (!usedItemName.empty()) {
if (usedItemName.equalsIgnoreCase("NEWSPAPER")) {
(void)runtime.startupScript().setRuntimeFlagValue(kGivenPaperTodayFlag, true);
- restoreItemToRah("NEWSPAPER");
+ restoreItemToRah(runtime, "NEWSPAPER");
runtime.executeAndQueueAction("ACTV_HOUSE_EXIT");
- lineError = playPaperHandoffPreludeIfNeeded();
+ lineError = playPaperHandoffPreludeIfNeeded(runtime);
if (lineError.getCode() != Common::kNoError)
return lineError;
@@ -226,7 +228,7 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
if (lineError.getCode() != Common::kNoError)
return lineError;
- completeSneakersTrade();
+ completeSneakersTrade(runtime);
return Common::kNoError;
}
@@ -235,7 +237,7 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
if (state.firstNoItemLinePending) {
state.firstNoItemLinePending = false;
- lineError = playPaperHandoffPreludeIfNeeded();
+ lineError = playPaperHandoffPreludeIfNeeded(runtime);
if (lineError.getCode() != Common::kNoError)
return lineError;
@@ -266,12 +268,12 @@ Common::Error JimmyDialogueHandler::handleDialogue(DialogueRuntime &runtime,
if (lineError.getCode() != Common::kNoError)
return lineError;
- lineError = runJimmySneakersPrompt(kJimmySecondResponseLineIndex);
+ lineError = runJimmySneakersPrompt(runtime, kJimmySecondResponseLineIndex);
if (lineError.getCode() != Common::kNoError)
return lineError;
}
- return playJimmyNoItemFallback();
+ return playJimmyNoItemFallback(runtime);
}
} // End of namespace Harvester
diff --git a/engines/harvester/npc/jimmy_dialogue.h b/engines/harvester/npc/jimmy_dialogue.h
index 5d563ff962a..d0e6b770250 100644
--- a/engines/harvester/npc/jimmy_dialogue.h
+++ b/engines/harvester/npc/jimmy_dialogue.h
@@ -42,6 +42,11 @@ public:
const Common::String &usedItemName, DialogueSharedState &sharedState) override;
private:
+ Common::Error playPaperHandoffPreludeIfNeeded(DialogueRuntime &runtime);
+ Common::Error runSneakersOfferMenu(DialogueRuntime &runtime);
+ Common::Error runJimmySneakersPrompt(DialogueRuntime &runtime, int responseLineIndex);
+ Common::Error playJimmyNoItemFallback(DialogueRuntime &runtime);
+
JimmyRoomDialogueState _state;
};
Commit: 5e44c1d40fb49bd5db39e1a439c2e5d9081b9049
https://github.com/scummvm/scummvm/commit/5e44c1d40fb49bd5db39e1a439c2e5d9081b9049
Author: Alex Bevilacqua (alex at alexbevi.com)
Date: 2026-10-20T21:17:53-04:00
Commit Message:
HARVESTER: Extract Buster fatal dialogue sequence
Assisted-by: Codex:gpt-6.1-sol
Changed paths:
engines/harvester/npc/buster_dialogue.cpp
diff --git a/engines/harvester/npc/buster_dialogue.cpp b/engines/harvester/npc/buster_dialogue.cpp
index 598d4f425f1..5084013ea28 100644
--- a/engines/harvester/npc/buster_dialogue.cpp
+++ b/engines/harvester/npc/buster_dialogue.cpp
@@ -171,6 +171,23 @@ static const DialogueLineEntry kBusterTopic0x40Or0x41PreludeLines[] = {
{ 0x17f7, "BUSTER", 0 }
};
+static Common::Error playFatalBusterBranch(DialogueRuntime &runtime, int wavId, int headVariant = 0) {
+ Common::Error lineError = runtime.playDialogueLineWithVariant(wavId, "BUSTER", headVariant);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ Graphics::Screen *screen = runtime.engine().getScreen();
+ if (screen) {
+ screen->fillRect(screen->getBounds(), 0);
+ screen->update();
+ }
+ lineError = runtime.playDialogueFst(kDialogueC086FstPath);
+ if (lineError.getCode() != Common::kNoError)
+ return lineError;
+
+ return runtime.runGameOverScreen();
+}
+
} // End of namespace
Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
@@ -180,26 +197,6 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
DialogueTopicBuffer topics(runtime, topicBuffer, topicBufferLineIndex, "Buster topic buffer");
- auto clearScreenToBlack = [&]() {
- Graphics::Screen *screen = runtime.engine().getScreen();
- if (!screen)
- return;
-
- screen->fillRect(screen->getBounds(), 0);
- screen->update();
- };
- auto playFatalBusterBranch = [&](int wavId, int headVariant = 0) -> Common::Error {
- Common::Error lineError = runtime.playDialogueLineWithVariant(wavId, "BUSTER", headVariant);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
-
- clearScreenToBlack();
- lineError = runtime.playDialogueFst(kDialogueC086FstPath);
- if (lineError.getCode() != Common::kNoError)
- return lineError;
-
- return runtime.runGameOverScreen();
- };
auto isCorpsePhoto = [&](const Common::String &itemName) {
return itemName.equalsIgnoreCase("CASKET_PHOTO") ||
itemName.equalsIgnoreCase("CASKET_PHOTOCOPY");
@@ -278,7 +275,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
return responseError;
if (responseIndex == 1)
- return playFatalBusterBranch(0x183a, 2);
+ return playFatalBusterBranch(runtime, 0x183a, 2);
if (responseIndex == 2) {
lineError = runtime.playDialogueLineWithVariant(0x1841, "BUSTER", 0);
if (lineError.getCode() != Common::kNoError)
@@ -304,7 +301,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
return responseError;
if (followupResponseIndex == 1)
- return playFatalBusterBranch(0x1850);
+ return playFatalBusterBranch(runtime, 0x1850);
if (followupResponseIndex == 2) {
lineError = runtime.playDialogueLineWithVariant(0x1858, "BUSTER", 0);
if (lineError.getCode() != Common::kNoError)
@@ -364,7 +361,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
return responseError;
if (responseIndex == 1)
- return playFatalBusterBranch(0x16d0);
+ return playFatalBusterBranch(runtime, 0x16d0);
if (responseIndex == 2) {
lineError = runtime.playDialogueLineWithVariant(0x16d6, "BUSTER", 0);
if (lineError.getCode() != Common::kNoError)
@@ -388,7 +385,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
return responseError;
if (nestedResponseIndex == 1)
- return playFatalBusterBranch(0x16ef);
+ return playFatalBusterBranch(runtime, 0x16ef);
if (nestedResponseIndex == 2) {
lineError = runtime.playDialogueLineWithVariant(0x16f6, "BUSTER", 0);
if (lineError.getCode() != Common::kNoError)
@@ -397,7 +394,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
topics.assign(0x31);
}
} else if (followupResponseIndex == 2) {
- return playFatalBusterBranch(0x1702);
+ return playFatalBusterBranch(runtime, 0x1702);
}
}
continue;
@@ -509,7 +506,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
return responseError;
if (responseIndex == 1)
- return playFatalBusterBranch(0x1804);
+ return playFatalBusterBranch(runtime, 0x1804);
if (responseIndex == 2) {
lineError = runtime.playDialogueLineWithVariant(0x180b, "BUSTER", 0);
if (lineError.getCode() != Common::kNoError)
@@ -531,7 +528,7 @@ Common::Error BusterDialogueHandler::handleDialogue(DialogueRuntime &runtime,
}
if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x48Line))
- return playFatalBusterBranch(0x18df, 2);
+ return playFatalBusterBranch(runtime, 0x18df, 2);
if (runtime.matchesResponseLine(selectedTopic, kBusterTopic0x49Line)) {
Common::Error lineError = runtime.playDialogueLineWithVariant(0x186f, "BUSTER", 0);
More information about the Scummvm-git-logs
mailing list