/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * $URL$ * $Id$ * */ #include "common/stream.h" #include "draci/draci.h" #include "draci/game.h" #include "draci/barchive.h" #include "draci/script.h" #include "draci/animation.h" #include namespace Draci { static double real_to_double(byte real[6]); Game::Game(DraciEngine *vm) : _vm(vm) { unsigned int i; BArchive *initArchive = _vm->_initArchive; BAFile *file; // Read in persons file = initArchive->getFile(5); Common::MemoryReadStream personData(file->_data, file->_length); unsigned int numPersons = file->_length / personSize; _persons = new Person[numPersons]; for (i = 0; i < numPersons; ++i) { _persons[i]._x = personData.readUint16LE(); _persons[i]._y = personData.readUint16LE(); _persons[i]._fontColour = personData.readByte(); } // Close persons file file->close(); // Read in dialog offsets file = initArchive->getFile(4); Common::MemoryReadStream dialogData(file->_data, file->_length); unsigned int numDialogs = file->_length / sizeof(uint16); _dialogOffsets = new uint[numDialogs]; unsigned int curOffset; for (i = 0, curOffset = 0; i < numDialogs; ++i) { _dialogOffsets[i] = curOffset; curOffset += dialogData.readUint16LE(); } // Close dialogs file file->close(); // 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._numIcons = gameData.readUint16LE(); _info._numVariables = gameData.readByte(); _info._numPersons = gameData.readByte(); _info._numDialogs = gameData.readByte(); _info._maxIconWidth = gameData.readUint16LE(); _info._maxIconHeight = 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._numDialogBlocks = curOffset; // Close game info file file->close(); // Read in variables file = initArchive->getFile(2); unsigned int numVariables = file->_length / sizeof (int16); _variables = new int[numVariables]; Common::MemoryReadStream variableData(file->_data, file->_length); for (i = 0; i < numVariables; ++i) { _variables[i] = variableData.readUint16LE(); } // Close variables file file->close(); // Read in item icon status file = initArchive->getFile(1); _iconStatus = file->_data; uint numIcons = file->_length; // Read in object status file = initArchive->getFile(0); unsigned int numObjects = file->_length; _objects = new GameObject[numObjects]; Common::MemoryReadStream objStatus(file->_data, file->_length); for (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; } // Close object status file file->close(); assert(numDialogs == _info._numDialogs); assert(numPersons == _info._numPersons); assert(numVariables == _info._numVariables); assert(numObjects == _info._numObjects); assert(numIcons == _info._numIcons); } void Game::start() { while (!shouldQuit()) { // If the scheduled room differs from the current one, do a room change if (_newRoom != _currentRoom._roomNum) { // Set the first two variables to the new room / gate _variables[0] = _newGate; _variables[1] = _newRoom; // 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; } setLoopSubstatus(kStatusOrdinary); // Do the actual change changeRoom(_newRoom); // Set the current room / gate to the new value _currentRoom._roomNum = _newRoom; _currentGate = _newGate; // HACK: Won't be needed once I've implemented the loop properly _roomChange = false; // Run the program for the gate the dragon came through runGateProgram(_newGate); } // Mimic the original engine by setting the loop status to Ordinary before // entering the main loop setLoopStatus(kStatusOrdinary); loop(); } } void Game::init() { _shouldQuit = false; _shouldExitLoop = false; // HACK: Won't be needed once I've implemented the loop properly _roomChange = false; _loopStatus = kStatusOrdinary; _objUnderCursor = kOverlayImage; // Initialize animation for object / room titles Animation *titleAnim = _vm->_anims->addText(kTitleText, true); Text *title = new Text("", _vm->_bigFont, kFontColour3, 0, 0); titleAnim->addFrame(title); // Initialize animation for speech text Animation *speechAnim = _vm->_anims->addText(kSpeechText, true); Text *speech = new Text("", _vm->_bigFont, kFontColour1, 0, 0); speechAnim->addFrame(speech); loadObject(kDragonObject); GameObject *dragon = getObject(kDragonObject); debugC(4, kDraciLogicDebugLevel, "Running init program for the dragon object..."); _vm->_script->run(dragon->_program, dragon->_init); _currentRoom._roomNum = _info._startRoom; _currentGate = 0; _newRoom = _currentRoom._roomNum; _newGate = _currentGate; _variables[0] = _currentGate; _variables[1] = _currentRoom._roomNum; changeRoom(_currentRoom._roomNum); runGateProgram(_currentGate); } void Game::loop() { do { _vm->handleEvents(); if (_currentRoom._mouseOn) { int x = _vm->_mouse->getPosX(); int y = _vm->_mouse->getPosY(); if (_vm->_mouse->lButtonPressed() && _currentRoom._walkingMap.isWalkable(x, y)) { walkHero(x, y); } int animUnderCursor = _vm->_anims->getTopAnimationID(x, y); //Animation *anim = _vm->_anims->getAnimation(animUnderCursor); int curObject = getObjectWithAnimation(animUnderCursor); GameObject *obj = &_objects[curObject]; Animation *titleAnim = _vm->_anims->getAnimation(kTitleText); // TODO: Handle displaying title in the proper location if (curObject != kNotFound) { titleAnim->markDirtyRect(_vm->_screen->getSurface()); reinterpret_cast(titleAnim->getFrame())->setText(obj->_title); // HACK: Test running look and use scripts if (_vm->_mouse->lButtonPressed()) { _vm->_mouse->lButtonSet(false); _vm->_script->run(obj->_program, obj->_look); } if (_vm->_mouse->rButtonPressed()) { _vm->_mouse->rButtonSet(false); _vm->_script->run(obj->_program, obj->_use); } } else { titleAnim->markDirtyRect(_vm->_screen->getSurface()); reinterpret_cast(titleAnim->getFrame())->setText(""); } debugC(2, kDraciAnimationDebugLevel, "Anim under cursor: %d", animUnderCursor); } if (_loopSubstatus == kStatusTalk) { Animation *speechAnim = _vm->_anims->getAnimation(kSpeechText); Text *speechFrame = reinterpret_cast(speechAnim->getFrame()); uint speechDuration = kBaseSpeechDuration + speechFrame->getLength() * kSpeechTimeUnit / (128 / 16 + 1); if ((_vm->_system->getMillis() - _speechTick) >= speechDuration) { _shouldExitLoop = true; } else { _shouldExitLoop = false; } } if (shouldQuit()) return; _vm->_anims->drawScene(_vm->_screen->getSurface()); _vm->_screen->copyToScreen(); _vm->_system->delayMillis(20); // HACK: Won't be needed once the game loop is implemented properly _shouldExitLoop = _shouldExitLoop || _roomChange; } while (!shouldExitLoop()); } int Game::getObjectWithAnimation(int animID) { for (uint i = 0; i < _info._numObjects; ++i) { GameObject *obj = &_objects[i]; for (uint j = 0; j < obj->_anims.size(); ++j) { if (obj->_anims[j] == animID) { return i; } } } return kNotFound; } void Game::walkHero(int x, int y) { // Fetch dragon's animation ID // FIXME: Need to add proper walking (this only warps the dragon to position) int animID = getObject(kDragonObject)->_anims[0]; Animation *anim = _vm->_anims->getAnimation(animID); // Calculate scaling factors double scaleX = _currentRoom._pers0 + _currentRoom._persStep * y; double scaleY = scaleX; // Set the Z coordinate for the dragon's animation anim->setZ(y+1); // Fetch current frame Drawable *frame = anim->getFrame(); // Fetch base height of the frame uint height = frame->getHeight(); _persons[kDragonObject]._x = x; _persons[kDragonObject]._y = y - lround(scaleY) * height; // 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 y -= (int)(scaleY * height); anim->setRelative(x, y); // Set the per-animation scaling factor anim->setScaleFactors(scaleX, scaleY); // Play the animation _vm->_anims->play(animID); debugC(4, kDraciLogicDebugLevel, "Walk to x: %d y: %d", x, y); } void Game::loadRoom(int roomNum) { BAFile *f; f = _vm->_roomsArchive->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 _currentRoom._music = roomReader.readByte(); int mapIdx = roomReader.readByte() - 1; f = _vm->_walkingMapsArchive->getFile(mapIdx); _currentRoom._walkingMap.load(f->_data, f->_length); _currentRoom._palette = roomReader.readByte() - 1; _currentRoom._numOverlays = roomReader.readSint16LE(); _currentRoom._init = roomReader.readSint16LE(); _currentRoom._look = roomReader.readSint16LE(); _currentRoom._use = roomReader.readSint16LE(); _currentRoom._canUse = roomReader.readSint16LE(); _currentRoom._imInit = roomReader.readByte(); _currentRoom._imLook = roomReader.readByte(); _currentRoom._imUse = roomReader.readByte(); _currentRoom._mouseOn = roomReader.readByte(); _currentRoom._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(); } _currentRoom._pers0 = real_to_double(real); for (int i = 5; i >= 0; --i) { real[i] = roomReader.readByte(); } _currentRoom._persStep = real_to_double(real); _currentRoom._escRoom = roomReader.readByte() - 1; _currentRoom._numGates = roomReader.readByte(); debugC(4, kDraciLogicDebugLevel, "Music: %d", _currentRoom._music); debugC(4, kDraciLogicDebugLevel, "Map: %d", mapIdx); debugC(4, kDraciLogicDebugLevel, "Palette: %d", _currentRoom._palette); debugC(4, kDraciLogicDebugLevel, "Overlays: %d", _currentRoom._numOverlays); debugC(4, kDraciLogicDebugLevel, "Init: %d", _currentRoom._init); debugC(4, kDraciLogicDebugLevel, "Look: %d", _currentRoom._look); debugC(4, kDraciLogicDebugLevel, "Use: %d", _currentRoom._use); debugC(4, kDraciLogicDebugLevel, "CanUse: %d", _currentRoom._canUse); debugC(4, kDraciLogicDebugLevel, "ImInit: %d", _currentRoom._imInit); debugC(4, kDraciLogicDebugLevel, "ImLook: %d", _currentRoom._imLook); debugC(4, kDraciLogicDebugLevel, "ImUse: %d", _currentRoom._imUse); debugC(4, kDraciLogicDebugLevel, "MouseOn: %d", _currentRoom._mouseOn); debugC(4, kDraciLogicDebugLevel, "HeroOn: %d", _currentRoom._heroOn); debugC(4, kDraciLogicDebugLevel, "Pers0: %f", _currentRoom._pers0); debugC(4, kDraciLogicDebugLevel, "PersStep: %f", _currentRoom._persStep); debugC(4, kDraciLogicDebugLevel, "EscRoom: %d", _currentRoom._escRoom); debugC(4, kDraciLogicDebugLevel, "Gates: %d", _currentRoom._numGates); // Read in the gates' numbers _currentRoom._gates.clear(); for (uint i = 0; i < _currentRoom._numGates; ++i) { _currentRoom._gates.push_back(roomReader.readSint16LE()); } // 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, roomNum); if (_objects[i]._location == roomNum) { debugC(6, kDraciLogicDebugLevel, "Loading object %d from room %d", i, roomNum); loadObject(i); } } // 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 == roomNum) { debugC(6, kDraciLogicDebugLevel, "Running init program for object %d (offset %d)", i, getObject(i)->_init); _vm->_script->run(getObject(i)->_program, getObject(i)->_init); } } // Load the room's GPL program and run the init part f = _vm->_roomsArchive->getFile(roomNum * 4 + 3); _currentRoom._program._bytecode = f->_data; _currentRoom._program._length = f->_length; debugC(4, kDraciLogicDebugLevel, "Running room init program..."); _vm->_script->run(_currentRoom._program, _currentRoom._init); // Set room palette f = _vm->_paletteArchive->getFile(_currentRoom._palette); _vm->_screen->setPalette(f->_data, 0, kNumColours); // 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(); } else { debugC(6, kDraciLogicDebugLevel, "Mouse: OFF"); _vm->_mouse->cursorOff(); } _vm->_mouse->setCursorType(kNormalCursor); // HACK: Create a visible overlay from the walking map so we can test it byte *wlk = new byte[kScreenWidth * kScreenHeight]; memset(wlk, 255, kScreenWidth * kScreenHeight); for (uint i = 0; i < kScreenWidth; ++i) { for (uint j = 0; j < kScreenHeight; ++j) { if (_currentRoom._walkingMap.isWalkable(i, j)) { wlk[j * kScreenWidth + i] = 2; } } } Sprite *ov = new Sprite(wlk, kScreenWidth, kScreenHeight, 0, 0, false); Animation *map = _vm->_anims->addAnimation(kWalkingMapOverlay, 255, false); map->addFrame(ov); } int Game::loadAnimation(uint animNum, uint z) { BAFile *animFile = _vm->_animationsArchive->getFile(animNum); Common::MemoryReadStream animationReader(animFile->_data, animFile->_length); uint numFrames = animationReader.readByte(); // FIXME: handle these properly animationReader.readByte(); // Memory logic field, not used animationReader.readByte(); // Disable erasing field, not used bool cyclic = animationReader.readByte(); animationReader.readByte(); // Relative field, not used Animation *anim = _vm->_anims->addAnimation(animNum, z, false); anim->setLooping(cyclic); for (uint i = 0; i < numFrames; ++i) { uint spriteNum = animationReader.readUint16LE() - 1; int x = animationReader.readSint16LE(); int y = animationReader.readSint16LE(); uint scaledWidth = animationReader.readUint16LE(); uint scaledHeight = animationReader.readUint16LE(); byte mirror = animationReader.readByte(); /* uint sample = */ animationReader.readUint16LE(); /* uint freq = */ animationReader.readUint16LE(); uint delay = animationReader.readUint16LE(); BAFile *spriteFile = _vm->_spritesArchive->getFile(spriteNum); Sprite *sp = new Sprite(spriteFile->_data, spriteFile->_length, x, y, true); // Some frames set the scaled dimensions to 0 even though other frames // from the same animations have them set to normal values // We work around this by assuming it means no scaling is necessary if (scaledWidth == 0) { scaledWidth = sp->getWidth(); } if (scaledHeight == 0) { scaledHeight = sp->getHeight(); } sp->setScaled(scaledWidth, scaledHeight); if (mirror) sp->setMirrorOn(); sp->setDelay(delay * 10); anim->addFrame(sp); } return animNum; } void Game::loadObject(uint objNum) { BAFile *file; file = _vm->_objectsArchive->getFile(objNum * 3); Common::MemoryReadStream objReader(file->_data, file->_length); GameObject *obj = _objects + objNum; obj->_init = objReader.readUint16LE(); obj->_look = objReader.readUint16LE(); obj->_use = objReader.readUint16LE(); obj->_canUse = objReader.readUint16LE(); obj->_imInit = objReader.readByte(); obj->_imLook = objReader.readByte(); obj->_imUse = objReader.readByte(); obj->_walkDir = objReader.readByte(); obj->_z = objReader.readByte(); objReader.readUint16LE(); // idxSeq field, not used objReader.readUint16LE(); // numSeq field, not used obj->_lookX = objReader.readUint16LE(); obj->_lookY = objReader.readUint16LE(); obj->_useX = objReader.readUint16LE(); obj->_useY = objReader.readUint16LE(); obj->_lookDir = objReader.readByte(); obj->_useDir = objReader.readByte(); obj->_absNum = objNum; file = _vm->_objectsArchive->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]); obj->_title = Common::String((char *)(file->_data+1), file->_length-1); file = _vm->_objectsArchive->getFile(objNum * 3 + 2); obj->_program._bytecode = file->_data; obj->_program._length = file->_length; } GameObject *Game::getObject(uint objNum) { return _objects + objNum; } uint Game::getNumObjects() { return _info._numObjects; } void Game::loadOverlays() { uint x, y, z, num; BAFile *overlayHeader; overlayHeader = _vm->_roomsArchive->getFile(_currentRoom._roomNum * 4 + 2); Common::MemoryReadStream overlayReader(overlayHeader->_data, overlayHeader->_length); BAFile *overlayFile; for (int i = 0; i < _currentRoom._numOverlays; i++) { num = overlayReader.readUint16LE() - 1; x = overlayReader.readUint16LE(); y = overlayReader.readUint16LE(); z = overlayReader.readByte(); overlayFile = _vm->_overlaysArchive->getFile(num); Sprite *sp = new Sprite(overlayFile->_data, overlayFile->_length, x, y, true); _vm->_anims->addOverlay(sp, z); } _vm->_overlaysArchive->clearCache(); _vm->_screen->getSurface()->markDirty(); } void Game::changeRoom(uint roomNum) { debugC(1, kDraciLogicDebugLevel, "Changing to room %d", roomNum); // Clear archives _vm->_roomsArchive->clearCache(); _vm->_spritesArchive->clearCache(); _vm->_paletteArchive->clearCache(); _vm->_animationsArchive->clearCache(); _vm->_walkingMapsArchive->clearCache(); _vm->_screen->clearScreen(); _vm->_anims->deleteOverlays(); // Delete walking map testing overlay _vm->_anims->deleteAnimation(kWalkingMapOverlay); int oldRoomNum = _currentRoom._roomNum; // TODO: Make objects capable of stopping their own animations GameObject *dragon = getObject(kDragonObject); for (uint i = 0; i < dragon->_anims.size(); ++i) { _vm->_anims->stop(dragon->_anims[i]); } for (uint i = 0; i < _info._numObjects; ++i) { GameObject *obj = &_objects[i]; if (i != 0 && (obj->_location == oldRoomNum)) { for (uint j = 0; j < obj->_anims.size(); ++j) { _vm->_anims->deleteAnimation(obj->_anims[j]); } obj->_anims.clear(); } } _currentRoom._roomNum = roomNum; loadRoom(roomNum); loadOverlays(); } void Game::runGateProgram(int gate) { debugC(6, kDraciLogicDebugLevel, "Running program for gate %d", gate); // Set the appropriate loop statu before executing the gate program setLoopStatus(kStatusGate); // Mark last animation int lastAnimIndex = _vm->_anims->getLastIndex(); // Run gate program _vm->_script->run(_currentRoom._program, _currentRoom._gates[gate]); // 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 (uint j = 0; j < obj->_anims.size(); ++j) { Animation *anim; anim = _vm->_anims->getAnimation(obj->_anims[j]); if (anim != NULL && anim->getIndex() > lastAnimIndex) obj->_anims.remove_at(j); } } _vm->_anims->deleteAfterIndex(lastAnimIndex); setExitLoop(false); } int Game::getRoomNum() { return _currentRoom._roomNum; } void Game::setRoomNum(int room) { _newRoom = room; } int Game::getGateNum() { return _currentGate; } void Game::setGateNum(int gate) { _newGate = gate; } void Game::setLoopStatus(LoopStatus status) { _loopStatus = status; } void Game::setLoopSubstatus(LoopStatus status) { _loopSubstatus = status; } LoopStatus Game::getLoopStatus() { return _loopStatus; } LoopStatus Game::getLoopSubstatus() { return _loopSubstatus; } int Game::getVariable(int numVar) { return _variables[numVar]; } void Game::setVariable(int numVar, int value) { _variables[numVar] = value; } int Game::getIconStatus(int iconID) { return _iconStatus[iconID]; } Person *Game::getPerson(int personID) { return &_persons[personID]; } void Game::setSpeechTick(uint tick) { _speechTick = tick; } /** * The GPL command Mark sets the animation index (which specifies the order in which * animations were loaded in) which is then used by the Release command to delete * all animations that have an index greater than the one marked. */ int Game::getMarkedAnimationIndex() { return _markedAnimationIndex; } /** * See Game::getMarkedAnimationIndex(). */ void Game::setMarkedAnimationIndex(int index) { _markedAnimationIndex = index; } Game::~Game() { delete[] _persons; delete[] _variables; delete[] _dialogOffsets; delete[] _objects; } bool WalkingMap::isWalkable(int x, int y) { // Convert to map pixels x = x / _deltaX; y = y / _deltaY; int pixelIndex = _mapWidth * y + x; int byteIndex = pixelIndex / 8; int mapByte = _data[byteIndex]; return mapByte & (1 << pixelIndex % 8); } 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); } }