scummvm/engines/agi/sprite.cpp
2010-06-15 12:34:12 +00:00

735 lines
17 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*
*/
#include "agi/agi.h"
#include "agi/sprite.h"
#include "agi/graphics.h"
namespace Agi {
/**
* Sprite structure.
* This structure holds information on visible and priority data of
* a rectangular area of the AGI screen. Sprites are chained in two
* circular lists, one for updating and other for non-updating sprites.
*/
struct Sprite {
VtEntry *v; /**< pointer to view table entry */
int16 xPos; /**< x coordinate of the sprite */
int16 yPos; /**< y coordinate of the sprite */
int16 xSize; /**< width of the sprite */
int16 ySize; /**< height of the sprite */
uint8 *buffer; /**< buffer to store background data */
};
/*
* Sprite pool replaces dynamic allocation
*/
#undef ALLOC_DEBUG
#define POOL_SIZE 68000 // Gold Rush mine room needs > 50000
// Speeder bike challenge needs > 67000
void *SpritesMgr::poolAlloc(int size) {
uint8 *x;
// Adjust size to sizeof(void *) boundary to prevent data misalignment
// errors.
const int alignPadding = sizeof(void*) - 1;
size = (size + alignPadding) & ~alignPadding;
x = _poolTop;
_poolTop += size;
if (_poolTop >= (uint8 *)_spritePool + POOL_SIZE) {
debugC(1, kDebugLevelMain | kDebugLevelResources, "not enough memory");
_poolTop = x;
return NULL;
}
return x;
}
// Note: it's critical that pool_release() is called in the exact
// reverse order of pool_alloc()
void SpritesMgr::poolRelease(void *s) {
_poolTop = (uint8 *)s;
}
/*
* Blitter functions
*/
// Blit one pixel considering the priorities
void SpritesMgr::blitPixel(uint8 *p, uint8 *end, uint8 col, int spr, int width, int *hidden) {
int epr = 0, pr = 0; // effective and real priorities
// CM: priority 15 overrides control lines and is ignored when
// tracking effective priority. This tweak is needed to fix
// Sarien bug #451768, and should not affect Sierra games because
// sprites shouldn't have priority 15 (like the AGI Mouse
// demo "mouse pointer")
//
// Update: this solution breaks other games, and can't be used.
if (p >= end)
return;
// Check if we're on a control line
if ((pr = *p & 0xf0) < 0x30) {
uint8 *p1;
// Yes, get effective priority going down
for (p1 = p; p1 < end && (epr = *p1 & 0xf0) < 0x30; p1 += width)
;
if (p1 >= end)
epr = 0x40;
} else {
epr = pr;
}
if (spr >= epr) {
// Keep control line information visible, but put our
// priority over water (0x30) surface
if (_vm->getFeatures() & (GF_AGI256 | GF_AGI256_2))
*(p + FROM_SBUF16_TO_SBUF256_OFFSET) = col; // Write to 256 color buffer
else
*p = (pr < 0x30 ? pr : spr) | col; // Write to 16 color (+control line/priority info) buffer
*hidden = false;
// Except if our priority is 15, which should never happen
// (fixes Sarien bug #451768)
//
// Update: breaks other games, can't be used
//
// if (spr == 0xf0)
// *p = spr | col;
}
}
int SpritesMgr::blitCel(int x, int y, int spr, ViewCel *c, bool agi256_2) {
uint8 *p0, *p, *q = NULL, *end;
int i, j, t, m, col;
int hidden = true;
// Fixes Sarien bug #477841 (crash in PQ1 map C4 when y == -2)
if (y < 0)
y = 0;
if (x < 0)
x = 0;
if (y >= _HEIGHT)
y = _HEIGHT - 1;
if (x >= _WIDTH)
x = _WIDTH - 1;
q = c->data;
t = c->transparency;
m = c->mirror;
spr <<= 4;
p0 = &_vm->_game.sbuf16c[x + y * _WIDTH + m * (c->width - 1)];
end = _vm->_game.sbuf16c + _WIDTH * _HEIGHT;
for (i = 0; i < c->height; i++) {
p = p0;
while (*q) {
col = agi256_2 ? *q : (*q & 0xf0) >> 4; // Uses whole byte for color info with AGI256-2
for (j = agi256_2 ? 1 : *q & 0x0f; j; j--, p += 1 - 2 * m) { // No RLE with AGI256-2
if (col != t) {
blitPixel(p, end, col, spr, _WIDTH, &hidden);
}
}
q++;
}
p0 += _WIDTH;
q++;
}
return hidden;
}
void SpritesMgr::objsSaveArea(Sprite *s) {
int y;
int16 xPos = s->xPos, yPos = s->yPos;
int16 xSize = s->xSize, ySize = s->ySize;
uint8 *p0, *q;
if (xPos + xSize > _WIDTH)
xSize = _WIDTH - xPos;
if (xPos < 0) {
xSize += xPos;
xPos = 0;
}
if (yPos + ySize > _HEIGHT)
ySize = _HEIGHT - yPos;
if (yPos < 0) {
ySize += yPos;
yPos = 0;
}
if (xSize <= 0 || ySize <= 0)
return;
p0 = &_vm->_game.sbuf[xPos + yPos * _WIDTH];
q = s->buffer;
for (y = 0; y < ySize; y++) {
memcpy(q, p0, xSize);
q += xSize;
p0 += _WIDTH;
}
}
void SpritesMgr::objsRestoreArea(Sprite *s) {
int y, offset;
int16 xPos = s->xPos, yPos = s->yPos;
int16 xSize = s->xSize, ySize = s->ySize;
uint8 *q;
uint32 pos0;
if (xPos + xSize > _WIDTH)
xSize = _WIDTH - xPos;
if (xPos < 0) {
xSize += xPos;
xPos = 0;
}
if (yPos + ySize > _HEIGHT)
ySize = _HEIGHT - yPos;
if (yPos < 0) {
ySize += yPos;
yPos = 0;
}
if (xSize <= 0 || ySize <= 0)
return;
pos0 = xPos + yPos * _WIDTH;
q = s->buffer;
offset = _vm->_game.lineMinPrint * CHAR_LINES;
for (y = 0; y < ySize; y++) {
memcpy(&_vm->_game.sbuf[pos0], q, xSize);
_gfx->putPixelsA(xPos, yPos + y + offset, xSize, &_vm->_game.sbuf16c[pos0]);
q += xSize;
pos0 += _WIDTH;
}
// WORKAROUND (see ScummVM bug #1945716)
// When set.view command is called, current code cannot detect this situation while updating
// Thus we force removal of the old sprite
if (s->v->viewReplaced) {
commitBlock(xPos, yPos, xPos + xSize, yPos + ySize);
s->v->viewReplaced = false;
}
}
/**
* Condition to determine whether a sprite will be in the 'updating' list.
*/
bool SpritesMgr::testUpdating(VtEntry *v, AgiEngine *agi) {
// Sanity check (see Sarien bug #779302)
if (~agi->_game.dirView[v->currentView].flags & RES_LOADED)
return false;
return (v->flags & (ANIMATED | UPDATE | DRAWN)) == (ANIMATED | UPDATE | DRAWN);
}
/**
* Condition to determine whether a sprite will be in the 'non-updating' list.
*/
bool SpritesMgr::testNotUpdating(VtEntry *v, AgiEngine *vm) {
// Sanity check (see Sarien bug #779302)
if (~vm->_game.dirView[v->currentView].flags & RES_LOADED)
return false;
return (v->flags & (ANIMATED | UPDATE | DRAWN)) == (ANIMATED | DRAWN);
}
/**
* Convert sprite priority to y value.
*/
int SpritesMgr::prioToY(int p) {
int i;
if (p == 0)
return -1;
for (i = 167; i >= 0; i--) {
if (_vm->_game.priTable[i] < p)
return i;
}
return -1; // (p - 5) * 12 + 48;
}
/**
* Create and initialize a new sprite structure.
*/
Sprite *SpritesMgr::newSprite(VtEntry *v) {
Sprite *s;
s = (Sprite *)poolAlloc(sizeof(Sprite));
if (s == NULL)
return NULL;
s->v = v; // link sprite to associated view table entry
s->xPos = v->xPos;
s->yPos = v->yPos - v->ySize + 1;
s->xSize = v->xSize;
s->ySize = v->ySize;
s->buffer = (uint8 *)poolAlloc(s->xSize * s->ySize);
v->s = s; // link view table entry to this sprite
return s;
}
/**
* Insert sprite in the specified sprite list.
*/
void SpritesMgr::sprAddlist(SpriteList &l, VtEntry *v) {
Sprite *s = newSprite(v);
l.push_back(s);
}
/**
* Sort sprites from lower y values to build a sprite list.
*/
void SpritesMgr::buildList(SpriteList &l, bool (*test)(VtEntry *, AgiEngine *)) {
int i, j, k;
VtEntry *v;
VtEntry *entry[0x100];
int yVal[0x100];
int minY = 0xff, minIndex = 0;
// fill the arrays with all sprites that satisfy the 'test'
// condition and their y values
i = 0;
for (v = _vm->_game.viewTable; v < &_vm->_game.viewTable[MAX_VIEWTABLE]; v++) {
if ((*test)(v, _vm)) {
entry[i] = v;
yVal[i] = v->flags & FIXED_PRIORITY ? prioToY(v->priority) : v->yPos;
i++;
}
}
debugC(5, kDebugLevelSprites, "buildList() --> entries %d", i);
// now look for the smallest y value in the array and put that
// sprite in the list
for (j = 0; j < i; j++) {
minY = 0xff;
for (k = 0; k < i; k++) {
if (yVal[k] < minY) {
minIndex = k;
minY = yVal[k];
}
}
yVal[minIndex] = 0xff;
sprAddlist(l, entry[minIndex]);
}
}
/**
* Build list of updating sprites.
*/
void SpritesMgr::buildUpdBlitlist() {
buildList(_sprUpd, testUpdating);
}
/**
* Build list of non-updating sprites.
*/
void SpritesMgr::buildNonupdBlitlist() {
buildList(_sprNonupd, testNotUpdating);
}
/**
* Clear the given sprite list.
*/
void SpritesMgr::freeList(SpriteList &l) {
SpriteList::iterator iter;
for (iter = l.reverse_begin(); iter != l.end(); ) {
Sprite* s = *iter;
poolRelease(s->buffer);
poolRelease(s);
iter = l.reverse_erase(iter);
}
}
/**
* Copy sprites from the pic buffer to the screen buffer, and check if
* sprites of the given list have moved.
*/
void SpritesMgr::commitSprites(SpriteList &l, bool immediate) {
SpriteList::iterator iter;
for (iter = l.begin(); iter != l.end(); ++iter) {
Sprite *s = *iter;
int x1, y1, x2, y2;
x1 = MIN((int)MIN(s->v->xPos, s->v->xPos2), MIN(s->v->xPos + s->v->celData->width, s->v->xPos2 + s->v->celData2->width));
x2 = MAX((int)MAX(s->v->xPos, s->v->xPos2), MAX(s->v->xPos + s->v->celData->width, s->v->xPos2 + s->v->celData2->width));
y1 = MIN((int)MIN(s->v->yPos, s->v->yPos2), MIN(s->v->yPos - s->v->celData->height, s->v->yPos2 - s->v->celData2->height));
y2 = MAX((int)MAX(s->v->yPos, s->v->yPos2), MAX(s->v->yPos - s->v->celData->height, s->v->yPos2 - s->v->celData2->height));
s->v->celData2 = s->v->celData;
commitBlock(x1, y1, x2, y2, immediate);
if (s->v->stepTimeCount != s->v->stepTime)
continue;
if (s->v->xPos == s->v->xPos2 && s->v->yPos == s->v->yPos2) {
s->v->flags |= DIDNT_MOVE;
continue;
}
s->v->xPos2 = s->v->xPos;
s->v->yPos2 = s->v->yPos;
s->v->flags &= ~DIDNT_MOVE;
}
}
/**
* Erase all sprites in the given list.
*/
void SpritesMgr::eraseSprites(SpriteList &l) {
SpriteList::iterator iter;
for (iter = l.reverse_begin(); iter != l.end(); --iter) {
Sprite *s = *iter;
objsRestoreArea(s);
}
freeList(l);
}
/**
* Blit all sprites in the given list.
*/
void SpritesMgr::blitSprites(SpriteList& l) {
int hidden;
SpriteList::iterator iter;
for (iter = l.begin(); iter != l.end(); ++iter) {
Sprite *s = *iter;
objsSaveArea(s);
debugC(8, kDebugLevelSprites, "blitSprites(): s->v->entry = %d (prio %d)", s->v->entry, s->v->priority);
hidden = blitCel(s->xPos, s->yPos, s->v->priority, s->v->celData, s->v->viewData->agi256_2);
if (s->v->entry == 0) { // if ego, update f1
_vm->setflag(fEgoInvisible, hidden);
}
}
}
/*
* Public functions
*/
void SpritesMgr::commitUpdSprites() {
commitSprites(_sprUpd, true);
}
void SpritesMgr::commitNonupdSprites() {
commitSprites(_sprNonupd, true);
}
// check moves in both lists
void SpritesMgr::commitBoth() {
commitUpdSprites();
commitNonupdSprites();
}
/**
* Erase updating sprites.
* This function follows the list of all updating sprites and restores
* the visible and priority data of their background buffers back to
* the AGI screen.
*
* @see erase_nonupd_sprites()
* @see erase_both()
*/
void SpritesMgr::eraseUpdSprites() {
eraseSprites(_sprUpd);
}
/**
* Erase non-updating sprites.
* This function follows the list of all non-updating sprites and restores
* the visible and priority data of their background buffers back to
* the AGI screen.
*
* @see erase_upd_sprites()
* @see erase_both()
*/
void SpritesMgr::eraseNonupdSprites() {
eraseSprites(_sprNonupd);
}
/**
* Erase all sprites.
* This function follows the lists of all updating and non-updating
* sprites and restores the visible and priority data of their background
* buffers back to the AGI screen.
*
* @see erase_upd_sprites()
* @see erase_nonupd_sprites()
*/
void SpritesMgr::eraseBoth() {
eraseUpdSprites();
eraseNonupdSprites();
}
/**
* Blit updating sprites.
* This function follows the list of all updating sprites and blits
* them on the AGI screen.
*
* @see blit_nonupd_sprites()
* @see blit_both()
*/
void SpritesMgr::blitUpdSprites() {
debugC(7, kDebugLevelSprites, "blitUpdSprites()");
buildUpdBlitlist();
blitSprites(_sprUpd);
}
/**
* Blit non-updating sprites.
* This function follows the list of all non-updating sprites and blits
* them on the AGI screen.
*
* @see blit_upd_sprites()
* @see blit_both()
*/
void SpritesMgr::blitNonupdSprites() {
debugC(7, kDebugLevelSprites, "blitNonupdSprites()");
buildNonupdBlitlist();
blitSprites(_sprNonupd);
}
/**
* Blit all sprites.
* This function follows the lists of all updating and non-updating
* sprites and blits them on the AGI screen.
*
* @see blit_upd_sprites()
* @see blit_nonupd_sprites()
*/
void SpritesMgr::blitBoth() {
blitNonupdSprites();
blitUpdSprites();
}
/**
* Add view to picture.
* This function is used to implement the add.to.pic AGI command. It
* copies the specified cel from a view resource on the current picture.
* This cel is not a sprite, it can't be moved or removed.
* @param view number of view resource
* @param loop number of loop in the specified view resource
* @param cel number of cel in the specified loop
* @param x x coordinate to place the view
* @param y y coordinate to place the view
* @param pri priority to use
* @param mar if < 4, create a margin around the the base of the cel
*/
void SpritesMgr::addToPic(int view, int loop, int cel, int x, int y, int pri, int mar) {
ViewCel *c = NULL;
int x1, y1, x2, y2, y3;
uint8 *p1, *p2;
debugC(3, kDebugLevelSprites, "addToPic(view=%d, loop=%d, cel=%d, x=%d, y=%d, pri=%d, mar=%d)", view, loop, cel, x, y, pri, mar);
_vm->recordImageStackCall(ADD_VIEW, view, loop, cel, x, y, pri, mar);
// Was hardcoded to 8, changed to pri_table[y] to fix Gold
// Rush (see Sarien bug #587558)
if (pri == 0)
pri = _vm->_game.priTable[y];
c = &_vm->_game.views[view].loop[loop].cel[cel];
x1 = x;
y1 = y - c->height + 1;
x2 = x + c->width - 1;
y2 = y;
if (x1 < 0) {
x2 -= x1;
x1 = 0;
}
if (y1 < 0) {
y2 -= y1;
y1 = 0;
}
if (x2 >= _WIDTH)
x2 = _WIDTH - 1;
if (y2 >= _HEIGHT)
y2 = _HEIGHT - 1;
eraseBoth();
debugC(4, kDebugLevelSprites, "blitCel(%d, %d, %d, c)", x, y, pri);
blitCel(x1, y1, pri, c, _vm->_game.views[view].agi256_2);
// If margin is 0, 1, 2, or 3, the base of the cel is
// surrounded with a rectangle of the corresponding priority.
// If margin >= 4, this extra margin is not shown.
if (mar < 4) {
// add rectangle around object, don't clobber control
// info in priority data. The box extends to the end of
// its priority band!
y3 = (y2 / 12) * 12;
// SQ1 needs +1 (see Sarien bug #810331)
if (_vm->getGameID() == GID_SQ1)
y3++;
// don't let box extend below y.
if (y3 > y2) y3 = y2;
p1 = &_vm->_game.sbuf16c[x1 + y3 * _WIDTH];
p2 = &_vm->_game.sbuf16c[x2 + y3 * _WIDTH];
for (y = y3; y <= y2; y++) {
if ((*p1 >> 4) >= 4)
*p1 = (mar << 4) | (*p1 & 0x0f);
if ((*p2 >> 4) >= 4)
*p2 = (mar << 4) | (*p2 & 0x0f);
p1 += _WIDTH;
p2 += _WIDTH;
}
debugC(4, kDebugLevelSprites, "pri box: %d %d %d %d (%d)", x1, y3, x2, y2, mar);
p1 = &_vm->_game.sbuf16c[x1 + y3 * _WIDTH];
p2 = &_vm->_game.sbuf16c[x1 + y2 * _WIDTH];
for (x = x1; x <= x2; x++) {
if ((*p1 >> 4) >= 4)
*p1 = (mar << 4) | (*p1 & 0x0f);
if ((*p2 >> 4) >= 4)
*p2 = (mar << 4) | (*p2 & 0x0f);
p1++;
p2++;
}
}
blitBoth();
commitBlock(x1, y1, x2, y2, true);
}
/**
* Show object and description
* This function shows an object from the player's inventory, displaying
* a message box with the object description.
* @param n Number of the object to show
*/
void SpritesMgr::showObj(int n) {
ViewCel *c;
Sprite s;
int x1, y1, x2, y2;
_vm->agiLoadResource(rVIEW, n);
if (!(c = &_vm->_game.views[n].loop[0].cel[0]))
return;
x1 = (_WIDTH - c->width) / 2;
y1 = 112;
x2 = x1 + c->width - 1;
y2 = y1 + c->height - 1;
s.xPos = x1;
s.yPos = y1;
s.xSize = c->width;
s.ySize = c->height;
s.buffer = (uint8 *)malloc(s.xSize * s.ySize);
objsSaveArea(&s);
blitCel(x1, y1, 15, c, _vm->_game.views[n].agi256_2);
commitBlock(x1, y1, x2, y2, true);
_vm->messageBox(_vm->_game.views[n].descr);
objsRestoreArea(&s);
commitBlock(x1, y1, x2, y2, true);
free(s.buffer);
}
void SpritesMgr::commitBlock(int x1, int y1, int x2, int y2, bool immediate) {
int i, w, offset;
uint8 *q;
if (!_vm->_game.pictureShown)
return;
x1 = CLIP(x1, 0, _WIDTH - 1);
x2 = CLIP(x2, 0, _WIDTH - 1);
y1 = CLIP(y1, 0, _HEIGHT - 1);
y2 = CLIP(y2, 0, _HEIGHT - 1);
debugC(7, kDebugLevelSprites, "commitBlock(%d, %d, %d, %d)", x1, y1, x2, y2);
w = x2 - x1 + 1;
q = &_vm->_game.sbuf16c[x1 + _WIDTH * y1];
offset = _vm->_game.lineMinPrint * CHAR_LINES;
for (i = y1; i <= y2; i++) {
_gfx->putPixelsA(x1, i + offset, w, q);
q += _WIDTH;
}
_gfx->flushBlockA(x1, y1 + offset, x2, y2 + offset);
if (immediate)
_gfx->doUpdate();
}
SpritesMgr::SpritesMgr(AgiEngine *agi, GfxMgr *gfx) {
_vm = agi;
_gfx = gfx;
_spritePool = (uint8 *)malloc(POOL_SIZE);
_poolTop = _spritePool;
}
SpritesMgr::~SpritesMgr() {
free(_spritePool);
}
} // End of namespace Agi