/* 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 "common/events.h" #include "common/memstream.h" #include "adl/adl_v3.h" #include "adl/detection.h" #include "adl/display_a2.h" #include "adl/graphics.h" #include "adl/disk.h" namespace Adl { #define IDI_HR4_NUM_ROOMS 164 #define IDI_HR4_NUM_MESSAGES 255 #define IDI_HR4_NUM_VARS 40 #define IDI_HR4_NUM_ITEM_DESCS 44 #define IDI_HR4_NUM_ITEM_PICS 41 #define IDI_HR4_NUM_ITEM_OFFSETS 40 // Messages used outside of scripts #define IDI_HR4_MSG_CANT_GO_THERE 110 #define IDI_HR4_MSG_DONT_UNDERSTAND 112 #define IDI_HR4_MSG_ITEM_DOESNT_MOVE 114 #define IDI_HR4_MSG_ITEM_NOT_HERE 115 #define IDI_HR4_MSG_THANKS_FOR_PLAYING 113 class HiRes4Engine : public AdlEngine_v3 { public: HiRes4Engine(OSystem *syst, const AdlGameDescription *gd) : AdlEngine_v3(syst, gd), _boot(nullptr) { _brokenRooms.push_back(121); } ~HiRes4Engine(); private: // AdlEngine void runIntro(); void init(); void initGameState(); void putSpace(uint x, uint y) const; void drawChar(byte c, Common::SeekableReadStream &shapeTable, Common::Point &pos) const; void drawText(const Common::String &str, Common::SeekableReadStream &shapeTable, const float ht, const float vt) const; void runIntroAdvise(Common::SeekableReadStream &menu); void runIntroLogo(Common::SeekableReadStream &ms2); void runIntroTitle(Common::SeekableReadStream &menu, Common::SeekableReadStream &ms2); void runIntroInstructions(Common::SeekableReadStream &instructions); void runIntroLoading(Common::SeekableReadStream &adventure); static const uint kClock = 1022727; // Apple II CPU clock rate DiskImage *_boot; }; // TODO: It might be worth replacing this with a more generic variant that // can be used in both hires4 and hires6 static Common::MemoryReadStream *readSkewedSectors(DiskImage *disk, byte track, byte sector, byte count) { const uint bytesPerSector = disk->getBytesPerSector(); const uint sectorsPerTrack = disk->getSectorsPerTrack(); const uint bufSize = count * bytesPerSector; byte *const buf = (byte *)malloc(bufSize); byte *p = buf; while (count-- != 0) { StreamPtr stream(disk->createReadStream(track, sector)); stream->read(p, bytesPerSector); if (stream->err() || stream->eos()) error("Error loading from disk image"); p += bytesPerSector; sector += 5; sector %= sectorsPerTrack; if (sector == 0) ++track; } return new Common::MemoryReadStream(buf, bufSize, DisposeAfterUse::YES); } static Common::MemoryReadStream *decodeData(Common::SeekableReadStream &stream, const uint startOffset, uint endOffset, const byte xorVal) { assert(stream.size() >= 0); uint streamSize(stream.size()); if (endOffset > streamSize) endOffset = streamSize; byte *const buf = (byte *)malloc(streamSize); stream.read(buf, streamSize); if (stream.err() || stream.eos()) error("Failed to read data for decoding"); for (uint i = startOffset; i < endOffset; ++i) buf[i] ^= xorVal; return new Common::MemoryReadStream(buf, streamSize, DisposeAfterUse::YES); } HiRes4Engine::~HiRes4Engine() { delete _boot; } void HiRes4Engine::putSpace(uint x, uint y) const { if (shouldQuit()) return; _display->moveCursorTo(Common::Point(x, y)); _display->printChar(' '); _display->renderText(); delay(2); } void HiRes4Engine::drawChar(byte c, Common::SeekableReadStream &shapeTable, Common::Point &pos) const { shapeTable.seek(0); byte entries = shapeTable.readByte(); if (c >= entries) error("Character %d is not in the shape table", c); shapeTable.seek(c * 2 + 2); uint16 offset = shapeTable.readUint16LE(); shapeTable.seek(offset); _graphics->drawShape(shapeTable, pos); } void HiRes4Engine::drawText(const Common::String &str, Common::SeekableReadStream &shapeTable, const float ht, const float vt) const { if (shouldQuit()) return; Common::Point pos((int16)(ht * 7), (int16)(vt * 7.7f)); drawChar(99, shapeTable, pos); for (uint i = 0; i < str.size(); ++i) { const byte c = str[i] - 32; drawChar(c, shapeTable, pos); drawChar(98, shapeTable, pos); _display->renderGraphics(); delay(15); } } void HiRes4Engine::runIntroAdvise(Common::SeekableReadStream &menu) { Common::StringArray backupText; backupText.push_back(readStringAt(menu, 0x659, '"')); backupText.push_back(readStringAt(menu, 0x682, '"')); backupText.push_back(readStringAt(menu, 0x6a9, '"')); backupText.push_back(readStringAt(menu, 0x6c6, '"')); _display->setMode(Display::kModeText); for (uint x = 2; x <= 36; ++x) putSpace(x, 2); for (uint y = 3; y <= 20; ++y) { putSpace(2, y); putSpace(36, y); } for (uint x = 2; x <= 36; ++x) putSpace(x, 20); for (uint x = 0; x <= 38; ++x) putSpace(x, 0); for (uint y = 1; y <= 21; ++y) { putSpace(0, y); putSpace(38, y); } for (uint x = 0; x <= 38; ++x) putSpace(x, 22); int y = 7; for (uint i = 0; i < backupText.size(); ++i) { uint x = 0; do { if (shouldQuit()) return; ++x; Common::String left = backupText[i]; left.erase(x, Common::String::npos); Common::String right = backupText[i]; right.erase(0, right.size() - x); _display->moveCursorTo(Common::Point(19 - x, y)); _display->printAsciiString(left); _display->moveCursorTo(Common::Point(19, y)); _display->printAsciiString(right); _display->renderText(); delay(35); } while (x != backupText[i].size() / 2); if (i == 2) y = 18; else y += 2; } Common::String cursor = readStringAt(menu, 0x781, '"'); uint cursorIdx = 0; while (!shouldQuit()) { Common::Event event; if (pollEvent(event)) { if (event.type == Common::EVENT_KEYDOWN) break; } _display->moveCursorTo(Common::Point(32, 18)); _display->printChar(_display->asciiToNative(cursor[cursorIdx])); _display->renderText(); g_system->delayMillis(25); cursorIdx = (cursorIdx + 1) % cursor.size(); } } void HiRes4Engine::runIntroLogo(Common::SeekableReadStream &ms2) { Display_A2 *display = static_cast(_display); const uint width = display->getGfxWidth(); const uint height = display->getGfxHeight(); const uint pitch = display->getGfxPitch(); display->clear(0x00); display->setMode(Display::kModeGraphics); byte *logo = new byte[pitch * height]; display->loadFrameBuffer(ms2, logo); for (uint x = 0; x < width; ++x) { for (uint y = 0; y < height; ++y) { const byte p = logo[y * pitch + x / 7]; display->setPixelBit(Common::Point(x, y), p); if (x % 7 == 6) display->setPixelPalette(Common::Point(x, y), p); } display->renderGraphics(); if (shouldQuit()) { delete[] logo; return; } delay(7); } delete[] logo; for (uint i = 38; i != 0; --i) { Common::Point p; for (p.y = 1; p.y < (int)height; ++p.y) for (p.x = 0; p.x < (int)width; p.x += 7) display->setPixelByte(Common::Point(p.x, p.y - 1), display->getPixelByte(p)); display->renderGraphics(); Tones tone; tone.push_back(Tone(kClock / 2.0 / ((i * 4 + 1) * 10.0 + 10.0), 12.5)); playTones(tone, false, false); if (shouldQuit()) return; } } void HiRes4Engine::runIntroTitle(Common::SeekableReadStream &menu, Common::SeekableReadStream &ms2) { ms2.seek(0x2290); StreamPtr shapeTable(ms2.readStream(0x450)); if (ms2.err() || ms2.eos()) error("Failed to read shape table"); Common::String titleString(readStringAt(menu, 0x1f5, '"')); drawText(titleString, *shapeTable, 4.0f, 22.5f); titleString = readStringAt(menu, 0x22b, '"'); drawText(titleString, *shapeTable, 5.0f, 24.0f); // Draw "TM" with lines _graphics->drawLine(Common::Point(200, 170), Common::Point(200, 174), 0x7f); _graphics->drawLine(Common::Point(198, 170), Common::Point(202, 170), 0x7f); _display->renderGraphics(); delay(7); _graphics->drawLine(Common::Point(204, 170), Common::Point(204, 174), 0x7f); _graphics->drawLine(Common::Point(204, 170), Common::Point(207, 173), 0x7f); _graphics->drawLine(Common::Point(207, 173), Common::Point(209, 170), 0x7f); _graphics->drawLine(Common::Point(209, 170), Common::Point(209, 174), 0x7f); _display->renderGraphics(); delay(7); titleString = readStringAt(menu, 0x46c); drawText(titleString, *shapeTable, 20.0f - titleString.size() / 2.0f, 10.6f); titleString = readStringAt(menu, 0x490); drawText(titleString, *shapeTable, 20.0f - titleString.size() / 2.0f, 11.8f); Common::StringArray menuStrings; menuStrings.push_back(readStringAt(menu, 0x515)); menuStrings.push_back(readStringAt(menu, 0x52b)); for (uint i = 0; i < menuStrings.size(); ++i) drawText(Common::String::format("%d) ", i + 1) + menuStrings[i], *shapeTable, 12.5f, 14.0f + i * 1.2f); titleString = readStringAt(menu, 0x355, '"'); drawText(titleString, *shapeTable, 12.5f, 14.0f + menuStrings.size() * 1.2f + 2.0f); } void HiRes4Engine::runIntroInstructions(Common::SeekableReadStream &instructions) { Common::String line; Common::String pressKey(readStringAt(instructions, 0xad6, '"')); instructions.seek(0); _display->home(); _display->setMode(Display::kModeText); // Search for PRINT commands in tokenized BASIC while (1) { char c; do { c = instructions.readByte(); if (instructions.err() || instructions.eos()) error("Error reading instructions file"); // GOSUB (calls "press any key" routine) if (c == (char)0xb0) { _display->moveCursorTo(Common::Point(6, 23)); _display->printAsciiString(pressKey); inputKey(); if (shouldQuit()) return; _display->home(); } } while (c != (char)0xba); // PRINT uint quotes = 0; while (1) { c = instructions.readByte(); if (instructions.err() || instructions.eos()) error("Error reading instructions file"); if (c == '"') { ++quotes; continue; } if (c == 0) break; if (quotes == 1) line += c; else if (c == ':') // Separator break; else if (c == '4') // CTRL-D before "RUN MENU" return; }; line += '\r'; _display->printAsciiString(line); line.clear(); } } void HiRes4Engine::runIntroLoading(Common::SeekableReadStream &adventure) { _display->home(); _display->setMode(Display::kModeText); const uint kStrings = 4; const uint kStringLen = 39; char text[kStrings][kStringLen]; adventure.seek(0x2eb); if (adventure.read(text, sizeof(text)) < sizeof(text)) error("Failed to read loading screen text"); const uint yPos[kStrings] = { 2, 19, 8, 22 }; for (uint i = 0; i < kStrings; ++i) { _display->moveCursorTo(Common::Point(0, yPos[i])); _display->printString(Common::String(text[i], kStringLen)); } delay(4000); } void HiRes4Engine::runIntro() { Common::ScopedPtr files(new Files_AppleDOS()); files->open(getDiskImageName(0)); while (!shouldQuit()) { StreamPtr menu(files->createReadStream("MENU")); runIntroAdvise(*menu); if (shouldQuit()) return; StreamPtr ms2(files->createReadStream("MS2")); runIntroLogo(*ms2); if (shouldQuit()) return; _graphics->setBounds(Common::Rect(280, 192)); runIntroTitle(*menu, *ms2); _graphics->setBounds(Common::Rect(280, 160)); while (1) { char key = inputKey(); if (shouldQuit()) return; if (key == _display->asciiToNative('1')) { StreamPtr instructions(files->createReadStream("INSTRUCTIONS")); runIntroInstructions(*instructions); break; } else if (key == _display->asciiToNative('2')) { StreamPtr adventure(files->createReadStream("THE ADVENTURE")); runIntroLoading(*adventure); return; } }; } } void HiRes4Engine::init() { _graphics = new GraphicsMan_v2(*static_cast(_display)); _boot = new DiskImage(); if (!_boot->open(getDiskImageName(0))) error("Failed to open disk image '%s'", getDiskImageName(0).c_str()); insertDisk(1); StreamPtr stream(readSkewedSectors(_boot, 0x05, 0x6, 1)); _strings.verbError = readStringAt(*stream, 0x4f); _strings.nounError = readStringAt(*stream, 0x8e); _strings.enterCommand = readStringAt(*stream, 0xbc); stream.reset(readSkewedSectors(_boot, 0x05, 0x3, 1)); stream->skip(0xd7); _strings_v2.time = readString(*stream, 0xff); stream.reset(readSkewedSectors(_boot, 0x05, 0x7, 2)); _strings.lineFeeds = readStringAt(*stream, 0xf8); stream.reset(readSkewedSectors(_boot, 0x06, 0xf, 3)); _strings_v2.saveInsert = readStringAt(*stream, 0x5f); _strings_v2.saveReplace = readStringAt(*stream, 0xe5); _strings_v2.restoreInsert = readStringAt(*stream, 0x132); _strings_v2.restoreReplace = readStringAt(*stream, 0x1c2); _strings.playAgain = readStringAt(*stream, 0x225); _messageIds.cantGoThere = IDI_HR4_MSG_CANT_GO_THERE; _messageIds.dontUnderstand = IDI_HR4_MSG_DONT_UNDERSTAND; _messageIds.itemDoesntMove = IDI_HR4_MSG_ITEM_DOESNT_MOVE; _messageIds.itemNotHere = IDI_HR4_MSG_ITEM_NOT_HERE; _messageIds.thanksForPlaying = IDI_HR4_MSG_THANKS_FOR_PLAYING; stream.reset(readSkewedSectors(_boot, 0x0a, 0x0, 5)); loadMessages(*stream, IDI_HR4_NUM_MESSAGES); stream.reset(readSkewedSectors(_boot, 0x05, 0x2, 1)); stream->skip(0x80); loadPictures(*stream); stream.reset(readSkewedSectors(_boot, 0x09, 0x2, 1)); stream->skip(0x05); loadItemPictures(*stream, IDI_HR4_NUM_ITEM_PICS); stream.reset(readSkewedSectors(_boot, 0x04, 0x0, 3)); stream->skip(0x15); loadItemDescriptions(*stream, IDI_HR4_NUM_ITEM_DESCS); stream.reset(readSkewedSectors(_boot, 0x08, 0x2, 6)); stream->skip(0xa5); readCommands(*stream, _roomCommands); stream.reset(readSkewedSectors(_boot, 0x04, 0xc, 4)); stream.reset(decodeData(*stream, 0x218, 0x318, 0xee)); readCommands(*stream, _globalCommands); stream.reset(readSkewedSectors(_boot, 0x06, 0x6, 1)); stream->skip(0x15); loadDroppedItemOffsets(*stream, IDI_HR4_NUM_ITEM_OFFSETS); stream.reset(readSkewedSectors(_boot, 0x05, 0x0, 4)); loadWords(*stream, _verbs, _priVerbs); stream.reset(readSkewedSectors(_boot, 0x0b, 0xb, 7)); loadWords(*stream, _nouns, _priNouns); } void HiRes4Engine::initGameState() { _state.vars.resize(IDI_HR4_NUM_VARS); StreamPtr stream(readSkewedSectors(_boot, 0x0b, 0x9, 10)); stream->skip(0x0e); loadRooms(*stream, IDI_HR4_NUM_ROOMS); stream.reset(readSkewedSectors(_boot, 0x0b, 0x0, 13)); stream.reset(decodeData(*stream, 0x43, 0x143, 0x91)); loadItems(*stream); } class HiRes4Engine_Atari : public AdlEngine_v3 { public: HiRes4Engine_Atari(OSystem *syst, const AdlGameDescription *gd) : AdlEngine_v3(syst, gd), _boot(nullptr), _curDisk(0) { _brokenRooms.push_back(121); } ~HiRes4Engine_Atari(); private: // AdlEngine void init(); void initGameState(); void loadRoom(byte roomNr); Common::String formatVerbError(const Common::String &verb) const; Common::String formatNounError(const Common::String &verb, const Common::String &noun) const; // AdlEngine_v2 void adjustDataBlockPtr(byte &track, byte §or, byte &offset, byte &size) const; Common::SeekableReadStream *createReadStream(DiskImage *disk, byte track, byte sector, byte offset = 0, byte size = 0) const; void loadCommonData(); void insertDisk(byte diskNr); void rebindDisk(); DiskImage *_boot; byte _curDisk; }; static const char *const atariDisks[] = { "ULYS1A.XFD", "ULYS1B.XFD", "ULYS2C.XFD" }; HiRes4Engine_Atari::~HiRes4Engine_Atari() { delete _boot; } void HiRes4Engine_Atari::init() { _graphics = new GraphicsMan_v2(*static_cast(_display)); _boot = new DiskImage(); if (!_boot->open(atariDisks[0])) error("Failed to open disk image '%s'", atariDisks[0]); insertDisk(1); loadCommonData(); StreamPtr stream(createReadStream(_boot, 0x06, 0x2)); _strings.verbError = readStringAt(*stream, 0x4f); _strings.nounError = readStringAt(*stream, 0x83); _strings.enterCommand = readStringAt(*stream, 0xa6); stream.reset(createReadStream(_boot, 0x05, 0xb, 0xd7)); _strings_v2.time = readString(*stream, 0xff); stream.reset(createReadStream(_boot, 0x06, 0x7, 0x00, 2)); _strings_v2.saveInsert = readStringAt(*stream, 0x62); _strings_v2.saveReplace = readStringAt(*stream, 0xdd); _strings_v2.restoreInsert = readStringAt(*stream, 0x12a); _strings_v2.restoreReplace = readStringAt(*stream, 0x1b8); _strings.playAgain = readStringAt(*stream, 0x21b); // TODO: restart sequence has "insert side a/b" strings _messageIds.cantGoThere = IDI_HR4_MSG_CANT_GO_THERE; _messageIds.dontUnderstand = IDI_HR4_MSG_DONT_UNDERSTAND; _messageIds.itemDoesntMove = IDI_HR4_MSG_ITEM_DOESNT_MOVE; _messageIds.itemNotHere = IDI_HR4_MSG_ITEM_NOT_HERE; _messageIds.thanksForPlaying = IDI_HR4_MSG_THANKS_FOR_PLAYING; stream.reset(createReadStream(_boot, 0x06, 0xd, 0x12, 2)); loadItemDescriptions(*stream, IDI_HR4_NUM_ITEM_DESCS); stream.reset(createReadStream(_boot, 0x07, 0x1, 0xf4)); loadDroppedItemOffsets(*stream, IDI_HR4_NUM_ITEM_OFFSETS); stream.reset(createReadStream(_boot, 0x08, 0xe, 0xa5, 5)); readCommands(*stream, _roomCommands); stream.reset(createReadStream(_boot, 0x0a, 0x9, 0x00, 3)); readCommands(*stream, _globalCommands); stream.reset(createReadStream(_boot, 0x05, 0x4, 0x00, 3)); loadWords(*stream, _verbs, _priVerbs); stream.reset(createReadStream(_boot, 0x03, 0xb, 0x00, 6)); loadWords(*stream, _nouns, _priNouns); } void HiRes4Engine_Atari::loadRoom(byte roomNr) { if (roomNr >= 59 && roomNr < 113) { if (_curDisk != 2) { insertDisk(2); rebindDisk(); } } else if (_curDisk != 1) { insertDisk(1); rebindDisk(); } AdlEngine_v3::loadRoom(roomNr); } Common::String HiRes4Engine_Atari::formatVerbError(const Common::String &verb) const { Common::String err = _strings.verbError; for (uint i = 0; i < verb.size(); ++i) err.setChar(verb[i], i + 8); return err; } Common::String HiRes4Engine_Atari::formatNounError(const Common::String &verb, const Common::String &noun) const { Common::String err = _strings.nounError; for (uint i = 0; i < verb.size(); ++i) err.setChar(verb[i], i + 8); for (uint i = 0; i < noun.size(); ++i) err.setChar(noun[i], i + 19); return err; } void HiRes4Engine_Atari::insertDisk(byte diskNr) { if (_curDisk == diskNr) return; _curDisk = diskNr; delete _disk; _disk = new DiskImage(); if (!_disk->open(atariDisks[diskNr])) error("Failed to open disk image '%s'", atariDisks[diskNr]); } void HiRes4Engine_Atari::rebindDisk() { // As room.data is bound to the DiskImage, we need to rebind them here // We cannot simply reload the rooms as that would reset their state // FIXME: Remove DataBlockPtr-DiskImage coupling? StreamPtr stream(createReadStream(_boot, 0x03, 0x1, 0x0e, 9)); for (uint i = 0; i < IDI_HR4_NUM_ROOMS; ++i) { stream->skip(7); _state.rooms[i].data = readDataBlockPtr(*stream); stream->skip(3); } // Rebind data that is on both side B and C loadCommonData(); } void HiRes4Engine_Atari::loadCommonData() { _messages.clear(); StreamPtr stream(createReadStream(_boot, 0x0a, 0x4, 0x00, 3)); loadMessages(*stream, IDI_HR4_NUM_MESSAGES); _pictures.clear(); stream.reset(createReadStream(_boot, 0x05, 0xe, 0x80)); loadPictures(*stream); _itemPics.clear(); stream.reset(createReadStream(_boot, 0x09, 0xe, 0x05)); loadItemPictures(*stream, IDI_HR4_NUM_ITEM_PICS); } void HiRes4Engine_Atari::initGameState() { _state.vars.resize(IDI_HR4_NUM_VARS); StreamPtr stream(createReadStream(_boot, 0x03, 0x1, 0x0e, 9)); loadRooms(*stream, IDI_HR4_NUM_ROOMS); stream.reset(createReadStream(_boot, 0x02, 0xc, 0x00, 12)); loadItems(*stream); // FIXME _display->moveCursorTo(Common::Point(0, 23)); } Common::SeekableReadStream *HiRes4Engine_Atari::createReadStream(DiskImage *disk, byte track, byte sector, byte offset, byte size) const { adjustDataBlockPtr(track, sector, offset, size); return disk->createReadStream(track, sector, offset, size); } void HiRes4Engine_Atari::adjustDataBlockPtr(byte &track, byte §or, byte &offset, byte &size) const { // Convert the Apple II disk offsets in the game, to Atari disk offsets uint sectorIndex = (track * 16 + sector + 1) << 1; // Atari uses 128 bytes per sector vs. 256 on the Apple II // Note that size indicates *additional* sectors to read after reading one sector size *= 2; if (offset >= 128) { // Offset in the second half of an Apple II sector, skip one sector and adjust offset ++sectorIndex; offset -= 128; } else { // Offset in the first half of an Apple II sector, we need to read one additional sector ++size; } // Compute track/sector for Atari's 18 sectors per track (sectorIndex is 1-based) track = (sectorIndex - 1) / 18; sector = (sectorIndex - 1) % 18; } Engine *HiRes4Engine_create(OSystem *syst, const AdlGameDescription *gd) { switch (getPlatform(*gd)) { case Common::kPlatformApple2: return new HiRes4Engine(syst, gd); case Common::kPlatformAtari8Bit: return new HiRes4Engine_Atari(syst, gd); default: error("Unsupported platform"); } } } // End of namespace Adl