scummvm/engines/agi/view.cpp
2021-12-26 18:48:43 +01:00

693 lines
20 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "agi/agi.h"
#include "agi/graphics.h"
#include "agi/sprite.h"
namespace Agi {
void AgiEngine::updateView(ScreenObjEntry *screenObj) {
int16 celNr, lastCelNr;
if (screenObj->flags & fDontupdate) {
screenObj->flags &= ~fDontupdate;
return;
}
celNr = screenObj->currentCelNr;
lastCelNr = screenObj->celCount - 1;
switch (screenObj->cycle) {
case kCycleNormal:
celNr++;
if (celNr > lastCelNr)
celNr = 0;
break;
case kCycleEndOfLoop:
if (celNr < lastCelNr) {
debugC(5, kDebugLevelResources, "cel %d (last = %d)", celNr + 1, lastCelNr);
if (++celNr != lastCelNr)
break;
}
setFlag(screenObj->loop_flag, true);
screenObj->flags &= ~fCycling;
screenObj->direction = 0;
screenObj->cycle = kCycleNormal;
break;
case kCycleRevLoop:
if (celNr) {
celNr--;
if (celNr)
break;
}
setFlag(screenObj->loop_flag, true);
screenObj->flags &= ~fCycling;
screenObj->direction = 0;
screenObj->cycle = kCycleNormal;
break;
case kCycleReverse:
if (celNr == 0) {
celNr = lastCelNr;
} else {
celNr--;
}
break;
default:
break;
}
setCel(screenObj, celNr);
}
/*
* Public functions
*/
/**
* Decode an AGI view resource.
* This function decodes the raw data of the specified AGI view resource
* and fills the corresponding views array element.
* @param n number of view resource to decode
*/
int AgiEngine::decodeView(byte *resourceData, uint16 resourceSize, int16 viewNr) {
AgiView *viewData = &_game.views[viewNr];
uint16 headerId = 0;
byte headerStepSize = 0;
byte headerCycleTime = 0;
byte headerLoopCount = 0;
uint16 headerDescriptionOffset = 0;
bool isAGI256Data = false;
AgiViewLoop *loopData = nullptr;
uint16 loopOffset = 0;
byte loopHeaderCelCount = 0;
AgiViewCel *celData = nullptr;
uint16 celOffset = 0;
byte celHeaderWidth = 0;
byte celHeaderHeight = 0;
byte celHeaderTransparencyMirror = 0;
byte celHeaderClearKey = 0;
bool celHeaderMirrored = false;
byte celHeaderMirrorLoop = 0;
byte *celCompressedData = nullptr;
uint16 celCompressedSize = 0;
debugC(5, kDebugLevelResources, "decode_view(%d)", viewNr);
if (resourceSize < 5)
error("unexpected end of view data for view %d", viewNr);
headerId = READ_LE_UINT16(resourceData);
if (getVersion() < 0x2000) {
headerStepSize = resourceData[0];
headerCycleTime = resourceData[1];
}
headerLoopCount = resourceData[2];
headerDescriptionOffset = READ_LE_UINT16(resourceData + 3);
if (headerId == 0xF00F)
isAGI256Data = true; // AGI 256-2 view detected, 256 color view
viewData->headerStepSize = headerStepSize;
viewData->headerCycleTime = headerCycleTime;
viewData->loopCount = headerLoopCount;
viewData->description = nullptr;
viewData->loop = nullptr;
if (headerDescriptionOffset) {
// Figure out length of description
uint16 descriptionPos = headerDescriptionOffset;
uint16 descriptionLen = 0;
while (descriptionPos < resourceSize) {
if (resourceData[descriptionPos] == 0)
break;
descriptionPos++;
descriptionLen++;
}
// Allocate memory for description
viewData->description = new byte[descriptionLen + 1];
// Copy description over
memcpy(viewData->description, resourceData + headerDescriptionOffset, descriptionLen);
viewData->description[descriptionLen] = 0; // set terminator
}
if (!viewData->loopCount) // no loops, exit now
return errOK;
// Check, if at least the loop-offsets are available
if (resourceSize < 5 + (headerLoopCount * 2))
error("unexpected end of view data for view %d", viewNr);
// Allocate space for loop-information
loopData = new AgiViewLoop[headerLoopCount];
viewData->loop = loopData;
for (int16 loopNr = 0; loopNr < headerLoopCount; loopNr++) {
loopOffset = READ_LE_UINT16(resourceData + 5 + (loopNr * 2));
// Check, if at least the loop-header is available
if (resourceSize < (loopOffset + 1))
error("unexpected end of view data for view %d", viewNr);
// loop-header:
// celCount:BYTE
// relativeCelOffset[0]:WORD
// relativeCelOffset[1]:WORD
// etc.
loopHeaderCelCount = resourceData[loopOffset];
loopData->celCount = loopHeaderCelCount;
loopData->cel = nullptr;
// Check, if at least the cel-offsets for current loop are available
if (resourceSize < (loopOffset + 1 + (loopHeaderCelCount * 2)))
error("unexpected end of view data for view %d", viewNr);
if (loopHeaderCelCount) {
// Allocate space for cel-information of current loop
celData = new AgiViewCel[loopHeaderCelCount];
loopData->cel = celData;
for (int16 celNr = 0; celNr < loopHeaderCelCount; celNr++) {
celOffset = READ_LE_UINT16(resourceData + loopOffset + 1 + (celNr * 2));
celOffset += loopOffset; // cel offset is relative to loop offset, so adjust accordingly
// Check, if at least the cel-header is available
if (resourceSize < (celOffset + 3))
error("unexpected end of view data for view %d", viewNr);
// cel-header:
// width:BYTE
// height:BYTE
// Transparency + Mirroring:BYTE
// celData follows
celHeaderWidth = resourceData[celOffset + 0];
celHeaderHeight = resourceData[celOffset + 1];
celHeaderTransparencyMirror = resourceData[celOffset + 2];
if (!isAGI256Data) {
// regular AGI view data
// Transparency + Mirroring byte is as follows:
// Bit 0-3 - clear key
// Bit 4-6 - original loop, that is not supposed to be mirrored in any case
// Bit 7 - apply mirroring
celHeaderClearKey = celHeaderTransparencyMirror & 0x0F; // bit 0-3 is the clear key
celHeaderMirrored = false;
if (celHeaderTransparencyMirror & 0x80) {
// mirror bit is set
celHeaderMirrorLoop = (celHeaderTransparencyMirror >> 4) & 0x07;
if (celHeaderMirrorLoop != loopNr) {
// only set to mirror'd in case we are not the original loop
celHeaderMirrored = true;
}
}
} else {
// AGI256-2 view data
celHeaderClearKey = celHeaderTransparencyMirror; // full 8 bits for clear key
celHeaderMirrored = false;
}
celData->width = celHeaderWidth;
celData->height = celHeaderHeight;
celData->clearKey = celHeaderClearKey;
celData->mirrored = celHeaderMirrored;
// Now decompress cel-data
if ((celHeaderWidth == 0) && (celHeaderHeight == 0))
error("view cel is 0x0");
celCompressedData = resourceData + celOffset + 3;
celCompressedSize = resourceSize - (celOffset + 3);
if (celCompressedSize == 0)
error("compressed size of loop within view %d is 0 bytes", viewNr);
if (!isAGI256Data) {
unpackViewCelData(celData, celCompressedData, celCompressedSize);
} else {
unpackViewCelDataAGI256(celData, celCompressedData, celCompressedSize);
}
celData++;
}
}
loopData++;
}
return errOK;
}
void AgiEngine::unpackViewCelData(AgiViewCel *celData, byte *compressedData, uint16 compressedSize) {
byte *rawBitmap = new byte[celData->width * celData->height];
int16 remainingHeight = celData->height;
int16 remainingWidth = celData->width;
bool isMirrored = celData->mirrored;
byte curByte;
byte curColor;
byte curChunkLen;
int16 adjustPreChangeSingle = 0;
int16 adjustAfterChangeSingle = +1;
celData->rawBitmap = rawBitmap;
if (isMirrored) {
adjustPreChangeSingle = -1;
adjustAfterChangeSingle = 0;
rawBitmap += celData->width;
}
while (remainingHeight) {
if (!compressedSize)
error("unexpected end of data, while unpacking AGI256 data");
curByte = *compressedData++;
compressedSize--;
if (curByte == 0) {
curColor = celData->clearKey;
curChunkLen = remainingWidth;
} else {
curColor = curByte >> 4;
curChunkLen = curByte & 0x0F;
if (curChunkLen > remainingWidth)
error("invalid chunk in view data");
}
switch (curChunkLen) {
case 0:
break;
case 1:
rawBitmap += adjustPreChangeSingle;
*rawBitmap = curColor;
rawBitmap += adjustAfterChangeSingle;
break;
default:
if (isMirrored)
rawBitmap -= curChunkLen;
memset(rawBitmap, curColor, curChunkLen);
if (!isMirrored)
rawBitmap += curChunkLen;
break;
}
remainingWidth -= curChunkLen;
if (curByte == 0) {
remainingWidth = celData->width;
remainingHeight--;
if (isMirrored)
rawBitmap += celData->width * 2;
}
}
// for CGA rendering, apply dithering
switch (_renderMode) {
case Common::kRenderCGA: {
uint16 totalPixels = celData->width * celData->height;
// dither clear key
celData->clearKey = _gfx->getCGAMixtureColor(celData->clearKey);
rawBitmap = celData->rawBitmap;
for (uint16 pixelNr = 0; pixelNr < totalPixels; pixelNr++) {
curColor = *rawBitmap;
*rawBitmap = _gfx->getCGAMixtureColor(curColor);
rawBitmap++;
}
break;
}
default:
break;
}
}
void AgiEngine::unpackViewCelDataAGI256(AgiViewCel *celData, byte *compressedData, uint16 compressedSize) {
byte *rawBitmap = new byte[celData->width * celData->height];
int16 remainingHeight = celData->height;
int16 remainingWidth = celData->width;
byte curByte;
celData->rawBitmap = rawBitmap;
while (remainingHeight) {
if (!compressedSize)
error("unexpected end of data, while unpacking AGI256 view");
curByte = *compressedData++;
compressedSize--;
if (curByte == 0) {
// Go to next vertical position
if (remainingWidth) {
// fill remaining bytes with clear key
memset(rawBitmap, celData->clearKey, remainingWidth);
rawBitmap += remainingWidth;
remainingWidth = 0;
}
} else {
if (!remainingWidth) {
error("broken view data, while unpacking AGI256 view");
break;
}
*rawBitmap = curByte;
rawBitmap++;
remainingWidth--;
}
if (curByte == 0) {
remainingWidth = celData->width;
remainingHeight--;
}
}
}
/**
* Unloads all data in a view resource
* @param n number of view resource
*/
void AgiEngine::unloadView(int16 viewNr) {
AgiView *viewData = &_game.views[viewNr];
debugC(5, kDebugLevelResources, "discard view %d", viewNr);
if (!(_game.dirView[viewNr].flags & RES_LOADED))
return;
// Rebuild sprite list, see Sarien bug #779302
_sprites->eraseSprites();
// free data
for (int16 loopNr = 0; loopNr < viewData->loopCount; loopNr++) {
AgiViewLoop *loopData = &viewData->loop[loopNr];
for (int16 celNr = 0; celNr < loopData->celCount; celNr++) {
AgiViewCel *celData = &loopData->cel[celNr];
delete[] celData->rawBitmap;
}
delete[] loopData->cel;
}
delete[] viewData->loop;
if (viewData->description)
delete[] viewData->description;
viewData->headerCycleTime = 0;
viewData->headerStepSize = 0;
viewData->description = nullptr;
viewData->loop = nullptr;
viewData->loopCount = 0;
// Mark this view as not loaded anymore
_game.dirView[viewNr].flags &= ~RES_LOADED;
_sprites->buildAllSpriteLists();
_sprites->drawAllSpriteLists();
}
/**
* Set a view table entry to use the specified view resource.
* @param screenObj pointer to screen object
* @param viewNr number of AGI view resource
*/
void AgiEngine::setView(ScreenObjEntry *screenObj, int16 viewNr) {
if (!(_game.dirView[viewNr].flags & RES_LOADED)) {
// View resource currently not loaded, this is probably a game bug
// Load the resource now to fix the issue, and give out a warning
// This happens in at least Larry 1 for Apple IIgs right after getting beaten up by taxi driver
// Original interpreter bombs out in this situation saying "view not loaded, Press ESC to quit"
warning("setView() called on screen object %d to use view %d, but view not loaded", screenObj->objectNr, viewNr);
warning("probably game script bug, trying to load view into memory");
if (agiLoadResource(RESOURCETYPE_VIEW, viewNr) != errOK) {
// loading failed, we better error() out now
error("setView() called to set view %d for screen object %d, which is not loaded atm and loading failed", viewNr, screenObj->objectNr);
return;
};
}
screenObj->viewResource = &_game.views[viewNr];
screenObj->currentViewNr = viewNr;
screenObj->loopCount = screenObj->viewResource->loopCount;
screenObj->viewReplaced = true;
if (getVersion() < 0x2000) {
screenObj->stepSize = screenObj->viewResource->headerStepSize;
screenObj->cycleTime = screenObj->viewResource->headerCycleTime;
screenObj->cycleTimeCount = 0;
}
if (screenObj->currentLoopNr >= screenObj->loopCount) {
setLoop(screenObj, 0);
} else {
setLoop(screenObj, screenObj->currentLoopNr);
}
}
/**
* Set a view table entry to use the specified loop of the current view.
* @param screenObj pointer to screen object
* @param loopNr number of loop
*/
void AgiEngine::setLoop(ScreenObjEntry *screenObj, int16 loopNr) {
if (!(_game.dirView[screenObj->currentViewNr].flags & RES_LOADED)) {
error("setLoop() called on screen object %d, which has no loaded view resource assigned to it", screenObj->objectNr);
return;
}
assert(screenObj->viewResource);
if (screenObj->loopCount == 0) {
warning("setLoop() called on screen object %d, which has no loops (view %d)", screenObj->objectNr, screenObj->currentViewNr);
return;
}
if (loopNr >= screenObj->loopCount) {
// requested loop not existent
// instead of error()ing out, we instead clip it
// At least required for possibly Manhunter 1 according to previous comment when leaving the arcade machine
// TODO: Check MH1
// TODO: This causes an issue in KQ1, when bowing to the king in room 53
// Ego will face away from the king, because the scripts set the loop first and then the view
// Loop is corrected by us, because at that time it's invalid. Was already present in 1.7.0
// We should probably script-patch it out.
int16 requestedLoopNr = loopNr;
loopNr = screenObj->loopCount - 1;
warning("Non-existent loop requested for screen object %d", screenObj->objectNr);
warning("view %d, requested loop %d -> clipped to loop %d", screenObj->currentViewNr, requestedLoopNr, loopNr);
}
AgiViewLoop *curViewLoop = &_game.views[screenObj->currentViewNr].loop[loopNr];
screenObj->currentLoopNr = loopNr;
screenObj->loopData = curViewLoop;
screenObj->celCount = curViewLoop->celCount;
if (screenObj->currentCelNr >= screenObj->celCount) {
setCel(screenObj, 0);
} else {
setCel(screenObj, screenObj->currentCelNr);
}
}
/**
* Set a view table entry to use the specified cel of the current loop.
* @param screenObj pointer to screen object
* @param celNr number of cel
*/
void AgiEngine::setCel(ScreenObjEntry *screenObj, int16 celNr) {
if (!(_game.dirView[screenObj->currentViewNr].flags & RES_LOADED)) {
error("setCel() called on screen object %d, which has no loaded view resource assigned to it", screenObj->objectNr);
return;
}
assert(screenObj->viewResource);
if (screenObj->loopCount == 0) {
warning("setLoop() called on screen object %d, which has no loops (view %d)", screenObj->objectNr, screenObj->currentViewNr);
return;
}
AgiViewLoop *curViewLoop = &_game.views[screenObj->currentViewNr].loop[screenObj->currentLoopNr];
// Added by Amit Vainsencher <amitv@subdimension.com> to prevent
// crash in KQ1 -- not in the Sierra interpreter
if (curViewLoop->celCount == 0) {
warning("setCel() called on screen object %d, which has no cels (view %d)", screenObj->objectNr, screenObj->currentViewNr);
return;
}
if (celNr >= screenObj->celCount) {
// requested cel not existent
// instead of error()ing out, we instead clip it
// At least required for King's Quest 3 on Apple IIgs - walking the planks death cutscene
// see bug #5832, which is a game bug!
int16 requestedCelNr = celNr;
celNr = screenObj->celCount - 1;
warning("Non-existent cel requested for screen object %d", screenObj->objectNr);
warning("view %d, loop %d, requested cel %d -> clipped to cel %d", screenObj->currentViewNr, screenObj->currentLoopNr, requestedCelNr, celNr);
}
screenObj->currentCelNr = celNr;
AgiViewCel *curViewCel;
curViewCel = &curViewLoop->cel[celNr];
screenObj->celData = curViewCel;
screenObj->xSize = curViewCel->width;
screenObj->ySize = curViewCel->height;
// If position isn't appropriate, update it accordingly
clipViewCoordinates(screenObj);
}
/**
* Restrict view table entry's position so it stays wholly inside the screen.
* Also take horizon into account when clipping if not set to ignore it.
* @param v pointer to view table entry
*/
void AgiEngine::clipViewCoordinates(ScreenObjEntry *screenObj) {
if (screenObj->xPos + screenObj->xSize > SCRIPT_WIDTH) {
screenObj->flags |= fUpdatePos;
screenObj->xPos = SCRIPT_WIDTH - screenObj->xSize;
}
if (screenObj->yPos - screenObj->ySize + 1 < 0) {
screenObj->flags |= fUpdatePos;
screenObj->yPos = screenObj->ySize - 1;
}
if (screenObj->yPos <= _game.horizon && (~screenObj->flags & fIgnoreHorizon)) {
screenObj->flags |= fUpdatePos;
screenObj->yPos = _game.horizon + 1;
}
if (getVersion() < 0x2000) {
screenObj->flags |= fDontupdate;
}
}
/**
* Set the view table entry as updating.
* @param v pointer to view table entry
*/
void AgiEngine::startUpdate(ScreenObjEntry *v) {
if (~v->flags & fUpdate) {
_sprites->eraseSprites();
v->flags |= fUpdate;
_sprites->buildAllSpriteLists();
_sprites->drawAllSpriteLists();
}
}
/**
* Set the view table entry as non-updating.
* @param v pointer to view table entry
*/
void AgiEngine::stopUpdate(ScreenObjEntry *viewPtr) {
if (viewPtr->flags & fUpdate) {
_sprites->eraseSprites();
viewPtr->flags &= ~fUpdate;
_sprites->buildAllSpriteLists();
_sprites->drawAllSpriteLists();
}
}
// loops to use according to direction and number of loops in
// the view resource
static int loopTable2[] = {
0x04, 0x04, 0x00, 0x00, 0x00, 0x04, 0x01, 0x01, 0x01
};
static int loopTable4[] = {
0x04, 0x03, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01, 0x01
};
/**
* Update view table entries.
* This function is called at the end of each interpreter cycle
* to update the view table entries and blit the sprites.
*/
void AgiEngine::updateScreenObjTable() {
ScreenObjEntry *screenObj;
int16 changeCount, loopNr;
changeCount = 0;
for (screenObj = _game.screenObjTable; screenObj < &_game.screenObjTable[SCREENOBJECTS_MAX]; screenObj++) {
if ((screenObj->flags & (fAnimated | fUpdate | fDrawn)) != (fAnimated | fUpdate | fDrawn)) {
continue;
}
changeCount++;
loopNr = 4;
if (!(screenObj->flags & fFixLoop)) {
switch (screenObj->loopCount) {
case 2:
case 3:
loopNr = loopTable2[screenObj->direction];
break;
case 4:
loopNr = loopTable4[screenObj->direction];
break;
default:
// for KQ4
if (getVersion() == 0x3086 || getGameID() == GID_KQ4)
loopNr = loopTable4[screenObj->direction];
break;
}
}
// AGI 2.272 (ddp, xmas) doesn't test step_time_count!
if (loopNr != 4 && loopNr != screenObj->currentLoopNr) {
if (getVersion() <= 0x2272 || screenObj->stepTimeCount == 1) {
setLoop(screenObj, loopNr);
}
}
if (screenObj->flags & fCycling) {
if (screenObj->cycleTimeCount) {
screenObj->cycleTimeCount--;
if (screenObj->cycleTimeCount == 0) {
updateView(screenObj);
screenObj->cycleTimeCount = screenObj->cycleTime;
}
}
}
}
if (changeCount) {
_sprites->eraseRegularSprites();
updatePosition();
_sprites->buildRegularSpriteList();
_sprites->drawRegularSpriteList();
_sprites->showRegularSpriteList();
_game.screenObjTable[SCREENOBJECTS_EGO_ENTRY].flags &= ~(fOnWater | fOnLand);
}
}
bool AgiEngine::isEgoView(const ScreenObjEntry *screenObj) {
return screenObj == _game.screenObjTable;
}
} // End of namespace Agi