scummvm/engines/cine/gfx.cpp
Kari Salminen e82b4faeed CINE: Fix overlay rendering with NULL source mask
In French Amiga Future Wars from bug #10643 when walking left from the
scene with the open manhole cover a call to renderOverlay with a
type 0 (color sprite) overlay happens. The source mask for the overlay
is NULL and a memcpy using a NULL pointer as source is initiated.
This results in a memory access violation.

This fixes that memory access violation by simply setting the
destination mask to all zeroes when the source mask is NULL.
Seems to work fine at least in this case.

Fixes bug #10643.
2020-08-31 09:26:36 +02:00

2265 lines
60 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.
*
*/
#include "cine/cine.h"
#include "cine/bg.h"
#include "cine/bg_list.h"
#include "cine/various.h"
#include "cine/pal.h"
#include "common/config-manager.h"
#include "common/endian.h"
#include "common/events.h"
#include "common/str.h"
#include "common/system.h"
#include "common/textconsole.h"
#include "graphics/cursorman.h"
#include "graphics/primitives.h"
namespace Cine {
byte *collisionPage;
FWRenderer *renderer = NULL;
#define DEFAULT_MESSAGE_BG 1
#define DEFAULT_CMD_Y 185
// Constants related to kLowPalFormat
#define kLowPalBytesPerColor 2
#define kLowPalNumColors 16
#define kLowPalNumBytes ((kLowPalNumColors) * (kLowPalBytesPerColor))
/** Low resolution (9-bit) color format used in Cine's 16-color modes. */
#define kLowPalFormat Graphics::PixelFormat(kLowPalBytesPerColor, 3, 3, 3, 0, 8, 4, 0, 0)
// Constants related to kHighPalFormat
#define kHighPalBytesPerColor 3
#define kHighPalNumColors 256
#define kHighPalNumBytes ((kHighPalNumColors) * (kHighPalBytesPerColor))
/** High resolution (24-bit) color format used in Cine's 256-color modes. */
#define kHighPalFormat Graphics::PixelFormat(kHighPalBytesPerColor, 8, 8, 8, 0, 0, 8, 16, 0)
static const byte mouseCursorNormal[] = {
0x00, 0x00, 0x40, 0x00, 0x60, 0x00, 0x70, 0x00,
0x78, 0x00, 0x7C, 0x00, 0x7E, 0x00, 0x7F, 0x00,
0x7F, 0x80, 0x7C, 0x00, 0x6C, 0x00, 0x46, 0x00,
0x06, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00,
0xC0, 0x00, 0xE0, 0x00, 0xF0, 0x00, 0xF8, 0x00,
0xFC, 0x00, 0xFE, 0x00, 0xFF, 0x00, 0xFF, 0x80,
0xFF, 0xC0, 0xFF, 0xC0, 0xFE, 0x00, 0xFF, 0x00,
0xCF, 0x00, 0x07, 0x80, 0x07, 0x80, 0x03, 0x80
};
static const byte mouseCursorDisk[] = {
0x7F, 0xFC, 0x9F, 0x12, 0x9F, 0x12, 0x9F, 0x12,
0x9F, 0x12, 0x9F, 0xE2, 0x80, 0x02, 0x9F, 0xF2,
0xA0, 0x0A, 0xA0, 0x0A, 0xA0, 0x0A, 0xA0, 0x0A,
0xA0, 0x0A, 0xA0, 0x0A, 0x7F, 0xFC, 0x00, 0x00,
0x7F, 0xFC, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE,
0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE,
0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE,
0xFF, 0xFE, 0xFF, 0xFE, 0x7F, 0xFC, 0x00, 0x00
};
static const byte mouseCursorCross[] = {
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7C, 0x7C,
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00,
0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80,
0x03, 0x80, 0x03, 0x80, 0xFF, 0xFE, 0xFE, 0xFE,
0xFF, 0xFE, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80,
0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x00, 0x00
};
static const struct MouseCursor {
int hotspotX;
int hotspotY;
const byte *bitmap;
} mouseCursors[] = {
{ 1, 1, mouseCursorNormal },
{ 0, 0, mouseCursorDisk },
{ 7, 7, mouseCursorCross }
};
static const byte cursorPalette[] = {
0, 0, 0, 0xff,
0xff, 0xff, 0xff, 0xff
};
void plotPoint(int x, int y, int color, void *data) {
byte *output = (byte *)data;
if (x >= 0 && x < 320 && y >= 0 && y < 200) {
output[y * 320 + x] = (byte)color;
}
}
/**
* Initialize renderer
*/
FWRenderer::FWRenderer() : _savedBackBuffers(), _background(NULL), _backupPal(), _cmd(""),
_messageBg(DEFAULT_MESSAGE_BG), _cmdY(DEFAULT_CMD_Y), _backBuffer(new byte[_screenSize]),
_activePal(), _changePal(0), _showCollisionPage(false), _fadeToBlackLastCalledMs(0) {
assert(_backBuffer);
memset(_backBuffer, 0, _screenSize);
memset(_bgName, 0, sizeof(_bgName));
}
void FWRenderer::removeSavedBackBuffers() {
for (int i = 0; i < ARRAYSIZE(_savedBackBuffers); i++) {
if (_savedBackBuffers[i]) {
delete[] _savedBackBuffers[i];
_savedBackBuffers[i] = nullptr;
}
}
}
/**
* Destroy renderer
*/
FWRenderer::~FWRenderer() {
delete[] _background;
delete[] _backBuffer;
removeSavedBackBuffers();
clearMenuStack();
}
bool FWRenderer::initialize() {
_backupPal = _activePal = Palette(kLowPalFormat, kLowPalNumColors);
return true;
}
/**
* Reset renderer state
*/
void FWRenderer::clear() {
delete[] _background;
_background = NULL;
_backupPal.clear();
_activePal.clear();
memset(_backBuffer, 0, _screenSize);
removeSavedBackBuffers();
_cmd = "";
_cmdY = DEFAULT_CMD_Y;
_messageBg = DEFAULT_MESSAGE_BG;
_changePal = 0;
_showCollisionPage = false;
}
const Cine::Palette& FWRenderer::getFadeInSourcePalette() {
return _backupPal;
}
/**
* Draw 1bpp sprite using selected color
* @param obj Object info
* @param fillColor Sprite color
*/
void FWRenderer::fillSprite(const ObjectStruct &obj, uint8 color) {
const byte *data = g_cine->_animDataTable[obj.frame].data();
int x, y, width, height;
x = obj.x;
y = obj.y;
width = g_cine->_animDataTable[obj.frame]._realWidth;
height = g_cine->_animDataTable[obj.frame]._height;
gfxFillSprite(data, width, height, _backBuffer, x, y, color);
}
/**
* Draw 1bpp sprite using selected color on background
* @param obj Object info
* @param fillColor Sprite color
*/
void FWRenderer::incrustMask(const BGIncrust &incrust, uint8 color) {
const ObjectStruct &obj = g_cine->_objectTable[incrust.objIdx];
const byte *data = g_cine->_animDataTable[obj.frame].data();
int x, y, width, height;
x = obj.x;
y = obj.y;
width = g_cine->_animDataTable[obj.frame]._realWidth;
height = g_cine->_animDataTable[obj.frame]._height;
gfxFillSprite(data, width, height, _background, x, y, color);
}
/**
* Draw color sprite using with external mask
* @param obj Object info
* @param mask External mask
*/
void FWRenderer::drawMaskedSprite(const ObjectStruct &obj, const byte *mask) {
const byte *data = g_cine->_animDataTable[obj.frame].data();
int x, y, width, height;
x = obj.x;
y = obj.y;
width = g_cine->_animDataTable[obj.frame]._realWidth;
height = g_cine->_animDataTable[obj.frame]._height;
assert(mask);
drawSpriteRaw(data, mask, width, height, _backBuffer, x, y);
}
/**
* Draw color sprite
* @param obj Object info
*/
void FWRenderer::drawSprite(const ObjectStruct &obj) {
const byte *mask = g_cine->_animDataTable[obj.frame].mask();
drawMaskedSprite(obj, mask);
}
/**
* Draw color sprite on background
* @param obj Object info
*/
void FWRenderer::incrustSprite(const BGIncrust &incrust) {
const ObjectStruct &obj = g_cine->_objectTable[incrust.objIdx];
const byte *data = g_cine->_animDataTable[obj.frame].data();
const byte *mask = g_cine->_animDataTable[obj.frame].mask();
int x, y, width, height;
x = obj.x;
y = obj.y;
width = g_cine->_animDataTable[obj.frame]._realWidth;
height = g_cine->_animDataTable[obj.frame]._height;
// There was an assert(mask) here before but it made savegame loading
// in Future Wars sometimes fail the assertion (e.g. see bug #2055912).
// Not drawing sprites that have no mask seems to work, but not sure
// if this is really a correct way to fix this.
if (mask) {
drawSpriteRaw(data, mask, width, height, _background, x, y);
} else { // mask == NULL
warning("FWRenderer::incrustSprite: Skipping maskless sprite (frame=%d)", obj.frame);
}
}
/**
* Draw command box on screen
*/
void FWRenderer::drawCommand() {
if (disableSystemMenu == 0) {
int x = 10, y = _cmdY;
drawPlainBox(x, y, 301, 11, 0);
drawBorder(x - 1, y - 1, 302, 12, 2);
x += 2;
y += 2;
for (unsigned int i = 0; i < _cmd.size(); i++) {
x = drawChar(_cmd[i], x, y);
}
}
}
void FWRenderer::drawString(const char *string, byte param) {
int width;
byte minBrightnessColorIndex = 4;
bool useEnsureContrast = true;
if (useEnsureContrast && g_cine->getGameType() == Cine::GType_OS) {
bool paletteChanged = _activePal.ensureContrast(minBrightnessColorIndex);
if (paletteChanged) {
clearBackBuffer();
setPalette();
}
}
// Both Future Wars and Operation Stealth 16 color PC versions do this
int y = 80;
if (param == 1) {
y = 20;
} else if (param == 2) {
y = 140;
}
width = getStringWidth(string);
if (width == 0) {
return;
}
width = MIN<int>(width + 20, 300);
drawMessage(string, (320 - width) / 2, y, width, minBrightnessColorIndex);
blit();
}
/**
* Draw message in a box
* @param str Message to draw
* @param x Top left message box corner coordinate
* @param y Top left message box corner coordinate
* @param width Message box width
* @param color Message box background color (Or if negative draws only the text)
* @note Negative colors are used in Operation Stealth's timed cutscenes
* (e.g. when first meeting The Movement for the Liberation of Santa Paragua).
*/
void FWRenderer::drawMessage(const char *str, int x, int y, int width, int color) {
int i, tx, ty, tw;
int line = 0, words = 0, cw = 0;
int space = 0, extraSpace = 0;
if (color >= 0) {
if (useTransparentDialogBoxes())
drawTransparentBox(x, y, width, 4);
else
drawPlainBox(x, y, width, 4, color);
}
tx = x + 4;
ty = str[0] ? y - 5 : y + 4;
tw = width - 8;
for (i = 0; str[i]; i++, line--) {
// Fit line of text into textbox
if (!line) {
while (str[i] == ' ')
i++;
line = fitLine(str + i, tw, words, cw);
if (str[i + line] != '\0' && str[i + line] != 0x7C && words) {
space = (tw - cw) / words;
extraSpace = (tw - cw) % words;
} else {
space = 5;
extraSpace = 0;
}
ty += 9;
if (color >= 0) {
if (useTransparentDialogBoxes())
drawTransparentBox(x, ty, width, 9);
else
drawPlainBox(x, ty, width, 9, color);
}
tx = x + 4;
}
// draw characters
if (str[i] == ' ') {
tx += space + extraSpace;
if (extraSpace) {
extraSpace = 0;
}
} else {
tx = drawChar(str[i], tx, ty);
}
}
ty += 9;
if (color >= 0) {
if (useTransparentDialogBoxes())
drawTransparentBox(x, ty, width, 4);
else
drawPlainBox(x, ty, width, 4, color);
drawDoubleBorder(x, y, width, ty - y + 4, (useTransparentDialogBoxes() ? transparentDialogBoxStartColor() : 0) + 2);
}
}
/**
* Draw rectangle on screen
* @param x Top left corner coordinate
* @param y Top left corner coordinate
* @param width Rectangle width (Negative values draw the box horizontally flipped)
* @param height Rectangle height (Negative values draw the box vertically flipped)
* @param color Fill color
* @note An on-screen rectangle's drawn width is always at least one.
* @note An on-screen rectangle's drawn height is always at least one.
*/
void FWRenderer::drawPlainBox(int x, int y, int width, int height, byte color) {
// Handle horizontally flipped boxes
if (width < 0) {
width = ABS(width);
x -= width;
}
// Handle vertically flipped boxes
if (height < 0) {
height = ABS(height);
y -= height;
}
// Clip the rectangle to screen dimensions
Common::Rect boxRect(x, y, x + width, y + height);
Common::Rect screenRect(320, 200);
boxRect.clip(screenRect);
byte *dest = _backBuffer + boxRect.top * 320 + boxRect.left;
for (int i = 0; i < boxRect.height(); i++) {
memset(dest + i * 320, color, boxRect.width());
}
}
bool FWRenderer::useTransparentDialogBoxes() {
return _activePal.colorCount() == 16 &&
((g_cine->getPlatform() == Common::kPlatformAmiga) ||
ConfMan.getBool("transparentdialogboxes"));
}
byte FWRenderer::transparentDialogBoxStartColor() {
return 16;
}
void FWRenderer::drawTransparentBox(int x, int y, int width, int height) {
byte startColor = transparentDialogBoxStartColor();
// Handle horizontally flipped boxes
if (width < 0) {
width = ABS(width);
x -= width;
}
// Handle vertically flipped boxes
if (height < 0) {
height = ABS(height);
y -= height;
}
// Clip the rectangle to screen dimensions
Common::Rect boxRect(x, y, x + width, y + height);
Common::Rect screenRect(320, 200);
boxRect.clip(screenRect);
byte *dest = _backBuffer + boxRect.top * 320 + boxRect.left;
const int lineAdd = 320 - boxRect.width();
for (int i = 0; i < boxRect.height(); ++i) {
for (int j = 0; j < boxRect.width(); ++j, ++dest) {
if (*dest < startColor)
*dest += startColor;
}
dest += lineAdd;
}
}
/**
* Draw empty rectangle
* @param x Top left corner coordinate
* @param y Top left corner coordinate
* @param width Rectangle width
* @param height Rectangle height
* @param color Line color
*/
void FWRenderer::drawBorder(int x, int y, int width, int height, byte color) {
drawLine(x, y, width, 1, color);
drawLine(x, y + height, width, 1, color);
drawLine(x, y, 1, height, color);
drawLine(x + width, y, 1, height + 1, color);
}
/**
* Draw empty 2 color rectangle (inner line color is black)
* @param x Top left corner coordinate
* @param y Top left corner coordinate
* @param width Rectangle width
* @param height Rectangle height
* @param color Outter line color
*/
void FWRenderer::drawDoubleBorder(int x, int y, int width, int height, byte color) {
drawBorder(x + 1, y + 1, width - 2, height - 2, 0);
drawBorder(x, y, width, height, color);
}
/**
* Draw text character on screen
* @param character Character to draw
* @param x Character coordinate
* @param y Character coordinate
*/
int FWRenderer::drawChar(char character, int x, int y) {
int width;
if (character == ' ') {
x += 5;
} else if ((width = g_cine->_textHandler.fontParamTable[(unsigned char)character].characterWidth)) {
int idx = g_cine->_textHandler.fontParamTable[(unsigned char)character].characterIdx;
drawSpriteRaw(g_cine->_textHandler.textTable[idx][FONT_DATA], g_cine->_textHandler.textTable[idx][FONT_MASK], FONT_WIDTH, FONT_HEIGHT, _backBuffer, x, y);
x += width + 1;
}
return x;
}
/**
* Clears the character glyph to black
* This function is called "undrawChar", because the original only applies
* this drawing after the original glyph has been drawn.
* Possible TODO: Find a better name.
* @param character Character to undraw
* @param x Character coordinate
* @param y Character coordinate
*/
int FWRenderer::undrawChar(char character, int x, int y) {
int width;
if (character == ' ') {
x += 5;
} else if ((width = g_cine->_textHandler.fontParamTable[(unsigned char)character].characterWidth)) {
int idx = g_cine->_textHandler.fontParamTable[(unsigned char)character].characterIdx;
const byte *sprite = g_cine->_textHandler.textTable[idx][FONT_DATA];
for (uint i = 0; i < FONT_HEIGHT; ++i) {
byte *dst = _backBuffer + (y + i) * 320 + x;
for (uint j = 0; j < FONT_WIDTH; ++j, ++dst) {
// The original does this based on whether bit 1 of the pixel
// is set. Since that's the only bit ever set in (FW) this
// check should be fine.
// TODO: Check how Operation Stealth Amiga works
if (*sprite++) {
*dst = 0;
}
}
}
x += width + 1;
}
return x;
}
int FWRenderer::getStringWidth(const char *str) {
int padding = (g_cine->getGameType() == Cine::GType_OS) ? 2 : 1;
const char *p = str;
int width = 0;
int maxWidth = 0;
while (*p) {
unsigned char currChar = (unsigned char)*p;
if (currChar == '|') {
maxWidth = MAX<int>(width, maxWidth);
width = 0;
} else if (currChar == ' ')
width += 5;
else
width += g_cine->_textHandler.fontParamTable[currChar].characterWidth + padding;
p++;
}
maxWidth = MAX<int>(width, maxWidth);
return width;
}
/**
* Draw Line
* @param x Line end coordinate
* @param y Line end coordinate
* @param width Horizontal line length
* @param height Vertical line length
* @param color Line color
* @note Either width or height must be equal to 1
*/
void FWRenderer::drawLine(int x, int y, int width, int height, byte color) {
// this line is a special case of rectangle ;-)
drawPlainBox(x, y, width, height, color);
}
/**
* Hide invisible parts of the sprite
* @param[in,out] mask Mask to be updated
* @param it Overlay info from overlayList
*/
void FWRenderer::remaskSprite(byte *mask, Common::List<overlay>::iterator it) {
AnimData &sprite = g_cine->_animDataTable[g_cine->_objectTable[it->objIdx].frame];
int x, y, width, height, idx;
int mx, my, mw, mh;
x = g_cine->_objectTable[it->objIdx].x;
y = g_cine->_objectTable[it->objIdx].y;
width = sprite._realWidth;
height = sprite._height;
for (++it; it != g_cine->_overlayList.end(); ++it) {
if (it->type != 5) {
continue;
}
idx = ABS(g_cine->_objectTable[it->objIdx].frame);
mx = g_cine->_objectTable[it->objIdx].x;
my = g_cine->_objectTable[it->objIdx].y;
mw = g_cine->_animDataTable[idx]._realWidth;
mh = g_cine->_animDataTable[idx]._height;
gfxUpdateSpriteMask(mask, x, y, width, height, g_cine->_animDataTable[idx].data(), mx, my, mw, mh);
}
}
/**
* Draw background to backbuffer
*/
void FWRenderer::drawBackground() {
assert(_background);
memcpy(_backBuffer, _background, _screenSize);
}
void FWRenderer::clearBackBuffer() {
if (_backBuffer) {
memset(_backBuffer, 0, _screenSize);
}
}
/**
* Draw one overlay
* @param it Overlay info
*/
void FWRenderer::renderOverlay(const Common::List<overlay>::iterator &it) {
int idx, len, width;
ObjectStruct *obj;
AnimData *sprite;
byte *mask;
switch (it->type) {
// color sprite
case 0:
if (g_cine->_objectTable[it->objIdx].frame < 0) {
return;
}
sprite = &g_cine->_animDataTable[g_cine->_objectTable[it->objIdx].frame];
len = sprite->_realWidth * sprite->_height;
mask = new byte[len];
if (sprite->mask() != NULL) {
memcpy(mask, sprite->mask(), len);
} else {
// This case happens in French Amiga Future Wars (Bug #10643) when
// walking left from the scene with the open manhole cover. This
// seems to work fine at least in this case.
memset(mask, 0, len);
}
remaskSprite(mask, it);
drawMaskedSprite(g_cine->_objectTable[it->objIdx], mask);
delete[] mask;
break;
// game message
case 2:
if (it->objIdx >= g_cine->_messageTable.size()) {
return;
}
_messageLen += g_cine->_messageTable[it->objIdx].size();
drawMessage(g_cine->_messageTable[it->objIdx].c_str(), it->x, it->y, it->width, it->color);
waitForPlayerClick = 1;
break;
// action failure message
case 3:
idx = it->objIdx * 4 + g_cine->_rnd.getRandomNumber(3);
len = strlen(failureMessages[idx]);
_messageLen += len;
width = 6 * len + 20;
width = width > 300 ? 300 : width;
drawMessage(failureMessages[idx], (320 - width) / 2, 80, width, 4);
waitForPlayerClick = 1;
break;
// bitmap
case 4:
assert(it->objIdx < NUM_MAX_OBJECT);
obj = &g_cine->_objectTable[it->objIdx];
if (obj->frame < 0) {
return;
}
if (!g_cine->_animDataTable[obj->frame].data()) {
return;
}
fillSprite(*obj);
break;
default:
break;
}
}
/**
* Draw overlays
*/
void FWRenderer::drawOverlays() {
// WORKAROUND: Show player behind stairs by moving him behind everything
// in the scene right after leaving Dr. Why's control room.
if (g_cine->getGameType() == Cine::GType_OS &&
g_cine->_overlayList.size() >= 2 &&
g_cine->_overlayList.back().objIdx == 1 &&
g_cine->_objectTable.size() >= 2 &&
g_cine->_objectTable[1].x == 231 &&
g_cine->_objectTable[1].y >= 142 &&
scumm_stricmp(renderer->getBgName(), "56VIDE.PI1") == 0) {
Cine::overlay playerOverlay = g_cine->_overlayList.back();
g_cine->_overlayList.pop_back();
g_cine->_overlayList.push_front(playerOverlay);
}
Common::List<overlay>::iterator it;
for (it = g_cine->_overlayList.begin(); it != g_cine->_overlayList.end(); ++it) {
renderOverlay(it);
}
}
/**
* Draw another frame
*/
void FWRenderer::drawFrame(bool wait) {
drawBackground();
drawOverlays();
if (!_cmd.empty()) {
drawCommand();
}
// DIFFERENCE FROM DISASSEMBLY:
// Waiting for g_cine->getTimerDelay() since last call to this function
// from mainLoop() was in Future Wars and Operation Stealth disassembly here.
// The wait did nothing else but simply wait for the waiting period to end.
// It has been moved to manageEvents() function call in executePlayerInput()
// to make better use of the waiting period. Now it is used to read mouse button
// status and possibly update the command line while moving the mouse
// (e.g. "EXAMINE DOOR" -> "EXAMINE BUTTON").
if (reloadBgPalOnNextFlip) {
_activePal = getFadeInSourcePalette();
reloadBgPalOnNextFlip = 0;
_changePal = 1; // From disassembly
}
if (_changePal) { // From disassembly
setPalette();
_changePal = 0; // From disassembly
}
const int menus = _menuStack.size();
for (int i = 0; i < menus; ++i)
_menuStack[i]->drawMenu(*this, (i == menus - 1));
blit();
if (gfxFadeInRequested) {
fadeFromBlack();
gfxFadeInRequested = 0;
}
}
/**
* Turn on or off the showing of the collision page.
* If turned on the blitting routine shows the collision page instead of the back buffer.
* @note Useful for debugging collision page related problems.
*/
void FWRenderer::showCollisionPage(bool state) {
_showCollisionPage = state;
}
void FWRenderer::blitBackBuffer() {
blit(false);
}
void FWRenderer::blit(bool useCollisionPage) {
// Show the back buffer or the collision page. Normally the back
// buffer but showing the collision page is useful for debugging.
byte *source = (useCollisionPage ? collisionPage : _backBuffer);
g_system->copyRectToScreen(source, 320, 0, 0, 320, 200);
g_system->updateScreen();
}
/**
* Update screen
*/
void FWRenderer::blit() {
blit(_showCollisionPage);
}
bool FWRenderer::hasSavedBackBuffer(BackBufferSource source) {
return source >= 0 && source < MAX_BACK_BUFFER_SOURCES && _savedBackBuffers[source];
}
void FWRenderer::saveBackBuffer(BackBufferSource source) {
if (_backBuffer && source >= 0 && source < MAX_BACK_BUFFER_SOURCES) {
if (!_savedBackBuffers[source]) {
_savedBackBuffers[source] = new byte[_screenSize];
}
memcpy(_savedBackBuffers[source], _backBuffer, _screenSize);
}
}
void FWRenderer::popSavedBackBuffer(BackBufferSource source) {
restoreSavedBackBuffer(source);
removeSavedBackBuffer(source);
}
void FWRenderer::restoreSavedBackBuffer(BackBufferSource source) {
if (_backBuffer && hasSavedBackBuffer(source)) {
memcpy(_backBuffer, _savedBackBuffers[source], _screenSize);
blitBackBuffer();
}
}
void FWRenderer::removeSavedBackBuffer(BackBufferSource source) {
delete[] _savedBackBuffers[source];
_savedBackBuffers[source] = nullptr;
}
/**
* Set player command string
* @param cmd New command string
*/
void FWRenderer::setCommand(Common::String cmd) {
_cmd = cmd;
}
Common::String FWRenderer::getCommand() {
return _cmd;
}
void FWRenderer::setBlackPalette(bool updateChangePal) {
_activePal.fillWithBlack();
if (updateChangePal) {
_changePal = 1; // From disassembly when called from main loop's initialization section
}
}
void FWRenderer::setPalette() {
assert(_activePal.isValid() && !_activePal.empty());
_activePal.setGlobalOSystemPalette();
}
int16 FWRenderer::addBackground(const char *bgName, uint16 bgIdx) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
/**
* Load background into renderer
* @param bg Raw background data
* @todo Combine with OSRenderer's version of loadBg16
*/
void FWRenderer::loadBg16(const byte *bg, const char *name, unsigned int idx) {
assert(idx == 0);
if (!_background) {
_background = new byte[_screenSize];
}
assert(_background);
Common::strlcpy(_bgName, name, sizeof(_bgName));
// Load the 16 color palette
_backupPal.load(bg, kLowPalNumBytes, kLowPalFormat, kLowPalNumColors, CINE_BIG_ENDIAN);
// Jump over the palette data to the background data
bg += kLowPalNumBytes;
gfxConvertSpriteToRaw(_background, bg, 160, 200);
}
/**
* Placeholder for Operation Stealth implementation
*/
void FWRenderer::loadCt16(const byte *ct, const char *name) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
/**
* Placeholder for Operation Stealth implementation
*/
void FWRenderer::loadBg256(const byte *bg, const char *name, unsigned int idx) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
/**
* Placeholder for Operation Stealth implementation
*/
void FWRenderer::loadCt256(const byte *ct, const char *name) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
/**
* Placeholder for Operation Stealth implementation
*/
void FWRenderer::selectBg(unsigned int idx) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
/**
* Placeholder for Operation Stealth implementation
*/
void FWRenderer::selectScrollBg(unsigned int idx) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
/**
* Placeholder for Operation Stealth implementation
*/
void FWRenderer::setScroll(unsigned int shift) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
/**
* Future Wars has no scrolling backgrounds so scroll value is always zero.
*/
uint FWRenderer::getScroll() const {
return 0;
}
/**
* Placeholder for Operation Stealth implementation
*/
void FWRenderer::removeBg(unsigned int idx) {
error("Future Wars renderer doesn't support multiple backgrounds");
}
void FWRenderer::saveBgNames(Common::OutSaveFile &fHandle) {
fHandle.write(_bgName, 13);
}
const char *FWRenderer::getBgName(uint idx) const {
assert(idx == 0);
return _bgName;
}
/**
* Restore active and backup palette from save
* @param fHandle Savefile open for reading
*/
void FWRenderer::restorePalette(Common::SeekableReadStream &fHandle, int version) {
byte buf[kLowPalNumBytes];
// Load the active 16 color palette from file
fHandle.read(buf, kLowPalNumBytes);
_activePal.load(buf, sizeof(buf), kLowPalFormat, kLowPalNumColors, CINE_BIG_ENDIAN);
// Load the backup 16 color palette from file
fHandle.read(buf, kLowPalNumBytes);
_backupPal.load(buf, sizeof(buf), kLowPalFormat, kLowPalNumColors, CINE_BIG_ENDIAN);
_changePal = 1; // From disassembly
}
/**
* Write active and backup palette to save
* @param fHandle Savefile open for writing
*/
void FWRenderer::savePalette(Common::OutSaveFile &fHandle) {
byte buf[kLowPalNumBytes];
// Make sure the active palette has the correct format and color count
assert(_activePal.colorFormat() == kLowPalFormat);
assert(_activePal.colorCount() == kLowPalNumColors);
// Make sure the backup palette has the correct format and color count
assert(_backupPal.colorFormat() == kLowPalFormat);
assert(_backupPal.colorCount() == kLowPalNumColors);
// Write the active palette to the file
_activePal.save(buf, sizeof(buf), CINE_BIG_ENDIAN);
fHandle.write(buf, kLowPalNumBytes);
// Write the backup palette to the file
_backupPal.save(buf, sizeof(buf), CINE_BIG_ENDIAN);
fHandle.write(buf, kLowPalNumBytes);
}
/**
* Write active and backup palette to save
* @param fHandle Savefile open for writing
* @todo Add support for saving the palette in the 16 color version of Operation Stealth.
* Possibly combine with FWRenderer's savePalette-method?
*/
void OSRenderer::savePalette(Common::OutSaveFile &fHandle) {
byte buf[kHighPalNumBytes];
// We can have 16 color palette in many cases
fHandle.writeUint16LE(_activePal.colorCount());
// Write the active 256 color palette.
_activePal.save(buf, sizeof(buf), CINE_LITTLE_ENDIAN);
fHandle.write(buf, kHighPalNumBytes);
// Write the backup 256 color palette.
_backupPal.save(buf, sizeof(buf), CINE_LITTLE_ENDIAN);
fHandle.write(buf, kHighPalNumBytes);
}
/**
* Restore active and backup palette from save
* @param fHandle Savefile open for reading
*/
void OSRenderer::restorePalette(Common::SeekableReadStream &fHandle, int version) {
byte buf[kHighPalNumBytes];
uint colorCount = (version > 0) ? fHandle.readUint16LE() : kHighPalNumColors;
// Load the active color palette
fHandle.read(buf, kHighPalNumBytes);
if (colorCount == kHighPalNumColors) {
// Load the active 256 color palette from file
_activePal.load(buf, sizeof(buf), kHighPalFormat, kHighPalNumColors, CINE_LITTLE_ENDIAN);
} else {
// Load the active 16 color palette from file
_activePal.load(buf, sizeof(buf), kLowPalFormat, kLowPalNumColors, CINE_LITTLE_ENDIAN);
}
// Load the backup color palette
fHandle.read(buf, kHighPalNumBytes);
if (colorCount == kHighPalNumColors) {
_backupPal.load(buf, sizeof(buf), kHighPalFormat, kHighPalNumColors, CINE_LITTLE_ENDIAN);
} else {
_backupPal.load(buf, sizeof(buf), kLowPalFormat, kLowPalNumColors, CINE_LITTLE_ENDIAN);
}
_changePal = 1; // From disassembly
}
/**
* Rotate active palette
* @param a First color to rotate
* @param b Last color to rotate
* @param c Possibly rotation step, must be 0 or 1 at the moment
*/
void FWRenderer::rotatePalette(int firstIndex, int lastIndex, int mode) {
if (mode == 1) {
_activePal.rotateRight(firstIndex, lastIndex);
} else if (mode == 2) {
_activePal.rotateLeft(firstIndex, lastIndex);
} else {
_activePal = _backupPal;
}
setPalette();
}
void OSRenderer::rotatePalette(int firstIndex, int lastIndex, int mode) {
if (mode == 1) {
_activePal.rotateRight(firstIndex, lastIndex);
} else if (mode == 2) {
_activePal.rotateLeft(firstIndex, lastIndex);
} else if (_currentBg > 0 && _currentBg < 8) {
_activePal = _bgTable[_currentBg].pal;
} else { // background indices 0 and 8 use backup palette
_activePal = _backupPal;
}
setPalette();
}
/**
* Copy part of backup palette to active palette and transform
* @param first First color to transform
* @param last Last color to transform
* @param r Red channel transformation
* @param g Green channel transformation
* @param b Blue channel transformation
*/
void FWRenderer::transformPalette(int first, int last, int r, int g, int b) {
if (!_activePal.isValid() || _activePal.empty()) {
_activePal = Cine::Palette(kLowPalFormat, kLowPalNumColors);
}
_backupPal.saturatedAddColor(_activePal, first, last, r, g, b);
_changePal = 1; // From disassembly
gfxFadeOutCompleted = 0;
}
uint FWRenderer::fadeDelayMs() {
// For PC wait for vertical retrace and wait for three timer interrupt ticks.
// On PC vertical retrace is 70Hz (1000ms / 70 ~= 14.29ms) and
// timer interrupt tick is set to (10923000ms / 1193180) ~= 9.15ms.
// So 14.29ms + 3 * 9.15ms ~= 41.74ms ~= 42ms. That's the maximum to wait for PC.
// Because the vertical retrace might come earlier the minimum to wait is
// 0ms + 3 * 9.15ms (The wait for three timer ticks is absolute) = 27.45ms ~= 27ms.
// So the wait on PC is something between 27ms and 42ms.
// Probably something else on Amiga (Didn't they have 50Hz or 60Hz monitors?).
return 42;
}
uint FWRenderer::fadeToBlackMinMs() {
return 1000;
}
/**
* Fade to black
* \bug Operation Stealth sometimes seems to fade to black using
* transformPalette resulting in double fadeout
*/
void FWRenderer::fadeToBlack() {
assert(_activePal.isValid() && !_activePal.empty());
bool skipFade = false;
uint32 now = g_system->getMillis();
// HACK: Try to cirmumvent double fade outs by throttling function call.
if (hacksEnabled && _fadeToBlackLastCalledMs != 0 && (now - _fadeToBlackLastCalledMs) < fadeToBlackMinMs()) {
skipFade = true;
warning("Skipping fade to black (Time since last called = %d ms < throttling value of %d ms)",
now - _fadeToBlackLastCalledMs, fadeToBlackMinMs());
} else {
_fadeToBlackLastCalledMs = now;
}
for (int i = (skipFade ? 7 : 0); i < 8; i++) {
// Fade out the whole palette by 1/7th
// (Operation Stealth used 36 / 252, which is 1 / 7. Future Wars used 1 / 7 directly).
_activePal.saturatedAddNormalizedGray(_activePal, 0, _activePal.colorCount() - 1, -1, 7);
setPalette();
g_system->updateScreen();
g_system->delayMillis(fadeDelayMs());
}
clearBackBuffer();
forbidBgPalReload = gfxFadeOutCompleted = 1;
// HACK: This is not present in disassembly
// but this is an attempt to prevent flashing a
// normally illuminated screen and then fading it in by
// resetting possible pending background palette reload.
if (hacksEnabled) {
reloadBgPalOnNextFlip = 0;
}
}
void FWRenderer::fadeFromBlack() {
assert(_activePal.isValid() && !_activePal.empty());
const Palette& sourcePalette = getFadeInSourcePalette();
// Initialize active palette to source palette's format and size if they differ
if (_activePal.colorFormat() != sourcePalette.colorFormat() || _activePal.colorCount() != sourcePalette.colorCount()) {
_activePal = Cine::Palette(sourcePalette.colorFormat(), sourcePalette.colorCount());
}
for (int i = 7; i >= 0; i--) {
sourcePalette.saturatedAddNormalizedGray(_activePal, 0, _activePal.colorCount() - 1, -i, 7);
setPalette();
g_system->updateScreen();
g_system->delayMillis(fadeDelayMs());
}
forbidBgPalReload = gfxFadeOutCompleted = 0;
}
// Menu implementation
void FWRenderer::pushMenu(Menu *menu) {
_menuStack.push(menu);
}
Menu *FWRenderer::popMenu() {
if (_menuStack.empty())
return 0;
Menu *menu = _menuStack.top();
_menuStack.pop();
return menu;
}
void FWRenderer::clearMenuStack() {
Menu *menu = 0;
while ((menu = popMenu()) != 0)
delete menu;
}
SelectionMenu::SelectionMenu(Common::Point p, int width, Common::StringArray elements)
: Menu(kSelectionMenu), _pos(p), _width(width), _elements(elements), _selection(-1) {
}
void SelectionMenu::setSelection(int selection) {
if (selection >= getElementCount() || selection < -1) {
warning("Invalid selection %d", selection);
selection = -1;
}
_selection = selection;
}
void SelectionMenu::drawMenu(FWRenderer &r, bool top) {
const int height = getElementCount() * 9 + 10;
int x = _pos.x;
int y = _pos.y;
if (x + _width > 319)
x = 319 - _width;
if (y + height > 199)
y = 199 - height;
byte doubleBorderColor = (r.useTransparentDialogBoxes() ? r.transparentDialogBoxStartColor() : 0) + 2;
if (r.useTransparentDialogBoxes()) {
r.drawTransparentBox(x, y, _width, height);
r.drawDoubleBorder(x, y, _width, height, doubleBorderColor);
} else {
r.drawPlainBox(x, y, _width, height, r._messageBg);
r.drawDoubleBorder(x, y, _width, height, doubleBorderColor);
}
int lineY = y + 4;
const int elemCount = getElementCount();
for (int i = 0; i < elemCount; ++i, lineY += 9) {
int charX = x + 4;
if (i == _selection) {
int color = (r.useTransparentDialogBoxes() ? 2 : 0);
if (!top) {
color += (r.useTransparentDialogBoxes() ? r.transparentDialogBoxStartColor() : 0);
}
r.drawPlainBox(x + 2, lineY - 1, _width - 3, 9, color);
}
const int size = _elements[i].size();
for (int j = 0; j < size; ++j) {
if (r.useTransparentDialogBoxes() && i == _selection) {
charX = r.undrawChar(_elements[i][j], charX, lineY);
} else {
charX = r.drawChar(_elements[i][j], charX, lineY);
}
}
}
}
TextInputMenu::TextInputMenu(Common::Point p, int width, const char *info)
: Menu(kTextInputMenu), _pos(p), _width(width), _info(info), _input(), _cursor(0) {
}
void TextInputMenu::setInput(const char *input, int cursor) {
_input = input;
_cursor = cursor;
}
void TextInputMenu::drawMenu(FWRenderer &r, bool top) {
const int x = _pos.x;
const int y = _pos.y;
int i, tx, ty, tw;
int line = 0, words = 0, cw = 0;
int space = 0, extraSpace = 0;
if (r.useTransparentDialogBoxes())
r.drawTransparentBox(x, y, _width, 4);
else
r.drawPlainBox(x, y, _width, 4, r._messageBg);
tx = x + 4;
ty = _info[0] ? y - 5 : y + 4;
tw = _width - 8;
const int infoSize = _info.size();
// input box info message
for (i = 0; i < infoSize; i++, line--) {
// fit line of text
if (!line) {
line = fitLine(_info.c_str() + i, tw, words, cw);
if (i + line < infoSize && words) {
space = (tw - cw) / words;
extraSpace = (tw - cw) % words;
} else {
space = 5;
extraSpace = 0;
}
ty += 9;
if (r.useTransparentDialogBoxes())
r.drawTransparentBox(x, ty, _width, 9);
else
r.drawPlainBox(x, ty, _width, 9, r._messageBg);
tx = x + 4;
}
// draw characters
if (_info[i] == ' ') {
tx += space + extraSpace;
if (extraSpace) {
extraSpace = 0;
}
} else {
tx = r.drawChar(_info[i], tx, ty);
}
}
// input area background
ty += 9;
if (r.useTransparentDialogBoxes())
r.drawTransparentBox(x, ty, _width, 9);
else
r.drawPlainBox(x, ty, _width, 9, r._messageBg);
r.drawPlainBox(x + 16, ty - 1, _width - 32, 9, 0);
tx = x + 20;
// text in input area
const int inputSize = _input.size();
for (i = 0; i < inputSize; i++) {
tx = r.drawChar(_input[i], tx, ty);
if (_cursor == i + 2) {
r.drawLine(tx, ty - 1, 1, 9, 2);
}
}
if (_input.empty() || _cursor == 1) {
r.drawLine(x + 20, ty - 1, 1, 9, 2);
}
ty += 9;
if (r.useTransparentDialogBoxes())
r.drawTransparentBox(x, ty, _width, 4);
else
r.drawPlainBox(x, ty, _width, 4, r._messageBg);
r.drawDoubleBorder(x, y, _width, ty - y + 4, (r.useTransparentDialogBoxes() ? r.transparentDialogBoxStartColor() : 0) + 2);
}
// -------------------
/**
* Initialize Operation Stealth renderer
*/
OSRenderer::OSRenderer() : FWRenderer(), _bgTable(), _currentBg(0), _scrollBg(0),
_bgShift(0) {
_bgTable.resize(9); // Resize the background table to its required size
}
/**
* Destroy Operation Stealth renderer
*/
OSRenderer::~OSRenderer() {
for (uint i = 0; i < _bgTable.size(); i++) {
_bgTable[i].clear();
}
}
bool OSRenderer::initialize() {
_backupPal = _activePal = Palette(kHighPalFormat, kHighPalNumColors);
return true;
}
/**
* Reset Operation Stealth renderer state
*/
void OSRenderer::clear() {
for (uint i = 0; i < _bgTable.size(); i++) {
_bgTable[i].clear();
}
_currentBg = 0;
_scrollBg = 0;
_bgShift = 0;
FWRenderer::clear();
}
/**
* Draw 1bpp sprite using selected color on backgrounds
* @param obj Object info
* @param fillColor Sprite color
*/
void OSRenderer::incrustMask(const BGIncrust &incrust, uint8 color) {
const ObjectStruct &obj = g_cine->_objectTable[incrust.objIdx];
const byte *data = g_cine->_animDataTable[obj.frame].data();
int x, y, width, height;
x = incrust.x;
y = incrust.y;
width = g_cine->_animDataTable[obj.frame]._realWidth;
height = g_cine->_animDataTable[obj.frame]._height;
if (_bgTable[incrust.bgIdx].bg) {
gfxFillSprite(data, width, height, _bgTable[incrust.bgIdx].bg, x, y, color);
}
}
const Cine::Palette& OSRenderer::getFadeInSourcePalette() {
assert(_currentBg <= 8);
if (_currentBg == 0) {
return _backupPal;
} else {
return _bgTable[_currentBg].pal;
}
}
/**
* Draw color sprite
* @param obj Object info
*/
void OSRenderer::drawSprite(const ObjectStruct &obj) {
const byte *data = g_cine->_animDataTable[obj.frame].data();
int x, y, width, height, transColor;
x = obj.x;
y = obj.y;
transColor = obj.part;
width = g_cine->_animDataTable[obj.frame]._realWidth;
height = g_cine->_animDataTable[obj.frame]._height;
drawSpriteRaw2(data, transColor, width, height, _backBuffer, x, y);
}
/**
* Draw color sprite
* @param obj Object info
*/
void OSRenderer::incrustSprite(const BGIncrust &incrust) {
const ObjectStruct &obj = g_cine->_objectTable[incrust.objIdx];
const byte *data = g_cine->_animDataTable[incrust.frame].data();
int x, y, width, height, transColor;
x = incrust.x;
y = incrust.y;
transColor = obj.part;
width = g_cine->_animDataTable[incrust.frame]._realWidth;
height = g_cine->_animDataTable[incrust.frame]._height;
if (_bgTable[incrust.bgIdx].bg) {
// HACK: Fix transparency colors of shadings near walls
// in labyrinth scene in Operation Stealth after loading a savegame
// saved in the labyrinth.
if (hacksEnabled && incrust.objIdx == 1 && incrust.frame < 16 && transColor == 5 &&
scumm_stricmp(currentPrcName, "LABY.PRC") == 0) {
transColor = 0;
}
drawSpriteRaw2(data, transColor, width, height, _bgTable[incrust.bgIdx].bg, x, y);
}
}
/**
* Draw text character on screen
* @param character Character to draw
* @param x Character coordinate
* @param y Character coordinate
*/
int OSRenderer::drawChar(char character, int x, int y) {
int width;
if (character == ' ') {
x += 5;
} else if ((width = g_cine->_textHandler.fontParamTable[(unsigned char)character].characterWidth)) {
int idx = g_cine->_textHandler.fontParamTable[(unsigned char)character].characterIdx;
drawSpriteRaw2(g_cine->_textHandler.textTable[idx][FONT_DATA], 0, FONT_WIDTH, FONT_HEIGHT, _backBuffer, x, y);
x += width + 1;
}
return x;
}
/**
* Draw background to backbuffer
*/
void OSRenderer::drawBackground() {
byte *main;
main = _bgTable[_currentBg].bg;
assert(main);
if (!_bgShift) {
memcpy(_backBuffer, main, _screenSize);
} else {
unsigned int rowShift = _bgShift % 200;
byte *scroll = _bgTable[_scrollBg].bg;
assert(scroll);
if (!rowShift) {
memcpy(_backBuffer, scroll, _screenSize);
} else {
int mainShift = rowShift * _screenWidth;
int mainSize = _screenSize - mainShift;
if (mainSize > 0) { // Just a precaution
memcpy(_backBuffer, main + mainShift, mainSize);
}
if (mainShift > 0) { // Just a precaution
memcpy(_backBuffer + mainSize, scroll, mainShift);
}
}
}
}
/**
* Draw one overlay
* @param it Overlay info
* @todo Add handling of type 22 overlays
*/
void OSRenderer::renderOverlay(const Common::List<overlay>::iterator &it) {
int len, idx, width, height;
ObjectStruct *obj;
AnimData *sprite;
byte color, transparentColor;
bool useTopLeftForTransCol = false;
switch (it->type) {
// color sprite
case 0:
if (g_cine->_objectTable[it->objIdx].frame < 0) {
break;
}
sprite = &g_cine->_animDataTable[g_cine->_objectTable[it->objIdx].frame];
obj = &g_cine->_objectTable[it->objIdx];
transparentColor = obj->part & 0x0F;
// HACK: Correct transparency color from 6 to 0 for the first frame of sea animation
// in 16 color DOS version of Operation Stealth in the flower shop scene
// (The scene in which the player arrives immediately after leaving the airport).
if (hacksEnabled && it->objIdx == 141 && obj->frame == 100 && obj->part == 6 && sprite->_bpp == 4 &&
scumm_stricmp(currentPrcName, "AIRPORT.PRC") == 0 &&
scumm_stricmp(renderer->getBgName(), "21.PI1") == 0) {
useTopLeftForTransCol = true;
}
// HACK: Correct transparency color from 8 to 51 for the player's walking animation
// in 256 color DOS version of Operation Stealth in the scene right after
// leaving Dr. Why's control room.
if (hacksEnabled && it->objIdx == 1 && obj->part == 8 && sprite->_bpp == 5 &&
scumm_stricmp(currentPrcName, "ILE.PRC") == 0 &&
scumm_stricmp(renderer->getBgName(), "56VIDE.PI1") == 0) {
useTopLeftForTransCol = true;
}
// HACK: Correct transparency color from 1 to 3 for the player emerging from a manhole
// in 256 color DOS version of Operation Stealth when entering the Dr. Why's island.
if (hacksEnabled && it->objIdx == 43 && obj->frame >= 100 && obj->frame <= 102 &&
obj->part == 1 && sprite->_bpp == 5 &&
scumm_stricmp(currentPrcName, "SOUSMAR2.PRC") == 0 &&
scumm_stricmp(renderer->getBgName(), "56.PI1") == 0) {
useTopLeftForTransCol = true;
}
if (useTopLeftForTransCol) {
// Use top left corner value for transparency
transparentColor = sprite->getColor(0, 0);
}
drawSprite(&(*it), sprite->data(), sprite->_realWidth, sprite->_height, _backBuffer, g_cine->_objectTable[it->objIdx].x, g_cine->_objectTable[it->objIdx].y, transparentColor, sprite->_bpp);
break;
// game message
case 2:
if (it->objIdx >= g_cine->_messageTable.size()) {
return;
}
_messageLen += g_cine->_messageTable[it->objIdx].size();
drawMessage(g_cine->_messageTable[it->objIdx].c_str(), it->x, it->y, it->width, it->color);
if (it->color >= 0) { // This test isn't in Future Wars's implementation
waitForPlayerClick = 1;
}
break;
// action failure message
case 3:
idx = it->objIdx * 4 + g_cine->_rnd.getRandomNumber(3);
len = strlen(failureMessages[idx]);
_messageLen += len;
width = 6 * len + 20;
width = width > 300 ? 300 : width;
// The used color here differs from Future Wars
drawMessage(failureMessages[idx], (320 - width) / 2, 80, width, _messageBg);
waitForPlayerClick = 1;
break;
// bitmap
case 4:
if (g_cine->_objectTable[it->objIdx].frame >= 0) {
FWRenderer::renderOverlay(it);
}
break;
// masked background
case 20:
assert(it->objIdx < NUM_MAX_OBJECT);
lastType20OverlayBgIdx = it->x; // A global variable updated here!
obj = &g_cine->_objectTable[it->objIdx];
sprite = &g_cine->_animDataTable[obj->frame];
if (obj->frame < 0 || it->x < 0 || it->x > 8 || !_bgTable[it->x].bg || sprite->_bpp != 1) {
break;
}
maskBgOverlay(it->x, _bgTable[it->x].bg, sprite->data(), sprite->_realWidth, sprite->_height, _backBuffer, obj->x, obj->y);
break;
// line drawing
case 22:
assert(it->objIdx < NUM_MAX_OBJECT);
obj = &g_cine->_objectTable[it->objIdx];
color = obj->part & 0x0F;
width = obj->frame;
height = obj->costume;
// Using Bresenham's algorithm, looks good enough for visual purposes in Operation Stealth
Graphics::drawLine(obj->x, obj->y, width, height, color, plotPoint, _backBuffer);
break;
// something else
default:
FWRenderer::renderOverlay(it);
break;
}
}
/**
* Copy part of backup palette to active palette and transform
* @param first First color to transform
* @param last Last color to transform
* @param r Red channel transformation
* @param g Green channel transformation
* @param b Blue channel transformation
*/
void OSRenderer::transformPalette(int first, int last, int r, int g, int b) {
// Background indices 0 and 8 use backup palette
const Cine::Palette& srcPal =
(_currentBg > 0 && _currentBg < 8) ? _bgTable[_currentBg].pal : _backupPal;
// Initialize active palette to current background's palette format and size if they differ
if (_activePal.colorFormat() != srcPal.colorFormat() || _activePal.colorCount() != srcPal.colorCount()) {
_activePal = Cine::Palette(srcPal.colorFormat(), srcPal.colorCount());
}
// If asked to change whole 16 color palette then
// assume it means the whole palette regardless of size.
// In Operation Stealth DOS 16 color and 256 color disassembly mapping was from 0-15 to 0-255.
if (first == 0 && last == 15) {
last = srcPal.colorCount() - 1;
}
srcPal.saturatedAddColor(_activePal, first, last, r, g, b, kLowPalFormat);
_changePal = 1; // From disassembly
gfxFadeOutCompleted = 0;
}
int16 OSRenderer::addBackground(const char *bgName, uint16 bgIdx) {
byte *ptr, *dataPtr;
int16 fileIdx = findFileInBundle(bgName);
if (fileIdx < 0) {
warning("OSRenderer::addBackground(\"%s\", %d): Could not find background in file bundle.", bgName, bgIdx);
return -1;
}
checkDataDisk(-1);
ptr = dataPtr = readBundleFile(fileIdx);
uint16 bpp = READ_BE_UINT16(ptr); ptr += 2;
if (!_bgTable[bgIdx].bg) {
_bgTable[bgIdx].bg = new byte[_screenSize];
}
Common::strlcpy(_bgTable[bgIdx].name, bgName, sizeof(_bgTable[bgIdx].name));
if (bpp == 8) {
_bgTable[bgIdx].pal.load(ptr, kHighPalNumBytes, kHighPalFormat, kHighPalNumColors, CINE_LITTLE_ENDIAN);
memcpy(_bgTable[bgIdx].bg, ptr + kHighPalNumBytes, _screenSize);
} else {
_bgTable[bgIdx].pal.load(ptr, kLowPalNumBytes, kLowPalFormat, kLowPalNumColors, CINE_BIG_ENDIAN);
gfxConvertSpriteToRaw(_bgTable[bgIdx].bg, ptr + kLowPalNumBytes, 160, 200);
}
free(dataPtr);
return 0;
}
/**
* Load 16 color background into renderer
* @param bg Raw background data
* @param name Background filename
* @param pos Background index
* @todo Combine with FWRenderer's version of loadBg16
*/
void OSRenderer::loadBg16(const byte *bg, const char *name, unsigned int idx) {
assert(idx < 9);
if (!_bgTable[idx].bg) {
_bgTable[idx].bg = new byte[_screenSize];
}
assert(_bgTable[idx].bg);
Common::strlcpy(_bgTable[idx].name, name, sizeof(_bgTable[idx].name));
// Load the 16 color palette
_backupPal.load(bg, kLowPalNumBytes, kLowPalFormat, kLowPalNumColors, CINE_BIG_ENDIAN);
// Jump over the palette data to the background data
bg += kLowPalNumBytes;
gfxConvertSpriteToRaw(_bgTable[idx].bg, bg, 160, 200);
}
/**
* Load 16 color CT data as background into renderer
* @param ct Raw CT data
* @param name Background filename
*/
void OSRenderer::loadCt16(const byte *ct, const char *name) {
assert(collisionPage);
// Make the 9th background point directly to the collision page
// and load the picture into it.
setBackground8ToCollisionPage();
_bgTable[kCollisionPageBgIdxAlias].pal.load(ct, kLowPalNumBytes, kLowPalFormat, kLowPalNumColors, CINE_BIG_ENDIAN);
gfxConvertSpriteToRaw(_bgTable[kCollisionPageBgIdxAlias].bg, ct + kLowPalNumBytes, 160, 200);
}
void OSRenderer::setBackground8ToCollisionPage() {
byte* oldBg = _bgTable[kCollisionPageBgIdxAlias].bg;
if (oldBg && oldBg != collisionPage) {
delete[] _bgTable[kCollisionPageBgIdxAlias].bg;
}
_bgTable[kCollisionPageBgIdxAlias].bg = collisionPage;
}
/**
* Load 256 color background into renderer
* @param bg Raw background data
* @param name Background filename
* @param pos Background index
*/
void OSRenderer::loadBg256(const byte *bg, const char *name, unsigned int idx) {
assert(idx < 9);
if (!_bgTable[idx].bg) {
_bgTable[idx].bg = new byte[_screenSize];
}
assert(_bgTable[idx].bg);
Common::strlcpy(_bgTable[idx].name, name, sizeof(_bgTable[idx].name));
_backupPal.load(bg, kHighPalNumBytes, kHighPalFormat, kHighPalNumColors, CINE_LITTLE_ENDIAN);
memcpy(_bgTable[idx].bg, bg + kHighPalNumBytes, _screenSize);
}
/**
* Load 256 color CT data as background into renderer
* @param ct Raw CT data
* @param name Background filename
*/
void OSRenderer::loadCt256(const byte *ct, const char *name) {
assert(collisionPage);
// Make the 9th background point directly to the collision page
// and load the picture into it.
setBackground8ToCollisionPage();
_bgTable[kCollisionPageBgIdxAlias].pal.load(ct, kHighPalNumBytes, kHighPalFormat, kHighPalNumColors, CINE_LITTLE_ENDIAN);
memcpy(_bgTable[kCollisionPageBgIdxAlias].bg, ct + kHighPalNumBytes, _screenSize);
}
/**
* Select active background and load its palette
* @param idx Background index
*/
void OSRenderer::selectBg(unsigned int idx) {
assert(idx < 9);
if (idx <= 8 && _bgTable[idx].bg) {
_currentBg = idx;
if (!forbidBgPalReload) {
reloadBgPalOnNextFlip = 1;
}
} else
warning("OSRenderer::selectBg(%d) - attempt to select null background", idx);
}
/**
* Select scroll background
* @param idx Scroll background index
*/
void OSRenderer::selectScrollBg(unsigned int idx) {
assert(idx < 9);
if (idx <= 8 && _bgTable[idx].bg) {
_scrollBg = idx;
}
}
/**
* Set background scroll
* @param shift Background scroll in pixels
*/
void OSRenderer::setScroll(unsigned int shift) {
_bgShift = shift % 400;
}
/**
* Get background scroll
* @return Background scroll in pixels
*/
uint OSRenderer::getScroll() const {
return _bgShift;
}
/**
* Unload background from renderer
* @param idx Background to unload
*/
void OSRenderer::removeBg(unsigned int idx) {
assert(idx > 0 && idx < 9);
if (_currentBg == idx) {
_currentBg = 0;
}
if (_scrollBg == idx) {
_scrollBg = 0;
}
_bgTable[idx].clear();
}
void OSRenderer::saveBgNames(Common::OutSaveFile &fHandle) {
for (int i = 0; i < 8; i++) {
fHandle.write(_bgTable[i].name, 13);
}
}
const char *OSRenderer::getBgName(uint idx) const {
assert(idx < 9);
return _bgTable[idx].name;
}
void setMouseCursor(int cursor) {
static int currentMouseCursor = -1;
assert(cursor >= 0 && cursor < 3);
if (currentMouseCursor != cursor) {
byte mouseCursor[16 * 16];
const MouseCursor *mc = &mouseCursors[cursor];
const byte *src = mc->bitmap;
for (int i = 0; i < 32; ++i) {
int offs = i * 8;
for (byte mask = 0x80; mask != 0; mask >>= 1) {
if (src[0] & mask) {
mouseCursor[offs] = 1;
} else if (src[32] & mask) {
mouseCursor[offs] = 0;
} else {
mouseCursor[offs] = 0xFF;
}
++offs;
}
++src;
}
CursorMan.replaceCursor(mouseCursor, 16, 16, mc->hotspotX, mc->hotspotY, 0xFF);
CursorMan.replaceCursorPalette(cursorPalette, 0, 2);
currentMouseCursor = cursor;
}
}
void gfxFillSprite(const byte *spritePtr, uint16 width, uint16 height, byte *page, int16 x, int16 y, uint8 fillColor) {
int16 i, j;
for (i = 0; i < height; i++) {
byte *destPtr = page + x + y * 320;
destPtr += i * 320;
for (j = 0; j < width; j++) {
if (x + j >= 0 && x + j < 320 && i + y >= 0 && i + y < 200 && !*spritePtr) {
*destPtr = fillColor;
}
destPtr++;
spritePtr++;
}
}
}
void gfxDrawMaskedSprite(const byte *spritePtr, const byte *maskPtr, uint16 width, uint16 height, byte *page, int16 x, int16 y) {
int16 i, j;
for (i = 0; i < height; i++) {
byte *destPtr = page + x + y * 320;
destPtr += i * 320;
for (j = 0; j < width; j++) {
if (x + j >= 0 && x + j < 320 && i + y >= 0 && i + y < 200 && *maskPtr == 0) {
*destPtr = *spritePtr;
}
++destPtr;
++spritePtr;
++maskPtr;
}
}
}
void gfxUpdateSpriteMask(byte *destMask, int16 x, int16 y, int16 width, int16 height, const byte *srcMask, int16 xm, int16 ym, int16 maskWidth, int16 maskHeight) {
int16 i, j, d, spritePitch, maskPitch;
spritePitch = width;
maskPitch = maskWidth;
// crop update area to overlapping parts of masks
if (y > ym) {
d = y - ym;
srcMask += d * maskPitch;
maskHeight -= d;
} else if (y < ym) {
d = ym - y;
destMask += d * spritePitch;
height -= d;
}
if (x > xm) {
d = x - xm;
srcMask += d;
maskWidth -= d;
} else if (x < xm) {
d = xm - x;
destMask += d;
width -= d;
}
// update mask
for (j = 0; j < MIN(maskHeight, height); ++j) {
for (i = 0; i < MIN(maskWidth, width); ++i) {
destMask[i] |= srcMask[i] ^ 1;
}
destMask += spritePitch;
srcMask += maskPitch;
}
}
void gfxUpdateIncrustMask(byte *destMask, int16 x, int16 y, int16 width, int16 height, const byte *srcMask, int16 xm, int16 ym, int16 maskWidth, int16 maskHeight) {
int16 i, j, d, spritePitch, maskPitch;
spritePitch = width;
maskPitch = maskWidth;
// crop update area to overlapping parts of masks
if (y > ym) {
d = y - ym;
srcMask += d * maskPitch;
maskHeight -= d;
} else if (y < ym) {
d = ym - y > height ? height : ym - y;
memset(destMask, 1, d * spritePitch);
destMask += d * spritePitch;
height -= d;
}
if (x > xm) {
d = x - xm;
xm = x;
srcMask += d;
maskWidth -= d;
}
d = xm - x;
maskWidth += d;
// update mask
for (j = 0; j < MIN(maskHeight, height); ++j) {
for (i = 0; i < width; ++i) {
destMask[i] |= i < d || i >= maskWidth ? 1 : srcMask[i - d];
}
destMask += spritePitch;
srcMask += maskPitch;
}
if (j < height) {
memset(destMask, 1, (height - j) * spritePitch);
}
}
void gfxDrawLine(int16 x1, int16 y1, int16 x2, int16 y2, byte color, byte *page) {
if (x1 == x2) {
if (y1 > y2) {
SWAP(y1, y2);
}
while (y1 <= y2) {
*(page + (y1 * 320 + x1)) = color;
y1++;
}
} else {
if (x1 > x2) {
SWAP(x1, x2);
}
while (x1 <= x2) {
*(page + (y1 * 320 + x1)) = color;
x1++;
}
}
}
void gfxDrawPlainBoxRaw(int16 x1, int16 y1, int16 x2, int16 y2, byte color, byte *page) {
int16 t;
if (x1 > x2) {
SWAP(x1, x2);
}
if (y1 > y2) {
SWAP(y1, y2);
}
t = x1;
while (y1 <= y2) {
x1 = t;
while (x1 <= x2) {
*(page + y1 * 320 + x1) = color;
x1++;
}
y1++;
}
}
int16 gfxGetBit(int16 x, int16 y, const byte *ptr, int16 width) {
const byte *ptrToData = (ptr) + y * width + x;
if (x > width) {
return 0;
}
if (*ptrToData) {
return 0;
}
return 1;
}
void gfxResetRawPage(byte *pageRaw) {
memset(pageRaw, 0, 320 * 200);
}
void gfxConvertSpriteToRaw(byte *dst, const byte *src, uint16 w, uint16 h) {
// Output is 4 bits per pixel.
// Pixels are in 16 pixel chunks (8 bytes of source per 16 pixels of output).
// The source data is interleaved so that
// 1st big-endian 16-bit value contains all bit position 0 values for 16 pixels,
// 2nd big-endian 16-bit value contains all bit position 1 values for 16 pixels,
// 3rd big-endian 16-bit value contains all bit position 2 values for 16 pixels,
// 4th big-endian 16-bit value contains all bit position 3 values for 16 pixels.
// 1st pixel's bits are in the 16th bits,
// 2nd pixel's bits are in the 15th bits,
// 3rd pixel's bits are in the 14th bits etc.
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w / 8; ++x) {
for (int bit = 0; bit < 16; ++bit) {
uint8 color = 0;
for (int p = 0; p < 4; ++p) {
if (READ_BE_UINT16(src + p * 2) & (1 << (15 - bit))) {
color |= 1 << p;
}
}
*dst++ = color;
}
src += 8;
}
}
}
void drawSpriteRaw(const byte *spritePtr, const byte *maskPtr, int16 width, int16 height, byte *page, int16 x, int16 y) {
int16 i, j;
// FIXME: Is it a bug if maskPtr == NULL?
if (!maskPtr)
warning("drawSpriteRaw: maskPtr == NULL");
for (i = 0; i < height; i++) {
byte *destPtr = page + x + y * 320;
destPtr += i * 320;
for (j = 0; j < width; j++) {
if ((!maskPtr || !(*maskPtr)) && x + j >= 0 && x + j < 320 && i + y >= 0 && i + y < 200) {
*(destPtr++) = *(spritePtr++);
} else {
destPtr++;
spritePtr++;
}
if (maskPtr)
maskPtr++;
}
}
}
void OSRenderer::drawSprite(overlay *overlayPtr, const byte *spritePtr, int16 width, int16 height, byte *page, int16 x, int16 y, byte transparentColor, byte bpp) {
byte *pMask = NULL;
// draw the mask based on next objects in the list
Common::List<overlay>::iterator it;
for (it = g_cine->_overlayList.begin(); it != g_cine->_overlayList.end(); ++it) {
if (&(*it) == overlayPtr) {
break;
}
}
while (it != g_cine->_overlayList.end()) {
overlay *pCurrentOverlay = &(*it);
if ((pCurrentOverlay->type == 5) || ((pCurrentOverlay->type == 21) && (pCurrentOverlay->x == overlayPtr->objIdx))) {
AnimData *sprite = &g_cine->_animDataTable[g_cine->_objectTable[it->objIdx].frame];
if (pMask == NULL) {
pMask = new byte[width * height];
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
byte spriteColor = spritePtr[width * i + j];
pMask[width * i + j] = spriteColor;
}
}
}
for (int i = 0; i < sprite->_realWidth; i++) {
for (int j = 0; j < sprite->_height; j++) {
int inMaskX = (g_cine->_objectTable[it->objIdx].x + i) - x;
int inMaskY = (g_cine->_objectTable[it->objIdx].y + j) - y;
if (inMaskX >= 0 && inMaskX < width) {
if (inMaskY >= 0 && inMaskY < height) {
if (sprite->_bpp == 1) {
if (!sprite->getColor(i, j)) {
pMask[inMaskY * width + inMaskX] = page[x + y * 320 + inMaskX + inMaskY * 320];
}
}
}
}
}
}
}
++it;
}
// now, draw with the mask we created
if (pMask) {
spritePtr = pMask;
}
// ignore transparent color in 1bpp
if (bpp == 1) {
transparentColor = 1;
}
{
for (int i = 0; i < height; i++) {
byte *destPtr = page + x + y * 320;
destPtr += i * 320;
for (int j = 0; j < width; j++) {
byte color = *(spritePtr++);
if ((transparentColor != color) && x + j >= 0 && x + j < 320 && i + y >= 0 && i + y < 200) {
*(destPtr++) = color;
} else {
destPtr++;
}
}
}
}
delete[] pMask;
}
void drawSpriteRaw2(const byte *spritePtr, byte transColor, int16 width, int16 height, byte *page, int16 x, int16 y) {
int16 i, j;
for (i = 0; i < height; i++) {
byte *destPtr = page + x + y * 320;
destPtr += i * 320;
for (j = 0; j < width; j++) {
if ((*spritePtr != transColor) && (x + j >= 0 && x + j < 320 && i + y >= 0 && i + y < 200)) {
*destPtr = *spritePtr;
}
destPtr++;
spritePtr++;
}
}
}
void maskBgOverlay(int targetBgIdx, const byte *bgPtr, const byte *maskPtr, int16 width, int16 height,
byte *page, int16 x, int16 y) {
int16 i, j, tmpWidth, tmpHeight;
Common::List<BGIncrust>::iterator it;
const byte *backup = maskPtr;
// background pass
for (i = 0; i < height; i++) {
byte *destPtr = page + x + y * 320;
const byte *srcPtr = bgPtr + x + y * 320;
destPtr += i * 320;
srcPtr += i * 320;
for (j = 0; j < width; j++) {
if ((!maskPtr || !(*maskPtr)) && (x + j >= 0
&& x + j < 320 && i + y >= 0 && i + y < 200)) {
*destPtr = *srcPtr;
}
destPtr++;
srcPtr++;
if (maskPtr)
maskPtr++;
}
}
maskPtr = backup;
// incrust pass
for (it = g_cine->_bgIncrustList.begin(); it != g_cine->_bgIncrustList.end(); ++it) {
// HACK: Remove drawing of red corners around doors in rat maze in Operation Stealth
// by skipping drawing of possible collision table data to non-collision table page.
if (hacksEnabled && it->bgIdx == kCollisionPageBgIdxAlias && targetBgIdx != kCollisionPageBgIdxAlias) {
continue;
}
tmpWidth = g_cine->_animDataTable[it->frame]._realWidth;
tmpHeight = g_cine->_animDataTable[it->frame]._height;
byte *mask = (byte *)malloc(tmpWidth * tmpHeight);
if (it->param == 0) {
generateMask(g_cine->_animDataTable[it->frame].data(), mask, tmpWidth * tmpHeight, it->part);
gfxUpdateIncrustMask(mask, it->x, it->y, tmpWidth, tmpHeight, maskPtr, x, y, width, height);
gfxDrawMaskedSprite(g_cine->_animDataTable[it->frame].data(), mask, tmpWidth, tmpHeight, page, it->x, it->y);
} else {
memcpy(mask, g_cine->_animDataTable[it->frame].data(), tmpWidth * tmpHeight);
gfxUpdateIncrustMask(mask, it->x, it->y, tmpWidth, tmpHeight, maskPtr, x, y, width, height);
gfxFillSprite(mask, tmpWidth, tmpHeight, page, it->x, it->y);
}
free(mask);
}
}
} // End of namespace Cine