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

sev- noreply at scummvm.org
Sat Dec 31 19:47:20 UTC 2022


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

Summary:
fdccec381d NEVERHOOD: Make most of fields of ResourceFileEntry private
0adf22278b COMMON: Uplift SafeMutexedSeekableSubReadStream to common
552420ec31 NEVERHOOD: Add support for NHC patch archives
69aa08d531 NEVERHOOD: Move game options from advanced options to custom widget
c0455314c9 NEVERHOOD: Add NHC selection item
103bb43ab2 NEVERHOOD: Load nhc file
031c929681 NEVERHOOD: Add loading of subtitle font
97b500643e NEVERHOOD: Add subtitle player
feb1d77ec7 NEVERHOOD: Show subtitles in smackerplayer
03a59a2953 NEVERHOOD: Support blitting with alphaColor != 0
f4da2d054a NEVERHOOD: Add drawDoubleSurface2Alpha
ffdf90df45 NEVERHOOD: Allocate more space for font surface
5d6ba18008 NEVERHOOD: Support subtitles for sprites
64fb44790e NEVERHOOD: Enable subtitle on/off if .nhc is available
7bb97c18d0 NEVERHOOD: Make subtitles being able to turn off/on
41aa78b8a6 NEVERHOOD: Add ability to support for repeating useful Willie's hint
eec64ea340 NEWS: Add entry about supporting Ctpax-Che at ter & Rigel localizations.


Commit: fdccec381dcd14650a0b476dd775fe99d7f92ca3
    https://github.com/scummvm/scummvm/commit/fdccec381dcd14650a0b476dd775fe99d7f92ca3
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Make most of fields of ResourceFileEntry private

Changed paths:
    engines/neverhood/resourceman.h


diff --git a/engines/neverhood/resourceman.h b/engines/neverhood/resourceman.h
index 339e02aa1c8..45ef6a928b0 100644
--- a/engines/neverhood/resourceman.h
+++ b/engines/neverhood/resourceman.h
@@ -30,10 +30,20 @@
 
 namespace Neverhood {
 
-struct ResourceFileEntry {
+class ResourceMan;
+struct ResourceHandle;
+
+class ResourceFileEntry {
+private:
 	int resourceHandle;
 	BlbArchive *archive;
 	BlbArchiveEntry *archiveEntry;
+
+	friend class ResourceHandle;
+	friend class ResourceMan;
+
+public:
+	ResourceFileEntry() : resourceHandle(-1), archive(nullptr), archiveEntry(nullptr) {}
 };
 
 struct Resource {
@@ -47,8 +57,6 @@ struct ResourceData {
 	ResourceData() : data(NULL), dataRefCount() {}
 };
 
-class ResourceMan;
-
 struct ResourceHandle {
 friend class ResourceMan;
 public:


Commit: 0adf22278b45366e1b22955f64266ad369505892
    https://github.com/scummvm/scummvm/commit/0adf22278b45366e1b22955f64266ad369505892
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
COMMON: Uplift SafeMutexedSeekableSubReadStream to common

Changed paths:
    common/stream.cpp
    common/substream.h
    engines/neverhood/blbarchive.cpp


diff --git a/common/stream.cpp b/common/stream.cpp
index 176e35a9bb8..9eaf4333eb4 100644
--- a/common/stream.cpp
+++ b/common/stream.cpp
@@ -581,4 +581,9 @@ WriteStream *wrapBufferedWriteStream(WriteStream *parentStream, uint32 bufSize)
 	return nullptr;
 }
 
+uint32 SafeMutexedSeekableSubReadStream::read(void *dataPtr, uint32 dataSize) {
+	Common::StackLock lock(_mutex);
+	return Common::SafeSeekableSubReadStream::read(dataPtr, dataSize);
+}
+
 } // End of namespace Common
diff --git a/common/substream.h b/common/substream.h
index a6dd390ba9d..70cf88889d4 100644
--- a/common/substream.h
+++ b/common/substream.h
@@ -22,6 +22,7 @@
 #ifndef COMMON_SUBSTREAM_H
 #define COMMON_SUBSTREAM_H
 
+#include "common/mutex.h"
 #include "common/ptr.h"
 #include "common/stream.h"
 #include "common/types.h"
@@ -133,6 +134,24 @@ public:
 	virtual uint32 read(void *dataPtr, uint32 dataSize);
 };
 
+/**
+ * A special variant of SafeSeekableSubReadStream which locks a mutex during each read.
+ * This is necessary if the music is streamed from disk and it could happen
+ * that a sound effect or another music track is played from the same read stream
+ * while the first music track is updated/read.
+ */
+
+class SafeMutexedSeekableSubReadStream : public Common::SafeSeekableSubReadStream {
+public:
+	SafeMutexedSeekableSubReadStream(SeekableReadStream *parentStream, uint32 begin, uint32 end, DisposeAfterUse::Flag disposeParentStream,
+		Common::Mutex &mutex)
+		: SafeSeekableSubReadStream(parentStream, begin, end, disposeParentStream), _mutex(mutex) {
+	}
+	uint32 read(void *dataPtr, uint32 dataSize) override;
+protected:
+	Common::Mutex &_mutex;
+};
+
 /** @} */
 
 } // End of namespace Common
diff --git a/engines/neverhood/blbarchive.cpp b/engines/neverhood/blbarchive.cpp
index 4b5a8ac589e..53799bcb012 100644
--- a/engines/neverhood/blbarchive.cpp
+++ b/engines/neverhood/blbarchive.cpp
@@ -24,29 +24,6 @@
 
 namespace Neverhood {
 
-/**
- * A special variant of SafeSeekableSubReadStream which locks a mutex during each read.
- * This is necessary because the music is streamed from disk and it could happen
- * that a sound effect or another music track is played from the same read stream
- * while the first music track is updated/read.
- */
-
-class SafeMutexedSeekableSubReadStream : public Common::SafeSeekableSubReadStream {
-public:
-	SafeMutexedSeekableSubReadStream(SeekableReadStream *parentStream, uint32 begin, uint32 end, DisposeAfterUse::Flag disposeParentStream,
-		Common::Mutex &mutex)
-		: SafeSeekableSubReadStream(parentStream, begin, end, disposeParentStream), _mutex(mutex) {
-	}
-	uint32 read(void *dataPtr, uint32 dataSize) override;
-protected:
-	Common::Mutex &_mutex;
-};
-
-uint32 SafeMutexedSeekableSubReadStream::read(void *dataPtr, uint32 dataSize) {
-	Common::StackLock lock(_mutex);
-	return Common::SafeSeekableSubReadStream::read(dataPtr, dataSize);
-}
-
 BlbArchive::BlbArchive() : _extData(nullptr) {
 }
 
@@ -158,7 +135,7 @@ Common::SeekableReadStream *BlbArchive::createStream(uint index) {
 }
 
 Common::SeekableReadStream *BlbArchive::createStream(BlbArchiveEntry *entry) {
-	return new SafeMutexedSeekableSubReadStream(&_fd, entry->offset, entry->offset + entry->diskSize,
+	return new Common::SafeMutexedSeekableSubReadStream(&_fd, entry->offset, entry->offset + entry->diskSize,
 		DisposeAfterUse::NO, _mutex);
 }
 


Commit: 552420ec314b781b3601e4d976d41fb31753cafa
    https://github.com/scummvm/scummvm/commit/552420ec314b781b3601e4d976d41fb31753cafa
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Add support for NHC patch archives

Changed paths:
  A engines/neverhood/nhcarchive.cpp
  A engines/neverhood/nhcarchive.h
    engines/neverhood/module.mk
    engines/neverhood/resource.h
    engines/neverhood/resourceman.cpp
    engines/neverhood/resourceman.h


diff --git a/engines/neverhood/module.mk b/engines/neverhood/module.mk
index 364df833c8b..e7b7e97167e 100644
--- a/engines/neverhood/module.mk
+++ b/engines/neverhood/module.mk
@@ -56,6 +56,7 @@ MODULE_OBJS = \
 	mouse.o \
 	navigationscene.o \
 	neverhood.o \
+	nhcarchive.o \
 	palette.o \
 	resource.o \
 	resourceman.o \
diff --git a/engines/neverhood/nhcarchive.cpp b/engines/neverhood/nhcarchive.cpp
new file mode 100644
index 00000000000..213763b3ed6
--- /dev/null
+++ b/engines/neverhood/nhcarchive.cpp
@@ -0,0 +1,83 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/compression/dcl.h"
+#include "neverhood/nhcarchive.h"
+
+namespace Neverhood {
+
+bool NhcArchive::open(const Common::String &filename, bool isOptional) {
+	_entries.clear();
+
+	if (!_fd.open(filename)) {
+		if (!isOptional)
+			error("NhcArchive::open() Could not open %s", filename.c_str());
+		return false;
+	}
+
+	uint32 id = _fd.readUint32BE();
+	if (id != MKTAG('N', 'H', 'C', 0))
+		return false;
+	/* version = */ _fd.readUint32LE();
+	/* totalSize = */ _fd.readUint32LE();
+	uint32 fileCount = _fd.readUint32LE();
+
+	debug(4, "%s: fileCount = %d", filename.c_str(), fileCount);
+
+	_entries.reserve(fileCount);
+
+	// Load file hashes
+	for (uint i = 0; i < fileCount; i++) {
+		NhcArchiveEntry entry;
+		entry.fileHash = _fd.readUint32LE();
+		entry.type = _fd.readUint32LE();
+		entry.offset = _fd.readUint32LE();
+		entry.size = _fd.readUint32LE();
+		_entries.push_back(entry);
+	}
+
+	return true;
+}
+
+void NhcArchive::load(uint index, byte *buffer, uint32 size) {
+	load(&_entries[index], buffer, size);
+}
+
+void NhcArchive::load(NhcArchiveEntry *entry, byte *buffer, uint32 size) {
+	Common::StackLock lock(_mutex);
+
+	_fd.seek(entry->offset);
+
+	if (size == 0)
+		size = entry->size;
+	_fd.read(buffer, size);
+}
+
+Common::SeekableReadStream *NhcArchive::createStream(uint index) {
+	return createStream(&_entries[index]);
+}
+
+Common::SeekableReadStream *NhcArchive::createStream(NhcArchiveEntry *entry) {
+	return new Common::SafeMutexedSeekableSubReadStream(&_fd, entry->offset, entry->offset + entry->size,
+		DisposeAfterUse::NO, _mutex);
+}
+
+} // End of namespace Neverhood
diff --git a/engines/neverhood/nhcarchive.h b/engines/neverhood/nhcarchive.h
new file mode 100644
index 00000000000..a574bb3cb82
--- /dev/null
+++ b/engines/neverhood/nhcarchive.h
@@ -0,0 +1,68 @@
+/* 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 NEVERHOOD_NHCARCHIVE_H
+#define NEVERHOOD_NHCARCHIVE_H
+
+#include "common/array.h"
+#include "common/file.h"
+#include "common/mutex.h"
+#include "common/stream.h"
+#include "common/substream.h"
+#include "neverhood/neverhood.h"
+
+namespace Neverhood {
+
+struct NhcArchiveEntry {
+	uint32 fileHash;
+	uint32 type;
+	uint32 offset;
+	uint32 size;
+
+	bool isNormal() const {
+		// Resources 0-10 replace resources with the same ID
+		// Resources 11 and 12 are under custom ID for subtitle font and saveload dialog messages
+		// 13 adds subtitles to a video with the same ID and so should not replace it
+		return type <= 10;
+	}
+};
+
+class NhcArchive {
+public:
+	NhcArchive() {}
+	~NhcArchive() {}
+	bool open(const Common::String &filename, bool isOptional);
+	void load(uint index, byte *buffer, uint32 size);
+	void load(NhcArchiveEntry *entry, byte *buffer, uint32 size);
+	uint32 getSize(uint index) { return _entries[index].size; }
+	NhcArchiveEntry *getEntry(uint index) { return &_entries[index]; }
+	uint getCount() { return _entries.size(); }
+	Common::SeekableReadStream *createStream(uint index);
+	Common::SeekableReadStream *createStream(NhcArchiveEntry *entry);
+private:
+	Common::File _fd;
+	Common::Mutex _mutex;
+	Common::Array<NhcArchiveEntry> _entries;
+};
+
+} // End of namespace Neverhood
+
+#endif /* NEVERHOOD_BLBARCHIVE_H */
diff --git a/engines/neverhood/resource.h b/engines/neverhood/resource.h
index 9fdb56975cc..ad471deedb1 100644
--- a/engines/neverhood/resource.h
+++ b/engines/neverhood/resource.h
@@ -41,6 +41,13 @@ enum {
 	kResTypeVideo		= 10
 };
 
+enum {
+	kResNhcTypeSubFont	= 11,
+	kResNhcTypeMessages	= 12,
+	kResNhcTypeSubtitles	= 13
+};
+
+
 class SpriteResource {
 public:
 	SpriteResource(NeverhoodEngine *vm);
diff --git a/engines/neverhood/resourceman.cpp b/engines/neverhood/resourceman.cpp
index 76f6dc6507d..2f0394f9873 100644
--- a/engines/neverhood/resourceman.cpp
+++ b/engines/neverhood/resourceman.cpp
@@ -46,7 +46,7 @@ void ResourceMan::addArchive(const Common::String &filename, bool isOptional) {
 		BlbArchiveEntry *archiveEntry = archive->getEntry(archiveEntryIndex);
 		ResourceFileEntry *entry = findEntrySimple(archiveEntry->fileHash);
 		if (entry) {
-			if (archiveEntry->timeStamp > entry->archiveEntry->timeStamp) {
+			if (entry->archiveEntry == nullptr || archiveEntry->timeStamp > entry->archiveEntry->timeStamp) {
 				entry->archive = archive;
 				entry->archiveEntry = archiveEntry;
 			}
@@ -55,11 +55,39 @@ void ResourceMan::addArchive(const Common::String &filename, bool isOptional) {
 			newEntry.resourceHandle = -1;
 			newEntry.archive = archive;
 			newEntry.archiveEntry = archiveEntry;
+			newEntry.nhcArchive = nullptr;
+			newEntry.nhcArchiveEntry = nullptr;
 			_entries[archiveEntry->fileHash] = newEntry;
 		}
 	}
 }
 
+bool ResourceMan::addNhcArchive(const Common::String &filename) {
+	NhcArchive *archive = new NhcArchive();
+	if (!archive->open(filename, true))
+		return false;
+	_nhcArchives.push_back(archive);
+	debug(3, "ResourceMan::addArchive(%s) %d files", filename.c_str(), archive->getCount());
+	for (uint archiveEntryIndex = 0; archiveEntryIndex < archive->getCount(); archiveEntryIndex++) {
+		NhcArchiveEntry *archiveEntry = archive->getEntry(archiveEntryIndex);
+		ResourceFileEntry *entry = findEntrySimple(archiveEntry->fileHash);
+		if (entry) {
+			entry->nhcArchive = archive;
+			entry->nhcArchiveEntry = archiveEntry;
+		} else {
+			ResourceFileEntry newEntry;
+			newEntry.resourceHandle = -1;
+			newEntry.archive = nullptr;
+			newEntry.archiveEntry = nullptr;
+			newEntry.nhcArchive = archive;
+			newEntry.nhcArchiveEntry = archiveEntry;
+			_entries[archiveEntry->fileHash] = newEntry;
+		}
+	}
+
+	return true;
+}
+
 ResourceFileEntry *ResourceMan::findEntrySimple(uint32 fileHash) {
 	EntriesMap::iterator p = _entries.find(fileHash);
 	return p != _entries.end() ? &(*p)._value : nullptr;
@@ -69,14 +97,29 @@ ResourceFileEntry *ResourceMan::findEntry(uint32 fileHash, ResourceFileEntry **f
 	ResourceFileEntry *entry = findEntrySimple(fileHash);
 	if (firstEntry)
 		*firstEntry = entry;
-	for (; entry && entry->archiveEntry->comprType == 0x65; fileHash = entry->archiveEntry->diskSize)
+	for (; entry && entry->archiveEntry != nullptr && entry->archiveEntry->comprType == 0x65; fileHash = entry->archiveEntry->diskSize)
 		entry = findEntrySimple(fileHash);
 	return entry;
 }
 
 Common::SeekableReadStream *ResourceMan::createStream(uint32 fileHash) {
 	ResourceFileEntry *entry = findEntry(fileHash);
-	return entry ? entry->archive->createStream(entry->archiveEntry) : nullptr;
+	if (!entry)
+		return nullptr;
+	if (entry->nhcArchiveEntry && entry->nhcArchive && entry->nhcArchiveEntry->isNormal())
+		return entry->nhcArchive->createStream(entry->nhcArchiveEntry);
+	if (entry->archiveEntry && entry->archive)
+		return entry->archive->createStream(entry->archiveEntry);
+	return nullptr;
+}
+
+Common::SeekableReadStream *ResourceMan::createNhcStream(uint32 fileHash, uint32 type) {
+	ResourceFileEntry *entry = findEntry(fileHash);
+	if (!entry)
+		return nullptr;
+	if (entry->nhcArchiveEntry && entry->nhcArchive && entry->nhcArchiveEntry->type == type)
+		return entry->nhcArchive->createStream(entry->nhcArchiveEntry);
+	return nullptr;
 }
 
 void ResourceMan::queryResource(uint32 fileHash, ResourceHandle &resourceHandle) {
@@ -138,19 +181,26 @@ void ResourceMan::loadResource(ResourceHandle &resourceHandle, bool applyResourc
 		if (resourceData->data != nullptr) {
 			resourceData->dataRefCount++;
 		} else {
-			BlbArchiveEntry *entry = resourceHandle._resourceFileEntry->archiveEntry;
-
-			// Apply fixes for broken resources in Russian versions
-			if (applyResourceFixes) {
-				for (const EntrySizeFix *cur = entrySizeFixes; cur->fileHash > 0; ++cur) {
-					if (entry->fileHash == cur->fileHash && entry->offset == cur->offset &&
-						entry->diskSize == cur->diskSize && entry->size == cur->size)
-						entry->size = cur->fixedSize;
+			NhcArchiveEntry *nhcEntry = resourceHandle._resourceFileEntry->nhcArchiveEntry;
+			// TODO: types B (subfont), C (MgsText), D (SubText)
+			if (nhcEntry && nhcEntry->isNormal()) {
+				resourceData->data = new byte[nhcEntry->size];
+				resourceHandle._resourceFileEntry->nhcArchive->load(nhcEntry, resourceData->data, 0);
+			} else {
+				BlbArchiveEntry *entry = resourceHandle._resourceFileEntry->archiveEntry;
+
+				// Apply fixes for broken resources in Russian versions
+				if (applyResourceFixes) {
+					for (const EntrySizeFix *cur = entrySizeFixes; cur->fileHash > 0; ++cur) {
+						if (entry->fileHash == cur->fileHash && entry->offset == cur->offset &&
+						    entry->diskSize == cur->diskSize && entry->size == cur->size)
+							entry->size = cur->fixedSize;
+					}
 				}
-			}
 
-			resourceData->data = new byte[entry->size];
-			resourceHandle._resourceFileEntry->archive->load(entry, resourceData->data, 0);
+				resourceData->data = new byte[entry->size];
+				resourceHandle._resourceFileEntry->archive->load(entry, resourceData->data, 0);
+			}
 			resourceData->dataRefCount = 1;
 		}
 		resourceHandle._data = resourceData->data;
diff --git a/engines/neverhood/resourceman.h b/engines/neverhood/resourceman.h
index 45ef6a928b0..deedb2507e4 100644
--- a/engines/neverhood/resourceman.h
+++ b/engines/neverhood/resourceman.h
@@ -27,6 +27,7 @@
 #include "common/hashmap.h"
 #include "neverhood/neverhood.h"
 #include "neverhood/blbarchive.h"
+#include "neverhood/nhcarchive.h"
 
 namespace Neverhood {
 
@@ -39,11 +40,14 @@ private:
 	BlbArchive *archive;
 	BlbArchiveEntry *archiveEntry;
 
+	NhcArchive *nhcArchive;
+	NhcArchiveEntry *nhcArchiveEntry;
+
 	friend class ResourceHandle;
 	friend class ResourceMan;
 
 public:
-	ResourceFileEntry() : resourceHandle(-1), archive(nullptr), archiveEntry(nullptr) {}
+	ResourceFileEntry() : resourceHandle(-1), archive(nullptr), archiveEntry(nullptr), nhcArchive(nullptr), nhcArchiveEntry(nullptr) {}
 };
 
 struct Resource {
@@ -62,12 +66,38 @@ friend class ResourceMan;
 public:
 	ResourceHandle();
 	~ResourceHandle();
-	bool isValid() const { return _resourceFileEntry != NULL && _resourceFileEntry->archiveEntry != NULL; }
-	byte type() const { return isValid() ? _resourceFileEntry->archiveEntry->type : 0; };
+	bool isValid() const { return _resourceFileEntry != NULL
+			&& (_resourceFileEntry->archiveEntry != NULL
+			    || (_resourceFileEntry->nhcArchiveEntry != NULL && _resourceFileEntry->nhcArchiveEntry->isNormal())); }
+	byte type() const {
+		if (_resourceFileEntry == NULL)
+			return 0;
+		if (_resourceFileEntry->nhcArchiveEntry != NULL && _resourceFileEntry->nhcArchiveEntry->isNormal())
+			return _resourceFileEntry->nhcArchiveEntry->type;
+		if (_resourceFileEntry->archiveEntry != NULL)
+			return _resourceFileEntry->archiveEntry->type;
+		return 0;
+	}
 	const byte *data() const { return _data; }
-	uint32 size() const { return isValid() ? _resourceFileEntry->archiveEntry->size : 0; };
-	const byte *extData() const { return _extData; };
-	uint32 fileHash() const { return isValid() ? _resourceFileEntry->archiveEntry->fileHash : 0; };
+	uint32 size() const {
+		if (_resourceFileEntry == NULL)
+			return 0;
+		if (_resourceFileEntry->nhcArchiveEntry != NULL && _resourceFileEntry->nhcArchiveEntry->isNormal())
+			return _resourceFileEntry->nhcArchiveEntry->size;
+		if (_resourceFileEntry->archiveEntry != NULL)
+			return _resourceFileEntry->archiveEntry->size;
+		return 0;
+	}
+	const byte *extData() const { return _extData; }
+	uint32 fileHash() const {
+		if (_resourceFileEntry == NULL)
+			return 0;
+		if (_resourceFileEntry->nhcArchiveEntry != NULL && _resourceFileEntry->nhcArchiveEntry->isNormal())
+			return _resourceFileEntry->nhcArchiveEntry->fileHash;
+		if (_resourceFileEntry->archiveEntry != NULL)
+			return _resourceFileEntry->archiveEntry->fileHash;
+		return 0;
+	}
 protected:
 	ResourceFileEntry *_resourceFileEntry;
 	const byte *_extData;
@@ -79,9 +109,11 @@ public:
 	ResourceMan();
 	~ResourceMan();
 	void addArchive(const Common::String &filename, bool isOptional = false);
+	bool addNhcArchive(const Common::String &filename);
 	ResourceFileEntry *findEntrySimple(uint32 fileHash);
 	ResourceFileEntry *findEntry(uint32 fileHash, ResourceFileEntry **firstEntry = NULL);
 	Common::SeekableReadStream *createStream(uint32 fileHash);
+	Common::SeekableReadStream *createNhcStream(uint32 fileHash, uint32 type);
 	const ResourceFileEntry& getEntry(uint index) { return _entries[index]; }
 	uint getEntryCount() { return _entries.size(); }
 	void queryResource(uint32 fileHash, ResourceHandle &resourceHandle);
@@ -91,6 +123,7 @@ public:
 protected:
 	typedef Common::HashMap<uint32, ResourceFileEntry> EntriesMap;
 	Common::Array<BlbArchive*> _archives;
+	Common::Array<NhcArchive*> _nhcArchives;
 	EntriesMap _entries;
 	Common::HashMap<uint32, ResourceData*> _data;
 	Common::Array<Resource*> _resources;


Commit: 69aa08d5314c6f1e3efba9b17637bf69831aa738
    https://github.com/scummvm/scummvm/commit/69aa08d5314c6f1e3efba9b17637bf69831aa738
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Move game options from advanced options to custom widget

This will allow to add NHC drop-down.

Changed paths:
  A engines/neverhood/dialogs.cpp
  A engines/neverhood/dialogs.h
    engines/neverhood/metaengine.cpp
    engines/neverhood/module.mk


diff --git a/engines/neverhood/dialogs.cpp b/engines/neverhood/dialogs.cpp
new file mode 100644
index 00000000000..25da10a0172
--- /dev/null
+++ b/engines/neverhood/dialogs.cpp
@@ -0,0 +1,105 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "neverhood/neverhood.h"
+#include "neverhood/dialogs.h"
+
+#include "gui/gui-manager.h"
+#include "gui/message.h"
+#include "gui/saveload.h"
+#include "gui/ThemeEval.h"
+#include "gui/widget.h"
+#include "gui/widgets/popup.h"
+
+#include "common/gui_options.h"
+#include "common/system.h"
+#include "common/translation.h"
+
+namespace Neverhood {
+
+NeverhoodOptionsWidget::NeverhoodOptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain) :
+		OptionsContainerWidget(boss, name, "NeverhoodGameOptionsDialog", false, domain),
+		_originalSaveLoadCheckbox(nullptr),
+		_skipHallOfRecordsCheckbox(nullptr),
+		_scaleMakingOfVideosCheckbox(nullptr) {
+
+	_originalSaveLoadCheckbox = new GUI::CheckboxWidget(
+		widgetsBoss(),
+		"NeverhoodGameOptionsDialog.OriginalSaveLoad",
+		_("Use original save/load screens"),
+		_("Use the original save/load screens instead of the ScummVM ones"));
+
+	_skipHallOfRecordsCheckbox = new GUI::CheckboxWidget(
+		widgetsBoss(),
+		"NeverhoodGameOptionsDialog.SkipHallOfRecords", _("Skip the Hall of Records storyboard scenes"),
+		_("Allows the player to skip past the Hall of Records storyboard scenes"));
+
+	_scaleMakingOfVideosCheckbox = new GUI::CheckboxWidget(
+		widgetsBoss(),
+		"NeverhoodGameOptionsDialog.ScaleMakingOfVideos", _("Scale the making of videos to full screen"),
+		_("Scale the making of videos, so that they use the whole screen"));
+}
+
+NeverhoodOptionsWidget::~NeverhoodOptionsWidget() {
+}
+
+void NeverhoodOptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const {
+	layouts.addDialog(layoutName, overlayedLayout)
+	            .addLayout(GUI::ThemeLayout::kLayoutVertical)
+	                .addPadding(16, 16, 16, 16)
+	                .addWidget("OriginalSaveLoad", "Checkbox")
+	                .addWidget("SkipHallOfRecords", "Checkbox")
+		        .addWidget("ScaleMakingOfVideos", "Checkbox")
+	            .closeLayout()
+	        .closeDialog();
+}
+
+void NeverhoodOptionsWidget::load() {
+	if (_originalSaveLoadCheckbox) {
+		_originalSaveLoadCheckbox->setState(ConfMan.getBool("originalsaveload", _domain));
+	}
+
+	if (_skipHallOfRecordsCheckbox) {
+		_skipHallOfRecordsCheckbox->setState(ConfMan.getBool("skiphallofrecordsscenes", _domain));
+	}
+
+	if (_scaleMakingOfVideosCheckbox) {
+		_scaleMakingOfVideosCheckbox->setState(ConfMan.getBool("scalemakingofvideos", _domain));
+	}
+}
+
+bool NeverhoodOptionsWidget::save() {
+	if (_originalSaveLoadCheckbox) {
+		ConfMan.setBool("originalsaveload", _originalSaveLoadCheckbox->getState(), _domain);
+	}
+
+	if (_skipHallOfRecordsCheckbox) {
+		ConfMan.setBool("skiphallofrecordsscenes", _skipHallOfRecordsCheckbox->getState(), _domain);
+	}
+
+	if (_scaleMakingOfVideosCheckbox) {
+		ConfMan.setBool("scalemakingofvideos", _scaleMakingOfVideosCheckbox->getState(), _domain);
+	}
+
+	return true;
+}
+
+} // End of namespace Neverhood
diff --git a/engines/neverhood/dialogs.h b/engines/neverhood/dialogs.h
new file mode 100644
index 00000000000..ebcae285a30
--- /dev/null
+++ b/engines/neverhood/dialogs.h
@@ -0,0 +1,64 @@
+/* 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 NEVERHOOD_DIALOGS_H
+#define NEVERHOOD_DIALOGS_H
+
+#include "neverhood/neverhood.h"
+
+#include "common/events.h"
+#include "common/str.h"
+#include "common/ustr.h"
+
+#include "engines/dialogs.h"
+
+#include "gui/dialog.h"
+#include "gui/widget.h"
+
+namespace GUI {
+class PopUpWidget;
+}
+
+namespace Neverhood {
+
+class NeverhoodEngine;
+
+class NeverhoodOptionsWidget : public GUI::OptionsContainerWidget {
+public:
+	NeverhoodOptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain);
+	~NeverhoodOptionsWidget() override;
+
+	// OptionsContainerWidget API
+	void load() override;
+	bool save() override;
+
+private:
+	// OptionsContainerWidget API
+	void defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const override;
+
+	GUI::CheckboxWidget *_originalSaveLoadCheckbox;
+	GUI::CheckboxWidget *_skipHallOfRecordsCheckbox;
+	GUI::CheckboxWidget *_scaleMakingOfVideosCheckbox;
+};
+
+} // End of namespace Neverhood
+
+#endif
diff --git a/engines/neverhood/metaengine.cpp b/engines/neverhood/metaengine.cpp
index 060f805e7a1..1a9217fda19 100644
--- a/engines/neverhood/metaengine.cpp
+++ b/engines/neverhood/metaengine.cpp
@@ -25,48 +25,12 @@
 #include "common/file.h"
 #include "common/translation.h"
 
+#include "neverhood/dialogs.h"
 #include "neverhood/neverhood.h"
 #include "neverhood/detection.h"
 
 namespace Neverhood {
 
-static const ADExtraGuiOptionsMap optionsList[] = {
-	{
-		GAMEOPTION_ORIGINAL_SAVELOAD,
-		{
-			_s("Use original save/load screens"),
-			_s("Use the original save/load screens instead of the ScummVM ones"),
-			"originalsaveload",
-			false,
-			0,
-			0
-		}
-	},
-	{
-		GAMEOPTION_SKIP_HALL_OF_RECORDS,
-		{
-			_s("Skip the Hall of Records storyboard scenes"),
-			_s("Allows the player to skip past the Hall of Records storyboard scenes"),
-			"skiphallofrecordsscenes",
-			false,
-			0,
-			0
-		}
-	},
-	{
-		GAMEOPTION_SCALE_MAKING_OF_VIDEOS,
-		{
-			_s("Scale the making of videos to full screen"),
-			_s("Scale the making of videos, so that they use the whole screen"),
-			"scalemakingofvideos",
-			false,
-			0,
-			0
-		}
-	},
-	AD_EXTRA_GUI_OPTIONS_TERMINATOR
-};
-
 const char *NeverhoodEngine::getGameId() const {
 	return _gameDescription->gameId;
 }
@@ -99,8 +63,8 @@ public:
 		return "neverhood";
 	}
 
-	const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
-		return Neverhood::optionsList;
+	GUI::OptionsContainerWidget *buildEngineOptionsWidget(GUI::GuiObject *boss, const Common::String &name, const Common::String &target) const {
+		return new Neverhood::NeverhoodOptionsWidget(boss, name, target);
 	}
 
 	bool hasFeature(MetaEngineFeature f) const override;
diff --git a/engines/neverhood/module.mk b/engines/neverhood/module.mk
index e7b7e97167e..c396d8f6aa9 100644
--- a/engines/neverhood/module.mk
+++ b/engines/neverhood/module.mk
@@ -4,6 +4,7 @@ MODULE_OBJS = \
 	background.o \
 	blbarchive.o \
 	console.o \
+	dialogs.o \
 	diskplayerscene.o \
 	entity.o \
 	gamemodule.o \


Commit: c0455314c97e70aa0c6f7ef5a7d06844a9198766
    https://github.com/scummvm/scummvm/commit/c0455314c97e70aa0c6f7ef5a7d06844a9198766
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Add NHC selection item

Changed paths:
    engines/neverhood/dialogs.cpp
    engines/neverhood/dialogs.h


diff --git a/engines/neverhood/dialogs.cpp b/engines/neverhood/dialogs.cpp
index 25da10a0172..b458fbe2250 100644
--- a/engines/neverhood/dialogs.cpp
+++ b/engines/neverhood/dialogs.cpp
@@ -39,7 +39,8 @@ NeverhoodOptionsWidget::NeverhoodOptionsWidget(GuiObject *boss, const Common::St
 		OptionsContainerWidget(boss, name, "NeverhoodGameOptionsDialog", false, domain),
 		_originalSaveLoadCheckbox(nullptr),
 		_skipHallOfRecordsCheckbox(nullptr),
-		_scaleMakingOfVideosCheckbox(nullptr) {
+		_scaleMakingOfVideosCheckbox(nullptr),
+		_nhcPopUp(nullptr) {
 
 	_originalSaveLoadCheckbox = new GUI::CheckboxWidget(
 		widgetsBoss(),
@@ -56,6 +57,31 @@ NeverhoodOptionsWidget::NeverhoodOptionsWidget(GuiObject *boss, const Common::St
 		widgetsBoss(),
 		"NeverhoodGameOptionsDialog.ScaleMakingOfVideos", _("Scale the making of videos to full screen"),
 		_("Scale the making of videos, so that they use the whole screen"));
+
+	Common::String path = ConfMan.get("path", _domain);
+	Common::FSDirectory dir(path);
+	Common::FSDirectory *langdir = dir.getSubDirectory("language");
+	_nhcFiles.push_back("");
+	if (langdir) {
+		Common::ArchiveMemberList nhcFileList;
+		langdir->listMatchingMembers(nhcFileList, "*.nhc");
+
+		for (Common::ArchiveMemberList::iterator iter = nhcFileList.begin(); iter != nhcFileList.end(); ++iter) {
+			Common::String nhcFileName = (*iter)->getName();
+			nhcFileName.erase(nhcFileName.size() - 4); // remove .nhc extension
+			_nhcFiles.push_back(nhcFileName);
+		}
+	}
+
+	if (_nhcFiles.size() > 1) {
+		GUI::StaticTextWidget *nhcCaption = new GUI::StaticTextWidget(widgetsBoss(), "NeverhoodGameOptionsDialog.NhcDesc", _("NHC replacement:"));
+		nhcCaption->setAlign(Graphics::kTextAlignRight);
+
+		_nhcPopUp = new GUI::PopUpWidget(widgetsBoss(), "NeverhoodGameOptionsDialog.Nhc");
+
+		for (uint i = 0; i < _nhcFiles.size(); i++)
+			_nhcPopUp->appendEntry(_nhcFiles[i].empty() ? _("<original>") : Common::U32String(_nhcFiles[i]), i);
+	}
 }
 
 NeverhoodOptionsWidget::~NeverhoodOptionsWidget() {
@@ -68,6 +94,11 @@ void NeverhoodOptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common:
 	                .addWidget("OriginalSaveLoad", "Checkbox")
 	                .addWidget("SkipHallOfRecords", "Checkbox")
 		        .addWidget("ScaleMakingOfVideos", "Checkbox")
+	                .addLayout(GUI::ThemeLayout::kLayoutHorizontal)
+	                    .addPadding(0, 0, 0, 0)
+	                    .addWidget("NhcDesc", "OptionsLabel")
+	                    .addWidget("Nhc", "PopUp")
+	                .closeLayout()
 	            .closeLayout()
 	        .closeDialog();
 }
@@ -84,6 +115,13 @@ void NeverhoodOptionsWidget::load() {
 	if (_scaleMakingOfVideosCheckbox) {
 		_scaleMakingOfVideosCheckbox->setState(ConfMan.getBool("scalemakingofvideos", _domain));
 	}
+
+	if (_nhcPopUp) {
+		Common::String nhcFile(ConfMan.get("nhc_file", _domain));
+		for (uint i = 0; i < _nhcFiles.size(); i++)
+			if (_nhcFiles[i].equalsIgnoreCase(nhcFile))
+				_nhcPopUp->setSelectedTag(i);
+	}
 }
 
 bool NeverhoodOptionsWidget::save() {
@@ -99,6 +137,12 @@ bool NeverhoodOptionsWidget::save() {
 		ConfMan.setBool("scalemakingofvideos", _scaleMakingOfVideosCheckbox->getState(), _domain);
 	}
 
+	if (_nhcPopUp) {
+		uint32 selectedNhcFile = _nhcPopUp->getSelectedTag();
+		if (selectedNhcFile < _nhcFiles.size())
+			ConfMan.set("nhc_file", _nhcFiles[selectedNhcFile], _domain);
+	}
+
 	return true;
 }
 
diff --git a/engines/neverhood/dialogs.h b/engines/neverhood/dialogs.h
index ebcae285a30..0c683e6d5ba 100644
--- a/engines/neverhood/dialogs.h
+++ b/engines/neverhood/dialogs.h
@@ -57,6 +57,9 @@ private:
 	GUI::CheckboxWidget *_originalSaveLoadCheckbox;
 	GUI::CheckboxWidget *_skipHallOfRecordsCheckbox;
 	GUI::CheckboxWidget *_scaleMakingOfVideosCheckbox;
+
+	GUI::PopUpWidget *_nhcPopUp;
+	Common::StringArray _nhcFiles;
 };
 
 } // End of namespace Neverhood


Commit: 103bb43ab252b7d98c31be36c3a9f49fd3542d8a
    https://github.com/scummvm/scummvm/commit/103bb43ab252b7d98c31be36c3a9f49fd3542d8a
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Load nhc file

Changed paths:
    engines/neverhood/neverhood.cpp


diff --git a/engines/neverhood/neverhood.cpp b/engines/neverhood/neverhood.cpp
index eb89bf80e96..0cffb91bffc 100644
--- a/engines/neverhood/neverhood.cpp
+++ b/engines/neverhood/neverhood.cpp
@@ -100,6 +100,10 @@ Common::Error NeverhoodEngine::run() {
 		_res->addArchive("t.blb");
 	}
 
+	Common::String nhcFile = ConfMan.get("nhc_file");
+	if (!nhcFile.empty())
+		_res->addNhcArchive("language/" + nhcFile + ".nhc");
+
 	CursorMan.showMouse(false);
 
 	_soundMan = new SoundMan(this);


Commit: 031c9296811d2484f3ce296dfdb1595c6d9c9253
    https://github.com/scummvm/scummvm/commit/031c9296811d2484f3ce296dfdb1595c6d9c9253
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Add loading of subtitle font

Changed paths:
    engines/neverhood/neverhood.cpp
    engines/neverhood/neverhood.h


diff --git a/engines/neverhood/neverhood.cpp b/engines/neverhood/neverhood.cpp
index 0cffb91bffc..e7f53b91552 100644
--- a/engines/neverhood/neverhood.cpp
+++ b/engines/neverhood/neverhood.cpp
@@ -47,7 +47,7 @@
 namespace Neverhood {
 
 NeverhoodEngine::NeverhoodEngine(OSystem *syst, const ADGameDescription *gameDesc) :
-		Engine(syst), _gameDescription(gameDesc) {
+		Engine(syst), _gameDescription(gameDesc), _haveSubtitles(false) {
 	// Setup mixer
 	if (!_mixer->isReady()) {
 		warning("Sound initialization failed.");
@@ -101,8 +101,23 @@ Common::Error NeverhoodEngine::run() {
 	}
 
 	Common::String nhcFile = ConfMan.get("nhc_file");
-	if (!nhcFile.empty())
-		_res->addNhcArchive("language/" + nhcFile + ".nhc");
+	if (!nhcFile.empty() && _res->addNhcArchive("language/" + nhcFile + ".nhc")) {
+		Common::SeekableReadStream *s = _res->createNhcStream(0x544E4F46, kResNhcTypeSubFont);
+		if (s && s->size() >= 4096) {
+			for (uint i = 0; i < 256; i++) {
+				s->read(&_subFont[i].bitmap, sizeof(_subFont[i].bitmap));
+				for (uint j = 0; j < 16; j++)
+					_subFont[i].outline[j] = (_subFont[i].bitmap[j] << 1) | (_subFont[i].bitmap[j] >> 1);
+				for (uint j = 1; j < 16; j++)
+					_subFont[i].outline[j] |= _subFont[i].bitmap[j-1];
+				for (uint j = 0; j < 15; j++)
+					_subFont[i].outline[j] |= _subFont[i].bitmap[j+1];
+				for (uint j = 0; j < 16; j++)
+					_subFont[i].outline[j] &= ~_subFont[i].bitmap[j];
+			}
+			_haveSubtitles = true;
+		}
+	}
 
 	CursorMan.showMouse(false);
 
diff --git a/engines/neverhood/neverhood.h b/engines/neverhood/neverhood.h
index c370153ca4c..5b13eefe831 100644
--- a/engines/neverhood/neverhood.h
+++ b/engines/neverhood/neverhood.h
@@ -52,6 +52,11 @@ struct GameState {
 	int which;
 };
 
+struct SubtitleGlyph {
+	byte bitmap[16];
+	byte outline[16];
+};
+
 class NeverhoodEngine : public ::Engine {
 protected:
 
@@ -134,10 +139,16 @@ public:
 	void toggleMusic(bool state) { _enableMusic = state; }
 	bool musicIsEnabled() { return _enableMusic; }
 
+	const SubtitleGlyph *getSubfont() const {
+		return _haveSubtitles ? _subFont : nullptr;
+	}
+
 private:
 	bool _updateSound;
 	bool _enableMusic;
 
+	SubtitleGlyph _subFont[256];
+	bool _haveSubtitles;
 };
 
 } // End of namespace Neverhood


Commit: 97b500643e6760e9e2c30c20dbc3c3ab1424f112
    https://github.com/scummvm/scummvm/commit/97b500643e6760e9e2c30c20dbc3c3ab1424f112
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Add subtitle player

Changed paths:
  A engines/neverhood/subtitles.cpp
  A engines/neverhood/subtitles.h
    engines/neverhood/module.mk


diff --git a/engines/neverhood/module.mk b/engines/neverhood/module.mk
index c396d8f6aa9..6f27485eaa1 100644
--- a/engines/neverhood/module.mk
+++ b/engines/neverhood/module.mk
@@ -68,7 +68,8 @@ MODULE_OBJS = \
 	smackerplayer.o \
 	sound.o \
 	sprite.o \
-	staticdata.o
+	staticdata.o \
+	subtitles.o
 
 # This module can be built as a plugin
 ifeq ($(ENABLE_NEVERHOOD), DYNAMIC_PLUGIN)
diff --git a/engines/neverhood/subtitles.cpp b/engines/neverhood/subtitles.cpp
new file mode 100644
index 00000000000..b94b41e1863
--- /dev/null
+++ b/engines/neverhood/subtitles.cpp
@@ -0,0 +1,120 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "neverhood/resource.h"
+#include "neverhood/resourceman.h"
+#include "neverhood/subtitles.h"
+
+namespace Neverhood {
+
+namespace {
+void drawSubtitles(Graphics::Surface *surf, const Common::String &str, const SubtitleGlyph *subfont, int x0) {
+	if (!surf || surf->h < SubtitlePlayer::kSubtitleCharHeight || !subfont)
+		return;
+
+	byte *dest0 = (byte*)surf->getBasePtr(0, 0);
+
+	int lastx = MIN<int>(str.size() * SubtitlePlayer::kSubtitleCharWidth + x0, surf->w);
+	for (int16 yc = 0; yc < SubtitlePlayer::kSubtitleCharHeight; yc++) {
+		byte *dest = dest0 + yc * surf->pitch;
+		memset(dest, SubtitlePlayer::kSubtitleAlpha, x0);
+		memset(dest + lastx, SubtitlePlayer::kSubtitleAlpha, surf->w - lastx);
+	}
+
+	for (int i = 0; i < (int)str.size() && i * SubtitlePlayer::kSubtitleCharWidth < surf->w; i++) {
+		byte c = str[i];
+		byte *dest = dest0 + i * SubtitlePlayer::kSubtitleCharWidth + x0;
+		for (int16 yc = 0; yc < SubtitlePlayer::kSubtitleCharHeight; yc++) {
+			byte *row = dest;
+			for (int16 xc = 0; xc < SubtitlePlayer::kSubtitleCharWidth; xc++, row++) {
+				if ((subfont[c].bitmap[yc] << xc) & 0x80)
+					*row = 0xff;
+				else if ((subfont[c].outline[yc] << xc) & 0x80)
+					*row = 0x00;
+				else
+					*row = SubtitlePlayer::kSubtitleAlpha;
+			}
+			dest += surf->pitch;
+		}
+	}
+}
+}
+		
+SubtitlePlayer::SubtitlePlayer(NeverhoodEngine *vm, uint32 fileHash, int width) :
+	_vm(vm), _haveBottomSubs(false), _haveTopSubs(false), _currentFrame(-1), _isValid(false) {
+	if (!vm->getSubfont())
+		return;
+	_isValid = true;
+  	_bottomSubs.create(width, kSubtitleCharHeight, Graphics::PixelFormat::createFormatCLUT8());
+  	_topSubs.create(width, kSubtitleCharHeight, Graphics::PixelFormat::createFormatCLUT8());
+
+	Common::SeekableReadStream *s = vm->_res->createNhcStream(fileHash, kResNhcTypeSubtitles);
+	while (s && !s->eos()) {
+		Subtitle sub;
+		sub.fromFrame = s->readUint32LE();
+		sub.toFrame = s->readUint32LE();
+		sub.text = s->readString('\0', 40);
+		if (!sub.text.empty() && sub.text[0] == '^') {
+			sub.isTop = true;
+			sub.text = sub.text.substr(1);
+		} else {
+			sub.isTop = false;
+		}
+		_subtitles.push_back(sub);
+	}
+	delete s;
+}
+
+void SubtitlePlayer::renderFrame(uint frameNumber, int centerX) {
+	// Reuse old rendering if no frame has passed
+	if (_currentFrame == (int64)frameNumber)
+		return;
+
+	const SubtitleGlyph *subFont = _vm->getSubfont();
+	if (!subFont)
+		return;
+
+	int screenWidth = _bottomSubs.w;
+
+	_haveBottomSubs = false;
+	_haveTopSubs = false;
+
+	// TODO: Optimize this
+	for (uint i = 0; i < _subtitles.size(); i++) {
+		if (frameNumber < _subtitles[i].fromFrame || frameNumber > _subtitles[i].toFrame)
+			continue;
+		Common::String curStr = _subtitles[i].text;
+		if ((int)curStr.size() > screenWidth / SubtitlePlayer::kSubtitleCharWidth)
+			curStr = curStr.substr(0, screenWidth / SubtitlePlayer::kSubtitleCharWidth - 3) + "...";
+		int width = curStr.size() * SubtitlePlayer::kSubtitleCharWidth;
+		int startX = MAX(MIN(centerX - width / 2, screenWidth - width), 0);
+
+		if (_subtitles[i].isTop) {
+			drawSubtitles(&_topSubs, curStr, subFont, startX);
+			_haveTopSubs = true;
+		} else {
+			drawSubtitles(&_bottomSubs, curStr, subFont, startX);
+			_haveBottomSubs = true;
+		}
+	}
+}
+
+}
diff --git a/engines/neverhood/subtitles.h b/engines/neverhood/subtitles.h
new file mode 100644
index 00000000000..3cd4d656aee
--- /dev/null
+++ b/engines/neverhood/subtitles.h
@@ -0,0 +1,64 @@
+/* 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 NEVERHOOD_SUBTITLES_H
+#define NEVERHOOD_SUBTITLES_H
+
+#include "common/str.h"
+#include "neverhood/neverhood.h"
+#include "graphics/surface.h"
+
+namespace Neverhood {
+
+struct Subtitle {
+	uint32 fromFrame;
+	uint32 toFrame;
+	Common::String text;
+	bool isTop;
+};
+
+class SubtitlePlayer {
+public:
+	SubtitlePlayer(NeverhoodEngine *vm, uint32 fileHash, int width);
+
+	void renderFrame(uint frameNumber, int centerX);
+	const Graphics::Surface *getBottomSubs() const { return _haveBottomSubs ? &_bottomSubs : nullptr; }
+	const Graphics::Surface *getTopSubs() const { return _haveTopSubs ? &_topSubs : nullptr; }
+	bool isValid() const { return _isValid && !_subtitles.empty(); }
+
+	static const byte kSubtitleAlpha = 0x77;
+	static const int kSubtitleCharHeight = 16;
+	static const int kSubtitleCharWidth = 8;
+
+private:
+	NeverhoodEngine *_vm;
+	bool _isValid;
+	Common::Array<Subtitle> _subtitles;
+	Graphics::Surface _bottomSubs;
+	Graphics::Surface _topSubs;
+	bool _haveBottomSubs;
+	bool _haveTopSubs;
+	int64 _currentFrame;
+};
+
+} // End of namespace Neverhood
+
+#endif /* NEVERHOOD_SUBTITLES_H */


Commit: feb1d77ec723faa80110fe4b0ce25ca0d45eae0a
    https://github.com/scummvm/scummvm/commit/feb1d77ec723faa80110fe4b0ce25ca0d45eae0a
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Show subtitles in smackerplayer

Changed paths:
    engines/neverhood/smackerplayer.cpp
    engines/neverhood/smackerplayer.h


diff --git a/engines/neverhood/smackerplayer.cpp b/engines/neverhood/smackerplayer.cpp
index 4b158aa4809..2f460854f02 100644
--- a/engines/neverhood/smackerplayer.cpp
+++ b/engines/neverhood/smackerplayer.cpp
@@ -35,8 +35,30 @@ SmackerSurface::SmackerSurface(NeverhoodEngine *vm)
 }
 
 void SmackerSurface::draw() {
-	if (_smackerFrame && _visible && _drawRect.width > 0 && _drawRect.height > 0)
+	if (_smackerFrame && _visible && _drawRect.width > 0 && _drawRect.height > 0) {
 		_vm->_screen->drawSurface2(_smackerFrame, _drawRect, _clipRect, false, ++_version);
+		if (_subtitles && _subtitles->isValid()) {
+			_subtitles->renderFrame(frameNumber, 160);
+			const Graphics::Surface *bottom = _subtitles->getBottomSubs();
+			if (bottom) {
+				NDrawRect subDrawRect;
+				subDrawRect.x = _drawRect.x;
+				subDrawRect.y = _drawRect.y + _drawRect.height - 17;
+				subDrawRect.width = _drawRect.width;
+				subDrawRect.height = 16;
+				_vm->_screen->drawSurface2(bottom, subDrawRect, _clipRect, true, ++_version, nullptr, _subtitles->kSubtitleAlpha);
+			}
+			const Graphics::Surface *top = _subtitles->getTopSubs();
+			if (top) {
+				NDrawRect subDrawRect;
+				subDrawRect.x = _drawRect.x;
+				subDrawRect.y = _drawRect.y + 1;
+				subDrawRect.width = _drawRect.width;
+				subDrawRect.height = 16;
+				_vm->_screen->drawSurface2(top, subDrawRect, _clipRect, true, ++_version, nullptr, _subtitles->kSubtitleAlpha);
+			}
+		}
+	}
 }
 
 void SmackerSurface::setSmackerFrame(const Graphics::Surface *smackerFrame) {
@@ -70,8 +92,30 @@ SmackerDoubleSurface::SmackerDoubleSurface(NeverhoodEngine *vm)
 }
 
 void SmackerDoubleSurface::draw() {
-	if (_smackerFrame && _visible && _drawRect.width > 0 && _drawRect.height > 0)
+	if (_smackerFrame && _visible && _drawRect.width > 0 && _drawRect.height > 0) {
 		_vm->_screen->drawDoubleSurface2(_smackerFrame, _drawRect);
+		if (_subtitles && _subtitles->isValid()) {
+			_subtitles->renderFrame(frameNumber, 160);
+			const Graphics::Surface *bottom = _subtitles->getBottomSubs();
+			if (bottom) {
+				NDrawRect subDrawRect;
+				subDrawRect.x = _drawRect.x;
+				subDrawRect.y = _drawRect.y + _drawRect.height * 2 - 34;
+				subDrawRect.width = _drawRect.width;
+				subDrawRect.height = 16;
+				_vm->_screen->drawDoubleSurface2Alpha(bottom, subDrawRect, _subtitles->kSubtitleAlpha);
+			}
+			const Graphics::Surface *top = _subtitles->getTopSubs();
+			if (top) {
+				NDrawRect subDrawRect;
+				subDrawRect.x = _drawRect.x;
+				subDrawRect.y = _drawRect.y + 2;
+				subDrawRect.width = _drawRect.width;
+				subDrawRect.height = 16;
+				_vm->_screen->drawDoubleSurface2Alpha(top, subDrawRect, _subtitles->kSubtitleAlpha);
+			}
+		}
+	}
 }
 
 // NeverhoodSmackerDecoder
@@ -134,6 +178,8 @@ void SmackerPlayer::open(uint32 fileHash, bool keepLastFrame) {
 
 	_stream = _vm->_res->createStream(fileHash);
 
+	_smackerSurface->_subtitles.reset(new SubtitlePlayer(_vm, fileHash, 320));
+
 	_smackerDecoder = new NeverhoodSmackerDecoder();
 	_smackerDecoder->loadStream(_stream);
 
@@ -222,6 +268,8 @@ void SmackerPlayer::updateFrame() {
 
 	const Graphics::Surface *smackerFrame = _smackerDecoder->decodeNextFrame();
 
+	_smackerSurface->frameNumber = _smackerDecoder->getCurFrame();
+
 	if (_smackerFirst) {
 		_smackerSurface->setSmackerFrame(smackerFrame);
 		if (_drawX < 0 || _drawY < 0) {
diff --git a/engines/neverhood/smackerplayer.h b/engines/neverhood/smackerplayer.h
index c2010a2974b..24dd4e7c8bd 100644
--- a/engines/neverhood/smackerplayer.h
+++ b/engines/neverhood/smackerplayer.h
@@ -25,11 +25,13 @@
 #include "video/smk_decoder.h"
 #include "neverhood/neverhood.h"
 #include "neverhood/entity.h"
+#include "neverhood/subtitles.h"
 
 namespace Neverhood {
 
 class Scene;
 class Palette;
+class SmackerPlayer;
 
 class SmackerSurface : public BaseSurface {
 public:
@@ -37,8 +39,12 @@ public:
 	void draw() override;
 	void setSmackerFrame(const Graphics::Surface *smackerFrame);
 	void unsetSmackerFrame();
+	void renderSubtitles();
 protected:
 	const Graphics::Surface *_smackerFrame;
+	Common::ScopedPtr<SubtitlePlayer> _subtitles;
+	uint32 frameNumber;
+	friend class SmackerPlayer;
 };
 
 class SmackerDoubleSurface : public SmackerSurface {


Commit: 03a59a295380d3ad6888c2e806090e35241101e5
    https://github.com/scummvm/scummvm/commit/03a59a295380d3ad6888c2e806090e35241101e5
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Support blitting with alphaColor != 0

Changed paths:
    engines/neverhood/screen.cpp
    engines/neverhood/screen.h


diff --git a/engines/neverhood/screen.cpp b/engines/neverhood/screen.cpp
index 1e60499dcd5..1defb192211 100644
--- a/engines/neverhood/screen.cpp
+++ b/engines/neverhood/screen.cpp
@@ -186,7 +186,7 @@ void Screen::clearRenderQueue() {
 }
 
 void Screen::drawSurface2(const Graphics::Surface *surface, NDrawRect &drawRect, NRect &clipRect, bool transparent, byte version,
-	const Graphics::Surface *shadowSurface) {
+			  const Graphics::Surface *shadowSurface, byte alphaColor) {
 
 	int16 destX, destY;
 	NRect ddRect;
@@ -217,7 +217,7 @@ void Screen::drawSurface2(const Graphics::Surface *surface, NDrawRect &drawRect,
 		ddRect.y1 = 0;
 	}
 
-	queueBlit(surface, destX, destY, ddRect, transparent, version, shadowSurface);
+	queueBlit(surface, destX, destY, ddRect, transparent, version, shadowSurface, alphaColor);
 
 }
 
@@ -350,7 +350,7 @@ void Screen::drawSurfaceClipRects(const Graphics::Surface *surface, NDrawRect &d
 }
 
 void Screen::queueBlit(const Graphics::Surface *surface, int16 destX, int16 destY, NRect &ddRect, bool transparent, byte version,
-	const Graphics::Surface *shadowSurface) {
+		       const Graphics::Surface *shadowSurface, byte alphaColor) {
 
 	const int width = ddRect.x2 - ddRect.x1;
 	const int height = ddRect.y2 - ddRect.y1;
@@ -369,6 +369,7 @@ void Screen::queueBlit(const Graphics::Surface *surface, int16 destX, int16 dest
 	renderItem._height = height;
 	renderItem._transparent = transparent;
 	renderItem._version = version;
+	renderItem._alphaColor = alphaColor;
 	_renderQueue->push_back(renderItem);
 
 }
@@ -406,7 +407,7 @@ void Screen::blitRenderItem(const RenderItem &renderItem, const Common::Rect &cl
 			source += surface->pitch;
 			dest += _backScreen->pitch;
 		}
-	} else {
+	} else if (renderItem._alphaColor == 0) {
 		while (height--) {
 			for (int xc = 0; xc < width; xc++)
 				if (source[xc] != 0)
@@ -414,6 +415,14 @@ void Screen::blitRenderItem(const RenderItem &renderItem, const Common::Rect &cl
 			source += surface->pitch;
 			dest += _backScreen->pitch;
 		}
+	} else {
+		while (height--) {
+			for (int xc = 0; xc < width; xc++)
+				if (source[xc] != renderItem._alphaColor)
+					dest[xc] = source[xc];
+			source += surface->pitch;
+			dest += _backScreen->pitch;
+		}
 	}
 
 }
diff --git a/engines/neverhood/screen.h b/engines/neverhood/screen.h
index b52dd9a9df9..31c899f70bf 100644
--- a/engines/neverhood/screen.h
+++ b/engines/neverhood/screen.h
@@ -42,6 +42,7 @@ struct RenderItem {
 	bool _transparent;
 	byte _version;
 	bool _refresh;
+	byte _alphaColor;
 	bool operator==(const RenderItem &second) const {
 		return
 			_surface == second._surface &&
@@ -53,7 +54,8 @@ struct RenderItem {
 			_width == second._width &&
 			_height == second._height &&
 			_transparent == second._transparent &&
-			_version == second._version;
+			_version == second._version &&
+			_alphaColor == second._alphaColor;
 	}
 };
 
@@ -79,14 +81,14 @@ public:
 	void clear();
 	void clearRenderQueue();
 	void drawSurface2(const Graphics::Surface *surface, NDrawRect &drawRect, NRect &clipRect, bool transparent, byte version,
-		const Graphics::Surface *shadowSurface = NULL);
+			  const Graphics::Surface *shadowSurface = NULL, byte alphaColor = 0);
 	void drawSurface3(const Graphics::Surface *surface, int16 x, int16 y, NDrawRect &drawRect, NRect &clipRect, bool transparent, byte version);
 	void drawDoubleSurface2(const Graphics::Surface *surface, NDrawRect &drawRect);
 	void drawUnk(const Graphics::Surface *surface, NDrawRect &drawRect, NDrawRect &sysRect, NRect &clipRect, bool transparent, byte version);
 	void drawSurfaceClipRects(const Graphics::Surface *surface, NDrawRect &drawRect, NRect *clipRects, uint clipRectsCount, bool transparent, byte version);
 	void setSmackerDecoder(Video::SmackerDecoder *smackerDecoder) { _smackerDecoder = smackerDecoder; }
 	void queueBlit(const Graphics::Surface *surface, int16 destX, int16 destY, NRect &ddRect, bool transparent, byte version,
-		const Graphics::Surface *shadowSurface = NULL);
+		       const Graphics::Surface *shadowSurface = NULL, byte alphaColor = 0);
 	void blitRenderItem(const RenderItem &renderItem, const Common::Rect &clipRect);
 protected:
 	NeverhoodEngine *_vm;


Commit: f4da2d054a8fea2d32fd4fa82499985fa30bc74a
    https://github.com/scummvm/scummvm/commit/f4da2d054a8fea2d32fd4fa82499985fa30bc74a
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Add drawDoubleSurface2Alpha

Changed paths:
    engines/neverhood/screen.cpp
    engines/neverhood/screen.h


diff --git a/engines/neverhood/screen.cpp b/engines/neverhood/screen.cpp
index 1defb192211..fc0d7879aec 100644
--- a/engines/neverhood/screen.cpp
+++ b/engines/neverhood/screen.cpp
@@ -276,6 +276,31 @@ void Screen::drawDoubleSurface2(const Graphics::Surface *surface, NDrawRect &dra
 
 }
 
+void Screen::drawDoubleSurface2Alpha(const Graphics::Surface *surface, NDrawRect &drawRect, byte alphaColor) {
+
+	const byte *source = (const byte*)surface->getPixels();
+	byte *dest = (byte*)_backScreen->getBasePtr(drawRect.x, drawRect.y);
+
+	for (int16 yc = 0; yc < surface->h; yc++) {
+		byte *row = dest;
+		for (int16 xc = 0; xc < surface->w; xc++) {
+			if (*source != alphaColor) {
+				row[0] = *source;
+				row[1] = *source;
+				row[_backScreen->pitch] = *source;
+				row[_backScreen->pitch + 1] = *source;
+			}
+			source++;
+			row += 2;
+		}
+		dest += _backScreen->pitch;
+		dest += _backScreen->pitch;
+	}
+
+	_fullRefresh = true; // See Screen::update
+
+}
+
 void Screen::drawUnk(const Graphics::Surface *surface, NDrawRect &drawRect, NDrawRect &sysRect, NRect &clipRect, bool transparent, byte version) {
 
 	int16 x, y;
diff --git a/engines/neverhood/screen.h b/engines/neverhood/screen.h
index 31c899f70bf..7bd2f3b4fc5 100644
--- a/engines/neverhood/screen.h
+++ b/engines/neverhood/screen.h
@@ -84,6 +84,7 @@ public:
 			  const Graphics::Surface *shadowSurface = NULL, byte alphaColor = 0);
 	void drawSurface3(const Graphics::Surface *surface, int16 x, int16 y, NDrawRect &drawRect, NRect &clipRect, bool transparent, byte version);
 	void drawDoubleSurface2(const Graphics::Surface *surface, NDrawRect &drawRect);
+	void drawDoubleSurface2Alpha(const Graphics::Surface *surface, NDrawRect &drawRect, byte alphaColor);
 	void drawUnk(const Graphics::Surface *surface, NDrawRect &drawRect, NDrawRect &sysRect, NRect &clipRect, bool transparent, byte version);
 	void drawSurfaceClipRects(const Graphics::Surface *surface, NDrawRect &drawRect, NRect *clipRects, uint clipRectsCount, bool transparent, byte version);
 	void setSmackerDecoder(Video::SmackerDecoder *smackerDecoder) { _smackerDecoder = smackerDecoder; }


Commit: ffdf90df45f0213ee45e63af7fed4a65fdfbd5e0
    https://github.com/scummvm/scummvm/commit/ffdf90df45f0213ee45e63af7fed4a65fdfbd5e0
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Allocate more space for font surface

NHC-based Russian translation has extra unused space in the sprite.
This causes the code to skip drawing sprite on the surface altogether.
The simplest solution is to simply allocate a bit more with only
downside is a small increase in memory footprint

Changed paths:
    engines/neverhood/graphics.cpp


diff --git a/engines/neverhood/graphics.cpp b/engines/neverhood/graphics.cpp
index 135007c144d..5fda85f155c 100644
--- a/engines/neverhood/graphics.cpp
+++ b/engines/neverhood/graphics.cpp
@@ -151,7 +151,7 @@ void ShadowSurface::draw() {
 // FontSurface
 
 FontSurface::FontSurface(NeverhoodEngine *vm, NPointArray *tracking, uint charsPerRow, uint16 numRows, byte firstChar, uint16 charWidth, uint16 charHeight)
-	: BaseSurface(vm, 0, charWidth * charsPerRow, charHeight * numRows, "font"), _charsPerRow(charsPerRow), _numRows(numRows),
+	: BaseSurface(vm, 0, charWidth * charsPerRow, charHeight * numRows + 4, "font"), _charsPerRow(charsPerRow), _numRows(numRows),
 	_firstChar(firstChar), _charWidth(charWidth), _charHeight(charHeight), _tracking(nullptr) {
 
 	_tracking = new NPointArray();
@@ -160,7 +160,7 @@ FontSurface::FontSurface(NeverhoodEngine *vm, NPointArray *tracking, uint charsP
 }
 
 FontSurface::FontSurface(NeverhoodEngine *vm, uint32 fileHash, uint charsPerRow, uint16 numRows, byte firstChar, uint16 charWidth, uint16 charHeight)
-	: BaseSurface(vm, 0, charWidth * charsPerRow, charHeight * numRows, "font"), _charsPerRow(charsPerRow), _numRows(numRows),
+	: BaseSurface(vm, 0, charWidth * charsPerRow, charHeight * numRows + 4, "font"), _charsPerRow(charsPerRow), _numRows(numRows),
 	_firstChar(firstChar), _charWidth(charWidth), _charHeight(charHeight), _tracking(nullptr) {
 
 	SpriteResource fontSpriteResource(_vm);


Commit: 5d6ba180088b7b710ac7f7d9ecd81f0c8dbe1077
    https://github.com/scummvm/scummvm/commit/5d6ba180088b7b710ac7f7d9ecd81f0c8dbe1077
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Support subtitles for sprites

Changed paths:
    engines/neverhood/scene.cpp
    engines/neverhood/sprite.cpp
    engines/neverhood/sprite.h


diff --git a/engines/neverhood/scene.cpp b/engines/neverhood/scene.cpp
index 7cf526a8d64..d79f1187572 100644
--- a/engines/neverhood/scene.cpp
+++ b/engines/neverhood/scene.cpp
@@ -158,6 +158,7 @@ void Scene::printSurfaces(Console *con) {
 Sprite *Scene::addSprite(Sprite *sprite) {
 	addEntity(sprite);
 	addSurface(sprite->getSurface());
+	addSurface(sprite->getSubtitleSurface());
 	return sprite;
 }
 
diff --git a/engines/neverhood/sprite.cpp b/engines/neverhood/sprite.cpp
index 4685038026f..808c3a953f3 100644
--- a/engines/neverhood/sprite.cpp
+++ b/engines/neverhood/sprite.cpp
@@ -187,13 +187,13 @@ void StaticSprite::updatePosition() {
 // AnimatedSprite
 
 AnimatedSprite::AnimatedSprite(NeverhoodEngine *vm, int objectPriority)
-	: Sprite(vm, objectPriority), _animResource(vm) {
+	: Sprite(vm, objectPriority), _animResource(vm), _subtitleSurface(vm, this) {
 
 	init();
 }
 
 AnimatedSprite::AnimatedSprite(NeverhoodEngine *vm, uint32 fileHash, int surfacePriority, int16 x, int16 y)
-	: Sprite(vm, 1100), _animResource(vm) {
+	: Sprite(vm, 1100), _animResource(vm), _subtitleSurface(vm, this) {
 
 	init();
 	SetUpdateHandler(&AnimatedSprite::update);
@@ -313,6 +313,7 @@ void AnimatedSprite::updateAnim() {
 			if (_animStatus == 1) {
 				if (_animResource.load(_newAnimFileHash)) {
 					_currAnimFileHash = _newAnimFileHash;
+					_subtitles.reset(new SubtitlePlayer(_vm, _newAnimFileHash, kSubtitleWidth));
 				} else {
 					_animResource.load(calcHash("sqDefault"));
 					_currAnimFileHash = 0;
@@ -326,6 +327,7 @@ void AnimatedSprite::updateAnim() {
 			} else {
 				if (_animResource.load(_newAnimFileHash)) {
 					_currAnimFileHash = _newAnimFileHash;
+					_subtitles.reset(new SubtitlePlayer(_vm, _newAnimFileHash, kSubtitleWidth));
 				} else {
 					_animResource.load(calcHash("sqDefault"));
 					_currAnimFileHash = 0;
@@ -369,6 +371,12 @@ void AnimatedSprite::updatePosition() {
 		_surface->getDrawRect().y = filterY(_y + _drawOffset.y);
 	}
 
+	int subCenterX = _surface->getDrawRect().x + _surface->getDrawRect().width / 2;
+	_subtitleSurface.getDrawRect().x = MAX(subCenterX - kSubtitleWidth / 2, 0);
+	_subtitleSurface.getDrawRect().width = kSubtitleWidth;
+	_subtitleSurface.getDrawRect().y = MIN(_surface->getDrawRect().y + _surface->getDrawRect().height + 1, 480 - (SubtitlePlayer::kSubtitleCharHeight - 1));
+	_subtitleSurface.getDrawRect().height = SubtitlePlayer::kSubtitleCharHeight;
+
 	if (_needRefresh) {
 		_surface->drawAnimResource(_animResource, _currFrameIndex, _doDeltaX, _doDeltaY, _drawOffset.width, _drawOffset.height);
 		_needRefresh = false;
@@ -398,6 +406,25 @@ void AnimatedSprite::updateFrameIndex() {
 	}
 }
 
+AnimatedSprite::AnimatedSpriteSubtitles::AnimatedSpriteSubtitles(NeverhoodEngine *vm, AnimatedSprite *backref) :
+	_backref(backref), BaseSurface(vm, 0xffff, kSubtitleWidth, SubtitlePlayer::kSubtitleCharHeight, "animated sprite subtitles") {
+}
+
+void AnimatedSprite::AnimatedSpriteSubtitles::draw() {
+	if (_backref->_subtitles && _backref->_subtitles->isValid() &&
+	    _backref->_currFrameIndex != 0 &&
+	    _backref->_currFrameIndex < _backref->_lastFrameIndex) {
+		int subCenterX = _backref->_surface->getDrawRect().x + _backref->_surface->getDrawRect().width / 2;
+		_backref->_subtitles->renderFrame(_backref->_currFrameIndex, subCenterX - getDrawRect().x);
+		const Graphics::Surface *bottom = _backref->_subtitles->getBottomSubs();
+		if (bottom) {
+			_vm->_screen->drawSurface2(bottom, _drawRect, _clipRect, true, ++_version, nullptr, _backref->_subtitles->kSubtitleAlpha);
+		}
+		if (_backref->_subtitles->getTopSubs())
+			warning("Top subs are unsupported");
+	}
+}
+
 void AnimatedSprite::updateFrameInfo() {
 	debug(8, "AnimatedSprite::updateFrameInfo()");
 	const AnimFrameInfo &frameInfo = _animResource.getFrameInfo(_currFrameIndex);
diff --git a/engines/neverhood/sprite.h b/engines/neverhood/sprite.h
index ce8e45c6661..28f0f83da92 100644
--- a/engines/neverhood/sprite.h
+++ b/engines/neverhood/sprite.h
@@ -26,6 +26,7 @@
 #include "neverhood/entity.h"
 #include "neverhood/graphics.h"
 #include "neverhood/resource.h"
+#include "neverhood/subtitles.h"
 
 namespace Neverhood {
 
@@ -56,6 +57,7 @@ public:
 	~Sprite() override;
 	void init() {}
 	BaseSurface *getSurface() { return _surface; }
+	virtual BaseSurface *getSubtitleSurface() { return nullptr; }
 	void updateBounds();
 	void setDoDeltaX(int type);
 	void setDoDeltaY(int type);
@@ -149,8 +151,20 @@ public:
 	int16 getFrameIndex(uint32 frameHash) { return _animResource.getFrameIndex(frameHash); }
 	void setNewHashListIndex(int value) { _newStickFrameIndex = value; }
 	void startAnimation(uint32 fileHash, int16 plFirstFrameIndex, int16 plLastFrameIndex);
+	BaseSurface *getSubtitleSurface() override { return &_subtitleSurface; }
 protected:
+	class AnimatedSpriteSubtitles : public BaseSurface {
+	public:
+		void draw() override;
+		AnimatedSpriteSubtitles(NeverhoodEngine *vm, AnimatedSprite *backRef);
+	private:
+		AnimatedSprite *_backref;
+	};
+	
+	static const int kSubtitleWidth = 320;
+	AnimatedSpriteSubtitles _subtitleSurface;
 	typedef void (AnimatedSprite::*AnimationCb)();
+	Common::ScopedPtr<SubtitlePlayer> _subtitles;
 	AnimResource _animResource;
 	uint32 _currAnimFileHash, _newAnimFileHash, _nextAnimFileHash;
 	int16 _currFrameIndex, _lastFrameIndex;


Commit: 64fb44790e5e194dc4497c9400dcd644060101ba
    https://github.com/scummvm/scummvm/commit/64fb44790e5e194dc4497c9400dcd644060101ba
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Enable subtitle on/off if .nhc is available

Changed paths:
    engines/neverhood/detection.cpp


diff --git a/engines/neverhood/detection.cpp b/engines/neverhood/detection.cpp
index 856333bcef5..34b1ea9ebf6 100644
--- a/engines/neverhood/detection.cpp
+++ b/engines/neverhood/detection.cpp
@@ -134,7 +134,36 @@ static const ADGameDescription gameDescriptions[] = {
 class NeverhoodMetaEngineDetection : public AdvancedMetaEngineDetection {
 public:
 	NeverhoodMetaEngineDetection() : AdvancedMetaEngineDetection(Neverhood::gameDescriptions, sizeof(ADGameDescription), neverhoodGames) {
-		_guiOptions = GUIO5(GUIO_NOSUBTITLES, GUIO_NOMIDI, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_SKIP_HALL_OF_RECORDS, GAMEOPTION_SCALE_MAKING_OF_VIDEOS);
+		_guiOptions = GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_SKIP_HALL_OF_RECORDS, GAMEOPTION_SCALE_MAKING_OF_VIDEOS);
+	}
+
+	DetectedGames detectGames(const Common::FSList &fslist, uint32 skipADFlags, bool skipIncomplete) override {
+		DetectedGames detGames(AdvancedMetaEngineDetection::detectGames(fslist, skipADFlags, skipIncomplete));
+		bool hasSubs = false;
+
+		if (detGames.empty())
+			return detGames;
+
+		for (Common::FSList::const_iterator dr = fslist.begin(); dr != fslist.end() && !hasSubs; dr++) {
+			if (dr->getName().equalsIgnoreCase("language") && dr->isDirectory()) {
+				Common::FSList files;
+				if (!dr->getChildren(files, Common::FSNode::kListAll))
+					continue;
+				for (Common::FSList::const_iterator file = files.begin(); file != files.end(); file++) {
+					Common::String fname = file->getName();
+					if (fname.matchString("*.nhc", true) && !fname.equalsIgnoreCase("Fargusfx.nhc")) {
+						hasSubs = true;
+						break;
+					}
+				}
+			}
+		}
+		if (hasSubs)
+			return detGames;
+		for (DetectedGames::iterator it = detGames.begin(); it != detGames.end(); it++) {
+			it->appendGUIOptions("sndNoSubs");
+		}
+		return detGames;
 	}
 
 	const char *getName() const override {


Commit: 7bb97c18d0db44a53443b49b24397b0d2d3705c5
    https://github.com/scummvm/scummvm/commit/7bb97c18d0db44a53443b49b24397b0d2d3705c5
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Make subtitles being able to turn off/on

Changed paths:
    engines/neverhood/neverhood.cpp


diff --git a/engines/neverhood/neverhood.cpp b/engines/neverhood/neverhood.cpp
index e7f53b91552..90a9f5b13b8 100644
--- a/engines/neverhood/neverhood.cpp
+++ b/engines/neverhood/neverhood.cpp
@@ -102,20 +102,22 @@ Common::Error NeverhoodEngine::run() {
 
 	Common::String nhcFile = ConfMan.get("nhc_file");
 	if (!nhcFile.empty() && _res->addNhcArchive("language/" + nhcFile + ".nhc")) {
-		Common::SeekableReadStream *s = _res->createNhcStream(0x544E4F46, kResNhcTypeSubFont);
-		if (s && s->size() >= 4096) {
-			for (uint i = 0; i < 256; i++) {
-				s->read(&_subFont[i].bitmap, sizeof(_subFont[i].bitmap));
-				for (uint j = 0; j < 16; j++)
-					_subFont[i].outline[j] = (_subFont[i].bitmap[j] << 1) | (_subFont[i].bitmap[j] >> 1);
-				for (uint j = 1; j < 16; j++)
-					_subFont[i].outline[j] |= _subFont[i].bitmap[j-1];
-				for (uint j = 0; j < 15; j++)
-					_subFont[i].outline[j] |= _subFont[i].bitmap[j+1];
-				for (uint j = 0; j < 16; j++)
-					_subFont[i].outline[j] &= ~_subFont[i].bitmap[j];
+		if (ConfMan.getBool("subtitles")) {
+			Common::SeekableReadStream *s = _res->createNhcStream(0x544E4F46, kResNhcTypeSubFont);
+			if (s && s->size() >= 4096) {
+				for (uint i = 0; i < 256; i++) {
+					s->read(&_subFont[i].bitmap, sizeof(_subFont[i].bitmap));
+					for (uint j = 0; j < 16; j++)
+						_subFont[i].outline[j] = (_subFont[i].bitmap[j] << 1) | (_subFont[i].bitmap[j] >> 1);
+					for (uint j = 1; j < 16; j++)
+						_subFont[i].outline[j] |= _subFont[i].bitmap[j-1];
+					for (uint j = 0; j < 15; j++)
+						_subFont[i].outline[j] |= _subFont[i].bitmap[j+1];
+					for (uint j = 0; j < 16; j++)
+						_subFont[i].outline[j] &= ~_subFont[i].bitmap[j];
+				}
+				_haveSubtitles = true;
 			}
-			_haveSubtitles = true;
 		}
 	}
 


Commit: 41aa78b8a6d87529846b06e527755fce089b0e50
    https://github.com/scummvm/scummvm/commit/41aa78b8a6d87529846b06e527755fce089b0e50
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEVERHOOD: Add ability to support for repeating useful Willie's hint

Changed paths:
    engines/neverhood/detection.cpp
    engines/neverhood/detection.h
    engines/neverhood/dialogs.cpp
    engines/neverhood/dialogs.h
    engines/neverhood/modules/module1000.cpp
    engines/neverhood/modules/module1000.h


diff --git a/engines/neverhood/detection.cpp b/engines/neverhood/detection.cpp
index 34b1ea9ebf6..a3d9ef6534f 100644
--- a/engines/neverhood/detection.cpp
+++ b/engines/neverhood/detection.cpp
@@ -134,7 +134,8 @@ static const ADGameDescription gameDescriptions[] = {
 class NeverhoodMetaEngineDetection : public AdvancedMetaEngineDetection {
 public:
 	NeverhoodMetaEngineDetection() : AdvancedMetaEngineDetection(Neverhood::gameDescriptions, sizeof(ADGameDescription), neverhoodGames) {
-		_guiOptions = GUIO4(GUIO_NOMIDI, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_SKIP_HALL_OF_RECORDS, GAMEOPTION_SCALE_MAKING_OF_VIDEOS);
+		_guiOptions = GUIO5(GUIO_NOMIDI, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_SKIP_HALL_OF_RECORDS,
+				    GAMEOPTION_SCALE_MAKING_OF_VIDEOS, GAMEOPTION_REPEAT_WILLIE_HINT);
 	}
 
 	DetectedGames detectGames(const Common::FSList &fslist, uint32 skipADFlags, bool skipIncomplete) override {
diff --git a/engines/neverhood/detection.h b/engines/neverhood/detection.h
index e15505a5aff..1a403957583 100644
--- a/engines/neverhood/detection.h
+++ b/engines/neverhood/detection.h
@@ -31,6 +31,7 @@ enum NeverhoodGameFeatures {
 #define GAMEOPTION_ORIGINAL_SAVELOAD      GUIO_GAMEOPTIONS1
 #define GAMEOPTION_SKIP_HALL_OF_RECORDS   GUIO_GAMEOPTIONS2
 #define GAMEOPTION_SCALE_MAKING_OF_VIDEOS GUIO_GAMEOPTIONS3
+#define GAMEOPTION_REPEAT_WILLIE_HINT     GUIO_GAMEOPTIONS4
 
 } // End of namespace Neverhood
 
diff --git a/engines/neverhood/dialogs.cpp b/engines/neverhood/dialogs.cpp
index b458fbe2250..9270587b223 100644
--- a/engines/neverhood/dialogs.cpp
+++ b/engines/neverhood/dialogs.cpp
@@ -58,6 +58,11 @@ NeverhoodOptionsWidget::NeverhoodOptionsWidget(GuiObject *boss, const Common::St
 		"NeverhoodGameOptionsDialog.ScaleMakingOfVideos", _("Scale the making of videos to full screen"),
 		_("Scale the making of videos, so that they use the whole screen"));
 
+	_repeatWillieHint = new GUI::CheckboxWidget(
+		widgetsBoss(),
+		"NeverhoodGameOptionsDialog.RepeatWillieHint", _("Repeat useful Willie's hint"),
+		_("Repeat actual useful hint by Willie"));
+
 	Common::String path = ConfMan.get("path", _domain);
 	Common::FSDirectory dir(path);
 	Common::FSDirectory *langdir = dir.getSubDirectory("language");
@@ -94,6 +99,7 @@ void NeverhoodOptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common:
 	                .addWidget("OriginalSaveLoad", "Checkbox")
 	                .addWidget("SkipHallOfRecords", "Checkbox")
 		        .addWidget("ScaleMakingOfVideos", "Checkbox")
+			.addWidget("RepeatWillieHint", "Checkbox")
 	                .addLayout(GUI::ThemeLayout::kLayoutHorizontal)
 	                    .addPadding(0, 0, 0, 0)
 	                    .addWidget("NhcDesc", "OptionsLabel")
@@ -116,6 +122,10 @@ void NeverhoodOptionsWidget::load() {
 		_scaleMakingOfVideosCheckbox->setState(ConfMan.getBool("scalemakingofvideos", _domain));
 	}
 
+	if (_repeatWillieHint) {
+		_repeatWillieHint->setState(ConfMan.getBool("repeatwilliehint", _domain));
+	}
+
 	if (_nhcPopUp) {
 		Common::String nhcFile(ConfMan.get("nhc_file", _domain));
 		for (uint i = 0; i < _nhcFiles.size(); i++)
@@ -137,6 +147,10 @@ bool NeverhoodOptionsWidget::save() {
 		ConfMan.setBool("scalemakingofvideos", _scaleMakingOfVideosCheckbox->getState(), _domain);
 	}
 
+	if (_repeatWillieHint) {
+		ConfMan.setBool("repeatwilliehint", _repeatWillieHint->getState(), _domain);
+	}
+
 	if (_nhcPopUp) {
 		uint32 selectedNhcFile = _nhcPopUp->getSelectedTag();
 		if (selectedNhcFile < _nhcFiles.size())
diff --git a/engines/neverhood/dialogs.h b/engines/neverhood/dialogs.h
index 0c683e6d5ba..0999b132642 100644
--- a/engines/neverhood/dialogs.h
+++ b/engines/neverhood/dialogs.h
@@ -57,6 +57,7 @@ private:
 	GUI::CheckboxWidget *_originalSaveLoadCheckbox;
 	GUI::CheckboxWidget *_skipHallOfRecordsCheckbox;
 	GUI::CheckboxWidget *_scaleMakingOfVideosCheckbox;
+	GUI::CheckboxWidget *_repeatWillieHint;
 
 	GUI::PopUpWidget *_nhcPopUp;
 	Common::StringArray _nhcFiles;
diff --git a/engines/neverhood/modules/module1000.cpp b/engines/neverhood/modules/module1000.cpp
index 03d2793207b..23892efd79f 100644
--- a/engines/neverhood/modules/module1000.cpp
+++ b/engines/neverhood/modules/module1000.cpp
@@ -22,6 +22,8 @@
 #include "neverhood/modules/module1000.h"
 #include "neverhood/modules/module1000_sprites.h"
 
+#include "common/config-manager.h"
+
 namespace Neverhood {
 
 Module1000::Module1000(NeverhoodEngine *vm, Module *parentModule, int which)
@@ -686,7 +688,7 @@ uint32 Scene1005::getTextIndex() {
 		textIndex = getKloggsTextIndex();
 	}
 	if (getGlobalVar(V_TEXT_FLAG1) && getGlobalVar(V_TEXT_INDEX) == textIndex) {
-		textIndex = getTextIndex3();
+		textIndex = getTextIndex3(textIndex);
 	} else {
 		setGlobalVar(V_TEXT_FLAG1, 1);
 		setGlobalVar(V_TEXT_INDEX, textIndex);
@@ -773,8 +775,12 @@ uint32 Scene1005::getKloggsTextIndex() {
 	return textIndex + 40;
 }
 
-uint32 Scene1005::getTextIndex3() {
+uint32 Scene1005::getTextIndex3(uint32 usefulHint) {
 	uint32 textIndex = getGlobalVar(V_TEXT_COUNTING_INDEX2);
+	if (textIndex + 1 > 10 && ConfMan.getBool("repeatwilliehint")) {
+		setGlobalVar(V_TEXT_COUNTING_INDEX2, 0);
+		return usefulHint;
+	}
 	if (textIndex + 1 > 10) {
 		textIndex = 0;
 	}
diff --git a/engines/neverhood/modules/module1000.h b/engines/neverhood/modules/module1000.h
index 35c68f8d601..229b40dc198 100644
--- a/engines/neverhood/modules/module1000.h
+++ b/engines/neverhood/modules/module1000.h
@@ -101,7 +101,7 @@ protected:
 	uint32 getTextIndex();
 	uint32 getTextIndex1();
 	uint32 getKloggsTextIndex();
-	uint32 getTextIndex3();
+	uint32 getTextIndex3(uint32 usefulHint);
 };
 
 } // End of namespace Neverhood


Commit: eec64ea340d0c051a70fb7e20bdf33f247cf28ea
    https://github.com/scummvm/scummvm/commit/eec64ea340d0c051a70fb7e20bdf33f247cf28ea
Author: Vladimir Serbinenko (phcoder at gmail.com)
Date: 2022-12-31T20:47:10+01:00

Commit Message:
NEWS: Add entry about supporting Ctpax-Che at ter & Rigel localizations.

Changed paths:
    NEWS.md


diff --git a/NEWS.md b/NEWS.md
index 0ae46a6f4db..1854ca2f28a 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -60,6 +60,7 @@ For a more comprehensive changelog of the latest experimental code, see:
 
  Neverhood:
    - Added support for Japanese version of Neverhood.
+   - Support localizations by Ctpax-Che at ter & Rigel.
 
  Plumbers:
    - Fixed crash with windows version.




More information about the Scummvm-git-logs mailing list