/* 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 "common/system.h" #include "scumm/scumm.h" #include "scumm/actor.h" #include "scumm/charset.h" #include "scumm/intern.h" #ifndef DISABLE_HE #include "scumm/he/intern_he.h" #endif #include "scumm/resource.h" #include "scumm/usage_bits.h" #include "scumm/he/wiz_he.h" #include "scumm/util.h" #ifdef USE_ARM_GFX_ASM extern "C" void DrawStripToScreenARM(int height, int width, byte const* text, byte const* src, byte* dst, int vsPitch, int vmScreenWidth, int textSurfacePitch); extern "C" void Copy8ColARM(byte* dst, int dstPitch, const byte* src, int height); #endif /* USE_ARM_GFX_ASM */ namespace Scumm { static void blit(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h); static void fill(byte *dst, int dstPitch, byte color, int w, int h); static void copy8Col(byte *dst, int dstPitch, const byte *src, int height); static void clear8Col(byte *dst, int dstPitch, int height); static void ditherHerc(byte *src, byte *hercbuf, int srcPitch, int *x, int *y, int *width, int *height); static void scale2x(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h); struct StripTable { int offsets[160]; int run[160]; int color[160]; int zoffsets[120]; // FIXME: Why only 120 here? int zrun[120]; // FIXME: Why only 120 here? }; enum { kScrolltime = 500, // ms scrolling is supposed to take kPictureDelay = 20, kFadeDelay = 4 // 1/4th of a jiffie }; #define NUM_SHAKE_POSITIONS 8 static const int8 shake_positions[NUM_SHAKE_POSITIONS] = { 0, 1 * 2, 2 * 2, 1 * 2, 0 * 2, 2 * 2, 3 * 2, 1 * 2 }; /** * The following structs define four basic fades/transitions used by * transitionEffect(), each looking differently to the user. * Note that the stripTables contain strip numbers, and they assume * that the screen has 40 vertical strips (i.e. 320 pixel), and 25 horizontal * strips (i.e. 200 pixel). There is a hack in transitionEffect that * makes it work correctly in games which have a different screen height * (for example, 240 pixel), but nothing is done regarding the width, so this * code won't work correctly in COMI. Also, the number of iteration depends * on min(vertStrips, horizStrips}. So the 13 is derived from 25/2, rounded up. * And the 25 = min(25,40). Hence for Zak256 instead of 13 and 25, the values * 15 and 30 should be used, and for COMI probably 30 and 60. */ struct TransitionEffect { byte numOfIterations; int8 deltaTable[16]; // four times l / t / r / b byte stripTable[16]; // ditto }; static const TransitionEffect transitionEffects[6] = { // Iris effect (looks like an opening/closing camera iris) { 13, // Number of iterations { 1, 1, -1, 1, -1, 1, -1, -1, 1, -1, -1, -1, 1, 1, 1, -1 }, { 0, 0, 39, 0, 39, 0, 39, 24, 0, 24, 39, 24, 0, 0, 0, 24 } }, // Box wipe (a box expands from the upper-left corner to the lower-right corner) { 25, // Number of iterations { 0, 1, 2, 1, 2, 0, 2, 1, 2, 0, 2, 1, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 255, 0, 0, 0 } }, // Box wipe (a box expands from the lower-right corner to the upper-left corner) { 25, // Number of iterations { -2, -1, 0, -1, -2, -1, -2, 0, -2, -1, -2, 0, 0, 0, 0, 0 }, { 39, 24, 39, 24, 39, 24, 39, 24, 38, 24, 38, 24, 255, 0, 0, 0 } }, // Inverse box wipe { 25, // Number of iterations { 0, -1, -2, -1, -2, 0, -2, -1, -2, 0, -2, -1, 0, 0, 0, 0 }, { 0, 24, 39, 24, 39, 0, 39, 24, 38, 0, 38, 24, 255, 0, 0, 0 } }, // Inverse iris effect, specially tailored for V1/V2 games { 9, // Number of iterations { -1, -1, 1, -1, -1, 1, 1, 1, -1, -1, -1, 1, 1, -1, 1, 1 }, { 7, 7, 32, 7, 7, 8, 32, 8, 7, 8, 7, 8, 32, 7, 32, 8 } }, // Horizontal wipe (a box expands from left to right side). For MM NES { 16, // Number of iterations { 2, 0, 2, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 15, 1, 0, 1, 15, 255, 0, 0, 0, 255, 0, 0, 0 } } }; Gdi::Gdi(ScummEngine *vm) : _vm(vm) { _numZBuffer = 0; memset(_imgBufOffs, 0, sizeof(_imgBufOffs)); _numStrips = 0; _paletteMod = 0; _roomPalette = vm->_roomPalette; _transparentColor = 255; _decomp_shr = 0; _decomp_mask = 0; _vertStripNextInc = 0; _zbufferDisabled = false; _objectMode = false; } Gdi::~Gdi() { } GdiNES::GdiNES(ScummEngine *vm) : Gdi(vm) { memset(&_NES, 0, sizeof(_NES)); } GdiV1::GdiV1(ScummEngine *vm) : Gdi(vm) { memset(&_C64, 0, sizeof(_C64)); } GdiV2::GdiV2(ScummEngine *vm) : Gdi(vm) { _roomStrips = 0; } GdiV2::~GdiV2() { free(_roomStrips); } void Gdi::init() { _numStrips = _vm->_screenWidth / 8; // Increase the number of screen strips by one; needed for smooth scrolling if (_vm->_game.version >= 7) { // We now have mostly working smooth scrolling code in place for V7+ games // (i.e. The Dig, Full Throttle and COMI). It seems to work very well so far. // // To understand how we achieve smooth scrolling, first note that with it, the // virtual screen strips don't match the display screen strips anymore. To // overcome that problem, we simply use a screen pitch that is 8 pixel wider // than the actual screen width, and always draw one strip more than needed to // the backbuf (thus we have to treat the right border seperately). _numStrips += 1; } } void Gdi::roomChanged(byte *roomptr) { } void GdiNES::roomChanged(byte *roomptr) { decodeNESGfx(roomptr); } void GdiV1::roomChanged(byte *roomptr) { for (int i = 0; i < 4; i++){ _C64.colors[i] = roomptr[6 + i]; } decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 10), _C64.charMap, 2048); decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 12), _C64.picMap, roomptr[4] * roomptr[5]); decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 14), _C64.colorMap, roomptr[4] * roomptr[5]); decodeC64Gfx(roomptr + READ_LE_UINT16(roomptr + 16), _C64.maskMap, roomptr[4] * roomptr[5]); // Read the mask data. The 16bit length value seems to always be 8 too big. // See bug #1837375 for details on this. const byte *maskPtr = roomptr + READ_LE_UINT16(roomptr + 18); decodeC64Gfx(maskPtr + 2, _C64.maskChar, READ_LE_UINT16(maskPtr) - 8); _objectMode = true; } void GdiV2::roomChanged(byte *roomptr) { _roomStrips = generateStripTable(roomptr + READ_LE_UINT16(roomptr + 0x0A), _vm->_roomWidth, _vm->_roomHeight, _roomStrips); } #pragma mark - #pragma mark --- Virtual Screens --- #pragma mark - void ScummEngine::initScreens(int b, int h) { int i; int adj = 0; for (i = 0; i < 3; i++) { _res->nukeResource(rtBuffer, i + 1); _res->nukeResource(rtBuffer, i + 5); } if (!getResourceAddress(rtBuffer, 4)) { // Since the size of screen 3 is fixed, there is no need to reallocate // it if its size changed. // Not sure what it is good for, though. I think it may have been used // in pre-V7 for the games messages (like 'Pause', Yes/No dialogs, // version display, etc.). I don't know about V7, maybe the same is the // case there. If so, we could probably just remove it completely. if (_game.version >= 7) { initVirtScreen(kUnkVirtScreen, (_screenHeight / 2) - 10, _screenWidth, 13, false, false); } else { initVirtScreen(kUnkVirtScreen, 80, _screenWidth, 13, false, false); } } if ((_game.platform == Common::kPlatformNES) && (h != _screenHeight)) { // This is a hack to shift the whole screen downwards to match the original. // Otherwise we would have to do lots of coordinate adjustments all over // the code. adj = 16; initVirtScreen(kUnkVirtScreen, 0, _screenWidth, adj, false, false); } initVirtScreen(kMainVirtScreen, b + adj, _screenWidth, h - b, true, true); initVirtScreen(kTextVirtScreen, adj, _screenWidth, b, false, false); initVirtScreen(kVerbVirtScreen, h + adj, _screenWidth, _screenHeight - h - adj, false, false); _screenB = b; _screenH = h; _gdi->init(); } void ScummEngine::initVirtScreen(VirtScreenNumber slot, int top, int width, int height, bool twobufs, bool scrollable) { VirtScreen *vs = &_virtscr[slot]; int size; assert(height >= 0); assert(slot >= 0 && slot < 4); if (_game.version >= 7) { if (slot == kMainVirtScreen && (_roomHeight != 0)) height = _roomHeight; } vs->number = slot; vs->w = width; vs->topline = top; vs->h = height; vs->hasTwoBuffers = twobufs; vs->xstart = 0; vs->backBuf = NULL; vs->bytesPerPixel = 1; vs->pitch = width; if (_game.version >= 7) { // Increase the pitch by one; needed to accomodate the extra screen // strip which we use to implement smooth scrolling. See Gdi::init(). vs->pitch += 8; } size = vs->pitch * vs->h; if (scrollable) { // Allow enough spaces so that rooms can be up to 4 resp. 8 screens // wide. To achieve horizontal scrolling, SCUMM uses a neat trick: // only the offset into the screen buffer (xstart) is changed. That way // very little of the screen has to be redrawn, and we have a very low // memory overhead (namely for every pixel we want to scroll, we need // one additional byte in the buffer). if (_game.version >= 7) { size += vs->pitch * 8; } else { size += vs->pitch * 4; } } _res->createResource(rtBuffer, slot + 1, size); vs->pixels = getResourceAddress(rtBuffer, slot + 1); memset(vs->pixels, 0, size); // reset background if (twobufs) { vs->backBuf = _res->createResource(rtBuffer, slot + 5, size); } if (slot != 3) { vs->setDirtyRange(0, height); } } VirtScreen *ScummEngine::findVirtScreen(int y) { VirtScreen *vs = _virtscr; int i; for (i = 0; i < 3; i++, vs++) { if (y >= vs->topline && y < vs->topline + vs->h) { return vs; } } return NULL; } void ScummEngine::markRectAsDirty(VirtScreenNumber virt, int left, int right, int top, int bottom, int dirtybit) { VirtScreen *vs = &_virtscr[virt]; int lp, rp; if (left > right || top > bottom) return; if (top > vs->h || bottom < 0) return; if (top < 0) top = 0; if (bottom > vs->h) bottom = vs->h; if (virt == kMainVirtScreen && dirtybit) { lp = left / 8 + _screenStartStrip; if (lp < 0) lp = 0; rp = (right + vs->xstart) / 8; if (_game.version >= 7) { if (rp > 409) rp = 409; } else { if (rp >= 200) rp = 200; } for (; lp <= rp; lp++) setGfxUsageBit(lp, dirtybit); } // The following code used to be in the separate method setVirtscreenDirty lp = left / 8; rp = right / 8; if ((lp >= _gdi->_numStrips) || (rp < 0)) return; if (lp < 0) lp = 0; if (rp >= _gdi->_numStrips) rp = _gdi->_numStrips - 1; while (lp <= rp) { if (top < vs->tdirty[lp]) vs->tdirty[lp] = top; if (bottom > vs->bdirty[lp]) vs->bdirty[lp] = bottom; lp++; } } /** * Update all dirty screen areas. This method blits all of the internal engine * graphics to the actual display, as needed. In addition, the 'shaking' * code in the backend is controlled from here. */ void ScummEngine::drawDirtyScreenParts() { // Update verbs updateDirtyScreen(kVerbVirtScreen); // Update the conversation area (at the top of the screen) updateDirtyScreen(kTextVirtScreen); // Update game area ("stage") if (camera._last.x != camera._cur.x || (_game.version >= 7 && (camera._cur.y != camera._last.y))) { // Camera moved: redraw everything VirtScreen *vs = &_virtscr[kMainVirtScreen]; drawStripToScreen(vs, 0, vs->w, 0, vs->h); vs->setDirtyRange(vs->h, 0); } else { updateDirtyScreen(kMainVirtScreen); } // Handle shaking if (_shakeEnabled) { _shakeFrame = (_shakeFrame + 1) % NUM_SHAKE_POSITIONS; _system->setShakePos(shake_positions[_shakeFrame]); } else if (!_shakeEnabled &&_shakeFrame != 0) { _shakeFrame = 0; _system->setShakePos(0); } } void ScummEngine_v6::drawDirtyScreenParts() { // For the Full Throttle credits to work properly, the blast // texts have to be drawn before the blast objects. Unless // someone can think of a better way to achieve this effect. if (_game.version >= 7 && VAR(VAR_BLAST_ABOVE_TEXT) == 1) { drawBlastTexts(); drawBlastObjects(); if (_game.version == 8) { // Does this case ever happen? We need to draw the // actor over the blast object, so we're forced to // also draw it over the subtitles. processUpperActors(); } } else { drawBlastObjects(); if (_game.version == 8) { // Do this before drawing blast texts. Subtitles go on // top of the CoMI verb coin, e.g. when Murray is // talking to himself early in the game. processUpperActors(); } drawBlastTexts(); } // Call the original method. ScummEngine::drawDirtyScreenParts(); // Remove all blasted objects/text again. removeBlastTexts(); removeBlastObjects(); } /** * Blit the dirty data from the given VirtScreen to the display. If the camera moved, * a full blit is done, otherwise only the visible dirty areas are updated. */ void ScummEngine::updateDirtyScreen(VirtScreenNumber slot) { VirtScreen *vs = &_virtscr[slot]; // Do nothing for unused virtual screens if (vs->h == 0) return; int i; int w = 8; int start = 0; for (i = 0; i < _gdi->_numStrips; i++) { if (vs->bdirty[i]) { const int top = vs->tdirty[i]; const int bottom = vs->bdirty[i]; vs->tdirty[i] = vs->h; vs->bdirty[i] = 0; if (i != (_gdi->_numStrips - 1) && vs->bdirty[i + 1] == bottom && vs->tdirty[i + 1] == top) { // Simple optimizations: if two or more neighbouring strips // form one bigger rectangle, coalesce them. w += 8; continue; } drawStripToScreen(vs, start * 8, w, top, bottom); w = 8; } start = i + 1; } } /** * Blit the specified rectangle from the given virtual screen to the display. * Note: t and b are in *virtual screen* coordinates, while x is relative to * the *real screen*. This is due to the way tdirty/vdirty work: they are * arrays which map 'strips' (sections of the real screen) to dirty areas as * specified by top/bottom coordinate in the virtual screen. */ void ScummEngine::drawStripToScreen(VirtScreen *vs, int x, int width, int top, int bottom) { // Short-circuit if nothing has to be drawn if (bottom <= top || top >= vs->h) return; // Some paranoia checks assert(top >= 0 && bottom <= vs->h); assert(x >= 0 && width <= vs->pitch); assert(_textSurface.pixels); // Perform some clipping if (width > vs->w - x) width = vs->w - x; if (top < _screenTop) top = _screenTop; if (bottom > _screenTop + _screenHeight) bottom = _screenTop + _screenHeight; // Convert the vertical coordinates to real screen coords int y = vs->topline + top - _screenTop; int height = bottom - top; if (width <= 0 || height <= 0) return; const byte *src = vs->getPixels(x, top); int m = _textSurfaceMultiplier; byte *dst; int vsPitch; int pitch = vs->pitch; if (_useCJKMode && _textSurfaceMultiplier == 2) { dst = _fmtownsBuf; scale2x(dst, _screenWidth * m, src, vs->pitch, width, height); src = dst; vsPitch = _screenWidth * m - width * m; } else { vsPitch = vs->pitch - width; } dst = _compositeBuf; if (_game.version < 7) { // For The Dig, FT and COMI, we just blit everything to the screen at once. // For older games, things are more complicated. First off, we need to // deal with the _textSurface, which needs to be composited over the // screen contents. Secondly, a rendering mode might be active, which // means a filter has to be applied. // Compute pointer to the text surface assert(_compositeBuf); const byte *text = (byte *)_textSurface.getBasePtr(x * m, y * m); // The values x, width, etc. are all multiples of 8 at this point, // so loop unrolloing might be a good idea... assert(0 == ((long)text & 3)); assert(0 == (width & 3)); // Compose the text over the game graphics // TODO: Optimize this code. There are several things that come immediately to mind: // (1) Loop unrolling: We could read 4 or even 8 pixels at once, since everything is // a multiple of 8 here. // (2) More ASM versions (in particular, the ARM code for the NDS could be used on // all ARM systems, couldn't it?) // (3) Better encoding of the text surface data. This is the one with the biggest // potential. // (a) Keep an "isEmpty" marker for each pixel row in the _textSurface. The idea // is that most rows won't contain any text data, so we can just use memcpy. // (b) RLE encode the _textSurface row-wise. This is an improved variant of (a), // but also more complicated to implement, and incurs a bigger overhead when // writing to the text surface. #ifdef ARM_USE_GFX_ASM DrawStripToScreenARM(height, width, text, src, dst, vs->pitch, width, _textSurface.pitch); #else for (int h = 0; h < height * m; ++h) { for (int w = 0; w < width * m; ++w) { byte tmp = *text++; if (tmp == CHARSET_MASK_TRANSPARENCY) tmp = *src; *dst++ = tmp; src++; } src += vsPitch; text += _textSurface.pitch - width * m; } #endif src = _compositeBuf; pitch = width; if (_renderMode == Common::kRenderHercA || _renderMode == Common::kRenderHercG) { ditherHerc(_compositeBuf, _herculesBuf, width, &x, &y, &width, &height); src = _herculesBuf + x + y * Common::kHercW; pitch = Common::kHercW; // center image on the screen x += (Common::kHercW - _screenWidth * 2) / 2; // (720 - 320*2)/2 = 40 } else if (_useCJKMode && m == 2) { pitch *= m; x *= m; y *= m; width *= m; height *= m; } else { if (_renderMode == Common::kRenderCGA) ditherCGA(_compositeBuf, width, x, y, width, height); // HACK: This is dirty hack which renders narrow NES rooms centered // NES can address negative number strips and that poses problem for // our code. So instead of adding zillions of fixes and potentially // breaking other games, we shift it right at the rendering stage. if ((_game.platform == Common::kPlatformNES) && (((_NESStartStrip > 0) && (vs->number == kMainVirtScreen)) || (vs->number == kTextVirtScreen))) { x += 16; while (x + width >= _screenWidth) width -= 16; if (width < 0) return; } } } // Finally blit the whole thing to the screen _system->copyRectToScreen(src, pitch, x, y, width, height); } // CGA // indy3 loom maniac monkey1 zak // // Herc (720x350) // maniac monkey1 zak // // EGA // monkey2 loom maniac monkey1 atlantis indy3 zak loomcd static const byte cgaDither[2][2][16] = { {{0, 1, 0, 1, 2, 2, 0, 0, 3, 1, 3, 1, 3, 2, 1, 3}, {0, 0, 1, 1, 0, 2, 2, 3, 0, 3, 1, 1, 3, 3, 1, 3}}, {{0, 0, 1, 1, 0, 2, 2, 3, 0, 3, 1, 1, 3, 3, 1, 3}, {0, 1, 0, 1, 2, 2, 0, 0, 3, 1, 1, 1, 3, 2, 1, 3}}}; // CGA dithers 4x4 square with direct substitutes // Odd lines have colors swapped, so there will be checkered patterns. // But apparently there is a mistake for 10th color. void ScummEngine::ditherCGA(byte *dst, int dstPitch, int x, int y, int width, int height) const { byte *ptr; int idx1, idx2; for (int y1 = 0; y1 < height; y1++) { ptr = dst + y1 * dstPitch; if (_game.version == 2) idx1 = 0; else idx1 = (y + y1) % 2; for (int x1 = 0; x1 < width; x1++) { idx2 = (x + x1) % 2; *ptr = cgaDither[idx1][idx2][*ptr & 0xF]; ptr++; } } } // Hercules dithering. It uses same dithering tables but output is 1bpp and // it stretches in this way: // aaaa0 // aa aaaa1 // bb bbbb0 Here 0 and 1 mean dithering table row number // cc --> bbbb1 // dd cccc0 // cccc1 // dddd0 void ditherHerc(byte *src, byte *hercbuf, int srcPitch, int *x, int *y, int *width, int *height) { byte *srcptr, *dstptr; const int xo = *x, yo = *y, widtho = *width, heighto = *height; int dsty = yo*2 - yo/4; for (int y1 = 0; y1 < heighto;) { assert(dsty < Common::kHercH); srcptr = src + y1 * srcPitch; dstptr = hercbuf + dsty * Common::kHercW + xo * 2; const int idx1 = (dsty % 7) % 2; for (int x1 = 0; x1 < widtho; x1++) { const int idx2 = (xo + x1) % 2; const byte tmp = cgaDither[idx1][idx2][*srcptr & 0xF]; *dstptr++ = tmp >> 1; *dstptr++ = tmp & 0x1; srcptr++; } if (idx1 || dsty % 7 == 6) y1++; dsty++; } *x *= 2; *y = yo*2 - yo/4; *width *= 2; *height = dsty - *y; } void scale2x(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h) { byte *dstL1 = dst; byte *dstL2 = dst + dstPitch; int dstAdd = dstPitch * 2 - w * 2; int srcAdd = srcPitch - w; while (h--) { for (int x = 0; x < w; ++x, dstL1 += 2, dstL2 += 2) { uint16 col = *src++; col |= col << 8; *(uint16*)(dstL1) = col; *(uint16*)(dstL2) = col; } dstL1 += dstAdd; dstL2 += dstAdd; src += srcAdd; } } #pragma mark - #pragma mark --- Background buffers & charset mask --- #pragma mark - void ScummEngine::initBGBuffers(int height) { const byte *ptr; int size, itemsize, i; byte *room; if (_game.version >= 7) { // Resize main virtual screen in V7 games. This is necessary // because in V7, rooms may be higher than one screen, so we have // to accomodate for that. initVirtScreen(kMainVirtScreen, _virtscr[kMainVirtScreen].topline, _screenWidth, height, true, true); } if (_game.heversion >= 70) room = getResourceAddress(rtRoomImage, _roomResource); else room = getResourceAddress(rtRoom, _roomResource); if (_game.version <= 3) { _gdi->_numZBuffer = 2; } else if (_game.features & GF_SMALL_HEADER) { int off; ptr = findResourceData(MKID_BE('SMAP'), room); _gdi->_numZBuffer = 0; if (_game.features & GF_16COLOR) off = READ_LE_UINT16(ptr); else off = READ_LE_UINT32(ptr); while (off && _gdi->_numZBuffer < 4) { _gdi->_numZBuffer++; ptr += off; off = READ_LE_UINT16(ptr); } } else if (_game.version == 8) { // in V8 there is no RMIH and num z buffers is in RMHD ptr = findResource(MKID_BE('RMHD'), room); _gdi->_numZBuffer = READ_LE_UINT32(ptr + 24) + 1; } else if (_game.heversion >= 70) { ptr = findResource(MKID_BE('RMIH'), room); _gdi->_numZBuffer = READ_LE_UINT16(ptr + 8) + 1; } else { ptr = findResource(MKID_BE('RMIH'), findResource(MKID_BE('RMIM'), room)); _gdi->_numZBuffer = READ_LE_UINT16(ptr + 8) + 1; } assert(_gdi->_numZBuffer >= 1 && _gdi->_numZBuffer <= 8); if (_game.version >= 7) itemsize = (_roomHeight + 10) * _gdi->_numStrips; else itemsize = (_roomHeight + 4) * _gdi->_numStrips; size = itemsize * _gdi->_numZBuffer; memset(_res->createResource(rtBuffer, 9, size), 0, size); for (i = 0; i < (int)ARRAYSIZE(_gdi->_imgBufOffs); i++) { if (i < _gdi->_numZBuffer) _gdi->_imgBufOffs[i] = i * itemsize; else _gdi->_imgBufOffs[i] = (_gdi->_numZBuffer - 1) * itemsize; } } /** * Redraw background as needed, i.e. the left/right sides if scrolling took place etc. * Note that this only updated the virtual screen, not the actual display. */ void ScummEngine::redrawBGAreas() { int i; int diff; int val = 0; if (_game.id != GID_PASS && _game.version >= 4 && _game.version <= 6) { // Starting with V4 games (with the exception of the PASS demo), text // is drawn over the game graphics (as opposed to be drawn in a // separate region of the screen). So, when scrolling in one of these // games (pre-new camera system), if actor text is visible (as indicated // by the _hasMask flag), we first remove it before proceeding. if (camera._cur.x != camera._last.x && _charset->_hasMask) stopTalk(); } // Redraw parts of the background which are marked as dirty. if (!_fullRedraw && _bgNeedsRedraw) { for (i = 0; i != _gdi->_numStrips; i++) { if (testGfxUsageBit(_screenStartStrip + i, USAGE_BIT_DIRTY)) { redrawBGStrip(i, 1); } } } if (_game.version >= 7) { diff = camera._cur.x / 8 - camera._last.x / 8; if (_fullRedraw || ABS(diff) >= _gdi->_numStrips) { _bgNeedsRedraw = false; redrawBGStrip(0, _gdi->_numStrips); } else if (diff > 0) { val = -diff; redrawBGStrip(_gdi->_numStrips - diff, diff); } else if (diff < 0) { val = -diff; redrawBGStrip(0, -diff); } } else { diff = camera._cur.x - camera._last.x; if (!_fullRedraw && diff == 8) { val = -1; redrawBGStrip(_gdi->_numStrips - 1, 1); } else if (!_fullRedraw && diff == -8) { val = +1; redrawBGStrip(0, 1); } else if (_fullRedraw || diff != 0) { if (_game.version <= 5) { ((ScummEngine_v5 *)this)->clearFlashlight(); } _bgNeedsRedraw = false; redrawBGStrip(0, _gdi->_numStrips); } } drawRoomObjects(val); _bgNeedsRedraw = false; } #ifndef DISABLE_HE void ScummEngine_v71he::redrawBGAreas() { if (camera._cur.x != camera._last.x && _charset->_hasMask) stopTalk(); byte *room = getResourceAddress(rtRoomImage, _roomResource) + _IM00_offs; if (_fullRedraw) { _bgNeedsRedraw = false; _gdi->drawBMAPBg(room, &_virtscr[kMainVirtScreen]); } drawRoomObjects(0); _bgNeedsRedraw = false; } void ScummEngine_v72he::redrawBGAreas() { ScummEngine_v71he::redrawBGAreas(); _wiz->flushWizBuffer(); } #endif void ScummEngine::redrawBGStrip(int start, int num) { byte *room; int s = _screenStartStrip + start; for (int i = 0; i < num; i++) setGfxUsageBit(s + i, USAGE_BIT_DIRTY); if (_game.heversion >= 70) room = getResourceAddress(rtRoomImage, _roomResource); else room = getResourceAddress(rtRoom, _roomResource); _gdi->drawBitmap(room + _IM00_offs, &_virtscr[kMainVirtScreen], s, 0, _roomWidth, _virtscr[kMainVirtScreen].h, s, num, 0); } void ScummEngine::restoreBackground(Common::Rect rect, byte backColor) { VirtScreen *vs; byte *screenBuf; if (rect.top < 0) rect.top = 0; if (rect.left >= rect.right || rect.top >= rect.bottom) return; if ((vs = findVirtScreen(rect.top)) == NULL) return; if (rect.left > vs->w) return; // Convert 'rect' to local (virtual screen) coordinates rect.top -= vs->topline; rect.bottom -= vs->topline; rect.clip(vs->w, vs->h); markRectAsDirty(vs->number, rect, USAGE_BIT_RESTORED); screenBuf = vs->getPixels(rect.left, rect.top); const int height = rect.height(); const int width = rect.width(); if (!height) return; if (vs->hasTwoBuffers && _currentRoom != 0 && isLightOn()) { blit(screenBuf, vs->pitch, vs->getBackPixels(rect.left, rect.top), vs->pitch, width, height); if (vs->number == kMainVirtScreen && _charset->_hasMask) { byte *mask = (byte *)_textSurface.getBasePtr(rect.left, rect.top - _screenTop); fill(mask, _textSurface.pitch, CHARSET_MASK_TRANSPARENCY, width, height); } } else { fill(screenBuf, vs->pitch, backColor, width, height); } } void ScummEngine::restoreCharsetBg() { _nextLeft = _string[0].xpos; _nextTop = _string[0].ypos + _screenTop; if (_charset->_hasMask) { _charset->_hasMask = false; _charset->_str.left = -1; _charset->_left = -1; // Restore background on the whole text area. This code is based on // restoreBackground(), but was changed to only restore those parts which are // currently covered by the charset mask. VirtScreen *vs = &_virtscr[_charset->_textScreenID]; if (!vs->h) return; markRectAsDirty(vs->number, Common::Rect(vs->w, vs->h), USAGE_BIT_RESTORED); byte *screenBuf = vs->getPixels(0, 0); if (vs->hasTwoBuffers && _currentRoom != 0 && isLightOn()) { if (vs->number != kMainVirtScreen) { // Restore from back buffer const byte *backBuf = vs->getBackPixels(0, 0); blit(screenBuf, vs->pitch, backBuf, vs->pitch, vs->w, vs->h); } } else { // Clear area memset(screenBuf, 0, vs->h * vs->pitch); } if (vs->hasTwoBuffers) { // Clean out the charset mask clearTextSurface(); } } } void ScummEngine::clearCharsetMask() { memset(getResourceAddress(rtBuffer, 9), 0, _gdi->_imgBufOffs[1]); } void ScummEngine::clearTextSurface() { memset(_textSurface.pixels, CHARSET_MASK_TRANSPARENCY, _textSurface.pitch * _textSurface.h); } byte *ScummEngine::getMaskBuffer(int x, int y, int z) { return _gdi->getMaskBuffer((x + _virtscr[kMainVirtScreen].xstart) / 8, y, z); } byte *Gdi::getMaskBuffer(int x, int y, int z) { return _vm->getResourceAddress(rtBuffer, 9) + x + y * _numStrips + _imgBufOffs[z]; } #pragma mark - #pragma mark --- Misc --- #pragma mark - static void blit(byte *dst, int dstPitch, const byte *src, int srcPitch, int w, int h) { assert(w > 0); assert(h > 0); assert(src != NULL); assert(dst != NULL); if (w == srcPitch && w == dstPitch) { memcpy(dst, src, w*h); } else { do { memcpy(dst, src, w); dst += dstPitch; src += srcPitch; } while (--h); } } static void fill(byte *dst, int dstPitch, byte color, int w, int h) { assert(h > 0); assert(dst != NULL); if (w == dstPitch) { memset(dst, color, w*h); } else { do { memset(dst, color, w); dst += dstPitch; } while (--h); } } #ifdef ARM_USE_GFX_ASM #define copy8Col(A,B,C,D) copy8ColARM(A,B,C,D) #else static void copy8Col(byte *dst, int dstPitch, const byte *src, int height) { do { #if defined(SCUMM_NEED_ALIGNMENT) memcpy(dst, src, 8); #else ((uint32 *)dst)[0] = ((const uint32 *)src)[0]; ((uint32 *)dst)[1] = ((const uint32 *)src)[1]; #endif dst += dstPitch; src += dstPitch; } while (--height); } #endif /* ARM_USE_GFX_ASM */ static void clear8Col(byte *dst, int dstPitch, int height) { do { #if defined(SCUMM_NEED_ALIGNMENT) memset(dst, 0, 8); #else ((uint32 *)dst)[0] = 0; ((uint32 *)dst)[1] = 0; #endif dst += dstPitch; } while (--height); } void ScummEngine::drawBox(int x, int y, int x2, int y2, int color) { int width, height; VirtScreen *vs; byte *backbuff, *bgbuff; if ((vs = findVirtScreen(y)) == NULL) return; if (x > x2) SWAP(x, x2); if (y > y2) SWAP(y, y2); x2++; y2++; // Adjust for the topline of the VirtScreen y -= vs->topline; y2 -= vs->topline; // Clip the coordinates if (x < 0) x = 0; else if (x >= vs->w) return; if (x2 < 0) return; else if (x2 > vs->w) x2 = vs->w; if (y < 0) y = 0; else if (y > vs->h) return; if (y2 < 0) return; else if (y2 > vs->h) y2 = vs->h; width = x2 - x; height = y2 - y; // This will happen in the Sam & Max intro - see bug #1039162 - where // it would trigger an assertion in blit(). if (width <= 0 || height <= 0) return; markRectAsDirty(vs->number, x, x2, y, y2); backbuff = vs->getPixels(x, y); bgbuff = vs->getBackPixels(x, y); if (color == -1) { if (vs->number != kMainVirtScreen) error("can only copy bg to main window"); blit(backbuff, vs->pitch, bgbuff, vs->pitch, width, height); if (_charset->_hasMask) { byte *mask = (byte *)_textSurface.getBasePtr(x * _textSurfaceMultiplier, (y - _screenTop) * _textSurfaceMultiplier); fill(mask, _textSurface.pitch, CHARSET_MASK_TRANSPARENCY, width * _textSurfaceMultiplier, height * _textSurfaceMultiplier); } } else if (_game.heversion >= 71) { // Flags are used for different methods in HE games uint32 flags = color; if ((flags & 0x2000) || (flags & 0x4000000)) { blit(backbuff, vs->pitch, bgbuff, vs->pitch, width, height); } else if ((flags & 0x4000) || (flags & 0x2000000)) { blit(bgbuff, vs->pitch, backbuff, vs->pitch, width, height); } else if ((flags & 0x8000) || (flags & 0x1000000)) { flags &= (flags & 0x1000000) ? 0xFFFFFF : 0x7FFF; fill(backbuff, vs->pitch, flags, width, height); fill(bgbuff, vs->pitch, flags, width, height); } else { fill(backbuff, vs->pitch, flags, width, height); } } else if (_game.version >= 60) { // Flags are used for different methods in HE games uint16 flags = color; if (flags & 0x2000) { blit(backbuff, vs->pitch, bgbuff, vs->pitch, width, height); } else if (flags & 0x4000) { blit(bgbuff, vs->pitch, backbuff, vs->pitch, width, height); } else if (flags & 0x8000) { flags &= 0x7FFF; fill(backbuff, vs->pitch, flags, width, height); fill(bgbuff, vs->pitch, flags, width, height); } else { fill(backbuff, vs->pitch, flags, width, height); } } else { fill(backbuff, vs->pitch, color, width, height); } } /** * Moves the screen content by the offset specified via dx/dy. * Only the region from x=0 till x=height-1 is affected. * @param dx the horizontal offset. * @param dy the vertical offset. * @param height the number of lines which in which the move will be done. */ void ScummEngine::moveScreen(int dx, int dy, int height) { // Short circuit check - do we have to do anything anyway? if ((dx == 0 && dy == 0) || height <= 0) return; Graphics::Surface *screen = _system->lockScreen(); if (!screen) return; screen->move(dx, dy, height); _system->unlockScreen(); } void ScummEngine_v5::clearFlashlight() { _flashlight.isDrawn = false; _flashlight.buffer = NULL; } void ScummEngine_v5::drawFlashlight() { int i, j, x, y; VirtScreen *vs = &_virtscr[kMainVirtScreen]; // Remove the flash light first if it was previously drawn if (_flashlight.isDrawn) { markRectAsDirty(kMainVirtScreen, _flashlight.x, _flashlight.x + _flashlight.w, _flashlight.y, _flashlight.y + _flashlight.h, USAGE_BIT_DIRTY); if (_flashlight.buffer) { fill(_flashlight.buffer, vs->pitch, 0, _flashlight.w, _flashlight.h); } _flashlight.isDrawn = false; } if (_flashlight.xStrips == 0 || _flashlight.yStrips == 0) return; // Calculate the area of the flashlight if (_game.id == GID_ZAK || _game.id == GID_MANIAC) { x = _mouse.x + vs->xstart; y = _mouse.y - vs->topline; } else { Actor *a = derefActor(VAR(VAR_EGO), "drawFlashlight"); x = a->getPos().x; y = a->getPos().y; } _flashlight.w = _flashlight.xStrips * 8; _flashlight.h = _flashlight.yStrips * 8; _flashlight.x = x - _flashlight.w / 2 - _screenStartStrip * 8; _flashlight.y = y - _flashlight.h / 2; if (_game.id == GID_LOOM) _flashlight.y -= 12; // Clip the flashlight at the borders if (_flashlight.x < 0) _flashlight.x = 0; else if (_flashlight.x + _flashlight.w > _gdi->_numStrips * 8) _flashlight.x = _gdi->_numStrips * 8 - _flashlight.w; if (_flashlight.y < 0) _flashlight.y = 0; else if (_flashlight.y + _flashlight.h> vs->h) _flashlight.y = vs->h - _flashlight.h; // Redraw any actors "under" the flashlight for (i = _flashlight.x / 8; i < (_flashlight.x + _flashlight.w) / 8; i++) { assert(0 <= i && i < _gdi->_numStrips); setGfxUsageBit(_screenStartStrip + i, USAGE_BIT_DIRTY); vs->tdirty[i] = 0; vs->bdirty[i] = vs->h; } byte *bgbak; _flashlight.buffer = vs->getPixels(_flashlight.x, _flashlight.y); bgbak = vs->getBackPixels(_flashlight.x, _flashlight.y); blit(_flashlight.buffer, vs->pitch, bgbak, vs->pitch, _flashlight.w, _flashlight.h); // Round the corners. To do so, we simply hard-code a set of nicely // rounded corners. static const int corner_data[] = { 8, 6, 4, 3, 2, 2, 1, 1 }; int minrow = 0; int maxcol = _flashlight.w - 1; int maxrow = (_flashlight.h - 1) * vs->pitch; for (i = 0; i < 8; i++, minrow += vs->pitch, maxrow -= vs->pitch) { int d = corner_data[i]; for (j = 0; j < d; j++) { _flashlight.buffer[minrow + j] = 0; _flashlight.buffer[minrow + maxcol - j] = 0; _flashlight.buffer[maxrow + j] = 0; _flashlight.buffer[maxrow + maxcol - j] = 0; } } _flashlight.isDrawn = true; } // V0 Maniac doesn't have a ScummVar for VAR_CURRENT_LIGHTS, and just uses // an internal variable. Emulate this to prevent overwriting script vars... // And V6 games do not use the "lights" at all. There, the whole screen is // always visible, and actors are always colored, so we fake the correct // light value for it. int ScummEngine::getCurrentLights() const { if (_game.id == GID_MANIAC && _game.version == 0) return _currentLights; else if (_game.version >= 6) return LIGHTMODE_room_lights_on | LIGHTMODE_actor_use_colors; else return VAR(VAR_CURRENT_LIGHTS); } bool ScummEngine::isLightOn() const { return (getCurrentLights() & LIGHTMODE_room_lights_on) != 0; } void ScummEngine::setShake(int mode) { if (_shakeEnabled != (mode != 0)) _fullRedraw = true; _shakeEnabled = mode != 0; _shakeFrame = 0; _system->setShakePos(0); } #pragma mark - #pragma mark --- Image drawing --- #pragma mark - void Gdi::prepareDrawBitmap(const byte *ptr, VirtScreen *vs, const int x, const int y, const int width, const int height, int stripnr, int numstrip) { // Do nothing by default } void GdiV1::prepareDrawBitmap(const byte *ptr, VirtScreen *vs, const int x, const int y, const int width, const int height, int stripnr, int numstrip) { if (_objectMode) { decodeC64Gfx(ptr, _C64.objectMap, (width / 8) * (height / 8) * 3); } } void GdiNES::prepareDrawBitmap(const byte *ptr, VirtScreen *vs, const int x, const int y, const int width, const int height, int stripnr, int numstrip) { if (_objectMode) { decodeNESObject(ptr, x - stripnr, y, width, height); } } void GdiV2::prepareDrawBitmap(const byte *ptr, VirtScreen *vs, const int x, const int y, const int width, const int height, int stripnr, int numstrip) { // // Since V3, all graphics data was encoded in strips, which is very efficient // for redrawing only parts of the screen. However, V2 is different: here // the whole graphics are encoded as one big chunk. That makes it rather // dificult to draw only parts of a room/object. We handle the V2 graphics // differently from all other (newer) graphic formats for this reason. // StripTable *table = (_objectMode ? 0 : _roomStrips); const int left = (stripnr * 8); const int right = left + (numstrip * 8); byte *dst; byte *mask_ptr; const byte *src; byte color, data = 0; int run; bool dither = false; byte dither_table[128]; byte *ptr_dither_table; int theX, theY, maxX; memset(dither_table, 0, sizeof(dither_table)); if (vs->hasTwoBuffers) dst = vs->backBuf + y * vs->pitch + x * 8; else dst = (byte *)vs->pixels + y * vs->pitch + x * 8; mask_ptr = getMaskBuffer(x, y, 1); if (table) { run = table->run[stripnr]; color = table->color[stripnr]; src = ptr + table->offsets[stripnr]; theX = left; maxX = right; } else { run = 1; color = 0; src = ptr; theX = 0; maxX = width; } // Decode and draw the image data. assert(height <= 128); for (; theX < maxX; theX++) { ptr_dither_table = dither_table; for (theY = 0; theY < height; theY++) { if (--run == 0) { data = *src++; if (data & 0x80) { run = data & 0x7f; dither = true; } else { run = data >> 4; dither = false; } color = _roomPalette[data & 0x0f]; if (run == 0) { run = *src++; } } if (!dither) { *ptr_dither_table = color; } if (left <= theX && theX < right) { *dst = *ptr_dither_table++; dst += vs->pitch; } } if (left <= theX && theX < right) { dst -= _vertStripNextInc; } } // Draw mask (zplane) data theY = 0; if (table) { src = ptr + table->zoffsets[stripnr]; run = table->zrun[stripnr]; theX = left; } else { run = *src++; theX = 0; } while (theX < right) { const byte runFlag = run & 0x80; if (runFlag) { run &= 0x7f; data = *src++; } do { if (!runFlag) data = *src++; if (left <= theX) { *mask_ptr = data; mask_ptr += _numStrips; } theY++; if (theY >= height) { if (left <= theX) { mask_ptr -= _numStrips * height - 1; } theY = 0; theX += 8; if (theX >= right) break; } } while (--run); run = *src++; } } int Gdi::getZPlanes(const byte *ptr, const byte *zplane_list[9], bool bmapImage) const { int numzbuf; int i; if ((_vm->_game.features & GF_SMALL_HEADER) || _vm->_game.version == 8) zplane_list[0] = ptr; else if (bmapImage) zplane_list[0] = _vm->findResource(MKID_BE('BMAP'), ptr); else zplane_list[0] = _vm->findResource(MKID_BE('SMAP'), ptr); if (_zbufferDisabled) numzbuf = 0; else if (_numZBuffer <= 1 || (_vm->_game.version <= 2)) numzbuf = _numZBuffer; else { numzbuf = _numZBuffer; assert(numzbuf <= 9); if (_vm->_game.features & GF_SMALL_HEADER) { if (_vm->_game.features & GF_16COLOR) zplane_list[1] = ptr + READ_LE_UINT16(ptr); else { zplane_list[1] = ptr + READ_LE_UINT32(ptr); if (_vm->_game.features & GF_OLD256) { if (0 == READ_LE_UINT32(zplane_list[1])) zplane_list[1] = 0; } } for (i = 2; i < numzbuf; i++) { zplane_list[i] = zplane_list[i-1] + READ_LE_UINT16(zplane_list[i-1]); } } else if (_vm->_game.version == 8) { // Find the OFFS chunk of the ZPLN chunk const byte *zplnOffsChunkStart = ptr + 24 + READ_BE_UINT32(ptr + 12); // Each ZPLN contains a WRAP chunk, which has (as always) an OFFS subchunk pointing // at ZSTR chunks. These once more contain a WRAP chunk which contains nothing but // an OFFS chunk. The content of this OFFS chunk contains the offsets to the // Z-planes. // We do not directly make use of this, but rather hard code offsets (like we do // for all other Scumm-versions, too). Clearly this is a bit hackish, but works // well enough, and there is no reason to assume that there are any cases where it // might fail. Still, doing this properly would have the advantage of catching // invalid/damaged data files, and allow us to exit gracefully instead of segfaulting. for (i = 1; i < numzbuf; i++) { zplane_list[i] = zplnOffsChunkStart + READ_LE_UINT32(zplnOffsChunkStart + 4 + i*4) + 16; } } else { const uint32 zplane_tags[] = { MKID_BE('ZP00'), MKID_BE('ZP01'), MKID_BE('ZP02'), MKID_BE('ZP03'), MKID_BE('ZP04') }; for (i = 1; i < numzbuf; i++) { zplane_list[i] = _vm->findResource(zplane_tags[i], ptr); } } } return numzbuf; } /** * Draw a bitmap onto a virtual screen. This is main drawing method for room backgrounds * and objects, used throughout all SCUMM versions. */ void Gdi::drawBitmap(const byte *ptr, VirtScreen *vs, int x, const int y, const int width, const int height, int stripnr, int numstrip, byte flag) { assert(ptr); assert(height > 0); byte *dstPtr; const byte *smap_ptr; const byte *zplane_list[9]; int numzbuf; int sx; bool transpStrip = false; // Check whether lights are turned on or not const bool lightsOn = _vm->isLightOn(); if (_vm->_game.id == GID_LOOM && _vm->_game.platform == Common::kPlatformPCEngine) { // FIXME: Image format unknown return; } if (_vm->_game.features & GF_SMALL_HEADER) { smap_ptr = ptr; } else if (_vm->_game.version == 8) { // Skip to the BSTR->WRAP->OFFS chunk smap_ptr = ptr + 24; } else { smap_ptr = _vm->findResource(MKID_BE('SMAP'), ptr); assert(smap_ptr); } numzbuf = getZPlanes(ptr, zplane_list, false); const byte *tmsk_ptr = NULL; if (_vm->_game.heversion >= 72) { tmsk_ptr = _vm->findResource(MKID_BE('TMSK'), ptr); } if (y + height > vs->h) { warning("Gdi::drawBitmap, strip drawn to %d below window bottom %d", y + height, vs->h); } _vertStripNextInc = height * vs->pitch - 1; _objectMode = (flag & dbObjectMode) == dbObjectMode; prepareDrawBitmap(ptr, vs, x, y, width, height, stripnr, numstrip); sx = x - vs->xstart / 8; if (sx < 0) { numstrip -= -sx; x += -sx; stripnr += -sx; sx = 0; } // Compute the number of strips we have to iterate over. // TODO/FIXME: The computation of its initial value looks very fishy. // It was added as a kind of hack to fix some corner cases, but it compares // the room width to the virtual screen width; but the former should always // be bigger than the latter (except for MM NES, maybe)... strange int limit = MAX(_vm->_roomWidth, (int) vs->w) / 8 - x; if (limit > numstrip) limit = numstrip; if (limit > _numStrips - sx) limit = _numStrips - sx; for (int k = 0; k < limit; ++k, ++stripnr, ++sx, ++x) { if (y < vs->tdirty[sx]) vs->tdirty[sx] = y; if (y + height > vs->bdirty[sx]) vs->bdirty[sx] = y + height; // In the case of a double buffered virtual screen, we draw to // the backbuffer, otherwise to the primary surface memory. if (vs->hasTwoBuffers) dstPtr = vs->backBuf + y * vs->pitch + x * 8; else dstPtr = (byte *)vs->pixels + y * vs->pitch + x * 8; transpStrip = drawStrip(dstPtr, vs, x, y, width, height, stripnr, smap_ptr); // COMI and HE games only uses flag value if (_vm->_game.version == 8 || _vm->_game.heversion >= 60) transpStrip = true; if (vs->hasTwoBuffers) { byte *frontBuf = (byte *)vs->pixels + y * vs->pitch + x * 8; if (lightsOn) copy8Col(frontBuf, vs->pitch, dstPtr, height); else clear8Col(frontBuf, vs->pitch, height); } decodeMask(x, y, width, height, stripnr, numzbuf, zplane_list, transpStrip, flag, tmsk_ptr); #if 0 // HACK: blit mask(s) onto normal screen. Useful to debug masking for (int i = 0; i < numzbuf; i++) { byte *dst1, *dst2; dst1 = dst2 = (byte *)vs->pixels + y * vs->pitch + x * 8; if (vs->hasTwoBuffers) dst2 = vs->backBuf + y * vs->pitch + x * 8; byte *mask_ptr = getMaskBuffer(x, y, i); for (int h = 0; h < height; h++) { int maskbits = *mask_ptr; for (int j = 0; j < 8; j++) { if (maskbits & 0x80) dst1[j] = dst2[j] = 12 + i; maskbits <<= 1; } dst1 += vs->pitch; dst2 += vs->pitch; mask_ptr += _numStrips; } } #endif } } bool Gdi::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height, int stripnr, const byte *smap_ptr) { // Do some input verification and make sure the strip/strip offset // are actually valid. Normally, this should never be a problem, // but if e.g. a savegame gets corrupted, we can easily get into // trouble here. See also bug #795214. int offset = -1, smapLen; if (_vm->_game.id == GID_LOOM && _vm->_game.platform == Common::kPlatformPCEngine) { // Length of offsets segment only smapLen = READ_LE_UINT16(smap_ptr); if (stripnr * 2 + 2 < smapLen) { offset = READ_LE_UINT16(smap_ptr + stripnr * 2 + 2); offset += stripnr * 2 + 3; } debug(0, "stripnr %d len %d offset %d", stripnr, smapLen, offset); } else if (_vm->_game.features & GF_16COLOR) { smapLen = READ_LE_UINT16(smap_ptr); if (stripnr * 2 + 2 < smapLen) { offset = READ_LE_UINT16(smap_ptr + stripnr * 2 + 2); } assertRange(0, offset, smapLen-1, "screen strip"); } else if (_vm->_game.features & GF_SMALL_HEADER) { smapLen = READ_LE_UINT32(smap_ptr); if (stripnr * 4 + 4 < smapLen) offset = READ_LE_UINT32(smap_ptr + stripnr * 4 + 4); assertRange(0, offset, smapLen-1, "screen strip"); } else { smapLen = READ_BE_UINT32(smap_ptr); if (stripnr * 4 + 8 < smapLen) offset = READ_LE_UINT32(smap_ptr + stripnr * 4 + 8); assertRange(0, offset, smapLen-1, "screen strip"); } return decompressBitmap(dstPtr, vs->pitch, smap_ptr + offset, height); } bool GdiNES::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height, int stripnr, const byte *smap_ptr) { byte *mask_ptr = getMaskBuffer(x, y, 1); drawStripNES(dstPtr, mask_ptr, vs->pitch, stripnr, y, height); return false; } bool GdiV1::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height, int stripnr, const byte *smap_ptr) { if (_objectMode) drawStripC64Object(dstPtr, vs->pitch, stripnr, width, height); else drawStripC64Background(dstPtr, vs->pitch, stripnr, height); return false; } bool GdiV2::drawStrip(byte *dstPtr, VirtScreen *vs, int x, int y, const int width, const int height, int stripnr, const byte *smap_ptr) { // Do nothing here for V2 games - drawing was already handled. return false; } void Gdi::decodeMask(int x, int y, const int width, const int height, int stripnr, int numzbuf, const byte *zplane_list[9], bool transpStrip, byte flag, const byte *tmsk_ptr) { int i; byte *mask_ptr; const byte *z_plane_ptr; if (flag & dbDrawMaskOnAll) { // Sam & Max uses dbDrawMaskOnAll for things like the inventory // box and the speech icons. While these objects only have one // mask, it should be applied to all the Z-planes in the room, // i.e. they should mask every actor. // // This flag used to be called dbDrawMaskOnBoth, and all it // would do was to mask Z-plane 0. (Z-plane 1 would also be // masked, because what is now the else-clause used to be run // always.) While this seems to be the only way there is to // mask Z-plane 0, this wasn't good enough since actors in // Z-planes >= 2 would not be masked. // // The flag is also used by The Dig and Full Throttle, but I // don't know what for. At the time of writing, these games // are still too unstable for me to investigate. if (_vm->_game.version == 8) z_plane_ptr = zplane_list[1] + READ_LE_UINT32(zplane_list[1] + stripnr * 4 + 8); else z_plane_ptr = zplane_list[1] + READ_LE_UINT16(zplane_list[1] + stripnr * 2 + 8); for (i = 0; i < numzbuf; i++) { mask_ptr = getMaskBuffer(x, y, i); if (transpStrip && (flag & dbAllowMaskOr)) decompressMaskImgOr(mask_ptr, z_plane_ptr, height); else decompressMaskImg(mask_ptr, z_plane_ptr, height); } } else { for (i = 1; i < numzbuf; i++) { uint32 offs; if (!zplane_list[i]) continue; if (_vm->_game.features & GF_OLD_BUNDLE) offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2); else if (_vm->_game.features & GF_OLD256) offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 4); else if (_vm->_game.features & GF_SMALL_HEADER) offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 2); else if (_vm->_game.version == 8) offs = READ_LE_UINT32(zplane_list[i] + stripnr * 4 + 8); else offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 8); mask_ptr = getMaskBuffer(x, y, i); if (offs) { z_plane_ptr = zplane_list[i] + offs; if (tmsk_ptr) { const byte *tmsk = tmsk_ptr + READ_LE_UINT16(tmsk_ptr + 8); decompressTMSK(mask_ptr, tmsk, z_plane_ptr, height); } else if (transpStrip && (flag & dbAllowMaskOr)) { decompressMaskImgOr(mask_ptr, z_plane_ptr, height); } else { decompressMaskImg(mask_ptr, z_plane_ptr, height); } } else { if (!(transpStrip && (flag & dbAllowMaskOr))) for (int h = 0; h < height; h++) mask_ptr[h * _numStrips] = 0; } } } } void GdiNES::decodeMask(int x, int y, const int width, const int height, int stripnr, int numzbuf, const byte *zplane_list[9], bool transpStrip, byte flag, const byte *tmsk_ptr) { byte *mask_ptr = getMaskBuffer(x, y, 1); drawStripNESMask(mask_ptr, stripnr, y, height); } void GdiV1::decodeMask(int x, int y, const int width, const int height, int stripnr, int numzbuf, const byte *zplane_list[9], bool transpStrip, byte flag, const byte *tmsk_ptr) { byte *mask_ptr = getMaskBuffer(x, y, 1); drawStripC64Mask(mask_ptr, stripnr, width, height); } void GdiV2::decodeMask(int x, int y, const int width, const int height, int stripnr, int numzbuf, const byte *zplane_list[9], bool transpStrip, byte flag, const byte *tmsk_ptr) { // Do nothing here for V2 games - zplane was already handled. } #ifndef DISABLE_HE /** * Draw a bitmap onto a virtual screen. This is main drawing method for room backgrounds * used throughout HE71+ versions. * * @note This function essentially is a stripped down & special cased version of * the generic Gdi::drawBitmap() method. */ void Gdi::drawBMAPBg(const byte *ptr, VirtScreen *vs) { const byte *z_plane_ptr; byte *mask_ptr; const byte *zplane_list[9]; const byte *bmap_ptr = _vm->findResourceData(MKID_BE('BMAP'), ptr); assert(bmap_ptr); byte code = *bmap_ptr++; byte *dst = vs->getBackPixels(0, 0); // The following few lines more or less duplicate decompressBitmap(), only // for an area spanning multiple strips. In particular, the codecs 13 & 14 // in decompressBitmap call drawStripHE() _decomp_shr = code % 10; _decomp_mask = 0xFF >> (8 - _decomp_shr); switch (code) { case 134: case 135: case 136: case 137: case 138: drawStripHE(dst, vs->pitch, bmap_ptr, vs->w, vs->h, false); break; case 144: case 145: case 146: case 147: case 148: drawStripHE(dst, vs->pitch, bmap_ptr, vs->w, vs->h, true); break; case 150: fill(dst, vs->pitch, *bmap_ptr, vs->w, vs->h); break; default: // Alternative russian freddi3 uses badly formatted bitmaps debug(0, "Gdi::drawBMAPBg: default case %d", code); } ((ScummEngine_v71he *)_vm)->restoreBackgroundHE(Common::Rect(vs->w, vs->h)); int numzbuf = getZPlanes(ptr, zplane_list, true); if (numzbuf <= 1) return; uint32 offs; for (int stripnr = 0; stripnr < _numStrips; stripnr++) { for (int i = 1; i < numzbuf; i++) { if (!zplane_list[i]) continue; offs = READ_LE_UINT16(zplane_list[i] + stripnr * 2 + 8); mask_ptr = getMaskBuffer(stripnr, 0, i); if (offs) { z_plane_ptr = zplane_list[i] + offs; decompressMaskImg(mask_ptr, z_plane_ptr, vs->h); } } #if 0 // HACK: blit mask(s) onto normal screen. Useful to debug masking for (int i = 0; i < numzbuf; i++) { byte *dst1 = (byte *)vs->pixels + stripnr * 8; byte *dst2 = vs->backBuf + stripnr * 8; mask_ptr = getMaskBuffer(stripnr, 0, i); for (int h = 0; h < vs->h; h++) { int maskbits = *mask_ptr; for (int j = 0; j < 8; j++) { if (maskbits & 0x80) dst1[j] = dst2[j] = 12 + i; maskbits <<= 1; } dst1 += vs->pitch; dst2 += vs->pitch; mask_ptr += _numStrips; } } #endif } } void Gdi::drawBMAPObject(const byte *ptr, VirtScreen *vs, int obj, int x, int y, int w, int h) { const byte *bmap_ptr = _vm->findResourceData(MKID_BE('BMAP'), ptr); assert(bmap_ptr); byte code = *bmap_ptr++; int scrX = _vm->_screenStartStrip * 8; if (code == 8 || code == 9) { Common::Rect rScreen(0, 0, vs->w, vs->h); byte *dst = (byte *)_vm->_virtscr[kMainVirtScreen].backBuf + scrX; Wiz::copyWizImage(dst, bmap_ptr, vs->w, vs->h, x - scrX, y, w, h, &rScreen); } Common::Rect rect1(x, y, x + w, y + h); Common::Rect rect2(scrX, 0, vs->w + scrX, vs->h); if (rect1.intersects(rect2)) { rect1.clip(rect2); rect1.left -= rect2.left; rect1.right -= rect2.left; rect1.top -= rect2.top; rect1.bottom -= rect2.top; ((ScummEngine_v71he *)_vm)->restoreBackgroundHE(rect1); } } void ScummEngine_v70he::restoreBackgroundHE(Common::Rect rect, int dirtybit) { byte *src, *dst; VirtScreen *vs = &_virtscr[kMainVirtScreen]; if (rect.top > vs->h || rect.bottom < 0) return; if (rect.left > vs->w || rect.right < 0) return; rect.left = MAX(0, (int)rect.left); rect.left = MIN((int)rect.left, (int)vs->w - 1); rect.right = MAX(0, (int)rect.right); rect.right = MIN((int)rect.right, (int)vs->w); rect.top = MAX(0, (int)rect.top); rect.top = MIN((int)rect.top, (int)vs->h - 1); rect.bottom = MAX(0, (int)rect.bottom); rect.bottom = MIN((int)rect.bottom, (int)vs->h); const int rw = rect.width(); const int rh = rect.height(); if (rw == 0 || rh == 0) return; src = _virtscr[kMainVirtScreen].getBackPixels(rect.left, rect.top); dst = _virtscr[kMainVirtScreen].getPixels(rect.left, rect.top); assert(rw <= _screenWidth && rw > 0); assert(rh <= _screenHeight && rh > 0); blit(dst, _virtscr[kMainVirtScreen].pitch, src, _virtscr[kMainVirtScreen].pitch, rw, rh); markRectAsDirty(kMainVirtScreen, rect, dirtybit); } #endif /** * Reset the background behind an actor or blast object. */ void Gdi::resetBackground(int top, int bottom, int strip) { VirtScreen *vs = &_vm->_virtscr[kMainVirtScreen]; byte *backbuff_ptr, *bgbak_ptr; int numLinesToProcess; if (top < 0) top = 0; if (bottom > vs->h) bottom = vs->h; if (top >= bottom) return; assert(0 <= strip && strip < _numStrips); if (top < vs->tdirty[strip]) vs->tdirty[strip] = top; if (bottom > vs->bdirty[strip]) vs->bdirty[strip] = bottom; bgbak_ptr = (byte *)vs->backBuf + top * vs->pitch + (strip + vs->xstart/8) * 8; backbuff_ptr = (byte *)vs->pixels + top * vs->pitch + (strip + vs->xstart/8) * 8; numLinesToProcess = bottom - top; if (numLinesToProcess) { if (_vm->isLightOn()) { copy8Col(backbuff_ptr, vs->pitch, bgbak_ptr, numLinesToProcess); } else { clear8Col(backbuff_ptr, vs->pitch, numLinesToProcess); } } } bool Gdi::decompressBitmap(byte *dst, int dstPitch, const byte *src, int numLinesToProcess) { assert(numLinesToProcess); if (_vm->_game.features & GF_16COLOR) { drawStripEGA(dst, dstPitch, src, numLinesToProcess); return false; } if ((_vm->_game.platform == Common::kPlatformAmiga) && (_vm->_game.version >= 4)) _paletteMod = 16; else _paletteMod = 0; byte code = *src++; bool transpStrip = false; _decomp_shr = code % 10; _decomp_mask = 0xFF >> (8 - _decomp_shr); switch (code) { case 1: drawStripRaw(dst, dstPitch, src, numLinesToProcess, false); break; case 2: unkDecode8(dst, dstPitch, src, numLinesToProcess); /* Ender - Zak256/Indy256 */ break; case 3: unkDecode9(dst, dstPitch, src, numLinesToProcess); /* Ender - Zak256/Indy256 */ break; case 4: unkDecode10(dst, dstPitch, src, numLinesToProcess); /* Ender - Zak256/Indy256 */ break; case 7: unkDecode11(dst, dstPitch, src, numLinesToProcess); /* Ender - Zak256/Indy256 */ break; case 8: // Used in 3DO versions of HE games transpStrip = true; drawStrip3DO(dst, dstPitch, src, numLinesToProcess, true); break; case 9: drawStrip3DO(dst, dstPitch, src, numLinesToProcess, false); break; case 10: // Used in Amiga version of Monkey Island 1 drawStripEGA(dst, dstPitch, src, numLinesToProcess); break; case 14: case 15: case 16: case 17: case 18: drawStripBasicV(dst, dstPitch, src, numLinesToProcess, false); break; case 24: case 25: case 26: case 27: case 28: drawStripBasicH(dst, dstPitch, src, numLinesToProcess, false); break; case 34: case 35: case 36: case 37: case 38: transpStrip = true; drawStripBasicV(dst, dstPitch, src, numLinesToProcess, true); break; case 44: case 45: case 46: case 47: case 48: transpStrip = true; drawStripBasicH(dst, dstPitch, src, numLinesToProcess, true); break; case 64: case 65: case 66: case 67: case 68: case 104: case 105: case 106: case 107: case 108: drawStripComplex(dst, dstPitch, src, numLinesToProcess, false); break; case 84: case 85: case 86: case 87: case 88: case 124: case 125: case 126: case 127: case 128: transpStrip = true; drawStripComplex(dst, dstPitch, src, numLinesToProcess, true); break; case 134: case 135: case 136: case 137: case 138: drawStripHE(dst, dstPitch, src, 8, numLinesToProcess, false); break; case 143: // Triggered by Russian water case 144: case 145: case 146: case 147: case 148: transpStrip = true; drawStripHE(dst, dstPitch, src, 8, numLinesToProcess, true); break; case 149: drawStripRaw(dst, dstPitch, src, numLinesToProcess, true); break; default: error("Gdi::decompressBitmap: default case %d", code); } return transpStrip; } void Gdi::decompressMaskImg(byte *dst, const byte *src, int height) const { byte b, c; while (height) { b = *src++; if (b & 0x80) { b &= 0x7F; c = *src++; do { *dst = c; dst += _numStrips; --height; } while (--b && height); } else { do { *dst = *src++; dst += _numStrips; --height; } while (--b && height); } } } void Gdi::decompressTMSK(byte *dst, const byte *tmsk, const byte *src, int height) const { byte srcbits = 0; byte srcFlag = 0; byte maskFlag = 0; byte srcCount = 0; byte maskCount = 0; byte maskbits = 0; while (height) { if (srcCount == 0) { srcCount = *src++; srcFlag = srcCount & 0x80; if (srcFlag) { srcCount &= 0x7F; srcbits = *src++; } } if (srcFlag == 0) { srcbits = *src++; } srcCount--; if (maskCount == 0) { maskCount = *tmsk++; maskFlag = maskCount & 0x80; if (maskFlag) { maskCount &= 0x7F; maskbits = *tmsk++; } } if (maskFlag == 0) { maskbits = *tmsk++; } maskCount--; *dst |= srcbits; *dst &= ~maskbits; dst += _numStrips; height--; } } void Gdi::decompressMaskImgOr(byte *dst, const byte *src, int height) const { byte b, c; while (height) { b = *src++; if (b & 0x80) { b &= 0x7F; c = *src++; do { *dst |= c; dst += _numStrips; --height; } while (--b && height); } else { do { *dst |= *src++; dst += _numStrips; --height; } while (--b && height); } } } static void decodeNESTileData(const byte *src, byte *dest) { int len = READ_LE_UINT16(src); src += 2; const byte *end = src + len; src++; // skip number-of-tiles byte, assume it is correct while (src < end) { byte data = *src++; for (int j = 0; j < (data & 0x7F); j++) *dest++ = (data & 0x80) ? (*src++) : (*src); if (!(data & 0x80)) src++; } } void ScummEngine::decodeNESBaseTiles() { byte *basetiles = getResourceAddress(rtCostume, 37); _NESBaseTiles = basetiles[2]; decodeNESTileData(basetiles, _NESPatTable[1]); } static const int v1MMNEScostTables[2][6] = { /* desc lens offs data gfx pal */ { 25, 27, 29, 31, 33, 35}, { 26, 28, 30, 32, 34, 36} }; void ScummEngine::NES_loadCostumeSet(int n) { int i; _NESCostumeSet = n; _NEScostdesc = getResourceAddress(rtCostume, v1MMNEScostTables[n][0]) + 2; _NEScostlens = getResourceAddress(rtCostume, v1MMNEScostTables[n][1]) + 2; _NEScostoffs = getResourceAddress(rtCostume, v1MMNEScostTables[n][2]) + 2; _NEScostdata = getResourceAddress(rtCostume, v1MMNEScostTables[n][3]) + 2; decodeNESTileData(getResourceAddress(rtCostume, v1MMNEScostTables[n][4]), _NESPatTable[0]); byte *palette = getResourceAddress(rtCostume, v1MMNEScostTables[n][5]) + 2; for (i = 0; i < 16; i++) { byte c = *palette++; if (c == 0x1D) // HACK - switch around colors 0x00 and 0x1D c = 0; // so we don't need a zillion extra checks else if (c == 0)// for determining the proper background color c = 0x1D; _NESPalette[1][i] = c; } } void GdiNES::decodeNESGfx(const byte *room) { const byte *gdata = room + READ_LE_UINT16(room + 0x0A); int tileset = *gdata++; int width = READ_LE_UINT16(room + 0x04); // int height = READ_LE_UINT16(room + 0x06); int i, j, n; // We have narrow room. so expand it if (width < 32) { _vm->_NESStartStrip = (32 - width) >> 1; } else { _vm->_NESStartStrip = 0; } decodeNESTileData(_vm->getResourceAddress(rtCostume, 37 + tileset), _vm->_NESPatTable[1] + _vm->_NESBaseTiles * 16); for (i = 0; i < 16; i++) { byte c = *gdata++; if (c == 0x0D) c = 0x1D; if (c == 0x1D) // HACK - switch around colors 0x00 and 0x1D c = 0; // so we don't need a zillion extra checks else if (c == 0) // for determining the proper background color c = 0x1D; _vm->_NESPalette[0][i] = c; } for (i = 0; i < 16; i++) { _NES.nametable[i][0] = _NES.nametable[i][1] = 0; n = 0; while (n < width) { byte data = *gdata++; for (j = 0; j < (data & 0x7F); j++) _NES.nametable[i][2 + n++] = (data & 0x80) ? (*gdata++) : (*gdata); if (!(data & 0x80)) gdata++; } _NES.nametable[i][width+2] = _NES.nametable[i][width+3] = 0; } memcpy(_NES.nametableObj,_NES.nametable, 16*64); const byte *adata = room + READ_LE_UINT16(room + 0x0C); for (n = 0; n < 64;) { byte data = *adata++; for (j = 0; j < (data & 0x7F); j++) _NES.attributes[n++] = (data & 0x80) ? (*adata++) : (*adata); if (!(n & 7) && (width == 0x1C)) n += 8; if (!(data & 0x80)) adata++; } memcpy(_NES.attributesObj, _NES.attributes, 64); const byte *mdata = room + READ_LE_UINT16(room + 0x0E); int mask = *mdata++; if (mask == 0) { _NES.hasmask = false; return; } _NES.hasmask = true; if (mask != 1) debug(0,"NES room %i has irregular mask count %i",_vm->_currentRoom,mask); int mwidth = *mdata++; for (i = 0; i < 16; i++) { n = 0; while (n < mwidth) { byte data = *mdata++; for (j = 0; j < (data & 0x7F); j++) _NES.masktable[i][n++] = (data & 0x80) ? (*mdata++) : (*mdata); if (!(data & 0x80)) mdata++; } } memcpy(_NES.masktableObj, _NES.masktable, 16*8); } void GdiNES::decodeNESObject(const byte *ptr, int xpos, int ypos, int width, int height) { int x, y; _NES.objX = xpos; // decode tile update data width /= 8; ypos /= 8; height /= 8; for (y = ypos; y < ypos + height; y++) { x = xpos; while (x < xpos + width) { byte len = *ptr++; for (int i = 0; i < (len & 0x7F); i++) _NES.nametableObj[y][2 + x++] = (len & 0x80) ? (*ptr++) : (*ptr); if (!(len & 0x80)) ptr++; } } int ax, ay; // decode attribute update data y = height / 2; ay = ypos; while (y) { ax = xpos + 2; x = 0; int adata = 0; while (x < (width >> 1)) { if (!(x & 3)) adata = *ptr++; byte *dest = &_NES.attributesObj[((ay << 2) & 0x30) | ((ax >> 2) & 0xF)]; int aand = 3; int aor = adata & 3; if (ay & 0x02) { aand <<= 4; aor <<= 4; } if (ax & 0x02) { aand <<= 2; aor <<= 2; } *dest = ((~aand) & *dest) | aor; adata >>= 2; ax += 2; x++; } ay += 2; y--; } // decode mask update data if (!_NES.hasmask) return; int mx, mwidth; int lmask, rmask; mx = *ptr++; mwidth = *ptr++; lmask = *ptr++; rmask = *ptr++; for (y = 0; y < height; ++y) { byte *dest = &_NES.masktableObj[y + ypos][mx]; *dest = (*dest & lmask) | *ptr++; dest++; for (x = 1; x < mwidth; x++) { if (x + 1 == mwidth) *dest = (*dest & rmask) | *ptr++; else *dest = *ptr++; dest++; } } } void GdiNES::drawStripNES(byte *dst, byte *mask, int dstPitch, int stripnr, int top, int height) { top /= 8; height /= 8; int x = stripnr + 2; // NES version has a 2 tile gap on each edge if (_objectMode) x += _NES.objX; // for objects, need to start at the left edge of the object, not the screen if (x > 63) { debug(0,"NES tried to render invalid strip %i",stripnr); return; } for (int y = top; y < top + height; y++) { int palette = ((_objectMode ? _NES.attributesObj : _NES.attributes)[((y << 2) & 0x30) | ((x >> 2) & 0xF)] >> (((y & 2) << 1) | (x & 2))) & 0x3; int tile = (_objectMode ? _NES.nametableObj : _NES.nametable)[y][x]; for (int i = 0; i < 8; i++) { byte c0 = _vm->_NESPatTable[1][tile * 16 + i]; byte c1 = _vm->_NESPatTable[1][tile * 16 + i + 8]; for (int j = 0; j < 8; j++) dst[j] = _vm->_NESPalette[0][((c0 >> (7 - j)) & 1) | (((c1 >> (7 - j)) & 1) << 1) | (palette << 2)]; dst += dstPitch; *mask = c0 | c1; mask += _numStrips; } } } void GdiNES::drawStripNESMask(byte *dst, int stripnr, int top, int height) const { top /= 8; height /= 8; int x = stripnr; // masks, unlike room graphics, should NOT be adjusted if (_objectMode) x += _NES.objX; // for objects, need to start at the left edge of the object, not the screen if (x > 63) { debug(0,"NES tried to mask invalid strip %i",stripnr); return; } for (int y = top; y < top + height; y++) { byte c; if (_NES.hasmask) c = (((_objectMode ? _NES.masktableObj : _NES.masktable)[y][x >> 3] >> (x & 7)) & 1) ? 0xFF : 0x00; else c = 0; for (int i = 0; i < 8; i++) { *dst &= c; dst += _numStrips; } } } void GdiV1::drawStripC64Background(byte *dst, int dstPitch, int stripnr, int height) { int charIdx; height /= 8; for (int y = 0; y < height; y++) { _C64.colors[3] = (_C64.colorMap[y + stripnr * height] & 7); // Check for room color change in V1 zak if (_roomPalette[0] == 255) { _C64.colors[2] = _roomPalette[2]; _C64.colors[1] = _roomPalette[1]; } charIdx = _C64.picMap[y + stripnr * height] * 8; for (int i = 0; i < 8; i++) { byte c = _C64.charMap[charIdx + i]; dst[0] = dst[1] = _C64.colors[(c >> 6) & 3]; dst[2] = dst[3] = _C64.colors[(c >> 4) & 3]; dst[4] = dst[5] = _C64.colors[(c >> 2) & 3]; dst[6] = dst[7] = _C64.colors[(c >> 0) & 3]; dst += dstPitch; } } } void GdiV1::drawStripC64Object(byte *dst, int dstPitch, int stripnr, int width, int height) { int charIdx; height /= 8; width /= 8; for (int y = 0; y < height; y++) { _C64.colors[3] = (_C64.objectMap[(y + height) * width + stripnr] & 7); charIdx = _C64.objectMap[y * width + stripnr] * 8; for (int i = 0; i < 8; i++) { byte c = _C64.charMap[charIdx + i]; dst[0] = dst[1] = _C64.colors[(c >> 6) & 3]; dst[2] = dst[3] = _C64.colors[(c >> 4) & 3]; dst[4] = dst[5] = _C64.colors[(c >> 2) & 3]; dst[6] = dst[7] = _C64.colors[(c >> 0) & 3]; dst += dstPitch; } } } void GdiV1::drawStripC64Mask(byte *dst, int stripnr, int width, int height) const { int maskIdx; height /= 8; width /= 8; for (int y = 0; y < height; y++) { if (_objectMode) maskIdx = _C64.objectMap[(y + 2 * height) * width + stripnr] * 8; else maskIdx = _C64.maskMap[y + stripnr * height] * 8; for (int i = 0; i < 8; i++) { byte c = _C64.maskChar[maskIdx + i]; // V1/C64 masks are inverted compared to what ScummVM expects *dst = c ^ 0xFF; dst += _numStrips; } } } void GdiV1::decodeC64Gfx(const byte *src, byte *dst, int size) const { int x, z; byte color, run, common[4]; for (z = 0; z < 4; z++) { common[z] = *src++; } x = 0; while (x < size) { run = *src++; if (run & 0x80) { color = common[(run >> 5) & 3]; run &= 0x1F; for (z = 0; z <= run; z++) { dst[x++] = color; } } else if (run & 0x40) { run &= 0x3F; color = *src++; for (z = 0; z <= run; z++) { dst[x++] = color; } } else { for (z = 0; z <= run; z++) { dst[x++] = *src++; } } } } /** * Create and fill a table with offsets to the graphic and mask strips in the * given V2 EGA bitmap. * @param src the V2 EGA bitmap * @param width the width of the bitmap * @param height the height of the bitmap * @param table the strip table to fill * @return filled strip table */ StripTable *GdiV2::generateStripTable(const byte *src, int width, int height, StripTable *table) const { // If no strip table was given to use, allocate a new one if (table == 0) table = (StripTable *)calloc(1, sizeof(StripTable)); const byte *bitmapStart = src; byte color = 0, data = 0; int x, y, length = 0; byte run = 1; // Decode the graphics strips, and memorize the run/color values // as well as the byte offset. for (x = 0 ; x < width; x++) { if ((x % 8) == 0) { assert(x / 8 < 160); table->run[x / 8] = run; table->color[x / 8] = color; table->offsets[x / 8] = src - bitmapStart; } for (y = 0; y < height; y++) { if (--run == 0) { data = *src++; if (data & 0x80) { run = data & 0x7f; } else { run = data >> 4; } if (run == 0) { run = *src++; } color = data & 0x0f; } } } // The mask data follows immediately after the graphics. x = 0; y = height; width /= 8; for (;;) { length = *src++; const byte runFlag = length & 0x80; if (runFlag) { length &= 0x7f; data = *src++; } do { if (!runFlag) data = *src++; if (y == height) { assert(x < 120); table->zoffsets[x] = src - bitmapStart - 1; table->zrun[x] = length | runFlag; } if (--y == 0) { if (--width == 0) return table; x++; y = height; } } while (--length); } return table; } void Gdi::drawStripEGA(byte *dst, int dstPitch, const byte *src, int height) const { byte color = 0; int run = 0, x = 0, y = 0, z; while (x < 8) { color = *src++; if (color & 0x80) { run = color & 0x3f; if (color & 0x40) { color = *src++; if (run == 0) { run = *src++; } for (z = 0; z < run; z++) { *(dst + y * dstPitch + x) = (z & 1) ? _roomPalette[color & 0xf] + _paletteMod : _roomPalette[color >> 4] + _paletteMod; y++; if (y >= height) { y = 0; x++; } } } else { if (run == 0) { run = *src++; } for (z = 0; z < run; z++) { *(dst + y * dstPitch + x) = *(dst + y * dstPitch + x - 1); y++; if (y >= height) { y = 0; x++; } } } } else { run = color >> 4; if (run == 0) { run = *src++; } for (z = 0; z < run; z++) { *(dst + y * dstPitch + x) = _roomPalette[color & 0xf] + _paletteMod; y++; if (y >= height) { y = 0; x++; } } } } } #define READ_BIT (shift--, dataBit = data & 1, data >>= 1, dataBit) #define FILL_BITS(n) do { \ if (shift < n) { \ data |= *src++ << shift; \ shift += 8; \ } \ } while (0) // NOTE: drawStripHE is actually very similar to drawStripComplex void Gdi::drawStripHE(byte *dst, int dstPitch, const byte *src, int width, int height, const bool transpCheck) const { static const int delta_color[] = { -4, -3, -2, -1, 1, 2, 3, 4 }; uint32 dataBit, data; byte color; int shift; color = *src++; data = READ_LE_UINT24(src); src += 3; shift = 24; int x = width; while (1) { if (!transpCheck || color != _transparentColor) *dst = _roomPalette[color]; dst++; --x; if (x == 0) { x = width; dst += dstPitch - width; --height; if (height == 0) return; } FILL_BITS(1); if (READ_BIT) { FILL_BITS(1); if (READ_BIT) { FILL_BITS(3); color += delta_color[data & 7]; shift -= 3; data >>= 3; } else { FILL_BITS(_decomp_shr); color = data & _decomp_mask; shift -= _decomp_shr; data >>= _decomp_shr; } } } } #undef READ_BIT #undef FILL_BITS void Gdi::drawStrip3DO(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const { if (height == 0) return; int decSize = height * 8; int curSize = 0; do { uint8 data = *src++; uint8 rle = data & 1; int len = (data >> 1) + 1; len = MIN(decSize, len); decSize -= len; if (!rle) { for (; len > 0; len--, src++, dst++) { if (!transpCheck || *src != _transparentColor) *dst = _roomPalette[*src]; curSize++; if (!(curSize & 7)) dst += dstPitch - 8; // Next row } } else { byte color = *src++; for (; len > 0; len--, dst++) { if (!transpCheck || color != _transparentColor) *dst = _roomPalette[color]; curSize++; if (!(curSize & 7)) dst += dstPitch - 8; // Next row } } } while (decSize > 0); } #define READ_BIT (cl--, bit = bits & 1, bits >>= 1, bit) #define FILL_BITS do { \ if (cl <= 8) { \ bits |= (*src++ << cl); \ cl += 8; \ } \ } while (0) void Gdi::drawStripComplex(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const { byte color = *src++; uint bits = *src++; byte cl = 8; byte bit; byte incm, reps; do { int x = 8; do { FILL_BITS; if (!transpCheck || color != _transparentColor) *dst = _roomPalette[color] + _paletteMod; dst++; againPos: if (!READ_BIT) { } else if (!READ_BIT) { FILL_BITS; color = bits & _decomp_mask; bits >>= _decomp_shr; cl -= _decomp_shr; } else { incm = (bits & 7) - 4; cl -= 3; bits >>= 3; if (incm) { color += incm; } else { FILL_BITS; reps = bits & 0xFF; do { if (!--x) { x = 8; dst += dstPitch - 8; if (!--height) return; } if (!transpCheck || color != _transparentColor) *dst = _roomPalette[color] + _paletteMod; dst++; } while (--reps); bits >>= 8; bits |= (*src++) << (cl - 8); goto againPos; } } } while (--x); dst += dstPitch - 8; } while (--height); } void Gdi::drawStripBasicH(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const { byte color = *src++; uint bits = *src++; byte cl = 8; byte bit; int8 inc = -1; do { int x = 8; do { FILL_BITS; if (!transpCheck || color != _transparentColor) *dst = _roomPalette[color] + _paletteMod; dst++; if (!READ_BIT) { } else if (!READ_BIT) { FILL_BITS; color = bits & _decomp_mask; bits >>= _decomp_shr; cl -= _decomp_shr; inc = -1; } else if (!READ_BIT) { color += inc; } else { inc = -inc; color += inc; } } while (--x); dst += dstPitch - 8; } while (--height); } void Gdi::drawStripBasicV(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const { byte color = *src++; uint bits = *src++; byte cl = 8; byte bit; int8 inc = -1; int x = 8; do { int h = height; do { FILL_BITS; if (!transpCheck || color != _transparentColor) *dst = _roomPalette[color] + _paletteMod; dst += dstPitch; if (!READ_BIT) { } else if (!READ_BIT) { FILL_BITS; color = bits & _decomp_mask; bits >>= _decomp_shr; cl -= _decomp_shr; inc = -1; } else if (!READ_BIT) { color += inc; } else { inc = -inc; color += inc; } } while (--h); dst -= _vertStripNextInc; } while (--x); } #undef READ_BIT #undef FILL_BITS /* Ender - Zak256/Indy256 decoders */ #define READ_BIT_256 \ do { \ if ((mask <<= 1) == 256) { \ buffer = *src++; \ mask = 1; \ } \ bits = ((buffer & mask) != 0); \ } while (0) #define READ_N_BITS(n, c) \ do { \ c = 0; \ for (int b = 0; b < n; b++) { \ READ_BIT_256; \ c += (bits << b); \ } \ } while (0) #define NEXT_ROW \ do { \ dst += dstPitch; \ if (--h == 0) { \ if (!--x) \ return; \ dst -= _vertStripNextInc; \ h = height; \ } \ } while (0) void Gdi::drawStripRaw(byte *dst, int dstPitch, const byte *src, int height, const bool transpCheck) const { int x; if (_vm->_game.features & GF_OLD256) { uint h = height; x = 8; for (;;) { *dst = _roomPalette[*src++]; NEXT_ROW; } } else { do { for (x = 0; x < 8; x ++) { byte color = *src++; if (!transpCheck || color != _transparentColor) dst[x] = _roomPalette[color] + _paletteMod; } dst += dstPitch; } while (--height); } } void Gdi::unkDecode8(byte *dst, int dstPitch, const byte *src, int height) const { uint h = height; int x = 8; for (;;) { uint run = (*src++) + 1; byte color = *src++; do { *dst = _roomPalette[color]; NEXT_ROW; } while (--run); } } void Gdi::unkDecode9(byte *dst, int dstPitch, const byte *src, int height) const { byte c, bits, color, run; int i; uint buffer = 0, mask = 128; int h = height; i = run = 0; int x = 8; for (;;) { READ_N_BITS(4, c); switch (c >> 2) { case 0: READ_N_BITS(4, color); for (i = 0; i < ((c & 3) + 2); i++) { *dst = _roomPalette[run * 16 + color]; NEXT_ROW; } break; case 1: for (i = 0; i < ((c & 3) + 1); i++) { READ_N_BITS(4, color); *dst = _roomPalette[run * 16 + color]; NEXT_ROW; } break; case 2: READ_N_BITS(4, run); break; } } } void Gdi::unkDecode10(byte *dst, int dstPitch, const byte *src, int height) const { int i; byte local_palette[256], numcolors = *src++; uint h = height; for (i = 0; i < numcolors; i++) local_palette[i] = *src++; int x = 8; for (;;) { byte color = *src++; if (color < numcolors) { *dst = _roomPalette[local_palette[color]]; NEXT_ROW; } else { uint run = color - numcolors + 1; color = *src++; do { *dst = _roomPalette[color]; NEXT_ROW; } while (--run); } } } void Gdi::unkDecode11(byte *dst, int dstPitch, const byte *src, int height) const { int bits, i; uint buffer = 0, mask = 128; byte inc = 1, color = *src++; int x = 8; do { int h = height; do { *dst = _roomPalette[color]; dst += dstPitch; for (i = 0; i < 3; i++) { READ_BIT_256; if (!bits) break; } switch (i) { case 1: inc = -inc; color -= inc; break; case 2: color -= inc; break; case 3: inc = 1; READ_N_BITS(8, color); break; } } while (--h); dst -= _vertStripNextInc; } while (--x); } #undef NEXT_ROW #undef READ_BIT_256 #pragma mark - #pragma mark --- Transition effects --- #pragma mark - void ScummEngine::fadeIn(int effect) { if (_disableFadeInEffect) { // fadeIn() calls can be disabled in TheDig after a SMUSH movie // has been played. Like the original interpreter, we introduce // an extra flag to handle this. _disableFadeInEffect = false; _doEffect = false; _screenEffectFlag = true; return; } updatePalette(); switch (effect) { case 0: // seems to do nothing break; case 1: case 2: case 3: case 4: case 5: case 6: // Some of the transition effects won't work properly unless // the screen is marked as clean first. At first I thought I // could safely do this every time fadeIn() was called, but // that broke the FOA intro. Probably other things as well. // // Hopefully it's safe to do it at this point, at least. _virtscr[kMainVirtScreen].setDirtyRange(0, 0); transitionEffect(effect - 1); break; case 128: unkScreenEffect6(); break; case 129: break; case 130: case 131: case 132: case 133: scrollEffect(133 - effect); break; case 134: dissolveEffect(1, 1); break; case 135: dissolveEffect(1, _virtscr[kMainVirtScreen].h); break; default: error("Unknown screen effect, %d", effect); } _screenEffectFlag = true; } void ScummEngine::fadeOut(int effect) { VirtScreen *vs = &_virtscr[kMainVirtScreen]; vs->setDirtyRange(0, 0); if (_game.version < 7) camera._last.x = camera._cur.x; // TheDig can disable fadeIn(), and may call fadeOut() several times // successively. Disabling the _screenEffectFlag check forces the screen // to get cleared. This fixes glitches, at least, in the first cutscenes // when bypassed of FT and TheDig. if ((_game.version == 7 || _screenEffectFlag) && effect != 0) { // Fill screen 0 with black memset(vs->getPixels(0, 0), 0, vs->pitch * vs->h); // Fade to black with the specified effect, if any. switch (effect) { case 1: case 2: case 3: case 4: case 5: case 6: transitionEffect(effect - 1); break; case 128: unkScreenEffect6(); break; case 129: // Just blit screen 0 to the display (i.e. display will be black) vs->setDirtyRange(0, vs->h); updateDirtyScreen(kMainVirtScreen); break; case 134: dissolveEffect(1, 1); break; case 135: dissolveEffect(1, _virtscr[kMainVirtScreen].h); break; default: error("fadeOut: default case %d", effect); } } // Update the palette at the end (once we faded to black) to avoid // some nasty effects when the palette is changed updatePalette(); _screenEffectFlag = false; } /** * Perform a transition effect. There are four different effects possible: * 0: Iris effect * 1: Box wipe (a black box expands from the upper-left corner to the lower-right corner) * 2: Box wipe (a black box expands from the lower-right corner to the upper-left corner) * 3: Inverse box wipe * All effects operate on 8x8 blocks of the screen. These blocks are updated * in a certain order; the exact order determines how the effect appears to the user. * @param a the transition effect to perform */ void ScummEngine::transitionEffect(int a) { int delta[16]; // Offset applied during each iteration int tab_2[16]; int i, j; int bottom; int l, t, r, b; const int height = MIN((int)_virtscr[kMainVirtScreen].h, _screenHeight); const int delay = (VAR_FADE_DELAY != 0xFF) ? VAR(VAR_FADE_DELAY) * kFadeDelay : kPictureDelay; for (i = 0; i < 16; i++) { delta[i] = transitionEffects[a].deltaTable[i]; j = transitionEffects[a].stripTable[i]; if (j == 24) j = height / 8 - 1; tab_2[i] = j; } bottom = height / 8; for (j = 0; j < transitionEffects[a].numOfIterations; j++) { for (i = 0; i < 4; i++) { l = tab_2[i * 4]; t = tab_2[i * 4 + 1]; r = tab_2[i * 4 + 2]; b = tab_2[i * 4 + 3]; if (t == b) { while (l <= r) { if (l >= 0 && l < _gdi->_numStrips && t < bottom) { _virtscr[kMainVirtScreen].tdirty[l] = _screenTop + t * 8; _virtscr[kMainVirtScreen].bdirty[l] = _screenTop + (b + 1) * 8; } l++; } } else { if (l < 0 || l >= _gdi->_numStrips || b <= t) continue; if (b > bottom) b = bottom; if (t < 0) t = 0; _virtscr[kMainVirtScreen].tdirty[l] = _screenTop + t * 8; _virtscr[kMainVirtScreen].bdirty[l] = _screenTop + (b + 1) * 8; } updateDirtyScreen(kMainVirtScreen); } for (i = 0; i < 16; i++) tab_2[i] += delta[i]; // Draw the current state to the screen and wait a few secs so the // user can watch the effect taking place. waitForTimer(delay); } } /** * Update width*height areas of the screen, in random order, until the whole * screen has been updated. For instance: * * dissolveEffect(1, 1) produces a pixel-by-pixel dissolve * dissolveEffect(8, 8) produces a square-by-square dissolve * dissolveEffect(virtsrc[0].width, 1) produces a line-by-line dissolve */ void ScummEngine::dissolveEffect(int width, int height) { VirtScreen *vs = &_virtscr[kMainVirtScreen]; int *offsets; int blits_before_refresh, blits; int x, y; int w, h; int i; // There's probably some less memory-hungry way of doing this. But // since we're only dealing with relatively small images, it shouldn't // be too bad. w = vs->w / width; h = vs->h / height; // When used correctly, vs->width % width and vs->height % height // should both be zero, but just to be safe... if (vs->w % width) w++; if (vs->h % height) h++; offsets = (int *) malloc(w * h * sizeof(int)); if (offsets == NULL) error("dissolveEffect: out of memory"); // Create a permutation of offsets into the frame buffer if (width == 1 && height == 1) { // Optimized case for pixel-by-pixel dissolve for (i = 0; i < vs->w * vs->h; i++) offsets[i] = i; for (i = 1; i < w * h; i++) { int j; j = _rnd.getRandomNumber(i - 1); offsets[i] = offsets[j]; offsets[j] = i; } } else { int *offsets2; for (i = 0, x = 0; x < vs->w; x += width) for (y = 0; y < vs->h; y += height) offsets[i++] = y * vs->pitch + x; offsets2 = (int *) malloc(w * h * sizeof(int)); if (offsets2 == NULL) error("dissolveEffect: out of memory"); memcpy(offsets2, offsets, w * h * sizeof(int)); for (i = 1; i < w * h; i++) { int j; j = _rnd.getRandomNumber(i - 1); offsets[i] = offsets[j]; offsets[j] = offsets2[i]; } free(offsets2); } // Blit the image piece by piece to the screen. The idea here is that // the whole update should take about a quarter of a second, assuming // most of the time is spent in waitForTimer(). It looks good to me, // but might still need some tuning. blits = 0; blits_before_refresh = (3 * w * h) / 25; // Speed up the effect for CD Loom since it uses it so often. I don't // think the original had any delay at all, so on modern hardware it // wasn't even noticeable. if (_game.id == GID_LOOM && (_game.version == 4)) blits_before_refresh *= 2; for (i = 0; i < w * h; i++) { x = offsets[i] % vs->pitch; y = offsets[i] / vs->pitch; if (_useCJKMode && _textSurfaceMultiplier == 2) { int m = _textSurfaceMultiplier; byte *dst = _fmtownsBuf + x * m + y * m * _screenWidth * m; scale2x(dst, _screenWidth * m, vs->getPixels(x, y), vs->pitch, width, height); _system->copyRectToScreen(dst, _screenWidth * m, x * m, (y + vs->topline) * m, width * m, height * m); } else { _system->copyRectToScreen(vs->getPixels(x, y), vs->pitch, x, y + vs->topline, width, height); } if (++blits >= blits_before_refresh) { blits = 0; waitForTimer(30); } } free(offsets); if (blits != 0) { waitForTimer(30); } } void ScummEngine::scrollEffect(int dir) { VirtScreen *vs = &_virtscr[kMainVirtScreen]; int x, y; int step; const int delay = (VAR_FADE_DELAY != 0xFF) ? VAR(VAR_FADE_DELAY) * kFadeDelay : kPictureDelay; if ((dir == 0) || (dir == 1)) step = vs->h; else step = vs->w; step = (step * delay) / kScrolltime; byte *src; int m = _textSurfaceMultiplier; int vsPitch = vs->pitch; switch (dir) { case 0: //up y = 1 + step; while (y < vs->h) { moveScreen(0, -step, vs->h); src = vs->getPixels(0, y - step); if (_useCJKMode && m == 2) { int x1 = 0, y1 = vs->h - step; byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m; scale2x(dst, _screenWidth * m, src, vs->pitch, vs->w, step); src = dst; vsPitch = _screenWidth * 2; } _system->copyRectToScreen(src, vsPitch, 0 * m, (vs->h - step) * m, vs->w * m, step * m); _system->updateScreen(); waitForTimer(delay); y += step; } break; case 1: // down y = 1 + step; while (y < vs->h) { moveScreen(0, step, vs->h); src = vs->getPixels(0, vs->h - y); if (_useCJKMode && m == 2) { int x1 = 0, y1 = 0; byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m; scale2x(dst, _screenWidth * m, src, vs->pitch, vs->w, step); src = dst; vsPitch = _screenWidth * 2; } _system->copyRectToScreen(src, vsPitch, 0, 0, vs->w * m, step * m); _system->updateScreen(); waitForTimer(delay); y += step; } break; case 2: // left x = 1 + step; while (x < vs->w) { moveScreen(-step, 0, vs->h); src = vs->getPixels(x - step, 0); if (_useCJKMode && m == 2) { int x1 = vs->w - step, y1 = 0; byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m; scale2x(dst, _screenWidth * m, src, vs->pitch, step, vs->h); src = dst; vsPitch = _screenWidth * 2; } _system->copyRectToScreen(src, vsPitch, (vs->w - step) * m, 0, step * m, vs->h * m); _system->updateScreen(); waitForTimer(delay); x += step; } break; case 3: // right x = 1 + step; while (x < vs->w) { moveScreen(step, 0, vs->h); src = vs->getPixels(vs->w - x, 0); if (_useCJKMode && m == 2) { int x1 = 0, y1 = 0; byte *dst = _fmtownsBuf + x1 * m + y1 * m * _screenWidth * m; scale2x(dst, _screenWidth * m, src, vs->pitch, step, vs->h); src = dst; vsPitch = _screenWidth * 2; } _system->copyRectToScreen(src, vsPitch, 0, 0, step, vs->h); _system->updateScreen(); waitForTimer(delay); x += step; } break; } } void ScummEngine::unkScreenEffect6() { // CD Loom (but not EGA Loom!) uses a more fine-grained dissolve if (_game.id == GID_LOOM && (_game.version == 4)) dissolveEffect(1, 1); else dissolveEffect(8, 4); } } // End of namespace Scumm