Files
scummvm-cursorfix/engines/alcachofa/graphics.cpp
2026-02-02 04:50:13 +01:00

1000 lines
31 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 "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<SeekableReadStream> 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<int8>(0, MIN<int8>(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<TextLine>(_allLines, lineCount);
_posX = Span<int>(_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<FadeDrawRequest>(_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;
}
}