/* 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 . * */ #include "alcachofa/graphics.h" #include "alcachofa/alcachofa.h" #include "alcachofa/shape.h" #include "alcachofa/global-ui.h" #include "common/system.h" #include "common/file.h" #include "common/substream.h" #include "common/bufferedstream.h" #include "image/tga.h" using namespace Common; using namespace Math; using namespace Image; using namespace Graphics; namespace Alcachofa { ITexture::ITexture(Point size) : _size(size) { if ((!isPowerOfTwo(size.x) || !isPowerOfTwo(size.y)) && g_engine->renderer().requiresPoTTextures()) warning("Created unsupported NPOT texture (%dx%d)", size.x, size.y); } void IDebugRenderer::debugShape(const Shape &shape, Color color) { constexpr uint kMaxPoints = 16; Vector2d points2d[kMaxPoints]; for (auto polygon : shape) { // I don't think this will happen but let's be sure assert(polygon._points.size() <= kMaxPoints); for (uint i = 0; i < polygon._points.size(); i++) { const auto p3d = polygon._points[i]; const auto p2d = g_engine->camera().transform3Dto2D(Vector3d(p3d.x, p3d.y, kBaseScale)); points2d[i] = Vector2d(p2d.x(), p2d.y()); } debugPolygon({ points2d, polygon._points.size() }, color); } } AnimationBase::AnimationBase(String fileName, AnimationFolder folder) : _fileName(reencode(fileName)) , _folder(folder) {} AnimationBase::~AnimationBase() { freeImages(); } void AnimationBase::load() { if (_isLoaded) return; String fullPath; switch (_folder) { case AnimationFolder::Animations: fullPath = "Animaciones/"; break; case AnimationFolder::Masks: fullPath = "Mascaras/"; break; case AnimationFolder::Backgrounds: fullPath = "Fondos/"; break; default: assert(false && "Invalid AnimationFolder"); break; } if (_fileName.size() < 4 || scumm_strnicmp(_fileName.end() - 4, ".AN0", 4) != 0) _fileName += ".AN0"; fullPath += _fileName; File file; if (!file.open(fullPath.c_str())) { // original fallback fullPath = "Mascaras/" + _fileName; if (!file.open(fullPath.c_str())) { loadMissingAnimation(); return; } } // Reading the images is a major bottleneck in loading, buffering helps a lot with that ScopedPtr stream(wrapBufferedSeekableReadStream(&file, file.size(), DisposeAfterUse::NO)); uint spriteCount = stream->readUint32LE(); assert(spriteCount < kMaxSpriteIDs); _spriteBases.reserve(spriteCount); uint imageCount = stream->readUint32LE(); _images.reserve(imageCount); _imageOffsets.reserve(imageCount); for (uint i = 0; i < imageCount; i++) { _images.push_back(readImage(*stream)); } // an inconsistency, maybe a historical reason: // the sprite bases are also stored as fixed 256 array, but as sprite *indices* // have to be contiguous we do not need to do that ourselves. // but let's check in Debug to be sure for (uint i = 0; i < spriteCount; i++) { _spriteBases.push_back(stream->readUint32LE()); assert(_spriteBases.back() < imageCount); } #ifdef ALCACHOFA_DEBUG for (uint i = spriteCount; i < kMaxSpriteIDs; i++) assert(stream->readSint32LE() == 0); #else stream->skip(sizeof(int32) * (kMaxSpriteIDs - spriteCount)); #endif for (uint i = 0; i < imageCount; i++) _imageOffsets.push_back(readPoint(*stream)); for (uint i = 0; i < kMaxSpriteIDs; i++) _spriteIndexMapping[i] = stream->readSint32LE(); uint frameCount = stream->readUint32LE(); _frames.reserve(frameCount); _spriteOffsets.reserve(frameCount * spriteCount); _totalDuration = 0; for (uint i = 0; i < frameCount; i++) { for (uint j = 0; j < spriteCount; j++) _spriteOffsets.push_back(stream->readUint32LE()); AnimationFrame frame; frame._center = readPoint(*stream); frame._offset = readPoint(*stream); frame._duration = stream->readUint32LE(); _frames.push_back(frame); _totalDuration += frame._duration; } _isLoaded = true; } void AnimationBase::freeImages() { if (!_isLoaded) return; for (auto *image : _images) { if (image != nullptr) delete image; } _images.clear(); _spriteOffsets.clear(); _spriteBases.clear(); _frames.clear(); _imageOffsets.clear(); _isLoaded = false; } ManagedSurface *AnimationBase::readImage(SeekableReadStream &stream) const { SeekableSubReadStream subStream(&stream, stream.pos(), stream.size()); TGADecoder decoder; if (!decoder.loadStream(subStream)) error("Failed to load TGA from animation %s", _fileName.c_str()); // The length of the image is unknown but TGADecoder does not read // the end marker, so let's search for it. static const char *kExpectedMarker = "TRUEVISION-XFILE."; static const uint kMarkerLength = 18; char buffer[kMarkerLength] = { 0 }; char *potentialStart = buffer + kMarkerLength; do { uint nextRead = potentialStart - buffer; if (potentialStart < buffer + kMarkerLength) memmove(buffer, potentialStart, kMarkerLength - nextRead); if (stream.read(buffer + kMarkerLength - nextRead, nextRead) != nextRead) error("Unexpected end-of-file in animation %s", _fileName.c_str()); potentialStart = find(buffer + 1, buffer + kMarkerLength, kExpectedMarker[0]); } while (strncmp(buffer, kExpectedMarker, kMarkerLength) != 0); // instead of not storing unused frame images the animation contains // transparent 2x1 images. Let's just ignore them. auto source = decoder.getSurface(); if (source->w == 2 && source->h == 1) return nullptr; const auto &palette = decoder.getPalette(); auto target = new ManagedSurface(); target->setPalette(palette.data(), 0, palette.size()); target->convertFrom(*source, g_engine->renderer().getPixelFormat()); return target; } void AnimationBase::loadMissingAnimation() { // only allow missing animations we know are faulty in the original game g_engine->game().missingAnimation(_fileName); // otherwise setup a functioning but empty animation _isLoaded = true; _totalDuration = 1; _spriteIndexMapping[0] = 0; _spriteOffsets.push_back(1); _spriteBases.push_back(0); _images.push_back(nullptr); _imageOffsets.push_back(Point()); _frames.push_back({ Point(), Point(), 1 }); } // unfortunately ScummVMs BLEND_NORMAL does not blend alpha // but this also bad, let's find/discuss a better solution later void AnimationBase::fullBlend(const ManagedSurface &source, ManagedSurface &destination, int offsetX, int offsetY) { // TODO: Support other pixel formats assert(source.format == Graphics::PixelFormat::createFormatRGBA32() || source.format == Graphics::PixelFormat::createFormatBGRA32()); assert(destination.format == source.format); assert(offsetX >= 0 && offsetX + source.w <= destination.w); assert(offsetY >= 0 && offsetY + source.h <= destination.h); const byte *sourceLine = (const byte *)source.getPixels(); byte *destinationLine = (byte *)destination.getPixels() + offsetY * destination.pitch + offsetX * 4; for (int y = 0; y < source.h; y++) { const byte *sourcePixel = sourceLine; byte *destPixel = destinationLine; for (int x = 0; x < source.w; x++) { byte alpha = sourcePixel[3]; for (int i = 0; i < 3; i++) destPixel[i] = ((byte)(alpha * sourcePixel[i] / 255)) + ((byte)((255 - alpha) * destPixel[i] / 255)); destPixel[3] = alpha + ((byte)((255 - alpha) * destPixel[3] / 255)); sourcePixel += 4; destPixel += 4; } sourceLine += source.pitch; destinationLine += destination.pitch; } } Point AnimationBase::imageSize(int32 imageI) const { auto image = _images[imageI]; return image == nullptr ? Point() : Point(image->w, image->h); } Animation::Animation(String fileName, AnimationFolder folder) : AnimationBase(fileName, folder) {} void Animation::load() { if (_isLoaded) return; AnimationBase::load(); Rect maxBounds = maxFrameBounds(); int16 texWidth = maxBounds.width(), texHeight = maxBounds.height(); if (g_engine->renderer().requiresPoTTextures()) { texWidth = nextHigher2(maxBounds.width()); texHeight = nextHigher2(maxBounds.height()); } _renderedSurface.create(texWidth, texHeight, g_engine->renderer().getPixelFormat()); _renderedTexture = g_engine->renderer().createTexture(texWidth, texHeight, true); // We always create mipmaps, even for the backgrounds that usually do not scale much, // the exception to this is the thumbnails for the savestates. // If we need to reduce graphics memory usage in the future, we can change it right here } void Animation::freeImages() { if (!_isLoaded) return; AnimationBase::freeImages(); _renderedSurface.free(); _renderedTexture.reset(nullptr); _renderedFrameI = -1; _premultiplyAlpha = 100; } int32 Animation::imageIndex(int32 frameI, int32 spriteId) const { assert(frameI >= 0 && (uint)frameI < frameCount()); assert(spriteId >= 0 && (uint)spriteId < spriteCount()); int32 spriteIndex = _spriteIndexMapping[spriteId]; int32 offset = _spriteOffsets[frameI * spriteCount() + spriteIndex]; return offset <= 0 ? -1 : offset + _spriteBases[spriteIndex] - 1; } Rect Animation::spriteBounds(int32 frameI, int32 spriteId) const { int32 imageI = imageIndex(frameI, spriteId); auto image = imageI < 0 ? nullptr : _images[imageI]; return image == nullptr ? Rect(imageI < 0 ? Point() : _imageOffsets[imageI], 2, 1) : Rect(_imageOffsets[imageI], image->w, image->h); } Rect Animation::frameBounds(int32 frameI) const { if (spriteCount() == 0) return Rect(); Rect bounds = spriteBounds(frameI, 0); for (uint spriteI = 1; spriteI < spriteCount(); spriteI++) bounds.extend(spriteBounds(frameI, spriteI)); return bounds; } Rect Animation::maxFrameBounds() const { if (frameCount() == 0) return Rect(); Rect bounds = frameBounds(0); for (uint frameI = 1; frameI < frameCount(); frameI++) bounds.extend(frameBounds(frameI)); return bounds; } Point Animation::totalFrameOffset(int32 frameI) const { const auto &frame = _frames[frameI]; const auto bounds = frameBounds(frameI); return Point( bounds.left - frame._center.x + frame._offset.x, bounds.top - frame._center.y + frame._offset.y); } int32 Animation::frameAtTime(uint32 time) const { for (int32 i = 0; (uint)i < _frames.size(); i++) { if (time <= _frames[i]._duration) return i; time -= _frames[i]._duration; } return -1; } void Animation::overrideTexture(const ManagedSurface &surface) { int16 texWidth = surface.w, texHeight = surface.h; if (g_engine->renderer().requiresPoTTextures()) { texWidth = nextHigher2(texWidth); texHeight = nextHigher2(texHeight); } // In order to really use the overridden surface we have to override all // values used for calculating the output size _renderedFrameI = 0; _renderedPremultiplyAlpha = _premultiplyAlpha; _renderedSurface.free(); _renderedSurface.w = texWidth; _renderedSurface.h = texHeight; _images[0]->free(); _images[0]->w = surface.w; _images[0]->h = surface.h; if (_renderedTexture->size() != Point(texWidth, texHeight)) { _renderedTexture = Common::move( g_engine->renderer().createTexture(texWidth, texHeight, false)); } if (surface.w == texWidth && surface.h == texHeight) _renderedTexture->update(surface); else { ManagedSurface tmpSurface(texWidth, texHeight, g_engine->renderer().getPixelFormat()); tmpSurface.blitFrom(surface); _renderedTexture->update(tmpSurface); } } void Animation::prerenderFrame(int32 frameI) { assert(frameI >= 0 && (uint)frameI < frameCount()); if (frameI == _renderedFrameI && _renderedPremultiplyAlpha == _premultiplyAlpha) return; auto bounds = frameBounds(frameI); _renderedSurface.clear(); for (uint spriteI = 0; spriteI < spriteCount(); spriteI++) { int32 imageI = imageIndex(frameI, spriteI); auto image = imageI < 0 ? nullptr : _images[imageI]; if (image == nullptr) continue; int offsetX = _imageOffsets[imageI].x - bounds.left; int offsetY = _imageOffsets[imageI].y - bounds.top; fullBlend(*image, _renderedSurface, offsetX, offsetY); } // Here was some alpha premultiplication, but it only produces bugs so is ignored _renderedTexture->update(_renderedSurface); _renderedFrameI = frameI; _renderedPremultiplyAlpha = _premultiplyAlpha; } void Animation::outputRect2D(int32 frameI, float scale, Vector2d &topLeft, Vector2d &size) const { auto bounds = frameBounds(frameI); topLeft += as2D(totalFrameOffset(frameI)) * scale; size = Vector2d(bounds.width(), bounds.height()) * scale; } void Animation::draw2D(int32 frameI, Vector2d topLeft, float scale, BlendMode blendMode, Color color) { prerenderFrame(frameI); auto bounds = frameBounds(frameI); Vector2d texMin(0, 0); Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h); Vector2d size; outputRect2D(frameI, scale, topLeft, size); auto &renderer = g_engine->renderer(); renderer.setTexture(_renderedTexture.get()); renderer.setBlendMode(blendMode); renderer.quad(topLeft, size, color, Angle(), texMin, texMax); } void Animation::outputRect3D(int32 frameI, float scale, Vector3d &topLeft, Vector2d &size) const { auto bounds = frameBounds(frameI); topLeft += as3D(totalFrameOffset(frameI)) * scale; topLeft = g_engine->camera().transform3Dto2D(topLeft); size = Vector2d(bounds.width(), bounds.height()) * scale * topLeft.z(); } void Animation::draw3D(int32 frameI, Vector3d topLeft, float scale, BlendMode blendMode, Color color) { prerenderFrame(frameI); auto bounds = frameBounds(frameI); Vector2d texMin(0, 0); Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h); Vector2d size; outputRect3D(frameI, scale, topLeft, size); const auto rotation = -g_engine->camera().rotation(); auto &renderer = g_engine->renderer(); renderer.setTexture(_renderedTexture.get()); renderer.setBlendMode(blendMode); renderer.quad(as2D(topLeft), size, color, rotation, texMin, texMax); } void Animation::drawEffect(int32 frameI, Vector3d topLeft, Vector2d size, Vector2d texOffset, BlendMode blendMode) { prerenderFrame(frameI); auto bounds = frameBounds(frameI); Vector2d texMin(0, 0); Vector2d texMax((float)bounds.width() / _renderedSurface.w, (float)bounds.height() / _renderedSurface.h); topLeft += as3D(totalFrameOffset(frameI)); topLeft = g_engine->camera().transform3Dto2D(topLeft); const auto rotation = -g_engine->camera().rotation(); size(0, 0) *= bounds.width() * topLeft.z() / _renderedSurface.w; size(1, 0) *= bounds.height() * topLeft.z() / _renderedSurface.h; auto &renderer = g_engine->renderer(); renderer.setTexture(_renderedTexture.get()); renderer.setBlendMode(blendMode); renderer.quad(as2D(topLeft), size, kWhite, rotation, texMin + texOffset, texMax + texOffset); } Font::Font(String fileName) : AnimationBase(fileName) {} void Font::load() { if (_isLoaded) return; AnimationBase::load(); // We now render all frames into a 16x16 atlas and fill up to power of two size just because it is easy here // However in two out of three fonts the character 128 is massive, it looks like a bug // as we want easy regular-sized characters it is ignored Point cellSize; for (auto image : _images) { assert(image != nullptr); // no fake pictures in fonts please if (image == _images[128]) continue; cellSize.x = MAX(cellSize.x, image->w); cellSize.y = MAX(cellSize.y, image->h); } _texMins.resize(_images.size()); _texMaxs.resize(_images.size()); ManagedSurface atlasSurface(nextHigher2(cellSize.x * 16), nextHigher2(cellSize.y * 16), g_engine->renderer().getPixelFormat()); cellSize.x = atlasSurface.w / 16; cellSize.y = atlasSurface.h / 16; const float invWidth = 1.0f / atlasSurface.w; const float invHeight = 1.0f / atlasSurface.h; for (uint i = 0; i < _images.size(); i++) { if (i == 128) continue; int offsetX = (i % 16) * cellSize.x + (cellSize.x - _images[i]->w) / 2; int offsetY = (i / 16) * cellSize.y + (cellSize.y - _images[i]->h) / 2; fullBlend(*_images[i], atlasSurface, offsetX, offsetY); _texMins[i].setX(offsetX * invWidth); _texMins[i].setY(offsetY * invHeight); _texMaxs[i].setX((offsetX + _images[i]->w) * invWidth); _texMaxs[i].setY((offsetY + _images[i]->h) * invHeight); } _texture = g_engine->renderer().createTexture(atlasSurface.w, atlasSurface.h, false); _texture->update(atlasSurface); debugCN(1, kDebugGraphics, "Rendered font atlas %s at %dx%d", _fileName.c_str(), atlasSurface.w, atlasSurface.h); } void Font::freeImages() { if (!_isLoaded) return; AnimationBase::freeImages(); _texture.reset(); _texMins.clear(); _texMaxs.clear(); } void Font::drawCharacter(int32 imageI, Point centerPoint, Color color) { assert(imageI >= 0 && (uint)imageI < _images.size()); Vector2d center = as2D(centerPoint + _imageOffsets[imageI]); Vector2d size(_images[imageI]->w, _images[imageI]->h); auto &renderer = g_engine->renderer(); renderer.setTexture(_texture.get()); renderer.setBlendMode(BlendMode::Tinted); renderer.quad(center, size, color, Angle(), _texMins[imageI], _texMaxs[imageI]); } Graphic::Graphic() {} Graphic::Graphic(ReadStream &stream) { _topLeft.x = stream.readSint16LE(); _topLeft.y = stream.readSint16LE(); _scale = stream.readSint16LE(); _order = stream.readSByte(); auto animationName = readVarString(stream); if (!animationName.empty()) setAnimation(animationName, AnimationFolder::Animations); } Graphic::Graphic(const Graphic &other) : _animation(other._animation) , _topLeft(other._topLeft) , _scale(other._scale) , _order(other._order) , _color(other._color) , _isPaused(other._isPaused) , _isLooping(other._isLooping) , _lastTime(other._lastTime) , _frameI(other._frameI) , _depthScale(other._depthScale) {} Graphic &Graphic::operator= (const Graphic &other) { _ownedAnimation.reset(); _animation = other._animation; _topLeft = other._topLeft; _scale = other._scale; _order = other._order; _color = other._color; _isPaused = other._isPaused; _isLooping = other._isLooping; _lastTime = other._lastTime; _frameI = other._frameI; _depthScale = other._depthScale; return *this; } void Graphic::loadResources() { if (_animation != nullptr) _animation->load(); } void Graphic::freeResources() { if (_ownedAnimation == nullptr) _animation = nullptr; else { _ownedAnimation->freeImages(); _animation = _ownedAnimation.get(); } } void Graphic::update() { if (_animation == nullptr || _animation->frameCount() == 0) return; const uint32 totalDuration = _animation->totalDuration(); uint32 curTime = _isPaused ? _lastTime : g_engine->getMillis() - _lastTime; if (curTime > totalDuration) { if (_isLooping && totalDuration > 0) curTime %= totalDuration; else { pause(); curTime = totalDuration ? totalDuration - 1 : 0; _lastTime = curTime; } } _frameI = totalDuration == 0 ? 0 : _animation->frameAtTime(curTime); assert(_frameI >= 0); } void Graphic::start(bool isLooping) { _isPaused = false; _isLooping = isLooping; _lastTime = g_engine->getMillis(); } void Graphic::pause() { _isPaused = true; _isLooping = false; _lastTime = g_engine->getMillis() - _lastTime; } void Graphic::reset() { _frameI = 0; _lastTime = _isPaused ? 0 : g_engine->getMillis(); } void Graphic::setAnimation(const Common::String &fileName, AnimationFolder folder) { _ownedAnimation.reset(new Animation(fileName, folder)); _animation = _ownedAnimation.get(); } void Graphic::setAnimation(Animation *animation) { _animation = animation; } void Graphic::syncGame(Serializer &serializer) { syncPoint(serializer, _topLeft); serializer.syncAsSint16LE(_scale); serializer.syncAsUint32LE(_lastTime); serializer.syncAsByte(_isPaused); serializer.syncAsByte(_isLooping); serializer.syncAsFloatLE(_depthScale); } static int8 shiftAndClampOrder(int8 order) { return MAX(0, MIN(kOrderCount - 1, order + kForegroundOrderCount)); } IDrawRequest::IDrawRequest(int8 order) : _order(shiftAndClampOrder(order)) {} AnimationDrawRequest::AnimationDrawRequest(Graphic &graphic, bool is3D, BlendMode blendMode, float lodBias) : IDrawRequest(graphic._order) , _is3D(is3D) , _animation(&graphic.animation()) , _frameI(graphic._frameI) , _topLeft(graphic._topLeft.x, graphic._topLeft.y, graphic._scale) , _scale(graphic._scale * graphic._depthScale) , _color(graphic.color()) , _blendMode(blendMode) , _lodBias(lodBias) { assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount()); } AnimationDrawRequest::AnimationDrawRequest(Animation *animation, int32 frameI, Vector2d center, int8 order) : IDrawRequest(order) , _is3D(false) , _animation(animation) , _frameI(frameI) , _topLeft(as3D(center)) , _scale(kBaseScale) , _color(kWhite) , _blendMode(BlendMode::AdditiveAlpha) , _lodBias(0.0f) { assert(animation != nullptr && animation->isLoaded()); assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount()); } void AnimationDrawRequest::draw() { g_engine->renderer().setLodBias(_lodBias); if (_is3D) _animation->draw3D(_frameI, _topLeft, _scale * kInvBaseScale, _blendMode, _color); else _animation->draw2D(_frameI, as2D(_topLeft), _scale * kInvBaseScale, _blendMode, _color); } SpecialEffectDrawRequest::SpecialEffectDrawRequest(Graphic &graphic, Point topLeft, Point bottomRight, Vector2d texOffset, BlendMode blendMode) : IDrawRequest(graphic._order) , _animation(&graphic.animation()) , _frameI(graphic._frameI) , _topLeft(topLeft.x, topLeft.y, graphic._scale) , _size(bottomRight.x - topLeft.x, bottomRight.y - topLeft.y) , _texOffset(texOffset) , _blendMode(blendMode) { assert(_frameI >= 0 && (uint)_frameI < _animation->frameCount()); } void SpecialEffectDrawRequest::draw() { _animation->drawEffect(_frameI, _topLeft, _size, _texOffset, _blendMode); } static const byte *trimLeading(const byte *text, const byte *end) { while (*text && text < end && *text <= ' ') text++; return text; } static const byte *trimTrailing(const byte *text, const byte *begin, bool trimSpaces) { while (text != begin && (*text <= ' ') == trimSpaces) text--; return text; } static Point characterSize(const Font &font, byte ch) { if (ch <= ' ' || (uint)(ch - ' ') >= font.imageCount()) ch = 0; else ch -= ' '; return font.imageSize(ch); } TextDrawRequest::TextDrawRequest(Font &font, const char *originalText, Point pos, int maxWidth, bool centered, Color color, int8 order) : IDrawRequest(order) , _font(font) , _color(color) { const int screenW = g_system->getWidth(); const int screenH = g_system->getHeight(); if (maxWidth < 0) maxWidth = screenW; // allocate on drawQueue to prevent having destruct it assert(originalText != nullptr); auto textLen = strlen(originalText); char *text = (char *)g_engine->drawQueue().allocator().allocateRaw(textLen + 1, 1); memcpy(text, originalText, textLen + 1); // split into trimmed lines uint lineCount = 0; const byte *itChar = (byte *)text, *itLine = (byte *)text, *textEnd = itChar + textLen + 1; int lineWidth = 0; while (true) { if (lineCount >= kMaxLines) { g_engine->game().tooManyDialogLines(lineCount, kMaxLines); break; } if (*itChar != '\r' && *itChar) lineWidth += characterSize(font, *itChar).x; if (lineWidth <= maxWidth && *itChar != '\r' && *itChar) { itChar++; continue; } // now we are in new-line territory if (*itChar > ' ') itChar = trimTrailing(itChar, itLine, false); // trim last word if (centered) { itChar = trimTrailing(itChar, itLine, true) + 1; itLine = trimLeading(itLine, itChar); _allLines[lineCount] = TextLine(itLine, itChar - itLine); } else _allLines[lineCount] = TextLine(itLine, itChar - itLine); itChar = trimLeading(itChar, textEnd); lineCount++; lineWidth = 0; itLine = itChar; if (!*itChar) break; } _lines = Span(_allLines, lineCount); _posX = Span(_allPosX, lineCount); // calc line widths and max line width _width = 0; for (uint i = 0; i < lineCount; i++) { lineWidth = 0; for (auto ch : _lines[i]) { if (ch != '\r' && ch) lineWidth += characterSize(font, ch).x; } _posX[i] = lineWidth; _width = MAX(_width, lineWidth); } // setup line positions if (centered) { if (pos.x - _width / 2 < 0) pos.x = _width / 2 + 1; if (pos.x + _width / 2 >= screenW) pos.x = screenW - _width / 2 - 1; for (auto &linePosX : _posX) linePosX = pos.x - linePosX / 2; } else fill(_posX.begin(), _posX.end(), pos.x); // setup height and y position _height = (int)lineCount * (font.imageSize(0).y * 4 / 3); _posY = pos.y; if (centered) _posY -= _height / 2; if (_posY < 0) _posY = 0; if (_posY + _height >= screenH) _posY = screenH - _height; } void TextDrawRequest::draw() { const Point spaceSize = _font.imageSize(0); Point cursor(0, _posY); for (uint i = 0; i < _lines.size(); i++) { cursor.x = _posX[i]; for (auto ch : _lines[i]) { const Point charSize = characterSize(_font, ch); if (ch > ' ' && (uint)(ch - ' ') < _font.imageCount()) _font.drawCharacter(ch - ' ', Point(cursor.x, cursor.y), _color); cursor.x += charSize.x; } cursor.y += spaceSize.y * 4 / 3; } } FadeDrawRequest::FadeDrawRequest(FadeType type, float value, int8 order) : IDrawRequest(order) , _type(type) , _value(value) {} void FadeDrawRequest::draw() { Color color; const byte valueAsByte = (byte)(_value * 255); switch (_type) { case FadeType::ToBlack: color = { 0, 0, 0, valueAsByte }; g_engine->renderer().setBlendMode(BlendMode::AdditiveAlpha); break; case FadeType::ToWhite: color = { valueAsByte, valueAsByte, valueAsByte, valueAsByte }; g_engine->renderer().setBlendMode(BlendMode::Additive); break; default: g_engine->game().unknownFadeType((int)_type); return; } g_engine->renderer().setTexture(nullptr); g_engine->renderer().quad(Vector2d(0, 0), as2D(Point(g_system->getWidth(), g_system->getHeight())), color); } struct FadeTask final : public Task { FadeTask(Process &process, FadeType fadeType, float from, float to, uint32 duration, EasingType easingType, int8 order, PermanentFadeAction permanentFadeAction) : Task(process) , _fadeType(fadeType) , _from(from) , _to(to) , _duration(duration) , _easingType(easingType) , _order(order) , _permanentFadeAction(permanentFadeAction) {} FadeTask(Process &process, Serializer &s) : Task(process) { FadeTask::syncGame(s); } TaskReturn run() override { TASK_BEGIN; if (_permanentFadeAction == PermanentFadeAction::UnsetFaded) g_engine->globalUI().isPermanentFaded() = false; _startTime = g_engine->getMillis(); while (g_engine->getMillis() - _startTime < _duration) { draw((g_engine->getMillis() - _startTime) / (float)_duration); TASK_YIELD(1); } draw(1.0f); // so that during a loading lag the screen is completly black/white if (_permanentFadeAction == PermanentFadeAction::SetFaded) g_engine->globalUI().isPermanentFaded() = true; TASK_END; } void debugPrint() override { uint32 remaining = g_engine->getMillis() - _startTime <= _duration ? _duration - (g_engine->getMillis() - _startTime) : 0; g_engine->console().debugPrintf("Fade (%d) from %.2f to %.2f with %ums remaining\n", (int)_fadeType, _from, _to, remaining); } void syncGame(Serializer &s) override { Task::syncGame(s); syncEnum(s, _fadeType); syncEnum(s, _easingType); syncEnum(s, _permanentFadeAction); s.syncAsFloatLE(_from); s.syncAsFloatLE(_to); s.syncAsUint32LE(_startTime); s.syncAsUint32LE(_duration); s.syncAsSByte(_order); } const char *taskName() const override; private: void draw(float t) { g_engine->drawQueue().add(_fadeType, _from + (_to - _from) * ease(t, _easingType), _order); } FadeType _fadeType = {}; float _from = 0, _to = 0; uint32 _startTime = 0, _duration = 0; EasingType _easingType = {}; int8 _order = 0; PermanentFadeAction _permanentFadeAction = {}; }; DECLARE_TASK(FadeTask) Task *fade(Process &process, FadeType fadeType, float from, float to, int32 duration, EasingType easingType, int8 order, PermanentFadeAction permanentFadeAction) { if (duration <= 0) return new DelayTask(process, 0); if (!process.isActiveForPlayer()) return new DelayTask(process, (uint32)duration); return new FadeTask(process, fadeType, from, to, duration, easingType, order, permanentFadeAction); } BorderDrawRequest::BorderDrawRequest(Rect rect, Color color) : IDrawRequest(-kForegroundOrderCount) , _rect(rect) , _color(color) {} void BorderDrawRequest::draw() { auto &renderer = g_engine->renderer(); renderer.setTexture(nullptr); renderer.setBlendMode(BlendMode::AdditiveAlpha); renderer.quad({ (float)_rect.left, (float)_rect.top }, { (float)_rect.width(), (float)_rect.height() }, _color); } DrawQueue::DrawQueue(IRenderer *renderer) : _renderer(renderer) , _allocator(1024) { assert(renderer != nullptr); } void DrawQueue::clear() { _allocator.deallocateAll(); memset(_requestsPerOrderCount, 0, sizeof(_requestsPerOrderCount)); memset(_lodBiasPerOrder, 0, sizeof(_lodBiasPerOrder)); } void DrawQueue::addRequest(IDrawRequest *drawRequest) { assert(drawRequest != nullptr && drawRequest->order() >= 0 && drawRequest->order() < kOrderCount); auto order = drawRequest->order(); if (_requestsPerOrderCount[order] < kMaxDrawRequestsPerOrder) _requestsPerOrder[order][_requestsPerOrderCount[order]++] = drawRequest; else g_engine->game().tooManyDrawRequests(order); } void DrawQueue::setLodBias(int8 orderFrom, int8 orderTo, float newLodBias) { orderFrom = shiftAndClampOrder(orderFrom); orderTo = shiftAndClampOrder(orderTo); if (orderFrom <= orderTo) { Common::fill(_lodBiasPerOrder + orderFrom, _lodBiasPerOrder + orderTo + 1, newLodBias); } } void DrawQueue::draw() { for (int8 order = kOrderCount - 1; order >= 0; order--) { _renderer->setLodBias(_lodBiasPerOrder[order]); for (uint8 requestI = 0; requestI < _requestsPerOrderCount[order]; requestI++) { _requestsPerOrder[order][requestI]->draw(); _requestsPerOrder[order][requestI]->~IDrawRequest(); } } _allocator.deallocateAll(); } BumpAllocator::BumpAllocator(size_t pageSize) : _pageSize(pageSize) { allocatePage(); } BumpAllocator::~BumpAllocator() { for (auto page : _pages) free(page); } void *BumpAllocator::allocateRaw(size_t size, size_t align) { assert(size <= _pageSize); uintptr_t page = (uintptr_t)_pages[_pageI]; uintptr_t top = page + _used; top += align - 1; top -= top % align; if (page + _pageSize - top >= size) { _used = top + size - page; return (void *)top; } _used = 0; _pageI++; if (_pageI >= _pages.size()) allocatePage(); return allocateRaw(size, align); } void BumpAllocator::allocatePage() { auto page = malloc(_pageSize); if (page == nullptr) error("Out of memory in BumpAllocator"); _pages.push_back(page); } void BumpAllocator::deallocateAll() { _pageI = 0; _used = 0; } }