/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "common/keyboard.h" #include "common/serializer.h" #include "common/memstream.h" #include "common/system.h" #include "common/util.h" #include "draci/draci.h" #include "draci/animation.h" #include "draci/game.h" #include "draci/barchive.h" #include "draci/font.h" #include "draci/mouse.h" #include "draci/screen.h" #include "draci/script.h" #include "draci/sound.h" #include "draci/surface.h" namespace Draci { static const char *const dialoguePath = "ROZH"; static double real_to_double(byte real[6]); enum { kWalkingMapOverlayColor = 2, kWalkingShortestPathOverlayColor = 120, kWalkingObliquePathOverlayColor = 73 }; Game::Game(DraciEngine *vm) : _vm(vm), _walkingState(vm) { _dialogueLinesNum = 0; _blockNum = 0; for (uint i = 0; i < kDialogueLines; i++) _dialogueAnims[0] = nullptr; _loopStatus = kStatusOrdinary; _loopSubstatus = kOuterLoop; _speechTick = 0; _speechDuration = 0; _markedAnimationIndex = 0; _scheduledPalette = 0; _fadePhases = 0; _fadePhase = 0; _fadeTick = 0; _mouseChangeTick = 0; _previousItemPosition = 0; _shouldQuit = false; _shouldExitLoop = false; _isReloaded = false; _isPositionLoaded = false; _isFadeOut = true; _enableQuickHero = false; _wantQuickHero = false; _enableSpeedText = false; _isPositionLoaded = false; _objUnderCursor = nullptr; _animUnderCursor = nullptr; _titleAnim = nullptr; _inventoryAnim = nullptr; _walkingMapOverlay = nullptr; _walkingShortestPathOverlay = nullptr; _walkingObliquePathOverlay = nullptr; _currentItem = nullptr; _itemUnderCursor = nullptr; for (int i = 0; i < kInventorySlots; i++) _inventory[i] = nullptr; _newRoom = 0; _newGate = 0; _previousRoom = 0; _pushedNewRoom = 0; _pushedNewGate = 0; _currentDialogue = 0; _dialogueArchive = nullptr; _dialogueBlocks = nullptr; _dialogueBegin = false; _dialogueExit = false; _currentBlock = 0; _lastBlock = 0; BArchive *initArchive = _vm->_initArchive; const BAFile *file; // Read in persons file = initArchive->getFile(5); Common::MemoryReadStream personData(file->_data, file->_length); const int personSize = sizeof(uint16) * 2 + sizeof(byte); uint numPersons = file->_length / personSize; _persons = new Person[numPersons]; for (uint i = 0; i < numPersons; ++i) { _persons[i]._x = personData.readUint16LE(); _persons[i]._y = personData.readUint16LE(); _persons[i]._fontColor = personData.readByte(); } // Read in dialogue offsets file = initArchive->getFile(4); Common::MemoryReadStream dialogueData(file->_data, file->_length); uint numDialogues = file->_length / sizeof(uint16); _dialogueOffsets = new uint[numDialogues]; uint curOffset, idx; for (idx = 0, curOffset = 0; idx < numDialogues; ++idx) { _dialogueOffsets[idx] = curOffset; curOffset += dialogueData.readUint16LE(); } _dialogueVars = new int[curOffset]; memset(_dialogueVars, 0, sizeof(int) * curOffset); // Read in game info file = initArchive->getFile(3); Common::MemoryReadStream gameData(file->_data, file->_length); _info._startRoom = gameData.readByte() - 1; _info._mapRoom = gameData.readByte() - 1; _info._numObjects = gameData.readUint16LE(); _info._numItems = gameData.readUint16LE(); _info._numVariables = gameData.readByte(); _info._numPersons = gameData.readByte(); _info._numDialogues = gameData.readByte(); _info._maxItemWidth = gameData.readUint16LE(); _info._maxItemHeight = gameData.readUint16LE(); _info._musicLength = gameData.readUint16LE(); _info._crc[0] = gameData.readUint16LE(); _info._crc[1] = gameData.readUint16LE(); _info._crc[2] = gameData.readUint16LE(); _info._crc[3] = gameData.readUint16LE(); _info._numDialogueBlocks = curOffset; // Read in variables file = initArchive->getFile(2); uint numVariables = file->_length / sizeof (int16); _variables = new int[numVariables]; Common::MemoryReadStream variableData(file->_data, file->_length); for (uint i = 0; i < numVariables; ++i) { _variables[i] = variableData.readUint16LE(); } // Read in item icon status file = initArchive->getFile(1); uint numItems = file->_length; _itemStatus = new byte[numItems]; memcpy(_itemStatus, file->_data, numItems); _items = new GameItem[numItems]; // Read in object status file = initArchive->getFile(0); uint numObjects = file->_length; _objects = new GameObject[numObjects]; Common::MemoryReadStream objStatus(file->_data, file->_length); for (uint i = 0; i < numObjects; ++i) { byte tmp = objStatus.readByte(); // Set object visibility _objects[i]._visible = tmp & (1 << 7); // Set object location _objects[i]._location = (~(1 << 7) & tmp) - 1; _objects[i]._playingAnim = -1; _objects[i]._absNum = i; // _anims have been initialized by the constructor } assert(numDialogues == _info._numDialogues); assert(numPersons == _info._numPersons); assert(numVariables == _info._numVariables); assert(numObjects == _info._numObjects); assert(numItems == _info._numItems); // Deallocate all cached files, because we have copied them into our own data structures. initArchive->clearCache(); } void Game::start() { while (!gameShouldQuit()) { // Reset the flag allowing to run the scripts. _vm->_script->endCurrentProgram(false); enterNewRoom(); if (_vm->_script->shouldEndProgram()) { // Escape pressed during the intro or map animations run in the // init scripts. This flag was turned on to skip the rest of // those programs. Don't call loop(), because the // location may have changed. fadePalette(true); continue; } // Call the outer loop doing all the hard job. loop(kOuterLoop, false); // Fade out the palette after leaving the location. fadePalette(true); if (!isReloaded()) { // We are changing location. Run the hero's LOOK // program to trigger a possible cut-scene. This is // the behavior of the original game player, whose // intention was to run the cut sequences after the // certain location change. const GameObject *dragon = getObject(kDragonObject); _vm->_script->run(dragon->_program, dragon->_look); } } } void Game::init() { setQuit(false); setExitLoop(false); setIsReloaded(false); _scheduledPalette = 0; _fadePhases = _fadePhase = 0; setEnableQuickHero(true); setWantQuickHero(false); setEnableSpeedText(true); setLoopStatus(kStatusGate); setLoopSubstatus(kOuterLoop); _animUnderCursor = nullptr; _currentItem = _itemUnderCursor = nullptr; _previousItemPosition = -1; _vm->_mouse->setCursorType(kHighlightedCursor); // anything different from kNormalCursor _objUnderCursor = nullptr; // Set the inventory to empty initially memset(_inventory, 0, kInventorySlots * sizeof(GameItem *)); // Initialize animation for object / room titles _titleAnim = new Animation(_vm, kTitleText, 257, true); _titleAnim->addFrame(new Text("", _vm->_smallFont, kTitleColor, 0, 0, 0), NULL); _vm->_anims->insert(_titleAnim, false); // Initialize animation for speech text Animation *speechAnim = new Animation(_vm, kSpeechText, 257, true); speechAnim->addFrame(new Text("", _vm->_bigFont, kFontColor1, 0, 0, 0), NULL); _vm->_anims->insert(speechAnim, false); // Initialize inventory animation. _iconsArchive is never flushed. const BAFile *f = _vm->_iconsArchive->getFile(13); _inventoryAnim = new Animation(_vm, kInventorySprite, 255, false); Sprite *inventorySprite = new Sprite(f->_data, f->_length, 0, 0, true); _inventoryAnim->addFrame(inventorySprite, NULL); _inventoryAnim->setRelative((kScreenWidth - inventorySprite->getWidth()) / 2, (kScreenHeight - inventorySprite->getHeight()) / 2); _vm->_anims->insert(_inventoryAnim, true); for (uint i = 0; i < kDialogueLines; ++i) { _dialogueAnims[i] = new Animation(_vm, kDialogueLinesID - i, 254, true); _dialogueAnims[i]->addFrame(new Text("", _vm->_smallFont, kLineInactiveColor, 0, 0, 0), NULL); _dialogueAnims[i]->setRelative(1, kScreenHeight - (i + 1) * _vm->_smallFont->getFontHeight()); _vm->_anims->insert(_dialogueAnims[i], false); Text *text = reinterpret_cast(_dialogueAnims[i]->getCurrentFrame()); text->setText(""); } for (uint i = 0; i < _info._numItems; ++i) { _items[i].load(i, _vm->_itemsArchive); } _objects[kDragonObject].load(kDragonObject, _vm->_objectsArchive); const GameObject *dragon = getObject(kDragonObject); debugC(4, kDraciLogicDebugLevel, "Running init program for the dragon object..."); _vm->_script->run(dragon->_program, dragon->_init); // Add overlays for the walking map and shortest/obliqued paths. initWalkingOverlays(); // Make sure we enter the right room in start(). setRoomNum(-1); rememberRoomNumAsPrevious(); scheduleEnteringRoomUsingGate(_info._startRoom, 0); _pushedNewRoom = _pushedNewGate = -1; _mouseChangeTick = kMouseDoNotSwitch; } void Game::handleOrdinaryLoop(int x, int y) { // During the normal game-play, in particular not when // running the init-scripts, enable interactivity. if (_loopSubstatus != kOuterLoop) { return; } if (_vm->_mouse->lButtonPressed()) { _vm->_mouse->lButtonSet(false); if (getCurrentItem()) { putItem(getCurrentItem(), getPreviousItemPosition()); updateOrdinaryCursor(); } else { if (_objUnderCursor) { _walkingState.setCallback(&_objUnderCursor->_program, _objUnderCursor->_look); if (_objUnderCursor->_imLook || !_currentRoom._heroOn) { _walkingState.callback(); } else { if (_objUnderCursor->_lookDir == kDirectionLast) { walkHero(x, y, _objUnderCursor->_lookDir); } else { walkHero(_objUnderCursor->_lookX, _objUnderCursor->_lookY, _objUnderCursor->_lookDir); } } } else { _walkingState.setCallback(NULL, 0); walkHero(x, y, kDirectionLast); } } } if (_vm->_mouse->rButtonPressed()) { _vm->_mouse->rButtonSet(false); if (_objUnderCursor) { if (_vm->_script->testExpression(_objUnderCursor->_program, _objUnderCursor->_canUse)) { _walkingState.setCallback(&_objUnderCursor->_program, _objUnderCursor->_use); if (_objUnderCursor->_imUse || !_currentRoom._heroOn) { _walkingState.callback(); } else { if (_objUnderCursor->_useDir == kDirectionLast) { walkHero(x, y, _objUnderCursor->_useDir); } else { walkHero(_objUnderCursor->_useX, _objUnderCursor->_useY, _objUnderCursor->_useDir); } } } else { _walkingState.setCallback(NULL, 0); walkHero(x, y, kDirectionLast); } } else { if (_vm->_script->testExpression(_currentRoom._program, _currentRoom._canUse)) { _walkingState.setCallback(&_currentRoom._program, _currentRoom._use); _walkingState.callback(); } else { _walkingState.setCallback(NULL, 0); walkHero(x, y, kDirectionLast); } } } } int Game::inventoryPositionFromMouse() const { const int column = CLIP(scummvm_lround( (_vm->_mouse->getPosX() - kInventoryX + kInventoryItemWidth / 2.) / kInventoryItemWidth) - 1, 0L, (long) kInventoryColumns - 1); const int line = CLIP(scummvm_lround( (_vm->_mouse->getPosY() - kInventoryY + kInventoryItemHeight / 2.) / kInventoryItemHeight) - 1, 0L, (long) kInventoryLines - 1); return line * kInventoryColumns + column; } void Game::handleInventoryLoop() { if (_loopSubstatus != kOuterLoop) { return; } // If we are in inventory mode, all the animations except game items' // images will necessarily be paused so we can safely assume that any // animation under the cursor (a value returned by // AnimationManager::getTopAnimation()) will be an item animation or // an overlay, for which we check. Item animations have their IDs // calculated by offseting their itemID from the ID of the last "special" // animation ID. In this way, we obtain its itemID. if (_animUnderCursor != nullptr && _animUnderCursor != _inventoryAnim && _animUnderCursor->getID() != kOverlayImage) { _itemUnderCursor = getItem(kInventoryItemsID - _animUnderCursor->getID()); assert(_itemUnderCursor != nullptr); assert(_itemUnderCursor->_anim == _animUnderCursor); } else { _itemUnderCursor = nullptr; } // If the user pressed the left mouse button if (_vm->_mouse->lButtonPressed()) { _vm->_mouse->lButtonSet(false); // If there is an inventory item under the cursor and we aren't // holding any item, run its look GPL program if (_itemUnderCursor && !getCurrentItem()) { _vm->_script->runWrapper(_itemUnderCursor->_program, _itemUnderCursor->_look, true, false); // Otherwise, if we are holding an item, try to place it inside the // inventory } else if (getCurrentItem()) { putItem(getCurrentItem(), inventoryPositionFromMouse()); updateInventoryCursor(); } } else if (_vm->_mouse->rButtonPressed()) { _vm->_mouse->rButtonSet(false); // If we right-clicked outside the inventory, close it if (_animUnderCursor != _inventoryAnim && !_itemUnderCursor) { inventoryDone(); // If there is an inventory item under our cursor } else if (_itemUnderCursor) { // Again, we have two possibilities: // The first is that there is no item in our hands. // In that case, just take the inventory item from the inventory. if (!getCurrentItem()) { setCurrentItem(_itemUnderCursor); setPreviousItemPosition(inventoryPositionFromMouse()); removeItem(_itemUnderCursor); // The second is that there *is* an item in our hands. // In that case, run the canUse script for the inventory item // which will check if the two items are combinable and, finally, // run the use script for the item. } else { if (_vm->_script->testExpression(_itemUnderCursor->_program, _itemUnderCursor->_canUse)) { _vm->_script->runWrapper(_itemUnderCursor->_program, _itemUnderCursor->_use, true, false); } } updateInventoryCursor(); } } } void Game::handleDialogueLoop() { if (_loopSubstatus != kInnerDuringDialogue) { return; } Text *text; for (int i = 0; i < kDialogueLines; ++i) { text = reinterpret_cast(_dialogueAnims[i]->getCurrentFrame()); if (_animUnderCursor == _dialogueAnims[i]) { text->setColor(kLineActiveColor); } else { text->setColor(kLineInactiveColor); } } if (_vm->_mouse->lButtonPressed() || _vm->_mouse->rButtonPressed()) { setExitLoop(true); _vm->_mouse->lButtonSet(false); _vm->_mouse->rButtonSet(false); } } void Game::fadePalette(bool fading_out) { _isFadeOut = fading_out; const byte *startPal = NULL; const byte *endPal = _currentRoom._palette >= 0 ? _vm->_paletteArchive->getFile(_currentRoom._palette)->_data : NULL; if (fading_out) { startPal = endPal; endPal = NULL; } for (int i = 1; i <= kBlackFadingIterations; ++i) { _vm->_system->delayMillis(kBlackFadingTimeUnit); _vm->_screen->interpolatePalettes(startPal, endPal, 0, kNumColors, i, kBlackFadingIterations); _vm->_screen->copyToScreen(); } } void Game::advanceAnimationsAndTestLoopExit() { // Fade the palette if requested if (_fadePhase > 0 && (_vm->_system->getMillis() - _fadeTick) >= kFadingTimeUnit) { _fadeTick = _vm->_system->getMillis(); --_fadePhase; const byte *startPal = _currentRoom._palette >= 0 ? _vm->_paletteArchive->getFile(_currentRoom._palette)->_data : NULL; const byte *endPal = getScheduledPalette() >= 0 ? _vm->_paletteArchive->getFile(getScheduledPalette())->_data : NULL; _vm->_screen->interpolatePalettes(startPal, endPal, 0, kNumColors, _fadePhases - _fadePhase, _fadePhases); if (_fadePhase == 0) { if (_loopSubstatus == kInnerWhileFade) { setExitLoop(true); } // Rewrite the palette index of the current room. This // is necessary when two fadings are called after each // other, such as in the intro. _currentRoom._palette = getScheduledPalette(); } } // Handle character talking (if there is any) if (_loopSubstatus == kInnerWhileTalk) { // If the current speech text has expired or the user clicked a mouse button, // advance to the next line of text if ((getEnableSpeedText() && (_vm->_mouse->lButtonPressed() || _vm->_mouse->rButtonPressed())) || (_vm->_system->getMillis() - _speechTick) >= _speechDuration) { setExitLoop(true); } _vm->_mouse->lButtonSet(false); _vm->_mouse->rButtonSet(false); } // A script has scheduled changing the room (either triggered // by the user clicking on something or run at the end of a // gate script in the intro). if ((_loopStatus == kStatusOrdinary || _loopStatus == kStatusGate) && (_newRoom != getRoomNum() || _newGate != _variables[0] - 1)) { // TODO: don't use _variables but a new named attribute setExitLoop(true); } // This returns true if we got a signal to quit the game if (gameShouldQuit()) { setExitLoop(true); } // Walk the hero. The WalkingState class handles everything including // proper timing. bool walkingFinished = false; if (_walkingState.isActive()) { walkingFinished = !_walkingState.continueWalkingOrClearPath(); // If walking has finished, the path won't be active anymore. } // Advance animations (this may also call setExitLoop(true) in the // callbacks) and redraw screen _vm->_anims->drawScene(_vm->_screen->getSurface()); _vm->_screen->copyToScreen(); _vm->_system->delayMillis(kTimeUnit); if(_isFadeOut) { fadePalette(false); // Set cursor state // Need to do this after we set the palette since the cursors use it if (_currentRoom._mouseOn) { debugC(6, kDraciLogicDebugLevel, "Mouse: ON"); _vm->_mouse->cursorOn(); _vm->_mouse->setCursorType(kNormalCursor); } else { debugC(6, kDraciLogicDebugLevel, "Mouse: OFF"); _vm->_mouse->cursorOff(); } } // If the hero has arrived at his destination, after even the last // phase was correctly animated, run the callback. if (walkingFinished) { bool exitLoop = false; if (_loopSubstatus == kInnerUntilExit) { // The callback may run another inner loop (for // example, a dialogue). Reset the loop // substatus temporarily to the outer one. exitLoop = true; setLoopSubstatus(kOuterLoop); } debugC(2, kDraciWalkingDebugLevel, "Finished walking"); _walkingState.callback(); // clears callback pointer first if (exitLoop) { debugC(3, kDraciWalkingDebugLevel, "Exiting from the inner loop"); setExitLoop(true); setLoopSubstatus(kInnerUntilExit); } } } void Game::loop(LoopSubstatus substatus, bool shouldExit) { // Can run both as an outer and inner loop. In both mode it updates // the screen according to the timer. It the outer mode (kOuterLoop) // it also reacts to user events. In the inner mode (all kInner* // enums), the loop runs until its stopping condition, possibly // stopping earlier if the user interrupts it, however no other user // intervention is allowed. assert(getLoopSubstatus() == kOuterLoop); setLoopSubstatus(substatus); setExitLoop(shouldExit); // Always enter the first pass of the loop, even if shouldExitLoop() is // true, exactly to ensure to make at least one pass. do { debugC(4, kDraciLogicDebugLevel, "loopstatus: %d, loopsubstatus: %d", _loopStatus, _loopSubstatus); _vm->handleEvents(); if (isReloaded()) { // Cannot continue with the same animation objects, // because the real data structures of the game have // completely been changed. break; } advanceAnimationsAndTestLoopExit(); if (_vm->_mouse->isCursorOn()) { // Find animation under cursor and the game object // corresponding to it int x = _vm->_mouse->getPosX(); int y = _vm->_mouse->getPosY(); _animUnderCursor = _vm->_anims->getTopAnimation(x, y); _objUnderCursor = getObjectWithAnimation(_animUnderCursor); debugC(5, kDraciLogicDebugLevel, "Anim under cursor: %d", _animUnderCursor ? _animUnderCursor->getID() : -1); switch (_loopStatus) { case kStatusOrdinary: updateOrdinaryCursor(); updateTitle(x, y); handleOrdinaryLoop(x, y); handleStatusChangeByMouse(); break; case kStatusInventory: updateInventoryCursor(); updateTitle(x, y); handleInventoryLoop(); handleStatusChangeByMouse(); break; case kStatusDialogue: handleDialogueLoop(); break; case kStatusGate: // cannot happen when isCursonOn; added for completeness default: break; } } } while (!shouldExitLoop()); setLoopSubstatus(kOuterLoop); setExitLoop(false); } void Game::handleStatusChangeByMouse() { const int mouseY = _vm->_mouse->getPosY(); bool wantsChange = false; if (_loopStatus == kStatusOrdinary) { if (getRoomNum() == getMapRoom()) { wantsChange = mouseY >= kScreenHeight - 1; } else { wantsChange = mouseY == 0 || mouseY >= kScreenHeight - 1; } } else if (_loopStatus == kStatusInventory) { wantsChange = _animUnderCursor != _inventoryAnim && !_itemUnderCursor && mouseY != 0; } if (!wantsChange) { // Turn off the timer, but enable switching. _mouseChangeTick = kMouseEnableSwitching; // Otherwise the mouse signalizes that the mode should be changed. } else if (_mouseChangeTick == kMouseEnableSwitching) { // If the timer is currently disabled, this is the first time // when the mouse left the region. Start counting. _mouseChangeTick = _vm->_system->getMillis(); } else if (_mouseChangeTick == kMouseDoNotSwitch) { // Do nothing. This exception is good when the status has just // changed. Even if the mouse starts in the outside region // (e.g., due to flipping the change by a key or due to // flipping back being triggered by the same hot area), the // timeout won't kick in until it moves into the inside region // for the first time. } else if (_vm->_system->getMillis() - _mouseChangeTick >= kStatusChangeTimeout) { if (_loopStatus == kStatusOrdinary) { if (getRoomNum() == getMapRoom()) { scheduleEnteringRoomUsingGate(getPreviousRoomNum(), 0); } else if (mouseY >= kScreenHeight - 1) { scheduleEnteringRoomUsingGate(getMapRoom(), 0); } else if (mouseY == 0) { inventoryInit(); } } else { inventoryDone(); } } // We don't implement the original game player's main menu that pops up // when the mouse gets to the bottom of the screen. It contains icons // for displaying the map, loading/saving the game, quiting the game, // and displaying the credits. The essential options are implemented // in ScummVM's main menu, I don't wanna implement the credits, and so // I allocate the whole bottom line for switching to/from the map. } void Game::updateOrdinaryCursor() { // Fetch mouse coordinates bool mouseChanged = false; // If there is no game object under the cursor, try using the room itself if (!_objUnderCursor) { if (_vm->_script->testExpression(_currentRoom._program, _currentRoom._canUse)) { if (!getCurrentItem()) { _vm->_mouse->setCursorType(kHighlightedCursor); } else { _vm->_mouse->loadItemCursor(getCurrentItem(), true); } mouseChanged = true; } // If there *is* a game object under the cursor, update the cursor image } else { // If there is no walking direction set on the object (i.e. the object // is not a gate / exit), test whether it can be used and, if so, // update the cursor image (highlight it). if (_objUnderCursor->_walkDir == 0) { if (_vm->_script->testExpression(_objUnderCursor->_program, _objUnderCursor->_canUse)) { if (!getCurrentItem()) { _vm->_mouse->setCursorType(kHighlightedCursor); } else { _vm->_mouse->loadItemCursor(getCurrentItem(), true); } mouseChanged = true; } // If the walking direction *is* set, the game object is a gate, so update // the cursor image to the appropriate arrow. } else { _vm->_mouse->setCursorType((CursorType)_objUnderCursor->_walkDir); mouseChanged = true; } } // Load the appropriate cursor (item image if an item is held or ordinary cursor // if not) if (!mouseChanged) { if (!getCurrentItem()) { _vm->_mouse->setCursorType(kNormalCursor); } else { _vm->_mouse->loadItemCursor(getCurrentItem(), false); } } } void Game::updateInventoryCursor() { // Fetch mouse coordinates bool mouseChanged = false; if (_itemUnderCursor) { if (_vm->_script->testExpression(_itemUnderCursor->_program, _itemUnderCursor->_canUse)) { if (!getCurrentItem()) { _vm->_mouse->setCursorType(kHighlightedCursor); } else { _vm->_mouse->loadItemCursor(getCurrentItem(), true); } mouseChanged = true; } } if (!mouseChanged) { if (!getCurrentItem()) { _vm->_mouse->setCursorType(kNormalCursor); } else { _vm->_mouse->loadItemCursor(getCurrentItem(), false); } } } void Game::updateTitle(int x, int y) { // Fetch current surface and height of the small font (used for titles) Surface *surface = _vm->_screen->getSurface(); const int smallFontHeight = _vm->_smallFont->getFontHeight(); // Fetch the dedicated objects' title animation / current frame Text *title = reinterpret_cast(_titleAnim->getCurrentFrame()); // Mark dirty rectangle to delete the previous text _titleAnim->markDirtyRect(surface); if (_loopStatus == kStatusInventory) { // If there is no item under the cursor, delete the title. // Otherwise, show the item's title. title->setText(_itemUnderCursor ? _itemUnderCursor->_title : ""); } else { // If there is no object under the cursor, delete the title. // Otherwise, show the object's title. title->setText(_objUnderCursor ? _objUnderCursor->_title : ""); } // Move the title to the correct place (just above the cursor) int newX = surface->centerOnX(x, title->getWidth()); int newY = surface->putAboveY(y - smallFontHeight / 2, title->getHeight()); _titleAnim->setRelative(newX, newY); // If we are currently playing the title, mark it dirty so it gets updated. // Otherwise, start playing the title animation. if (_titleAnim->isPlaying()) { _titleAnim->markDirtyRect(surface); } else { _titleAnim->play(); } } const GameObject *Game::getObjectWithAnimation(const Animation *anim) const { for (uint i = 0; i < _info._numObjects; ++i) { GameObject *obj = &_objects[i]; if (obj->_playingAnim >= 0 && obj->_anim[obj->_playingAnim] == anim) { return obj; } } return NULL; } void Game::removeItem(GameItem *item) { if (!item) return; for (uint i = 0; i < kInventorySlots; ++i) { if (_inventory[i] == item) { _inventory[i] = nullptr; item->_anim->stop(); break; } } } void Game::loadItemAnimation(GameItem *item) { if (item->_anim) return; item->_anim = new Animation(_vm, kInventoryItemsID - item->_absNum, 256, false); _vm->_anims->insert(item->_anim, false); // _itemImagesArchive is never flushed. const BAFile *img = _vm->_itemImagesArchive->getFile(2 * item->_absNum); item->_anim->addFrame(new Sprite(img->_data, img->_length, 0, 0, true), NULL); } void Game::putItem(GameItem *item, int position) { // Empty our hands setCurrentItem(NULL); if (!item) return; assert(position >= 0); for (int i = 0; i < kInventorySlots; ++i) { int pos = (position + i) % kInventorySlots; if (!_inventory[pos] || _inventory[pos] == item) { _inventory[pos] = item; position = pos; break; } } setPreviousItemPosition(position); const int line = position / kInventoryColumns + 1; const int column = position % kInventoryColumns + 1; loadItemAnimation(item); Animation *anim = item->_anim; Drawable *frame = anim->getCurrentFrame(); const int x = kInventoryX + (column * kInventoryItemWidth) - (kInventoryItemWidth / 2) - (frame->getWidth() / 2); const int y = kInventoryY + (line * kInventoryItemHeight) - (kInventoryItemHeight / 2) - (frame->getHeight() / 2); debug(2, "itemID: %d position: %d line: %d column: %d x: %d y: %d", item->_absNum, position, line, column, x, y); anim->setRelative(x, y); // If we are in inventory mode, we need to play the item animation, immediately // upon returning it to its slot but *not* in other modes because it should be // invisible then (along with the inventory) if (_loopStatus == kStatusInventory && _loopSubstatus == kOuterLoop) { anim->play(); } } void Game::inventoryInit() { // Pause all "background" animations _vm->_anims->pauseAnimations(); // Draw the inventory and the current items inventoryDraw(); // Turn cursor on if it is off _vm->_mouse->cursorOn(); // Set the appropriate loop status setLoopStatus(kStatusInventory); if (_walkingState.isActive()) { _walkingState.stopWalking(); walkHero(_hero.x, _hero.y, kDirectionLast); } else { _lastTarget = _hero; } // Don't return from the inventory mode immediately if the mouse is out. _mouseChangeTick = kMouseDoNotSwitch; } void Game::inventoryDone() { _vm->_mouse->cursorOn(); setLoopStatus(kStatusOrdinary); _vm->_anims->unpauseAnimations(); _inventoryAnim->stop(); for (uint i = 0; i < kInventorySlots; ++i) { if (_inventory[i]) { _inventory[i]->_anim->stop(); } } // Start moving to last target walkHero(_lastTarget.x, _lastTarget.y, kDirectionLast); _walkingState.callbackLast(); // Reset item under cursor _itemUnderCursor = nullptr; // Don't start the inventory mode again if the mouse is on the top. _mouseChangeTick = kMouseDoNotSwitch; } void Game::inventoryDraw() { _inventoryAnim->play(); for (uint i = 0; i < kInventorySlots; ++i) { if (_inventory[i]) { _inventory[i]->_anim->play(); } } } void Game::inventoryReload() { // Make sure all items are loaded into memory (e.g., after loading a // savegame) by re-putting them on the same spot in the inventory. GameItem *tempItem = _currentItem; for (uint i = 0; i < kInventorySlots; ++i) { putItem(_inventory[i], i); } setPreviousItemPosition(0); _currentItem = tempItem; } void Game::inventorySwitch(int keycode) { switch (keycode) { case Common::KEYCODE_SLASH: // Switch between holding an item and the ordinary mouse cursor. if (!getCurrentItem()) { if (getPreviousItemPosition() >= 0) { GameItem* last_item = _inventory[getPreviousItemPosition()]; setCurrentItem(last_item); removeItem(last_item); } } else { putItem(getCurrentItem(), getPreviousItemPosition()); } break; case Common::KEYCODE_COMMA: case Common::KEYCODE_PERIOD: // Iterate between the items in the inventory. if (getCurrentItem()) { assert(getPreviousItemPosition() >= 0); int direction = keycode == Common::KEYCODE_PERIOD ? +1 : -1; // Find the next available item. int pos = getPreviousItemPosition() + direction; while (true) { if (pos < 0) pos += kInventorySlots; else if (pos >= kInventorySlots) pos -= kInventorySlots; if (pos == getPreviousItemPosition() || _inventory[pos]) { break; } pos += direction; } // Swap it with the current item. putItem(getCurrentItem(), getPreviousItemPosition()); GameItem* new_item = _inventory[pos]; setCurrentItem(new_item); setPreviousItemPosition(pos); removeItem(new_item); } break; default: break; } if (getLoopStatus() == kStatusOrdinary) { updateOrdinaryCursor(); } else { updateInventoryCursor(); } } void Game::dialogueMenu(int dialogueID) { int oldLines, hit; Common::String name; name = dialoguePath + Common::String::format("%d.dfw", dialogueID + 1); _dialogueArchive = new BArchive(name); debugC(4, kDraciLogicDebugLevel, "Starting dialogue (ID: %d, Archive: %s)", dialogueID, name.c_str()); _currentDialogue = dialogueID; oldLines = 255; dialogueInit(dialogueID); do { _dialogueExit = false; hit = dialogueDraw(); debugC(7, kDraciLogicDebugLevel, "hit: %d, _lines[hit]: %d, lastblock: %d, dialogueLines: %d, dialogueExit: %d", hit, (hit >= 0 ? _lines[hit] : -1), _lastBlock, _dialogueLinesNum, _dialogueExit); if ((!_dialogueExit) && (hit >= 0) && (_lines[hit] != -1)) { if ((oldLines == 1) && (_dialogueLinesNum == 1) && (_lines[hit] == _lastBlock)) { break; } _currentBlock = _lines[hit]; // Run the dialogue program _vm->_script->runWrapper(_dialogueBlocks[_lines[hit]]._program, 1, false, true); } else { break; } _lastBlock = _lines[hit]; _dialogueVars[_dialogueOffsets[dialogueID] + _lastBlock] += 1; _dialogueBegin = false; oldLines = _dialogueLinesNum; } while (!_dialogueExit); dialogueDone(); _currentDialogue = -1; } int Game::dialogueDraw() { _dialogueLinesNum = 0; int i = 0; int ret = 0; Animation *anim; Text *dialogueLine; while ((_dialogueLinesNum < 4) && (i < _blockNum)) { GPL2Program blockTest; blockTest._bytecode = _dialogueBlocks[i]._canBlock; blockTest._length = _dialogueBlocks[i]._canLen; debugC(3, kDraciLogicDebugLevel, "Testing dialogue block %d", i); if (_vm->_script->testExpression(blockTest, 1)) { anim = _dialogueAnims[_dialogueLinesNum]; dialogueLine = reinterpret_cast(anim->getCurrentFrame()); dialogueLine->setText(_dialogueBlocks[i]._title); dialogueLine->setColor(kLineInactiveColor); _lines[_dialogueLinesNum] = i; _dialogueLinesNum++; } ++i; } for (i = _dialogueLinesNum; i < kDialogueLines; ++i) { _lines[i] = -1; anim = _dialogueAnims[i]; dialogueLine = reinterpret_cast(anim->getCurrentFrame()); dialogueLine->setText(""); } if (_dialogueLinesNum > 1) { // Call the game loop to enable interactivity until the user // selects his choice. _animUnderCursor will be set. _vm->_mouse->cursorOn(); loop(kInnerDuringDialogue, false); _vm->_mouse->cursorOff(); bool notDialogueAnim = true; for (uint j = 0; j < kDialogueLines; ++j) { if (_dialogueAnims[j] == _animUnderCursor) { notDialogueAnim = false; break; } } if (notDialogueAnim) { ret = -1; } else { ret = kDialogueLinesID - _animUnderCursor->getID(); } } else { ret = _dialogueLinesNum - 1; } for (i = 0; i < kDialogueLines; ++i) { dialogueLine = reinterpret_cast(_dialogueAnims[i]->getCurrentFrame()); _dialogueAnims[i]->markDirtyRect(_vm->_screen->getSurface()); dialogueLine->setText(""); } return ret; } void Game::dialogueInit(int dialogID) { _vm->_mouse->setCursorType(kDialogueCursor); _blockNum = _dialogueArchive->size() / 3; _dialogueBlocks = new Dialogue[_blockNum]; const BAFile *f; for (uint i = 0; i < kDialogueLines; ++i) { _lines[i] = 0; } for (int i = 0; i < _blockNum; ++i) { f = _dialogueArchive->getFile(i * 3); _dialogueBlocks[i]._canLen = f->_length; _dialogueBlocks[i]._canBlock = f->_data; f = _dialogueArchive->getFile(i * 3 + 1); // The first byte of the file is the length of the string (without the length) assert(f->_length - 1 == f->_data[0]); _dialogueBlocks[i]._title = Common::String((char *)(f->_data+1), f->_length-1); f = _dialogueArchive->getFile(i * 3 + 2); _dialogueBlocks[i]._program._bytecode = f->_data; _dialogueBlocks[i]._program._length = f->_length; } for (uint i = 0; i < kDialogueLines; ++i) { _dialogueAnims[i]->play(); } setLoopStatus(kStatusDialogue); _lastBlock = -1; _dialogueBegin = true; } void Game::dialogueDone() { for (uint i = 0; i < kDialogueLines; ++i) { _dialogueAnims[i]->stop(); } delete _dialogueArchive; delete[] _dialogueBlocks; setLoopStatus(kStatusOrdinary); _vm->_mouse->setCursorType(kNormalCursor); } int Game::playHeroAnimation(int anim_index) { GameObject *dragon = getObject(kDragonObject); const int current_anim_index = dragon->_playingAnim; Animation *anim = dragon->_anim[anim_index]; if (anim_index == current_anim_index) { anim->markDirtyRect(_vm->_screen->getSurface()); } else { dragon->stopAnim(); } positionAnimAsHero(anim); if (anim_index == current_anim_index) { anim->markDirtyRect(_vm->_screen->getSurface()); } else { dragon->playAnim(anim_index); } return anim->currentFrameNum(); } void Game::redrawWalkingPath(Animation *anim, byte color, const WalkingPath &path) { Sprite *ov = _walkingMap.newOverlayFromPath(path, color); delete anim->getFrame(0); anim->replaceFrame(0, ov, NULL); anim->markDirtyRect(_vm->_screen->getSurface()); } void Game::setHeroPosition(const Common::Point &p) { debugC(3, kDraciWalkingDebugLevel, "Jump to x: %d y: %d", p.x, p.y); _hero = p; } void Game::walkHero(int x, int y, SightDirection dir) { if (!_currentRoom._heroOn) { // Nothing to do. Happens for example in the map. return; } // Find the closest walkable point. Common::Point target = findNearestWalkable(x, y); if (target.x < 0 || target.y < 0) { debug(1, "The is no walkable point on the map"); return; } // Compute the shortest and obliqued path. WalkingPath shortestPath, obliquePath; if (!_walkingMap.findShortestPath(_hero, target, &shortestPath)) { debug(1, "Unreachable point [%d,%d]", target.x, target.y); return; } // Save point of player's last target. if (_loopStatus != kStatusInventory) { _lastTarget = target; } _walkingMap.obliquePath(shortestPath, &obliquePath); debugC(2, kDraciWalkingDebugLevel, "Walking path lengths: shortest=%d oblique=%d", shortestPath.size(), obliquePath.size()); if (_vm->_showWalkingMap) { redrawWalkingPath(_walkingShortestPathOverlay, kWalkingShortestPathOverlayColor, shortestPath); redrawWalkingPath(_walkingObliquePathOverlay, kWalkingObliquePathOverlayColor, obliquePath); } // Start walking. Walking will be gradually advanced by // advanceAnimationsAndTestLoopExit(), which also handles calling the // callback and stopping the walk at the end. If the hero is already // walking at this point, this command will cancel the previous path // and replace it by the current one (the callback has already been // reset by our caller). _walkingState.startWalking(_hero, target, Common::Point(x, y), dir, _walkingMap.getDelta(), obliquePath); } void Game::initWalkingOverlays() { _walkingMapOverlay = new Animation(_vm, kWalkingMapOverlay, 256, _vm->_showWalkingMap); _walkingMapOverlay->addFrame(NULL, NULL); // rewritten below by loadWalkingMap() _vm->_anims->insert(_walkingMapOverlay, true); _walkingShortestPathOverlay = new Animation(_vm, kWalkingShortestPathOverlay, 257, _vm->_showWalkingMap); _walkingObliquePathOverlay = new Animation(_vm, kWalkingObliquePathOverlay, 258, _vm->_showWalkingMap); WalkingPath emptyPath; _walkingShortestPathOverlay->addFrame(_walkingMap.newOverlayFromPath(emptyPath, 0), NULL); _walkingObliquePathOverlay->addFrame(_walkingMap.newOverlayFromPath(emptyPath, 0), NULL); _vm->_anims->insert(_walkingShortestPathOverlay, true); _vm->_anims->insert(_walkingObliquePathOverlay, true); } void Game::loadRoomObjects() { // Load the room's objects for (uint i = 0; i < _info._numObjects; ++i) { debugC(7, kDraciLogicDebugLevel, "Checking if object %d (%d) is at the current location (%d)", i, _objects[i]._location, getRoomNum()); if (_objects[i]._location == getRoomNum()) { debugC(6, kDraciLogicDebugLevel, "Loading object %d from room %d", i, getRoomNum()); _objects[i].load(i, _vm->_objectsArchive); } } // Run the init scripts for room objects // We can't do this in the above loop because some objects' scripts reference // other objects that may not yet be loaded for (uint i = 0; i < _info._numObjects; ++i) { if (_objects[i]._location == getRoomNum()) { const GameObject *obj = getObject(i); debugC(6, kDraciLogicDebugLevel, "Running init program for object %d (offset %d)", i, obj->_init); _vm->_script->run(obj->_program, obj->_init); } } // Run the init part of the GPL program debugC(4, kDraciLogicDebugLevel, "Running room init program..."); _vm->_script->run(_currentRoom._program, _currentRoom._init); } void Game::loadWalkingMap(int mapID) { const BAFile *f; f = _vm->_walkingMapsArchive->getFile(mapID); _walkingMap.load(f->_data, f->_length); Sprite *ov = _walkingMap.newOverlayFromMap(kWalkingMapOverlayColor); delete _walkingMapOverlay->getFrame(0); _walkingMapOverlay->replaceFrame(0, ov, NULL); _walkingMapOverlay->markDirtyRect(_vm->_screen->getSurface()); } void Game::switchWalkingAnimations(bool enabled) { if (enabled) { _walkingMapOverlay->play(); _walkingShortestPathOverlay->play(); _walkingObliquePathOverlay->play(); } else { _walkingMapOverlay->stop(); _walkingShortestPathOverlay->stop(); _walkingObliquePathOverlay->stop(); } } void Game::loadOverlays() { uint x, y, z, num; const BAFile *overlayHeader; overlayHeader = _vm->_roomsArchive->getFile(getRoomNum() * 4 + 2); Common::MemoryReadStream overlayReader(overlayHeader->_data, overlayHeader->_length); for (int i = 0; i < _currentRoom._numOverlays; i++) { num = overlayReader.readUint16LE() - 1; x = overlayReader.readUint16LE(); y = overlayReader.readUint16LE(); z = overlayReader.readByte(); // _overlaysArchive is flushed when entering a room and this // code is called after the flushing has been done. const BAFile *overlayFile; overlayFile = _vm->_overlaysArchive->getFile(num); Sprite *sp = new Sprite(overlayFile->_data, overlayFile->_length, x, y, true); Animation *anim = new Animation(_vm, kOverlayImage, z, true); anim->addFrame(sp, NULL); // Since this is an overlay, we don't need it to be deleted // when the GPL Release command is invoked _vm->_anims->insert(anim, false); } _vm->_screen->getSurface()->markDirty(); } void Game::deleteObjectAnimations() { // Deallocate all animations, because their sound samples will not // survive clearing the sound sample cache when changing the location. // It's OK to unload them even if they are still in the inventory, // because we only need their icons which survive. // Start from 1, because 0==kDragonObject. for (uint i = 1; i < _info._numObjects; ++i) { GameObject *obj = &_objects[i]; obj->deleteAnims(); } // WORKAROUND // // An absolutely horrible hack follows. The current memory management // is completely broken and it needs to be seriously hacked to work at // all. The problem is caching sound samples in BArchive and clearing // all caches when entering a new location. The animation sequences // store pointers to samples owned by the BArchive cache, which gets // invalidated during the location change. If an animation sequence // survives location change and refers to any sound sample, we get // SIGSEGV when next played, because this sound sample will have been // deallocated by the cache although still referred to by the animation. // // Caveat: when I tried to perform a deep copy and make the animation // object own the sound samples and deallocate them when the animation // has been deleted, I get an almost immediate SIGSEGV in runWrapper() // on many objects, because often an animation graphically ends and is // stopped, but the last sound sample still plays for a short while // afterwards. If the sound sample is cached, it's OK, but if it's // deallocated with releaseAnims==true, we get SIGSEGV in the sound // mixer. This problem doesn't occur when changing locations, because // there we first explicitly stop all playing sounds. // // The loop above deallocates all animations corresponding to non-hero // objects. Hero is special, because the first ~20 animations are // standard and loaded for the whole game (standing, walking, etc.). // They are loaded by the GPL2 init routine for object hero. Luckily // the animations don't refer to any sound samples. The remaining // animations thus must be deallocated manually, otherwise they won't // be re-loaded next time assuming that they are already correctly // loaded. // // Why this only occurred for sound samples and not sprites? This bug is // concealed by a complete coincidence, that all sprites are stored // column-wise and our class Sprite detects this and creates a local // copy. If this wasn't the case, each animation (not just with sound // samples) would fail and preserving the ~20 hero animations wouldn't // work either. // // TODO: completely rewrite the resource management. maybe implement // usage counters? maybe completely ignore the GPL2 hints and manage // memory completely on my own? // We don't want to deallocate the first ~20 resident dragon // animations, because they are loaded exactly once in dragon's init // script and we rely upon their existence. GameObject *dragon = &_objects[kDragonObject]; dragon->deleteAnimsFrom(kFirstTemporaryAnimation); if (dragon->_playingAnim < 0) { // For the hero, we always need to have exactly 1 playing // animation, otherwise we index an array with -1. dragon->_playingAnim = 0; } } void Game::enterNewRoom() { debugC(1, kDraciLogicDebugLevel, "Entering room %d using gate %d", _newRoom, _newGate); _vm->_mouse->cursorOff(); // Make sure all sounds are stopped before we deallocate their memory // by clearing the cache. We don't have to wait for sounds to end, // because the timeout for voice is set exactly according to the length // of the sound. If the loop ends earlier, e.g. per user's request, we // do wanna end the sounds immediately. _vm->_sound->stopAll(); // Clear archives _vm->_roomsArchive->clearCache(); _vm->_spritesArchive->clearCache(); _vm->_paletteArchive->clearCache(); _vm->_animationsArchive->clearCache(); _vm->_walkingMapsArchive->clearCache(); _vm->_soundsArchive->clearCache(); _vm->_dubbingArchive->clearCache(); _vm->_overlaysArchive->clearCache(); _vm->_screen->clearScreen(); _vm->_anims->deleteOverlays(); GameObject *dragon = getObject(kDragonObject); dragon->stopAnim(); // Remember the previous room for returning back from the map. rememberRoomNumAsPrevious(); deleteObjectAnimations(); // Before setting these variables we have to convert the values to 1-based indexing // because this is how everything is stored in the data files _variables[0] = _newGate + 1; _variables[1] = _newRoom + 1; // If the new room is the map room, set the appropriate coordinates // for the dragon in the persons array if (_newRoom == _info._mapRoom) { _persons[kDragonObject]._x = 160; _persons[kDragonObject]._y = 0; } // Set the appropriate loop status before loading the room setLoopStatus(kStatusGate); setIsReloaded(false); // Make sure the possible walking path from the previous room is // cleaned up. Some rooms (e.g., the map) don't support walking. _walkingState.stopWalking(); // Stop a possible palette fading. _fadePhases = _fadePhase = 0; _currentRoom.load(_newRoom, _vm->_roomsArchive); loadWalkingMap(getMapID()); loadRoomObjects(); loadOverlays(); // Draw the scene with the black palette and slowly fade into the right palette. _vm->_screen->setPalette(NULL, 0, kNumColors); _vm->_anims->drawScene(_vm->_screen->getSurface()); _vm->_screen->copyToScreen(); // Run the program for the gate the dragon came through debugC(6, kDraciLogicDebugLevel, "Running program for gate %d", _newGate); _vm->_script->runWrapper(_currentRoom._program, _currentRoom._gates[_newGate], true, true); // Reset the loop status. setLoopStatus(kStatusOrdinary); setExitLoop(false); // Don't immediately switch to the map or inventory even if the mouse // position tell us to. _mouseChangeTick = kMouseDoNotSwitch; } void Game::positionAnimAsHero(Animation *anim) { // Calculate scaling factors const double scale = getPers0() + getPersStep() * _hero.y; // Set the Z coordinate for the dragon's animation anim->setZ(_hero.y); // Fetch current frame Drawable *frame = anim->getCurrentFrame(); // We naturally want the dragon to position its feet to the location of the // click but sprites are drawn from their top-left corner so we subtract // the current height of the dragon's sprite Common::Point p = _hero; p.x -= scummvm_lround(scale * frame->getWidth() / 2); p.y -= scummvm_lround(scale * frame->getHeight()); // Since _persons[] is used for placing talking text, we use the non-adjusted x value // so the text remains centered over the dragon. _persons[kDragonObject]._x = _hero.x; _persons[kDragonObject]._y = p.y; if (anim->isRelative()) { // Set the per-animation scaling factor and relative position anim->setScaleFactors(scale, scale); anim->setRelative(p.x, p.y); // Clear the animation's shift so that the real sprite stays at place // regardless of what the current phase is. If the animation starts // from the beginning, the shift is already [0,0], but if it is in the // middle, it may be different. anim->clearShift(); // Otherwise this dragon animation is used at exactly one place // in the game (such as jumping into the secret entrance), // which can is recognized by it using absolute coordinates. // Bypass our animation positioning system, otherwise there two // shifts will get summed and the animation will be placed // outside the screen. } } void Game::positionHeroAsAnim(Animation *anim) { // Check out where the hero has moved to by composing the relative // shifts of the sprites. _hero = anim->getCurrentFramePosition(); // Update our hero coordinates (don't forget that our control point is // elsewhere). This is formula is the exact inverse of the formula // used in positionAnimAsHero() and even rounding errors are exactly // the same. Drawable *frame = anim->getCurrentFrame(); _hero.x += scummvm_lround(anim->getScaleX() * frame->getWidth() / 2); _hero.y += scummvm_lround(anim->getScaleY() * frame->getHeight()); } void Game::pushNewRoom() { _pushedNewRoom = _newRoom; _pushedNewGate = _newGate; } void Game::popNewRoom() { if (_loopStatus != kStatusInventory && _pushedNewRoom >= 0) { scheduleEnteringRoomUsingGate(_pushedNewRoom, _pushedNewGate); _pushedNewRoom = _pushedNewGate = -1; } } void Game::setSpeechTiming(uint tick, uint duration) { _speechTick = tick; _speechDuration = duration; } void Game::shiftSpeechAndFadeTick(int delta) { _speechTick += delta; _fadeTick += delta; } void Game::initializeFading(int phases) { _fadePhases = _fadePhase = phases; _fadeTick = _vm->_system->getMillis(); } void Game::deleteAnimationsAfterIndex(int lastAnimIndex) { // Delete all animations loaded after the marked one // (from objects and from the AnimationManager) for (uint i = 0; i < getNumObjects(); ++i) { GameObject *obj = &_objects[i]; for (int j = obj->_anim.size() - 1; j >= 0; --j) { Animation *anim = obj->_anim[j]; if (anim->getIndex() > lastAnimIndex) { obj->_anim.remove_at(j); if (j == obj->_playingAnim) { obj->_playingAnim = -1; } } } } _vm->_anims->deleteAfterIndex(lastAnimIndex); } Game::~Game() { delete[] _persons; delete[] _variables; delete[] _dialogueOffsets; delete[] _dialogueVars; delete[] _objects; delete[] _itemStatus; delete[] _items; } void Game::synchronize(Common::Serializer &s, uint8 saveVersion) { s.syncAsUint16LE(_currentRoom._roomNum); for (uint i = 0; i < _info._numObjects; ++i) { GameObject& obj = _objects[i]; s.syncAsSint16LE(obj._location); s.syncAsByte(obj._visible); } for (uint i = 0; i < _info._numItems; ++i) { s.syncAsByte(_itemStatus[i]); } for (int i = 0; i < kInventorySlots; ++i) { if (s.isSaving()) { int itemID = _inventory[i] ? _inventory[i]->_absNum : -1; s.syncAsSint16LE(itemID); } else { int itemID = -1; s.syncAsSint16LE(itemID); _inventory[i] = getItem(itemID); } } for (int i = 0; i < _info._numVariables; ++i) { s.syncAsSint16LE(_variables[i]); } for (uint i = 0; i < _info._numDialogueBlocks; ++i) { s.syncAsSint16LE(_dialogueVars[i]); } if(saveVersion >= 2) { setPositionLoaded(true); if (s.isSaving()) { s.syncAsSint16LE(_hero.x); s.syncAsSint16LE(_hero.y); int handItemID = _currentItem ? _currentItem->_absNum : -1; s.syncAsSint16LE(handItemID); } else { s.syncAsSint16LE(_heroLoading.x); s.syncAsSint16LE(_heroLoading.y); int handItemID = -1; s.syncAsSint16LE(handItemID); _currentItem = getItem(handItemID); } } else { _currentItem = nullptr; } } static double real_to_double(byte real[6]) { // Extract sign bit int sign = real[0] & (1 << 7); // Extract exponent and adjust for bias int exp = real[5] - 129; double mantissa; double tmp = 0.0; if (real[5] == 0) { mantissa = 0.0; } else { // Process the first four least significant bytes for (int i = 4; i >= 1; --i) { tmp += real[i]; tmp /= 1 << 8; } // Process the most significant byte (remove the sign bit) tmp += real[0] & ((1 << 7) - 1); tmp /= 1 << 8; // Calculate mantissa mantissa = 1.0; mantissa += 2.0 * tmp; } // Flip sign if necessary if (sign) { mantissa = -mantissa; } // Calculate final value return ldexp(mantissa, exp); } int GameObject::getAnim(int animID) const { for (uint i = 0; i < _anim.size(); ++i) { if (_anim[i]->getID() == animID) { return i; } } return -1; } int GameObject::addAnim(Animation *anim) { anim->setZ(_z); _anim.push_back(anim); int index = _anim.size() - 1; if (_absNum == kDragonObject && index <= kLastTurning) { // Index to _anim is the Movement type. All walking and // turning movements can be accelerated. anim->supportsQuickAnimation(true); } return index; } void GameObject::playAnim(int i) { _anim[i]->play(); _playingAnim = i; } void GameObject::stopAnim() { if (_playingAnim >= 0) { _anim[_playingAnim]->stop(); _playingAnim = -1; } } void GameObject::deleteAnims() { deleteAnimsFrom(0); } void GameObject::deleteAnimsFrom(int index) { for (int j = _anim.size() - 1; j >= index; --j) { _anim.back()->del(); _anim.pop_back(); } if (_playingAnim >= index) { _playingAnim = -1; } } void GameObject::load(uint objNum, BArchive *archive) { const BAFile *file; file = archive->getFile(objNum * 3); Common::MemoryReadStream objReader(file->_data, file->_length); _init = objReader.readUint16LE(); _look = objReader.readUint16LE(); _use = objReader.readUint16LE(); _canUse = objReader.readUint16LE(); _imInit = objReader.readByte(); _imLook = objReader.readByte(); _imUse = objReader.readByte(); _walkDir = objReader.readByte() - 1; _z = objReader.readByte(); objReader.readUint16LE(); // idxSeq field, not used objReader.readUint16LE(); // numSeq field, not used _lookX = objReader.readUint16LE(); _lookY = objReader.readUint16LE(); _useX = objReader.readUint16LE(); _useY = objReader.readUint16LE(); _lookDir = static_cast (objReader.readByte()); _useDir = static_cast (objReader.readByte()); _absNum = objNum; file = archive->getFile(objNum * 3 + 1); // The first byte of the file is the length of the string (without the length) assert(file->_length - 1 == file->_data[0]); _title = Common::String((char *)(file->_data+1), file->_length-1); file = archive->getFile(objNum * 3 + 2); _program._bytecode = file->_data; _program._length = file->_length; _playingAnim = -1; deleteAnims(); // If the object has already been loaded, then discard the previous animations } void GameItem::load(int itemID, BArchive *archive) { const BAFile *f = archive->getFile(itemID * 3); Common::MemoryReadStream itemReader(f->_data, f->_length); _init = itemReader.readSint16LE(); _look = itemReader.readSint16LE(); _use = itemReader.readSint16LE(); _canUse = itemReader.readSint16LE(); _imInit = itemReader.readByte(); _imLook = itemReader.readByte(); _imUse = itemReader.readByte(); _absNum = itemID; f = archive->getFile(itemID * 3 + 1); // The first byte is the length of the string _title = Common::String((const char *)f->_data + 1, f->_length - 1); assert(f->_data[0] == _title.size()); f = archive->getFile(itemID * 3 + 2); _program._bytecode = f->_data; _program._length = f->_length; _anim = NULL; } void Room::load(int roomNum, BArchive *archive) { const BAFile *f; f = archive->getFile(roomNum * 4); Common::MemoryReadStream roomReader(f->_data, f->_length); roomReader.readUint32LE(); // Pointer to room program, not used roomReader.readUint16LE(); // Program length, not used roomReader.readUint32LE(); // Pointer to room title, not used // Set the current room to the new value _roomNum = roomNum; // Music will be played by the GPL2 command startMusic when needed. _music = roomReader.readByte(); _mapID = roomReader.readByte() - 1; _palette = roomReader.readByte() - 1; _numOverlays = roomReader.readSint16LE(); _init = roomReader.readSint16LE(); _look = roomReader.readSint16LE(); _use = roomReader.readSint16LE(); _canUse = roomReader.readSint16LE(); _imInit = roomReader.readByte(); _imLook = roomReader.readByte(); _imUse = roomReader.readByte(); _mouseOn = roomReader.readByte(); _heroOn = roomReader.readByte(); // Read in pers0 and persStep (stored as 6-byte Pascal reals) byte real[6]; for (int i = 5; i >= 0; --i) { real[i] = roomReader.readByte(); } _pers0 = real_to_double(real); for (int i = 5; i >= 0; --i) { real[i] = roomReader.readByte(); } _persStep = real_to_double(real); _escRoom = roomReader.readByte() - 1; _numGates = roomReader.readByte(); debugC(4, kDraciLogicDebugLevel, "Music: %d", _music); debugC(4, kDraciLogicDebugLevel, "Map: %d", _mapID); debugC(4, kDraciLogicDebugLevel, "Palette: %d", _palette); debugC(4, kDraciLogicDebugLevel, "Overlays: %d", _numOverlays); debugC(4, kDraciLogicDebugLevel, "Init: %d", _init); debugC(4, kDraciLogicDebugLevel, "Look: %d", _look); debugC(4, kDraciLogicDebugLevel, "Use: %d", _use); debugC(4, kDraciLogicDebugLevel, "CanUse: %d", _canUse); debugC(4, kDraciLogicDebugLevel, "ImInit: %d", _imInit); debugC(4, kDraciLogicDebugLevel, "ImLook: %d", _imLook); debugC(4, kDraciLogicDebugLevel, "ImUse: %d", _imUse); debugC(4, kDraciLogicDebugLevel, "MouseOn: %d", _mouseOn); debugC(4, kDraciLogicDebugLevel, "HeroOn: %d", _heroOn); debugC(4, kDraciLogicDebugLevel, "Pers0: %f", _pers0); debugC(4, kDraciLogicDebugLevel, "PersStep: %f", _persStep); debugC(4, kDraciLogicDebugLevel, "EscRoom: %d", _escRoom); debugC(4, kDraciLogicDebugLevel, "Gates: %d", _numGates); // Read in the gates' numbers _gates.clear(); for (uint i = 0; i < _numGates; ++i) { _gates.push_back(roomReader.readSint16LE()); } // Load the room's GPL program f = archive->getFile(roomNum * 4 + 3); _program._bytecode = f->_data; _program._length = f->_length; } } // End of namespace Draci