Files
2026-02-02 04:50:13 +01:00

730 lines
18 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 "bagel/spacebar/boflib/gfx/sprite.h"
#include "bagel/boflib/misc.h"
namespace Bagel {
namespace SpaceBar {
CBofRect *CBofSprite::_cDirtyRect;
CBofSprite *CBofSprite::_pSpriteChain = nullptr; // Pointer to chain of linked sprites
CBofSprite *CBofSprite::_pTouchedSprite = nullptr; // Pointer to sprite overlapped during painting
CBofBitmap *CBofSprite::_pWorkBmp = nullptr; // Offscreen work area
CBofPalette *CBofSprite::_pSharedPalette = nullptr; // Shared palette for ALL sprites
int CBofSprite::_nWorkDX = 0;
int CBofSprite::_nWorkDY = 0;
void CBofSprite::initialize() {
_cDirtyRect = new CBofRect();
_pSpriteChain = nullptr;
_pTouchedSprite = nullptr;
_pWorkBmp = nullptr;
_pSharedPalette = nullptr;
_nWorkDX = 0;
_nWorkDY = 0;
}
void CBofSprite::shutdown() {
delete _cDirtyRect;
}
void CBofSprite::openLibrary(CBofPalette *pPal) {
// Must have a valid palette to do any sprite related stuff
assert(pPal != nullptr);
clearDirtyRect();
setSharedPalette(pPal);
// Set up a default work area
setupWorkArea(200, 200);
}
void CBofSprite::closeLibrary() {
flushSpriteChain();
tearDownWorkArea();
_pSharedPalette = nullptr;
}
CBofSprite::CBofSprite() {
_pImage = nullptr; // No initial bitmap image for the sprite
_cSize = CBofSize(0, 0); // There is no size to the sprite image
_cRect.setRectEmpty(); // Rectangular bounds not yet defined
_cImageRect = _cRect; // Image rectangle starts same as display bounds
_cPosition = CBofPoint(0, 0); // Default position to upper left corner of display
_bPositioned = false; // Not yet positioned
_bDuplicated = false; // Not sharing resources with other sprites
_nZOrder = SPRITE_TOPMOST; // Default to top most in fore/back ground order
_nCelCount = 1; // Number of frames in animated cel strip
_nCelID = _nCelCount - 1; // Cel identifier not pointing at a cel
_bAnimated = false; // Not initially animated
_bLinked = false; // Not initially linked into the sprite chain
_nMaskColor = NOT_TRANSPARENT; // Default to NO transparency
_bReadOnly = true;
setBlockAdvance(false); // Default always advance next sprite
}
CBofSprite::~CBofSprite() {
assert(isValidObject(this));
unlinkSprite();
clearImage(); // Clear the sprite image bitmap and context
}
void CBofSprite::linkSprite() {
assert(isValidObject(this));
if (!_bLinked) {
// Set for linked into chain
_bLinked = true;
if (_pSpriteChain != nullptr) {
switch (_nZOrder) {
case SPRITE_TOPMOST:
_pSpriteChain->addToTail(this);
break;
case SPRITE_HINDMOST:
_pSpriteChain->addToHead(this);
_pSpriteChain = this;
break;
default: {
CBofSprite *pSprite;
CBofSprite *pLastSprite = pSprite = _pSpriteChain;
while (pSprite != nullptr && pSprite->_nZOrder > _nZOrder) {
pLastSprite = pSprite;
pSprite = (CBofSprite *)pSprite->_pNext;
}
pLastSprite->Insert(this);
break;
}
}
} else {
_pSpriteChain = this;
}
// _pSpriteChain must always point to the head of the linked list
assert(_pSpriteChain == (CBofSprite *)_pSpriteChain->getHead());
}
}
void CBofSprite::unlinkSprite() {
assert(isValidObject(this));
if (_bLinked) {
// Set for not linked into chain
_bLinked = false;
if (_pSpriteChain == this)
_pSpriteChain = (CBofSprite *)_pNext;
Delete();
}
}
void CBofSprite::flushSpriteChain() {
CBofSprite *pSprite = getSpriteChain();
// Cycle getting head of chain, un-linking it and then deleting it
while (pSprite != nullptr) {
pSprite->unlinkSprite();
delete pSprite;
pSprite = getSpriteChain();
}
}
void CBofSprite::setupWorkArea(int dx, int dy) {
// Do we already have a work area?
if (_pWorkBmp != nullptr) {
// Yes, so lets tear it down before we start a new one
tearDownWorkArea();
}
// Create an offscreen bitmap where we do all the work;
_pWorkBmp = new CBofBitmap(dx, dy, _pSharedPalette);
_nWorkDX = dx;
_nWorkDY = dy;
}
void CBofSprite::tearDownWorkArea() {
delete _pWorkBmp;
_pWorkBmp = nullptr;
}
CBofSprite *CBofSprite::duplicateSprite() {
assert(isValidObject(this));
// Create an object for the sprite
CBofSprite *pSprite = new CBofSprite;
duplicateSprite(pSprite);
return pSprite;
}
void CBofSprite::duplicateSprite(CBofSprite *pSprite) {
if (!isValidObject(this) || (pSprite == nullptr))
error("duplicateSprite - Invalid source or destination sprite");
pSprite->_pImage = _pImage;
pSprite->_cRect = _cRect;
pSprite->_cImageRect = _cImageRect;
pSprite->_cSize = _cSize;
pSprite->_cPosition = _cPosition;
pSprite->_nZOrder = _nZOrder;
pSprite->_nCelID = _nCelID;
pSprite->_nCelCount = _nCelCount;
pSprite->_bAnimated = _bAnimated;
pSprite->_nMaskColor = _nMaskColor;
pSprite->_bDuplicated = true; // Mark it as a sprite with shared resources
}
bool CBofSprite::loadSprite(const char *pszPathName, int nCels) {
assert(isValidObject(this));
assert(pszPathName != nullptr);
assert(nCels >= 1);
// Create an object for the sprite's image
CBofBitmap *pBitmap = new CBofBitmap(pszPathName, _pSharedPalette);
return loadSprite(pBitmap, nCels);
}
bool CBofSprite::loadSprite(CBofBitmap *pBitmap, int nCels) {
assert(isValidObject(this));
// Can't load an invalid bitmap
assert(pBitmap != nullptr);
assert(nCels >= 1);
clearImage(); // Clear out any/all existing bitmaps, palettes,
_pImage = pBitmap; // Save pointer to bitmap
pBitmap->setReadOnly(_bReadOnly);
_cSize = pBitmap->getSize();
_cRect.setRect(0, 0, _cSize.cx - 1, _cSize.cy - 1);
_cImageRect.setRect(0, 0, _cSize.cx - 1, _cSize.cy - 1);
_nCelCount = 1;
_nCelID = _nCelCount - 1;
if (nCels != 1) {
setupCels(nCels);
// Assume it's animated
_bAnimated = true;
}
return true; // Return success
}
bool CBofSprite::setupCels(const int nCels) {
assert(isValidObject(this));
assert(nCels > 0);
_nCelCount = nCels; // Set cel count
_nCelID = _nCelCount - 1; // No current cel
int nStripWidth = _cSize.cx; // Temp place toRetain cell strip pixel length
_cSize.cx /= nCels; // Calculate width of a cel
if (_cSize.cx * nCels == nStripWidth) { // Verify we have an even multiple
_cRect.right = _cRect.left + _cSize.cx; // Reset sprite rectangular bounds
_cRect.bottom = _cRect.top + _cSize.cy; // ... based on cel dimensions
_cImageRect.setRect(0, 0, _cSize.cx - 1, _cSize.cy - 1); // Set bounds for first cel in strip
return true;
}
return false;
}
void CBofSprite::nextCel() {
assert(isValidObject(this));
// verify old cel id
assert(_nCelID >= 0 && _nCelID < _nCelCount);
if (getBlockAdvance() == false) {
if (++_nCelID >= _nCelCount)
_nCelID = 0;
setCel(_nCelID);
}
}
void CBofSprite::prevCel() {
assert(isValidObject(this));
// verify old cel id
assert(_nCelID >= 0 && _nCelID < _nCelCount);
if (--_nCelID < 0)
_nCelID = _nCelCount - 1;
setCel(_nCelID);
}
bool CBofSprite::paintSprite(CBofWindow *pWnd, const int x, const int y) {
assert(isValidObject(this));
// Can't paint to a non-existent window
assert(pWnd != nullptr);
// The window MUST have a backdrop
assert(pWnd->getBackdrop() != nullptr);
batchPaint(x, y);
updateDirtyRect(pWnd, this);
return !errorOccurred();
}
bool CBofSprite::paintSprite(CBofBitmap *pBmp, const int x, const int y) {
assert(isValidObject(this));
// Can't paint to a non-existent window
assert(pBmp != nullptr);
batchPaint(x, y);
updateDirtyRect(pBmp, this);
return !errorOccurred();
}
bool CBofSprite::paintCel(CBofWindow *pWnd, int nCelId, const int x, const int y) {
setCel(nCelId - 1);
return paintSprite(pWnd, x, y);
}
bool CBofSprite::paintCel(CBofBitmap *pBmp, int nCelId, const int x, const int y) {
setCel(nCelId - 1);
return paintSprite(pBmp, x, y);
}
void CBofSprite::batchPaint(const int x, const int y) {
assert(isValidObject(this));
CBofRect cDstRect;
// Default to no sprite being overlapped by this painting operation
_pTouchedSprite = nullptr;
// Calculate destination rectangle
cDstRect.setRect(x, y, x + _cSize.cx - 1, y + _cSize.cy - 1);
// Add the destination position to the dirty rectangle list
addToDirtyRect(&cDstRect);
// If the sprite is already on screen, then we must also add it's old
// current location to the dirty rect list so that it is erase properly
if (_bPositioned) {
addToDirtyRect(&_cRect);
}
// Now establish the sprite's new position
setPosition(x, y);
if (_bAnimated && (_nCelCount > 1))
// Advance to the next cel in the strip
nextCel();
}
bool CBofSprite::updateDirtyRect(CBofWindow *pWnd, CBofSprite *pPrimarySprite) {
assert(pWnd != nullptr);
// The window MUST have a backdrop associated with it. If that's not feasible, then
// use CSprites instead of CBofSprites
assert(pWnd->getBackdrop() != nullptr);
//
// Repaint the contents of the specified rectangle
//
CBofBitmap *pBackdrop = pWnd->getBackdrop();
if (pBackdrop != nullptr) {
CBofRect *pRect = _cDirtyRect;
if (pRect->width() != 0 && pRect->height() != 0) {
// Need a work area
CBofBitmap *pWork = _pWorkBmp;
int dx = pRect->width();
int dy = pRect->height();
bool bTempWorkArea = false;
if ((pWork == nullptr) || (dx > _nWorkDX) || (dy > _nWorkDY)) {
bTempWorkArea = true;
pWork = new CBofBitmap(dx, dy, _pSharedPalette);
}
pWork->lock();
// Paint the background into the work area
pBackdrop->paint(pWork, 0, 0, pRect);
// Only need to search the sprite list if current sprite is linked
CBofSprite *pSprite = pPrimarySprite;
if (pPrimarySprite == nullptr || pPrimarySprite->_bLinked) {
pSprite = _pSpriteChain;
}
CBofRect cRect, cSrcRect;
// Run through the sprite list
while (pSprite != nullptr) {
// and paint each partial sprite overlap to the work area
if (pSprite->_bPositioned && cRect.intersectRect(&pSprite->_cRect, pRect)) {
if (pPrimarySprite != pSprite)
_pTouchedSprite = pSprite;
cSrcRect = cRect - pSprite->_cRect.topLeft();
cSrcRect += pSprite->_cImageRect.topLeft();
cRect -= pRect->topLeft();
pSprite->_pImage->paint(pWork, &cRect, &cSrcRect, pSprite->_nMaskColor);
}
pSprite = (CBofSprite *)pSprite->_pNext;
}
// Paint final outcome to the screen
cSrcRect.setRect(0, 0, pRect->width() - 1, pRect->height() - 1);
pWork->paint(pWnd, pRect, &cSrcRect);
pWork->unlock();
if (bTempWorkArea) {
delete pWork;
}
}
}
clearDirtyRect();
return true;
}
bool CBofSprite::updateDirtyRect(CBofBitmap *pBmp, CBofSprite *pPrimarySprite) {
assert(pBmp != nullptr);
//
// Repaint the contents of the specified rectangle
//
CBofRect *pRect = getDirtyRect();
// Only need to search the sprite list if current sprite is linked
CBofSprite *pSprite = pPrimarySprite;
if (pPrimarySprite == nullptr || pPrimarySprite->_bLinked) {
pSprite = _pSpriteChain;
}
CBofRect cRect;
// Run through the sprite list
while (pSprite != nullptr) {
// and paint each partial sprite overlap to the work area
if (pSprite->_bPositioned && cRect.intersectRect(&pSprite->_cRect, pRect)) {
if (pPrimarySprite != pSprite)
_pTouchedSprite = pSprite;
CBofRect cSrcRect = cRect - pSprite->_cRect.topLeft();
cSrcRect += pSprite->_cImageRect.topLeft();
pSprite->_pImage->paint(pBmp, &cRect, &cSrcRect, pSprite->_nMaskColor);
}
pSprite = (CBofSprite *)pSprite->_pNext;
}
clearDirtyRect();
return true;
}
void CBofSprite::addToDirtyRect(CBofRect *pRect) {
assert(pRect != nullptr);
CBofRect cRect;
if (_cDirtyRect->isRectEmpty()) {
cRect = *pRect;
} else {
cRect.unionRect(_cDirtyRect, pRect);
}
*_cDirtyRect = cRect;
}
void CBofSprite::setCel(const int nCelID) {
assert(isValidObject(this));
// All sprites must have at least 1 frame
assert(_nCelCount > 0);
if (_nCelID != nCelID) {
_nCelID = nCelID % _nCelCount;
if ((_nCelID != 0) && (nCelID < 0)) {
_nCelID = _nCelCount + _nCelID;
}
}
// Verify new cel id
assert(_nCelID >= 0 && _nCelID < _nCelCount);
_cImageRect.left = _nCelID * _cSize.cx;
_cImageRect.right = _cImageRect.left + _cSize.cx;
}
bool CBofSprite::eraseSprite(CBofWindow *pWnd) {
assert(isValidObject(this));
assert(pWnd != nullptr);
batchErase();
updateDirtyRect(pWnd);
return !errorOccurred();
}
void CBofSprite::batchErase() {
if (_bPositioned) {
_bPositioned = false;
addToDirtyRect(&_cRect);
}
}
bool CBofSprite::testInterception(CBofSprite *pTestSprite, CBofPoint *pPoint) {
assert(isValidObject(this));
assert(pTestSprite != nullptr);
// Punt if no interception allowed
if (pTestSprite != nullptr) {
// be sure to not test against ourself
if (this != pTestSprite) {
CBofRect overlapRect; // Area of overlap between rectangles
// Use simple rectangle screening first
if (overlapRect.intersectRect(&_cRect, &pTestSprite->_cRect)) {
// ... and if that succeeds, see if we
// ... have image masks that overlap
if ((_nMaskColor == NOT_TRANSPARENT) || (pTestSprite->_nMaskColor == NOT_TRANSPARENT) || spritesOverlap(pTestSprite, pPoint)) {
return true;
}
}
}
}
return false;
}
CBofSprite *CBofSprite::interception(CBofRect *pNewRect, CBofSprite *pTestSprite) {
assert(isValidObject(this));
assert(pNewRect != nullptr);
// Get first sprite to be tested
CBofSprite *pSprite = pTestSprite;
// Thumb through the sprite chain
while (pSprite != nullptr) {
// be sure to not test against ourself
// ... and only test against overlapping sprites
if (this != pSprite) {
CBofRect overlapRect; // Area of overlap between rectangles
// Sprites touch if their rectangles intersect.
// does our sprite overlap another?
if (overlapRect.intersectRect(pNewRect, &pSprite->_cRect))
// ... if so return a pointer to it
return pSprite;
}
// Fetch next sprite in chain for testing
pSprite = (CBofSprite *)pSprite->_pNext;
}
return nullptr;
}
CBofSprite *CBofSprite::interception(CBofSprite *pTestSprite) {
assert(isValidObject(this));
CBofSprite *pSprite = pTestSprite; // Get first sprite to be tested
while (pSprite != nullptr) { // Thumb through the entire sprite collection
if (testInterception(pSprite, nullptr)) // ... testing against each sprite in turn
return pSprite; // found an interception
pSprite = (CBofSprite *)pSprite->_pNext; // fetch next sprite in chain for testing
}
return nullptr;
}
bool CBofSprite::spritesOverlap(CBofSprite *pSprite, CBofPoint *pPoint) {
assert(isValidObject(this));
assert(pSprite != nullptr);
// Assume no overlap
bool bHit = false;
// If the sprite's rectangles overlap
CBofRect overlapRect;
if (overlapRect.intersectRect(&_cRect, &pSprite->_cRect)) {
int32 dx = overlapRect.width();
int32 dy = overlapRect.height();
int32 x1 = overlapRect.left - _cRect.left + _cImageRect.left;
int32 y1 = overlapRect.top - _cRect.top + _cImageRect.top;
int32 x2 = overlapRect.left - pSprite->_cRect.left + pSprite->_cImageRect.left;
int32 y2 = overlapRect.top - pSprite->_cRect.top + pSprite->_cImageRect.top;
int32 dx1 = _pImage->widthBytes();
int32 dx2 = pSprite->_pImage->widthBytes();
byte m1 = (byte)_nMaskColor;
byte m2 = (byte)pSprite->_nMaskColor;
// Lock down these bitmaps
_pImage->lock();
pSprite->_pImage->lock();
byte *pDib1 = (byte *)_pImage->getPixelAddress((int)x1, (int)y1);
byte *pDib2 = (byte *)pSprite->_pImage->getPixelAddress((int)x2, (int)y2);
if (!_pImage->isTopDown()) {
dx1 = -dx1;
}
if (!pSprite->_pImage->isTopDown()) {
dx2 = -dx2;
}
for (int32 y = 0; y < dy; y++) {
byte *pPtr1 = pDib1;
byte *pPtr2 = pDib2;
for (int32 x = 0; x < dx; x++) {
if ((*pPtr1 != m1) && (*pPtr2 != m2)) {
if (pPoint != nullptr) {
pPoint->x = (int)x;
pPoint->y = (int)y;
}
bHit = true;
goto endroutine;
}
pPtr1++;
pPtr2++;
}
pDib1 += dx1;
pDib2 += dx2;
}
}
endroutine:
// Don't need access to these bitmaps any more
pSprite->_pImage->unlock();
_pImage->unlock();
return bHit;
}
void CBofSprite::setPosition(int x, int y) {
assert(isValidObject(this));
// Now have a real location establish the new location of the sprite
// and setup the bitmap's bounding rectangle
_bPositioned = true;
_cPosition.x = x;
_cPosition.y = y;
_cRect.setRect(_cPosition.x, _cPosition.y, _cPosition.x + _cSize.cx - 1, _cPosition.y + _cSize.cy - 1);
}
void CBofSprite::clearImage() {
assert(isValidObject(this));
if (!_bDuplicated && (_pImage != nullptr)) {
delete _pImage;
}
_pImage = nullptr;
}
void CBofSprite::setSharedPalette(CBofPalette *pPal) {
assert(pPal != nullptr);
_pSharedPalette = pPal;
}
void CBofSprite::setZOrder(int nValue) {
assert(isValidObject(this));
assert(nValue >= SPRITE_TOPMOST && nValue <= SPRITE_HINDMOST);
_nZOrder = nValue;
// Relinking this sprite after setting it's new Z-Order will
// add the sprite to the correct Z-Order sorted location (Insertion Sort)
if (_bLinked) {
unlinkSprite();
linkSprite();
}
}
} // namespace SpaceBar
} // namespace Bagel