[Scummvm-git-logs] scummvm master -> 2224546ffd64f16923209a48bdaf0e57809f9d72
bluegr
noreply at scummvm.org
Sun Oct 18 07:43:46 UTC 2026
This automated email contains information about 22 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
ad36b32050 NANCY: NANCY20: Handle the new CIF tree format
db98d693b8 NANCY: NANCY20: Read the newer HIS sound header
ace747bbcc NANCY: NANCY19: Play nothing for a sound named "silence"
915ae91532 NANCY: NANCY20: Boot from the scripts of Nancy20
7ba0f12dce NANCY: NANCY20: Add the table.sort and math.isnan methods
c936bc2738 NANCY: NANCY20: Read the conversations of Nancy20
f2062dba12 NANCY: NANCY20: Find the flag that turns off the tooltips
8a6ed6d4e2 NANCY: NANCY19: Build the UI before the first scene runs
ede547c0d2 NANCY: NANCY20: Go to scenes that have a name instead of a number
f614efb169 NANCY: NANCY20: Tell the scripts when a scene ends or a movie pauses
193d668936 NANCY: NANCY20: Remove the records the scripts no longer use
a86fdf4ed5 NANCY: NANCY20: Add new functions for rectangles, vectors and matrices
a21161fbf0 NANCY: NANCY20: Allow navigation hotspots shaped by an image
57c5626736 NANCY: NANCY20: Make Ref images out of several pictures
bae5948358 NANCY: NANCY20: Add the new functions of the script objects
33756b3c7f NANCY: NANCY20: Ask before saving over a game
c154ecca5f NANCY: NANCY20: Change to unstable - the game now starts
491813fff8 NANCY: NANCY10: Show the puzzle arrow over QuizPuzzle text boxes
ecfd2ad313 NANCY: NANCY10: Highlight held items over hotspots with a script cursor
5a85f0b861 NANCY: NANCY10: Fix BeadPuzzle exits, cursor, sounds and captions
f238d8b5b4 NANCY: NANCY10: Fix cursors and the held ring in SortPuzzle
2224546ffd NANCY: NANCY13: Add detection for another English version
Commit: ad36b320509663bc0c99704bdb9e09c8db96ba02
https://github.com/scummvm/scummvm/commit/ad36b320509663bc0c99704bdb9e09c8db96ba02
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:38+03:00
Commit Message:
NANCY: NANCY20: Handle the new CIF tree format
Nancy20 made the file names in the index of its CIF trees 64 bytes
long, up from 33.
Changed paths:
engines/nancy/cif.cpp
diff --git a/engines/nancy/cif.cpp b/engines/nancy/cif.cpp
index e8603ac7c18..1855b558a85 100644
--- a/engines/nancy/cif.cpp
+++ b/engines/nancy/cif.cpp
@@ -471,8 +471,13 @@ bool CifTree::sync(Common::Serializer &ser) {
return true;
}
+// Nancy20 made room for longer names in the index
+static uint getIndexNameSize() {
+ return g_nancy->getGameType() >= kGameTypeNancy20 ? 64 : 33;
+}
+
bool CifTree::readIndex(Common::SeekableReadStream &stream) {
- const uint kNameSize = 33;
+ const uint kNameSize = getIndexNameSize();
const uint kIndexEntrySize = kNameSize + 4;
Common::Serializer headerSer(&stream, nullptr);
@@ -502,10 +507,7 @@ bool CifTree::readIndex(Common::SeekableReadStream &stream) {
Common::Array<CifInfo> infos(numFiles);
for (CifInfo &info : infos) {
- char name[kNameSize + 1];
- stream.read(name, kNameSize);
- name[kNameSize] = 0;
- info.name = name;
+ info.name = stream.readString(0, kNameSize);
info.dataOffset = stream.readUint32LE();
}
@@ -534,7 +536,7 @@ bool CifTree::readIndex(Common::SeekableReadStream &stream) {
}
void CifTree::write(Common::SeekableWriteStream &stream, const Common::Array<Common::SeekableReadStream *> &fileData) {
- const uint kNameSize = 33;
+ const uint kNameSize = getIndexNameSize();
const uint kIndexEntrySize = kNameSize + 4;
assert(fileData.size() == _writeFileMap.size());
@@ -554,7 +556,7 @@ void CifTree::write(Common::SeekableWriteStream &stream, const Common::Array<Com
stream.writeUint32LE(_writeFileMap.size());
for (const CifInfo &info : _writeFileMap) {
- char name[kNameSize] = {};
+ char name[64] = {};
Common::strlcpy(name, info.name.baseName().c_str(), kNameSize);
stream.write(name, kNameSize);
stream.writeUint32LE(info.dataOffset);
Commit: db98d693b87cd5a4c2a0ebd81e6ad8ca86749fde
https://github.com/scummvm/scummvm/commit/db98d693b87cd5a4c2a0ebd81e6ad8ca86749fde
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:41+03:00
Commit Message:
NANCY: NANCY20: Read the newer HIS sound header
The sound files of Nancy20 have a 32-bit type field, which makes the
header two bytes longer.
Changed paths:
engines/nancy/sound.cpp
diff --git a/engines/nancy/sound.cpp b/engines/nancy/sound.cpp
index 76b818e587c..214d1726277 100644
--- a/engines/nancy/sound.cpp
+++ b/engines/nancy/sound.cpp
@@ -133,7 +133,8 @@ bool readHISHeader(Common::SeekableReadStream *stream, SoundType &type, uint16 &
size = stream->readUint32LE();
if (hasType) {
- uint16 tp = stream->readUint16LE();
+ // Nancy20 made the type 32 bits wide
+ uint32 tp = g_nancy->getGameType() >= kGameTypeNancy20 ? stream->readUint32LE() : stream->readUint16LE();
switch (tp) {
case 1:
type = kSoundTypeRaw;
@@ -142,7 +143,7 @@ bool readHISHeader(Common::SeekableReadStream *stream, SoundType &type, uint16 &
type = kSoundTypeOgg;
break;
default:
- warning("Unsupported sound type %d found in HIS file", tp);
+ warning("Unsupported sound type %u found in HIS file", tp);
return false;
}
} else
Commit: ace747bbcc5695f7d3b48e2f45126e67af0bb0a3
https://github.com/scummvm/scummvm/commit/ace747bbcc5695f7d3b48e2f45126e67af0bb0a3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:42+03:00
Commit Message:
NANCY: NANCY19: Play nothing for a sound named "silence"
Scripts use it to stop a channel, e.g. in scenes 3050 and 6320 of
Nancy19. There is no file of that name.
Changed paths:
engines/nancy/sound.cpp
diff --git a/engines/nancy/sound.cpp b/engines/nancy/sound.cpp
index 214d1726277..a79032c7610 100644
--- a/engines/nancy/sound.cpp
+++ b/engines/nancy/sound.cpp
@@ -360,6 +360,12 @@ void SoundManager::loadSound(const SoundDescription &description, SoundEffectDes
return;
}
+ // Nancy19 and up play nothing for a sound named "silence", which only
+ // stops the channel
+ if (g_nancy->getGameType() >= kGameTypeNancy19 && soundName == "silence") {
+ return;
+ }
+
Common::Path path(soundName + (g_nancy->getGameType() == kGameTypeVampire ? ".dwd" : ".his"));
Common::SeekableReadStream *file = SearchMan.createReadStreamForMember(path);
if (file) {
Commit: 915ae915326c03011056055851d430274dae3c95
https://github.com/scummvm/scummvm/commit/915ae915326c03011056055851d430274dae3c95
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:47+03:00
Commit Message:
NANCY: NANCY20: Boot from the scripts of Nancy20
The scripts end in "_SC" now, the texts are in files the boot script
lists, and every scene runs a few shared scripts (Table_SC, Math_SC,
Timer_SC, Fader_SC) before its own.
Changed paths:
engines/nancy/enginedata.h
engines/nancy/nancy.cpp
engines/nancy/script/bootapi.cpp
engines/nancy/script/scenecontext.cpp
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index e2188ed7d24..e9019cf724a 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -1131,10 +1131,15 @@ struct ScriptBootData : public EngineData {
ScriptBootData() {}
Common::String windowName;
- Common::String saveFolderName;
+ Common::String saveFolderName; // Nancy19
double playerTimeScale = 1.0;
int diagnosticFontID = 0;
+ // Nancy20 names the scripts holding the texts, and the scripts that get
+ // run in front of every scene
+ Common::Array<Common::String> textFileNames;
+ Common::Array<Common::String> includeFileNames;
+
Common::String cheatSavePrefix;
Common::String saveExtension;
Common::String defaultSaveName;
diff --git a/engines/nancy/nancy.cpp b/engines/nancy/nancy.cpp
index 0ebbdfd4ee2..ef980c7c6e1 100644
--- a/engines/nancy/nancy.cpp
+++ b/engines/nancy/nancy.cpp
@@ -296,7 +296,7 @@ bool NancyEngine::setPlayerCharacter(uint characterIndex) {
tables.boot = boot;
tables.playerIndex = characterIndex;
- const Common::String scriptName = treeName + "_boot";
+ const Common::String scriptName = treeName + (getGameType() >= kGameTypeNancy20 ? "_BOOT_SC" : "_boot");
if (!Script::runBootScript(scriptName.c_str(), tables)) {
error("Failed to load boot script for player character %s", treeName.c_str());
}
@@ -903,8 +903,14 @@ void NancyEngine::bootGameEngine() {
#ifdef ENABLE_NANCY_V4
static void runRequiredBootScript(const char *name, const Script::BootTables &tables) {
- if (!Script::runBootScript(name, tables)) {
- error("Failed to load boot script %s", name);
+ // Nancy20 ends the names of all scripts in "_SC"
+ Common::String scriptName = name;
+ if (g_nancy->getGameType() >= kGameTypeNancy20) {
+ scriptName += "_SC";
+ }
+
+ if (!Script::runBootScript(scriptName.c_str(), tables)) {
+ error("Failed to load boot script %s", scriptName.c_str());
}
}
@@ -947,11 +953,13 @@ void NancyEngine::bootScriptedGameEngine() {
runRequiredBootScript("Cursors", inventoryTables);
Script::BootTables textTables;
- textTables.texts = autotext;
- runRequiredBootScript("Autotext", textTables);
+ if (getGameType() <= kGameTypeNancy19) {
+ textTables.texts = autotext;
+ runRequiredBootScript("Autotext", textTables);
- textTables.texts = convo;
- runRequiredBootScript("Convo", textTables);
+ textTables.texts = convo;
+ runRequiredBootScript("Convo", textTables);
+ }
Script::BootTables bootTables;
bootTables.boot = boot;
@@ -960,11 +968,21 @@ void NancyEngine::bootScriptedGameEngine() {
bootTables.viewport = viewport;
runRequiredBootScript("Boot", bootTables);
+ // Nancy20 names its files of texts in the boot script, and keeps the
+ // lines of the conversations together with the other texts
+ textTables.texts = autotext;
+ for (const Common::String &name : boot->textFileNames) {
+ if (!Script::runBootScript(name.c_str(), textTables)) {
+ error("Failed to load text file %s", name.c_str());
+ }
+ }
+
_staticData.numItems = inventory->items.size();
_staticData.numEventFlags = (uint16)(kNumGenericEventFlags + flags->numRecords);
// Saved games get matched against the name of the game
- Common::strlcpy((char *)bootSummary->header, boot->saveFolderName.c_str(), sizeof(bootSummary->header));
+ const Common::String &saveName = boot->saveFolderName.empty() ? boot->defaultSaveName : boot->saveFolderName;
+ Common::strlcpy((char *)bootSummary->header, saveName.c_str(), sizeof(bootSummary->header));
bootSummary->firstScene.sceneID = 0;
// The clock of the game runs a little faster than a real one
diff --git a/engines/nancy/script/bootapi.cpp b/engines/nancy/script/bootapi.cpp
index 10a42e276eb..866e07d5a4d 100644
--- a/engines/nancy/script/bootapi.cpp
+++ b/engines/nancy/script/bootapi.cpp
@@ -22,6 +22,7 @@
#include "common/algorithm.h"
#include "engines/nancy/enginedata.h"
+#include "engines/nancy/nancy.h"
#include "engines/nancy/script/api.h"
#include "engines/nancy/script/luabind.h"
@@ -105,9 +106,22 @@ static int bootCreate(lua_State *L) {
ScriptBootData &boot = *s_tables->boot;
readField(L, 2, "windowName", boot.windowName);
- requireField(L, readField(L, 2, "saveFolderName", boot.saveFolderName), "saveFolderName");
requireField(L, readField(L, 2, "playerTimeScale", boot.playerTimeScale), "playerTimeScale");
requireField(L, readField(L, 2, "diagFontNum", boot.diagnosticFontID), "diagFontNum");
+
+ if (g_nancy->getGameType() <= kGameTypeNancy19) {
+ requireField(L, readField(L, 2, "saveFolderName", boot.saveFolderName), "saveFolderName");
+ return 0;
+ }
+
+ // Nancy20 names the files of texts, and the scripts every scene starts with
+ requireField(L, pushField(L, 2, "textFiles"), "textFiles");
+ readStringList(L, -1, boot.textFileNames);
+ lua_pop(L, 1);
+
+ requireField(L, pushField(L, 2, "includeFiles"), "includeFiles");
+ readStringList(L, -1, boot.includeFileNames);
+ lua_pop(L, 1);
return 0;
}
@@ -553,6 +567,20 @@ static const InitObject kInitObjects[] = {
{ "AutotextInit", "ND.AutotextInit", autotextCreate }
};
+// Nancy20 sets InventoryInit.addToFront before the items get made. It picks
+// where new item buttons go in the inventory, and the only value used is the
+// one the earlier games always had, so it is only taken in.
+static int inventorySetField(lua_State *L) {
+ checkUserData(L, 1, "ND.InventoryInit");
+ const char *name = luaL_checkstring(L, 2);
+ if (scumm_stricmp(name, "addToFront") != 0) {
+ luaL_error(L, "InventoryInit has no field %s", name);
+ }
+
+ luaL_checktype(L, 3, LUA_TBOOLEAN);
+ return 0;
+}
+
void registerBootAPI(lua_State *L) {
for (const InitObject &object : kInitObjects) {
const luaL_Reg functions[] = {
@@ -564,6 +592,11 @@ void registerBootAPI(lua_State *L) {
registerGlobal(L, object.globalName, object.typeName, functions);
}
+ luaL_getmetatable(L, "ND.InventoryInit");
+ lua_pushcfunction(L, inventorySetField);
+ lua_setfield(L, -2, "__newindex");
+ lua_pop(L, 1);
+
static const luaL_Reg kCursorInitFunctions[] = {
{ "__index", indexFunctions },
{ "Create", cursorCreate },
diff --git a/engines/nancy/script/scenecontext.cpp b/engines/nancy/script/scenecontext.cpp
index da63e39b28a..07dabf9f8a1 100644
--- a/engines/nancy/script/scenecontext.cpp
+++ b/engines/nancy/script/scenecontext.cpp
@@ -79,6 +79,16 @@ lua_State *SceneContext::getLua() const {
}
bool SceneContext::load() {
+ // Nancy20 runs a few shared scripts in every scene before its own
+ const ScriptBootData *boot = (const ScriptBootData *)g_nancy->getEngineData("ScriptBootData");
+ if (boot) {
+ for (const Common::String &name : boot->includeFileNames) {
+ if (!_lua->runScript(name)) {
+ return false;
+ }
+ }
+ }
+
if (!_lua->runScript(_sceneName)) {
return false;
}
Commit: 7ba0f12dce19cc11e2bd6de5f75e7919261071df
https://github.com/scummvm/scummvm/commit/7ba0f12dce19cc11e2bd6de5f75e7919261071df
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:48+03:00
Commit Message:
NANCY: NANCY20: Add the table.sort and math.isnan methods
Nancy20 replaces the sort of Lua with its own, which returns a sorted
copy of the table and leaves equal values in their order. Scripts like
the beach detection puzzle rely on it.
Changed paths:
engines/nancy/script/luastate.cpp
diff --git a/engines/nancy/script/luastate.cpp b/engines/nancy/script/luastate.cpp
index ffef799d184..7716aa162d3 100644
--- a/engines/nancy/script/luastate.cpp
+++ b/engines/nancy/script/luastate.cpp
@@ -50,6 +50,99 @@ static void removeGlobal(lua_State *L, const char *name) {
lua_setglobal(L, name);
}
+static void setLibraryFunction(lua_State *L, const char *library, const char *name, lua_CFunction function) {
+ lua_getglobal(L, library);
+ lua_pushcfunction(L, function);
+ lua_setfield(L, -2, name);
+ lua_pop(L, 1);
+}
+
+// Whether the comparison function of a sort puts the value at index a in
+// front of the one at index b. It gets told the table, then each key with its value.
+static bool sortsBefore(lua_State *L, int a, int b) {
+ lua_pushvalue(L, 2);
+ lua_pushvalue(L, 1);
+ lua_pushinteger(L, a);
+ lua_pushinteger(L, a);
+ lua_gettable(L, 1);
+ lua_pushinteger(L, b);
+ lua_pushinteger(L, b);
+ lua_gettable(L, 1);
+ lua_call(L, 5, 1);
+
+ bool result = lua_toboolean(L, -1);
+ lua_pop(L, 1);
+ return result;
+}
+
+// Nancy20's table.sort(table, function) leaves the table as it is, and gives
+// back a new one with the values in order. Values the function finds equal keep
+// their order.
+//
+// The values are Lua tables, and only the function of the script knows how they
+// compare, so the keys are sorted here and each comparison is a call to Lua.
+// The sort is a merge sort rather than Common::sort, since that one doesn't keep
+// equal values in their order.
+static int tableSort(lua_State *L) {
+ luaL_checktype(L, 1, LUA_TTABLE);
+ luaL_checktype(L, 2, LUA_TFUNCTION);
+ lua_settop(L, 2);
+
+ const int count = lua_objlen(L, 1);
+
+ // The keys in order, and room to merge them. A failing comparison leaves
+ // with a longjmp, which a Common::Array wouldn't survive, so Lua keeps them.
+ int *order = (int *)lua_newuserdata(L, 2 * count * sizeof(int));
+ int *merged = order + count;
+ for (int i = 0; i < count; ++i) {
+ order[i] = i + 1;
+ }
+
+ for (int width = 1; width < count; width *= 2) {
+ for (int low = 0; low < count; low += 2 * width) {
+ const int middle = MIN(low + width, count);
+ const int high = MIN(low + 2 * width, count);
+
+ int left = low;
+ int right = middle;
+ int out = low;
+ while (left < middle && right < high) {
+ if (sortsBefore(L, order[right], order[left])) {
+ merged[out++] = order[right++];
+ } else {
+ merged[out++] = order[left++];
+ }
+ }
+
+ while (left < middle) {
+ merged[out++] = order[left++];
+ }
+
+ while (right < high) {
+ merged[out++] = order[right++];
+ }
+ }
+
+ memcpy(order, merged, count * sizeof(int));
+ }
+
+ lua_createtable(L, count, 0);
+ for (int i = 0; i < count; ++i) {
+ lua_pushinteger(L, order[i]);
+ lua_gettable(L, 1);
+ lua_rawseti(L, -2, i + 1);
+ }
+
+ return 1;
+}
+
+// Nancy20's math.isnan(number)
+static int mathIsNaN(lua_State *L) {
+ const lua_Number number = luaL_checknumber(L, 1);
+ lua_pushboolean(L, number != number);
+ return 1;
+}
+
LuaState::LuaState(SceneContext *context) {
_state = luaL_newstate();
if (!_state) {
@@ -63,6 +156,11 @@ LuaState::LuaState(SceneContext *context) {
openLibrary(_state, luaopen_string, LUA_STRLIBNAME);
openLibrary(_state, luaopen_math, LUA_MATHLIBNAME);
+ if (g_nancy->getGameType() >= kGameTypeNancy20) {
+ setLibraryFunction(_state, LUA_TABLIBNAME, "sort", tableSort);
+ setLibraryFunction(_state, LUA_MATHLIBNAME, "isnan", mathIsNaN);
+ }
+
removeGlobal(_state, "collectgarbage");
removeGlobal(_state, "dofile");
removeGlobal(_state, "load");
Commit: c936bc2738db7e997bf175d60a8a0b76a6715a79
https://github.com/scummvm/scummvm/commit/c936bc2738db7e997bf175d60a8a0b76a6715a79
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:50+03:00
Commit Message:
NANCY: NANCY20: Read the conversations of Nancy20
A conversation's sound and its XSheet have different names now, e.g.
the sound GFL01_SFX goes with the XSheet GFL01_XS. The XSheet also has
two more bytes before its rectangles.
Changed paths:
engines/nancy/script/convo.cpp
diff --git a/engines/nancy/script/convo.cpp b/engines/nancy/script/convo.cpp
index 37f0785d72d..a6339430101 100644
--- a/engines/nancy/script/convo.cpp
+++ b/engines/nancy/script/convo.cpp
@@ -51,6 +51,30 @@ static const uint kXSheetFrameCountOffset = 0x22;
static const uint kXSheetMaxTrees = 4;
//static const uint kXSheetNameSize = 33;
+// The sound of a video conversation is named after its XSheet. In Nancy20 the
+// two names end differently: GFL01_XS goes with the sound GFL01_SFX.
+static Common::String xsheetToSoundName(const Common::String &xsheetName) {
+ if (g_nancy->getGameType() <= kGameTypeNancy19) {
+ return xsheetName;
+ }
+
+ Common::String soundName = xsheetName;
+ soundName.toUppercase();
+ Common::replace(soundName, "_XS", "_SFX");
+ return soundName;
+}
+
+static Common::String soundToXSheetName(const Common::String &soundName) {
+ if (g_nancy->getGameType() <= kGameTypeNancy19) {
+ return soundName;
+ }
+
+ Common::String xsheetName = soundName;
+ xsheetName.toUppercase();
+ Common::replace(xsheetName, "_SFX", "_XS");
+ return xsheetName;
+}
+
// The record a conversation or a response keeps for later, which has to be a NavLogic
static Record *checkHeldRecord(lua_State *L, int index) {
Record *record = checkRecord(L, index);
@@ -96,10 +120,9 @@ void ConvoRecord::readTable(int tableIndex) {
lua_pop(L, 1);
}
- // The sound of a video conversation is named after its XSheet
Common::String xsheetName;
if (readField(L, tableIndex, "xsheet", xsheetName)) {
- _soundNames.push_back(xsheetName);
+ _soundNames.push_back(xsheetToSoundName(xsheetName));
}
if (pushField(L, tableIndex, "sounds")) {
@@ -146,6 +169,11 @@ bool ConvoRecord::loadXSheet(const Common::String &name) {
readFilename(*xsheet, treeName);
}
+ // Nancy20 lines the rectangles up on four bytes
+ if (g_nancy->getGameType() >= kGameTypeNancy20) {
+ xsheet->skip(2);
+ }
+
// Where each part goes in the viewport; the right and bottom edge are left out
Common::Array<Common::Rect> treeRects;
readRectArray(*xsheet, treeRects, kXSheetMaxTrees);
@@ -306,7 +334,7 @@ void ConvoRecord::runOnce() {
context.setConvo(this);
if (_isVideo) {
- loadXSheet(_soundNames[0]);
+ loadXSheet(soundToXSheetName(_soundNames[0]));
}
// The scripts named by the record add the responses they have to its own
Commit: f2062dba124b110469e3048519dfcc49506a007b
https://github.com/scummvm/scummvm/commit/f2062dba124b110469e3048519dfcc49506a007b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:53+03:00
Commit Message:
NANCY: NANCY20: Find the flag that turns off the tooltips
All flags of Nancy20 end in "_FL", so the flag that keeps tooltips
from showing is PreventTooltips_FL.
Changed paths:
engines/nancy/ui/ndui/panel.cpp
diff --git a/engines/nancy/ui/ndui/panel.cpp b/engines/nancy/ui/ndui/panel.cpp
index 1bc2ecd7d54..b832e6c4d64 100644
--- a/engines/nancy/ui/ndui/panel.cpp
+++ b/engines/nancy/ui/ndui/panel.cpp
@@ -168,7 +168,6 @@ void Panel::addBuiltInTooltip() {
static const double kShowAfter = 1.0;
static const double kHideAfter = 10.0;
static const char *const kFontName = "UIFont";
- static const char *const kPreventFlagName = "PreventTooltips";
// TODO: The colors of the original aren't known; these are the usual ones
// of a tooltip
@@ -178,11 +177,14 @@ void Panel::addBuiltInTooltip() {
// Centered, and on a single line
static const int32 kAlignCenter = 2;
+ // Nancy20 ends the names of all flags in "_FL"
+ const char *preventFlagName = g_nancy->getGameType() >= kGameTypeNancy20 ? "PreventTooltips_FL" : "PreventTooltips";
+
int16 preventFlag = -1;
const EVNT *flags = GetEngineData(EVNT);
if (flags) {
for (uint i = 0; i < flags->eventFlagNames.size(); ++i) {
- if (flags->eventFlagNames[i].equalsIgnoreCase(kPreventFlagName)) {
+ if (flags->eventFlagNames[i].equalsIgnoreCase(preventFlagName)) {
preventFlag = (int16)(EVNT::kFirstEventFlag + i);
}
}
Commit: 8a6ed6d4e2729af2e8b666b66b9b9df7b4e4b9c4
https://github.com/scummvm/scummvm/commit/8a6ed6d4e2729af2e8b666b66b9b9df7b4e4b9c4
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:56+03:00
Commit Message:
NANCY: NANCY19: Build the UI before the first scene runs
The UI is now loaded as soon as it is started, as in the original,
so a scene can hide parts of it on its first frame. Nancy20 hides
some of the buttons while its logos play.
Changed paths:
engines/nancy/state/scene.cpp
engines/nancy/state/scenestream.cpp
engines/nancy/state/scenestream.h
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 22fc2d2f5b8..f56ec873a3d 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -3038,7 +3038,10 @@ void Scene::initUI() {
#ifdef ENABLE_NANCY_V4
_isScriptedUIReady = false;
#endif
+ // The panels are there before the first frame of the scene, which can
+ // hide some of them right away
_streams.begin("UIStream", pcui->uiName.toString(), 0, StreamFlags());
+ _streams.loadNow("UIStream");
return;
}
diff --git a/engines/nancy/state/scenestream.cpp b/engines/nancy/state/scenestream.cpp
index e2c77d69c78..75c10f60af9 100644
--- a/engines/nancy/state/scenestream.cpp
+++ b/engines/nancy/state/scenestream.cpp
@@ -71,20 +71,32 @@ bool SceneStream::update() {
processRecords();
- if (_hasNextScene && !_isEnded) {
- _hasNextScene = false;
-
- if (loadScene()) {
- processRecords();
- } else if (_sceneName.empty()) {
- // There was nothing to run in the first place
- _isEnded = true;
- }
+ if (loadNextScene()) {
+ processRecords();
}
return !_isEnded;
}
+bool SceneStream::loadNextScene() {
+ if (!_hasNextScene || _isEnded) {
+ return false;
+ }
+
+ _hasNextScene = false;
+
+ if (loadScene()) {
+ return true;
+ }
+
+ if (_sceneName.empty()) {
+ // There was nothing to run in the first place
+ _isEnded = true;
+ }
+
+ return false;
+}
+
void SceneStream::processRecords() {
#ifdef ENABLE_NANCY_V4
if (_scriptScene) {
@@ -339,6 +351,18 @@ void SceneStreamManager::begin(const Common::String &name, const Common::String
}
}
+void SceneStreamManager::loadNow(const Common::String &name) {
+ SceneStream *stream = find(name);
+ if (!stream) {
+ return;
+ }
+
+ SceneStream *running = _running;
+ _running = stream;
+ stream->loadNextScene();
+ _running = running;
+}
+
void SceneStreamManager::end(const Common::String &name) {
SceneStream *stream = name.empty() ? _running : find(name);
if (!stream) {
diff --git a/engines/nancy/state/scenestream.h b/engines/nancy/state/scenestream.h
index bb17e9a6f00..f6346e24d70 100644
--- a/engines/nancy/state/scenestream.h
+++ b/engines/nancy/state/scenestream.h
@@ -94,6 +94,10 @@ public:
void end() { _isEnded = true; }
bool isEnded() const { return _isEnded; }
+ // Loads the scene the stream was asked to go to, without running it.
+ // Returns whether there was one, and it could be loaded.
+ bool loadNextScene();
+
void setEventFlag(int16 index, byte flag);
byte getEventFlag(int16 index) const;
@@ -146,6 +150,11 @@ class SceneStreamManager {
public:
void begin(const Common::String &name, const Common::String &sceneName, uint16 frameID, const StreamFlags &flags);
void end(const Common::String &name);
+
+ // Loads the scene a stream that was just begun starts with right away,
+ // instead of with the next frame
+ void loadNow(const Common::String &name);
+
void changeScene(const Common::String &name, const SceneChangeDescription &sceneDescription);
void changeScene(const Common::String &name, const Common::String &sceneName, uint16 frameID);
Common::String getSceneName(const Common::String &name) const;
Commit: ede547c0d2d61761aa71ef6373acee08643e8430
https://github.com/scummvm/scummvm/commit/ede547c0d2d61761aa71ef6373acee08643e8430
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:42:59+03:00
Commit Message:
NANCY: NANCY20: Go to scenes that have a name instead of a number
Scenes like s1002a_SC were taken for scene 1002, so scene 1002
started over and over. Saved games of Nancy20 keep the name of the
scene.
Changed paths:
engines/nancy/script/scenecontext.cpp
engines/nancy/state/scene.cpp
engines/nancy/state/scene.h
engines/nancy/state/scenestream.cpp
engines/nancy/util.cpp
engines/nancy/util.h
diff --git a/engines/nancy/script/scenecontext.cpp b/engines/nancy/script/scenecontext.cpp
index 07dabf9f8a1..153ea83156e 100644
--- a/engines/nancy/script/scenecontext.cpp
+++ b/engines/nancy/script/scenecontext.cpp
@@ -26,6 +26,7 @@
#include "engines/nancy/enginedata.h"
#include "engines/nancy/input.h"
#include "engines/nancy/nancy.h"
+#include "engines/nancy/util.h"
#include "engines/nancy/script/convo.h"
#include "engines/nancy/script/luabind.h"
@@ -467,10 +468,11 @@ void SceneContext::changeScene(const Common::String &streamName, const Common::S
_hasChangedScene = true;
- // The scenes of the main stream go by number
- if (sceneName.size() > 1 && tolower(sceneName[0]) == 's' && Common::isDigit(sceneName[1])) {
+ // The scenes of the main stream mostly go by number
+ uint16 sceneID;
+ if (parseSceneName(sceneName, sceneID)) {
SceneChangeDescription scene;
- scene.sceneID = (uint16)atoi(sceneName.c_str() + 1);
+ scene.sceneID = sceneID;
scene.frameID = frame;
scene.continueSceneSound = kContinueSceneSound;
streams.changeScene(streamName, scene);
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index f56ec873a3d..bcd83de1f6d 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -304,10 +304,51 @@ void Scene::changeScene(const SceneChangeDescription &sceneDescription) {
_sceneState.nextScene = sceneDescription;
_state = kLoad;
+#ifdef ENABLE_NANCY_V4
+ // A numbered scene, which no name of an earlier change may stand in for
+ _nextScriptSceneName.clear();
+#endif
+
+ if (_viewportFader) {
+ _viewportFader->onSceneChange();
+ }
+}
+
+#ifdef ENABLE_NANCY_V4
+void Scene::changeScene(const Common::String &sceneName, uint16 frameID) {
+ SceneChangeDescription scene;
+ scene.frameID = frameID;
+ scene.continueSceneSound = kContinueSceneSound;
+
+ if (parseSceneName(sceneName, scene.sceneID)) {
+ changeScene(scene);
+ return;
+ }
+
+ if (_state == kLoad) {
+ return;
+ }
+
+ // The name stands for the scene; its number stays kNoScene
+ _sceneState.nextScene = scene;
+ _nextScriptSceneName = sceneName;
+ _state = kLoad;
+
if (_viewportFader) {
_viewportFader->onSceneChange();
}
}
+#endif
+
+Common::String Scene::getMainSceneName() const {
+#ifdef ENABLE_NANCY_V4
+ if (!_scriptSceneName.empty()) {
+ return _scriptSceneName;
+ }
+#endif
+
+ return Common::String::format("s%u", _sceneState.currentScene.sceneID);
+}
void Scene::pushScene(int16 itemID) {
if (itemID == -1) {
@@ -1657,6 +1698,17 @@ void Scene::synchronize(Common::Serializer &ser) {
}
}
+#ifdef ENABLE_NANCY_V4
+ // Nancy20 has scenes that go by name, which no saved game of the earlier
+ // games can be in
+ if (g_nancy->getGameType() >= kGameTypeNancy20) {
+ ser.syncString(_scriptSceneName);
+
+ // The scene gets loaded again below, from nextScene and this name
+ _nextScriptSceneName = _scriptSceneName;
+ }
+#endif
+
if (ser.isLoading()) {
_sceneState.currentScene.continueSceneSound = kLoadSceneSound;
_sceneState.nextScene = _sceneState.currentScene;
@@ -2199,7 +2251,9 @@ void Scene::load(bool fromSaveFile) {
// Nancy19 scenes are Lua scripts. A script makes the records of its scene when
// it is run, the first of which names the background.
void Scene::loadScriptedScene(bool fromSaveFile) {
- const Common::String sceneName = Common::String::format("s%u", _sceneState.nextScene.sceneID);
+ // A scene that goes by name has no number; see _nextScriptSceneName
+ const Common::String sceneName = _nextScriptSceneName.empty() ?
+ Common::String::format("s%u", _sceneState.nextScene.sceneID) : _nextScriptSceneName;
Script::SceneContext *scriptScene = new Script::SceneContext("MainStream", sceneName, _sceneState.nextScene.frameID, 0);
if (!scriptScene->load()) {
@@ -2225,7 +2279,11 @@ void Scene::loadScriptedScene(bool fromSaveFile) {
debugC(0, kDebugScene, "Loading new scene %s, frame %i", sceneName.c_str(), _sceneState.nextScene.frameID);
+ // The name moves along with the description, so the two always describe the
+ // same scene
_sceneState.currentScene = _sceneState.nextScene;
+ _scriptSceneName = _nextScriptSceneName;
+ _nextScriptSceneName.clear();
Script::SummaryRecord *summary = _scriptScene->getSummary();
_sceneState.summary.description = sceneName;
@@ -2247,7 +2305,8 @@ void Scene::loadScriptedScene(bool fromSaveFile) {
_timers.sceneTime = 0;
g_nancy->_sound->clearListenerPositionOverride();
- if (!fromSaveFile) {
+ // Only numbered scenes are counted, since the others have no number to count
+ if (!fromSaveFile && _scriptSceneName.empty()) {
_flags.sceneCounts.getOrCreateVal(_sceneState.currentScene.sceneID)++;
}
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 2a0b67f4277..56bafeb51ef 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -302,6 +302,10 @@ public:
SceneChangeDescription &getSceneInfo() { return _streams.getRunning() ? _streams.getRunning()->getSceneInfo() : _sceneState.currentScene; }
const SceneChangeDescription &getMainSceneInfo() const { return _sceneState.currentScene; }
+
+ // Nancy19+: the name of the script of the scene the main stream is in, which
+ // is "s" and the scene number unless the scene goes by a name of its own
+ Common::String getMainSceneName() const;
SceneChangeDescription &getNextSceneInfo() { return _sceneState.nextScene; }
const SceneSummary &getSceneSummary() const { return _sceneState.summary; }
@@ -321,6 +325,11 @@ public:
UI::NDUI::Manager *getNDUI() { return _ndui; }
#ifdef ENABLE_NANCY_V4
+ // Nancy20: goes to a scene of the main stream that has a name of its own
+ // instead of a number. Numbered scenes ("s1002") are taken as well, and
+ // go through changeScene(const SceneChangeDescription &) like before.
+ void changeScene(const Common::String &sceneName, uint16 frameID);
+
// Nancy19: the scripts of the UI stream have made the panels
void onScriptedUILoaded();
@@ -525,6 +534,15 @@ private:
// Nancy19: the script of the scene, which takes the place of the action records
Script::SceneContext *_scriptScene = nullptr;
bool _isScriptedUIReady = false;
+
+ // Nancy20: the main stream can also be in a scene that has a name instead of
+ // a number, like "s1002a_SC". These hold the name of the scene it is in and
+ // of the one it goes to next. When a name is set, it is what counts: the
+ // sceneID of _sceneState.currentScene or nextScene is kNoScene then, while
+ // the frame and the rest of the description still apply. For numbered scenes
+ // both stay empty, and the earlier games never set them.
+ Common::String _scriptSceneName;
+ Common::String _nextScriptSceneName;
#endif
double _frameSeconds = 0.0; // how long the last frame took
Action::PlaySecondaryMovie *_activeMovie;
diff --git a/engines/nancy/state/scenestream.cpp b/engines/nancy/state/scenestream.cpp
index 75c10f60af9..8fdf0ef98e4 100644
--- a/engines/nancy/state/scenestream.cpp
+++ b/engines/nancy/state/scenestream.cpp
@@ -217,8 +217,7 @@ void SceneStream::beginScene() {
// Numbered scenes are named after their number
_sceneInfo.sceneID = kNoSceneID;
- if (_sceneName.size() > 1 && tolower(_sceneName[0]) == 's' && Common::isDigit(_sceneName[1])) {
- _sceneInfo.sceneID = atoi(_sceneName.c_str() + 1);
+ if (parseSceneName(_sceneName, _sceneInfo.sceneID)) {
NancySceneState.addSceneVisit(_sceneInfo.sceneID);
}
@@ -402,6 +401,14 @@ void SceneStreamManager::changeScene(const Common::String &name, const Common::S
SceneStream *stream = name.empty() ? _running : find(name);
if (stream) {
stream->changeScene(sceneName, frameID);
+#ifdef ENABLE_NANCY_V4
+ } else if (name.empty() || name.equalsIgnoreCase(kMainStreamName)) {
+ // Nancy20 has scenes of the main stream that go by name
+ SceneStream *running = _running;
+ _running = nullptr;
+ NancySceneState.changeScene(sceneName, frameID);
+ _running = running;
+#endif
} else {
warning("Tried to change the scene of stream %s, which is not running", name.c_str());
}
@@ -409,7 +416,7 @@ void SceneStreamManager::changeScene(const Common::String &name, const Common::S
Common::String SceneStreamManager::getSceneName(const Common::String &name) const {
if (name.equalsIgnoreCase(kMainStreamName)) {
- return Common::String::format("s%u", NancySceneState.getMainSceneInfo().sceneID);
+ return NancySceneState.getMainSceneName();
}
const SceneStream *stream = name.empty() ? _running : find(name);
diff --git a/engines/nancy/util.cpp b/engines/nancy/util.cpp
index 14b7ea6f312..944e0f7aabd 100644
--- a/engines/nancy/util.cpp
+++ b/engines/nancy/util.cpp
@@ -381,6 +381,21 @@ void readUIPopupHeader(Common::SeekableReadStream &stream, UIPopupHeader &dst) {
// A text line will often be broken up into chunks separated by nulls, use
// this function to put it back together as a Common::String
+bool parseSceneName(const Common::String &sceneName, uint16 &sceneID) {
+ if (sceneName.size() < 2 || tolower(sceneName[0]) != 's') {
+ return false;
+ }
+
+ for (uint i = 1; i < sceneName.size(); ++i) {
+ if (!Common::isDigit(sceneName[i])) {
+ return false;
+ }
+ }
+
+ sceneID = (uint16)atoi(sceneName.c_str() + 1);
+ return true;
+}
+
void assembleTextLine(char *rawCaption, Common::String &output, uint size) {
for (uint i = 0; i < size; ++i) {
// A single line can be broken up into bits, look for them and
diff --git a/engines/nancy/util.h b/engines/nancy/util.h
index a399a68358f..cf9b2061984 100644
--- a/engines/nancy/util.h
+++ b/engines/nancy/util.h
@@ -64,6 +64,11 @@ void readFilenameArray(Common::Serializer &stream, Common::Array<Common::Path> &
void assembleTextLine(char *rawCaption, Common::String &output, uint size);
+// Scenes of Nancy19 and up that go by number are named "s" and the number, like
+// "s1002". Returns false for the ones that go by a name of their own, like the
+// "s1002a_SC" of Nancy20.
+bool parseSceneName(const Common::String &sceneName, uint16 &sceneID);
+
// Resolves a subtitle/caption string that may be a key into an engine-data CVTX text
// table (AUTOTEXT by default, CONVO for some puzzles). Returns the table's entry for
// `keyOrText` when the table exists and contains that key, otherwise returns `fallback`.
Commit: f614efb169fac61c47f3aa2655b1145b460d6162
https://github.com/scummvm/scummvm/commit/f614efb169fac61c47f3aa2655b1145b460d6162
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:01+03:00
Commit Message:
NANCY: NANCY20: Tell the scripts when a scene ends or a movie pauses
When a scene is left, its scripts now get to undo what they did to
the UI and the flags, e.g. hiding the air meter after a dive. Movies
and animations that stop on their last frame now tell their scripts.
Changed paths:
engines/nancy/script/record.h
engines/nancy/script/records.cpp
engines/nancy/script/records.h
engines/nancy/script/scenecontext.cpp
engines/nancy/script/scenecontext.h
engines/nancy/state/scene.cpp
engines/nancy/state/scenestream.cpp
engines/nancy/state/scenestream.h
diff --git a/engines/nancy/script/record.h b/engines/nancy/script/record.h
index fccbee20c11..87f36d52f3b 100644
--- a/engines/nancy/script/record.h
+++ b/engines/nancy/script/record.h
@@ -104,6 +104,9 @@ public:
// something across scenes store it
virtual void onSave() {}
+ // The scene of the record is being taken down
+ virtual void onDie() {}
+
// Whether messages sent to the given name are for this record
virtual bool hasMessagingName(const Common::String &name) { return false; }
diff --git a/engines/nancy/script/records.cpp b/engines/nancy/script/records.cpp
index 8bfc1e3aeb0..85d4a120e8d 100644
--- a/engines/nancy/script/records.cpp
+++ b/engines/nancy/script/records.cpp
@@ -979,6 +979,7 @@ void AnimOverlayRecord::run(double sceneTime, double elapsedTime) {
if (_numLoops != 0 && _numLoopsDone >= _numLoops) {
if (_pausesOnLastFrame) {
_isPaused = true;
+ callFunction("OnPause");
} else {
finish();
}
@@ -1483,15 +1484,24 @@ void MovieRecord::run(double sceneTime, double elapsedTime) {
drawFrame();
}
- if (!_player.isRangePlaying() && !_pausesOnLastFrame) {
+ if (_player.isRangePlaying()) {
+ return;
+ }
+
+ if (!_pausesOnLastFrame) {
_endTime = sceneTime;
finish();
+ } else if (!_hasCalledOnPause) {
+ // Nancy20 scripts can be told once that the movie stopped on its last frame
+ _hasCalledOnPause = true;
+ callFunction("OnPause");
}
}
bool MovieRecord::prepareReset() {
_endTime = 0.0;
_isPaused = false;
+ _hasCalledOnPause = false;
_player.goToFrame(_firstFrame);
drawFrame();
return true;
diff --git a/engines/nancy/script/records.h b/engines/nancy/script/records.h
index 25bd89fb524..411e7ae1f8d 100644
--- a/engines/nancy/script/records.h
+++ b/engines/nancy/script/records.h
@@ -184,6 +184,7 @@ public:
void updateGraphics() override;
void onSave() override { callFunction("OnSave"); }
+ void onDie() override { callFunction("OnDie"); }
bool hasMessagingName(const Common::String &name) override;
protected:
@@ -390,6 +391,7 @@ private:
int _firstFrame = 0;
int _lastFrame = 0;
bool _pausesOnLastFrame = false;
+ bool _hasCalledOnPause = false;
bool _isPaused = false;
bool _isVisible = true;
double _endTime = 0.0;
diff --git a/engines/nancy/script/scenecontext.cpp b/engines/nancy/script/scenecontext.cpp
index 153ea83156e..bf8a35026d3 100644
--- a/engines/nancy/script/scenecontext.cpp
+++ b/engines/nancy/script/scenecontext.cpp
@@ -196,6 +196,12 @@ bool SceneContext::skipMovies() {
return true;
}
+void SceneContext::end() {
+ for (Record *record : _records) {
+ record->onDie();
+ }
+}
+
// A scene that has a function for it runs that in place of its records
void SceneContext::onSave() {
if (callSceneFunction("OnSave", 0)) {
diff --git a/engines/nancy/script/scenecontext.h b/engines/nancy/script/scenecontext.h
index d8d49307937..96e36014eb1 100644
--- a/engines/nancy/script/scenecontext.h
+++ b/engines/nancy/script/scenecontext.h
@@ -63,6 +63,10 @@ public:
// Tells the records that the game is about to be saved
void onSave();
+ // Tells the records that the scene is being left, while the game goes on.
+ // They use it to undo what they did to the rest of the game, the UI for one.
+ void end();
+
// Hands a message to the scene when it is the one it names, and to the
// records that go by that name otherwise, until one of them takes it.
// Returns false when the message is for neither.
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index bcd83de1f6d..80e2d4c2ce5 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -2271,10 +2271,15 @@ void Scene::loadScriptedScene(bool fromSaveFile) {
clearSceneData();
+ // The streams of the outgoing scene end first, then the scene itself
+ _streams.onMainSceneChange();
+ if (_scriptScene) {
+ _scriptScene->end();
+ }
+
delete _scriptScene;
_scriptScene = scriptScene;
- _streams.onMainSceneChange();
g_nancy->_graphics->suppressNextDraw();
debugC(0, kDebugScene, "Loading new scene %s, frame %i", sceneName.c_str(), _sceneState.nextScene.frameID);
diff --git a/engines/nancy/state/scenestream.cpp b/engines/nancy/state/scenestream.cpp
index 8fdf0ef98e4..036fd5886ca 100644
--- a/engines/nancy/state/scenestream.cpp
+++ b/engines/nancy/state/scenestream.cpp
@@ -198,10 +198,7 @@ bool SceneStream::loadScriptedScene() {
// Takes the stream from the scene it was running to the one it asked for
void SceneStream::beginScene() {
// The streams that were started for the outgoing scene end with it
- const bool hadScene = !_sceneName.empty();
- clearScene();
-
- if (hadScene) {
+ if (!_sceneName.empty()) {
for (uint i = 0; i < _children.size(); ) {
if (_children[i].lifetime == StreamFlags::kEndsWithParentScene) {
NancySceneState.getStreams().end(_children[i].name);
@@ -212,6 +209,9 @@ void SceneStream::beginScene() {
}
}
+ endScene();
+ clearScene();
+
_sceneName = _nextSceneName;
_sceneInfo.frameID = _nextFrameID;
@@ -289,6 +289,14 @@ bool SceneStream::loadScene() {
return true;
}
+void SceneStream::endScene() {
+#ifdef ENABLE_NANCY_V4
+ if (_scriptScene) {
+ _scriptScene->end();
+ }
+#endif
+}
+
void SceneStream::clearScene() {
// Nothing of a stream's scene carries over to the next one
for (Action::ActionRecord *record : _actionManager._records) {
@@ -448,6 +456,7 @@ void SceneStreamManager::process() {
for (uint i = 0; i < _streams.size(); ) {
if (_streams[i] && _streams[i]->isEnded()) {
+ _streams[i]->endScene();
delete _streams[i];
_streams.remove_at(i);
} else {
diff --git a/engines/nancy/state/scenestream.h b/engines/nancy/state/scenestream.h
index f6346e24d70..b9b11883e0b 100644
--- a/engines/nancy/state/scenestream.h
+++ b/engines/nancy/state/scenestream.h
@@ -94,6 +94,9 @@ public:
void end() { _isEnded = true; }
bool isEnded() const { return _isEnded; }
+ // Nancy20: lets the records of the scene know that it is being left
+ void endScene();
+
// Loads the scene the stream was asked to go to, without running it.
// Returns whether there was one, and it could be loaded.
bool loadNextScene();
Commit: 193d6689362d1413b159db68c1cdf53af4c76469
https://github.com/scummvm/scummvm/commit/193d6689362d1413b159db68c1cdf53af4c76469
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:02+03:00
Commit Message:
NANCY: NANCY20: Remove the records the scripts no longer use
Scripts can mark a record as theirs with "scriptOwns". Once nothing
in the scripts refers to such a record anymore, it is now stopped and
removed at the start of the next frame, e.g. a timer that was replaced
by a new one.
Changed paths:
engines/nancy/script/record.cpp
engines/nancy/script/record.h
engines/nancy/script/scenecontext.cpp
engines/nancy/script/scenecontext.h
diff --git a/engines/nancy/script/record.cpp b/engines/nancy/script/record.cpp
index 76bdc1de2cf..f663636bb7c 100644
--- a/engines/nancy/script/record.cpp
+++ b/engines/nancy/script/record.cpp
@@ -92,6 +92,7 @@ void Record::create(int tableIndex) {
}
readField(L, tableIndex, "z", _z);
+ readField(L, tableIndex, "scriptOwns", _isScriptOwned);
lua_getfield(L, tableIndex, "active");
if (lua_isboolean(L, -1)) {
@@ -113,6 +114,28 @@ void Record::create(int tableIndex) {
void Record::push() {
Record **object = (Record **)pushUserData(getLua(), sizeof(Record *), _typeName);
*object = this;
+ ++_numObjects;
+}
+
+void Record::onObjectCollected() {
+ if (_numObjects > 0) {
+ --_numObjects;
+ }
+
+ if (_numObjects > 0 || !_isScriptOwned || _isDropped) {
+ return;
+ }
+
+ // Nothing the scripts stored in it is needed anymore
+ lua_State *L = getLua();
+ lua_pushlightuserdata(L, this);
+ lua_pushnil(L);
+ lua_rawset(L, LUA_REGISTRYINDEX);
+
+ _isActive = false;
+ _hasActiveFunction = false;
+ _isDone = true;
+ _isDropped = true;
}
void Record::update(double sceneTime, double elapsedTime) {
@@ -483,8 +506,25 @@ static int recordOnKeyEvent(lua_State *L) {
return 1;
}
+// Lua lets go of an object standing for a record
+static int recordCollected(lua_State *L) {
+ // The records of a scene that is going away are deleted before its state
+ SceneContext *context = LuaState::getContext(L);
+ if (!context || context->isEnding()) {
+ return 0;
+ }
+
+ Record **object = (Record **)lua_touserdata(L, 1);
+ if (object && *object) {
+ (*object)->onObjectCollected();
+ }
+
+ return 0;
+}
+
static const luaL_Reg kRecordFunctions[] = {
{ "__index", recordIndex },
+ { "__gc", recordCollected },
{ "__newindex", recordNewIndex },
{ "__eq", recordEquals },
{ "Reset", recordReset },
diff --git a/engines/nancy/script/record.h b/engines/nancy/script/record.h
index 87f36d52f3b..dbdbc6ccffd 100644
--- a/engines/nancy/script/record.h
+++ b/engines/nancy/script/record.h
@@ -82,6 +82,12 @@ public:
// Pushes an object for the scripts that stands for this record
void push();
+ // Nancy20: a record made with "scriptOwns" lives only as long as scripts can
+ // still reach it. Once the last object standing for it has been collected, it
+ // stops and is taken out of the scene.
+ void onObjectCollected();
+ bool isDropped() const { return _isDropped; }
+
// One frame. Times are in seconds.
void update(double sceneTime, double elapsedTime);
@@ -200,6 +206,10 @@ private:
bool _wasActive = false;
bool _isDone = false;
bool _hasRunOnce = false;
+
+ bool _isScriptOwned = false;
+ uint _numObjects = 0; // the objects standing for the record that Lua still has
+ bool _isDropped = false;
};
// Makes the "AR" object, and the types of the records
diff --git a/engines/nancy/script/scenecontext.cpp b/engines/nancy/script/scenecontext.cpp
index bf8a35026d3..70d8e1f7820 100644
--- a/engines/nancy/script/scenecontext.cpp
+++ b/engines/nancy/script/scenecontext.cpp
@@ -326,6 +326,15 @@ void SceneContext::runFrame(double elapsedTime) {
_records.push_back(_newRecords);
_newRecords.clear();
+ // Nancy20 collects what the scripts let go of once a frame, and only then.
+ // That takes out the records that belonged to the scripts.
+ if (g_nancy->getGameType() >= kGameTypeNancy20) {
+ lua_gc(L, LUA_GCRESTART, 0);
+ lua_gc(L, LUA_GCCOLLECT, 0);
+ lua_gc(L, LUA_GCSTOP, 0);
+ deleteDroppedRecords();
+ }
+
// The records run in the order they were made, but what the player does
// goes to the ones closest to them first, and among the ones just as close
// to the one made last. A z can change at any time.
@@ -360,6 +369,27 @@ void SceneContext::runFrame(double elapsedTime) {
}
}
+void SceneContext::deleteDroppedRecords() {
+ for (uint i = 0; i < _records.size(); ) {
+ Record *record = _records[i];
+ if (!record->isDropped()) {
+ ++i;
+ continue;
+ }
+
+ if (record == _summary) {
+ _summary = nullptr;
+ }
+
+ if (record == _convo) {
+ _convo = nullptr;
+ }
+
+ _records.remove_at(i);
+ delete record;
+ }
+}
+
bool SceneContext::getViewportMouse(const NancyInput &input, ScriptPoint &point) {
const UI::Viewport &viewport = NancySceneState.getViewport();
const Common::Rect screenPosition = viewport.getScreenPosition();
diff --git a/engines/nancy/script/scenecontext.h b/engines/nancy/script/scenecontext.h
index 96e36014eb1..6ec2bf5585e 100644
--- a/engines/nancy/script/scenecontext.h
+++ b/engines/nancy/script/scenecontext.h
@@ -67,6 +67,9 @@ public:
// They use it to undo what they did to the rest of the game, the UI for one.
void end();
+ // The scene is being deleted
+ bool isEnding() const { return _isEnding; }
+
// Hands a message to the scene when it is the one it names, and to the
// records that go by that name otherwise, until one of them takes it.
// Returns false when the message is for neither.
@@ -126,6 +129,7 @@ private:
bool callSceneFunction(const char *name, int numArgs);
void pushMessage(const Message &message);
void runFrame(double elapsedTime);
+ void deleteDroppedRecords();
void renderAttachment();
bool hasSkippableRecord() const;
void pushSceneFunctions();
Commit: a86fdf4ed56506cde1aa5d67577e393443af2485
https://github.com/scummvm/scummvm/commit/a86fdf4ed56506cde1aa5d67577e393443af2485
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:04+03:00
Commit Message:
NANCY: NANCY20: Add new functions for rectangles, vectors and matrices
The puzzles of Nancy20 move their pieces with these: the corners of a
rectangle and ways to place it by them, reflecting a vector, turning
around an axis, and quaternions to blend turns with.
Changed paths:
engines/nancy/script/mathtypes.cpp
engines/nancy/script/mathtypes.h
engines/nancy/script/types.cpp
diff --git a/engines/nancy/script/mathtypes.cpp b/engines/nancy/script/mathtypes.cpp
index b7775ffdec1..06e13fdc123 100644
--- a/engines/nancy/script/mathtypes.cpp
+++ b/engines/nancy/script/mathtypes.cpp
@@ -32,6 +32,17 @@ static const char *const kVectorType = "ND.Vector";
static const char *const kVectorHelperType = "ND.VectorHelper";
static const char *const kMatrixType = "ND.Matrix";
static const char *const kMatrixHelperType = "ND.MatrixHelper";
+static const char *const kQuaternionType = "ND.Quaternion";
+static const char *const kQuaternionHelperType = "ND.QuaternionHelper";
+
+// Nancy20. A rotation, kept in single precision as the DirectX math of the
+// original does
+struct ScriptQuaternion {
+ float x = 0.0f;
+ float y = 0.0f;
+ float z = 0.0f;
+ float w = 1.0f;
+};
ScriptMatrix::ScriptMatrix() {
for (int row = 0; row < 4; ++row) {
@@ -126,10 +137,31 @@ static ScriptVector &checkVector(lua_State *L, int index) {
return *(ScriptVector *)checkUserData(L, index, kVectorType);
}
-// Vector:New(x, y[, z[, w]])
+// Vector:New(x, y[, z[, w]]), or Vector:New(vector or place) in Nancy20
static int vectorNew(lua_State *L) {
ScriptVector vector;
vector.size = lua_gettop(L) - 1;
+ if (vector.size == 1) {
+ // A copy of a vector, or a place made into a 2D vector
+ const ScriptVector *other = testVector(L, 2);
+ if (other) {
+ pushVector(L, *other);
+ return 1;
+ }
+
+ lua_getfield(L, 2, "x");
+ lua_getfield(L, 2, "y");
+ if (!lua_isnumber(L, -2) || !lua_isnumber(L, -1)) {
+ return luaL_argerror(L, 2, "Single parameter must be a Vector, a Point or a table with x and y fields");
+ }
+
+ vector.size = 2;
+ vector.v[0] = lua_tonumber(L, -2);
+ vector.v[1] = lua_tonumber(L, -1);
+ pushVector(L, vector);
+ return 1;
+ }
+
if (vector.size < 2 || vector.size > 4) {
return luaL_error(L, "Only 2D, 3D and 4D vectors are supported");
}
@@ -358,6 +390,25 @@ static int vectorLerp(lua_State *L) {
return 1;
}
+// vector:Reflect(normal), the vector bounced off a surface facing that way
+static int vectorReflect(lua_State *L) {
+ ScriptVector result = checkVector(L, 1);
+ const ScriptVector &normal = checkVector(L, 2);
+ checkSameSize(L, result, normal);
+
+ double dot = 0.0;
+ for (int i = 0; i < result.size; ++i) {
+ dot += result.v[i] * normal.v[i];
+ }
+
+ for (int i = 0; i < result.size; ++i) {
+ result.v[i] -= 2.0 * dot * normal.v[i];
+ }
+
+ pushVector(L, result);
+ return 1;
+}
+
// Moves a vector by a matrix, as a place ("coord") or as a direction ("normal")
static int pushTransformedVector(lua_State *L, const ScriptVector &vector, const ScriptMatrix &matrix, const char *kind) {
bool isPlace;
@@ -411,12 +462,23 @@ static const luaL_Reg kVectorFunctions[] = {
{ "Norm", vectorNormalize },
{ "Normalize", vectorNormalize },
{ "Lerp", vectorLerp },
+ { "Reflect", vectorReflect },
{ "Transform", vectorTransform },
{ nullptr, nullptr }
};
+// Vector.origin is a new vector every time, since scripts can change vectors
+static int vectorHelperIndex(lua_State *L) {
+ if (lua_type(L, 2) == LUA_TSTRING && strcmp(lua_tostring(L, 2), "origin") == 0) {
+ pushVector(L, ScriptVector(0.0, 0.0, 0.0));
+ return 1;
+ }
+
+ return indexFunctions(L);
+}
+
static const luaL_Reg kVectorHelperFunctions[] = {
- { "__index", indexFunctions },
+ { "__index", vectorHelperIndex },
{ "New", vectorNew },
{ nullptr, nullptr }
};
@@ -498,6 +560,57 @@ static int matrixRotateAngle(lua_State *L) {
return 1;
}
+// A rotation around the vector v, which RotateAxis and RotateQuaternion both
+// come down to: a multiple of v times itself, a number on the diagonal, and the
+// turn, which goes in the same direction as RotateAngle
+static ScriptMatrix makeRotation(const double v[3], double outer, double diagonal, double turn) {
+ ScriptMatrix matrix;
+ for (int row = 0; row < 3; ++row) {
+ for (int column = 0; column < 3; ++column) {
+ matrix.m[row][column] = outer * v[row] * v[column];
+ }
+
+ matrix.m[row][row] += diagonal;
+ }
+
+ matrix.m[0][1] += turn * v[2];
+ matrix.m[1][0] -= turn * v[2];
+ matrix.m[2][0] += turn * v[1];
+ matrix.m[0][2] -= turn * v[1];
+ matrix.m[1][2] += turn * v[0];
+ matrix.m[2][1] -= turn * v[0];
+ return matrix;
+}
+
+// Matrix:RotateAxis(axis, angle), a turn by the angle in radians around a 3D vector
+static int matrixRotateAxis(lua_State *L) {
+ const ScriptVector *axis = testVector(L, 2);
+ if (!axis || axis->size != 3) {
+ return luaL_argerror(L, 2, "Axis must be a 3D vector");
+ }
+
+ const double angle = luaL_checknumber(L, 3);
+
+ const double length = getVectorLength(*axis);
+ double n[3];
+ for (int i = 0; i < 3; ++i) {
+ n[i] = length != 0.0 ? axis->v[i] / length : axis->v[i];
+ }
+
+ pushMatrix(L, makeRotation(n, 1.0 - cos(angle), cos(angle), sin(angle)));
+ return 1;
+}
+
+// Matrix:RotateQuaternion(quaternion)
+static int matrixRotateQuaternion(lua_State *L) {
+ const ScriptQuaternion &q = *(ScriptQuaternion *)checkUserData(L, 2, kQuaternionType);
+
+ const double v[3] = { q.x, q.y, q.z };
+ const double lengthSquared = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
+ pushMatrix(L, makeRotation(v, 2.0, 1.0 - 2.0 * lengthSquared, 2.0 * q.w));
+ return 1;
+}
+
// A row of the matrix, as a vector
static int matrixIndex(lua_State *L) {
const ScriptMatrix &matrix = checkMatrix(L, 1);
@@ -565,9 +678,8 @@ static int matrixTranspose(lua_State *L) {
return 1;
}
-// The matrix that undoes this one; one that can't be undone comes back as it is
-static int matrixInverse(lua_State *L) {
- ScriptMatrix work = checkMatrix(L, 1);
+ScriptMatrix ScriptMatrix::inverse() const {
+ ScriptMatrix work = *this;
ScriptMatrix result;
for (int column = 0; column < 4; ++column) {
@@ -580,8 +692,7 @@ static int matrixInverse(lua_State *L) {
}
if (work.m[pivot][column] == 0.0) {
- pushMatrix(L, checkMatrix(L, 1));
- return 1;
+ return *this;
}
for (int i = 0; i < 4; ++i) {
@@ -608,7 +719,11 @@ static int matrixInverse(lua_State *L) {
}
}
- pushMatrix(L, result);
+ return result;
+}
+
+static int matrixInverse(lua_State *L) {
+ pushMatrix(L, checkMatrix(L, 1).inverse());
return 1;
}
@@ -630,8 +745,18 @@ static const luaL_Reg kMatrixFunctions[] = {
{ nullptr, nullptr }
};
+// Matrix.identity is a new matrix every time, since scripts can change matrices
+static int matrixHelperIndex(lua_State *L) {
+ if (lua_type(L, 2) == LUA_TSTRING && strcmp(lua_tostring(L, 2), "identity") == 0) {
+ pushMatrix(L, ScriptMatrix());
+ return 1;
+ }
+
+ return indexFunctions(L);
+}
+
static const luaL_Reg kMatrixHelperFunctions[] = {
- { "__index", indexFunctions },
+ { "__index", matrixHelperIndex },
{ "New", matrixNew },
{ "Translation", matrixTranslate },
{ "Translate", matrixTranslate },
@@ -639,6 +764,80 @@ static const luaL_Reg kMatrixHelperFunctions[] = {
{ "Scale", matrixScale },
{ "RotationAngle", matrixRotateAngle },
{ "RotateAngle", matrixRotateAngle },
+ { "RotationAxis", matrixRotateAxis },
+ { "RotateAxis", matrixRotateAxis },
+ { "RotationQuaternion", matrixRotateQuaternion },
+ { "RotateQuaternion", matrixRotateQuaternion },
+ { nullptr, nullptr }
+};
+
+// Quaternion, which the puzzles of Nancy20 blend rotations with
+
+static void pushQuaternion(lua_State *L, const ScriptQuaternion &quaternion) {
+ pushValue(L, kQuaternionType, quaternion);
+}
+
+static const ScriptQuaternion &checkQuaternion(lua_State *L, int index) {
+ return *(ScriptQuaternion *)checkUserData(L, index, kQuaternionType);
+}
+
+// Quaternion:New(quaternion), a copy
+static int quaternionNew(lua_State *L) {
+ pushQuaternion(L, checkQuaternion(L, 2));
+ return 1;
+}
+
+// Quaternion:RotateZ(angle), a turn by the angle in radians around the z axis
+static int quaternionRotateZ(lua_State *L) {
+ const float angle = (float)luaL_checknumber(L, 2);
+
+ ScriptQuaternion quaternion;
+ quaternion.z = sinf(angle / 2.0f);
+ quaternion.w = cosf(angle / 2.0f);
+ pushQuaternion(L, quaternion);
+ return 1;
+}
+
+// quaternion:Slerp(other, amount), the rotation that far along the way to the
+// other one, going the short way around
+static int quaternionSlerp(lua_State *L) {
+ const ScriptQuaternion &a = checkQuaternion(L, 1);
+ const ScriptQuaternion &b = checkQuaternion(L, 2);
+ float amount = (float)luaL_checknumber(L, 3);
+ float rest = 1.0f - amount;
+
+ float dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
+ if (dot < 0.0f) {
+ amount = -amount;
+ dot = -dot;
+ }
+
+ if (1.0f - dot > 0.001f) {
+ const float theta = acosf(dot);
+ rest = sinf(theta * rest) / sinf(theta);
+ amount = sinf(theta * amount) / sinf(theta);
+ }
+
+ ScriptQuaternion result;
+ result.x = rest * a.x + amount * b.x;
+ result.y = rest * a.y + amount * b.y;
+ result.z = rest * a.z + amount * b.z;
+ result.w = rest * a.w + amount * b.w;
+ pushQuaternion(L, result);
+ return 1;
+}
+
+static const luaL_Reg kQuaternionFunctions[] = {
+ { "__index", indexFunctions },
+ { "Slerp", quaternionSlerp },
+ { nullptr, nullptr }
+};
+
+static const luaL_Reg kQuaternionHelperFunctions[] = {
+ { "__index", indexFunctions },
+ { "New", quaternionNew },
+ { "RotationZ", quaternionRotateZ },
+ { "RotateZ", quaternionRotateZ },
{ nullptr, nullptr }
};
@@ -655,10 +854,8 @@ void registerMathTypes(lua_State *L) {
registerType(L, kMatrixType, kMatrixFunctions);
registerGlobal(L, "Matrix", kMatrixHelperType, kMatrixHelperFunctions);
- luaL_getmetatable(L, kMatrixHelperType);
- pushMatrix(L, ScriptMatrix());
- lua_setfield(L, -2, "identity");
- lua_pop(L, 1);
+ registerType(L, kQuaternionType, kQuaternionFunctions);
+ registerGlobal(L, "Quaternion", kQuaternionHelperType, kQuaternionHelperFunctions);
// Rectangles can be moved by a matrix as well
luaL_getmetatable(L, "ND.Rect");
diff --git a/engines/nancy/script/mathtypes.h b/engines/nancy/script/mathtypes.h
index b852b6acabd..7033c633ba5 100644
--- a/engines/nancy/script/mathtypes.h
+++ b/engines/nancy/script/mathtypes.h
@@ -57,6 +57,9 @@ struct ScriptMatrix {
// Whether the matrix does nothing but move things
bool isTranslation() const;
+
+ // The matrix that undoes this one; one that can't be undone comes back as it is
+ ScriptMatrix inverse() const;
};
void registerMathTypes(lua_State *L);
diff --git a/engines/nancy/script/types.cpp b/engines/nancy/script/types.cpp
index 64ae727d6d9..90a80db78f7 100644
--- a/engines/nancy/script/types.cpp
+++ b/engines/nancy/script/types.cpp
@@ -20,6 +20,7 @@
*/
#include "engines/nancy/script/luabind.h"
+#include "engines/nancy/script/mathtypes.h"
#include "engines/nancy/script/types.h"
namespace Nancy {
@@ -71,6 +72,30 @@ static ScriptPoint checkPointArguments(lua_State *L, int index) {
return point;
}
+// The same, without rounding the numbers
+static void checkPlaceArguments(lua_State *L, int index, double &x, double &y) {
+ if (lua_gettop(L) != index) {
+ x = luaL_checknumber(L, index);
+ y = luaL_checknumber(L, index + 1);
+ return;
+ }
+
+ if (lua_type(L, index) == LUA_TUSERDATA || lua_type(L, index) == LUA_TTABLE) {
+ lua_getfield(L, index, "x");
+ lua_getfield(L, index, "y");
+ const bool hasBoth = lua_isnumber(L, -1) && lua_isnumber(L, -2);
+ x = lua_tonumber(L, -2);
+ y = lua_tonumber(L, -1);
+ lua_pop(L, 2);
+
+ if (hasBoth) {
+ return;
+ }
+ }
+
+ luaL_argerror(L, index, "Single parameter must be a Point or a table with x and y fields");
+}
+
static int rectNew(lua_State *L) {
ScriptRect rect;
rect.left = luaL_checkint(L, 2);
@@ -109,6 +134,21 @@ static int rectIndex(lua_State *L) {
lua_pushinteger(L, rect.height());
} else if (key == "center") {
pushPoint(L, ScriptPoint(rect.left + rect.width() / 2, rect.top + rect.height() / 2));
+ } else if (key == "centerV") {
+ // Not rounded, unlike "center"
+ ScriptVector center;
+ center.size = 2;
+ center.v[0] = rect.left + rect.width() * 0.5f;
+ center.v[1] = rect.top + rect.height() * 0.5f;
+ pushVector(L, center);
+ } else if (key == "leftTop") {
+ pushPoint(L, ScriptPoint(rect.left, rect.top));
+ } else if (key == "rightTop") {
+ pushPoint(L, ScriptPoint(rect.right, rect.top));
+ } else if (key == "leftBottom") {
+ pushPoint(L, ScriptPoint(rect.left, rect.bottom));
+ } else if (key == "rightBottom") {
+ pushPoint(L, ScriptPoint(rect.right, rect.bottom));
} else {
return indexFunctions(L);
}
@@ -187,6 +227,45 @@ static int rectCenteredOn(lua_State *L) {
return 1;
}
+// rect:Expand(amount) grows the rectangle by as much on every side
+static int rectExpand(lua_State *L) {
+ ScriptRect rect = checkRect(L, 1);
+ const int32 amount = luaL_checkint(L, 2);
+ pushRect(L, ScriptRect(rect.left - amount, rect.top - amount, rect.right + amount, rect.bottom + amount));
+ return 1;
+}
+
+// rect:CenterSet(place) moves the rectangle so that its middle is at the place
+static int rectCenterSet(lua_State *L) {
+ ScriptRect rect = checkRect(L, 1);
+ double x, y;
+ checkPlaceArguments(L, 2, x, y);
+
+ const float centerX = rect.left + rect.width() * 0.5f;
+ const float centerY = rect.top + rect.height() * 0.5f;
+ rect.translate((int32)(x - centerX), (int32)(y - centerY));
+ pushRect(L, rect);
+ return 1;
+}
+
+// rect:LeftTopSet(place) and rect:LeftBottomSet(place) move the rectangle so that
+// that corner is at the place
+static int rectLeftTopSet(lua_State *L) {
+ const ScriptRect &rect = checkRect(L, 1);
+ double x, y;
+ checkPlaceArguments(L, 2, x, y);
+ pushRect(L, ScriptRect((int32)x, (int32)y, (int32)(rect.width() + x), (int32)(rect.height() + y)));
+ return 1;
+}
+
+static int rectLeftBottomSet(lua_State *L) {
+ const ScriptRect &rect = checkRect(L, 1);
+ double x, y;
+ checkPlaceArguments(L, 2, x, y);
+ pushRect(L, ScriptRect((int32)x, (int32)(y - rect.height()), (int32)(rect.width() + x), (int32)y));
+ return 1;
+}
+
static const luaL_Reg kRectFunctions[] = {
{ "__index", rectIndex },
{ "__newindex", rectNewIndex },
@@ -195,6 +274,10 @@ static const luaL_Reg kRectFunctions[] = {
{ "Offset", rectOffset },
{ "Intersect", rectIntersect },
{ "CenteredOn", rectCenteredOn },
+ { "Expand", rectExpand },
+ { "CenterSet", rectCenterSet },
+ { "LeftTopSet", rectLeftTopSet },
+ { "LeftBottomSet", rectLeftBottomSet },
{ nullptr, nullptr }
};
@@ -236,10 +319,19 @@ bool readPoint(lua_State *L, int index, ScriptPoint &point) {
return hasBoth;
}
+// Point:New(x, y), or Point:New(place) in Nancy20
static int pointNew(lua_State *L) {
ScriptPoint point;
- point.x = luaL_checkint(L, 2);
- point.y = luaL_checkint(L, 3);
+ if (lua_gettop(L) == 2) {
+ double x, y;
+ checkPlaceArguments(L, 2, x, y);
+ point.x = (int32)x;
+ point.y = (int32)y;
+ } else {
+ point.x = luaL_checkint(L, 2);
+ point.y = luaL_checkint(L, 3);
+ }
+
pushPoint(L, point);
return 1;
}
@@ -339,7 +431,19 @@ static int getColorComponentShift(const Common::String &name) {
return -1;
}
+// Color:New(a, r, g, b), or Color:New(color) in Nancy20, where the color can
+// also be one number holding all four
static int colorNew(lua_State *L) {
+ if (lua_gettop(L) == 2) {
+ if (lua_isnumber(L, 2)) {
+ pushColor(L, (uint32)(int64)lua_tonumber(L, 2));
+ } else {
+ pushColor(L, checkColor(L, 2));
+ }
+
+ return 1;
+ }
+
uint32 a = luaL_checkint(L, 2) & 0xFF;
uint32 r = luaL_checkint(L, 3) & 0xFF;
uint32 g = luaL_checkint(L, 4) & 0xFF;
Commit: a21161fbf00675a51eae73def5cd3fc5e65afdd3
https://github.com/scummvm/scummvm/commit/a21161fbf00675a51eae73def5cd3fc5e65afdd3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:06+03:00
Commit Message:
NANCY: NANCY20: Allow navigation hotspots shaped by an image
A hotspot can take its shape from a picture now: the places that
aren't transparent, or that have a given color, like the tiles of the
color square game. Hotspots can also wait for the right button.
Changed paths:
A engines/nancy/script/ref.h
engines/nancy/script/records.cpp
engines/nancy/script/records.h
engines/nancy/script/ref.cpp
engines/nancy/script/scenecontext.cpp
engines/nancy/script/scenecontext.h
diff --git a/engines/nancy/script/records.cpp b/engines/nancy/script/records.cpp
index 85d4a120e8d..fad705542c4 100644
--- a/engines/nancy/script/records.cpp
+++ b/engines/nancy/script/records.cpp
@@ -34,6 +34,7 @@
#include "engines/nancy/script/convo.h"
#include "engines/nancy/script/luabind.h"
#include "engines/nancy/script/records.h"
+#include "engines/nancy/script/ref.h"
#include "engines/nancy/script/scenecontext.h"
#include "engines/nancy/script/types.h"
@@ -482,6 +483,12 @@ void NavLogicRecord::readCursor(int valueIndex) {
_hasCursor = testCursor(getLua(), valueIndex, _cursorType, _cursorIsHot);
}
+void NavLogicRecord::readRef(int valueIndex) {
+ lua_State *L = getLua();
+
+ _ref = lua_isnil(L, valueIndex) ? nullptr : getContext().loadRef(luaL_checkstring(L, valueIndex));
+}
+
void NavLogicRecord::readTable(int tableIndex) {
lua_State *L = getLua();
@@ -503,6 +510,24 @@ void NavLogicRecord::readTable(int tableIndex) {
lua_pop(L, 1);
}
+ if (pushField(L, tableIndex, "ref")) {
+ readRef(-1);
+ lua_pop(L, 1);
+ }
+
+ if (pushField(L, tableIndex, "refColor")) {
+ _refColor = checkColor(L, -1);
+ _hasRefColor = true;
+ lua_pop(L, 1);
+ }
+
+ if (pushField(L, tableIndex, "source")) {
+ _source = checkRect(L, -1);
+ lua_pop(L, 1);
+ }
+
+ readField(L, tableIndex, "rightClick", _isRightClick);
+
// The flags to set, by name
if (pushField(L, tableIndex, "flags")) {
luaL_checktype(L, -1, LUA_TTABLE);
@@ -526,13 +551,41 @@ void NavLogicRecord::readTable(int tableIndex) {
void NavLogicRecord::run(double sceneTime, double elapsedTime) {
// Without a place to click, there is nothing to wait for
- if (_hotspot.isEmpty()) {
+ if (_hotspot.isEmpty() && !_ref) {
trigger();
}
}
bool NavLogicRecord::isOver(const ScriptPoint &point) const {
- return transformRect(_hotspot, getTransform()).contains(point.x, point.y);
+ if (!_ref) {
+ return transformRect(_hotspot, getTransform()).contains(point.x, point.y);
+ }
+
+ double x = point.x;
+ double y = point.y;
+ getTransform().inverse().transformPoint(x, y);
+
+ int refX = (int)x;
+ int refY = (int)y;
+ if (!_source.isEmpty() || !_hotspot.isEmpty()) {
+ if (!_hotspot.contains(refX, refY)) {
+ return false;
+ }
+
+ refX += _source.left - _hotspot.left;
+ refY += _source.top - _hotspot.top;
+ }
+
+ uint32 pixel;
+ if (!_ref->getPixel(refX, refY, pixel)) {
+ return false;
+ }
+
+ return _hasRefColor ? pixel == _refColor : (pixel >> 24) != 0;
+}
+
+void NavLogicRecord::onMouseMove(const ScriptPoint &point, bool inViewport) {
+ _isMouseOver = inViewport && isOver(point);
}
bool NavLogicRecord::getCursor(const ScriptPoint &point, bool inViewport, uint &cursorType, bool &isHot) {
@@ -546,7 +599,8 @@ bool NavLogicRecord::getCursor(const ScriptPoint &point, bool inViewport, uint &
}
bool NavLogicRecord::onMouseEvent(RecordEvent event, const ScriptPoint &point, bool inViewport) {
- if (event != kEventLeftClick || !isOver(point)) {
+ const RecordEvent clickEvent = _isRightClick ? kEventRightClick : kEventLeftClick;
+ if (event != clickEvent || !isOver(point)) {
return false;
}
@@ -579,6 +633,14 @@ bool NavLogicRecord::getField(const Common::String &key) {
lua_pushinteger(L, _frame);
} else if (key == "onScreen") {
pushRect(L, _hotspot);
+ } else if (key == "mouseOver") {
+ lua_pushboolean(L, _isMouseOver);
+ } else if (key == "refColor" && _hasRefColor) {
+ pushColor(L, _refColor);
+ } else if (key == "source") {
+ pushRect(L, _source);
+ } else if (key == "rightClick") {
+ lua_pushboolean(L, _isRightClick);
} else {
return false;
}
@@ -599,6 +661,15 @@ bool NavLogicRecord::setField(const Common::String &key) {
_frame = luaL_checkint(L, 3);
} else if (key == "onScreen") {
_hotspot = checkRect(L, 3);
+ } else if (key == "ref") {
+ readRef(3);
+ } else if (key == "refColor") {
+ _hasRefColor = !lua_isnil(L, 3);
+ _refColor = _hasRefColor ? checkColor(L, 3) : 0;
+ } else if (key == "source") {
+ _source = checkRect(L, 3);
+ } else if (key == "rightClick") {
+ _isRightClick = lua_toboolean(L, 3) != 0;
} else {
return false;
}
diff --git a/engines/nancy/script/records.h b/engines/nancy/script/records.h
index 411e7ae1f8d..4af17ebae24 100644
--- a/engines/nancy/script/records.h
+++ b/engines/nancy/script/records.h
@@ -41,6 +41,8 @@ class Viewport;
namespace Script {
+struct RefImage;
+
// The kinds of records a script can make, in the order of the names below
enum RecordKind {
kRecordNavLogic = 0,
@@ -149,6 +151,7 @@ protected:
bool getField(const Common::String &key) override;
bool setField(const Common::String &key) override;
+ void onMouseMove(const ScriptPoint &point, bool inViewport) override;
bool getCursor(const ScriptPoint &point, bool inViewport, uint &cursorType, bool &isHot) override;
bool onMouseEvent(RecordEvent event, const ScriptPoint &point, bool inViewport) override;
@@ -160,6 +163,7 @@ private:
void readScene(int valueIndex);
void readCursor(int valueIndex);
+ void readRef(int valueIndex);
bool isOver(const ScriptPoint &point) const;
void trigger();
@@ -172,6 +176,18 @@ private:
uint _cursorType = 0;
bool _cursorIsHot = false;
+ // Nancy20 can take the shape of the hotspot from a Ref image: where it isn't
+ // transparent, or where it has the given color. The part of the image named
+ // by the source is laid over the hotspot; without both, the image covers
+ // the viewport.
+ const RefImage *_ref = nullptr;
+ bool _hasRefColor = false;
+ uint32 _refColor = 0;
+ ScriptRect _source;
+
+ bool _isRightClick = false; // Nancy20: the right button clicks it, not the left
+ bool _isMouseOver = false;
+
Common::Array<Flag> _flags;
};
diff --git a/engines/nancy/script/ref.cpp b/engines/nancy/script/ref.cpp
index aa3e456dc38..834c0a69eda 100644
--- a/engines/nancy/script/ref.cpp
+++ b/engines/nancy/script/ref.cpp
@@ -19,39 +19,19 @@
*
*/
-#include "graphics/managed_surface.h"
-
#include "engines/nancy/nancy.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/script/api.h"
#include "engines/nancy/script/luabind.h"
+#include "engines/nancy/script/ref.h"
#include "engines/nancy/script/types.h"
namespace Nancy {
namespace Script {
-// A "Ref" is an image scripts look things up in instead of showing it: the
-// colors of its pixels say what is where. The map the player walks around on
-// has one for every part of it, painted in the colors of where one can go.
-
static const char *const kRefType = "ND.Ref";
-// What to do about a place that is outside of the image
-enum RefEdgeMode {
- kRefClamp = 0, // take the nearest pixel
- kRefWrap = 1, // start over at the other side
- kRefDismiss = 2 // there is nothing there
-};
-
-struct RefImage {
- Graphics::ManagedSurface image;
- RefEdgeMode edgeMode = kRefClamp;
-
- // Returns false when there is no pixel at that place
- bool getPixel(int x, int y, uint32 &argb) const;
-};
-
bool RefImage::getPixel(int x, int y, uint32 &argb) const {
if (image.w == 0 || image.h == 0) {
return false;
diff --git a/engines/nancy/script/ref.h b/engines/nancy/script/ref.h
new file mode 100644
index 00000000000..cc845f5a34c
--- /dev/null
+++ b/engines/nancy/script/ref.h
@@ -0,0 +1,52 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_SCRIPT_REF_H
+#define NANCY_SCRIPT_REF_H
+
+#include "graphics/managed_surface.h"
+
+namespace Nancy {
+namespace Script {
+
+// A "Ref" is an image scripts look things up in instead of showing it: the
+// colors of its pixels say what is where. The map the player walks around on
+// has one for every part of it, painted in the colors of where one can go.
+
+// What to do about a place that is outside of the image
+enum RefEdgeMode {
+ kRefClamp = 0, // take the nearest pixel
+ kRefWrap = 1, // start over at the other side
+ kRefDismiss = 2 // there is nothing there
+};
+
+struct RefImage {
+ Graphics::ManagedSurface image;
+ RefEdgeMode edgeMode = kRefClamp;
+
+ // Returns false when there is no pixel at that place
+ bool getPixel(int x, int y, uint32 &argb) const;
+};
+
+} // End of namespace Script
+} // End of namespace Nancy
+
+#endif // NANCY_SCRIPT_REF_H
diff --git a/engines/nancy/script/scenecontext.cpp b/engines/nancy/script/scenecontext.cpp
index 70d8e1f7820..aabd18c0726 100644
--- a/engines/nancy/script/scenecontext.cpp
+++ b/engines/nancy/script/scenecontext.cpp
@@ -26,6 +26,7 @@
#include "engines/nancy/enginedata.h"
#include "engines/nancy/input.h"
#include "engines/nancy/nancy.h"
+#include "engines/nancy/resource.h"
#include "engines/nancy/util.h"
#include "engines/nancy/script/convo.h"
@@ -33,6 +34,7 @@
#include "engines/nancy/script/luastate.h"
#include "engines/nancy/script/record.h"
#include "engines/nancy/script/records.h"
+#include "engines/nancy/script/ref.h"
#include "engines/nancy/script/scenecontext.h"
#include "engines/nancy/state/scene.h"
@@ -72,9 +74,27 @@ SceneContext::~SceneContext() {
delete response;
}
+ for (auto &ref : _refs) {
+ delete ref._value;
+ }
+
delete _lua;
}
+const RefImage *SceneContext::loadRef(const Common::String &name) {
+ RefImage *ref = _refs.getValOrDefault(name, nullptr);
+ if (!ref) {
+ ref = new RefImage();
+ if (!g_nancy->_resource->loadImage(Common::Path(name), ref->image)) {
+ warning("Could not load Ref %s", name.c_str());
+ }
+
+ _refs.setVal(name, ref);
+ }
+
+ return ref;
+}
+
lua_State *SceneContext::getLua() const {
return _lua->get();
}
diff --git a/engines/nancy/script/scenecontext.h b/engines/nancy/script/scenecontext.h
index 6ec2bf5585e..b1f0c281092 100644
--- a/engines/nancy/script/scenecontext.h
+++ b/engines/nancy/script/scenecontext.h
@@ -23,6 +23,7 @@
#define NANCY_SCRIPT_SCENECONTEXT_H
#include "common/array.h"
+#include "common/hash-str.h"
#include "common/keyboard.h"
#include "common/str.h"
@@ -38,6 +39,7 @@ namespace Script {
class LuaState;
class Record;
+struct RefImage;
class ConvoRecord;
struct ConvoResponse;
class SummaryRecord;
@@ -70,6 +72,10 @@ public:
// The scene is being deleted
bool isEnding() const { return _isEnding; }
+ // The Ref image of that name, which the records of the scene share. Places
+ // outside of it take the nearest pixel.
+ const RefImage *loadRef(const Common::String &name);
+
// Hands a message to the scene when it is the one it names, and to the
// records that go by that name otherwise, until one of them takes it.
// Returns false when the message is for neither.
@@ -149,6 +155,7 @@ private:
SummaryRecord *_summary = nullptr;
ConvoRecord *_convo = nullptr;
Common::Array<ConvoResponse *> _convoResponses;
+ Common::HashMap<Common::String, RefImage *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _refs;
double _time = 0.0;
bool _hasRunOnce = false;
Commit: 57c5626736b77b0ccd3d98116be16b51d0baee63
https://github.com/scummvm/scummvm/commit/57c5626736b77b0ccd3d98116be16b51d0baee63
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:08+03:00
Commit Message:
NANCY: NANCY20: Make Ref images out of several pictures
The large scenes put Ref pictures side by side, adding and removing
them as the player moves around, with a color for the places that no
picture covers.
Changed paths:
engines/nancy/script/ref.cpp
diff --git a/engines/nancy/script/ref.cpp b/engines/nancy/script/ref.cpp
index 834c0a69eda..8dd84dc42e6 100644
--- a/engines/nancy/script/ref.cpp
+++ b/engines/nancy/script/ref.cpp
@@ -60,8 +60,75 @@ bool RefImage::getPixel(int x, int y, uint32 &argb) const {
return true;
}
-static RefImage *checkRef(lua_State *L, int index) {
- return *(RefImage **)checkUserData(L, index, kRefType);
+// What a Ref object of the scripts holds. In Nancy19 that is one image; Nancy20
+// lays any number of images side by side, and has a color for the places none
+// of them covers.
+struct ScriptRef {
+ struct Layer {
+ RefImage image;
+ ScriptRect rect;
+ };
+
+ ~ScriptRef();
+
+ bool getPixel(int x, int y, uint32 &argb) const;
+
+ Common::Array<Layer *> layers;
+ RefEdgeMode edgeMode = kRefClamp;
+ bool hasBorder = false; // Nancy20
+ uint32 borderColor = 0x7FFFFFFF;
+};
+
+ScriptRef::~ScriptRef() {
+ for (Layer *layer : layers) {
+ delete layer;
+ }
+}
+
+bool ScriptRef::getPixel(int x, int y, uint32 &argb) const {
+ if (!hasBorder) {
+ return !layers.empty() && layers[0]->image.getPixel(x, y, argb);
+ }
+
+ for (const Layer *layer : layers) {
+ if (layer->rect.contains(x, y)) {
+ return layer->image.getPixel(x - layer->rect.left, y - layer->rect.top, argb);
+ }
+ }
+
+ argb = borderColor;
+ return true;
+}
+
+static ScriptRef *checkRef(lua_State *L, int index) {
+ return *(ScriptRef **)checkUserData(L, index, kRefType);
+}
+
+// Puts an image into a Nancy20 Ref, unless it would overlap one that is in it
+// already. An empty rectangle stands for the size of the image.
+static bool addRefLayer(ScriptRef &ref, const Common::String &imageName, const ScriptRect &rect) {
+ ScriptRef::Layer *layer = new ScriptRef::Layer();
+ layer->image.edgeMode = ref.edgeMode;
+ if (!g_nancy->_resource->loadImage(Common::Path(imageName), layer->image.image)) {
+ warning("Could not load Ref %s", imageName.c_str());
+ }
+
+ layer->rect = rect;
+ if (rect.isEmpty()) {
+ layer->rect = ScriptRect(0, 0, layer->image.image.w, layer->image.image.h);
+ }
+
+ for (const ScriptRef::Layer *other : ref.layers) {
+ const ScriptRect &a = other->rect;
+ const ScriptRect &b = layer->rect;
+ if (MAX(a.left, b.left) < MIN(a.right, b.right) && MAX(a.top, b.top) < MIN(a.bottom, b.bottom)) {
+ delete layer;
+ return false;
+ }
+ }
+
+ ref.layers.push_back(layer);
+ return true;
}
static ScriptPoint checkPlace(lua_State *L, int index) {
@@ -73,35 +140,103 @@ static ScriptPoint checkPlace(lua_State *L, int index) {
return point;
}
-// Ref:New(image, edgeMode)
+// Ref:New(image, edgeMode) in Nancy19. Nancy20 takes a table instead:
+// Ref:New({ mode = "clamp" or "wrap", borderColor = color, ref = image })
static int refNew(lua_State *L) {
- const char *imageName = luaL_checkstring(L, 2);
- const Common::String edgeMode = luaL_optstring(L, 3, "clamp");
+ ScriptRef *ref = new ScriptRef();
+
+ if (lua_istable(L, 2)) {
+ ref->hasBorder = true;
+
+ Common::String edgeMode = "clamp";
+ readField(L, 2, "mode", edgeMode);
+ if (edgeMode == "wrap") {
+ ref->edgeMode = kRefWrap;
+ } else if (edgeMode != "clamp") {
+ delete ref;
+ return luaL_argerror(L, 2, "Ref mode unrecognized");
+ }
- RefImage *ref = new RefImage();
- if (edgeMode == "wrap") {
- ref->edgeMode = kRefWrap;
- } else if (edgeMode == "dismiss") {
- ref->edgeMode = kRefDismiss;
- }
+ if (pushField(L, 2, "borderColor")) {
+ ref->borderColor = checkColor(L, -1);
+ lua_pop(L, 1);
+ }
- g_nancy->_resource->loadImage(Common::Path(imageName), ref->image);
+ Common::String imageName;
+ if (readField(L, 2, "ref", imageName)) {
+ addRefLayer(*ref, imageName, ScriptRect());
+ }
+ } else {
+ const char *imageName = luaL_checkstring(L, 2);
+ const Common::String edgeMode = luaL_optstring(L, 3, "clamp");
+
+ if (edgeMode == "wrap") {
+ ref->edgeMode = kRefWrap;
+ } else if (edgeMode == "dismiss") {
+ ref->edgeMode = kRefDismiss;
+ }
- RefImage **object = (RefImage **)pushUserData(L, sizeof(RefImage *), kRefType);
+ ScriptRef::Layer *layer = new ScriptRef::Layer();
+ layer->image.edgeMode = ref->edgeMode;
+ g_nancy->_resource->loadImage(Common::Path(imageName), layer->image.image);
+ ref->layers.push_back(layer);
+ }
+
+ ScriptRef **object = (ScriptRef **)pushUserData(L, sizeof(ScriptRef *), kRefType);
*object = ref;
return 1;
}
static int refCollect(lua_State *L) {
- RefImage **object = (RefImage **)checkUserData(L, 1, kRefType);
+ ScriptRef **object = (ScriptRef **)checkUserData(L, 1, kRefType);
delete *object;
*object = nullptr;
return 0;
}
+// ref:Add(image, rect), Nancy20. Returns false when the image would overlap
+// one that is in the Ref already.
+static int refAdd(lua_State *L) {
+ ScriptRef *ref = checkRef(L, 1);
+ const char *imageName = luaL_checkstring(L, 2);
+ const ScriptRect &rect = checkRect(L, 3);
+
+ lua_pushboolean(L, addRefLayer(*ref, imageName, rect));
+ return 1;
+}
+
+// ref:Remove(rect), Nancy20, which takes out the image put at that rectangle
+static int refRemove(lua_State *L) {
+ ScriptRef *ref = checkRef(L, 1);
+ const ScriptRect &rect = checkRect(L, 2);
+
+ for (uint i = 0; i < ref->layers.size(); ++i) {
+ if (ref->layers[i]->rect == rect) {
+ delete ref->layers[i];
+ ref->layers.remove_at(i);
+ lua_pushboolean(L, true);
+ return 1;
+ }
+ }
+
+ lua_pushboolean(L, false);
+ return 1;
+}
+
+// ref:At(place), Nancy20, the color there
+static int refAt(lua_State *L) {
+ const ScriptRef *ref = checkRef(L, 1);
+ const ScriptPoint place = checkPlace(L, 2);
+
+ uint32 pixel = 0;
+ ref->getPixel(place.x, place.y, pixel);
+ pushColor(L, pixel);
+ return 1;
+}
+
// ref:IsAt(place, color)
static int refIsAt(lua_State *L) {
- const RefImage *ref = checkRef(L, 1);
+ const ScriptRef *ref = checkRef(L, 1);
const ScriptPoint place = checkPlace(L, 2);
const uint32 color = checkColor(L, 3);
@@ -112,7 +247,7 @@ static int refIsAt(lua_State *L) {
// ref:IsOpaqueAt(place)
static int refIsOpaqueAt(lua_State *L) {
- const RefImage *ref = checkRef(L, 1);
+ const ScriptRef *ref = checkRef(L, 1);
const ScriptPoint place = checkPlace(L, 2);
uint32 pixel;
@@ -122,7 +257,7 @@ static int refIsOpaqueAt(lua_State *L) {
// How much of the square of the given size around a place passes a test, from 0 to 1.
// The places that have no pixel count as failing it.
-static double getAreaShare(const RefImage *ref, const ScriptPoint &place, int size, bool testColor, uint32 color) {
+static double getAreaShare(const ScriptRef *ref, const ScriptPoint &place, int size, bool testColor, uint32 color) {
if (size <= 0) {
return 0.0;
}
@@ -149,7 +284,7 @@ static double getAreaShare(const RefImage *ref, const ScriptPoint &place, int si
// ref:IsInArea(place, color, size)
static int refIsInArea(lua_State *L) {
- const RefImage *ref = checkRef(L, 1);
+ const ScriptRef *ref = checkRef(L, 1);
const ScriptPoint place = checkPlace(L, 2);
const uint32 color = checkColor(L, 3);
const int size = luaL_checkint(L, 4);
@@ -160,7 +295,7 @@ static int refIsInArea(lua_State *L) {
// ref:IsOpaqueInArea(place, size)
static int refIsOpaqueInArea(lua_State *L) {
- const RefImage *ref = checkRef(L, 1);
+ const ScriptRef *ref = checkRef(L, 1);
const ScriptPoint place = checkPlace(L, 2);
const int size = luaL_checkint(L, 3);
@@ -172,6 +307,9 @@ void registerRefAPI(lua_State *L) {
static const luaL_Reg kRefFunctions[] = {
{ "__index", indexFunctions },
{ "__gc", refCollect },
+ { "Add", refAdd },
+ { "Remove", refRemove },
+ { "At", refAt },
{ "IsAt", refIsAt },
{ "IsOpaqueAt", refIsOpaqueAt },
{ "IsInArea", refIsInArea },
Commit: bae59483587be54e71b5febb3e7d12b2314f7fa5
https://github.com/scummvm/scummvm/commit/bae59483587be54e71b5febb3e7d12b2314f7fa5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:11+03:00
Commit Message:
NANCY: NANCY20: Add the new functions of the script objects
Scripts can now change the scene of a stream, read how long the last
frame took, get the volume of a channel and whether it plays, draw
lines and circles, use more keys, and put the held item back into the
inventory, among others.
Changed paths:
engines/nancy/script/gameapi.cpp
engines/nancy/script/scenecontext.cpp
engines/nancy/script/scenecontext.h
engines/nancy/sound.cpp
engines/nancy/sound.h
diff --git a/engines/nancy/script/gameapi.cpp b/engines/nancy/script/gameapi.cpp
index 269231a8a4a..4766ada03cc 100644
--- a/engines/nancy/script/gameapi.cpp
+++ b/engines/nancy/script/gameapi.cpp
@@ -233,6 +233,12 @@ static int cursorIndex(lua_State *L) {
return 1;
}
+ if (key == "inViewport") {
+ ScriptPoint point;
+ lua_pushboolean(L, SceneContext::getViewportMouse(g_nancy->_input->getInput(), point));
+ return 1;
+ }
+
return luaL_error(L, "Tried to change the cursor to %s, which is not valid.", name);
}
@@ -260,8 +266,25 @@ static int cursorVariantIndex(lua_State *L) {
return 1;
}
+// Nancy20: "Cursor.currentInv = nil" puts the item that is held back into the
+// inventory. Items are only ever put into the hand by Player:AddInventoryItem().
+static int cursorNewIndex(lua_State *L) {
+ const char *name = luaL_checkstring(L, 2);
+ if (strcmp(name, "currentInv") != 0) {
+ return luaL_error(L, "Cursor does not have field %s", name);
+ }
+
+ if (!lua_isnil(L, 3)) {
+ return luaL_error(L, "Inventory cursor is only settable to nil. Use Player:AddInventoryItem(...) to put in hand.");
+ }
+
+ NancySceneState.setNoHeldItem();
+ return 0;
+}
+
static const luaL_Reg kCursorFunctions[] = {
{ "__index", cursorIndex },
+ { "__newindex", cursorNewIndex },
{ nullptr, nullptr }
};
@@ -284,6 +307,8 @@ static const KeyName kKeyNames[] = {
{ "SPACE", Common::KEYCODE_SPACE },
{ "END", Common::KEYCODE_END },
{ "HOME", Common::KEYCODE_HOME },
+ { "PGUP", Common::KEYCODE_PAGEUP },
+ { "PGDOWN", Common::KEYCODE_PAGEDOWN },
{ "LEFT", Common::KEYCODE_LEFT },
{ "UP", Common::KEYCODE_UP },
{ "RIGHT", Common::KEYCODE_RIGHT },
@@ -296,7 +321,18 @@ static const KeyName kKeyNames[] = {
{ "TAB", Common::KEYCODE_TAB },
{ "CONTROL", Common::KEYCODE_LCTRL },
{ "CTRL", Common::KEYCODE_LCTRL },
- { "SHIFT", Common::KEYCODE_LSHIFT }
+ { "SHIFT", Common::KEYCODE_LSHIFT },
+ { "BACKSPACE", Common::KEYCODE_BACKSPACE }, // Nancy20
+ { "NUMPAD0", Common::KEYCODE_KP0 },
+ { "NUMPAD1", Common::KEYCODE_KP1 },
+ { "NUMPAD2", Common::KEYCODE_KP2 },
+ { "NUMPAD3", Common::KEYCODE_KP3 },
+ { "NUMPAD4", Common::KEYCODE_KP4 },
+ { "NUMPAD5", Common::KEYCODE_KP5 },
+ { "NUMPAD6", Common::KEYCODE_KP6 },
+ { "NUMPAD7", Common::KEYCODE_KP7 },
+ { "NUMPAD8", Common::KEYCODE_KP8 },
+ { "NUMPAD9", Common::KEYCODE_KP9 }
};
void pushKey(lua_State *L, Common::KeyCode key) {
@@ -389,20 +425,97 @@ static const luaL_Reg kGameFunctions[] = {
// Draw, for the "Render" functions of the records
// TODO: Draw:Tri
+static Graphics::ManagedSurface *checkDrawTarget(lua_State *L) {
+ Graphics::ManagedSurface *target = getDrawTarget();
+ if (!target) {
+ luaL_error(L, "Draw can only be used while a record is rendering");
+ }
+
+ return target;
+}
+
+static uint32 getDrawColor(const Graphics::ManagedSurface &target, uint32 argb) {
+ return target.format.ARGBToColor(argb >> 24, (argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF);
+}
+
+// A place given as a point, a vector or a table with x and y
+static void checkDrawPlace(lua_State *L, int index, double &x, double &y) {
+ index = absoluteIndex(L, index);
+ if (lua_type(L, index) != LUA_TUSERDATA && lua_type(L, index) != LUA_TTABLE) {
+ luaL_argerror(L, index, "Parameter must be a Point, a Vector or a table with x and y fields");
+ }
+
+ lua_getfield(L, index, "x");
+ lua_getfield(L, index, "y");
+ if (!lua_isnumber(L, -2) || !lua_isnumber(L, -1)) {
+ luaL_argerror(L, index, "Parameter must be a Point, a Vector or a table with x and y fields");
+ }
+
+ x = lua_tonumber(L, -2);
+ y = lua_tonumber(L, -1);
+ lua_pop(L, 2);
+}
+
// Draw:Rect(rect, color)
static int drawRect(lua_State *L) {
const ScriptRect &rect = checkRect(L, 2);
const uint32 color = checkColor(L, 3);
-
- Graphics::ManagedSurface *target = getDrawTarget();
- if (!target) {
- return luaL_error(L, "Draw can only be used while a record is rendering");
- }
+ Graphics::ManagedSurface *target = checkDrawTarget(L);
Common::Rect area = rect.toRect();
area.clip(Common::Rect(target->w, target->h));
if (!area.isEmpty()) {
- target->fillRect(area, target->format.ARGBToColor(color >> 24, (color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF));
+ target->fillRect(area, getDrawColor(*target, color));
+ }
+
+ return 0;
+}
+
+// Draw:Line(from, to, color), Nancy20
+static int drawLine(lua_State *L) {
+ double x0, y0, x1, y1;
+ checkDrawPlace(L, 2, x0, y0);
+ checkDrawPlace(L, 3, x1, y1);
+ const uint32 color = checkColor(L, 4);
+ Graphics::ManagedSurface *target = checkDrawTarget(L);
+
+ target->drawLine((int)x0, (int)y0, (int)x1, (int)y1, getDrawColor(*target, color));
+ return 0;
+}
+
+// Draw:Circle(circle, color), Nancy20, where the circle is a table with a
+// center and a radius. Only the outline is drawn, as a polygon of 19 sides.
+static int drawCircle(lua_State *L) {
+ static const int kNumPoints = 20;
+
+ luaL_checktype(L, 2, LUA_TTABLE);
+ lua_getfield(L, 2, "center");
+ lua_getfield(L, 2, "radius");
+ if (lua_isnil(L, -2) || !lua_isnumber(L, -1)) {
+ return luaL_argerror(L, 2, "Parameter is not a circle (table with center and radius fields)");
+ }
+
+ double centerX, centerY;
+ checkDrawPlace(L, -2, centerX, centerY);
+ const double radius = lua_tonumber(L, -1);
+ lua_pop(L, 2);
+
+ const uint32 color = checkColor(L, 3);
+ Graphics::ManagedSurface *target = checkDrawTarget(L);
+ const uint32 drawColor = getDrawColor(*target, color);
+
+ int lastX = 0;
+ int lastY = 0;
+ for (int i = 0; i < kNumPoints; ++i) {
+ const double angle = i / (double)(kNumPoints - 1) * 2.0 * M_PI;
+ const int x = (int)(cos(angle) * radius + centerX);
+ const int y = (int)(sin(angle) * radius + centerY);
+ if (i > 0) {
+ target->drawLine(lastX, lastY, x, y, drawColor);
+ }
+
+ lastX = x;
+ lastY = y;
}
return 0;
@@ -411,6 +524,8 @@ static int drawRect(lua_State *L) {
static const luaL_Reg kDrawFunctions[] = {
{ "__index", indexFunctions },
{ "Rect", drawRect },
+ { "Line", drawLine },
+ { "Circle", drawCircle },
{ nullptr, nullptr }
};
@@ -721,6 +836,33 @@ static int soundSetVolume(lua_State *L) {
return 0;
}
+// Sound:GetVolume(channel), from 0 to 1. It has to be a single channel.
+static int soundGetVolume(lua_State *L) {
+ const uint16 channel = checkSoundChannel(L, 2);
+ uint16 first, last;
+ getChannelRange(channel, first, last);
+ if (first != last) {
+ return luaL_error(L, "Passed in Channel Group to GetVolume.");
+ }
+
+ lua_pushnumber(L, g_nancy->_sound->getRequestedVolume(channel) / 100.0);
+ return 1;
+}
+
+// Sound:IsPlaying(channel), which is true for a group when any of its channels plays
+static int soundIsPlaying(lua_State *L) {
+ uint16 first, last;
+ getChannelRange(checkSoundChannel(L, 2), first, last);
+
+ bool isPlaying = false;
+ for (uint16 channel = first; channel <= last && !isPlaying; ++channel) {
+ isPlaying = g_nancy->_sound->isSoundPlaying(channel);
+ }
+
+ lua_pushboolean(L, isPlaying);
+ return 1;
+}
+
static const luaL_Reg kSoundChannelHelperFunctions[] = {
{ "__index", soundChannelIndex },
{ nullptr, nullptr }
@@ -734,6 +876,8 @@ static const luaL_Reg kSoundFunctions[] = {
{ "__index", indexFunctions },
{ "Stop", soundStop },
{ "SetVolume", soundSetVolume },
+ { "GetVolume", soundGetVolume },
+ { "IsPlaying", soundIsPlaying },
{ nullptr, nullptr }
};
@@ -756,6 +900,8 @@ static int sceneIndex(lua_State *L) {
lua_pushstring(L, context->getSceneName().c_str());
} else if (key == "time") {
lua_pushnumber(L, context->getTime());
+ } else if (key == "elapsedTime") {
+ lua_pushnumber(L, context->getElapsedTime());
} else if (key == "frame") {
lua_pushinteger(L, context->getFrame());
} else if (key == "streamName") {
@@ -804,6 +950,17 @@ static int sceneInclude(lua_State *L) {
return 0;
}
+// Scene:ChangeScene(stream, scene), which goes to the first frame of the scene
+static int sceneChangeScene(lua_State *L) {
+ SceneContext *context = checkContext(L);
+ const char *streamName = luaL_checkstring(L, 2);
+ const char *sceneName = luaL_checkstring(L, 3);
+
+ context->changeScene(streamName, sceneName, 0);
+ lua_pushboolean(L, true);
+ return 1;
+}
+
// Scene:BeginStream(name, scene, frame, lifetime, capturesInput, runsFirst)
static int sceneBeginStream(lua_State *L) {
checkContext(L);
@@ -892,6 +1049,7 @@ static const luaL_Reg kSceneFunctions[] = {
{ "Include", sceneInclude },
{ "BeginStream", sceneBeginStream },
{ "EndStream", sceneEndStream },
+ { "ChangeScene", sceneChangeScene },
{ nullptr, nullptr }
};
@@ -1256,13 +1414,58 @@ static int logWrite(lua_State *L) {
return 0;
}
+// Log:Assert(condition, text) writes down the text when the condition doesn't hold
+static int logAssert(lua_State *L) {
+ if (!lua_toboolean(L, 2)) {
+ luaL_where(L, 1);
+ debugC(1, kDebugScene, "Script assertion failed: %s%s", lua_tostring(L, -1), luaL_optstring(L, 3, ""));
+ lua_pop(L, 1);
+ }
+
+ return 0;
+}
+
+// Log:Watch(function) and Log:Clear() are for the debug console of the original,
+// which isn't there
+static int logWatch(lua_State *L) {
+ luaL_checktype(L, 2, LUA_TFUNCTION);
+ return 0;
+}
+
+static int logClear(lua_State *L) {
+ return 0;
+}
+
static const luaL_Reg kLogFunctions[] = {
{ "__index", indexFunctions },
{ "Write", logWrite },
+ { "Assert", logAssert },
+ { "Watch", logWatch },
+ { "Clear", logClear },
+ { nullptr, nullptr }
+};
+
+// Autotext, for the captions
+
+// Autotext:RemoveCC(anything) takes down the caption that shows. The original
+// only checks which caption it is when no parameter is given, which no script does.
+static int autotextRemoveCC(lua_State *L) {
+ UI::NDUI::Manager *ui = NancySceneState.getNDUI();
+ if (ui) {
+ ui->setCaption(Common::String());
+ }
+
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+static const luaL_Reg kAutotextFunctions[] = {
+ { "__index", indexFunctions },
+ { "RemoveCC", autotextRemoveCC },
{ nullptr, nullptr }
};
-// TODO: The Autotext, Generics and UIMgr objects, and Log:WriteAll
+// TODO: Autotext:ShowCC, the Generics and UIMgr objects, and Log:WriteAll
void registerGameAPI(lua_State *L) {
registerGlobal(L, "Flags", "ND.Flags", kFlagsFunctions);
registerGlobal(L, "VarTable", "ND.VarTable", kVarTableFunctions);
@@ -1281,6 +1484,7 @@ void registerGameAPI(lua_State *L) {
registerGlobal(L, "Sound", "ND.Sound", kSoundFunctions);
registerGlobal(L, "Scene", "ND.Scene", kSceneFunctions);
+ registerGlobal(L, "Autotext", "ND.Autotext", kAutotextFunctions);
registerGlobal(L, "Streams", "ND.Streams", kStreamsFunctions);
registerGlobal(L, "Player", "ND.CurPlayer", kPlayerFunctions);
registerGlobal(L, "Message", "ND.Message", kMessageFunctions);
diff --git a/engines/nancy/script/scenecontext.cpp b/engines/nancy/script/scenecontext.cpp
index aabd18c0726..b390e43bd7a 100644
--- a/engines/nancy/script/scenecontext.cpp
+++ b/engines/nancy/script/scenecontext.cpp
@@ -341,6 +341,7 @@ void SceneContext::runFrame(double elapsedTime) {
lua_State *L = getLua();
_time += elapsedTime;
+ _elapsedTime = elapsedTime;
// The records made since the last frame join in
_records.push_back(_newRecords);
diff --git a/engines/nancy/script/scenecontext.h b/engines/nancy/script/scenecontext.h
index b1f0c281092..a4db8e8642e 100644
--- a/engines/nancy/script/scenecontext.h
+++ b/engines/nancy/script/scenecontext.h
@@ -101,6 +101,7 @@ public:
// The seconds the scene has been running for
double getTime() const { return _time; }
+ double getElapsedTime() const { return _elapsedTime; } // of the last frame
// Enhancement: skips the movies that are playing. They are played through
// a frame at a time, without their sounds, so that everything the scripts
@@ -158,6 +159,7 @@ private:
Common::HashMap<Common::String, RefImage *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _refs;
double _time = 0.0;
+ double _elapsedTime = 0.0;
bool _hasRunOnce = false;
bool _makesUI = false;
bool _isSkipping = false;
diff --git a/engines/nancy/sound.cpp b/engines/nancy/sound.cpp
index a79032c7610..1e86655135a 100644
--- a/engines/nancy/sound.cpp
+++ b/engines/nancy/sound.cpp
@@ -342,6 +342,7 @@ void SoundManager::loadSound(const SoundDescription &description, SoundEffectDes
chan.playCommands = description.playCommands;
chan.numLoops = description.numLoops;
chan.volume = description.volume;
+ chan.requestedVolume = description.volume;
chan.panAnchorFrame = description.panAnchorFrame;
chan.isPanning = description.isPanning;
@@ -580,12 +581,21 @@ byte SoundManager::getVolume(const Common::String &chunkName) {
}
void SoundManager::setVolume(uint16 channelID, uint16 volume) {
- if (channelID >= _channels.size() || !isSoundPlaying(channelID))
+ if (channelID >= _channels.size())
+ return;
+
+ _channels[channelID].requestedVolume = volume;
+
+ if (!isSoundPlaying(channelID))
return;
_mixer->setChannelVolume(_channels[channelID].handle, getAdjustedVolume(volume) * 255 / 100);
}
+uint16 SoundManager::getRequestedVolume(uint16 channelID) const {
+ return channelID < _channels.size() ? _channels[channelID].requestedVolume : 0;
+}
+
void SoundManager::setVolume(const SoundDescription &description, uint16 volume) {
if (description.name != "NO SOUND") {
setVolume(description.channelID, volume);
diff --git a/engines/nancy/sound.h b/engines/nancy/sound.h
index fc047e7c46b..717933abcb4 100644
--- a/engines/nancy/sound.h
+++ b/engines/nancy/sound.h
@@ -94,6 +94,10 @@ public:
void setVolume(const SoundDescription &description, uint16 volume);
void setVolume(const Common::String &chunkName, uint16 volume);
+ // The volume a channel was last given, by the sound loaded into it or by
+ // setVolume(), from 0 to 100. Nancy20 scripts read it back.
+ uint16 getRequestedVolume(uint16 channelID) const;
+
// Nancy 11+ Update3DSound (AR 156). Adjusts a playing fixed-position
// 3D sound's position and/or its min/max audible distance.
void update3DSoundPosition(uint16 channelID, int32 x, int32 y, int32 z);
@@ -144,6 +148,7 @@ protected:
uint16 playCommands = 1;
uint16 numLoops = 0;
uint volume = 0;
+ uint16 requestedVolume = 0;
uint16 panAnchorFrame = 0;
bool isPanning = false;
Audio::SeekableAudioStream *stream = nullptr;
Commit: 33756b3c7faac53dbcc55cc2ea9e9f5f36a97c0b
https://github.com/scummvm/scummvm/commit/33756b3c7faac53dbcc55cc2ea9e9f5f36a97c0b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:14+03:00
Commit Message:
NANCY: NANCY20: Ask before saving over a game
Saving under the name of an existing game now brings up the question
the game asks, and only saves once the answer is yes.
Changed paths:
engines/nancy/ui/ndui/enginehandlers.cpp
engines/nancy/ui/ndui/manager.h
engines/nancy/ui/ndui/saveload.cpp
diff --git a/engines/nancy/ui/ndui/enginehandlers.cpp b/engines/nancy/ui/ndui/enginehandlers.cpp
index bd6925d8af9..7229296f17f 100644
--- a/engines/nancy/ui/ndui/enginehandlers.cpp
+++ b/engines/nancy/ui/ndui/enginehandlers.cpp
@@ -125,6 +125,8 @@ void Manager::takeEngineAction(const Action &action) {
onSaverAction(action);
} else if (action.target.equalsIgnoreCase("Engine_SaveQuery")) {
onSaveQueryAction(action);
+ } else if (action.target.equalsIgnoreCase("Engine_SaveOverwrite")) {
+ onSaveOverwriteAction(action);
} else if (action.target.equalsIgnoreCase("Engine_GameWindow")) {
onGameWindowAction(action);
} else if (action.target.equalsIgnoreCase("Engine_Matte")) {
diff --git a/engines/nancy/ui/ndui/manager.h b/engines/nancy/ui/ndui/manager.h
index 00a08ef8fb7..37af648f8ea 100644
--- a/engines/nancy/ui/ndui/manager.h
+++ b/engines/nancy/ui/ndui/manager.h
@@ -186,6 +186,7 @@ private:
void onLoaderAction(const Action &action);
void onSaverAction(const Action &action);
void onSaveQueryAction(const Action &action);
+ void onSaveOverwriteAction(const Action &action);
void onGameWindowAction(const Action &action);
void onCellPhoneAction(const Action &action);
void onItemStreamClicked(const InvButton &button);
diff --git a/engines/nancy/ui/ndui/saveload.cpp b/engines/nancy/ui/ndui/saveload.cpp
index 66502a3188b..b373318743f 100644
--- a/engines/nancy/ui/ndui/saveload.cpp
+++ b/engines/nancy/ui/ndui/saveload.cpp
@@ -298,6 +298,13 @@ void Manager::onSaverAction(const Action &action) {
takeAction(kActionHide, "SaveDialog", 0.0);
takeAction(kActionSetText, "SaveList", Common::String());
+ // Nancy20 asks first before saving over a game of the same name. The
+ // answer comes back with a value of 0.
+ if (action.value > 0 && findPanel("SaveOverwriteDialog") && findSaveSlot(_typedSaveName) != -1) {
+ takeAction(kActionShow, "SaveOverwriteDialog", 0.0);
+ break;
+ }
+
requestSave(_typedSaveName);
_lastSaveName = _typedSaveName;
_typedSaveName.clear();
@@ -308,6 +315,23 @@ void Manager::onSaverAction(const Action &action) {
}
}
+// The answer to whether to save over a game: yes saves, no goes back to the
+// dialog for saving
+void Manager::onSaveOverwriteAction(const Action &action) {
+ switch (action.type) {
+ case kActionEnable:
+ takeAction(kActionHide, "SaveOverwriteDialog", 0.0);
+ takeAction(kActionSetValue, "Engine_Saver", 0.0);
+ break;
+ case kActionDisable:
+ takeAction(kActionHide, "SaveOverwriteDialog", 0.0);
+ takeAction(kActionShow, "Engine_Saver", 0.0);
+ break;
+ default:
+ break;
+ }
+}
+
void Manager::onGameWindowAction(const Action &action) {
switch (action.type) {
case kActionHide:
Commit: c154ecca5f2544086eb9f0bf62419ff2cd928b82
https://github.com/scummvm/scummvm/commit/c154ecca5f2544086eb9f0bf62419ff2cd928b82
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:15+03:00
Commit Message:
NANCY: NANCY20: Change to unstable - the game now starts
Changed paths:
engines/nancy/detection_tables.h
diff --git a/engines/nancy/detection_tables.h b/engines/nancy/detection_tables.h
index 2ed4a541255..d68ca3bad3c 100644
--- a/engines/nancy/detection_tables.h
+++ b/engines/nancy/detection_tables.h
@@ -1042,7 +1042,7 @@ static const NancyGameDescription gameDescriptions[] = {
AD_ENTRY1s("ciftree.dat", "02d56eb098f317a02ed9a675f40a4468", 91315479),
Common::EN_ANY,
Common::kPlatformWindows,
- ADGF_UNSUPPORTED | ADGF_DROPPLATFORM,
+ ADGF_UNSTABLE | ADGF_DROPPLATFORM,
NANCY8_GUIOPTIONS
},
kGameTypeNancy20
Commit: 491813fff85f75cb099d230d304072a1a539c7b3
https://github.com/scummvm/scummvm/commit/491813fff85f75cb099d230d304072a1a539c7b3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:17+03:00
Commit Message:
NANCY: NANCY10: Show the puzzle arrow over QuizPuzzle text boxes
Fix #17393
Changed paths:
engines/nancy/action/puzzle/quizpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/quizpuzzle.cpp b/engines/nancy/action/puzzle/quizpuzzle.cpp
index ace4563d334..41702434d33 100644
--- a/engines/nancy/action/puzzle/quizpuzzle.cpp
+++ b/engines/nancy/action/puzzle/quizpuzzle.cpp
@@ -922,7 +922,9 @@ void QuizPuzzle::handleInput(NancyInput &input) {
if (!screenRect.contains(input.mousePos))
continue;
- g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ // The puzzle arrow cursor only exists in Nancy 10 and up
+ g_nancy->_cursor->setCursorType(g_nancy->getGameType() >= kGameTypeNancy10 ?
+ CursorManager::kPuzzleArrow : CursorManager::kHotspot);
if (input.input & NancyInput::kLeftMouseButtonUp) {
if (i != _currentBox) {
Commit: ecfd2ad313776609358d8350b8d399b72f14dc94
https://github.com/scummvm/scummvm/commit/ecfd2ad313776609358d8350b8d399b72f14dc94
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:20+03:00
Commit Message:
NANCY: NANCY10: Highlight held items over hotspots with a script cursor
Holding an item over a hotspot whose script sets a cursor now shows
the item's highlighted cursor (e.g. the lariat over the jail chair in
scene 3569), as the original games do
Fix #17394
Changed paths:
engines/nancy/cursor.cpp
diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index 9e25cc44521..ae6bb3bdf5f 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -228,11 +228,13 @@ uint CursorManager::resolveNancy10CursorID(CursorType type, int16 itemID, bool s
// each item getting one [idle, hotspot] pair. Held items only
// override the cursor for kNormal / kHotspot; directional /
// rotate / arrow types render their system sprite directly even
- // while the player is carrying something.
+ // while the player is carrying something. A script cursor of type
+ // 0/1 is a hotspot's hover cursor, so the item takes its highlighted
+ // sprite.
if (itemID != -1 && (type == kNormal || type == kHotspot)) {
_hasItem = true;
const uint itemsOffset = (uint)_numCursorTypes * 2;
- const uint variant = (type == kHotspot) ? 1 : 0;
+ const uint variant = (setFromScript ? hotspotVariant : type == kHotspot) ? 1 : 0;
return itemsOffset + (uint)itemID * 2 + variant;
}
@@ -291,11 +293,12 @@ uint CursorManager::resolveNancy10CursorID(CursorType type, int16 itemID, bool s
uint CursorManager::resolveNancy13CursorID(CursorType type, int16 itemID, bool setFromScript, bool hotspotVariant) {
// Held-item cursors: the item block follows the 45 system-cursor pairs;
// each item owns an [idle, hotspot] pair now indexing _invCursorsSurface
- // (chosen by applyCursor()).
+ // (chosen by applyCursor()). A script cursor of type 0/1 is a hotspot's
+ // hover cursor, so the item takes its highlighted sprite.
if (itemID != -1 && (type == kNormal || type == kHotspot)) {
_hasItem = true;
const uint itemsOffset = (uint)_numCursorTypes * 2;
- const uint variant = (type == kHotspot) ? 1 : 0;
+ const uint variant = (setFromScript ? hotspotVariant : type == kHotspot) ? 1 : 0;
return itemsOffset + (uint)itemID * 2 + variant;
}
Commit: 5a85f0b861db59cee90b1d793c4fe1358e8d5cbf
https://github.com/scummvm/scummvm/commit/5a85f0b861db59cee90b1d793c4fe1358e8d5cbf
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:21+03:00
Commit Message:
NANCY: NANCY10: Fix BeadPuzzle exits, cursor, sounds and captions
The solved exit comes before the give-up one, so backing out of the
unfinished puzzle went to the completed poppy scene. Also read the bead
cursor, loop the placing sound while the bead slides, animate removed
beads off the thread, and show captions for Nancy's result remarks.
Fix #17391, #17392
Changed paths:
engines/nancy/action/puzzle/beadpuzzle.cpp
engines/nancy/action/puzzle/beadpuzzle.h
diff --git a/engines/nancy/action/puzzle/beadpuzzle.cpp b/engines/nancy/action/puzzle/beadpuzzle.cpp
index bb955d8e93d..5563b063380 100644
--- a/engines/nancy/action/puzzle/beadpuzzle.cpp
+++ b/engines/nancy/action/puzzle/beadpuzzle.cpp
@@ -27,6 +27,7 @@
#include "engines/nancy/util.h"
#include "engines/nancy/state/scene.h"
+#include "engines/nancy/ui/textbox.h"
#include "engines/nancy/puzzledata.h"
#include "engines/nancy/action/puzzle/beadpuzzle.h"
@@ -66,7 +67,7 @@ void BeadPuzzle::readData(Common::SeekableReadStream &stream) {
for (int i = 0; i < kMaxSlots; ++i)
_solution[i] = stream.readUint16LE();
- stream.skip(2);
+ _beadCursorType = stream.readSint16LE();
_pickupSound.readNormal(stream);
_placeSound.readNormal(stream);
@@ -85,10 +86,10 @@ void BeadPuzzle::readData(Common::SeekableReadStream &stream) {
_perfectFlag.label = stream.readSint16LE();
_perfectFlag.flag = stream.readByte();
- _exitScene._sceneChange.readData(stream);
- stream.skip(2);
_solveScene._sceneChange.readData(stream);
stream.skip(2);
+ _exitScene._sceneChange.readData(stream);
+ stream.skip(2);
readRect(stream, _exitHotspot);
@@ -117,6 +118,7 @@ void BeadPuzzle::init() {
}
_heldBead = -1;
+ _movingBead = -1;
_subState = kPlaying;
_resultKind = kNoResult;
_resultSoundPlayed = false;
@@ -150,15 +152,35 @@ void BeadPuzzle::execute() {
break;
case kDroppingBead:
- if (g_system->getMillis() >= _dropNextTick) {
- if (_dropCurrentSlot <= (int)_placed.size()) {
- _placed.push_back(_heldBead);
- holdBead(-1, nullptr);
+ // The place sound loops until the bead reaches its slot
+ if (_placeSound.name != "NO SOUND" && !g_nancy->_sound->isSoundPlaying(_placeSound))
+ g_nancy->_sound->playSound(_placeSound);
+
+ if (g_system->getMillis() >= _moveNextTick) {
+ if (_movingSlot <= (int)_placed.size()) {
+ _placed.push_back(_movingBead);
+ _movingBead = -1;
+ g_nancy->_sound->stopSound(_placeSound);
_subState = kPlaying;
persistState();
} else {
- --_dropCurrentSlot;
- _dropNextTick = g_system->getMillis() + kDropTickMs;
+ --_movingSlot;
+ _moveNextTick = g_system->getMillis() + kDropTickMs;
+ }
+ redraw();
+ }
+ break;
+
+ case kRemovingBead:
+ // The removed bead slides off the far end of the thread
+ if (g_system->getMillis() >= _moveNextTick) {
+ if (_movingSlot >= (int)_numSlots - 1) {
+ _movingBead = -1;
+ g_nancy->_sound->stopSound(_removeSound);
+ _subState = kPlaying;
+ } else {
+ ++_movingSlot;
+ _moveNextTick = g_system->getMillis() + kDropTickMs;
}
redraw();
}
@@ -166,13 +188,7 @@ void BeadPuzzle::execute() {
case kShowingResult:
if (!_resultSoundPlayed) {
- const SoundDescription *snd = nullptr;
- if (_resultKind == kPartial) snd = &_partialSound;
- else if (_resultKind == kWrong) snd = &_wrongSound;
- if (snd && snd->name != "NO SOUND") {
- g_nancy->_sound->loadSound(*snd);
- g_nancy->_sound->playSound(*snd);
- }
+ playResultSound(_resultKind == kPartial ? _partialSound : _wrongSound);
_resultSoundPlayed = true;
}
break;
@@ -209,6 +225,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
if (_state != kRun)
return;
if (_subState == kDroppingBead ||
+ _subState == kRemovingBead ||
_subState == kPerfectWaitSound ||
_subState == kExitToSolved ||
_subState == kExitToDefault)
@@ -225,6 +242,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
if (input.input & NancyInput::kLeftMouseButtonUp) {
g_nancy->_sound->stopSound(_partialSound);
g_nancy->_sound->stopSound(_wrongSound);
+ NancySceneState.getTextbox().clear();
if (!_placed.empty())
_placed.pop_back();
holdBead(-1, nullptr);
@@ -240,7 +258,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
for (int i = 0; i < (int)_numBeadTypes; ++i) {
if (_pickupHotspots[i].isEmpty() || !_pickupHotspots[i].contains(mouseVP))
continue;
- g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_beadCursorType, true);
if (input.input & NancyInput::kLeftMouseButtonUp) {
if ((int)_placed.size() < (int)_numSlots) {
holdBead((int16)i, &input);
@@ -255,9 +273,13 @@ void BeadPuzzle::handleInput(NancyInput &input) {
}
if (!_removeHotspot.isEmpty() && _removeHotspot.contains(mouseVP) && !_placed.empty()) {
- g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_beadCursorType, true);
if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _movingBead = _placed.back();
_placed.pop_back();
+ _movingSlot = (int16)_placed.size();
+ _moveNextTick = g_system->getMillis() + kDropTickMs;
+ _subState = kRemovingBead;
if (_removeSound.name != "NO SOUND") {
g_nancy->_sound->loadSound(_removeSound);
g_nancy->_sound->playSound(_removeSound);
@@ -271,7 +293,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
if ((int)_placed.size() < (int)_numSlots &&
!_removeHotspot.isEmpty() &&
_removeHotspot.contains(mouseVP)) {
- g_nancy->_cursor->setCursorType(CursorManager::kDragHand);
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_beadCursorType, true);
if (input.input & NancyInput::kLeftMouseButtonUp) {
g_nancy->_sound->stopSound(_pickupSound);
if (_placeSound.name != "NO SOUND") {
@@ -281,11 +303,10 @@ void BeadPuzzle::handleInput(NancyInput &input) {
// The bead leaves the cursor and slides from the bottom of the
// thread up to the next free slot before it is committed.
_subState = kDroppingBead;
- _heldBeadObject.setVisible(false);
- _heldBeadObject.putDown();
- _dropCurrentSlot = (int16)((int)_numSlots - 1);
- _dropNextTick = g_system->getMillis() + kDropTickMs;
- redraw();
+ _movingBead = _heldBead;
+ _movingSlot = (int16)((int)_numSlots - 1);
+ _moveNextTick = g_system->getMillis() + kDropTickMs;
+ holdBead(-1, nullptr);
}
return;
}
@@ -293,7 +314,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
if (_heldBead >= 0 && _heldBead < (int)_numBeadTypes &&
!_pickupHotspots[_heldBead].isEmpty() &&
_pickupHotspots[_heldBead].contains(mouseVP)) {
- g_nancy->_cursor->setCursorType(CursorManager::kDragHand);
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_beadCursorType, true);
if (input.input & NancyInput::kLeftMouseButtonUp) {
holdBead(-1, nullptr);
g_nancy->_sound->stopSound(_pickupSound);
@@ -322,10 +343,7 @@ void BeadPuzzle::evaluate() {
_resultKind = kPerfect;
if (_perfectFlag.label != -1)
NancySceneState.setEventFlag(_perfectFlag);
- if (_perfectSound.name != "NO SOUND") {
- g_nancy->_sound->loadSound(_perfectSound);
- g_nancy->_sound->playSound(_perfectSound);
- }
+ playResultSound(_perfectSound);
_perfectExitTime = g_system->getMillis() + 4000;
_subState = kPerfectWaitSound;
} else if (mismatches > _mistakeThreshold) {
@@ -341,6 +359,20 @@ void BeadPuzzle::evaluate() {
redraw();
}
+// Plays a result remark and shows its caption, looked up by the sound name.
+void BeadPuzzle::playResultSound(const SoundDescription &sound) {
+ if (sound.name == "NO SOUND")
+ return;
+
+ g_nancy->_sound->loadSound(sound);
+ g_nancy->_sound->playSound(sound);
+
+ Common::String caption = resolveSubtitleText(sound.name);
+ if (caption.empty())
+ caption = resolveSubtitleText(sound.name, Common::String(), "CONVO");
+ showSubtitle(caption);
+}
+
// Puts a bead on the cursor, or takes the held one off it for a bead of -1. The
// input is only needed when picking one up, to place it under the cursor at once.
void BeadPuzzle::holdBead(int16 bead, NancyInput *input) {
@@ -384,10 +416,11 @@ void BeadPuzzle::redraw() {
_drawSurface.blitFrom(_image, src, Common::Point(dst.left, dst.top));
}
- if (_subState == kDroppingBead && _heldBead >= 0 && _heldBead < (int)_numBeadTypes &&
- _dropCurrentSlot >= 0 && _dropCurrentSlot < (int)_numSlots) {
- const Common::Rect &src = _beadSrcRects[_heldBead];
- const Common::Rect &dst = _slotDestRects[_dropCurrentSlot];
+ if ((_subState == kDroppingBead || _subState == kRemovingBead) &&
+ _movingBead >= 0 && _movingBead < (int)_numBeadTypes &&
+ _movingSlot >= 0 && _movingSlot < (int)_numSlots) {
+ const Common::Rect &src = _beadSrcRects[_movingBead];
+ const Common::Rect &dst = _slotDestRects[_movingSlot];
if (!src.isEmpty() && !dst.isEmpty())
_drawSurface.blitFrom(_image, src, Common::Point(dst.left, dst.top));
}
diff --git a/engines/nancy/action/puzzle/beadpuzzle.h b/engines/nancy/action/puzzle/beadpuzzle.h
index 9ff1255bec9..70d230ce2d2 100644
--- a/engines/nancy/action/puzzle/beadpuzzle.h
+++ b/engines/nancy/action/puzzle/beadpuzzle.h
@@ -72,8 +72,10 @@ protected:
uint16 _solution[kMaxSlots] = {}; // expected bead type per slot (file stores type+1; we store the raw value)
+ int16 _beadCursorType = 0; // cursor over the beads and the needle while playing
+
SoundDescription _pickupSound; // played while a bead is held next to the cursor
- SoundDescription _placeSound; // played when a bead is placed onto the thread
+ SoundDescription _placeSound; // loops while a placed bead slides along the thread
SoundDescription _removeSound; // played when a bead is taken off the thread
int16 _mistakeThreshold = 0;
@@ -90,6 +92,7 @@ protected:
enum SubState {
kPlaying = 0,
kDroppingBead,
+ kRemovingBead,
kShowingResult,
kPerfectWaitSound,
kExitToSolved,
@@ -102,8 +105,10 @@ protected:
int16 _heldBead = -1;
MouseFollowObject _heldBeadObject;
- int16 _dropCurrentSlot = 0;
- uint32 _dropNextTick = 0;
+ // The bead sliding along the thread while it is placed or removed
+ int16 _movingBead = -1;
+ int16 _movingSlot = 0;
+ uint32 _moveNextTick = 0;
enum ResultKind { kNoResult, kPerfect, kPartial, kWrong };
ResultKind _resultKind = kNoResult;
@@ -114,6 +119,7 @@ protected:
void redraw();
void holdBead(int16 bead, NancyInput *input);
void evaluate();
+ void playResultSound(const SoundDescription &sound);
void persistState();
};
Commit: f238d8b5b454ab815b3f3ee1eae679bfdd8a2afe
https://github.com/scummvm/scummvm/commit/f238d8b5b454ab815b3f3ee1eae679bfdd8a2afe
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:23+03:00
Commit Message:
NANCY: NANCY10: Fix cursors and the held ring in SortPuzzle
- Show the held ring again after returning from another state, e.g.
the main menu, which clears all render objects
- Use the original cursors: highlighted drag hand over a ring,
highlighted drop hand over a cell while carrying one, and a blank
cursor off the grid, so that the size tag stays readable
- Don't allow leaving the puzzle while a ring is carried, like the
original
- Shrink the cell hotspots like Nancy 12 does, which the original
also does here
- Store a carried ring back in its slot when persisting the state, so
that saving while holding one doesn't lose it
Fix #17390
Changed paths:
engines/nancy/action/puzzle/sortpuzzle.cpp
engines/nancy/action/puzzle/sortpuzzle.h
engines/nancy/cursor.cpp
engines/nancy/cursor.h
diff --git a/engines/nancy/action/puzzle/sortpuzzle.cpp b/engines/nancy/action/puzzle/sortpuzzle.cpp
index a22f4756e09..7c15b082006 100644
--- a/engines/nancy/action/puzzle/sortpuzzle.cpp
+++ b/engines/nancy/action/puzzle/sortpuzzle.cpp
@@ -185,11 +185,6 @@ void SortPuzzle::readDataNancy12(Common::SeekableReadStream &stream) {
_cellWidth = _valueSrcRects[0].width();
_cellHeight = _valueSrcRects[0].height();
-
- // Cells only react to clicks well inside their own bounds, so that the
- // near-contiguous grid doesn't drop a piece into the neighboring slot
- _hotspotInsetX = (_cellWidth - 1) / 4;
- _hotspotInsetY = (_cellHeight - 1) / 4;
}
Common::Rect SortPuzzle::cellSprite(const Cell &cell) const {
@@ -286,8 +281,24 @@ void SortPuzzle::persistState() {
SortPuzzleData *spd = (SortPuzzleData *)NancySceneState.getPuzzleData(SortPuzzleData::getTag());
if (!spd)
return;
+
+ // A piece that is being carried is stored back in the slot it came from,
+ // so that it isn't lost when the puzzle is left or a game is saved
+ Cell *heldSlot = nullptr;
+ Cell emptySlot;
+ if (_hasHeld && _heldFromRow >= 0 && _heldFromCol >= 0) {
+ heldSlot = &_current[_heldFromRow][_heldFromCol];
+ emptySlot = *heldSlot;
+ *heldSlot = _held;
+ heldSlot->isEmpty = false;
+ }
+
packGrid(_current, _rows, _cols, spd->currentState);
packGrid(_solved, _rows, _cols, spd->solvedState);
+
+ if (heldSlot) {
+ *heldSlot = emptySlot;
+ }
}
void SortPuzzle::init() {
@@ -308,6 +319,11 @@ void SortPuzzle::init() {
_cursorImage.setTransparentColor(_drawSurface.getTransparentColor());
}
+ // Cells only react to clicks well inside their own bounds, so that the
+ // near-contiguous grid doesn't drop a piece into the neighboring slot
+ _hotspotInsetX = (_cellWidth - 1) / 4;
+ _hotspotInsetY = (_cellHeight - 1) / 4;
+
initState();
_hasHeld = false;
@@ -317,12 +333,18 @@ void SortPuzzle::init() {
redraw();
}
+// The held piece has to be registered again too when returning from another
+// state (e.g. the main menu), as that clears all render objects
+void SortPuzzle::registerGraphics() {
+ PuzzleRecord::registerGraphics();
+ _heldObject.registerGraphics();
+}
+
void SortPuzzle::execute() {
switch (_state) {
case kBegin:
init();
registerGraphics();
- _heldObject.registerGraphics();
NancySceneState.setNoHeldItem();
_state = kRun;
// fall through
@@ -424,10 +446,10 @@ void SortPuzzle::handleInput(NancyInput &input) {
_heldObject.handleInput(input);
- // Nancy 12 uses the dedicated puzzle hands: a closed one over a piece that can
- // be picked up, an open one over a slot the carried piece can go into. Off the
+ // The dedicated puzzle hands are used: a closed one over a piece that can be
+ // picked up, an open one over a slot the carried piece can go into. Off the
// grid the cursor goes blank, since the carried piece is drawn at the mouse.
- const bool useNewCursors = g_nancy->getGameType() >= kGameTypeNancy12;
+ const bool isNancy12 = g_nancy->getGameType() >= kGameTypeNancy12;
int row, col;
bool hitCell = hitTestCell(mouseVP, row, col);
@@ -438,21 +460,23 @@ void SortPuzzle::handleInput(NancyInput &input) {
}
if (!hitCell) {
- if (hoverExitHotspot(input)) {
+ // Before Nancy 12, the puzzle can't be left while a piece is carried
+ if (_hasHeld && !isNancy12) {
+ g_nancy->_cursor->setCursorType(CursorManager::kNewBlank);
+ } else if (hoverExitHotspot(input)) {
if (input.input & NancyInput::kLeftMouseButtonUp)
_subState = kExitToCancel;
- } else if (_hasHeld && useNewCursors) {
+ } else if (_hasHeld) {
g_nancy->_cursor->setCursorType(CursorManager::kNewBlank);
}
return;
}
- if (useNewCursors) {
- g_nancy->_cursor->setCursorType(_hasHeld ? CursorManager::kNewUseHandHotspot
- : CursorManager::kNewDragHandHotspot);
+ if (!_hasHeld) {
+ g_nancy->_cursor->setCursorType(CursorManager::kNewDragHandHotspot);
} else {
- g_nancy->_cursor->setCursorType(_hasHeld ? CursorManager::kDropHand
- : CursorManager::kHotspot);
+ g_nancy->_cursor->setCursorType(isNancy12 ? CursorManager::kNewUseHandHotspot
+ : CursorManager::kNewDropHandHotspot);
}
if (!(input.input & NancyInput::kLeftMouseButtonUp))
@@ -461,6 +485,8 @@ void SortPuzzle::handleInput(NancyInput &input) {
if (!_hasHeld) {
holdCell(_current[row][col], true, input);
_current[row][col].isEmpty = true;
+ _heldFromRow = row;
+ _heldFromCol = col;
if (_pickupSound.name != "NO SOUND") {
g_nancy->_sound->loadSound(_pickupSound);
g_nancy->_sound->playSound(_pickupSound);
diff --git a/engines/nancy/action/puzzle/sortpuzzle.h b/engines/nancy/action/puzzle/sortpuzzle.h
index 0efe934fb21..4ee27308e23 100644
--- a/engines/nancy/action/puzzle/sortpuzzle.h
+++ b/engines/nancy/action/puzzle/sortpuzzle.h
@@ -40,6 +40,7 @@ public:
virtual ~SortPuzzle() {}
void init() override;
+ void registerGraphics() override;
void readData(Common::SeekableReadStream &stream) override;
void readDataNancy12(Common::SeekableReadStream &stream);
@@ -131,6 +132,8 @@ protected:
Cell _held;
bool _hasHeld = false;
+ int _heldFromRow = -1; // The slot the held piece was picked up from, empty while it is carried
+ int _heldFromCol = -1;
bool _isSolved = false;
MouseFollowObject _heldObject;
diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index ae6bb3bdf5f..099b1424a54 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -285,6 +285,7 @@ uint CursorManager::resolveNancy10CursorID(CursorType type, int16 itemID, bool s
case kNewPuzzleSlideLeft: return kNewPuzzleSlideLeft;
case kNewPuzzleSlideRight: return kNewPuzzleSlideRight;
case kDropHand: return kNewDropHand;
+ case kNewDropHandHotspot: return kNewDropHandHotspot;
default:
return kNewNormal;
}
diff --git a/engines/nancy/cursor.h b/engines/nancy/cursor.h
index c4907db2b9f..348a2d6b100 100644
--- a/engines/nancy/cursor.h
+++ b/engines/nancy/cursor.h
@@ -105,6 +105,7 @@ public:
kNewPuzzleSlideLeft = 51, // Type 25 hotspot
kNewPuzzleSlideRight = 53, // Type 26 hotspot
kNewDropHand = 64, // Type 32 â Hand shown when a held piece is dropped (briefly set on the drop action)
+ kNewDropHandHotspot = 65, // Type 32 hotspot â Hand shown over a slot a held piece can be dropped into
kNewBlank = 72, // Type 36 â Empty sprite, used while a puzzle draws a carried piece at the mouse itself
// Cursor types in Nancy13 and newer games. Nancy13 rebuilt the CURS
Commit: 2224546ffd64f16923209a48bdaf0e57809f9d72
https://github.com/scummvm/scummvm/commit/2224546ffd64f16923209a48bdaf0e57809f9d72
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-10-18T10:43:25+03:00
Commit Message:
NANCY: NANCY13: Add detection for another English version
Fix #17397
Changed paths:
engines/nancy/detection_tables.h
diff --git a/engines/nancy/detection_tables.h b/engines/nancy/detection_tables.h
index d68ca3bad3c..fc46b98c596 100644
--- a/engines/nancy/detection_tables.h
+++ b/engines/nancy/detection_tables.h
@@ -909,6 +909,17 @@ static const NancyGameDescription gameDescriptions[] = {
},
kGameTypeNancy13
},
+ { // MD5 by webangel3 from bug #17397
+ {
+ "nancy13", nullptr,
+ AD_ENTRY1s("ciftree.dat", "9d39bd2ff549c83a8300fae70e057593", 58580611),
+ Common::EN_ANY,
+ Common::kPlatformWindows,
+ ADGF_TESTING | ADGF_DROPPLATFORM,
+ NANCY8_GUIOPTIONS
+ },
+ kGameTypeNancy13
+ },
{ // MD5 by BacchusEmperor from bug #17316
{
"nancy13", nullptr,
More information about the Scummvm-git-logs
mailing list