/* 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/system.h" #include "common/debug.h" #include "common/error.h" #include "common/file.h" #include "common/stream.h" #include "adl/adl_v4.h" #include "adl/detection.h" #include "adl/display.h" #include "adl/graphics.h" #include "adl/disk.h" #include "adl/sound.h" namespace Adl { class HiRes5Engine : public AdlEngine_v4 { public: HiRes5Engine(OSystem *syst, const AdlGameDescription *gd) : AdlEngine_v4(syst, gd), _doAnimation(false) { } private: // AdlEngine void setupOpcodeTables(); void runIntro(); void init(); void initGameState(); void applyRegionWorkarounds(); void applyRoomWorkarounds(byte roomNr); Common::String getLine(); // AdlEngine_v4 bool isInventoryFull(); void loadSong(Common::ReadStream &stream); void drawLight(uint index, byte color) const; void animateLights() const; int o_checkItemTimeLimits(ScriptEnv &e); int o_startAnimation(ScriptEnv &e); int o_winGame(ScriptEnv &e); static const uint kClock = 1022727; // Apple II CPU clock rate static const uint kRegions = 41; static const uint kItems = 69; Common::Array _itemTimeLimits; Common::String _itemTimeLimitMsg; Tones _song; bool _doAnimation; struct { Common::String itemTimeLimit; Common::String carryingTooMuch; } _gameStrings; }; Common::String HiRes5Engine::getLine() { if (_doAnimation) { animateLights(); _doAnimation = false; } return AdlEngine_v4::getLine(); } void HiRes5Engine::drawLight(uint index, byte color) const { const byte xCoord[5] = { 189, 161, 133, 105, 77 }; const byte yCoord = 72; assert(index < 5); for (int yDelta = 0; yDelta < 4; ++yDelta) for (int xDelta = 0; xDelta < 7; ++xDelta) _display->putPixel(Common::Point(xCoord[index] + xDelta, yCoord + yDelta), color); _display->updateHiResScreen(); } void HiRes5Engine::animateLights() const { int index; byte color = 0x2a; for (index = 4; index >= 0; --index) drawLight(index, color); index = 4; while (!g_engine->shouldQuit()) { drawLight(index, color ^ 0x7f); // There's a delay here in the original engine. We leave it out as // we're already slower than the original without any delay. const uint kLoopCycles = 25; const byte period = (index + 1) << 4; const double freq = kClock / 2.0 / (period * kLoopCycles); const double len = 128 * period * kLoopCycles * 1000 / (double)kClock; Tones tone; tone.push_back(Tone(freq, len)); if (playTones(tone, false, true)) break; drawLight(index, color ^ 0xff); if (--index < 0) { index = 4; color ^= 0xff; } } } typedef Common::Functor1Mem OpcodeH5; #define SetOpcodeTable(x) table = &x; #define Opcode(x) table->push_back(new OpcodeH5(this, &HiRes5Engine::x)) #define OpcodeUnImpl() table->push_back(new OpcodeH5(this, 0)) void HiRes5Engine::setupOpcodeTables() { Common::Array *table = 0; SetOpcodeTable(_condOpcodes); // 0x00 OpcodeUnImpl(); Opcode(o2_isFirstTime); Opcode(o2_isRandomGT); Opcode(o4_isItemInRoom); // 0x04 Opcode(o3_isNounNotInRoom); Opcode(o1_isMovesGT); Opcode(o1_isVarEQ); Opcode(o2_isCarryingSomething); // 0x08 Opcode(o4_isVarGT); Opcode(o1_isCurPicEQ); OpcodeUnImpl(); SetOpcodeTable(_actOpcodes); // 0x00 OpcodeUnImpl(); Opcode(o1_varAdd); Opcode(o1_varSub); Opcode(o1_varSet); // 0x04 Opcode(o1_listInv); Opcode(o4_moveItem); Opcode(o1_setRoom); Opcode(o2_setCurPic); // 0x08 Opcode(o2_setPic); Opcode(o1_printMsg); Opcode(o4_setRegionToPrev); Opcode(o_checkItemTimeLimits); // 0x0c Opcode(o4_moveAllItems); Opcode(o1_quit); Opcode(o4_setRegion); Opcode(o4_save); // 0x10 Opcode(o4_restore); Opcode(o4_restart); Opcode(o4_setRegionRoom); Opcode(o_startAnimation); // 0x14 Opcode(o1_resetPic); Opcode(o1_goDirection); Opcode(o1_goDirection); Opcode(o1_goDirection); // 0x18 Opcode(o1_goDirection); Opcode(o1_goDirection); Opcode(o1_goDirection); Opcode(o1_takeItem); // 0x1c Opcode(o1_dropItem); Opcode(o4_setRoomPic); Opcode(o_winGame); OpcodeUnImpl(); // 0x20 Opcode(o2_initDisk); } bool HiRes5Engine::isInventoryFull() { Common::List::const_iterator item; byte weight = 0; for (item = _state.items.begin(); item != _state.items.end(); ++item) { if (item->room == IDI_ANY) weight += item->description; } if (weight >= 100) { printString(_gameStrings.carryingTooMuch); inputString(); return true; } return false; } void HiRes5Engine::loadSong(Common::ReadStream &stream) { while (true) { const byte period = stream.readByte(); if (stream.err() || stream.eos()) error("Error loading song"); if (period == 0xff) return; byte length = stream.readByte(); if (stream.err() || stream.eos()) error("Error loading song"); const uint kLoopCycles = 20; // Delay loop cycles double freq = 0.0; // This computation ignores CPU cycles spent on overflow handling and // speaker control. As a result, our tone frequencies will be slightly // higher than those on original hardware. if (period != 0) freq = kClock / 2.0 / (period * kLoopCycles); const double len = (length > 0 ? length - 1 : 255) * 256 * kLoopCycles * 1000 / (double)kClock; _song.push_back(Tone(freq, len)); } } int HiRes5Engine::o_checkItemTimeLimits(ScriptEnv &e) { OP_DEBUG_1("\tCHECK_ITEM_TIME_LIMITS(VARS[%d])", e.arg(1)); bool lostAnItem = false; Common::List::iterator item; for (item = _state.items.begin(); item != _state.items.end(); ++item) { const byte room = item->room; const byte region = item->region; if (room == IDI_ANY || room == IDI_CUR_ROOM || (room == _state.room && region == _state.region)) { if (getVar(e.arg(1)) < _itemTimeLimits[item->id - 1]) { item->room = IDI_VOID_ROOM; lostAnItem = true; } } } if (lostAnItem) { printString(_gameStrings.itemTimeLimit); inputString(); } return 1; } int HiRes5Engine::o_startAnimation(ScriptEnv &e) { OP_DEBUG_0("\tSTART_ANIMATION()"); _doAnimation = true; return 0; } int HiRes5Engine::o_winGame(ScriptEnv &e) { OP_DEBUG_0("\tWIN_GAME()"); showRoom(); playTones(_song, true); return o1_quit(e); } void HiRes5Engine::runIntro() { insertDisk(2); StreamPtr stream(_disk->createReadStream(0x10, 0x0, 0x00, 31)); _display->setMode(DISPLAY_MODE_HIRES); _display->loadFrameBuffer(*stream); _display->updateHiResScreen(); inputKey(); _display->home(); _display->setMode(DISPLAY_MODE_TEXT); stream.reset(_disk->createReadStream(0x03, 0xc, 0x34, 1)); Common::String menu(readString(*stream)); while (!g_engine->shouldQuit()) { _display->home(); _display->printString(menu); Common::String cmd(inputString()); // We ignore the backup and format menu options if (!cmd.empty() && cmd[0] == APPLECHAR('1')) break; }; } void HiRes5Engine::init() { _graphics = new Graphics_v2(*_display); insertDisk(2); StreamPtr stream(_disk->createReadStream(0x5, 0x0, 0x02)); loadRegionLocations(*stream, kRegions); stream.reset(_disk->createReadStream(0xd, 0x2, 0x04)); loadRegionInitDataOffsets(*stream, kRegions); stream.reset(_disk->createReadStream(0x7, 0xe)); _strings.verbError = readStringAt(*stream, 0x4f); _strings.nounError = readStringAt(*stream, 0x8e); _strings.enterCommand = readStringAt(*stream, 0xbc); stream.reset(_disk->createReadStream(0x7, 0xc)); _strings.lineFeeds = readString(*stream); stream.reset(_disk->createReadStream(0x8, 0x3, 0x00, 2)); _strings_v2.saveInsert = readStringAt(*stream, 0x66); _strings_v2.saveReplace = readStringAt(*stream, 0x112); _strings_v2.restoreInsert = readStringAt(*stream, 0x180); _strings.playAgain = readStringAt(*stream, 0x247, 0xff); _messageIds.cantGoThere = 110; _messageIds.dontUnderstand = 112; _messageIds.itemDoesntMove = 114; _messageIds.itemNotHere = 115; _messageIds.thanksForPlaying = 113; stream.reset(_disk->createReadStream(0xe, 0x1, 0x13, 4)); loadItemDescriptions(*stream, kItems); stream.reset(_disk->createReadStream(0x8, 0xd, 0xfd, 1)); loadDroppedItemOffsets(*stream, 16); stream.reset(_disk->createReadStream(0xb, 0xa, 0x05, 1)); loadItemPicIndex(*stream, kItems); stream.reset(_disk->createReadStream(0x7, 0x8, 0x01)); for (uint i = 0; i < kItems; ++i) _itemTimeLimits.push_back(stream->readByte()); if (stream->eos() || stream->err()) error("Failed to read item time limits"); stream.reset(_disk->createReadStream(0x8, 0x2, 0x2d)); _gameStrings.itemTimeLimit = readString(*stream); stream.reset(_disk->createReadStream(0x8, 0x7, 0x02)); _gameStrings.carryingTooMuch = readString(*stream); stream.reset(_disk->createReadStream(0xc, 0xb, 0x20)); loadSong(*stream); } void HiRes5Engine::initGameState() { _state.vars.resize(40); insertDisk(2); StreamPtr stream(_disk->createReadStream(0x5, 0x1, 0x00, 3)); loadItems(*stream); // A combined total of 1213 rooms static const byte rooms[kRegions] = { 6, 16, 24, 57, 40, 30, 76, 40, 54, 38, 44, 21, 26, 42, 49, 32, 59, 69, 1, 1, 1, 1, 1, 18, 25, 13, 28, 28, 11, 23, 9, 31, 6, 29, 29, 34, 9, 10, 95, 86, 1 }; initRegions(rooms, kRegions); loadRegion(1); _state.room = 5; _doAnimation = false; } void HiRes5Engine::applyRegionWorkarounds() { // WORKAROUND: Remove/fix buggy commands switch (_state.region) { case 3: // "USE PIN" references a missing message, but cannot // be triggered due to shadowing of the "USE" verb. // We remove it anyway to allow script dumping to proceed. // TODO: Investigate if we should fix this command instead // of removing it. removeCommand(_roomCommands, 12); break; case 14: // "WITH SHOVEL" references a missing message. This bug // is game-breaking in the original, but unlikely to occur // in practice due to the "DIG" command not asking for what // to dig with. Probably a remnant of an earlier version // of the script. removeCommand(_roomCommands, 0); } } void HiRes5Engine::applyRoomWorkarounds(byte roomNr) { // WORKAROUND: Remove/fix buggy commands if (_state.region == 17 && roomNr == 49) { // "GET WATER" references a missing message when you already // have water. This message should be 117 instead of 17. getCommand(_roomData.commands, 8).script[4] = 117; } } Engine *HiRes5Engine_create(OSystem *syst, const AdlGameDescription *gd) { return new HiRes5Engine(syst, gd); } } // End of namespace Adl