scummvm/engines/mohawk/riven.cpp
2010-12-21 18:16:20 +00:00

717 lines
20 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*
*/
#include "common/config-manager.h"
#include "common/events.h"
#include "common/EventRecorder.h"
#include "common/keyboard.h"
#include "common/translation.h"
#include "mohawk/cursors.h"
#include "mohawk/graphics.h"
#include "mohawk/resource.h"
#include "mohawk/riven.h"
#include "mohawk/riven_external.h"
#include "mohawk/riven_saveload.h"
#include "mohawk/dialogs.h"
#include "mohawk/sound.h"
#include "mohawk/video.h"
#include "mohawk/console.h"
namespace Mohawk {
Common::Rect *g_atrusJournalRect1;
Common::Rect *g_atrusJournalRect2;
Common::Rect *g_cathJournalRect2;
Common::Rect *g_atrusJournalRect3;
Common::Rect *g_cathJournalRect3;
Common::Rect *g_trapBookRect3;
Common::Rect *g_demoExitRect;
MohawkEngine_Riven::MohawkEngine_Riven(OSystem *syst, const MohawkGameDescription *gamedesc) : MohawkEngine(syst, gamedesc) {
_showHotspots = false;
_cardData.hasData = false;
_gameOver = false;
_activatedSLST = false;
_ignoreNextMouseUp = false;
_extrasFile = NULL;
_curStack = aspit;
_hotspots = NULL;
// NOTE: We can never really support CD swapping. All of the music files
// (*_Sounds.mhk) are stored on disc 1. They are copied to the hard drive
// during install and used from there. The same goes for the extras.mhk
// file. The following directories allow Riven to be played directly
// from the DVD.
const Common::FSNode gameDataDir(ConfMan.get("path"));
SearchMan.addSubDirectoryMatching(gameDataDir, "all");
SearchMan.addSubDirectoryMatching(gameDataDir, "data");
SearchMan.addSubDirectoryMatching(gameDataDir, "exe");
SearchMan.addSubDirectoryMatching(gameDataDir, "assets1");
g_atrusJournalRect1 = new Common::Rect(295, 402, 313, 426);
g_atrusJournalRect2 = new Common::Rect(259, 402, 278, 426);
g_cathJournalRect2 = new Common::Rect(328, 408, 348, 419);
g_atrusJournalRect3 = new Common::Rect(222, 402, 240, 426);
g_cathJournalRect3 = new Common::Rect(291, 408, 311, 419);
g_trapBookRect3 = new Common::Rect(363, 396, 386, 432);
g_demoExitRect = new Common::Rect(291, 408, 317, 419);
}
MohawkEngine_Riven::~MohawkEngine_Riven() {
delete _gfx;
delete _console;
delete _externalScriptHandler;
delete _extrasFile;
delete _saveLoad;
delete _scriptMan;
delete[] _vars;
delete _optionsDialog;
delete _rnd;
delete[] _hotspots;
delete g_atrusJournalRect1;
delete g_atrusJournalRect2;
delete g_cathJournalRect2;
delete g_atrusJournalRect3;
delete g_cathJournalRect3;
delete g_trapBookRect3;
delete g_demoExitRect;
}
GUI::Debugger *MohawkEngine_Riven::getDebugger() {
return _console;
}
Common::Error MohawkEngine_Riven::run() {
MohawkEngine::run();
_gfx = new RivenGraphics(this);
_console = new RivenConsole(this);
_saveLoad = new RivenSaveLoad(this, _saveFileMan);
_externalScriptHandler = new RivenExternal(this);
_optionsDialog = new RivenOptionsDialog(this);
_scriptMan = new RivenScriptManager(this);
_cursor = new RivenCursorManager();
_rnd = new Common::RandomSource();
g_eventRec.registerRandomSource(*_rnd, "riven");
initVars();
// Open extras.mhk for common images
_extrasFile = new MohawkArchive();
if (!_extrasFile->open("extras.mhk"))
error("Could not open extras.mhk");
// Start at main cursor
_cursor->setCursor(kRivenMainCursor);
// Let's begin, shall we?
if (getFeatures() & GF_DEMO) {
// Start the demo off with the videos
changeToStack(aspit);
changeToCard(6);
} else if (ConfMan.hasKey("save_slot")) {
// Load game from launcher/command line if requested
uint32 gameToLoad = ConfMan.getInt("save_slot");
Common::StringArray savedGamesList = _saveLoad->generateSaveGameList();
if (gameToLoad > savedGamesList.size())
error ("Could not find saved game");
_saveLoad->loadGame(savedGamesList[gameToLoad]);
} else {
// Otherwise, start us off at aspit's card 1 (the main menu)
changeToStack(aspit);
changeToCard(1);
}
while (!_gameOver && !shouldQuit())
handleEvents();
return Common::kNoError;
}
void MohawkEngine_Riven::handleEvents() {
Common::Event event;
// Update background videos and the water effect
bool needsUpdate = _gfx->runScheduledWaterEffects();
needsUpdate |= _video->updateBackgroundMovies();
while (_eventMan->pollEvent(event)) {
switch (event.type) {
case Common::EVENT_MOUSEMOVE:
checkHotspotChange();
if (!(getFeatures() & GF_DEMO)) {
// Check to show the inventory, but it is always "showing" in the demo
if (_eventMan->getMousePos().y >= 392)
_gfx->showInventory();
else
_gfx->hideInventory();
}
needsUpdate = true;
break;
case Common::EVENT_LBUTTONDOWN:
if (_curHotspot >= 0)
runHotspotScript(_curHotspot, kMouseDownScript);
break;
case Common::EVENT_LBUTTONUP:
// See RivenScript::switchCard() for more information on why we sometimes
// disable the next up event.
if (!_ignoreNextMouseUp) {
if (_curHotspot >= 0)
runHotspotScript(_curHotspot, kMouseUpScript);
else
checkInventoryClick();
}
_ignoreNextMouseUp = false;
break;
case Common::EVENT_KEYDOWN:
switch (event.kbd.keycode) {
case Common::KEYCODE_d:
if (event.kbd.flags & Common::KBD_CTRL) {
_console->attach();
_console->onFrame();
}
break;
case Common::KEYCODE_SPACE:
pauseGame();
break;
case Common::KEYCODE_F4:
_showHotspots = !_showHotspots;
if (_showHotspots) {
for (uint16 i = 0; i < _hotspotCount; i++)
_gfx->drawRect(_hotspots[i].rect, _hotspots[i].enabled);
needsUpdate = true;
} else
refreshCard();
break;
case Common::KEYCODE_F5:
runDialog(*_optionsDialog);
updateZipMode();
break;
case Common::KEYCODE_r:
// Return to the main menu in the demo on ctrl+r
if (event.kbd.flags & Common::KBD_CTRL && getFeatures() & GF_DEMO) {
if (_curStack != aspit)
changeToStack(aspit);
changeToCard(1);
}
break;
case Common::KEYCODE_p:
// Play the intro videos in the demo on ctrl+p
if (event.kbd.flags & Common::KBD_CTRL && getFeatures() & GF_DEMO) {
if (_curStack != aspit)
changeToStack(aspit);
changeToCard(6);
}
break;
default:
break;
}
break;
default:
break;
}
}
if (_curHotspot >= 0)
runHotspotScript(_curHotspot, kMouseInsideScript);
// Update the screen if we need to
if (needsUpdate)
_system->updateScreen();
// Cut down on CPU usage
_system->delayMillis(10);
}
// Stack/Card-Related Functions
void MohawkEngine_Riven::changeToStack(uint16 n) {
// The endings are in reverse order because of the way the 1.02 patch works.
// The only "Data3" file is j_Data3.mhk from that patch. Patch files have higher
// priorities over the regular files and are therefore loaded and checked first.
static const char *endings[] = { "_Data3.mhk", "_Data2.mhk", "_Data1.mhk", "_Data.mhk", "_Sounds.mhk" };
// Don't change stack to the current stack (if the files are loaded)
if (_curStack == n && !_mhk.empty())
return;
_curStack = n;
// Stop any videos playing
_video->stopVideos();
_video->clearMLST();
// Clear the graphics cache; images aren't used across stack boundaries
_gfx->clearCache();
// Clear the old stack files out
for (uint32 i = 0; i < _mhk.size(); i++)
delete _mhk[i];
_mhk.clear();
// Get the prefix character for the destination stack
char prefix = getStackName(_curStack)[0];
// Load any file that fits the patterns
for (int i = 0; i < ARRAYSIZE(endings); i++) {
Common::String filename = Common::String(prefix) + endings[i];
MohawkArchive *mhk = new MohawkArchive();
if (mhk->open(filename))
_mhk.push_back(mhk);
else
delete mhk;
}
// Make sure we have loaded files
if (_mhk.empty())
error("Could not load stack %s", getStackName(_curStack).c_str());
// Stop any currently playing sounds
_sound->stopAllSLST();
}
// Riven uses some hacks to change stacks for linking books
// Otherwise, script command 27 changes stacks
struct RivenSpecialChange {
byte startStack;
uint32 startCardRMAP;
byte targetStack;
uint32 targetCardRMAP;
} rivenSpecialChange[] = {
{ aspit, 0x1f04, ospit, 0x44ad }, // Trap Book
{ bspit, 0x1c0e7, ospit, 0x2e76 }, // Dome Linking Book
{ gspit, 0x111b1, ospit, 0x2e76 }, // Dome Linking Book
{ jspit, 0x28a18, rspit, 0xf94 }, // Tay Linking Book
{ jspit, 0x26228, ospit, 0x2e76 }, // Dome Linking Book
{ ospit, 0x5f0d, pspit, 0x3bf0 }, // Return from 233rd Age
{ ospit, 0x470a, jspit, 0x1508e }, // Return from 233rd Age
{ ospit, 0x5c52, gspit, 0x10bea }, // Return from 233rd Age
{ ospit, 0x5d68, bspit, 0x1adfd }, // Return from 233rd Age
{ ospit, 0x5e49, tspit, 0xe87 }, // Return from 233rd Age
{ pspit, 0x4108, ospit, 0x2e76 }, // Dome Linking Book
{ rspit, 0x32d8, jspit, 0x1c474 }, // Return from Tay
{ tspit, 0x21b69, ospit, 0x2e76 } // Dome Linking Book
};
void MohawkEngine_Riven::changeToCard(uint16 dest) {
_curCard = dest;
debug (1, "Changing to card %d", _curCard);
// Clear the graphics cache (images typically aren't used
// on different cards).
_gfx->clearCache();
if (!(getFeatures() & GF_DEMO)) {
for (byte i = 0; i < 13; i++)
if (_curStack == rivenSpecialChange[i].startStack && _curCard == matchRMAPToCard(rivenSpecialChange[i].startCardRMAP)) {
changeToStack(rivenSpecialChange[i].targetStack);
_curCard = matchRMAPToCard(rivenSpecialChange[i].targetCardRMAP);
}
}
if (_cardData.hasData)
runCardScript(kCardLeaveScript);
loadCard(_curCard);
refreshCard(); // Handles hotspots and scripts
}
void MohawkEngine_Riven::refreshCard() {
loadHotspots(_curCard);
_gfx->_updatesEnabled = true;
_gfx->clearWaterEffects();
_gfx->_activatedPLSTs.clear();
_video->stopVideos();
_gfx->drawPLST(1);
_activatedSLST = false;
runCardScript(kCardLoadScript);
_gfx->updateScreen();
runCardScript(kCardOpenScript);
// Activate the first sound list if none have been activated
if (!_activatedSLST)
_sound->playSLST(1, _curCard);
if (_showHotspots) {
for (uint16 i = 0; i < _hotspotCount; i++)
_gfx->drawRect(_hotspots[i].rect, _hotspots[i].enabled);
}
// Now we need to redraw the cursor if necessary and handle mouse over scripts
_curHotspot = -1;
checkHotspotChange();
}
void MohawkEngine_Riven::loadCard(uint16 id) {
// NOTE: The card scripts are cleared by the RivenScriptManager automatically.
Common::SeekableReadStream* inStream = getResource(ID_CARD, id);
_cardData.name = inStream->readSint16BE();
_cardData.zipModePlace = inStream->readUint16BE();
_cardData.scripts = _scriptMan->readScripts(inStream);
_cardData.hasData = true;
delete inStream;
if (_cardData.zipModePlace) {
Common::String cardName = getName(CardNames, _cardData.name);
if (cardName.empty())
return;
ZipMode zip;
zip.name = cardName;
zip.id = id;
if (!(Common::find(_zipModeData.begin(), _zipModeData.end(), zip) != _zipModeData.end()))
_zipModeData.push_back(zip);
}
}
void MohawkEngine_Riven::loadHotspots(uint16 id) {
// Clear old hotspots
delete[] _hotspots;
// NOTE: The hotspot scripts are cleared by the RivenScriptManager automatically.
Common::SeekableReadStream *inStream = getResource(ID_HSPT, id);
_hotspotCount = inStream->readUint16BE();
_hotspots = new RivenHotspot[_hotspotCount];
for (uint16 i = 0; i < _hotspotCount; i++) {
_hotspots[i].enabled = true;
_hotspots[i].blstID = inStream->readUint16BE();
_hotspots[i].name_resource = inStream->readSint16BE();
int16 left = inStream->readSint16BE();
int16 top = inStream->readSint16BE();
int16 right = inStream->readSint16BE();
int16 bottom = inStream->readSint16BE();
// Riven has some invalid rects, disable them here
// Known weird hotspots:
// - tspit 371 (DVD: 377), hotspot 4
if (left >= right || top >= bottom) {
warning("%s %d hotspot %d is invalid: (%d, %d, %d, %d)", getStackName(_curStack).c_str(), _curCard, i, left, top, right, bottom);
left = top = right = bottom = 0;
_hotspots[i].enabled = 0;
}
_hotspots[i].rect = Common::Rect(left, top, right, bottom);
_hotspots[i].u0 = inStream->readUint16BE();
_hotspots[i].mouse_cursor = inStream->readUint16BE();
_hotspots[i].index = inStream->readUint16BE();
_hotspots[i].u1 = inStream->readSint16BE();
_hotspots[i].zipModeHotspot = inStream->readUint16BE();
// Read in the scripts now
_hotspots[i].scripts = _scriptMan->readScripts(inStream);
}
delete inStream;
updateZipMode();
}
void MohawkEngine_Riven::updateZipMode() {
// Check if a zip mode hotspot is enabled by checking the name/id against the ZIPS records.
for (uint32 i = 0; i < _hotspotCount; i++) {
if (_hotspots[i].zipModeHotspot) {
if (*getVar("azip") != 0) {
// Check if a zip mode hotspot is enabled by checking the name/id against the ZIPS records.
Common::String hotspotName = getName(HotspotNames, _hotspots[i].name_resource);
bool foundMatch = false;
if (!hotspotName.empty())
for (uint16 j = 0; j < _zipModeData.size(); j++)
if (_zipModeData[j].name == hotspotName) {
foundMatch = true;
break;
}
_hotspots[i].enabled = foundMatch;
} else // Disable the hotspot if zip mode is disabled
_hotspots[i].enabled = false;
}
}
}
void MohawkEngine_Riven::checkHotspotChange() {
uint16 hotspotIndex = 0;
bool foundHotspot = false;
for (uint16 i = 0; i < _hotspotCount; i++)
if (_hotspots[i].enabled && _hotspots[i].rect.contains(_eventMan->getMousePos())) {
foundHotspot = true;
hotspotIndex = i;
}
if (foundHotspot) {
if (_curHotspot != hotspotIndex) {
_curHotspot = hotspotIndex;
_cursor->setCursor(_hotspots[_curHotspot].mouse_cursor);
}
} else {
_curHotspot = -1;
_cursor->setCursor(kRivenMainCursor);
}
}
Common::String MohawkEngine_Riven::getHotspotName(uint16 hotspot) {
assert(hotspot < _hotspotCount);
if (_hotspots[hotspot].name_resource < 0)
return Common::String();
return getName(HotspotNames, _hotspots[hotspot].name_resource);
}
void MohawkEngine_Riven::checkInventoryClick() {
Common::Point mousePos = _eventMan->getMousePos();
// Don't even bother. We're not in the inventory portion of the screen.
if (mousePos.y < 392)
return;
// In the demo, check if we've clicked the exit button
if (getFeatures() & GF_DEMO) {
if (g_demoExitRect->contains(mousePos)) {
if (_curStack == aspit && _curCard == 1) {
// From the main menu, go to the "quit" screen
changeToCard(12);
} else if (_curStack == aspit && _curCard == 12) {
// From the "quit" screen, just quit
_gameOver = true;
} else {
// Otherwise, return to the main menu
if (_curStack != aspit)
changeToStack(aspit);
changeToCard(1);
}
}
return;
}
// No inventory shown on aspit
if (_curStack == aspit)
return;
// Set the return stack/card id's.
*getVar("returnstackid") = _curStack;
*getVar("returncardid") = _curCard;
// See RivenGraphics::showInventory() for an explanation
// of the variables' meanings.
bool hasCathBook = *getVar("acathbook") != 0;
bool hasTrapBook = *getVar("atrapbook") != 0;
// Go to the book if a hotspot contains the mouse
if (!hasCathBook) {
if (g_atrusJournalRect1->contains(mousePos)) {
_gfx->hideInventory();
changeToStack(aspit);
changeToCard(5);
}
} else if (!hasTrapBook) {
if (g_atrusJournalRect2->contains(mousePos)) {
_gfx->hideInventory();
changeToStack(aspit);
changeToCard(5);
} else if (g_cathJournalRect2->contains(mousePos)) {
_gfx->hideInventory();
changeToStack(aspit);
changeToCard(6);
}
} else {
if (g_atrusJournalRect3->contains(mousePos)) {
_gfx->hideInventory();
changeToStack(aspit);
changeToCard(5);
} else if (g_cathJournalRect3->contains(mousePos)) {
_gfx->hideInventory();
changeToStack(aspit);
changeToCard(6);
} else if (g_trapBookRect3->contains(mousePos)) {
_gfx->hideInventory();
changeToStack(aspit);
changeToCard(7);
}
}
}
Common::SeekableReadStream *MohawkEngine_Riven::getExtrasResource(uint32 tag, uint16 id) {
return _extrasFile->getResource(tag, id);
}
Common::String MohawkEngine_Riven::getName(uint16 nameResource, uint16 nameID) {
Common::SeekableReadStream* nameStream = getResource(ID_NAME, nameResource);
uint16 fieldCount = nameStream->readUint16BE();
uint16* stringOffsets = new uint16[fieldCount];
Common::String name;
char c;
if (nameID < fieldCount) {
for (uint16 i = 0; i < fieldCount; i++)
stringOffsets[i] = nameStream->readUint16BE();
for (uint16 i = 0; i < fieldCount; i++)
nameStream->readUint16BE(); // Skip unknown values
nameStream->seek(stringOffsets[nameID], SEEK_CUR);
c = (char)nameStream->readByte();
while (c) {
name += c;
c = (char)nameStream->readByte();
}
}
delete nameStream;
delete [] stringOffsets;
return name;
}
uint16 MohawkEngine_Riven::matchRMAPToCard(uint32 rmapCode) {
uint16 index = 0;
Common::SeekableReadStream *rmapStream = getResource(ID_RMAP, 1);
for (uint16 i = 1; rmapStream->pos() < rmapStream->size(); i++) {
uint32 code = rmapStream->readUint32BE();
if (code == rmapCode)
index = i;
}
delete rmapStream;
if (!index)
error ("Could not match RMAP code %08x", rmapCode);
return index - 1;
}
uint32 MohawkEngine_Riven::getCurCardRMAP() {
Common::SeekableReadStream *rmapStream = getResource(ID_RMAP, 1);
rmapStream->seek(_curCard * 4);
uint32 rmapCode = rmapStream->readUint32BE();
delete rmapStream;
return rmapCode;
}
void MohawkEngine_Riven::runCardScript(uint16 scriptType) {
assert(_cardData.hasData);
for (uint16 i = 0; i < _cardData.scripts.size(); i++)
if (_cardData.scripts[i]->getScriptType() == scriptType) {
_cardData.scripts[i]->runScript();
break;
}
}
void MohawkEngine_Riven::runHotspotScript(uint16 hotspot, uint16 scriptType) {
assert(hotspot < _hotspotCount);
for (uint16 i = 0; i < _hotspots[hotspot].scripts.size(); i++)
if (_hotspots[hotspot].scripts[i]->getScriptType() == scriptType) {
_hotspots[hotspot].scripts[i]->runScript();
break;
}
}
void MohawkEngine_Riven::delayAndUpdate(uint32 ms) {
uint32 startTime = _system->getMillis();
while (_system->getMillis() < startTime + ms && !shouldQuit()) {
bool needsUpdate = _gfx->runScheduledWaterEffects();
needsUpdate |= _video->updateBackgroundMovies();
Common::Event event;
while (_system->getEventManager()->pollEvent(event))
;
if (needsUpdate)
_system->updateScreen();
_system->delayMillis(10); // Ease off the CPU
}
}
void MohawkEngine_Riven::runLoadDialog() {
GUI::SaveLoadChooser slc(_("Load game:"), _("Load"));
slc.setSaveMode(false);
Common::String gameId = ConfMan.get("gameid");
const EnginePlugin *plugin = 0;
EngineMan.findGame(gameId, &plugin);
int slot = slc.runModal(plugin, ConfMan.getActiveDomainName());
if (slot >= 0)
loadGameState(slot);
slc.close();
}
Common::Error MohawkEngine_Riven::loadGameState(int slot) {
return _saveLoad->loadGame(_saveLoad->generateSaveGameList()[slot]) ? Common::kNoError : Common::kUnknownError;
}
Common::Error MohawkEngine_Riven::saveGameState(int slot, const char *desc) {
Common::StringArray saveList = _saveLoad->generateSaveGameList();
if ((uint)slot < saveList.size())
_saveLoad->deleteSave(saveList[slot]);
return _saveLoad->saveGame(Common::String(desc)) ? Common::kNoError : Common::kUnknownError;
}
Common::String MohawkEngine_Riven::getStackName(uint16 stack) const {
static const char *rivenStackNames[] = {
"aspit",
"bspit",
"gspit",
"jspit",
"ospit",
"pspit",
"rspit",
"tspit"
};
return rivenStackNames[stack];
}
bool ZipMode::operator== (const ZipMode &z) const {
return z.name == name && z.id == id;
}
} // End of namespace Mohawk