mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-14 05:38:56 +00:00
756 lines
20 KiB
C++
756 lines
20 KiB
C++
/* 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 "mtropolis/subtitles.h"
|
|
|
|
#include "common/archive.h"
|
|
#include "common/array.h"
|
|
#include "common/file.h"
|
|
#include "common/hashmap.h"
|
|
#include "common/stream.h"
|
|
#include "common/hash-str.h"
|
|
|
|
#include "graphics/font.h"
|
|
#include "graphics/fonts/ttf.h"
|
|
#include "graphics/fontman.h"
|
|
#include "graphics/managed_surface.h"
|
|
|
|
#include "mtropolis/render.h"
|
|
#include "mtropolis/runtime.h"
|
|
|
|
namespace MTropolis {
|
|
|
|
class SubtitleCSVLoader {
|
|
public:
|
|
explicit SubtitleCSVLoader(Common::ReadStream *stream);
|
|
|
|
bool readLine(Common::Array<Common::String> &outStrings);
|
|
|
|
private:
|
|
bool readQuotedCel(Common::String &str);
|
|
bool readUnquotedCel(Common::String &str);
|
|
|
|
Common::Array<char> _contents;
|
|
uint _readOffset;
|
|
uint _line;
|
|
};
|
|
|
|
SubtitleCSVLoader::SubtitleCSVLoader(Common::ReadStream *stream) : _readOffset(0), _line(1) {
|
|
char chunk[4096];
|
|
while (!stream->eos() && !stream->err()) {
|
|
uint32 countRead = stream->read(chunk, sizeof(chunk));
|
|
if (countRead == 0)
|
|
return;
|
|
|
|
_contents.resize(_contents.size() + countRead);
|
|
memcpy(&_contents[_contents.size() - countRead], chunk, countRead);
|
|
}
|
|
}
|
|
|
|
bool SubtitleCSVLoader::readLine(Common::Array<Common::String> &outStrings) {
|
|
uint numStrs = 0;
|
|
while (_readOffset < _contents.size()) {
|
|
if (outStrings.size() == numStrs)
|
|
outStrings.push_back(Common::String());
|
|
|
|
Common::String &celStr = outStrings[numStrs];
|
|
|
|
char nextChar = _contents[_readOffset];
|
|
bool celStatus = false;
|
|
if (nextChar == '\"')
|
|
celStatus = readQuotedCel(celStr);
|
|
else
|
|
celStatus = readUnquotedCel(celStr);
|
|
|
|
if (!celStatus)
|
|
return false;
|
|
|
|
numStrs++;
|
|
|
|
if (_readOffset == _contents.size())
|
|
break;
|
|
|
|
char dividerChar = _contents[_readOffset];
|
|
if (dividerChar == ',')
|
|
_readOffset++;
|
|
else if (dividerChar == '\r' || dividerChar == '\n') {
|
|
_readOffset++;
|
|
if (dividerChar == '\r' && _readOffset < _contents.size() && _contents[_readOffset] == '\n')
|
|
_readOffset++;
|
|
break;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
outStrings.resize(numStrs);
|
|
_line++;
|
|
return true;
|
|
}
|
|
|
|
bool SubtitleCSVLoader::readQuotedCel(Common::String &str) {
|
|
assert(_contents[_readOffset] == '\"');
|
|
_readOffset++;
|
|
|
|
str.clear();
|
|
for (;;) {
|
|
if (_readOffset == _contents.size())
|
|
return false;
|
|
|
|
char nextChar = _contents[_readOffset];
|
|
if (nextChar == '\"') {
|
|
_readOffset++;
|
|
if (_readOffset < _contents.size()) {
|
|
char subsequentChar = _contents[_readOffset];
|
|
if (subsequentChar == '\"') {
|
|
str += '\"';
|
|
_readOffset++;
|
|
} else
|
|
break;
|
|
}
|
|
} else {
|
|
str += nextChar;
|
|
_readOffset++;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SubtitleCSVLoader::readUnquotedCel(Common::String &str) {
|
|
assert(_contents[_readOffset] != '\"');
|
|
|
|
str.clear();
|
|
for (;;) {
|
|
if (_readOffset == _contents.size())
|
|
return true;
|
|
|
|
char nextChar = _contents[_readOffset];
|
|
if (nextChar == ',' || nextChar == '\n' || nextChar == '\r')
|
|
break;
|
|
|
|
str += nextChar;
|
|
_readOffset++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
SubtitleAssetMappingTable::SubtitleAssetMappingTable() {
|
|
}
|
|
|
|
Common::ErrorCode SubtitleAssetMappingTable::load(const Common::String &filePath) {
|
|
Common::File f;
|
|
if (!f.open(Common::Path(filePath)))
|
|
return Common::kPathDoesNotExist;
|
|
|
|
SubtitleCSVLoader loader(&f);
|
|
|
|
Common::Array<Common::String> strs;
|
|
if (!loader.readLine(strs))
|
|
return Common::kReadingFailed;
|
|
|
|
if (strs.size() != 3 || strs[0] != "subtitle_set_id" || strs[1] != "asset_id" || strs[2] != "asset_name")
|
|
return Common::kReadingFailed;
|
|
|
|
while (loader.readLine(strs)) {
|
|
if (strs.size() == 0)
|
|
break;
|
|
|
|
if (strs.size() != 3)
|
|
return Common::kReadingFailed;
|
|
|
|
uint assetID = 0;
|
|
if (sscanf(strs[1].c_str(), "%u", &assetID) == 1 && assetID != 0)
|
|
_assetIDToSubtitleSet[assetID] = strs[0];
|
|
|
|
if (strs[2].size() > 0)
|
|
_assetNameToSubtitleSet[strs[2]] = strs[0];
|
|
}
|
|
|
|
return Common::kNoError;
|
|
}
|
|
|
|
const Common::String *SubtitleAssetMappingTable::findSubtitleSetForAssetID(uint32 assetID) const {
|
|
Common::HashMap<uint32, Common::String>::const_iterator it = _assetIDToSubtitleSet.find(assetID);
|
|
if (it == _assetIDToSubtitleSet.end())
|
|
return nullptr;
|
|
return &it->_value;
|
|
}
|
|
|
|
const Common::String *SubtitleAssetMappingTable::findSubtitleSetForAssetName(const Common::String &assetName) const {
|
|
Common::HashMap<Common::String, Common::String>::const_iterator it = _assetNameToSubtitleSet.find(assetName);
|
|
if (it == _assetNameToSubtitleSet.end())
|
|
return nullptr;
|
|
return &it->_value;
|
|
}
|
|
|
|
|
|
SubtitleModifierMappingTable::SubtitleModifierMappingTable() {
|
|
}
|
|
|
|
Common::ErrorCode SubtitleModifierMappingTable::load(const Common::String &filePath) {
|
|
Common::File f;
|
|
if (!f.open(Common::Path(filePath)))
|
|
return Common::kPathDoesNotExist;
|
|
|
|
SubtitleCSVLoader loader(&f);
|
|
|
|
Common::Array<Common::String> strs;
|
|
if (!loader.readLine(strs))
|
|
return Common::kReadingFailed;
|
|
|
|
if (strs.size() != 2 || strs[0] != "subtitle_set_id" || strs[1] != "modifier_guid")
|
|
return Common::kReadingFailed;
|
|
|
|
while (loader.readLine(strs)) {
|
|
if (strs.size() == 0)
|
|
break;
|
|
|
|
if (strs.size() != 2)
|
|
return Common::kReadingFailed;
|
|
|
|
uint modifierGUID = 0;
|
|
for (char c : strs[1]) {
|
|
modifierGUID *= 0x10;
|
|
if (c >= '0' && c <= '9') {
|
|
modifierGUID += (c - '0');
|
|
} else if (c >= 'a' && c <= 'f') {
|
|
modifierGUID += (c - 'a' + 0xa);
|
|
} else if (c >= 'A' && c <= 'F') {
|
|
modifierGUID += (c - 'A' + 0xa);
|
|
} else {
|
|
return Common::kReadingFailed;
|
|
}
|
|
}
|
|
|
|
_guidToSubtitleSet[modifierGUID] = strs[0];
|
|
}
|
|
|
|
return Common::kNoError;
|
|
}
|
|
|
|
const Common::String *SubtitleModifierMappingTable::findSubtitleSetForModifierGUID(uint32 guid) const {
|
|
Common::HashMap<uint32, Common::String>::const_iterator it = _guidToSubtitleSet.find(guid);
|
|
if (it == _guidToSubtitleSet.end())
|
|
return nullptr;
|
|
return &it->_value;
|
|
}
|
|
|
|
SubtitleSpeakerTable::SubtitleSpeakerTable() {
|
|
}
|
|
|
|
Common::ErrorCode SubtitleSpeakerTable::load(const Common::String &filePath) {
|
|
Common::File f;
|
|
if (!f.open(Common::Path(filePath)))
|
|
return Common::kPathDoesNotExist;
|
|
|
|
SubtitleCSVLoader loader(&f);
|
|
|
|
Common::Array<Common::String> strs;
|
|
if (!loader.readLine(strs))
|
|
return Common::kReadingFailed;
|
|
|
|
if (strs.size() != 2 || strs[0] != "speaker" || strs[1] != "text")
|
|
return Common::kReadingFailed;
|
|
|
|
_speakers.resize(1);
|
|
|
|
while (loader.readLine(strs)) {
|
|
if (strs.size() == 0)
|
|
break;
|
|
|
|
if (strs.size() != 2)
|
|
return Common::kReadingFailed;
|
|
|
|
_speakerToID[strs[0]] = _speakers.size();
|
|
_speakers.push_back(strs[1]);
|
|
}
|
|
|
|
return Common::kNoError;
|
|
}
|
|
|
|
const Common::Array<Common::String> &SubtitleSpeakerTable::getSpeakers() const {
|
|
return _speakers;
|
|
}
|
|
|
|
uint SubtitleSpeakerTable::getSpeakerID(const Common::String &speakerName) const {
|
|
Common::HashMap<Common::String, uint>::const_iterator it = _speakerToID.find(speakerName);
|
|
if (it == _speakerToID.end())
|
|
return 0;
|
|
return it->_value;
|
|
}
|
|
|
|
|
|
SubtitleLineTable::SubtitleLineTable() {
|
|
}
|
|
|
|
Common::ErrorCode SubtitleLineTable::load(const Common::String &filePath, const SubtitleSpeakerTable &speakerTable) {
|
|
Common::File f;
|
|
if (!f.open(Common::Path(filePath)))
|
|
return Common::kPathDoesNotExist;
|
|
|
|
SubtitleCSVLoader loader(&f);
|
|
|
|
Common::Array<Common::String> strs;
|
|
if (!loader.readLine(strs))
|
|
return Common::kReadingFailed;
|
|
|
|
if (strs.size() != 8 || strs[0] != "subtitle_set_id" || strs[1] != "text" || strs[2] != "time" || strs[3] != "duration" || strs[4] != "slot" || strs[5] != "speaker" || strs[6] != "class" || strs[7] != "position")
|
|
return Common::kReadingFailed;
|
|
|
|
uint currentLine = 0;
|
|
while (loader.readLine(strs)) {
|
|
if (strs.size() == 0)
|
|
break;
|
|
|
|
if (strs.size() != 8)
|
|
return Common::kReadingFailed;
|
|
|
|
double timestamp = 0;
|
|
double duration = 0;
|
|
double position = 0;
|
|
uint slot = 0;
|
|
LineClass lineClass = kLineClassDefault;
|
|
if (sscanf(strs[2].c_str(), "%lf", ×tamp) != 1)
|
|
timestamp = 0;
|
|
|
|
if (sscanf(strs[3].c_str(), "%lf", &duration) != 1)
|
|
duration = 0;
|
|
|
|
if (sscanf(strs[4].c_str(), "%u", &slot) != 1)
|
|
slot = 0;
|
|
|
|
if (strs[6] == "gameplay")
|
|
lineClass = kLineClassGameplay;
|
|
|
|
if (sscanf(strs[7].c_str(), "%lf", &position) != 1)
|
|
position = 0;
|
|
|
|
LineData lineData;
|
|
lineData.durationMSec = static_cast<uint32>(duration * 1000.0);
|
|
lineData.timeOffsetMSec = static_cast<uint32>(timestamp * 1000.0);
|
|
lineData.textUTF8 = strs[1];
|
|
lineData.slot = slot;
|
|
lineData.speakerID = speakerTable.getSpeakerID(strs[5]);
|
|
lineData.lineClass = lineClass;
|
|
lineData.position = position;
|
|
|
|
LineRange &range = _lineRanges[strs[0]];
|
|
if (range.numLines == 0)
|
|
range.linesStart = currentLine;
|
|
else if (range.linesStart + range.numLines != currentLine) {
|
|
warning("Failed to load lines table, subtitle set '%s' was not contiguous", strs[0].c_str());
|
|
return Common::kReadingFailed;
|
|
}
|
|
|
|
range.numLines++;
|
|
|
|
_lines.push_back(lineData);
|
|
|
|
currentLine++;
|
|
}
|
|
|
|
return Common::kNoError;
|
|
}
|
|
|
|
SubtitleLineTable::LineRange::LineRange() : linesStart(0), numLines(0) {
|
|
}
|
|
|
|
|
|
SubtitleLineTable::LineData::LineData() : timeOffsetMSec(0), slot(0), durationMSec(0), speakerID(0), lineClass(kLineClassDefault), position(0) {
|
|
}
|
|
|
|
const Common::Array<SubtitleLineTable::LineData> &SubtitleLineTable::getAllLines() const {
|
|
return _lines;
|
|
}
|
|
|
|
const SubtitleLineTable::LineRange *SubtitleLineTable::getLinesForSubtitleSetID(const Common::String &subtitleSetID) const {
|
|
Common::HashMap<Common::String, LineRange>::const_iterator it = _lineRanges.find(subtitleSetID);
|
|
if (it == _lineRanges.end())
|
|
return nullptr;
|
|
return &it->_value;
|
|
}
|
|
|
|
SubtitleRenderer::DisplayItem::DisplayItem() : expireTime(0) {
|
|
}
|
|
|
|
SubtitleDisplayItem::SubtitleDisplayItem(const Common::String &text, const Common::String &speaker, uint slot, double position)
|
|
: _slot(slot), _position(position) {
|
|
_text = text.decode(Common::kUtf8);
|
|
_speaker = speaker.decode(Common::kUtf8);
|
|
}
|
|
|
|
const Common::U32String &SubtitleDisplayItem::getText() const {
|
|
return _text;
|
|
}
|
|
|
|
const Common::U32String &SubtitleDisplayItem::getSpeaker() const {
|
|
return _speaker;
|
|
}
|
|
|
|
uint SubtitleDisplayItem::getSlot() const {
|
|
return _slot;
|
|
}
|
|
|
|
double SubtitleDisplayItem::getPosition() const {
|
|
return _position;
|
|
}
|
|
|
|
SubtitleRenderer::SubtitleRenderer(bool enableGameplaySubtitles)
|
|
: _nonImmediateDisappearTime(3500), _isDirty(true), _lastTime(0), _fontHeight(0), _enableGameplaySubtitles(enableGameplaySubtitles) {
|
|
#ifdef USE_FREETYPE2
|
|
Common::File fontFile;
|
|
const char *fontPath = "LiberationSans-Bold.ttf";
|
|
|
|
_font.reset(Graphics::loadTTFFontFromArchive(fontPath, 14, Graphics::kTTFSizeModeCell));
|
|
|
|
if (_font) {
|
|
_fontHeight = _font->getFontHeight();
|
|
} else
|
|
warning("Couldn't open '%s', subtitles will not work", fontPath);
|
|
|
|
// TODO: Maybe support subtitles some other way if FreeType isn't enabled
|
|
#endif
|
|
}
|
|
|
|
void SubtitleRenderer::addDisplayItem(const Common::SharedPtr<SubtitleDisplayItem> &item, uint duration) {
|
|
assert(item.get() != nullptr);
|
|
|
|
_isDirty = true;
|
|
|
|
for (DisplayItem &existingItem : _displayItems) {
|
|
if (existingItem.item->getSlot() == item->getSlot()) {
|
|
existingItem.item = item;
|
|
existingItem.surface.reset();
|
|
if (duration > 0)
|
|
existingItem.expireTime = _lastTime + duration;
|
|
else
|
|
existingItem.expireTime = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
DisplayItem newItem;
|
|
newItem.expireTime = 0;
|
|
newItem.item = item;
|
|
|
|
if (duration > 0)
|
|
newItem.expireTime = _lastTime + duration;
|
|
|
|
_displayItems.push_back(newItem);
|
|
}
|
|
|
|
void SubtitleRenderer::removeDisplayItem(const SubtitleDisplayItem *item, bool immediately) {
|
|
if (item == nullptr)
|
|
return;
|
|
|
|
for (uint i = 0; i < _displayItems.size(); i++) {
|
|
DisplayItem &existingItem = _displayItems[i];
|
|
if (existingItem.item.get() == item) {
|
|
if (immediately) {
|
|
_displayItems.remove_at(i);
|
|
_isDirty = true;
|
|
} else {
|
|
if (existingItem.expireTime == 0)
|
|
existingItem.expireTime = _lastTime + _nonImmediateDisappearTime;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SubtitleRenderer::update(uint64 currentTime) {
|
|
_lastTime = currentTime;
|
|
|
|
for (uint ridx = _displayItems.size(); ridx > 0; ridx--) {
|
|
uint i = ridx - 1;
|
|
DisplayItem &item = _displayItems[i];
|
|
if (item.expireTime != 0 && item.expireTime <= currentTime) {
|
|
_displayItems.remove_at(i);
|
|
_isDirty = true;
|
|
}
|
|
}
|
|
|
|
if (_isDirty) {
|
|
render();
|
|
_isDirty = false;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SubtitleRenderer::composite(Window &window) const {
|
|
for (const DisplayItem& displayItem : _displayItems) {
|
|
if (displayItem.surface) {
|
|
Graphics::ManagedSurface *windowSurf = window.getSurface().get();
|
|
if (windowSurf) {
|
|
int x = (windowSurf->w - displayItem.surface->w) / 2;
|
|
int y = (windowSurf->h + 300) / 2 - displayItem.surface->h + static_cast<int>(_fontHeight * displayItem.item->getPosition());
|
|
windowSurf->blitFrom(*displayItem.surface, Common::Point(x, y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SubtitleRenderer::isDirty() const {
|
|
return _isDirty;
|
|
}
|
|
|
|
bool SubtitleRenderer::isGameplaySubtitlesEnabled() const {
|
|
return _enableGameplaySubtitles;
|
|
}
|
|
|
|
|
|
void SubtitleRenderer::render() {
|
|
if (!_font)
|
|
return;
|
|
|
|
for (uint ri = _displayItems.size(); ri > 0; ri--) {
|
|
uint i = ri - 1;
|
|
|
|
DisplayItem &item = _displayItems[i];
|
|
if (!item.surface) {
|
|
uint widestLine = 0;
|
|
|
|
const SubtitleDisplayItem &dispItem = *item.item;
|
|
Common::Array<Common::U32String> lines;
|
|
|
|
splitLines(dispItem.getText(), lines);
|
|
|
|
for (const Common::U32String &str : lines) {
|
|
uint width = _font->getStringWidth(str);
|
|
if (width > widestLine)
|
|
widestLine = width;
|
|
}
|
|
|
|
uint itemLines = lines.size();
|
|
|
|
const Common::U32String &speaker = item.item->getSpeaker();
|
|
|
|
if (speaker.size() > 0) {
|
|
itemLines++;
|
|
uint speakerWidth = _font->getStringWidth(speaker);
|
|
if (speakerWidth > widestLine)
|
|
widestLine = speakerWidth;
|
|
}
|
|
|
|
if (itemLines == 0 || widestLine == 0) {
|
|
// Nothing to render, remove the item
|
|
_displayItems.remove_at(i);
|
|
_isDirty = true;
|
|
continue;
|
|
}
|
|
|
|
const int borderWidth = 1;
|
|
const int verticalPadding = 5;
|
|
const int horizontalPadding = 20;
|
|
|
|
int surfaceWidth = static_cast<int>(widestLine) + borderWidth * 2 + horizontalPadding * 2;
|
|
int surfaceHeight = static_cast<int>(itemLines) * _fontHeight + borderWidth * 2 + verticalPadding * 2;
|
|
|
|
Graphics::PixelFormat fmt = Graphics::createPixelFormat<8888>();
|
|
Common::SharedPtr<Graphics::ManagedSurface> surface(new Graphics::ManagedSurface(surfaceWidth, surfaceHeight, fmt));
|
|
|
|
surface->fillRect(Common::Rect(0, 0, surfaceWidth, surfaceHeight), fmt.RGBToColor(0, 0, 0));
|
|
|
|
for (int drawPass = 0; drawPass < 2; drawPass++) {
|
|
int textStartLine = 0;
|
|
if (speaker.size() > 0) {
|
|
textStartLine++;
|
|
|
|
int speakerWidth = _font->getStringWidth(speaker);
|
|
|
|
int startX = (surfaceWidth - speakerWidth) / 2;
|
|
|
|
uint32 drawColor = 0;
|
|
if (drawPass == 0)
|
|
drawColor = fmt.RGBToColor(255, 0, 0);
|
|
else
|
|
drawColor = fmt.RGBToColor(255, 255, 127);
|
|
|
|
_font->drawString(surface.get(), speaker, startX, verticalPadding + borderWidth, speakerWidth, drawColor);
|
|
}
|
|
|
|
for (uint lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
|
|
const Common::U32String &line = lines[lineIndex];
|
|
|
|
int lineWidth = _font->getStringWidth(line);
|
|
|
|
int startX = (surfaceWidth - lineWidth) / 2;
|
|
|
|
uint32 drawColor = 0;
|
|
if (drawPass == 0)
|
|
drawColor = fmt.RGBToColor(255, 0, 0);
|
|
else
|
|
drawColor = fmt.RGBToColor(255, 255, 255);
|
|
|
|
_font->drawString(surface.get(), line, startX, (textStartLine + static_cast<int>(lineIndex)) * _fontHeight + verticalPadding + borderWidth, lineWidth, drawColor);
|
|
}
|
|
|
|
if (drawPass == 0) {
|
|
int w = surface->w;
|
|
int h = surface->h;
|
|
|
|
// Horizontal pass (max r -> g)
|
|
for (int y = borderWidth; y < h - borderWidth; y++) {
|
|
uint32 *rowPixels = static_cast<uint32 *>(surface->getBasePtr(0, y));
|
|
|
|
for (int x = borderWidth; x < w - borderWidth; x++) {
|
|
uint8 r, g, b, a;
|
|
uint8 brightest = 0;
|
|
|
|
for (int kxo = -borderWidth; kxo < borderWidth; kxo++) {
|
|
fmt.colorToARGB(rowPixels[x + kxo], a, r, g, b);
|
|
|
|
if (r > brightest)
|
|
brightest = r;
|
|
}
|
|
|
|
fmt.colorToARGB(rowPixels[x], a, r, g, b);
|
|
rowPixels[x] = fmt.ARGBToColor(a, r, brightest, b);
|
|
}
|
|
}
|
|
|
|
// Vertical pass (max g -> b)
|
|
int pitch = surface->pitch;
|
|
for (int x = borderWidth; x < w - borderWidth; x++) {
|
|
char *basePixelPtr = static_cast<char *>(surface->getBasePtr(x, 0));
|
|
|
|
for (int y = borderWidth; y < h - borderWidth; y++) {
|
|
uint8 r, g, b, a;
|
|
uint8 brightest = 0;
|
|
|
|
for (int kyo = -borderWidth; kyo < borderWidth; kyo++) {
|
|
uint32 *offsetPxPtr = reinterpret_cast<uint32 *>(basePixelPtr + (y + kyo) * pitch);
|
|
fmt.colorToARGB(*offsetPxPtr, a, r, g, b);
|
|
|
|
if (g > brightest)
|
|
brightest = g;
|
|
}
|
|
|
|
uint32 *pxPtr = reinterpret_cast<uint32 *>(basePixelPtr + y * pitch);
|
|
fmt.colorToARGB(*pxPtr, a, r, g, b);
|
|
*pxPtr = fmt.ARGBToColor(a, r, g, brightest);
|
|
}
|
|
}
|
|
|
|
// Shadow backdrop pass
|
|
for (int y = 0; y < h; y++) {
|
|
uint32 *rowPixels = static_cast<uint32 *>(surface->getBasePtr(0, y));
|
|
|
|
for (int x = 0; x < w; x++) {
|
|
uint8 a, r, g, b;
|
|
fmt.colorToARGB(rowPixels[x], a, r, g, b);
|
|
|
|
uint8 minGrayAlpha = 224;
|
|
uint8 grayAlpha = (((256 - minGrayAlpha) * b) >> 8) + minGrayAlpha;
|
|
|
|
rowPixels[x] = fmt.ARGBToColor(grayAlpha, 0, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Done drawing
|
|
item.surface = surface;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SubtitleRenderer::splitLines(const Common::U32String &str, Common::Array<Common::U32String> &outLines) {
|
|
uint32 splitScanStart = 0;
|
|
while (splitScanStart < str.size()) {
|
|
size_t splitLoc = str.find('\\', splitScanStart);
|
|
if (splitLoc == Common::U32String::npos)
|
|
break;
|
|
|
|
outLines.push_back(str.substr(splitScanStart, splitLoc - splitScanStart));
|
|
splitScanStart = static_cast<uint32>(splitLoc + 1);
|
|
}
|
|
|
|
outLines.push_back(str.substr(splitScanStart));
|
|
}
|
|
|
|
|
|
SubtitlePlayer::SubtitlePlayer(Runtime *runtime, const Common::String &subtitleSetID, const SubtitleTables &tables) : _runtime(runtime) {
|
|
const SubtitleLineTable::LineRange *lineRange = tables.lines->getLinesForSubtitleSetID(subtitleSetID);
|
|
if (lineRange) {
|
|
_lineRange = *lineRange;
|
|
} else {
|
|
warning("Subtitle set '%s' was defined, but no lines were defined", subtitleSetID.c_str());
|
|
return;
|
|
}
|
|
|
|
_speakers = tables.speakers;
|
|
_lines = tables.lines;
|
|
}
|
|
|
|
SubtitlePlayer::~SubtitlePlayer() {
|
|
stop();
|
|
}
|
|
|
|
void SubtitlePlayer::update(uint64 prevTime, uint64 newTime) {
|
|
if (!_lineRange.numLines)
|
|
return;
|
|
|
|
const Common::Array<SubtitleLineTable::LineData> &allLines = _lines->getAllLines();
|
|
|
|
for (uint i = 0; i < _lineRange.numLines; i++) {
|
|
const SubtitleLineTable::LineData &line = allLines[_lineRange.linesStart + i];
|
|
if (line.timeOffsetMSec >= prevTime && line.timeOffsetMSec < newTime)
|
|
triggerSubtitleLine(line);
|
|
}
|
|
}
|
|
|
|
void SubtitlePlayer::stop() {
|
|
SubtitleRenderer *renderer = _runtime->getSubtitleRenderer().get();
|
|
if (renderer) {
|
|
for (const Common::SharedPtr<SubtitleDisplayItem> &item : _items)
|
|
renderer->removeDisplayItem(item.get(), false);
|
|
}
|
|
_items.clear();
|
|
}
|
|
|
|
void SubtitlePlayer::triggerSubtitleLine(const SubtitleLineTable::LineData &line) {
|
|
SubtitleRenderer *renderer = _runtime->getSubtitleRenderer().get();
|
|
if (renderer) {
|
|
if (line.lineClass == SubtitleLineTable::kLineClassGameplay && !renderer->isGameplaySubtitlesEnabled())
|
|
return;
|
|
|
|
Common::SharedPtr<SubtitleDisplayItem> dispItem(new SubtitleDisplayItem(line.textUTF8, _speakers->getSpeakers()[line.speakerID], line.slot, line.position));
|
|
for (uint i = 0; i < _items.size(); i++) {
|
|
if (_items[i]->getSlot() == line.slot) {
|
|
renderer->removeDisplayItem(_items[i].get(), true);
|
|
_items.remove_at(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
renderer->addDisplayItem(dispItem, line.durationMSec);
|
|
_items.push_back(dispItem);
|
|
}
|
|
}
|
|
|
|
} // End of namespace MTropolis
|