diff options
author | Max Horn | 2006-02-11 22:45:04 +0000 |
---|---|---|
committer | Max Horn | 2006-02-11 22:45:04 +0000 |
commit | 26ee630756ebdd7c96bccede0881a8c8b98e8f2b (patch) | |
tree | 26e378d5cf990a2b81c2c96e9e683a7f333b62e8 /engines/sword2 | |
parent | 2a9a0d4211b1ea5723f1409d91cb95de8984429e (diff) | |
download | scummvm-rg350-26ee630756ebdd7c96bccede0881a8c8b98e8f2b.tar.gz scummvm-rg350-26ee630756ebdd7c96bccede0881a8c8b98e8f2b.tar.bz2 scummvm-rg350-26ee630756ebdd7c96bccede0881a8c8b98e8f2b.zip |
Moved engines to the new engines/ directory
svn-id: r20582
Diffstat (limited to 'engines/sword2')
55 files changed, 23606 insertions, 0 deletions
diff --git a/engines/sword2/_mouse.cpp b/engines/sword2/_mouse.cpp new file mode 100644 index 0000000000..db0cf00f73 --- /dev/null +++ b/engines/sword2/_mouse.cpp @@ -0,0 +1,247 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/system.h" +#include "common/stream.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/mouse.h" + +namespace Sword2 { + +// This is the maximum mouse cursor size in the SDL backend +#define MAX_MOUSE_W 80 +#define MAX_MOUSE_H 80 + +#define MOUSEFLASHFRAME 6 + +void Mouse::decompressMouse(byte *decomp, byte *comp, uint8 frame, int width, int height, int pitch, int xOff, int yOff) { + int32 size = width * height; + int32 i = 0; + int x = 0; + int y = 0; + + comp = comp + READ_LE_UINT32(comp + frame * 4) - MOUSE_ANIM_HEADER_SIZE; + + while (i < size) { + if (*comp > 183) { + decomp[(y + yOff) * pitch + x + xOff] = *comp++; + if (++x >= width) { + x = 0; + y++; + } + i++; + } else { + x += *comp; + while (x >= width) { + y++; + x -= width; + } + i += *comp++; + } + } +} + +void Mouse::drawMouse() { + byte mouseData[MAX_MOUSE_W * MAX_MOUSE_H]; + + if (!_mouseAnim.data && !_luggageAnim.data) + return; + + // When an object is used in the game, the mouse cursor should be a + // combination of a standard mouse cursor and a luggage cursor. + // + // However, judging by the original code luggage cursors can also + // appear on their own. I have no idea which cases though. + + uint16 mouse_width = 0; + uint16 mouse_height = 0; + uint16 hotspot_x = 0; + uint16 hotspot_y = 0; + int deltaX = 0; + int deltaY = 0; + + if (_mouseAnim.data) { + hotspot_x = _mouseAnim.xHotSpot; + hotspot_y = _mouseAnim.yHotSpot; + mouse_width = _mouseAnim.mousew; + mouse_height = _mouseAnim.mouseh; + } + + if (_luggageAnim.data) { + if (!_mouseAnim.data) { + hotspot_x = _luggageAnim.xHotSpot; + hotspot_y = _luggageAnim.yHotSpot; + } + if (_luggageAnim.mousew > mouse_width) + mouse_width = _luggageAnim.mousew; + if (_luggageAnim.mouseh > mouse_height) + mouse_height = _luggageAnim.mouseh; + } + + if (_mouseAnim.data && _luggageAnim.data) { + deltaX = _mouseAnim.xHotSpot - _luggageAnim.xHotSpot; + deltaY = _mouseAnim.yHotSpot - _luggageAnim.yHotSpot; + } + + assert(deltaX >= 0); + assert(deltaY >= 0); + + // HACK for maximum cursor size. (The SDL backend imposes this + // restriction) + + if (mouse_width + deltaX > MAX_MOUSE_W) + deltaX = 80 - mouse_width; + if (mouse_height + deltaY > MAX_MOUSE_H) + deltaY = 80 - mouse_height; + + mouse_width += deltaX; + mouse_height += deltaY; + + if ((uint32)(mouse_width * mouse_height) > sizeof(mouseData)) { + warning("Mouse cursor too large"); + return; + } + + memset(mouseData, 0, mouse_width * mouse_height); + + if (_luggageAnim.data) + decompressMouse(mouseData, _luggageAnim.data, 0, + _luggageAnim.mousew, _luggageAnim.mouseh, + mouse_width, deltaX, deltaY); + + if (_mouseAnim.data) + decompressMouse(mouseData, _mouseAnim.data, _mouseFrame, + _mouseAnim.mousew, _mouseAnim.mouseh, mouse_width); + + _vm->_system->setMouseCursor(mouseData, mouse_width, mouse_height, hotspot_x, hotspot_y, 0); +} + +/** + * Animates the current mouse pointer + */ + +int32 Mouse::animateMouse() { + uint8 prevMouseFrame = _mouseFrame; + + if (!_mouseAnim.data) + return RDERR_UNKNOWN; + + if (++_mouseFrame == _mouseAnim.noAnimFrames) + _mouseFrame = MOUSEFLASHFRAME; + + if (_mouseFrame != prevMouseFrame) + drawMouse(); + + return RD_OK; +} + +/** + * Sets the mouse cursor animation. + * @param ma a pointer to the animation data, or NULL to clear the current one + * @param size the size of the mouse animation data + * @param mouseFlash RDMOUSE_FLASH or RDMOUSE_NOFLASH, depending on whether + * or not there is a lead-in animation + */ + +int32 Mouse::setMouseAnim(byte *ma, int32 size, int32 mouseFlash) { + free(_mouseAnim.data); + _mouseAnim.data = NULL; + + if (ma) { + if (mouseFlash == RDMOUSE_FLASH) + _mouseFrame = 0; + else + _mouseFrame = MOUSEFLASHFRAME; + + Common::MemoryReadStream readS(ma, size); + + _mouseAnim.runTimeComp = readS.readByte(); + _mouseAnim.noAnimFrames = readS.readByte(); + _mouseAnim.xHotSpot = readS.readSByte(); + _mouseAnim.yHotSpot = readS.readSByte(); + _mouseAnim.mousew = readS.readByte(); + _mouseAnim.mouseh = readS.readByte(); + + _mouseAnim.data = (byte *)malloc(size - MOUSE_ANIM_HEADER_SIZE); + if (!_mouseAnim.data) + return RDERR_OUTOFMEMORY; + + readS.read(_mouseAnim.data, size - MOUSE_ANIM_HEADER_SIZE); + + animateMouse(); + drawMouse(); + + _vm->_system->showMouse(true); + } else { + if (_luggageAnim.data) + drawMouse(); + else + _vm->_system->showMouse(false); + } + + return RD_OK; +} + +/** + * Sets the "luggage" animation to accompany the mouse animation. Luggage + * sprites are of the same format as mouse sprites. + * @param ma a pointer to the animation data, or NULL to clear the current one + * @param size the size of the animation data + */ + +int32 Mouse::setLuggageAnim(byte *ma, int32 size) { + free(_luggageAnim.data); + _luggageAnim.data = NULL; + + if (ma) { + Common::MemoryReadStream readS(ma, size); + + _luggageAnim.runTimeComp = readS.readByte(); + _luggageAnim.noAnimFrames = readS.readByte(); + _luggageAnim.xHotSpot = readS.readSByte(); + _luggageAnim.yHotSpot = readS.readSByte(); + _luggageAnim.mousew = readS.readByte(); + _luggageAnim.mouseh = readS.readByte(); + + _luggageAnim.data = (byte *)malloc(size - MOUSE_ANIM_HEADER_SIZE); + if (!_luggageAnim.data) + return RDERR_OUTOFMEMORY; + + readS.read(_luggageAnim.data, size - MOUSE_ANIM_HEADER_SIZE); + + animateMouse(); + drawMouse(); + + _vm->_system->showMouse(true); + } else { + if (_mouseAnim.data) + drawMouse(); + else + _vm->_system->showMouse(false); + } + + return RD_OK; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/animation.cpp b/engines/sword2/animation.cpp new file mode 100644 index 0000000000..2ba5df6792 --- /dev/null +++ b/engines/sword2/animation.cpp @@ -0,0 +1,535 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/file.h" +#include "common/config-manager.h" +#include "common/system.h" +#include "sound/vorbis.h" +#include "sound/mp3.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/maketext.h" +#include "sword2/resman.h" +#include "sword2/sound.h" +#include "sword2/animation.h" + +namespace Sword2 { + +AnimationState::AnimationState(Sword2Engine *vm) + : BaseAnimationState(vm->_mixer, vm->_system, 640, 480), _vm(vm) { +} + +AnimationState::~AnimationState() { +} + +#ifdef BACKEND_8BIT + +void AnimationState::setPalette(byte *pal) { + _vm->_screen->setPalette(0, 256, pal, RDPAL_INSTANT); +} + +#else + +void AnimationState::drawTextObject(SpriteInfo *s, byte *src) { + OverlayColor *dst = _overlay + RENDERWIDE * s->y + s->x; + + // FIXME: These aren't the "right" colours, but look good to me. + + OverlayColor pen = _sys->RGBToColor(255, 255, 255); + OverlayColor border = _sys->RGBToColor(0, 0, 0); + + for (int y = 0; y < s->h; y++) { + for (int x = 0; x < s->w; x++) { + switch (src[x]) { + case 1: + dst[x] = border; + break; + case 255: + dst[x] = pen; + break; + default: + break; + } + } + dst += RENDERWIDE; + src += s->w; + } +} + +#endif + +void AnimationState::clearScreen() { +#ifdef BACKEND_8BIT + memset(_vm->_screen->getScreen(), 0, _movieWidth * _movieHeight); +#else + OverlayColor black = _sys->RGBToColor(0, 0, 0); + + for (int i = 0; i < _movieWidth * _movieHeight; i++) + _overlay[i] = black; +#endif +} + +void AnimationState::updateScreen() { +#ifdef BACKEND_8BIT + byte *buf = _vm->_screen->getScreen() + ((480 - _movieHeight) / 2) * RENDERWIDE + (640 - _movieWidth) / 2; + + _vm->_system->copyRectToScreen(buf, _movieWidth, (640 - _movieWidth) / 2, (480 - _movieHeight) / 2, _movieWidth, _movieHeight); +#else + _sys->copyRectToOverlay(_overlay, _movieWidth, 0, 0, _movieWidth, _movieHeight); +#endif + _vm->_system->updateScreen(); +} + +void AnimationState::drawYUV(int width, int height, byte *const *dat) { +#ifdef BACKEND_8BIT + _vm->_screen->plotYUV(_lut, width, height, dat); +#else + plotYUV(width, height, dat); +#endif +} + +MovieInfo MoviePlayer::_movies[] = { + { "carib", 222, false }, + { "escape", 187, false }, + { "eye", 248, false }, + { "finale", 1485, false }, + { "guard", 75, false }, + { "intro", 1800, false }, + { "jungle", 186, false }, + { "museum", 167, false }, + { "pablo", 75, false }, + { "pyramid", 60, false }, + { "quaram", 184, false }, + { "river", 656, false }, + { "sailing", 138, false }, + { "shaman", 788, true }, + { "stone1", 34, false }, + { "stone2", 282, false }, + { "stone3", 65, false }, + { "demo", 60, false }, + { "enddemo", 110, false } +}; + +MoviePlayer::MoviePlayer(Sword2Engine *vm) + : _vm(vm), _snd(_vm->_mixer), _sys(_vm->_system), _textSurface(NULL) { +} + +void MoviePlayer::openTextObject(MovieTextObject *obj) { + if (obj->textSprite) + _vm->_screen->createSurface(obj->textSprite, &_textSurface); +} + +void MoviePlayer::closeTextObject(MovieTextObject *obj) { + if (_textSurface) { + _vm->_screen->deleteSurface(_textSurface); + _textSurface = NULL; + } +} + +void MoviePlayer::drawTextObject(AnimationState *anim, MovieTextObject *obj) { + if (obj->textSprite && _textSurface) { +#ifdef BACKEND_8BIT + _vm->_screen->drawSurface(obj->textSprite, _textSurface); +#else + if (anim) + anim->drawTextObject(obj->textSprite, _textSurface); + else + _vm->_screen->drawSurface(obj->textSprite, _textSurface); +#endif + } +} + +/** + * Plays an animated cutscene. + * @param filename the file name of the cutscene file + * @param text the subtitles and voiceovers for the cutscene + * @param leadInRes lead-in music resource id + * @param leadOutRes lead-out music resource id + */ + +int32 MoviePlayer::play(const char *filename, MovieTextObject *text[], int32 leadInRes, int32 leadOutRes) { + Audio::SoundHandle leadInHandle; + + // This happens if the user quits during the "eye" smacker + if (_vm->_quit) + return RD_OK; + + if (leadInRes) { + byte *leadIn = _vm->_resman->openResource(leadInRes); + uint32 leadInLen = _vm->_resman->fetchLen(leadInRes) - ResHeader::size(); + + assert(_vm->_resman->fetchType(leadIn) == WAV_FILE); + + leadIn += ResHeader::size(); + + _vm->_sound->playFx(&leadInHandle, leadIn, leadInLen, Audio::Mixer::kMaxChannelVolume, 0, false, Audio::Mixer::kMusicSoundType); + } + + byte *leadOut = NULL; + uint32 leadOutLen = 0; + + if (leadOutRes) { + leadOut = _vm->_resman->openResource(leadOutRes); + leadOutLen = _vm->_resman->fetchLen(leadOutRes) - ResHeader::size(); + + assert(_vm->_resman->fetchType(leadOut) == WAV_FILE); + + leadOut += ResHeader::size(); + } + + _leadOutFrame = (uint)-1; + + int i; + + for (i = 0; i < ARRAYSIZE(_movies); i++) { + if (scumm_stricmp(filename, _movies[i].name) == 0) { + _seamless = _movies[i].seamless; + if (_movies[i].frames > 60) + _leadOutFrame = _movies[i].frames - 60; + break; + } + } + + if (i == ARRAYSIZE(_movies)) + warning("Unknown movie, '%s'", filename); + +#ifdef USE_MPEG2 + playMPEG(filename, text, leadOut, leadOutLen); +#else + // No MPEG2? Use the old 'Narration Only' hack + playDummy(filename, text, leadOut, leadOutLen); +#endif + + _snd->stopHandle(leadInHandle); + + // Wait for the lead-out to stop, if there is any. Though don't do it + // for seamless movies, since they are meant to blend into the rest of + // the game. + + if (!_seamless) { + while (_vm->_mixer->isSoundHandleActive(_leadOutHandle)) { + _vm->_screen->updateDisplay(); + _vm->_system->delayMillis(30); + } + } + + if (leadInRes) + _vm->_resman->closeResource(leadInRes); + + if (leadOutRes) + _vm->_resman->closeResource(leadOutRes); + + return RD_OK; +} + +void MoviePlayer::playMPEG(const char *filename, MovieTextObject *text[], byte *leadOut, uint32 leadOutLen) { + uint frameCounter = 0, textCounter = 0; + Audio::SoundHandle handle; + bool skipCutscene = false, textVisible = false; + uint32 flags = Audio::Mixer::FLAG_16BITS; + bool startNextText = false; + + byte oldPal[256 * 4]; + memcpy(oldPal, _vm->_screen->getPalette(), sizeof(oldPal)); + + AnimationState *anim = new AnimationState(_vm); + + if (!anim->init(filename)) { + delete anim; + // Missing Files? Use the old 'Narration Only' hack + playDummy(filename, text, leadOut, leadOutLen); + return; + } + + // Clear the screen, because whatever is on it will be visible when the + // overlay is removed. And if there isn't an overlay, we don't want it + // to be visible during the cutscene. (Not all cutscenes cover the + // entire screen.) + _vm->_screen->clearScene(); + _vm->_screen->updateDisplay(); + +#ifndef SCUMM_BIG_ENDIAN + flags |= Audio::Mixer::FLAG_LITTLE_ENDIAN; +#endif + + while (!skipCutscene && anim->decodeFrame()) { + // The frame has been drawn. Now draw the subtitles, if any, + // before updating the screen. + + if (text && text[textCounter]) { + if (frameCounter == text[textCounter]->startFrame) { + openTextObject(text[textCounter]); + textVisible = true; + + if (text[textCounter]->speech) { + startNextText = true; + } + } + + if (startNextText && !_snd->isSoundHandleActive(handle)) { + _snd->playRaw(&handle, text[textCounter]->speech, text[textCounter]->speechBufferSize, 22050, flags); + startNextText = false; + } + + if (frameCounter == text[textCounter]->endFrame) { + closeTextObject(text[textCounter]); + textCounter++; + textVisible = false; + } + + if (textVisible) + drawTextObject(anim, text[textCounter]); + } + + anim->updateScreen(); + frameCounter++; + + if (frameCounter == _leadOutFrame && leadOut) + _vm->_sound->playFx(&_leadOutHandle, leadOut, leadOutLen, Audio::Mixer::kMaxChannelVolume, 0, false, Audio::Mixer::kMusicSoundType); + + OSystem::Event event; + while (_sys->pollEvent(event)) { + switch (event.type) { +#ifndef BACKEND_8BIT + case OSystem::EVENT_SCREEN_CHANGED: + anim->buildLookup(); + break; +#endif + case OSystem::EVENT_KEYDOWN: + if (event.kbd.keycode == 27) + skipCutscene = true; + break; + case OSystem::EVENT_QUIT: + _vm->closeGame(); + skipCutscene = true; + break; + default: + break; + } + } + } + + if (!skipCutscene) { + // Sleep for one frame so that the last frame is displayed. + _sys->delayMillis(1000 / 12); + } + + if (!_seamless) { + // Most movies fade to black on their own, but not all of them. + // Since we may be hanging around in the cutscene player for a + // while longer, waiting for the lead-out sound to finish, + // paint the overlay black. + + anim->clearScreen(); + } + + // If the speech is still playing, redraw the subtitles. At least in + // the English version this is most noticeable in the "carib" cutscene. + + if (textVisible && _snd->isSoundHandleActive(handle)) + drawTextObject(anim, text[textCounter]); + + if (text) + closeTextObject(text[textCounter]); + + anim->updateScreen(); + + // Wait for the voice to stop playing. This is to make sure that we + // don't cut off the speech in mid-sentence, and - even more + // importantly - that we don't free the sound buffer while it's in use. + + if (skipCutscene) + _snd->stopHandle(handle); + + while (_snd->isSoundHandleActive(handle)) { + _vm->_screen->updateDisplay(false); + _sys->delayMillis(100); + } + + if (!_seamless) { + // Clear the screen again + anim->clearScreen(); + anim->updateScreen(); + } + + _vm->_screen->setPalette(0, 256, oldPal, RDPAL_INSTANT); + + delete anim; +} + +/** + * This just plays the cutscene with voiceovers / subtitles, in case the files + * are missing. + */ + +void MoviePlayer::playDummy(const char *filename, MovieTextObject *text[], byte *leadOut, uint32 leadOutLen) { + if (!text) + return; + + int frameCounter = 0, textCounter = 0; + + byte oldPal[256 * 4]; + byte tmpPal[256 * 4]; + + _vm->_screen->clearScene(); + + // HACK: Draw instructions + // + // I'm using the the menu area, because that's unlikely to be touched + // by anything else during the cutscene. + + memset(_vm->_screen->getScreen(), 0, _vm->_screen->getScreenWide() * MENUDEEP); + + byte *data; + + // Russian version substituted latin characters with cyrillic. That's + // why it renders completely unreadable with default message + if (Common::parseLanguage(ConfMan.get("language")) == Common::RU_RUS) { + byte msg[] = "Po\344uk - to\344\345ko pev\345: hagmute k\344abuwy Ucke\343n, u\344u nocetute ca\343t npoekta u ckava\343te budeo po\344uku"; + data = _vm->_fontRenderer->makeTextSprite(msg, RENDERWIDE, 255, _vm->_speechFontId); + } else { + // TODO: Translate message to other languages? +#ifdef USE_MPEG2 + byte msg[] = "Cutscene - Narration Only: Press ESC to exit, or visit www.scummvm.org to download cutscene videos"; +#else + byte msg[] = "Cutscene - Narration Only: Press ESC to exit, or recompile ScummVM with MPEG2 support"; +#endif + + data = _vm->_fontRenderer->makeTextSprite(msg, RENDERWIDE, 255, _vm->_speechFontId); + } + + FrameHeader frame_head; + SpriteInfo msgSprite; + byte *msgSurface; + + frame_head.read(data); + + msgSprite.x = _vm->_screen->getScreenWide() / 2 - frame_head.width / 2; + msgSprite.y = MENUDEEP / 2 - frame_head.height / 2; + msgSprite.w = frame_head.width; + msgSprite.h = frame_head.height; + msgSprite.type = RDSPR_NOCOMPRESSION; + msgSprite.data = data + FrameHeader::size(); + + _vm->_screen->createSurface(&msgSprite, &msgSurface); + _vm->_screen->drawSurface(&msgSprite, msgSurface); + _vm->_screen->deleteSurface(msgSurface); + + free(data); + + // In case the cutscene has a long lead-in, start just before the first + // line of text. + + frameCounter = text[0]->startFrame - 12; + + // Fake a palette that will hopefully make the text visible. In the + // opening cutscene it seems to use colours 1 (black) and 255 (white). + + memcpy(oldPal, _vm->_screen->getPalette(), sizeof(oldPal)); + memset(tmpPal, 0, sizeof(tmpPal)); + tmpPal[255 * 4 + 0] = 255; + tmpPal[255 * 4 + 1] = 255; + tmpPal[255 * 4 + 2] = 255; + _vm->_screen->setPalette(0, 256, tmpPal, RDPAL_INSTANT); + + Audio::SoundHandle handle; + + bool skipCutscene = false; + + uint32 flags = Audio::Mixer::FLAG_16BITS; + +#ifndef SCUMM_BIG_ENDIAN + flags |= Audio::Mixer::FLAG_LITTLE_ENDIAN; +#endif + + while (1) { + if (!text[textCounter]) + break; + + if (frameCounter == text[textCounter]->startFrame) { + _vm->_screen->clearScene(); + openTextObject(text[textCounter]); + drawTextObject(NULL, text[textCounter]); + if (text[textCounter]->speech) { + _snd->playRaw(&handle, text[textCounter]->speech, text[textCounter]->speechBufferSize, 22050, flags); + } + } + + if (frameCounter == text[textCounter]->endFrame) { + closeTextObject(text[textCounter]); + _vm->_screen->clearScene(); + _vm->_screen->setNeedFullRedraw(); + textCounter++; + } + + frameCounter++; + _vm->_screen->updateDisplay(); + + KeyboardEvent *ke = _vm->keyboardEvent(); + + if ((ke && ke->keycode == 27) || _vm->_quit) { + _snd->stopHandle(handle); + skipCutscene = true; + break; + } + + // Simulate ~12 frames per second. I don't know what frame rate + // the original movies had, or even if it was constant, but + // this seems to work reasonably. + + _sys->delayMillis(90); + } + + // Wait for the voice to stop playing. This is to make sure that we + // don't cut off the speech in mid-sentence, and - even more + // importantly - that we don't free the sound buffer while it's in use. + + while (_snd->isSoundHandleActive(handle)) { + _vm->_screen->updateDisplay(false); + _sys->delayMillis(100); + } + + closeTextObject(text[textCounter]); + + _vm->_screen->clearScene(); + _vm->_screen->setNeedFullRedraw(); + + // HACK: Remove the instructions created above + Common::Rect r; + + memset(_vm->_screen->getScreen(), 0, _vm->_screen->getScreenWide() * MENUDEEP); + r.left = r.top = 0; + r.right = _vm->_screen->getScreenWide(); + r.bottom = MENUDEEP; + _vm->_screen->updateRect(&r); + + // FIXME: For now, only play the lead-out music for cutscenes that have + // subtitles. + + if (!skipCutscene && leadOut) + _vm->_sound->playFx(&_leadOutHandle, leadOut, leadOutLen, Audio::Mixer::kMaxChannelVolume, 0, false, Audio::Mixer::kMusicSoundType); + + _vm->_screen->setPalette(0, 256, oldPal, RDPAL_INSTANT); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/animation.h b/engines/sword2/animation.h new file mode 100644 index 0000000000..e3a554ccd7 --- /dev/null +++ b/engines/sword2/animation.h @@ -0,0 +1,103 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + * + */ + +#ifndef ANIMATION_H +#define ANIMATION_H + +#include "graphics/animation.h" +#include "sound/mixer.h" + +namespace Sword2 { + +struct SpriteInfo; + +// This is the structure which is passed to the sequence player. It includes +// the smack to play, and any text lines which are to be displayed over the top +// of the sequence. + +struct MovieTextObject { + uint16 startFrame; + uint16 endFrame; + SpriteInfo *textSprite; + uint32 speechBufferSize; + uint16 *speech; +}; + +class AnimationState : public ::Graphics::BaseAnimationState { +private: + Sword2Engine *_vm; + +public: + AnimationState(Sword2Engine *vm); + ~AnimationState(); + +#ifndef BACKEND_8BIT + void drawTextObject(SpriteInfo *s, byte *src); +#endif + + void clearScreen(); + void updateScreen(); + +private: + void drawYUV(int width, int height, byte *const *dat); + +#ifdef BACKEND_8BIT + void setPalette(byte *pal); +#endif +}; + +struct MovieInfo { + char name[9]; + uint frames; + bool seamless; +}; + +class MoviePlayer { +private: + Sword2Engine *_vm; + Audio::Mixer *_snd; + OSystem *_sys; + + byte *_textSurface; + + Audio::SoundHandle _leadOutHandle; + + uint _leadOutFrame; + bool _seamless; + + static struct MovieInfo _movies[]; + + void openTextObject(MovieTextObject *obj); + void closeTextObject(MovieTextObject *obj); + void drawTextObject(AnimationState *anim, MovieTextObject *obj); + + void playMPEG(const char *filename, MovieTextObject *text[], byte *leadOut, uint32 leadOutLen); + void playDummy(const char *filename, MovieTextObject *text[], byte *leadOut, uint32 leadOutLen); + +public: + MoviePlayer(Sword2Engine *vm); + int32 play(const char *filename, MovieTextObject *text[], int32 leadInRes, int32 leadOutRes); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/anims.cpp b/engines/sword2/anims.cpp new file mode 100644 index 0000000000..e64903a61f --- /dev/null +++ b/engines/sword2/anims.cpp @@ -0,0 +1,308 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// --------------------------------------------------------------------------- +// A more intelligent version of the old ANIMS.C +// All this stuff by James +// DON'T TOUCH! +// --------------------------------------------------------------------------- + +#include "common/stdafx.h" +#include "common/file.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/build_display.h" +#include "sword2/interpreter.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/resman.h" +#include "sword2/router.h" +#include "sword2/sound.h" +#include "sword2/animation.h" + +namespace Sword2 { + +int Router::doAnimate(byte *ob_logic, byte *ob_graph, int32 animRes, bool reverse) { + AnimHeader anim_head; + byte *anim_file; + + ObjectLogic obLogic(ob_logic); + ObjectGraphic obGraph(ob_graph); + + if (obLogic.getLooping() == 0) { + byte *ptr; + + // This is the start of the anim - set up the first frame + + // For testing all anims! + // A script loop can send every resource number to the anim + // function & it will only run the valid ones. See + // 'testing_routines' object in George's Player Character + // section of linc + + if (_vm->_logic->readVar(SYSTEM_TESTING_ANIMS)) { + if (!_vm->_resman->checkValid(animRes)) { + // Not a valid resource number. Switch off + // the sprite. Don't animate - just continue + // script next cycle. + setSpriteStatus(ob_graph, NO_SPRITE); + return IR_STOP; + } + + ptr = _vm->_resman->openResource(animRes); + + // if it's not an animation file + if (_vm->_resman->fetchType(animRes) != ANIMATION_FILE) { + _vm->_resman->closeResource(animRes); + + // switch off the sprite + // don't animate - just continue + // script next cycle + setSpriteStatus(ob_graph, NO_SPRITE); + return IR_STOP; + } + + _vm->_resman->closeResource(animRes); + + // switch on the sprite + setSpriteStatus(ob_graph, SORT_SPRITE); + } + + assert(animRes); + + // open anim file + anim_file = _vm->_resman->openResource(animRes); + + assert(_vm->_resman->fetchType(animRes) == ANIMATION_FILE); + + // point to anim header + anim_head.read(_vm->fetchAnimHeader(anim_file)); + + // now running an anim, looping back to this call again + obLogic.setLooping(1); + obGraph.setAnimResource(animRes); + + if (reverse) + obGraph.setAnimPc(anim_head.noAnimFrames - 1); + else + obGraph.setAnimPc(0); + } else if (_vm->_logic->getSync() != -1) { + // We've received a sync - return to script immediately + debug(5, "**sync stopped %d**", _vm->_logic->readVar(ID)); + + // If sync received, anim finishes right now (remaining on + // last frame). Quit animation, but continue script. + obLogic.setLooping(0); + return IR_CONT; + } else { + // Not first frame, and no sync received - set up the next + // frame of the anim. + + // open anim file and point to anim header + anim_file = _vm->_resman->openResource(obGraph.getAnimResource()); + anim_head.read(_vm->fetchAnimHeader(anim_file)); + + if (reverse) + obGraph.setAnimPc(obGraph.getAnimPc() - 1); + else + obGraph.setAnimPc(obGraph.getAnimPc() + 1); + } + + // check for end of anim + + if (reverse) { + if (obGraph.getAnimPc() == 0) + obLogic.setLooping(0); + } else { + if (obGraph.getAnimPc() == anim_head.noAnimFrames - 1) + obLogic.setLooping(0); + } + + // close the anim file + _vm->_resman->closeResource(obGraph.getAnimResource()); + + // check if we want the script to loop back & call this function again + return obLogic.getLooping() ? IR_REPEAT : IR_STOP; +} + +int Router::megaTableAnimate(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *animTable, bool reverse) { + int32 animRes = 0; + + // If this is the start of the anim, read the anim table to get the + // appropriate anim resource + + ObjectLogic obLogic(ob_logic); + + if (obLogic.getLooping() == 0) { + ObjectMega obMega(ob_mega); + + // Appropriate anim resource is in 'table[direction]' + animRes = READ_LE_UINT32(animTable + 4 * obMega.getCurDir()); + } + + return doAnimate(ob_logic, ob_graph, animRes, reverse); +} + +void Router::setSpriteStatus(byte *ob_graph, uint32 type) { + ObjectGraphic obGraph(ob_graph); + + // Remove the previous status, but don't affect the shading upper-word + obGraph.setType((obGraph.getType() & 0xffff0000) | type); +} + +void Router::setSpriteShading(byte *ob_graph, uint32 type) { + ObjectGraphic obGraph(ob_graph); + + // Remove the previous shading, but don't affect the status lower-word. + // Note that mega frames may still be shaded automatically, even when + // not sent 'RDSPR_SHADOW'. + obGraph.setType((obGraph.getType() & 0x0000ffff) | type); +} + +void Logic::createSequenceSpeech(MovieTextObject *sequenceText[]) { + uint32 line; + uint32 local_text; + uint32 text_res; + byte *text; + uint32 wavId; // ie. offical text number (actor text number) + bool speechRunning; + + // for each sequence text line that's been logged + for (line = 0; line < _sequenceTextLines; line++) { + // allocate this structure + sequenceText[line] = new MovieTextObject; + + sequenceText[line]->startFrame = _sequenceTextList[line].startFrame; + sequenceText[line]->endFrame = _sequenceTextList[line].endFrame; + + // pull out the text line to get the official text number + // (for wav id) + + text_res = _sequenceTextList[line].textNumber / SIZE; + local_text = _sequenceTextList[line].textNumber & 0xffff; + + // open text resource & get the line + text = _vm->fetchTextLine(_vm->_resman->openResource(text_res), local_text); + wavId = (int32)READ_LE_UINT16(text); + + // now ok to close the text file + _vm->_resman->closeResource(text_res); + + // 1st word of text line is the official line number + debug(5,"(%d) SEQUENCE TEXT: %s", READ_LE_UINT16(text), text + 2); + + // is it to be speech or subtitles or both? + // assume speech is not running until know otherwise + + speechRunning = false; + _sequenceTextList[line].speech_mem = NULL; + sequenceText[line]->speech = NULL; + + if (!_vm->_sound->isSpeechMute()) { + _sequenceTextList[line].speechBufferSize = _vm->_sound->preFetchCompSpeech(wavId, &_sequenceTextList[line].speech_mem); + if (_sequenceTextList[line].speechBufferSize) { + // ok, we've got speech! + speechRunning = true; + } + } + + // if we want subtitles, or speech failed to load + + if (_vm->getSubtitles() || !speechRunning) { + // open text resource & get the line + text = _vm->fetchTextLine(_vm->_resman->openResource(text_res), local_text); + // make the sprite + // 'text+2' to skip the first 2 bytes which form the + // line reference number + + // NB. The mem block containing the text sprite is + // currently FLOATING! + + // When rendering text over a sequence we need a + // different colour for the border. + + _sequenceTextList[line].text_mem = _vm->_fontRenderer->makeTextSprite(text + 2, 600, 255, _vm->_speechFontId, 1); + + // ok to close the text resource now + _vm->_resman->closeResource(text_res); + } else { + _sequenceTextList[line].text_mem = NULL; + sequenceText[line]->textSprite = NULL; + } + } + + // for drivers: NULL-terminate the array of pointers to + // MovieTextObject's + sequenceText[_sequenceTextLines] = NULL; + + for (line = 0; line < _sequenceTextLines; line++) { + // if we've made a text sprite for this line... + + if (_sequenceTextList[line].text_mem) { + // now fill out the SpriteInfo structure in the + // MovieTextObjectStructure + FrameHeader frame; + + frame.read(_sequenceTextList[line].text_mem); + + sequenceText[line]->textSprite = new SpriteInfo; + + // center text at bottom of screen + sequenceText[line]->textSprite->x = 320 - frame.width / 2; + sequenceText[line]->textSprite->y = 440 - frame.height; + sequenceText[line]->textSprite->w = frame.width; + sequenceText[line]->textSprite->h = frame.height; + sequenceText[line]->textSprite->type = RDSPR_DISPLAYALIGN | RDSPR_NOCOMPRESSION; + sequenceText[line]->textSprite->data = _sequenceTextList[line].text_mem + FrameHeader::size(); + } + + // if we've loaded a speech sample for this line... + + if (_sequenceTextList[line].speech_mem) { + // for drivers: set up pointer to decompressed wav in + // memory + + sequenceText[line]->speechBufferSize = _sequenceTextList[line].speechBufferSize; + sequenceText[line]->speech = _sequenceTextList[line].speech_mem; + } + } +} + +void Logic::clearSequenceSpeech(MovieTextObject *sequenceText[]) { + for (uint i = 0; i < _sequenceTextLines; i++) { + // free up the memory used by this MovieTextObject + delete sequenceText[i]; + + // free up the mem block containing this text sprite + if (_sequenceTextList[i].text_mem) + free(_sequenceTextList[i].text_mem); + + // free up the mem block containing this speech sample + if (_sequenceTextList[i].speech_mem) + free(_sequenceTextList[i].speech_mem); + } + + // IMPORTANT! Reset the line count ready for the next sequence! + _sequenceTextLines = 0; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/build_display.cpp b/engines/sword2/build_display.cpp new file mode 100644 index 0000000000..bc5a31a1f9 --- /dev/null +++ b/engines/sword2/build_display.cpp @@ -0,0 +1,1070 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// --------------------------------------------------------------------------- +// BUILD_DISPLAY.CPP like the old spr_engi but slightly more aptly named +// --------------------------------------------------------------------------- + +#include "common/stdafx.h" +#include "common/system.h" + +#include "sword2/sword2.h" +#include "sword2/console.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +namespace Sword2 { + +Screen::Screen(Sword2Engine *vm, int16 width, int16 height) { + _vm = vm; + + _dirtyGrid = _buffer = NULL; + + _vm->_system->initSize(width, height); + + _screenWide = width; + _screenDeep = height; + + _gridWide = width / CELLWIDE; + _gridDeep = height / CELLDEEP; + + if ((width % CELLWIDE) || (height % CELLDEEP)) + error("Bad cell size"); + + _dirtyGrid = (byte *)calloc(_gridWide, _gridDeep); + if (!_dirtyGrid) + error("Could not initialise dirty grid"); + + _buffer = (byte *)malloc(width * height); + if (!_buffer) + error("Could not initialise display"); + + for (int i = 0; i < ARRAYSIZE(_blockSurfaces); i++) + _blockSurfaces[i] = NULL; + + _lightMask = NULL; + _needFullRedraw = false; + + memset(&_thisScreen, 0, sizeof(_thisScreen)); + + _fps = 0; + _frameCount = 0; + _cycleTime = 0; + + _lastPaletteRes = 0; + + _scrollFraction = 16; + + _largestLayerArea = 0; + _largestSpriteArea = 0; + + strcpy(_largestLayerInfo, "largest layer: none registered"); + strcpy(_largestSpriteInfo, "largest sprite: none registered"); + + _fadeStatus = RDFADE_NONE; + _renderAverageTime = 60; + + _layer = 0; +} + +Screen::~Screen() { + free(_buffer); + free(_dirtyGrid); + closeBackgroundLayer(); + free(_lightMask); +} + +void Screen::buildDisplay() { + if (_thisScreen.new_palette) { + // start the layer palette fading up + startNewPalette(); + + // should be reset to zero at start of each screen change + _largestLayerArea = 0; + _largestSpriteArea = 0; + } + + // Does this ever happen? + if (!_thisScreen.background_layer_id) + return; + + // there is a valid screen to run + + setScrollTarget(_thisScreen.scroll_offset_x, _thisScreen.scroll_offset_y); + _vm->_mouse->animateMouse(); + startRenderCycle(); + + byte *file = _vm->_resman->openResource(_thisScreen.background_layer_id); + + MultiScreenHeader screenLayerTable; + + screenLayerTable.read(file + ResHeader::size()); + + // Render at least one frame, but if the screen is scrolling, and if + // there is time left, we will render extra frames to smooth out the + // scrolling. + + do { + // first background parallax + related anims + if (screenLayerTable.bg_parallax[0]) { + renderParallax(_vm->fetchBackgroundParallaxLayer(file, 0), 0); + drawBackPar0Frames(); + } + + // second background parallax + related anims + if (screenLayerTable.bg_parallax[1]) { + renderParallax(_vm->fetchBackgroundParallaxLayer(file, 1), 1); + drawBackPar1Frames(); + } + + // normal backround layer (just the one!) + renderParallax(_vm->fetchBackgroundLayer(file), 2); + + // sprites & layers + drawBackFrames(); // background sprites + drawSortFrames(file); // sorted sprites & layers + drawForeFrames(); // foreground sprites + + // first foreground parallax + related anims + + if (screenLayerTable.fg_parallax[0]) { + renderParallax(_vm->fetchForegroundParallaxLayer(file, 0), 3); + drawForePar0Frames(); + } + + // second foreground parallax + related anims + + if (screenLayerTable.fg_parallax[1]) { + renderParallax(_vm->fetchForegroundParallaxLayer(file, 1), 4); + drawForePar1Frames(); + } + + _vm->_debugger->drawDebugGraphics(); + _vm->_fontRenderer->printTextBlocs(); + _vm->_mouse->processMenu(); + + updateDisplay(); + + _frameCount++; + if (_vm->getMillis() > _cycleTime) { + _fps = _frameCount; + _frameCount = 0; + _cycleTime = _vm->getMillis() + 1000; + } + } while (!endRenderCycle()); + + _vm->_resman->closeResource(_thisScreen.background_layer_id); +} + +/** + * Fades down and displays a message on the screen. + * @param text The message + * @param time The number of seconds to display the message, or 0 to display it + * until the user clicks the mouse or presses a key. + */ + +void Screen::displayMsg(byte *text, int time) { + byte pal[256 * 4]; + byte oldPal[256 * 4]; + + debug(2, "DisplayMsg: %s", text); + + if (getFadeStatus() != RDFADE_BLACK) { + fadeDown(); + waitForFade(); + } + + _vm->_mouse->setMouse(0); + _vm->_mouse->setLuggage(0); + _vm->_mouse->closeMenuImmediately(); + + clearScene(); + + byte *text_spr = _vm->_fontRenderer->makeTextSprite(text, 640, 187, _vm->_speechFontId); + + FrameHeader frame; + + frame.read(text_spr); + + SpriteInfo spriteInfo; + + spriteInfo.x = _screenWide / 2 - frame.width / 2; + if (!time) + spriteInfo.y = _screenDeep / 2 - frame.height / 2 - MENUDEEP; + else + spriteInfo.y = 400 - frame.height; + spriteInfo.w = frame.width; + spriteInfo.h = frame.height; + spriteInfo.scale = 0; + spriteInfo.scaledWidth = 0; + spriteInfo.scaledHeight = 0; + spriteInfo.type = RDSPR_DISPLAYALIGN | RDSPR_NOCOMPRESSION | RDSPR_TRANS; + spriteInfo.blend = 0; + spriteInfo.data = text_spr + FrameHeader::size(); + spriteInfo.colourTable = 0; + + uint32 rv = drawSprite(&spriteInfo); + if (rv) + error("Driver Error %.8x (in DisplayMsg)", rv); + + memcpy(oldPal, _palette, sizeof(oldPal)); + memset(pal, 0, sizeof(pal)); + + pal[187 * 4 + 0] = 255; + pal[187 * 4 + 1] = 255; + pal[187 * 4 + 2] = 255; + + setPalette(0, 256, pal, RDPAL_FADE); + fadeUp(); + free(text_spr); + waitForFade(); + + if (time > 0) { + uint32 targetTime = _vm->getMillis() + (time * 1000); + _vm->sleepUntil(targetTime); + } else { + while (!_vm->_quit) { + MouseEvent *me = _vm->mouseEvent(); + if (me && (me->buttons & (RD_LEFTBUTTONDOWN | RD_RIGHTBUTTONDOWN))) + break; + + if (_vm->keyboardEvent()) + break; + + updateDisplay(); + _vm->_system->delayMillis(50); + } + } + + fadeDown(); + waitForFade(); + clearScene(); + setPalette(0, 256, oldPal, RDPAL_FADE); + fadeUp(); +} + +void Screen::drawBackPar0Frames() { + // frame attached to 1st background parallax + for (uint i = 0; i < _curBgp0; i++) + processImage(&_bgp0List[i]); +} + +void Screen::drawBackPar1Frames() { + // frame attached to 2nd background parallax + for (uint i = 0; i < _curBgp1; i++) + processImage(&_bgp1List[i]); +} + +void Screen::drawBackFrames() { + // background sprite, fixed to main background + for (uint i = 0; i < _curBack; i++) + processImage(&_backList[i]); +} + +void Screen::drawSortFrames(byte *file) { + uint i, j; + + // Sort the sort list. Used to be a separate function, but it was only + // called once, right before calling drawSortFrames(). + + if (_curSort > 1) { + for (i = 0; i < _curSort - 1; i++) { + for (j = 0; j < _curSort - 1; j++) { + if (_sortList[_sortOrder[j]].sort_y > _sortList[_sortOrder[j + 1]].sort_y) { + SWAP(_sortOrder[j], _sortOrder[j + 1]); + } + } + } + } + + // Draw the sorted frames - layers, shrinkers & normal flat sprites + + for (i = 0; i < _curSort; i++) { + if (_sortList[_sortOrder[i]].layer_number) { + // it's a layer - minus 1 for true layer number + // we need to know from the BuildUnit because the + // layers will have been sorted in random order + processLayer(file, _sortList[_sortOrder[i]].layer_number - 1); + } else { + // it's a sprite + processImage(&_sortList[_sortOrder[i]]); + } + } +} + +void Screen::drawForeFrames() { + // foreground sprite, fixed to main background + for (uint i = 0; i < _curFore; i++) + processImage(&_foreList[i]); +} + +void Screen::drawForePar0Frames() { + // frame attached to 1st foreground parallax + for (uint i = 0; i < _curFgp0; i++) + processImage(&_fgp0List[i]); +} + +void Screen::drawForePar1Frames() { + // frame attached to 2nd foreground parallax + for (uint i = 0; i < _curFgp1; i++) + processImage(&_fgp1List[i]); +} + +void Screen::processLayer(byte *file, uint32 layer_number) { + LayerHeader layer_head; + + layer_head.read(_vm->fetchLayerHeader(file, layer_number)); + + SpriteInfo spriteInfo; + + spriteInfo.x = layer_head.x; + spriteInfo.y = layer_head.y; + spriteInfo.w = layer_head.width; + spriteInfo.scale = 0; + spriteInfo.scaledWidth = 0; + spriteInfo.scaledHeight = 0; + spriteInfo.h = layer_head.height; + spriteInfo.type = RDSPR_TRANS | RDSPR_RLE256FAST; + spriteInfo.blend = 0; + spriteInfo.data = file + ResHeader::size() + layer_head.offset; + spriteInfo.colourTable = 0; + + // check for largest layer for debug info + + uint32 current_layer_area = layer_head.width * layer_head.height; + + if (current_layer_area > _largestLayerArea) { + byte buf[NAME_LEN]; + + _largestLayerArea = current_layer_area; + sprintf(_largestLayerInfo, + "largest layer: %s layer(%d) is %dx%d", + _vm->_resman->fetchName(_thisScreen.background_layer_id, buf), + layer_number, layer_head.width, layer_head.height); + } + + uint32 rv = drawSprite(&spriteInfo); + if (rv) + error("Driver Error %.8x in processLayer(%d)", rv, layer_number); +} + +void Screen::processImage(BuildUnit *build_unit) { + byte *file = _vm->_resman->openResource(build_unit->anim_resource); + byte *colTablePtr = NULL; + + byte *frame = _vm->fetchFrameHeader(file, build_unit->anim_pc); + + AnimHeader anim_head; + CdtEntry cdt_entry; + FrameHeader frame_head; + + anim_head.read(_vm->fetchAnimHeader(file)); + cdt_entry.read(_vm->fetchCdtEntry(file, build_unit->anim_pc)); + frame_head.read(frame); + + // so that 0-colour is transparent + uint32 spriteType = RDSPR_TRANS; + + if (anim_head.blend) + spriteType |= RDSPR_BLEND; + + // if the frame is to be flipped (only really applicable to frames + // using offsets) + if (cdt_entry.frameType & FRAME_FLIPPED) + spriteType |= RDSPR_FLIP; + + if (cdt_entry.frameType & FRAME_256_FAST) { + // scaling, shading & blending don't work with RLE256FAST + // but the same compression can be decompressed using the + // RLE256 routines! + + // NOTE: If this restriction refers to drawSprite(), I don't + // think we have it any more. But I'm not sure. + + if (build_unit->scale || anim_head.blend || build_unit->shadingFlag) + spriteType |= RDSPR_RLE256; + else + spriteType |= RDSPR_RLE256FAST; + } else { + switch (anim_head.runTimeComp) { + case NONE: + spriteType |= RDSPR_NOCOMPRESSION; + break; + case RLE256: + spriteType |= RDSPR_RLE256; + break; + case RLE16: + spriteType |= RDSPR_RLE16; + // points to just after last cdt_entry, ie. + // start of colour table + colTablePtr = _vm->fetchAnimHeader(file) + AnimHeader::size() + anim_head.noAnimFrames * CdtEntry::size(); + break; + } + } + + // if we want this frame to be affected by the shading mask, + // add the status bit + if (build_unit->shadingFlag) + spriteType |= RDSPR_SHADOW; + + SpriteInfo spriteInfo; + + spriteInfo.x = build_unit->x; + spriteInfo.y = build_unit->y; + spriteInfo.w = frame_head.width; + spriteInfo.h = frame_head.height; + spriteInfo.scale = build_unit->scale; + spriteInfo.scaledWidth = build_unit->scaled_width; + spriteInfo.scaledHeight = build_unit->scaled_height; + spriteInfo.type = spriteType; + spriteInfo.blend = anim_head.blend; + // points to just after frame header, ie. start of sprite data + spriteInfo.data = frame + FrameHeader::size(); + spriteInfo.colourTable = colTablePtr; + + // check for largest layer for debug info + uint32 current_sprite_area = frame_head.width * frame_head.height; + + if (current_sprite_area > _largestSpriteArea) { + byte buf[NAME_LEN]; + + _largestSpriteArea = current_sprite_area; + sprintf(_largestSpriteInfo, + "largest sprite: %s frame(%d) is %dx%d", + _vm->_resman->fetchName(build_unit->anim_resource, buf), + build_unit->anim_pc, + frame_head.width, + frame_head.height); + } + + if (_vm->_logic->readVar(SYSTEM_TESTING_ANIMS)) { // see anims.cpp + // bring the anim into the visible screen + // but leave extra pixel at edge for box + if (spriteInfo.x + spriteInfo.scaledWidth >= 639) + spriteInfo.x = 639 - spriteInfo.scaledWidth; + + if (spriteInfo.y + spriteInfo.scaledHeight >= 399) + spriteInfo.y = 399 - spriteInfo.scaledHeight; + + if (spriteInfo.x < 1) + spriteInfo.x = 1; + + if (spriteInfo.y < 1) + spriteInfo.y = 1; + + // create box to surround sprite - just outside sprite box + _vm->_debugger->_rectX1 = spriteInfo.x - 1; + _vm->_debugger->_rectY1 = spriteInfo.y - 1; + _vm->_debugger->_rectX2 = spriteInfo.x + spriteInfo.scaledWidth; + _vm->_debugger->_rectY2 = spriteInfo.y + spriteInfo.scaledHeight; + } + + uint32 rv = drawSprite(&spriteInfo); + if (rv) { + byte buf[NAME_LEN]; + + error("Driver Error %.8x with sprite %s (%d) in processImage", + rv, + _vm->_resman->fetchName(build_unit->anim_resource, buf), + build_unit->anim_resource); + } + + // release the anim resource + _vm->_resman->closeResource(build_unit->anim_resource); +} + +void Screen::resetRenderLists() { + // reset the sort lists - do this before a logic loop + // takes into account the fact that the start of the list is pre-built + // with the special sortable layers + + _curBgp0 = 0; + _curBgp1 = 0; + _curBack = 0; + // beginning of sort list is setup with the special sort layers + _curSort = _thisScreen.number_of_layers; + _curFore = 0; + _curFgp0 = 0; + _curFgp1 = 0; + + if (_curSort) { + // there are some layers - so rebuild the sort order list + for (uint i = 0; i < _curSort; i++) + _sortOrder[i] = i; + } +} + +void Screen::registerFrame(byte *ob_mouse, byte *ob_graph, byte *ob_mega, BuildUnit *build_unit) { + ObjectGraphic obGraph(ob_graph); + ObjectMega obMega(ob_mega); + + assert(obGraph.getAnimResource()); + + byte *file = _vm->_resman->openResource(obGraph.getAnimResource()); + + AnimHeader anim_head; + CdtEntry cdt_entry; + FrameHeader frame_head; + + anim_head.read(_vm->fetchAnimHeader(file)); + cdt_entry.read(_vm->fetchCdtEntry(file, obGraph.getAnimPc())); + frame_head.read(_vm->fetchFrameHeader(file, obGraph.getAnimPc())); + + // update player graphic details for on-screen debug info + if (_vm->_logic->readVar(ID) == CUR_PLAYER_ID) { + _vm->_debugger->_graphType = obGraph.getType(); + _vm->_debugger->_graphAnimRes = obGraph.getAnimResource(); + // counting 1st frame as 'frame 1' + _vm->_debugger->_graphAnimPc = obGraph.getAnimPc() + 1; + _vm->_debugger->_graphNoFrames = anim_head.noAnimFrames; + } + + // fill in the BuildUnit structure for this frame + + build_unit->anim_resource = obGraph.getAnimResource(); + build_unit->anim_pc = obGraph.getAnimPc(); + build_unit->layer_number = 0; + + // Affected by shading mask? + if (obGraph.getType() & SHADED_SPRITE) + build_unit->shadingFlag = true; + else + build_unit->shadingFlag = false; + + // Check if this frame has offsets ie. this is a scalable mega frame + + int scale = 0; + + if (cdt_entry.frameType & FRAME_OFFSET) { + scale = obMega.calcScale(); + + // calc final render coordinates (top-left of sprite), based + // on feet coords & scaled offsets + + // add scaled offsets to feet coords + build_unit->x = obMega.getFeetX() + (cdt_entry.x * scale) / 256; + build_unit->y = obMega.getFeetY() + (cdt_entry.y * scale) / 256; + + // Work out new width and height. Always divide by 256 after + // everything else, to maintain accurary + build_unit->scaled_width = ((scale * frame_head.width) / 256); + build_unit->scaled_height = ((scale * frame_head.height) / 256); + } else { + // It's a non-scaling anim. Get render coords for sprite, from cdt + build_unit->x = cdt_entry.x; + build_unit->y = cdt_entry.y; + + // Get width and height + build_unit->scaled_width = frame_head.width; + build_unit->scaled_height = frame_head.height; + } + + // either 0 or required scale, depending on whether 'scale' computed + build_unit->scale = scale; + + // calc the bottom y-coord for sorting purposes + build_unit->sort_y = build_unit->y + build_unit->scaled_height - 1; + + if (ob_mouse) { + // passed a mouse structure, so add to the _mouseList + _vm->_mouse->registerMouse(ob_mouse, build_unit); + + } + + _vm->_resman->closeResource(obGraph.getAnimResource()); +} + +void Screen::registerFrame(byte *ob_mouse, byte *ob_graph, byte *ob_mega) { + ObjectGraphic obGraph(ob_graph); + + // check low word for sprite type + switch (obGraph.getType() & 0x0000ffff) { + case BGP0_SPRITE: + assert(_curBgp0 < MAX_bgp0_sprites); + registerFrame(ob_mouse, ob_graph, ob_mega, &_bgp0List[_curBgp0]); + _curBgp0++; + break; + case BGP1_SPRITE: + assert(_curBgp1 < MAX_bgp1_sprites); + registerFrame(ob_mouse, ob_graph, ob_mega, &_bgp1List[_curBgp1]); + _curBgp1++; + break; + case BACK_SPRITE: + assert(_curBack < MAX_back_sprites); + registerFrame(ob_mouse, ob_graph, ob_mega, &_backList[_curBack]); + _curBack++; + break; + case SORT_SPRITE: + assert(_curSort < MAX_sort_sprites); + _sortOrder[_curSort] = _curSort; + registerFrame(ob_mouse, ob_graph, ob_mega, &_sortList[_curSort]); + _curSort++; + break; + case FORE_SPRITE: + assert(_curFore < MAX_fore_sprites); + registerFrame(ob_mouse, ob_graph, ob_mega, &_foreList[_curFore]); + _curFore++; + break; + case FGP0_SPRITE: + assert(_curFgp0 < MAX_fgp0_sprites); + registerFrame(ob_mouse, ob_graph, ob_mega, &_fgp0List[_curFgp0]); + _curFgp0++; + break; + case FGP1_SPRITE: + assert(_curFgp1 < MAX_fgp1_sprites); + registerFrame(ob_mouse, ob_graph, ob_mega, &_fgp1List[_curFgp1]); + _curFgp1++; + break; + default: + // NO_SPRITE no registering! + break; + } +} + +// FIXME: +// +// The original credits used a different font. I think it's stored in the +// font.clu file, but I don't know how to interpret it. +// +// The original used the entire screen. This version cuts off the top and +// bottom of the screen, because that's where the menus would usually be. +// +// The original had some sort of smoke effect at the bottom of the screen. + +enum { + LINE_LEFT, + LINE_CENTER, + LINE_RIGHT +}; + +struct CreditsLine { + char *str; + byte type; + int top; + int height; + byte *sprite; + + CreditsLine() { + str = NULL; + sprite = NULL; + }; + + ~CreditsLine() { + free(str); + free(sprite); + str = NULL; + sprite = NULL; + } +}; + +#define CREDITS_FONT_HEIGHT 25 +#define CREDITS_LINE_SPACING 20 + +void Screen::rollCredits() { + uint32 loopingMusicId = _vm->_sound->getLoopingMusicId(); + + // Prepare for the credits by fading down, stoping the music, etc. + + _vm->_mouse->setMouse(0); + + _vm->_sound->muteFx(true); + _vm->_sound->muteSpeech(true); + + waitForFade(); + fadeDown(); + waitForFade(); + + _vm->_mouse->closeMenuImmediately(); + + // There are three files which I believe are involved in showing the + // credits: + // + // credits.bmp - The "Smacker" logo, stored as follows: + // + // width 2 bytes, little endian + // height 2 bytes, little endian + // palette 3 * 256 bytes + // data width * height bytes + // + // Note that the maximum colour component in the palette is 0x3F. + // This is the same resolution as the _paletteMatch table. I doubt + // that this is a coincidence, but let's use the image palette + // directly anyway, just to be safe. + // + // credits.clu - The credits text + // + // This is simply a text file with CRLF line endings. + // '^' is not shown, but used to mark the center of the line. + // '@' is used as a placeholder for the "Smacker" logo. At least + // when it appears alone. + // Remaining lines are centered. + // The German version also contains character code 9 for no + // apparent reason. We ignore them. + // + // fonts.clu - The credits font? + // + // FIXME: At this time I don't know how to interpret fonts.clu. For + // now, let's just the standard speech font instead. + + SpriteInfo spriteInfo; + Common::File f; + int i; + + // Read the "Smacker" logo + + uint16 logoWidth = 0; + uint16 logoHeight = 0; + byte *logoData = NULL; + byte palette[256 * 4]; + + if (f.open("credits.bmp")) { + logoWidth = f.readUint16LE(); + logoHeight = f.readUint16LE(); + + for (i = 0; i < 256; i++) { + palette[i * 4 + 0] = f.readByte() << 2; + palette[i * 4 + 1] = f.readByte() << 2; + palette[i * 4 + 2] = f.readByte() << 2; + palette[i * 4 + 3] = 0; + } + + logoData = (byte *)malloc(logoWidth * logoHeight); + + f.read(logoData, logoWidth * logoHeight); + f.close(); + } else { + warning("Can't find credits.bmp"); + memset(palette, 0, sizeof(palette)); + palette[14 * 4 + 0] = 252; + palette[14 * 4 + 1] = 252; + palette[14 * 4 + 2] = 252; + palette[14 * 4 + 3] = 0; + } + + setPalette(0, 256, palette, RDPAL_INSTANT); + + // Read the credits text + + Common::Array<CreditsLine *> creditsLines; + + int lineCount = 0; + int lineTop = 400; + int paragraphStart = 0; + bool hasCenterMark = false; + + if (!f.open("credits.clu")) { + warning("Can't find credits.clu"); + return; + } + + while (1) { + char buffer[80]; + char *line = f.readLine(buffer, sizeof(buffer)); + + if (!line || *line == 0) { + if (!hasCenterMark) { + for (i = paragraphStart; i < lineCount; i++) + creditsLines[i]->type = LINE_CENTER; + } + paragraphStart = lineCount; + hasCenterMark = false; + if (paragraphStart == lineCount) + lineTop += CREDITS_LINE_SPACING; + + if (!line) + break; + + continue; + } + + // The German credits contains character code 9. We don't want + // the credits to show the 'dud' symbol, so we replace them + // with spaces. + + for (char *ptr = line; *ptr; ptr++) { + if (*ptr < 32) + *ptr = 32; + } + + char *center_mark = strchr(line, '^'); + + if (center_mark) { + // The current paragraph has at least one center mark. + hasCenterMark = true; + + if (center_mark != line) { + creditsLines.push_back(new CreditsLine); + + // The center mark is somewhere inside the + // line. Split it into left and right side. + *center_mark = 0; + + creditsLines[lineCount]->top = lineTop; + creditsLines[lineCount]->height = CREDITS_FONT_HEIGHT; + creditsLines[lineCount]->type = LINE_LEFT; + creditsLines[lineCount]->str = strdup(line); + + lineCount++; + *center_mark = '^'; + } + + line = center_mark; + } + + creditsLines.push_back(new CreditsLine); + + creditsLines[lineCount]->top = lineTop; + + if (*line == '^') { + creditsLines[lineCount]->type = LINE_RIGHT; + line++; + } else + creditsLines[lineCount]->type = LINE_LEFT; + + if (strcmp(line, "@") == 0) { + creditsLines[lineCount]->height = logoHeight; + lineTop += logoHeight; + } else { + creditsLines[lineCount]->height = CREDITS_FONT_HEIGHT; + lineTop += CREDITS_LINE_SPACING; + } + + creditsLines[lineCount]->str = strdup(line); + lineCount++; + } + + f.close(); + + // We could easily add some ScummVM stuff to the credits, if we wanted + // to. On the other hand, anyone with the attention span to actually + // read all the credits probably already knows. :-) + + // Start the music and roll the credits + + // The credits music (which can also be heard briefly in the "carib" + // cutscene) is played once. + + _vm->_sound->streamCompMusic(309, false); + + clearScene(); + fadeUp(0); + + spriteInfo.scale = 0; + spriteInfo.scaledWidth = 0; + spriteInfo.scaledHeight = 0; + spriteInfo.type = RDSPR_DISPLAYALIGN | RDSPR_NOCOMPRESSION | RDSPR_TRANS; + spriteInfo.blend = 0; + + int startLine = 0; + int scrollPos = 0; + + bool abortCredits = false; + + int scrollSteps = lineTop + CREDITS_FONT_HEIGHT; + uint32 musicStart = _vm->getMillis(); + + // Ideally the music should last just a tiny bit longer than the + // credits. Note that musicTimeRemaining() will return 0 if the music + // is muted, so we need a sensible fallback for that case. + + uint32 musicLength = MAX((int32)(1000 * (_vm->_sound->musicTimeRemaining() - 3)), 25 * (int32)scrollSteps); + + while (scrollPos < scrollSteps && !_vm->_quit) { + clearScene(); + + for (i = startLine; i < lineCount; i++) { + if (!creditsLines[i]) + continue; + + // Free any sprites that have scrolled off the screen + + if (creditsLines[i]->top + creditsLines[i]->height < scrollPos) { + debug(2, "Freeing line %d: '%s'", i, creditsLines[i]->str); + + delete creditsLines[i]; + creditsLines[i] = NULL; + + startLine = i + 1; + } else if (creditsLines[i]->top < scrollPos + 400) { + if (!creditsLines[i]->sprite) { + debug(2, "Creating line %d: '%s'", i, creditsLines[i]->str); + creditsLines[i]->sprite = _vm->_fontRenderer->makeTextSprite((byte *)creditsLines[i]->str, 600, 14, _vm->_speechFontId, 0); + } + + FrameHeader frame; + + frame.read(creditsLines[i]->sprite); + + spriteInfo.y = creditsLines[i]->top - scrollPos; + spriteInfo.w = frame.width; + spriteInfo.h = frame.height; + spriteInfo.data = creditsLines[i]->sprite + FrameHeader::size(); + + switch (creditsLines[i]->type) { + case LINE_LEFT: + spriteInfo.x = RENDERWIDE / 2 - 5 - frame.width; + break; + case LINE_RIGHT: + spriteInfo.x = RENDERWIDE / 2 + 5; + break; + case LINE_CENTER: + if (strcmp(creditsLines[i]->str, "@") == 0) { + spriteInfo.data = logoData; + spriteInfo.x = (RENDERWIDE - logoWidth) / 2; + spriteInfo.w = logoWidth; + spriteInfo.h = logoHeight; + } else + spriteInfo.x = (RENDERWIDE - frame.width) / 2; + break; + } + + if (spriteInfo.data) + drawSprite(&spriteInfo); + } else + break; + } + + updateDisplay(); + + KeyboardEvent *ke = _vm->keyboardEvent(); + + if (ke && ke->keycode == 27) { + if (!abortCredits) { + abortCredits = true; + fadeDown(); + } + } + + if (abortCredits && getFadeStatus() == RDFADE_BLACK) + break; + + _vm->sleepUntil(musicStart + (musicLength * scrollPos) / scrollSteps); + scrollPos++; + } + + // We're done. Clean up and try to put everything back where it was + // before the credits. + + for (i = 0; i < lineCount; i++) { + delete creditsLines[i]; + } + + free(logoData); + + if (!abortCredits) { + fadeDown(); + + // The music should either have stopped or be about to stop, so + // wait for it to really happen. + + while (_vm->_sound->musicTimeRemaining() && !_vm->_quit) { + updateDisplay(false); + _vm->_system->delayMillis(100); + } + } + + if (_vm->_quit) + return; + + waitForFade(); + + _vm->_sound->muteFx(false); + _vm->_sound->muteSpeech(false); + + if (loopingMusicId) + _vm->_sound->streamCompMusic(loopingMusicId, true); + else + _vm->_sound->stopMusic(false); + + if (!_vm->_mouse->getMouseStatus() || _vm->_mouse->isChoosing()) + _vm->_mouse->setMouse(NORMAL_MOUSE_ID); + + if (_vm->_logic->readVar(DEAD)) + _vm->_mouse->buildSystemMenu(); +} + +// This image used to be shown by CacheNewCluster() while copying a data file +// from the CD to the hard disk. ScummVM doesn't do that, so the image is never +// shown. It'd be nice if we could do something useful with it some day... + +void Screen::splashScreen() { + byte *bgfile = _vm->_resman->openResource(2950); + + initialiseBackgroundLayer(NULL); + initialiseBackgroundLayer(NULL); + initialiseBackgroundLayer(_vm->fetchBackgroundLayer(bgfile)); + initialiseBackgroundLayer(NULL); + initialiseBackgroundLayer(NULL); + + setPalette(0, 256, _vm->fetchPalette(bgfile), RDPAL_FADE); + renderParallax(_vm->fetchBackgroundLayer(bgfile), 2); + + closeBackgroundLayer(); + + byte *loadingBar = _vm->_resman->openResource(2951); + byte *frame = _vm->fetchFrameHeader(loadingBar, 0); + + AnimHeader animHead; + CdtEntry cdt; + FrameHeader frame_head; + + animHead.read(_vm->fetchAnimHeader(loadingBar)); + cdt.read(_vm->fetchCdtEntry(loadingBar, 0)); + frame_head.read(_vm->fetchFrameHeader(loadingBar, 0)); + + SpriteInfo barSprite; + + barSprite.x = cdt.x; + barSprite.y = cdt.y; + barSprite.w = frame_head.width; + barSprite.h = frame_head.height; + barSprite.scale = 0; + barSprite.scaledWidth = 0; + barSprite.scaledHeight = 0; + barSprite.type = RDSPR_RLE256FAST | RDSPR_TRANS; + barSprite.blend = 0; + barSprite.colourTable = 0; + barSprite.data = frame + FrameHeader::size(); + + drawSprite(&barSprite); + + fadeUp(); + waitForFade(); + + for (int i = 0; i < animHead.noAnimFrames; i++) { + frame = _vm->fetchFrameHeader(loadingBar, i); + barSprite.data = frame + FrameHeader::size(); + drawSprite(&barSprite); + updateDisplay(); + _vm->_system->delayMillis(30); + } + + _vm->_resman->closeResource(2951); + + fadeDown(); + waitForFade(); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/build_display.h b/engines/sword2/build_display.h new file mode 100644 index 0000000000..1a362da137 --- /dev/null +++ b/engines/sword2/build_display.h @@ -0,0 +1,441 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _BUILD_DISPLAY +#define _BUILD_DISPLAY + +#include "common/rect.h" +#include "common/stream.h" + +#define MAX_bgp0_sprites 6 +#define MAX_bgp1_sprites 6 +#define MAX_back_sprites 30 +#define MAX_sort_sprites 30 +#define MAX_fore_sprites 30 +#define MAX_fgp0_sprites 6 +#define MAX_fgp1_sprites 6 + +#define PALTABLESIZE (64 * 64 * 64) + +#define BLOCKWIDTH 64 +#define BLOCKHEIGHT 64 +#define MAXLAYERS 5 + +#define MENUDEEP 40 +#define RENDERWIDE 640 +#define RENDERDEEP (480 - (MENUDEEP * 2)) + +// Maximum scaled size of a sprite +#define SCALE_MAXWIDTH 512 +#define SCALE_MAXHEIGHT 512 + +// Dirty grid cell size +#define CELLWIDE 10 +#define CELLDEEP 20 + +namespace Sword2 { + +class Sword2Engine; + +// Sprite defines + +enum { + // This is the low byte part of the sprite type. + + RDSPR_TRANS = 0x0001, + RDSPR_BLEND = 0x0004, + RDSPR_FLIP = 0x0008, + RDSPR_SHADOW = 0x0010, + RDSPR_DISPLAYALIGN = 0x0020, + RDSPR_NOCOMPRESSION = 0x0040, + RDSPR_EDGEBLEND = 0x0080, // Unused + + // This is the high byte part of the sprite type, which defines what + // type of compression is used. Unless RDSPR_NOCOMPRESSION is set. + + RDSPR_RLE16 = 0x0000, + RDSPR_RLE256 = 0x0100, + RDSPR_RLE256FAST = 0x0200 +}; + +// Fading defines + +enum { + RDFADE_NONE, + RDFADE_UP, + RDFADE_DOWN, + RDFADE_BLACK +}; + +// Palette defines + +enum { + RDPAL_FADE, + RDPAL_INSTANT +}; + +// Blitting FX defines + +enum { + RDBLTFX_SPRITEBLEND = 0x01, + RDBLTFX_SHADOWBLEND = 0x02, + RDBLTFX_EDGEBLEND = 0x04 +}; + +// Structure filled out by each object to register its graphic printing +// requrements + +struct BuildUnit { + int16 x; + int16 y; + uint16 scaled_width; + uint16 scaled_height; + int16 sort_y; + uint32 anim_resource; + uint16 anim_pc; + + // Denotes a scaling sprite at print time - and holds the scaling value + // for the shrink routine + + uint16 scale; + + // Non-zero means this item is a layer - retrieve from background layer + // and send to special renderer + + uint16 layer_number; + + // True means we want this frame to be affected by the shading mask + + bool shadingFlag; +}; + +struct ScreenInfo { + uint16 scroll_offset_x; // Position x + uint16 scroll_offset_y; // Position y + uint16 max_scroll_offset_x; // Calc'ed in fnInitBackground + uint16 max_scroll_offset_y; + int16 player_feet_x; // Feet coordinates to use - cant just + int16 player_feet_y; // fetch the player compact anymore + int16 feet_x; // Special offset-to-player position - + int16 feet_y; // tweek as desired - always set in + // screen manager object startup + uint16 screen_wide; // Size of background layer - hence + uint16 screen_deep; // size of back buffer itself (Paul + // actually malloc's it) + uint32 background_layer_id; // Id of the normal background layer + // from the header of the main + // background layer + uint16 number_of_layers; + uint8 new_palette; // Set to non zero to start the + // palette held within layer file + // fading up after a build_display + uint8 scroll_flag; // Scroll mode 0 off 1 on + bool mask_flag; // Using shading mask +}; + +// The SpriteInfo structure is used to tell the driver96 code what attributes +// are linked to a sprite for drawing. These include position, scaling and +// compression. + +struct SpriteInfo { + int16 x; // coords for top-left of sprite + int16 y; + uint16 w; // dimensions of sprite (before scaling) + uint16 h; + uint16 scale; // scale at which to draw, given in 256ths ['0' or '256' MEANS DON'T SCALE] + uint16 scaledWidth; // new dimensions (we calc these for the mouse area, so may as well pass to you to save time) + uint16 scaledHeight; // + uint16 type; // mask containing 'RDSPR_' bits specifying compression type, flip, transparency, etc + uint16 blend; // holds the blending values. + byte *data; // pointer to the sprite data + byte *colourTable; // pointer to 16-byte colour table, only applicable to 16-col compression type +}; + +struct BlockSurface { + byte data[BLOCKWIDTH * BLOCKHEIGHT]; + bool transparent; +}; + +struct Parallax { + uint16 w; + uint16 h; + + // The dimensions are followed by an offset table, but we don't know in + // advance how big it is. See initializeBackgroundLayer(). + + static const int size() { + return 4; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + w = readS.readUint16LE(); + h = readS.readUint16LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint16LE(w); + writeS.writeUint16LE(h); + } +}; + +class Screen { +private: + Sword2Engine *_vm; + + // _thisScreen describes the current back buffer and its in-game scroll + // positions, etc. + + ScreenInfo _thisScreen; + + int32 _renderCaps; + int8 _renderLevel; + + byte *_buffer; + byte *_lightMask; + + // Game screen metrics + int16 _screenWide; + int16 _screenDeep; + + bool _needFullRedraw; + + // Scroll variables. _scrollX and _scrollY hold the current scroll + // position, and _scrollXTarget and _scrollYTarget are the target + // position for the end of the game cycle. + + int16 _scrollX; + int16 _scrollY; + + int16 _scrollXTarget; + int16 _scrollYTarget; + int16 _scrollXOld; + int16 _scrollYOld; + + int16 _parallaxScrollX; // current x offset to link a sprite to the + // parallax layer + int16 _parallaxScrollY; // current y offset to link a sprite to the + // parallax layer + int16 _locationWide; + int16 _locationDeep; + + // Dirty grid handling + byte *_dirtyGrid; + + uint16 _gridWide; + uint16 _gridDeep; + + byte _palette[256 * 4]; + byte _paletteMatch[PALTABLESIZE]; + + uint8 _fadeStatus; + int32 _fadeStartTime; + int32 _fadeTotalTime; + + // 'frames per second' counting stuff + uint32 _fps; + uint32 _cycleTime; + uint32 _frameCount; + + int32 _initialTime; + int32 _startTime; + int32 _totalTime; + int32 _renderAverageTime; + int32 _framesPerGameCycle; + bool _renderTooSlow; + + void startNewPalette(); + + void resetRenderEngine(); + + void startRenderCycle(); + bool endRenderCycle(); + + // Holds the order of the sort list, i.e. the list stays static and we + // sort this array. + + uint16 _sortOrder[MAX_sort_sprites]; + + BuildUnit _bgp0List[MAX_bgp0_sprites]; + BuildUnit _bgp1List[MAX_bgp1_sprites]; + BuildUnit _backList[MAX_back_sprites]; + BuildUnit _sortList[MAX_sort_sprites]; + BuildUnit _foreList[MAX_fore_sprites]; + BuildUnit _fgp0List[MAX_fgp0_sprites]; + BuildUnit _fgp1List[MAX_fgp1_sprites]; + + uint32 _curBgp0; + uint32 _curBgp1; + uint32 _curBack; + uint32 _curSort; + uint32 _curFore; + uint32 _curFgp0; + uint32 _curFgp1; + + void drawBackPar0Frames(); + void drawBackPar1Frames(); + void drawBackFrames(); + void drawSortFrames(byte *file); + void drawForeFrames(); + void drawForePar0Frames(); + void drawForePar1Frames(); + + void processLayer(byte *file, uint32 layer_number); + void processImage(BuildUnit *build_unit); + + uint8 _scrollFraction; + + // Last palette used - so that we can restore the correct one after a + // pause (which dims the screen) and it's not always the main screen + // palette that we want, eg. during the eclipse + + // This flag gets set in startNewPalette() and setFullPalette() + + uint32 _lastPaletteRes; + + // Debugging stuff + uint32 _largestLayerArea; + uint32 _largestSpriteArea; + char _largestLayerInfo[128]; + char _largestSpriteInfo[128]; + + void registerFrame(byte *ob_mouse, byte *ob_graph, byte *ob_mega, BuildUnit *build_unit); + + void mirrorSprite(byte *dst, byte *src, int16 w, int16 h); + int32 decompressRLE256(byte *dst, byte *src, int32 decompSize); + void unwindRaw16(byte *dst, byte *src, uint8 blockSize, byte *colTable); + int32 decompressRLE16(byte *dst, byte *src, int32 decompSize, byte *colTable); + void renderParallax(byte *ptr, int16 layer); + + void markAsDirty(int16 x0, int16 y0, int16 x1, int16 y1); + + uint8 _xBlocks[MAXLAYERS]; + uint8 _yBlocks[MAXLAYERS]; + + // An array of sub-blocks, one for each of the parallax layers. + + BlockSurface **_blockSurfaces[MAXLAYERS]; + + uint16 _xScale[SCALE_MAXWIDTH]; + uint16 _yScale[SCALE_MAXHEIGHT]; + + void blitBlockSurface(BlockSurface *s, Common::Rect *r, Common::Rect *clipRect); + + uint16 _layer; + +public: + Screen(Sword2Engine *vm, int16 width, int16 height); + ~Screen(); + + int8 getRenderLevel(); + void setRenderLevel(int8 level); + + byte *getScreen() { return _buffer; } + byte *getPalette() { return _palette; } + ScreenInfo *getScreenInfo() { return &_thisScreen; } + + int16 getScreenWide() { return _screenWide; } + int16 getScreenDeep() { return _screenDeep; } + + uint32 getCurBgp0() { return _curBgp0; } + uint32 getCurBgp1() { return _curBgp1; } + uint32 getCurBack() { return _curBack; } + uint32 getCurSort() { return _curSort; } + uint32 getCurFore() { return _curFore; } + uint32 getCurFgp0() { return _curFgp0; } + uint32 getCurFgp1() { return _curFgp1; } + + uint32 getFps() { return _fps; } + + uint32 getLargestLayerArea() { return _largestLayerArea; } + uint32 getLargestSpriteArea() { return _largestSpriteArea; } + char *getLargestLayerInfo() { return _largestLayerInfo; } + char *getLargestSpriteInfo() { return _largestSpriteInfo; } + + void setNeedFullRedraw(); + + void clearScene(); + + void resetRenderLists(); + + void setLocationMetrics(uint16 w, uint16 h); + int32 initialiseBackgroundLayer(byte *parallax); + void closeBackgroundLayer(); + + void initialiseRenderCycle(); + + void initBackground(int32 res, int32 new_palette); + void registerFrame(byte *ob_mouse, byte *ob_graph, byte *ob_mega); + + void setScrollFraction(uint8 f) { _scrollFraction = f; } + void setScrollTarget(int16 x, int16 y); + void setScrolling(); + + void setFullPalette(int32 palRes); + void setPalette(int16 startEntry, int16 noEntries, byte *palette, uint8 setNow); + uint8 quickMatch(uint8 r, uint8 g, uint8 b); + int32 fadeUp(float time = 0.75); + int32 fadeDown(float time = 0.75); + uint8 getFadeStatus(); + void dimPalette(); + void waitForFade(); + void fadeServer(); + + void updateDisplay(bool redrawScene = true); + + void displayMsg(byte *text, int time); + + int32 createSurface(SpriteInfo *s, byte **surface); + void drawSurface(SpriteInfo *s, byte *surface, Common::Rect *clipRect = NULL); + void deleteSurface(byte *surface); + int32 drawSprite(SpriteInfo *s); + + void scaleImageFast(byte *dst, uint16 dstPitch, uint16 dstWidth, + uint16 dstHeight, byte *src, uint16 srcPitch, uint16 srcWidth, + uint16 srcHeight); + void scaleImageGood(byte *dst, uint16 dstPitch, uint16 dstWidth, + uint16 dstHeight, byte *src, uint16 srcPitch, uint16 srcWidth, + uint16 srcHeight, byte *backbuf); + + void updateRect(Common::Rect *r); + + int32 openLightMask(SpriteInfo *s); + int32 closeLightMask(); + + void buildDisplay(); + + void plotPoint(int x, int y, uint8 colour); + void drawLine(int x0, int y0, int x1, int y1, uint8 colour); + +#ifdef BACKEND_8BIT + void plotYUV(byte *lut, int width, int height, byte *const *dat); +#endif + + void rollCredits(); + void splashScreen(); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/console.cpp b/engines/sword2/console.cpp new file mode 100644 index 0000000000..b465dc369f --- /dev/null +++ b/engines/sword2/console.cpp @@ -0,0 +1,826 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/console.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/memory.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +#include "common/debugger.cpp" + +namespace Sword2 { + +Debugger::Debugger(Sword2Engine *vm) + : Common::Debugger<Debugger>() { + _vm = vm; + + memset(_debugTextBlocks, 0, sizeof(_debugTextBlocks)); + memset(_showVar, 0, sizeof(_showVar)); + + _displayDebugText = false; // "INFO" + _displayWalkGrid = false; // "WALKGRID" + _displayMouseMarker = false; // "MOUSE" + _displayTime = false; // "TIME" + _displayPlayerMarker = false; // "PLAYER" + _displayTextNumbers = false; // "TEXT" + + _definingRectangles = false; // "RECT" + _draggingRectangle = 0; // 0 = waiting to start new rect + // 1 = currently dragging a rectangle + + _rectX1 = _rectY1 = 0; + _rectX2 = _rectY2 = 0; + _rectFlicker = false; + + _testingSnR = false; // "SAVEREST" - for system to kill all + // object resources (except player) in + // fnAddHuman() + + _speechScriptWaiting = 0; // The id of whoever we're waiting for + // in a speech script. See fnTheyDo(), + // fnTheyDoWeWait(), fnWeWait(), and + // fnTimedWait(). + + _startTime = 0; // "TIMEON" & "TIMEOFF" - system start + // time + + _textNumber = 0; // Current system text line number + + _graphNoFrames = 0; // No. of frames in currently displayed + // anim + + // Register commands + + DCmd_Register("continue", &Debugger::Cmd_Exit); + DCmd_Register("exit", &Debugger::Cmd_Exit); + DCmd_Register("quit", &Debugger::Cmd_Exit); + DCmd_Register("q", &Debugger::Cmd_Exit); + DCmd_Register("help", &Debugger::Cmd_Help); + DCmd_Register("mem", &Debugger::Cmd_Mem); + DCmd_Register("tony", &Debugger::Cmd_Tony); + DCmd_Register("res", &Debugger::Cmd_Res); + DCmd_Register("reslist", &Debugger::Cmd_ResList); + DCmd_Register("starts", &Debugger::Cmd_Starts); + DCmd_Register("start", &Debugger::Cmd_Start); + DCmd_Register("s", &Debugger::Cmd_Start); + DCmd_Register("info", &Debugger::Cmd_Info); + DCmd_Register("walkgrid", &Debugger::Cmd_WalkGrid); + DCmd_Register("mouse", &Debugger::Cmd_Mouse); + DCmd_Register("player", &Debugger::Cmd_Player); + DCmd_Register("reslook", &Debugger::Cmd_ResLook); + DCmd_Register("cur", &Debugger::Cmd_CurrentInfo); + DCmd_Register("runlist", &Debugger::Cmd_RunList); + DCmd_Register("kill", &Debugger::Cmd_Kill); + DCmd_Register("nuke", &Debugger::Cmd_Nuke); + DCmd_Register("var", &Debugger::Cmd_Var); + DCmd_Register("rect", &Debugger::Cmd_Rect); + DCmd_Register("clear", &Debugger::Cmd_Clear); + DCmd_Register("debugon", &Debugger::Cmd_DebugOn); + DCmd_Register("debugoff", &Debugger::Cmd_DebugOff); + DCmd_Register("saverest", &Debugger::Cmd_SaveRest); + DCmd_Register("timeon", &Debugger::Cmd_TimeOn); + DCmd_Register("timeoff", &Debugger::Cmd_TimeOff); + DCmd_Register("text", &Debugger::Cmd_Text); + DCmd_Register("showvar", &Debugger::Cmd_ShowVar); + DCmd_Register("hidevar", &Debugger::Cmd_HideVar); + DCmd_Register("version", &Debugger::Cmd_Version); + DCmd_Register("animtest", &Debugger::Cmd_AnimTest); + DCmd_Register("texttest", &Debugger::Cmd_TextTest); + DCmd_Register("linetest", &Debugger::Cmd_LineTest); + DCmd_Register("events", &Debugger::Cmd_Events); + DCmd_Register("sfx", &Debugger::Cmd_Sfx); + DCmd_Register("english", &Debugger::Cmd_English); + DCmd_Register("finnish", &Debugger::Cmd_Finnish); + DCmd_Register("polish", &Debugger::Cmd_Polish); +} + +void Debugger::varGet(int var) { + DebugPrintf("%d\n", _vm->_logic->readVar(var)); +} + +void Debugger::varSet(int var, int val) { + DebugPrintf("was %d, ", _vm->_logic->readVar(var)); + _vm->_logic->writeVar(var, val); + DebugPrintf("now %d\n", _vm->_logic->readVar(var)); +} + +void Debugger::preEnter() { + // Pause sound output + if (_vm->_sound) { + _vm->_sound->pauseFx(); + _vm->_sound->pauseSpeech(); + _vm->_sound->pauseMusic(); + } +} + +void Debugger::postEnter() { + if (_vm->_sound) { + // Resume previous sound state + _vm->_sound->unpauseFx(); + _vm->_sound->unpauseSpeech(); + _vm->_sound->unpauseMusic(); + } + + if (_vm->_mouse) { + // Restore old mouse cursor + _vm->_mouse->drawMouse(); + } +} + +// Now the fun stuff: Commands + +bool Debugger::Cmd_Exit(int argc, const char **argv) { + _detach_now = true; + _vm->clearInputEvents(); + return false; +} + +bool Debugger::Cmd_Help(int argc, const char **argv) { + // console normally has 78 line width + // wrap around nicely + int width = 0; + + DebugPrintf("Commands are:\n"); + for (int i = 0 ; i < _dcmd_count ; i++) { + int size = strlen(_dcmds[i].name) + 1; + + if (width + size >= 75) { + DebugPrintf("\n"); + width = size; + } else + width += size; + + DebugPrintf("%s ", _dcmds[i].name); + } + + DebugPrintf("\n"); + return true; +} + +static int compare_blocks(const void *p1, const void *p2) { + const MemBlock *m1 = *(const MemBlock * const *)p1; + const MemBlock *m2 = *(const MemBlock * const *)p2; + + if (m1->size < m2->size) + return 1; + if (m1->size > m2->size) + return -1; + return 0; +} + +bool Debugger::Cmd_Mem(int argc, const char **argv) { + int16 numBlocks = _vm->_memory->getNumBlocks(); + MemBlock *memBlocks = _vm->_memory->getMemBlocks(); + + MemBlock **blocks = (MemBlock **)malloc(numBlocks * sizeof(MemBlock)); + + int i, j; + + for (i = 0, j = 0; i < MAX_MEMORY_BLOCKS; i++) { + if (memBlocks[i].ptr) + blocks[j++] = &memBlocks[i]; + } + + qsort(blocks, numBlocks, sizeof(MemBlock *), compare_blocks); + + DebugPrintf(" size id res type name\n"); + DebugPrintf("---------------------------------------------------------------------------\n"); + + for (i = 0; i < numBlocks; i++) { + const char *type; + + switch (_vm->_resman->fetchType(blocks[i]->ptr)) { + case ANIMATION_FILE: + type = "ANIMATION_FILE"; + break; + case SCREEN_FILE: + type = "SCREEN_FILE"; + break; + case GAME_OBJECT: + type = "GAME_OBJECT"; + break; + case WALK_GRID_FILE: + type = "WALK_GRID_FILE"; + break; + case GLOBAL_VAR_FILE: + type = "GLOBAL_VAR_FILE"; + break; + case PARALLAX_FILE_null: + type = "PARALLAX_FILE_null"; + break; + case RUN_LIST: + type = "RUN_LIST"; + break; + case TEXT_FILE: + type = "TEXT_FILE"; + break; + case SCREEN_MANAGER: + type = "SCREEN_MANAGER"; + break; + case MOUSE_FILE: + type = "MOUSE_FILE"; + break; + case WAV_FILE: + type = "WAV_FILE"; + break; + case ICON_FILE: + type = "ICON_FILE"; + break; + case PALETTE_FILE: + type = "PALETTE_FILE"; + break; + default: + type = "<unknown>"; + break; + } + + DebugPrintf("%9ld %-3d %-4d %-20s %s\n", blocks[i]->size, blocks[i]->id, blocks[i]->uid, type, _vm->_resman->fetchName(blocks[i]->ptr)); + } + + free(blocks); + + DebugPrintf("---------------------------------------------------------------------------\n"); + DebugPrintf("%9ld\n", _vm->_memory->getTotAlloc()); + + return true; +} + +bool Debugger::Cmd_Tony(int argc, const char **argv) { + DebugPrintf("What about him?\n"); + return true; +} + +bool Debugger::Cmd_Res(int argc, const char **argv) { + uint32 numClusters = _vm->_resman->getNumClusters(); + + if (!numClusters) { + DebugPrintf("Argh! No resources!\n"); + return true; + } + + ResourceFile *resFiles = _vm->_resman->getResFiles(); + + for (uint i = 0; i < numClusters; i++) { + const char *locStr[3] = { "HDD", "CD1", "CD2" }; + + DebugPrintf("%-20s %d\n", resFiles[i].fileName, locStr[resFiles[i].cd]); + } + + DebugPrintf("%d resources\n", _vm->_resman->getNumResFiles()); + return true; +} + +bool Debugger::Cmd_ResList(int argc, const char **argv) { + // By default, list only resources that are being held open. + uint minCount = 1; + + if (argc > 1) + minCount = atoi(argv[1]); + + uint32 numResFiles = _vm->_resman->getNumResFiles(); + Resource *resList = _vm->_resman->getResList(); + + for (uint i = 0; i < numResFiles; i++) { + if (resList[i].ptr && resList[i].refCount >= minCount) { + DebugPrintf("%-4d: %-35s refCount: %-3d\n", i, _vm->_resman->fetchName(resList[i].ptr), resList[i].refCount); + } + } + + return true; +} + +bool Debugger::Cmd_Starts(int argc, const char **argv) { + uint32 numStarts = _vm->getNumStarts(); + + if (!numStarts) { + DebugPrintf("Sorry - no startup positions registered?\n"); + + uint32 numScreenManagers = _vm->getNumScreenManagers(); + + if (!numScreenManagers) + DebugPrintf("There is a problem with startup.inf\n"); + else + DebugPrintf(" (%d screen managers found in startup.inf)\n", numScreenManagers); + return true; + } + + StartUp *startList = _vm->getStartList(); + + for (uint i = 0; i < numStarts; i++) + DebugPrintf("%d (%s)\n", i, startList[i].description); + + return true; +} + +bool Debugger::Cmd_Start(int argc, const char **argv) { + uint8 pal[4] = { 255, 255, 255, 0 }; + + if (argc != 2) { + DebugPrintf("Usage: %s number\n", argv[0]); + return true; + } + + uint32 numStarts = _vm->getNumStarts(); + + if (!numStarts) { + DebugPrintf("Sorry - there are no startups!\n"); + return true; + } + + int start = atoi(argv[1]); + + if (start < 0 || start >= (int)numStarts) { + DebugPrintf("Not a legal start position\n"); + return true; + } + + DebugPrintf("Running start %d\n", start); + + _vm->runStart(start); + _vm->_screen->setPalette(187, 1, pal, RDPAL_INSTANT); + return true; +} + +bool Debugger::Cmd_Info(int argc, const char **argv) { + _displayDebugText = !_displayDebugText; + + if (_displayDebugText) + DebugPrintf("Info text on\n"); + else + DebugPrintf("Info Text off\n"); + + return true; +} + +bool Debugger::Cmd_WalkGrid(int argc, const char **argv) { + _displayWalkGrid = !_displayWalkGrid; + + if (_displayWalkGrid) + DebugPrintf("Walk-grid display on\n"); + else + DebugPrintf("Walk-grid display off\n"); + + return true; +} + +bool Debugger::Cmd_Mouse(int argc, const char **argv) { + _displayMouseMarker = !_displayMouseMarker; + + if (_displayMouseMarker) + DebugPrintf("Mouse marker on\n"); + else + DebugPrintf("Mouse marker off\n"); + + return true; +} + +bool Debugger::Cmd_Player(int argc, const char **argv) { + _displayPlayerMarker = !_displayPlayerMarker; + + if (_displayPlayerMarker) + DebugPrintf("Player feet marker on\n"); + else + DebugPrintf("Player feet marker off\n"); + + return true; +} + +bool Debugger::Cmd_ResLook(int argc, const char **argv) { + if (argc != 2) { + DebugPrintf("Usage: %s number\n", argv[0]); + return true; + } + + int res = atoi(argv[1]); + uint32 numResFiles = _vm->_resman->getNumResFiles(); + + if (res < 0 || res >= (int)numResFiles) { + DebugPrintf("Illegal resource %d. There are %d resources, 0-%d.\n", + res, numResFiles, numResFiles - 1); + return true; + } + + if (!_vm->_resman->checkValid(res)) { + DebugPrintf("%d is a null & void resource number\n", res); + return true; + } + + // Open up the resource and take a look inside! + uint8 type = _vm->_resman->fetchType(res);; + byte name[NAME_LEN]; + + _vm->_resman->fetchName(res, name); + + switch (type) { + case ANIMATION_FILE: + DebugPrintf("<anim> %s\n", name); + break; + case SCREEN_FILE: + DebugPrintf("<layer> %s\n", name); + break; + case GAME_OBJECT: + DebugPrintf("<game object> %s\n", name); + break; + case WALK_GRID_FILE: + DebugPrintf("<walk grid> %s\n", name); + break; + case GLOBAL_VAR_FILE: + DebugPrintf("<global variables> %s\n", name); + break; + case PARALLAX_FILE_null: + DebugPrintf("<parallax file NOT USED!> %s\n", name); + break; + case RUN_LIST: + DebugPrintf("<run list> %s\n", name); + break; + case TEXT_FILE: + DebugPrintf("<text file> %s\n", name); + break; + case SCREEN_MANAGER: + DebugPrintf("<screen manager> %s\n", name); + break; + case MOUSE_FILE: + DebugPrintf("<mouse pointer> %s\n", name); + break; + case ICON_FILE: + DebugPrintf("<menu icon> %s\n", name); + break; + default: + DebugPrintf("unrecognised fileType %d\n", type); + break; + } + + return true; +} + +bool Debugger::Cmd_CurrentInfo(int argc, const char **argv) { + // prints general stuff about the screen, etc. + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + if (screenInfo->background_layer_id) { + DebugPrintf("background layer id %d\n", screenInfo->background_layer_id); + DebugPrintf("%d wide, %d high\n", screenInfo->screen_wide, screenInfo->screen_deep); + DebugPrintf("%d normal layers\n", screenInfo->number_of_layers); + + Cmd_RunList(argc, argv); + } else + DebugPrintf("No screen\n"); + return true; +} + +bool Debugger::Cmd_RunList(int argc, const char **argv) { + uint32 runList = _vm->_logic->getRunList(); + + if (runList) { + Common::MemoryReadStream readS(_vm->_resman->openResource(runList), _vm->_resman->fetchLen(runList)); + + readS.seek(ResHeader::size()); + + DebugPrintf("Runlist number %d\n", runList); + + while (1) { + uint32 res = readS.readUint32LE(); + if (!res) + break; + + byte name[NAME_LEN]; + + DebugPrintf("%d %s\n", res, _vm->_resman->fetchName(res, name)); + } + + _vm->_resman->closeResource(runList); + } else + DebugPrintf("No run list set\n"); + + return true; +} + +bool Debugger::Cmd_Kill(int argc, const char **argv) { + if (argc != 2) { + DebugPrintf("Usage: %s number\n", argv[0]); + return true; + } + + int res = atoi(argv[1]); + uint32 numResFiles = _vm->_resman->getNumResFiles(); + + if (res < 0 || res >= (int)numResFiles) { + DebugPrintf("Illegal resource %d. There are %d resources, 0-%d.\n", + res, numResFiles, numResFiles - 1); + return true; + } + + Resource *resList = _vm->_resman->getResList(); + + if (!resList[res].ptr) { + DebugPrintf("Resource %d is not in memory\n", res); + return true; + } + + if (resList[res].refCount) { + DebugPrintf("Resource %d is open - cannot remove\n", res); + return true; + } + + _vm->_resman->remove(res); + DebugPrintf("Trashed %d\n", res); + return true; +} + +bool Debugger::Cmd_Nuke(int argc, const char **argv) { + DebugPrintf("Killing all resources except variable file and player object\n"); + _vm->_resman->killAll(true); + return true; +} + +bool Debugger::Cmd_Var(int argc, const char **argv) { + switch (argc) { + case 2: + varGet(atoi(argv[1])); + break; + case 3: + varSet(atoi(argv[1]), atoi(argv[2])); + break; + default: + DebugPrintf("Usage: %s number value\n", argv[0]); + break; + } + + return true; +} + +bool Debugger::Cmd_Rect(int argc, const char **argv) { + uint32 filter = _vm->setInputEventFilter(0); + + _definingRectangles = !_definingRectangles; + + if (_definingRectangles) { + _vm->setInputEventFilter(filter & ~(RD_LEFTBUTTONUP | RD_RIGHTBUTTONUP)); + DebugPrintf("Mouse rectangles enabled\n"); + } else { + _vm->setInputEventFilter(filter | RD_LEFTBUTTONUP | RD_RIGHTBUTTONUP); + DebugPrintf("Mouse rectangles disabled\n"); + } + + _draggingRectangle = 0; + return true; +} + +bool Debugger::Cmd_Clear(int argc, const char **argv) { + _vm->_resman->killAllObjects(true); + return true; +} + +bool Debugger::Cmd_DebugOn(int argc, const char **argv) { + _displayDebugText = true; + _displayWalkGrid = true; + _displayMouseMarker = true; + _displayPlayerMarker = true; + _displayTextNumbers = true; + DebugPrintf("Enabled all on-screen debug info\n"); + return true; +} + +bool Debugger::Cmd_DebugOff(int argc, const char **argv) { + _displayDebugText = false; + _displayWalkGrid = false; + _displayMouseMarker = false; + _displayPlayerMarker = false; + _displayTextNumbers = false; + DebugPrintf("Disabled all on-screen debug info\n"); + return true; +} + +bool Debugger::Cmd_SaveRest(int argc, const char **argv) { + _testingSnR = !_testingSnR; + + if (_testingSnR) + DebugPrintf("Enabled S&R logic_script stability checking\n"); + else + DebugPrintf("Disabled S&R logic_script stability checking\n"); + + return true; +} + +bool Debugger::Cmd_TimeOn(int argc, const char **argv) { + if (argc == 2) + _startTime = _vm->_system->getMillis() - atoi(argv[1]) * 1000; + else if (_startTime == 0) + _startTime = _vm->_system->getMillis(); + _displayTime = true; + DebugPrintf("Timer display on\n"); + return true; +} + +bool Debugger::Cmd_TimeOff(int argc, const char **argv) { + _displayTime = false; + DebugPrintf("Timer display off\n"); + return true; +} + +bool Debugger::Cmd_Text(int argc, const char **argv) { + _displayTextNumbers = !_displayTextNumbers; + + if (_displayTextNumbers) + DebugPrintf("Text numbers on\n"); + else + DebugPrintf("Text numbers off\n"); + + return true; +} + +bool Debugger::Cmd_ShowVar(int argc, const char **argv) { + int32 showVarNo = 0; + int32 varNo; + + if (argc != 2) { + DebugPrintf("Usage: %s number\n", argv[0]); + return true; + } + + varNo = atoi(argv[1]); + + // search for a spare slot in the watch-list, but also watch out for + // this variable already being in the list + + while (showVarNo < MAX_SHOWVARS && _showVar[showVarNo] != 0 && _showVar[showVarNo] != varNo) + showVarNo++; + + // if we've found a spare slot or the variable's already there + if (showVarNo < MAX_SHOWVARS) { + if (_showVar[showVarNo] == 0) { + // empty slot - add it to the list at this slot + _showVar[showVarNo] = varNo; + DebugPrintf("var(%d) added to the watch-list\n", varNo); + } else + DebugPrintf("var(%d) already in the watch-list!\n", varNo); + } else + DebugPrintf("Sorry - no more allowed - hide one or extend the system watch-list\n"); + + return true; +} + +bool Debugger::Cmd_HideVar(int argc, const char **argv) { + int32 showVarNo = 0; + int32 varNo; + + if (argc != 2) { + DebugPrintf("Usage: %s number\n", argv[0]); + return true; + } + + varNo = atoi(argv[1]); + + // search for 'varNo' in the watch-list + while (showVarNo < MAX_SHOWVARS && _showVar[showVarNo] != varNo) + showVarNo++; + + if (showVarNo < MAX_SHOWVARS) { + // We've found 'varNo' in the list - clear this slot + _showVar[showVarNo] = 0; + DebugPrintf("var(%d) removed from watch-list\n", varNo); + } else + DebugPrintf("Sorry - can't find var(%d) in the list\n", varNo); + + return true; +} + +bool Debugger::Cmd_Version(int argc, const char **argv) { + // This function used to print more information, but nothing we + // particularly care about. + + DebugPrintf("\"Broken Sword II\" (c) Revolution Software 1997.\n"); + return true; +} + +bool Debugger::Cmd_AnimTest(int argc, const char **argv) { + if (argc != 2) { + DebugPrintf("Usage: %s value\n", argv[0]); + return true; + } + + // Automatically do "s 32" to run the animation testing start script + _vm->runStart(32); + + // Same as typing "VAR 912 <value>" at the console + varSet(912, atoi(argv[1])); + + DebugPrintf("Setting flag 'system_testing_anims'\n"); + return true; +} + +bool Debugger::Cmd_TextTest(int argc, const char **argv) { + if (argc != 2) { + DebugPrintf("Usage: %s value\n", argv[0]); + return true; + } + + // Automatically do "s 33" to run the text/speech testing start script + _vm->runStart(33); + + // Same as typing "VAR 1230 <value>" at the console + varSet(1230, atoi(argv[1])); + + _displayTextNumbers = true; + + DebugPrintf("Setting flag 'system_testing_text'\n"); + DebugPrintf("Text numbers on\n"); + return true; +} + +bool Debugger::Cmd_LineTest(int argc, const char **argv) { + if (argc != 3) { + DebugPrintf("Usage: %s value1 value2\n", argv[0]); + return true; + } + + // Automatically do "s 33" to run the text/speech testing start script + _vm->runStart(33); + + // Same as typing "VAR 1230 <value>" at the console + varSet(1230, atoi(argv[1])); + + // Same as typing "VAR 1264 <value>" at the console + varSet(1264, atoi(argv[2])); + + _displayTextNumbers = true; + + DebugPrintf("Setting flag 'system_testing_text'\n"); + DebugPrintf("Setting flag 'system_test_line_no'\n"); + DebugPrintf("Text numbers on\n"); + return true; +} + +bool Debugger::Cmd_Events(int argc, const char **argv) { + EventUnit *eventList = _vm->_logic->getEventList(); + + DebugPrintf("EVENT LIST:\n"); + + for (uint32 i = 0; i < MAX_events; i++) { + if (eventList[i].id) { + byte buf[NAME_LEN]; + uint32 target = eventList[i].id; + uint32 script = eventList[i].interact_id; + + DebugPrintf("slot %2d: id = %s (%d)\n", i, _vm->_resman->fetchName(target, buf), target); + DebugPrintf(" script = %s (%d) pos %d\n", _vm->_resman->fetchName(script / 65536, buf), script / 65536, script % 65536); + } + } + + return true; +} + +bool Debugger::Cmd_Sfx(int argc, const char **argv) { + _vm->_wantSfxDebug = !_vm->_wantSfxDebug; + + if (_vm->_wantSfxDebug) + DebugPrintf("SFX logging activated\n"); + else + DebugPrintf("SFX logging deactivated\n"); + + return true; +} + +bool Debugger::Cmd_English(int argc, const char **argv) { + _vm->initialiseFontResourceFlags(DEFAULT_TEXT); + DebugPrintf("Default fonts selected\n"); + return true; +} + +bool Debugger::Cmd_Finnish(int argc, const char **argv) { + _vm->initialiseFontResourceFlags(FINNISH_TEXT); + DebugPrintf("Finnish fonts selected\n"); + return true; +} + +bool Debugger::Cmd_Polish(int argc, const char **argv) { + _vm->initialiseFontResourceFlags(POLISH_TEXT); + DebugPrintf("Polish fonts selected\n"); + return true; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/console.h b/engines/sword2/console.h new file mode 100644 index 0000000000..95bfbe946d --- /dev/null +++ b/engines/sword2/console.h @@ -0,0 +1,127 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef C_ONSOLE_H +#define C_ONSOLE_H + +#include "common/debugger.h" +#include "sword2/debug.h" + +namespace Sword2 { + +class Debugger : public Common::Debugger<Debugger> { +private: + void varGet(int var); + void varSet(int var, int val); + + bool _displayDebugText; + bool _displayWalkGrid; + bool _displayMouseMarker; + bool _displayTime; + bool _displayPlayerMarker; + bool _displayTextNumbers; + + bool _rectFlicker; + + int32 _startTime; + + int32 _showVar[MAX_SHOWVARS]; + + byte _debugTextBlocks[MAX_DEBUG_TEXTS]; + + void clearDebugTextBlocks(); + void makeDebugTextBlock(char *text, int16 x, int16 y); + + void plotCrossHair(int16 x, int16 y, uint8 pen); + void drawRect(int16 x, int16 y, int16 x2, int16 y2, uint8 pen); + +public: + Debugger(Sword2Engine *vm); + + int16 _rectX1, _rectY1; + int16 _rectX2, _rectY2; + + uint8 _draggingRectangle; + bool _definingRectangles; + + bool _testingSnR; + + int32 _speechScriptWaiting; + + int32 _textNumber; + + int32 _graphType; + int32 _graphAnimRes; + int32 _graphAnimPc; + uint32 _graphNoFrames; + + void buildDebugText(); + void drawDebugGraphics(); + +protected: + Sword2Engine *_vm; + + virtual void preEnter(); + virtual void postEnter(); + + // Commands + bool Cmd_Exit(int argc, const char **argv); + bool Cmd_Help(int argc, const char **argv); + bool Cmd_Mem(int argc, const char **argv); + bool Cmd_Tony(int argc, const char **argv); + bool Cmd_Res(int argc, const char **argv); + bool Cmd_ResList(int argc, const char **argv); + bool Cmd_Starts(int argc, const char **argv); + bool Cmd_Start(int argc, const char **argv); + bool Cmd_Info(int argc, const char **argv); + bool Cmd_WalkGrid(int argc, const char **argv); + bool Cmd_Mouse(int argc, const char **argv); + bool Cmd_Player(int argc, const char **argv); + bool Cmd_ResLook(int argc, const char **argv); + bool Cmd_CurrentInfo(int argc, const char **argv); + bool Cmd_RunList(int argc, const char **argv); + bool Cmd_Kill(int argc, const char **argv); + bool Cmd_Nuke(int argc, const char **argv); + bool Cmd_Var(int argc, const char **argv); + bool Cmd_Rect(int argc, const char **argv); + bool Cmd_Clear(int argc, const char **argv); + bool Cmd_DebugOn(int argc, const char **argv); + bool Cmd_DebugOff(int argc, const char **argv); + bool Cmd_SaveRest(int argc, const char **argv); + bool Cmd_TimeOn(int argc, const char **argv); + bool Cmd_TimeOff(int argc, const char **argv); + bool Cmd_Text(int argc, const char **argv); + bool Cmd_ShowVar(int argc, const char **argv); + bool Cmd_HideVar(int argc, const char **argv); + bool Cmd_Version(int argc, const char **argv); + bool Cmd_AnimTest(int argc, const char **argv); + bool Cmd_TextTest(int argc, const char **argv); + bool Cmd_LineTest(int argc, const char **argv); + bool Cmd_Events(int argc, const char **argv); + bool Cmd_Sfx(int argc, const char **argv); + bool Cmd_English(int argc, const char **argv); + bool Cmd_Finnish(int argc, const char **argv); + bool Cmd_Polish(int argc, const char **argv); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/controls.cpp b/engines/sword2/controls.cpp new file mode 100644 index 0000000000..df1b38c83e --- /dev/null +++ b/engines/sword2/controls.cpp @@ -0,0 +1,1416 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/rect.h" +#include "common/config-manager.h" +#include "common/system.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/controls.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +#define MAX_STRING_LEN 64 // 20 was too low; better to be safe ;) +#define CHARACTER_OVERLAP 2 // overlap characters by 3 pixels + +// our fonts start on SPACE character (32) +#define SIZE_OF_CHAR_SET (256 - 32) + +namespace Sword2 { + +static int baseSlot = 0; + +class Widget; + +/** + * Base class for all widgets. + */ + +class Widget { +protected: + Sword2Engine *_vm; + Dialog *_parent; + + SpriteInfo *_sprites; + + struct WidgetSurface { + byte *_surface; + bool _original; + }; + + WidgetSurface *_surfaces; + int _numStates; + int _state; + + Common::Rect _hitRect; + +public: + Widget(Dialog *parent, int states); + + virtual ~Widget(); + + void createSurfaceImage(int state, uint32 res, int x, int y, uint32 pc); + void linkSurfaceImage(Widget *from, int state, int x, int y); + + void createSurfaceImages(uint32 res, int x, int y); + void linkSurfaceImages(Widget *from, int x, int y); + + void setHitRect(int x, int y, int width, int height); + bool isHit(int16 x, int16 y); + + void setState(int state); + int getState(); + + virtual void paint(Common::Rect *clipRect = NULL); + + virtual void onMouseEnter() {} + virtual void onMouseExit() {} + virtual void onMouseMove(int x, int y) {} + virtual void onMouseDown(int x, int y) {} + virtual void onMouseUp(int x, int y) {} + virtual void onWheelUp(int x, int y) {} + virtual void onWheelDown(int x, int y) {} + virtual void onKey(KeyboardEvent *ke) {} + virtual void onTick() {} + + virtual void releaseMouse(int x, int y) {} +}; + +/** + * This class is used to draw text in dialogs, buttons, etc. + */ + +class FontRendererGui { +private: + Sword2Engine *_vm; + + struct Glyph { + byte *_data; + int _width; + int _height; + }; + + Glyph _glyph[SIZE_OF_CHAR_SET]; + + int _fontId; + +public: + enum { + kAlignLeft, + kAlignRight, + kAlignCenter + }; + + FontRendererGui(Sword2Engine *vm, int fontId); + ~FontRendererGui(); + + void fetchText(uint32 textId, byte *buf); + + int getCharWidth(byte c); + int getCharHeight(byte c); + + int getTextWidth(byte *text); + int getTextWidth(uint32 textId); + + void drawText(byte *text, int x, int y, int alignment = kAlignLeft); + void drawText(uint32 textId, int x, int y, int alignment = kAlignLeft); +}; + +FontRendererGui::FontRendererGui(Sword2Engine *vm, int fontId) + : _vm(vm), _fontId(fontId) { + byte *font = _vm->_resman->openResource(fontId); + SpriteInfo sprite; + + sprite.type = RDSPR_NOCOMPRESSION | RDSPR_TRANS; + + for (int i = 0; i < SIZE_OF_CHAR_SET; i++) { + byte *frame = _vm->fetchFrameHeader(font, i); + + FrameHeader frame_head; + + frame_head.read(frame); + + sprite.data = frame + FrameHeader::size(); + sprite.w = frame_head.width; + sprite.h = frame_head.height; + _vm->_screen->createSurface(&sprite, &_glyph[i]._data); + _glyph[i]._width = frame_head.width; + _glyph[i]._height = frame_head.height; + } + + _vm->_resman->closeResource(fontId); +} + +FontRendererGui::~FontRendererGui() { + for (int i = 0; i < SIZE_OF_CHAR_SET; i++) + _vm->_screen->deleteSurface(_glyph[i]._data); +} + +void FontRendererGui::fetchText(uint32 textId, byte *buf) { + byte *data = _vm->fetchTextLine(_vm->_resman->openResource(textId / SIZE), textId & 0xffff); + int i; + + for (i = 0; data[i + 2]; i++) { + if (buf) + buf[i] = data[i + 2]; + } + + buf[i] = 0; + _vm->_resman->closeResource(textId / SIZE); +} + +int FontRendererGui::getCharWidth(byte c) { + if (c < 32) + return 0; + return _glyph[c - 32]._width; +} + +int FontRendererGui::getCharHeight(byte c) { + if (c < 32) + return 0; + return _glyph[c - 32]._height; +} + +int FontRendererGui::getTextWidth(byte *text) { + int textWidth = 0; + + for (int i = 0; text[i]; i++) + if (text[i] >= ' ') + textWidth += (getCharWidth(text[i]) - CHARACTER_OVERLAP); + return textWidth; +} + +int FontRendererGui::getTextWidth(uint32 textId) { + byte text[MAX_STRING_LEN]; + + fetchText(textId, text); + return getTextWidth(text); +} + +void FontRendererGui::drawText(byte *text, int x, int y, int alignment) { + SpriteInfo sprite; + int i; + + if (alignment != kAlignLeft) { + int textWidth = getTextWidth(text); + + switch (alignment) { + case kAlignRight: + x -= textWidth; + break; + case kAlignCenter: + x -= (textWidth / 2); + break; + } + } + + sprite.x = x; + sprite.y = y; + + for (i = 0; text[i]; i++) { + if (text[i] >= ' ') { + sprite.w = getCharWidth(text[i]); + sprite.h = getCharHeight(text[i]); + + _vm->_screen->drawSurface(&sprite, _glyph[text[i] - 32]._data); + + sprite.x += (getCharWidth(text[i]) - CHARACTER_OVERLAP); + } + } +} + +void FontRendererGui::drawText(uint32 textId, int x, int y, int alignment) { + byte text[MAX_STRING_LEN]; + + fetchText(textId, text); + drawText(text, x, y, alignment); +} + +// +// Dialog class functions +// + +Dialog::Dialog(Sword2Engine *vm) + : _numWidgets(0), _finish(false), _result(0), _vm(vm) { + _vm->_screen->setFullPalette(CONTROL_PANEL_PALETTE); + _vm->_screen->clearScene(); + _vm->_screen->updateDisplay(); + + // Usually the mouse pointer will already be "normal", but not always. + _vm->_mouse->setMouse(NORMAL_MOUSE_ID); +} + +Dialog::~Dialog() { + for (int i = 0; i < _numWidgets; i++) + delete _widgets[i]; + _vm->_screen->clearScene(); + _vm->_screen->updateDisplay(); +} + +void Dialog::registerWidget(Widget *widget) { + if (_numWidgets < MAX_WIDGETS) + _widgets[_numWidgets++] = widget; +} + +void Dialog::paint() { + _vm->_screen->clearScene(); + for (int i = 0; i < _numWidgets; i++) + _widgets[i]->paint(); +} + +void Dialog::setResult(int result) { + _result = result; + _finish = true; +} + +int Dialog::runModal() { + uint32 oldFilter = _vm->setInputEventFilter(0); + + int i; + + paint(); + + int oldMouseX = -1; + int oldMouseY = -1; + + while (!_finish) { + // So that the menu icons will reach their full size + _vm->_mouse->processMenu(); + _vm->_screen->updateDisplay(false); + + int newMouseX, newMouseY; + + _vm->_mouse->getPos(newMouseX, newMouseY); + + newMouseY += 40; + + MouseEvent *me = _vm->mouseEvent(); + KeyboardEvent *ke = _vm->keyboardEvent(); + + if (ke) { + if (ke->keycode == 27) + setResult(0); + else if (ke->keycode == '\n' || ke->keycode == '\r') + setResult(1); + } + + int oldHit = -1; + int newHit = -1; + + // Find out which widget the mouse was over the last time, and + // which it is currently over. This assumes the widgets do not + // overlap. + + for (i = 0; i < _numWidgets; i++) { + if (_widgets[i]->isHit(oldMouseX, oldMouseY)) + oldHit = i; + if (_widgets[i]->isHit(newMouseX, newMouseY)) + newHit = i; + } + + // Was the mouse inside a widget the last time? + + if (oldHit >= 0) { + if (newHit != oldHit) + _widgets[oldHit]->onMouseExit(); + } + + // Is the mouse currently in a widget? + + if (newHit >= 0) { + if (newHit != oldHit) + _widgets[newHit]->onMouseEnter(); + + if (me) { + switch (me->buttons) { + case RD_LEFTBUTTONDOWN: + _widgets[newHit]->onMouseDown(newMouseX, newMouseY); + break; + case RD_LEFTBUTTONUP: + _widgets[newHit]->onMouseUp(newMouseX, newMouseY); + break; + case RD_WHEELUP: + _widgets[newHit]->onWheelUp(newMouseX, newMouseY); + break; + case RD_WHEELDOWN: + _widgets[newHit]->onWheelDown(newMouseX, newMouseY); + break; + } + } + } + + // Some events are passed to the widgets regardless of where + // the mouse cursor is. + + for (i = 0; i < _numWidgets; i++) { + if (me && me->buttons == RD_LEFTBUTTONUP) { + // So that slider widgets will know when the + // user releases the mouse button, even if the + // cursor is outside of the slider's hit area. + _widgets[i]->releaseMouse(newMouseX, newMouseY); + } + + // This is to make it easier to drag the slider widget + + if (newMouseX != oldMouseX || newMouseY != oldMouseY) + _widgets[i]->onMouseMove(newMouseX, newMouseY); + + if (ke) + _widgets[i]->onKey(ke); + + _widgets[i]->onTick(); + } + + oldMouseX = newMouseX; + oldMouseY = newMouseY; + + _vm->_system->delayMillis(20); + + if (_vm->_quit) + setResult(0); + } + + _vm->setInputEventFilter(oldFilter); + return _result; +} + +// +// Widget functions +// + +Widget::Widget(Dialog *parent, int states) + : _vm(parent->_vm), _parent(parent), _numStates(states), _state(0) { + _sprites = (SpriteInfo *)calloc(states, sizeof(SpriteInfo)); + _surfaces = (WidgetSurface *)calloc(states, sizeof(WidgetSurface)); + + _hitRect.left = _hitRect.right = _hitRect.top = _hitRect.bottom = -1; +} + +Widget::~Widget() { + for (int i = 0; i < _numStates; i++) { + if (_surfaces[i]._original) + _vm->_screen->deleteSurface(_surfaces[i]._surface); + } + free(_sprites); + free(_surfaces); +} + +void Widget::createSurfaceImage(int state, uint32 res, int x, int y, uint32 pc) { + byte *file, *colTablePtr = NULL; + AnimHeader anim_head; + FrameHeader frame_head; + CdtEntry cdt_entry; + uint32 spriteType = RDSPR_TRANS; + + // open anim resource file, point to base + file = _vm->_resman->openResource(res); + + byte *frame = _vm->fetchFrameHeader(file, pc); + + anim_head.read(_vm->fetchAnimHeader(file)); + cdt_entry.read(_vm->fetchCdtEntry(file, pc)); + frame_head.read(frame); + + // If the frame is flipped. (Only really applicable to frames using + // offsets.) + + if (cdt_entry.frameType & FRAME_FLIPPED) + spriteType |= RDSPR_FLIP; + + // Which compression was used? + + switch (anim_head.runTimeComp) { + case NONE: + spriteType |= RDSPR_NOCOMPRESSION; + break; + case RLE256: + spriteType |= RDSPR_RLE256; + break; + case RLE16: + spriteType |= RDSPR_RLE256; + // Points to just after last cdt_entry, i.e. start of colour + // table + colTablePtr = _vm->fetchAnimHeader(file) + AnimHeader::size() + + anim_head.noAnimFrames * CdtEntry::size(); + break; + } + + _sprites[state].x = x; + _sprites[state].y = y; + _sprites[state].w = frame_head.width; + _sprites[state].h = frame_head.height; + _sprites[state].scale = 0; + _sprites[state].type = spriteType; + _sprites[state].blend = anim_head.blend; + + // Points to just after frame header, ie. start of sprite data + _sprites[state].data = frame + FrameHeader::size(); + + _vm->_screen->createSurface(&_sprites[state], &_surfaces[state]._surface); + _surfaces[state]._original = true; + + // Release the anim resource + _vm->_resman->closeResource(res); +} + +void Widget::linkSurfaceImage(Widget *from, int state, int x, int y) { + _sprites[state].x = x; + _sprites[state].y = y; + _sprites[state].w = from->_sprites[state].w; + _sprites[state].h = from->_sprites[state].h; + _sprites[state].scale = from->_sprites[state].scale; + _sprites[state].type = from->_sprites[state].type; + _sprites[state].blend = from->_sprites[state].blend; + + _surfaces[state]._surface = from->_surfaces[state]._surface; + _surfaces[state]._original = false; +} + +void Widget::createSurfaceImages(uint32 res, int x, int y) { + for (int i = 0; i < _numStates; i++) + createSurfaceImage(i, res, x, y, i); +} + +void Widget::linkSurfaceImages(Widget *from, int x, int y) { + for (int i = 0; i < from->_numStates; i++) + linkSurfaceImage(from, i, x, y); +} + +void Widget::setHitRect(int x, int y, int width, int height) { + _hitRect.left = x; + _hitRect.right = x + width; + _hitRect.top = y; + _hitRect.bottom = y + height; +} + +bool Widget::isHit(int16 x, int16 y) { + return _hitRect.left >= 0 && _hitRect.contains(x, y); +} + +void Widget::setState(int state) { + if (state != _state) { + _state = state; + paint(); + } +} + +int Widget::getState() { + return _state; +} + +void Widget::paint(Common::Rect *clipRect) { + _vm->_screen->drawSurface(&_sprites[_state], _surfaces[_state]._surface, clipRect); +} + +/** + * Standard button class. + */ + +class Button : public Widget { +public: + Button(Dialog *parent, int x, int y, int w, int h) + : Widget(parent, 2) { + setHitRect(x, y, w, h); + } + + virtual void onMouseExit() { + setState(0); + } + + virtual void onMouseDown(int x, int y) { + setState(1); + } + + virtual void onMouseUp(int x, int y) { + if (getState() != 0) { + setState(0); + _parent->onAction(this); + } + } +}; + +/** + * Scroll buttons are used to scroll the savegame list. The difference between + * this and a normal button is that we want this to repeat. + */ + +class ScrollButton : public Widget { +private: + uint32 _holdCounter; + +public: + ScrollButton(Dialog *parent, int x, int y, int w, int h) + : Widget(parent, 2), _holdCounter(0) { + setHitRect(x, y, w, h); + } + + virtual void onMouseExit() { + setState(0); + } + + virtual void onMouseDown(int x, int y) { + setState(1); + _parent->onAction(this); + _holdCounter = 0; + } + + virtual void onMouseUp(int x, int y) { + setState(0); + } + + virtual void onTick() { + if (getState() != 0) { + _holdCounter++; + if (_holdCounter > 16 && (_holdCounter % 4) == 0) + _parent->onAction(this); + } + } +}; + +/** + * A switch is a button that changes state when clicked, and keeps that state + * until clicked again. + */ + +class Switch : public Widget { +private: + bool _holding, _value; + int _upState, _downState; + +public: + Switch(Dialog *parent, int x, int y, int w, int h) + : Widget(parent, 2), _holding(false), _value(false), + _upState(0), _downState(1) { + setHitRect(x, y, w, h); + } + + // The sound mute switches have 0 as their "down" state and 1 as + // their "up" state, so this function is needed to get consistent + // behaviour. + + void reverseStates() { + _upState = 1; + _downState = 0; + } + + void setValue(bool value) { + _value = value; + if (_value) + setState(_downState); + else + setState(_upState); + } + + bool getValue() { + return _value; + } + + virtual void onMouseExit() { + if (_holding && !_value) + setState(_upState); + _holding = false; + } + + virtual void onMouseDown(int x, int y) { + _holding = true; + setState(_downState); + } + + virtual void onMouseUp(int x, int y) { + if (_holding) { + _holding = false; + _value = !_value; + if (_value) + setState(_downState); + else + setState(_upState); + _parent->onAction(this, getState()); + } + } +}; + +/** + * A slider is used to specify a value within a pre-defined range. + */ + +class Slider : public Widget { +private: + Widget *_background; + bool _dragging; + int _value, _targetValue; + int _maxValue; + int _valueStep; + int _dragOffset; + + int posFromValue(int value) { + return _hitRect.left + (value * (_hitRect.width() - 38)) / _maxValue; + } + + int valueFromPos(int x) { + return (int)((double)(_maxValue * (x - _hitRect.left)) / (double)(_hitRect.width() - 38) + 0.5); + } + +public: + Slider(Dialog *parent, Widget *background, int max, + int x, int y, int w, int h, int step, Widget *base = NULL) + : Widget(parent, 1), _background(background), + _dragging(false), _value(0), _targetValue(0), + _maxValue(max), _valueStep(step) { + setHitRect(x, y, w, h); + + if (_valueStep <= 0) + _valueStep = 1; + + if (base) + linkSurfaceImages(base, x, y); + else + createSurfaceImages(3406, x, y); + } + + virtual void paint(Common::Rect *clipRect = NULL) { + // This will redraw a bit more than is strictly necessary, + // but I doubt that will make any noticeable difference. + + _background->paint(&_hitRect); + Widget::paint(clipRect); + } + + void setValue(int value) { + _value = value; + _targetValue = value; + _sprites[0].x = posFromValue(_value); + paint(); + } + + int getValue() { + return _value; + } + + virtual void onMouseMove(int x, int y) { + if (_dragging) { + int newX = x - _dragOffset; + int newValue; + + if (newX < _hitRect.left) + newX = _hitRect.left; + else if (newX + 38 > _hitRect.right) + newX = _hitRect.right - 38; + + _sprites[0].x = newX; + + newValue = valueFromPos(newX); + if (newValue != _value) { + _value = newValue; + _targetValue = newValue; + _parent->onAction(this, newValue); + } + + paint(); + } + } + + virtual void onMouseDown(int x, int y) { + if (x >= _sprites[0].x && x < _sprites[0].x + 38) { + _dragging = true; + _dragOffset = x - _sprites[0].x; + } else if (x < _sprites[0].x) { + if (_targetValue >= _valueStep) + _targetValue -= _valueStep; + else + _targetValue = 0; + } else { + if (_targetValue < _maxValue - _valueStep) + _targetValue += _valueStep; + else + _targetValue = _maxValue; + } + } + + virtual void releaseMouse(int x, int y) { + if (_dragging) + _dragging = false; + } + + virtual void onTick() { + if (!_dragging) { + int target = posFromValue(_targetValue); + + if (target != _sprites[0].x) { + if (target < _sprites[0].x) { + _sprites[0].x -= 4; + if (_sprites[0].x < target) + _sprites[0].x = target; + } else if (target > _sprites[0].x) { + _sprites[0].x += 4; + if (_sprites[0].x > target) + _sprites[0].x = target; + } + + int newValue = valueFromPos(_sprites[0].x); + if (newValue != _value) { + _value = newValue; + _parent->onAction(this, newValue); + } + + paint(); + } + } + } +}; + +/** + * The "mini" dialog. + */ + +MiniDialog::MiniDialog(Sword2Engine *vm, uint32 headerTextId, uint32 okTextId, uint32 cancelTextId) : Dialog(vm) { + _headerTextId = headerTextId; + _okTextId = okTextId; + _cancelTextId = cancelTextId; + + _fr = new FontRendererGui(_vm, _vm->_controlsFontId); + + _panel = new Widget(this, 1); + _panel->createSurfaceImages(1996, 203, 104); + + _okButton = new Button(this, 243, 214, 24, 24); + _okButton->createSurfaceImages(2002, 243, 214); + + _cancelButton = new Button(this, 243, 276, 24, 24); + _cancelButton->linkSurfaceImages(_okButton, 243, 276); + + registerWidget(_panel); + registerWidget(_okButton); + registerWidget(_cancelButton); +} + +MiniDialog::~MiniDialog() { + delete _fr; +} + +void MiniDialog::paint() { + Dialog::paint(); + + if (_headerTextId) + _fr->drawText(_headerTextId, 310, 134, FontRendererGui::kAlignCenter); + _fr->drawText(_okTextId, 270, 214); + _fr->drawText(_cancelTextId, 270, 276); +} + +void MiniDialog::onAction(Widget *widget, int result) { + if (widget == _okButton) + setResult(1); + else if (widget == _cancelButton) + setResult(0); +} + +StartDialog::StartDialog(Sword2Engine *vm) : MiniDialog(vm, 0) {} + +int StartDialog::runModal() { + while (1) { + MiniDialog startDialog(_vm, 0, TEXT_RESTART, TEXT_RESTORE); + + if (startDialog.runModal()) + return 1; + + if (_vm->_quit) + return 0; + + RestoreDialog restoreDialog(_vm); + + if (restoreDialog.runModal()) + return 0; + + if (_vm->_quit) + return 0; + } + + return 1; +} + +/** + * The restart dialog. + */ + +RestartDialog::RestartDialog(Sword2Engine *vm) : MiniDialog(vm, TEXT_RESTART) {} + +int RestartDialog::runModal() { + int result = MiniDialog::runModal(); + + if (result) + _vm->restartGame(); + + return result; +} + +/** + * The quit dialog. + */ + +QuitDialog::QuitDialog(Sword2Engine *vm) : MiniDialog(vm, TEXT_QUIT) {} + +int QuitDialog::runModal() { + int result = MiniDialog::runModal(); + + if (result) + _vm->closeGame(); + + return result; +} + +/** + * The game settings dialog. + */ + +OptionsDialog::OptionsDialog(Sword2Engine *vm) : Dialog(vm) { + _fr = new FontRendererGui(_vm, _vm->_controlsFontId); + + _mixer = _vm->_mixer; + + _panel = new Widget(this, 1); + _panel->createSurfaceImages(3405, 0, 40); + + _objectLabelsSwitch = new Switch(this, 304, 100, 53, 32); + _objectLabelsSwitch->createSurfaceImages(3687, 304, 100); + + _subtitlesSwitch = new Switch(this, 510, 100, 53, 32); + _subtitlesSwitch->linkSurfaceImages(_objectLabelsSwitch, 510, 100); + + _reverseStereoSwitch = new Switch(this, 304, 293, 53, 32); + _reverseStereoSwitch->linkSurfaceImages(_objectLabelsSwitch, 304, 293); + + _musicSwitch = new Switch(this, 516, 157, 40, 32); + _musicSwitch->createSurfaceImages(3315, 516, 157); + _musicSwitch->reverseStates(); + + _speechSwitch = new Switch(this, 516, 205, 40, 32); + _speechSwitch->linkSurfaceImages(_musicSwitch, 516, 205); + _speechSwitch->reverseStates(); + + _fxSwitch = new Switch(this, 516, 250, 40, 32); + _fxSwitch->linkSurfaceImages(_musicSwitch, 516, 250); + _fxSwitch->reverseStates(); + + int volStep = Audio::Mixer::kMaxMixerVolume / 10; + + _musicSlider = new Slider(this, _panel, Audio::Mixer::kMaxMixerVolume, 309, 161, 170, 27, volStep); + _speechSlider = new Slider(this, _panel, Audio::Mixer::kMaxMixerVolume, 309, 208, 170, 27, volStep, _musicSlider); + _fxSlider = new Slider(this, _panel, Audio::Mixer::kMaxMixerVolume, 309, 254, 170, 27, volStep, _musicSlider); + _gfxSlider = new Slider(this, _panel, 3, 309, 341, 170, 27, 1, _musicSlider); + + _gfxPreview = new Widget(this, 4); + _gfxPreview->createSurfaceImages(256, 495, 310); + + _okButton = new Button(this, 203, 382, 53, 32); + _okButton->createSurfaceImages(901, 203, 382); + + _cancelButton = new Button(this, 395, 382, 53, 32); + _cancelButton->linkSurfaceImages(_okButton, 395, 382); + + registerWidget(_panel); + registerWidget(_objectLabelsSwitch); + registerWidget(_subtitlesSwitch); + registerWidget(_reverseStereoSwitch); + registerWidget(_musicSwitch); + registerWidget(_speechSwitch); + registerWidget(_fxSwitch); + registerWidget(_musicSlider); + registerWidget(_speechSlider); + registerWidget(_fxSlider); + registerWidget(_gfxSlider); + registerWidget(_gfxPreview); + registerWidget(_okButton); + registerWidget(_cancelButton); + + _objectLabelsSwitch->setValue(_vm->_mouse->getObjectLabels()); + _subtitlesSwitch->setValue(_vm->getSubtitles()); + _reverseStereoSwitch->setValue(_vm->_sound->isReverseStereo()); + _musicSwitch->setValue(!_vm->_sound->isMusicMute()); + _speechSwitch->setValue(!_vm->_sound->isSpeechMute()); + _fxSwitch->setValue(!_vm->_sound->isFxMute()); + + _musicSlider->setValue(_mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType)); + _speechSlider->setValue(_mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType)); + _fxSlider->setValue(_mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType)); + + _gfxSlider->setValue(_vm->_screen->getRenderLevel()); + _gfxPreview->setState(_vm->_screen->getRenderLevel()); +} + +OptionsDialog::~OptionsDialog() { + delete _fr; +} + +void OptionsDialog::paint() { + Dialog::paint(); + + int maxWidth = 0; + int width; + + uint32 alignTextIds[] = { + TEXT_OBJECT_LABELS, + TEXT_MUSIC_VOLUME, + TEXT_SPEECH_VOLUME, + TEXT_FX_VOLUME, + TEXT_GFX_QUALITY, + TEXT_REVERSE_STEREO + }; + + for (int i = 0; i < ARRAYSIZE(alignTextIds); i++) { + width = _fr->getTextWidth(alignTextIds[i]); + if (width > maxWidth) + maxWidth = width; + } + + _fr->drawText(TEXT_OPTIONS, 321, 55, FontRendererGui::kAlignCenter); + _fr->drawText(TEXT_SUBTITLES, 500, 103, FontRendererGui::kAlignRight); + _fr->drawText(TEXT_OBJECT_LABELS, 299 - maxWidth, 103); + _fr->drawText(TEXT_MUSIC_VOLUME, 299 - maxWidth, 161); + _fr->drawText(TEXT_SPEECH_VOLUME, 299 - maxWidth, 208); + _fr->drawText(TEXT_FX_VOLUME, 299 - maxWidth, 254); + _fr->drawText(TEXT_REVERSE_STEREO, 299 - maxWidth, 296); + _fr->drawText(TEXT_GFX_QUALITY, 299 - maxWidth, 341); + _fr->drawText(TEXT_OK, 193, 382, FontRendererGui::kAlignRight); + _fr->drawText(TEXT_CANCEL, 385, 382, FontRendererGui::kAlignRight); +} + +void OptionsDialog::onAction(Widget *widget, int result) { + // Since there is music playing while the dialog is displayed we need + // to update music volume immediately. + + if (widget == _musicSwitch) { + _vm->_sound->muteMusic(result != 0); + } else if (widget == _musicSlider) { + _vm->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, result); + _vm->_sound->muteMusic(result == 0); + _musicSwitch->setValue(result != 0); + } else if (widget == _speechSlider) { + _speechSwitch->setValue(result != 0); + } else if (widget == _fxSlider) { + _fxSwitch->setValue(result != 0); + } else if (widget == _gfxSlider) { + _gfxPreview->setState(result); + _vm->_screen->setRenderLevel(result); + } else if (widget == _okButton) { + // Apply the changes + _vm->setSubtitles(_subtitlesSwitch->getValue()); + _vm->_mouse->setObjectLabels(_objectLabelsSwitch->getValue()); + _vm->_sound->muteMusic(!_musicSwitch->getValue()); + _vm->_sound->muteSpeech(!_speechSwitch->getValue()); + _vm->_sound->muteFx(!_fxSwitch->getValue()); + _vm->_sound->setReverseStereo(_reverseStereoSwitch->getValue()); + _mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, _musicSlider->getValue()); + _mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, _speechSlider->getValue()); + _mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, _fxSlider->getValue()); + _vm->_screen->setRenderLevel(_gfxSlider->getValue()); + + _vm->writeSettings(); + setResult(1); + } else if (widget == _cancelButton) { + // Revert the changes + _vm->readSettings(); + setResult(0); + } +} + +// Slot button actions. Note that keyboard input generates positive actions + +enum { + kSelectSlot = -1, + kDeselectSlot = -2, + kWheelDown = -3, + kWheelUp = -4, + kStartEditing = -5, + kCursorTick = -6 +}; + +class Slot : public Widget { +private: + int _mode; + FontRendererGui *_fr; + byte _text[SAVE_DESCRIPTION_LEN]; + bool _clickable; + bool _editable; + +public: + Slot(Dialog *parent, int x, int y, int w, int h) + : Widget(parent, 2), _clickable(false), _editable(false) { + setHitRect(x, y, w, h); + _text[0] = 0; + } + + void setMode(int mode) { + _mode = mode; + } + + void setClickable(bool clickable) { + _clickable = clickable; + } + + void setEditable(bool editable) { + _editable = editable; + _vm->_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, editable); + } + + bool isEditable() { + return _editable; + } + + void setText(FontRendererGui *fr, int slot, byte *text) { + _fr = fr; + if (text) + sprintf((char *)_text, "%d. %s", slot, text); + else + sprintf((char *)_text, "%d. ", slot); + } + + byte *getText() { + return &_text[0]; + } + + virtual void paint(Common::Rect *clipRect = NULL) { + Widget::paint(); + + // HACK: The main dialog is responsible for drawing the text + // when in editing mode. + + if (!_editable) + _fr->drawText(_text, _sprites[0].x + 16, _sprites[0].y + 4 + 2 * getState()); + } + + virtual void onMouseDown(int x, int y) { + if (_clickable) { + if (getState() == 0) { + setState(1); + _parent->onAction(this, kSelectSlot); + if (_mode == kSaveDialog) + _parent->onAction(this, kStartEditing); + } else if (_mode == kRestoreDialog) { + setState(0); + _parent->onAction(this, kDeselectSlot); + } + } + } + + virtual void onWheelUp(int x, int y) { + _parent->onAction(this, kWheelUp); + } + + virtual void onWheelDown(int x, int y) { + _parent->onAction(this, kWheelDown); + } + + virtual void onKey(KeyboardEvent *ke) { + if (_editable) { + if (ke->keycode == 8) + _parent->onAction(this, 8); + else if (ke->ascii >= ' ' && ke->ascii <= 255) { + // Accept the character if the font renderer + // has what looks like a valid glyph for it. + if (_fr->getCharWidth(ke->ascii)) + _parent->onAction(this, ke->ascii); + } + } + } + + virtual void onTick() { + if (_editable) + _parent->onAction(this, kCursorTick); + } + + void setY(int y) { + for (int i = 0; i < _numStates; i++) + _sprites[i].y = y; + setHitRect(_hitRect.left, y, _hitRect.width(), _hitRect.height()); + } + + int getY() { + return _sprites[0].y; + } +}; + +SaveRestoreDialog::SaveRestoreDialog(Sword2Engine *vm, int mode) : Dialog(vm) { + int i; + + _mode = mode; + _selectedSlot = -1; + + // FIXME: The "control font" and the "red font" are currently always + // the same font, so one should be eliminated. + + _fr1 = new FontRendererGui(_vm, _vm->_controlsFontId); + _fr2 = new FontRendererGui(_vm, _vm->_redFontId); + + _panel = new Widget(this, 1); + _panel->createSurfaceImages(2016, 0, 40); + + for (i = 0; i < 4; i++) { + _slotButton[i] = new Slot(this, 114, 0, 384, 36); + _slotButton[i]->createSurfaceImages(2006 + i, 114, 0); + _slotButton[i]->setMode(mode); + _slotButton[i + 4] = new Slot(this, 114, 0, 384, 36); + _slotButton[i + 4]->linkSurfaceImages(_slotButton[i], 114, 0); + _slotButton[i + 4]->setMode(mode); + } + + updateSlots(); + + _zupButton = new ScrollButton(this, 516, 65, 17, 17); + _zupButton->createSurfaceImages(1982, 516, 65); + + _upButton = new ScrollButton(this, 516, 85, 17, 17); + _upButton->createSurfaceImages(2067, 516, 85); + + _downButton = new ScrollButton(this, 516, 329, 17, 17); + _downButton->createSurfaceImages(1986, 516, 329); + + _zdownButton = new ScrollButton(this, 516, 350, 17, 17); + _zdownButton->createSurfaceImages(1988, 516, 350); + + _okButton = new Button(this, 130, 377, 24, 24); + _okButton->createSurfaceImages(2002, 130, 377); + + _cancelButton = new Button(this, 350, 377, 24, 24); + _cancelButton->linkSurfaceImages(_okButton, 350, 377); + + registerWidget(_panel); + + for (i = 0; i < 8; i++) + registerWidget(_slotButton[i]); + + registerWidget(_zupButton); + registerWidget(_upButton); + registerWidget(_downButton); + registerWidget(_zdownButton); + registerWidget(_okButton); + registerWidget(_cancelButton); +} + +SaveRestoreDialog::~SaveRestoreDialog() { + delete _fr1; + delete _fr2; +} + +// There aren't really a hundred different button objects of course, there are +// only eight. Re-arrange them to simulate scrolling. + +void SaveRestoreDialog::updateSlots() { + for (int i = 0; i < 8; i++) { + Slot *slot = _slotButton[(baseSlot + i) % 8]; + FontRendererGui *fr; + byte description[SAVE_DESCRIPTION_LEN]; + + slot->setY(72 + i * 36); + + if (baseSlot + i == _selectedSlot) { + slot->setEditable(_mode == kSaveDialog); + slot->setState(1); + fr = _fr2; + } else { + slot->setEditable(false); + slot->setState(0); + fr = _fr1; + } + + if (_vm->getSaveDescription(baseSlot + i, description) == SR_OK) { + slot->setText(fr, baseSlot + i, description); + slot->setClickable(true); + } else { + slot->setText(fr, baseSlot + i, NULL); + slot->setClickable(_mode == kSaveDialog); + } + + if (slot->isEditable()) + drawEditBuffer(slot); + else + slot->paint(); + } +} + +void SaveRestoreDialog::drawEditBuffer(Slot *slot) { + if (_selectedSlot == -1) + return; + + // This will redraw a bit more than is strictly necessary, but I doubt + // that will make any noticeable difference. + + slot->paint(); + _fr2->drawText(_editBuffer, 130, 78 + (_selectedSlot - baseSlot) * 36); +} + +void SaveRestoreDialog::onAction(Widget *widget, int result) { + if (widget == _zupButton) { + if (baseSlot > 0) { + if (baseSlot >= 8) + baseSlot -= 8; + else + baseSlot = 0; + updateSlots(); + } + } else if (widget == _upButton) { + if (baseSlot > 0) { + baseSlot--; + updateSlots(); + } + } else if (widget == _downButton) { + if (baseSlot < 92) { + baseSlot++; + updateSlots(); + } + } else if (widget == _zdownButton) { + if (baseSlot < 92) { + if (baseSlot <= 84) + baseSlot += 8; + else + baseSlot = 92; + updateSlots(); + } + } else if (widget == _okButton) { + setResult(1); + } else if (widget == _cancelButton) { + setResult(0); + } else { + Slot *slot = (Slot *)widget; + int textWidth; + byte tmp; + int i; + int j; + + switch (result) { + case kWheelUp: + onAction(_upButton); + break; + case kWheelDown: + onAction(_downButton); + break; + case kSelectSlot: + case kDeselectSlot: + if (result == kSelectSlot) + _selectedSlot = baseSlot + (slot->getY() - 72) / 35; + else if (result == kDeselectSlot) + _selectedSlot = -1; + + for (i = 0; i < 8; i++) + if (widget == _slotButton[i]) + break; + + for (j = 0; j < 8; j++) { + if (j != i) { + _slotButton[j]->setEditable(false); + _slotButton[j]->setState(0); + } + } + break; + case kStartEditing: + if (_selectedSlot >= 10) + _firstPos = 5; + else + _firstPos = 4; + + strcpy((char *)_editBuffer, (char *)slot->getText()); + _editPos = strlen((char *)_editBuffer); + _cursorTick = 0; + _editBuffer[_editPos] = '_'; + _editBuffer[_editPos + 1] = 0; + slot->setEditable(true); + drawEditBuffer(slot); + break; + case kCursorTick: + _cursorTick++; + if (_cursorTick == 7) { + _editBuffer[_editPos] = ' '; + drawEditBuffer(slot); + } else if (_cursorTick == 14) { + _cursorTick = 0; + _editBuffer[_editPos] = '_'; + drawEditBuffer(slot); + } + break; + case 8: + if (_editPos > _firstPos) { + _editBuffer[_editPos - 1] = _editBuffer[_editPos]; + _editBuffer[_editPos--] = 0; + drawEditBuffer(slot); + } + break; + default: + tmp = _editBuffer[_editPos]; + _editBuffer[_editPos] = 0; + textWidth = _fr2->getTextWidth(_editBuffer); + _editBuffer[_editPos] = tmp; + + if (textWidth < 340 && _editPos < SAVE_DESCRIPTION_LEN - 2) { + _editBuffer[_editPos + 1] = _editBuffer[_editPos]; + _editBuffer[_editPos + 2] = 0; + _editBuffer[_editPos++] = result; + drawEditBuffer(slot); + } + break; + } + } +} + +void SaveRestoreDialog::paint() { + Dialog::paint(); + + _fr1->drawText((_mode == kRestoreDialog) ? TEXT_RESTORE : TEXT_SAVE, 165, 377); + _fr1->drawText(TEXT_CANCEL, 382, 377); +} + +void SaveRestoreDialog::setResult(int result) { + if (result) { + if (_selectedSlot == -1) + return; + + if (_mode == kSaveDialog) { + if (_editPos <= _firstPos) + return; + } + } + + Dialog::setResult(result); +} + +int SaveRestoreDialog::runModal() { + int result = Dialog::runModal(); + + if (result) { + switch (_mode) { + case kSaveDialog: + // Remove the cursor character from the savegame name + _editBuffer[_editPos] = 0; + + if (_vm->saveGame(_selectedSlot, (byte *)&_editBuffer[_firstPos]) != SR_OK) + result = 0; + break; + case kRestoreDialog: + if (_vm->restoreGame(_selectedSlot) != SR_OK) + result = 0; + break; + } + } + + return result; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/controls.h b/engines/sword2/controls.h new file mode 100644 index 0000000000..719489b3ae --- /dev/null +++ b/engines/sword2/controls.h @@ -0,0 +1,183 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _CONTROL_S +#define _CONTROL_S + +#include "sword2/defs.h" + +#define MAX_WIDGETS 25 + +namespace Sword2 { + +class Sword2Engine; +class FontRendererGui; +class Widget; +class Switch; +class Slider; +class Button; +class ScrollButton; +class Slot; + +enum { + kSaveDialog, + kRestoreDialog +}; + +/** + * Base class for all dialogs. + */ + +class Dialog { +private: + int _numWidgets; + Widget *_widgets[MAX_WIDGETS]; + bool _finish; + int _result; + +public: + Sword2Engine *_vm; + + Dialog(Sword2Engine *vm); + virtual ~Dialog(); + + void registerWidget(Widget *widget); + + virtual void paint(); + virtual void setResult(int result); + + virtual int runModal(); + + virtual void onAction(Widget *widget, int result = 0) {} +}; + +class OptionsDialog : public Dialog { +private: + FontRendererGui *_fr; + Widget *_panel; + Switch *_objectLabelsSwitch; + Switch *_subtitlesSwitch; + Switch *_reverseStereoSwitch; + Switch *_musicSwitch; + Switch *_speechSwitch; + Switch *_fxSwitch; + Slider *_musicSlider; + Slider *_speechSlider; + Slider *_fxSlider; + Slider *_gfxSlider; + Widget *_gfxPreview; + Button *_okButton; + Button *_cancelButton; + + Audio::Mixer *_mixer; + +public: + OptionsDialog(Sword2Engine *vm); + ~OptionsDialog(); + + virtual void paint(); + virtual void onAction(Widget *widget, int result = 0); +}; + +class SaveRestoreDialog : public Dialog { +private: + int _mode, _selectedSlot; + byte _editBuffer[SAVE_DESCRIPTION_LEN]; + int _editPos, _firstPos; + int _cursorTick; + + FontRendererGui *_fr1; + FontRendererGui *_fr2; + Widget *_panel; + Slot *_slotButton[8]; + ScrollButton *_zupButton; + ScrollButton *_upButton; + ScrollButton *_downButton; + ScrollButton *_zdownButton; + Button *_okButton; + Button *_cancelButton; + +public: + SaveRestoreDialog(Sword2Engine *vm, int mode); + ~SaveRestoreDialog(); + + void updateSlots(); + void drawEditBuffer(Slot *slot); + + virtual void onAction(Widget *widget, int result = 0); + virtual void paint(); + virtual void setResult(int result); + virtual int runModal(); +}; + +/** + * A "mini" dialog is usually a yes/no question, but also used for the + * restart/restore dialog at the beginning of the game. + */ + +class MiniDialog : public Dialog { +private: + uint32 _headerTextId; + uint32 _okTextId; + uint32 _cancelTextId; + FontRendererGui *_fr; + Widget *_panel; + Button *_okButton; + Button *_cancelButton; + +public: + MiniDialog(Sword2Engine *vm, uint32 headerTextId, uint32 okTextId = TEXT_OK, uint32 cancelTextId = TEXT_CANCEL); + virtual ~MiniDialog(); + virtual void paint(); + virtual void onAction(Widget *widget, int result = 0); +}; + +class StartDialog : public MiniDialog { +public: + StartDialog(Sword2Engine *vm); + virtual int runModal(); +}; + +class RestartDialog : public MiniDialog { +public: + RestartDialog(Sword2Engine *vm); + virtual int runModal(); +}; + +class QuitDialog : public MiniDialog { +public: + QuitDialog(Sword2Engine *vm); + virtual int runModal(); +}; + +class SaveDialog : public SaveRestoreDialog { +public: + SaveDialog(Sword2Engine *vm) : SaveRestoreDialog(vm, kSaveDialog) {} +}; + +class RestoreDialog : public SaveRestoreDialog { +public: + RestoreDialog(Sword2Engine *vm) : SaveRestoreDialog(vm, kRestoreDialog) {} +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/d_draw.cpp b/engines/sword2/d_draw.cpp new file mode 100644 index 0000000000..7f278996fb --- /dev/null +++ b/engines/sword2/d_draw.cpp @@ -0,0 +1,71 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/system.h" +#include "sword2/sword2.h" +#include "sword2/mouse.h" + +namespace Sword2 { + +/** + * @return the graphics detail setting + */ + +int8 Screen::getRenderLevel() { + return _renderLevel; +} + +void Screen::setRenderLevel(int8 level) { + _renderLevel = level; + + switch (_renderLevel) { + case 0: + // Lowest setting: no fancy stuff + _renderCaps = 0; + break; + case 1: + // Medium-low setting: transparency-blending + _renderCaps = RDBLTFX_SPRITEBLEND; + break; + case 2: + // Medium-high setting: transparency-blending + shading + _renderCaps = RDBLTFX_SPRITEBLEND | RDBLTFX_SHADOWBLEND; + break; + case 3: + // Highest setting: transparency-blending + shading + + // edge-blending + improved stretching + _renderCaps = RDBLTFX_SPRITEBLEND | RDBLTFX_SHADOWBLEND | RDBLTFX_EDGEBLEND; + break; + } +} + +/** + * Fill the screen buffer with palette colour zero. Note that it does not + * touch the menu areas of the screen. + */ + +void Screen::clearScene() { + memset(_buffer + MENUDEEP * _screenWide, 0, _screenWide * RENDERDEEP); + _needFullRedraw = true; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/debug.cpp b/engines/sword2/debug.cpp new file mode 100644 index 0000000000..53fc200241 --- /dev/null +++ b/engines/sword2/debug.cpp @@ -0,0 +1,377 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/console.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/memory.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/router.h" + +namespace Sword2 { + +void Debugger::clearDebugTextBlocks() { + uint8 blockNo = 0; + + while (blockNo < MAX_DEBUG_TEXTS && _debugTextBlocks[blockNo] > 0) { + // kill the system text block + _vm->_fontRenderer->killTextBloc(_debugTextBlocks[blockNo]); + + // clear this element of our array of block numbers + _debugTextBlocks[blockNo] = 0; + + blockNo++; + } +} + +void Debugger::makeDebugTextBlock(char *text, int16 x, int16 y) { + uint8 blockNo = 0; + + while (blockNo < MAX_DEBUG_TEXTS && _debugTextBlocks[blockNo] > 0) + blockNo++; + + assert(blockNo < MAX_DEBUG_TEXTS); + + _debugTextBlocks[blockNo] = _vm->_fontRenderer->buildNewBloc((byte *)text, x, y, 640 - x, 0, RDSPR_DISPLAYALIGN, CONSOLE_FONT_ID, NO_JUSTIFICATION); +} + +void Debugger::buildDebugText() { + char buf[128]; + + int32 showVarNo; // for variable watching + int32 showVarPos; + int32 varNo; + + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + // clear the array of text block numbers for the debug text + clearDebugTextBlocks(); + + // mouse coords + // print mouse coords beside mouse-marker, if it's being displayed + if (_displayMouseMarker) { + int mouseX, mouseY; + + _vm->_mouse->getPos(mouseX, mouseY); + + sprintf(buf, "%d,%d", mouseX + screenInfo->scroll_offset_x, mouseY + screenInfo->scroll_offset_y); + if (mouseX > 560) + makeDebugTextBlock(buf, mouseX - 50, mouseY - 15); + else + makeDebugTextBlock(buf, mouseX + 5, mouseY - 15); + } + + // mouse area coords + + // defining a mouse area the easy way, by creating a box on-screen + if (_draggingRectangle || _vm->_logic->readVar(SYSTEM_TESTING_ANIMS)) { + // so we can see what's behind the lines + _rectFlicker = !_rectFlicker; + + sprintf(buf, "x1=%d", _rectX1); + makeDebugTextBlock(buf, 0, 120); + + sprintf(buf, "y1=%d", _rectY1); + makeDebugTextBlock(buf, 0, 135); + + sprintf(buf, "x2=%d", _rectX2); + makeDebugTextBlock(buf, 0, 150); + + sprintf(buf, "y2=%d", _rectY2); + makeDebugTextBlock(buf, 0, 165); + } + + // testingSnR indicator + + if (_testingSnR) { // see fnAddHuman() + sprintf(buf, "TESTING LOGIC STABILITY!"); + makeDebugTextBlock(buf, 0, 105); + } + +#ifdef SWORD2_DEBUG + // speed-up indicator + + if (_vm->_renderSkip) { // see sword2.cpp + sprintf(buf, "SKIPPING FRAMES FOR SPEED-UP!"); + makeDebugTextBlock(buf, 0, 120); + } +#endif + + // debug info at top of screen - enabled/disabled as one complete unit + + if (_displayTime) { + int32 time = _vm->getMillis(); + + if ((time - _startTime) / 1000 >= 10000) + _startTime = time; + + time -= _startTime; + sprintf(buf, "Time %.2d:%.2d:%.2d.%.3d", (time / 3600000) % 60, (time / 60000) % 60, (time / 1000) % 60, time % 1000); + makeDebugTextBlock(buf, 500, 360); + sprintf(buf, "Game %d", _vm->_gameCycle); + makeDebugTextBlock(buf, 500, 380); + } + + // current text number & speech-sample resource id + + if (_displayTextNumbers) { + if (_textNumber) { + if (_vm->_logic->readVar(SYSTEM_TESTING_TEXT)) { + if (_vm->_logic->readVar(SYSTEM_WANT_PREVIOUS_LINE)) + sprintf(buf, "backwards"); + else + sprintf(buf, "forwards"); + + makeDebugTextBlock(buf, 0, 340); + } + + sprintf(buf, "res: %d", _textNumber / SIZE); + makeDebugTextBlock(buf, 0, 355); + + sprintf(buf, "pos: %d", _textNumber & 0xffff); + makeDebugTextBlock(buf, 0, 370); + + sprintf(buf, "TEXT: %d", _vm->_logic->_officialTextNumber); + makeDebugTextBlock(buf, 0, 385); + } + } + + // resource number currently being checking for animation + + if (_vm->_logic->readVar(SYSTEM_TESTING_ANIMS)) { + sprintf(buf, "trying resource %d", _vm->_logic->readVar(SYSTEM_TESTING_ANIMS)); + makeDebugTextBlock(buf, 0, 90); + } + + // general debug info + + if (_displayDebugText) { + byte name[NAME_LEN]; + +/* + // CD in use + sprintf(buf, "CD-%d", currentCD); + makeDebugTextBlock(buf, 0, 0); +*/ + + // mouse coords & object pointed to + + if (_vm->_logic->readVar(CLICKED_ID)) + sprintf(buf, "last click at %d,%d (id %d: %s)", + _vm->_logic->readVar(MOUSE_X), + _vm->_logic->readVar(MOUSE_Y), + _vm->_logic->readVar(CLICKED_ID), + _vm->_resman->fetchName(_vm->_logic->readVar(CLICKED_ID), name)); + else + sprintf(buf, "last click at %d,%d (---)", + _vm->_logic->readVar(MOUSE_X), + _vm->_logic->readVar(MOUSE_Y)); + + makeDebugTextBlock(buf, 0, 15); + + uint32 mouseTouching = _vm->_mouse->getMouseTouching(); + + int mouseX, mouseY; + + _vm->_mouse->getPos(mouseX, mouseY); + + if (mouseTouching) + sprintf(buf, "mouse %d,%d (id %d: %s)", + mouseX + screenInfo->scroll_offset_x, + mouseY + screenInfo->scroll_offset_y, + mouseTouching, + _vm->_resman->fetchName(mouseTouching, name)); + else + sprintf(buf, "mouse %d,%d (not touching)", + mouseX + screenInfo->scroll_offset_x, + mouseY + screenInfo->scroll_offset_y); + + makeDebugTextBlock(buf, 0, 30); + + // player coords & graphic info + // if player objct has a graphic + + if (_graphAnimRes) + sprintf(buf, "player %d,%d %s (%d) #%d/%d", + screenInfo->player_feet_x, + screenInfo->player_feet_y, + _vm->_resman->fetchName(_graphAnimRes, name), + _graphAnimRes, + _graphAnimPc, + _graphNoFrames); + else + sprintf(buf, "player %d,%d --- %d", + screenInfo->player_feet_x, + screenInfo->player_feet_y, + _graphAnimPc); + + makeDebugTextBlock(buf, 0, 45); + + // frames-per-second counter + + sprintf(buf, "fps %d", _vm->_screen->getFps()); + makeDebugTextBlock(buf, 440, 0); + + // location number + + sprintf(buf, "location=%d", _vm->_logic->readVar(LOCATION)); + makeDebugTextBlock(buf, 440, 15); + + // "result" variable + + sprintf(buf, "result=%d", _vm->_logic->readVar(RESULT)); + makeDebugTextBlock(buf, 440, 30); + + // no. of events in event list + + sprintf(buf, "events=%d", _vm->_logic->countEvents()); + makeDebugTextBlock(buf, 440, 45); + + // sprite list usage + + sprintf(buf, "bgp0: %d/%d", _vm->_screen->getCurBgp0(), MAX_bgp0_sprites); + makeDebugTextBlock(buf, 560, 0); + + sprintf(buf, "bgp1: %d/%d", _vm->_screen->getCurBgp1(), MAX_bgp1_sprites); + makeDebugTextBlock(buf, 560, 15); + + sprintf(buf, "back: %d/%d", _vm->_screen->getCurBack(), MAX_back_sprites); + makeDebugTextBlock(buf, 560, 30); + + sprintf(buf, "sort: %d/%d", _vm->_screen->getCurSort(), MAX_sort_sprites); + makeDebugTextBlock(buf, 560, 45); + + sprintf(buf, "fore: %d/%d", _vm->_screen->getCurFore(), MAX_fore_sprites); + makeDebugTextBlock(buf, 560, 60); + + sprintf(buf, "fgp0: %d/%d", _vm->_screen->getCurFgp0(), MAX_fgp0_sprites); + makeDebugTextBlock(buf, 560, 75); + + sprintf(buf, "fgp1: %d/%d", _vm->_screen->getCurFgp1(), MAX_fgp1_sprites); + makeDebugTextBlock(buf, 560, 90); + + // largest layer & sprite + + // NB. Strings already constructed in Build_display.cpp + makeDebugTextBlock(_vm->_screen->getLargestLayerInfo(), 0, 60); + makeDebugTextBlock(_vm->_screen->getLargestSpriteInfo(), 0, 75); + + // "waiting for person" indicator - set form fnTheyDo and + // fnTheyDoWeWait + + if (_speechScriptWaiting) { + sprintf(buf, "script waiting for %s (%d)", + _vm->_resman->fetchName(_speechScriptWaiting, name), + _speechScriptWaiting); + makeDebugTextBlock(buf, 0, 90); + } + + // variable watch display + + showVarPos = 115; // y-coord for first showVar + + for (showVarNo = 0; showVarNo < MAX_SHOWVARS; showVarNo++) { + varNo = _showVar[showVarNo]; // get variable number + + // if non-zero ie. cannot watch 'id' but not needed + // anyway because it changes throughout the logic loop + + if (varNo) { + sprintf(buf, "var(%d) = %d", varNo, _vm->_logic->readVar(varNo)); + makeDebugTextBlock(buf, 530, showVarPos); + showVarPos += 15; // next line down + } + } + + // memory indicator - this should come last, to show all the + // sprite blocks above! + + uint32 totAlloc = _vm->_memory->getTotAlloc(); + int16 numBlocks = _vm->_memory->getNumBlocks(); + + if (totAlloc < 1024) + sprintf(buf, "%u bytes in %d memory blocks", totAlloc, numBlocks); + else if (totAlloc < 1024 * 1024) + sprintf(buf, "%uK in %d memory blocks", totAlloc / 1024, numBlocks); + else + sprintf(buf, "%.02fM in %d memory blocks", totAlloc / 1048576., numBlocks); + + makeDebugTextBlock(buf, 0, 0); + } +} + +void Debugger::drawDebugGraphics() { + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + // walk-grid + + if (_displayWalkGrid) + _vm->_logic->_router->plotWalkGrid(); + + // player feet coord marker + + if (_displayPlayerMarker) + plotCrossHair(screenInfo->player_feet_x, screenInfo->player_feet_y, 215); + + // mouse marker & coords + + if (_displayMouseMarker) { + int mouseX, mouseY; + + _vm->_mouse->getPos(mouseX, mouseY); + + plotCrossHair(mouseX + screenInfo->scroll_offset_x, mouseY + screenInfo->scroll_offset_y, 215); + } + + // mouse area rectangle / sprite box rectangle when testing anims + + if (_vm->_logic->readVar(SYSTEM_TESTING_ANIMS)) { + // draw box around current frame + drawRect(_rectX1, _rectY1, _rectX2, _rectY2, 184); + } else if (_draggingRectangle) { + // defining a mouse area the easy way, by creating a box + // on-screen + if (_rectFlicker) + drawRect(_rectX1, _rectY1, _rectX2, _rectY2, 184); + } +} + +void Debugger::plotCrossHair(int16 x, int16 y, uint8 pen) { + _vm->_screen->plotPoint(x, y, pen); + + _vm->_screen->drawLine(x - 2, y, x - 5, y, pen); + _vm->_screen->drawLine(x + 2, y, x + 5, y, pen); + + _vm->_screen->drawLine(x, y - 2, x, y - 5, pen); + _vm->_screen->drawLine(x, y + 2, x, y + 5, pen); +} + +void Debugger::drawRect(int16 x1, int16 y1, int16 x2, int16 y2, uint8 pen) { + _vm->_screen->drawLine(x1, y1, x2, y1, pen); // top edge + _vm->_screen->drawLine(x1, y2, x2, y2, pen); // bottom edge + _vm->_screen->drawLine(x1, y1, x1, y2, pen); // left edge + _vm->_screen->drawLine(x2, y1, x2, y2, pen); // right edge +} + +} // End of namespace Sword2 diff --git a/engines/sword2/debug.h b/engines/sword2/debug.h new file mode 100644 index 0000000000..9b0b40f9ec --- /dev/null +++ b/engines/sword2/debug.h @@ -0,0 +1,33 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef D_DEBUG +#define D_DEBUG + +// FIXME: I don't know how large this constant used to be +#define MAX_DEBUG_TEXTS 55 + +#define MAX_SHOWVARS 15 + +namespace Sword2 { +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/defs.h b/engines/sword2/defs.h new file mode 100644 index 0000000000..4acb484f34 --- /dev/null +++ b/engines/sword2/defs.h @@ -0,0 +1,205 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef DEFS +#define DEFS + +#define SIZE 0x10000 // 65536 items per section +#define NuSIZE 0xffff // & with this + +// Return codes + +enum { + // Generic error codes + RD_OK = 0x00000000, + RDERR_UNKNOWN = 0x00000001, + RDERR_OUTOFMEMORY = 0x00000003, + RDERR_INVALIDFILENAME = 0x00000004, + + // Drawing error codes + RDERR_DECOMPRESSION = 0x00010007, + + // Sprite drawing error codes + RDERR_NOTIMPLEMENTED = 0x00060001, + RDERR_NOTCLOSED = 0x00050005, + RDERR_NOTOPEN = 0x00050006, + + // Menubar error codes + RDERR_INVALIDMENU = 0x00060000, + RDERR_INVALIDPOCKET = 0x00060001, + RDERR_INVALIDCOMMAND = 0x00060002, + + // Palette fading error codes + RDERR_FADEINCOMPLETE = 0x00070000, + + // Sound engine error codes + RDERR_SPEECHPLAYING = 0x00080004, + RDERR_SPEECHNOTPLAYING = 0x00080005, + RDERR_INVALIDWAV = 0x00080006, + RDERR_FXALREADYOPEN = 0x00080009, + RDERR_FXNOTOPEN = 0x0008000B, + RDERR_INVALIDID = 0x0008000D +}; + +// Text ids for the control panel etc. + +enum { + TEXT_OK = 0x08EB0000, + TEXT_CANCEL = 0x08EB0001, + TEXT_RESTORE = 0x08EB0002, + TEXT_SAVE = 0x08EB0003, + TEXT_QUIT = 0x08EB0004, + TEXT_RESTART = 0x08EB0005, + TEXT_OPTIONS = 0x08EB000A, + TEXT_SUBTITLES = 0x08EB000B, + TEXT_OBJECT_LABELS = 0x08EB000C, + TEXT_MUSIC_VOLUME = 0x08EB000E, + TEXT_SPEECH_VOLUME = 0x08EB000F, + TEXT_FX_VOLUME = 0x08EB0010, + TEXT_GFX_QUALITY = 0x08EB0011, + TEXT_REVERSE_STEREO = 0x08EB0015, + TEXT_RESTORE_CANT_OPEN = 0x0CBA017E, + TEXT_RESTORE_INCOMPATIBLE = 0x0CBA017F, + TEXT_RESTORE_FAILED = 0x0CBA0181, + TEXT_SAVE_CANT_OPEN = 0x0CBA0182, + TEXT_SAVE_FAILED = 0x0CBA0184 +}; + +// Always 8 (George object used for Nico player character as well) +#define CUR_PLAYER_ID 8 + +// Global variable references + +enum { + ID = 0, + RESULT = 1, + PLAYER_ACTION = 2, + // CUR_PLAYER_ID = 3, + PLAYER_ID = 305, + TALK_FLAG = 13, + + MOUSE_X = 4, + MOUSE_Y = 5, + LEFT_BUTTON = 109, + RIGHT_BUTTON = 110, + CLICKED_ID = 178, + + IN_SUBJECT = 6, + COMBINE_BASE = 7, + OBJECT_HELD = 14, + + SPEECH_ID = 9, + INS1 = 10, + INS2 = 11, + INS3 = 12, + INS4 = 60, + INS5 = 61, + INS_COMMAND = 59, + + PLAYER_FEET_X = 141, + PLAYER_FEET_Y = 142, + PLAYER_CUR_DIR = 937, + + // for debug.cpp + LOCATION = 62, + + // so scripts can force scroll offsets + SCROLL_X = 345, + SCROLL_Y = 346, + + EXIT_CLICK_ID = 710, + EXIT_FADING = 713, + + SYSTEM_TESTING_ANIMS = 912, + SYSTEM_TESTING_TEXT = 1230, + SYSTEM_WANT_PREVIOUS_LINE = 1245, + + // 1=on 0=off (set in fnAddHuman and fnNoHuman) + MOUSE_AVAILABLE = 686, + + // used in fnChoose + AUTO_SELECTED = 1115, + + // see fnStartConversation and fnChooser + CHOOSER_COUNT_FLAG = 15, + + // signifies a demo mode + DEMO = 1153, + + // Indicates to script whether this is the Playstation version. + // PSXFLAG = 1173, + + // for the poor PSX so it knows what language is running. + // GAME_LANGUAGE = 111, + + // 1 = dead + DEAD = 1256, + + // If set indicates that the speech anim is to run through only once. + SPEECHANIMFLAG = 1278, + + // for the engine + SCROLL_OFFSET_X = 1314 +}; + +// Resource IDs + +enum { + // mouse mointers - It's pretty much safe to do it like this + NORMAL_MOUSE_ID = 17, + SCROLL_LEFT_MOUSE_ID = 1440, + SCROLL_RIGHT_MOUSE_ID = 1441, + + // Console Font - does not use game text - only English required + CONSOLE_FONT_ID = 340, + + // Speech Font + ENGLISH_SPEECH_FONT_ID = 341, + FINNISH_SPEECH_FONT_ID = 956, + POLISH_SPEECH_FONT_ID = 955, + + // Control Panel Font (and un-selected savegame descriptions) + ENGLISH_CONTROLS_FONT_ID = 2005, + FINNISH_CONTROLS_FONT_ID = 959, + POLISH_CONTROLS_FONT_ID = 3686, + + // Red Font (for selected savegame descriptions) + // BS2 doesn't draw selected savegames in red, so I guess this is a + // left-over from BS1 + ENGLISH_RED_FONT_ID = 2005, // 1998 // Redfont + FINNISH_RED_FONT_ID = 959, // 960 // FinRedFn + POLISH_RED_FONT_ID = 3686, // 3688 // PolRedFn + + // Control panel palette resource id + CONTROL_PANEL_PALETTE = 261, + + // res id's of the system menu icons + OPTIONS_ICON = 344, + QUIT_ICON = 335, + SAVE_ICON = 366, + RESTORE_ICON = 364, + RESTART_ICON = 342, + + // conversation exit icon, 'EXIT' menu icon (used in fnChoose) + EXIT_ICON = 65 +}; + +#endif diff --git a/engines/sword2/events.cpp b/engines/sword2/events.cpp new file mode 100644 index 0000000000..e8090414aa --- /dev/null +++ b/engines/sword2/events.cpp @@ -0,0 +1,99 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" + +namespace Sword2 { + +void Logic::sendEvent(uint32 id, uint32 interact_id) { + for (int i = 0; i < ARRAYSIZE(_eventList); i++) { + if (_eventList[i].id == id || !_eventList[i].id) { + _eventList[i].id = id; + _eventList[i].interact_id = interact_id; + return; + } + } + + error("sendEvent() ran out of event slots"); +} + +void Logic::setPlayerActionEvent(uint32 id, uint32 interact_id) { + // Full script id of action script number 2 + sendEvent(id, (interact_id << 16) | 2); +} + +int Logic::checkEventWaiting() { + for (int i = 0; i < ARRAYSIZE(_eventList); i++) { + if (_eventList[i].id == readVar(ID)) + return 1; + } + + return 0; +} + +void Logic::startEvent() { + // call this from stuff like fnWalk + // you must follow with a return IR_TERMINATE + + for (int i = 0; i < ARRAYSIZE(_eventList); i++) { + if (_eventList[i].id == readVar(ID)) { + logicOne(_eventList[i].interact_id); + _eventList[i].id = 0; + return; + } + } + + error("startEvent() can't find event for id %d", readVar(ID)); +} + +void Logic::clearEvent(uint32 id) { + for (int i = 0; i < ARRAYSIZE(_eventList); i++) { + if (_eventList[i].id == id) { + _eventList[i].id = 0; + return; + } + } +} + +void Logic::killAllIdsEvents(uint32 id) { + for (int i = 0; i < ARRAYSIZE(_eventList); i++) { + if (_eventList[i].id == id) + _eventList[i].id = 0; + } +} + +// For the debugger + +uint32 Logic::countEvents() { + uint32 count = 0; + + for (int i = 0; i < ARRAYSIZE(_eventList); i++) { + if (_eventList[i].id) + count++; + } + + return count; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/function.cpp b/engines/sword2/function.cpp new file mode 100644 index 0000000000..e749e7b497 --- /dev/null +++ b/engines/sword2/function.cpp @@ -0,0 +1,2479 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/system.h" +#include "common/file.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/build_display.h" +#include "sword2/console.h" +#include "sword2/interpreter.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/router.h" +#include "sword2/sound.h" +#include "sword2/animation.h" + +namespace Sword2 { + +int32 Logic::fnTestFunction(int32 *params) { + // params: 0 address of a flag + return IR_CONT; +} + +int32 Logic::fnTestFlags(int32 *params) { + // params: 0 value of flag + return IR_CONT; +} + +int32 Logic::fnRegisterStartPoint(int32 *params) { + // params: 0 id of startup script to call - key + // 1 pointer to ascii message + + int32 key = params[0]; + char *name = (char *)decodePtr(params[1]); + + _vm->registerStartPoint(key, name); + return IR_CONT; +} + +int32 Logic::fnInitBackground(int32 *params) { + // this screen defines the size of the back buffer + + // params: 0 res id of normal background layer - cannot be 0 + // 1 1 yes 0 no for a new palette + + _vm->_screen->initBackground(params[0], params[1]); + return IR_CONT; +} + +/** + * This function is used by start scripts. + */ + +int32 Logic::fnSetSession(int32 *params) { + // params: 0 id of new run list + + expressChangeSession(params[0]); + return IR_CONT; +} + +int32 Logic::fnBackSprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), BACK_SPRITE); + return IR_CONT; +} + +int32 Logic::fnSortSprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), SORT_SPRITE); + return IR_CONT; +} + +int32 Logic::fnForeSprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), FORE_SPRITE); + return IR_CONT; +} + +int32 Logic::fnRegisterMouse(int32 *params) { + // this call would be made from an objects service script 0 + // the object would be one with no graphic but with a mouse - i.e. a + // floor or one whose mouse area is manually defined rather than + // intended to fit sprite shape + + // params: 0 pointer to ObjectMouse or 0 for no write to mouse + // list + + _vm->_mouse->registerMouse(decodePtr(params[0]), NULL); + return IR_CONT; +} + +int32 Logic::fnAnim(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 resource id of animation file + + // Normal forward animation + return _router->doAnimate( + decodePtr(params[0]), + decodePtr(params[1]), + params[2], false); +} + +int32 Logic::fnRandom(int32 *params) { + // params: 0 min + // 1 max + + writeVar(RESULT, _vm->_rnd.getRandomNumberRng(params[0], params[1])); + return IR_CONT; +} + +int32 Logic::fnPreLoad(int32 *params) { + // Forces a resource into memory before it's "officially" opened for + // use. eg. if an anim needs to run on smoothly from another, + // "preloading" gets it into memory in advance to avoid the cacheing + // delay that normally occurs before the first frame. + + // params: 0 resource to preload + + _vm->_resman->openResource(params[0]); + _vm->_resman->closeResource(params[0]); + return IR_CONT; +} + +int32 Logic::fnAddSubject(int32 *params) { + // params: 0 id + // 1 daves reference number + _vm->_mouse->addSubject(params[0], params[1]); + return IR_CONT; +} + +int32 Logic::fnInteract(int32 *params) { + // Run targets action on a subroutine. Called by player on his base + // level 0 idle, for example. + + // params: 0 id of target from which we derive action script + // reference + + writeVar(PLAYER_ACTION, 0); // must clear this + logicUp((params[0] << 16) | 2); // 3rd script of clicked on id + + // Out, up and around again - pc is saved for current level to be + // returned to. + return IR_GOSUB; +} + +int32 Logic::fnChoose(int32 *params) { + // params: none + + // This opcode is used to open the conversation menu. The human is + // switched off so there will be no normal mouse engine. + + // The player's choice is piggy-backed on the standard opcode return + // values, to be used with the CP_JUMP_ON_RETURNED opcode. As far as I + // can tell, this is the only function that uses that feature. + + uint32 response = _vm->_mouse->chooseMouse(); + + if (response == (uint32)-1) + return IR_REPEAT; + + return IR_CONT | (response << 3); +} + +/** + * Walk mega to (x,y,dir). Set RESULT to 0 if it succeeded. Otherwise, set + * RESULT to 1. + */ + +int32 Logic::fnWalk(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to object's walkdata structure + // 4 target x-coord + // 5 target y-coord + // 6 target direction (8 means end walk on ANY direction) + + return _router->doWalk( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + params[4], params[5], params[6]); +} + +/** + * Walk mega to start position of anim + */ + +int32 Logic::fnWalkToAnim(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to object's walkdata structure + // 4 anim resource id + + return _router->walkToAnim( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + params[4]); +} + +/** + * Turn mega to the specified direction. + */ + +int32 Logic::fnTurn(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to object's walkdata structure + // 4 target direction + + return _router->doFace( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + params[4]); +} + +/** + * Stand mega at (x,y,dir) + * Sets up the graphic object, but also needs to set the new 'current_dir' in + * the mega object, so the router knows in future + */ + +int32 Logic::fnStandAt(int32 *params) { + // params: 0 pointer to object's graphic structure + // 1 pointer to object's mega structure + // 2 target x-coord + // 3 target y-coord + // 4 target direction + + _router->standAt( + decodePtr(params[0]), + decodePtr(params[1]), + params[2], params[3], params[4]); + return IR_CONT; +} + +/** + * Stand mega into the specified direction at current feet coords. + * Just needs to call standAt() with current feet coords. + */ + +int32 Logic::fnStand(int32 *params) { + // params: 0 pointer to object's graphic structure + // 1 pointer to object's mega structure + // 2 target direction + byte *ob_mega = decodePtr(params[1]); + + ObjectMega obMega(ob_mega); + + _router->standAt( + decodePtr(params[0]), + ob_mega, obMega.getFeetX(), obMega.getFeetY(), params[2]); + return IR_CONT; +} + +/** + * stand mega at end position of anim + */ + +int32 Logic::fnStandAfterAnim(int32 *params) { + // params: 0 pointer to object's graphic structure + // 1 pointer to object's mega structure + // 2 anim resource id + + _router->standAfterAnim( + decodePtr(params[0]), + decodePtr(params[1]), + params[2]); + return IR_CONT; +} + +int32 Logic::fnPause(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 number of game-cycles to pause + + // NB. Pause-value of 0 causes script to continue, 1 causes a 1-cycle + // quit, 2 gives 2 cycles, etc. + + ObjectLogic obLogic(decodePtr(params[0])); + + if (obLogic.getLooping() == 0) { + obLogic.setLooping(1); + obLogic.setPause(params[1]); + } + + if (obLogic.getPause()) { + obLogic.setPause(obLogic.getPause() - 1); + return IR_REPEAT; + } + + obLogic.setLooping(0); + return IR_CONT; +} + +int32 Logic::fnMegaTableAnim(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to animation table + + // Normal forward anim + return _router->megaTableAnimate( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + false); +} + +int32 Logic::fnAddMenuObject(int32 *params) { + // params: 0 pointer to a MenuObject structure to copy down + + _vm->_mouse->addMenuObject(decodePtr(params[0])); + return IR_CONT; +} + +/** + * Start a conversation. + * + * Note that fnStartConversation() might accidentally be called every time the + * script loops back for another chooser, but we only want to reset the chooser + * count flag the first time this function is called, i.e. when the talk flag + * is zero. + */ + +int32 Logic::fnStartConversation(int32 *params) { + // params: none + + _vm->_mouse->startConversation(); + return IR_CONT; +} + +/** + * End a conversation. + */ + +int32 Logic::fnEndConversation(int32 *params) { + // params: none + + _vm->_mouse->endConversation(); + return IR_CONT; +} + +int32 Logic::fnSetFrame(int32 *params) { + // params: 0 pointer to object's graphic structure + // 1 resource id of animation file + // 2 frame flag (0=first 1=last) + + int32 res = params[1]; + assert(res); + + // open the resource (& check it's valid) + byte *anim_file = _vm->_resman->openResource(res); + + assert(_vm->_resman->fetchType(res) == ANIMATION_FILE); + + // set up pointer to the animation header + AnimHeader anim_head; + + anim_head.read(_vm->fetchAnimHeader(anim_file)); + + // set up anim resource in graphic object + ObjectGraphic obGraph(decodePtr(params[0])); + + obGraph.setAnimResource(res); + obGraph.setAnimPc(params[2] ? anim_head.noAnimFrames - 1 : 0); + + // Close the anim file and drop out of script + _vm->_resman->closeResource(obGraph.getAnimResource()); + return IR_CONT; +} + +int32 Logic::fnRandomPause(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 minimum number of game-cycles to pause + // 2 maximum number of game-cycles to pause + + ObjectLogic obLogic(decodePtr(params[0])); + int32 pars[2]; + + if (obLogic.getLooping() == 0) { + pars[0] = params[1]; + pars[1] = params[2]; + fnRandom(pars); + pars[1] = readVar(RESULT); + } + + pars[0] = params[0]; + return fnPause(pars); +} + +int32 Logic::fnRegisterFrame(int32 *params) { + // this call would be made from an objects service script 0 + + // params: 0 pointer to mouse structure or NULL for no write to + // mouse list (non-zero means write sprite-shape to + // mouse list) + // 1 pointer to graphic structure + // 2 pointer to mega structure or NULL if not a mega + + _vm->_screen->registerFrame( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2])); + return IR_CONT; +} + +int32 Logic::fnNoSprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), NO_SPRITE); + return IR_CONT; +} + +int32 Logic::fnSendSync(int32 *params) { + // params: 0 sync's recipient + // 1 sync value + + sendSync(params[0], params[1]); + return IR_CONT; +} + +int32 Logic::fnUpdatePlayerStats(int32 *params) { + // engine needs to know certain info about the player + + // params: 0 pointer to mega structure + + ObjectMega obMega(decodePtr(params[0])); + + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + screenInfo->player_feet_x = obMega.getFeetX(); + screenInfo->player_feet_y = obMega.getFeetY(); + + // for the script + writeVar(PLAYER_FEET_X, obMega.getFeetX()); + writeVar(PLAYER_FEET_Y, obMega.getFeetY()); + writeVar(PLAYER_CUR_DIR, obMega.getCurDir()); + writeVar(SCROLL_OFFSET_X, screenInfo->scroll_offset_x); + + debug(5, "fnUpdatePlayerStats: %d %d", + obMega.getFeetX(), obMega.getFeetY()); + + return IR_CONT; +} + +int32 Logic::fnPassGraph(int32 *params) { + // makes an engine local copy of passed ObjectGraphic - run script 4 + // of an object to request this used by fnTurnTo(id) etc + // + // remember, we cannot simply read a compact any longer but instead + // must request it from the object itself + + // params: 0 pointer to an ObjectGraphic structure + + warning("fnPassGraph() is a no-op now"); + return IR_CONT; +} + +int32 Logic::fnInitFloorMouse(int32 *params) { + // params: 0 pointer to object's mouse structure + + byte *ob_mouse = decodePtr(params[0]); + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + // floor is always lowest priority + + ObjectMouse mouse; + + mouse.x1 = 0; + mouse.y1 = 0; + mouse.x2 = screenInfo->screen_wide - 1; + mouse.y2 = screenInfo->screen_deep - 1; + mouse.priority = 9; + mouse.pointer = NORMAL_MOUSE_ID; + + mouse.write(ob_mouse); + return IR_CONT; +} + +int32 Logic::fnPassMega(int32 *params) { + // makes an engine local copy of passed mega_structure - run script 4 + // of an object to request this used by fnTurnTo(id) etc + // + // remember, we cannot simply read a compact any longer but instead + // must request it from the object itself + + // params: 0 pointer to a mega structure + + memcpy(_engineMega, decodePtr(params[0]), ObjectMega::size()); + return IR_CONT; +} + +/** + * Turn mega to face point (x,y) on the floor + */ + +int32 Logic::fnFaceXY(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to object's walkdata structure + // 4 target x-coord + // 5 target y-coord + + return _router->faceXY( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + params[4], params[5]); +} + +/** + * Causes no more objects in this logic loop to be processed. The logic engine + * will restart at the beginning of the new list. The current screen will not + * be drawn! + */ + +int32 Logic::fnEndSession(int32 *params) { + // params: 0 id of new run-list + + // terminate current and change to next run-list + expressChangeSession(params[0]); + + // stop the script - logic engine will now go around and the new + // screen will begin + return IR_STOP; +} + +int32 Logic::fnNoHuman(int32 *params) { + // params: none + + _vm->_mouse->noHuman(); + return IR_CONT; +} + +int32 Logic::fnAddHuman(int32 *params) { + // params: none + + _vm->_mouse->addHuman(); + return IR_CONT; +} + +/** + * Wait for a target to become waiting, i.e. not busy. + */ + +int32 Logic::fnWeWait(int32 *params) { + // params: 0 target + + assert(_vm->_resman->fetchType(params[0]) == GAME_OBJECT); + + // Run the target's get-speech-state script + runResScript(params[0], 5); + + if (readVar(RESULT) == 0) { + // The target is busy. Try again. + _vm->_debugger->_speechScriptWaiting = params[0]; + return IR_REPEAT; + } + + // The target is waiting, i.e. not busy. + + _vm->_debugger->_speechScriptWaiting = 0; + return IR_CONT; +} + +/** + * Wait for a target to become waiting, i.e. not busy, send a command to it, + * then wait for it to finish. + */ + +int32 Logic::fnTheyDoWeWait(int32 *params) { + // params: 0 pointer to ob_logic + // 1 target + // 2 command + // 3 ins1 + // 4 ins2 + // 5 ins3 + // 6 ins4 + // 7 ins5 + + assert(_vm->_resman->fetchType(params[1]) == GAME_OBJECT); + + // Run the target's get-speech-state script + runResScript(params[1], 5); + + ObjectLogic obLogic(decodePtr(params[0])); + + if (readVar(RESULT) == 1 && readVar(INS_COMMAND) == 0 && obLogic.getLooping() == 0) { + // The target is waiting, i.e. not busy, and there is no other + // command queued. We haven't sent the command yet, so do it. + + debug(5, "fnTheyDoWeWait: sending command to %d", params[1]); + + _vm->_debugger->_speechScriptWaiting = params[1]; + obLogic.setLooping(1); + + writeVar(SPEECH_ID, params[1]); + writeVar(INS_COMMAND, params[2]); + writeVar(INS1, params[3]); + writeVar(INS2, params[4]); + writeVar(INS3, params[5]); + writeVar(INS4, params[6]); + writeVar(INS5, params[7]); + + return IR_REPEAT; + } + + if (obLogic.getLooping() == 0) { + // The command has not been sent yet. Keep waiting. + _vm->_debugger->_speechScriptWaiting = params[1]; + return IR_REPEAT; + } + + if (readVar(RESULT) == 0) { + // The command has been sent, and the target is busy doing it. + // Wait for it to finish. + + debug(5, "fnTheyDoWeWait: Waiting for %d to finish", params[1]); + + _vm->_debugger->_speechScriptWaiting = params[1]; + return IR_REPEAT; + } + + debug(5, "fnTheyDoWeWait: %d finished", params[1]); + + obLogic.setLooping(0); + _vm->_debugger->_speechScriptWaiting = 0; + return IR_CONT; +} + +/** + * Wait for a target to become waiting, i.e. not busy, then send a command to + * it. + */ + +int32 Logic::fnTheyDo(int32 *params) { + // params: 0 target + // 1 command + // 2 ins1 + // 3 ins2 + // 4 ins3 + // 5 ins4 + // 6 ins5 + + assert(_vm->_resman->fetchType(params[0]) == GAME_OBJECT); + + // Run the target's get-speech-state script + runResScript(params[0], 5); + + if (readVar(RESULT) == 1 && !readVar(INS_COMMAND)) { + // The target is waiting, i.e. not busy, and there is no other + // command queued. Send the command. + + debug(5, "fnTheyDo: sending command to %d", params[0]); + + _vm->_debugger->_speechScriptWaiting = 0; + + writeVar(SPEECH_ID, params[0]); + writeVar(INS_COMMAND, params[1]); + writeVar(INS1, params[2]); + writeVar(INS2, params[3]); + writeVar(INS3, params[4]); + writeVar(INS4, params[5]); + writeVar(INS5, params[6]); + + return IR_CONT; + } + + // The target is busy. Come back again next cycle. + + _vm->_debugger->_speechScriptWaiting = params[0]; + return IR_REPEAT; +} + +/** + * Route to the left or right hand side of target id, if possible. + */ + +int32 Logic::fnWalkToTalkToMega(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to object's walkdata structure + // 4 id of target mega to face + // 5 separation + + return _router->walkToTalkToMega( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + params[4], params[5]); +} + +int32 Logic::fnFadeDown(int32 *params) { + // NONE means up! can only be called when screen is fully faded up - + // multiple calls wont have strange effects + + // params: none + + if (_vm->_screen->getFadeStatus() == RDFADE_NONE) + _vm->_screen->fadeDown(); + + return IR_CONT; +} + +enum { + S_OB_GRAPHIC = 0, + S_OB_SPEECH = 1, + S_OB_LOGIC = 2, + S_OB_MEGA = 3, + + S_TEXT = 4, + S_WAV = 5, + S_ANIM = 6, + S_DIR_TABLE = 7, + S_ANIM_MODE = 8 +}; + +/** + * It's the super versatile fnSpeak. Text and wavs can be selected in any + * combination. + * + * @note We can assume no human - there should be no human, at least! + */ + +int32 Logic::fnISpeak(int32 *params) { + // params: 0 pointer to ob_graphic + // 1 pointer to ob_speech + // 2 pointer to ob_logic + // 3 pointer to ob_mega + // 4 encoded text number + // 5 wav res id + // 6 anim res id + // 7 anim table res id + // 8 animation mode 0 lip synced, + // 1 just straight animation + + static bool cycle_skip = false; + static bool speechRunning; + + // Set up the pointers which we know we'll always need + + ObjectLogic obLogic(decodePtr(params[S_OB_LOGIC])); + ObjectGraphic obGraph(decodePtr(params[S_OB_GRAPHIC])); + + // FIRST TIME ONLY: create the text, load the wav, set up the anim, + // etc. + + if (obLogic.getLooping() == 0) { + // New fudge to wait for smacker samples to finish + // since they can over-run into the game + + if (_vm->_sound->getSpeechStatus() != RDSE_SAMPLEFINISHED) + return IR_REPEAT; + + // New fudge for 'fx' subtitles: If subtitles switched off, and + // we don't want to use a wav for this line either, then just + // quit back to script right now! + + if (!_vm->getSubtitles() && !wantSpeechForLine(params[S_WAV])) + return IR_CONT; + + // Drop out for 1st cycle to allow walks/anims to end and + // display last frame before system locks while speech loaded + + if (!cycle_skip) { + cycle_skip = true; + return IR_REPEAT; + } + + cycle_skip = false; + + _vm->_debugger->_textNumber = params[S_TEXT]; + + // Pull out the text line to get the official text number + // (for wav id). Once the wav id's go into all script text + // commands, we'll only need this for debugging. + + uint32 text_res = params[S_TEXT] / SIZE; + uint32 local_text = params[S_TEXT] & 0xffff; + + // For testing all text & speech! + // + // A script loop can send any text number to fnISpeak and it + // will only run the valid ones or return with 'result' equal + // to '1' or '2' to mean 'invalid text resource' and 'text + // number out of range' respectively + // + // See 'testing_routines' object in George's Player Character + // section of linc + + if (readVar(SYSTEM_TESTING_TEXT)) { + if (!_vm->_resman->checkValid(text_res)) { + // Not a valid resource number - invalid (null + // resource) + writeVar(RESULT, 1); + return IR_CONT; + } + + if (_vm->_resman->fetchType(text_res) != TEXT_FILE) { + // Invalid - not a text resource + _vm->_resman->closeResource(text_res); + writeVar(RESULT, 1); + return IR_CONT; + } + + if (!_vm->checkTextLine(_vm->_resman->openResource(text_res), local_text)) { + // Line number out of range + _vm->_resman->closeResource(text_res); + writeVar(RESULT, 2); + return IR_CONT; + } + + _vm->_resman->closeResource(text_res); + writeVar(RESULT, 0); + } + + byte *text = _vm->fetchTextLine(_vm->_resman->openResource(text_res), local_text); + _officialTextNumber = READ_LE_UINT16(text); + _vm->_resman->closeResource(text_res); + + // Prevent dud lines from appearing while testing text & speech + // since these will not occur in the game anyway + + if (readVar(SYSTEM_TESTING_TEXT)) { + // If actor number is 0 and text line is just a 'dash' + // character + if (_officialTextNumber == 0 && text[2] == '-' && text[3] == 0) { + writeVar(RESULT, 3); + return IR_CONT; + } + } + + // Set the 'looping_flag' and the text-click-delays. We can + // left-click past the text after half a second, and + // right-click past it after a quarter of a second. + + obLogic.setLooping(1); + _leftClickDelay = 6; + _rightClickDelay = 3; + + if (readVar(PLAYER_ID) != CUR_PLAYER_ID) + debug(5, "(%d) Nico: %s", _officialTextNumber, text + 2); + else { + byte buf[NAME_LEN]; + + debug(5, "(%d) %s: %s", _officialTextNumber, _vm->_resman->fetchName(readVar(ID), buf), text + 2); + } + + // Set up the speech animation + + if (params[S_ANIM]) { + // Just a straight anim. + _animId = params[6]; + } else if (params[S_DIR_TABLE]) { + // Use this direction table to derive the anim + // NB. ASSUMES WE HAVE A MEGA OBJECT!! + + ObjectMega obMega(decodePtr(params[S_OB_MEGA])); + byte *anim_table = decodePtr(params[S_DIR_TABLE]); + + _animId = READ_LE_UINT32(anim_table + 4 * obMega.getCurDir()); + } else { + // No animation choosen + _animId = 0; + } + + if (_animId) { + // Set the talker's graphic to the first frame of this + // speech anim for now. + + _speechAnimType = readVar(SPEECHANIMFLAG); + obGraph.setAnimResource(_animId); + obGraph.setAnimPc(0); + } + + // Default back to looped lip synced anims. + writeVar(SPEECHANIMFLAG, 0); + + // Set up _textX and _textY for speech panning and/or text + // sprite position. + + locateTalker(params); + + // Is it to be speech or subtitles or both? + + // Assume not running until know otherwise + speechRunning = false; + + // New fudge for 'fx' subtitles: If speech is selected, and + // this line is allowed speech (not if it's an fx subtitle!) + + if (!_vm->_sound->isSpeechMute() && wantSpeechForLine(_officialTextNumber)) { + // If the wavId parameter is zero because not yet + // compiled into speech command, we can still get it + // from the 1st 2 chars of the text line. + + if (!params[S_WAV]) + params[S_WAV] = (int32)_officialTextNumber; + + // Panning goes from -16 (left) to 16 (right) + int8 speech_pan = ((_textX - 320) * 16) / 320; + + if (speech_pan < -16) + speech_pan = -16; + else if (speech_pan > 16) + speech_pan = 16; + + uint32 rv = _vm->_sound->playCompSpeech(params[S_WAV], 16, speech_pan); + + if (rv == RD_OK) { + // Ok, we've got something to play. Set it + // playing now. (We might want to do this the + // next cycle, don't know yet.) + + speechRunning = true; + _vm->_sound->unpauseSpeech(); + } else { + debug(5, "ERROR: PlayCompSpeech(wav=%d (res=%d pos=%d)) returned %.8x", params[S_WAV], text_res, local_text, rv); + } + } + + if (_vm->getSubtitles() || !speechRunning) { + // We want subtitles, or the speech failed to load. + // Either way, we're going to show the text so create + // the text sprite. + + formText(params); + } + } + + // EVERY TIME: run a cycle of animation, if there is one + + if (_animId) { + // There is an animation - Increment the anim frame number. + obGraph.setAnimPc(obGraph.getAnimPc() + 1); + + byte *anim_file = _vm->_resman->openResource(obGraph.getAnimResource()); + AnimHeader anim_head; + + anim_head.read(_vm->fetchAnimHeader(anim_file)); + + if (!_speechAnimType) { + // ANIM IS TO BE LIP-SYNC'ED & REPEATING + + if (obGraph.getAnimPc() == (int32)anim_head.noAnimFrames) { + // End of animation - restart from frame 0 + obGraph.setAnimPc(0); + } else if (speechRunning && _vm->_sound->amISpeaking() == RDSE_QUIET) { + // The speech is running, but we're at a quiet + // bit. Restart from frame 0 (closed mouth). + obGraph.setAnimPc(0); + } + } else { + // ANIM IS TO PLAY ONCE ONLY + if (obGraph.getAnimPc() == (int32)anim_head.noAnimFrames - 1) { + // Reached the last frame of the anim. Hold + // anim on this last frame + _animId = 0; + } + } + + _vm->_resman->closeResource(obGraph.getAnimResource()); + } else if (_speechAnimType) { + // Placed here so we actually display the last frame of the + // anim. + _speechAnimType = 0; + } + + // EVERY TIME: FIND OUT IF WE NEED TO STOP THE SPEECH NOW... + + // If there is a wav then we're using that to end the speech naturally + + bool speechFinished = false; + + // If playing a sample + + if (speechRunning) { + // Has it finished? + if (_vm->_sound->getSpeechStatus() == RDSE_SAMPLEFINISHED) + speechFinished = true; + } else if (!speechRunning && _speechTime) { + // Counting down text time because there is no sample - this + // ends the speech + + // if no sample then we're using _speechTime to end speech + // naturally + + _speechTime--; + if (!_speechTime) + speechFinished = true; + } + + // Ok, all is running along smoothly - but a click means stop + // unnaturally + + int mouseX, mouseY; + + _vm->_mouse->getPos(mouseX, mouseY); + + // So that we can go to the options panel while text & speech is + // being tested + if (readVar(SYSTEM_TESTING_TEXT) == 0 || mouseY > 0) { + MouseEvent *me = _vm->mouseEvent(); + + // Note that we now have TWO click-delays - one for LEFT + // button, one for RIGHT BUTTON + + if ((!_leftClickDelay && me && (me->buttons & RD_LEFTBUTTONDOWN)) || + (!_rightClickDelay && me && (me->buttons & RD_RIGHTBUTTONDOWN))) { + // Mouse click, after click_delay has expired -> end + // the speech. + + // if testing text & speech + if (readVar(SYSTEM_TESTING_TEXT)) { + // and RB used to click past text + if (me->buttons & RD_RIGHTBUTTONDOWN) { + // then we want the previous line again + writeVar(SYSTEM_WANT_PREVIOUS_LINE, 1); + } else { + // LB just want next line again + writeVar(SYSTEM_WANT_PREVIOUS_LINE, 0); + } + } + + speechFinished = true; + + // if speech sample playing, halt it prematurely + if (speechRunning) + _vm->_sound->stopSpeech(); + } + } + + // If we are finishing the speech this cycle, do the business + + // !speechAnimType, as we want an anim which is playing once to have + // finished. + + if (speechFinished && !_speechAnimType) { + // If there is text, kill it + if (_speechTextBlocNo) { + _vm->_fontRenderer->killTextBloc(_speechTextBlocNo); + _speechTextBlocNo = 0; + } + + // if there is a speech anim, end it on closed mouth frame + if (_animId) { + _animId = 0; + obGraph.setAnimPc(0); + } + + speechRunning = false; + + // no longer in a script function loop + obLogic.setLooping(0); + + _vm->_debugger->_textNumber = 0; + + // reset to zero, in case text line not even extracted (since + // this number comes from the text line) + _officialTextNumber = 0; + + writeVar(RESULT, 0); + return IR_CONT; + } + + // Speech still going, so decrement the click_delay if it's still + // active + + if (_leftClickDelay) + _leftClickDelay--; + + if (_rightClickDelay) + _rightClickDelay--; + + return IR_REPEAT; +} + +/** + * Reset the object and restart script 1 on level 0 + */ + +int32 Logic::fnTotalRestart(int32 *params) { + // mega runs this to restart its base logic again - like being cached + // in again + + // params: none + + _curObjectHub.setLogicLevel(0); + _curObjectHub.setScriptPc(0, 1); + + return IR_TERMINATE; +} + +int32 Logic::fnSetWalkGrid(int32 *params) { + // params: none + + warning("fnSetWalkGrid() is no longer a valid opcode"); + return IR_CONT; +} + +/** + * Receive and sequence the commands sent from the conversation script. We have + * to do this in a slightly tweeky manner as we can no longer have generic + * scripts. + */ + +enum { + INS_talk = 1, + INS_anim = 2, + INS_reverse_anim = 3, + INS_walk = 4, + INS_turn = 5, + INS_face = 6, + INS_trace = 7, + INS_no_sprite = 8, + INS_sort = 9, + INS_foreground = 10, + INS_background = 11, + INS_table_anim = 12, + INS_reverse_table_anim = 13, + INS_walk_to_anim = 14, + INS_set_frame = 15, + INS_stand_after_anim = 16, + INS_quit = 42 +}; + +int32 Logic::fnSpeechProcess(int32 *params) { + // params: 0 pointer to ob_graphic + // 1 pointer to ob_speech + // 2 pointer to ob_logic + // 3 pointer to ob_mega + // 4 pointer to ob_walkdata + + ObjectSpeech obSpeech(decodePtr(params[1])); + + while (1) { + int32 pars[9]; + + // Check which command we're waiting for, and call the + // appropriate function. Once we're done, clear the command + // and set wait_state to 1. + // + // Note: we could save a var and ditch wait_state and check + // 'command' for non zero means busy + // + // Note: I can't see that we ever check the value of wait_state + // but perhaps it accesses that memory location directly? + + switch (obSpeech.getCommand()) { + case 0: + break; + case INS_talk: + pars[0] = params[0]; // ob_graphic + pars[1] = params[1]; // ob_speech + pars[2] = params[2]; // ob_logic + pars[3] = params[3]; // ob_mega + pars[4] = obSpeech.getIns1(); // encoded text number + pars[5] = obSpeech.getIns2(); // wav res id + pars[6] = obSpeech.getIns3(); // anim res id + pars[7] = obSpeech.getIns4(); // anim table res id + pars[8] = obSpeech.getIns5(); // animation mode - 0 lip synced, 1 just straight animation + + if (fnISpeak(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_turn: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = params[3]; // ob_mega + pars[3] = params[4]; // ob_walkdata + pars[4] = obSpeech.getIns1(); // direction to turn to + + if (fnTurn(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_face: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = params[3]; // ob_mega + pars[3] = params[4]; // ob_walkdata + pars[4] = obSpeech.getIns1(); // target + + if (fnFaceMega(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_anim: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = obSpeech.getIns1(); // anim res + + if (fnAnim(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_reverse_anim: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = obSpeech.getIns1(); // anim res + + if (fnReverseAnim(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_table_anim: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = params[3]; // ob_mega + pars[3] = obSpeech.getIns1(); // pointer to anim table + + if (fnMegaTableAnim(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_reverse_table_anim: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = params[3]; // ob_mega + pars[3] = obSpeech.getIns1(); // pointer to anim table + + if (fnReverseMegaTableAnim(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_no_sprite: + fnNoSprite(params); // ob_graphic + + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + return IR_REPEAT ; + case INS_sort: + fnSortSprite(params); // ob_graphic + + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + return IR_REPEAT; + case INS_foreground: + fnForeSprite(params); // ob_graphic + + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + return IR_REPEAT; + case INS_background: + fnBackSprite(params); // ob_graphic + + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + return IR_REPEAT; + case INS_walk: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = params[3]; // ob_mega + pars[3] = params[4]; // ob_walkdata + pars[4] = obSpeech.getIns1(); // target x + pars[5] = obSpeech.getIns2(); // target y + pars[6] = obSpeech.getIns3(); // target direction + + if (fnWalk(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_walk_to_anim: + pars[0] = params[2]; // ob_logic + pars[1] = params[0]; // ob_graphic + pars[2] = params[3]; // ob_mega + pars[3] = params[4]; // ob_walkdata + pars[4] = obSpeech.getIns1(); // anim resource + + if (fnWalkToAnim(pars) != IR_REPEAT) { + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + } + + return IR_REPEAT; + case INS_stand_after_anim: + pars[0] = params[0]; // ob_graphic + pars[1] = params[3]; // ob_mega + pars[2] = obSpeech.getIns1(); // anim resource + + fnStandAfterAnim(pars); + + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + return IR_REPEAT; + case INS_set_frame: + pars[0] = params[0]; // ob_graphic + pars[1] = obSpeech.getIns1(); // anim_resource + pars[2] = obSpeech.getIns2(); // FIRST_FRAME or LAST_FRAME + fnSetFrame(pars); + + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + return IR_REPEAT; + case INS_quit: + // That's it - we're finished with this + obSpeech.setCommand(0); + // obSpeech.setWaitState(0); + return IR_CONT; + default: + // Unimplemented command - just cancel + obSpeech.setCommand(0); + obSpeech.setWaitState(1); + break; + } + + if (readVar(SPEECH_ID) == readVar(ID)) { + // There's a new command for us! Grab the command - + // potentially we only have this cycle to do this - and + // set things up so that the command will be picked up + // on the next iteration of the while loop. + + debug(5, "fnSpeechProcess: Received new command %d", readVar(INS_COMMAND)); + + writeVar(SPEECH_ID, 0); + + obSpeech.setCommand(readVar(INS_COMMAND)); + obSpeech.setIns1(readVar(INS1)); + obSpeech.setIns2(readVar(INS2)); + obSpeech.setIns3(readVar(INS3)); + obSpeech.setIns4(readVar(INS4)); + obSpeech.setIns5(readVar(INS5)); + obSpeech.setWaitState(0); + + writeVar(INS_COMMAND, 0); + } else { + // No new command. We could run a blink anim (or + // something) here. + + obSpeech.setWaitState(1); + return IR_REPEAT; + } + } +} + +int32 Logic::fnSetScaling(int32 *params) { + // params: 0 pointer to object's mega structure + // 1 scale constant A + // 2 scale constant B + + // 256 * s = A * y + B + + // Where s is system scale, which itself is (256 * actual_scale) ie. + // s == 128 is half size + + ObjectMega obMega(decodePtr(params[0])); + + obMega.setScaleA(params[1]); + obMega.setScaleB(params[2]); + + return IR_CONT; +} + +int32 Logic::fnStartEvent(int32 *params) { + // params: none + + startEvent(); + return IR_TERMINATE; +} + +int32 Logic::fnCheckEventWaiting(int32 *params) { + // params: none + + writeVar(RESULT, checkEventWaiting()); + return IR_CONT; +} + +int32 Logic::fnRequestSpeech(int32 *params) { + // change current script - must be followed by a TERMINATE script + // directive + + // params: 0 id of target to catch the event and startup speech + // servicing + + // Full script id to interact with - megas run their own 7th script + sendEvent(params[0], (params[0] << 16) | 6); + return IR_CONT; +} + +int32 Logic::fnGosub(int32 *params) { + // params: 0 id of script + + // Hurray, script subroutines. Logic goes up - pc is saved for current + // level. + logicUp(params[0]); + return IR_GOSUB; +} + +/** + * Wait for a target to become waiting, i.e. not busy, or until we time out. + * This is useful when clicking on a target to talk to it, and it doesn't + * reply. This way, we won't lock up. + * + * If the target becomes waiting, RESULT is set to 0. If we time out, RESULT is + * set to 1. + */ + +int32 Logic::fnTimedWait(int32 *params) { + // params: 0 ob_logic + // 1 target + // 2 number of cycles before give up + + assert(_vm->_resman->fetchType(params[1]) == GAME_OBJECT); + + ObjectLogic obLogic(decodePtr(params[0])); + + if (obLogic.getLooping() == 0) { + // This is the first time, so set up the time-out. + obLogic.setLooping(params[2]); + } + + // Run the target's get-speech-state script + runResScript(params[1], 5); + + if (readVar(RESULT) == 1) { + // The target is waiting, i.e. not busy + + _vm->_debugger->_speechScriptWaiting = 0; + + obLogic.setLooping(0); + writeVar(RESULT, 0); + return IR_CONT; + } + + obLogic.setLooping(obLogic.getLooping() - 1); + + if (obLogic.getLooping() == 0) { + // Time's up. + + debug(5, "fnTimedWait: Timed out waiting for %d", params[1]); + _vm->_debugger->_speechScriptWaiting = 0; + + // Clear the event that hasn't been picked up - in theory, + // none of this should ever happen. + + killAllIdsEvents(params[1]); + writeVar(RESULT, 1); + return IR_CONT; + } + + // Target is busy. Keep trying. + + _vm->_debugger->_speechScriptWaiting = params[1]; + return IR_REPEAT; +} + +int32 Logic::fnPlayFx(int32 *params) { + // params: 0 sample resource id + // 1 type (FX_SPOT, FX_RANDOM, FX_LOOP) + // 2 delay (0..65535) + // 3 volume (0..16) + // 4 pan (-16..16) + + // example script: + // fnPlayFx (FXWATER, FX_LOOP, 0, 10, 15); + // // fx_water is just a local script flag + // fx_water = result; + // . + // . + // . + // fnStopFx (fx_water); + + int32 res = params[0]; + int32 type = params[1]; + int32 delay = params[2]; + int32 volume = params[3]; + int32 pan = params[4]; + + _vm->_sound->queueFx(res, type, delay, volume, pan); + return IR_CONT; +} + +int32 Logic::fnStopFx(int32 *params) { + // params: 0 position in queue + if (_vm->_sound->stopFx(params[0]) != RD_OK) + debug(5, "SFX ERROR: Trying to stop an inactive sound slot"); + + return IR_CONT; +} + +/** + * Start a tune playing, to play once or to loop until stopped or next one + * played. + */ + +int32 Logic::fnPlayMusic(int32 *params) { + // params: 0 tune id + // 1 loop flag (0 or 1) + + char filename[128]; + bool loopFlag; + uint32 rv; + + loopFlag = (params[1] == FX_LOOP); + + rv = _vm->_sound->streamCompMusic(params[0], loopFlag); + + if (rv) + debug(5, "ERROR: streamCompMusic(%s, %d, %d) returned error 0x%.8x", filename, params[0], loopFlag, rv); + + return IR_CONT; +} + +int32 Logic::fnStopMusic(int32 *params) { + // params: none + + _vm->_sound->stopMusic(false); + return IR_CONT; +} + +int32 Logic::fnSetValue(int32 *params) { + // temp. function! + + // used for setting far-referenced megaset resource field in mega + // object, from start script + + // params: 0 pointer to object's mega structure + // 1 value to set it to + + ObjectMega obMega(decodePtr(params[0])); + + obMega.setMegasetRes(params[1]); + return IR_CONT; +} + +int32 Logic::fnNewScript(int32 *params) { + // change current script - must be followed by a TERMINATE script + // directive + + // params: 0 id of script + + writeVar(PLAYER_ACTION, 0); // must clear this + logicReplace(params[0]); + return IR_TERMINATE; +} + +/** + * Like getSync(), but called from scripts. Sets the RESULT variable to + * the sync value, or 0 if none is found. + */ + +int32 Logic::fnGetSync(int32 *params) { + // params: none + + int slot = getSync(); + + writeVar(RESULT, (slot != -1) ? _syncList[slot].sync : 0); + return IR_CONT; +} + +/** + * Wait for sync to happen. Sets the RESULT variable to the sync value, once + * it has been found. + */ + +int32 Logic::fnWaitSync(int32 *params) { + // params: none + + debug(6, "fnWaitSync: %d waits", readVar(ID)); + + int slot = getSync(); + + if (slot == -1) + return IR_REPEAT; + + debug(5, "fnWaitSync: %d got sync %d", readVar(ID), _syncList[slot].sync); + writeVar(RESULT, _syncList[slot].sync); + return IR_CONT; +} + +int32 Logic::fnRegisterWalkGrid(int32 *params) { + // params: none + + warning("fnRegisterWalkGrid() is no longer a valid opcode"); + return IR_CONT; +} + +int32 Logic::fnReverseMegaTableAnim(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to animation table + + // Reverse anim + return _router->megaTableAnimate( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + true); +} + +int32 Logic::fnReverseAnim(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 resource id of animation file + + // Reverse anim + return _router->doAnimate( + decodePtr(params[0]), + decodePtr(params[1]), + params[2], true); +} + +/** + * Mark this object for killing - to be killed when player leaves this screen. + * Object reloads and script restarts upon re-entry to screen, which causes + * this object's startup logic to be re-run every time we enter the screen. + * "Which is nice." + * + * @note Call ONCE from object's logic script, i.e. in startup code, so not + * re-called every time script frops off and restarts! + */ + +int32 Logic::fnAddToKillList(int32 *params) { + // params: none + uint32 id = readVar(ID); + + // DON'T EVER KILL GEORGE! + if (id == CUR_PLAYER_ID) + return IR_CONT; + + // Scan the list to see if it's already included + + for (uint32 i = 0; i < _kills; i++) { + if (_objectKillList[i] == id) + return IR_CONT; + } + + assert(_kills < OBJECT_KILL_LIST_SIZE); // no room at the inn + + _objectKillList[_kills++] = id; + + // "another one bites the dust" + + // When we leave the screen, all these object resources are to be + // cleaned out of memory and the kill list emptied by doing + // '_kills = 0', ensuring that all resources are in fact still in + // memory and, more importantly, closed before killing! + + return IR_CONT; +} + +/** + * Set the standby walk coords to be used by fnWalkToAnim() and + * fnStandAfterAnim() when the anim header's start/end coords are zero. + * Useful during development; can stay in final game anyway. + */ + +int32 Logic::fnSetStandbyCoords(int32 *params) { + // params: 0 x-coord + // 1 y-coord + // 2 direction (0..7) + + _router->setStandbyCoords(params[0], params[1], params[2]); + return IR_CONT; +} + +int32 Logic::fnBackPar0Sprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), BGP0_SPRITE); + return IR_CONT; +} + +int32 Logic::fnBackPar1Sprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), BGP1_SPRITE); + return IR_CONT; +} + +int32 Logic::fnForePar0Sprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), FGP0_SPRITE); + return IR_CONT; +} + +int32 Logic::fnForePar1Sprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteStatus(decodePtr(params[0]), FGP1_SPRITE); + return IR_CONT; +} + +int32 Logic::fnSetPlayerActionEvent(int32 *params) { + // we want to intercept the player character and have him interact + // with an object - from script this code is the same as the mouse + // engine calls when you click on an object - here, a third party + // does the clicking IYSWIM + + // note - this routine used CUR_PLAYER_ID as the target + + // params: 0 id to interact with + + setPlayerActionEvent(CUR_PLAYER_ID, params[0]); + return IR_CONT; +} + +/** + * Set the special scroll offset variables + * + * Call when starting screens and to change the camera within screens + * + * call AFTER fnInitBackground() to override the defaults + */ + +int32 Logic::fnSetScrollCoordinate(int32 *params) { + // params: 0 feet_x value + // 1 feet_y value + + // Called feet_x and feet_y to retain intellectual compatibility with + // Sword1! + // + // feet_x & feet_y refer to the physical screen coords where the + // system will try to maintain George's feet + + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + screenInfo->feet_x = params[0]; + screenInfo->feet_y = params[1]; + return IR_CONT; +} + +/** + * Stand mega at start position of anim + */ + +int32 Logic::fnStandAtAnim(int32 *params) { + // params: 0 pointer to object's graphic structure + // 1 pointer to object's mega structure + // 2 anim resource id + + _router->standAtAnim( + decodePtr(params[0]), + decodePtr(params[1]), + params[2]); + return IR_CONT; +} + +#define SCROLL_MOUSE_WIDTH 20 + +int32 Logic::fnSetScrollLeftMouse(int32 *params) { + // params: 0 pointer to object's mouse structure + + byte *ob_mouse = decodePtr(params[0]); + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + // Highest priority + + ObjectMouse mouse; + + mouse.x1 = 0; + mouse.y1 = 0; + mouse.x2 = screenInfo->scroll_offset_x + SCROLL_MOUSE_WIDTH; + mouse.y2 = screenInfo->screen_deep - 1; + mouse.priority = 0; + + if (screenInfo->scroll_offset_x > 0) { + // not fully scrolled to the left + mouse.pointer = SCROLL_LEFT_MOUSE_ID; + } else { + // so the mouse area doesn't get registered + mouse.pointer = 0; + } + + mouse.write(ob_mouse); + return IR_CONT; +} + +int32 Logic::fnSetScrollRightMouse(int32 *params) { + // params: 0 pointer to object's mouse structure + + byte *ob_mouse = decodePtr(params[0]); + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + // Highest priority + + ObjectMouse mouse; + + mouse.x1 = screenInfo->scroll_offset_x + _vm->_screen->getScreenWide() - SCROLL_MOUSE_WIDTH; + mouse.y1 = 0; + mouse.x2 = screenInfo->screen_wide - 1; + mouse.y2 = screenInfo->screen_deep - 1; + mouse.priority = 0; + + if (screenInfo->scroll_offset_x < screenInfo->max_scroll_offset_x) { + // not fully scrolled to the right + mouse.pointer = SCROLL_RIGHT_MOUSE_ID; + } else { + // so the mouse area doesn't get registered + mouse.pointer = 0; + } + + mouse.write(ob_mouse); + return IR_CONT; +} + +int32 Logic::fnColour(int32 *params) { + // set border colour - useful during script development + // eg. set to colour during a timer situation, then black when timed + // out + + // params 0: colour (see defines above) + +#ifdef SWORD2_DEBUG + // what colour? + switch (params[0]) { + case BLACK: + _vm->_screen->setPalette(0, 1, black, RDPAL_INSTANT); + break; + case WHITE: + _vm->_screen->setPalette(0, 1, white, RDPAL_INSTANT); + break; + case RED: + _vm->_screen->setPalette(0, 1, red, RDPAL_INSTANT); + break; + case GREEN: + _vm->_screen->setPalette(0, 1, green, RDPAL_INSTANT); + break; + case BLUE: + _vm->_screen->setPalette(0, 1, blue, RDPAL_INSTANT); + break; + } +#endif + + return IR_CONT; +} + +#ifdef SWORD2_DEBUG +#define BLACK 0 +#define WHITE 1 +#define RED 2 +#define GREEN 3 +#define BLUE 4 + +static uint8 black[4] = { 0, 0, 0, 0 }; +static uint8 white[4] = { 255, 255, 255, 0 }; +static uint8 red[4] = { 255, 0, 0, 0 }; +static uint8 green[4] = { 0, 255, 0, 0 }; +static uint8 blue[4] = { 0, 0, 255, 0 }; +#endif + +int32 Logic::fnFlash(int32 *params) { + // flash colour 0 (ie. border) - useful during script development + // eg. fnFlash(BLUE) where a text line is missed; RED when some code + // missing, etc + + // params: 0 colour to flash + +#ifdef SWORD2_DEBUG + // what colour? + switch (params[0]) { + case WHITE: + _vm->_screen->setPalette(0, 1, white, RDPAL_INSTANT); + break; + case RED: + _vm->_screen->setPalette(0, 1, red, RDPAL_INSTANT); + break; + case GREEN: + _vm->_screen->setPalette(0, 1, green, RDPAL_INSTANT); + break; + case BLUE: + _vm->_screen->setPalette(0, 1, blue, RDPAL_INSTANT); + break; + } + + // There used to be a busy-wait loop here, so I don't know how long + // the delay was meant to be. Probably doesn't matter much. + + _vm->_screen->updateDisplay(); + _vm->_system->delayMillis(250); + _vm->_screen->setPalette(0, 1, black, RDPAL_INSTANT); +#endif + + return IR_CONT; +} + +int32 Logic::fnPreFetch(int32 *params) { + // Go fetch resource in the background. + + // params: 0 resource to fetch [guess] + + return IR_CONT; +} + +/** + * Reverse of fnPassPlayerSaveData() - run script 8 of player object. + */ + +int32 Logic::fnGetPlayerSaveData(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + + byte *ob_logic = decodePtr(params[0]); + byte *ob_graph = decodePtr(params[1]); + byte *ob_mega = decodePtr(params[2]); + + // Copy from savegame buffers to player object + + memcpy(ob_logic, _saveLogic, ObjectLogic::size()); + memcpy(ob_graph, _saveGraphic, ObjectGraphic::size()); + memcpy(ob_mega, _saveMega, ObjectMega::size()); + + // Any walk-data must be cleared - the player will be set to stand if + // he was walking when saved. + + ObjectMega obMega(ob_mega); + + if (obMega.getIsWalking()) { + ObjectLogic obLogic(ob_logic); + + obMega.setIsWalking(0); + + int32 pars[3]; + + pars[0] = params[1]; // ob_graphic; + pars[1] = params[2]; // ob_mega + pars[2] = obMega.getCurDir(); + + fnStand(pars); + + // Reset looping flag (which would have been 1 during fnWalk) + obLogic.setLooping(0); + } + + return IR_CONT; +} + +/** + * Copies the 4 essential player structures into the savegame header - run + * script 7 of player object to request this. + * + * Remember, we cannot simply read a compact any longer but instead must + * request it from the object itself. + */ + +int32 Logic::fnPassPlayerSaveData(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + + // Copy from player object to savegame buffers + + memcpy(_saveLogic, decodePtr(params[0]), ObjectLogic::size()); + memcpy(_saveGraphic, decodePtr(params[1]), ObjectGraphic::size()); + memcpy(_saveMega, decodePtr(params[2]), ObjectMega::size()); + + return IR_CONT; +} + +int32 Logic::fnSendEvent(int32 *params) { + // we want to intercept the player character and have him interact + // with an object - from script + + // params: 0 id to receive event + // 1 script to run + + sendEvent(params[0], params[1]); + return IR_CONT; +} + +/** + * Add this walkgrid resource to the list of those used for routing in this + * location. Note that this is ignored if the resource is already in the list. + */ + +int32 Logic::fnAddWalkGrid(int32 *params) { + // params: 0 id of walkgrid resource + + // All objects that add walkgrids must be restarted whenever we + // re-enter a location. + + // DON'T EVER KILL GEORGE! + if (readVar(ID) != CUR_PLAYER_ID) { + // Need to call this in case it wasn't called in script! + fnAddToKillList(NULL); + } + + _router->addWalkGrid(params[0]); + fnPreLoad(params); + return IR_CONT; +} + +/** + * Remove this walkgrid resource from the list of those used for routing in + * this location. Note that this is ignored if the resource isn't actually + * in the list. + */ + +int32 Logic::fnRemoveWalkGrid(int32 *params) { + // params: 0 id of walkgrid resource + + _router->removeWalkGrid(params[0]); + return IR_CONT; +} + +// like fnCheckEventWaiting, but starts the event rather than setting RESULT +// to 1 + +int32 Logic::fnCheckForEvent(int32 *params) { + // params: none + + if (checkEventWaiting()) { + startEvent(); + return IR_TERMINATE; + } + + return IR_CONT; +} + +// combination of fnPause and fnCheckForEvent +// - ie. does a pause, but also checks for event each cycle + +int32 Logic::fnPauseForEvent(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 number of game-cycles to pause + + ObjectLogic obLogic(decodePtr(params[0])); + + if (checkEventWaiting()) { + obLogic.setLooping(0); + startEvent(); + return IR_TERMINATE; + } + + return fnPause(params); +} + +int32 Logic::fnClearEvent(int32 *params) { + // params: none + + clearEvent(readVar(ID)); + return IR_CONT; +} + +int32 Logic::fnFaceMega(int32 *params) { + // params: 0 pointer to object's logic structure + // 1 pointer to object's graphic structure + // 2 pointer to object's mega structure + // 3 pointer to object's walkdata structure + // 4 id of target mega to face + + return _router->faceMega( + decodePtr(params[0]), + decodePtr(params[1]), + decodePtr(params[2]), + decodePtr(params[3]), + params[4]); +} + +int32 Logic::fnPlaySequence(int32 *params) { + // params: 0 pointer to null-terminated ascii filename + // 1 number of frames in the sequence, used for PSX. + + char filename[30]; + MovieTextObject *sequenceSpeechArray[MAX_SEQUENCE_TEXT_LINES + 1]; + + // The original code had some #ifdef blocks for skipping or muting the + // cutscenes - fondly described as "the biggest fudge in the history + // of computer games" - but at the very least we want to show the + // cutscene subtitles, so I removed them. + + debug(5, "fnPlaySequence(\"%s\");", (const char *)decodePtr(params[0])); + + // add the appropriate file extension & play it + + strcpy(filename, (const char *)decodePtr(params[0])); + + // Write to walkthrough file (zebug0.txt) + debug(5, "PLAYING SEQUENCE \"%s\"", filename); + + // now create the text sprites, if any + + if (_sequenceTextLines) + createSequenceSpeech(sequenceSpeechArray); + + // don't want to carry on streaming game music when smacker starts! + fnStopMusic(NULL); + + // pause sfx during sequence + _vm->_sound->pauseFx(); + + MoviePlayer player(_vm); + uint32 rv; + + if (_sequenceTextLines && !readVar(DEMO)) + rv = player.play(filename, sequenceSpeechArray, _smackerLeadIn, _smackerLeadOut); + else + rv = player.play(filename, NULL, _smackerLeadIn, _smackerLeadOut); + + // check the error return-value + if (rv) + debug(5, "MoviePlayer.play(\"%s\") returned 0x%.8x", filename, rv); + + // unpause sound fx again, in case we're staying in same location + _vm->_sound->unpauseFx(); + + _smackerLeadIn = 0; + _smackerLeadOut = 0; + + // now clear the text sprites, if any + + if (_sequenceTextLines) + clearSequenceSpeech(sequenceSpeechArray); + + // now clear the screen in case the Sequence was quitted (using ESC) + // rather than fading down to black + + _vm->_screen->clearScene(); + + // zero the entire palette in case we're about to fade up! + + byte pal[4 * 256]; + + memset(pal, 0, sizeof(pal)); + _vm->_screen->setPalette(0, 256, pal, RDPAL_INSTANT); + + debug(5, "fnPlaySequence FINISHED"); + return IR_CONT; +} + +int32 Logic::fnShadedSprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteShading(decodePtr(params[0]), SHADED_SPRITE); + return IR_CONT; +} + +int32 Logic::fnUnshadedSprite(int32 *params) { + // params: 0 pointer to object's graphic structure + _router->setSpriteShading(decodePtr(params[0]), UNSHADED_SPRITE); + return IR_CONT; +} + +int32 Logic::fnFadeUp(int32 *params) { + // params: none + + _vm->_screen->waitForFade(); + + if (_vm->_screen->getFadeStatus() == RDFADE_BLACK) + _vm->_screen->fadeUp(); + + return IR_CONT; +} + +int32 Logic::fnDisplayMsg(int32 *params) { + // Display a message to the user on the screen. + + // params: 0 Text number of message to be displayed. + + uint32 local_text = params[0] & 0xffff; + uint32 text_res = params[0] / SIZE; + + // Display message for three seconds. + + // +2 to skip the encoded text number in the first 2 chars; 3 is + // duration in seconds + + _vm->_screen->displayMsg(_vm->fetchTextLine(_vm->_resman->openResource(text_res), local_text) + 2, 3); + _vm->_resman->closeResource(text_res); + + return IR_CONT; +} + +int32 Logic::fnSetObjectHeld(int32 *params) { + // params: 0 luggage icon to set + uint32 res = (uint32)params[0]; + + _vm->_mouse->setObjectHeld(res); + return IR_CONT; +} + +int32 Logic::fnAddSequenceText(int32 *params) { + // params: 0 text number + // 1 frame number to start the text displaying + // 2 frame number to stop the text dispalying + + assert(_sequenceTextLines < MAX_SEQUENCE_TEXT_LINES); + + _sequenceTextList[_sequenceTextLines].textNumber = params[0]; + _sequenceTextList[_sequenceTextLines].startFrame = params[1]; + _sequenceTextList[_sequenceTextLines].endFrame = params[2]; + _sequenceTextLines++; + return IR_CONT; +} + +int32 Logic::fnResetGlobals(int32 *params) { + // fnResetGlobals is used by the demo - so it can loop back & restart + // itself + + // params: none + + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + byte *globals = _vm->_resman->openResource(1) + ResHeader::size(); + int32 size = _vm->_resman->fetchLen(1) - ResHeader::size(); + + debug(5, "globals size: %d", size); + + // blank each global variable + memset(globals, 0, size); + + _vm->_resman->closeResource(1); + + // all objects but george + _vm->_resman->killAllObjects(false); + + // FOR THE DEMO - FORCE THE SCROLLING TO BE RESET! + // - this is taken from fnInitBackground + + // switch on scrolling (2 means first time on screen) + screenInfo->scroll_flag = 2; + + // Used to be IR_CONT, but that's a bad idea. We may just have killed + // our own script resource -- continuing will cause a bad memory read + // access. + return IR_STOP; +} + +int32 Logic::fnSetPalette(int32 *params) { + // params: 0 resource number of palette file, or 0 if it's to be + // the palette from the current screen + + _vm->_screen->setFullPalette(params[0]); + return IR_CONT; +} + +// use this in the object's service script prior to registering the mouse area +// ie. before fnRegisterMouse or fnRegisterFrame +// - best if kept at very top of service script + +int32 Logic::fnRegisterPointerText(int32 *params) { + // params: 0 local id of text line to use as pointer text + + _vm->_mouse->registerPointerText(params[0]); + return IR_CONT; +} + +int32 Logic::fnFetchWait(int32 *params) { + // Fetches a resource in the background but prevents the script from + // continuing until the resource is in memory. + + // params: 0 resource to fetch [guess] + + return IR_CONT; +} + +int32 Logic::fnRelease(int32 *params) { + // Releases a resource from memory. Used for freeing memory for + // sprites that have just been used and will not be used again. + // Sometimes it is better to kick out a sprite straight away so that + // the memory can be used for more frequent animations. + + // params: 0 resource to release [guess] + + return IR_CONT; +} + +int32 Logic::fnPrepareMusic(int32 *params) { + // params: 1 id of music to prepare [guess] + return IR_CONT; +} + +int32 Logic::fnSoundFetch(int32 *params) { + // params: 0 id of sound to fetch [guess] + return IR_CONT; +} + +int32 Logic::fnSmackerLeadIn(int32 *params) { + // params: 0 id of lead-in music + + // ready for use in fnPlaySequence + _smackerLeadIn = params[0]; + return IR_CONT; +} + +int32 Logic::fnSmackerLeadOut(int32 *params) { + // params: 0 id of lead-out music + + // ready for use in fnPlaySequence + _smackerLeadOut = params[0]; + return IR_CONT; +} + +/** + * Stops all FX and clears the entire FX queue. + */ + +int32 Logic::fnStopAllFx(int32 *params) { + // params: none + + _vm->_sound->clearFxQueue(); + return IR_CONT; +} + +int32 Logic::fnCheckPlayerActivity(int32 *params) { + // Used to decide when to trigger music cues described as "no player + // activity for a while" + + // params: 0 threshold delay in seconds, ie. what we want to + // check the actual delay against + + uint32 seconds = (uint32)params[0]; + + _vm->_mouse->checkPlayerActivity(seconds); + return IR_CONT; +} + +int32 Logic::fnResetPlayerActivityDelay(int32 *params) { + // Use if you want to deliberately reset the "no player activity" + // counter for any reason + + // params: none + + _vm->_mouse->resetPlayerActivityDelay(); + return IR_CONT; +} + +int32 Logic::fnCheckMusicPlaying(int32 *params) { + // params: none + + // sets result to no. of seconds of current tune remaining + // or 0 if no music playing + + // in seconds, rounded up to the nearest second + writeVar(RESULT, _vm->_sound->musicTimeRemaining()); + return IR_CONT; +} + +int32 Logic::fnPlayCredits(int32 *params) { + // This function just quits the game if this is the playable demo, ie. + // credits are NOT played in the demo any more! + + // params: none + + if (readVar(DEMO)) { + _vm->closeGame(); + return IR_STOP; + } + + _vm->_screen->rollCredits(); + return IR_CONT; +} + +int32 Logic::fnSetScrollSpeedNormal(int32 *params) { + // params: none + + _vm->_screen->setScrollFraction(16); + return IR_CONT; +} + +int32 Logic::fnSetScrollSpeedSlow(int32 *params) { + // params: none + + _vm->_screen->setScrollFraction(32); + return IR_CONT; +} + +// Called from speech scripts to remove the chooser bar when it's not +// appropriate to keep it displayed + +int32 Logic::fnRemoveChooser(int32 *params) { + // params: none + + _vm->_mouse->hideMenu(RDMENU_BOTTOM); + return IR_CONT; +} + +/** + * Alter the volume and pan of a currently playing FX + */ + +int32 Logic::fnSetFxVolAndPan(int32 *params) { + // params: 0 id of fx (ie. the id returned in 'result' from + // fnPlayFx + // 1 new volume (0..16) + // 2 new pan (-16..16) + + debug(5, "fnSetFxVolAndPan(%d, %d, %d)", params[0], params[1], params[2]); + + _vm->_sound->setFxIdVolumePan(params[0], params[1], params[2]); + return IR_CONT; +} + +/** + * Alter the volume of a currently playing FX + */ + +int32 Logic::fnSetFxVol(int32 *params) { + // params: 0 id of fx (ie. the id returned in 'result' from + // fnPlayFx + // 1 new volume (0..16) + + _vm->_sound->setFxIdVolumePan(params[0], params[1]); + return IR_CONT; +} + +int32 Logic::fnRestoreGame(int32 *params) { + // params: none + return IR_CONT; +} + +int32 Logic::fnRefreshInventory(int32 *params) { + // Called from 'menu_look_or_combine' script in 'menu_master' object + // to update the menu to display a combined object while George runs + // voice-over. Note that 'object_held' must be set to the graphic of + // the combined object + + // params: none + + _vm->_mouse->refreshInventory(); + return IR_CONT; +} + +int32 Logic::fnChangeShadows(int32 *params) { + // params: none + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + // if last screen was using a shading mask (see below) + if (screenInfo->mask_flag) { + uint32 rv = _vm->_screen->closeLightMask(); + if (rv) + error("Driver Error %.8x", rv); + screenInfo->mask_flag = false; + } + + return IR_CONT; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/header.h b/engines/sword2/header.h new file mode 100644 index 0000000000..9aefa52832 --- /dev/null +++ b/engines/sword2/header.h @@ -0,0 +1,502 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _HEADER +#define _HEADER + +#include "common/stream.h" + +namespace Sword2 { + +//---------------------------------------------------------- +// SYSTEM FILE & FRAME HEADERS +//---------------------------------------------------------- + +//---------------------------------------------------------- +// ALL FILES +//---------------------------------------------------------- + +// Standard File Header + +#define NAME_LEN 34 + +struct ResHeader { + uint8 fileType; // Byte to define file type (see below) + uint8 compType; // Type of file compression used ie. + // on whole file (see below) + uint32 compSize; // Length of compressed file (ie. + // length on disk) + uint32 decompSize; // Length of decompressed file held in + // memory (NB. frames still held + // compressed) + byte name[NAME_LEN]; // Name of object + + static const int size() { + return 44; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + fileType = readS.readByte(); + compType = readS.readByte(); + compSize = readS.readUint32LE(); + decompSize = readS.readUint32LE(); + readS.read(name, NAME_LEN); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeByte(fileType); + writeS.writeByte(compType); + writeS.writeUint32LE(compSize); + writeS.writeUint32LE(decompSize); + writeS.write(name, NAME_LEN); + } +}; + +// fileType + +enum { + // 0 something's wrong! + ANIMATION_FILE = 1, // All normal animations & sprites + // including mega-sets & font files + // which are the same format (but all + // frames always uncompressed) + SCREEN_FILE = 2, // Each contains background, palette, + // layer sprites, parallax layers & + // shading mask + GAME_OBJECT = 3, // Each contains object hub + + // structures + script data + WALK_GRID_FILE = 4, // Walk-grid data + GLOBAL_VAR_FILE = 5, // All the global script variables in + // one file; "there can be only one" + PARALLAX_FILE_null = 6, // NOT USED + RUN_LIST = 7, // Each contains a list of object + // resource id's + TEXT_FILE = 8, // Each contains all the lines of text + // for a location or a character's + // conversation script + SCREEN_MANAGER = 9, // One for each location; this contains + // special startup scripts + MOUSE_FILE = 10, // Mouse pointers and luggage icons + // (sprites in General / Mouse pointers + // & Luggage icons) + WAV_FILE = 11, // WAV file + ICON_FILE = 12, // Menu icon (sprites in General / Menu + // icons) + PALETTE_FILE = 13 // separate palette file (see also + // _paletteHeader) +}; + +// compType + +enum { + NO_COMPRESSION = 0, + FILE_COMPRESSION = 1 // standard whole-file compression + // (not yet devised!) +}; + +//---------------------------------------------------------- +// (1) ANIMATION FILES +//---------------------------------------------------------- + +// an animation file consists of: +// +// standard file header +// animation header +// a string of CDT entries (one per frame of the anim) +// a 16-byte colour table ONLY if (runTimeComp==RLE16) +// a string of groups of (frame header + frame data) + +// Animation Header + +struct AnimHeader { + uint8 runTimeComp; // Type of runtime compression used for the + // frame data (see below) + uint16 noAnimFrames; // Number of frames in the anim (ie. no. of + // CDT entries) + uint16 feetStartX; // Start coords for mega to walk to, before + uint16 feetStartY; // running anim + uint8 feetStartDir; // Direction to start in before running anim + uint16 feetEndX; // End coords for mega to stand at after + uint16 feetEndY; // running anim (vital if anim starts from an + // off-screen position, or ends in a different + // place from the start) + uint8 feetEndDir; // Direction to start in after running anim + uint16 blend; + + static const int size() { + return 15; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + runTimeComp = readS.readByte(); + noAnimFrames = readS.readUint16LE(); + feetStartX = readS.readUint16LE(); + feetStartY = readS.readUint16LE(); + feetStartDir = readS.readByte(); + feetEndX = readS.readUint16LE(); + feetEndY = readS.readUint16LE(); + feetEndDir = readS.readByte(); + blend = readS.readUint16LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeByte(runTimeComp); + writeS.writeUint16LE(noAnimFrames); + writeS.writeUint16LE(feetStartX); + writeS.writeUint16LE(feetStartY); + writeS.writeByte(feetStartDir); + writeS.writeUint16LE(feetEndX); + writeS.writeUint16LE(feetEndY); + writeS.writeByte(feetEndDir); + writeS.writeUint16LE(blend); + } + +}; + +// runtimeComp - compression used on each frame of the anim + +enum { + NONE = 0, // No frame compression + RLE256 = 1, // James's RLE for 256-colour sprites + RLE16 = 2 // James's RLE for 16- or 17-colour sprites + // (raw blocks have max 16 colours for 2 pixels + // per byte, so '0's are encoded only as FLAT + // for 17-colour sprites eg. George's mega-set) +}; + +// CDT Entry + +struct CdtEntry { + int16 x; // sprite x-coord OR offset to add to mega's + // feet x-coord to calc sprite y-coord + int16 y; // sprite y-coord OR offset to add to mega's + // feet y-coord to calc sprite y-coord + uint32 frameOffset; // points to start of frame header (from start + // of file header) + uint8 frameType; // 0 = print sprite normally with top-left + // corner at (x,y), otherwise see below... + + static const int size() { + return 9; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + x = readS.readUint16LE(); + y = readS.readUint16LE(); + frameOffset = readS.readUint32LE(); + frameType = readS.readByte(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint16LE(x); + writeS.writeUint16LE(y); + writeS.writeUint32LE(frameOffset); + writeS.writeByte(frameType); + } +}; + +// 'frameType' bit values + +enum { + FRAME_OFFSET = 1, // Print at (feetX + x, feetY + y), with + // scaling according to feetY + FRAME_FLIPPED = 2, // Print the frame flipped Left->Right + FRAME_256_FAST = 4 // Frame has been compressed using Pauls fast + // RLE 256 compression. +}; + +// Frame Header + +struct FrameHeader { + uint32 compSize; // Compressed size of frame - NB. compression + // type is now in Anim Header + uint16 width; // Dimensions of frame + uint16 height; + + static const int size() { + return 8; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + compSize = readS.readUint32LE(); + width = readS.readUint16LE(); + height = readS.readUint16LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint32LE(compSize); + writeS.writeUint16LE(width); + writeS.writeUint16LE(height); + } +}; + +//---------------------------------------------------------- +// (2) SCREEN FILES +//---------------------------------------------------------- +// a screen file consists of: +// +// standard file header +// multi screen header +// 4 * 256 bytes of palette data +// 256k palette match table +// 2 background parallax layers +// 1 background layer with screen header +// 2 foreground parallax layers +// a string of layer headers +// a string of layer masks + +// Multi screen header +// Goes at the beginning of a screen file after the standard header. +// Gives offsets from start of table of each of the components + +struct MultiScreenHeader { + uint32 palette; + uint32 bg_parallax[2]; + uint32 screen; + uint32 fg_parallax[2]; + uint32 layers; + uint32 paletteTable; + uint32 maskOffset; + + static const int size() { + return 36; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + palette = readS.readUint32LE(); + bg_parallax[0] = readS.readUint32LE(); + bg_parallax[1] = readS.readUint32LE(); + screen = readS.readUint32LE(); + fg_parallax[0] = readS.readUint32LE(); + fg_parallax[1] = readS.readUint32LE(); + layers = readS.readUint32LE(); + paletteTable = readS.readUint32LE(); + maskOffset = readS.readUint32LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint32LE(palette); + writeS.writeUint32LE(bg_parallax[0]); + writeS.writeUint32LE(bg_parallax[1]); + writeS.writeUint32LE(screen); + writeS.writeUint32LE(fg_parallax[0]); + writeS.writeUint32LE(fg_parallax[1]); + writeS.writeUint32LE(layers); + writeS.writeUint32LE(paletteTable); + writeS.writeUint32LE(maskOffset); + } +}; + +// Screen Header + +struct ScreenHeader { + uint16 width; // dimensions of the background screen + uint16 height; + uint16 noLayers; // number of layer areas + + static const int size() { + return 6; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + width = readS.readUint16LE(); + height = readS.readUint16LE(); + noLayers = readS.readUint16LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint16LE(width); + writeS.writeUint16LE(height); + writeS.writeUint16LE(noLayers); + } +}; + +// Layer Header + +// Note that all the layer headers are kept together, rather than being placed +// before each layer mask, in order to simplify the sort routine. + +struct LayerHeader { + uint16 x; // coordinates of top-left pixel of area + uint16 y; + uint16 width; + uint16 height; + uint32 maskSize; + uint32 offset; // where to find mask data (from start of + // standard file header) + + static const int size() { + return 16; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + x = readS.readUint16LE(); + y = readS.readUint16LE(); + width = readS.readUint16LE(); + height = readS.readUint16LE(); + maskSize = readS.readUint32LE(); + offset = readS.readUint32LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint16LE(x); + writeS.writeUint16LE(y); + writeS.writeUint16LE(width); + writeS.writeUint16LE(height); + writeS.writeUint32LE(maskSize); + writeS.writeUint32LE(offset); + } +}; + +//---------------------------------------------------------- +// (3) SCRIPT OBJECT FILES +//---------------------------------------------------------- +// a script object file consists of: +// +// standard file header +// script object header +// script object data + +//---------------------------------------------------------- +// (5) PALETTE FILES +//---------------------------------------------------------- +// a palette file consists of: +// +// standard file header +// 4 * 256 bytes of palette data +// 256k palette match table + +// an object hub - which represents all that remains of the compact concept + +class ObjectHub { + // int32 type; // type of object + // uint32 logic_level; // what level? + // uint32 logic[3] // NOT USED + // uint32 script_id[3] // need this if script + // uint32 script_pc[3] // need this also + +private: + byte *_addr; + +public: + ObjectHub() { + _addr = NULL; + } + + static const int size() { + return 44; + } + + byte *data() { + return _addr; + } + + void setAddress(byte *addr) { + _addr = addr; + } + + byte *getScriptPcPtr(int level) { + return _addr + 32 + 4 * level; + } + + uint32 getLogicLevel() { + return READ_LE_UINT32(_addr + 4); + } + uint32 getScriptId(int level) { + return READ_LE_UINT32(_addr + 20 + 4 * level); + } + uint32 getScriptPc(int level) { + return READ_LE_UINT32(_addr + 32 + 4 * level); + } + + void setLogicLevel(uint32 x) { + WRITE_LE_UINT32(_addr + 4, x); + } + void setScriptId(int level, uint32 x) { + WRITE_LE_UINT32(_addr + 20 + 4 * level, x); + } + void setScriptPc(int level, uint32 x) { + WRITE_LE_UINT32(_addr + 32 + 4 * level, x); + } +}; + +// (6) text module header + +struct TextHeader { + uint32 noOfLines; // how many lines of text are there in this + // module + + static const int size() { + return 4; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + noOfLines = readS.readUint32LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint32LE(noOfLines); + } +}; + +// a text file has: +// +// ResHeader +// TextHeader +// look up table, to +// line of text,0 +// line of text,0 + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/icons.cpp b/engines/sword2/icons.cpp new file mode 100644 index 0000000000..980c20e3b4 --- /dev/null +++ b/engines/sword2/icons.cpp @@ -0,0 +1,216 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/stream.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" + +namespace Sword2 { + +void Mouse::addMenuObject(byte *ptr) { + assert(_totalTemp < TOTAL_engine_pockets); + + Common::MemoryReadStream readS(ptr, 2 * sizeof(int32)); + + _tempList[_totalTemp].icon_resource = readS.readSint32LE(); + _tempList[_totalTemp].luggage_resource = readS.readSint32LE(); + _totalTemp++; +} + +void Mouse::addSubject(int32 id, int32 ref) { + uint32 in_subject = _vm->_logic->readVar(IN_SUBJECT); + + if (in_subject == 0) { + // This is the start of the new subject list. Set the default + // repsonse id to zero in case we're never passed one. + _defaultResponseId = 0; + } + + if (id == -1) { + // Id -1 is used for setting the default response, i.e. the + // response when someone uses an object on a person and he + // doesn't know anything about it. See fnChoose(). + + _defaultResponseId = ref; + } else { + debug(5, "fnAddSubject res %d, uid %d", id, ref); + _subjectList[in_subject].res = id; + _subjectList[in_subject].ref = ref; + _vm->_logic->writeVar(IN_SUBJECT, in_subject + 1); + } +} + +/** + * Create and start the inventory (bottom) menu + */ + +void Mouse::buildMenu() { + uint32 i, j; + + // Clear the temporary inventory list, since we are going to build a + // new one from scratch. + + for (i = 0; i < TOTAL_engine_pockets; i++) + _tempList[i].icon_resource = 0; + + _totalTemp = 0; + + // Run the 'build_menu' script in the 'menu_master' object. This will + // register all carried menu objects. + + _vm->_logic->runResScript(MENU_MASTER_OBJECT, 0); + + // Create a new master list based on the old master inventory list and + // the new temporary inventory list. The purpose of all this is, as + // far as I can tell, that the new list is ordered in the same way as + // the old list, with new objects added to the end of it. + + // Compare new with old. Anything in master thats not in new gets + // removed from master - if found in new too, remove from temp + + for (i = 0; i < _totalMasters; i++) { + bool found_in_temp = false; + + for (j = 0; j < TOTAL_engine_pockets; j++) { + if (_masterMenuList[i].icon_resource == _tempList[j].icon_resource) { + // We alread know about this object, so kill it + // in the temporary list. + _tempList[j].icon_resource = 0; + found_in_temp = true; + break; + } + } + + if (!found_in_temp) { + // The object is in the master list, but not in the + // temporary list. The player must have lost the object + // since the last time we checked, so kill it in the + // master list. + _masterMenuList[i].icon_resource = 0; + } + } + + // Eliminate blank entries from the master list. + + _totalMasters = 0; + + for (i = 0; i < TOTAL_engine_pockets; i++) { + if (_masterMenuList[i].icon_resource) { + if (i != _totalMasters) { + memcpy(&_masterMenuList[_totalMasters], &_masterMenuList[i], sizeof(MenuObject)); + _masterMenuList[i].icon_resource = 0; + } + _totalMasters++; + } + } + + // Add the new objects - i.e. the ones still in the temporary list but + // not yet in the master list - to the end of the master. + + for (i = 0; i < TOTAL_engine_pockets; i++) { + if (_tempList[i].icon_resource) { + memcpy(&_masterMenuList[_totalMasters++], &_tempList[i], sizeof(MenuObject)); + } + } + + // Initialise the menu from the master list. + + for (i = 0; i < 15; i++) { + uint32 res = _masterMenuList[i].icon_resource; + byte *icon = NULL; + + if (res) { + bool icon_coloured; + + uint32 object_held = _vm->_logic->readVar(OBJECT_HELD); + uint32 combine_base = _vm->_logic->readVar(COMBINE_BASE); + + if (_examiningMenuIcon) { + // When examining an object, that object is + // coloured. The rest are greyed out. + icon_coloured = (res == object_held); + } else if (combine_base) { + // When combining two menu object (i.e. using + // one on another), both are coloured. The rest + // are greyed out. + icon_coloured = (res == object_held || combine_base); + } else { + // If an object is selected but we are not yet + // doing anything with it, the selected object + // is greyed out. The rest are coloured. + icon_coloured = (res != object_held); + } + + icon = _vm->_resman->openResource(res) + ResHeader::size(); + + // The coloured icon is stored directly after the + // greyed out one. + + if (icon_coloured) + icon += (RDMENU_ICONWIDE * RDMENU_ICONDEEP); + } + + setMenuIcon(RDMENU_BOTTOM, i, icon); + + if (res) + _vm->_resman->closeResource(res); + } + + showMenu(RDMENU_BOTTOM); +} + +/** + * Build a fresh system (top) menu. + */ + +void Mouse::buildSystemMenu() { + uint32 icon_list[5] = { + OPTIONS_ICON, + QUIT_ICON, + SAVE_ICON, + RESTORE_ICON, + RESTART_ICON + }; + + // Build them all high in full colour - when one is clicked on all the + // rest will grey out. + + for (int i = 0; i < ARRAYSIZE(icon_list); i++) { + byte *icon = _vm->_resman->openResource(icon_list[i]) + ResHeader::size(); + + // The only case when an icon is grayed is when the player + // is dead. Then SAVE is not available. + + if (!_vm->_logic->readVar(DEAD) || icon_list[i] != SAVE_ICON) + icon += (RDMENU_ICONWIDE * RDMENU_ICONDEEP); + + setMenuIcon(RDMENU_TOP, i, icon); + _vm->_resman->closeResource(icon_list[i]); + } + + showMenu(RDMENU_TOP); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/icons.h b/engines/sword2/icons.h new file mode 100644 index 0000000000..ab5ea578ca --- /dev/null +++ b/engines/sword2/icons.h @@ -0,0 +1,41 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _ICONS +#define _ICONS + +#define MENU_MASTER_OBJECT 44 +#define TOTAL_subjects (375 - 256 + 1) // the speech subject bar +#define TOTAL_engine_pockets (15 + 10) // +10 for overflow + +namespace Sword2 { + +// define these in a script and then register them with the system + +struct MenuObject { + int32 icon_resource; // icon graphic graphic + int32 luggage_resource; // luggage icon resource (for attaching to + // mouse pointer) +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/interpreter.cpp b/engines/sword2/interpreter.cpp new file mode 100644 index 0000000000..1a6e7080b8 --- /dev/null +++ b/engines/sword2/interpreter.cpp @@ -0,0 +1,753 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/util.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/interpreter.h" +#include "sword2/logic.h" +#include "sword2/memory.h" +#include "sword2/resman.h" + +namespace Sword2 { + +#define STACK_SIZE 10 + +// The machine code table + +#ifndef REDUCE_MEMORY_USAGE +# define OPCODE(x) { &Logic::x, #x } +#else +# define OPCODE(x) { &Logic::x, "" } +#endif + +typedef int32 (Logic::*OpcodeProc)(int32 *); +struct OpcodeEntry { + OpcodeProc proc; + const char *desc; +}; + +static const OpcodeEntry opcodes[] = { + /* 00 */ + OPCODE(fnTestFunction), + OPCODE(fnTestFlags), + OPCODE(fnRegisterStartPoint), + OPCODE(fnInitBackground), + /* 04 */ + OPCODE(fnSetSession), + OPCODE(fnBackSprite), + OPCODE(fnSortSprite), + OPCODE(fnForeSprite), + /* 08 */ + OPCODE(fnRegisterMouse), + OPCODE(fnAnim), + OPCODE(fnRandom), + OPCODE(fnPreLoad), + /* 0C */ + OPCODE(fnAddSubject), + OPCODE(fnInteract), + OPCODE(fnChoose), + OPCODE(fnWalk), + /* 10 */ + OPCODE(fnWalkToAnim), + OPCODE(fnTurn), + OPCODE(fnStandAt), + OPCODE(fnStand), + /* 14 */ + OPCODE(fnStandAfterAnim), + OPCODE(fnPause), + OPCODE(fnMegaTableAnim), + OPCODE(fnAddMenuObject), + /* 18 */ + OPCODE(fnStartConversation), + OPCODE(fnEndConversation), + OPCODE(fnSetFrame), + OPCODE(fnRandomPause), + /* 1C */ + OPCODE(fnRegisterFrame), + OPCODE(fnNoSprite), + OPCODE(fnSendSync), + OPCODE(fnUpdatePlayerStats), + /* 20 */ + OPCODE(fnPassGraph), + OPCODE(fnInitFloorMouse), + OPCODE(fnPassMega), + OPCODE(fnFaceXY), + /* 24 */ + OPCODE(fnEndSession), + OPCODE(fnNoHuman), + OPCODE(fnAddHuman), + OPCODE(fnWeWait), + /* 28 */ + OPCODE(fnTheyDoWeWait), + OPCODE(fnTheyDo), + OPCODE(fnWalkToTalkToMega), + OPCODE(fnFadeDown), + /* 2C */ + OPCODE(fnISpeak), + OPCODE(fnTotalRestart), + OPCODE(fnSetWalkGrid), + OPCODE(fnSpeechProcess), + /* 30 */ + OPCODE(fnSetScaling), + OPCODE(fnStartEvent), + OPCODE(fnCheckEventWaiting), + OPCODE(fnRequestSpeech), + /* 34 */ + OPCODE(fnGosub), + OPCODE(fnTimedWait), + OPCODE(fnPlayFx), + OPCODE(fnStopFx), + /* 38 */ + OPCODE(fnPlayMusic), + OPCODE(fnStopMusic), + OPCODE(fnSetValue), + OPCODE(fnNewScript), + /* 3C */ + OPCODE(fnGetSync), + OPCODE(fnWaitSync), + OPCODE(fnRegisterWalkGrid), + OPCODE(fnReverseMegaTableAnim), + /* 40 */ + OPCODE(fnReverseAnim), + OPCODE(fnAddToKillList), + OPCODE(fnSetStandbyCoords), + OPCODE(fnBackPar0Sprite), + /* 44 */ + OPCODE(fnBackPar1Sprite), + OPCODE(fnForePar0Sprite), + OPCODE(fnForePar1Sprite), + OPCODE(fnSetPlayerActionEvent), + /* 48 */ + OPCODE(fnSetScrollCoordinate), + OPCODE(fnStandAtAnim), + OPCODE(fnSetScrollLeftMouse), + OPCODE(fnSetScrollRightMouse), + /* 4C */ + OPCODE(fnColour), + OPCODE(fnFlash), + OPCODE(fnPreFetch), + OPCODE(fnGetPlayerSaveData), + /* 50 */ + OPCODE(fnPassPlayerSaveData), + OPCODE(fnSendEvent), + OPCODE(fnAddWalkGrid), + OPCODE(fnRemoveWalkGrid), + /* 54 */ + OPCODE(fnCheckForEvent), + OPCODE(fnPauseForEvent), + OPCODE(fnClearEvent), + OPCODE(fnFaceMega), + /* 58 */ + OPCODE(fnPlaySequence), + OPCODE(fnShadedSprite), + OPCODE(fnUnshadedSprite), + OPCODE(fnFadeUp), + /* 5C */ + OPCODE(fnDisplayMsg), + OPCODE(fnSetObjectHeld), + OPCODE(fnAddSequenceText), + OPCODE(fnResetGlobals), + /* 60 */ + OPCODE(fnSetPalette), + OPCODE(fnRegisterPointerText), + OPCODE(fnFetchWait), + OPCODE(fnRelease), + /* 64 */ + OPCODE(fnPrepareMusic), + OPCODE(fnSoundFetch), + OPCODE(fnPrepareMusic), // Again, apparently + OPCODE(fnSmackerLeadIn), + /* 68 */ + OPCODE(fnSmackerLeadOut), + OPCODE(fnStopAllFx), + OPCODE(fnCheckPlayerActivity), + OPCODE(fnResetPlayerActivityDelay), + /* 6C */ + OPCODE(fnCheckMusicPlaying), + OPCODE(fnPlayCredits), + OPCODE(fnSetScrollSpeedNormal), + OPCODE(fnSetScrollSpeedSlow), + /* 70 */ + OPCODE(fnRemoveChooser), + OPCODE(fnSetFxVolAndPan), + OPCODE(fnSetFxVol), + OPCODE(fnRestoreGame), + /* 74 */ + OPCODE(fnRefreshInventory), + OPCODE(fnChangeShadows) +}; + +#define push(value) \ +do { \ + assert(stackPtr < ARRAYSIZE(stack)); \ + stack[stackPtr++] = (value); \ +} while (false) + +#define push_ptr(ptr) push(_vm->_memory->encodePtr(ptr)) + +#define pop() (assert(stackPtr < ARRAYSIZE(stack)), stack[--stackPtr]) + +int Logic::runResScript(uint32 scriptRes, uint32 offset) { + byte *scriptAddr; + int result; + + scriptAddr = _vm->_resman->openResource(scriptRes); + result = runScript(scriptAddr, scriptAddr, offset); + _vm->_resman->closeResource(scriptRes); + + return result; +} + +int Logic::runResObjScript(uint32 scriptRes, uint32 objRes, uint32 offset) { + byte *scriptAddr, *objAddr; + int result; + + scriptAddr = _vm->_resman->openResource(scriptRes); + objAddr = _vm->_resman->openResource(objRes); + result = runScript(scriptAddr, objAddr, offset); + _vm->_resman->closeResource(objRes); + _vm->_resman->closeResource(scriptRes); + + return result; +} + +int Logic::runScript(byte *scriptData, byte *objectData, uint32 offset) { + byte pc[4]; + + WRITE_LE_UINT32(pc, offset); + return runScript2(scriptData, objectData, pc); +} + +// This form of the runScript function is only called directly from +// the processSession() function, which uses it to update the script PC in the +// current object hub. For reasons which I do not understand, I couldn't get it +// to work if I called the function first with a dummy offset variable, and +// and then updated the object hub myself. + +int Logic::runScript2(byte *scriptData, byte *objectData, byte *offsetPtr) { + // Interestingly, unlike our BASS engine the stack is a local variable. + // I don't know whether or not this is relevant to the working of the + // BS2 engine. + + int32 stack[STACK_SIZE]; + int32 stackPtr = 0; + + uint32 offset = READ_LE_UINT32(offsetPtr); + + ResHeader header; + + header.read(scriptData); + + scriptData += ResHeader::size() + ObjectHub::size(); + + // The script data format: + // int32_TYPE 1 Size of variable space in bytes + // ... The variable space + // int32_TYPE 1 numberOfScripts + // int32_TYPE numberOfScripts The offsets for each script + + // Initialise some stuff + + uint32 ip = 0; // Code pointer + int scriptNumber; + + // Get the start of variables and start of code + + byte *localVars = scriptData + 4; + byte *code = scriptData + READ_LE_UINT32(scriptData) + 4; + uint32 noScripts = READ_LE_UINT32(code); + + code += 4; + + byte *offsetTable = code; + + if (offset < noScripts) { + ip = READ_LE_UINT32(offsetTable + offset * 4); + scriptNumber = offset; + debug(8, "Starting script %d from %d", scriptNumber, ip); + } else { + uint i; + + ip = offset; + + for (i = 1; i < noScripts; i++) { + if (READ_LE_UINT32(offsetTable + 4 * i) >= ip) + break; + } + + scriptNumber = i - 1; + debug(8, "Resuming script %d from %d", scriptNumber, ip); + } + + // There are a couple of known script bugs related to interacting with + // certain objects. We try to work around a few of them. + + bool checkMopBug = false; + bool checkPyramidBug = false; + bool checkElevatorBug = false; + + if (scriptNumber == 2) { + if (strcmp((char *)header.name, "mop_73") == 0) + checkMopBug = true; + else if (strcmp((char *)header.name, "titipoco_81") == 0) + checkPyramidBug = true; + else if (strcmp((char *)header.name, "lift_82") == 0) + checkElevatorBug = true; + } + + code += noScripts * 4; + + // Code should now be pointing at an identifier and a checksum + byte *checksumBlock = code; + + code += 4 * 3; + + if (READ_LE_UINT32(checksumBlock) != 12345678) { + error("Invalid script in object %s", header.name); + return 0; + } + + int32 codeLen = READ_LE_UINT32(checksumBlock + 4); + int32 checksum = 0; + + for (int i = 0; i < codeLen; i++) + checksum += (unsigned char) code[i]; + + if (checksum != (int32)READ_LE_UINT32(checksumBlock + 8)) { + debug(1, "Checksum error in object %s", header.name); + // This could be bad, but there has been a report about someone + // who had problems running the German version because of + // checksum errors. Could there be a version where checksums + // weren't properly calculated? + } + + bool runningScript = true; + + int parameterReturnedFromMcodeFunction = 0; // Allow scripts to return things + int savedStartOfMcode = 0; // For saving start of mcode commands + + while (runningScript) { + int i; + int32 a, b; + int curCommand, parameter, value; // Command and parameter variables + int retVal; + int caseCount; + bool foundCase; + byte *ptr; + + curCommand = code[ip++]; + + switch (curCommand) { + + // Script-related opcodes + + case CP_END_SCRIPT: + // End the script + runningScript = false; + + // WORKAROUND: The dreaded pyramid bug makes the torch + // untakeable when you speak to Titipoco. This is + // because one of the conditions for the torch to be + // takeable is that Titipoco isn't doing anything out + // of the ordinary. Global variable 913 has to be 0 to + // signify that he is in his "idle" state. + // + // Unfortunately, simply the act of speaking to him + // sets variable 913 to 1 (probably to stop him from + // turning around every now and then). The script may + // then go on to set the variable to different values + // to trigger various behaviours in him, but if you + // have run out of these cases the script won't ever + // set it back to 0 again. + // + // So if his click hander finishes, and variable 913 is + // 1, we set it back to 0 manually. + + if (checkPyramidBug && readVar(913) == 1) { + warning("Working around pyramid bug: Resetting Titipoco's state"); + writeVar(913, 0); + } + + // WORKAROUND: The not-so-known-but-should-be-dreaded + // elevator bug. + // + // The click handler for the top of the elevator only + // handles using the elevator, not examining it. When + // examining it, the mouse cursor is removed but never + // restored. + + if (checkElevatorBug && readVar(RIGHT_BUTTON)) { + warning("Working around elevator bug: Restoring mouse pointer"); + fnAddHuman(NULL); + } + + debug(9, "CP_END_SCRIPT"); + break; + case CP_QUIT: + // Quit out for a cycle + WRITE_LE_UINT32(offsetPtr, ip); + debug(9, "CP_QUIT"); + return 0; + case CP_TERMINATE: + // Quit out immediately without affecting the offset + // pointer + debug(9, "CP_TERMINATE"); + return 3; + case CP_RESTART_SCRIPT: + // Start the script again + ip = FROM_LE_32(offsetTable[scriptNumber]); + debug(9, "CP_RESTART_SCRIPT"); + break; + + // Stack-related opcodes + + case CP_PUSH_INT32: + // Push a long word value on to the stack + Read32ip(parameter); + push(parameter); + debug(9, "CP_PUSH_INT32: %d", parameter); + break; + case CP_PUSH_LOCAL_VAR32: + // Push the contents of a local variable + Read16ip(parameter); + push(READ_LE_UINT32(localVars + parameter)); + debug(9, "CP_PUSH_LOCAL_VAR32: localVars[%d] => %d", parameter / 4, READ_LE_UINT32(localVars + parameter)); + break; + case CP_PUSH_GLOBAL_VAR32: + // Push a global variable + Read16ip(parameter); + push(readVar(parameter)); + debug(9, "CP_PUSH_GLOBAL_VAR32: scriptVars[%d] => %d", parameter, readVar(parameter)); + break; + case CP_PUSH_LOCAL_ADDR: + // Push the address of a local variable + + // From what I understand, some scripts store data + // (e.g. mouse pointers) in their local variable space + // from the very beginning, and use this mechanism to + // pass that data to the opcode function. I don't yet + // know the conceptual difference between this and the + // CP_PUSH_DEREFERENCED_STRUCTURE opcode. + + Read16ip(parameter); + push_ptr(localVars + parameter); + debug(9, "CP_PUSH_LOCAL_ADDR: &localVars[%d] => %p", parameter / 4, localVars + parameter); + break; + case CP_PUSH_STRING: + // Push the address of a string on to the stack + // Get the string size + Read8ip(parameter); + + // ip now points to the string + ptr = code + ip; + push_ptr(ptr); + debug(9, "CP_PUSH_STRING: \"%s\"", ptr); + ip += (parameter + 1); + break; + case CP_PUSH_DEREFERENCED_STRUCTURE: + // Push the address of a dereferenced structure + Read32ip(parameter); + ptr = objectData + 4 + ResHeader::size() + ObjectHub::size() + parameter; + push_ptr(ptr); + debug(9, "CP_PUSH_DEREFERENCED_STRUCTURE: %d => %p", parameter, ptr); + break; + case CP_POP_LOCAL_VAR32: + // Pop a value into a local word variable + Read16ip(parameter); + value = pop(); + WRITE_LE_UINT32(localVars + parameter, value); + debug(9, "CP_POP_LOCAL_VAR32: localVars[%d] = %d", parameter / 4, value); + break; + case CP_POP_GLOBAL_VAR32: + // Pop a global variable + Read16ip(parameter); + value = pop(); + + // WORKAROUND for bug #1214168: The not-at-all dreaded + // mop bug. + // + // At the London Docks, global variable 1003 keeps + // track of Nico: + // + // 0: Hiding behind the first crate. + // 1: Hiding behind the second crate. + // 2: Standing in plain view on the deck. + // 3: Hiding on the roof. + // + // The bug happens when trying to pick up the mop while + // hiding on the roof. Nico climbs down, the mop is + // picked up, but the variable remains set to 3. + // Visually, everything looks ok. But as far as the + // scripts are concerned, she's still hiding up on the + // roof. This is not fatal, but leads to a number of + // glitches until the state is corrected. E.g. trying + // to climb back up the ladder will cause Nico to climb + // down again. + // + // Global variable 1017 keeps track of the mop. Setting + // it to 2 means that the mop has been picked up. We + // use that as the signal that Nico's state needs to be + // updated as well. + + if (checkMopBug && parameter == 1017 && readVar(1003) != 2) { + warning("Working around mop bug: Setting Nico's state"); + writeVar(1003, 2); + } + + writeVar(parameter, value); + debug(9, "CP_POP_GLOBAL_VAR32: scriptsVars[%d] = %d", parameter, value); + break; + case CP_ADDNPOP_LOCAL_VAR32: + Read16ip(parameter); + value = READ_LE_UINT32(localVars + parameter) + pop(); + WRITE_LE_UINT32(localVars + parameter, value); + debug(9, "CP_ADDNPOP_LOCAL_VAR32: localVars[%d] => %d", parameter / 4, value); + break; + case CP_SUBNPOP_LOCAL_VAR32: + Read16ip(parameter); + value = READ_LE_UINT32(localVars + parameter) - pop(); + WRITE_LE_UINT32(localVars + parameter, value); + debug(9, "CP_SUBNPOP_LOCAL_VAR32: localVars[%d] => %d", parameter / 4, value); + break; + case CP_ADDNPOP_GLOBAL_VAR32: + // Add and pop a global variable + Read16ip(parameter); + value = readVar(parameter) + pop(); + writeVar(parameter, value); + debug(9, "CP_ADDNPOP_GLOBAL_VAR32: scriptVars[%d] => %d", parameter, value); + break; + case CP_SUBNPOP_GLOBAL_VAR32: + // Sub and pop a global variable + Read16ip(parameter); + value = readVar(parameter) - pop(); + writeVar(parameter, value); + debug(9, "CP_SUBNPOP_GLOBAL_VAR32: scriptVars[%d] => %d", parameter, value); + break; + + // Jump opcodes + + case CP_SKIPONTRUE: + // Skip if the value on the stack is true + Read32ipLeaveip(parameter); + value = pop(); + if (!value) { + ip += 4; + debug(9, "CP_SKIPONTRUE: %d (IS FALSE (NOT SKIPPED))", parameter); + } else { + ip += parameter; + debug(9, "CP_SKIPONTRUE: %d (IS TRUE (SKIPPED))", parameter); + } + break; + case CP_SKIPONFALSE: + // Skip if the value on the stack is false + Read32ipLeaveip(parameter); + value = pop(); + if (value) { + ip += 4; + debug(9, "CP_SKIPONFALSE: %d (IS TRUE (NOT SKIPPED))", parameter); + } else { + ip += parameter; + debug(9, "CP_SKIPONFALSE: %d (IS FALSE (SKIPPED))", parameter); + } + break; + case CP_SKIPALWAYS: + // skip a block + Read32ipLeaveip(parameter); + ip += parameter; + debug(9, "CP_SKIPALWAYS: %d", parameter); + break; + case CP_SWITCH: + // switch + value = pop(); + Read32ip(caseCount); + + // Search the cases + foundCase = false; + for (i = 0; i < caseCount && !foundCase; i++) { + if (value == (int32)READ_LE_UINT32(code + ip)) { + // We have found the case, so lets + // jump to it + foundCase = true; + ip += READ_LE_UINT32(code + ip + 4); + } else + ip += 4 * 2; + } + + // If we found no matching case then use the default + + if (!foundCase) + ip += READ_LE_UINT32(code + ip); + + debug(9, "CP_SWITCH: [SORRY, NO DEBUG INFO]"); + break; + case CP_SAVE_MCODE_START: + // Save the start position on an mcode instruction in + // case we need to restart it again + savedStartOfMcode = ip - 1; + debug(9, "CP_SAVE_MCODE_START"); + break; + case CP_CALL_MCODE: + // Call an mcode routine + Read16ip(parameter); + assert(parameter < ARRAYSIZE(opcodes)); + // amount to adjust stack by (no of parameters) + Read8ip(value); + debug(9, "CP_CALL_MCODE: '%s', %d", opcodes[parameter].desc, value); + stackPtr -= value; + assert(stackPtr >= 0); + retVal = (this->*opcodes[parameter].proc)(&stack[stackPtr]); + + switch (retVal & 7) { + case IR_STOP: + // Quit out for a cycle + WRITE_LE_UINT32(offsetPtr, ip); + return 0; + case IR_CONT: + // Continue as normal + break; + case IR_TERMINATE: + // Return without updating the offset + return 2; + case IR_REPEAT: + // Return setting offset to start of this + // function call + WRITE_LE_UINT32(offsetPtr, savedStartOfMcode); + return 0; + case IR_GOSUB: + // that's really neat + WRITE_LE_UINT32(offsetPtr, ip); + return 2; + default: + error("Bad return code (%d) from '%s'", retVal & 7, opcodes[parameter].desc); + } + parameterReturnedFromMcodeFunction = retVal >> 3; + break; + case CP_JUMP_ON_RETURNED: + // Jump to a part of the script depending on + // the return value from an mcode routine + + // Get the maximum value + Read8ip(parameter); + debug(9, "CP_JUMP_ON_RETURNED: %d => %d", + parameterReturnedFromMcodeFunction, + READ_LE_UINT32(code + ip + parameterReturnedFromMcodeFunction * 4)); + ip += READ_LE_UINT32(code + ip + parameterReturnedFromMcodeFunction * 4); + break; + + // Operators + + case OP_ISEQUAL: + b = pop(); + a = pop(); + push(a == b); + debug(9, "OP_ISEQUAL: RESULT = %d", a == b); + break; + case OP_NOTEQUAL: + b = pop(); + a = pop(); + push(a != b); + debug(9, "OP_NOTEQUAL: RESULT = %d", a != b); + break; + case OP_GTTHAN: + b = pop(); + a = pop(); + push(a > b); + debug(9, "OP_GTTHAN: RESULT = %d", a > b); + break; + case OP_LSTHAN: + b = pop(); + a = pop(); + push(a < b); + debug(9, "OP_LSTHAN: RESULT = %d", a < b); + break; + case OP_GTTHANE: + b = pop(); + a = pop(); + push(a >= b); + debug(9, "OP_GTTHANE: RESULT = %d", a >= b); + break; + case OP_LSTHANE: + b = pop(); + a = pop(); + push(a <= b); + debug(9, "OP_LSTHANE: RESULT = %d", a <= b); + break; + case OP_PLUS: + b = pop(); + a = pop(); + push(a + b); + debug(9, "OP_PLUS: RESULT = %d", a + b); + break; + case OP_MINUS: + b = pop(); + a = pop(); + push(a - b); + debug(9, "OP_MINUS: RESULT = %d", a - b); + break; + case OP_TIMES: + b = pop(); + a = pop(); + push(a * b); + debug(9, "OP_TIMES: RESULT = %d", a * b); + break; + case OP_DIVIDE: + b = pop(); + a = pop(); + push(a / b); + debug(9, "OP_DIVIDE: RESULT = %d", a / b); + break; + case OP_ANDAND: + b = pop(); + a = pop(); + push(a && b); + debug(9, "OP_ANDAND: RESULT = %d", a && b); + break; + case OP_OROR: + b = pop(); + a = pop(); + push(a || b); + debug(9, "OP_OROR: RESULT = %d", a || b); + break; + + // Debugging opcodes, I think + + case CP_DEBUGON: + debug(9, "CP_DEBUGON"); + break; + case CP_DEBUGOFF: + debug(9, "CP_DEBUGOFF"); + break; + case CP_TEMP_TEXT_PROCESS: + Read32ip(parameter); + debug(9, "CP_TEMP_TEXT_PROCESS: %d", parameter); + break; + default: + error("Invalid script command %d", curCommand); + return 3; + } + } + + return 1; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/interpreter.h b/engines/sword2/interpreter.h new file mode 100644 index 0000000000..365f818a98 --- /dev/null +++ b/engines/sword2/interpreter.h @@ -0,0 +1,96 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _INTERPRETER +#define _INTERPRETER + +namespace Sword2 { + +// Interpreter return codes + +enum { + IR_STOP = 0, // Quit for a cycle + IR_CONT = 1, // Continue as normal + IR_TERMINATE = 2, // Return without updating the offset + IR_REPEAT = 3, // Return; offset at start of function call + IR_GOSUB = 4 // Return with updated offset +}; + +// Get parameter fix so that the playstation version can handle words not on +// word boundaries + +#define Read8ip(var) { var = code[ip]; ip++; } +#define Read16ip(var) { var = (int16)READ_LE_UINT16(code + ip); ip += 2; } +#define Read32ip(var) { var = (int32)READ_LE_UINT32(code + ip); ip += 4; } +#define Read32ipLeaveip(var) { var = (int32)READ_LE_UINT32(code + ip); } + +enum { + // Compiled tokens + + CP_END_SCRIPT = 0, + CP_PUSH_LOCAL_VAR32 = 1, // Push a local variable on to the stack + CP_PUSH_GLOBAL_VAR32 = 2, // Push a global variable + CP_POP_LOCAL_VAR32 = 3, // Pop a local variable from the stack + CP_CALL_MCODE = 4, // Call a machine code function + CP_PUSH_LOCAL_ADDR = 5, // Push the address of a local variable + CP_PUSH_INT32 = 6, // Adjust the stack after calling an fn function + CP_SKIPONFALSE = 7, // Skip if the bottom value on the stack is false + CP_SKIPALWAYS = 8, // Skip a block of code + CP_SWITCH = 9, // Switch on last stack value + CP_ADDNPOP_LOCAL_VAR32 = 10, // Add to a local varible + CP_SUBNPOP_LOCAL_VAR32 = 11, // Subtract from a local variable + CP_SKIPONTRUE = 12, // Skip if the bottom value on the stack is true + CP_POP_GLOBAL_VAR32 = 13, // Pop a global variable + CP_ADDNPOP_GLOBAL_VAR32 = 14, // Add to a global variable + CP_SUBNPOP_GLOBAL_VAR32 = 15, // Subtract from a global variable + CP_DEBUGON = 16, // Turn debugging on + CP_DEBUGOFF = 17, // Turn debugging off + CP_QUIT = 18, // Quit for a cycle + CP_TERMINATE = 19, // Quit script completely + + // Operators + + OP_ISEQUAL = 20, // '==' + OP_PLUS = 21, // '+' + OP_MINUS = 22, // '-' + OP_TIMES = 23, // '*' + OP_DIVIDE = 24, // '/' + OP_NOTEQUAL = 25, // '==' + OP_ANDAND = 26, // '&&' + OP_GTTHAN = 27, // '>' + OP_LSTHAN = 28, // '<' + + // More tokens, mixed types + + CP_JUMP_ON_RETURNED = 29, // Use table of jumps with value returned from fn_mcode + CP_TEMP_TEXT_PROCESS = 30, // A dummy text process command for me + CP_SAVE_MCODE_START = 31, // Save the mcode code start for restarting when necessary + CP_RESTART_SCRIPT = 32, // Start the script from the beginning + CP_PUSH_STRING = 33, // Push a pointer to a string on the stack + CP_PUSH_DEREFERENCED_STRUCTURE = 34, // Push the address of a structure thing + OP_GTTHANE = 35, // >= + OP_LSTHANE = 36, // <= + OP_OROR = 37 // || or OR +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/layers.cpp b/engines/sword2/layers.cpp new file mode 100644 index 0000000000..0b59b5a9b1 --- /dev/null +++ b/engines/sword2/layers.cpp @@ -0,0 +1,191 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// high level layer initialising + +// the system supports: +// 1 optional background parallax layer +// 1 not optional normal backdrop layer +// 3 normal sorted layers +// up to 2 foreground parallax layers + +#include "common/stdafx.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +namespace Sword2 { + +/** + * This function is called when entering a new room. + * @param res resource id of the normal background layer + * @param new_palette 1 for new palette, otherwise 0 + */ + +void Screen::initBackground(int32 res, int32 new_palette) { + byte buf[NAME_LEN]; + int i; + + assert(res); + + _vm->_sound->clearFxQueue(); + waitForFade(); + + debug(1, "CHANGED TO LOCATION \"%s\"", _vm->_resman->fetchName(res, buf)); + + // if last screen was using a shading mask (see below) + if (_thisScreen.mask_flag) { + if (closeLightMask() != RD_OK) + error("Could not close light mask"); + } + + // Close the previous screen, if one is open + if (_thisScreen.background_layer_id) + closeBackgroundLayer(); + + _thisScreen.background_layer_id = res; + _thisScreen.new_palette = new_palette; + + // ok, now read the resource and pull out all the normal sort layer + // info/and set them up at the beginning of the sort list - why do it + // each cycle + + byte *file = _vm->_resman->openResource(_thisScreen.background_layer_id); + ScreenHeader screen_head; + + screen_head.read(_vm->fetchScreenHeader(file)); + + // set number of special sort layers + _thisScreen.number_of_layers = screen_head.noLayers; + _thisScreen.screen_wide = screen_head.width; + _thisScreen.screen_deep = screen_head.height; + + debug(2, "layers=%d width=%d depth=%d", screen_head.noLayers, screen_head.width, screen_head.height); + + // initialise the driver back buffer + setLocationMetrics(screen_head.width, screen_head.height); + + for (i = 0; i < screen_head.noLayers; i++) { + debug(3, "init layer %d", i); + + LayerHeader layer; + + layer.read(_vm->fetchLayerHeader(file, i)); + + // Add the layer to the sort list. We only provide just enough + // information so that it's clear that it's a layer, and where + // to sort it in relation to other things in the list. + + _sortList[i].layer_number = i + 1; + _sortList[i].sort_y = layer.y + layer.height; + } + + // reset scroll offsets + _thisScreen.scroll_offset_x = 0; + _thisScreen.scroll_offset_y = 0; + + if (screen_head.width > _screenWide || screen_head.height > _screenDeep) { + // The layer is larger than the physical screen. Switch on + // scrolling. (2 means first time on screen) + _thisScreen.scroll_flag = 2; + + // Note: if we've already set the player up then we could do + // the initial scroll set here + + // Calculate the maximum scroll offsets to prevent scrolling + // off the edge. The minimum offsets are both 0. + + _thisScreen.max_scroll_offset_x = screen_head.width - _screenWide; + _thisScreen.max_scroll_offset_y = screen_head.height - (_screenDeep - (MENUDEEP * 2)); + } else { + // The later fits on the phyiscal screen. Switch off scrolling. + _thisScreen.scroll_flag = 0; + } + + resetRenderEngine(); + + // These are the physical screen coords where the system will try to + // maintain George's actual feet coords. + + _thisScreen.feet_x = 320; + _thisScreen.feet_y = 340; + + // shading mask + + MultiScreenHeader screenLayerTable; + + screenLayerTable.read(file + ResHeader::size()); + + if (screenLayerTable.maskOffset) { + SpriteInfo spriteInfo; + + spriteInfo.x = 0; + spriteInfo.y = 0; + spriteInfo.w = screen_head.width; + spriteInfo.h = screen_head.height; + spriteInfo.scale = 0; + spriteInfo.scaledWidth = 0; + spriteInfo.scaledHeight = 0; + spriteInfo.type = 0; + spriteInfo.blend = 0; + spriteInfo.data = _vm->fetchShadingMask(file); + spriteInfo.colourTable = 0; + + if (openLightMask(&spriteInfo) != RD_OK) + error("Could not open light mask"); + + // so we know to close it later! (see above) + _thisScreen.mask_flag = true; + } else { + // no need to close a mask later + _thisScreen.mask_flag = false; + } + + // Background parallax layers + + for (i = 0; i < 2; i++) { + if (screenLayerTable.bg_parallax[i]) + initialiseBackgroundLayer(_vm->fetchBackgroundParallaxLayer(file, i)); + else + initialiseBackgroundLayer(NULL); + } + + // Normal backround layer + + initialiseBackgroundLayer(_vm->fetchBackgroundLayer(file)); + + // Foreground parallax layers + + for (i = 0; i < 2; i++) { + if (screenLayerTable.fg_parallax[i]) + initialiseBackgroundLayer(_vm->fetchForegroundParallaxLayer(file, i)); + else + initialiseBackgroundLayer(NULL); + } + + _vm->_resman->closeResource(_thisScreen.background_layer_id); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/layers.h b/engines/sword2/layers.h new file mode 100644 index 0000000000..88aff933b3 --- /dev/null +++ b/engines/sword2/layers.h @@ -0,0 +1,29 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _LAYERS +#define _LAYERS + +namespace Sword2 { + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/logic.cpp b/engines/sword2/logic.cpp new file mode 100644 index 0000000000..c26d5615b9 --- /dev/null +++ b/engines/sword2/logic.cpp @@ -0,0 +1,267 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/resman.h" +#include "sword2/router.h" +#include "sword2/sound.h" + +namespace Sword2 { + +Logic::Logic(Sword2Engine *vm) : + _vm(vm), _kills(0), _currentRunList(0), _smackerLeadIn(0), + _smackerLeadOut(0), _sequenceTextLines(0), _speechTime(0), _animId(0), + _speechAnimType(0), _leftClickDelay(0), _rightClickDelay(0), + _officialTextNumber(0), _speechTextBlocNo(0) { + + _scriptVars = NULL; + memset(_eventList, 0, sizeof(_eventList)); + memset(_syncList, 0, sizeof(_syncList)); + _router = new Router(_vm); +} + +Logic::~Logic() { + delete _router; +} + +/** + * Do one cycle of the current session. + */ + +int Logic::processSession() { + // might change during the session, so take a copy here + uint32 run_list = _currentRunList; + + _pc = 0; // first object in list + + // by minusing the pc we can cause an immediate cessation of logic + // processing on the current list + + while (_pc != 0xffffffff) { + byte *game_object_list, *head, *raw_script_ad, *raw_data_ad; + uint32 level, ret, script, id; + + game_object_list = _vm->_resman->openResource(run_list) + ResHeader::size(); + + assert(_vm->_resman->fetchType(run_list) == RUN_LIST); + + // read the next id + id = READ_LE_UINT32(game_object_list + 4 * _pc); + _pc++; + + writeVar(ID, id); + + _vm->_resman->closeResource(run_list); + + if (!id) { + // End of list - end the session naturally + return 0; + } + + assert(_vm->_resman->fetchType(id) == GAME_OBJECT); + + head = _vm->_resman->openResource(id); + _curObjectHub.setAddress(head + ResHeader::size()); + + level = _curObjectHub.getLogicLevel(); + + debug(5, "Level %d id(%d) pc(%d)", + level, + _curObjectHub.getScriptId(level), + _curObjectHub.getScriptPc(level)); + + // Do the logic for this object. We keep going until a function + // says to stop - remember, system operations are run via + // function calls to drivers now. + + do { + // There is a distinction between running one of our + // own scripts and that of another object. + + level = _curObjectHub.getLogicLevel(); + script = _curObjectHub.getScriptId(level); + + if (script / SIZE == readVar(ID)) { + // It's our own script + + debug(5, "Run script %d pc=%d", + script / SIZE, + _curObjectHub.getScriptPc(level)); + + // This is the script data. Script and data + // object are the same. + + raw_script_ad = head; + + ret = runScript2(raw_script_ad, raw_script_ad, _curObjectHub.getScriptPcPtr(level)); + } else { + // We're running the script of another game + // object - get our data object address. + + uint8 type = _vm->_resman->fetchType(script / SIZE); + + assert(type == GAME_OBJECT || type == SCREEN_MANAGER); + + raw_script_ad = _vm->_resman->openResource(script / SIZE); + raw_data_ad = head; + + ret = runScript2(raw_script_ad, raw_data_ad, _curObjectHub.getScriptPcPtr(level)); + + _vm->_resman->closeResource(script / SIZE); + + // reset to us for service script + raw_script_ad = raw_data_ad; + } + + if (ret == 1) { + level = _curObjectHub.getLogicLevel(); + + // The script finished - drop down a level + if (level) { + _curObjectHub.setLogicLevel(level - 1); + } else { + // Hmmm, level 0 terminated :-| Let's + // be different this time and simply + // let it restart next go :-) + + // Note that this really does happen a + // lot, so don't make it a warning. + + debug(5, "object %d script 0 terminated!", id); + + // reset to rerun, drop out for a cycle + _curObjectHub.setScriptPc(level, _curObjectHub.getScriptId(level) & 0xffff); + ret = 0; + } + } else if (ret > 2) { + error("processSession: illegal script return type %d", ret); + } + + // if ret == 2 then we simply go around again - a new + // script or subroutine will kick in and run + + // keep processing scripts until 0 for quit is returned + } while (ret); + + // Any post logic system requests to go here + + // Clear any syncs that were waiting for this character - it + // has used them or now looses them + + clearSyncs(readVar(ID)); + + if (_pc != 0xffffffff) { + // The session is still valid so run the graphics/mouse + // service script + runScript(raw_script_ad, raw_script_ad, 0); + } + + // and that's it so close the object resource + + _vm->_resman->closeResource(readVar(ID)); + } + + // Leaving a room so remove all ids that must reboot correctly. Then + // restart the loop. + + for (uint32 i = 0; i < _kills; i++) + _vm->_resman->remove(_objectKillList[i]); + + resetKillList(); + return 1; +} + +/** + * Bring an immediate halt to the session and cause a new one to start without + * a screen update. + */ + +void Logic::expressChangeSession(uint32 sesh_id) { + // Set new session and force the old one to quit. + _currentRunList = sesh_id; + _pc = 0xffffffff; + + // Reset now in case we double-clicked an exit prior to changing screen + writeVar(EXIT_FADING, 0); + + // We're trashing the list - presumably to change room. In theory, + // sync waiting in the list could be left behind and never removed - + // so we trash the lot + memset(_syncList, 0, sizeof(_syncList)); + + // Various clean-ups + _router->clearWalkGridList(); + _vm->_sound->clearFxQueue(); + _router->freeAllRouteMem(); +} + +/** + * @return The private _currentRunList variable. + */ + +uint32 Logic::getRunList() { + return _currentRunList; +} + +/** + * Move the current object up a level. Called by fnGosub command. Remember: + * only the logic object has access to _curObjectHub. + */ + +void Logic::logicUp(uint32 new_script) { + debug(5, "new pc = %d", new_script & 0xffff); + + // going up a level - and we'll keep going this cycle + _curObjectHub.setLogicLevel(_curObjectHub.getLogicLevel() + 1); + + assert(_curObjectHub.getLogicLevel() < 3); // Can be 0, 1, 2 + logicReplace(new_script); +} + +/** + * Force the level to one. + */ + +void Logic::logicOne(uint32 new_script) { + _curObjectHub.setLogicLevel(1); + logicReplace(new_script); +} + +/** + * Change current logic. Script must quit with a TERMINATE directive, which + * does not write to &pc + */ + +void Logic::logicReplace(uint32 new_script) { + uint32 level = _curObjectHub.getLogicLevel(); + + _curObjectHub.setScriptId(level, new_script); + _curObjectHub.setScriptPc(level, new_script & 0xffff); +} + +void Logic::resetKillList() { + _kills = 0; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/logic.h b/engines/sword2/logic.h new file mode 100644 index 0000000000..9e264f603c --- /dev/null +++ b/engines/sword2/logic.h @@ -0,0 +1,316 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// logic management + +#ifndef _LOGIC +#define _LOGIC + +#include "sword2/memory.h" + +namespace Sword2 { + +struct MovieTextObject; + +#define MAX_events 10 + +#define TREE_SIZE 3 + +// This must allow for the largest number of objects in a screen +#define OBJECT_KILL_LIST_SIZE 50 + +#define MAX_SEQUENCE_TEXT_LINES 15 + +class Sword2Engine; +class Router; + +struct EventUnit { + uint32 id; + uint32 interact_id; +}; + +class Logic { +private: + Sword2Engine *_vm; + + inline byte *decodePtr(int32 n) { + return _vm->_memory->decodePtr(n); + } + + uint32 _objectKillList[OBJECT_KILL_LIST_SIZE]; + + // keeps note of no. of objects in the kill list + uint32 _kills; + + // denotes the res id of the game-object-list in current use + uint32 _currentRunList; + + //pc during logic loop + uint32 _pc; + + // each object has one of these tacked onto the beginning + ObjectHub _curObjectHub; + + EventUnit _eventList[MAX_events]; + + // Resource id of the wav to use as lead-in/lead-out from smacker + uint32 _smackerLeadIn; + uint32 _smackerLeadOut; + + // keeps count of number of text lines to disaply during the sequence + uint32 _sequenceTextLines; + + // FOR TEXT LINES IN SEQUENCE PLAYER + + struct SequenceTextInfo { + uint32 textNumber; + uint16 startFrame; + uint16 endFrame; + byte *text_mem; + uint32 speechBufferSize; + uint16 *speech_mem; + }; + + SequenceTextInfo _sequenceTextList[MAX_SEQUENCE_TEXT_LINES]; + + void createSequenceSpeech(MovieTextObject *sequenceText[]); + void clearSequenceSpeech(MovieTextObject *sequenceText[]); + + // when not playing a wav we calculate the speech time based upon + // length of ascii + + uint32 _speechTime; + + uint32 _animId; + + // 0 lip synced and repeating - 1 normal once through + uint32 _speechAnimType; + + uint32 _leftClickDelay; // click-delay for LEFT mouse button + uint32 _rightClickDelay; // click-delay for RIGHT mouse button + + // calculated by locateTalker() for use in speech-panning & text-sprite + // positioning + + int16 _textX, _textY; + + void locateTalker(int32 *params); + void formText(int32 *params); + bool wantSpeechForLine(uint32 wavId); + + // Set by fnPassMega() + byte _engineMega[56]; + +public: + Logic(Sword2Engine *vm); + ~Logic(); + + EventUnit *getEventList() { return _eventList; } + byte *getEngineMega() { return _engineMega; } + + byte _saveLogic[8]; + byte _saveGraphic[12]; + byte _saveMega[56]; + + // Point to the global variable data + byte *_scriptVars; + + // "TEXT" - current official text line number - will match the wav + // filenames + + int16 _officialTextNumber; + + // so speech text cleared when running a new start-script + uint32 _speechTextBlocNo; + + uint32 readVar(int n) { + return READ_LE_UINT32(_scriptVars + 4 * n); + } + + void writeVar(int n, uint32 value) { + WRITE_LE_UINT32(_scriptVars + 4 * n, value); + } + + int runResScript(uint32 scriptRes, uint32 offset); + int runResObjScript(uint32 scriptRes, uint32 objRes, uint32 offset); + int runScript(byte *scriptData, byte *objectData, uint32 offset); + int runScript2(byte *scriptData, byte *objectData, byte *offset); + + void sendEvent(uint32 id, uint32 interact_id); + void setPlayerActionEvent(uint32 id, uint32 interact_id); + void startEvent(); + int checkEventWaiting(); + void clearEvent(uint32 id); + void killAllIdsEvents(uint32 id); + + uint32 countEvents(); + + struct SyncUnit { + uint32 id; + uint32 sync; + }; + + // There won't be many, will there? Probably 2 at most i reckon + SyncUnit _syncList[10]; + + void clearSyncs(uint32 id); + void sendSync(uint32 id, uint32 sync); + int getSync(); + + Router *_router; + + int32 fnTestFunction(int32 *params); + int32 fnTestFlags(int32 *params); + int32 fnRegisterStartPoint(int32 *params); + int32 fnInitBackground(int32 *params); + int32 fnSetSession(int32 *params); + int32 fnBackSprite(int32 *params); + int32 fnSortSprite(int32 *params); + int32 fnForeSprite(int32 *params); + int32 fnRegisterMouse(int32 *params); + int32 fnAnim(int32 *params); + int32 fnRandom(int32 *params); + int32 fnPreLoad(int32 *params); + int32 fnAddSubject(int32 *params); + int32 fnInteract(int32 *params); + int32 fnChoose(int32 *params); + int32 fnWalk(int32 *params); + int32 fnWalkToAnim(int32 *params); + int32 fnTurn(int32 *params); + int32 fnStandAt(int32 *params); + int32 fnStand(int32 *params); + int32 fnStandAfterAnim(int32 *params); + int32 fnPause(int32 *params); + int32 fnMegaTableAnim(int32 *params); + int32 fnAddMenuObject(int32 *params); + int32 fnStartConversation(int32 *params); + int32 fnEndConversation(int32 *params); + int32 fnSetFrame(int32 *params); + int32 fnRandomPause(int32 *params); + int32 fnRegisterFrame(int32 *params); + int32 fnNoSprite(int32 *params); + int32 fnSendSync(int32 *params); + int32 fnUpdatePlayerStats(int32 *params); + int32 fnPassGraph(int32 *params); + int32 fnInitFloorMouse(int32 *params); + int32 fnPassMega(int32 *params); + int32 fnFaceXY(int32 *params); + int32 fnEndSession(int32 *params); + int32 fnNoHuman(int32 *params); + int32 fnAddHuman(int32 *params); + int32 fnWeWait(int32 *params); + int32 fnTheyDoWeWait(int32 *params); + int32 fnTheyDo(int32 *params); + int32 fnWalkToTalkToMega(int32 *params); + int32 fnFadeDown(int32 *params); + int32 fnISpeak(int32 *params); + int32 fnTotalRestart(int32 *params); + int32 fnSetWalkGrid(int32 *params); + int32 fnSpeechProcess(int32 *params); + int32 fnSetScaling(int32 *params); + int32 fnStartEvent(int32 *params); + int32 fnCheckEventWaiting(int32 *params); + int32 fnRequestSpeech(int32 *params); + int32 fnGosub(int32 *params); + int32 fnTimedWait(int32 *params); + int32 fnPlayFx(int32 *params); + int32 fnStopFx(int32 *params); + int32 fnPlayMusic(int32 *params); + int32 fnStopMusic(int32 *params); + int32 fnSetValue(int32 *params); + int32 fnNewScript(int32 *params); + int32 fnGetSync(int32 *params); + int32 fnWaitSync(int32 *params); + int32 fnRegisterWalkGrid(int32 *params); + int32 fnReverseMegaTableAnim(int32 *params); + int32 fnReverseAnim(int32 *params); + int32 fnAddToKillList(int32 *params); + int32 fnSetStandbyCoords(int32 *params); + int32 fnBackPar0Sprite(int32 *params); + int32 fnBackPar1Sprite(int32 *params); + int32 fnForePar0Sprite(int32 *params); + int32 fnForePar1Sprite(int32 *params); + int32 fnSetPlayerActionEvent(int32 *params); + int32 fnSetScrollCoordinate(int32 *params); + int32 fnStandAtAnim(int32 *params); + int32 fnSetScrollLeftMouse(int32 *params); + int32 fnSetScrollRightMouse(int32 *params); + int32 fnColour(int32 *params); + int32 fnFlash(int32 *params); + int32 fnPreFetch(int32 *params); + int32 fnGetPlayerSaveData(int32 *params); + int32 fnPassPlayerSaveData(int32 *params); + int32 fnSendEvent(int32 *params); + int32 fnAddWalkGrid(int32 *params); + int32 fnRemoveWalkGrid(int32 *params); + int32 fnCheckForEvent(int32 *params); + int32 fnPauseForEvent(int32 *params); + int32 fnClearEvent(int32 *params); + int32 fnFaceMega(int32 *params); + int32 fnPlaySequence(int32 *params); + int32 fnShadedSprite(int32 *params); + int32 fnUnshadedSprite(int32 *params); + int32 fnFadeUp(int32 *params); + int32 fnDisplayMsg(int32 *params); + int32 fnSetObjectHeld(int32 *params); + int32 fnAddSequenceText(int32 *params); + int32 fnResetGlobals(int32 *params); + int32 fnSetPalette(int32 *params); + int32 fnRegisterPointerText(int32 *params); + int32 fnFetchWait(int32 *params); + int32 fnRelease(int32 *params); + int32 fnPrepareMusic(int32 *params); + int32 fnSoundFetch(int32 *params); + int32 fnSmackerLeadIn(int32 *params); + int32 fnSmackerLeadOut(int32 *params); + int32 fnStopAllFx(int32 *params); + int32 fnCheckPlayerActivity(int32 *params); + int32 fnResetPlayerActivityDelay(int32 *params); + int32 fnCheckMusicPlaying(int32 *params); + int32 fnPlayCredits(int32 *params); + int32 fnSetScrollSpeedNormal(int32 *params); + int32 fnSetScrollSpeedSlow(int32 *params); + int32 fnRemoveChooser(int32 *params); + int32 fnSetFxVolAndPan(int32 *params); + int32 fnSetFxVol(int32 *params); + int32 fnRestoreGame(int32 *params); + int32 fnRefreshInventory(int32 *params); + int32 fnChangeShadows(int32 *params); + + // do one cycle of the current session + int processSession(); + + // cause the logic loop to terminate and drop out + void expressChangeSession(uint32 sesh_id); + + uint32 getRunList(); + + // setup script_id and script_pc in _curObjectHub - called by fnGosub() + void logicUp(uint32 new_script); + + void logicReplace(uint32 new_script); + void logicOne(uint32 new_script); + void resetKillList(); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/maketext.cpp b/engines/sword2/maketext.cpp new file mode 100644 index 0000000000..5edaf5e41e --- /dev/null +++ b/engines/sword2/maketext.cpp @@ -0,0 +1,579 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// MAKETEXT - Constructs a single-frame text sprite: returns a handle to a +// FLOATING memory block containing the sprite, given a +// null-terminated string, max width allowed, pen colour and +// pointer to required character set. +// +// NB 1) The routine does not create a standard file header or +// an anim header for the text sprite - the data simply begins +// with the frame header. +// +// NB 2) If pen colour is zero, it copies the characters into +// the sprite without remapping the colours. +// ie. It can handle both the standard 2-colour font for speech +// and any multicoloured fonts for control panels, etc. +// +// Based on textsprt.c as used for Broken Sword 1, but updated +// for new system by JEL on 9oct96 and updated again (for font +// as a resource) on 5dec96. + +#include "common/stdafx.h" +#include "common/system.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/resman.h" + +namespace Sword2 { + +#define MAX_LINES 30 // max character lines in output sprite + +#define BORDER_COL 200 // source colour for character border (only + // needed for remapping colours) +#define LETTER_COL 193 // source colour for bulk of character ( " ) +#define SPACE ' ' +#define FIRST_CHAR SPACE // first character in character set +#define LAST_CHAR 255 // last character in character set +#define DUD 64 // the first "chequered flag" (dud) symbol in + // our character set is in the '@' position + +/** + * This function creates a new text sprite. The sprite data contains a + * FrameHeader, but not a standard file header. + * + * @param sentence pointer to a null-terminated string + * @param maxWidth the maximum allowed text sprite width in pixels + * @param pen the text colour, or zero to use the source colours + * @param fontRes the font resource id + * @param border the border colour; black by default + * @return a handle to a floating memory block containing the text sprite + * @note The sentence must contain no leading, trailing or extra spaces. + * Out-of-range characters in the string are replaced by a special + * error-signal character (chequered flag) + */ + +byte *FontRenderer::makeTextSprite(byte *sentence, uint16 maxWidth, uint8 pen, uint32 fontRes, uint8 border) { + debug(5, "makeTextSprite(\"%s\", maxWidth=%u)", sentence, maxWidth); + + _borderPen = border; + + // Line- and character spacing are hard-wired, rather than being part + // of the resource. + + if (fontRes == _vm->_speechFontId) { + _lineSpacing = -6; + _charSpacing = -3; + } else if (fontRes == CONSOLE_FONT_ID) { + _lineSpacing = 0; + _charSpacing = 1; + } else { + _lineSpacing = 0; + _charSpacing = 0; + } + + // Allocate memory for array of lineInfo structures + + byte *line = (byte *)malloc(MAX_LINES * sizeof(LineInfo)); + + // Get details of sentence breakdown into array of LineInfo structures + // and get the number of lines involved + + uint16 noOfLines = analyseSentence(sentence, maxWidth, fontRes, (LineInfo *)line); + + // Construct the sprite based on the info gathered - returns floating + // mem block + + byte *textSprite = buildTextSprite(sentence, fontRes, pen, (LineInfo *)line, noOfLines); + + free(line); + return textSprite; +} + +uint16 FontRenderer::analyseSentence(byte *sentence, uint16 maxWidth, uint32 fontRes, LineInfo *line) { + // joinWidth = how much extra space is needed to append a word to a + // line. NB. SPACE requires TWICE the '_charSpacing' to join a word + // to line + + uint16 joinWidth = charWidth(SPACE, fontRes) + 2 * _charSpacing; + + uint16 lineNo = 0; + uint16 pos = 0; + bool firstWord = true; + + byte ch; + + do { + uint16 wordWidth = 0; + uint16 wordLength = 0; + + // Calculate the width of the word. + + ch = sentence[pos++]; + + while (ch && ch != SPACE) { + wordWidth += charWidth(ch, fontRes) + _charSpacing; + wordLength++; + ch = sentence[pos++]; + } + + // Don't include any character spacing at the end of the word. + wordWidth -= _charSpacing; + + // 'ch' is now the SPACE or NULL following the word + // 'pos' indexes to the position following 'ch' + + if (firstWord) { + // This is the first word on the line, so no separating + // space is needed. + + line[0].width = wordWidth; + line[0].length = wordLength; + firstWord = false; + } else { + // See how much extra space this word will need to + // fit on current line (with a separating space + // character - also overlapped) + + uint16 spaceNeeded = joinWidth + wordWidth; + + if (line[lineNo].width + spaceNeeded <= maxWidth) { + // The word fits on this line. + line[lineNo].width += spaceNeeded; + line[lineNo].length += (1 + wordLength); + } else { + // The word spills over to the next line, i.e. + // no separating space. + + lineNo++; + + assert(lineNo < MAX_LINES); + + line[lineNo].width = wordWidth; + line[lineNo].length = wordLength; + } + } + } while (ch); + + return lineNo + 1; +} + +/** + * This function creates a new text sprite in a movable memory block. It must + * be locked before use, i.e. lock, draw sprite, unlock/free. The sprite data + * contains a FrameHeader, but not a standard file header. + * + * @param sentence pointer to a null-terminated string + * @param fontRes the font resource id + * @param pen the text colour, or zero to use the source colours + * @param line array of LineInfo structures, created by analyseSentence() + * @param noOfLines the number of lines, i.e. the number of elements in 'line' + * @return a handle to a floating memory block containing the text sprite + * @note The sentence must contain no leading, trailing or extra spaces. + * Out-of-range characters in the string are replaced by a special + * error-signal character (chequered flag) + */ + +byte *FontRenderer::buildTextSprite(byte *sentence, uint32 fontRes, uint8 pen, LineInfo *line, uint16 noOfLines) { + uint16 i; + + // Find the width of the widest line in the output text + + uint16 spriteWidth = 0; + + for (i = 0; i < noOfLines; i++) + if (line[i].width > spriteWidth) + spriteWidth = line[i].width; + + // Find the total height of the text sprite: the total height of the + // text lines, plus the total height of the spacing between them. + + uint16 char_height = charHeight(fontRes); + uint16 spriteHeight = char_height * noOfLines + _lineSpacing * (noOfLines - 1); + + // Allocate memory for the text sprite + + uint32 sizeOfSprite = spriteWidth * spriteHeight; + byte *textSprite = (byte *)malloc(FrameHeader::size() + sizeOfSprite); + + // At this stage, textSprite points to an unmovable memory block. Set + // up the frame header. + + FrameHeader frame_head; + + frame_head.compSize = 0; + frame_head.width = spriteWidth; + frame_head.height = spriteHeight; + + frame_head.write(textSprite); + + debug(4, "Text sprite size: %ux%u", spriteWidth, spriteHeight); + + // Clear the entire sprite to make it transparent. + + byte *linePtr = textSprite + FrameHeader::size(); + memset(linePtr, 0, sizeOfSprite); + + byte *charSet = _vm->_resman->openResource(fontRes); + + // Build the sprite, one line at a time + + uint16 pos = 0; + + for (i = 0; i < noOfLines; i++) { + // Center each line + byte *spritePtr = linePtr + (spriteWidth - line[i].width) / 2; + + // copy the sprite for each character in this line to the + // text sprite and inc the sprite ptr by the character's + // width minus the 'overlap' + + for (uint j = 0; j < line[i].length; j++) { + byte *charPtr = findChar(sentence[pos++], charSet); + + frame_head.read(charPtr); + + assert(frame_head.height == char_height); + copyChar(charPtr, spritePtr, spriteWidth, pen); + spritePtr += frame_head.width + _charSpacing; + } + + // Skip space at end of last word in this line + pos++; + + linePtr += (char_height + _lineSpacing) * spriteWidth; + } + + _vm->_resman->closeResource(fontRes); + + return textSprite; +} + +/** + * @param ch the ASCII code of the character + * @param fontRes the font resource id + * @return the width of the character + */ + +uint16 FontRenderer::charWidth(byte ch, uint32 fontRes) { + byte *charSet = _vm->_resman->openResource(fontRes); + + FrameHeader frame_head; + + frame_head.read(findChar(ch, charSet)); + _vm->_resman->closeResource(fontRes); + + return frame_head.width; +} + +/** + * @param fontRes the font resource id + * @return the height of a character sprite + * @note All characters in a font are assumed to have the same height, so + * there is no need to specify which one to look at. + */ + +// Returns the height of a character sprite, given the character's ASCII code +// and a pointer to the start of the character set. + +uint16 FontRenderer::charHeight(uint32 fontRes) { + byte *charSet = _vm->_resman->openResource(fontRes); + + FrameHeader frame_head; + + frame_head.read(findChar(FIRST_CHAR, charSet)); + _vm->_resman->closeResource(fontRes); + + return frame_head.height; +} + +/** + * @param ch the ASCII code of the character to find + * @param charSet pointer to the start of the character set + * @return pointer to the requested character or, if it's out of range, the + * 'dud' character (chequered flag) + */ + +byte *FontRenderer::findChar(byte ch, byte *charSet) { + if (ch < FIRST_CHAR) + ch = DUD; + return _vm->fetchFrameHeader(charSet, ch - FIRST_CHAR); +} + +/** + * Copies a character sprite to the sprite buffer. + * @param charPtr pointer to the character sprite + * @param spritePtr pointer to the sprite buffer + * @param spriteWidth the width of the character + * @param pen If zero, copy the data directly. Otherwise remap the + * sprite's colours from BORDER_COL to _borderPen and from + * LETTER_COL to pen. + */ + +void FontRenderer::copyChar(byte *charPtr, byte *spritePtr, uint16 spriteWidth, uint8 pen) { + FrameHeader frame; + + frame.read(charPtr); + + byte *source = charPtr + FrameHeader::size(); + byte *rowPtr = spritePtr; + + for (uint i = 0; i < frame.height; i++) { + byte *dest = rowPtr; + + if (pen) { + // Use the specified colours + for (uint j = 0; j < frame.width; j++) { + switch (*source++) { + case LETTER_COL: + *dest = pen; + break; + case BORDER_COL: + // Don't do a border pixel if there's + // already a bit of another character + // underneath (for overlapping!) + if (!*dest) + *dest = _borderPen; + break; + default: + // Do nothing if source pixel is zero, + // ie. transparent + break; + } + dest++; + } + } else { + // Pen is zero, so just copy character sprites + // directly into text sprite without remapping colours. + // Apparently overlapping is never considered here? + memcpy(dest, source, frame.width); + source += frame.width; + } + rowPtr += spriteWidth; + } +} + +// Distance to keep speech text from edges of screen +#define TEXT_MARGIN 12 + +/** + * Creates a text bloc in the list and returns the bloc number. The list of + * blocs is read and blitted at render time. Choose alignment type + * RDSPR_DISPLAYALIGN or 0 + */ + +uint32 FontRenderer::buildNewBloc(byte *ascii, int16 x, int16 y, uint16 width, uint8 pen, uint32 type, uint32 fontRes, uint8 justification) { + uint32 i = 0; + + while (i < MAX_text_blocs && _blocList[i].text_mem) + i++; + + assert(i < MAX_text_blocs); + + // Create and position the sprite + + _blocList[i].text_mem = makeTextSprite(ascii, width, pen, fontRes); + + // 'NO_JUSTIFICATION' means print sprite with top-left at (x,y) + // without margin checking - used for debug text + + if (justification != NO_JUSTIFICATION) { + FrameHeader frame_head; + + frame_head.read(_blocList[i].text_mem); + + switch (justification) { + case POSITION_AT_CENTRE_OF_BASE: + // This one is always used for SPEECH TEXT; possibly + // also for pointer text + x -= (frame_head.width / 2); + y -= frame_head.height; + break; + case POSITION_AT_CENTRE_OF_TOP: + x -= (frame_head.width / 2); + break; + case POSITION_AT_LEFT_OF_TOP: + // The given coords are already correct for this! + break; + case POSITION_AT_RIGHT_OF_TOP: + x -= frame_head.width; + break; + case POSITION_AT_LEFT_OF_BASE: + y -= frame_head.height; + break; + case POSITION_AT_RIGHT_OF_BASE: + x -= frame_head.width; + y -= frame_head.height; + break; + case POSITION_AT_LEFT_OF_CENTRE: + y -= (frame_head.height / 2); + break; + case POSITION_AT_RIGHT_OF_CENTRE: + x -= frame_head.width; + y -= (frame_head.height) / 2; + break; + } + + // Ensure text sprite is a few pixels inside the visible screen + // remember - it's RDSPR_DISPLAYALIGN + + uint16 text_left_margin = TEXT_MARGIN; + uint16 text_right_margin = 640 - TEXT_MARGIN - frame_head.width; + uint16 text_top_margin = TEXT_MARGIN; + uint16 text_bottom_margin = 400 - TEXT_MARGIN - frame_head.height; + + // Move if too far left or too far right + + if (x < text_left_margin) + x = text_left_margin; + else if (x > text_right_margin) + x = text_right_margin; + + // Move if too high or too low + + if (y < text_top_margin) + y = text_top_margin; + else if (y > text_bottom_margin) + y = text_bottom_margin; + } + + // The sprite is always uncompressed + _blocList[i].type = type | RDSPR_NOCOMPRESSION; + + _blocList[i].x = x; + _blocList[i].y = y; + + return i + 1; +} + +/** + * Called by buildDisplay() + */ + +void FontRenderer::printTextBlocs() { + for (uint i = 0; i < MAX_text_blocs; i++) { + if (_blocList[i].text_mem) { + FrameHeader frame_head; + SpriteInfo spriteInfo; + + frame_head.read(_blocList[i].text_mem); + + spriteInfo.x = _blocList[i].x; + spriteInfo.y = _blocList[i].y; + spriteInfo.w = frame_head.width; + spriteInfo.h = frame_head.height; + spriteInfo.scale = 0; + spriteInfo.scaledWidth = 0; + spriteInfo.scaledHeight = 0; + spriteInfo.type = _blocList[i].type; + spriteInfo.blend = 0; + spriteInfo.data = _blocList[i].text_mem + FrameHeader::size(); + spriteInfo.colourTable = 0; + + uint32 rv = _vm->_screen->drawSprite(&spriteInfo); + if (rv) + error("Driver Error %.8x in printTextBlocs", rv); + } + } +} + +void FontRenderer::killTextBloc(uint32 bloc_number) { + bloc_number--; + free(_blocList[bloc_number].text_mem); + _blocList[bloc_number].text_mem = NULL; +} + +// Resource 3258 contains text from location script for 152 (install, save & +// restore text, etc) + +#define TEXT_RES 3258 + +// Local line number of "save" (actor no. 1826) + +#define SAVE_LINE_NO 1 + +void Sword2Engine::initialiseFontResourceFlags() { + byte *textFile = _resman->openResource(TEXT_RES); + + // If language is Polish or Finnish it requires alternate fonts. + // Otherwise, use regular fonts + + // "tallenna" Finnish for "save" + // "zapisz" Polish for "save" + + // Get the text line (& skip the 2 chars containing the wavId) + char *textLine = (char *)fetchTextLine(textFile, SAVE_LINE_NO) + 2; + + if (strcmp(textLine, "tallenna") == 0) + initialiseFontResourceFlags(FINNISH_TEXT); + else if (strcmp(textLine, "zapisz") == 0) + initialiseFontResourceFlags(POLISH_TEXT); + else + initialiseFontResourceFlags(DEFAULT_TEXT); + + // Get the game name for the windows application + + // According to the GetNameFunction(), which was never called and has + // therefore been removed, the name of the game is: + // + // ENGLISH: "Broken Sword II" + // AMERICAN: "Circle of Blood II" + // GERMAN: "Baphomet's Fluch II" + // default: "Some game or other, part 86" + // + // But we get it from the text resource instead. + + if (_logic->readVar(DEMO)) + textLine = (char *)fetchTextLine(textFile, 451) + 2; + else + textLine = (char *)fetchTextLine(textFile, 54) + 2; + + _system->setWindowCaption(textLine); + _resman->closeResource(TEXT_RES); +} + +/** + * Called from initialiseFontResourceFlags(), and also from console.cpp + */ + +void Sword2Engine::initialiseFontResourceFlags(uint8 language) { + switch (language) { + case FINNISH_TEXT: + _speechFontId = FINNISH_SPEECH_FONT_ID; + _controlsFontId = FINNISH_CONTROLS_FONT_ID; + _redFontId = FINNISH_RED_FONT_ID; + break; + case POLISH_TEXT: + _speechFontId = POLISH_SPEECH_FONT_ID; + _controlsFontId = POLISH_CONTROLS_FONT_ID; + _redFontId = POLISH_RED_FONT_ID; + break; + default: + _speechFontId = ENGLISH_SPEECH_FONT_ID; + _controlsFontId = ENGLISH_CONTROLS_FONT_ID; + _redFontId = ENGLISH_RED_FONT_ID; + break; + } +} + +} // End of namespace Sword2 diff --git a/engines/sword2/maketext.h b/engines/sword2/maketext.h new file mode 100644 index 0000000000..364a412857 --- /dev/null +++ b/engines/sword2/maketext.h @@ -0,0 +1,119 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _MAKETEXT_H +#define _MAKETEXT_H + +#include "sword2/debug.h" + +namespace Sword2 { + +// Output colour for character border - should be be black but note that we +// have to use a different pen number during sequences + +#define BORDER_PEN 194 + +// Usually the only texts on screen are the subtitles and the mouse-over text, +// but there can also be a considerable number of debugging messages... + +#define MAX_text_blocs MAX_DEBUG_TEXTS + 1 + +enum { + // Doesn't keep the text inside the screen - only for debug text! + NO_JUSTIFICATION = 0, + + // These all force text inside the screen edge margin when necessary + POSITION_AT_CENTRE_OF_BASE = 1, + POSITION_AT_CENTRE_OF_TOP = 2, + POSITION_AT_LEFT_OF_TOP = 3, + POSITION_AT_RIGHT_OF_TOP = 4, + POSITION_AT_LEFT_OF_BASE = 5, + POSITION_AT_RIGHT_OF_BASE = 6, + POSITION_AT_LEFT_OF_CENTRE = 7, + POSITION_AT_RIGHT_OF_CENTRE = 8 +}; + +enum { + DEFAULT_TEXT = 0, + FINNISH_TEXT = 1, + POLISH_TEXT = 2 +}; + +// Info about the text, used to create the SpriteInfo struct + + struct TextBloc { + int16 x; + int16 y; + uint16 type; + byte *text_mem; +}; + +// Info for each line of words in the output text sprite + +struct LineInfo { + uint16 width; // Width in pixels + uint16 length; // Length in characters +}; + +class FontRenderer { +private: + Sword2Engine *_vm; + TextBloc _blocList[MAX_text_blocs]; + + // Layout variables - these used to be defines, but now we're dealing + // with three character sets + + int8 _lineSpacing; // no. of pixels to separate lines of + // characters in the output sprite - negative + // for overlap + int8 _charSpacing; // no. of pixels to separate characters along + // each line - negative for overlap + uint8 _borderPen; // output pen colour of character borders + + uint16 analyseSentence(byte *sentence, uint16 maxWidth, uint32 fontRes, LineInfo *line); + byte *buildTextSprite(byte *sentence, uint32 fontRes, uint8 pen, LineInfo *line, uint16 noOfLines); + uint16 charWidth(byte ch, uint32 fontRes); + uint16 charHeight(uint32 fontRes); + byte *findChar(byte ch, byte *charSet); + void copyChar(byte *charPtr, byte *spritePtr, uint16 spriteWidth, uint8 pen); + +public: + FontRenderer(Sword2Engine *vm) : _vm(vm) { + for (int i = 0; i < MAX_text_blocs; i++) + _blocList[i].text_mem = NULL; + } + + ~FontRenderer() { + for (int i = 0; i < MAX_text_blocs; i++) + free(_blocList[i].text_mem); + } + + byte *makeTextSprite(byte *sentence, uint16 maxWidth, uint8 pen, uint32 fontRes, uint8 border = BORDER_PEN); + + void killTextBloc(uint32 bloc_number); + void printTextBlocs(); + + uint32 buildNewBloc(byte *ascii, int16 x, int16 y, uint16 width, uint8 pen, uint32 type, uint32 fontRes, uint8 justification); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/memory.cpp b/engines/sword2/memory.cpp new file mode 100644 index 0000000000..7da4e86b51 --- /dev/null +++ b/engines/sword2/memory.cpp @@ -0,0 +1,244 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// The new memory manager, now only used by the resource manager. The original +// one would allocated a 12 MB memory pool at startup, which may have been +// appropriate for the original Playstation version but didn't work very well +// with our PocketPC version. +// +// There is one thing that prevents us from replacing the whole memory manager +// with the standard memory allocation functions: Broken Sword 2 absolutely, +// positively needs to be able to encode pointers as 32-bit integers. The +// original engine did this simply by casting between pointers and integers, +// but as far as I know that's not a very portable thing to do. +// +// If it had only used pointers as opcode parameters it would have been +// possible, albeit messy, to extend the stack data type. However, there is +// code in walker.cpp that obviously violates that assumption, and there are +// probably other cases as well. +// +// Instead, we take advantage of the fact that the original memory manager +// could only handle up to 999 blocks of memory. That means we can encode a +// pointer as a 10-bit id and a 22-bit offset into the block. Judging by early +// testing, both should be plenty. +// +// The number zero is used to represent the NULL pointer. + +#include "common/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/memory.h" + +namespace Sword2 { + +MemoryManager::MemoryManager(Sword2Engine *vm) : _vm(vm) { + // The id stack contains all the possible ids for the memory blocks. + // We use this to ensure that no two blocks ever have the same id. + + // The memory blocks are stored in an array, indexed on the block's + // id. This means that given a block id we can find the pointer with a + // simple array lookup. + + // The memory block index is an array of pointers to the memory block + // array, sorted on the memory block's pointer. This means that given + // a pointer into a memory block we can find its id with binary + // searching. + // + // A balanced tree might have been more efficient - the index has to + // be re-sorted every time a block is allocated or freed - but such + // beasts are tricky to implement. Anyway, it wouldn't have made + // encoding or decoding pointers any faster, and these are by far the + // most common operations. + + _idStack = (int16 *)malloc(MAX_MEMORY_BLOCKS * sizeof(int16)); + _memBlocks = (MemBlock *)malloc(MAX_MEMORY_BLOCKS * sizeof(MemBlock)); + _memBlockIndex = (MemBlock **)malloc(MAX_MEMORY_BLOCKS * sizeof(MemBlock *)); + + _totAlloc = 0; + _numBlocks = 0; + + for (int i = 0; i < MAX_MEMORY_BLOCKS; i++) { + _idStack[i] = MAX_MEMORY_BLOCKS - i - 1; + _memBlocks[i].ptr = NULL; + _memBlockIndex[i] = NULL; + } + + _idStackPtr = MAX_MEMORY_BLOCKS; +} + +MemoryManager::~MemoryManager() { + for (int i = 0; i < MAX_MEMORY_BLOCKS; i++) + free(_memBlocks[i].ptr); + free(_memBlocks); + free(_memBlockIndex); + free(_idStack); +} + +int32 MemoryManager::encodePtr(byte *ptr) { + if (ptr == NULL) + return 0; + + int idx = findPointerInIndex(ptr); + + assert(idx != -1); + + uint32 id = _memBlockIndex[idx]->id; + uint32 offset = ptr - _memBlocks[id].ptr; + + assert(id < 0x03ff); + assert(offset <= 0x003fffff); + assert(offset < _memBlocks[id].size); + + return ((id + 1) << 22) | (ptr - _memBlocks[id].ptr); +} + +byte *MemoryManager::decodePtr(int32 n) { + if (n == 0) + return NULL; + + uint32 id = ((n & 0xffc00000) >> 22) - 1; + uint32 offset = n & 0x003fffff; + + assert(_memBlocks[id].ptr); + assert(offset < _memBlocks[id].size); + + return _memBlocks[id].ptr + offset; +} + +int16 MemoryManager::findExactPointerInIndex(byte *ptr) { + int left = 0; + int right = _numBlocks - 1; + + while (right >= left) { + int n = (left + right) / 2; + + if (_memBlockIndex[n]->ptr == ptr) + return n; + + if (_memBlockIndex[n]->ptr > ptr) + right = n - 1; + else + left = n + 1; + } + + return -1; +} + +int16 MemoryManager::findPointerInIndex(byte *ptr) { + int left = 0; + int right = _numBlocks - 1; + + while (right >= left) { + int n = (left + right) / 2; + + if (_memBlockIndex[n]->ptr <= ptr && _memBlockIndex[n]->ptr + _memBlockIndex[n]->size > ptr) + return n; + + if (_memBlockIndex[n]->ptr > ptr) + right = n - 1; + else + left = n + 1; + } + + return -1; +} + +int16 MemoryManager::findInsertionPointInIndex(byte *ptr) { + if (_numBlocks == 0) + return 0; + + int left = 0; + int right = _numBlocks - 1; + int n = 0; + + while (right >= left) { + n = (left + right) / 2; + + if (_memBlockIndex[n]->ptr == ptr) + return -1; + + if (_memBlockIndex[n]->ptr > ptr) + right = n - 1; + else + left = n + 1; + } + + if (_memBlockIndex[n]->ptr < ptr) + n++; + + return n; +} + +byte *MemoryManager::memAlloc(uint32 size, int16 uid) { + assert(_idStackPtr > 0); + + // Get the new block's id from the stack. + int16 id = _idStack[--_idStackPtr]; + + // Allocate the new memory block + byte *ptr = (byte *)malloc(size); + + assert(ptr); + + _memBlocks[id].id = id; + _memBlocks[id].uid = uid; + _memBlocks[id].ptr = ptr; + _memBlocks[id].size = size; + + // Update the memory block index. + int16 idx = findInsertionPointInIndex(ptr); + + assert(idx != -1); + + for (int i = _numBlocks; i > idx; i--) + _memBlockIndex[i] = _memBlockIndex[i - 1]; + + _memBlockIndex[idx] = &_memBlocks[id]; + _numBlocks++; + _totAlloc += size; + + return _memBlocks[id].ptr; +} + +void MemoryManager::memFree(byte *ptr) { + int16 idx = findExactPointerInIndex(ptr); + + if (idx == -1) { + warning("Freeing non-allocated pointer %p", ptr); + return; + } + + // Put back the id on the stack + _idStack[_idStackPtr++] = _memBlockIndex[idx]->id; + + // Release the memory block + free(_memBlockIndex[idx]->ptr); + _memBlockIndex[idx]->ptr = NULL; + + _totAlloc -= _memBlockIndex[idx]->size; + + // Remove the memory block from the index + _numBlocks--; + + for (int i = idx; i < _numBlocks; i++) + _memBlockIndex[i] = _memBlockIndex[i + 1]; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/memory.h b/engines/sword2/memory.h new file mode 100644 index 0000000000..3154842cd9 --- /dev/null +++ b/engines/sword2/memory.h @@ -0,0 +1,70 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef MEMORY_H +#define MEMORY_H + +#define MAX_MEMORY_BLOCKS 999 + +namespace Sword2 { + +struct MemBlock { + int16 id; + int16 uid; + byte *ptr; + uint32 size; +}; + +class MemoryManager { +private: + Sword2Engine *_vm; + + MemBlock *_memBlocks; + MemBlock **_memBlockIndex; + int16 _numBlocks; + + uint32 _totAlloc; + + int16 *_idStack; + int16 _idStackPtr; + + int16 findExactPointerInIndex(byte *ptr); + int16 findPointerInIndex(byte *ptr); + int16 findInsertionPointInIndex(byte *ptr); + +public: + MemoryManager(Sword2Engine *vm); + ~MemoryManager(); + + int16 getNumBlocks() { return _numBlocks; } + uint32 getTotAlloc() { return _totAlloc; } + MemBlock *getMemBlocks() { return _memBlocks; } + + int32 encodePtr(byte *ptr); + byte *decodePtr(int32 n); + + byte *memAlloc(uint32 size, int16 uid); + void memFree(byte *ptr); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/menu.cpp b/engines/sword2/menu.cpp new file mode 100644 index 0000000000..07e00accb6 --- /dev/null +++ b/engines/sword2/menu.cpp @@ -0,0 +1,291 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/mouse.h" + +namespace Sword2 { + +#define MENUDEEP 40 +#define MAXMENUANIMS 8 + +void Mouse::clearIconArea(int menu, int pocket, Common::Rect *r) { + byte *buf = _vm->_screen->getScreen(); + int16 screenWide = _vm->_screen->getScreenWide(); + + r->top = menu * (RENDERDEEP + MENUDEEP) + (MENUDEEP - RDMENU_ICONDEEP) / 2; + r->bottom = r->top + RDMENU_ICONDEEP; + r->left = RDMENU_ICONSTART + pocket * (RDMENU_ICONWIDE + RDMENU_ICONSPACING); + r->right = r->left + RDMENU_ICONWIDE; + + byte *dst = buf + r->top * screenWide + r->left; + + for (int i = 0; i < RDMENU_ICONDEEP; i++) { + memset(dst, 0, RDMENU_ICONWIDE); + dst += screenWide; + } +} + +/** + * This function should be called regularly to process the menubar system. The + * rate at which this function is called will dictate how smooth the menu + * system is. + */ + +void Mouse::processMenu() { + uint8 menu; + uint8 i, j; + uint8 frameCount; + Common::Rect r1, r2; + static int32 lastTime = 0; + + byte *buf = _vm->_screen->getScreen(); + int16 screenWide = _vm->_screen->getScreenWide(); + + if (lastTime == 0) { + lastTime = _vm->getMillis(); + frameCount = 1; + } else { + int32 delta = _vm->getMillis() - lastTime; + + if (delta > 250) { + lastTime += delta; + delta = 250; + frameCount = 1; + } else { + frameCount = (uint8) ((_iconCount + 8) * delta / 750); + lastTime += frameCount * 750 / (_iconCount + 8); + } + } + + // Note: The "almost hidden" menu state exists only so that the menu + // will be redrawn one last time before it's completely hidden. We do + // not need a corresponding "almost shown" state because the menu will + // always be redrawn while it's shown anyway. (We may want to change + // this later.) + + while (frameCount-- > 0) { + for (menu = RDMENU_TOP; menu <= RDMENU_BOTTOM; menu++) { + if (_menuStatus[menu] == RDMENU_HIDDEN || _menuStatus[menu] == RDMENU_ALMOST_HIDDEN || _menuStatus[menu] == RDMENU_SHOWN) + continue; + + int target, direction, nextState; + + if (_menuStatus[menu] == RDMENU_OPENING) { + target = MAXMENUANIMS; + direction = 1; + nextState = RDMENU_SHOWN; + } else { + target = 0; + direction = -1; + nextState = RDMENU_ALMOST_HIDDEN; + } + + bool complete = true; + + // Propagate animation from the first icon... + for (i = RDMENU_MAXPOCKETS - 1; i > 0; i--) { + _pocketStatus[menu][i] = _pocketStatus[menu][i - 1]; + + if (_pocketStatus[menu][i] != target) + complete = false; + } + + if (_pocketStatus[menu][i] != target) + complete = false; + + // ...and animate the first icon + if (_pocketStatus[menu][0] != target) + _pocketStatus[menu][0] += direction; + + if (complete) + _menuStatus[menu] = nextState; + } + } + + for (menu = RDMENU_TOP; menu <= RDMENU_BOTTOM; menu++) { + if (_menuStatus[menu] == RDMENU_HIDDEN) + continue; + + if (_menuStatus[menu] == RDMENU_ALMOST_HIDDEN) + _menuStatus[menu] = RDMENU_HIDDEN; + + // Draw the menu here. + int32 curx = RDMENU_ICONSTART + RDMENU_ICONWIDE / 2; + int32 cury = (MENUDEEP / 2) + (RENDERDEEP + MENUDEEP) * menu; + + for (i = 0; i < RDMENU_MAXPOCKETS; i++) { + if (_icons[menu][i]) { + int32 xoff, yoff; + + // Since we no longer clear the screen after + // each frame we need to clear the icon area. + + clearIconArea(menu, i, &r1); + + if (_pocketStatus[menu][i] == MAXMENUANIMS) { + xoff = (RDMENU_ICONWIDE / 2); + r2.left = curx - xoff; + r2.right = r2.left + RDMENU_ICONWIDE; + yoff = (RDMENU_ICONDEEP / 2); + r2.top = cury - yoff; + r2.bottom = r2.top + RDMENU_ICONDEEP; + } else { + xoff = (RDMENU_ICONWIDE / 2) * _pocketStatus[menu][i] / MAXMENUANIMS; + r2.left = curx - xoff; + r2.right = curx + xoff; + yoff = (RDMENU_ICONDEEP / 2) * _pocketStatus[menu][i] / MAXMENUANIMS; + r2.top = cury - yoff; + r2.bottom = cury + yoff; + } + + if (xoff != 0 && yoff != 0) { + byte *dst = buf + r2.top * screenWide + r2.left; + byte *src = _icons[menu][i]; + + if (_pocketStatus[menu][i] != MAXMENUANIMS) { + _vm->_screen->scaleImageFast( + dst, screenWide, r2.right - r2.left, r2.bottom - r2.top, + src, RDMENU_ICONWIDE, RDMENU_ICONWIDE, RDMENU_ICONDEEP); + } else { + for (j = 0; j < RDMENU_ICONDEEP; j++) { + memcpy(dst, src, RDMENU_ICONWIDE); + src += RDMENU_ICONWIDE; + dst += screenWide; + } + } + } + _vm->_screen->updateRect(&r1); + } + curx += (RDMENU_ICONSPACING + RDMENU_ICONWIDE); + } + } +} + +/** + * This function brings a specified menu into view. + * @param menu RDMENU_TOP or RDMENU_BOTTOM, depending on which menu to show + * @return RD_OK, or an error code + */ + +int32 Mouse::showMenu(uint8 menu) { + // Check for invalid menu parameter + if (menu > RDMENU_BOTTOM) + return RDERR_INVALIDMENU; + + // Check that the menu is not currently shown, or in the process of + // being shown. + if (_menuStatus[menu] == RDMENU_SHOWN || _menuStatus[menu] == RDMENU_OPENING) + return RDERR_INVALIDCOMMAND; + + _menuStatus[menu] = RDMENU_OPENING; + return RD_OK; +} + +/** + * This function hides a specified menu. + * @param menu RDMENU_TOP or RDMENU_BOTTOM depending on which menu to hide + * @return RD_OK, or an error code + */ + +int32 Mouse::hideMenu(uint8 menu) { + // Check for invalid menu parameter + if (menu > RDMENU_BOTTOM) + return RDERR_INVALIDMENU; + + // Check that the menu is not currently hidden, or in the process of + // being hidden. + if (_menuStatus[menu] == RDMENU_HIDDEN || _menuStatus[menu] == RDMENU_CLOSING) + return RDERR_INVALIDCOMMAND; + + _menuStatus[menu] = RDMENU_CLOSING; + return RD_OK; +} + +/** + * This function hides both menus immediately. + */ + +void Mouse::closeMenuImmediately() { + Common::Rect r; + int i; + + _menuStatus[RDMENU_TOP] = RDMENU_HIDDEN; + _menuStatus[RDMENU_BOTTOM] = RDMENU_HIDDEN; + + for (i = 0; i < RDMENU_MAXPOCKETS; i++) { + if (_icons[RDMENU_TOP][i]) { + clearIconArea(RDMENU_TOP, i, &r); + _vm->_screen->updateRect(&r); + } + if (_icons[RDMENU_BOTTOM][i]) { + clearIconArea(RDMENU_BOTTOM, i, &r); + _vm->_screen->updateRect(&r); + } + } + + memset(_pocketStatus, 0, sizeof(uint8) * 2 * RDMENU_MAXPOCKETS); +} + +/** + * This function sets a menubar icon. + * @param menu RDMENU_TOP or RDMENU_BOTTOM, depending on which menu to change + * @param pocket the menu pocket to change + * @param icon icon data, or NULL to clear the icon + * @return RD_OK, or an error code + */ + +int32 Mouse::setMenuIcon(uint8 menu, uint8 pocket, byte *icon) { + Common::Rect r; + + // Check for invalid menu parameter. + if (menu > RDMENU_BOTTOM) + return RDERR_INVALIDMENU; + + // Check for invalid pocket parameter + if (pocket >= RDMENU_MAXPOCKETS) + return RDERR_INVALIDPOCKET; + + // If there is an icon in the requested menu/pocket, clear it out. + if (_icons[menu][pocket]) { + _iconCount--; + free(_icons[menu][pocket]); + _icons[menu][pocket] = NULL; + clearIconArea(menu, pocket, &r); + _vm->_screen->updateRect(&r); + } + + // Only put the icon in the pocket if it is not NULL + if (icon != NULL) { + _iconCount++; + _icons[menu][pocket] = (byte *)malloc(RDMENU_ICONWIDE * RDMENU_ICONDEEP); + if (_icons[menu][pocket] == NULL) + return RDERR_OUTOFMEMORY; + memcpy(_icons[menu][pocket], icon, RDMENU_ICONWIDE * RDMENU_ICONDEEP); + } + + return RD_OK; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/module.mk b/engines/sword2/module.mk new file mode 100644 index 0000000000..2ee3f39ebb --- /dev/null +++ b/engines/sword2/module.mk @@ -0,0 +1,48 @@ +MODULE := engines/sword2 + +MODULE_OBJS := \ + engines/sword2/_mouse.o \ + engines/sword2/animation.o \ + engines/sword2/anims.o \ + engines/sword2/build_display.o \ + engines/sword2/console.o \ + engines/sword2/controls.o \ + engines/sword2/d_draw.o \ + engines/sword2/debug.o \ + engines/sword2/events.o \ + engines/sword2/function.o \ + engines/sword2/icons.o \ + engines/sword2/interpreter.o \ + engines/sword2/layers.o \ + engines/sword2/logic.o \ + engines/sword2/maketext.o \ + engines/sword2/memory.o \ + engines/sword2/menu.o \ + engines/sword2/mouse.o \ + engines/sword2/music.o \ + engines/sword2/palette.o \ + engines/sword2/protocol.o \ + engines/sword2/rdwin.o \ + engines/sword2/render.o \ + engines/sword2/resman.o \ + engines/sword2/router.o \ + engines/sword2/save_rest.o \ + engines/sword2/scroll.o \ + engines/sword2/sound.o \ + engines/sword2/speech.o \ + engines/sword2/sprite.o \ + engines/sword2/startup.o \ + engines/sword2/sword2.o \ + engines/sword2/sync.o \ + engines/sword2/walker.o + +MODULE_DIRS += \ + engines/sword2 + +# This module can be built as a plugin +ifdef BUILD_PLUGINS +PLUGIN := 1 +endif + +# Include common rules +include $(srcdir)/common.rules diff --git a/engines/sword2/mouse.cpp b/engines/sword2/mouse.cpp new file mode 100644 index 0000000000..f8c315a47f --- /dev/null +++ b/engines/sword2/mouse.cpp @@ -0,0 +1,1437 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/console.h" +#include "sword2/controls.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +namespace Sword2 { + +// Pointer resource id's + +enum { + CROSHAIR = 18, + EXIT0 = 788, + EXIT1 = 789, + EXIT2 = 790, + EXIT3 = 791, + EXIT4 = 792, + EXIT5 = 793, + EXIT6 = 794, + EXIT7 = 795, + EXITDOWN = 796, + EXITUP = 797, + MOUTH = 787, + NORMAL = 17, + PICKUP = 3099, + SCROLL_L = 1440, + SCROLL_R = 1441, + USE = 3100 +}; + +Mouse::Mouse(Sword2Engine *vm) { + _vm = vm; + + setPos(0, 0); + resetMouseList(); + + _mouseTouching = 0; + _oldMouseTouching = 0; + _menuSelectedPos = 0; + _examiningMenuIcon = false; + _mousePointerRes = 0; + _mouseMode = 0; + _mouseStatus = false; + _mouseModeLocked = false; + _currentLuggageResource = 0; + _oldButton = 0; + _buttonClick = 0; + _pointerTextBlocNo = 0; + _playerActivityDelay = 0; + _realLuggageItem = 0; + + _mouseAnim.data = NULL; + _luggageAnim.data = NULL; + + // For the menus + _totalTemp = 0; + memset(_tempList, 0, sizeof(_tempList)); + + _totalMasters = 0; + memset(_masterMenuList, 0, sizeof(_masterMenuList)); + memset(_mouseList, 0, sizeof(_mouseList)); + memset(_subjectList, 0, sizeof(_subjectList)); + + _defaultResponseId = 0; + _choosing = false; + + _iconCount = 0; + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < RDMENU_MAXPOCKETS; j++) { + _icons[i][j] = NULL; + _pocketStatus[i][j] = 0; + } + + _menuStatus[i] = RDMENU_HIDDEN; + } +} + +Mouse::~Mouse() { + free(_mouseAnim.data); + free(_luggageAnim.data); + for (int i = 0; i < 2; i++) + for (int j = 0; j < RDMENU_MAXPOCKETS; j++) + free(_icons[i][j]); +} + +void Mouse::getPos(int &x, int &y) { + x = _pos.x; + y = _pos.y; +} + +void Mouse::setPos(int x, int y) { + _pos.x = x; + _pos.y = y; +} + +/** + * Call at beginning of game loop + */ + +void Mouse::resetMouseList() { + _curMouse = 0; +} + +void Mouse::registerMouse(byte *ob_mouse, BuildUnit *build_unit) { + assert(_curMouse < TOTAL_mouse_list); + + ObjectMouse mouse; + + mouse.read(ob_mouse); + + if (!mouse.pointer) + return; + + if (build_unit) { + _mouseList[_curMouse].rect.left = build_unit->x; + _mouseList[_curMouse].rect.top = build_unit->y; + _mouseList[_curMouse].rect.right = 1 + build_unit->x + build_unit->scaled_width; + _mouseList[_curMouse].rect.bottom = 1 + build_unit->y + build_unit->scaled_height; + } else { + _mouseList[_curMouse].rect.left = mouse.x1; + _mouseList[_curMouse].rect.top = mouse.y1; + _mouseList[_curMouse].rect.right = 1 + mouse.x2; + _mouseList[_curMouse].rect.bottom = 1 + mouse.y2; + } + + _mouseList[_curMouse].priority = mouse.priority; + _mouseList[_curMouse].pointer = mouse.pointer; + + // Change all COGS pointers to CROSHAIR. I'm guessing that this was a + // design decision made in mid-development and they didn't want to go + // back and re-generate the resource files. + + if (_mouseList[_curMouse].pointer == USE) + _mouseList[_curMouse].pointer = CROSHAIR; + + // Check if pointer text field is set due to previous object using this + // slot (ie. not correct for this one) + + // If 'pointer_text' field is set, but the 'id' field isn't same is + // current id then we don't want this "left over" pointer text + + if (_mouseList[_curMouse].pointer_text && _mouseList[_curMouse].id != (int32)_vm->_logic->readVar(ID)) + _mouseList[_curMouse].pointer_text = 0; + + // Get id from system variable 'id' which is correct for current object + _mouseList[_curMouse].id = _vm->_logic->readVar(ID); + + _curMouse++; +} + +void Mouse::registerPointerText(int32 text_id) { + assert(_curMouse < TOTAL_mouse_list); + + // current object id - used for checking pointer_text when mouse area + // registered (in fnRegisterMouse and fnRegisterFrame) + + _mouseList[_curMouse].id = _vm->_logic->readVar(ID); + _mouseList[_curMouse].pointer_text = text_id; +} + +/** + * This function is called every game cycle. + */ + +void Mouse::mouseEngine() { + monitorPlayerActivity(); + clearPointerText(); + + // If George is dead, the system menu is visible all the time, and is + // the only thing that can be used. + + if (_vm->_logic->readVar(DEAD)) { + if (_mouseMode != MOUSE_system_menu) { + _mouseMode = MOUSE_system_menu; + + if (_mouseTouching) { + _oldMouseTouching = 0; + _mouseTouching = 0; + } + + setMouse(NORMAL_MOUSE_ID); + buildSystemMenu(); + } + systemMenuMouse(); + return; + } + + // If the mouse is not visible, do nothing + + if (_mouseStatus) + return; + + switch (_mouseMode) { + case MOUSE_normal: + normalMouse(); + break; + case MOUSE_menu: + menuMouse(); + break; + case MOUSE_drag: + dragMouse(); + break; + case MOUSE_system_menu: + systemMenuMouse(); + break; + case MOUSE_holding: + if (_pos.y < 400) { + _mouseMode = MOUSE_normal; + debug(5, " releasing"); + } + break; + default: + break; + } +} + +#if RIGHT_CLICK_CLEARS_LUGGAGE +bool Mouse::heldIsInInventory() { + int32 object_held = (int32)_vm->_logic->readVar(OBJECT_HELD); + + for (uint i = 0; i < _totalMasters; i++) { + if (_masterMenuList[i].icon_resource == object_held) + return true; + } + return false; +} +#endif + +int Mouse::menuClick(int menu_items) { + if (_pos.x < RDMENU_ICONSTART) + return -1; + + if (_pos.x > RDMENU_ICONSTART + menu_items * (RDMENU_ICONWIDE + RDMENU_ICONSPACING) - RDMENU_ICONSPACING) + return -1; + + return (_pos.x - RDMENU_ICONSTART) / (RDMENU_ICONWIDE + RDMENU_ICONSPACING); +} + +void Mouse::systemMenuMouse() { + uint32 safe_looping_music_id; + MouseEvent *me; + int hit; + byte *icon; + int32 pars[2]; + uint32 icon_list[5] = { + OPTIONS_ICON, + QUIT_ICON, + SAVE_ICON, + RESTORE_ICON, + RESTART_ICON + }; + + // If the mouse is moved off the menu, close it. Unless the player is + // dead, in which case the menu should always be visible. + + if (_pos.y > 0 && !_vm->_logic->readVar(DEAD)) { + _mouseMode = MOUSE_normal; + hideMenu(RDMENU_TOP); + return; + } + + // Check if the user left-clicks anywhere in the menu area. + + me = _vm->mouseEvent(); + + if (!me || !(me->buttons & RD_LEFTBUTTONDOWN)) + return; + + if (_pos.y > 0) + return; + + hit = menuClick(ARRAYSIZE(icon_list)); + + if (hit < 0) + return; + + // No save when dead + + if (icon_list[hit] == SAVE_ICON && _vm->_logic->readVar(DEAD)) + return; + + // Gray out all he icons, except the one that was clicked + + for (int i = 0; i < ARRAYSIZE(icon_list); i++) { + if (i != hit) { + icon = _vm->_resman->openResource(icon_list[i]) + ResHeader::size(); + setMenuIcon(RDMENU_TOP, i, icon); + _vm->_resman->closeResource(icon_list[i]); + } + } + + _vm->_sound->pauseFx(); + + // NB. Need to keep a safe copy of '_loopingMusicId' for savegame & for + // playing when returning from control panels because control panel + // music will overwrite it! + + safe_looping_music_id = _vm->_sound->getLoopingMusicId(); + + pars[0] = 221; + pars[1] = FX_LOOP; + _vm->_logic->fnPlayMusic(pars); + + // HACK: Restore proper looping_music_id + _vm->_sound->setLoopingMusicId(safe_looping_music_id); + + processMenu(); + + // call the relevant screen + + switch (hit) { + case 0: + { + OptionsDialog dialog(_vm); + dialog.runModal(); + } + break; + case 1: + { + QuitDialog dialog(_vm); + dialog.runModal(); + } + break; + case 2: + { + SaveDialog dialog(_vm); + dialog.runModal(); + } + break; + case 3: + { + RestoreDialog dialog(_vm); + dialog.runModal(); + } + break; + case 4: + { + RestartDialog dialog(_vm); + dialog.runModal(); + } + break; + } + + // Menu stays open on death screen. Otherwise it's closed. + + if (!_vm->_logic->readVar(DEAD)) { + _mouseMode = MOUSE_normal; + hideMenu(RDMENU_TOP); + } else { + setMouse(NORMAL_MOUSE_ID); + buildSystemMenu(); + } + + // Back to the game again + + processMenu(); + + // Reset game palette, but not after a successful restore or restart! + // See RestoreFromBuffer() in save_rest.cpp + + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + if (screenInfo->new_palette != 99) { + // 0 means put back game screen palette; see build_display.cpp + _vm->_screen->setFullPalette(0); + + // Stop the engine fading in the restored screens palette + screenInfo->new_palette = 0; + } else + screenInfo->new_palette = 1; + + _vm->_sound->unpauseFx(); + + // If there was looping music before coming into the control panels + // then restart it! NB. If a game has been restored the music will be + // restarted twice, but this shouldn't cause any harm. + + if (_vm->_sound->getLoopingMusicId()) { + pars[0] = _vm->_sound->getLoopingMusicId(); + pars[1] = FX_LOOP; + _vm->_logic->fnPlayMusic(pars); + } else + _vm->_logic->fnStopMusic(NULL); +} + +void Mouse::dragMouse() { + byte buf1[NAME_LEN], buf2[NAME_LEN]; + MouseEvent *me; + int hit; + + // We can use dragged object both on other inventory objects, or on + // objects in the scene, so if the mouse moves off the inventory menu, + // then close it. + + if (_pos.y < 400) { + _mouseMode = MOUSE_normal; + hideMenu(RDMENU_BOTTOM); + return; + } + + // Handles cursors and the luggage on/off according to type + + mouseOnOff(); + + // Now do the normal click stuff + + me = _vm->mouseEvent(); + + if (!me) + return; + +#if RIGHT_CLICK_CLEARS_LUGGAGE + if ((me->buttons & RD_RIGHTBUTTONDOWN) && heldIsInInventory()) { + _vm->_logic->writeVar(OBJECT_HELD, 0); + _menuSelectedPos = 0; + _mouseMode = MOUSE_menu; + setLuggage(0); + buildMenu(); + return; + } +#endif + + if (!(me->buttons & RD_LEFTBUTTONDOWN)) + return; + + // there's a mouse event to be processed + + // could be clicking on an on screen object or on the menu + // which is currently displayed + + if (_mouseTouching) { + // mouse is over an on screen object - and we have luggage + + // Depending on type we'll maybe kill the object_held - like + // for exits + + // Set global script variable 'button'. We know that it was the + // left button, not the right one. + + _vm->_logic->writeVar(LEFT_BUTTON, 1); + _vm->_logic->writeVar(RIGHT_BUTTON, 0); + + // These might be required by the action script about to be run + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + _vm->_logic->writeVar(MOUSE_X, _pos.x + screenInfo->scroll_offset_x); + _vm->_logic->writeVar(MOUSE_Y, _pos.y + screenInfo->scroll_offset_y); + + // For scripts to know what's been clicked. First used for + // 'room_13_turning_script' in object 'biscuits_13' + + _vm->_logic->writeVar(CLICKED_ID, _mouseTouching); + + _vm->_logic->setPlayerActionEvent(CUR_PLAYER_ID, _mouseTouching); + + debug(2, "Used \"%s\" on \"%s\"", + _vm->_resman->fetchName(_vm->_logic->readVar(OBJECT_HELD), buf1), + _vm->_resman->fetchName(_vm->_logic->readVar(CLICKED_ID), buf2)); + + // Hide menu - back to normal menu mode + + hideMenu(RDMENU_BOTTOM); + _mouseMode = MOUSE_normal; + + return; + } + + // Better check for combine/cancel. Cancel puts us back in MOUSE_menu + // mode + + hit = menuClick(TOTAL_engine_pockets); + + if (hit < 0 || !_masterMenuList[hit].icon_resource) + return; + + // Always back into menu mode. Remove the luggage as well. + + _mouseMode = MOUSE_menu; + setLuggage(0); + + if ((uint)hit == _menuSelectedPos) { + // If we clicked on the same icon again, reset the first icon + + _vm->_logic->writeVar(OBJECT_HELD, 0); + _menuSelectedPos = 0; + } else { + // Otherwise, combine the two icons + + _vm->_logic->writeVar(COMBINE_BASE, _masterMenuList[hit].icon_resource); + _vm->_logic->setPlayerActionEvent(CUR_PLAYER_ID, MENU_MASTER_OBJECT); + + // Turn off mouse now, to prevent player trying to click + // elsewhere BUT leave the bottom menu open + + hideMouse(); + + debug(2, "Used \"%s\" on \"%s\"", + _vm->_resman->fetchName(_vm->_logic->readVar(OBJECT_HELD), buf1), + _vm->_resman->fetchName(_vm->_logic->readVar(COMBINE_BASE), buf2)); + } + + // Refresh the menu + + buildMenu(); +} + +void Mouse::menuMouse() { + byte buf[NAME_LEN]; + MouseEvent *me; + int hit; + + // If the mouse is moved off the menu, close it. + + if (_pos.y < 400) { + _mouseMode = MOUSE_normal; + hideMenu(RDMENU_BOTTOM); + return; + } + + me = _vm->mouseEvent(); + + if (!me) + return; + + hit = menuClick(TOTAL_engine_pockets); + + // Check if we clicked on an actual icon. + + if (hit < 0 || !_masterMenuList[hit].icon_resource) + return; + + if (me->buttons & RD_RIGHTBUTTONDOWN) { + // Right button - examine an object, identified by its icon + // resource id. + + _examiningMenuIcon = true; + _vm->_logic->writeVar(OBJECT_HELD, _masterMenuList[hit].icon_resource); + + // Must clear this so next click on exit becomes 1st click + // again + + _vm->_logic->writeVar(EXIT_CLICK_ID, 0); + + _vm->_logic->setPlayerActionEvent(CUR_PLAYER_ID, MENU_MASTER_OBJECT); + + // Refresh the menu + + buildMenu(); + + // Turn off mouse now, to prevent player trying to click + // elsewhere BUT leave the bottom menu open + + hideMouse(); + + debug(2, "Right-click on \"%s\" icon", + _vm->_resman->fetchName(_vm->_logic->readVar(OBJECT_HELD), buf)); + + return; + } + + if (me->buttons & RD_LEFTBUTTONDOWN) { + // Left button - bung us into drag luggage mode. The object is + // identified by its icon resource id. We need the luggage + // resource id for mouseOnOff + + _mouseMode = MOUSE_drag; + + _menuSelectedPos = hit; + _vm->_logic->writeVar(OBJECT_HELD, _masterMenuList[hit].icon_resource); + _currentLuggageResource = _masterMenuList[hit].luggage_resource; + + // Must clear this so next click on exit becomes 1st click + // again + + _vm->_logic->writeVar(EXIT_CLICK_ID, 0); + + // Refresh the menu + + buildMenu(); + + setLuggage(_masterMenuList[hit].luggage_resource); + + debug(2, "Left-clicked on \"%s\" icon - switch to drag mode", + _vm->_resman->fetchName(_vm->_logic->readVar(OBJECT_HELD), buf)); + } +} + +void Mouse::normalMouse() { + // The gane is playing and none of the menus are activated - but, we + // need to check if a menu is to start. Note, won't have luggage + + MouseEvent *me; + + // Check if the cursor has moved onto the system menu area. No save in + // big-object menu lock situation, of if the player is dragging an + // object. + + if (_pos.y < 0 && !_mouseModeLocked && !_vm->_logic->readVar(OBJECT_HELD)) { + _mouseMode = MOUSE_system_menu; + + if (_mouseTouching) { + // We were on something, but not anymore + _oldMouseTouching = 0; + _mouseTouching = 0; + } + + // Reset mouse cursor - in case we're between mice + + setMouse(NORMAL_MOUSE_ID); + buildSystemMenu(); + return; + } + + // Check if the cursor has moved onto the inventory menu area. No + // inventory in big-object menu lock situation, + + if (_pos.y > 399 && !_mouseModeLocked) { + // If an object is being held, i.e. if the mouse cursor has a + // luggage, go to drag mode instead of menu mode, but the menu + // is still opened. + // + // That way, we can still use an object on another inventory + // object, even if the inventory menu was closed after the + // first object was selected. + + if (!_vm->_logic->readVar(OBJECT_HELD)) + _mouseMode = MOUSE_menu; + else + _mouseMode = MOUSE_drag; + + // If mouse is moving off an object and onto the menu then do a + // standard get-off + + if (_mouseTouching) { + _oldMouseTouching = 0; + _mouseTouching = 0; + } + + // Reset mouse cursor + + setMouse(NORMAL_MOUSE_ID); + buildMenu(); + return; + } + + // Check for moving the mouse on or off things + + mouseOnOff(); + + me = _vm->mouseEvent(); + + if (!me) + return; + + bool button_down = (me->buttons & (RD_LEFTBUTTONDOWN | RD_RIGHTBUTTONDOWN)) != 0; + + // For debugging. We can draw a rectangle on the screen and see its + // coordinates. This was probably used to help defining hit areas. + + if (_vm->_debugger->_definingRectangles) { + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + if (_vm->_debugger->_draggingRectangle == 0) { + // Not yet dragging a rectangle, so need click to start + + if (button_down) { + // set both (x1,y1) and (x2,y2) to this point + _vm->_debugger->_rectX1 = _vm->_debugger->_rectX2 = (uint32)_pos.x + screenInfo->scroll_offset_x; + _vm->_debugger->_rectY1 = _vm->_debugger->_rectY2 = (uint32)_pos.y + screenInfo->scroll_offset_y; + _vm->_debugger->_draggingRectangle = 1; + } + } else if (_vm->_debugger->_draggingRectangle == 1) { + // currently dragging a rectangle - click means reset + + if (button_down) { + // lock rectangle, so you can let go of mouse + // to type in the coords + _vm->_debugger->_draggingRectangle = 2; + } else { + // drag rectangle + _vm->_debugger->_rectX2 = (uint32)_pos.x + screenInfo->scroll_offset_x; + _vm->_debugger->_rectY2 = (uint32)_pos.y + screenInfo->scroll_offset_y; + } + } else { + // currently locked to avoid knocking out of place + // while reading off the coords + + if (button_down) { + // click means reset - back to start again + _vm->_debugger->_draggingRectangle = 0; + } + } + + return; + } + +#if RIGHT_CLICK_CLEARS_LUGGAGE + if (_vm->_logic->readVar(OBJECT_HELD) && (me->buttons & RD_RIGHTBUTTONDOWN) && heldIsInInventory()) { + _vm->_logic->writeVar(OBJECT_HELD, 0); + _menuSelectedPos = 0; + setLuggage(0); + return; + } +#endif + + // Now do the normal click stuff + + // We only care about down clicks when the mouse is over an object. + + if (!_mouseTouching || !button_down) + return; + + // There's a mouse event to be processed and the mouse is on something. + // Notice that the floor itself is considered an object. + + // There are no menus about so its nice and simple. This is as close to + // the old advisor_188 script as we get, I'm sorry to say. + + // If player is walking or relaxing then those need to terminate + // correctly. Otherwise set player run the targets action script or, do + // a special walk if clicking on the scroll-more icon + + // PLAYER_ACTION script variable - whatever catches this must reset to + // 0 again + // _vm->_logic->writeVar(PLAYER_ACTION, _mouseTouching); + + // Idle or router-anim will catch it + + // Set global script variable 'button' + + if (me->buttons & RD_LEFTBUTTONDOWN) { + _vm->_logic->writeVar(LEFT_BUTTON, 1); + _vm->_logic->writeVar(RIGHT_BUTTON, 0); + _buttonClick = 0; // for re-click + } else { + _vm->_logic->writeVar(LEFT_BUTTON, 0); + _vm->_logic->writeVar(RIGHT_BUTTON, 1); + _buttonClick = 1; // for re-click + } + + // These might be required by the action script about to be run + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + _vm->_logic->writeVar(MOUSE_X, _pos.x + screenInfo->scroll_offset_x); + _vm->_logic->writeVar(MOUSE_Y, _pos.y + screenInfo->scroll_offset_y); + + if (_mouseTouching == _vm->_logic->readVar(EXIT_CLICK_ID) && (me->buttons & RD_LEFTBUTTONDOWN)) { + // It's the exit double click situation. Let the existing + // interaction continue and start fading down. Switch the human + // off too + + noHuman(); + _vm->_logic->fnFadeDown(NULL); + + // Tell the walker + + _vm->_logic->writeVar(EXIT_FADING, 1); + } else if (_oldButton == _buttonClick && _mouseTouching == _vm->_logic->readVar(CLICKED_ID) && _mousePointerRes != NORMAL_MOUSE_ID) { + // Re-click. Do nothing, except on floors + } else { + // For re-click + + _oldButton = _buttonClick; + + // For scripts to know what's been clicked. First used for + // 'room_13_turning_script' in object 'biscuits_13' + + _vm->_logic->writeVar(CLICKED_ID, _mouseTouching); + + // Must clear these two double-click control flags - do it here + // so reclicks after exit clicks are cleared up + + _vm->_logic->writeVar(EXIT_CLICK_ID, 0); + _vm->_logic->writeVar(EXIT_FADING, 0); + + _vm->_logic->setPlayerActionEvent(CUR_PLAYER_ID, _mouseTouching); + + byte buf1[NAME_LEN], buf2[NAME_LEN]; + + if (_vm->_logic->readVar(OBJECT_HELD)) + debug(2, "Used \"%s\" on \"%s\"", + _vm->_resman->fetchName(_vm->_logic->readVar(OBJECT_HELD), buf1), + _vm->_resman->fetchName(_vm->_logic->readVar(CLICKED_ID), buf2)); + else if (_vm->_logic->readVar(LEFT_BUTTON)) + debug(2, "Left-clicked on \"%s\"", + _vm->_resman->fetchName(_vm->_logic->readVar(CLICKED_ID), buf1)); + else // RIGHT BUTTON + debug(2, "Right-clicked on \"%s\"", + _vm->_resman->fetchName(_vm->_logic->readVar(CLICKED_ID), buf1)); + } +} + +uint32 Mouse::chooseMouse() { + // Unlike the other mouse "engines", this one is called directly by the + // fnChoose() opcode. + + uint i; + + _vm->_logic->writeVar(AUTO_SELECTED, 0); + + uint32 in_subject = _vm->_logic->readVar(IN_SUBJECT); + uint32 object_held = _vm->_logic->readVar(OBJECT_HELD); + + if (object_held) { + // The player used an object on a person. In this case it + // triggered a conversation menu. Act as if the user tried to + // talk to the person about that object. If the person doesn't + // know anything about it, use the default response. + + uint32 response = _defaultResponseId; + + for (i = 0; i < in_subject; i++) { + if (_subjectList[i].res == object_held) { + response = _subjectList[i].ref; + break; + } + } + + // The user won't be holding the object any more, and the + // conversation menu will be closed. + + _vm->_logic->writeVar(OBJECT_HELD, 0); + _vm->_logic->writeVar(IN_SUBJECT, 0); + return response; + } + + if (_vm->_logic->readVar(CHOOSER_COUNT_FLAG) == 0 && in_subject == 1 && _subjectList[0].res == EXIT_ICON) { + // This is the first time the chooser is coming up in this + // conversation, there is only one subject and that's the + // EXIT icon. + // + // In other words, the player doesn't have anything to talk + // about. Skip it. + + // The conversation menu will be closed. We set AUTO_SELECTED + // because the speech script depends on it. + + _vm->_logic->writeVar(AUTO_SELECTED, 1); + _vm->_logic->writeVar(IN_SUBJECT, 0); + return _subjectList[0].ref; + } + + byte *icon; + + if (!_choosing) { + // This is a new conversation menu. + + if (!in_subject) + error("fnChoose with no subjects"); + + for (i = 0; i < in_subject; i++) { + icon = _vm->_resman->openResource(_subjectList[i].res) + ResHeader::size() + RDMENU_ICONWIDE * RDMENU_ICONDEEP; + setMenuIcon(RDMENU_BOTTOM, i, icon); + _vm->_resman->closeResource(_subjectList[i].res); + } + + for (; i < 15; i++) + setMenuIcon(RDMENU_BOTTOM, (uint8) i, NULL); + + showMenu(RDMENU_BOTTOM); + setMouse(NORMAL_MOUSE_ID); + _choosing = true; + return (uint32)-1; + } + + // The menu is there - we're just waiting for a click. We only care + // about left clicks. + + MouseEvent *me = _vm->mouseEvent(); + int mouseX, mouseY; + + getPos(mouseX, mouseY); + + if (!me || !(me->buttons & RD_LEFTBUTTONDOWN) || mouseY < 400) + return (uint32)-1; + + // Check for click on a menu. + + int hit = _vm->_mouse->menuClick(in_subject); + if (hit < 0) + return (uint32)-1; + + // Hilight the clicked icon by greying the others. This can look a bit + // odd when you click on the exit icon, but there are also cases when + // it looks strange if you don't do it. + + for (i = 0; i < in_subject; i++) { + if ((int)i != hit) { + icon = _vm->_resman->openResource(_subjectList[i].res) + ResHeader::size(); + _vm->_mouse->setMenuIcon(RDMENU_BOTTOM, i, icon); + _vm->_resman->closeResource(_subjectList[i].res); + } + } + + // For non-speech scripts that manually call the chooser + _vm->_logic->writeVar(RESULT, _subjectList[hit].res); + + // The conversation menu will be closed + + _choosing = false; + _vm->_logic->writeVar(IN_SUBJECT, 0); + setMouse(0); + + return _subjectList[hit].ref; +} + +void Mouse::mouseOnOff() { + // this handles the cursor graphic when moving on and off mouse areas + // it also handles the luggage thingy + + uint32 pointer_type; + static uint8 mouse_flicked_off = 0; + + _oldMouseTouching = _mouseTouching; + + // don't detect objects that are hidden behind the menu bars (ie. in + // the scrolled-off areas of the screen) + + if (_pos.y < 0 || _pos.y > 399) { + pointer_type = 0; + _mouseTouching = 0; + } else { + // set '_mouseTouching' & return pointer_type + pointer_type = checkMouseList(); + } + + // same as previous cycle? + if (!mouse_flicked_off && _oldMouseTouching == _mouseTouching) { + // yes, so nothing to do + // BUT CARRY ON IF MOUSE WAS FLICKED OFF! + return; + } + + // can reset this now + mouse_flicked_off = 0; + + //the cursor has moved onto something + if (!_oldMouseTouching && _mouseTouching) { + // make a copy of the object we've moved onto because one day + // we'll move back off again! (but the list positioning could + // theoretically have changed) + + // we can only move onto something from being on nothing - we + // stop the system going from one to another when objects + // overlap + + _oldMouseTouching = _mouseTouching; + + // run get on + + if (pointer_type) { + // 'pointer_type' holds the resource id of the + // pointer anim + + setMouse(pointer_type); + + // setup luggage icon + if (_vm->_logic->readVar(OBJECT_HELD)) { + setLuggage(_currentLuggageResource); + } + } else { + byte buf[NAME_LEN]; + + error("ERROR: mouse.pointer==0 for object %d (%s) - update logic script!", _mouseTouching, _vm->_resman->fetchName(_mouseTouching, buf)); + } + } else if (_oldMouseTouching && !_mouseTouching) { + // the cursor has moved off something - reset cursor to + // normal pointer + + _oldMouseTouching = 0; + setMouse(NORMAL_MOUSE_ID); + + // reset luggage only when necessary + } else if (_oldMouseTouching && _mouseTouching) { + // The cursor has moved off something and onto something + // else. Flip to a blank cursor for a cycle. + + // ignore the new id this cycle - should hit next cycle + _mouseTouching = 0; + _oldMouseTouching = 0; + setMouse(0); + + // so we know to set the mouse pointer back to normal if 2nd + // hot-spot doesn't register because mouse pulled away + // quickly (onto nothing) + + mouse_flicked_off = 1; + + // reset luggage only when necessary + } else { + // Mouse was flicked off for one cycle, but then moved onto + // nothing before 2nd hot-spot registered + + // both '_oldMouseTouching' & '_mouseTouching' will be zero + // reset cursor to normal pointer + + setMouse(NORMAL_MOUSE_ID); + } + + // possible check for edge of screen more-to-scroll here on large + // screens +} + +void Mouse::setMouse(uint32 res) { + // high level - whats the mouse - for the engine + _mousePointerRes = res; + + if (res) { + byte *icon = _vm->_resman->openResource(res) + ResHeader::size(); + uint32 len = _vm->_resman->fetchLen(res) - ResHeader::size(); + + // don't pulse the normal pointer - just do the regular anim + // loop + + if (res == NORMAL_MOUSE_ID) + setMouseAnim(icon, len, RDMOUSE_NOFLASH); + else + setMouseAnim(icon, len, RDMOUSE_FLASH); + + _vm->_resman->closeResource(res); + } else { + // blank cursor + setMouseAnim(NULL, 0, 0); + } +} + +void Mouse::setLuggage(uint32 res) { + _realLuggageItem = res; + + if (res) { + byte *icon = _vm->_resman->openResource(res) + ResHeader::size(); + uint32 len = _vm->_resman->fetchLen(res) - ResHeader::size(); + + setLuggageAnim(icon, len); + _vm->_resman->closeResource(res); + } else + setLuggageAnim(NULL, 0); +} + +void Mouse::setObjectHeld(uint32 res) { + setLuggage(res); + + _vm->_logic->writeVar(OBJECT_HELD, res); + _currentLuggageResource = res; + + // mode locked - no menu available + _mouseModeLocked = true; +} + +uint32 Mouse::checkMouseList() { + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + Common::Point mousePos(_pos.x + screenInfo->scroll_offset_x, _pos.y + screenInfo->scroll_offset_y); + + // Number of priorities subject to implementation needs + for (int priority = 0; priority < 10; priority++) { + for (uint i = 0; i < _curMouse; i++) { + // If the mouse pointer is over this + // mouse-detection-box + + if (_mouseList[i].priority == priority && _mouseList[i].rect.contains(mousePos)) { + // Record id + _mouseTouching = _mouseList[i].id; + + createPointerText(_mouseList[i].pointer_text, _mouseList[i].pointer); + + // Return pointer type + return _mouseList[i].pointer; + } + } + } + + // Touching nothing; no pointer to return + _mouseTouching = 0; + return 0; +} + +#define POINTER_TEXT_WIDTH 640 // just in case! +#define POINTER_TEXT_PEN 184 // white + +void Mouse::createPointerText(uint32 text_id, uint32 pointer_res) { + uint32 local_text; + uint32 text_res; + byte *text; + // offsets for pointer text sprite from pointer position + int16 xOffset, yOffset; + uint8 justification; + + if (!_objectLabels || !text_id) + return; + + // Check what the pointer is, to set offsets correctly for text + // position + + switch (pointer_res) { + case CROSHAIR: + yOffset = -7; + xOffset = +10; + break; + case EXIT0: + yOffset = +15; + xOffset = +20; + break; + case EXIT1: + yOffset = +16; + xOffset = -10; + break; + case EXIT2: + yOffset = +10; + xOffset = -22; + break; + case EXIT3: + yOffset = -16; + xOffset = -10; + break; + case EXIT4: + yOffset = -15; + xOffset = +15; + break; + case EXIT5: + yOffset = -12; + xOffset = +10; + break; + case EXIT6: + yOffset = +10; + xOffset = +25; + break; + case EXIT7: + yOffset = +16; + xOffset = +20; + break; + case EXITDOWN: + yOffset = -20; + xOffset = -10; + break; + case EXITUP: + yOffset = +20; + xOffset = +20; + break; + case MOUTH: + yOffset = -10; + xOffset = +15; + break; + case NORMAL: + yOffset = -10; + xOffset = +15; + break; + case PICKUP: + yOffset = -40; + xOffset = +10; + break; + case SCROLL_L: + yOffset = -20; + xOffset = +20; + break; + case SCROLL_R: + yOffset = -20; + xOffset = -20; + break; + case USE: + yOffset = -8; + xOffset = +20; + break; + default: + // Shouldn't happen if we cover all the different mouse + // pointers above + yOffset = -10; + xOffset = +10; + break; + } + + // Set up justification for text sprite, based on its offsets from the + // pointer position + + if (yOffset < 0) { + // Above pointer + if (xOffset < 0) { + // Above left + justification = POSITION_AT_RIGHT_OF_BASE; + } else if (xOffset > 0) { + // Above right + justification = POSITION_AT_LEFT_OF_BASE; + } else { + // Above centre + justification = POSITION_AT_CENTRE_OF_BASE; + } + } else if (yOffset > 0) { + // Below pointer + if (xOffset < 0) { + // Below left + justification = POSITION_AT_RIGHT_OF_TOP; + } else if (xOffset > 0) { + // Below right + justification = POSITION_AT_LEFT_OF_TOP; + } else { + // Below centre + justification = POSITION_AT_CENTRE_OF_TOP; + } + } else { + // Same y-coord as pointer + if (xOffset < 0) { + // Centre left + justification = POSITION_AT_RIGHT_OF_CENTRE; + } else if (xOffset > 0) { + // Centre right + justification = POSITION_AT_LEFT_OF_CENTRE; + } else { + // Centre centre - shouldn't happen anyway! + justification = POSITION_AT_LEFT_OF_CENTRE; + } + } + + // Text resource number, and line number within the resource + + text_res = text_id / SIZE; + local_text = text_id & 0xffff; + + // open text file & get the line + text = _vm->fetchTextLine(_vm->_resman->openResource(text_res), local_text); + + // 'text+2' to skip the first 2 bytes which form the + // line reference number + + _pointerTextBlocNo = _vm->_fontRenderer->buildNewBloc( + text + 2, _pos.x + xOffset, + _pos.y + yOffset, + POINTER_TEXT_WIDTH, POINTER_TEXT_PEN, + RDSPR_TRANS | RDSPR_DISPLAYALIGN, + _vm->_speechFontId, justification); + + // now ok to close the text file + _vm->_resman->closeResource(text_res); +} + +void Mouse::clearPointerText() { + if (_pointerTextBlocNo) { + _vm->_fontRenderer->killTextBloc(_pointerTextBlocNo); + _pointerTextBlocNo = 0; + } +} + +void Mouse::hideMouse() { + // leaves the menus open + // used by the system when clicking right on a menu item to examine + // it and when combining objects + + // for logic scripts + _vm->_logic->writeVar(MOUSE_AVAILABLE, 0); + + // human/mouse off + _mouseStatus = true; + + setMouse(0); + setLuggage(0); +} + +void Mouse::noHuman() { + hideMouse(); + clearPointerText(); + + // Must be normal mouse situation or a largely neutral situation - + // special menus use hideMouse() + + // Don't hide menu in conversations + if (_vm->_logic->readVar(TALK_FLAG) == 0) + hideMenu(RDMENU_BOTTOM); + + if (_mouseMode == MOUSE_system_menu) { + // Close menu + _mouseMode = MOUSE_normal; + hideMenu(RDMENU_TOP); + } +} + +void Mouse::addHuman() { + // For logic scripts + _vm->_logic->writeVar(MOUSE_AVAILABLE, 1); + + if (_mouseStatus) { + // Force engine to choose a cursor + _mouseStatus = false; + _mouseTouching = 1; + } + + // Clear this to reset no-second-click system + _vm->_logic->writeVar(CLICKED_ID, 0); + + // This is now done outside the OBJECT_HELD check in case it's set to + // zero before now! + + // Unlock the mouse from possible large object lock situtations - see + // syphon in rm 3 + + _mouseModeLocked = false; + + if (_vm->_logic->readVar(OBJECT_HELD)) { + // Was dragging something around - need to clear this again + _vm->_logic->writeVar(OBJECT_HELD, 0); + + // And these may also need clearing, just in case + _examiningMenuIcon = false; + _vm->_logic->writeVar(COMBINE_BASE, 0); + + setLuggage(0); + } + + // If mouse is over menu area + if (_pos.y > 399) { + if (_mouseMode != MOUSE_holding) { + // VITAL - reset things & rebuild the menu + _mouseMode = MOUSE_normal; + } + setMouse(NORMAL_MOUSE_ID); + } + + // Enabled/disabled from console; status printed with on-screen debug + // info + + if (_vm->_debugger->_testingSnR) { + uint8 black[4] = { 0, 0, 0, 0 }; + uint8 white[4] = { 255, 255, 255, 0 }; + + // Testing logic scripts by simulating instant Save & Restore + + _vm->_screen->setPalette(0, 1, white, RDPAL_INSTANT); + + // Stops all fx & clears the queue - eg. when leaving a room + _vm->_sound->clearFxQueue(); + + // Trash all object resources so they load in fresh & restart + // their logic scripts + + _vm->_resman->killAllObjects(false); + + _vm->_screen->setPalette(0, 1, black, RDPAL_INSTANT); + } +} + +void Mouse::refreshInventory() { + // Can reset this now + _vm->_logic->writeVar(COMBINE_BASE, 0); + + // Cause 'object_held' icon to be greyed. The rest are coloured. + _examiningMenuIcon = true; + buildMenu(); + _examiningMenuIcon = false; +} + +void Mouse::startConversation() { + if (_vm->_logic->readVar(TALK_FLAG) == 0) { + // See fnChooser & speech scripts + _vm->_logic->writeVar(CHOOSER_COUNT_FLAG, 0); + } + + noHuman(); +} + +void Mouse::endConversation() { + hideMenu(RDMENU_BOTTOM); + + if (_pos.y > 399) { + // Will wait for cursor to move off the bottom menu + _mouseMode = MOUSE_holding; + } + + // In case DC forgets + _vm->_logic->writeVar(TALK_FLAG, 0); +} + +void Mouse::monitorPlayerActivity() { + // if there is at least one mouse event outstanding + if (_vm->checkForMouseEvents()) { + // reset activity delay counter + _playerActivityDelay = 0; + } else { + // no. of game cycles since mouse event queue last empty + _playerActivityDelay++; + } +} + +void Mouse::checkPlayerActivity(uint32 seconds) { + // Convert seconds to game cycles + uint32 threshold = seconds * 12; + + // If the actual delay is at or above the given threshold, reset the + // activity delay counter now that we've got a positive check. + + if (_playerActivityDelay >= threshold) { + _playerActivityDelay = 0; + _vm->_logic->writeVar(RESULT, 1); + } else + _vm->_logic->writeVar(RESULT, 0); +} + +void Mouse::pauseGame() { + // Make the mouse cursor normal. This is the only place where we are + // allowed to clear the luggage this way. + + clearPointerText(); + setLuggageAnim(NULL, 0); + setMouse(0); + setMouseTouching(1); +} + +void Mouse::unpauseGame() { + if (_vm->_logic->readVar(OBJECT_HELD) && _realLuggageItem) + setLuggage(_realLuggageItem); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/mouse.h b/engines/sword2/mouse.h new file mode 100644 index 0000000000..bed5e032ba --- /dev/null +++ b/engines/sword2/mouse.h @@ -0,0 +1,257 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef MOUSE_H +#define MOUSE_H + +#define MAX_SUBJECT_LIST 30 // is that enough? + +#define TOTAL_mouse_list 50 + +namespace Sword2 { + +struct BuildUnit; + +// Menubar defines. + +#define RDMENU_TOP 0 +#define RDMENU_BOTTOM 1 + +enum { + MOUSE_normal = 0, // normal in game + MOUSE_menu = 1, // menu chooser + MOUSE_drag = 2, // dragging luggage + MOUSE_system_menu = 3, // system menu chooser + MOUSE_holding = 4 // special +}; + +enum { + RDMOUSE_NOFLASH, + RDMOUSE_FLASH +}; + +enum { + RDMENU_HIDDEN, + RDMENU_SHOWN, + RDMENU_OPENING, + RDMENU_CLOSING, + RDMENU_ALMOST_HIDDEN +}; + +#define RDMENU_ICONWIDE 35 +#define RDMENU_ICONDEEP 30 +#define RDMENU_ICONSTART 24 +#define RDMENU_ICONSPACING 5 +#define RDMENU_MAXPOCKETS 15 + +#define MOUSE_ANIM_HEADER_SIZE 6 + +struct MouseAnim { + uint8 runTimeComp; // type of runtime compression used for the + // frame data + uint8 noAnimFrames; // number of frames in the anim + int8 xHotSpot; + int8 yHotSpot; + uint8 mousew; + uint8 mouseh; + + byte *data; +}; + +// The MOUSE_holding mode is entered when the conversation menu is closed, and +// exited when the mouse cursor moves off that menu area. I don't know why yet. + +// mouse unit - like ObjectMouse, but with anim resource & pc (needed if +// sprite is to act as mouse detection mask) + +struct MouseUnit { + // Basically the same information as in ObjectMouse, except the + // coordinates are adjusted to conform to standard ScummVM usage. + + Common::Rect rect; + int32 priority; + int32 pointer; + + // In addition, we need an id when checking the mouse list, and a + // text id for mouse-overs. + + int32 id; + int32 pointer_text; +}; + +// Array of these for subject menu build up + + struct SubjectUnit { + uint32 res; + uint32 ref; +}; + +class Mouse { +private: + Sword2Engine *_vm; + + Common::Point _pos; + + MouseUnit _mouseList[TOTAL_mouse_list]; + uint32 _curMouse; + + MenuObject _tempList[TOTAL_engine_pockets]; + uint32 _totalTemp; + + MenuObject _masterMenuList[TOTAL_engine_pockets]; + uint32 _totalMasters; + + SubjectUnit _subjectList[MAX_SUBJECT_LIST]; + + // ref number for default response when luggage icon is used on a + // person & it doesn't match any of the icons which would have been in + // the chooser + + uint32 _defaultResponseId; + + // could alternately use logic->looping of course + bool _choosing; + + uint8 _menuStatus[2]; + byte *_icons[2][RDMENU_MAXPOCKETS]; + uint8 _pocketStatus[2][RDMENU_MAXPOCKETS]; + + uint8 _iconCount; + + // If it's NORMAL_MOUSE_ID (ie. normal pointer) then it's over a floor + // area (or hidden hot-zone) + + uint32 _mousePointerRes; + + MouseAnim _mouseAnim; + MouseAnim _luggageAnim; + + uint8 _mouseFrame; + + uint32 _mouseMode; + + bool _mouseStatus; // Human 0 on/1 off + bool _mouseModeLocked; // 0 not !0 mode cannot be changed from + // normal mouse to top menu (i.e. when + // carrying big objects) + uint32 _realLuggageItem; // Last minute for pause mode + uint32 _currentLuggageResource; + uint32 _oldButton; // For the re-click stuff - must be + // the same button you see + uint32 _buttonClick; + uint32 _pointerTextBlocNo; + uint32 _playerActivityDelay; // Player activity delay counter + + bool _examiningMenuIcon; + + // Set by checkMouseList() + uint32 _mouseTouching; + uint32 _oldMouseTouching; + + bool _objectLabels; + + uint32 _menuSelectedPos; + + void decompressMouse(byte *decomp, byte *comp, uint8 frame, int width, int height, int pitch, int xOff = 0, int yOff = 0); + + int32 setMouseAnim(byte *ma, int32 size, int32 mouseFlash); + int32 setLuggageAnim(byte *la, int32 size); + + void clearIconArea(int menu, int pocket, Common::Rect *r); + +public: + Mouse(Sword2Engine *vm); + ~Mouse(); + + void getPos(int &x, int &y); + void setPos(int x, int y); + + bool getObjectLabels() { return _objectLabels; } + void setObjectLabels(bool b) { _objectLabels = b; } + + bool getMouseStatus() { return _mouseStatus; } + uint32 getMouseTouching() { return _mouseTouching; } + void setMouseTouching(uint32 touching) { _mouseTouching = touching; } + + void pauseGame(); + void unpauseGame(); + + void setMouse(uint32 res); + void setLuggage(uint32 res); + + void setObjectHeld(uint32 res); + + void resetMouseList(); + + void registerMouse(byte *ob_mouse, BuildUnit *build_unit); + void registerPointerText(int32 text_id); + + void createPointerText(uint32 text_id, uint32 pointer_res); + void clearPointerText(); + + void drawMouse(); + int32 animateMouse(); + + void processMenu(); + + void addMenuObject(byte *ptr); + void addSubject(int32 id, int32 ref); + + void buildMenu(); + void buildSystemMenu(); + + int32 showMenu(uint8 menu); + int32 hideMenu(uint8 menu); + int32 setMenuIcon(uint8 menu, uint8 pocket, byte *icon); + + void closeMenuImmediately(); + + void refreshInventory(); + + void startConversation(); + void endConversation(); + + void hideMouse(); + void noHuman(); + void addHuman(); + + void resetPlayerActivityDelay() { _playerActivityDelay = 0; } + void monitorPlayerActivity(); + void checkPlayerActivity(uint32 seconds); + + void mouseOnOff(); + uint32 checkMouseList(); + void mouseEngine(); + + void normalMouse(); + void menuMouse(); + void dragMouse(); + void systemMenuMouse(); + + bool isChoosing() { return _choosing; } + uint32 chooseMouse(); + + int menuClick(int menu_items); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/music.cpp b/engines/sword2/music.cpp new file mode 100644 index 0000000000..847f4808cf --- /dev/null +++ b/engines/sword2/music.cpp @@ -0,0 +1,856 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// One feature still missing is the original's DipMusic() function which, as +// far as I can understand, softened the music volume when someone was +// speaking, but only (?) if the music was playing loudly at the time. +// +// All things considered, I think this is more bother than it's worth. + +#include "common/stdafx.h" +#include "common/file.h" +#include "common/system.h" +#include "sound/mp3.h" +#include "sound/vorbis.h" +#include "sound/flac.h" +#include "sound/rate.h" +#include "sound/wave.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +namespace Sword2 { + +static AudioStream *makeCLUStream(Common::File *fp, int size); + +static AudioStream *getAudioStream(SoundFileHandle *fh, const char *base, int cd, uint32 id, uint32 *numSamples) { + debug(3, "Playing %s from CD %d", base, cd); + + if (!fh->file.isOpen()) { + struct { + const char *ext; + int mode; + } file_types[] = { + #ifdef USE_MAD + { "cl3", kMP3Mode }, + #endif + #ifdef USE_VORBIS + { "clg", kVorbisMode }, + #endif + #ifdef USE_FLAC + { "clf", kFlacMode }, + #endif + { "clu", kCLUMode } + }; + + int soundMode = 0; + char filename[20]; + + for (int i = 0; i < ARRAYSIZE(file_types); i++) { + Common::File f; + + sprintf(filename, "%s%d.%s", base, cd, file_types[i].ext); + if (f.open(filename)) { + soundMode = file_types[i].mode; + break; + } + + sprintf(filename, "%s.%s", base, file_types[i].ext); + if (f.open(filename)) { + soundMode = file_types[i].mode; + break; + } + } + + if (soundMode == 0) + return NULL; + + fh->file.open(filename); + fh->fileType = soundMode; + if (!fh->file.isOpen()) { + warning("Very strange fopen error"); + return NULL; + } + if (fh->fileSize != fh->file.size()) { + if (fh->idxTab) { + free(fh->idxTab); + fh->idxTab = NULL; + } + } + } + + uint32 entrySize = (fh->fileType == kCLUMode) ? 2 : 3; + + if (!fh->idxTab) { + fh->file.seek(0); + fh->idxLen = fh->file.readUint32LE(); + fh->file.seek(entrySize * 4); + + fh->idxTab = (uint32*)malloc(fh->idxLen * 3 * sizeof(uint32)); + for (uint32 cnt = 0; cnt < fh->idxLen; cnt++) { + fh->idxTab[cnt * 3 + 0] = fh->file.readUint32LE(); + fh->idxTab[cnt * 3 + 1] = fh->file.readUint32LE(); + if (fh->fileType == kCLUMode) { + fh->idxTab[cnt * 3 + 2] = fh->idxTab[cnt * 3 + 1]; + fh->idxTab[cnt * 3 + 1]--; + } else + fh->idxTab[cnt * 3 + 2] = fh->file.readUint32LE(); + } + } + + uint32 pos = fh->idxTab[id * 3 + 0]; + uint32 len = fh->idxTab[id * 3 + 1]; + uint32 enc_len = fh->idxTab[id * 3 + 2]; + + if (numSamples) + *numSamples = len; + + if (!pos || !len) { + fh->file.close(); + return NULL; + } + + fh->file.seek(pos, SEEK_SET); + + switch (fh->fileType) { + case kCLUMode: + return makeCLUStream(&fh->file, enc_len); +#ifdef USE_MAD + case kMP3Mode: + return makeMP3Stream(&fh->file, enc_len); +#endif +#ifdef USE_VORBIS + case kVorbisMode: + return makeVorbisStream(&fh->file, enc_len); +#endif +#ifdef USE_FLAC + case kFlacMode: + return makeFlacStream(&fh->file, enc_len); +#endif + default: + return NULL; + } +} + +// ---------------------------------------------------------------------------- +// Custom AudioStream class to handle Broken Sword 2's audio compression. +// ---------------------------------------------------------------------------- + +#define GetCompressedShift(n) ((n) >> 4) +#define GetCompressedSign(n) (((n) >> 3) & 1) +#define GetCompressedAmplitude(n) ((n) & 7) + +CLUInputStream::CLUInputStream(Common::File *file, int size) + : _file(file), _firstTime(true), _bufferEnd(_outbuf + BUFFER_SIZE) { + + _file->incRef(); + + // Determine the end position. + _file_pos = _file->pos(); + _end_pos = _file_pos + size; + + // Read in initial data + refill(); +} + +CLUInputStream::~CLUInputStream() { + _file->decRef(); +} + +int CLUInputStream::readBuffer(int16 *buffer, const int numSamples) { + int samples = 0; + while (samples < numSamples && !eosIntern()) { + const int len = MIN(numSamples - samples, (int)(_bufferEnd - _pos)); + memcpy(buffer, _pos, len * 2); + buffer += len; + _pos += len; + samples += len; + if (_pos >= _bufferEnd) { + refill(); + } + } + return samples; +} + +void CLUInputStream::refill() { + byte *in = _inbuf; + int16 *out = _outbuf; + + _file->seek(_file_pos, SEEK_SET); + + uint len_left = _file->read(in, MIN((uint32)BUFFER_SIZE, _end_pos - _file->pos())); + + _file_pos = _file->pos(); + + while (len_left > 0) { + uint16 sample; + + if (_firstTime) { + _firstTime = false; + _prev = READ_LE_UINT16(in); + sample = _prev; + len_left -= 2; + in += 2; + } else { + uint16 delta = GetCompressedAmplitude(*in) << GetCompressedShift(*in); + if (GetCompressedSign(*in)) + sample = _prev - delta; + else + sample = _prev + delta; + + _prev = sample; + len_left--; + in++; + } + + *out++ = sample; + } + + _pos = _outbuf; + _bufferEnd = out; +} + +AudioStream *makeCLUStream(Common::File *file, int size) { + return new CLUInputStream(file, size); +} + +// ---------------------------------------------------------------------------- +// Another custom AudioStream class, to wrap around the various AudioStream +// classes used for music decompression, and to add looping, fading, etc. +// ---------------------------------------------------------------------------- + +// The length of a fade-in/out, in milliseconds. +#define FADE_LENGTH 3000 + +MusicInputStream::MusicInputStream(int cd, SoundFileHandle *fh, uint32 musicId, bool looping) { + _cd = cd; + _fh = fh; + _musicId = musicId; + _looping = looping; + + _bufferEnd = _buffer + BUFFER_SIZE; + _remove = false; + _fading = 0; + + _decoder = getAudioStream(_fh, "music", _cd, _musicId, &_numSamples); + if (_decoder) { + _samplesLeft = _numSamples; + _fadeSamples = (getRate() * FADE_LENGTH) / 1000; + fadeUp(); + + // Read in initial data + refill(); + } +} + +MusicInputStream::~MusicInputStream() { + delete _decoder; +} + +int MusicInputStream::readBuffer(int16 *buffer, const int numSamples) { + if (!_decoder) + return 0; + + int samples = 0; + while (samples < numSamples && !eosIntern()) { + const int len = MIN(numSamples - samples, (int)(_bufferEnd - _pos)); + memcpy(buffer, _pos, len * 2); + buffer += len; + _pos += len; + samples += len; + if (_pos >= _bufferEnd) { + refill(); + } + } + return samples; +} + +void MusicInputStream::refill() { + int16 *buf = _buffer; + uint32 numSamples = 0; + uint32 len_left; + bool endFade = false; + + len_left = BUFFER_SIZE; + + if (_fading > 0 && (uint32)_fading < len_left) + len_left = _fading; + + if (_samplesLeft < len_left) + len_left = _samplesLeft; + + if (!_looping) { + // Non-looping music is faded out at the end. If this fade + // out would have started somewhere within the len_left samples + // to read, we only read up to that point. This way, we can + // treat this fade as any other. + + if (!_fading) { + uint32 currentlyAt = _numSamples - _samplesLeft; + uint32 fadeOutAt = _numSamples - _fadeSamples; + uint32 readTo = currentlyAt + len_left; + + if (fadeOutAt == currentlyAt) + fadeDown(); + else if (fadeOutAt > currentlyAt && fadeOutAt <= readTo) { + len_left = fadeOutAt - currentlyAt; + endFade = true; + } + } + } + + int desired = len_left - numSamples; + int len = _decoder->readBuffer(buf, desired); + + // Shouldn't happen, but if it does it could cause an infinite loop. + // Of course there were bugs that caused it to happen several times + // during development. :-) + + if (len < desired) { + warning("Expected %d samples, but got %d", desired, len); + _samplesLeft = len; + } + + buf += len; + numSamples += len; + len_left -= len; + _samplesLeft -= len; + + int16 *ptr; + + if (_fading > 0) { + // Fade down + for (ptr = _buffer; ptr < buf; ptr++) { + if (_fading > 0) { + _fading--; + *ptr = (*ptr * _fading) / _fadeSamples; + } + if (_fading == 0) { + _looping = false; + _remove = true; + *ptr = 0; + } + } + } else if (_fading < 0) { + // Fade up + for (ptr = _buffer; ptr < buf; ptr++) { + _fading--; + *ptr = -(*ptr * _fading) / _fadeSamples; + if (_fading <= -_fadeSamples) { + _fading = 0; + break; + } + } + } + + if (endFade) + fadeDown(); + + if (!_samplesLeft) { + if (_looping) { + delete _decoder; + _decoder = getAudioStream(_fh, "music", _cd, _musicId, &_numSamples); + _samplesLeft = _numSamples; + } else + _remove = true; + } + + _pos = _buffer; + _bufferEnd = buf; +} + +void MusicInputStream::fadeUp() { + if (_fading > 0) + _fading = -_fading; + else if (_fading == 0) + _fading = -1; +} + +void MusicInputStream::fadeDown() { + if (_fading < 0) + _fading = -_fading; + else if (_fading == 0) + _fading = _fadeSamples; +} + +bool MusicInputStream::readyToRemove() { + return _remove; +} + +int32 MusicInputStream::getTimeRemaining() { + // This is far from exact, but it doesn't have to be. + return (_samplesLeft + BUFFER_SIZE) / getRate(); +} + +// ---------------------------------------------------------------------------- +// Main sound class +// ---------------------------------------------------------------------------- + +// AudioStream API + +int Sound::readBuffer(int16 *buffer, const int numSamples) { + Common::StackLock lock(_mutex); + int i; + + if (_musicPaused) + return 0; + + for (i = 0; i < MAXMUS; i++) { + if (_music[i] && _music[i]->readyToRemove()) { + delete _music[i]; + _music[i] = NULL; + } + } + + memset(buffer, 0, 2 * numSamples); + + if (!_mixBuffer || numSamples > _mixBufferLen) { + if (_mixBuffer) + _mixBuffer = (int16 *)realloc(_mixBuffer, 2 * numSamples); + else + _mixBuffer = (int16 *)malloc(2 * numSamples); + + _mixBufferLen = numSamples; + } + + if (!_mixBuffer) + return 0; + + for (i = 0; i < MAXMUS; i++) { + if (!_music[i]) + continue; + + int len = _music[i]->readBuffer(_mixBuffer, numSamples); + + if (!_musicMuted) { + for (int j = 0; j < len; j++) { + Audio::clampedAdd(buffer[j], _mixBuffer[j]); + } + } + } + + bool inUse[MAXMUS]; + + for (i = 0; i < MAXMUS; i++) + inUse[i] = false; + + for (i = 0; i < MAXMUS; i++) { + if (_music[i]) { + if (_music[i]->getCD() == 1) + inUse[0] = true; + else + inUse[1] = true; + } + } + + for (i = 0; i < MAXMUS; i++) { + if (!inUse[i] && !_musicFile[i].inUse && _musicFile[i].file.isOpen()) + _musicFile[i].file.close(); + } + + return numSamples; +} + +bool Sound::endOfData() const { + for (int i = 0; i < MAXMUS; i++) { + if (_musicFile[i].file.isOpen()) + return false; + } + + return true; +} + +// ---------------------------------------------------------------------------- +// MUSIC +// ---------------------------------------------------------------------------- + +/** + * Stops the music dead in its tracks. Any music that is currently being + * streamed is paused. + */ + +void Sound::pauseMusic() { + Common::StackLock lock(_mutex); + + _musicPaused = true; +} + +/** + * Restarts the music from where it was stopped. + */ + +void Sound::unpauseMusic() { + Common::StackLock lock(_mutex); + + _musicPaused = false; +} + +/** + * Fades out and stops the music. + */ + +void Sound::stopMusic(bool immediately) { + Common::StackLock lock(_mutex); + + _loopingMusicId = 0; + + for (int i = 0; i < MAXMUS; i++) { + if (_music[i]) { + if (immediately) { + delete _music[i]; + _music[i] = NULL; + } else + _music[i]->fadeDown(); + } + } +} + +/** + * Streams music from a cluster file. + * @param musicId the id of the music to stream + * @param loop true if the music is to loop back to the start + * @return RD_OK or an error code + */ +int32 Sound::streamCompMusic(uint32 musicId, bool loop) { + //Common::StackLock lock(_mutex); + + _mutex.lock(); + int cd = _vm->_resman->getCD(); + + if (loop) + _loopingMusicId = musicId; + else + _loopingMusicId = 0; + + int primary = -1; + int secondary = -1; + + // If both music streams are active, one of them will have to go. + + if (_music[0] && _music[1]) { + int32 fade0 = _music[0]->isFading(); + int32 fade1 = _music[1]->isFading(); + + if (!fade0 && !fade1) { + // Neither is fading. This shouldn't happen, so just + // pick one and be done with it. + primary = 0; + } else if (fade0 && !fade1) { + // Stream 0 is fading, so pick that one. + primary = 0; + } else if (!fade0 && fade1) { + // Stream 1 is fading, so pick that one. + primary = 1; + } else { + // Both streams are fading. Pick the one that is + // closest to silent. + if (ABS(fade0) < ABS(fade1)) + primary = 0; + else + primary = 1; + } + + delete _music[primary]; + _music[primary] = NULL; + } + + // Pick the available music stream. If no music is playing it doesn't + // matter which we use. + + if (_music[0] || _music[1]) { + if (_music[0]) { + primary = 1; + secondary = 0; + } else { + primary = 0; + secondary = 1; + } + } else + primary = 0; + + // Don't start streaming if the volume is off. + if (isMusicMute()) { + _mutex.unlock(); + return RD_OK; + } + + if (secondary != -1) + _music[secondary]->fadeDown(); + SoundFileHandle *fh = (cd == 1) ? &_musicFile[0] : &_musicFile[1]; + fh->inUse = true; + _mutex.unlock(); + + MusicInputStream *tmp = new MusicInputStream(cd, fh, musicId, loop); + + if (tmp->isReady()) { + _mutex.lock(); + _music[primary] = tmp; + fh->inUse = false; + _mutex.unlock(); + return RD_OK; + } else { + _mutex.lock(); + fh->inUse = false; + _mutex.unlock(); + delete tmp; + return RDERR_INVALIDFILENAME; + } +} + +/** + * @return the time left for the current music, in seconds. + */ + +int32 Sound::musicTimeRemaining() { + Common::StackLock lock(_mutex); + + for (int i = 0; i < MAXMUS; i++) { + if (_music[i] && _music[i]->isFading() <= 0) + return _music[i]->getTimeRemaining(); + } + + return 0; +} + +// ---------------------------------------------------------------------------- +// SPEECH +// ---------------------------------------------------------------------------- + +/** + * Mutes/Unmutes the speech. + * @param mute If mute is false, restore the volume to the last set master + * level. Otherwise the speech is muted (volume 0). + */ + +void Sound::muteSpeech(bool mute) { + _speechMuted = mute; + + if (_vm->_mixer->isSoundHandleActive(_soundHandleSpeech)) { + uint volume = mute ? 0 : Audio::Mixer::kMaxChannelVolume; + + _vm->_mixer->setChannelVolume(_soundHandleSpeech, volume); + } +} + +/** + * Stops the speech dead in its tracks. + */ + +void Sound::pauseSpeech() { + _speechPaused = true; + _vm->_mixer->pauseHandle(_soundHandleSpeech, true); +} + +/** + * Restarts the speech from where it was stopped. + */ + +void Sound::unpauseSpeech() { + _speechPaused = false; + _vm->_mixer->pauseHandle(_soundHandleSpeech, false); +} + +/** + * Stops the speech from playing. + */ + +int32 Sound::stopSpeech() { + if (_vm->_mixer->isSoundHandleActive(_soundHandleSpeech)) { + _vm->_mixer->stopHandle(_soundHandleSpeech); + return RD_OK; + } + + return RDERR_SPEECHNOTPLAYING; +} + +/** + * @return Either RDSE_SAMPLEPLAYING or RDSE_SAMPLEFINISHED + */ + +int32 Sound::getSpeechStatus() { + return _vm->_mixer->isSoundHandleActive(_soundHandleSpeech) ? RDSE_SAMPLEPLAYING : RDSE_SAMPLEFINISHED; +} + +/** + * Returns either RDSE_QUIET or RDSE_SPEAKING + */ + +int32 Sound::amISpeaking() { + if (!_speechMuted && !_speechPaused && _vm->_mixer->isSoundHandleActive(_soundHandleSpeech)) + return RDSE_SPEAKING; + + return RDSE_QUIET; +} + +/** + * This function loads and decompresses a list of speech from a cluster, but + * does not play it. This is used for cutscene voice-overs, presumably to + * avoid having to read from more than one file on the CD during playback. + * @param speechId the text line id used to reference the speech + * @param buf a pointer to the buffer that will be allocated for the sound + */ + +uint32 Sound::preFetchCompSpeech(uint32 speechId, uint16 **buf) { + int cd = _vm->_resman->getCD(); + uint32 numSamples; + + SoundFileHandle *fh = (cd == 1) ? &_speechFile[0] : &_speechFile[1]; + + AudioStream *input = getAudioStream(fh, "speech", cd, speechId, &numSamples); + + if (!input) + return 0; + + *buf = NULL; + + // Decompress data into speech buffer. + + uint32 bufferSize = 2 * numSamples; + + *buf = (uint16 *)malloc(bufferSize); + if (!*buf) { + delete input; + fh->file.close(); + return 0; + } + + uint32 readSamples = input->readBuffer((int16 *)*buf, numSamples); + + fh->file.close(); + delete input; + + return 2 * readSamples; +} + +/** + * This function loads, decompresses and plays a line of speech. An error + * occurs if speech is already playing. + * @param speechId the text line id used to reference the speech + * @param vol volume, 0 (minimum) to 16 (maximum) + * @param pan panning, -16 (full left) to 16 (full right) + */ + +int32 Sound::playCompSpeech(uint32 speechId, uint8 vol, int8 pan) { + if (_speechMuted) + return RD_OK; + + if (getSpeechStatus() == RDERR_SPEECHPLAYING) + return RDERR_SPEECHPLAYING; + + int cd = _vm->_resman->getCD(); + SoundFileHandle *fh = (cd == 1) ? &_speechFile[0] : &_speechFile[1]; + + AudioStream *input = getAudioStream(fh, "speech", cd, speechId, NULL); + + if (!input) + return RDERR_INVALIDID; + + // Modify the volume according to the master volume + + byte volume = _speechMuted ? 0 : vol * Audio::Mixer::kMaxChannelVolume / 16; + int8 p = (pan * 127) / 16; + + if (isReverseStereo()) + p = -p; + + // Start the speech playing + _vm->_mixer->playInputStream(Audio::Mixer::kSpeechSoundType, &_soundHandleSpeech, input, -1, volume, p); + return RD_OK; +} + +// ---------------------------------------------------------------------------- +// SOUND EFFECTS +// ---------------------------------------------------------------------------- + +/** + * Mutes/Unmutes the sound effects. + * @param mute If mute is false, restore the volume to the last set master + * level. Otherwise the sound effects are muted (volume 0). + */ + +void Sound::muteFx(bool mute) { + _fxMuted = mute; + + // Now update the volume of any fxs playing + for (int i = 0; i < FXQ_LENGTH; i++) { + if (_fxQueue[i].resource) { + _vm->_mixer->setChannelVolume(_fxQueue[i].handle, mute ? 0 : _fxQueue[i].volume); + } + } +} + +/** + * Sets the volume and pan of the sample which is currently playing + * @param id the id of the sample + * @param vol volume + * @param pan panning + */ + +int32 Sound::setFxIdVolumePan(int32 id, int vol, int pan) { + if (!_fxQueue[id].resource) + return RDERR_FXNOTOPEN; + + if (vol > 16) + vol = 16; + + _fxQueue[id].volume = (vol * Audio::Mixer::kMaxChannelVolume) / 16; + + if (pan != 255) { + if (isReverseStereo()) + pan = -pan; + _fxQueue[id].pan = (pan * 127) / 16; + } + + if (!_fxMuted && _vm->_mixer->isSoundHandleActive(_fxQueue[id].handle)) { + _vm->_mixer->setChannelVolume(_fxQueue[id].handle, _fxQueue[id].volume); + if (pan != -1) + _vm->_mixer->setChannelBalance(_fxQueue[id].handle, _fxQueue[id].pan); + } + + return RD_OK; +} + +void Sound::pauseFx() { + if (_fxPaused) + return; + + for (int i = 0; i < FXQ_LENGTH; i++) { + if (_fxQueue[i].resource) + _vm->_mixer->pauseHandle(_fxQueue[i].handle, true); + } + + _fxPaused = true; +} + +void Sound::unpauseFx() { + if (!_fxPaused) + return; + + for (int i = 0; i < FXQ_LENGTH; i++) + if (_fxQueue[i].resource) + _vm->_mixer->pauseHandle(_fxQueue[i].handle, false); + + _fxPaused = false; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/object.h b/engines/sword2/object.h new file mode 100644 index 0000000000..1fd434f8a0 --- /dev/null +++ b/engines/sword2/object.h @@ -0,0 +1,342 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _SCRIPT_STRUCTURES +#define _SCRIPT_STRUCTURES + +#include "common/stream.h" + +namespace Sword2 { + +// these structures represent the broken up compact components +// these here declared to the system must be the same as those declared to +// LINC (or it wont work) + +// mouse structure - defines mouse detection area, detection priority & +// 'type' flag + +struct ObjectMouse { + int32 x1; // Top-left and bottom-right of mouse + int32 y1; // area. (These coords are inclusive.) + int32 x2; + int32 y2; + int32 priority; + int32 pointer; // type (or resource id?) of pointer used over this area + + static const int size() { + return 24; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + x1 = readS.readSint32LE(); + y1 = readS.readSint32LE(); + x2 = readS.readSint32LE(); + y2 = readS.readSint32LE(); + priority = readS.readSint32LE(); + pointer = readS.readSint32LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeSint32LE(x1); + writeS.writeSint32LE(y1); + writeS.writeSint32LE(x2); + writeS.writeSint32LE(y2); + writeS.writeSint32LE(priority); + writeS.writeSint32LE(pointer); + } +}; + +// logic structure - contains fields used in logic script processing + +class ObjectLogic { + // int32 looping; // 0 when first calling fn<function>; + // 1 when calling subsequent times in + // same loop + // int32 pause; // pause count, used by fnPause() + +private: + byte *_addr; + +public: + ObjectLogic(byte *addr) { + _addr = addr; + } + + static const int size() { + return 8; + } + + byte *data() { + return _addr; + } + + int32 getLooping() { return READ_LE_UINT32(_addr); } + int32 getPause() { return READ_LE_UINT32(_addr + 4); } + + void setLooping(int32 x) { WRITE_LE_UINT32(_addr, x); } + void setPause(int32 x) { WRITE_LE_UINT32(_addr + 4, x); } +}; + +// status bits for 'type' field of ObjectGraphic) + +// in low word: + +#define NO_SPRITE 0x00000000 // don't print +#define BGP0_SPRITE 0x00000001 // fixed to background parallax[0] +#define BGP1_SPRITE 0x00000002 // fixed to background parallax[1] +#define BACK_SPRITE 0x00000004 // 'background' sprite, fixed to main background +#define SORT_SPRITE 0x00000008 // 'sorted' sprite, fixed to main background +#define FORE_SPRITE 0x00000010 // 'foreground' sprite, fixed to main background +#define FGP0_SPRITE 0x00000020 // fixed to foreground parallax[0] +#define FGP1_SPRITE 0x00000040 // fixed to foreground parallax[0] + +// in high word: + +#define UNSHADED_SPRITE 0x00000000 // not to be shaded +#define SHADED_SPRITE 0x00010000 // to be shaded, based on shading mask + +// graphic structure - contains fields appropriate to sprite output + +class ObjectGraphic { + // int32 type; // see above + // int32 anim_resource; // resource id of animation file + // int32 anim_pc; // current frame number of animation + +private: + byte *_addr; + +public: + ObjectGraphic(byte *addr) { + _addr = addr; + } + + static const int size() { + return 12; + } + + byte *data() { + return _addr; + } + + int32 getType() { return READ_LE_UINT32(_addr); } + int32 getAnimResource() { return READ_LE_UINT32(_addr + 4); } + int32 getAnimPc() { return READ_LE_UINT32(_addr + 8); } + + void setType(int32 x) { WRITE_LE_UINT32(_addr, x); } + void setAnimResource(int32 x) { WRITE_LE_UINT32(_addr + 4, x); } + void setAnimPc(int32 x) { WRITE_LE_UINT32(_addr + 8, x); } +}; + +// speech structure - contains fields used by speech scripts & text output + +class ObjectSpeech { + // int32 pen; // colour to use for body of characters + // int32 width; // max width of text sprite + // int32 command; // speech script command id + // int32 ins1; // speech script instruction parameters + // int32 ins2; + // int32 ins3; + // int32 ins4; + // int32 ins5; + // int32 wait_state; // 0 not waiting, + // 1 waiting for next speech command + +private: + byte *_addr; + +public: + ObjectSpeech(byte *addr) { + _addr = addr; + } + + static const int size() { + return 36; + } + + byte *data() { + return _addr; + } + + int32 getPen() { return READ_LE_UINT32(_addr); } + int32 getWidth() { return READ_LE_UINT32(_addr + 4); } + int32 getCommand() { return READ_LE_UINT32(_addr + 8); } + int32 getIns1() { return READ_LE_UINT32(_addr + 12); } + int32 getIns2() { return READ_LE_UINT32(_addr + 16); } + int32 getIns3() { return READ_LE_UINT32(_addr + 20); } + int32 getIns4() { return READ_LE_UINT32(_addr + 24); } + int32 getIns5() { return READ_LE_UINT32(_addr + 28); } + int32 getWaitState() { return READ_LE_UINT32(_addr + 32); } + + void setPen(int32 x) { WRITE_LE_UINT32(_addr, x); } + void setWidth(int32 x) { WRITE_LE_UINT32(_addr + 4, x); } + void setCommand(int32 x) { WRITE_LE_UINT32(_addr + 8, x); } + void setIns1(int32 x) { WRITE_LE_UINT32(_addr + 12, x); } + void setIns2(int32 x) { WRITE_LE_UINT32(_addr + 16, x); } + void setIns3(int32 x) { WRITE_LE_UINT32(_addr + 20, x); } + void setIns4(int32 x) { WRITE_LE_UINT32(_addr + 24, x); } + void setIns5(int32 x) { WRITE_LE_UINT32(_addr + 28, x); } + void setWaitState(int32 x) { WRITE_LE_UINT32(_addr + 32, x); } +}; + +// mega structure - contains fields used for mega-character & mega-set +// processing + +class ObjectMega { + // int32 NOT_USED_1; // only free roaming megas need to + // check this before registering their + // graphics for drawing + // int32 NOT_USED_2; // id of floor on which we are standing + // int32 NOT_USED_3; // id of object which we are getting to + // int32 NOT_USED_4; // pixel distance to stand from player + // character when in conversation + // int32 currently_walking; // number given us by the auto router + // int32 walk_pc; // current frame number of walk-anim + // int32 scale_a; // current scale factors, taken from + // int32 scale_b; // floor data + // int32 feet_x; // mega feet coords - frame-offsets are + // int32 feet_y; // added to these position mega frames + // int32 current_dir; // current dirction faced by mega; used + // by autorouter to determine turns + // required + // int32 NOT_USED_5; // means were currently avoiding a + // collision (see fnWalk) + // int32 megaset_res; // resource id of mega-set file + // int32 NOT_USED_6; // NOT USED + +private: + byte *_addr; + +public: + ObjectMega(byte *addr) { + _addr = addr; + } + + static const int size() { + return 56; + } + + byte *data() { + return _addr; + } + + int32 getIsWalking() { return READ_LE_UINT32(_addr + 16); } + int32 getWalkPc() { return READ_LE_UINT32(_addr + 20); } + int32 getScaleA() { return READ_LE_UINT32(_addr + 24); } + int32 getScaleB() { return READ_LE_UINT32(_addr + 28); } + int32 getFeetX() { return READ_LE_UINT32(_addr + 32); } + int32 getFeetY() { return READ_LE_UINT32(_addr + 36); } + int32 getCurDir() { return READ_LE_UINT32(_addr + 40); } + int32 getMegasetRes() { return READ_LE_UINT32(_addr + 48); } + + void setIsWalking(int32 x) { WRITE_LE_UINT32(_addr + 16, x); } + void setWalkPc(int32 x) { WRITE_LE_UINT32(_addr + 20, x); } + void setScaleA(int32 x) { WRITE_LE_UINT32(_addr + 24, x); } + void setScaleB(int32 x) { WRITE_LE_UINT32(_addr + 28, x); } + void setFeetX(int32 x) { WRITE_LE_UINT32(_addr + 32, x); } + void setFeetY(int32 x) { WRITE_LE_UINT32(_addr + 36, x); } + void setCurDir(int32 x) { WRITE_LE_UINT32(_addr + 40, x); } + void setMegasetRes(int32 x) { WRITE_LE_UINT32(_addr + 48, x); } + + int32 calcScale() { + // Calc scale at which to print the sprite, based on feet + // y-coord & scaling constants (NB. 'scale' is actually + // 256 * true_scale, to maintain accuracy) + + // Ay+B gives 256 * scale ie. 256 * 256 * true_scale for even + // better accuracy, ie. scale = (Ay + B) / 256 + return (getScaleA() * getFeetY() + getScaleB()) / 256; + } +}; + +// walk-data structure - contains details of layout of frames in the +// mega-set, and how they are to be used + +struct ObjectWalkdata { + int32 nWalkFrames; // no. of frames per walk-cycle + int32 usingStandingTurnFrames; // 0 = no 1 = yes + int32 usingWalkingTurnFrames; // 0 = no 1 = yes + int32 usingSlowInFrames; // 0 = no 1 = yes + int32 usingSlowOutFrames; // 0 = no !0 = number of slow-out frames in each direction + int32 nSlowInFrames[8]; // no. of slow-in frames in each direction + int32 leadingLeg[8]; // leading leg for walk in each direction (0 = left 1 = right) + int32 dx[8 * (12 + 1)]; // walk step distances in x direction + int32 dy[8 * (12 + 1)]; // walk step distances in y direction + + static const int size() { + return 916; + } + + void read(byte *addr) { + Common::MemoryReadStream readS(addr, size()); + + nWalkFrames = readS.readUint32LE(); + usingStandingTurnFrames = readS.readUint32LE(); + usingWalkingTurnFrames = readS.readUint32LE(); + usingSlowInFrames = readS.readUint32LE(); + usingSlowOutFrames = readS.readUint32LE(); + + int i; + + for (i = 0; i < ARRAYSIZE(nSlowInFrames); i++) + nSlowInFrames[i] = readS.readUint32LE(); + + for (i = 0; i < ARRAYSIZE(leadingLeg); i++) + leadingLeg[i] = readS.readUint32LE(); + + for (i = 0; i < ARRAYSIZE(dx); i++) + dx[i] = readS.readUint32LE(); + + for (i = 0; i < ARRAYSIZE(dy); i++) + dy[i] = readS.readUint32LE(); + } + + void write(byte *addr) { + Common::MemoryWriteStream writeS(addr, size()); + + writeS.writeUint32LE(nWalkFrames); + writeS.writeUint32LE(usingStandingTurnFrames); + writeS.writeUint32LE(usingWalkingTurnFrames); + writeS.writeUint32LE(usingSlowInFrames); + writeS.writeUint32LE(usingSlowOutFrames); + + int i; + + for (i = 0; i < ARRAYSIZE(nSlowInFrames); i++) + writeS.writeUint32LE(nSlowInFrames[i]); + + for (i = 0; i < ARRAYSIZE(leadingLeg); i++) + writeS.writeUint32LE(leadingLeg[i]); + + for (i = 0; i < ARRAYSIZE(dx); i++) + writeS.writeUint32LE(dx[i]); + + for (i = 0; i < ARRAYSIZE(dy); i++) + writeS.writeUint32LE(dy[i]); + } +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/palette.cpp b/engines/sword2/palette.cpp new file mode 100644 index 0000000000..0b6b7a8ca2 --- /dev/null +++ b/engines/sword2/palette.cpp @@ -0,0 +1,267 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/system.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/resman.h" + +namespace Sword2 { + +/** + * Start layer palette fading up + */ + +void Screen::startNewPalette() { + // If the screen is still fading down then wait for black - could + // happen when everythings cached into a large memory model + waitForFade(); + + byte *screenFile = _vm->_resman->openResource(_thisScreen.background_layer_id); + + memcpy(_paletteMatch, _vm->fetchPaletteMatchTable(screenFile), PALTABLESIZE); + setPalette(0, 256, _vm->fetchPalette(screenFile), RDPAL_FADE); + + // Indicating that it's a screen palette + _lastPaletteRes = 0; + + _vm->_resman->closeResource(_thisScreen.background_layer_id); + fadeUp(); + _thisScreen.new_palette = 0; +} + +void Screen::setFullPalette(int32 palRes) { + // fudge for hut interior + // - unpausing should restore last palette as normal (could be screen + // palette or 'dark_palette_13') + // - but restoring the screen palette after 'dark_palette_13' should + // now work properly too! + + // "Hut interior" refers to the watchman's hut in Marseille, and this + // is apparently needed for the palette to be restored properly when + // you turn the light off. (I didn't even notice the light switch!) + + if (_vm->_logic->readVar(LOCATION) == 13) { + // unpausing + if (palRes == -1) { + // restore whatever palette was last set (screen + // palette or 'dark_palette_13') + palRes = _lastPaletteRes; + } + } else { + // check if we're just restoring the current screen palette + // because we might actually need to use a separate palette + // file anyway eg. for pausing & unpausing during the eclipse + + // unpausing (fudged for location 13) + if (palRes == -1) { + // we really meant '0' + palRes = 0; + } + + if (palRes == 0 && _lastPaletteRes) + palRes = _lastPaletteRes; + } + + // If non-zero, set palette to this separate palette file. Otherwise, + // set palette to current screen palette. + + if (palRes) { + byte *pal = _vm->_resman->openResource(palRes); + + assert(_vm->_resman->fetchType(pal) == PALETTE_FILE); + + pal += ResHeader::size(); + + // always set colour 0 to black because most background screen + // palettes have a bright colour 0 although it should come out + // as black in the game! + + pal[0] = 0; + pal[1] = 0; + pal[2] = 0; + pal[3] = 0; + + setPalette(0, 256, pal, RDPAL_INSTANT); + _vm->_resman->closeResource(palRes); + } else { + if (_thisScreen.background_layer_id) { + byte *data = _vm->_resman->openResource(_thisScreen.background_layer_id); + memcpy(_paletteMatch, _vm->fetchPaletteMatchTable(data), PALTABLESIZE); + setPalette(0, 256, _vm->fetchPalette(data), RDPAL_INSTANT); + _vm->_resman->closeResource(_thisScreen.background_layer_id); + } else + error("setFullPalette(0) called, but no current screen available!"); + } + + if (palRes != CONTROL_PANEL_PALETTE) + _lastPaletteRes = palRes; +} + +/** + * Matches a colour triplet to a palette index. + * @param r red colour component + * @param g green colour component + * @param b blue colour component + * @return the palette index of the closest matching colour in the palette + */ + +// FIXME: This used to be inlined - probably a good idea - but the +// linker complained when I tried to use it in sprite.cpp. + +uint8 Screen::quickMatch(uint8 r, uint8 g, uint8 b) { + return _paletteMatch[((int32)(r >> 2) << 12) + ((int32)(g >> 2) << 6) + (b >> 2)]; +} + +/** + * Sets the palette. + * @param startEntry the first colour entry to set + * @param noEntries the number of colour entries to set + * @param colourTable the new colour entries + * @param fadeNow whether to perform the change immediately or delayed + */ + +void Screen::setPalette(int16 startEntry, int16 noEntries, byte *colourTable, uint8 fadeNow) { + assert(noEntries > 0); + + memcpy(&_palette[4 * startEntry], colourTable, noEntries * 4); + + if (fadeNow == RDPAL_INSTANT) { + _vm->_system->setPalette(_palette, startEntry, noEntries); + setNeedFullRedraw(); + } +} + +void Screen::dimPalette() { + byte *p = _palette; + + for (int i = 0; i < 256; i++) { + p[i * 4 + 0] /= 2; + p[i * 4 + 1] /= 2; + p[i * 4 + 2] /= 2; + } + + _vm->_system->setPalette(p, 0, 256); + setNeedFullRedraw(); +} + +/** + * Fades the palette up from black to the current palette. + * @param time the time it will take the palette to fade up + */ + +int32 Screen::fadeUp(float time) { + if (getFadeStatus() != RDFADE_BLACK && getFadeStatus() != RDFADE_NONE) + return RDERR_FADEINCOMPLETE; + + _fadeTotalTime = (int32)(time * 1000); + _fadeStatus = RDFADE_UP; + _fadeStartTime = _vm->_system->getMillis(); + + return RD_OK; +} + +/** + * Fades the palette down to black from the current palette. + * @param time the time it will take the palette to fade down + */ + +int32 Screen::fadeDown(float time) { + if (getFadeStatus() != RDFADE_BLACK && getFadeStatus() != RDFADE_NONE) + return RDERR_FADEINCOMPLETE; + + _fadeTotalTime = (int32)(time * 1000); + _fadeStatus = RDFADE_DOWN; + _fadeStartTime = _vm->_system->getMillis(); + + return RD_OK; +} + +/** + * Get the current fade status + * @return RDFADE_UP (fading up), RDFADE_DOWN (fading down), RDFADE_NONE + * (not faded), or RDFADE_BLACK (completely faded down) + */ + +uint8 Screen::getFadeStatus() { + return _fadeStatus; +} + +void Screen::waitForFade() { + while (getFadeStatus() != RDFADE_NONE && getFadeStatus() != RDFADE_BLACK && !_vm->_quit) { + updateDisplay(); + _vm->_system->delayMillis(20); + } +} + +void Screen::fadeServer() { + static int32 previousTime = 0; + byte fadePalette[256 * 4]; + byte *newPalette = fadePalette; + int32 currentTime; + int16 fadeMultiplier; + int16 i; + + // If we're not in the process of fading, do nothing. + if (getFadeStatus() != RDFADE_UP && getFadeStatus() != RDFADE_DOWN) + return; + + // I don't know if this is necessary, but let's limit how often the + // palette is updated, just to be safe. + currentTime = _vm->_system->getMillis(); + if (currentTime - previousTime <= 25) + return; + + previousTime = currentTime; + + if (getFadeStatus() == RDFADE_UP) { + if (currentTime >= _fadeStartTime + _fadeTotalTime) { + _fadeStatus = RDFADE_NONE; + newPalette = _palette; + } else { + fadeMultiplier = (int16)(((int32)(currentTime - _fadeStartTime) * 256) / _fadeTotalTime); + for (i = 0; i < 256; i++) { + newPalette[i * 4 + 0] = (_palette[i * 4 + 0] * fadeMultiplier) >> 8; + newPalette[i * 4 + 1] = (_palette[i * 4 + 1] * fadeMultiplier) >> 8; + newPalette[i * 4 + 2] = (_palette[i * 4 + 2] * fadeMultiplier) >> 8; + } + } + } else { + if (currentTime >= _fadeStartTime + _fadeTotalTime) { + _fadeStatus = RDFADE_BLACK; + memset(newPalette, 0, sizeof(fadePalette)); + } else { + fadeMultiplier = (int16)(((int32)(_fadeTotalTime - (currentTime - _fadeStartTime)) * 256) / _fadeTotalTime); + for (i = 0; i < 256; i++) { + newPalette[i * 4 + 0] = (_palette[i * 4 + 0] * fadeMultiplier) >> 8; + newPalette[i * 4 + 1] = (_palette[i * 4 + 1] * fadeMultiplier) >> 8; + newPalette[i * 4 + 2] = (_palette[i * 4 + 2] * fadeMultiplier) >> 8; + } + } + } + + _vm->_system->setPalette(newPalette, 0, 256); + setNeedFullRedraw(); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/protocol.cpp b/engines/sword2/protocol.cpp new file mode 100644 index 0000000000..23010e27a9 --- /dev/null +++ b/engines/sword2/protocol.cpp @@ -0,0 +1,216 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/resman.h" + +namespace Sword2 { + +/** + * Returns a pointer to the first palette entry, given the pointer to the start + * of the screen file. + */ + +byte *Sword2Engine::fetchPalette(byte *screenFile) { + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + + byte *palette = screenFile + ResHeader::size() + mscreenHeader.palette; + + // Always set colour 0 to black, because while most background screen + // palettes have a bright colour 0 it should come out as black in the + // game. + + palette[0] = 0; + palette[1] = 0; + palette[2] = 0; + palette[3] = 0; + + return palette; +} + +/** + * Returns a pointer to the start of the palette match table, given the pointer + * to the start of the screen file. + */ + +byte *Sword2Engine::fetchPaletteMatchTable(byte *screenFile) { + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + + return screenFile + ResHeader::size() + mscreenHeader.paletteTable; +} + +/** + * Returns a pointer to the screen header, given the pointer to the start of + * the screen file. + */ + +byte *Sword2Engine::fetchScreenHeader(byte *screenFile) { + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + + return screenFile + ResHeader::size() + mscreenHeader.screen; +} + +/** + * Returns a pointer to the requested layer header, given the pointer to the + * start of the screen file. Drops out if the requested layer number exceeds + * the number of layers on this screen. + */ + +byte *Sword2Engine::fetchLayerHeader(byte *screenFile, uint16 layerNo) { +#ifdef SWORD2_DEBUG + ScreenHeader screenHead; + + screenHead.read(fetchScreenHeader(screenFile)); + assert(layerNo < screenHead.noLayers); +#endif + + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + + return screenFile + ResHeader::size() + mscreenHeader.layers + layerNo * LayerHeader::size(); +} + +/** + * Returns a pointer to the start of the shading mask, given the pointer to the + * start of the screen file. + */ + +byte *Sword2Engine::fetchShadingMask(byte *screenFile) { + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + + return screenFile + ResHeader::size() + mscreenHeader.maskOffset; +} + +/** + * Returns a pointer to the anim header, given the pointer to the start of the + * anim file. + */ + +byte *Sword2Engine::fetchAnimHeader(byte *animFile) { + return animFile + ResHeader::size(); +} + +/** + * Returns a pointer to the requested frame number's cdtEntry, given the + * pointer to the start of the anim file. Drops out if the requested frame + * number exceeds the number of frames in this anim. + */ + +byte *Sword2Engine::fetchCdtEntry(byte *animFile, uint16 frameNo) { +#ifdef SWORD2_DEBUG + AnimHeader animHead; + + animHead.read(fetchAnimHeader(animFile)); + + if (frameNo > animHead->noAnimFrames - 1) + error("fetchCdtEntry(animFile,%d) - anim only %d frames", frameNo, animHead.noAnimFrames); +#endif + + return fetchAnimHeader(animFile) + AnimHeader::size() + frameNo * CdtEntry::size(); +} + +/** + * Returns a pointer to the requested frame number's header, given the pointer + * to the start of the anim file. Drops out if the requested frame number + * exceeds the number of frames in this anim + */ + +byte *Sword2Engine::fetchFrameHeader(byte *animFile, uint16 frameNo) { + // required address = (address of the start of the anim header) + frameOffset + CdtEntry cdt; + + cdt.read(fetchCdtEntry(animFile, frameNo)); + + return animFile + ResHeader::size() + cdt.frameOffset; +} + +/** + * Returns a pointer to the requested parallax layer data. + */ + +byte *Sword2Engine::fetchBackgroundParallaxLayer(byte *screenFile, int layer) { + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + assert(mscreenHeader.bg_parallax[layer]); + + return screenFile + ResHeader::size() + mscreenHeader.bg_parallax[layer]; +} + +byte *Sword2Engine::fetchBackgroundLayer(byte *screenFile) { + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + assert(mscreenHeader.screen); + + return screenFile + ResHeader::size() + mscreenHeader.screen + ScreenHeader::size(); +} + +byte *Sword2Engine::fetchForegroundParallaxLayer(byte *screenFile, int layer) { + MultiScreenHeader mscreenHeader; + + mscreenHeader.read(screenFile + ResHeader::size()); + assert(mscreenHeader.fg_parallax[layer]); + + return screenFile + ResHeader::size() + mscreenHeader.fg_parallax[layer]; +} + +byte *Sword2Engine::fetchTextLine(byte *file, uint32 text_line) { + TextHeader text_header; + static byte errorLine[128]; + + text_header.read(file + ResHeader::size()); + + if (text_line >= text_header.noOfLines) { + sprintf((char *)errorLine, "xxMissing line %d of %s (only 0..%d)", text_line, _resman->fetchName(file), text_header.noOfLines - 1); + + // first 2 chars are NULL so that actor-number comes out as '0' + errorLine[0] = 0; + errorLine[1] = 0; + return errorLine; + } + + // The "number of lines" field is followed by a lookup table + + return file + READ_LE_UINT32(file + ResHeader::size() + 4 + 4 * text_line); +} + +// Used for testing text & speech (see fnISpeak in speech.cpp) + +bool Sword2Engine::checkTextLine(byte *file, uint32 text_line) { + TextHeader text_header; + + text_header.read(file + ResHeader::size()); + + return text_line < text_header.noOfLines; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/rdwin.cpp b/engines/sword2/rdwin.cpp new file mode 100644 index 0000000000..310b66abf7 --- /dev/null +++ b/engines/sword2/rdwin.cpp @@ -0,0 +1,118 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/system.h" +#include "sword2/sword2.h" + +namespace Sword2 { + +/** + * Tell updateDisplay() that the scene needs to be completely updated. + */ + +void Screen::setNeedFullRedraw() { + _needFullRedraw = true; +} + +/** + * Mark an area of the screen as dirty, first generation. + */ + +void Screen::markAsDirty(int16 x0, int16 y0, int16 x1, int16 y1) { + int16 gridX0 = x0 / CELLWIDE; + int16 gridY0 = y0 / CELLDEEP; + int16 gridX1 = x1 / CELLWIDE; + int16 gridY1 = y1 / CELLDEEP; + + for (int16 i = gridY0; i <= gridY1; i++) + for (int16 j = gridX0; j <= gridX1; j++) + _dirtyGrid[i * _gridWide + j] = 2; +} + +/** + * This function has two purposes: It redraws the scene, and it handles input + * events, palette fading, etc. It should be called at a high rate (> 20 per + * second), but the scene is usually only redrawn about 12 times per second, + * except when then screen is scrolling. + * + * @param redrawScene If true, redraw the scene. + */ + +void Screen::updateDisplay(bool redrawScene) { + _vm->parseInputEvents(); + fadeServer(); + + if (redrawScene) { + int i; + + // Note that the entire scene is always rendered, which is less + // than optimal, but at least we can try to be intelligent + // about updating the screen afterwards. + + if (_needFullRedraw) { + // Update the entire screen. This is necessary when + // scrolling, fading, etc. + + _vm->_system->copyRectToScreen(_buffer + MENUDEEP * _screenWide, _screenWide, 0, MENUDEEP, _screenWide, _screenDeep - 2 * MENUDEEP); + _needFullRedraw = false; + } else { + // Update only the dirty areas of the screen + + int j, x, y; + int stripWide; + + for (i = 0; i < _gridDeep; i++) { + stripWide = 0; + + for (j = 0; j < _gridWide; j++) { + if (_dirtyGrid[i * _gridWide + j]) { + stripWide++; + } else if (stripWide) { + x = CELLWIDE * (j - stripWide); + y = CELLDEEP * i; + _vm->_system->copyRectToScreen(_buffer + y * _screenWide + x, _screenWide, x, y, stripWide * CELLWIDE, CELLDEEP); + stripWide = 0; + } + } + + if (stripWide) { + x = CELLWIDE * (j - stripWide); + y = CELLDEEP * i; + _vm->_system->copyRectToScreen(_buffer + y * _screenWide + x, _screenWide, x, y, stripWide * CELLWIDE, CELLDEEP); + stripWide = 0; + } + } + } + + // Age the dirty cells one generation. This way we keep track + // of both the cells that were updated this time, and the ones + // that were updated the last time. + + for (i = 0; i < _gridWide * _gridDeep; i++) + _dirtyGrid[i] >>= 1; + } + + // We always need to update because of fades, menu animations, etc. + _vm->_system->updateScreen(); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/render.cpp b/engines/sword2/render.cpp new file mode 100644 index 0000000000..da60ecd6d9 --- /dev/null +++ b/engines/sword2/render.cpp @@ -0,0 +1,584 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/system.h" + +#include "graphics/primitives.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/build_display.h" + +#ifdef BACKEND_8BIT +#include "sword2/animation.h" +#endif + +namespace Sword2 { + +#define MILLISECSPERCYCLE 83 +#define RENDERAVERAGETOTAL 4 + +void Screen::updateRect(Common::Rect *r) { + _vm->_system->copyRectToScreen(_buffer + r->top * _screenWide + r->left, + _screenWide, r->left, r->top, r->right - r->left, + r->bottom - r->top); +} + +void Screen::blitBlockSurface(BlockSurface *s, Common::Rect *r, Common::Rect *clipRect) { + if (!r->intersects(*clipRect)) + return; + + byte *src = s->data; + + if (r->top < clipRect->top) { + src -= BLOCKWIDTH * (r->top - clipRect->top); + r->top = clipRect->top; + } + if (r->left < clipRect->left) { + src -= (r->left - clipRect->left); + r->left = clipRect->left; + } + if (r->bottom > clipRect->bottom) + r->bottom = clipRect->bottom; + if (r->right > clipRect->right) + r->right = clipRect->right; + + byte *dst = _buffer + r->top * _screenWide + r->left; + int i; + + if (s->transparent) { + for (i = 0; i < r->bottom - r->top; i++) { + for (int j = 0; j < r->right - r->left; j++) { + if (src[j]) + dst[j] = src[j]; + } + src += BLOCKWIDTH; + dst += _screenWide; + } + } else { + for (i = 0; i < r->bottom - r->top; i++) { + memcpy(dst, src, r->right - r->left); + src += BLOCKWIDTH; + dst += _screenWide; + } + } +} + +// There are two different separate functions for scaling the image - one fast +// and one good. Or at least that's the theory. I'm sure there are better ways +// to scale an image than this. The latter is used at the highest graphics +// quality setting. Note that the "good" scaler takes an extra parameter, a +// pointer to the area of the screen where the sprite will be drawn. +// +// This code isn't quite like the original DrawSprite(), but should be close +// enough. + +void Screen::scaleImageFast(byte *dst, uint16 dstPitch, uint16 dstWidth, uint16 dstHeight, byte *src, uint16 srcPitch, uint16 srcWidth, uint16 srcHeight) { + int x, y; + + for (x = 0; x < dstWidth; x++) + _xScale[x] = (x * srcWidth) / dstWidth; + + for (y = 0; y < dstHeight; y++) + _yScale[y] = (y * srcHeight) / dstHeight; + + for (y = 0; y < dstHeight; y++) { + for (x = 0; x < dstWidth; x++) { + dst[x] = src[_yScale[y] * srcPitch + _xScale[x]]; + } + dst += dstPitch; + } +} + +void Screen::scaleImageGood(byte *dst, uint16 dstPitch, uint16 dstWidth, uint16 dstHeight, byte *src, uint16 srcPitch, uint16 srcWidth, uint16 srcHeight, byte *backbuf) { + for (int y = 0; y < dstHeight; y++) { + for (int x = 0; x < dstWidth; x++) { + uint8 c1, c2, c3, c4; + + uint32 xPos = (x * srcWidth) / dstWidth; + uint32 yPos = (y * srcHeight) / dstHeight; + uint32 xFrac = dstWidth - (x * srcWidth) % dstWidth; + uint32 yFrac = dstHeight - (y * srcHeight) % dstHeight; + + byte *srcPtr = src + yPos * srcPitch + xPos; + byte *backPtr = backbuf + y * _screenWide + x; + + bool transparent = true; + + if (*srcPtr) { + c1 = *srcPtr; + transparent = false; + } else + c1 = *backPtr; + + if (x < dstWidth - 1) { + if (*(srcPtr + 1)) { + c2 = *(srcPtr + 1); + transparent = false; + } else + c2 = *(backPtr + 1); + } else + c2 = c1; + + if (y < dstHeight - 1) { + if (*(srcPtr + srcPitch)) { + c3 = *(srcPtr + srcPitch); + transparent = false; + } else + c3 = *(backPtr + _screenWide); + } else + c3 = c1; + + if (x < dstWidth - 1 && y < dstHeight - 1) { + if (*(srcPtr + srcPitch + 1)) { + c4 = *(srcPtr + srcPitch + 1); + transparent = false; + } else + c4 = *(backPtr + _screenWide + 1); + } else + c4 = c3; + + if (!transparent) { + uint32 r1 = _palette[c1 * 4 + 0]; + uint32 g1 = _palette[c1 * 4 + 1]; + uint32 b1 = _palette[c1 * 4 + 2]; + + uint32 r2 = _palette[c2 * 4 + 0]; + uint32 g2 = _palette[c2 * 4 + 1]; + uint32 b2 = _palette[c2 * 4 + 2]; + + uint32 r3 = _palette[c3 * 4 + 0]; + uint32 g3 = _palette[c3 * 4 + 1]; + uint32 b3 = _palette[c3 * 4 + 2]; + + uint32 r4 = _palette[c4 * 4 + 0]; + uint32 g4 = _palette[c4 * 4 + 1]; + uint32 b4 = _palette[c4 * 4 + 2]; + + uint32 r5 = (r1 * xFrac + r2 * (dstWidth - xFrac)) / dstWidth; + uint32 g5 = (g1 * xFrac + g2 * (dstWidth - xFrac)) / dstWidth; + uint32 b5 = (b1 * xFrac + b2 * (dstWidth - xFrac)) / dstWidth; + + uint32 r6 = (r3 * xFrac + r4 * (dstWidth - xFrac)) / dstWidth; + uint32 g6 = (g3 * xFrac + g4 * (dstWidth - xFrac)) / dstWidth; + uint32 b6 = (b3 * xFrac + b4 * (dstWidth - xFrac)) / dstWidth; + + uint32 r = (r5 * yFrac + r6 * (dstHeight - yFrac)) / dstHeight; + uint32 g = (g5 * yFrac + g6 * (dstHeight - yFrac)) / dstHeight; + uint32 b = (b5 * yFrac + b6 * (dstHeight - yFrac)) / dstHeight; + + dst[y * dstWidth + x] = quickMatch(r, g, b); + } else + dst[y * dstWidth + x] = 0; + } + } +} + +/** + * Plots a point relative to the top left corner of the screen. This is only + * used for debugging. + * @param x x-coordinate of the point + * @param y y-coordinate of the point + * @param colour colour of the point + */ + +void Screen::plotPoint(int x, int y, uint8 colour) { + byte *buf = _buffer + MENUDEEP * RENDERWIDE; + + x -= _scrollX; + y -= _scrollY; + + if (x >= 0 && x < RENDERWIDE && y >= 0 && y < RENDERDEEP) { + buf[y * RENDERWIDE + x] = colour; + markAsDirty(x, y + MENUDEEP, x, y + MENUDEEP); + } +} + +static void plot(int x, int y, int colour, void *data) { + Screen *screen = (Screen *)data; + screen->plotPoint(x, y, (uint8) colour); +} + +/** + * Draws a line from one point to another. This is only used for debugging. + * @param x0 x-coordinate of the start point + * @param y0 y-coordinate of the start point + * @param x1 x-coordinate of the end point + * @param y1 y-coordinate of the end point + * @param colour colour of the line + */ + +void Screen::drawLine(int x0, int y0, int x1, int y1, uint8 colour) { + Graphics::drawLine(x0, y0, x1, y1, colour, &plot, this); +} + +/** + * This function tells the driver the size of the background screen for the + * current location. + * @param w width of the current location + * @param h height of the current location + */ + +void Screen::setLocationMetrics(uint16 w, uint16 h) { + _locationWide = w; + _locationDeep = h; + setNeedFullRedraw(); +} + +/** + * Draws a parallax layer at the current position determined by the scroll. A + * parallax can be either foreground, background or the main screen. + */ + +void Screen::renderParallax(byte *ptr, int16 l) { + Parallax p; + int16 x, y; + Common::Rect r; + + p.read(ptr); + + if (_locationWide == _screenWide) + x = 0; + else + x = ((int32)((p.w - _screenWide) * _scrollX) / (int32)(_locationWide - _screenWide)); + + if (_locationDeep == _screenDeep - MENUDEEP * 2) + y = 0; + else + y = ((int32)((p.h - (_screenDeep - MENUDEEP * 2)) * _scrollY) / (int32)(_locationDeep - (_screenDeep - MENUDEEP * 2))); + + Common::Rect clipRect; + + // Leave enough space for the top and bottom menues + + clipRect.left = 0; + clipRect.right = _screenWide; + clipRect.top = MENUDEEP; + clipRect.bottom = _screenDeep - MENUDEEP; + + for (int j = 0; j < _yBlocks[l]; j++) { + for (int i = 0; i < _xBlocks[l]; i++) { + if (_blockSurfaces[l][i + j * _xBlocks[l]]) { + r.left = i * BLOCKWIDTH - x; + r.right = r.left + BLOCKWIDTH; + r.top = j * BLOCKHEIGHT - y + MENUDEEP; + r.bottom = r.top + BLOCKHEIGHT; + blitBlockSurface(_blockSurfaces[l][i + j * _xBlocks[l]], &r, &clipRect); + } + } + } + + _parallaxScrollX = _scrollX - x; + _parallaxScrollY = _scrollY - y; +} + +// Uncomment this when benchmarking the drawing routines. +#define LIMIT_FRAME_RATE + +/** + * Initialises the timers before the render loop is entered. + */ + +void Screen::initialiseRenderCycle() { + _initialTime = _vm->_system->getMillis(); + _totalTime = _initialTime + MILLISECSPERCYCLE; +} + +/** + * This function should be called when the game engine is ready to start the + * render cycle. + */ + +void Screen::startRenderCycle() { + _scrollXOld = _scrollX; + _scrollYOld = _scrollY; + + _startTime = _vm->_system->getMillis(); + + if (_startTime + _renderAverageTime >= _totalTime) { + _scrollX = _scrollXTarget; + _scrollY = _scrollYTarget; + _renderTooSlow = true; + } else { + _scrollX = (int16)(_scrollXOld + ((_scrollXTarget - _scrollXOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); + _scrollY = (int16)(_scrollYOld + ((_scrollYTarget - _scrollYOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); + _renderTooSlow = false; + } + + if (_scrollXOld != _scrollX || _scrollYOld != _scrollY) + setNeedFullRedraw(); + + _framesPerGameCycle = 0; +} + +/** + * This function should be called at the end of the render cycle. + * @return true if the render cycle is to be terminated, + * or false if it should continue + */ + +bool Screen::endRenderCycle() { + static int32 renderTimeLog[4] = { 60, 60, 60, 60 }; + static int32 renderCountIndex = 0; + int32 time; + + time = _vm->_system->getMillis(); + renderTimeLog[renderCountIndex] = time - _startTime; + _startTime = time; + _renderAverageTime = (renderTimeLog[0] + renderTimeLog[1] + renderTimeLog[2] + renderTimeLog[3]) >> 2; + + _framesPerGameCycle++; + + if (++renderCountIndex == RENDERAVERAGETOTAL) + renderCountIndex = 0; + + if (_renderTooSlow) { + initialiseRenderCycle(); + return true; + } + + if (_startTime + _renderAverageTime >= _totalTime) { + _totalTime += MILLISECSPERCYCLE; + _initialTime = time; + return true; + } + +#ifdef LIMIT_FRAME_RATE + if (_scrollXTarget == _scrollX && _scrollYTarget == _scrollY) { + // If we have already reached the scroll target sleep for the + // rest of the render cycle. + _vm->sleepUntil(_totalTime); + _initialTime = _vm->_system->getMillis(); + _totalTime += MILLISECSPERCYCLE; + return true; + } +#endif + + // This is an attempt to ensure that we always reach the scroll target. + // Otherwise the game frequently tries to pump out new interpolation + // frames without ever getting anywhere. + + if (ABS(_scrollX - _scrollXTarget) <= 1 && ABS(_scrollY - _scrollYTarget) <= 1) { + _scrollX = _scrollXTarget; + _scrollY = _scrollYTarget; + } else { + _scrollX = (int16)(_scrollXOld + ((_scrollXTarget - _scrollXOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); + _scrollY = (int16)(_scrollYOld + ((_scrollYTarget - _scrollYOld) * (_startTime - _initialTime + _renderAverageTime)) / (_totalTime - _initialTime)); + } + + if (_scrollX != _scrollXOld || _scrollY != _scrollYOld) + setNeedFullRedraw(); + +#ifdef LIMIT_FRAME_RATE + // Give the other threads some breathing space. This apparently helps + // against bug #875683, though I was never able to reproduce it for + // myself. + _vm->_system->delayMillis(10); +#endif + + return false; +} + +/** + * Reset scrolling stuff. This function is called from initBackground() + */ + +void Screen::resetRenderEngine() { + _parallaxScrollX = 0; + _parallaxScrollY = 0; + _scrollX = 0; + _scrollY = 0; +} + +/** + * This function should be called five times with either the parallax layer + * or a NULL pointer in order of background parallax to foreground parallax. + */ + +int32 Screen::initialiseBackgroundLayer(byte *parallax) { + Parallax p; + uint16 i, j, k; + byte *data; + byte *dst; + + debug(2, "initialiseBackgroundLayer"); + + assert(_layer < MAXLAYERS); + + if (!parallax) { + _layer++; + return RD_OK; + } + + p.read(parallax); + + _xBlocks[_layer] = (p.w + BLOCKWIDTH - 1) / BLOCKWIDTH; + _yBlocks[_layer] = (p.h + BLOCKHEIGHT - 1) / BLOCKHEIGHT; + + _blockSurfaces[_layer] = (BlockSurface **)calloc(_xBlocks[_layer] * _yBlocks[_layer], sizeof(BlockSurface *)); + if (!_blockSurfaces[_layer]) + return RDERR_OUTOFMEMORY; + + // Decode the parallax layer into a large chunk of memory + + byte *memchunk = (byte *)calloc(_xBlocks[_layer] * _yBlocks[_layer], BLOCKWIDTH * BLOCKHEIGHT); + if (!memchunk) + return RDERR_OUTOFMEMORY; + + for (i = 0; i < p.h; i++) { + uint32 p_offset = READ_LE_UINT32(parallax + Parallax::size() + 4 * i); + + if (!p_offset) + continue; + + byte *pLine = parallax + p_offset; + uint16 packets = READ_LE_UINT16(pLine); + uint16 offset = READ_LE_UINT16(pLine + 2); + + data = pLine + 4; + dst = memchunk + i * p.w + offset; + + if (!packets) { + memcpy(dst, data, p.w); + continue; + } + + bool zeros = false; + + for (j = 0; j < packets; j++) { + if (zeros) { + dst += *data; + offset += *data; + data++; + zeros = false; + } else if (!*data) { + data++; + zeros = true; + } else { + uint16 count = *data++; + memcpy(dst, data, count); + data += count; + dst += count; + offset += count; + zeros = true; + } + } + } + + // The large memory chunk is now divided into a number of smaller + // surfaces. For most parallax layers, we'll end up using less memory + // this way, and it will be faster to draw since completely transparent + // surfaces are discarded. + + for (i = 0; i < _xBlocks[_layer] * _yBlocks[_layer]; i++) { + bool block_has_data = false; + bool block_is_transparent = false; + + int x = BLOCKWIDTH * (i % _xBlocks[_layer]); + int y = BLOCKHEIGHT * (i / _xBlocks[_layer]); + + data = memchunk + p.w * y + x; + + for (j = 0; j < BLOCKHEIGHT; j++) { + for (k = 0; k < BLOCKWIDTH; k++) { + if (x + k < p.w && y + j < p.h) { + if (data[j * p.w + k]) + block_has_data = true; + else + block_is_transparent = true; + } + } + } + + // Only assign a surface to the block if it contains data. + + if (block_has_data) { + _blockSurfaces[_layer][i] = (BlockSurface *)malloc(sizeof(BlockSurface)); + + // Copy the data into the surfaces. + dst = _blockSurfaces[_layer][i]->data; + for (j = 0; j < BLOCKHEIGHT; j++) { + memcpy(dst, data, BLOCKWIDTH); + data += p.w; + dst += BLOCKWIDTH; + } + + _blockSurfaces[_layer][i]->transparent = block_is_transparent; + + } else + _blockSurfaces[_layer][i] = NULL; + } + + free(memchunk); + _layer++; + + return RD_OK; +} + +/** + * Should be called once after leaving the room to free up memory. + */ + +void Screen::closeBackgroundLayer() { + debug(2, "CloseBackgroundLayer"); + + for (int i = 0; i < MAXLAYERS; i++) { + if (_blockSurfaces[i]) { + for (int j = 0; j < _xBlocks[i] * _yBlocks[i]; j++) + if (_blockSurfaces[i][j]) + free(_blockSurfaces[i][j]); + free(_blockSurfaces[i]); + _blockSurfaces[i] = NULL; + } + } + + _layer = 0; +} + +#ifdef BACKEND_8BIT +void Screen::plotYUV(byte *lut, int width, int height, byte *const *dat) { + byte *buf = _buffer + ((480 - height) / 2) * RENDERWIDE + (640 - width) / 2; + + int x, y; + + int ypos = 0; + int cpos = 0; + int linepos = 0; + + for (y = 0; y < height; y += 2) { + for (x = 0; x < width; x += 2) { + int i = ((((dat[2][cpos] + ROUNDADD) >> SHIFT) * (BITDEPTH + 1)) + ((dat[1][cpos] + ROUNDADD) >> SHIFT)) * (BITDEPTH + 1); + cpos++; + + buf[linepos ] = lut[i + ((dat[0][ ypos ] + ROUNDADD) >> SHIFT)]; + buf[RENDERWIDE + linepos++] = lut[i + ((dat[0][width + ypos++] + ROUNDADD) >> SHIFT)]; + buf[linepos ] = lut[i + ((dat[0][ ypos ] + ROUNDADD) >> SHIFT)]; + buf[RENDERWIDE + linepos++] = lut[i + ((dat[0][width + ypos++] + ROUNDADD) >> SHIFT)]; + } + linepos += (2 * RENDERWIDE - width); + ypos += width; + } +} +#endif + + +} // End of namespace Sword2 diff --git a/engines/sword2/resman.cpp b/engines/sword2/resman.cpp new file mode 100644 index 0000000000..7387dc8f50 --- /dev/null +++ b/engines/sword2/resman.cpp @@ -0,0 +1,633 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/file.h" +#include "common/system.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/console.h" +#include "sword2/logic.h" +#include "sword2/memory.h" +#include "sword2/resman.h" +#include "sword2/router.h" +#include "sword2/sound.h" + +#define Debug_Printf _vm->_debugger->DebugPrintf + +namespace Sword2 { + +// Welcome to the easy resource manager - written in simple code for easy +// maintenance +// +// The resource compiler will create two files +// +// resource.inf which is a list of ascii cluster file names +// resource.tab which is a table which tells us which cluster a resource +// is located in and the number within the cluster + +enum { + BOTH = 0x0, // Cluster is on both CDs + CD1 = 0x1, // Cluster is on CD1 only + CD2 = 0x2, // Cluster is on CD2 only + LOCAL_CACHE = 0x4, // Cluster is cached on HDD + LOCAL_PERM = 0x8 // Cluster is on HDD. +}; + +struct CdInf { + uint8 clusterName[20]; // Null terminated cluster name. + uint8 cd; // Cd cluster is on and whether it is on the local drive or not. +}; + +ResourceManager::ResourceManager(Sword2Engine *vm) { + _vm = vm; + + // Until proven differently, assume we're on CD 1. This is so the start + // dialog will be able to play any music at all. + setCD(1); + + // We read in the resource info which tells us the names of the + // resource cluster files ultimately, although there might be groups + // within the clusters at this point it makes no difference. We only + // wish to know what resource files there are and what is in each + + Common::File file; + uint32 size; + byte *temp; + + _totalClusters = 0; + _resConvTable = NULL; + + if (!file.open("resource.inf")) + error("Cannot open resource.inf"); + + size = file.size(); + + // Get some space for the incoming resource file - soon to be trashed + temp = (byte *)malloc(size); + + if (file.read(temp, size) != size) { + file.close(); + error("init cannot *READ* resource.inf"); + } + + file.close(); + + // Ok, we've loaded in the resource.inf file which contains a list of + // all the files now extract the filenames. + + // Using this method the Gode generated resource.inf must have #0d0a on + // the last entry + + uint32 i = 0; + uint32 j = 0; + + do { + // item must have an #0d0a + while (temp[i] != 13) { + _resFiles[_totalClusters].fileName[j] = temp[i]; + i++; + j++; + } + + // NULL terminate our extracted string + _resFiles[_totalClusters].fileName[j] = '\0'; + _resFiles[_totalClusters].numEntries = -1; + _resFiles[_totalClusters].entryTab = NULL; + + // Reset position in current slot between entries, skip the + // 0x0a in the source and increase the number of clusters. + + j = 0; + i += 2; + _totalClusters++; + + // TODO: put overload check here + } while (i != size); + + free(temp); + + // Now load in the binary id to res conversion table + if (!file.open("resource.tab")) + error("Cannot open resource.tab"); + + // Find how many resources + size = file.size(); + + _totalResFiles = size / 4; + + // Table seems ok so malloc some space + _resConvTable = (uint16 *)malloc(size); + + for (i = 0; i < size / 2; i++) + _resConvTable[i] = file.readUint16LE(); + + if (file.ioFailed()) { + file.close(); + error("Cannot read resource.tab"); + } + + file.close(); + + if (!file.open("cd.inf")) + error("Cannot open cd.inf"); + + CdInf *cdInf = new CdInf[_totalClusters]; + + for (i = 0; i < _totalClusters; i++) { + file.read(cdInf[i].clusterName, sizeof(cdInf[i].clusterName)); + + cdInf[i].cd = file.readByte(); + + if (file.ioFailed()) + error("Cannot read cd.inf"); + + // It has been reported that there are two different versions + // of the cd.inf file: One where all clusters on CD also have + // the LOCAL_CACHE bit set. This bit is no longer used. To + // avoid future problems, let's normalize the flag once and for + // all here. + + if (cdInf[i].cd & LOCAL_PERM) + cdInf[i].cd = 0; + else if (cdInf[i].cd & CD1) + cdInf[i].cd = 1; + else if (cdInf[i].cd & CD2) + cdInf[i].cd = 2; + else + cdInf[i].cd = 0; + } + + file.close(); + + for (i = 0; i < _totalClusters; i++) { + for (j = 0; j < _totalClusters; j++) { + if (scumm_stricmp((char *)cdInf[j].clusterName, _resFiles[i].fileName) == 0) + break; + } + + if (j == _totalClusters) + error("%s is not in cd.inf", _resFiles[i].fileName); + + _resFiles[i].cd = cdInf[j].cd; + } + + delete [] cdInf; + + debug(1, "%d resources in %d cluster files", _totalResFiles, _totalClusters); + for (i = 0; i < _totalClusters; i++) + debug(2, "filename of cluster %d: -%s (%d)", i, _resFiles[i].fileName, _resFiles[i].cd); + + _resList = (Resource *)malloc(_totalResFiles * sizeof(Resource)); + + for (i = 0; i < _totalResFiles; i++) { + _resList[i].ptr = NULL; + _resList[i].size = 0; + _resList[i].refCount = 0; + _resList[i].prev = _resList[i].next = NULL; + } + _cacheStart = _cacheEnd = NULL; + _usedMem = 0; +} + +ResourceManager::~ResourceManager() { + Resource *res = _cacheStart; + while (res) { + _vm->_memory->memFree(res->ptr); + res = res->next; + } + for (uint i = 0; i < _totalClusters; i++) + free(_resFiles[i].entryTab); + free(_resList); + free(_resConvTable); +} + +/** + * Returns the address of a resource. Loads if not in memory. Retains a count. + */ + +byte *ResourceManager::openResource(uint32 res, bool dump) { + assert(res < _totalResFiles); + + // Is the resource in memory already? If not, load it. + + if (!_resList[res].ptr) { + // Fetch the correct file and read in the correct portion. + uint16 cluFileNum = _resConvTable[res * 2]; // points to the number of the ascii filename + assert(cluFileNum != 0xffff); + + // Relative resource within the file + // First we have to find the file via the _resConvTable + uint16 actual_res = _resConvTable[(res * 2) + 1]; + + debug(5, "openResource %s res %d", _resFiles[cluFileNum].fileName, res); + + // If we're loading a cluster that's only available from one + // of the CDs, remember which one so that we can play the + // correct speech and music. + + setCD(_resFiles[cluFileNum].cd); + + // Actually, as long as the file can be found we don't really + // care which CD it's on. But if we can't find it, keep asking + // for the CD until we do. + + Common::File *file = openCluFile(cluFileNum); + + if (_resFiles[cluFileNum].entryTab == NULL) { + // we didn't read from this file before, get its index table + readCluIndex(cluFileNum, file); + } + + uint32 pos = _resFiles[cluFileNum].entryTab[actual_res * 2 + 0]; + uint32 len = _resFiles[cluFileNum].entryTab[actual_res * 2 + 1]; + + file->seek(pos, SEEK_SET); + + debug(6, "res len %d", len); + + // Ok, we know the length so try and allocate the memory. + _resList[res].ptr = _vm->_memory->memAlloc(len, res); + _resList[res].size = len; + _resList[res].refCount = 0; + + file->read(_resList[res].ptr, len); + + debug(3, "Loaded resource '%s' from '%s' on CD %d (%d)", fetchName(_resList[res].ptr), _resFiles[cluFileNum].fileName, getCD(), _resFiles[cluFileNum].cd); + + if (dump) { + char buf[256]; + const char *tag; + Common::File out; + + switch (fetchType(_resList[res].ptr)) { + case ANIMATION_FILE: + tag = "anim"; + break; + case SCREEN_FILE: + tag = "layer"; + break; + case GAME_OBJECT: + tag = "object"; + break; + case WALK_GRID_FILE: + tag = "walkgrid"; + break; + case GLOBAL_VAR_FILE: + tag = "globals"; + break; + case PARALLAX_FILE_null: + tag = "parallax"; // Not used! + break; + case RUN_LIST: + tag = "runlist"; + break; + case TEXT_FILE: + tag = "text"; + break; + case SCREEN_MANAGER: + tag = "screen"; + break; + case MOUSE_FILE: + tag = "mouse"; + break; + case WAV_FILE: + tag = "wav"; + break; + case ICON_FILE: + tag = "icon"; + break; + case PALETTE_FILE: + tag = "palette"; + break; + default: + tag = "unknown"; + break; + } + +#if defined(MACOS_CARBON) + sprintf(buf, ":dumps:%s-%d.dmp", tag, res); +#else + sprintf(buf, "dumps/%s-%d.dmp", tag, res); +#endif + + if (!out.exists(buf, "")) { + if (out.open(buf, Common::File::kFileWriteMode, "")) + out.write(_resList[res].ptr, len); + } + } + + // close the cluster + file->close(); + delete file; + + _usedMem += len; + checkMemUsage(); + } else if (_resList[res].refCount == 0) + removeFromCacheList(_resList + res); + + _resList[res].refCount++; + + return _resList[res].ptr; +} + +void ResourceManager::closeResource(uint32 res) { + assert(res < _totalResFiles); + + // Don't try to close the resource if it has already been forcibly + // closed, e.g. by fnResetGlobals(). + + if (_resList[res].ptr == NULL) + return; + + assert(_resList[res].refCount > 0); + + _resList[res].refCount--; + if (_resList[res].refCount == 0) + addToCacheList(_resList + res); + + // It's tempting to free the resource immediately when refCount + // reaches zero, but that'd be a mistake. Closing a resource does not + // mean "I'm not going to use this resource any more". It means that + // "the next time I use this resource I'm going to ask for a new + // pointer to it". + // + // Since the original memory manager had to deal with memory + // fragmentation, keeping a resource open - and thus locked down to a + // specific memory address - was considered a bad thing. +} + +void ResourceManager::removeFromCacheList(Resource *res) { + if (_cacheStart == res) + _cacheStart = res->next; + + if (_cacheEnd == res) + _cacheEnd = res->prev; + + if (res->prev) + res->prev->next = res->next; + if (res->next) + res->next->prev = res->prev; + res->prev = res->next = NULL; +} + +void ResourceManager::addToCacheList(Resource *res) { + res->prev = NULL; + res->next = _cacheStart; + if (_cacheStart) + _cacheStart->prev = res; + _cacheStart = res; + if (!_cacheEnd) + _cacheEnd = res; +} + +Common::File *ResourceManager::openCluFile(uint16 fileNum) { + Common::File *file = new Common::File; + while (!file->open(_resFiles[fileNum].fileName)) { + // HACK: We have to check for this, or it'll be impossible to + // quit while the game is asking for the user to insert a CD. + // But recovering from this situation gracefully is just too + // much trouble, so quit now. + if (_vm->_quit) + g_system->quit(); + + // If the file is supposed to be on hard disk, or we're + // playing a demo, then we're in trouble if the file + // can't be found! + + if ((_vm->_features & GF_DEMO) || _resFiles[fileNum].cd == 0) + error("Could not find '%s'", _resFiles[fileNum].fileName); + + askForCD(_resFiles[fileNum].cd); + } + return file; +} + +void ResourceManager::readCluIndex(uint16 fileNum, Common::File *file) { + if (_resFiles[fileNum].entryTab == NULL) { + // we didn't read from this file before, get its index table + if (file == NULL) + file = openCluFile(fileNum); + else + file->incRef(); + + // 1st DWORD of a cluster is an offset to the look-up table + uint32 table_offset = file->readUint32LE(); + debug(6, "table offset = %d", table_offset); + uint32 tableSize = file->size() - table_offset; // the table is stored at the end of the file + file->seek(table_offset); + + assert((tableSize % 8) == 0); + _resFiles[fileNum].entryTab = (uint32*)malloc(tableSize); + _resFiles[fileNum].numEntries = tableSize / 8; + file->read(_resFiles[fileNum].entryTab, tableSize); + if (file->ioFailed()) + error("unable to read index table from file %s\n", _resFiles[fileNum].fileName); +#ifdef SCUMM_BIG_ENDIAN + for (int tabCnt = 0; tabCnt < _resFiles[fileNum].numEntries * 2; tabCnt++) + _resFiles[fileNum].entryTab[tabCnt] = FROM_LE_32(_resFiles[fileNum].entryTab[tabCnt]); +#endif + file->decRef(); + } +} + +/** + * Returns true if resource is valid, otherwise false. + */ + +bool ResourceManager::checkValid(uint32 res) { + // Resource number out of range + if (res >= _totalResFiles) + return false; + + // Points to the number of the ascii filename + uint16 parent_res_file = _resConvTable[res * 2]; + + // Null & void resource + if (parent_res_file == 0xffff) + return false; + + return true; +} + +/** + * Returns the total file length of a resource - i.e. all headers are included + * too. + */ + +uint32 ResourceManager::fetchLen(uint32 res) { + if (_resList[res].ptr) + return _resList[res].size; + + // Does this ever happen? + warning("fetchLen: Resource %u is not loaded; reading length from file", res); + + // Points to the number of the ascii filename + uint16 parent_res_file = _resConvTable[res * 2]; + + // relative resource within the file + uint16 actual_res = _resConvTable[(res * 2) + 1]; + + // first we have to find the file via the _resConvTable + // open the cluster file + + if (_resFiles[parent_res_file].entryTab == NULL) { + readCluIndex(parent_res_file); + } + return _resFiles[parent_res_file].entryTab[actual_res * 2 + 1]; +} + +void ResourceManager::checkMemUsage() { + while (_usedMem > MAX_MEM_CACHE) { + // we're using up more memory than we wanted to. free some old stuff. + // Newly loaded objects are added to the start of the list, + // we start freeing from the end, to free the oldest items first + if (_cacheEnd) { + Resource *tmp = _cacheEnd; + assert((tmp->refCount == 0) && (tmp->ptr) && (tmp->next == NULL)); + removeFromCacheList(tmp); + + _vm->_memory->memFree(tmp->ptr); + tmp->ptr = NULL; + _usedMem -= tmp->size; + } else { + warning("%d bytes of memory used, but cache list is empty!\n"); + return; + } + } +} + +void ResourceManager::remove(int res) { + if (_resList[res].ptr) { + removeFromCacheList(_resList + res); + + _vm->_memory->memFree(_resList[res].ptr); + _resList[res].ptr = NULL; + _resList[res].refCount = 0; + _usedMem -= _resList[res].size; + } +} + +/** + * Remove all res files from memory - ready for a total restart. This includes + * the player object and global variables resource. + */ + +void ResourceManager::removeAll() { + // We need to clear the FX queue, because otherwise the sound system + // will still believe that the sound resources are in memory, and that + // it's ok to close them. + + _vm->_sound->clearFxQueue(); + + for (uint i = 0; i < _totalResFiles; i++) + remove(i); +} + +/** + * Remove all resources from memory. + */ + +void ResourceManager::killAll(bool wantInfo) { + int nuked = 0; + + // We need to clear the FX queue, because otherwise the sound system + // will still believe that the sound resources are in memory, and that + // it's ok to close them. + + _vm->_sound->clearFxQueue(); + + for (uint i = 0; i < _totalResFiles; i++) { + // Don't nuke the global variables or the player object! + if (i == 1 || i == CUR_PLAYER_ID) + continue; + + if (_resList[i].ptr) { + if (wantInfo) + Debug_Printf("Nuked %5d: %s\n", i, fetchName(_resList[i].ptr)); + + remove(i); + nuked++; + } + } + + if (wantInfo) + Debug_Printf("Expelled %d resources\n", nuked); +} + +/** + * Like killAll but only kills objects (except George & the variable table of + * course) - ie. forcing them to reload & restart their scripts, which + * simulates the effect of a save & restore, thus checking that each object's + * re-entrant logic works correctly, and doesn't cause a statuette to + * disappear forever, or some plaster-filled holes in sand to crash the game & + * get James in trouble again. + */ + +void ResourceManager::killAllObjects(bool wantInfo) { + int nuked = 0; + + for (uint i = 0; i < _totalResFiles; i++) { + // Don't nuke the global variables or the player object! + if (i == 1 || i == CUR_PLAYER_ID) + continue; + + if (_resList[i].ptr) { + if (fetchType(_resList[i].ptr) == GAME_OBJECT) { + if (wantInfo) + Debug_Printf("Nuked %5d: %s\n", i, fetchName(_resList[i].ptr)); + + remove(i); + nuked++; + } + } + } + + if (wantInfo) + Debug_Printf("Expelled %d resources\n", nuked); +} + +void ResourceManager::askForCD(int cd) { + byte *textRes; + + // Stop any music from playing - so the system no longer needs the + // current CD - otherwise when we take out the CD, Windows will + // complain! + + _vm->_sound->stopMusic(true); + + textRes = openResource(2283); + _vm->_screen->displayMsg(_vm->fetchTextLine(textRes, 5 + cd) + 2, 0); + closeResource(2283); + + // The original code probably determined automagically when the correct + // CD had been inserted, but our backend doesn't support that, and + // anyway I don't know if all systems allow that sort of thing. So we + // wait for the user to press any key instead, or click the mouse. + // + // But just in case we ever try to identify the CDs by their labels, + // they should be: + // + // CD1: "RBSII1" (or "PCF76" for the PCF76 version, whatever that is) + // CD2: "RBSII2" +} + +} // End of namespace Sword2 diff --git a/engines/sword2/resman.h b/engines/sword2/resman.h new file mode 100644 index 0000000000..18b5cb8765 --- /dev/null +++ b/engines/sword2/resman.h @@ -0,0 +1,134 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef RESMAN_H +#define RESMAN_H + +namespace Common { + class File; +} + +#define MAX_MEM_CACHE (8 * 1024 * 1024) // we keep up to 8 megs of resource data files in memory +#define MAX_res_files 20 + +namespace Sword2 { + +class Sword2Engine; + +struct Resource { + byte *ptr; + uint32 size; + uint32 refCount; + Resource *next, *prev; +}; + +struct ResourceFile { + char fileName[20]; + int32 numEntries; + uint32 *entryTab; + uint8 cd; +}; + +class ResourceManager { +private: + Common::File *openCluFile(uint16 fileNum); + void readCluIndex(uint16 fileNum, Common::File *file = NULL); + void removeFromCacheList(Resource *res); + void addToCacheList(Resource *res); + void checkMemUsage(); + + Sword2Engine *_vm; + + int _curCD; + uint32 _totalResFiles; + uint32 _totalClusters; + + // Gode generated res-id to res number/rel number conversion table + + uint16 *_resConvTable; + ResourceFile _resFiles[MAX_res_files]; + Resource *_resList; + + Resource *_cacheStart, *_cacheEnd; + uint32 _usedMem; // amount of used memory in bytes + +public: + ResourceManager(Sword2Engine *vm); // read in the config file + ~ResourceManager(); + + uint32 getNumResFiles() { return _totalResFiles; } + uint32 getNumClusters() { return _totalClusters; } + ResourceFile *getResFiles() { return _resFiles; } + Resource *getResList() { return _resList; } + + byte *openResource(uint32 res, bool dump = false); + void closeResource(uint32 res); + + bool checkValid(uint32 res); + uint32 fetchLen(uint32 res); + uint8 fetchType(uint32 res) { + byte *ptr = openResource(res); + uint8 type = ptr[0]; + closeResource(res); + + return type; + } + + uint8 fetchType(byte *ptr) { + return ptr[0]; + } + + byte *fetchName(uint32 res, byte *buf) { + byte *ptr = openResource(res); + memcpy(buf, ptr + 10, NAME_LEN); + closeResource(res); + + return buf; + } + + byte *fetchName(byte *ptr) { + return ptr + 10; + } + + // Prompts the user for the specified CD. + void askForCD(int cd); + + void setCD(int cd) { + if (cd) + _curCD = cd; + } + + int getCD() { + return _curCD; + } + + void remove(int res); + void removeAll(); + + // ----console commands + + void killAll(bool wantInfo); + void killAllObjects(bool wantInfo); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/router.cpp b/engines/sword2/router.cpp new file mode 100644 index 0000000000..bbca609e3c --- /dev/null +++ b/engines/sword2/router.cpp @@ -0,0 +1,2506 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// --------------------------------------------------------------------------- +// ROUTER.CPP by James +// +// A rehash of Jeremy's original jrouter.c, containing low-level system +// routines for calculating routes between points inside a walk-grid, and +// constructing walk animations from mega-sets. +// +// jrouter.c underwent 2 major reworks from the original: +// (1) Restructured to allow more flexibility in the mega-sets, ie. more info +// taken from the walk-data +// - the new George & Nico mega-sets & walk-data were then tested & +// tweaked in the Sword1 system +// (2) Updated for the new Sword2 system, ie. new object structures +// - now compatible with Sword2, the essential code already having been +// tested +// +// --------------------------------------------------------------------------- + +/**************************************************************************** + * JROUTER.C polygon router with modular walks + * using a tree of modules + * 21 july 94 + * 3 november 94 + * System currently works by scanning grid data and coming up with a ROUTE + * as a series of way points(nodes), the smoothest eight directional PATH + * through these nodes is then found, and a WALK created to fit the PATH. + * + * Two funtions are called by the user, RouteFinder creates a route as a + * module list, HardWalk creates an animation list from the module list. + * The split is only provided to allow the possibility of turning the + * autorouter over two game cycles. + **************************************************************************** + * + * Routine timings on osborne 486 + * + * Read floor resource (file already loaded) 112 pixels + * + * Read mega resource (file already loaded) 112 pixels + * + * + * + **************************************************************************** + * + * Modified 12 Oct 95 + * + * Target Points within 1 pixel of a line are ignored ??? + * + * Modules split into Points within 1 pixel of a line are ignored ??? + * + **************************************************************************** + * + * TOTALLY REHASHED BY JAMES FOR NEW MEGAS USING OLD SYSTEM + * THEN REINCARNATED BY JAMES FOR NEW MEGAS USING NEW SYSTEM + * + ****************************************************************************/ + +#include "common/stdafx.h" +#include "common/stream.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/resman.h" +#include "sword2/router.h" + +namespace Sword2 { + +//---------------------------------------------------------- +// (4) WALK-GRID FILES +//---------------------------------------------------------- +// a walk-grid file consists of: +// +// standard file header +// walk-grid file header +// walk-grid data + +// Walk-Grid Header - taken directly from old "header.h" in STD_INC + +struct WalkGridHeader { + int32 numBars; // number of bars on the floor + int32 numNodes; // number of nodes +}; + +uint8 Router::returnSlotNo(uint32 megaId) { + if (_vm->_logic->readVar(ID) == CUR_PLAYER_ID) { + // George (8) + return 0; + } else { + // One of Nico's mega id's + return 1; + } +} + +void Router::allocateRouteMem() { + uint8 slotNo; + + // Player character always always slot 0, while the other mega + // (normally Nico) always uses slot 1 + // Better this way, so that if mega object removed from memory while + // in middle of route, the old route will be safely cleared from + // memory just before they create a new one + + slotNo = returnSlotNo(_vm->_logic->readVar(ID)); + + // if this slot is already used, then it can't be needed any more + // because this id is creating a new route! + + if (_routeSlots[slotNo]) + freeRouteMem(); + + _routeSlots[slotNo] = (WalkData *)malloc(sizeof(WalkData) * O_WALKANIM_SIZE); + + // 12000 bytes were used for this in Sword1 mega compacts, based on + // 20 bytes per 'WalkData' frame + // ie. allowing for 600 frames including end-marker + // Now 'WalkData' is 8 bytes, so 8*600 = 4800 bytes. + // Note that a 600 frame walk lasts about 48 seconds! + // (600fps / 12.5s = 48s) + + // mega keeps note of which slot contains the pointer to it's walk + // animation mem block + // +1 so that '0' can mean "not walking" + // megaObject->route_slot_id = slotNo + 1; +} + +WalkData *Router::getRouteMem() { + uint8 slotNo = returnSlotNo(_vm->_logic->readVar(ID)); + + return (WalkData *)_routeSlots[slotNo]; +} + +void Router::freeRouteMem() { + uint8 slotNo = returnSlotNo(_vm->_logic->readVar(ID)); + + free(_routeSlots[slotNo]); + _routeSlots[slotNo] = NULL; +} + +void Router::freeAllRouteMem() { + for (int i = 0; i < TOTAL_ROUTE_SLOTS; i++) { + free(_routeSlots[i]); + _routeSlots[i] = NULL; + } +} + +int32 Router::routeFinder(byte *ob_mega, byte *ob_walkdata, int32 x, int32 y, int32 dir) { + /********************************************************************* + * RouteFinder.C polygon router with modular walks + * 21 august 94 + * 3 november 94 + * routeFinder creates a list of modules that enables HardWalk to + * create an animation list. + * + * routeFinder currently works by scanning grid data and coming up + * with a ROUTE as a series of way points(nodes), the smoothest eight + * directional PATH through these nodes is then found, this + * information is made available to HardWalk for a WALK to be created + * to fit the PATH. + * + * 30 november 94 return values modified + * + * return 0 = failed to find a route + * + * 1 = found a route + * + * 2 = mega already at target + * + *********************************************************************/ + + int32 routeFlag = 0; + int32 solidFlag = 0; + WalkData *walkAnim; + + // megaId = id; + + setUpWalkGrid(ob_mega, x, y, dir); + loadWalkData(ob_walkdata); + + walkAnim = getRouteMem(); + + // All route data now loaded start finding a route + + // Check if we can get a route through the floor. changed 12 Oct95 JPS + + routeFlag = getRoute(); + + switch (routeFlag) { + case 2: + // special case for zero length route + + // if target direction specified as any + if (_targetDir > 7) + _targetDir = _startDir; + + // just a turn on the spot is required set an end module for + // the route let the animator deal with it + // modularPath is normally set by extractRoute + + _modularPath[0].dir = _startDir; + _modularPath[0].num = 0; + _modularPath[0].x = _startX; + _modularPath[0].y = _startY; + _modularPath[1].dir = _targetDir; + _modularPath[1].num = 0; + _modularPath[1].x = _startX; + _modularPath[1].y = _startY; + _modularPath[2].dir = 9; + _modularPath[2].num = ROUTE_END_FLAG; + + slidyWalkAnimator(walkAnim); + routeFlag = 2; + break; + case 1: + // A normal route. Convert the route to an exact path + smoothestPath(); + + // The Route had waypoints and direction options + + // The Path is an exact set of lines in 8 directions that + // reach the target. + + // The path is in module format, but steps taken in each + // direction are not accurate + + // if target dir = 8 then the walk isn't linked to an anim so + // we can create a route without sliding and miss the exact + // target + +#ifndef FORCE_SLIDY + if (_targetDir == 8) { + // can end facing ANY direction (ie. exact end + // position not vital) - so use SOLID walk to + // avoid sliding to exact position + + solidPath(); + solidFlag = solidWalkAnimator(walkAnim); + } +#endif + + if (!solidFlag) { + // if we failed to create a SOLID route, do a SLIDY + // one instead + + slidyPath(); + slidyWalkAnimator(walkAnim); + } + + break; + default: + // Route didn't reach target so assume point was off the floor + // routeFlag = 0; + break; + } + + return routeFlag; // send back null route +} + +int32 Router::getRoute() { + /********************************************************************* + * GetRoute.C extract a path from walk grid + * 12 october 94 + * + * GetRoute currently works by scanning grid data and coming up with + * a ROUTE as a series of way points(nodes). + * + * static routeData _route[O_ROUTE_SIZE]; + * + * return 0 = failed to find a route + * + * 1 = found a route + * + * 2 = mega already at target + * + * 3 = failed to find a route because target was on a line + * + *********************************************************************/ + + int32 routeGot = 0; + + if (_startX == _targetX && _startY == _targetY) + routeGot = 2; + else { + // 'else' added by JEL (23jan96) otherwise 'routeGot' affected + // even when already set to '2' above - causing some 'turns' + // to walk downwards on the spot + + // returns 3 if target on a line ( +- 1 pixel ) + routeGot = checkTarget(_targetX, _targetY); + } + + if (routeGot == 0) { + // still looking for a route check if target is within a pixel + // of a line + + // scan through the nodes linking each node to its nearest + // neighbour until no more nodes change + + // This is the routine that finds a route using scan() + + int32 level = 1; + + while (scan(level)) + level++; + + // Check to see if the route reached the target + + if (_node[_nNodes].dist < 9999) { + // it did so extract the route as nodes and the + // directions to go between each node + + routeGot = 1; + extractRoute(); + + // route.X,route.Y and route.Dir now hold all the + // route infomation with the target dir or route + // continuation + } + } + + return routeGot; +} + +// THE SLIDY PATH ROUTINES + +int32 Router::smoothestPath() { + // This is the second big part of the route finder and the the only + // bit that tries to be clever (the other bits are clever). + // + // This part of the autorouter creates a list of modules from a set of + // lines running across the screen. The task is complicated by two + // things: + // + // Firstly in choosing a route through the maze of nodes the routine + // tries to minimise the amount of each individual turn avoiding 90 + // degree and greater turns (where possible) and reduces the total + // number of turns (subject to two 45 degree turns being better than + // one 90 degree turn). + // + // Secondly when walking in a given direction the number of steps + // required to reach the end of that run is not calculated accurately. + // This is because I was unable to derive a function to relate number + // of steps taken between two points to the shrunken step size + + int i; + int32 steps = 0; + int32 lastDir; + int32 tempturns[4]; + int32 turns[4]; + int32 turntable[NO_DIRECTIONS] = { 0, 1, 3, 5, 7, 5, 3, 1 }; + + // route.X route.Y and route.Dir start at far end + + _smoothPath[0].x = _startX; + _smoothPath[0].y = _startY; + _smoothPath[0].dir = _startDir; + _smoothPath[0].num = 0; + + lastDir = _startDir; + + // for each section of the route + + for (int p = 0; p < _routeLength; p++) { + int32 dirS = _route[p].dirS; + int32 dirD = _route[p].dirD; + int32 nextDirS = _route[p + 1].dirS; + int32 nextDirD = _route[p + 1].dirD; + + // Check directions into and out of a pair of nodes going in + int32 dS = dirS - lastDir; + if (dS < 0) + dS = dS + NO_DIRECTIONS; + + int32 dD = dirD - lastDir; + if (dD < 0) + dD = dD + NO_DIRECTIONS; + + // coming out + int32 dSS = dirS - nextDirS; + if (dSS < 0) + dSS = dSS + NO_DIRECTIONS; + + int32 dDD = dirD - nextDirD; + if (dDD < 0) + dDD = dDD + NO_DIRECTIONS; + + int32 dSD = dirS - nextDirD; + if (dSD < 0) + dSD = dSD + NO_DIRECTIONS; + + int32 dDS = dirD - nextDirS; + if (dDS < 0) + dDS = dDS + NO_DIRECTIONS; + + // Determine the amount of turning involved in each possible + // path + + dS = turntable[dS]; + dD = turntable[dD]; + dSS = turntable[dSS]; + dDD = turntable[dDD]; + dSD = turntable[dSD]; + dDS = turntable[dDS]; + + // get the best path out ie assume next section uses best + // direction + + if (dSD < dSS) + dSS = dSD; + + if (dDS < dDD) + dDD = dDS; + + // Rate each option. Split routes look crap so weight against + // them + + int32 SS = dS + dSS + 3; + int32 SD = dS + dDD; + int32 DS = dD + dSS; + int32 DD = dD + dDD + 3; + + // set up turns as a sorted array of the turn values + + tempturns[0] = SS; + turns[0] = 0; + tempturns[1] = SD; + turns[1] = 1; + tempturns[2] = DS; + turns[2] = 2; + tempturns[3] = DD; + turns[3] = 3; + + for (i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (tempturns[j] > tempturns[j + 1]) { + SWAP(turns[j], turns[j + 1]); + SWAP(tempturns[j], tempturns[j + 1]); + } + } + } + + // best option matched in order of the priority we would like + // to see on the screen but each option must be checked to see + // if it can be walked + + int32 options = newCheck(1, _route[p].x, _route[p].y, _route[p + 1].x, _route[p + 1].y); + + assert(options); + + i = 0; + steps = 0; + + do { + int32 opt = 1 << turns[i]; + if (options & opt) + steps = smoothCheck(turns[i], p, dirS, dirD); + i++; + } while (steps == 0 && i < 4); + + assert(steps); + + // route.X route.Y route.dir and bestTurns start at far end + } + + // best turns will end heading as near as possible to target dir rest + // is down to anim for now + + _smoothPath[steps].dir = 9; + _smoothPath[steps].num = ROUTE_END_FLAG; + return 1; +} + +int32 Router::smoothCheck(int32 best, int32 p, int32 dirS, int32 dirD) { + /********************************************************************* + * Slip sliding away + * This path checker checks to see if a walk that exactly follows the + * path would be valid. This should be inherently true for atleast one + * of the turn options. + * No longer checks the data it only creates the smoothPath array JPS + *********************************************************************/ + + static int32 k; + int32 dsx, dsy; + int32 ddx, ddy; + int32 ss0, ss1, ss2; + int32 sd0, sd1, sd2; + + if (p == 0) + k = 1; + + int32 x = _route[p].x; + int32 y = _route[p].y; + int32 x2 = _route[p + 1].x; + int32 y2 = _route[p + 1].y; + int32 ldx = x2 - x; + int32 ldy = y2 - y; + int32 dirX = 1; + int32 dirY = 1; + + if (ldx < 0) { + ldx = -ldx; + dirX = -1; + } + + if (ldy < 0) { + ldy = -ldy; + dirY = -1; + } + + // set up sd0-ss2 to reflect possible movement in each direction + + if (dirS == 0 || dirS == 4) { // vert and diag + ddx = ldx; + ddy = (ldx * _diagonaly) / _diagonalx; + dsy = ldy - ddy; + ddx = ddx * dirX; + ddy = ddy * dirY; + dsy = dsy * dirY; + dsx = 0; + + sd0 = (ddx + _modX[dirD] / 2) / _modX[dirD]; + ss0 = (dsy + _modY[dirS] / 2) / _modY[dirS]; + sd1 = sd0 / 2; + ss1 = ss0 / 2; + sd2 = sd0 - sd1; + ss2 = ss0 - ss1; + } else { + ddy = ldy; + ddx = (ldy * _diagonalx) / _diagonaly; + dsx = ldx - ddx; + ddy = ddy * dirY; + ddx = ddx * dirX; + dsx = dsx * dirX; + dsy = 0; + + sd0 = (ddy + _modY[dirD] / 2) / _modY[dirD]; + ss0 = (dsx + _modX[dirS] / 2) / _modX[dirS]; + sd1 = sd0 / 2; + ss1 = ss0 / 2; + sd2 = sd0 - sd1; + ss2 = ss0 - ss1; + } + + switch (best) { + case 0: // halfsquare, diagonal, halfsquare + _smoothPath[k].x = x + dsx / 2; + _smoothPath[k].y = y + dsy / 2; + _smoothPath[k].dir = dirS; + _smoothPath[k].num = ss1; + k++; + + _smoothPath[k].x = x + dsx / 2 + ddx; + _smoothPath[k].y = y + dsy / 2 + ddy; + _smoothPath[k].dir = dirD; + _smoothPath[k].num = sd0; + k++; + + _smoothPath[k].x = x + dsx + ddx; + _smoothPath[k].y = y + dsy + ddy; + _smoothPath[k].dir = dirS; + _smoothPath[k].num = ss2; + k++; + + break; + case 1: // square, diagonal + _smoothPath[k].x = x + dsx; + _smoothPath[k].y = y + dsy; + _smoothPath[k].dir = dirS; + _smoothPath[k].num = ss0; + k++; + + _smoothPath[k].x = x2; + _smoothPath[k].y = y2; + _smoothPath[k].dir = dirD; + _smoothPath[k].num = sd0; + k++; + + break; + case 2: // diagonal square + _smoothPath[k].x = x + ddx; + _smoothPath[k].y = y + ddy; + _smoothPath[k].dir = dirD; + _smoothPath[k].num = sd0; + k++; + + _smoothPath[k].x = x2; + _smoothPath[k].y = y2; + _smoothPath[k].dir = dirS; + _smoothPath[k].num = ss0; + k++; + + break; + default: // halfdiagonal, square, halfdiagonal + _smoothPath[k].x = x + ddx / 2; + _smoothPath[k].y = y + ddy / 2; + _smoothPath[k].dir = dirD; + _smoothPath[k].num = sd1; + k++; + + _smoothPath[k].x = x + dsx + ddx / 2; + _smoothPath[k].y = y + dsy + ddy / 2; + _smoothPath[k].dir = dirS; + _smoothPath[k].num = ss0; + k++; + + _smoothPath[k].x = x2; + _smoothPath[k].y = y2; + _smoothPath[k].dir = dirD; + _smoothPath[k].num = sd2; + k++; + + break; + } + + return k; +} + +void Router::slidyPath() { + /********************************************************************* + * slidyPath creates a path based on part steps with no sliding to get + * as near as possible to the target without any sliding this routine + * is intended for use when just clicking about. + * + * produce a module list from the line data + *********************************************************************/ + + int32 smooth = 1; + int32 slidy = 1; + + // strip out the short sections + + _modularPath[0].x = _smoothPath[0].x; + _modularPath[0].y = _smoothPath[0].y; + _modularPath[0].dir = _smoothPath[0].dir; + _modularPath[0].num = 0; + + while (_smoothPath[smooth].num < ROUTE_END_FLAG) { + int32 scale = _scaleA * _smoothPath[smooth].y + _scaleB; + int32 deltaX = _smoothPath[smooth].x - _modularPath[slidy - 1].x; + int32 deltaY = _smoothPath[smooth].y - _modularPath[slidy - 1].y; + // quarter a step minimum + int32 stepX = (scale * _modX[_smoothPath[smooth].dir]) >> 19; + int32 stepY = (scale * _modY[_smoothPath[smooth].dir]) >> 19; + + if (ABS(deltaX) >= ABS(stepX) && ABS(deltaY) >= ABS(stepY)) { + _modularPath[slidy].x = _smoothPath[smooth].x; + _modularPath[slidy].y = _smoothPath[smooth].y; + _modularPath[slidy].dir = _smoothPath[smooth].dir; + _modularPath[slidy].num = 1; + slidy++; + } + smooth++; + } + + // in case the last bit had no steps + + if (slidy > 1) { + _modularPath[slidy - 1].x = _smoothPath[smooth - 1].x; + _modularPath[slidy - 1].y = _smoothPath[smooth - 1].y; + } + + // set up the end of the walk + + _modularPath[slidy].x = _smoothPath[smooth - 1].x; + _modularPath[slidy].y = _smoothPath[smooth - 1].y; + _modularPath[slidy].dir = _targetDir; + _modularPath[slidy].num = 0; + slidy++; + + _modularPath[slidy].x = _smoothPath[smooth - 1].x; + _modularPath[slidy].y = _smoothPath[smooth - 1].y; + _modularPath[slidy].dir = 9; + _modularPath[slidy].num = ROUTE_END_FLAG; + slidy++; +} + +// SLOW IN + +bool Router::addSlowInFrames(WalkData *walkAnim) { + if (_walkData.usingSlowInFrames && _modularPath[1].num > 0) { + for (int slowInFrameNo = 0; slowInFrameNo < _walkData.nSlowInFrames[_currentDir]; slowInFrameNo++) { + walkAnim[_stepCount].frame = _firstSlowInFrame[_currentDir] + slowInFrameNo; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = _currentDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + return true; + } + + return false; +} + +void Router::earlySlowOut(byte *ob_mega, byte *ob_walkdata) { + int32 slowOutFrameNo; + int32 walk_pc; + WalkData *walkAnim; + + ObjectMega obMega(ob_mega); + + debug(5, "EARLY SLOW-OUT"); + + loadWalkData(ob_walkdata); + + debug(5, "********************************"); + debug(5, "_framesPerStep = %d", _framesPerStep); + debug(5, "_numberOfSlowOutFrames = %d", _numberOfSlowOutFrames); + debug(5, "_firstWalkingTurnLeftFrame = %d", _firstWalkingTurnLeftFrame); + debug(5, "_firstWalkingTurnRightFrame = %d", _firstWalkingTurnRightFrame); + debug(5, "_firstSlowOutFrame = %d", _firstSlowOutFrame); + debug(5, "********************************"); + + walk_pc = obMega.getWalkPc(); + + walkAnim = getRouteMem(); + + // if this mega does actually have slow-out frames + if (_walkData.usingSlowOutFrames) { + // overwrite the next step (half a cycle) of the walk + // (ie .step - 0..5) + + do { + debug(5, "STEP NUMBER: walkAnim[%d].step = %d", walk_pc, walkAnim[walk_pc].step); + debug(5, "ORIGINAL FRAME: walkAnim[%d].frame = %d", walk_pc, walkAnim[walk_pc].frame); + + // map from existing walk frame across to correct + // frame number of slow-out - remember, there may be + // more slow-out frames than walk-frames! + + if (walkAnim[walk_pc].frame >= _firstWalkingTurnRightFrame) { + // if it's a walking turn-right, rather than a + // normal step, then map it to a normal step + // frame first + + walkAnim[walk_pc].frame -= _firstWalkingTurnRightFrame; + debug(5, "MAPPED TO WALK: walkAnim[%d].frame = %d (walking turn-right frame --> walk frame)", walk_pc, walkAnim[walk_pc].frame); + } else if (walkAnim[walk_pc].frame >= _firstWalkingTurnLeftFrame) { + // if it's a walking turn-left, rather than a + // normal step, then map it to a normal step + // frame first + + walkAnim[walk_pc].frame -= _firstWalkingTurnLeftFrame; + debug(5, "MAPPED TO WALK: walkAnim[%d].frame = %d (walking turn-left frame --> walk frame)", walk_pc, walkAnim[walk_pc].frame); + } + + walkAnim[walk_pc].frame += _firstSlowOutFrame + ((walkAnim[walk_pc].frame / _framesPerStep) * (_numberOfSlowOutFrames - _framesPerStep)); + walkAnim[walk_pc].step = 0; + debug(5, "SLOW-OUT FRAME: walkAnim[%d].frame = %d",walk_pc, walkAnim[walk_pc].frame); + walk_pc++; + } while (walkAnim[walk_pc].step > 0); + + // add stationary frame(s) (OPTIONAL) + + for (slowOutFrameNo = _framesPerStep; slowOutFrameNo < _numberOfSlowOutFrames; slowOutFrameNo++) { + walkAnim[walk_pc].frame = walkAnim[walk_pc - 1].frame + 1; + debug(5, "EXTRA FRAME: walkAnim[%d].frame = %d", walk_pc, walkAnim[walk_pc].frame); + walkAnim[walk_pc].step = 0; + walkAnim[walk_pc].dir = walkAnim[walk_pc - 1].dir; + walkAnim[walk_pc].x = walkAnim[walk_pc - 1].x; + walkAnim[walk_pc].y = walkAnim[walk_pc - 1].y; + walk_pc++; + } + } else { + // this mega doesn't have slow-out frames + // stand in current direction + + walkAnim[walk_pc].frame = _firstStandFrame + walkAnim[walk_pc - 1].dir; + walkAnim[walk_pc].step = 0; + walkAnim[walk_pc].dir = walkAnim[walk_pc - 1].dir; + walkAnim[walk_pc].x = walkAnim[walk_pc - 1].x; + walkAnim[walk_pc].y = walkAnim[walk_pc - 1].y; + walk_pc++; + } + + // end of sequence + walkAnim[walk_pc].frame = 512; + + // so that this doesn't happen again while 'george_walking' is still + // '2' + walkAnim[walk_pc].step = 99; +} + +// SLOW OUT + +void Router::addSlowOutFrames(WalkData *walkAnim) { + int32 slowOutFrameNo; + + // if the mega did actually walk, we overwrite the last step (half a + // cycle) with slow-out frames + add any necessary stationary frames + + if (_walkData.usingSlowOutFrames && _lastCount >= _framesPerStep) { + // place stop frames here + // slowdown at the end of the last walk + + slowOutFrameNo = _lastCount - _framesPerStep; + + debug(5, "SLOW OUT: slowOutFrameNo(%d) = _lastCount(%d) - _framesPerStep(%d)", slowOutFrameNo, _lastCount, _framesPerStep); + + // overwrite the last step (half a cycle) of the walk + + do { + // map from existing walk frame across to correct + // frame number of slow-out - remember, there may be + // more slow-out frames than walk-frames! + + walkAnim[slowOutFrameNo].frame += _firstSlowOutFrame + ((walkAnim[slowOutFrameNo].frame / _framesPerStep) * (_numberOfSlowOutFrames - _framesPerStep)); + + // because no longer a normal walk-step + walkAnim[slowOutFrameNo].step = 0; + + debug(5, "walkAnim[%d].frame = %d",slowOutFrameNo,walkAnim[slowOutFrameNo].frame); + slowOutFrameNo++; + } while (slowOutFrameNo < _lastCount); + + // add stationary frame(s) (OPTIONAL) + + for (slowOutFrameNo = _framesPerStep; slowOutFrameNo < _numberOfSlowOutFrames; slowOutFrameNo++) { + walkAnim[_stepCount].frame = walkAnim[_stepCount - 1].frame + 1; + + debug(5, "EXTRA FRAMES: walkAnim[%d].frame = %d", _stepCount, walkAnim[_stepCount].frame); + + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = walkAnim[_stepCount - 1].dir; + walkAnim[_stepCount].x = walkAnim[_stepCount - 1].x; + walkAnim[_stepCount].y = walkAnim[_stepCount - 1].y; + _stepCount++; + } + } +} + +void Router::slidyWalkAnimator(WalkData *walkAnim) { + /********************************************************************* + * Skidding every where HardWalk creates an animation that exactly + * fits the smoothPath and uses foot slipping to fit whole steps into + * the route + * + * Parameters: georgeg, mouseg + * Returns: rout + * + * produce a module list from the line data + *********************************************************************/ + + static int32 left = 0; + int32 p; + int32 lastDir; + int32 lastRealDir; + int32 turnDir; + int32 scale; + int32 step; + int32 module; + int32 moduleEnd; + int32 module16X; + int32 module16Y; + int32 stepX; + int32 stepY; + int32 errorX; + int32 errorY; + int32 lastErrorX; + int32 lastErrorY; + int32 frameCount; + int32 frames; + + p = 0; + lastDir = _modularPath[0].dir; + _currentDir = _modularPath[1].dir; + + if (_currentDir == NO_DIRECTIONS) + _currentDir = lastDir; + + _moduleX = _startX; + _moduleY = _startY; + module16X = _moduleX << 16; + module16Y = _moduleY << 16; + _stepCount = 0; + + // START THE WALK WITH THE FIRST STANDFRAME THIS MAY CAUSE A DELAY + // BUT IT STOPS THE PLAYER MOVING FOR COLLISIONS ARE DETECTED + + debug(5, "SLIDY: STARTING THE WALK"); + + module = _framesPerChar + lastDir; + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + + // TURN TO START THE WALK + + debug(5, "SLIDY: TURNING TO START THE WALK"); + // rotate if we need to + + if (lastDir != _currentDir) { + // get the direction to turn + turnDir = _currentDir - lastDir; + if (turnDir < 0) + turnDir += NO_DIRECTIONS; + + if (turnDir > 4) + turnDir = -1; + else if (turnDir > 0) + turnDir = 1; + + // rotate to new walk direction + // for george and nico put in a head turn at the start + + if (_walkData.usingStandingTurnFrames) { + // new frames for turn frames 29oct95jps + if (turnDir < 0) + module = _firstStandingTurnLeftFrame + lastDir; + else + module = _firstStandingTurnRightFrame + lastDir; + + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + // rotate till were facing new dir then go back 45 degrees + while (lastDir != _currentDir) { + lastDir += turnDir; + + // new frames for turn frames 29oct95jps + if (turnDir < 0) { + if ( lastDir < 0) + lastDir += NO_DIRECTIONS; + module = _firstStandingTurnLeftFrame + lastDir; + } else { + if ( lastDir > 7) + lastDir -= NO_DIRECTIONS; + module = _firstStandingTurnRightFrame + lastDir; + } + + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + // the back 45 degrees bit + // step back one because new head turn for george takes us + // past the new dir + _stepCount--; + } + + // his head is in the right direction + lastRealDir = _currentDir; + + // SLIDY: THE SLOW IN + + addSlowInFrames(walkAnim); + + // THE WALK + + debug(5, "SLIDY: THE WALK"); + + // start the walk on the left or right leg, depending on how the + // slow-in frames were drawn + + // (0 = left; 1 = right) + + if (_walkData.leadingLeg[_currentDir] == 0) { + // start the walk on the left leg (ie. at beginning of the + // first step of the walk cycle) + left = 0; + } else { + // start the walk on the right leg (ie. at beginning of the + // second step of the walk cycle) + left = _framesPerStep; + } + + _lastCount = _stepCount; + + // this ensures that we don't put in turn frames for the start + lastDir = 99; + + // this ensures that we don't put in turn frames for the start + _currentDir = 99; + + do { + assert(_stepCount < O_WALKANIM_SIZE); + while (_modularPath[p].num == 0) { + p++; + if (_currentDir != 99) + lastRealDir = _currentDir; + lastDir = _currentDir; + _lastCount = _stepCount; + } + + // calculate average amount to lose in each step on the way + // to the next node + + _currentDir = _modularPath[p].dir; + + if (_currentDir < NO_DIRECTIONS) { + module = _currentDir * _framesPerStep * 2 + left; + + if (left == 0) + left = _framesPerStep; + else + left = 0; + + moduleEnd = module + _framesPerStep; + step = 0; + scale = (_scaleA * _moduleY + _scaleB); + + do { + module16X += _walkData.dx[module] * scale; + module16Y += _walkData.dy[module] * scale; + _moduleX = module16X >> 16; + _moduleY = module16Y >> 16; + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = step; // normally 0,1,2,3,4,5,0,1,2,etc + walkAnim[_stepCount].dir = _currentDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + step++; + module++; + } while (module < moduleEnd); + + stepX = _modX[_modularPath[p].dir]; + stepY = _modY[_modularPath[p].dir]; + errorX = _modularPath[p].x - _moduleX; + errorX = errorX * stepX; + errorY = _modularPath[p].y - _moduleY; + errorY = errorY * stepY; + + if (errorX < 0 || errorY < 0) { + _modularPath[p].num = 0; // the end of the path + + // okay those last steps took us past our + // target but do we want to scoot or moonwalk + + frames = _stepCount - _lastCount; + errorX = _modularPath[p].x - walkAnim[_stepCount - 1].x; + errorY = _modularPath[p].y - walkAnim[_stepCount - 1].y; + + if (frames > _framesPerStep) { + lastErrorX = _modularPath[p].x - walkAnim[_stepCount - 7].x; + lastErrorY = _modularPath[p].y - walkAnim[_stepCount - 7].y; + + if (stepX == 0) { + if (3 * ABS(lastErrorY) < ABS(errorY)) { + // the last stop was + // closest + _stepCount -= _framesPerStep; + if (left == 0) + left = _framesPerStep; + else + left = 0; + } + } else { + if (3 * ABS(lastErrorX) < ABS(errorX)) { + //the last stop was + // closest + _stepCount -= _framesPerStep; + if (left == 0) + left = _framesPerStep; + else + left = 0; + } + } + } + + errorX = _modularPath[p].x - walkAnim[_stepCount-1].x; + errorY = _modularPath[p].y - walkAnim[_stepCount-1].y; + + // okay we've reached the end but we still + // have an error + + if (errorX != 0) { + frameCount = 0; + frames = _stepCount - _lastCount; + + do { + frameCount++; + walkAnim[_lastCount + frameCount - 1].x += errorX * frameCount / frames; + } while (frameCount < frames); + } + + if (errorY != 0) { + frameCount = 0; + frames = _stepCount - _lastCount; + do { + frameCount++; + walkAnim[_lastCount + frameCount - 1].y += errorY * frameCount / frames; + } while (frameCount < frames); + } + + // Now is the time to put in the turn frames + // for the last turn + + if (frames < _framesPerStep) { + // this ensures that we don't put in + // turn frames for this walk or the + // next + _currentDir = 99; + } + + if (_currentDir != 99) + lastRealDir = _currentDir; + + // check each turn condition in turn + + // only for george + if (lastDir != 99 && _currentDir != 99 && _walkData.usingWalkingTurnFrames) { + // 1 and -7 going right -1 and 7 going + // left + lastDir = _currentDir - lastDir; + + if (lastDir == -1 || lastDir == 7 || lastDir == -2 || lastDir == 6) { + // turn at the end of the last + // walk + + _frame = _lastCount - _framesPerStep; + do { + // turning left + walkAnim[_frame].frame += _firstWalkingTurnLeftFrame; + _frame++; + } while (_frame < _lastCount); + } else if (lastDir == 1 || lastDir == -7 || lastDir == 2 || lastDir == -6) { + // turn at the end of the + // current walk + + _frame = _lastCount - _framesPerStep; + do { + // turning right + walkAnim[_frame].frame += _firstWalkingTurnRightFrame; + _frame++; + } while (_frame < _lastCount); + } + lastDir = _currentDir; + } + + // all turns checked + + _lastCount = _stepCount; + _moduleX = walkAnim[_stepCount - 1].x; + _moduleY = walkAnim[_stepCount - 1].y; + module16X = _moduleX << 16; + module16Y = _moduleY << 16; + } + } + } while (_modularPath[p].dir < NO_DIRECTIONS); + +#ifdef SWORD2_DEBUG + if (lastRealDir == 99) + error("slidyWalkAnimatorlast direction error"); +#endif + + // THE SLOW OUT + addSlowOutFrames(walkAnim); + + // TURNS TO END THE WALK ? + + // We've done the walk now put in any turns at the end + + if (_targetDir == 8) { + // ANY direction -> stand in the last direction + + module = _firstStandFrame + lastRealDir; + _targetDir = lastRealDir; + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastRealDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + if (_targetDir == 9) { + // 'stance' was non-zero + if (_stepCount == 0) { + module = _framesPerChar + lastRealDir; + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastRealDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + } else if (_targetDir != lastRealDir) { + // rotate to target direction + turnDir = _targetDir - lastRealDir; + if ( turnDir < 0) + turnDir += NO_DIRECTIONS; + + if (turnDir > 4) + turnDir = -1; + else if (turnDir > 0) + turnDir = 1; + + // rotate to target direction + // for george and nico put in a head turn at the start + + if (_walkData.usingStandingTurnFrames) { + // new frames for turn frames 29oct95jps + if (turnDir < 0) + module = _firstStandingTurnLeftFrame + lastDir; + else + module = _firstStandingTurnRightFrame + lastDir; + + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastRealDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + // rotate if we need to + + while (lastRealDir != _targetDir) { + lastRealDir += turnDir; + + // new frames for turn frames 29oct95jps + if (turnDir < 0) { + if (lastRealDir < 0) + lastRealDir += NO_DIRECTIONS; + module = _firstStandingTurnLeftFrame + lastRealDir; + } else { + if (lastRealDir > 7) + lastRealDir -= NO_DIRECTIONS; + module = _firstStandingTurnRightFrame + lastRealDir; + } + + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastRealDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + module = _firstStandFrame + lastRealDir; + walkAnim[_stepCount - 1].frame = module; + } else { + // just stand at the end + module = _firstStandFrame + lastRealDir; + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastRealDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + walkAnim[_stepCount].frame = 512; + walkAnim[_stepCount].step = 99; + _stepCount++; + + walkAnim[_stepCount].frame = 512; + walkAnim[_stepCount].step = 99; + _stepCount++; + + walkAnim[_stepCount].frame = 512; + walkAnim[_stepCount].step = 99; + + // write all the frames to "debug.txt" + debug(5, "THE WALKDATA:"); + + for (_frame = 0; _frame <= _stepCount; _frame++) + debug(5, "walkAnim[%d].frame=%d", _frame, walkAnim[_frame].frame); + + debug(5, "routeFinder RouteSize is %d", _stepCount); + return; +} + +#ifndef FORCE_SLIDY + +// THE SOLID PATH ROUTINES + +int32 Router::solidPath() { + /********************************************************************* + * SolidPath creates a path based on whole steps with no sliding to + * get as near as possible to the target without any sliding this + * routine is currently unused, but is intended for use when just + * clicking about. + * + * produce a module list from the line data + *********************************************************************/ + + int32 smooth; + int32 solid; + int32 scale; + int32 stepX; + int32 stepY; + int32 deltaX; + int32 deltaY; + + // strip out the short sections + + solid = 1; + smooth = 1; + _modularPath[0].x = _smoothPath[0].x; + _modularPath[0].y = _smoothPath[0].y; + _modularPath[0].dir = _smoothPath[0].dir; + _modularPath[0].num = 0; + + do { + scale = _scaleA * _smoothPath[smooth].y + _scaleB; + deltaX = _smoothPath[smooth].x - _modularPath[solid - 1].x; + deltaY = _smoothPath[smooth].y - _modularPath[solid - 1].y; + stepX = _modX[_smoothPath[smooth].dir]; + stepY = _modY[_smoothPath[smooth].dir]; + stepX = stepX * scale; + stepY = stepY * scale; + stepX = stepX >> 16; + stepY = stepY >> 16; + + if (ABS(deltaX) >= ABS(stepX) && ABS(deltaY) >= ABS(stepY)) { + _modularPath[solid].x = _smoothPath[smooth].x; + _modularPath[solid].y = _smoothPath[smooth].y; + _modularPath[solid].dir = _smoothPath[smooth].dir; + _modularPath[solid].num = 1; + solid++; + } + + smooth++; + } while (_smoothPath[smooth].num < ROUTE_END_FLAG); + + // in case the last bit had no steps + + if (solid == 1) { + // there were no paths so put in a dummy end + solid = 2; + _modularPath[1].dir = _smoothPath[0].dir; + _modularPath[1].num = 0; + } + + _modularPath[solid - 1].x = _smoothPath[smooth - 1].x; + _modularPath[solid - 1].y = _smoothPath[smooth - 1].y; + + // set up the end of the walk + _modularPath[solid].x = _smoothPath[smooth - 1].x; + _modularPath[solid].y = _smoothPath[smooth - 1].y; + _modularPath[solid].dir = 9; + _modularPath[solid].num = ROUTE_END_FLAG; + + return 1; +} + +int32 Router::solidWalkAnimator(WalkData *walkAnim) { + /********************************************************************* + * SolidWalk creates an animation based on whole steps with no sliding + * to get as near as possible to the target without any sliding. This + * routine is is intended for use when just clicking about. + * + * produce a module list from the line data + * + * returns 0 if solid route not found + *********************************************************************/ + + int32 left; + int32 turnDir; + int32 scale; + int32 step; + int32 errorX; + int32 errorY; + int32 moduleEnd; + bool slowStart = false; + + // start at the beginning for a change + + int32 lastDir = _modularPath[0].dir; + int32 module = _framesPerChar + lastDir; + + _currentDir = _modularPath[1].dir; + _moduleX = _startX; + _moduleY = _startY; + _stepCount = 0; + + int32 module16X = _moduleX << 16; + int32 module16Y = _moduleY << 16; + + // START THE WALK WITH THE FIRST STANDFRAME THIS MAY CAUSE A DELAY + // BUT IT STOPS THE PLAYER MOVING FOR COLLISIONS ARE DETECTED + + debug(5, "SOLID: STARTING THE WALK"); + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + + // TURN TO START THE WALK + + debug(5, "SOLID: TURNING TO START THE WALK"); + + // rotate if we need to + + if (lastDir != _currentDir) { + // get the direction to turn + turnDir = _currentDir - lastDir; + if (turnDir < 0) + turnDir += NO_DIRECTIONS; + + if (turnDir > 4) + turnDir = -1; + else if (turnDir > 0) + turnDir = 1; + + // rotate to new walk direction + // for george and nico put in a head turn at the start + + if (_walkData.usingStandingTurnFrames) { + // new frames for turn frames 29oct95jps + if (turnDir < 0) + module = _firstStandingTurnLeftFrame + lastDir; + else + module = _firstStandingTurnRightFrame + lastDir; + + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + // rotate till were facing new dir then go back 45 degrees + + while (lastDir != _currentDir) { + lastDir += turnDir; + + // new frames for turn frames + if (turnDir < 0) { + if (lastDir < 0) + lastDir += NO_DIRECTIONS; + module = _firstStandingTurnLeftFrame + lastDir; + } else { + if (lastDir > 7) + lastDir -= NO_DIRECTIONS; + module = _firstStandingTurnRightFrame + lastDir; + } + + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = lastDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + } + + // the back 45 degrees bit + // step back one because new head turn for george takes us + // past the new dir + + _stepCount--; + } + + // THE SLOW IN + + slowStart = addSlowInFrames(walkAnim); + + // THE WALK + + debug(5, "SOLID: THE WALK"); + + // start the walk on the left or right leg, depending on how the + // slow-in frames were drawn + + // (0 = left; 1 = right) + if (_walkData.leadingLeg[_currentDir] == 0) { + // start the walk on the left leg (ie. at beginning of the + // first step of the walk cycle) + left = 0; + } else { + // start the walk on the right leg (ie. at beginning of the + // second step of the walk cycle) + left = _framesPerStep; + } + + _lastCount = _stepCount; + + // this ensures that we don't put in turn frames for the start + lastDir = 99; + + // this ensures that we don't put in turn frames for the start + _currentDir = 99; + + int32 p = 1; + + do { + while (_modularPath[p].num > 0) { + _currentDir = _modularPath[p].dir; + if (_currentDir < NO_DIRECTIONS) { + module = _currentDir * _framesPerStep * 2 + left; + + if (left == 0) + left = _framesPerStep; + else + left = 0; + + moduleEnd = module + _framesPerStep; + step = 0; + scale = (_scaleA * _moduleY + _scaleB); + + do { + module16X += _walkData.dx[module] * scale; + module16Y += _walkData.dy[module] * scale; + _moduleX = module16X >> 16; + _moduleY = module16Y >> 16; + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = step; // normally 0,1,2,3,4,5,0,1,2,etc + walkAnim[_stepCount].dir = _currentDir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + module++; + step++; + } while (module < moduleEnd); + + errorX = _modularPath[p].x - _moduleX; + errorX = errorX * _modX[_modularPath[p].dir]; + errorY = _modularPath[p].y - _moduleY; + errorY = errorY * _modY[_modularPath[p].dir]; + + if (errorX < 0 || errorY < 0) { + _modularPath[p].num = 0; + _stepCount -= _framesPerStep; + + if (left == 0) + left = _framesPerStep; + else + left = 0; + + // Okay this is the end of a section + + _moduleX = walkAnim[_stepCount - 1].x; + _moduleY = walkAnim[_stepCount - 1].y; + module16X = _moduleX << 16; + module16Y = _moduleY << 16; + _modularPath[p].x = _moduleX; + _modularPath[p].y = _moduleY; + + // Now is the time to put in the turn + // frames for the last turn + + if (_stepCount - _lastCount < _framesPerStep) { + // no step taken + + // clean up if a slow in but no + // walk + + if (slowStart) { + _stepCount -= _walkData.nSlowInFrames[_currentDir]; + _lastCount -= _walkData.nSlowInFrames[_currentDir]; + slowStart = false; + } + + // this ensures that we don't + // put in turn frames for this + // walk or the next + + _currentDir = 99; + } + + // check each turn condition in turn + if (lastDir != 99 && _currentDir != 99 && _walkData.usingWalkingTurnFrames) { + // only for george + // 1 and -7 going right -1 and + // 7 going left + + lastDir = _currentDir - lastDir; + + if (lastDir == -1 || lastDir == 7 || lastDir == -2 || lastDir == 6) { + // turn at the end of + // the last walk + + _frame = _lastCount - _framesPerStep; + + do { + // turning left + walkAnim[_frame].frame += _firstWalkingTurnLeftFrame; + _frame++; + } while (_frame < _lastCount); + } else if (lastDir == 1 || lastDir == -7 || lastDir == 2 || lastDir == -6) { + // turn at the end of + // the current walk + + _frame = _lastCount - _framesPerStep; + do { + // turning right + walkAnim[_frame].frame += _firstWalkingTurnRightFrame; + _frame++; + } while (_frame < _lastCount); + } + } + + // all turns checked + _lastCount = _stepCount; + } + } + } + p++; + lastDir = _currentDir; + + // can only be valid first time round + slowStart = false; + } while (_modularPath[p].dir < NO_DIRECTIONS); + + // THE SLOW OUT + + addSlowOutFrames(walkAnim); + + module = _framesPerChar + _modularPath[p - 1].dir; + walkAnim[_stepCount].frame = module; + walkAnim[_stepCount].step = 0; + walkAnim[_stepCount].dir = _modularPath[p - 1].dir; + walkAnim[_stepCount].x = _moduleX; + walkAnim[_stepCount].y = _moduleY; + _stepCount++; + + walkAnim[_stepCount].frame = 512; + walkAnim[_stepCount].step = 99; + _stepCount++; + + walkAnim[_stepCount].frame = 512; + walkAnim[_stepCount].step = 99; + _stepCount++; + + walkAnim[_stepCount].frame = 512; + walkAnim[_stepCount].step = 99; + + debug(5, "THE WALKDATA:"); + + for (_frame = 0; _frame <= _stepCount; _frame++) + debug(5, "walkAnim[%d].frame=%d", _frame, walkAnim[_frame].frame); + + // NO END TURNS + + debug(5, "routeFinder RouteSize is %d", _stepCount); + // now check the route + + int i = 0; + + do { + if (!check(_modularPath[i].x, _modularPath[i].y, _modularPath[i + 1].x, _modularPath[i + 1].y)) + p = 0; + i++; + } while (i < p - 1); + + if (p != 0) { + _targetDir = _modularPath[p - 1].dir; + if (checkTarget(_moduleX, _moduleY) == 3) { + // new target on a line + p = 0; + debug(5, "Solid walk target was on a line %d %d", _moduleX, _moduleY); + } + } + + return p; +} +#endif + +// THE SCAN ROUTINES + +bool Router::scan(int32 level) { + /********************************************************************* + * Called successively from routeFinder until no more changes take + * place in the grid array, ie he best path has been found + * + * Scans through every point in the node array and checks if there is + * a route between each point and if this route gives a new route. + * + * This routine could probably halve its processing time if it doubled + * up on the checks after each route check + * + *********************************************************************/ + + int32 x1, y1, x2, y2; + int32 distance; + bool changed = false; + + // For all the nodes that have new values and a distance less than + // enddist, ie dont check for new routes from a point we checked + // before or from a point that is already further away than the best + // route so far. + + for (int i = 0; i < _nNodes; i++) { + if (_node[i].dist < _node[_nNodes].dist && _node[i].level == level) { + x1 = _node[i].x; + y1 = _node[i].y; + + for (int j = _nNodes; j > 0; j--) { + if (_node[j].dist > _node[i].dist) { + x2 = _node[j].x; + y2 = _node[j].y; + + if (ABS(x2 - x1) > 4.5 * ABS(y2 - y1)) + distance = (8 * ABS(x2 - x1) + 18 * ABS(y2 - y1)) / (54 * 8) + 1; + else + distance = (6 * ABS(x2 - x1) + 36 * ABS(y2 - y1)) / (36 * 14) + 1; + + if (distance + _node[i].dist < _node[_nNodes].dist && distance + _node[i].dist < _node[j].dist) { + if (newCheck(0, x1, y1, x2, y2)) { + _node[j].level = level + 1; + _node[j].dist = distance + _node[i].dist; + _node[j].prev = i; + changed = true; + } + } + } + } + } + } + + return changed; +} + +int32 Router::newCheck(int32 status, int32 x1, int32 y1, int32 x2, int32 y2) { + /********************************************************************* + * newCheck routine checks if the route between two points can be + * achieved without crossing any of the bars in the Bars array. + * + * newCheck differs from check in that that 4 route options are + * considered corresponding to actual walked routes. + * + * Note distance doesnt take account of shrinking ??? + * + * Note Bars array must be properly calculated ie min max dx dy co + *********************************************************************/ + + int32 ldx; + int32 ldy; + int32 dlx; + int32 dly; + int32 dirX; + int32 dirY; + int32 step1; + int32 step2; + int32 step3; + int32 steps; + int32 options; + + steps = 0; + options = 0; + ldx = x2 - x1; + ldy = y2 - y1; + dirX = 1; + dirY = 1; + + if (ldx < 0) { + ldx = -ldx; + dirX = -1; + } + + if (ldy < 0) { + ldy = -ldy; + dirY = -1; + } + + // make the route options + + if (_diagonaly * ldx > _diagonalx * ldy) { + // dir = 1,2 or 2,3 or 5,6 or 6,7 + + dly = ldy; + dlx = (ldy * _diagonalx) / _diagonaly; + ldx = ldx - dlx; + dlx = dlx * dirX; + dly = dly * dirY; + ldx = ldx * dirX; + ldy = 0; + + // options are square, diagonal a code 1 route + + step1 = check(x1, y1, x1 + ldx, y1); + if (step1 != 0) { + step2 = check(x1 + ldx, y1, x2, y2); + if (step2 != 0) { + steps = step1 + step2; + options |= 2; + } + } + + // diagonal, square a code 2 route + + if (steps == 0 || status == 1) { + step1 = check(x1, y1, x1 + dlx, y1 + dly); + if (step1 != 0) { + step2 = check(x1 + dlx, y2, x2, y2); + if (step2 != 0) { + steps = step1 + step2; + options |= 4; + } + } + } + + // halfsquare, diagonal, halfsquare a code 0 route + + if (steps == 0 || status == 1) { + step1 = check(x1, y1, x1 + ldx / 2, y1); + if (step1 != 0) { + step2 = check(x1 + ldx / 2, y1, x1 + ldx / 2 + dlx, y2); + if (step2 != 0) { + step3 = check(x1 + ldx / 2 + dlx, y2, x2, y2); + if (step3 != 0) { + steps = step1 + step2 + step3; + options |= 1; + } + } + } + } + + //halfdiagonal, square, halfdiagonal a code 3 route + + if (steps == 0 || status == 1) { + step1 = check(x1, y1, x1 + dlx / 2, y1 + dly / 2); + if (step1 != 0) { + step2 = check(x1 + dlx / 2, y1 + dly / 2, x1 + ldx + dlx / 2, y1 + dly / 2); + if (step2 != 0) { + step3 = check(x1 + ldx + dlx / 2, y1 + dly / 2, x2, y2); + if (step3 != 0) { + steps = step1 + step2 + step3; + options |= 8; + } + } + } + } + } else { + // dir = 7,0 or 0,1 or 3,4 or 4,5 + + dlx = ldx; + dly = (ldx * _diagonaly) / _diagonalx; + ldy = ldy - dly; + dlx = dlx * dirX; + dly = dly * dirY; + ldy = ldy * dirY; + ldx = 0; + + // options are square, diagonal a code 1 route + + step1 = check(x1 ,y1, x1, y1 + ldy); + if (step1 != 0) { + step2 = check(x1, y1 + ldy, x2, y2); + if (step2 != 0) { + steps = step1 + step2; + options |= 2; + } + } + + // diagonal, square a code 2 route + + if (steps == 0 || status == 1) { + step1 = check(x1, y1, x2, y1 + dly); + if (step1 != 0) { + step2 = check(x2, y1 + dly, x2, y2); + if (step2 != 0) { + steps = step1 + step2; + options |= 4; + } + } + } + + // halfsquare, diagonal, halfsquare a code 0 route + + if (steps == 0 || status == 1) { + step1 = check(x1, y1, x1, y1 + ldy / 2); + if (step1 != 0) { + step2 = check(x1, y1 + ldy / 2, x2, y1 + ldy / 2 + dly); + if (step2 != 0) { + step3 = check(x2, y1 + ldy / 2 + dly, x2, y2); + if (step3 != 0) { + steps = step1 + step2 + step3; + options |= 1; + } + } + } + } + + // halfdiagonal, square, halfdiagonal a code 3 route + + if (steps == 0 || status == 1) { + step1 = check(x1, y1, x1 + dlx / 2, y1 + dly / 2); + if (step1 != 0) { + step2 = check(x1 + dlx / 2, y1 + dly / 2, x1 + dlx / 2, y1 + ldy + dly / 2); + if (step2 != 0) { + step3 = check(x1 + dlx / 2, y1 + ldy + dly / 2, x2, y2); + if (step3 != 0) { + steps = step1 + step2 + step3; + options |= 8; + } + } + } + } + } + + if (status == 0) + status = steps; + else + status = options; + + return status; +} + +// CHECK ROUTINES + +bool Router::check(int32 x1, int32 y1, int32 x2, int32 y2) { + // call the fastest line check for the given line + // returns true if line didn't cross any bars + + if (x1 == x2 && y1 == y2) + return true; + + if (x1 == x2) + return vertCheck(x1, y1, y2); + + if (y1 == y2) + return horizCheck(x1, y1, x2); + + return lineCheck(x1, y1, x2, y2); +} + +bool Router::lineCheck(int32 x1, int32 y1, int32 x2, int32 y2) { + bool linesCrossed = true; + + int32 xmin = MIN(x1, x2); + int32 xmax = MAX(x1, x2); + int32 ymin = MIN(y1, y2); + int32 ymax = MAX(y1, y2); + + // Line set to go one step in chosen direction so ignore if it hits + // anything + + int32 dirx = x2 - x1; + int32 diry = y2 - y1; + + int32 co = (y1 * dirx) - (x1 * diry); // new line equation + + for (int i = 0; i < _nBars && linesCrossed; i++) { + // skip if not on module + if (xmax >= _bars[i].xmin && xmin <= _bars[i].xmax && ymax >= _bars[i].ymin && ymin <= _bars[i].ymax) { + // Okay, it's a valid line. Calculate an intercept. Wow + // but all this arithmetic we must have loads of time + + // slope it he slope between the two lines + int32 slope = (_bars[i].dx * diry) - (_bars[i].dy *dirx); + // assuming parallel lines don't cross + if (slope != 0) { + // calculate x intercept and check its on both + // lines + int32 xc = ((_bars[i].co * dirx) - (co * _bars[i].dx)) / slope; + + // skip if not on module + if (xc >= xmin - 1 && xc <= xmax + 1) { + // skip if not on line + if (xc >= _bars[i].xmin - 1 && xc <= _bars[i].xmax + 1) { + int32 yc = ((_bars[i].co * diry) - (co * _bars[i].dy)) / slope; + + // skip if not on module + if (yc >= ymin - 1 && yc <= ymax + 1) { + // skip if not on line + if (yc >= _bars[i].ymin - 1 && yc <= _bars[i].ymax + 1) { + linesCrossed = false; + } + } + } + } + } + } + } + + return linesCrossed; +} + +bool Router::horizCheck(int32 x1, int32 y, int32 x2) { + bool linesCrossed = true; + + int32 xmin = MIN(x1, x2); + int32 xmax = MAX(x1, x2); + + // line set to go one step in chosen direction so ignore if it hits + // anything + + for (int i = 0; i < _nBars && linesCrossed; i++) { + // skip if not on module + if (xmax >= _bars[i].xmin && xmin <= _bars[i].xmax && y >= _bars[i].ymin && y <= _bars[i].ymax) { + // Okay, it's a valid line calculate an intercept. Wow + // but all this arithmetic we must have loads of time + + if (_bars[i].dy == 0) + linesCrossed = false; + else { + int32 ldy = y - _bars[i].y1; + int32 xc = _bars[i].x1 + (_bars[i].dx * ldy) / _bars[i].dy; + // skip if not on module + if (xc >= xmin - 1 && xc <= xmax + 1) + linesCrossed = false; + } + } + } + + return linesCrossed; +} + +bool Router::vertCheck(int32 x, int32 y1, int32 y2) { + bool linesCrossed = true; + + int32 ymin = MIN(y1, y2); + int32 ymax = MAX(y1, y2); + + // Line set to go one step in chosen direction so ignore if it hits + // anything + + for (int i = 0; i < _nBars && linesCrossed; i++) { + // skip if not on module + if (x >= _bars[i].xmin && x <= _bars[i].xmax && ymax >= _bars[i].ymin && ymin <= _bars[i].ymax) { + // Okay, it's a valid line calculate an intercept. Wow + // but all this arithmetic we must have loads of time + + // both lines vertical and overlap in x and y so they + // cross + + if (_bars[i].dx == 0) + linesCrossed = false; + else { + int32 ldx = x - _bars[i].x1; + int32 yc = _bars[i].y1 + (_bars[i].dy * ldx) / _bars[i].dx; + // the intercept overlaps + if (yc >= ymin - 1 && yc <= ymax + 1) + linesCrossed = false; + } + } + } + + return linesCrossed; +} + +int32 Router::checkTarget(int32 x, int32 y) { + int32 onLine = 0; + + int32 xmin = x - 1; + int32 xmax = x + 1; + int32 ymin = y - 1; + int32 ymax = y + 1; + + // check if point +- 1 is on the line + // so ignore if it hits anything + + for (int i = 0; i < _nBars && onLine == 0; i++) { + // overlapping line + if (xmax >= _bars[i].xmin && xmin <= _bars[i].xmax && ymax >= _bars[i].ymin && ymin <= _bars[i].ymax) { + int32 xc, yc; + + // okay this line overlaps the target calculate + // an y intercept for x + + // vertical line so we know it overlaps y + if (_bars[i].dx == 0) + yc = 0; + else { + int ldx = x - _bars[i].x1; + yc = _bars[i].y1 + (_bars[i].dy * ldx) / _bars[i].dx; + } + + // overlapping point for y + if (yc >= ymin && yc <= ymax) { + // target on a line so drop out + onLine = 3; + debug(5, "RouteFail due to target on a line %d %d", x, y); + } else { + // vertical line so we know it overlaps y + if (_bars[i].dy == 0) + xc = 0; + else { + int32 ldy = y - _bars[i].y1; + xc = _bars[i].x1 + (_bars[i].dx * ldy) / _bars[i].dy; + } + + // skip if not on module + if (xc >= xmin && xc <= xmax) { + // target on a line so drop out + onLine = 3; + debug(5, "RouteFail due to target on a line %d %d", x, y); + } + } + } + } + + return onLine; +} + +// THE SETUP ROUTINES + +void Router::loadWalkData(byte *ob_walkdata) { + uint16 firstFrameOfDirection; + uint16 walkFrameNo; + uint32 frameCounter = 0; // starts at frame 0 of mega set + int i; + + _walkData.read(ob_walkdata); + + // 0 = not using slow out frames; non-zero = using that many frames + // for each leading leg for each direction + + _numberOfSlowOutFrames = _walkData.usingSlowOutFrames; + + for (i = 0; i < NO_DIRECTIONS; i++) { + firstFrameOfDirection = i * _walkData.nWalkFrames; + + _modX[i] = 0; + _modY[i] = 0; + + for (walkFrameNo = firstFrameOfDirection; walkFrameNo < firstFrameOfDirection + _walkData.nWalkFrames / 2; walkFrameNo++) { + // eg. _modX[0] is the sum of the x-step sizes for the + // first half of the walk cycle for direction 0 + _modX[i] += _walkData.dx[walkFrameNo]; + _modY[i] += _walkData.dy[walkFrameNo]; + } + } + + _diagonalx = _modX[3]; + _diagonaly = _modY[3]; + + // interpret the walk data + + _framesPerStep = _walkData.nWalkFrames / 2; + _framesPerChar = _walkData.nWalkFrames * NO_DIRECTIONS; + + // offset pointers added Oct 30 95 JPS + // mega id references removed 16sep96 by JEL + + // WALK FRAMES + // start on frame 0 + + frameCounter += _framesPerChar; + + // STAND FRAMES + // stand frames come after the walk frames + // one stand frame for each direction + + _firstStandFrame = frameCounter; + frameCounter += NO_DIRECTIONS; + + // STANDING TURN FRAMES - OPTIONAL! + // standing turn-left frames come after the slow-out frames + // one for each direction + // standing turn-left frames come after the standing turn-right frames + // one for each direction + + if (_walkData.usingStandingTurnFrames) { + _firstStandingTurnLeftFrame = frameCounter; + frameCounter += NO_DIRECTIONS; + + _firstStandingTurnRightFrame = frameCounter; + frameCounter += NO_DIRECTIONS; + } else { + // refer instead to the normal stand frames + _firstStandingTurnLeftFrame = _firstStandFrame; + _firstStandingTurnRightFrame = _firstStandFrame; + } + + // WALKING TURN FRAMES - OPTIONAL! + // walking left-turn frames come after the stand frames + // walking right-turn frames come after the walking left-turn frames + + if (_walkData.usingWalkingTurnFrames) { + _firstWalkingTurnLeftFrame = frameCounter; + frameCounter += _framesPerChar; + + _firstWalkingTurnRightFrame = frameCounter; + frameCounter += _framesPerChar; + } else { + _firstWalkingTurnLeftFrame = 0; + _firstWalkingTurnRightFrame = 0; + } + + // SLOW-IN FRAMES - OPTIONAL! + // slow-in frames come after the walking right-turn frames + + if (_walkData.usingSlowInFrames) { + // Make note of frame number of first slow-in frame for each + // direction. There may be a different number of slow-in + // frames in each direction + + for (i = 0; i < NO_DIRECTIONS; i++) { + _firstSlowInFrame[i] = frameCounter; + frameCounter += _walkData.nSlowInFrames[i]; + } + } + + // SLOW-OUT FRAMES - OPTIONAL! + // slow-out frames come after the slow-in frames + + if (_walkData.usingSlowOutFrames) + _firstSlowOutFrame = frameCounter; +} + +// THE ROUTE EXTRACTOR + +void Router::extractRoute() { + /********************************************************************* + * extractRoute gets route from the node data after a full scan, route + * is written with just the basic way points and direction options for + * heading to the next point. + *********************************************************************/ + + int32 prev; + int32 prevx; + int32 prevy; + int32 last; + int32 point; + int32 p; + int32 dirx; + int32 diry; + int32 dir; + int32 ldx; + int32 ldy; + + // extract the route from the node data + + prev = _nNodes; + last = prev; + point = O_ROUTE_SIZE - 1; + _route[point].x = _node[last].x; + _route[point].y = _node[last].y; + + do { + point--; + prev = _node[last].prev; + prevx = _node[prev].x; + prevy = _node[prev].y; + _route[point].x = prevx; + _route[point].y = prevy; + last = prev; + } while (prev > 0); + + // now shuffle route down in the buffer + + _routeLength = 0; + + do { + _route[_routeLength].x = _route[point].x; + _route[_routeLength].y = _route[point].y; + point++; + _routeLength++; + } while (point < O_ROUTE_SIZE); + + _routeLength--; + + // okay the route exists as a series point now put in some directions + + p = 0; + + do { + ldx = _route[p + 1].x - _route[p].x; + ldy = _route[p + 1].y - _route[p].y; + dirx = 1; + diry = 1; + + if (ldx < 0) { + ldx = -ldx; + dirx = -1; + } + + if (ldy < 0) { + ldy = -ldy; + diry = -1; + } + + if (_diagonaly * ldx > _diagonalx * ldy) { + // dir = 1,2 or 2,3 or 5,6 or 6,7 + + // 2 or 6 + dir = 4 - 2 * dirx; + _route[p].dirS = dir; + + // 1, 3, 5 or 7 + dir = dir + diry * dirx; + _route[p].dirD = dir; + } else { + // dir = 7,0 or 0,1 or 3,4 or 4,5 + + // 0 or 4 + dir = 2 + 2 * diry; + _route[p].dirS = dir; + + // 2 or 6 + dir = 4 - 2 * dirx; + + // 1, 3, 5 or 7 + dir = dir + diry * dirx; + _route[p].dirD = dir; + } + p++; + } while (p < _routeLength); + + // set the last dir to continue previous route unless specified + + if (_targetDir == 8) { + // ANY direction + _route[p].dirS = _route[p - 1].dirS; + _route[p].dirD = _route[p - 1].dirD; + } else { + _route[p].dirS = _targetDir; + _route[p].dirD = _targetDir; + } + + return; +} + +void Router::setUpWalkGrid(byte *ob_mega, int32 x, int32 y, int32 dir) { + ObjectMega obMega(ob_mega); + + // get walk grid file + extra grid into 'bars' & 'node' arrays + loadWalkGrid(); + + // copy the mega structure into the local variables for use in all + // subroutines + + _startX = obMega.getFeetX(); + _startY = obMega.getFeetY(); + _startDir = obMega.getCurDir(); + _targetX = x; + _targetY = y; + _targetDir = dir; + + _scaleA = obMega.getScaleA(); + _scaleB = obMega.getScaleB(); + + // mega's current position goes into first node + + _node[0].x = _startX; + _node[0].y = _startY; + _node[0].level = 1; + _node[0].prev = 0; + _node[0].dist = 0; + + // reset other nodes + + for (int i = 1; i < _nNodes; i++) { + _node[i].level = 0; + _node[i].prev = 0; + _node[i].dist = 9999; + } + + // target position goes into final node + _node[_nNodes].x = _targetX; + _node[_nNodes].y = _targetY; + _node[_nNodes].level = 0; + _node[_nNodes].prev = 0; + _node[_nNodes].dist = 9999; +} + +void Router::plotWalkGrid() { + int32 i; + + // get walk grid file + extra grid into 'bars' & 'node' arrays + loadWalkGrid(); + + // lines + + for (i = 0; i < _nBars; i++) + _vm->_screen->drawLine(_bars[i].x1, _bars[i].y1, _bars[i].x2, _bars[i].y2, 254); + + // nodes + + // leave node 0 for start node + for (i = 1; i < _nNodes; i++) + plotCross(_node[i].x, _node[i].y, 184); +} + +void Router::plotCross(int16 x, int16 y, uint8 colour) { + _vm->_screen->drawLine(x - 1, y - 1, x + 1, y + 1, colour); + _vm->_screen->drawLine(x + 1, y - 1, x - 1, y + 1, colour); +} + +void Router::loadWalkGrid() { + WalkGridHeader floorHeader; + byte *fPolygrid; + uint16 fPolygridLen; + + _nBars = 0; // reset counts + _nNodes = 1; // leave node 0 for start-node + + // STATIC GRIDS (added/removed by object logics) + + // go through walkgrid list + for (int i = 0; i < MAX_WALKGRIDS; i++) { + if (_walkGridList[i]) { + int j; + + // open walk grid file + fPolygrid = _vm->_resman->openResource(_walkGridList[i]); + fPolygridLen = _vm->_resman->fetchLen(_walkGridList[i]); + + Common::MemoryReadStream readS(fPolygrid, fPolygridLen); + + readS.seek(ResHeader::size()); + + floorHeader.numBars = readS.readSint32LE(); + floorHeader.numNodes = readS.readSint32LE(); + + // check that we're not going to exceed the max + // allowed in the complete walkgrid arrays + + assert(_nBars + floorHeader.numBars < O_GRID_SIZE); + assert(_nNodes + floorHeader.numNodes < O_GRID_SIZE); + + // lines + + for (j = 0; j < floorHeader.numBars; j++) { + _bars[_nBars + j].x1 = readS.readSint16LE(); + _bars[_nBars + j].y1 = readS.readSint16LE(); + _bars[_nBars + j].x2 = readS.readSint16LE(); + _bars[_nBars + j].y2 = readS.readSint16LE(); + _bars[_nBars + j].xmin = readS.readSint16LE(); + _bars[_nBars + j].ymin = readS.readSint16LE(); + _bars[_nBars + j].xmax = readS.readSint16LE(); + _bars[_nBars + j].ymax = readS.readSint16LE(); + _bars[_nBars + j].dx = readS.readSint16LE(); + _bars[_nBars + j].dy = readS.readSint16LE(); + _bars[_nBars + j].co = readS.readSint32LE(); + } + + // nodes + + // leave node 0 for start node + for (j = 0; j < floorHeader.numNodes; j++) { + _node[_nNodes + j].x = readS.readSint16LE(); + _node[_nNodes + j].y = readS.readSint16LE(); + } + + // close walk grid file + _vm->_resman->closeResource(_walkGridList[i]); + + // increment counts of total bars & nodes in whole + // walkgrid + + _nBars += floorHeader.numBars; + _nNodes += floorHeader.numNodes; + } + } +} + +void Router::clearWalkGridList() { + memset(_walkGridList, 0, sizeof(_walkGridList)); +} + +// called from fnAddWalkGrid + +void Router::addWalkGrid(int32 gridResource) { + int i; + // First, scan the list to see if this grid is already included + + for (i = 0; i < MAX_WALKGRIDS; i++) { + if (_walkGridList[i] == gridResource) + return; + } + + // Scan the list for a free slot + + for (i = 0; i < MAX_WALKGRIDS; i++) { + if (_walkGridList[i] == 0) { + _walkGridList[i] = gridResource; + return; + } + } + + error("_walkGridList[] full"); +} + +// called from fnRemoveWalkGrid + +void Router::removeWalkGrid(int32 gridResource) { + for (int i = 0; i < MAX_WALKGRIDS; i++) { + if (_walkGridList[i] == gridResource) { + // If we've found it in the list, reset entry to zero. + // Otherwise just ignore the request. + _walkGridList[i] = 0; + break; + } + } +} + +} // End of namespace Sword2 diff --git a/engines/sword2/router.h b/engines/sword2/router.h new file mode 100644 index 0000000000..45efd046bd --- /dev/null +++ b/engines/sword2/router.h @@ -0,0 +1,255 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _ROUTER_H +#define _ROUTER_H + +// This used to be a variable, but it was never set. Actually, it wasn't even +// initialised! +// +// Define this to force the use of slidy router (so solid path not used when +// ending walk in ANY direction) +// +// #define FORCE_SLIDY + +#include "sword2/object.h" + +namespace Sword2 { + +struct WalkData { + uint16 frame; + int16 x; + int16 y; + uint8 step; + uint8 dir; +}; + +struct BarData { + int16 x1; + int16 y1; + int16 x2; + int16 y2; + int16 xmin; + int16 ymin; + int16 xmax; + int16 ymax; + int16 dx; // x2 - x1 + int16 dy; // y2 - y1 + int32 co; // co = (y1 * dx) - (x1 * dy) from an equation for a + // line y * dx = x * dy + co +}; + +struct NodeData { + int16 x; + int16 y; + int16 level; + int16 prev; + int16 dist; +}; + +// because we only have 2 megas in the game! +#define TOTAL_ROUTE_SLOTS 2 + +#define MAX_FRAMES_PER_CYCLE 16 +#define NO_DIRECTIONS 8 +#define MAX_FRAMES_PER_CHAR (MAX_FRAMES_PER_CYCLE * NO_DIRECTIONS) +#define ROUTE_END_FLAG 255 + +#define MAX_WALKGRIDS 10 + +#define O_WALKANIM_SIZE 600 // max number of nodes in router output +#define O_GRID_SIZE 200 // max 200 lines & 200 points +#define O_ROUTE_SIZE 50 // max number of modules in a route + +struct RouteData { + int32 x; + int32 y; + int32 dirS; + int32 dirD; +}; + +struct PathData { + int32 x; + int32 y; + int32 dir; + int32 num; +}; + +class Router { +private: + Sword2Engine *_vm; + + int16 _standbyX; // see fnSetStandbyCoords() + int16 _standbyY; + int16 _standbyDir; + + // stores pointers to mem blocks containing routes created & used by + // megas (NULL if slot not in use) + WalkData *_routeSlots[TOTAL_ROUTE_SLOTS]; + + BarData _bars[O_GRID_SIZE]; + NodeData _node[O_GRID_SIZE]; + + int32 _walkGridList[MAX_WALKGRIDS]; + + int32 _nBars; + int32 _nNodes; + + int32 _startX; + int32 _startY; + int32 _startDir; + int32 _targetX; + int32 _targetY; + int32 _targetDir; + int32 _scaleA; + int32 _scaleB; + + RouteData _route[O_ROUTE_SIZE]; + PathData _smoothPath[O_ROUTE_SIZE]; + PathData _modularPath[O_ROUTE_SIZE]; + int32 _routeLength; + + int32 _framesPerStep; + int32 _framesPerChar; + + ObjectWalkdata _walkData; + + int8 _modX[NO_DIRECTIONS]; + int8 _modY[NO_DIRECTIONS]; + int32 _diagonalx; + int32 _diagonaly; + + int32 _firstStandFrame; + + int32 _firstStandingTurnLeftFrame; + int32 _firstStandingTurnRightFrame; + + int32 _firstWalkingTurnLeftFrame; // left walking turn + int32 _firstWalkingTurnRightFrame; // right walking turn + + uint32 _firstSlowInFrame[NO_DIRECTIONS]; + + int32 _firstSlowOutFrame; + + // number of slow-out frames on for each leading-leg in each direction + // ie. total number of slow-out frames = (numberOfSlowOutFrames * 2 * + // NO_DIRECTIONS) + + int32 _numberOfSlowOutFrames; + + int32 _stepCount; + + int32 _moduleX; + int32 _moduleY; + int32 _currentDir; + int32 _lastCount; + int32 _frame; + + uint8 returnSlotNo(uint32 megaId); + + int32 getRoute(); + void extractRoute(); + void loadWalkGrid(); + void setUpWalkGrid(byte *ob_mega, int32 x, int32 y, int32 dir); + void loadWalkData(byte *ob_walkdata); + bool scan(int32 level); + + int32 newCheck(int32 status, int32 x1, int32 y1, int32 x2, int32 y2); + bool lineCheck(int32 x1, int32 x2, int32 y1, int32 y2); + bool vertCheck(int32 x, int32 y1, int32 y2); + bool horizCheck(int32 x1, int32 y, int32 x2); + bool check(int32 x1, int32 y1, int32 x2, int32 y2); + int32 checkTarget(int32 x, int32 y); + + int32 smoothestPath(); + void slidyPath(); + + int32 smoothCheck(int32 best, int32 p, int32 dirS, int32 dirD); + + bool addSlowInFrames(WalkData *walkAnim); + void addSlowOutFrames(WalkData *walkAnim); + void slidyWalkAnimator(WalkData *walkAnim); + +#ifndef FORCE_SLIDY + int32 solidPath(); + int32 solidWalkAnimator(WalkData *walkAnim); +#endif + + void plotCross(int16 x, int16 y, uint8 colour); + +public: + Router(Sword2Engine *vm) : _vm(vm), _diagonalx(0), _diagonaly(0) { + memset(_routeSlots, 0, sizeof(_routeSlots)); + memset(_bars, 0, sizeof(_bars)); + memset(_node, 0, sizeof(_node)); + memset(_walkGridList, 0, sizeof(_walkGridList)); + memset(_route, 0, sizeof(_route)); + memset(_smoothPath, 0, sizeof(_smoothPath)); + memset(_modularPath, 0, sizeof(_modularPath)); + memset(_modX, 0, sizeof(_modX)); + memset(_modY, 0, sizeof(_modY)); + memset(_firstSlowInFrame, 0, sizeof(_firstSlowInFrame)); + } + + void setStandbyCoords(int16 x, int16 y, uint8 dir); + int whatTarget(int startX, int startY, int destX, int destY); + + // Sprites + void setSpriteStatus(byte *ob_graph, uint32 type); + void setSpriteShading(byte *ob_graph, uint32 type); + + // Animation + int doAnimate(byte *ob_logic, byte *ob_graph, int32 animRes, bool reverse); + int megaTableAnimate(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *animTable, bool reverse); + + // Walking + int doWalk(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, int16 target_x, int16 target_y, uint8 target_dir); + int walkToAnim(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint32 animRes); + int walkToTalkToMega(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint32 megaId, uint32 separation); + + // Turning + int doFace(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint8 target_dir); + int faceXY(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, int16 target_x, int16 target_y); + int faceMega(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint32 megaId); + + // Standing + void standAt(byte *ob_graph, byte *ob_mega, int32 x, int32 y, int32 dir); + void standAfterAnim(byte *ob_graph, byte *ob_mega, uint32 animRes); + void standAtAnim(byte *ob_graph, byte *ob_mega, uint32 animRes); + + int32 routeFinder(byte *ob_mega, byte *ob_walkdata, int32 x, int32 y, int32 dir); + + void earlySlowOut(byte *ob_mega, byte *ob_walkdata); + + void allocateRouteMem(); + WalkData *getRouteMem(); + void freeRouteMem(); + void freeAllRouteMem(); + void addWalkGrid(int32 gridResource); + void removeWalkGrid(int32 gridResource); + void clearWalkGridList(); + + void plotWalkGrid(); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/save_rest.cpp b/engines/sword2/save_rest.cpp new file mode 100644 index 0000000000..df03c21be3 --- /dev/null +++ b/engines/sword2/save_rest.cpp @@ -0,0 +1,414 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// --------------------------------------------------------------------------- +// SAVE_REST.CPP save, restore & restart functions +// +// James 05feb97 +// +// "Jesus Saves", but could he Restore or Restart? He can now... +// +// --------------------------------------------------------------------------- + +#include "common/stdafx.h" +#include "common/savefile.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +namespace Sword2 { + +// A savegame consists of a header and the global variables + +// Max length of a savegame filename +#define MAX_FILENAME_LEN 128 + +/** + * Calculate size of required savegame buffer + */ + +uint32 Sword2Engine::findBufferSize() { + // Size of savegame header + size of global variables + return 212 + _resman->fetchLen(1); +} + +/** + * Save the game. + */ + +uint32 Sword2Engine::saveGame(uint16 slotNo, byte *desc) { + char description[SAVE_DESCRIPTION_LEN]; + uint32 bufferSize = findBufferSize(); + byte *saveBuffer = (byte *)malloc(bufferSize); + ScreenInfo *screenInfo = _screen->getScreenInfo(); + + memset(description, 0, sizeof(description)); + strncpy(description, (char *)desc, SAVE_DESCRIPTION_LEN - 1); + + Common::MemoryWriteStream writeS(saveBuffer, bufferSize); + + byte *globalVars = _resman->openResource(1); + byte *objectHub = _resman->openResource(CUR_PLAYER_ID) + ResHeader::size(); + + // Script no. 7 - 'george_savedata_request' calls fnPassPlayerSaveData + _logic->runResScript(CUR_PLAYER_ID, 7); + + writeS.writeUint32LE(0); // Checksum + writeS.write(description, SAVE_DESCRIPTION_LEN); + writeS.writeUint32LE(_resman->fetchLen(1)); + writeS.writeUint32LE(screenInfo->background_layer_id); + writeS.writeUint32LE(_logic->getRunList()); + writeS.writeUint32LE(screenInfo->feet_x); + writeS.writeUint32LE(screenInfo->feet_y); + writeS.writeUint32LE(_sound->getLoopingMusicId()); + writeS.write(objectHub, ObjectHub::size()); + writeS.write(_logic->_saveLogic, ObjectLogic::size()); + writeS.write(_logic->_saveGraphic, ObjectGraphic::size()); + writeS.write(_logic->_saveMega, ObjectMega::size()); + writeS.write(globalVars, _resman->fetchLen(1)); + + WRITE_LE_UINT32(saveBuffer, calcChecksum(saveBuffer + 4, bufferSize - 4)); + + _resman->closeResource(CUR_PLAYER_ID); + _resman->closeResource(1); + + uint32 errorCode = saveData(slotNo, saveBuffer, bufferSize); + + free(saveBuffer); + + if (errorCode != SR_OK) { + uint32 textId; + + switch (errorCode) { + case SR_ERR_FILEOPEN: + textId = TEXT_SAVE_CANT_OPEN; + break; + default: + textId = TEXT_SAVE_FAILED; + break; + } + + _screen->displayMsg(fetchTextLine(_resman->openResource(textId / SIZE), textId & 0xffff) + 2, 0); + } + + return errorCode; +} + +uint32 Sword2Engine::saveData(uint16 slotNo, byte *buffer, uint32 bufferSize) { + char saveFileName[MAX_FILENAME_LEN]; + + sprintf(saveFileName, "%s.%.3d", _targetName.c_str(), slotNo); + + Common::OutSaveFile *out; + + if (!(out = _saveFileMan->openForSaving(saveFileName))) { + return SR_ERR_FILEOPEN; + } + + out->write(buffer, bufferSize); + out->flush(); + + if (!out->ioFailed()) { + delete out; + return SR_OK; + } + + delete out; + return SR_ERR_WRITEFAIL; +} + +/** + * Restore the game. + */ + +uint32 Sword2Engine::restoreGame(uint16 slotNo) { + uint32 bufferSize = findBufferSize(); + byte *saveBufferMem = (byte *)malloc(bufferSize); + + uint32 errorCode = restoreData(slotNo, saveBufferMem, bufferSize); + + // If it was read in successfully, then restore the game from the + // buffer & free the buffer. Note that restoreFromBuffer() frees the + // buffer in order to clear it from memory before loading in the new + // screen and runlist, so we only need to free it in case of failure. + + if (errorCode == SR_OK) + errorCode = restoreFromBuffer(saveBufferMem, bufferSize); + else + free(saveBufferMem); + + if (errorCode != SR_OK) { + uint32 textId; + + switch (errorCode) { + case SR_ERR_FILEOPEN: + textId = TEXT_RESTORE_CANT_OPEN; + break; + case SR_ERR_INCOMPATIBLE: + textId = TEXT_RESTORE_INCOMPATIBLE; + break; + default: + textId = TEXT_RESTORE_FAILED; + break; + } + + _screen->displayMsg(fetchTextLine(_resman->openResource(textId / SIZE), textId & 0xffff) + 2, 0); + } else { + // Prime system with a game cycle + + // Reset the graphic 'BuildUnit' list before a new logic list + // (see fnRegisterFrame) + _screen->resetRenderLists(); + + // Reset the mouse hot-spot list. See fnRegisterMouse() + // and fnRegisterFrame() + _mouse->resetMouseList(); + + if (_logic->processSession()) + error("restore 1st cycle failed??"); + } + + // Force the game engine to pick a cursor. This appears to be needed + // when using the -x command-line option to restore a game. + _mouse->setMouseTouching(1); + return errorCode; +} + +uint32 Sword2Engine::restoreData(uint16 slotNo, byte *buffer, uint32 bufferSize) { + char saveFileName[MAX_FILENAME_LEN]; + + sprintf(saveFileName, "%s.%.3d", _targetName.c_str(), slotNo); + + Common::InSaveFile *in; + + if (!(in = _saveFileMan->openForLoading(saveFileName))) { + // error: couldn't open file + return SR_ERR_FILEOPEN; + } + + // Read savegame into the buffer + uint32 itemsRead = in->read(buffer, bufferSize); + + delete in; + + if (itemsRead != bufferSize) { + // We didn't get all of it. At the moment we have no way of + // knowing why, so assume that it's an incompatible savegame. + + return SR_ERR_INCOMPATIBLE; + } + + return SR_OK; +} + +uint32 Sword2Engine::restoreFromBuffer(byte *buffer, uint32 size) { + Common::MemoryReadStream readS(buffer, size); + + // Calc checksum & check that aginst the value stored in the header + + if (readS.readUint32LE() != calcChecksum(buffer + 4, size - 4)) { + free(buffer); + return SR_ERR_INCOMPATIBLE; + } + + readS.seek(SAVE_DESCRIPTION_LEN, SEEK_CUR); + + // Check savegame against length of current global variables resource + // This would most probably be trapped by the checksum test anyway, + // but it doesn't do any harm to check this as well. + + // Historical note: During development, earlier savegames would often + // be shorter than the current expected length. + + if (readS.readUint32LE() != _resman->fetchLen(1)) { + free(buffer); + return SR_ERR_INCOMPATIBLE; + } + + byte *globalVars = _resman->openResource(1); + byte *objectHub = _resman->openResource(CUR_PLAYER_ID) + ResHeader::size(); + + uint32 screenId = readS.readUint32LE(); + uint32 runListId = readS.readUint32LE(); + uint32 feetX = readS.readUint32LE(); + uint32 feetY = readS.readUint32LE(); + uint32 musicId = readS.readUint32LE(); + + // Trash all resources from memory except player object & global vars + _resman->killAll(false); + _logic->resetKillList(); + + readS.read(objectHub, ObjectHub::size()); + readS.read(_logic->_saveLogic, ObjectLogic::size()); + readS.read(_logic->_saveGraphic, ObjectGraphic::size()); + readS.read(_logic->_saveMega, ObjectMega::size()); + + // Fill out the player object structures from the savegame structures. + // Also run the appropriate scripts to set up George's anim tables and + // walkdata, and Nico's anim tables. + + // Script no. 8 - 'george_savedata_return' calls fnGetPlayerSaveData + _logic->runResScript(CUR_PLAYER_ID, 8); + + // Script no. 14 - 'set_up_nico_anim_tables' + _logic->runResScript(CUR_PLAYER_ID, 14); + + // Which megaset was the player at the time of saving? + ObjectMega obMega(_logic->_saveMega); + + uint32 scriptNo = 0; + + switch (obMega.getMegasetRes()) { + case 36: // GeoMega: + scriptNo = 9; // script no.9 - 'player_is_george' + break; + case 2003: // GeoMegaB: + scriptNo = 13; // script no.13 - 'player_is_georgeB' + break; + case 1366: // NicMegaA: + scriptNo = 11; // script no.11 - 'player_is_nicoA' + break; + case 1437: // NicMegaB: + scriptNo = 12; // script no.12 - 'player_is_nicoB' + break; + case 1575: // NicMegaC: + scriptNo = 10; // script no.10 - 'player_is_nicoC' + break; + } + + _logic->runResScript(CUR_PLAYER_ID, scriptNo); + + // Copy variables from savegame buffer to memory + readS.read(globalVars, _resman->fetchLen(1)); + + _resman->closeResource(CUR_PLAYER_ID); + _resman->closeResource(1); + + free(buffer); + + int32 pars[2]; + + pars[0] = screenId; + pars[1] = 1; + _logic->fnInitBackground(pars); + + ScreenInfo *screenInfo = _screen->getScreenInfo(); + + // So palette not restored immediately after control panel - we want to + // fade up instead! + screenInfo->new_palette = 99; + + // These need setting after the defaults get set in fnInitBackground. + // Remember that these can change through the game, so need saving & + // restoring too. + + screenInfo->feet_x = feetX; + screenInfo->feet_y = feetY; + + // Start the new run list + _logic->expressChangeSession(runListId); + + // Force in the new scroll position, so unsightly scroll-catch-up does + // not occur when screen first draws after returning from restore panel + + // Set the screen record of player position - ready for setScrolling() + + screenInfo->player_feet_x = obMega.getFeetX(); + screenInfo->player_feet_y = obMega.getFeetY(); + + // if this screen is wide, recompute the scroll offsets now + if (screenInfo->scroll_flag) + _screen->setScrolling(); + + // Any music required will be started after we've returned from + // restoreControl() - see systemMenuMouse() in mouse.cpp! + + // Restart any looping music. Originally this was - and still is - done + // in systemMenuMouse(), but with ScummVM we have other ways of + // restoring savegames so it's easier to put it here as well. + + if (musicId) { + pars[0] = musicId; + pars[1] = FX_LOOP; + _logic->fnPlayMusic(pars); + } else + _logic->fnStopMusic(NULL); + + return SR_OK; +} + +/** + * Get the description of a savegame + */ + +uint32 Sword2Engine::getSaveDescription(uint16 slotNo, byte *description) { + char saveFileName[MAX_FILENAME_LEN]; + + sprintf(saveFileName, "%s.%.3d", _targetName.c_str(), slotNo); + + Common::InSaveFile *in; + + if (!(in = _saveFileMan->openForLoading(saveFileName))) { + return SR_ERR_FILEOPEN; + } + + in->readUint32LE(); + in->read(description, SAVE_DESCRIPTION_LEN); + + delete in; + return SR_OK; +} + +bool Sword2Engine::saveExists() { + for (int i = 0; i <= 99; i++) + if (saveExists(i)) + return true; + return false; +} + +bool Sword2Engine::saveExists(uint16 slotNo) { + char saveFileName[MAX_FILENAME_LEN]; + + sprintf(saveFileName, "%s.%.3d", _targetName.c_str(), slotNo); + + Common::InSaveFile *in; + + if (!(in = _saveFileMan->openForLoading(saveFileName))) { + return false; + } + + delete in; + return true; +} + +uint32 Sword2Engine::calcChecksum(byte *buffer, uint32 size) { + uint32 total = 0; + + for (uint32 pos = 0; pos < size; pos++) + total += buffer[pos]; + + return total; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/save_rest.h b/engines/sword2/save_rest.h new file mode 100644 index 0000000000..9463f3fd4a --- /dev/null +++ b/engines/sword2/save_rest.h @@ -0,0 +1,47 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef SAVE_REST_H +#define SAVE_REST_H + +namespace Sword2 { + +#define SAVE_DESCRIPTION_LEN 64 + +// Save & Restore error codes + +enum { + SR_OK, // No worries + SR_ERR_FILEOPEN, // Can't open file - Couldn't create file for + // saving, or couldn't find file for loading. + SR_ERR_INCOMPATIBLE, // (Restore) Incompatible savegame data. + // Savegame file is obsolete. (Won't happen + // after development stops) + SR_ERR_READFAIL, // (Restore) Failed on reading savegame file - + // Something screwed up during the read + SR_ERR_WRITEFAIL // (Save) Failed on writing savegame file - + // Something screwed up during the write - + // could be hard-drive full..? +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/scroll.cpp b/engines/sword2/scroll.cpp new file mode 100644 index 0000000000..3d0a263bc6 --- /dev/null +++ b/engines/sword2/scroll.cpp @@ -0,0 +1,151 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" + +namespace Sword2 { + +// Max no of pixel allowed to scroll per cycle +#define MAX_SCROLL_DISTANCE 8 + +/** + * Sets the scroll target position for the end of the game cycle. The driver + * will then automatically scroll as many times as it can to reach this + * position in the allotted time. + */ + +void Screen::setScrollTarget(int16 sx, int16 sy) { + _scrollXTarget = sx; + _scrollYTarget = sy; +} + +/** + * If the room is larger than the physical screen, this function is called + * every game cycle to update the scroll offsets. + */ + +void Screen::setScrolling() { + // Normally we aim to get George's feet at (320,250) from top left + // of screen window + // feet_x = 128 + 320 + // feet_y = 128 + 250 + + // Set scroll offsets according to the player's coords + + // If the scroll offsets are being forced in script, ensure that they + // are neither too far to the right nor too far down. + uint32 scrollX = _vm->_logic->readVar(SCROLL_X); + uint32 scrollY = _vm->_logic->readVar(SCROLL_Y); + + if (scrollX || scrollY) { + _thisScreen.scroll_offset_x = MIN((uint16)scrollX, _thisScreen.max_scroll_offset_x); + _thisScreen.scroll_offset_y = MIN((uint16)scrollY, _thisScreen.max_scroll_offset_y); + return; + } + + // George's offset from the centre - the desired position for him + + int16 offset_x = _thisScreen.player_feet_x - _thisScreen.feet_x; + int16 offset_y = _thisScreen.player_feet_y - _thisScreen.feet_y; + + // Prevent scrolling too far left/right/up/down + + if (offset_x < 0) + offset_x = 0; + else if (offset_x > _thisScreen.max_scroll_offset_x) + offset_x = _thisScreen.max_scroll_offset_x; + + if (offset_y < 0) + offset_y = 0; + else if (offset_y > _thisScreen.max_scroll_offset_y) + offset_y = _thisScreen.max_scroll_offset_y; + + // First time on this screen - need absolute scroll immediately! + + if (_thisScreen.scroll_flag == 2) { + debug(5, "init scroll"); + _thisScreen.scroll_offset_x = offset_x; + _thisScreen.scroll_offset_y = offset_y; + _thisScreen.scroll_flag = 1; + return; + } + + // Catch up with required scroll offsets - speed depending on distance + // to catch up (dx and dy) and _scrollFraction used, but limit to + // certain number of pixels per cycle (MAX_SCROLL_DISTANCE) + + int16 dx = _thisScreen.scroll_offset_x - offset_x; + int16 dy = _thisScreen.scroll_offset_y - offset_y; + + uint16 scroll_distance_x; // how much we want to scroll + uint16 scroll_distance_y; + + if (dx < 0) { + // Current scroll_offset_x is less than the required value + + // NB. I'm adding 1 to the result of dx / SCROLL_FRACTION, + // because it would otherwise not scroll at all when + // dx < SCROLL_FRACTION + + // => inc by (fraction of the differnce) NB. dx is -ve, so we + // subtract dx / SCROLL_FRACTION + + scroll_distance_x = 1 - dx / _scrollFraction; + + if (scroll_distance_x > MAX_SCROLL_DISTANCE) + scroll_distance_x = MAX_SCROLL_DISTANCE; + + _thisScreen.scroll_offset_x += scroll_distance_x; + } else if (dx > 0) { + // Current scroll_offset_x is greater than + // the required value + + // => dec by (fraction of the differnce) + + scroll_distance_x = 1 + dx / _scrollFraction; + + if (scroll_distance_x > MAX_SCROLL_DISTANCE) + scroll_distance_x = MAX_SCROLL_DISTANCE; + + _thisScreen.scroll_offset_x -= scroll_distance_x; + } + + if (dy < 0) { + scroll_distance_y = 1 - dy / _scrollFraction; + + if (scroll_distance_y > MAX_SCROLL_DISTANCE) + scroll_distance_y = MAX_SCROLL_DISTANCE; + + _thisScreen.scroll_offset_y += scroll_distance_y; + } else if (dy > 0) { + scroll_distance_y = 1 + dy / _scrollFraction; + + if (scroll_distance_y > MAX_SCROLL_DISTANCE) + scroll_distance_y = MAX_SCROLL_DISTANCE; + + _thisScreen.scroll_offset_y -= scroll_distance_y; + } +} + +} // End of namespace Sword2 diff --git a/engines/sword2/sound.cpp b/engines/sword2/sound.cpp new file mode 100644 index 0000000000..d855e88afb --- /dev/null +++ b/engines/sword2/sound.cpp @@ -0,0 +1,319 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// --------------------------------------------------------------------------- +// BROKEN SWORD 2 +// +// SOUND.CPP Contains the sound engine, fx & music functions +// Some very 'sound' code in here ;) +// +// (16Dec96 JEL) +// +// --------------------------------------------------------------------------- + +#include "common/stdafx.h" +#include "common/file.h" +#include "common/system.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/resman.h" +#include "sword2/sound.h" + +#include "sound/wave.h" + +namespace Sword2 { + +Sound::Sound(Sword2Engine *vm) { + int i; + + _vm = vm; + + for (i = 0; i < FXQ_LENGTH; i++) + _fxQueue[i].resource = 0; + + for (i = 0; i < MAXMUS; i++) { + _music[i] = NULL; + + _musicFile[i].idxTab = NULL; + _musicFile[i].idxLen = 0; + _musicFile[i].fileSize = 0; + _musicFile[i].fileType = 0; + _musicFile[i].inUse = false; + + _speechFile[i].idxTab = NULL; + _speechFile[i].idxLen = 0; + _speechFile[i].fileSize = 0; + _speechFile[i].fileType = 0; + _speechFile[i].inUse = false; + } + + _speechPaused = false; + _musicPaused = false; + _fxPaused = false; + + _speechMuted = false; + _musicMuted = false; + _fxMuted = false; + + _reverseStereo = false; + + _loopingMusicId = 0; + + _mixBuffer = NULL; + _mixBufferLen = 0; + + _vm->_mixer->setupPremix(this, Audio::Mixer::kMusicSoundType); +} + +Sound::~Sound() { + _vm->_mixer->setupPremix(0); + + clearFxQueue(); + stopMusic(true); + stopSpeech(); + + free(_mixBuffer); + + for (int i = 0; i < MAXMUS; i++) { + if (_musicFile[i].file.isOpen()) + _musicFile[i].file.close(); + if (_speechFile[i].file.isOpen()) + _speechFile[i].file.close(); + + free(_musicFile[i].idxTab); + free(_speechFile[i].idxTab); + } +} + +void Sound::setReverseStereo(bool reverse) { + if (reverse != _reverseStereo) { + _reverseStereo = reverse; + + for (int i = 0; i < FXQ_LENGTH; i++) { + if (!_fxQueue[i].resource) + continue; + + _fxQueue[i].pan = -_fxQueue[i].pan; + _vm->_mixer->setChannelBalance(_fxQueue[i].handle, _fxQueue[i].pan); + } + } +} + +/** + * Stop all sounds, close their resources and clear the FX queue. + */ + +void Sound::clearFxQueue() { + for (int i = 0; i < FXQ_LENGTH; i++) { + if (_fxQueue[i].resource) { + stopFx(i); + } + } +} + +/** + * Process the FX queue. This function is called once every game cycle. + */ + +void Sound::processFxQueue() { + for (int i = 0; i < FXQ_LENGTH; i++) { + if (!_fxQueue[i].resource) + continue; + + switch (_fxQueue[i].type) { + case FX_RANDOM: + // 1 in 'delay' chance of this fx occurring + if (_vm->_rnd.getRandomNumber(_fxQueue[i].delay) == 0) + playFx(&_fxQueue[i]); + break; + case FX_SPOT: + if (_fxQueue[i].delay) + _fxQueue[i].delay--; + else { + playFx(&_fxQueue[i]); + _fxQueue[i].type = FX_SPOT2; + } + break; + case FX_LOOP: + playFx(&_fxQueue[i]); + _fxQueue[i].type = FX_LOOPING; + break; + case FX_SPOT2: + // Once the FX has finished remove it from the queue. + if (!_vm->_mixer->isSoundHandleActive(_fxQueue[i].handle)) { + _vm->_resman->closeResource(_fxQueue[i].resource); + _fxQueue[i].resource = 0; + } + break; + case FX_LOOPING: + // Once the looped FX has started we can ignore it, + // but we can't close it since the WAV data is in use. + break; + } + } +} + +/** + * Queue a sound effect for playing later. + * @param res the sound resource number + * @param type the type of sound effect + * @param delay when to play the sound effect + * @param volume the sound effect volume (0 through 16) + * @param pan the sound effect panning (-16 through 16) + */ + +void Sound::queueFx(int32 res, int32 type, int32 delay, int32 volume, int32 pan) { + if (_vm->_wantSfxDebug) { + const char *typeStr; + + switch (type) { + case FX_SPOT: + typeStr = "SPOT"; + break; + case FX_LOOP: + typeStr = "LOOPED"; + break; + case FX_RANDOM: + typeStr = "RANDOM"; + break; + default: + typeStr = "INVALID"; + break; + } + + byte buf[NAME_LEN]; + + debug(0, "SFX (sample=\"%s\", vol=%d, pan=%d, delay=%d, type=%s)", _vm->_resman->fetchName(res, buf), volume, pan, delay, typeStr); + } + + for (int i = 0; i < FXQ_LENGTH; i++) { + if (!_fxQueue[i].resource) { + byte *data = _vm->_resman->openResource(res); + + assert(_vm->_resman->fetchType(data) == WAV_FILE); + + uint32 len = _vm->_resman->fetchLen(res) - ResHeader::size(); + + if (type == FX_RANDOM) { + // For spot effects and loops the delay is the + // number of frames to wait. For random + // effects, however, it's the average number of + // seconds between playing the sound, so we + // have to multiply by the frame rate. + delay *= 12; + } + + volume = (volume * Audio::Mixer::kMaxChannelVolume) / 16; + pan = (pan * 127) / 16; + + if (isReverseStereo()) + pan = -pan; + + _fxQueue[i].resource = res; + _fxQueue[i].data = data + ResHeader::size(); + _fxQueue[i].len = len; + _fxQueue[i].delay = delay; + _fxQueue[i].volume = volume; + _fxQueue[i].pan = pan; + _fxQueue[i].type = type; + + // Keep track of the index in the loop so that + // fnStopFx() can be used later to kill this sound. + // Mainly for FX_LOOP and FX_RANDOM. + + _vm->_logic->writeVar(RESULT, i); + return; + } + } + + warning("No free slot in FX queue"); +} + +int32 Sound::playFx(FxQueueEntry *fx) { + return playFx(&fx->handle, fx->data, fx->len, fx->volume, fx->pan, (fx->type == FX_LOOP), Audio::Mixer::kSFXSoundType); +} + +int32 Sound::playFx(Audio::SoundHandle *handle, byte *data, uint32 len, uint8 vol, int8 pan, bool loop, Audio::Mixer::SoundType soundType) { + if (_fxMuted) + return RD_OK; + + if (_vm->_mixer->isSoundHandleActive(*handle)) + return RDERR_FXALREADYOPEN; + + Common::MemoryReadStream stream(data, len); + int rate, size; + byte flags; + + if (!loadWAVFromStream(stream, size, rate, flags)) { + warning("playFX: Not a valid WAV file"); + return RDERR_INVALIDWAV; + } + + // The resource manager must have complete control over when resources + // are freed, or reference counting will break horribly. Besides, the + // data pointer is not valid for passing to free(). Why the hell is the + // AUTOFREE flag set by default anyway? + + flags &= ~Audio::Mixer::FLAG_AUTOFREE; + + if (isReverseStereo()) + flags |= Audio::Mixer::FLAG_REVERSE_STEREO; + + if (loop) + flags |= Audio::Mixer::FLAG_LOOP; + + _vm->_mixer->playRaw(handle, data + stream.pos(), size, rate, flags, -1, vol, pan, 0, 0, soundType); + return RD_OK; +} + +/** + * This function closes a sound effect which has been previously opened for + * playing. Sound effects must be closed when they are finished with, otherwise + * you will run out of sound effect buffers. + * @param i the index of the sound to close + */ + +int32 Sound::stopFx(int32 i) { + if (!_fxQueue[i].resource) + return RDERR_FXNOTOPEN; + + _vm->_mixer->stopHandle(_fxQueue[i].handle); + + _vm->_resman->closeResource(_fxQueue[i].resource); + _fxQueue[i].resource = 0; + return RD_OK; +} + +void Sound::pauseAllSound() { + pauseMusic(); + pauseSpeech(); + pauseFx(); +} + +void Sound::unpauseAllSound() { + unpauseMusic(); + unpauseSpeech(); + unpauseFx(); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/sound.h b/engines/sword2/sound.h new file mode 100644 index 0000000000..7390a60bbc --- /dev/null +++ b/engines/sword2/sound.h @@ -0,0 +1,272 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +/***************************************************************************** + * SOUND.H Sound engine + * + * SOUND.CPP Contains the sound engine, fx & music functions + * Some very 'sound' code in here ;) + * + * (16Dec96 JEL) + * + ****************************************************************************/ + +#ifndef SOUND_H +#define SOUND_H + +#include "common/file.h" +#include "sound/audiostream.h" +#include "sound/mixer.h" + +// Max number of sound fx +#define MAXMUS 2 + +// Max number of fx in queue at once +#define FXQ_LENGTH 32 + +#define BUFFER_SIZE 4096 + +namespace Sword2 { + +enum { + kCLUMode = 1, + kMP3Mode, + kVorbisMode, + kFlacMode +}; + +enum { + // These three types correspond to types set by the scripts + FX_SPOT = 0, + FX_LOOP = 1, + FX_RANDOM = 2, + + // These are used for FX queue bookkeeping + FX_SPOT2 = 3, + FX_LOOPING = 4 +}; + +// Sound defines + +enum { + RDSE_SAMPLEFINISHED = 0, + RDSE_SAMPLEPLAYING = 1, + RDSE_FXTOCLEAR = 0, // Unused + RDSE_FXCACHED = 1, // Unused + RDSE_FXSPOT = 0, + RDSE_FXLOOP = 1, + RDSE_FXLEADIN = 2, + RDSE_FXLEADOUT = 3, + RDSE_QUIET = 1, + RDSE_SPEAKING = 0 +}; + +class CLUInputStream : public AudioStream { +private: + Common::File *_file; + bool _firstTime; + uint32 _file_pos; + uint32 _end_pos; + int16 _outbuf[BUFFER_SIZE]; + byte _inbuf[BUFFER_SIZE]; + const int16 *_bufferEnd; + const int16 *_pos; + + uint16 _prev; + + void refill(); + + inline bool eosIntern() const { + return _pos >= _bufferEnd; + } + +public: + CLUInputStream(Common::File *file, int size); + ~CLUInputStream(); + + int readBuffer(int16 *buffer, const int numSamples); + + bool endOfData() const { return eosIntern(); } + bool isStereo() const { return false; } + int getRate() const { return 22050; } +}; + +struct SoundFileHandle { + Common::File file; + uint32 *idxTab; + uint32 idxLen; + uint32 fileSize; + uint32 fileType; + volatile bool inUse; +}; + +class MusicInputStream : public AudioStream { +private: + int _cd; + SoundFileHandle *_fh; + uint32 _musicId; + AudioStream *_decoder; + int16 _buffer[BUFFER_SIZE]; + const int16 *_bufferEnd; + const int16 *_pos; + bool _remove; + uint32 _numSamples; + uint32 _samplesLeft; + bool _looping; + int32 _fading; + int32 _fadeSamples; + bool _paused; + + void refill(); + + inline bool eosIntern() const { + if (_looping) + return false; + return _remove || _pos >= _bufferEnd; + } + +public: + MusicInputStream(int cd, SoundFileHandle *fh, uint32 musicId, bool looping); + ~MusicInputStream(); + + int readBuffer(int16 *buffer, const int numSamples); + + bool endOfData() const { return eosIntern(); } + bool isStereo() const { return _decoder->isStereo(); } + int getRate() const { return _decoder->getRate(); } + + int getCD() { return _cd; } + + void fadeUp(); + void fadeDown(); + + bool isReady() { return _decoder != NULL; } + int32 isFading() { return _fading; } + + bool readyToRemove(); + int32 getTimeRemaining(); +}; + +class Sound : public AudioStream { +private: + Sword2Engine *_vm; + + Common::Mutex _mutex; + + struct FxQueueEntry { + Audio::SoundHandle handle; // sound handle + uint32 resource; // resource id of sample + byte *data; // pointer to WAV data + uint32 len; // WAV data length + uint16 delay; // cycles to wait before playing (or 'random chance' if FX_RANDOM) + uint8 volume; // sound volume + int8 pan; // sound panning + uint8 type; // FX_SPOT, FX_RANDOM, FX_LOOP + }; + + FxQueueEntry _fxQueue[FXQ_LENGTH]; + + void triggerFx(uint8 i); + + bool _reverseStereo; + + bool _speechMuted; + bool _fxMuted; + bool _musicMuted; + + bool _speechPaused; + bool _fxPaused; + bool _musicPaused; + + int32 _loopingMusicId; + + Audio::SoundHandle _soundHandleSpeech; + + MusicInputStream *_music[MAXMUS]; + SoundFileHandle _musicFile[MAXMUS]; + SoundFileHandle _speechFile[MAXMUS]; + + int16 *_mixBuffer; + int _mixBufferLen; + +public: + Sound(Sword2Engine *vm); + ~Sound(); + + // AudioStream API + + int readBuffer(int16 *buffer, const int numSamples); + bool isStereo() const { return false; } + bool endOfData() const; + int getRate() const { return 22050; } + + // End of AudioStream API + + void clearFxQueue(); + void processFxQueue(); + + void setReverseStereo(bool reverse); + bool isReverseStereo() const { return _reverseStereo; } + + void muteSpeech(bool mute); + bool isSpeechMute() const { return _speechMuted; } + + void muteFx(bool mute); + bool isFxMute() const { return _fxMuted; } + + void muteMusic(bool mute) { _musicMuted = mute; } + bool isMusicMute() const { return _musicMuted; } + + void setLoopingMusicId(int32 id) { _loopingMusicId = id; } + int32 getLoopingMusicId() const { return _loopingMusicId; } + + void pauseSpeech(); + void unpauseSpeech(); + + void pauseFx(); + void unpauseFx(); + + void pauseMusic(); + void unpauseMusic(); + + void pauseAllSound(); + void unpauseAllSound(); + + void queueFx(int32 res, int32 type, int32 delay, int32 volume, int32 pan); + int32 playFx(FxQueueEntry *fx); + int32 playFx(Audio::SoundHandle *handle, byte *data, uint32 len, uint8 vol, int8 pan, bool loop, Audio::Mixer::SoundType soundType); + int32 stopFx(int32 i); + int32 setFxIdVolumePan(int32 id, int vol, int pan = 255); + + int32 getSpeechStatus(); + int32 amISpeaking(); + int32 playCompSpeech(uint32 speechId, uint8 vol, int8 pan); + uint32 preFetchCompSpeech(uint32 speechId, uint16 **buf); + int32 stopSpeech(); + + int32 streamCompMusic(uint32 musicId, bool loop); + void stopMusic(bool immediately); + int32 musicTimeRemaining(); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/speech.cpp b/engines/sword2/speech.cpp new file mode 100644 index 0000000000..da6bd258ea --- /dev/null +++ b/engines/sword2/speech.cpp @@ -0,0 +1,229 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/file.h" + +#include "sword2/sword2.h" +#include "sword2/console.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/resman.h" + +namespace Sword2 { + +// To request the status of a target, we run its 4th script, get-speech-state. +// This will cause RESULT to be set to either 1 (target is waiting) or 0 +// (target is busy). + +// Distance kept above talking sprite +#define GAP_ABOVE_HEAD 20 + +enum { + S_OB_GRAPHIC = 0, + S_OB_SPEECH = 1, + S_OB_LOGIC = 2, + S_OB_MEGA = 3, + + S_TEXT = 4, + S_WAV = 5, + S_ANIM = 6, + S_DIR_TABLE = 7, + S_ANIM_MODE = 8 +}; + +/** + * Sets _textX and _textY for position of text sprite. Note that _textX is + * also used to calculate speech pan. + */ + +void Logic::locateTalker(int32 *params) { + // params: 0 pointer to ob_graphic + // 1 pointer to ob_speech + // 2 pointer to ob_logic + // 3 pointer to ob_mega + // 4 encoded text number + // 5 wav res id + // 6 anim res id + // 7 pointer to anim table + // 8 animation mode 0 lip synced, + // 1 just straight animation + + if (!_animId) { + // There is no animation. Assume it's voice-over text, and put + // it at the bottom of the screen. + + _textX = 320; + _textY = 400; + return; + } + + byte *file = _vm->_resman->openResource(_animId); + + // '0' means 1st frame + + CdtEntry cdt_entry; + FrameHeader frame_head; + + cdt_entry.read(_vm->fetchCdtEntry(file, 0)); + frame_head.read(_vm->fetchFrameHeader(file, 0)); + + // Note: This part of the code is quite similar to registerFrame(). + + if (cdt_entry.frameType & FRAME_OFFSET) { + // The frame has offsets, i.e. it's a scalable mega frame + ObjectMega obMega(decodePtr(params[S_OB_MEGA])); + + uint16 scale = obMega.calcScale(); + + // Calc suitable centre point above the head, based on scaled + // height + + // just use 'feet_x' as centre + _textX = obMega.getFeetX(); + + // Add scaled y-offset to feet_y coord to get top of sprite + _textY = obMega.getFeetY() + (cdt_entry.y * scale) / 256; + } else { + // It's a non-scaling anim - calc suitable centre point above + // the head, based on scaled width + + // x-coord + half of width + _textX = cdt_entry.x + frame_head.width / 2; + _textY = cdt_entry.y; + } + + _vm->_resman->closeResource(_animId); + + // Leave space above their head + _textY -= GAP_ABOVE_HEAD; + + // Adjust the text coords for RDSPR_DISPLAYALIGN + + ScreenInfo *screenInfo = _vm->_screen->getScreenInfo(); + + _textX -= screenInfo->scroll_offset_x; + _textY -= screenInfo->scroll_offset_y; +} + +/** + * This function is called the first time to build the text, if we need one. If + * If necessary it also brings in the wav and sets up the animation. + * + * If there is an animation it can be repeating lip-sync or run-once. + * + * If there is no wav, then the text comes up instead. There can be any + * combination of text/wav playing. + */ + +void Logic::formText(int32 *params) { + // params 0 pointer to ob_graphic + // 1 pointer to ob_speech + // 2 pointer to ob_logic + // 3 pointer to ob_mega + // 4 encoded text number + // 5 wav res id + // 6 anim res id + // 7 pointer to anim table + // 8 animation mode 0 lip synced, + // 1 just straight animation + + // There should always be a text line, as all text is derived from it. + // If there is none, that's bad... + + if (!params[S_TEXT]) { + warning("No text line for speech wav %d", params[S_WAV]); + return; + } + + ObjectSpeech obSpeech(decodePtr(params[S_OB_SPEECH])); + + // Establish the max width allowed for this text sprite. + uint32 textWidth = obSpeech.getWidth(); + + if (!textWidth) + textWidth = 400; + + // Pull out the text line, and make the sprite and text block + + uint32 text_res = params[S_TEXT] / SIZE; + uint32 local_text = params[S_TEXT] & 0xffff; + byte *text = _vm->fetchTextLine(_vm->_resman->openResource(text_res), local_text); + + // 'text + 2' to skip the first 2 bytes which form the line reference + // number + + _speechTextBlocNo = _vm->_fontRenderer->buildNewBloc( + text + 2, _textX, _textY, + textWidth, obSpeech.getPen(), + RDSPR_TRANS | RDSPR_DISPLAYALIGN, + _vm->_speechFontId, POSITION_AT_CENTRE_OF_BASE); + + _vm->_resman->closeResource(text_res); + + // Set speech duration, in case not using a wav. + _speechTime = strlen((char *)text) + 30; +} + +/** + * There are some hard-coded cases where speech is used to illustrate a sound + * effect. In this case there is no sound associated with the speech itself. + */ + +bool Logic::wantSpeechForLine(uint32 wavId) { + switch (wavId) { + case 1328: // AttendantSpeech + // SFX(Phone71); + // FX <Telephone rings> + case 2059: // PabloSpeech + // SFX (2059); + // FX <Sound of sporadic gunfire from below> + case 4082: // DuaneSpeech + // SFX (4082); + // FX <Pffffffffffft! Frp. (Unimpressive, flatulent noise.)> + case 4214: // cat_52 + // SFX (4214); + // 4214FXMeow! + case 4568: // trapdoor_13 + // SFX (4568); + // 4568fx<door slamming> + case 4913: // LobineauSpeech + // SFX (tone2); + // FX <Lobineau hangs up> + case 5120: // bush_66 + // SFX (5120); + // 5120FX<loud buzzing> + case 528: // PresidentaSpeech + // SFX (528); + // FX <Nearby Crash of Collapsing Masonry> + case 920: // Zombie Island forest maze (bird) + case 923: // Zombie Island forest maze (monkey) + case 926: // Zombie Island forest maze (zombie) + // Don't want speech for these lines! + return false; + default: + // Ok for all other lines + return true; + } +} + +} // End of namespace Sword2 diff --git a/engines/sword2/sprite.cpp b/engines/sword2/sprite.cpp new file mode 100644 index 0000000000..a0ba7f7189 --- /dev/null +++ b/engines/sword2/sprite.cpp @@ -0,0 +1,655 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/build_display.h" + +namespace Sword2 { + +/** + * This function takes a sprite and creates a mirror image of it. + * @param dst destination buffer + * @param src source buffer + * @param w width of the sprite + * @param h height of the sprite + */ + +void Screen::mirrorSprite(byte *dst, byte *src, int16 w, int16 h) { + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + *dst++ = *(src + w - x - 1); + } + src += w; + } +} + +/** + * This function takes a compressed frame of a sprite with up to 256 colours + * and decompresses it. + * @param dst destination buffer + * @param src source buffer + * @param decompSize the expected size of the decompressed sprite + */ + +int32 Screen::decompressRLE256(byte *dst, byte *src, int32 decompSize) { + // PARAMETERS: + // source points to the start of the sprite data for input + // decompSize gives size of decompressed data in bytes + // dest points to start of destination buffer for decompressed + // data + + byte headerByte; // block header byte + byte *endDest = dst + decompSize; // pointer to byte after end of decomp buffer + int32 rv; + + while (1) { + // FLAT block + // read FLAT block header & increment 'scan' to first pixel + // of block + headerByte = *src++; + + // if this isn't a zero-length block + if (headerByte) { + if (dst + headerByte > endDest) { + rv = 1; + break; + } + + // set the next 'headerByte' pixels to the next colour + // at 'source' + memset(dst, *src, headerByte); + + // increment destination pointer to just after this + // block + dst += headerByte; + + // increment source pointer to just after this colour + src++; + + // if we've decompressed all of the data + if (dst == endDest) { + rv = 0; // return "OK" + break; + } + } + + // RAW block + // read RAW block header & increment 'scan' to first pixel of + // block + headerByte = *src++; + + // if this isn't a zero-length block + if (headerByte) { + if (dst + headerByte > endDest) { + rv = 1; + break; + } + + // copy the next 'headerByte' pixels from source to + // destination + memcpy(dst, src, headerByte); + + // increment destination pointer to just after this + // block + dst += headerByte; + + // increment source pointer to just after this block + src += headerByte; + + // if we've decompressed all of the data + if (dst == endDest) { + rv = 0; // return "OK" + break; + } + } + } + + return rv; +} + +/** + * Unwinds a run of 16-colour data into 256-colour palette data. + */ + +void Screen::unwindRaw16(byte *dst, byte *src, uint8 blockSize, byte *colTable) { + // for each pair of pixels + while (blockSize > 1) { + // 1st colour = number in table at position given by upper + // nibble of source byte + *dst++ = colTable[(*src) >> 4]; + + // 2nd colour = number in table at position given by lower + // nibble of source byte + *dst++ = colTable[(*src) & 0x0f]; + + // point to next source byte + src++; + + // decrement count of how many pixels left to read + blockSize -= 2; + } + + // if there's a final odd pixel + if (blockSize) { + // colour = number in table at position given by upper nibble + // of source byte + *dst++ = colTable[(*src) >> 4]; + } +} + +/** + * This function takes a compressed frame of a sprite (with up to 16 colours) + * and decompresses it. + * @param dst destination buffer + * @param src source buffer + * @param decompSize the expected size of the uncompressed sprite + * @param colTable mapping from the 16 encoded colours to the current palette + */ + +int32 Screen::decompressRLE16(byte *dst, byte *src, int32 decompSize, byte *colTable) { + byte headerByte; // block header byte + byte *endDest = dst + decompSize; // pointer to byte after end of decomp buffer + int32 rv; + + while (1) { + // FLAT block + // read FLAT block header & increment 'scan' to first pixel + // of block + headerByte = *src++; + + // if this isn't a zero-length block + if (headerByte) { + if (dst + headerByte > endDest) { + rv = 1; + break; + } + + // set the next 'headerByte' pixels to the next + // colour at 'source' + memset(dst, *src, headerByte); + + // increment destination pointer to just after this + // block + dst += headerByte; + + // increment source pointer to just after this colour + src++; + + // if we've decompressed all of the data + if (dst == endDest) { + rv = 0; // return "OK" + break; + } + } + + // RAW block + // read RAW block header & increment 'scan' to first pixel of + // block + headerByte = *src++; + + // if this isn't a zero-length block + if (headerByte) { + if (dst + headerByte > endDest) { + rv = 1; + break; + } + + // copy the next 'headerByte' pixels from source to + // destination (NB. 2 pixels per byte) + unwindRaw16(dst, src, headerByte, colTable); + + // increment destination pointer to just after this + // block + dst += headerByte; + + // increment source pointer to just after this block + // (NB. headerByte gives pixels, so /2 for bytes) + src += (headerByte + 1) / 2; + + // if we've decompressed all of the data + if (dst >= endDest) { + rv = 0; // return "OK" + break; + } + } + } + + return rv; +} + +/** + * Creates a sprite surface. Sprite surfaces are used by the in-game dialogs + * and for displaying cutscene subtitles, which makes them much easier to draw + * than standard sprites. + * @param s information about how to decode the sprite + * @param sprite the buffer that will be created to store the surface + * @return RD_OK, or an error code + */ + +int32 Screen::createSurface(SpriteInfo *s, byte **sprite) { + *sprite = (byte *)malloc(s->w * s->h); + if (!*sprite) + return RDERR_OUTOFMEMORY; + + // Surfaces are either uncompressed or RLE256-compressed. No need to + // test for anything else. + + if (s->type & RDSPR_NOCOMPRESSION) { + memcpy(*sprite, s->data, s->w * s->h); + } else if (decompressRLE256(*sprite, s->data, s->w * s->h)) { + free(*sprite); + return RDERR_DECOMPRESSION; + } + + return RD_OK; +} + +/** + * Draws the sprite surface created earlier. + * @param s information about how to place the sprite + * @param surface pointer to the surface created earlier + * @param clipRect the clipping rectangle + */ + +void Screen::drawSurface(SpriteInfo *s, byte *surface, Common::Rect *clipRect) { + Common::Rect rd, rs; + uint16 x, y; + byte *src, *dst; + + rs.left = 0; + rs.right = s->w; + rs.top = 0; + rs.bottom = s->h; + + rd.left = s->x; + rd.right = rd.left + rs.right; + rd.top = s->y; + rd.bottom = rd.top + rs.bottom; + + Common::Rect defClipRect(0, 0, _screenWide, _screenDeep); + + if (!clipRect) { + clipRect = &defClipRect; + } + + if (clipRect->left > rd.left) { + rs.left += (clipRect->left - rd.left); + rd.left = clipRect->left; + } + + if (clipRect->top > rd.top) { + rs.top += (clipRect->top - rd.top); + rd.top = clipRect->top; + } + + if (clipRect->right < rd.right) { + rd.right = clipRect->right; + } + + if (clipRect->bottom < rd.bottom) { + rd.bottom = clipRect->bottom; + } + + if (rd.width() <= 0 || rd.height() <= 0) + return; + + src = surface + rs.top * s->w + rs.left; + dst = _buffer + _screenWide * rd.top + rd.left; + + // Surfaces are always transparent. + + for (y = 0; y < rd.height(); y++) { + for (x = 0; x < rd.width(); x++) { + if (src[x]) + dst[x] = src[x]; + } + src += s->w; + dst += _screenWide; + } + + updateRect(&rd); +} + +/** + * Destroys a surface. + */ + +void Screen::deleteSurface(byte *surface) { + free(surface); +} + +/** + * Draws a sprite onto the screen. The type of the sprite can be a combination + * of the following flags, some of which are mutually exclusive: + * RDSPR_DISPLAYALIGN The sprite is drawn relative to the top left corner + * of the screen + * RDSPR_FLIP The sprite is mirrored + * RDSPR_TRANS The sprite has a transparent colour zero + * RDSPR_BLEND The sprite is translucent + * RDSPR_SHADOW The sprite is affected by the light mask. (Scaled + * sprites always are.) + * RDSPR_NOCOMPRESSION The sprite data is not compressed + * RDSPR_RLE16 The sprite data is a 16-colour compressed sprite + * RDSPR_RLE256 The sprite data is a 256-colour compressed sprite + * @param s all the information needed to draw the sprite + * @warning Sprites will only be drawn onto the background, not over menubar + * areas. + */ + +// FIXME: I'm sure this could be optimized. There's plenty of data copying and +// mallocing here. + +int32 Screen::drawSprite(SpriteInfo *s) { + byte *src, *dst; + byte *sprite, *newSprite; + uint16 scale; + int16 i, j; + uint16 srcPitch; + bool freeSprite = false; + Common::Rect rd, rs; + + // ----------------------------------------------------------------- + // Decompression and mirroring + // ----------------------------------------------------------------- + + if (s->type & RDSPR_NOCOMPRESSION) + sprite = s->data; + else { + sprite = (byte *)malloc(s->w * s->h); + freeSprite = true; + if (!sprite) + return RDERR_OUTOFMEMORY; + if ((s->type & 0xff00) == RDSPR_RLE16) { + if (decompressRLE16(sprite, s->data, s->w * s->h, s->colourTable)) { + free(sprite); + return RDERR_DECOMPRESSION; + } + } else { + if (decompressRLE256(sprite, s->data, s->w * s->h)) { + free(sprite); + return RDERR_DECOMPRESSION; + } + } + } + + if (s->type & RDSPR_FLIP) { + newSprite = (byte *)malloc(s->w * s->h); + if (newSprite == NULL) { + if (freeSprite) + free(sprite); + return RDERR_OUTOFMEMORY; + } + mirrorSprite(newSprite, sprite, s->w, s->h); + if (freeSprite) + free(sprite); + sprite = newSprite; + freeSprite = true; + } + + // ----------------------------------------------------------------- + // Positioning and clipping. + // ----------------------------------------------------------------- + + int16 spriteX = s->x; + int16 spriteY = s->y; + + if (!(s->type & RDSPR_DISPLAYALIGN)) { + spriteX += _parallaxScrollX; + spriteY += _parallaxScrollY; + } + + spriteY += MENUDEEP; + + // A scale factor 0 or 256 means don't scale. Why do they use two + // different values to mean the same thing? Normalize it here for + // convenience. + + scale = (s->scale == 0) ? 256 : s->scale; + + rs.top = 0; + rs.left = 0; + + if (scale != 256) { + rs.right = s->scaledWidth; + rs.bottom = s->scaledHeight; + srcPitch = s->scaledWidth; + } else { + rs.right = s->w; + rs.bottom = s->h; + srcPitch = s->w; + } + + rd.top = spriteY; + rd.left = spriteX; + + if (!(s->type & RDSPR_DISPLAYALIGN)) { + rd.top -= _scrollY; + rd.left -= _scrollX; + } + + rd.right = rd.left + rs.right; + rd.bottom = rd.top + rs.bottom; + + // Check if the sprite would end up completely outside the screen. + + if (rd.left > RENDERWIDE || rd.top > RENDERDEEP + MENUDEEP || rd.right < 0 || rd.bottom < MENUDEEP) { + if (freeSprite) + free(sprite); + return RD_OK; + } + + if (rd.top < MENUDEEP) { + rs.top = MENUDEEP - rd.top; + rd.top = MENUDEEP; + } + if (rd.bottom > RENDERDEEP + MENUDEEP) { + rd.bottom = RENDERDEEP + MENUDEEP; + rs.bottom = rs.top + (rd.bottom - rd.top); + } + if (rd.left < 0) { + rs.left = -rd.left; + rd.left = 0; + } + if (rd.right > RENDERWIDE) { + rd.right = RENDERWIDE; + rs.right = rs.left + (rd.right - rd.left); + } + + // ----------------------------------------------------------------- + // Scaling + // ----------------------------------------------------------------- + + if (scale != 256) { + if (s->scaledWidth > SCALE_MAXWIDTH || s->scaledHeight > SCALE_MAXHEIGHT) { + if (freeSprite) + free(sprite); + return RDERR_NOTIMPLEMENTED; + } + + newSprite = (byte *)malloc(s->scaledWidth * s->scaledHeight); + if (newSprite == NULL) { + if (freeSprite) + free(sprite); + return RDERR_OUTOFMEMORY; + } + + if (_renderCaps & RDBLTFX_EDGEBLEND) + scaleImageGood(newSprite, s->scaledWidth, s->scaledWidth, s->scaledHeight, sprite, s->w, s->w, s->h, _buffer + _screenWide * rd.top + rd.left); + else + scaleImageFast(newSprite, s->scaledWidth, s->scaledWidth, s->scaledHeight, sprite, s->w, s->w, s->h); + + if (freeSprite) + free(sprite); + sprite = newSprite; + freeSprite = true; + } + + // ----------------------------------------------------------------- + // Light masking + // ----------------------------------------------------------------- + + // The light mask is an optional layer that covers the entire room + // and which is used to simulate light and shadows. Scaled sprites + // (actors, presumably) are always affected. + + if ((_renderCaps & RDBLTFX_SHADOWBLEND) && _lightMask && (scale != 256 || (s->type & RDSPR_SHADOW))) { + byte *lightMap; + + // Make sure that we never apply the shadow to the original + // resource data. This could only ever happen in the + // RDSPR_NOCOMPRESSION case. + + if (!freeSprite) { + newSprite = (byte *)malloc(s->w * s->h); + memcpy(newSprite, sprite, s->w * s->h); + sprite = newSprite; + freeSprite = true; + } + + src = sprite + rs.top * srcPitch + rs.left; + lightMap = _lightMask + (rd.top + _scrollY - MENUDEEP) * _locationWide + rd.left + _scrollX; + + for (i = 0; i < rs.height(); i++) { + for (j = 0; j < rs.width(); j++) { + if (src[j] && lightMap[j]) { + uint8 r = ((32 - lightMap[j]) * _palette[src[j] * 4 + 0]) >> 5; + uint8 g = ((32 - lightMap[j]) * _palette[src[j] * 4 + 1]) >> 5; + uint8 b = ((32 - lightMap[j]) * _palette[src[j] * 4 + 2]) >> 5; + src[j] = quickMatch(r, g, b); + } + } + src += srcPitch; + lightMap += _locationWide; + } + } + + // ----------------------------------------------------------------- + // Drawing + // ----------------------------------------------------------------- + + src = sprite + rs.top * srcPitch + rs.left; + dst = _buffer + _screenWide * rd.top + rd.left; + + if (s->type & RDSPR_BLEND) { + // The original code had two different blending cases. One for + // s->blend & 0x01 and one for s->blend & 0x02. However, the + // only values that actually appear in the cluster files are + // 0, 513 and 1025 so the s->blend & 0x02 case was never used. + // Which is just as well since that code made no sense to me. + + if (!(_renderCaps & RDBLTFX_SPRITEBLEND)) { + for (i = 0; i < rs.height(); i++) { + for (j = 0; j < rs.width(); j++) { + if (src[j] && ((i & 1) == (j & 1))) + dst[j] = src[j]; + } + src += srcPitch; + dst += _screenWide; + } + } else { + uint8 n = s->blend >> 8; + + for (i = 0; i < rs.height(); i++) { + for (j = 0; j < rs.width(); j++) { + if (src[j]) { + uint8 r1 = _palette[src[j] * 4 + 0]; + uint8 g1 = _palette[src[j] * 4 + 1]; + uint8 b1 = _palette[src[j] * 4 + 2]; + uint8 r2 = _palette[dst[j] * 4 + 0]; + uint8 g2 = _palette[dst[j] * 4 + 1]; + uint8 b2 = _palette[dst[j] * 4 + 2]; + + uint8 r = (r1 * n + r2 * (8 - n)) >> 3; + uint8 g = (g1 * n + g2 * (8 - n)) >> 3; + uint8 b = (b1 * n + b2 * (8 - n)) >> 3; + dst[j] = quickMatch(r, g, b); + } + } + src += srcPitch; + dst += _screenWide; + } + } + } else { + if (s->type & RDSPR_TRANS) { + for (i = 0; i < rs.height(); i++) { + for (j = 0; j < rs.width(); j++) { + if (src[j]) + dst[j] = src[j]; + } + src += srcPitch; + dst += _screenWide; + } + } else { + for (i = 0; i < rs.height(); i++) { + memcpy(dst, src, rs.width()); + src += srcPitch; + dst += _screenWide; + } + } + } + + if (freeSprite) + free(sprite); + + markAsDirty(rd.left, rd.top, rd.right - 1, rd.bottom - 1); + return RD_OK; +} + +/** + * Opens the light masking sprite for a room. + */ + +int32 Screen::openLightMask(SpriteInfo *s) { + // FIXME: The light mask is only needed on higher graphics detail + // settings, so to save memory we could simply ignore it on lower + // settings. But then we need to figure out how to ensure that it + // is properly loaded if the user changes the settings in mid-game. + + if (_lightMask) + return RDERR_NOTCLOSED; + + _lightMask = (byte *)malloc(s->w * s->h); + if (!_lightMask) + return RDERR_OUTOFMEMORY; + + if (decompressRLE256(_lightMask, s->data, s->w * s->h)) + return RDERR_DECOMPRESSION; + + return RD_OK; +} + +/** + * Closes the light masking sprite for a room. + */ + +int32 Screen::closeLightMask() { + if (!_lightMask) + return RDERR_NOTOPEN; + + free(_lightMask); + _lightMask = NULL; + return RD_OK; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/startup.cpp b/engines/sword2/startup.cpp new file mode 100644 index 0000000000..2e1dd7b0ae --- /dev/null +++ b/engines/sword2/startup.cpp @@ -0,0 +1,173 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "common/file.h" + +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/memory.h" +#include "sword2/resman.h" +#include "sword2/router.h" +#include "sword2/sound.h" + +namespace Sword2 { + +bool Sword2Engine::initStartMenu() { + // Print out a list of all the start points available. + // There should be a linc produced file called startup.txt. + // This file should contain ascii numbers of all the resource game + // objects that are screen managers. + // We query each in turn and setup an array of start structures. + // If the file doesn't exist then we say so and return a 0. + + Common::File fp; + + // ok, load in the master screen manager file + + _totalStartups = 0; + _totalScreenManagers = 0; + + if (!fp.open("startup.inf")) { + warning("Cannot open startup.inf - the debugger won't have a start menu"); + return false; + } + + // The startup.inf file which contains a list of all the files. Now + // extract the filenames + + int start_ids[MAX_starts]; + + while (1) { + bool done = false; + + start_ids[_totalScreenManagers] = 0; + + // Scan the string until the LF in CRLF + + int b; + + do { + b = fp.readByte(); + + if (fp.ioFailed()) { + done = true; + break; + } + + if (isdigit(b)) { + start_ids[_totalScreenManagers] *= 10; + start_ids[_totalScreenManagers] += (b - '0'); + } + } while (b != 10); + + if (done) + break; + + _totalScreenManagers++; + + if (_totalScreenManagers == MAX_starts) { + warning("MAX_starts exceeded"); + break; + } + } + + fp.close(); + + // Using this method the Gode generated resource.inf must have #0d0a + // on the last entry + + debug(1, "%d screen manager objects", _totalScreenManagers); + + // Open each object and make a query call. The object must fill in a + // startup structure. It may fill in several if it wishes - for + // instance a startup could be set for later in the game where + // specific vars are set + + for (uint i = 0; i < _totalScreenManagers; i++) { + _startRes = start_ids[i]; + + debug(2, "Querying screen manager %d", _startRes); + + // Open each one and run through the interpreter. Script 0 is + // the query request script + + // if the resource number is within range & it's not a null + // resource + // - need to check in case un-built sections included in + // start list + + if (_resman->checkValid(_startRes)) { + _logic->runResScript(_startRes, 0); + } else + warning("Start menu resource %d invalid", _startRes); + } + + return 1; +} + +void Sword2Engine::registerStartPoint(int32 key, char *name) { + assert(_totalStartups < MAX_starts); + + _startList[_totalStartups].start_res_id = _startRes; + _startList[_totalStartups].key = key; + + strncpy(_startList[_totalStartups].description, name, MAX_description); + _startList[_totalStartups].description[MAX_description - 1] = 0; + + _totalStartups++; +} + +void Sword2Engine::runStart(int start) { + // Restarting - stop sfx, music & speech! + + _sound->clearFxQueue(); + _logic->fnStopMusic(NULL); + _sound->unpauseSpeech(); + _sound->stopSpeech(); + + // Remove all resources from memory, including player object and global + // variables + + _resman->removeAll(); + + // Reopen global variables resource and player object + setupPersistentResources(); + + // Free all the route memory blocks from previous game + _logic->_router->freeAllRouteMem(); + + // If there was speech text, kill the text block + if (_logic->_speechTextBlocNo) { + _fontRenderer->killTextBloc(_logic->_speechTextBlocNo); + _logic->_speechTextBlocNo = 0; + } + + _logic->runResObjScript(_startList[start].start_res_id, CUR_PLAYER_ID, _startList[start].key & 0xffff); + + // Make sure there's a mouse, in case restarting while mouse not + // available + _logic->fnAddHuman(NULL); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/sword2.cpp b/engines/sword2/sword2.cpp new file mode 100644 index 0000000000..6056d636b7 --- /dev/null +++ b/engines/sword2/sword2.cpp @@ -0,0 +1,689 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" + +#include "backends/fs/fs.h" + +#include "base/gameDetector.h" +#include "base/plugins.h" + +#include "common/config-manager.h" +#include "common/file.h" +#include "common/system.h" + +#include "sword2/sword2.h" +#include "sword2/console.h" +#include "sword2/controls.h" +#include "sword2/defs.h" +#include "sword2/logic.h" +#include "sword2/maketext.h" +#include "sword2/memory.h" +#include "sword2/mouse.h" +#include "sword2/resman.h" +#include "sword2/router.h" +#include "sword2/sound.h" + +#ifdef _WIN32_WCE +extern bool isSmartphone(); +#endif + +struct Sword2GameSettings { + const char *gameid; + const char *description; + uint32 features; + const char *detectname; + GameSettings toGameSettings() const { + GameSettings dummy = { gameid, description, features }; + return dummy; + } +}; + +static const Sword2GameSettings sword2_settings[] = { + /* Broken Sword 2 */ + {"sword2", "Broken Sword 2: The Smoking Mirror", GF_DEFAULT_TO_1X_SCALER, "players.clu" }, + {"sword2alt", "Broken Sword 2: The Smoking Mirror (alt)", GF_DEFAULT_TO_1X_SCALER, "r2ctlns.ocx" }, + {"sword2demo", "Broken Sword 2: The Smoking Mirror (Demo)", GF_DEFAULT_TO_1X_SCALER | Sword2::GF_DEMO, "players.clu" }, + {NULL, NULL, 0, NULL} +}; + +GameList Engine_SWORD2_gameList() { + const Sword2GameSettings *g = sword2_settings; + GameList games; + while (g->gameid) { + games.push_back(g->toGameSettings()); + g++; + } + return games; +} + +DetectedGameList Engine_SWORD2_detectGames(const FSList &fslist) { + DetectedGameList detectedGames; + const Sword2GameSettings *g; + + // TODO: It would be nice if we had code here which distinguishes + // between the 'sword2' and 'sword2demo' targets. The current code + // can't do that since they use the same detectname. + + for (g = sword2_settings; g->gameid; ++g) { + // Iterate over all files in the given directory + for (FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) { + if (!file->isDirectory()) { + const char *gameName = file->displayName().c_str(); + + if (0 == scumm_stricmp(g->detectname, gameName)) { + // Match found, add to list of candidates, then abort inner loop. + detectedGames.push_back(g->toGameSettings()); + break; + } + } + } + } + return detectedGames; +} + +Engine *Engine_SWORD2_create(GameDetector *detector, OSystem *syst) { + return new Sword2::Sword2Engine(detector, syst); +} + +REGISTER_PLUGIN(SWORD2, "Broken Sword 2") + +namespace Sword2 { + +Sword2Engine::Sword2Engine(GameDetector *detector, OSystem *syst) : Engine(syst) { + // Add default file directories + Common::File::addDefaultDirectory(_gameDataPath + "CLUSTERS/"); + Common::File::addDefaultDirectory(_gameDataPath + "SWORD2/"); + Common::File::addDefaultDirectory(_gameDataPath + "VIDEO/"); + Common::File::addDefaultDirectory(_gameDataPath + "clusters/"); + Common::File::addDefaultDirectory(_gameDataPath + "sword2/"); + Common::File::addDefaultDirectory(_gameDataPath + "video/"); + + _features = detector->_game.features; + _targetName = detector->_targetName; + + _bootParam = ConfMan.getInt("boot_param"); + _saveSlot = ConfMan.getInt("save_slot"); + + _memory = NULL; + _resman = NULL; + _sound = NULL; + _screen = NULL; + _mouse = NULL; + _logic = NULL; + _fontRenderer = NULL; + _debugger = NULL; + + _keyboardEvent.pending = false; + _keyboardEvent.repeat = 0; + _mouseEvent.pending = false; + + _wantSfxDebug = false; + +#ifdef SWORD2_DEBUG + _stepOneCycle = false; + _renderSkip = false; +#endif + + _gamePaused = false; + _graphicsLevelFudged = false; + + _gameCycle = 0; + + _quit = false; +} + +Sword2Engine::~Sword2Engine() { + delete _debugger; + delete _sound; + delete _fontRenderer; + delete _screen; + delete _mouse; + delete _logic; + delete _resman; + delete _memory; +} + +void Sword2Engine::errorString(const char *buf1, char *buf2) { + strcpy(buf2, buf1); + +#ifdef _WIN32_WCE + if (isSmartphone()) + return; +#endif + + // Unless an error -originated- within the debugger, spawn the + // debugger. Otherwise exit out normally. + if (_debugger && !_debugger->isAttached()) { + // (Print it again in case debugger segfaults) + printf("%s\n", buf2); + _debugger->attach(buf2); + _debugger->onFrame(); + } +} + +void Sword2Engine::registerDefaultSettings() { + ConfMan.registerDefault("music_mute", false); + ConfMan.registerDefault("speech_mute", false); + ConfMan.registerDefault("sfx_mute", false); + ConfMan.registerDefault("gfx_details", 2); + ConfMan.registerDefault("subtitles", false); + ConfMan.registerDefault("reverse_stereo", false); +} + +void Sword2Engine::readSettings() { + _mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, ConfMan.getInt("music_volume")); + _mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, ConfMan.getInt("speech_volume")); + _mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, ConfMan.getInt("sfx_volume")); + setSubtitles(ConfMan.getBool("subtitles")); + _sound->muteMusic(ConfMan.getBool("music_mute")); + _sound->muteSpeech(ConfMan.getBool("speech_mute")); + _sound->muteFx(ConfMan.getBool("sfx_mute")); + _sound->setReverseStereo(ConfMan.getBool("reverse_stereo")); + _mouse->setObjectLabels(ConfMan.getBool("object_labels")); + _screen->setRenderLevel(ConfMan.getInt("gfx_details")); +} + +void Sword2Engine::writeSettings() { + ConfMan.set("music_volume", _mixer->getVolumeForSoundType(Audio::Mixer::kMusicSoundType)); + ConfMan.set("speech_volume", _mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType)); + ConfMan.set("sfx_volume", _mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType)); + ConfMan.set("music_mute", _sound->isMusicMute()); + ConfMan.set("speech_mute", _sound->isSpeechMute()); + ConfMan.set("sfx_mute", _sound->isFxMute()); + ConfMan.set("gfx_details", _screen->getRenderLevel()); + ConfMan.set("subtitles", getSubtitles()); + ConfMan.set("object_labels", _mouse->getObjectLabels()); + ConfMan.set("reverse_stereo", _sound->isReverseStereo()); + + ConfMan.flushToDisk(); +} + +/** + * The global script variables and player object should be kept open throughout + * the game, so that they are never expelled by the resource manager. + */ + +void Sword2Engine::setupPersistentResources() { + _logic->_scriptVars = _resman->openResource(1) + ResHeader::size(); + _resman->openResource(CUR_PLAYER_ID); +} + +int Sword2Engine::init(GameDetector &detector) { + // Get some falling RAM and put it in your pocket, never let it slip + // away + + _system->beginGFXTransaction(); + initCommonGFX(detector); + _screen = new Screen(this, 640, 480); + _system->endGFXTransaction(); + + // Create the debugger as early as possible (but not before the + // screen object!) so that errors can be displayed in it. In + // particular, we want errors about missing files to be clearly + // visible to the user. + + _debugger = new Debugger(this); + + _memory = new MemoryManager(this); + _resman = new ResourceManager(this); + _logic = new Logic(this); + _fontRenderer = new FontRenderer(this); + _sound = new Sound(this); + _mouse = new Mouse(this); + + // Setup mixer + if (!_mixer->isReady()) + warning("Sound initialization failed"); + + registerDefaultSettings(); + readSettings(); + + initStartMenu(); + + // During normal gameplay, we care neither about mouse button releases + // nor the scroll wheel. + setInputEventFilter(RD_LEFTBUTTONUP | RD_RIGHTBUTTONUP | RD_WHEELUP | RD_WHEELDOWN); + + setupPersistentResources(); + initialiseFontResourceFlags(); + + if (_features & GF_DEMO) + _logic->writeVar(DEMO, 1); + else + _logic->writeVar(DEMO, 0); + + if (_saveSlot != -1) { + if (saveExists(_saveSlot)) + restoreGame(_saveSlot); + else { + RestoreDialog dialog(this); + if (!dialog.runModal()) + startGame(); + } + } else if (!_bootParam && saveExists()) { + int32 pars[2] = { 221, FX_LOOP }; + bool result; + + _mouse->setMouse(NORMAL_MOUSE_ID); + _logic->fnPlayMusic(pars); + + StartDialog dialog(this); + + result = (dialog.runModal() != 0); + + // If the game is started from the beginning, the cutscene + // player will kill the music for us. Otherwise, the restore + // will either have killed the music, or done a crossfade. + + if (_quit) + return 0; + + if (result) + startGame(); + } else + startGame(); + + _screen->initialiseRenderCycle(); + + return 0; +} + +int Sword2Engine::go() { + while (1) { + if (_debugger->isAttached()) + _debugger->onFrame(); + +#ifdef SWORD2_DEBUG + if (_stepOneCycle) { + pauseGame(); + _stepOneCycle = false; + } +#endif + + KeyboardEvent *ke = keyboardEvent(); + + if (ke) { + if ((ke->modifiers == OSystem::KBD_CTRL && ke->keycode == 'd') || ke->ascii == '#' || ke->ascii == '~') { + _debugger->attach(); + } else if (ke->modifiers == 0 || ke->modifiers == OSystem::KBD_SHIFT) { + switch (ke->keycode) { + case 'p': + if (_gamePaused) + unpauseGame(); + else + pauseGame(); + break; + case 'c': + if (!_logic->readVar(DEMO) && !_mouse->isChoosing()) { + ScreenInfo *screenInfo = _screen->getScreenInfo(); + _logic->fnPlayCredits(NULL); + screenInfo->new_palette = 99; + } + break; +#ifdef SWORD2_DEBUG + case ' ': + if (_gamePaused) { + _stepOneCycle = true; + unpauseGame(); + } + break; + case 's': + _renderSkip = !_renderSkip; + break; +#endif + default: + break; + } + } + } + + // skip GameCycle if we're paused + if (!_gamePaused) { + _gameCycle++; + gameCycle(); + } + + // We can't use this as termination condition for the loop, + // because we want the break to happen before updating the + // screen again. + + if (_quit) + break; + + // creates the debug text blocks + _debugger->buildDebugText(); + +#ifdef SWORD2_DEBUG + // if not in console & '_renderSkip' is set, only render + // display once every 4 game-cycles + + if (!_renderSkip || (_gameCycle % 4) == 0) + _screen->buildDisplay(); +#else + _screen->buildDisplay(); +#endif + } + + return 0; +} + +void Sword2Engine::closeGame() { + _quit = true; +} + +void Sword2Engine::restartGame() { + ScreenInfo *screenInfo = _screen->getScreenInfo(); + uint32 temp_demo_flag; + + _mouse->closeMenuImmediately(); + + // Restart the game. To do this, we must... + + // Stop music instantly! + _sound->stopMusic(true); + + // In case we were dead - well we're not anymore! + _logic->writeVar(DEAD, 0); + + // Restart the game. Clear all memory and reset the globals + temp_demo_flag = _logic->readVar(DEMO); + + // Remove all resources from memory, including player object and + // global variables + _resman->removeAll(); + + // Reopen global variables resource and player object + setupPersistentResources(); + + _logic->writeVar(DEMO, temp_demo_flag); + + // Free all the route memory blocks from previous game + _logic->_router->freeAllRouteMem(); + + // Call the same function that first started us up + startGame(); + + // Prime system with a game cycle + + // Reset the graphic 'BuildUnit' list before a new logic list + // (see fnRegisterFrame) + _screen->resetRenderLists(); + + // Reset the mouse hot-spot list (see fnRegisterMouse and + // fnRegisterFrame) + _mouse->resetMouseList(); + + _mouse->closeMenuImmediately(); + + // FOR THE DEMO - FORCE THE SCROLLING TO BE RESET! + // - this is taken from fnInitBackground + // switch on scrolling (2 means first time on screen) + screenInfo->scroll_flag = 2; + + if (_logic->processSession()) + error("restart 1st cycle failed??"); + + // So palette not restored immediately after control panel - we want + // to fade up instead! + screenInfo->new_palette = 99; +} + +bool Sword2Engine::checkForMouseEvents() { + return _mouseEvent.pending; +} + +MouseEvent *Sword2Engine::mouseEvent() { + if (!_mouseEvent.pending) + return NULL; + + _mouseEvent.pending = false; + return &_mouseEvent; +} + +KeyboardEvent *Sword2Engine::keyboardEvent() { + if (!_keyboardEvent.pending) + return NULL; + + _keyboardEvent.pending = false; + return &_keyboardEvent; +} + +uint32 Sword2Engine::setInputEventFilter(uint32 filter) { + uint32 oldFilter = _inputEventFilter; + + _inputEventFilter = filter; + return oldFilter; +} + +/** + * Clear the input events. This is so that we won't get any keyboard repeat + * right after using the debugging console. + */ + +void Sword2Engine::clearInputEvents() { + _keyboardEvent.pending = false; + _keyboardEvent.repeat = 0; + _mouseEvent.pending = false; +} + +/** + * OSystem Event Handler. Full of cross platform goodness and 99% fat free! + */ + +void Sword2Engine::parseInputEvents() { + OSystem::Event event; + + uint32 now = _system->getMillis(); + + while (_system->pollEvent(event)) { + switch (event.type) { + case OSystem::EVENT_KEYDOWN: + if (!(_inputEventFilter & RD_KEYDOWN)) { + _keyboardEvent.pending = true; + _keyboardEvent.repeat = now + 400; + _keyboardEvent.ascii = event.kbd.ascii; + _keyboardEvent.keycode = event.kbd.keycode; + _keyboardEvent.modifiers = event.kbd.flags; + } + break; + case OSystem::EVENT_KEYUP: + _keyboardEvent.repeat = 0; + break; + case OSystem::EVENT_MOUSEMOVE: + if (!(_inputEventFilter & RD_KEYDOWN)) { + _mouse->setPos(event.mouse.x, event.mouse.y - MENUDEEP); + } + break; + case OSystem::EVENT_LBUTTONDOWN: + if (!(_inputEventFilter & RD_LEFTBUTTONDOWN)) { + _mouseEvent.pending = true; + _mouseEvent.buttons = RD_LEFTBUTTONDOWN; + } + break; + case OSystem::EVENT_RBUTTONDOWN: + if (!(_inputEventFilter & RD_RIGHTBUTTONDOWN)) { + _mouseEvent.pending = true; + _mouseEvent.buttons = RD_RIGHTBUTTONDOWN; + } + break; + case OSystem::EVENT_LBUTTONUP: + if (!(_inputEventFilter & RD_LEFTBUTTONUP)) { + _mouseEvent.pending = true; + _mouseEvent.buttons = RD_LEFTBUTTONUP; + } + break; + case OSystem::EVENT_RBUTTONUP: + if (!(_inputEventFilter & RD_RIGHTBUTTONUP)) { + _mouseEvent.pending = true; + _mouseEvent.buttons = RD_RIGHTBUTTONUP; + } + break; + case OSystem::EVENT_WHEELUP: + if (!(_inputEventFilter & RD_WHEELUP)) { + _mouseEvent.pending = true; + _mouseEvent.buttons = RD_WHEELUP; + } + break; + case OSystem::EVENT_WHEELDOWN: + if (!(_inputEventFilter & RD_WHEELDOWN)) { + _mouseEvent.pending = true; + _mouseEvent.buttons = RD_WHEELDOWN; + } + break; + case OSystem::EVENT_QUIT: + closeGame(); + break; + default: + break; + } + } + + // Handle keyboard auto-repeat + if (!_keyboardEvent.pending && _keyboardEvent.repeat && now >= _keyboardEvent.repeat) { + _keyboardEvent.pending = true; + _keyboardEvent.repeat = now + 100; + } +} + +void Sword2Engine::gameCycle() { + // Do one game cycle, that is run the logic session until a full loop + // has been performed. + + if (_logic->getRunList()) { + do { + // Reset the 'BuildUnit' and mouse hot-spot lists + // before each new logic list. The service scripts + // will fill thrm through fnRegisterFrame() and + // fnRegisterMouse(). + + _screen->resetRenderLists(); + _mouse->resetMouseList(); + + // Keep going as long as new lists keep getting put in + // - i.e. screen changes. + } while (_logic->processSession()); + } else { + // Start the console and print the start options perhaps? + _debugger->attach("AWAITING START COMMAND: (Enter 's 1' then 'q' to start from beginning)"); + } + + // If this screen is wide, recompute the scroll offsets every cycle + ScreenInfo *screenInfo = _screen->getScreenInfo(); + + if (screenInfo->scroll_flag) + _screen->setScrolling(); + + _mouse->mouseEngine(); + _sound->processFxQueue(); +} + +void Sword2Engine::startGame() { + // Boot the game straight into a start script. It's always George's + // script #1, but with different ScreenManager objects depending on + // if it's the demo or the full game, or if we're using a boot param. + + int screen_manager_id = 0; + + debug(5, "startGame() STARTING:"); + + if (!_bootParam) { + if (_logic->readVar(DEMO)) + screen_manager_id = 19; // DOCKS SECTION START + else + screen_manager_id = 949; // INTRO & PARIS START + } else { + // FIXME this could be validated against startup.inf for valid + // numbers to stop people shooting themselves in the foot + + if (_bootParam != 0) + screen_manager_id = _bootParam; + } + + _logic->runResObjScript(screen_manager_id, CUR_PLAYER_ID, 1); +} + +// FIXME: Move this to some better place? + +void Sword2Engine::sleepUntil(uint32 time) { + while (getMillis() < time) { + // Make sure menu animations and fades don't suffer, but don't + // redraw the entire scene. + _mouse->processMenu(); + _screen->updateDisplay(false); + _system->delayMillis(10); + } +} + +void Sword2Engine::pauseGame() { + // Don't allow Pause while screen fading or while black + if (_screen->getFadeStatus() != RDFADE_NONE) + return; + + _sound->pauseAllSound(); + _mouse->pauseGame(); + + // If render level is at max, turn it down because palette-matching + // won't work when the palette is dimmed. + + if (_screen->getRenderLevel() == 3) { + _screen->setRenderLevel(2); + _graphicsLevelFudged = true; + } + +#ifdef SWORD2_DEBUG + // Don't dim it if we're single-stepping through frames + // dim the palette during the pause + + if (!_stepOneCycle) + _screen->dimPalette(); +#else + _screen->dimPalette(); +#endif + + _gamePaused = true; +} + +void Sword2Engine::unpauseGame() { + _mouse->unpauseGame(); + _sound->unpauseAllSound(); + + // Put back game screen palette; see build_display.cpp + _screen->setFullPalette(-1); + + // If graphics level at max, turn up again + if (_graphicsLevelFudged) { + _screen->setRenderLevel(3); + _graphicsLevelFudged = false; + } + + _gamePaused = false; + + // If mouse is about or we're in a chooser menu + if (!_mouse->getMouseStatus() || _mouse->isChoosing()) + _mouse->setMouse(NORMAL_MOUSE_ID); +} + +uint32 Sword2Engine::getMillis() { + return _system->getMillis(); +} + +} // End of namespace Sword2 diff --git a/engines/sword2/sword2.h b/engines/sword2/sword2.h new file mode 100644 index 0000000000..bcb80ec957 --- /dev/null +++ b/engines/sword2/sword2.h @@ -0,0 +1,240 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +#ifndef _SWORD2 +#define _SWORD2 + +// Enable this to make it possible to clear the mouse cursor luggage by +// right-clicking. The original didn't do this, but it feels natural to me. +// However, I'm afraid that it'll interfer badly with parts of the game, so +// for now I'll keep it disabled. + +#define RIGHT_CLICK_CLEARS_LUGGAGE 0 + +#include "base/engine.h" + +#include "common/util.h" + +#include "sword2/build_display.h" +#include "sword2/header.h" +#include "sword2/icons.h" +#include "sword2/object.h" +#include "sword2/save_rest.h" + +#define MAX_starts 100 +#define MAX_description 100 + +class GameDetector; +class OSystem; + +namespace Sword2 { + +enum { + GF_DEMO = 1 << 0 +}; + +class MemoryManager; +class ResourceManager; +class Sound; +class Screen; +class Mouse; +class Logic; +class FontRenderer; +class Gui; +class Debugger; + +enum { + RD_LEFTBUTTONDOWN = 0x01, + RD_LEFTBUTTONUP = 0x02, + RD_RIGHTBUTTONDOWN = 0x04, + RD_RIGHTBUTTONUP = 0x08, + RD_WHEELUP = 0x10, + RD_WHEELDOWN = 0x20, + RD_KEYDOWN = 0x40, + RD_MOUSEMOVE = 0x80 +}; + +struct MouseEvent { + bool pending; + uint16 buttons; +}; + +struct KeyboardEvent { + bool pending; + uint32 repeat; + uint16 ascii; + int keycode; + int modifiers; +}; + +struct StartUp { + char description[MAX_description]; + + // id of screen manager object + uint32 start_res_id; + + // Tell the manager which startup you want (if there are more than 1) + // (i.e more than 1 entrance to a screen and/or separate game boots) + uint32 key; +}; + +class Sword2Engine : public Engine { +private: + uint32 _inputEventFilter; + + // The event "buffers" + MouseEvent _mouseEvent; + KeyboardEvent _keyboardEvent; + + uint32 _bootParam; + int32 _saveSlot; + + void getPlayerStructures(); + void putPlayerStructures(); + + uint32 saveData(uint16 slotNo, byte *buffer, uint32 bufferSize); + uint32 restoreData(uint16 slotNo, byte *buffer, uint32 bufferSize); + + uint32 calcChecksum(byte *buffer, uint32 size); + + void pauseGame(); + void unpauseGame(); + + uint32 _totalStartups; + uint32 _totalScreenManagers; + uint32 _startRes; + + bool _useSubtitles; + + StartUp _startList[MAX_starts]; + +public: + Sword2Engine(GameDetector *detector, OSystem *syst); + ~Sword2Engine(); + int go(); + int init(GameDetector &detector); + + void registerDefaultSettings(); + void readSettings(); + void writeSettings(); + + void setupPersistentResources(); + + bool getSubtitles() { return _useSubtitles; } + void setSubtitles(bool b) { _useSubtitles = b; } + + bool _quit; + + uint32 _features; + Common::String _targetName; // target name for saves + + MemoryManager *_memory; + ResourceManager *_resman; + Sound *_sound; + Screen *_screen; + Mouse *_mouse; + Logic *_logic; + FontRenderer *_fontRenderer; + + Debugger *_debugger; + + Common::RandomSource _rnd; + + uint32 _speechFontId; + uint32 _controlsFontId; + uint32 _redFontId; + + uint32 setInputEventFilter(uint32 filter); + + void clearInputEvents(); + void parseInputEvents(); + + bool checkForMouseEvents(); + MouseEvent *mouseEvent(); + KeyboardEvent *keyboardEvent(); + + bool _wantSfxDebug; + + int32 _gameCycle; + +#ifdef SWORD2_DEBUG + bool _renderSkip; + bool _stepOneCycle; +#endif + +#if RIGHT_CLICK_CLEARS_LUGGAGE + bool heldIsInInventory(); +#endif + + byte *fetchPalette(byte *screenFile); + byte *fetchScreenHeader(byte *screenFile); + byte *fetchLayerHeader(byte *screenFile, uint16 layerNo); + byte *fetchShadingMask(byte *screenFile); + + byte *fetchAnimHeader(byte *animFile); + byte *fetchCdtEntry(byte *animFile, uint16 frameNo); + byte *fetchFrameHeader(byte *animFile, uint16 frameNo); + byte *fetchBackgroundParallaxLayer(byte *screenFile, int layer); + byte *fetchBackgroundLayer(byte *screenFile); + byte *fetchForegroundParallaxLayer(byte *screenFile, int layer); + byte *fetchTextLine(byte *file, uint32 text_line); + bool checkTextLine(byte *file, uint32 text_line); + byte *fetchPaletteMatchTable(byte *screenFile); + + uint32 saveGame(uint16 slotNo, byte *description); + uint32 restoreGame(uint16 slotNo); + uint32 getSaveDescription(uint16 slotNo, byte *description); + bool saveExists(); + bool saveExists(uint16 slotNo); + uint32 restoreFromBuffer(byte *buffer, uint32 size); + uint32 findBufferSize(); + + bool _gamePaused; + bool _graphicsLevelFudged; + + void startGame(); + void gameCycle(); + void closeGame(); + void restartGame(); + + void sleepUntil(uint32 time); + + void errorString(const char *buf_input, char *buf_output); + void initialiseFontResourceFlags(); + void initialiseFontResourceFlags(uint8 language); + + bool initStartMenu(); + void registerStartPoint(int32 key, char *name); + + uint32 getNumStarts() { return _totalStartups; } + uint32 getNumScreenManagers() { return _totalScreenManagers; } + StartUp *getStartList() { return _startList; } + + void runStart(int start); + + // Convenience alias for OSystem::getMillis(). + // This is a bit hackish, of course :-). + uint32 getMillis(); +}; + +} // End of namespace Sword2 + +#endif diff --git a/engines/sword2/sync.cpp b/engines/sword2/sync.cpp new file mode 100644 index 0000000000..205e273873 --- /dev/null +++ b/engines/sword2/sync.cpp @@ -0,0 +1,76 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/logic.h" + +namespace Sword2 { + +/** + * Clear any syncs registered for this id. Call this just after the id has been + * processed. Theoretically there could be more than one sync waiting for us, + * so clear the lot. + */ + +void Logic::clearSyncs(uint32 id) { + for (int i = 0; i < ARRAYSIZE(_syncList); i++) { + if (_syncList[i].id == id) { + debug(5, "removing sync %d for %d", i, id); + _syncList[i].id = 0; + } + } +} + +void Logic::sendSync(uint32 id, uint32 sync) { + for (int i = 0; i < ARRAYSIZE(_syncList); i++) { + if (_syncList[i].id == 0) { + debug(5, "%d sends sync %d to %d", readVar(ID), sync, id); + _syncList[i].id = id; + _syncList[i].sync = sync; + return; + } + } + + // The original code didn't even check for this condition, so maybe + // it should be a fatal error? + + warning("No free sync slot"); +} + +/** + * Check for a sync waiting for this character. Called from fnAnim() to see if + * animation is to be finished. Returns an index into _syncList[], or -1. + */ + +int Logic::getSync() { + uint32 id = readVar(ID); + + for (int i = 0; i < ARRAYSIZE(_syncList); i++) { + if (_syncList[i].id == id) + return i; + } + + return -1; +} + +} // End of namespace Sword2 diff --git a/engines/sword2/walker.cpp b/engines/sword2/walker.cpp new file mode 100644 index 0000000000..82ef80f65f --- /dev/null +++ b/engines/sword2/walker.cpp @@ -0,0 +1,454 @@ +/* Copyright (C) 1994-1998 Revolution Software Ltd. + * Copyright (C) 2003-2006 The ScummVM project + * + * 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$ + */ + +// WALKER.CPP by James (14nov96) + +// Functions for moving megas about the place & also for keeping tabs on them + +#include "common/stdafx.h" +#include "sword2/sword2.h" +#include "sword2/defs.h" +#include "sword2/interpreter.h" +#include "sword2/logic.h" +#include "sword2/resman.h" +#include "sword2/router.h" + +namespace Sword2 { + +void Router::setStandbyCoords(int16 x, int16 y, uint8 dir) { + assert(dir <= 7); + + _standbyX = x; + _standbyY = y; + _standbyDir = dir; +} + +/** + * Work out direction from start to dest. + */ + +// Used in whatTarget(); not valid for all megas +#define diagonalx 36 +#define diagonaly 8 + +int Router::whatTarget(int startX, int startY, int destX, int destY) { + int deltaX = destX - startX; + int deltaY = destY - startY; + + // 7 0 1 + // 6 2 + // 5 4 3 + + // Flat route + + if (ABS(deltaY) * diagonalx < ABS(deltaX) * diagonaly / 2) + return (deltaX > 0) ? 2 : 6; + + // Vertical route + + if (ABS(deltaY) * diagonalx / 2 > ABS(deltaX) * diagonaly) + return (deltaY > 0) ? 4 : 0; + + // Diagonal route + + if (deltaX > 0) + return (deltaY > 0) ? 3 : 1; + + return (deltaY > 0) ? 5 : 7; +} + +/** + * Walk meta to (x,y,dir). Set RESULT to 0 if it succeeded. Otherwise, set + * RESULT to 1. Return true if the mega has finished walking. + */ + +int Router::doWalk(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, int16 target_x, int16 target_y, uint8 target_dir) { + ObjectLogic obLogic(ob_logic); + ObjectGraphic obGraph(ob_graph); + ObjectMega obMega(ob_mega); + + // If this is the start of the walk, calculate the route. + + if (obLogic.getLooping() == 0) { + // If we're already there, don't even bother allocating + // memory and calling the router, just quit back & continue + // the script! This avoids an embarassing mega stand frame + // appearing for one cycle when we're already in position for + // an anim eg. repeatedly clicking on same object to repeat + // an anim - no mega frame will appear in between runs of the + // anim. + + if (obMega.getFeetX() == target_x && obMega.getFeetY() == target_y && obMega.getCurDir() == target_dir) { + _vm->_logic->writeVar(RESULT, 0); + return IR_CONT; + } + + assert(target_dir <= 8); + + obMega.setWalkPc(0); + + // Set up mem for _walkData in route_slots[] & set mega's + // 'route_slot_id' accordingly + allocateRouteMem(); + + int32 route = routeFinder(ob_mega, ob_walkdata, target_x, target_y, target_dir); + + // 0 = can't make route to target + // 1 = created route + // 2 = zero route but may need to turn + + if (route != 1 && route != 2) { + freeRouteMem(); + _vm->_logic->writeVar(RESULT, 1); + return IR_CONT; + } + + // Walk is about to start + + obMega.setIsWalking(1); + obLogic.setLooping(1); + obGraph.setAnimResource(obMega.getMegasetRes()); + } else if (_vm->_logic->readVar(EXIT_FADING) && _vm->_screen->getFadeStatus() == RDFADE_BLACK) { + // Double clicked an exit, and the screen has faded down to + // black. Ok, that's it. Back to script and change screen. + + // We have to clear te EXIT_CLICK_ID variable in case there's a + // walk instruction on the new screen, or it'd be cut short. + + freeRouteMem(); + + obLogic.setLooping(0); + obMega.setIsWalking(0); + _vm->_logic->writeVar(EXIT_CLICK_ID, 0); + _vm->_logic->writeVar(RESULT, 0); + + return IR_CONT; + } + + // Get pointer to walkanim & current frame position + + WalkData *walkAnim = getRouteMem(); + int32 walk_pc = obMega.getWalkPc(); + + // If stopping the walk early, overwrite the next step with a + // slow-out, then finish + + if (_vm->_logic->checkEventWaiting() && walkAnim[walk_pc].step == 0 && walkAnim[walk_pc + 1].step == 1) { + // At the beginning of a step + earlySlowOut(ob_mega, ob_walkdata); + } + + // Get new frame of walk + + obGraph.setAnimPc(walkAnim[walk_pc].frame); + obMega.setCurDir(walkAnim[walk_pc].dir); + obMega.setFeetX(walkAnim[walk_pc].x); + obMega.setFeetY(walkAnim[walk_pc].y); + + // Is the NEXT frame is the end-marker (512) of the walk sequence? + + if (walkAnim[walk_pc + 1].frame != 512) { + // No, it wasn't. Increment the walk-anim frame number and + // come back next cycle. + obMega.setWalkPc(obMega.getWalkPc() + 1); + return IR_REPEAT; + } + + // We have reached the end-marker, which means we can return to the + // script just as the final (stand) frame of the walk is set. + + freeRouteMem(); + obLogic.setLooping(0); + obMega.setIsWalking(0); + + // If George's walk has been interrupted to run a new action script for + // instance or Nico's walk has been interrupted by player clicking on + // her to talk + + // There used to be code here for checking if two megas were colliding, + // but it had been commented out, and it was only run if a function + // that always returned zero returned non-zero. + + if (_vm->_logic->checkEventWaiting()) { + _vm->_logic->startEvent(); + _vm->_logic->writeVar(RESULT, 1); + return IR_TERMINATE; + } + + _vm->_logic->writeVar(RESULT, 0); + + // CONTINUE the script so that RESULT can be checked! Also, if an anim + // command follows the fnWalk command, the 1st frame of the anim (which + // is always a stand frame itself) can replace the final stand frame of + // the walk, to hide the slight difference between the shrinking on the + // mega frames and the pre-shrunk anim start-frame. + + return IR_CONT; +} + +/** + * Walk mega to start position of anim + */ + +int Router::walkToAnim(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint32 animRes) { + int16 target_x = 0; + int16 target_y = 0; + uint8 target_dir = 0; + + // Walkdata is needed for earlySlowOut if player clicks elsewhere + // during the walk. + + // If this is the start of the walk, read anim file to get start coords + + ObjectLogic obLogic(ob_logic); + + if (obLogic.getLooping() == 0) { + byte *anim_file = _vm->_resman->openResource(animRes); + AnimHeader anim_head; + + anim_head.read(_vm->fetchAnimHeader(anim_file)); + + target_x = anim_head.feetStartX; + target_y = anim_head.feetStartY; + target_dir = anim_head.feetStartDir; + + _vm->_resman->closeResource(animRes); + + // If start coords not yet set in anim header, use the standby + // coords (which should be set beforehand in the script). + + if (target_x == 0 && target_y == 0) { + target_x = _standbyX; + target_y = _standbyY; + target_dir = _standbyDir; + } + + assert(target_dir <= 7); + } + + return doWalk(ob_logic, ob_graph, ob_mega, ob_walkdata, target_x, target_y, target_dir); +} + +/** + * Route to the left or right hand side of target id, if possible. + */ + +int Router::walkToTalkToMega(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint32 megaId, uint32 separation) { + ObjectMega obMega(ob_mega); + + int16 target_x = 0; + int16 target_y = 0; + uint8 target_dir = 0; + + // If this is the start of the walk, calculate the route. + + ObjectLogic obLogic(ob_logic); + + if (obLogic.getLooping() == 0) { + assert(_vm->_resman->fetchType(megaId) == GAME_OBJECT); + + // Call the base script. This is the graphic/mouse service + // call, and will set _engineMega to the ObjectMega of mega we + // want to route to. + + _vm->_logic->runResScript(megaId, 3); + + ObjectMega targetMega(_vm->_logic->getEngineMega()); + + // Stand exactly beside the mega, ie. at same y-coord + target_y = targetMega.getFeetY(); + + int scale = obMega.calcScale(); + int mega_separation = (separation * scale) / 256; + + debug(4, "Target is at (%d, %d), separation %d", targetMega.getFeetX(), targetMega.getFeetY(), mega_separation); + + if (targetMega.getFeetX() < obMega.getFeetX()) { + // Target is left of us, so aim to stand to their + // right. Face down_left + + target_x = targetMega.getFeetX() + mega_separation; + target_dir = 5; + } else { + // Ok, must be right of us so aim to stand to their + // left. Face down_right. + + target_x = targetMega.getFeetX() - mega_separation; + target_dir = 3; + } + } + + return doWalk(ob_logic, ob_graph, ob_mega, ob_walkdata, target_x, target_y, target_dir); +} +/** + * Turn mega to the specified direction. Just needs to call doWalk() with + * current feet coords, so router can produce anim of turn frames. + */ + +int Router::doFace(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint8 target_dir) { + int16 target_x = 0; + int16 target_y = 0; + + // If this is the start of the turn, get the mega's current feet + // coords + the required direction + + ObjectLogic obLogic(ob_logic); + + if (obLogic.getLooping() == 0) { + assert(target_dir <= 7); + + ObjectMega obMega(ob_mega); + + target_x = obMega.getFeetX(); + target_y = obMega.getFeetY(); + } + + return doWalk(ob_logic, ob_graph, ob_mega, ob_walkdata, target_x, target_y, target_dir); +} + +/** + * Turn mega to face point (x,y) on the floor + */ + +int Router::faceXY(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, int16 target_x, int16 target_y) { + uint8 target_dir = 0; + + // If this is the start of the turn, get the mega's current feet + // coords + the required direction + + ObjectLogic obLogic(ob_logic); + + if (obLogic.getLooping() == 0) { + ObjectMega obMega(ob_mega); + + target_dir = whatTarget(obMega.getFeetX(), obMega.getFeetY(), target_x, target_y); + } + + return doFace(ob_logic, ob_graph, ob_mega, ob_walkdata, target_dir); +} + +/** + * Turn mega to face another mega. + */ + +int Router::faceMega(byte *ob_logic, byte *ob_graph, byte *ob_mega, byte *ob_walkdata, uint32 megaId) { + uint8 target_dir = 0; + + // If this is the start of the walk, decide where to walk to. + + ObjectLogic obLogic(ob_logic); + + if (obLogic.getLooping() == 0) { + assert(_vm->_resman->fetchType(megaId) == GAME_OBJECT); + + // Call the base script. This is the graphic/mouse service + // call, and will set _engineMega to the ObjectMega of mega we + // want to turn to face. + + _vm->_logic->runResScript(megaId, 3); + + ObjectMega obMega(ob_mega); + ObjectMega targetMega(_vm->_logic->getEngineMega()); + + target_dir = whatTarget(obMega.getFeetX(), obMega.getFeetY(), targetMega.getFeetX(), targetMega.getFeetY()); + } + + return doFace(ob_logic, ob_graph, ob_mega, ob_walkdata, target_dir); +} + +/** + * Stand mega at (x,y,dir) + * Sets up the graphic object, but also needs to set the new 'current_dir' in + * the mega object, so the router knows in future + */ + +void Router::standAt(byte *ob_graph, byte *ob_mega, int32 x, int32 y, int32 dir) { + assert(dir >= 0 && dir <= 7); + + ObjectGraphic obGraph(ob_graph); + ObjectMega obMega(ob_mega); + + // Set up the stand frame & set the mega's new direction + + obMega.setFeetX(x); + obMega.setFeetY(y); + obMega.setCurDir(dir); + + // Mega-set animation file + obGraph.setAnimResource(obMega.getMegasetRes()); + + // Dir + first stand frame (always frame 96) + obGraph.setAnimPc(dir + 96); +} + +/** + * Stand mega at end position of anim + */ + +void Router::standAfterAnim(byte *ob_graph, byte *ob_mega, uint32 animRes) { + byte *anim_file = _vm->_resman->openResource(animRes); + AnimHeader anim_head; + + anim_head.read(_vm->fetchAnimHeader(anim_file)); + + int32 x = anim_head.feetEndX; + int32 y = anim_head.feetEndY; + int32 dir = anim_head.feetEndDir; + + _vm->_resman->closeResource(animRes); + + // If start coords not available either use the standby coords (which + // should be set beforehand in the script) + + if (x == 0 && y == 0) { + x = _standbyX; + y = _standbyY; + dir = _standbyDir; + } + + standAt(ob_graph, ob_mega, x, y, dir); +} + +void Router::standAtAnim(byte *ob_graph, byte *ob_mega, uint32 animRes) { + byte *anim_file = _vm->_resman->openResource(animRes); + AnimHeader anim_head; + + anim_head.read(_vm->fetchAnimHeader(anim_file)); + + int32 x = anim_head.feetStartX; + int32 y = anim_head.feetStartY; + int32 dir = anim_head.feetStartDir; + + _vm->_resman->closeResource(animRes); + + // If start coords not available use the standby coords (which should + // be set beforehand in the script) + + if (x == 0 && y == 0) { + x = _standbyX; + y = _standbyY; + dir = _standbyDir; + } + + standAt(ob_graph, ob_mega, x, y, dir); +} + +} // End of namespace Sword2 |