https://github.com/scummvm/scummvm
Raw File
Tip revision: 5dbefa955166e4b22207d117104181d044f5590a authored by Lothar Serra Mari on 16 July 2022, 20:03:12 UTC
DISTS: Generated Code::Blocks and MSVC project files
Tip revision: 5dbefa9
subtitles.cpp
/* 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 "engines/myst3/subtitles.h"
#include "engines/myst3/myst3.h"
#include "engines/myst3/scene.h"
#include "engines/myst3/state.h"

#include "common/archive.h"

#include "graphics/fontman.h"
#include "graphics/font.h"
#include "graphics/fonts/ttf.h"

#include "video/bink_decoder.h"

namespace Myst3 {

class FontSubtitles : public Subtitles {
public:
	FontSubtitles(Myst3Engine *vm);
	virtual ~FontSubtitles();

protected:
	void loadResources() override;
	bool loadSubtitles(int32 id) override;
	void drawToTexture(const Phrase *phrase) override;

private:
	void loadCharset(int32 id);
	void createTexture();
	void readPhrases(const ResourceDescription *desc);
	static Common::String fakeBidiProcessing(const Common::String &phrase);

	const Graphics::Font *_font;
	Graphics::Surface *_surface;
	float _scale;
	uint8 *_charset;
};

FontSubtitles::FontSubtitles(Myst3Engine *vm) :
	Subtitles(vm),
	_font(nullptr),
	_surface(nullptr),
	_scale(1.0),
	_charset(nullptr) {
}

FontSubtitles::~FontSubtitles() {
	if (_surface) {
		_surface->free();
		delete _surface;
	}

	delete _font;
	delete[] _charset;
}

void FontSubtitles::loadResources() {
	// We draw the subtitles in the adequate resolution so that they are not
	// scaled up. This is the scale factor of the current resolution
	// compared to the original
	_scale = getPosition().width() / (float) getOriginalPosition().width();

#ifdef USE_FREETYPE2
	Common::String ttfFile;
	if (_fontFace == "Arial Narrow") {
		// Use the TTF font provided by the game if TTF support is available
		ttfFile = "arir67w.ttf";
	} else if (_fontFace == "MS Gothic") {
		// The Japanese font has to be supplied by the user
		ttfFile = "msgothic.ttf";
	} else if (_fontFace == "Arial2") {
		// The Hebrew font has to be supplied by the user
		ttfFile = "hebrew.ttf";
	} else {
		error("Unknown subtitles font face '%s'", _fontFace.c_str());
	}

	Common::SeekableReadStream *s = SearchMan.createReadStreamForMember(ttfFile);
	if (s) {
		_font = Graphics::loadTTFFont(*s, _fontSize * _scale);
		delete s;
	} else {
		warning("Unable to load the subtitles font '%s'", ttfFile.c_str());
	}
#endif
}

void FontSubtitles::loadCharset(int32 id) {
	ResourceDescription fontCharset = _vm->getFileDescription("CHAR", id, 0, Archive::kRawData);

	// Load the font charset if any
	if (fontCharset.isValid()) {
		Common::SeekableReadStream *data = fontCharset.getData();

		_charset = new uint8[data->size()];

		data->read(_charset, data->size());

		delete data;
	}
}

bool FontSubtitles::loadSubtitles(int32 id) {
	// No game-provided charset for the Japanese version
	if (_fontCharsetCode == 0) {
		loadCharset(1100);
	}

	int32 overridenId = checkOverridenId(id);

	ResourceDescription desc = loadText(overridenId, overridenId != id);

	if (!desc.isValid())
		return false;

	readPhrases(&desc);

	if (_vm->getGameLanguage() == Common::HE_ISR) {
		for (uint i = 0; i < _phrases.size(); i++) {
			_phrases[i].string = fakeBidiProcessing(_phrases[i].string);
		}
	}

	return true;
}

void FontSubtitles::readPhrases(const ResourceDescription *desc) {
	Common::SeekableReadStream *crypted = desc->getData();

	// Read the frames and associated text offsets
	while (true) {
		Phrase s;
		s.frame = crypted->readUint32LE();
		s.offset = crypted->readUint32LE();

		if (!s.frame)
			break;

		_phrases.push_back(s);
	}

	// Read and decrypt the frames subtitles
	for (uint i = 0; i < _phrases.size(); i++) {
		crypted->seek(_phrases[i].offset);

		uint8 key = 35;
		while (true) {
			uint8 c = crypted->readByte() ^ key++;

			if (c >= 32 && _charset)
				c = _charset[c - 32];

			if (!c)
				break;

			_phrases[i].string += c;
		}
	}

	delete crypted;
}

static bool isPunctuation(char c) {
	return c == '.' || c == ',' || c == '\"'  || c == '!' || c == '?';
}

Common::String FontSubtitles::fakeBidiProcessing(const Common::String &phrase) {
	// The Hebrew subtitles are stored in logical order:
	// .ABC DEF GHI
	// This line should be rendered in visual order as:
	// .IHG FED CBA

	// Notice how the dot is on the left both in logical and visual order. This is
	// because it is in left to right order while the Hebrew characters are in right to
	// left order. Text rendering code needs to apply what is called the BiDirectional
	// algorithm to know which parts of an input string are LTR or RTL and how to render
	// them. This is a quite complicated algorithm. Fortunately the subtitles in Myst III
	// only require very specific BiDi processing. The punctuation signs at the beginning of
	// each line need to be moved to the end so that they are visually to the left once
	// the string is rendered from right to left.
	// This method works around the need to implement proper BiDi processing
	// by exploiting that fact.

	uint punctuationCounter = 0;
	while (punctuationCounter < phrase.size() && isPunctuation(phrase[punctuationCounter])) {
		punctuationCounter++;
	}

	Common::String output = Common::String(phrase.c_str() + punctuationCounter);
	for (uint i = 0; i < punctuationCounter; i++) {
		output += phrase[i];
	}

	// Also reverse the string so that it is in visual order.
	// This is necessary because our text rendering code does not actually support RTL.
	for (int i = 0, j = output.size() - 1; i < j; i++, j--) {
		char c = output[i];
		output.setChar(output[j], i);
		output.setChar(c, j);
	}

	return output;
}

void FontSubtitles::createTexture() {
	// Create a surface to draw the subtitles on
	// Use RGB 565 to allow use of BDF fonts
	if (!_surface) {
		uint16 width = Renderer::kOriginalWidth * _scale;
		uint16 height = _surfaceHeight * _scale;

		// Make sure the width is even. Some graphics drivers have trouble reading from
		// surfaces with an odd width (Mesa 18 on Intel).
		width &= ~1;

		_surface = new Graphics::Surface();
		_surface->create(width, height, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0));
	}

	if (!_texture) {
		_texture = _vm->_gfx->createTexture2D(_surface);
	}
}

/** Return an encoding from a GDI Charset as provided to CreateFont */
static Common::CodePage getEncodingFromCharsetCode(uint32 gdiCharset) {
	static const struct {
		uint32 charset;
		Common::CodePage encoding;
	} codepages[] = {
			{ 128, Common::kWindows932            }, // SHIFTJIS_CHARSET
			{ 177, Common::kWindows1255           }, // HEBREW_CHARSET
			{ 204, Common::kWindows1251           }, // RUSSIAN_CHARSET
			{ 238, Common::kMacCentralEurope }  // EASTEUROPE_CHARSET
	};

	for (uint i = 0; i < ARRAYSIZE(codepages); i++) {
		if (gdiCharset == codepages[i].charset) {
			return codepages[i].encoding;
		}
	}

	error("Unknown font charset code '%d'", gdiCharset);
}

void FontSubtitles::drawToTexture(const Phrase *phrase) {
	const Graphics::Font *font;
	if (_font)
		font = _font;
	else
		font = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont);

	if (!font)
		error("No available font");

	if (!_texture || !_surface) {
		createTexture();
	}

	// Draw the new text
	memset(_surface->getPixels(), 0, _surface->pitch * _surface->h);


	if (_fontCharsetCode == 0) {
		font->drawString(_surface, phrase->string, 0, _singleLineTop * _scale, _surface->w, 0xFFFFFFFF, Graphics::kTextAlignCenter, 0, false);
	} else {
		Common::CodePage encoding = getEncodingFromCharsetCode(_fontCharsetCode);
		Common::U32String unicode = Common::U32String(phrase->string, encoding);
		font->drawString(_surface, unicode, 0, _singleLineTop * _scale, _surface->w, 0xFFFFFFFF, Graphics::kTextAlignCenter, 0, false);
	}

	// Update the texture
	_texture->update(_surface);
}

class MovieSubtitles : public Subtitles {
public:
	MovieSubtitles(Myst3Engine *vm);
	virtual ~MovieSubtitles();

protected:
	void loadResources() override;
	bool loadSubtitles(int32 id) override;
	void drawToTexture(const Phrase *phrase) override;

private:
	ResourceDescription loadMovie(int32 id, bool overriden);
	void readPhrases(const ResourceDescription *desc);

	Video::BinkDecoder _bink;
};

MovieSubtitles::MovieSubtitles(Myst3Engine *vm) :
		Subtitles(vm) {
}

MovieSubtitles::~MovieSubtitles() {
}

void MovieSubtitles::readPhrases(const ResourceDescription *desc) {
	Common::SeekableReadStream *frames = desc->getData();

	// Read the frames
	uint index = 0;
	while (true) {
		Phrase s;
		s.frame = frames->readUint32LE();
		s.offset = index;

		if (!s.frame)
			break;

		_phrases.push_back(s);
		index++;
	}

	delete frames;
}

ResourceDescription MovieSubtitles::loadMovie(int32 id, bool overriden) {
	ResourceDescription desc;
	if (overriden) {
		desc = _vm->getFileDescription("IMGR", 200000 + id, 0, Archive::kMovie);
	} else {
		desc = _vm->getFileDescription("", 200000 + id, 0, Archive::kMovie);
	}
	return desc;
}

bool MovieSubtitles::loadSubtitles(int32 id) {
	int32 overridenId = checkOverridenId(id);

	ResourceDescription phrases = loadText(overridenId, overridenId != id);
	ResourceDescription movie = loadMovie(overridenId, overridenId != id);

	if (!phrases.isValid() || !movie.isValid())
		return false;

	readPhrases(&phrases);

	// Load the movie
	Common::SeekableReadStream *movieStream = movie.getData();
	_bink.setDefaultHighColorFormat(Texture::getRGBAPixelFormat());
	_bink.loadStream(movieStream);
	_bink.start();

	return true;
}

void MovieSubtitles::loadResources() {
}

void MovieSubtitles::drawToTexture(const Phrase *phrase) {
	_bink.seekToFrame(phrase->offset);
	const Graphics::Surface *surface = _bink.decodeNextFrame();

	if (!_texture) {
		_texture = _vm->_gfx->createTexture2D(surface);
	} else {
		_texture->update(surface);
	}
}

Subtitles::Subtitles(Myst3Engine *vm) :
		Window(),
		_vm(vm),
		_texture(nullptr),
		_frame(-1) {
	_scaled = !_vm->isWideScreenModEnabled();
}

Subtitles::~Subtitles() {
	freeTexture();
}

void Subtitles::loadFontSettings(int32 id) {
	// Load font settings
	const ResourceDescription fontNums = _vm->getFileDescription("NUMB", id, 0, Archive::kNumMetadata);

	if (!fontNums.isValid())
		error("Unable to load font settings values");

	_fontSize = fontNums.getMiscData(0);
	_fontBold = fontNums.getMiscData(1);
	_surfaceHeight = fontNums.getMiscData(2);
	_singleLineTop = fontNums.getMiscData(3);
	_line1Top = fontNums.getMiscData(4);
	_line2Top = fontNums.getMiscData(5);
	_surfaceTop = fontNums.getMiscData(6);
	_fontCharsetCode = fontNums.getMiscData(7);

	if (_fontCharsetCode > 0) {
		_fontCharsetCode = 128; // The Japanese subtitles are encoded in CP 932 / Shift JIS
	}

	if (_vm->getGameLanguage() == Common::HE_ISR) {
		// The Hebrew subtitles are encoded in CP 1255, but the game data does not specify the appropriate encoding
		_fontCharsetCode = 177;
	}

	if (_fontCharsetCode < 0) {
		_fontCharsetCode = -_fontCharsetCode; // Negative values are GDI charset codes
	}

	ResourceDescription fontText = _vm->getFileDescription("TEXT", id, 0, Archive::kTextMetadata);

	if (!fontText.isValid())
		error("Unable to load font face");

	_fontFace = fontText.getTextData(0);
}

int32 Subtitles::checkOverridenId(int32 id) {
	// Subtitles may be overridden using a variable
	if (_vm->_state->getMovieOverrideSubtitles()) {
		id = _vm->_state->getMovieOverrideSubtitles();
		_vm->_state->setMovieOverrideSubtitles(0);
	}
	return id;
}

ResourceDescription Subtitles::loadText(int32 id, bool overriden) {
	ResourceDescription desc;
	if (overriden) {
		desc = _vm->getFileDescription("IMGR", 100000 + id, 0, Archive::kText);
	} else {
		desc = _vm->getFileDescription("", 100000 + id, 0, Archive::kText);
	}
	return desc;
}

void Subtitles::setFrame(int32 frame) {
	const Phrase *phrase = nullptr;

	for (uint i = 0; i < _phrases.size(); i++) {
		if (_phrases[i].frame > frame)
			break;

		phrase = &_phrases[i];
	}

	if (!phrase) {
		freeTexture();
		return;
	}

	if (phrase->frame == _frame) {
		return;
	}

	_frame = phrase->frame;

	drawToTexture(phrase);
}

void Subtitles::drawOverlay() {
	if (!_texture) return;

	Common::Rect screen = _vm->_gfx->viewport();
	Common::Rect bottomBorder = Common::Rect(Renderer::kOriginalWidth, _surfaceHeight);
	bottomBorder.translate(0, _surfaceTop);

	if (_vm->isWideScreenModEnabled()) {
		// Draw a black background to cover the main game frame
		_vm->_gfx->drawRect2D(Common::Rect(screen.width(), Renderer::kBottomBorderHeight), 0xFF, 0x00, 0x00, 0x00);

		// Center the subtitles in the screen
		bottomBorder.translate((screen.width() - Renderer::kOriginalWidth) / 2, 0);
	}

	Common::Rect textureRect = Common::Rect(_texture->width, _texture->height);

	_vm->_gfx->drawTexturedRect2D(bottomBorder, textureRect, _texture);
}

Subtitles *Subtitles::create(Myst3Engine *vm, uint32 id) {
	Subtitles *s;

	if (vm->getPlatform() == Common::kPlatformXbox) {
		s = new MovieSubtitles(vm);
	} else {
		s = new FontSubtitles(vm);
	}

	s->loadFontSettings(1100);

	if (!s->loadSubtitles(id)) {
		delete s;
		return nullptr;
	}

	s->loadResources();

	return s;
}

void Subtitles::freeTexture() {
	if (_texture) {
		delete _texture;
		_texture = nullptr;
	}
}

Common::Rect Subtitles::getPosition() const {
	Common::Rect screen = _vm->_gfx->viewport();

	Common::Rect frame;

	if (_vm->isWideScreenModEnabled()) {
		frame = Common::Rect(screen.width(), Renderer::kBottomBorderHeight);

		Common::Rect scenePosition = _vm->_scene->getPosition();
		int16 top = CLIP<int16>(screen.height() - frame.height(), 0, scenePosition.bottom);

		frame.translate(0, top);
	} else {
		frame = Common::Rect(screen.width(), screen.height() * Renderer::kBottomBorderHeight / Renderer::kOriginalHeight);
		frame.translate(screen.left, screen.top + screen.height() * (Renderer::kTopBorderHeight + Renderer::kFrameHeight) / Renderer::kOriginalHeight);
	}

	return frame;
}

Common::Rect Subtitles::getOriginalPosition() const {
	Common::Rect originalPosition = Common::Rect(Renderer::kOriginalWidth, Renderer::kBottomBorderHeight);
	originalPosition.translate(0, Renderer::kTopBorderHeight + Renderer::kFrameHeight);
	return originalPosition;
}

} // End of namespace Myst3
back to top