diff options
Diffstat (limited to 'engines/adl')
29 files changed, 6791 insertions, 0 deletions
diff --git a/engines/adl/POTFILES b/engines/adl/POTFILES new file mode 100644 index 0000000000..ca485932f7 --- /dev/null +++ b/engines/adl/POTFILES @@ -0,0 +1 @@ +engines/adl/detection.cpp diff --git a/engines/adl/adl.cpp b/engines/adl/adl.cpp new file mode 100644 index 0000000000..b6af54962e --- /dev/null +++ b/engines/adl/adl.cpp @@ -0,0 +1,1316 @@ +/* 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/scummsys.h" +#include "common/config-manager.h" +#include "common/debug.h" +#include "common/error.h" +#include "common/file.h" +#include "common/system.h" +#include "common/events.h" +#include "common/stream.h" +#include "common/savefile.h" + +#include "engines/util.h" + +#include "graphics/palette.h" +#include "graphics/thumbnail.h" + +#include "adl/adl.h" +#include "adl/display.h" +#include "adl/detection.h" +#include "adl/graphics.h" +#include "adl/speaker.h" + +namespace Adl { + +AdlEngine::~AdlEngine() { + delete _display; + delete _graphics; + delete _speaker; + delete _console; + delete _dumpFile; +} + +AdlEngine::AdlEngine(OSystem *syst, const AdlGameDescription *gd) : + Engine(syst), + _dumpFile(nullptr), + _display(nullptr), + _graphics(nullptr), + _isRestarting(false), + _isRestoring(false), + _skipOneCommand(false), + _gameDescription(gd), + _saveVerb(0), + _saveNoun(0), + _restoreVerb(0), + _restoreNoun(0), + _canSaveNow(false), + _canRestoreNow(false) { + + DebugMan.addDebugChannel(kDebugChannelScript, "Script", "Trace script execution"); +} + +bool AdlEngine::pollEvent(Common::Event &event) const { + _console->onFrame(); + + if (g_system->getEventManager()->pollEvent(event)) { + if (event.type != Common::EVENT_KEYDOWN) + return false; + + if (event.kbd.flags & Common::KBD_CTRL) { + if (event.kbd.keycode == Common::KEYCODE_q) { + quitGame(); + return false; + } + + if (event.kbd.keycode == Common::KEYCODE_d) { + _console->attach(); + return false; + } + } + + return true; + } + + return false; +} + +Common::String AdlEngine::readString(Common::ReadStream &stream, byte until) const { + Common::String str; + + while (1) { + byte b = stream.readByte(); + + if (stream.eos() || stream.err()) + error("Error reading string"); + + if (b == until) + break; + + str += b; + }; + + return str; +} + +Common::String AdlEngine::readStringAt(Common::SeekableReadStream &stream, uint offset, byte until) const { + stream.seek(offset); + return readString(stream, until); +} + +void AdlEngine::openFile(Common::File &file, const Common::String &name) const { + if (!file.open(name)) + error("Error opening '%s'", name.c_str()); +} + +void AdlEngine::printMessage(uint idx) { + printString(loadMessage(idx)); +} + +Common::String AdlEngine::getItemDescription(const Item &item) const { + if (item.description > 0) + return loadMessage(item.description); + else + return Common::String(); +} + +void AdlEngine::delay(uint32 ms) const { + uint32 start = g_system->getMillis(); + + while (!shouldQuit() && g_system->getMillis() - start < ms) { + Common::Event event; + pollEvent(event); + g_system->delayMillis(16); + } +} + +Common::String AdlEngine::inputString(byte prompt) const { + Common::String s; + + if (prompt > 0) + _display->printString(Common::String(prompt)); + + while (1) { + byte b = inputKey(); + + if (shouldQuit() || _isRestoring) + return 0; + + if (b == 0) + continue; + + if (b == ('\r' | 0x80)) { + s += b; + _display->printString(Common::String(b)); + return s; + } + + if (b < 0xa0) { + switch (b) { + case Common::KEYCODE_BACKSPACE | 0x80: + if (!s.empty()) { + _display->moveCursorBackward(); + _display->setCharAtCursor(APPLECHAR(' ')); + s.deleteLastChar(); + } + break; + }; + } else { + if (s.size() < 255) { + s += b; + _display->printString(Common::String(b)); + } + } + } +} + +byte AdlEngine::inputKey(bool showCursor) const { + byte key = 0; + + if (showCursor) + _display->showCursor(true); + + while (!shouldQuit() && !_isRestoring && key == 0) { + Common::Event event; + if (pollEvent(event)) { + if (event.type != Common::EVENT_KEYDOWN) + continue; + + switch (event.kbd.keycode) { + case Common::KEYCODE_BACKSPACE: + case Common::KEYCODE_RETURN: + key = convertKey(event.kbd.keycode); + break; + default: + if (event.kbd.ascii >= 0x20 && event.kbd.ascii < 0x80) + key = convertKey(event.kbd.ascii); + }; + } + + _display->updateTextScreen(); + g_system->delayMillis(16); + } + + _display->showCursor(false); + + return key; +} + +void AdlEngine::loadWords(Common::ReadStream &stream, WordMap &map, Common::StringArray &pri) const { + uint index = 0; + + map.clear(); + pri.clear(); + + while (1) { + ++index; + + byte buf[IDI_WORD_SIZE]; + + if (stream.read(buf, IDI_WORD_SIZE) < IDI_WORD_SIZE) + error("Error reading word list"); + + Common::String word((char *)buf, IDI_WORD_SIZE); + + if (!map.contains(word)) + map[word] = index; + + pri.push_back(Console::toAscii(word)); + + byte synonyms = stream.readByte(); + + if (stream.err() || stream.eos()) + error("Error reading word list"); + + if (synonyms == 0xff) + break; + + for (uint i = 0; i < synonyms; ++i) { + if (stream.read((char *)buf, IDI_WORD_SIZE) < IDI_WORD_SIZE) + error("Error reading word list"); + + word = Common::String((char *)buf, IDI_WORD_SIZE); + + if (!map.contains(word)) + map[word] = index; + } + } +} + +void AdlEngine::readCommands(Common::ReadStream &stream, Commands &commands) { + commands.clear(); + + while (1) { + Command command; + command.room = stream.readByte(); + + if (command.room == 0xff) + return; + + command.verb = stream.readByte(); + command.noun = stream.readByte(); + + byte scriptSize = stream.readByte() - 6; + + command.numCond = stream.readByte(); + command.numAct = stream.readByte(); + + for (uint i = 0; i < scriptSize; ++i) + command.script.push_back(stream.readByte()); + + if (stream.eos() || stream.err()) + error("Failed to read commands"); + + if (command.numCond == 0 && command.script[0] == IDO_ACT_SAVE) { + _saveVerb = command.verb; + _saveNoun = command.noun; + } + + if (command.numCond == 0 && command.script[0] == IDO_ACT_LOAD) { + _restoreVerb = command.verb; + _restoreNoun = command.noun; + } + + commands.push_back(command); + } +} + +void AdlEngine::checkInput(byte verb, byte noun) { + // Try room-local command list first + if (doOneCommand(_roomData.commands, verb, noun)) + return; + + // If no match was found, try the global list + if (!doOneCommand(_roomCommands, verb, noun)) + printMessage(_messageIds.dontUnderstand); +} + +bool AdlEngine::isInputValid(byte verb, byte noun, bool &is_any) { + if (isInputValid(_roomData.commands, verb, noun, is_any)) + return true; + return isInputValid(_roomCommands, verb, noun, is_any); +} + +bool AdlEngine::isInputValid(const Commands &commands, byte verb, byte noun, bool &is_any) { + Commands::const_iterator cmd; + + is_any = false; + for (cmd = commands.begin(); cmd != commands.end(); ++cmd) { + ScriptEnv env(*cmd, _state.room, verb, noun); + if (matchCommand(env)) { + if (cmd->verb == IDI_ANY || cmd->noun == IDI_ANY) + is_any = true; + return true; + } + } + + return false; +} + +typedef Common::Functor1Mem<ScriptEnv &, int, AdlEngine> OpcodeV1; +#define SetOpcodeTable(x) table = &x; +#define Opcode(x) table->push_back(new OpcodeV1(this, &AdlEngine::x)) +#define OpcodeUnImpl() table->push_back(new OpcodeV1(this, 0)) + +void AdlEngine::setupOpcodeTables() { + Common::Array<const Opcode *> *table = 0; + + SetOpcodeTable(_condOpcodes); + // 0x00 + OpcodeUnImpl(); + OpcodeUnImpl(); + OpcodeUnImpl(); + Opcode(o1_isItemInRoom); + // 0x04 + OpcodeUnImpl(); + Opcode(o1_isMovesGT); + Opcode(o1_isVarEQ); + OpcodeUnImpl(); + // 0x08 + OpcodeUnImpl(); + Opcode(o1_isCurPicEQ); + Opcode(o1_isItemPicEQ); + + SetOpcodeTable(_actOpcodes); + // 0x00 + OpcodeUnImpl(); + Opcode(o1_varAdd); + Opcode(o1_varSub); + Opcode(o1_varSet); + // 0x04 + Opcode(o1_listInv); + Opcode(o1_moveItem); + Opcode(o1_setRoom); + Opcode(o1_setCurPic); + // 0x08 + Opcode(o1_setPic); + Opcode(o1_printMsg); + Opcode(o1_setLight); + Opcode(o1_setDark); + // 0x0c + OpcodeUnImpl(); + Opcode(o1_quit); + OpcodeUnImpl(); + Opcode(o1_save); + // 0x10 + Opcode(o1_restore); + Opcode(o1_restart); + Opcode(o1_placeItem); + Opcode(o1_setItemPic); + // 0x14 + Opcode(o1_resetPic); + Opcode(o1_goDirection<IDI_DIR_NORTH>); + Opcode(o1_goDirection<IDI_DIR_SOUTH>); + Opcode(o1_goDirection<IDI_DIR_EAST>); + // 0x18 + Opcode(o1_goDirection<IDI_DIR_WEST>); + Opcode(o1_goDirection<IDI_DIR_UP>); + Opcode(o1_goDirection<IDI_DIR_DOWN>); + Opcode(o1_takeItem); + // 0x1c + Opcode(o1_dropItem); + Opcode(o1_setRoomPic); +} + +void AdlEngine::initState() { + _state = State(); + + initGameState(); +} + +byte AdlEngine::roomArg(byte room) const { + return room; +} + +void AdlEngine::clearScreen() const { + _display->setMode(DISPLAY_MODE_MIXED); + _display->clear(0x00); +} + +void AdlEngine::drawPic(byte pic, Common::Point pos) const { + if (_roomData.pictures.contains(pic)) + _graphics->drawPic(*_roomData.pictures[pic]->createReadStream(), pos); + else + _graphics->drawPic(*_pictures[pic]->createReadStream(), pos); +} + +void AdlEngine::bell(uint count) const { + _speaker->bell(count); +} + +const Room &AdlEngine::getRoom(uint i) const { + if (i < 1 || i > _state.rooms.size()) + error("Room %i out of range [1, %i]", i, _state.rooms.size()); + + return _state.rooms[i - 1]; +} + +Room &AdlEngine::getRoom(uint i) { + if (i < 1 || i > _state.rooms.size()) + error("Room %i out of range [1, %i]", i, _state.rooms.size()); + + return _state.rooms[i - 1]; +} + +const Room &AdlEngine::getCurRoom() const { + return getRoom(_state.room); +} + +Room &AdlEngine::getCurRoom() { + return getRoom(_state.room); +} + +const Item &AdlEngine::getItem(uint i) const { + Common::List<Item>::const_iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) + if (item->id == i) + return *item; + + error("Item %i not found", i); +} + +Item &AdlEngine::getItem(uint i) { + Common::List<Item>::iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) + if (item->id == i) + return *item; + + error("Item %i not found", i); +} + +byte AdlEngine::getVar(uint i) const { + if (i >= _state.vars.size()) + error("Variable %i out of range [0, %i]", i, _state.vars.size() - 1); + + return _state.vars[i]; +} + +void AdlEngine::setVar(uint i, byte value) { + if (i >= _state.vars.size()) + error("Variable %i out of range [0, %i]", i, _state.vars.size() - 1); + + _state.vars[i] = value; +} + +void AdlEngine::takeItem(byte noun) { + Common::List<Item>::iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) { + if (item->noun != noun || item->room != _state.room) + continue; + + if (item->state == IDI_ITEM_DOESNT_MOVE) { + printMessage(_messageIds.itemDoesntMove); + return; + } + + if (item->state == IDI_ITEM_DROPPED) { + item->room = IDI_ANY; + return; + } + + Common::Array<byte>::const_iterator pic; + for (pic = item->roomPictures.begin(); pic != item->roomPictures.end(); ++pic) { + if (*pic == getCurRoom().curPicture) { + item->room = IDI_ANY; + item->state = IDI_ITEM_DROPPED; + return; + } + } + } + + printMessage(_messageIds.itemNotHere); +} + +void AdlEngine::dropItem(byte noun) { + Common::List<Item>::iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) { + if (item->noun != noun || item->room != IDI_ANY) + continue; + + item->room = _state.room; + item->state = IDI_ITEM_DROPPED; + return; + } + + printMessage(_messageIds.dontUnderstand); +} + +Common::Error AdlEngine::run() { + _console = new Console(this); + _speaker = new Speaker(); + _display = new Display(); + + setupOpcodeTables(); + + init(); + + int saveSlot = ConfMan.getInt("save_slot"); + if (saveSlot >= 0) { + if (loadGameState(saveSlot).getCode() != Common::kNoError) + error("Failed to load save game from slot %i", saveSlot); + _display->moveCursorTo(Common::Point(0, 23)); + _isRestoring = true; + } else { + runIntro(); + initState(); + _display->printAsciiString(_strings.lineFeeds); + } + + _display->setMode(DISPLAY_MODE_MIXED); + + while (1) { + uint verb = 0, noun = 0; + _isRestarting = false; + + // When restoring from the launcher, we don't read + // input on the first iteration. This is needed to + // ensure that restoring from the launcher and + // restoring in-game brings us to the same game state. + // (Also see comment below.) + if (!_isRestoring) { + showRoom(); + + if (_isRestarting) + continue; + + _canSaveNow = _canRestoreNow = true; + getInput(verb, noun); + _canSaveNow = _canRestoreNow = false; + + if (shouldQuit()) + break; + + // If we just restored from the GMM, we skip this command + // set, as no command has been input by the user + if (!_isRestoring) + checkInput(verb, noun); + } + + if (_isRestoring) { + // We restored from the GMM or launcher. As restoring + // with "RESTORE GAME" does not end command processing, + // we don't break it off here either. This essentially + // means that restoring a game will always run through + // the global commands and increase the move counter + // before the first user input. + _display->printAsciiString("\r"); + _isRestoring = false; + verb = _restoreVerb; + noun = _restoreNoun; + } + + // Restarting does end command processing + if (_isRestarting) + continue; + + doAllCommands(_globalCommands, verb, noun); + + if (_isRestarting) + continue; + + advanceClock(); + _state.moves++; + } + + return Common::kNoError; +} + +bool AdlEngine::hasFeature(EngineFeature f) const { + switch (f) { + case kSupportsLoadingDuringRuntime: + case kSupportsSavingDuringRuntime: + case kSupportsRTL: + return true; + default: + return false; + } +} + +Common::Error AdlEngine::loadGameState(int slot) { + Common::String fileName = Common::String::format("%s.s%02d", _targetName.c_str(), slot); + Common::InSaveFile *inFile = getSaveFileManager()->openForLoading(fileName); + + if (!inFile) { + warning("Failed to open file '%s'", fileName.c_str()); + return Common::kUnknownError; + } + + if (inFile->readUint32BE() != MKTAG('A', 'D', 'L', ':')) { + warning("No header found in '%s'", fileName.c_str()); + delete inFile; + return Common::kUnknownError; + } + + byte saveVersion = inFile->readByte(); + if (saveVersion != SAVEGAME_VERSION) { + warning("Save game version %i not supported", saveVersion); + delete inFile; + return Common::kUnknownError; + } + + // Skip description + inFile->seek(SAVEGAME_NAME_LEN, SEEK_CUR); + // Skip save time + inFile->seek(6, SEEK_CUR); + + uint32 playTime = inFile->readUint32BE(); + + Graphics::skipThumbnail(*inFile); + + initState(); + + _state.room = inFile->readByte(); + _state.moves = inFile->readByte(); + _state.isDark = inFile->readByte(); + _state.time.hours = inFile->readByte(); + _state.time.minutes = inFile->readByte(); + + uint32 size = inFile->readUint32BE(); + if (size != _state.rooms.size()) + error("Room count mismatch (expected %i; found %i)", _state.rooms.size(), size); + + for (uint i = 0; i < size; ++i) { + _state.rooms[i].picture = inFile->readByte(); + _state.rooms[i].curPicture = inFile->readByte(); + _state.rooms[i].isFirstTime = inFile->readByte(); + } + + size = inFile->readUint32BE(); + if (size != _state.items.size()) + error("Item count mismatch (expected %i; found %i)", _state.items.size(), size); + + Common::List<Item>::iterator item; + for (item = _state.items.begin(); item != _state.items.end(); ++item) { + item->room = inFile->readByte(); + item->picture = inFile->readByte(); + item->position.x = inFile->readByte(); + item->position.y = inFile->readByte(); + item->state = inFile->readByte(); + } + + size = inFile->readUint32BE(); + if (size != _state.vars.size()) + error("Variable count mismatch (expected %i; found %i)", _state.vars.size(), size); + + for (uint i = 0; i < size; ++i) + _state.vars[i] = inFile->readByte(); + + if (inFile->err() || inFile->eos()) + error("Failed to load game '%s'", fileName.c_str()); + + delete inFile; + + setTotalPlayTime(playTime); + + _isRestoring = true; + return Common::kNoError; +} + +bool AdlEngine::canLoadGameStateCurrently() { + return _canRestoreNow; +} + +Common::Error AdlEngine::saveGameState(int slot, const Common::String &desc) { + Common::String fileName = Common::String::format("%s.s%02d", _targetName.c_str(), slot); + Common::OutSaveFile *outFile = getSaveFileManager()->openForSaving(fileName); + + if (!outFile) { + warning("Failed to open file '%s'", fileName.c_str()); + return Common::kUnknownError; + } + + outFile->writeUint32BE(MKTAG('A', 'D', 'L', ':')); + outFile->writeByte(SAVEGAME_VERSION); + + char name[SAVEGAME_NAME_LEN] = { }; + + if (!desc.empty()) + strncpy(name, desc.c_str(), sizeof(name) - 1); + else { + Common::String defaultName("Save "); + defaultName += 'A' + slot; + strncpy(name, defaultName.c_str(), sizeof(name) - 1); + } + + outFile->write(name, sizeof(name)); + + TimeDate t; + g_system->getTimeAndDate(t); + + outFile->writeUint16BE(t.tm_year); + outFile->writeByte(t.tm_mon); + outFile->writeByte(t.tm_mday); + outFile->writeByte(t.tm_hour); + outFile->writeByte(t.tm_min); + + uint32 playTime = getTotalPlayTime(); + outFile->writeUint32BE(playTime); + + _display->saveThumbnail(*outFile); + + outFile->writeByte(_state.room); + outFile->writeByte(_state.moves); + outFile->writeByte(_state.isDark); + outFile->writeByte(_state.time.hours); + outFile->writeByte(_state.time.minutes); + + outFile->writeUint32BE(_state.rooms.size()); + for (uint i = 0; i < _state.rooms.size(); ++i) { + outFile->writeByte(_state.rooms[i].picture); + outFile->writeByte(_state.rooms[i].curPicture); + outFile->writeByte(_state.rooms[i].isFirstTime); + } + + outFile->writeUint32BE(_state.items.size()); + Common::List<Item>::const_iterator item; + for (item = _state.items.begin(); item != _state.items.end(); ++item) { + outFile->writeByte(item->room); + outFile->writeByte(item->picture); + outFile->writeByte(item->position.x); + outFile->writeByte(item->position.y); + outFile->writeByte(item->state); + } + + outFile->writeUint32BE(_state.vars.size()); + for (uint i = 0; i < _state.vars.size(); ++i) + outFile->writeByte(_state.vars[i]); + + outFile->finalize(); + + if (outFile->err()) { + delete outFile; + warning("Failed to save game '%s'", fileName.c_str()); + return Common::kUnknownError; + } + + delete outFile; + return Common::kNoError; +} + +bool AdlEngine::canSaveGameStateCurrently() { + if (!_canSaveNow) + return false; + + Commands::const_iterator cmd; + + // Here we check whether or not the game currently accepts the command + // "SAVE GAME". This prevents saving via the GMM in situations where + // it wouldn't otherwise be possible to do so. + for (cmd = _roomCommands.begin(); cmd != _roomCommands.end(); ++cmd) { + ScriptEnv env(*cmd, _state.room, _saveVerb, _saveNoun); + if (matchCommand(env)) + return env.op() == IDO_ACT_SAVE; + } + + return false; +} + +byte AdlEngine::convertKey(uint16 ascii) const { + ascii = toupper(ascii); + + if (ascii >= 0x80) + return 0; + + ascii |= 0x80; + + if (ascii >= 0x80 && ascii <= 0xe0) + return ascii; + + return 0; +} + +Common::String AdlEngine::getLine() const { + // Original engine uses a global here, which isn't reset between + // calls and may not match actual mode + bool textMode = false; + + while (1) { + Common::String line = inputString(APPLECHAR('?')); + + if (shouldQuit() || _isRestoring) + return ""; + + if ((byte)line[0] == ('\r' | 0x80)) { + textMode = !textMode; + _display->setMode(textMode ? DISPLAY_MODE_TEXT : DISPLAY_MODE_MIXED); + continue; + } + + // Remove the return + line.deleteLastChar(); + return line; + } +} + +Common::String AdlEngine::getWord(const Common::String &line, uint &index) const { + Common::String str; + + for (uint i = 0; i < 8; ++i) + str += APPLECHAR(' '); + + int copied = 0; + + // Skip initial whitespace + while (1) { + if (index == line.size()) + return str; + if (line[index] != APPLECHAR(' ')) + break; + ++index; + } + + // Copy up to 8 characters + while (1) { + if (copied < 8) + str.setChar(line[index], copied++); + + index++; + + if (index == line.size() || line[index] == APPLECHAR(' ')) + return str; + } +} + +Common::String AdlEngine::formatVerbError(const Common::String &verb) const { + Common::String err = _strings.verbError; + for (uint i = 0; i < verb.size(); ++i) + err.setChar(verb[i], i + 19); + return err; +} + +Common::String AdlEngine::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 + 19); + for (uint i = 0; i < noun.size(); ++i) + err.setChar(noun[i], i + 30); + return err; +} + +void AdlEngine::getInput(uint &verb, uint &noun) { + while (1) { + _display->printString(_strings.enterCommand); + Common::String line = getLine(); + + if (shouldQuit() || _isRestoring) + return; + + uint index = 0; + Common::String verbString = getWord(line, index); + + if (!_verbs.contains(verbString)) { + _display->printString(formatVerbError(verbString)); + continue; + } + + verb = _verbs[verbString]; + + Common::String nounString = getWord(line, index); + + if (!_nouns.contains(nounString)) { + _display->printString(formatNounError(verbString, nounString)); + continue; + } + + noun = _nouns[nounString]; + return; + } +} + +bool AdlEngine::op_debug(const char *fmt, ...) const { + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) { + va_list va; + va_start(va, fmt); + Common::String output = Common::String::vformat(fmt, va); + va_end(va); + + output += '\n'; + if (_dumpFile) { + _dumpFile->write(output.c_str(), output.size()); + return true; + } else + debugN("%s", output.c_str()); + } + + return false; +} + +int AdlEngine::o1_isItemInRoom(ScriptEnv &e) { + OP_DEBUG_2("\t&& GET_ITEM_ROOM(%s) == %s", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); + + if (getItem(e.arg(1)).room == roomArg(e.arg(2))) + return 2; + + return -1; +} + +int AdlEngine::o1_isMovesGT(ScriptEnv &e) { + OP_DEBUG_1("\t&& MOVES > %d", e.arg(1)); + + if (_state.moves > e.arg(1)) + return 1; + + return -1; +} + +int AdlEngine::o1_isVarEQ(ScriptEnv &e) { + OP_DEBUG_2("\t&& VARS[%d] == %d", e.arg(1), e.arg(2)); + + if (getVar(e.arg(1)) == e.arg(2)) + return 2; + + return -1; +} + +int AdlEngine::o1_isCurPicEQ(ScriptEnv &e) { + OP_DEBUG_1("\t&& GET_CURPIC() == %d", e.arg(1)); + + if (getCurRoom().curPicture == e.arg(1)) + return 1; + + return -1; +} + +int AdlEngine::o1_isItemPicEQ(ScriptEnv &e) { + OP_DEBUG_2("\t&& GET_ITEM_PIC(%s) == %d", itemStr(e.arg(1)).c_str(), e.arg(2)); + + if (getItem(e.arg(1)).picture == e.arg(2)) + return 2; + + return -1; +} + +int AdlEngine::o1_varAdd(ScriptEnv &e) { + OP_DEBUG_2("\tVARS[%d] += %d", e.arg(2), e.arg(1)); + + setVar(e.arg(2), getVar(e.arg(2) + e.arg(1))); + return 2; +} + +int AdlEngine::o1_varSub(ScriptEnv &e) { + OP_DEBUG_2("\tVARS[%d] -= %d", e.arg(2), e.arg(1)); + + setVar(e.arg(2), getVar(e.arg(2)) - e.arg(1)); + return 2; +} + +int AdlEngine::o1_varSet(ScriptEnv &e) { + OP_DEBUG_2("\tVARS[%d] = %d", e.arg(1), e.arg(2)); + + setVar(e.arg(1), e.arg(2)); + return 2; +} + +int AdlEngine::o1_listInv(ScriptEnv &e) { + OP_DEBUG_0("\tLIST_INVENTORY()"); + + Common::List<Item>::const_iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) + if (item->room == IDI_ANY) + printMessage(item->description); + + return 0; +} + +int AdlEngine::o1_moveItem(ScriptEnv &e) { + OP_DEBUG_2("\tSET_ITEM_ROOM(%s, %s)", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); + + getItem(e.arg(1)).room = e.arg(2); + return 2; +} + +int AdlEngine::o1_setRoom(ScriptEnv &e) { + OP_DEBUG_1("\tROOM = %d", e.arg(1)); + + getCurRoom().curPicture = getCurRoom().picture; + _state.room = e.arg(1); + return 1; +} + +int AdlEngine::o1_setCurPic(ScriptEnv &e) { + OP_DEBUG_1("\tSET_CURPIC(%d)", e.arg(1)); + + getCurRoom().curPicture = e.arg(1); + return 1; +} + +int AdlEngine::o1_setPic(ScriptEnv &e) { + OP_DEBUG_1("\tSET_PIC(%d)", e.arg(1)); + + getCurRoom().picture = getCurRoom().curPicture = e.arg(1); + return 1; +} + +int AdlEngine::o1_printMsg(ScriptEnv &e) { + OP_DEBUG_1("\tPRINT(%s)", msgStr(e.arg(1)).c_str()); + + printMessage(e.arg(1)); + return 1; +} + +int AdlEngine::o1_setLight(ScriptEnv &e) { + OP_DEBUG_0("\tLIGHT()"); + + _state.isDark = false; + return 0; +} + +int AdlEngine::o1_setDark(ScriptEnv &e) { + OP_DEBUG_0("\tDARK()"); + + _state.isDark = true; + return 0; +} + +int AdlEngine::o1_save(ScriptEnv &e) { + OP_DEBUG_0("\tSAVE_GAME()"); + + saveGameState(0, ""); + return 0; +} + +int AdlEngine::o1_restore(ScriptEnv &e) { + OP_DEBUG_0("\tRESTORE_GAME()"); + + loadGameState(0); + _isRestoring = false; + return 0; +} + +int AdlEngine::o1_restart(ScriptEnv &e) { + OP_DEBUG_0("\tRESTART_GAME()"); + + _display->printString(_strings.playAgain); + Common::String input = inputString(); + + if (input.size() == 0 || input[0] != APPLECHAR('N')) { + _isRestarting = true; + _display->clear(0x00); + _display->updateHiResScreen(); + _display->printString(_strings.pressReturn); + initState(); + _display->printAsciiString(_strings.lineFeeds); + return -1; + } + + return o1_quit(e); +} + +int AdlEngine::o1_quit(ScriptEnv &e) { + OP_DEBUG_0("\tQUIT_GAME()"); + + printMessage(_messageIds.thanksForPlaying); + quitGame(); + return -1; +} + +int AdlEngine::o1_placeItem(ScriptEnv &e) { + OP_DEBUG_4("\tPLACE_ITEM(%s, %s, (%d, %d))", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str(), e.arg(3), e.arg(4)); + + Item &item = getItem(e.arg(1)); + + item.room = roomArg(e.arg(2)); + item.position.x = e.arg(3); + item.position.y = e.arg(4); + return 4; +} + +int AdlEngine::o1_setItemPic(ScriptEnv &e) { + OP_DEBUG_2("\tSET_ITEM_PIC(%s, %d)", itemStr(e.arg(2)).c_str(), e.arg(1)); + + getItem(e.arg(2)).picture = e.arg(1); + return 2; +} + +int AdlEngine::o1_resetPic(ScriptEnv &e) { + OP_DEBUG_0("\tRESET_PIC()"); + + getCurRoom().curPicture = getCurRoom().picture; + return 0; +} + +template <Direction D> +int AdlEngine::o1_goDirection(ScriptEnv &e) { + OP_DEBUG_0((Common::String("\tGO_") + dirStr(D) + "()").c_str()); + + byte room = getCurRoom().connections[D]; + + if (room == 0) { + printMessage(_messageIds.cantGoThere); + return -1; + } + + getCurRoom().curPicture = getCurRoom().picture; + _state.room = room; + return -1; +} + +int AdlEngine::o1_takeItem(ScriptEnv &e) { + OP_DEBUG_0("\tTAKE_ITEM()"); + + takeItem(e.getNoun()); + return 0; +} + +int AdlEngine::o1_dropItem(ScriptEnv &e) { + OP_DEBUG_0("\tDROP_ITEM()"); + + dropItem(e.getNoun()); + return 0; +} + +int AdlEngine::o1_setRoomPic(ScriptEnv &e) { + OP_DEBUG_2("\tSET_ROOM_PIC(%d, %d)", e.arg(1), e.arg(2)); + + getRoom(e.arg(1)).picture = getRoom(e.arg(1)).curPicture = e.arg(2); + return 2; +} + +bool AdlEngine::matchCommand(ScriptEnv &env) const { + if (!env.isMatch() && !_dumpFile) + return false; + + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) { + op_debug("IF\n\tROOM == %s", roomStr(env.getCommand().room).c_str()); + op_debug("\t&& SAID(%s, %s)", verbStr(env.getCommand().verb).c_str(), nounStr(env.getCommand().noun).c_str()); + } + + for (uint i = 0; i < env.getCondCount(); ++i) { + byte op = env.op(); + + if (op >= _condOpcodes.size() || !_condOpcodes[op] || !_condOpcodes[op]->isValid()) + error("Unimplemented condition opcode %02x", op); + + int numArgs = (*_condOpcodes[op])(env); + + if (numArgs < 0) { + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) + op_debug("FAIL\n"); + return false; + } + + env.skip(numArgs + 1); + } + + return true; +} + +void AdlEngine::doActions(ScriptEnv &env) { + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) + op_debug("THEN"); + + for (uint i = 0; i < env.getActCount(); ++i) { + byte op = env.op(); + + if (op >= _actOpcodes.size() || !_actOpcodes[op] || !_actOpcodes[op]->isValid()) + error("Unimplemented action opcode %02x", op); + + int numArgs = (*_actOpcodes[op])(env); + + if (numArgs < 0) { + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) + op_debug("ABORT\n"); + return; + } + + env.skip(numArgs + 1); + } + + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript)) + op_debug("END\n"); +} + +bool AdlEngine::doOneCommand(const Commands &commands, byte verb, byte noun) { + Commands::const_iterator cmd; + + for (cmd = commands.begin(); cmd != commands.end(); ++cmd) { + + if (_skipOneCommand) { + _skipOneCommand = false; + continue; + } + + ScriptEnv env(*cmd, _state.room, verb, noun); + if (matchCommand(env)) { + doActions(env); + return true; + } + } + + _skipOneCommand = false; + return false; +} + +void AdlEngine::doAllCommands(const Commands &commands, byte verb, byte noun) { + Commands::const_iterator cmd; + + for (cmd = commands.begin(); cmd != commands.end(); ++cmd) { + if (_skipOneCommand) { + _skipOneCommand = false; + continue; + } + + ScriptEnv env(*cmd, _state.room, verb, noun); + if (matchCommand(env)) { + doActions(env); + // The original long jumps on restart, so we need to abort here + if (_isRestarting) + return; + } + } + + _skipOneCommand = false; +} + +Common::String AdlEngine::toAscii(const Common::String &str) { + Common::String ascii = Console::toAscii(str); + if (ascii.lastChar() == '\r') + ascii.deleteLastChar(); + // FIXME: remove '\r's inside string? + return ascii; +} + +Common::String AdlEngine::itemStr(uint i) const { + byte desc = getItem(i).description; + byte noun = getItem(i).noun; + Common::String name = Common::String::format("%d", i); + if (noun > 0) { + name += "/"; + name += _priNouns[noun - 1]; + } + if (desc > 0) { + name += "/"; + name += toAscii(loadMessage(desc)); + } + return name; +} + +Common::String AdlEngine::itemRoomStr(uint i) const { + switch (i) { + case IDI_ANY: + return "CARRYING"; + case IDI_VOID_ROOM: + return "GONE"; + case IDI_CUR_ROOM: + return "HERE"; + default: + return Common::String::format("%d", i); + } +} + +Common::String AdlEngine::roomStr(uint i) const { + if (i == IDI_ANY) + return "*"; + else + return Common::String::format("%d", i); +} + +Common::String AdlEngine::verbStr(uint i) const { + if (i == IDI_ANY) + return "*"; + else + return Common::String::format("%d/%s", i, _priVerbs[i - 1].c_str()); +} + +Common::String AdlEngine::nounStr(uint i) const { + if (i == IDI_ANY) + return "*"; + else + return Common::String::format("%d/%s", i, _priNouns[i - 1].c_str()); +} + +Common::String AdlEngine::msgStr(uint i) const { + return Common::String::format("%d/%s", i, toAscii(loadMessage(i)).c_str()); +} + +Common::String AdlEngine::dirStr(Direction dir) const { + static const char *dirs[] = { "NORTH", "SOUTH", "EAST", "WEST", "UP", "DOWN" }; + return dirs[dir]; +} + +} // End of namespace Adl diff --git a/engines/adl/adl.h b/engines/adl/adl.h new file mode 100644 index 0000000000..c9d77fcc62 --- /dev/null +++ b/engines/adl/adl.h @@ -0,0 +1,393 @@ +/* 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. + * + */ + +#ifndef ADL_ADL_H +#define ADL_ADL_H + +#include "common/debug-channels.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/str.h" +#include "common/hashmap.h" +#include "common/hash-str.h" +#include "common/func.h" +#include "common/scummsys.h" + +#include "engines/engine.h" + +#include "audio/mixer.h" +#include "audio/softsynth/pcspk.h" + +#include "adl/console.h" +#include "adl/disk.h" + +namespace Common { +class ReadStream; +class SeekableReadStream; +class File; +struct Event; +} + +namespace Adl { + +class Console; +class Display; +class GraphicsMan; +class Speaker; +struct AdlGameDescription; +class ScriptEnv; + +enum kDebugChannels { + kDebugChannelScript = 1 << 0 +}; + +// Save and restore opcodes +#define IDO_ACT_SAVE 0x0f +#define IDO_ACT_LOAD 0x10 + +#define IDI_CUR_ROOM 0xfc +#define IDI_VOID_ROOM 0xfd +#define IDI_ANY 0xfe + +#define IDI_WORD_SIZE 8 + +enum Direction { + IDI_DIR_NORTH, + IDI_DIR_SOUTH, + IDI_DIR_EAST, + IDI_DIR_WEST, + IDI_DIR_UP, + IDI_DIR_DOWN, + IDI_DIR_TOTAL +}; + +struct Room { + Room() : + description(0), + picture(0), + curPicture(0) { + memset(connections, 0, sizeof(connections)); + } + + byte description; + byte connections[IDI_DIR_TOTAL]; + DataBlockPtr data; + byte picture; + byte curPicture; + bool isFirstTime; +}; + +typedef Common::HashMap<byte, DataBlockPtr> PictureMap; + +typedef Common::Array<byte> Script; + +struct Command { + byte room; + byte verb, noun; + byte numCond, numAct; + Script script; +}; + +class ScriptEnv { +public: + ScriptEnv(const Command &cmd, byte room, byte verb, byte noun) : + _cmd(cmd), _room(room), _verb(verb), _noun(noun), _ip(0) { } + + byte op() const { return _cmd.script[_ip]; } + // We keep this 1-based for easier comparison with the original engine + byte arg(uint i) const { return _cmd.script[_ip + i]; } + void skip(uint i) { _ip += i; } + + bool isMatch() const { + return (_cmd.room == IDI_ANY || _cmd.room == _room) && + (_cmd.verb == IDI_ANY || _cmd.verb == _verb) && + (_cmd.noun == IDI_ANY || _cmd.noun == _noun); + } + + byte getCondCount() const { return _cmd.numCond; } + byte getActCount() const { return _cmd.numAct; } + byte getNoun() const { return _noun; } + const Command &getCommand() const { return _cmd; } + +private: + const Command &_cmd; + const byte _room, _verb, _noun; + byte _ip; +}; + +enum { + IDI_ITEM_NOT_MOVED, + IDI_ITEM_DROPPED, + IDI_ITEM_DOESNT_MOVE +}; + +struct Item { + byte id; + byte noun; + byte room; + byte picture; + bool isLineArt; + Common::Point position; + int state; + byte description; + Common::Array<byte> roomPictures; + bool isOnScreen; +}; + +struct Time { + byte hours, minutes; + + Time() : hours(12), minutes(0) { } +}; + +struct State { + Common::Array<Room> rooms; + Common::List<Item> items; + Common::Array<byte> vars; + + byte room; + uint16 moves; + bool isDark; + Time time; + + State() : room(1), moves(1), isDark(false) { } +}; + +typedef Common::List<Command> Commands; +typedef Common::HashMap<Common::String, uint> WordMap; + +struct RoomData { + Common::String description; + PictureMap pictures; + Commands commands; +}; + +// Opcode debugging macros +#define OP_DEBUG_0(F) do { \ + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript) && op_debug(F)) \ + return 0; \ +} while (0) + +#define OP_DEBUG_1(F, P1) do { \ + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript) && op_debug(F, P1)) \ + return 1; \ +} while (0) + +#define OP_DEBUG_2(F, P1, P2) do { \ + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript) && op_debug(F, P1, P2)) \ + return 2; \ +} while (0) + +#define OP_DEBUG_3(F, P1, P2, P3) do { \ + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript) && op_debug(F, P1, P2, P3)) \ + return 3; \ +} while (0) + +#define OP_DEBUG_4(F, P1, P2, P3, P4) do { \ + if (DebugMan.isDebugChannelEnabled(kDebugChannelScript) && op_debug(F, P1, P2, P3, P4)) \ + return 4; \ +} while (0) + +class AdlEngine : public Engine { +friend class Console; +public: + virtual ~AdlEngine(); + + bool pollEvent(Common::Event &event) const; + +protected: + AdlEngine(OSystem *syst, const AdlGameDescription *gd); + + // Engine + Common::Error loadGameState(int slot); + Common::Error saveGameState(int slot, const Common::String &desc); + + Common::String readString(Common::ReadStream &stream, byte until = 0) const; + Common::String readStringAt(Common::SeekableReadStream &stream, uint offset, byte until = 0) const; + void openFile(Common::File &file, const Common::String &name) const; + + virtual void printString(const Common::String &str) = 0; + virtual Common::String loadMessage(uint idx) const = 0; + virtual void printMessage(uint idx); + virtual Common::String getItemDescription(const Item &item) const; + void delay(uint32 ms) const; + + Common::String inputString(byte prompt = 0) const; + byte inputKey(bool showCursor = true) const; + + virtual Common::String formatVerbError(const Common::String &verb) const; + virtual Common::String formatNounError(const Common::String &verb, const Common::String &noun) const; + void loadWords(Common::ReadStream &stream, WordMap &map, Common::StringArray &pri) const; + void readCommands(Common::ReadStream &stream, Commands &commands); + void checkInput(byte verb, byte noun); + virtual bool isInputValid(byte verb, byte noun, bool &is_any); + virtual bool isInputValid(const Commands &commands, byte verb, byte noun, bool &is_any); + + virtual void setupOpcodeTables(); + virtual void initState(); + virtual byte roomArg(byte room) const; + virtual void advanceClock() { } + + // Opcodes + int o1_isItemInRoom(ScriptEnv &e); + int o1_isMovesGT(ScriptEnv &e); + int o1_isVarEQ(ScriptEnv &e); + int o1_isCurPicEQ(ScriptEnv &e); + int o1_isItemPicEQ(ScriptEnv &e); + + int o1_varAdd(ScriptEnv &e); + int o1_varSub(ScriptEnv &e); + int o1_varSet(ScriptEnv &e); + int o1_listInv(ScriptEnv &e); + int o1_moveItem(ScriptEnv &e); + int o1_setRoom(ScriptEnv &e); + int o1_setCurPic(ScriptEnv &e); + int o1_setPic(ScriptEnv &e); + int o1_printMsg(ScriptEnv &e); + int o1_setLight(ScriptEnv &e); + int o1_setDark(ScriptEnv &e); + int o1_save(ScriptEnv &e); + int o1_restore(ScriptEnv &e); + int o1_restart(ScriptEnv &e); + int o1_quit(ScriptEnv &e); + int o1_placeItem(ScriptEnv &e); + int o1_setItemPic(ScriptEnv &e); + int o1_resetPic(ScriptEnv &e); + template <Direction D> + int o1_goDirection(ScriptEnv &e); + int o1_takeItem(ScriptEnv &e); + int o1_dropItem(ScriptEnv &e); + int o1_setRoomPic(ScriptEnv &e); + + // Graphics + void clearScreen() const; + void drawPic(byte pic, Common::Point pos = Common::Point()) const; + + // Sound + void bell(uint count = 1) const; + + // Game state functions + const Room &getRoom(uint i) const; + Room &getRoom(uint i); + const Room &getCurRoom() const; + Room &getCurRoom(); + const Item &getItem(uint i) const; + Item &getItem(uint i); + byte getVar(uint i) const; + void setVar(uint i, byte value); + virtual void takeItem(byte noun); + void dropItem(byte noun); + bool matchCommand(ScriptEnv &env) const; + void doActions(ScriptEnv &env); + bool doOneCommand(const Commands &commands, byte verb, byte noun); + void doAllCommands(const Commands &commands, byte verb, byte noun); + + // Debug functions + static Common::String toAscii(const Common::String &str); + Common::String itemStr(uint i) const; + Common::String roomStr(uint i) const; + Common::String itemRoomStr(uint i) const; + Common::String verbStr(uint i) const; + Common::String nounStr(uint i) const; + Common::String msgStr(uint i) const; + Common::String dirStr(Direction dir) const; + bool op_debug(const char *fmt, ...) const; + Common::DumpFile *_dumpFile; + + Display *_display; + GraphicsMan *_graphics; + Speaker *_speaker; + + // Opcodes + typedef Common::Functor1<ScriptEnv &, int> Opcode; + Common::Array<const Opcode *> _condOpcodes, _actOpcodes; + // Message strings in data file + Common::Array<DataBlockPtr> _messages; + // Picture data + PictureMap _pictures; + // Dropped item screen offsets + Common::Array<Common::Point> _itemOffsets; + // <room, verb, noun, script> lists + Commands _roomCommands; + Commands _globalCommands; + // Data related to the current room + RoomData _roomData; + + WordMap _verbs; + WordMap _nouns; + Common::StringArray _priVerbs; + Common::StringArray _priNouns; + + struct { + Common::String enterCommand; + Common::String verbError; + Common::String nounError; + Common::String playAgain; + Common::String pressReturn; + Common::String lineFeeds; + } _strings; + + struct { + uint cantGoThere; + uint dontUnderstand; + uint itemDoesntMove; + uint itemNotHere; + uint thanksForPlaying; + } _messageIds; + + // Game state + State _state; + + bool _isRestarting, _isRestoring; + bool _skipOneCommand; + +private: + virtual void runIntro() const { } + virtual void init() = 0; + virtual void initGameState() = 0; + virtual void drawItems() = 0; + virtual void drawItem(Item &item, const Common::Point &pos) = 0; + virtual void loadRoom(byte roomNr) = 0; + virtual void showRoom() = 0; + + // Engine + Common::Error run(); + bool hasFeature(EngineFeature f) const; + bool canLoadGameStateCurrently(); + bool canSaveGameStateCurrently(); + + // Text input + byte convertKey(uint16 ascii) const; + Common::String getLine() const; + Common::String getWord(const Common::String &line, uint &index) const; + void getInput(uint &verb, uint &noun); + + Console *_console; + GUI::Debugger *getDebugger() { return _console; } + const AdlGameDescription *_gameDescription; + byte _saveVerb, _saveNoun, _restoreVerb, _restoreNoun; + bool _canSaveNow, _canRestoreNow; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/adl_v2.cpp b/engines/adl/adl_v2.cpp new file mode 100644 index 0000000000..4fdf796701 --- /dev/null +++ b/engines/adl/adl_v2.cpp @@ -0,0 +1,539 @@ +/* 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/random.h" +#include "common/error.h" + +#include "adl/adl_v2.h" +#include "adl/display.h" +#include "adl/graphics.h" + +namespace Adl { + +AdlEngine_v2::~AdlEngine_v2() { + delete _random; + delete _disk; +} + +AdlEngine_v2::AdlEngine_v2(OSystem *syst, const AdlGameDescription *gd) : + AdlEngine(syst, gd), + _linesPrinted(0), + _disk(nullptr), + _itemRemoved(false), + _roomOnScreen(0), + _picOnScreen(0), + _itemsOnScreen(0) { + _random = new Common::RandomSource("adl"); +} + +typedef Common::Functor1Mem<ScriptEnv &, int, AdlEngine_v2> OpcodeV2; +#define SetOpcodeTable(x) table = &x; +#define Opcode(x) table->push_back(new OpcodeV2(this, &AdlEngine_v2::x)) +#define OpcodeUnImpl() table->push_back(new OpcodeV2(this, 0)) + +void AdlEngine_v2::setupOpcodeTables() { + Common::Array<const Opcode *> *table = 0; + + SetOpcodeTable(_condOpcodes); + // 0x00 + OpcodeUnImpl(); + Opcode(o2_isFirstTime); + Opcode(o2_isRandomGT); + Opcode(o1_isItemInRoom); + // 0x04 + Opcode(o2_isNounNotInRoom); + Opcode(o1_isMovesGT); + Opcode(o1_isVarEQ); + Opcode(o2_isCarryingSomething); + // 0x08 + OpcodeUnImpl(); + Opcode(o1_isCurPicEQ); + Opcode(o1_isItemPicEQ); + + SetOpcodeTable(_actOpcodes); + // 0x00 + OpcodeUnImpl(); + Opcode(o1_varAdd); + Opcode(o1_varSub); + Opcode(o1_varSet); + // 0x04 + Opcode(o1_listInv); + Opcode(o2_moveItem); + Opcode(o1_setRoom); + Opcode(o1_setCurPic); + // 0x08 + Opcode(o1_setPic); + Opcode(o1_printMsg); + Opcode(o1_setLight); + Opcode(o1_setDark); + // 0x0c + Opcode(o2_moveAllItems); + Opcode(o1_quit); + OpcodeUnImpl(); + Opcode(o2_save); + // 0x10 + Opcode(o2_restore); + Opcode(o1_restart); + Opcode(o2_placeItem); + Opcode(o1_setItemPic); + // 0x14 + Opcode(o1_resetPic); + Opcode(o1_goDirection<IDI_DIR_NORTH>); + Opcode(o1_goDirection<IDI_DIR_SOUTH>); + Opcode(o1_goDirection<IDI_DIR_EAST>); + // 0x18 + Opcode(o1_goDirection<IDI_DIR_WEST>); + Opcode(o1_goDirection<IDI_DIR_UP>); + Opcode(o1_goDirection<IDI_DIR_DOWN>); + Opcode(o1_takeItem); + // 0x1c + Opcode(o1_dropItem); + Opcode(o1_setRoomPic); + Opcode(o2_tellTime); + Opcode(o2_setRoomFromVar); + // 0x20 + Opcode(o2_initDisk); +} + +void AdlEngine_v2::initState() { + AdlEngine::initState(); + + _linesPrinted = 0; + _picOnScreen = 0; + _roomOnScreen = 0; + _itemRemoved = false; + _itemsOnScreen = 0; +} + +byte AdlEngine_v2::roomArg(byte room) const { + if (room == IDI_CUR_ROOM) + return _state.room; + return room; +} + +void AdlEngine_v2::advanceClock() { + Time &time = _state.time; + + time.minutes += 5; + + if (time.minutes == 60) { + time.minutes = 0; + + ++time.hours; + + if (time.hours == 13) + time.hours = 1; + } +} + +void AdlEngine_v2::checkTextOverflow(char c) { + if (c != APPLECHAR('\r')) + return; + + ++_linesPrinted; + + if (_linesPrinted < 4) + return; + + _linesPrinted = 0; + _display->updateTextScreen(); + bell(); + + while (true) { + char key = inputKey(false); + + if (shouldQuit()) + return; + + if (key == APPLECHAR('\r')) + break; + + bell(3); + } +} + +Common::String AdlEngine_v2::loadMessage(uint idx) const { + if (_messages[idx]) { + StreamPtr strStream(_messages[idx]->createReadStream()); + return readString(*strStream, 0xff); + } + + return Common::String(); +} + +void AdlEngine_v2::printString(const Common::String &str) { + Common::String s(str); + byte endPos = TEXT_WIDTH - 1; + byte pos = 0; + + while (true) { + while (pos <= endPos && pos != s.size()) { + s.setChar(APPLECHAR(s[pos]), pos); + ++pos; + } + + if (pos == s.size()) + break; + + while (s[pos] != APPLECHAR(' ') && s[pos] != APPLECHAR('\r')) + --pos; + + s.setChar(APPLECHAR('\r'), pos); + endPos = pos + TEXT_WIDTH; + ++pos; + } + + pos = 0; + while (pos != s.size()) { + checkTextOverflow(s[pos]); + _display->printChar(s[pos]); + ++pos; + } + + checkTextOverflow(APPLECHAR('\r')); + _display->printChar(APPLECHAR('\r')); + _display->updateTextScreen(); +} + +void AdlEngine_v2::drawItem(Item &item, const Common::Point &pos) { + item.isOnScreen = true; + StreamPtr stream(_itemPics[item.picture - 1]->createReadStream()); + stream->readByte(); // Skip clear opcode + _graphics->drawPic(*stream, pos); +} + +void AdlEngine_v2::loadRoom(byte roomNr) { + Room &room = getRoom(roomNr); + StreamPtr stream(room.data->createReadStream()); + + uint16 descOffset = stream->readUint16LE(); + uint16 commandOffset = stream->readUint16LE(); + + _roomData.pictures.clear(); + // There's no picture count. The original engine always checks at most + // five pictures. We use the description offset to bound our search. + uint16 picCount = (descOffset - 4) / 5; + + for (uint i = 0; i < picCount; ++i) { + byte nr = stream->readByte(); + _roomData.pictures[nr] = readDataBlockPtr(*stream); + } + + _roomData.description = readStringAt(*stream, descOffset, 0xff); + + _roomData.commands.clear(); + if (commandOffset != 0) { + stream->seek(commandOffset); + readCommands(*stream, _roomData.commands); + } +} + +void AdlEngine_v2::showRoom() { + bool redrawPic = false; + + if (_state.room != _roomOnScreen) { + loadRoom(_state.room); + clearScreen(); + + if (!_state.isDark) + redrawPic = true; + } else { + if (getCurRoom().curPicture != _picOnScreen || _itemRemoved) + redrawPic = true; + } + + if (redrawPic) { + _roomOnScreen = _state.room; + _picOnScreen = getCurRoom().curPicture; + + drawPic(getCurRoom().curPicture); + _itemRemoved = false; + _itemsOnScreen = 0; + + Common::List<Item>::iterator item; + for (item = _state.items.begin(); item != _state.items.end(); ++item) + item->isOnScreen = false; + } + + if (!_state.isDark) + drawItems(); + + _display->updateHiResScreen(); + printString(_roomData.description); + + // FIXME: move to main loop? + _linesPrinted = 0; +} + +void AdlEngine_v2::takeItem(byte noun) { + Common::List<Item>::iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) { + if (item->noun != noun || item->room != _state.room) + continue; + + if (item->state == IDI_ITEM_DOESNT_MOVE) { + printMessage(_messageIds.itemDoesntMove); + return; + } + + if (item->state == IDI_ITEM_DROPPED) { + item->room = IDI_ANY; + _itemRemoved = true; + return; + } + + Common::Array<byte>::const_iterator pic; + for (pic = item->roomPictures.begin(); pic != item->roomPictures.end(); ++pic) { + if (*pic == getCurRoom().curPicture || *pic == IDI_ANY) { + item->room = IDI_ANY; + _itemRemoved = true; + item->state = IDI_ITEM_DROPPED; + return; + } + } + } + + printMessage(_messageIds.itemNotHere); +} + +void AdlEngine_v2::drawItems() { + Common::List<Item>::iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) { + // Skip items not in this room + if (item->room != _state.room) + continue; + + if (item->isOnScreen) + continue; + + if (item->state == IDI_ITEM_DROPPED) { + // Draw dropped item if in normal view + if (getCurRoom().picture == getCurRoom().curPicture) + drawItem(*item, _itemOffsets[_itemsOnScreen++]); + } else { + // Draw fixed item if current view is in the pic list + Common::Array<byte>::const_iterator pic; + + for (pic = item->roomPictures.begin(); pic != item->roomPictures.end(); ++pic) { + if (*pic == getCurRoom().curPicture || *pic == IDI_ANY) { + drawItem(*item, item->position); + break; + } + } + } + } +} + +DataBlockPtr AdlEngine_v2::readDataBlockPtr(Common::ReadStream &f) const { + byte track = f.readByte(); + byte sector = f.readByte(); + byte offset = f.readByte(); + byte size = f.readByte(); + + if (f.eos() || f.err()) + error("Error reading DataBlockPtr"); + + if (track == 0 && sector == 0 && offset == 0 && size == 0) + return DataBlockPtr(); + + return _disk->getDataBlock(track, sector, offset, size); +} + +int AdlEngine_v2::o2_isFirstTime(ScriptEnv &e) { + OP_DEBUG_0("\t&& IS_FIRST_TIME()"); + + bool oldFlag = getCurRoom().isFirstTime; + + getCurRoom().isFirstTime = false; + + if (!oldFlag) + return -1; + + return 0; +} + +int AdlEngine_v2::o2_isRandomGT(ScriptEnv &e) { + OP_DEBUG_1("\t&& RAND() > %d", e.arg(1)); + + byte rnd = _random->getRandomNumber(255); + + if (rnd > e.arg(1)) + return 1; + + return -1; +} + +int AdlEngine_v2::o2_isNounNotInRoom(ScriptEnv &e) { + OP_DEBUG_1("\t&& NO_SUCH_ITEMS_IN_ROOM(%s)", itemRoomStr(e.arg(1)).c_str()); + + Common::List<Item>::const_iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) + if (item->noun == e.getNoun() && (item->room == roomArg(e.arg(1)))) + return -1; + + return 1; +} + +int AdlEngine_v2::o2_isCarryingSomething(ScriptEnv &e) { + OP_DEBUG_0("\t&& IS_CARRYING_SOMETHING()"); + + Common::List<Item>::const_iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) + if (item->room == IDI_ANY) + return 0; + return -1; +} + +int AdlEngine_v2::o2_moveItem(ScriptEnv &e) { + OP_DEBUG_2("\tSET_ITEM_ROOM(%s, %s)", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); + + byte room = roomArg(e.arg(2)); + + Item &item = getItem(e.arg(1)); + + if (item.room == _roomOnScreen) + _picOnScreen = 0; + + // Set items that move from inventory to a room to state "dropped" + if (item.room == IDI_ANY && room != IDI_VOID_ROOM) + item.state = IDI_ITEM_DROPPED; + + item.room = room; + return 2; +} + +int AdlEngine_v2::o2_moveAllItems(ScriptEnv &e) { + OP_DEBUG_2("\tMOVE_ALL_ITEMS(%s, %s)", itemRoomStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); + + byte room1 = roomArg(e.arg(1)); + + if (room1 == _state.room) + _picOnScreen = 0; + + byte room2 = roomArg(e.arg(2)); + + Common::List<Item>::iterator item; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) + if (item->room == room1) { + item->room = room2; + if (room1 == IDI_ANY) + item->state = IDI_ITEM_DROPPED; + } + + return 2; +} + +int AdlEngine_v2::o2_save(ScriptEnv &e) { + OP_DEBUG_0("\tSAVE_GAME()"); + + int slot = askForSlot(_strings_v2.saveInsert); + + if (slot < 0) + return -1; + + saveGameState(slot, ""); + + _display->printString(_strings_v2.saveReplace); + inputString(); + return 0; +} + +int AdlEngine_v2::o2_restore(ScriptEnv &e) { + OP_DEBUG_0("\tRESTORE_GAME()"); + + int slot = askForSlot(_strings_v2.restoreInsert); + + if (slot < 0) + return -1; + + loadGameState(slot); + _isRestoring = false; + + _display->printString(_strings_v2.restoreReplace); + inputString(); + _picOnScreen = 0; + _roomOnScreen = 0; + return 0; +} + +int AdlEngine_v2::o2_placeItem(ScriptEnv &e) { + OP_DEBUG_4("\tPLACE_ITEM(%s, %s, (%d, %d))", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str(), e.arg(3), e.arg(4)); + + Item &item = getItem(e.arg(1)); + + item.room = roomArg(e.arg(2)); + item.position.x = e.arg(3); + item.position.y = e.arg(4); + item.state = IDI_ITEM_NOT_MOVED; + + return 4; +} + +int AdlEngine_v2::o2_tellTime(ScriptEnv &e) { + OP_DEBUG_0("\tTELL_TIME()"); + + Common::String time = _strings_v2.time; + + time.setChar(APPLECHAR('0') + _state.time.hours / 10, 12); + time.setChar(APPLECHAR('0') + _state.time.hours % 10, 13); + time.setChar(APPLECHAR('0') + _state.time.minutes / 10, 15); + time.setChar(APPLECHAR('0') + _state.time.minutes % 10, 16); + + printString(time); + + return 0; +} + +int AdlEngine_v2::o2_setRoomFromVar(ScriptEnv &e) { + OP_DEBUG_1("\tROOM = VAR[%d]", e.arg(1)); + getCurRoom().curPicture = getCurRoom().picture; + _state.room = getVar(e.arg(1)); + return 1; +} + +int AdlEngine_v2::o2_initDisk(ScriptEnv &e) { + OP_DEBUG_0("\tINIT_DISK()"); + + _display->printAsciiString("NOT REQUIRED\r"); + return 0; +} + +int AdlEngine_v2::askForSlot(const Common::String &question) { + while (1) { + _display->printString(question); + + Common::String input = inputString(); + + if (shouldQuit()) + return -1; + + if (input.size() > 0 && input[0] >= APPLECHAR('A') && input[0] <= APPLECHAR('O')) + return input[0] - APPLECHAR('A'); + } +} + +} // End of namespace Adl diff --git a/engines/adl/adl_v2.h b/engines/adl/adl_v2.h new file mode 100644 index 0000000000..f18972b74b --- /dev/null +++ b/engines/adl/adl_v2.h @@ -0,0 +1,95 @@ +/* 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. + * + */ + +#ifndef ADL_ADL_V2_H +#define ADL_ADL_V2_H + +#include "adl/adl.h" + +// Note: this version of ADL redraws only when necessary, but +// this is not currently implemented. + +namespace Common { +class RandomSource; +} + +namespace Adl { + +class AdlEngine_v2 : public AdlEngine { +public: + virtual ~AdlEngine_v2(); + +protected: + AdlEngine_v2(OSystem *syst, const AdlGameDescription *gd); + + // AdlEngine + virtual void setupOpcodeTables(); + virtual void initState(); + byte roomArg(byte room) const; + void advanceClock(); + virtual void printString(const Common::String &str); + virtual Common::String loadMessage(uint idx) const; + void drawItems(); + void drawItem(Item &item, const Common::Point &pos); + void loadRoom(byte roomNr); + virtual void showRoom(); + void takeItem(byte noun); + + virtual DataBlockPtr readDataBlockPtr(Common::ReadStream &f) const; + + void checkTextOverflow(char c); + + int o2_isFirstTime(ScriptEnv &e); + int o2_isRandomGT(ScriptEnv &e); + int o2_isNounNotInRoom(ScriptEnv &e); + int o2_isCarryingSomething(ScriptEnv &e); + + int o2_moveItem(ScriptEnv &e); + int o2_moveAllItems(ScriptEnv &e); + int o2_save(ScriptEnv &e); + int o2_restore(ScriptEnv &e); + int o2_placeItem(ScriptEnv &e); + int o2_tellTime(ScriptEnv &e); + int o2_setRoomFromVar(ScriptEnv &e); + int o2_initDisk(ScriptEnv &e); + + struct { + Common::String time; + Common::String saveInsert, saveReplace; + Common::String restoreInsert, restoreReplace; + } _strings_v2; + + uint _linesPrinted; + DiskImage *_disk; + Common::Array<DataBlockPtr> _itemPics; + bool _itemRemoved; + byte _roomOnScreen, _picOnScreen, _itemsOnScreen; + +private: + int askForSlot(const Common::String &question); + + Common::RandomSource *_random; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/adl_v3.cpp b/engines/adl/adl_v3.cpp new file mode 100644 index 0000000000..623db661bc --- /dev/null +++ b/engines/adl/adl_v3.cpp @@ -0,0 +1,259 @@ +/* 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/random.h" +#include "common/error.h" + +#include "adl/adl_v3.h" +#include "adl/display.h" +#include "adl/graphics.h" + +namespace Adl { + +AdlEngine_v3::AdlEngine_v3(OSystem *syst, const AdlGameDescription *gd) : + AdlEngine_v2(syst, gd), + _curDisk(0) { +} + +Common::String AdlEngine_v3::loadMessage(uint idx) const { + Common::String str = AdlEngine_v2::loadMessage(idx); + + for (uint i = 0; i < str.size(); ++i) { + const char *xorStr = "AVISDURGAN"; + str.setChar(str[i] ^ xorStr[i % strlen(xorStr)], i); + } + + return str; +} + +Common::String AdlEngine_v3::getItemDescription(const Item &item) const { + return _itemDesc[item.id - 1]; +} + +void AdlEngine_v3::applyDiskOffset(byte &track, byte §or) const { + sector += _diskOffsets[_curDisk].sector; + if (sector >= 16) { + sector -= 16; + ++track; + } + + track += _diskOffsets[_curDisk].track; +} + +DataBlockPtr AdlEngine_v3::readDataBlockPtr(Common::ReadStream &f) const { + byte track = f.readByte(); + byte sector = f.readByte(); + byte offset = f.readByte(); + byte size = f.readByte(); + + if (f.eos() || f.err()) + error("Error reading DataBlockPtr"); + + if (track == 0 && sector == 0 && offset == 0 && size == 0) + return DataBlockPtr(); + + applyDiskOffset(track, sector); + + return _disk->getDataBlock(track, sector, offset, size); +} + +typedef Common::Functor1Mem<ScriptEnv &, int, AdlEngine_v3> OpcodeV3; +#define SetOpcodeTable(x) table = &x; +#define Opcode(x) table->push_back(new OpcodeV3(this, &AdlEngine_v3::x)) +#define OpcodeUnImpl() table->push_back(new OpcodeV3(this, 0)) + +void AdlEngine_v3::setupOpcodeTables() { + Common::Array<const Opcode *> *table = 0; + + SetOpcodeTable(_condOpcodes); + // 0x00 + OpcodeUnImpl(); + Opcode(o2_isFirstTime); + Opcode(o2_isRandomGT); + Opcode(o3_isItemInRoom); + // 0x04 + Opcode(o3_isNounNotInRoom); + Opcode(o1_isMovesGT); + Opcode(o1_isVarEQ); + Opcode(o2_isCarryingSomething); + // 0x08 + Opcode(o3_isVarGT); + Opcode(o1_isCurPicEQ); + Opcode(o3_skipOneCommand); + + SetOpcodeTable(_actOpcodes); + // 0x00 + OpcodeUnImpl(); + Opcode(o1_varAdd); + Opcode(o1_varSub); + Opcode(o1_varSet); + // 0x04 + Opcode(o1_listInv); + Opcode(o3_moveItem); + Opcode(o1_setRoom); + Opcode(o1_setCurPic); + // 0x08 + Opcode(o1_setPic); + Opcode(o1_printMsg); + Opcode(o3_dummy); + Opcode(o3_setTextMode); + // 0x0c + Opcode(o2_moveAllItems); + Opcode(o1_quit); + Opcode(o3_dummy); + Opcode(o2_save); + // 0x10 + Opcode(o2_restore); + Opcode(o1_restart); + Opcode(o3_setDisk); + Opcode(o3_dummy); + // 0x14 + Opcode(o1_resetPic); + Opcode(o1_goDirection<IDI_DIR_NORTH>); + Opcode(o1_goDirection<IDI_DIR_SOUTH>); + Opcode(o1_goDirection<IDI_DIR_EAST>); + // 0x18 + Opcode(o1_goDirection<IDI_DIR_WEST>); + Opcode(o1_goDirection<IDI_DIR_UP>); + Opcode(o1_goDirection<IDI_DIR_DOWN>); + Opcode(o1_takeItem); + // 0x1c + Opcode(o1_dropItem); + Opcode(o1_setRoomPic); + Opcode(o3_sound); + OpcodeUnImpl(); + // 0x20 + Opcode(o2_initDisk); +} + +int AdlEngine_v3::o3_isVarGT(ScriptEnv &e) { + OP_DEBUG_2("\t&& VARS[%d] > %d", e.arg(1), e.arg(2)); + + if (getVar(e.arg(1)) > e.arg(2)) + return 2; + + return -1; +} + +int AdlEngine_v3::o3_skipOneCommand(ScriptEnv &e) { + OP_DEBUG_0("\t&& SKIP_ONE_COMMAND()"); + + _skipOneCommand = true; + setVar(2, 0); + + return -1; +} + +// FIXME: Rename "isLineArt" and look at code duplication +int AdlEngine_v3::o3_isItemInRoom(ScriptEnv &e) { + OP_DEBUG_2("\t&& GET_ITEM_ROOM(%s) == %s", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); + + const Item &item = getItem(e.arg(1)); + + if (e.arg(2) != IDI_ANY && item.isLineArt != _curDisk) + return -1; + + if (item.room == roomArg(e.arg(2))) + return 2; + + return -1; +} + +int AdlEngine_v3::o3_isNounNotInRoom(ScriptEnv &e) { + OP_DEBUG_1("\t&& NO_SUCH_ITEMS_IN_ROOM(%s)", itemRoomStr(e.arg(1)).c_str()); + + Common::List<Item>::const_iterator item; + + setVar(24, 0); + + for (item = _state.items.begin(); item != _state.items.end(); ++item) + if (item->noun == e.getNoun()) { + setVar(24, 1); + + if (item->room == roomArg(e.arg(1))) + return -1; + } + + return 1; +} + +int AdlEngine_v3::o3_moveItem(ScriptEnv &e) { + OP_DEBUG_2("\tSET_ITEM_ROOM(%s, %s)", itemStr(e.arg(1)).c_str(), itemRoomStr(e.arg(2)).c_str()); + + byte room = roomArg(e.arg(2)); + + Item &item = getItem(e.arg(1)); + + if (item.room == _roomOnScreen) + _picOnScreen = 0; + + // Set items that move from inventory to a room to state "dropped" + if (item.room == IDI_ANY && room != IDI_VOID_ROOM) + item.state = IDI_ITEM_DROPPED; + + item.room = room; + item.isLineArt = _curDisk; + return 2; +} + +int AdlEngine_v3::o3_dummy(ScriptEnv &e) { + OP_DEBUG_0("\tDUMMY()"); + + return 0; +} + +int AdlEngine_v3::o3_setTextMode(ScriptEnv &e) { + OP_DEBUG_1("\tSET_TEXT_MODE(%d)", e.arg(1)); + + // TODO + // 1: 4-line mode + // 2: 24-line mode + + switch (e.arg(1)) { + case 3: + // We re-use the restarting flag here, to simulate a long jump + _isRestarting = true; + return -1; + } + + return 1; +} + +int AdlEngine_v3::o3_setDisk(ScriptEnv &e) { + OP_DEBUG_2("\tSET_DISK(%d, %d)", e.arg(1), e.arg(2)); + + // TODO + // Arg 1: disk + // Arg 2: room + + return 2; +} + +int AdlEngine_v3::o3_sound(ScriptEnv &e) { + OP_DEBUG_0("\tSOUND()"); + + // TODO + + return 0; +} + +} // End of namespace Adl diff --git a/engines/adl/adl_v3.h b/engines/adl/adl_v3.h new file mode 100644 index 0000000000..61dd5852e7 --- /dev/null +++ b/engines/adl/adl_v3.h @@ -0,0 +1,76 @@ +/* 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. + * + */ + +#ifndef ADL_ADL_V3_H +#define ADL_ADL_V3_H + +#include "adl/adl_v2.h" + +// Note: this version of ADL redraws only when necessary, but +// this is not currently implemented. + +namespace Common { +class RandomSource; +} + +struct DiskOffset { + byte track; + byte sector; +}; + +namespace Adl { + +class AdlEngine_v3 : public AdlEngine_v2 { +public: + virtual ~AdlEngine_v3() { } + +protected: + AdlEngine_v3(OSystem *syst, const AdlGameDescription *gd); + + // AdlEngine + virtual void setupOpcodeTables(); + virtual Common::String loadMessage(uint idx) const; + Common::String getItemDescription(const Item &item) const; + + // AdlEngine_v2 + virtual DataBlockPtr readDataBlockPtr(Common::ReadStream &f) const; + + void applyDiskOffset(byte &track, byte §or) const; + + int o3_isVarGT(ScriptEnv &e); + int o3_isItemInRoom(ScriptEnv &e); + int o3_isNounNotInRoom(ScriptEnv &e); + int o3_skipOneCommand(ScriptEnv &e); + int o3_moveItem(ScriptEnv &e); + int o3_dummy(ScriptEnv &e); + int o3_setTextMode(ScriptEnv &e); + int o3_setDisk(ScriptEnv &e); + int o3_sound(ScriptEnv &e); + + Common::Array<Common::String> _itemDesc; + byte _curDisk; + Common::Array<DiskOffset> _diskOffsets; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/configure.engine b/engines/adl/configure.engine new file mode 100644 index 0000000000..844e2b8e6a --- /dev/null +++ b/engines/adl/configure.engine @@ -0,0 +1,3 @@ +# This file is included from the main "configure" script +# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] +add_engine adl "ADL" no diff --git a/engines/adl/console.cpp b/engines/adl/console.cpp new file mode 100644 index 0000000000..c35e8b02aa --- /dev/null +++ b/engines/adl/console.cpp @@ -0,0 +1,333 @@ +/* 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/debug-channels.h" + +#include "adl/console.h" +#include "adl/display.h" +#include "adl/adl.h" + +namespace Adl { + +Console::Console(AdlEngine *engine) : GUI::Debugger() { + _engine = engine; + + registerCmd("nouns", WRAP_METHOD(Console, Cmd_Nouns)); + registerCmd("verbs", WRAP_METHOD(Console, Cmd_Verbs)); + registerCmd("dump_scripts", WRAP_METHOD(Console, Cmd_DumpScripts)); + registerCmd("valid_cmds", WRAP_METHOD(Console, Cmd_ValidCommands)); + registerCmd("room", WRAP_METHOD(Console, Cmd_Room)); + registerCmd("items", WRAP_METHOD(Console, Cmd_Items)); + registerCmd("give_item", WRAP_METHOD(Console, Cmd_GiveItem)); + registerCmd("vars", WRAP_METHOD(Console, Cmd_Vars)); + registerCmd("var", WRAP_METHOD(Console, Cmd_Var)); +} + +Common::String Console::toAscii(const Common::String &str) { + Common::String ascii(str); + + for (uint i = 0; i < ascii.size(); ++i) + ascii.setChar(ascii[i] & 0x7f, i); + + return ascii; +} + +Common::String Console::toAppleWord(const Common::String &str) { + Common::String apple(str); + + if (apple.size() > IDI_WORD_SIZE) + apple.erase(IDI_WORD_SIZE); + apple.toUppercase(); + + for (uint i = 0; i < apple.size(); ++i) + apple.setChar(APPLECHAR(apple[i]), i); + + while (apple.size() < IDI_WORD_SIZE) + apple += APPLECHAR(' '); + + return apple; +} + +bool Console::Cmd_Verbs(int argc, const char **argv) { + if (argc != 1) { + debugPrintf("Usage: %s\n", argv[0]); + return true; + } + + debugPrintf("Verbs in alphabetical order:\n"); + printWordMap(_engine->_verbs); + return true; +} + +bool Console::Cmd_Nouns(int argc, const char **argv) { + if (argc != 1) { + debugPrintf("Usage: %s\n", argv[0]); + return true; + } + + debugPrintf("Nouns in alphabetical order:\n"); + printWordMap(_engine->_nouns); + return true; +} + +bool Console::Cmd_ValidCommands(int argc, const char **argv) { + if (argc != 1) { + debugPrintf("Usage: %s\n", argv[0]); + return true; + } + + WordMap::const_iterator verb, noun; + bool is_any; + + for (verb = _engine->_verbs.begin(); verb != _engine->_verbs.end(); ++verb) { + for (noun = _engine->_nouns.begin(); noun != _engine->_nouns.end(); ++noun) { + if (_engine->isInputValid(verb->_value, noun->_value, is_any) && !is_any) + debugPrintf("%s %s\n", toAscii(verb->_key).c_str(), toAscii(noun->_key).c_str()); + } + if (_engine->isInputValid(verb->_value, IDI_ANY, is_any)) + debugPrintf("%s *\n", toAscii(verb->_key).c_str()); + } + if (_engine->isInputValid(IDI_ANY, IDI_ANY, is_any)) + debugPrintf("* *\n"); + + return true; +} + +bool Console::Cmd_DumpScripts(int argc, const char **argv) { + if (argc != 1) { + debugPrintf("Usage: %s\n", argv[0]); + return true; + } + + bool oldFlag = DebugMan.isDebugChannelEnabled(kDebugChannelScript); + + DebugMan.enableDebugChannel("Script"); + + _engine->_dumpFile = new Common::DumpFile(); + + for (byte roomNr = 1; roomNr <= _engine->_state.rooms.size(); ++roomNr) { + _engine->loadRoom(roomNr); + if (_engine->_roomData.commands.size() != 0) { + _engine->_dumpFile->open(Common::String::format("%03d.ADL", roomNr).c_str()); + _engine->doAllCommands(_engine->_roomData.commands, IDI_ANY, IDI_ANY); + _engine->_dumpFile->close(); + } + } + _engine->loadRoom(_engine->_state.room); + + _engine->_dumpFile->open("GLOBAL.ADL"); + _engine->doAllCommands(_engine->_globalCommands, IDI_ANY, IDI_ANY); + _engine->_dumpFile->close(); + + _engine->_dumpFile->open("RESPONSE.ADL"); + _engine->doAllCommands(_engine->_roomCommands, IDI_ANY, IDI_ANY); + _engine->_dumpFile->close(); + + delete _engine->_dumpFile; + _engine->_dumpFile = nullptr; + + if (!oldFlag) + DebugMan.disableDebugChannel("Script"); + + return true; +} + +bool Console::Cmd_Room(int argc, const char **argv) { + if (argc > 2) { + debugPrintf("Usage: %s [<new_room>]\n", argv[0]); + return true; + } + + if (argc == 2) { + if (!_engine->_canRestoreNow) { + debugPrintf("Cannot change rooms right now\n"); + return true; + } + + uint roomCount = _engine->_state.rooms.size(); + uint room = strtoul(argv[1], NULL, 0); + if (room < 1 || room > roomCount) { + debugPrintf("Room %u out of valid range [1, %u]\n", room, roomCount); + return true; + } + + _engine->_state.room = room; + _engine->clearScreen(); + _engine->loadRoom(_engine->_state.room); + _engine->showRoom(); + _engine->_display->updateTextScreen(); + _engine->_display->updateHiResScreen(); + } + + debugPrintf("Current room: %u\n", _engine->_state.room); + + return true; +} + +bool Console::Cmd_Items(int argc, const char **argv) { + if (argc != 1) { + debugPrintf("Usage: %s\n", argv[0]); + return true; + } + + Common::List<Item>::const_iterator item; + + for (item = _engine->_state.items.begin(); item != _engine->_state.items.end(); ++item) + printItem(*item); + + return true; +} + +bool Console::Cmd_GiveItem(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Usage: %s <ID | name>\n", argv[0]); + return true; + } + + Common::List<Item>::iterator item; + + char *end; + uint id = strtoul(argv[1], &end, 0); + + if (*end != 0) { + Common::Array<Item *> matches; + + Common::String name = toAppleWord(argv[1]); + + if (!_engine->_nouns.contains(name)) { + debugPrintf("Item '%s' not found\n", argv[1]); + return true; + } + + byte noun = _engine->_nouns[name]; + + for (item = _engine->_state.items.begin(); item != _engine->_state.items.end(); ++item) { + if (item->noun == noun) + matches.push_back(&*item); + } + + if (matches.size() == 0) { + debugPrintf("Item '%s' not found\n", argv[1]); + return true; + } + + if (matches.size() > 1) { + debugPrintf("Multiple matches found, please use item ID:\n"); + for (uint i = 0; i < matches.size(); ++i) + printItem(*matches[i]); + return true; + } + + matches[0]->room = IDI_ANY; + debugPrintf("OK\n"); + return true; + } + + for (item = _engine->_state.items.begin(); item != _engine->_state.items.end(); ++item) + if (item->id == id) { + item->room = IDI_ANY; + debugPrintf("OK\n"); + return true; + } + + debugPrintf("Item %i not found\n", id); + return true; +} + +bool Console::Cmd_Vars(int argc, const char **argv) { + if (argc != 1) { + debugPrintf("Usage: %s\n", argv[0]); + return true; + } + + Common::StringArray vars; + for (uint i = 0; i < _engine->_state.vars.size(); ++i) + vars.push_back(Common::String::format("%3d: %3d", i, _engine->_state.vars[i])); + + debugPrintf("Variables:\n"); + debugPrintColumns(vars); + + return true; +} + +bool Console::Cmd_Var(int argc, const char **argv) { + if (argc < 2 || argc > 3) { + debugPrintf("Usage: %s <index> [<value>]\n", argv[0]); + return true; + } + + uint varCount = _engine->_state.vars.size(); + uint var = strtoul(argv[1], NULL, 0); + + if (var >= varCount) { + debugPrintf("Variable %u out of valid range [0, %u]\n", var, varCount - 1); + return true; + } + + if (argc == 3) { + uint value = strtoul(argv[2], NULL, 0); + _engine->_state.vars[var] = value; + } + + debugPrintf("%3d: %3d\n", var, _engine->_state.vars[var]); + + return true; +} + +void Console::printItem(const Item &item) { + Common::String name, desc, state; + + if (item.noun > 0) + name = _engine->_priNouns[item.noun - 1]; + + desc = toAscii(_engine->getItemDescription(item)); + if (desc.lastChar() == '\r') + desc.deleteLastChar(); + + switch (item.state) { + case IDI_ITEM_NOT_MOVED: + state = "PLACED"; + break; + case IDI_ITEM_DROPPED: + state = "DROPPED"; + break; + case IDI_ITEM_DOESNT_MOVE: + state = "FIXED"; + break; + } + + debugPrintf("%3d %s %-30s %-10s %-8s (%3d, %3d)\n", item.id, name.c_str(), desc.c_str(), _engine->itemRoomStr(item.room).c_str(), state.c_str(), item.position.x, item.position.y); +} + +void Console::printWordMap(const WordMap &wordMap) { + Common::StringArray words; + WordMap::const_iterator verb; + + for (verb = wordMap.begin(); verb != wordMap.end(); ++verb) + words.push_back(Common::String::format("%s: %3d", toAscii(verb->_key).c_str(), wordMap[verb->_key])); + + Common::sort(words.begin(), words.end()); + + debugPrintColumns(words); +} + +} // End of namespace Adl diff --git a/engines/adl/console.h b/engines/adl/console.h new file mode 100644 index 0000000000..a8c6adc1cc --- /dev/null +++ b/engines/adl/console.h @@ -0,0 +1,65 @@ +/* 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. + * + */ + +#ifndef ADL_CONSOLE_H +#define ADL_CONSOLE_H + +#include "gui/debugger.h" + +#include "common/hashmap.h" + +namespace Common { +class String; +} + +namespace Adl { + +class AdlEngine; +struct Item; + +class Console : public GUI::Debugger { +public: + Console(AdlEngine *engine); + + static Common::String toAscii(const Common::String &str); + static Common::String toAppleWord(const Common::String &str); + +private: + bool Cmd_Nouns(int argc, const char **argv); + bool Cmd_Verbs(int argc, const char **argv); + bool Cmd_DumpScripts(int argc, const char **argv); + bool Cmd_ValidCommands(int argc, const char **argv); + bool Cmd_Room(int argc, const char **argv); + bool Cmd_Items(int argc, const char **argv); + bool Cmd_GiveItem(int argc, const char **argv); + bool Cmd_Vars(int argc, const char **argv); + bool Cmd_Var(int argc, const char **argv); + + void printItem(const Item &item); + void printWordMap(const Common::HashMap<Common::String, uint> &wordMap); + + AdlEngine *_engine; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/detection.cpp b/engines/adl/detection.cpp new file mode 100644 index 0000000000..12405e7c5e --- /dev/null +++ b/engines/adl/detection.cpp @@ -0,0 +1,328 @@ +/* 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/savefile.h" +#include "common/translation.h" + +#include "graphics/thumbnail.h" + +#include "engines/advancedDetector.h" + +#include "adl/detection.h" + +namespace Adl { + +#define GAMEOPTION_COLOR GUIO_GAMEOPTIONS1 +#define GAMEOPTION_SCANLINES GUIO_GAMEOPTIONS2 +#define GAMEOPTION_MONO GUIO_GAMEOPTIONS3 + +static const ADExtraGuiOptionsMap optionsList[] = { + { + GAMEOPTION_COLOR, + { + _s("Color mode"), + _s("Use color graphics"), + "color", + false + } + }, + + { + GAMEOPTION_MONO, + { + _s("Color mode"), + _s("Use color graphics"), + "color", + true + } + }, + + { + GAMEOPTION_SCANLINES, + { + _s("Scanlines"), + _s("Show scanlines"), + "scanlines", + false + } + }, + + AD_EXTRA_GUI_OPTIONS_TERMINATOR +}; + +static const PlainGameDescriptor adlGames[] = { + { "hires0", "Hi-Res Adventure #0: Mission Asteroid" }, + { "hires1", "Hi-Res Adventure #1: Mystery House" }, + { "hires2", "Hi-Res Adventure #2: Wizard and the Princess" }, + { "hires6", "Hi-Res Adventure #6: The Dark Crystal" }, + { 0, 0 } +}; + +static const AdlGameDescription gameDescriptions[] = { + { // Hi-Res Adventure #1: Mystery House - Apple II - 1987 PD release - Plain files + { + "hires1", 0, + { + { "ADVENTURE", 0, "22d9e63a11d69fa033ba1738715ad09a", 29952 }, + { "AUTO LOAD OBJ", 0, "23bfccfe9fcff9b22cf6c41bde9078ac", 12291 }, + { "MYSTERY.HELLO", 0, "2289b7fea300b506e902a4c597968369", 836 }, + AD_LISTEND + }, + Common::EN_ANY, + Common::kPlatformApple2GS, // FIXME + ADGF_UNSTABLE, + GUIO2(GAMEOPTION_COLOR, GAMEOPTION_SCANLINES) + }, + GAME_TYPE_HIRES1 + }, + { // Hi-Res Adventure #1: Mystery House - Apple II - 1987 PD release - .DSK format + { + "hires1", 0, + { + { "MYSTHOUS.DSK", 0, "34ba05e62bf51404c4475c349ca48921", 143360 }, + AD_LISTEND + }, + Common::EN_ANY, + Common::kPlatformApple2GS, // FIXME + ADGF_UNSTABLE, + GUIO2(GAMEOPTION_COLOR, GAMEOPTION_SCANLINES) + }, + GAME_TYPE_HIRES1 + }, + { // Hi-Res Adventure #2: Wizard and the Princess - Apple II - Roberta Williams Anthology + { + "hires2", 0, + { + { "WIZARD.DSK", 0, "816fdfc35e25496213c8db40ecf26569", 143360 }, + AD_LISTEND + }, + Common::EN_ANY, + Common::kPlatformApple2GS, // FIXME + ADGF_UNSTABLE, + GUIO2(GAMEOPTION_MONO, GAMEOPTION_SCANLINES) + }, + GAME_TYPE_HIRES2 + }, + { // Hi-Res Adventure #0: Mission Asteroid - Apple II - Roberta Williams Anthology + { + "hires0", 0, + { + { "MISSION.NIB", 0, "b158f6f79681d4edd651e1932f9e01d7", 232960 }, + AD_LISTEND + }, + Common::EN_ANY, + Common::kPlatformApple2GS, // FIXME + ADGF_UNSTABLE, + GUIO2(GAMEOPTION_MONO, GAMEOPTION_SCANLINES) + }, + GAME_TYPE_HIRES0 + }, + { // Hi-Res Adventure #6: The Dark Crystal - Apple II - Roberta Williams Anthology + { + "hires6", 0, + { + { "DARK1A.DSK", 0, "00c2646d6943d1405717332a6f42d493", 143360 }, + { "DARK2A.NIB", 0, "271eb92db107e8d5829437f8ba77991e", 232960 }, + { "DARK1B.NIB", 0, "dbedd736617343ade0e6bead8bf2b10c", 232960 }, + { "DARK2B.NIB", 0, "cb72044a9b391c4285f4752f746bea2e", 232960 }, + AD_LISTEND + }, + Common::EN_ANY, + Common::kPlatformApple2GS, // FIXME + ADGF_UNSTABLE, + GUIO2(GAMEOPTION_MONO, GAMEOPTION_SCANLINES) + }, + GAME_TYPE_HIRES6 + }, + { AD_TABLE_END_MARKER, GAME_TYPE_NONE } +}; + +class AdlMetaEngine : public AdvancedMetaEngine { +public: + AdlMetaEngine() : AdvancedMetaEngine(gameDescriptions, sizeof(AdlGameDescription), adlGames, optionsList) { } + + const char *getName() const { + return "ADL"; + } + + const char *getOriginalCopyright() const { + return "Copyright (C) Sierra On-Line"; + } + + bool hasFeature(MetaEngineFeature f) const; + SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const; + int getMaximumSaveSlot() const { return 'O' - 'A'; } + SaveStateList listSaves(const char *target) const; + void removeSaveState(const char *target, int slot) const; + + bool createInstance(OSystem *syst, Engine **engine, const ADGameDescription *gd) const; +}; + +bool AdlMetaEngine::hasFeature(MetaEngineFeature f) const { + switch(f) { + case kSupportsListSaves: + case kSupportsLoadingDuringStartup: + case kSupportsDeleteSave: + case kSavesSupportMetaInfo: + case kSavesSupportThumbnail: + case kSavesSupportCreationDate: + case kSavesSupportPlayTime: + return true; + default: + return false; + } +} + +SaveStateDescriptor AdlMetaEngine::querySaveMetaInfos(const char *target, int slot) const { + Common::String fileName = Common::String::format("%s.s%02d", target, slot); + Common::InSaveFile *inFile = g_system->getSavefileManager()->openForLoading(fileName); + + if (!inFile) + return SaveStateDescriptor(); + + if (inFile->readUint32BE() != MKTAG('A', 'D', 'L', ':')) { + delete inFile; + return SaveStateDescriptor(); + } + + byte saveVersion = inFile->readByte(); + if (saveVersion != SAVEGAME_VERSION) { + delete inFile; + return SaveStateDescriptor(); + } + + char name[SAVEGAME_NAME_LEN] = { }; + inFile->read(name, sizeof(name) - 1); + inFile->readByte(); + + if (inFile->eos() || inFile->err()) { + delete inFile; + return SaveStateDescriptor(); + } + + SaveStateDescriptor sd(slot, name); + + int year = inFile->readUint16BE(); + int month = inFile->readByte(); + int day = inFile->readByte(); + sd.setSaveDate(year + 1900, month + 1, day); + + int hour = inFile->readByte(); + int minutes = inFile->readByte(); + sd.setSaveTime(hour, minutes); + + uint32 playTime = inFile->readUint32BE(); + sd.setPlayTime(playTime); + + if (inFile->eos() || inFile->err()) { + delete inFile; + return SaveStateDescriptor(); + } + + Graphics::Surface *const thumbnail = Graphics::loadThumbnail(*inFile); + sd.setThumbnail(thumbnail); + + delete inFile; + return sd; +} + +SaveStateList AdlMetaEngine::listSaves(const char *target) const { + Common::SaveFileManager *saveFileMan = g_system->getSavefileManager(); + Common::StringArray files = saveFileMan->listSavefiles(Common::String(target) + ".s##"); + + SaveStateList saveList; + + for (uint i = 0; i < files.size(); ++i) { + const Common::String &fileName = files[i]; + Common::InSaveFile *inFile = saveFileMan->openForLoading(fileName); + if (!inFile) { + warning("Cannot open save file '%s'", fileName.c_str()); + continue; + } + + if (inFile->readUint32BE() != MKTAG('A', 'D', 'L', ':')) { + warning("No header found in '%s'", fileName.c_str()); + delete inFile; + continue; + } + + byte saveVersion = inFile->readByte(); + if (saveVersion != SAVEGAME_VERSION) { + warning("Unsupported save game version %i found in '%s'", saveVersion, fileName.c_str()); + delete inFile; + continue; + } + + char name[SAVEGAME_NAME_LEN] = { }; + inFile->read(name, sizeof(name) - 1); + delete inFile; + + int slotNum = atoi(fileName.c_str() + fileName.size() - 2); + SaveStateDescriptor sd(slotNum, name); + saveList.push_back(sd); + } + + // Sort saves based on slot number. + Common::sort(saveList.begin(), saveList.end(), SaveStateDescriptorSlotComparator()); + return saveList; +} + +void AdlMetaEngine::removeSaveState(const char *target, int slot) const { + Common::String fileName = Common::String::format("%s.s%02d", target, slot); + g_system->getSavefileManager()->removeSavefile(fileName); +} + +Engine *HiRes1Engine_create(OSystem *syst, const AdlGameDescription *gd); +Engine *HiRes2Engine_create(OSystem *syst, const AdlGameDescription *gd); +Engine *HiRes6Engine_create(OSystem *syst, const AdlGameDescription *gd); + +bool AdlMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *gd) const { + if (!gd) + return false; + + const AdlGameDescription *adlGd = (const AdlGameDescription *)gd; + + switch (adlGd->gameType) { + case GAME_TYPE_HIRES1: + *engine = HiRes1Engine_create(syst, adlGd); + break; + case GAME_TYPE_HIRES2: + *engine = HiRes2Engine_create(syst, adlGd); + break; + case GAME_TYPE_HIRES6: + *engine = HiRes6Engine_create(syst, adlGd); + break; + default: + error("Unknown GameType"); + } + + return true; +} + +} // End of namespace Adl + +#if PLUGIN_ENABLED_DYNAMIC(ADL) + REGISTER_PLUGIN_DYNAMIC(ADL, PLUGIN_TYPE_ENGINE, Adl::AdlMetaEngine); +#else + REGISTER_PLUGIN_STATIC(ADL, PLUGIN_TYPE_ENGINE, Adl::AdlMetaEngine); +#endif diff --git a/engines/adl/detection.h b/engines/adl/detection.h new file mode 100644 index 0000000000..533466c094 --- /dev/null +++ b/engines/adl/detection.h @@ -0,0 +1,48 @@ +/* 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. + * + */ + +#ifndef ADL_DETECTION_H +#define ADL_DETECTION_H + +#include "engines/advancedDetector.h" + +namespace Adl { + +#define SAVEGAME_VERSION 0 +#define SAVEGAME_NAME_LEN 32 + +enum GameType { + GAME_TYPE_NONE, + GAME_TYPE_HIRES0, + GAME_TYPE_HIRES1, + GAME_TYPE_HIRES2, + GAME_TYPE_HIRES6 +}; + +struct AdlGameDescription { + ADGameDescription desc; + GameType gameType; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/disk.cpp b/engines/adl/disk.cpp new file mode 100644 index 0000000000..214f76aeae --- /dev/null +++ b/engines/adl/disk.cpp @@ -0,0 +1,460 @@ +/* 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/stream.h" +#include "common/substream.h" +#include "common/memstream.h" + +#include "adl/disk.h" + +namespace Adl { + +const DataBlockPtr DiskImage_DSK::getDataBlock(uint track, uint sector, uint offset, uint size) const { + return Common::SharedPtr<DiskImage::DataBlock>(new DiskImage::DataBlock(this, track, sector, offset, size)); +} + +Common::SeekableReadStream *DiskImage_DSK::createReadStream(uint track, uint sector, uint offset, uint size) const { + _f->seek((track * _sectorsPerTrack + sector) * _bytesPerSector + offset); + Common::SeekableReadStream *stream = _f->readStream(size * _bytesPerSector + _bytesPerSector - offset); + + if (_f->eos() || _f->err()) + error("Error reading disk image"); + + return stream; +} + +bool DiskImage_DSK::open(const Common::String &filename) { + assert(!_f->isOpen()); + + if (!_f->open(filename)) + return false; + + uint filesize = _f->size(); + switch (filesize) { + case 143360: + _tracks = 35; + _sectorsPerTrack = 16; + _bytesPerSector = 256; + break; + default: + warning("Unrecognized disk image '%s' of size %d bytes", filename.c_str(), filesize); + return false; + } + + return true; +} + +const DataBlockPtr DiskImage_NIB::getDataBlock(uint track, uint sector, uint offset, uint size) const { + return Common::SharedPtr<DiskImage::DataBlock>(new DiskImage::DataBlock(this, track, sector, offset, size)); +} + +Common::SeekableReadStream *DiskImage_NIB::createReadStream(uint track, uint sector, uint offset, uint size) const { + _memStream->seek((track * _sectorsPerTrack + sector) * _bytesPerSector + offset); + Common::SeekableReadStream *stream = _memStream->readStream(size * _bytesPerSector + _bytesPerSector - offset); + + if (_memStream->eos() || _memStream->err()) + error("Error reading NIB image"); + + return stream; +} + +// 4-and-4 encoding (odd-even) +static uint8 read44(Common::SeekableReadStream *f) { + // 1s in the other fields, so we can just AND + uint8 ret = f->readByte(); + return ((ret << 1) | 1) & f->readByte(); +} + +bool DiskImage_NIB::open(const Common::String &filename) { + assert(!_f->isOpen()); + + if (!_f->open(filename)) + return false; + + uint filesize = _f->size(); + switch (filesize) { + case 232960: + _tracks = 35; + _sectorsPerTrack = 16; // we always pad it out + _bytesPerSector = 256; + break; + default: + error("Unrecognized NIB image '%s' of size %d bytes", filename.c_str(), filesize); + } + + // starting at 0xaa, 32 is invalid (see below) + const byte c_5and3_lookup[] = { 32, 0, 32, 1, 2, 3, 32, 32, 32, 32, 32, 4, 5, 6, 32, 32, 7, 8, 32, 9, 10, 11, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 12, 13, 32, 32, 14, 15, 32, 16, 17, 18, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 19, 20, 32, 21, 22, 23, 32, 32, 32, 32, 32, 24, 25, 26, 32, 32, 27, 28, 32, 29, 30, 31 }; + // starting at 0x96, 64 is invalid (see below) + const byte c_6and2_lookup[] = { 0, 1, 64, 64, 2, 3, 64, 4, 5, 6, 64, 64, 64, 64, 64, 64, 7, 8, 64, 64, 64, 9, 10, 11, 12, 13, 64, 64, 14, 15, 16, 17, 18, 19, 64, 20, 21, 22, 23, 24, 25, 26, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 27, 64, 28, 29, 30, 64, 64, 64, 31, 64, 64, 32, 33, 64, 34, 35, 36, 37, 38, 39, 40, 64, 64, 64, 64, 64, 41, 42, 43, 64, 44, 45, 46, 47, 48, 49, 50, 64, 64, 51, 52, 53, 54, 55, 56, 64, 57, 58, 59, 60, 61, 62, 63 }; + + uint32 diskSize = _tracks * _sectorsPerTrack * _bytesPerSector; + byte *diskImage = (byte *)calloc(diskSize, 1); + _memStream = new Common::MemoryReadStream(diskImage, diskSize, DisposeAfterUse::YES); + + bool sawAddress = false; + uint8 volNo, track, sector; + bool newStyle; + + while (_f->pos() < _f->size()) { + // Read until we find two sync bytes. + if (_f->readByte() != 0xd5 || _f->readByte() != 0xaa) + continue; + + byte prologue = _f->readByte(); + + if (sawAddress && (prologue == 0xb5 || prologue == 0x96)) { + warning("NIB: data for %02x/%02x/%02x missing", volNo, track, sector); + sawAddress = false; + } + + if (!sawAddress) { + sawAddress = true; + newStyle = false; + + // We should always find the address field first. + if (prologue != 0xb5) { + // Accept a DOS 3.3(?) header at the start. + if (prologue == 0x96) { + newStyle = true; + } else { + error("unknown NIB field prologue %02x", prologue); + } + } + + volNo = read44(_f); + track = read44(_f); + sector = read44(_f); + uint8 checksum = read44(_f); + if ((volNo ^ track ^ sector) != checksum) + error("invalid NIB checksum"); + + // FIXME: This is a hires0/hires2-specific hack. + if (volNo == 0xfe) { + if (track == 1) + track = 2; + else if (track == 2) + track = 1; + } + + // Epilogue is de/aa plus a gap, but we don't care. + continue; + } + + sawAddress = false; + + // We should always find the data field after an address field. + // TODO: we ignore volNo? + byte *output = diskImage + (track * _sectorsPerTrack + sector) * _bytesPerSector; + + if (newStyle) { + // We hardcode the DOS 3.3 mapping here. TODO: Do we also need raw/prodos? + int raw2dos[16] = { 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 }; + sector = raw2dos[sector]; + output = diskImage + (track * _sectorsPerTrack + sector) * _bytesPerSector; + + // 6-and-2 uses 342 on-disk bytes + byte inbuffer[342]; + _f->read(inbuffer, 342); + + byte oldVal = 0; + for (uint n = 0; n < 342; ++n) { + // expand + assert(inbuffer[n] >= 0x96); // corrupt file (TODO: assert?) + byte val = c_6and2_lookup[inbuffer[n] - 0x96]; + if (val == 0x40) { + error("NIB: invalid nibble value %02x", inbuffer[n]); + } + // undo checksum + oldVal = val ^ oldVal; + inbuffer[n] = oldVal; + } + + byte checksum = _f->readByte(); + if (checksum < 0x96 || oldVal != c_6and2_lookup[checksum - 0x96]) + warning("NIB: checksum mismatch @ (%x, %x)", track, sector); + + for (uint n = 0; n < 256; ++n) { + output[n] = inbuffer[86 + n] << 2; + if (n < 86) { // use first pair of bits + output[n] |= ((inbuffer[n] & 1) << 1); + output[n] |= ((inbuffer[n] & 2) >> 1); + } else if (n < 86*2) { // second pair + output[n] |= ((inbuffer[n-86] & 4) >> 1); + output[n] |= ((inbuffer[n-86] & 8) >> 3); + } else { // third pair + output[n] |= ((inbuffer[n-86*2] & 0x10) >> 3); + output[n] |= ((inbuffer[n-86*2] & 0x20) >> 5); + } + } + } else { + // 5-and-3 uses 410 on-disk bytes, decoding to just over 256 bytes + byte inbuffer[410]; + _f->read(inbuffer, 410); + + bool truncated = false; + byte oldVal = 0; + for (uint n = 0; n < 410; ++n) { + // expand + assert(inbuffer[n] >= 0xaa); // corrupt file (TODO: assert?) + if (inbuffer[n] == 0xd5) { + // Early end of block. + truncated = true; + _f->seek(-(410 - n), SEEK_CUR); + warning("NIB: early end of block @ 0x%x (%x, %x)", _f->pos(), track, sector); + break; + } + byte val = c_5and3_lookup[inbuffer[n] - 0xaa]; + if (val == 0x20) { + // Badly-encoded nibbles, stop trying to decode here. + truncated = true; + warning("NIB: bad nibble %02x @ 0x%x (%x, %x)", inbuffer[n], _f->pos(), track, sector); + _f->seek(-(410 - n), SEEK_CUR); + break; + } + // undo checksum + oldVal = val ^ oldVal; + inbuffer[n] = oldVal; + } + if (!truncated) { + byte checksum = _f->readByte(); + if (checksum < 0xaa || oldVal != c_5and3_lookup[checksum - 0xaa]) + warning("NIB: checksum mismatch @ (%x, %x)", track, sector); + } + + // 8 bytes of nibbles expand to 5 bytes + // so we have 51 of these batches (255 bytes), plus 2 bytes of 'leftover' nibbles for byte 256 + for (uint n = 0; n < 51; ++n) { + // e.g. figure 3.18 of Beneath Apple DOS + byte lowbits1 = inbuffer[51*3 - n]; + byte lowbits2 = inbuffer[51*2 - n]; + byte lowbits3 = inbuffer[51*1 - n]; + byte lowbits4 = (lowbits1 & 2) << 1 | (lowbits2 & 2) | (lowbits3 & 2) >> 1; + byte lowbits5 = (lowbits1 & 1) << 2 | (lowbits2 & 1) << 1 | (lowbits3 & 1); + output[250 - 5*n] = (inbuffer[n + 51*3 + 1] << 3) | ((lowbits1 >> 2) & 0x7); + output[251 - 5*n] = (inbuffer[n + 51*4 + 1] << 3) | ((lowbits2 >> 2) & 0x7); + output[252 - 5*n] = (inbuffer[n + 51*5 + 1] << 3) | ((lowbits3 >> 2) & 0x7); + output[253 - 5*n] = (inbuffer[n + 51*6 + 1] << 3) | lowbits4; + output[254 - 5*n] = (inbuffer[n + 51*7 + 1] << 3) | lowbits5; + } + output[255] = (inbuffer[409] << 3) | (inbuffer[0] & 0x7); + } + } + + return true; +} + +const DataBlockPtr Files_Plain::getDataBlock(const Common::String &filename, uint offset) const { + return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset)); +} + +Common::SeekableReadStream *Files_Plain::createReadStream(const Common::String &filename, uint offset) const { + Common::File *f(new Common::File()); + + if (!f->open(filename)) + error("Failed to open '%s'", filename.c_str()); + + if (offset == 0) + return f; + else + return new Common::SeekableSubReadStream(f, offset, f->size(), DisposeAfterUse::YES); +} + +Files_DOS33::~Files_DOS33() { + delete _disk; +} + +Files_DOS33::Files_DOS33() : + _disk(nullptr) { +} + +void Files_DOS33::readSectorList(TrackSector start, Common::Array<TrackSector> &list) { + TrackSector index = start; + + while (index.track != 0) { + Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(index.track, index.sector)); + + stream->readByte(); + index.track = stream->readByte(); + index.sector = stream->readByte(); + + stream->seek(9, SEEK_CUR); + + // This only handles sequential files + TrackSector ts; + ts.track = stream->readByte(); + ts.sector = stream->readByte(); + + while (ts.track != 0) { + list.push_back(ts); + + ts.track = stream->readByte(); + ts.sector = stream->readByte(); + + if (stream->err()) + error("Error reading sector list"); + + if (stream->eos()) + break; + } + } +} + +void Files_DOS33::readVTOC() { + Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(0x11, 0x00)); + stream->readByte(); + byte track = stream->readByte(); + byte sector = stream->readByte(); + + while (track != 0) { + char name[kFilenameLen + 1] = { }; + + stream.reset(_disk->createReadStream(track, sector)); + stream->readByte(); + track = stream->readByte(); + sector = stream->readByte(); + stream->seek(8, SEEK_CUR); + + for (uint i = 0; i < 7; ++i) { + TOCEntry entry; + TrackSector sectorList; + sectorList.track = stream->readByte(); + sectorList.sector = stream->readByte(); + entry.type = stream->readByte(); + stream->read(name, kFilenameLen); + + // Convert to ASCII + for (uint j = 0; j < kFilenameLen; j++) + name[j] &= 0x7f; + + // Strip trailing spaces + for (int j = kFilenameLen - 1; j >= 0; --j) { + if (name[j] == ' ') + name[j] = 0; + else + break; + } + + entry.totalSectors = stream->readUint16BE(); + + if (sectorList.track != 0) { + readSectorList(sectorList, entry.sectors); + _toc[name] = entry; + } + } + } +} + +const DataBlockPtr Files_DOS33::getDataBlock(const Common::String &filename, uint offset) const { + return Common::SharedPtr<Files::DataBlock>(new Files::DataBlock(this, filename, offset)); +} + +Common::SeekableReadStream *Files_DOS33::createReadStreamText(const TOCEntry &entry) const { + byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize); + byte *p = buf; + + for (uint i = 0; i < entry.sectors.size(); ++i) { + Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[i].track, entry.sectors[i].sector)); + + assert(stream->size() == kSectorSize); + + while (true) { + byte textChar = stream->readByte(); + + if (stream->eos() || textChar == 0) + break; + + if (stream->err()) + error("Error reading text file"); + + *p++ = textChar; + } + } + + return new Common::MemoryReadStream(buf, p - buf, DisposeAfterUse::YES); +} + +Common::SeekableReadStream *Files_DOS33::createReadStreamBinary(const TOCEntry &entry) const { + byte *buf = (byte *)malloc(entry.sectors.size() * kSectorSize); + + Common::ScopedPtr<Common::SeekableReadStream> stream(_disk->createReadStream(entry.sectors[0].track, entry.sectors[0].sector)); + + if (entry.type == kFileTypeBinary) + stream->readUint16LE(); // Skip start address + + uint16 size = stream->readUint16LE(); + uint16 offset = 0; + uint16 sectorIdx = 1; + + while (true) { + offset += stream->read(buf + offset, size - offset); + + if (offset == size) + break; + + if (stream->err()) + error("Error reading binary file"); + + assert(stream->eos()); + + if (sectorIdx == entry.sectors.size()) + error("Not enough sectors for binary file size"); + + stream.reset(_disk->createReadStream(entry.sectors[sectorIdx].track, entry.sectors[sectorIdx].sector)); + ++sectorIdx; + } + + return new Common::MemoryReadStream(buf, size, DisposeAfterUse::YES); +} + +Common::SeekableReadStream *Files_DOS33::createReadStream(const Common::String &filename, uint offset) const { + if (!_toc.contains(filename)) + error("Failed to locate '%s'", filename.c_str()); + + const TOCEntry &entry = _toc[filename]; + + Common::SeekableReadStream *stream; + + switch(entry.type) { + case kFileTypeText: + stream = createReadStreamText(entry); + break; + case kFileTypeAppleSoft: + case kFileTypeBinary: + stream = createReadStreamBinary(entry); + break; + default: + error("Unsupported file type %i", entry.type); + } + + return new Common::SeekableSubReadStream(stream, offset, stream->size(), DisposeAfterUse::YES); +} + +bool Files_DOS33::open(const Common::String &filename) { + _disk = new DiskImage_DSK(); + if (!_disk->open(filename)) + return false; + + readVTOC(); + return true; +} + +} // End of namespace Adl diff --git a/engines/adl/disk.h b/engines/adl/disk.h new file mode 100644 index 0000000000..43b9e387ba --- /dev/null +++ b/engines/adl/disk.h @@ -0,0 +1,188 @@ +/* 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/ptr.h" +#include "common/file.h" +#include "common/debug.h" + +#ifndef ADL_DISK_H +#define ADL_DISK_H + +namespace Common { +class SeekableReadStream; +class String; +} + +namespace Adl { + +class DataBlock { +public: + virtual ~DataBlock() { } + + virtual Common::SeekableReadStream *createReadStream() const = 0; +}; + +typedef Common::SharedPtr<DataBlock> DataBlockPtr; +typedef Common::ScopedPtr<Common::SeekableReadStream> StreamPtr; + +class Files { +public: + virtual ~Files() { } + + virtual const DataBlockPtr getDataBlock(const Common::String &filename, uint offset = 0) const = 0; + virtual Common::SeekableReadStream *createReadStream(const Common::String &filename, uint offset = 0) const = 0; + +protected: + class DataBlock : public Adl::DataBlock { + public: + DataBlock(const Files *files, const Common::String &filename, uint offset) : + _files(files), + _filename(filename), + _offset(offset) { } + + Common::SeekableReadStream *createReadStream() const { + return _files->createReadStream(_filename, _offset); + } + + private: + const Common::String _filename; + uint _offset; + const Files *_files; + }; +}; + +class DiskImage { +public: + DiskImage() : + _tracks(0), + _sectorsPerTrack(0), + _bytesPerSector(0) { + _f = new Common::File(); + } + + virtual ~DiskImage() { + delete _f; + } + + virtual bool open(const Common::String &filename) = 0; + virtual const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const = 0; + virtual Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0) const = 0; + +protected: + class DataBlock : public Adl::DataBlock { + public: + DataBlock(const DiskImage *disk, uint track, uint sector, uint offset, uint size) : + _track(track), + _sector(sector), + _offset(offset), + _size(size), + _disk(disk) { } + + Common::SeekableReadStream *createReadStream() const { + return _disk->createReadStream(_track, _sector, _offset, _size); + } + + private: + uint _track, _sector, _offset, _size; + const DiskImage *_disk; + }; + + Common::File *_f; + uint _tracks, _sectorsPerTrack, _bytesPerSector; +}; + +// Data in plain files +class Files_Plain : public Files { +public: + const DataBlockPtr getDataBlock(const Common::String &filename, uint offset = 0) const; + Common::SeekableReadStream *createReadStream(const Common::String &filename, uint offset = 0) const; +}; + +// .DSK disk image - 35 tracks, 16 sectors per track, 256 bytes per sector +class DiskImage_DSK : public DiskImage { +public: + bool open(const Common::String &filename); + const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const; + Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0) const; +}; + +// .NIB disk image +class DiskImage_NIB : public DiskImage { +public: + DiskImage_NIB() : _memStream(nullptr) { } + virtual ~DiskImage_NIB() { + delete _memStream; + } + + bool open(const Common::String &filename); + const DataBlockPtr getDataBlock(uint track, uint sector, uint offset = 0, uint size = 0) const; + Common::SeekableReadStream *createReadStream(uint track, uint sector, uint offset = 0, uint size = 0) const; + +private: + Common::SeekableReadStream *_memStream; +}; + +// Data in files contained in Apple DOS 3.3 disk image +class Files_DOS33 : public Files { +public: + Files_DOS33(); + ~Files_DOS33(); + + bool open(const Common::String &filename); + const DataBlockPtr getDataBlock(const Common::String &filename, uint offset = 0) const; + Common::SeekableReadStream *createReadStream(const Common::String &filename, uint offset = 0) const; + +private: + enum FileType { + kFileTypeText = 0, + kFileTypeAppleSoft = 2, + kFileTypeBinary = 4 + }; + + enum { + kSectorSize = 256, + kFilenameLen = 30 + }; + + struct TrackSector { + byte track; + byte sector; + }; + + struct TOCEntry { + byte type; + uint16 totalSectors; + Common::Array<TrackSector> sectors; + }; + + void readVTOC(); + void readSectorList(TrackSector start, Common::Array<TrackSector> &list); + Common::SeekableReadStream *createReadStreamText(const TOCEntry &entry) const; + Common::SeekableReadStream *createReadStreamBinary(const TOCEntry &entry) const; + + DiskImage *_disk; + Common::HashMap<Common::String, TOCEntry> _toc; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/display.cpp b/engines/adl/display.cpp new file mode 100644 index 0000000000..b7f6eb9923 --- /dev/null +++ b/engines/adl/display.cpp @@ -0,0 +1,545 @@ +/* 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/stream.h" +#include "common/rect.h" +#include "common/system.h" +#include "common/str.h" +#include "common/config-manager.h" + +#include "graphics/surface.h" +#include "graphics/palette.h" +#include "graphics/thumbnail.h" + +#include "engines/util.h" + +#include "adl/display.h" + +namespace Adl { + +// This implements the Apple II "Hi-Res" display mode + +#define DISPLAY_PITCH (DISPLAY_WIDTH / 7) +#define DISPLAY_SIZE (DISPLAY_PITCH * DISPLAY_HEIGHT) + +#define TEXT_BUF_SIZE (TEXT_WIDTH * TEXT_HEIGHT) + +#define COLOR_PALETTE_ENTRIES 8 +static const byte colorPalette[COLOR_PALETTE_ENTRIES * 3] = { + 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, + 0xc7, 0x34, 0xff, + 0x38, 0xcb, 0x00, + 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, + 0x0d, 0xa1, 0xff, + 0xf2, 0x5e, 0x00 +}; + +// Corresponding color in second palette +#define PAL2(X) ((X) | 0x04) + +// Alternate color for odd pixel rows (for scanlines) +#define ALTCOL(X) ((X) | 0x08) + +// Green monochrome palette +#define MONO_PALETTE_ENTRIES 2 +static const byte monoPalette[MONO_PALETTE_ENTRIES * 3] = { + 0x00, 0x00, 0x00, + 0x00, 0xc0, 0x01 +}; + +// Uppercase-only Apple II font (manually created). +static const byte font[64][5] = { + { 0x7c, 0x82, 0xba, 0xb2, 0x9c }, { 0xf8, 0x24, 0x22, 0x24, 0xf8 }, // @A + { 0xfe, 0x92, 0x92, 0x92, 0x6c }, { 0x7c, 0x82, 0x82, 0x82, 0x44 }, // BC + { 0xfe, 0x82, 0x82, 0x82, 0x7c }, { 0xfe, 0x92, 0x92, 0x92, 0x82 }, // DE + { 0xfe, 0x12, 0x12, 0x12, 0x02 }, { 0x7c, 0x82, 0x82, 0xa2, 0xe2 }, // FG + { 0xfe, 0x10, 0x10, 0x10, 0xfe }, { 0x00, 0x82, 0xfe, 0x82, 0x00 }, // HI + { 0x40, 0x80, 0x80, 0x80, 0x7e }, { 0xfe, 0x10, 0x28, 0x44, 0x82 }, // JK + { 0xfe, 0x80, 0x80, 0x80, 0x80 }, { 0xfe, 0x04, 0x18, 0x04, 0xfe }, // LM + { 0xfe, 0x08, 0x10, 0x20, 0xfe }, { 0x7c, 0x82, 0x82, 0x82, 0x7c }, // NO + { 0xfe, 0x12, 0x12, 0x12, 0x0c }, { 0x7c, 0x82, 0xa2, 0x42, 0xbc }, // PQ + { 0xfe, 0x12, 0x32, 0x52, 0x8c }, { 0x4c, 0x92, 0x92, 0x92, 0x64 }, // RS + { 0x02, 0x02, 0xfe, 0x02, 0x02 }, { 0x7e, 0x80, 0x80, 0x80, 0x7e }, // TU + { 0x3e, 0x40, 0x80, 0x40, 0x3e }, { 0xfe, 0x40, 0x30, 0x40, 0xfe }, // VW + { 0xc6, 0x28, 0x10, 0x28, 0xc6 }, { 0x06, 0x08, 0xf0, 0x08, 0x06 }, // XY + { 0xc2, 0xa2, 0x92, 0x8a, 0x86 }, { 0xfe, 0xfe, 0x82, 0x82, 0x82 }, // Z[ + { 0x04, 0x08, 0x10, 0x20, 0x40 }, { 0x82, 0x82, 0x82, 0xfe, 0xfe }, // \] + { 0x20, 0x10, 0x08, 0x10, 0x20 }, { 0x80, 0x80, 0x80, 0x80, 0x80 }, // ^_ + { 0x00, 0x00, 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0xbe, 0x00, 0x00 }, // ! + { 0x00, 0x0e, 0x00, 0x0e, 0x00 }, { 0x28, 0xfe, 0x28, 0xfe, 0x28 }, // "# + { 0x48, 0x54, 0xfe, 0x54, 0x24 }, { 0x46, 0x26, 0x10, 0xc8, 0xc4 }, // $% + { 0x6c, 0x92, 0xac, 0x40, 0xa0 }, { 0x00, 0x00, 0x0e, 0x00, 0x00 }, // &' + { 0x38, 0x44, 0x82, 0x00, 0x00 }, { 0x00, 0x00, 0x82, 0x44, 0x38 }, // () + { 0x44, 0x28, 0xfe, 0x28, 0x44 }, { 0x10, 0x10, 0x7c, 0x10, 0x10 }, // *+ + { 0x00, 0x80, 0x60, 0x00, 0x00 }, { 0x10, 0x10, 0x10, 0x10, 0x10 }, // ,- + { 0x00, 0x00, 0x80, 0x00, 0x00 }, { 0x40, 0x20, 0x10, 0x08, 0x04 }, // ./ + { 0x7c, 0xa2, 0x92, 0x8a, 0x7c }, { 0x00, 0x84, 0xfe, 0x80, 0x00 }, // 01 + { 0xc4, 0xa2, 0x92, 0x92, 0x8c }, { 0x42, 0x82, 0x92, 0x9a, 0x66 }, // 23 + { 0x30, 0x28, 0x24, 0xfe, 0x20 }, { 0x4e, 0x8a, 0x8a, 0x8a, 0x72 }, // 45 + { 0x78, 0x94, 0x92, 0x92, 0x62 }, { 0x02, 0xe2, 0x12, 0x0a, 0x06 }, // 67 + { 0x6c, 0x92, 0x92, 0x92, 0x6c }, { 0x8c, 0x92, 0x92, 0x52, 0x3c }, // 89 + { 0x00, 0x00, 0x28, 0x00, 0x00 }, { 0x00, 0x80, 0x68, 0x00, 0x00 }, // :; + { 0x10, 0x28, 0x44, 0x82, 0x00 }, { 0x28, 0x28, 0x28, 0x28, 0x28 }, // <= + { 0x00, 0x82, 0x44, 0x28, 0x10 }, { 0x04, 0x02, 0xb2, 0x0a, 0x04 } // >? +}; + +Display::Display() : + _mode(DISPLAY_MODE_TEXT), + _cursorPos(0), + _showCursor(false) { + + initGraphics(DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 2, true); + + _monochrome = !ConfMan.getBool("color"); + _scanlines = ConfMan.getBool("scanlines"); + + if (_monochrome) + g_system->getPaletteManager()->setPalette(monoPalette, 0, MONO_PALETTE_ENTRIES); + else + g_system->getPaletteManager()->setPalette(colorPalette, 0, COLOR_PALETTE_ENTRIES); + + showScanlines(_scanlines); + + _frameBuf = new byte[DISPLAY_SIZE]; + memset(_frameBuf, 0, DISPLAY_SIZE); + _frameBufSurface = new Graphics::Surface; + // We need 2x scaling to properly render the half-pixel shift + // of the second palette + _frameBufSurface->create(DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 2, Graphics::PixelFormat::createFormatCLUT8()); + + _textBuf = new byte[TEXT_BUF_SIZE]; + memset(_textBuf, APPLECHAR(' '), TEXT_BUF_SIZE); + _textBufSurface = new Graphics::Surface; + // For ease of copying, also use 2x scaling here + _textBufSurface->create(DISPLAY_WIDTH * 2, DISPLAY_HEIGHT * 2, Graphics::PixelFormat::createFormatCLUT8()); + + createFont(); +} + +Display::~Display() { + delete[] _frameBuf; + _frameBufSurface->free(); + delete _frameBufSurface; + + delete[] _textBuf; + _textBufSurface->free(); + delete _textBufSurface; + + _font->free(); + delete _font; +} + +void Display::setMode(DisplayMode mode) { + _mode = mode; + + if (_mode == DISPLAY_MODE_TEXT || _mode == DISPLAY_MODE_MIXED) + updateTextScreen(); + if (_mode == DISPLAY_MODE_HIRES || _mode == DISPLAY_MODE_MIXED) + updateHiResScreen(); +} + +void Display::updateTextScreen() { + updateTextSurface(); + + if (_mode == DISPLAY_MODE_TEXT) + g_system->copyRectToScreen(_textBufSurface->getPixels(), _textBufSurface->pitch, 0, 0, _textBufSurface->w, _textBufSurface->h); + else if (_mode == DISPLAY_MODE_MIXED) + g_system->copyRectToScreen(_textBufSurface->getBasePtr(0, _textBufSurface->h - 4 * 8 * 2), _textBufSurface->pitch, 0, _textBufSurface->h - 4 * 8 * 2, _textBufSurface->w, 4 * 8 * 2); + + g_system->updateScreen(); +} + +void Display::updateHiResScreen() { + updateHiResSurface(); + + if (_mode == DISPLAY_MODE_HIRES) + g_system->copyRectToScreen(_frameBufSurface->getPixels(), _frameBufSurface->pitch, 0, 0, _frameBufSurface->w, _frameBufSurface->h); + else if (_mode == DISPLAY_MODE_MIXED) + g_system->copyRectToScreen(_frameBufSurface->getPixels(), _frameBufSurface->pitch, 0, 0, _frameBufSurface->w, _frameBufSurface->h - 4 * 8 * 2); + + g_system->updateScreen(); +} + +bool Display::saveThumbnail(Common::WriteStream &out) { + if (_scanlines) { + showScanlines(false); + g_system->updateScreen(); + } + + bool retval = Graphics::saveThumbnail(out); + + if (_scanlines) { + showScanlines(true); + g_system->updateScreen(); + } + + return retval; +} + +void Display::loadFrameBuffer(Common::ReadStream &stream) { + byte *dst = _frameBuf; + for (uint j = 0; j < 8; ++j) { + for (uint i = 0; i < 8; ++i) { + stream.read(dst, DISPLAY_PITCH); + dst += DISPLAY_PITCH * 64; + stream.read(dst, DISPLAY_PITCH); + dst += DISPLAY_PITCH * 64; + stream.read(dst, DISPLAY_PITCH); + stream.readUint32LE(); + stream.readUint32LE(); + dst -= DISPLAY_PITCH * 120; + } + dst -= DISPLAY_PITCH * 63; + } + + if (stream.eos() || stream.err()) + error("Failed to read frame buffer"); +} + +void Display::putPixel(const Common::Point &p, byte color) { + byte offset = p.x / 7; + byte mask = 0x80 | (1 << (p.x % 7)); + + // Since white and black are in both palettes, we leave + // the palette bit alone + if ((color & 0x7f) == 0x7f || (color & 0x7f) == 0) + mask &= 0x7f; + + // Adjust colors starting with bits '01' or '10' for + // odd offsets + if (offset & 1) { + byte c = color << 1; + if (c >= 0x40 && c < 0xc0) + color ^= 0x7f; + } + + writeFrameBuffer(p, color, mask); +} + +void Display::setPixelBit(const Common::Point &p, byte color) { + writeFrameBuffer(p, color, 1 << (p.x % 7)); +} + +void Display::setPixelPalette(const Common::Point &p, byte color) { + writeFrameBuffer(p, color, 0x80); +} + +bool Display::getPixelBit(const Common::Point &p) const { + byte *b = _frameBuf + p.y * DISPLAY_PITCH + p.x / 7; + return *b & (1 << (p.x % 7)); +} + +void Display::clear(byte color) { + byte val = 0; + + byte c = color << 1; + if (c >= 0x40 && c < 0xc0) + val = 0x7f; + + for (uint i = 0; i < DISPLAY_SIZE; ++i) { + _frameBuf[i] = color; + color ^= val; + } +} + +void Display::home() { + memset(_textBuf, APPLECHAR(' '), TEXT_BUF_SIZE); + _cursorPos = 0; +} + +void Display::moveCursorForward() { + ++_cursorPos; + + if (_cursorPos >= TEXT_BUF_SIZE) + scrollUp(); +} + +void Display::moveCursorBackward() { + if (_cursorPos > 0) + --_cursorPos; +} + +void Display::moveCursorTo(const Common::Point &pos) { + _cursorPos = pos.y * TEXT_WIDTH + pos.x; + + if (_cursorPos >= TEXT_BUF_SIZE) + error("Cursor position (%i, %i) out of bounds", pos.x, pos.y); +} + +// FIXME: This does not currently update the surfaces +void Display::printChar(char c) { + if (c == APPLECHAR('\r')) + _cursorPos = (_cursorPos / TEXT_WIDTH + 1) * TEXT_WIDTH; + else if ((byte)c < 0x80 || (byte)c >= 0xa0) { + setCharAtCursor(c); + ++_cursorPos; + } + + if (_cursorPos == TEXT_BUF_SIZE) + scrollUp(); +} + +void Display::printString(const Common::String &str) { + Common::String::const_iterator c; + for (c = str.begin(); c != str.end(); ++c) + printChar(*c); + + updateTextScreen(); +} + +void Display::printAsciiString(const Common::String &str) { + Common::String aStr; + + Common::String::const_iterator it; + for (it = str.begin(); it != str.end(); ++it) + aStr += APPLECHAR(*it); + + printString(aStr); +} + +void Display::setCharAtCursor(byte c) { + _textBuf[_cursorPos] = c; +} + +void Display::showCursor(bool enable) { + _showCursor = enable; +} + +void Display::writeFrameBuffer(const Common::Point &p, byte color, byte mask) { + byte *b = _frameBuf + p.y * DISPLAY_PITCH + p.x / 7; + color ^= *b; + color &= mask; + *b ^= color; +} + +void Display::showScanlines(bool enable) { + byte pal[COLOR_PALETTE_ENTRIES * 3] = { }; + + if (enable) + g_system->getPaletteManager()->setPalette(pal, COLOR_PALETTE_ENTRIES, COLOR_PALETTE_ENTRIES); + else { + g_system->getPaletteManager()->grabPalette(pal, 0, COLOR_PALETTE_ENTRIES); + g_system->getPaletteManager()->setPalette(pal, COLOR_PALETTE_ENTRIES, COLOR_PALETTE_ENTRIES); + } +} + +static byte processColorBits(uint16 &bits, bool &odd, bool secondPal) { + byte color = 0; + + switch (bits & 0x7) { + case 0x3: // 011 (white) + case 0x6: // 110 + case 0x7: // 111 + color = 1; + break; + case 0x2: // 010 (color) + color = 2 + odd; + break; + case 0x5: // 101 (color) + color = 2 + !odd; + } + + if (secondPal) + color = PAL2(color); + + odd = !odd; + bits >>= 1; + + return color; +} + +static void renderPixelRowColor(byte *dst, byte *src) { + uint16 bits = (src[0] & 0x7f) << 1; + byte pal = src[0] >> 7; + + if (pal != 0) + *dst++ = 0; + + bool odd = false; + + for (uint i = 0; i < DISPLAY_PITCH; ++i) { + if (i != DISPLAY_PITCH - 1) { + bits |= (src[i + 1] & 0x7f) << 8; + pal |= (src[i + 1] >> 7) << 1; + } + + // For the first 6 bits in the block we draw two pixels + for (uint j = 0; j < 6; ++j) { + byte color = processColorBits(bits, odd, pal & 1); + *dst++ = color; + *dst++ = color; + } + + // Last bit of the block, draw one, two or three pixels + byte color = processColorBits(bits, odd, pal & 1); + + // Draw the first pixel + *dst++ = color; + + switch (pal) { + case 0x0: + case 0x3: + // If palette stays the same, draw a second pixel + *dst++ = color; + break; + case 0x2: + // If we're moving from first to second palette, + // draw a second pixel, and a third in the second + // palette. + *dst++ = color; + *dst++ = PAL2(color); + } + + pal >>= 1; + } +} + +static void renderPixelRowMono(byte *dst, byte *src) { + byte pal = src[0] >> 7; + + if (pal != 0) + *dst++ = 0; + + for (uint i = 0; i < DISPLAY_PITCH; ++i) { + if (i != DISPLAY_PITCH - 1) + pal |= (src[i + 1] >> 7) << 1; + + for (uint j = 0; j < 6; ++j) { + bool color = src[i] & (1 << j); + *dst++ = color; + *dst++ = color; + } + + bool color = src[i] & (1 << 6); + + *dst++ = color; + + switch (pal) { + case 0x0: + case 0x3: + *dst++ = color; + break; + case 0x2: + *dst++ = color; + *dst++ = color; + } + + pal >>= 1; + } +} + +static void copyEvenSurfaceRows(Graphics::Surface &surf) { + byte *src = (byte *)surf.getPixels(); + + for (uint y = 0; y < surf.h / 2; ++y) { + byte *dst = src + surf.pitch; + for (uint x = 0; x < surf.w; ++x) + dst[x] = ALTCOL(src[x]); + src += surf.pitch * 2; + } +} + +void Display::updateHiResSurface() { + byte *src = _frameBuf; + byte *dst = (byte *)_frameBufSurface->getPixels(); + + for (uint i = 0; i < DISPLAY_HEIGHT; ++i) { + if (_monochrome) + renderPixelRowMono(dst, src); + else + renderPixelRowColor(dst, src); + src += DISPLAY_PITCH; + dst += _frameBufSurface->pitch * 2; + } + + copyEvenSurfaceRows(*_frameBufSurface); +} + +void Display::updateTextSurface() { + for (uint row = 0; row < 24; ++row) + for (uint col = 0; col < TEXT_WIDTH; ++col) { + uint charPos = row * TEXT_WIDTH + col; + char c = _textBuf[row * TEXT_WIDTH + col]; + + if (charPos == _cursorPos && _showCursor) + c = (c & 0x3f) | 0x40; + + Common::Rect r(7 * 2, 8 * 2); + r.translate(((c & 0x3f) % 16) * 7 * 2, (c & 0x3f) / 16 * 8 * 2); + + if (!(c & 0x80)) { + if (!(c & 0x40) || ((g_system->getMillis() / 270) & 1)) + r.translate(0, 4 * 8 * 2); + } + + _textBufSurface->copyRectToSurface(*_font, col * 7 * 2, row * 8 * 2, r); + } +} + +void Display::drawChar(byte c, int x, int y) { + byte *buf = (byte *)_font->getPixels() + y * _font->pitch + x; + + for (uint row = 0; row < 8; ++row) { + for (uint col = 1; col < 6; ++col) { + if (font[c][col - 1] & (1 << row)) { + buf[col * 2] = 1; + buf[col * 2 + 1] = 1; + } + } + + buf += 2 * _font->pitch; + } +} + +void Display::createFont() { + _font = new Graphics::Surface; + _font->create(16 * 7 * 2, 4 * 8 * 2 * 2, Graphics::PixelFormat::createFormatCLUT8()); + + for (uint i = 0; i < 4; ++i) + for (uint j = 0; j < 16; ++j) + drawChar(i * 16 + j, j * 7 * 2, i * 8 * 2); + + // Create inverted font + byte *buf = (byte *)_font->getPixels(); + byte *bufInv = buf + (_font->h / 2) * _font->pitch; + + for (uint row = 0; row < _font->h / 2; row += 2) { + for (uint col = 0; col < _font->w; ++col) + bufInv[col] = (buf[col] ? 0 : 1); + + buf += _font->pitch * 2; + bufInv += _font->pitch * 2; + } + + copyEvenSurfaceRows(*_font); +} + +void Display::scrollUp() { + memmove(_textBuf, _textBuf + TEXT_WIDTH, TEXT_BUF_SIZE - TEXT_WIDTH); + memset(_textBuf + TEXT_BUF_SIZE - TEXT_WIDTH, APPLECHAR(' '), TEXT_WIDTH); + if (_cursorPos >= TEXT_WIDTH) + _cursorPos -= TEXT_WIDTH; +} + +} // End of namespace Adl diff --git a/engines/adl/display.h b/engines/adl/display.h new file mode 100644 index 0000000000..bc27b7cb6b --- /dev/null +++ b/engines/adl/display.h @@ -0,0 +1,109 @@ +/* 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. + * + */ + +#ifndef ADL_DISPLAY_H +#define ADL_DISPLAY_H + +#include <common/types.h> + +namespace Common { +class ReadStream; +class WriteStream; +class String; +struct Point; +} + +namespace Graphics { +struct Surface; +} + +namespace Adl { + +#define DISPLAY_WIDTH 280 +#define DISPLAY_HEIGHT 192 +#define TEXT_WIDTH 40 +#define TEXT_HEIGHT 24 + +enum DisplayMode { + DISPLAY_MODE_HIRES, + DISPLAY_MODE_TEXT, + DISPLAY_MODE_MIXED +}; + +#define APPLECHAR(C) ((char)((C) | 0x80)) + +class Display { +public: + Display(); + ~Display(); + + void setMode(DisplayMode mode); + void updateTextScreen(); + void updateHiResScreen(); + bool saveThumbnail(Common::WriteStream &out); + + // Graphics + void loadFrameBuffer(Common::ReadStream &stream); + void putPixel(const Common::Point &p, byte color); + void setPixelBit(const Common::Point &p, byte color); + void setPixelPalette(const Common::Point &p, byte color); + bool getPixelBit(const Common::Point &p) const; + void clear(byte color); + + // Text + void home(); + void moveCursorTo(const Common::Point &pos); + void moveCursorForward(); + void moveCursorBackward(); + void printChar(char c); + void printString(const Common::String &str); + void printAsciiString(const Common::String &str); + void setCharAtCursor(byte c); + void showCursor(bool enable); + +private: + void writeFrameBuffer(const Common::Point &p, byte color, byte mask); + void updateHiResSurface(); + void showScanlines(bool enable); + + void updateTextSurface(); + void drawChar(byte c, int x, int y); + void createFont(); + void scrollUp(); + + DisplayMode _mode; + + byte *_frameBuf; + Graphics::Surface *_frameBufSurface; + bool _scanlines; + bool _monochrome; + + byte *_textBuf; + Graphics::Surface *_textBufSurface; + Graphics::Surface *_font; + uint _cursorPos; + bool _showCursor; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/graphics.cpp b/engines/adl/graphics.cpp new file mode 100644 index 0000000000..f9af442a9f --- /dev/null +++ b/engines/adl/graphics.cpp @@ -0,0 +1,69 @@ +/* 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/stream.h" +#include "common/rect.h" + +#include "adl/display.h" +#include "adl/graphics.h" + +namespace Adl { + +// Draws a four-connected line +void GraphicsMan::drawLine(const Common::Point &p1, const Common::Point &p2, byte color) const { + int16 deltaX = p2.x - p1.x; + int8 xStep = 1; + + if (deltaX < 0) { + deltaX = -deltaX; + xStep = -1; + } + + int16 deltaY = p2.y - p1.y; + int8 yStep = -1; + + if (deltaY > 0) { + deltaY = -deltaY; + yStep = 1; + } + + Common::Point p(p1); + int16 steps = deltaX - deltaY + 1; + int16 err = deltaX + deltaY; + + while (true) { + _display.putPixel(p, color); + + if (--steps == 0) + return; + + if (err < 0) { + p.y += yStep; + err += deltaX; + } else { + p.x += xStep; + err += deltaY; + } + } +} + +} // End of namespace Adl diff --git a/engines/adl/graphics.h b/engines/adl/graphics.h new file mode 100644 index 0000000000..aab807696c --- /dev/null +++ b/engines/adl/graphics.h @@ -0,0 +1,76 @@ +/* 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. + * + */ + +#ifndef ADL_PICTURE_H +#define ADL_PICTURE_H + +namespace Common { +class SeekableReadStream; +struct Point; +} + +namespace Adl { + +class Display; + +class GraphicsMan { +public: + virtual ~GraphicsMan() { } + virtual void drawPic(Common::SeekableReadStream &pic, const Common::Point &pos) = 0; + +protected: + GraphicsMan(Display &display) : _display(display) { } + void drawLine(const Common::Point &p1, const Common::Point &p2, byte color) const; + + Display &_display; +}; + +class Graphics_v1 : public GraphicsMan { +public: + Graphics_v1(Display &display) : GraphicsMan(display) { } + void drawPic(Common::SeekableReadStream &pic, const Common::Point &pos); + void drawCorners(Common::ReadStream &corners, const Common::Point &pos, byte rotation = 0, byte scaling = 1, byte color = 0x7f) const; + +private: + void drawCornerPixel(Common::Point &p, byte color, byte bits, byte quadrant) const; +}; + +class Graphics_v2 : public GraphicsMan { +public: + Graphics_v2(Display &display) : GraphicsMan(display), _color(0) { } + void drawPic(Common::SeekableReadStream &pic, const Common::Point &pos); + +private: + void clear(); + void drawCorners(Common::SeekableReadStream &pic, bool yFirst); + void drawRelativeLines(Common::SeekableReadStream &pic); + void drawAbsoluteLines(Common::SeekableReadStream &pic); + void fillRow(const Common::Point &p, bool fillBit, byte pattern); + void fill(Common::SeekableReadStream &pic); + + byte _color; + Common::Point _offset; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/graphics_v1.cpp b/engines/adl/graphics_v1.cpp new file mode 100644 index 0000000000..29c5ef47af --- /dev/null +++ b/engines/adl/graphics_v1.cpp @@ -0,0 +1,120 @@ +/* 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/stream.h" +#include "common/rect.h" +#include "common/textconsole.h" + +#include "adl/display.h" +#include "adl/graphics.h" + +namespace Adl { + +void Graphics_v1::drawPic(Common::SeekableReadStream &pic, const Common::Point &pos) { + byte x, y; + bool bNewLine = false; + byte oldX = 0, oldY = 0; + while (1) { + x = pic.readByte(); + y = pic.readByte(); + + if (pic.err() || pic.eos()) + error("Error reading picture"); + + if (x == 0xff && y == 0xff) + return; + + if (x == 0 && y == 0) { + bNewLine = true; + continue; + } + + x += pos.x; + y += pos.y; + + if (y > 160) + y = 160; + + if (bNewLine) { + _display.putPixel(Common::Point(x, y), 0x7f); + bNewLine = false; + } else { + drawLine(Common::Point(oldX, oldY), Common::Point(x, y), 0x7f); + } + + oldX = x; + oldY = y; + } +} + +void Graphics_v1::drawCornerPixel(Common::Point &p, byte color, byte bits, byte quadrant) const { + if (bits & 4) + _display.putPixel(p, color); + + bits += quadrant; + + if (bits & 1) + p.x += (bits & 2 ? -1 : 1); + else + p.y += (bits & 2 ? 1 : -1); +} + +void Graphics_v1::drawCorners(Common::ReadStream &corners, const Common::Point &pos, byte rotation, byte scaling, byte color) const { + const byte stepping[] = { + 0xff, 0xfe, 0xfa, 0xf4, 0xec, 0xe1, 0xd4, 0xc5, + 0xb4, 0xa1, 0x8d, 0x78, 0x61, 0x49, 0x31, 0x18, + 0xff + }; + + byte quadrant = rotation >> 4; + rotation &= 0xf; + byte xStep = stepping[rotation]; + byte yStep = stepping[(rotation ^ 0xf) + 1] + 1; + + Common::Point p(pos); + + while (true) { + byte b = corners.readByte(); + + if (corners.eos() || corners.err()) + error("Error reading corners"); + + if (b == 0) + return; + + do { + byte xFrac = 0x80; + byte yFrac = 0x80; + for (uint j = 0; j < scaling; ++j) { + if (xFrac + xStep + 1 > 255) + drawCornerPixel(p, color, b, quadrant); + xFrac += xStep + 1; + if (yFrac + yStep > 255) + drawCornerPixel(p, color, b, quadrant + 1); + yFrac += yStep; + } + b >>= 3; + } while (b != 0); + } +} + +} // End of namespace Adl diff --git a/engines/adl/graphics_v2.cpp b/engines/adl/graphics_v2.cpp new file mode 100644 index 0000000000..40936f5b7d --- /dev/null +++ b/engines/adl/graphics_v2.cpp @@ -0,0 +1,304 @@ +/* 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/stream.h" +#include "common/rect.h" +#include "common/textconsole.h" + +#include "adl/display.h" +#include "adl/graphics.h" +#include "adl/adl.h" + +namespace Adl { + +// FIXME: Add clipping + +#define NUM_PATTERNS 22 +#define PATTERN_LEN 4 +static const byte fillPatterns[NUM_PATTERNS][PATTERN_LEN] = { + { 0x00, 0x00, 0x00, 0x00 }, + { 0x80, 0x80, 0x80, 0x80 }, + { 0xff, 0xff, 0xff, 0xff }, + { 0x7f, 0x7f, 0x7f, 0x7f }, + { 0x2a, 0x55, 0x2a, 0x55 }, + { 0xaa, 0xd5, 0xaa, 0xd5 }, + { 0x55, 0x2a, 0x55, 0x2a }, + { 0xd5, 0xaa, 0xd5, 0xaa }, + { 0x33, 0x66, 0x4c, 0x19 }, + { 0xb3, 0xe6, 0xcc, 0x99 }, + { 0x22, 0x44, 0x08, 0x11 }, + { 0xa2, 0xc4, 0x88, 0x91 }, + { 0x11, 0x22, 0x44, 0x08 }, + { 0x91, 0xa2, 0xc4, 0x88 }, + { 0x6e, 0x5d, 0x3b, 0x77 }, + { 0xee, 0xdd, 0xbb, 0xf7 }, + { 0x5d, 0x3b, 0x77, 0x6e }, + { 0xdd, 0xbb, 0xf7, 0xee }, + { 0x66, 0x4c, 0x19, 0x33 }, + { 0xe6, 0xcc, 0x99, 0xb3 }, + { 0x33, 0x66, 0x4c, 0x19 }, + { 0xb3, 0xe6, 0xcc, 0x99 } +}; + +#define MIN_COMMAND 0xe0 + +#define CHECK_COMMAND(X) \ + do { \ + if ((X) >= MIN_COMMAND) { \ + pic.seek(-1, SEEK_CUR); \ + return; \ + } \ + } while (0) + +#define READ_BYTE(b) \ + do { \ + b = pic.readByte(); \ + if (pic.eos() || pic.err()) \ + error("Error reading picture"); \ + CHECK_COMMAND(b); \ + } while (0) + +#define READ_POINT(p) \ + do { \ + READ_BYTE(p.x); \ + p.x += _offset.x; \ + p.x <<= 1; \ + READ_BYTE(p.y); \ + p.y += _offset.y; \ + } while (0) + +void Graphics_v2::clear() { + _display.clear(0xff); + _color = 0; +} + +void Graphics_v2::drawCorners(Common::SeekableReadStream &pic, bool yFirst) { + Common::Point p; + + READ_POINT(p); + + if (yFirst) + goto doYStep; + + while (true) { + int16 n; + + READ_BYTE(n); + n += _offset.x; + + _display.putPixel(p, _color); + + n <<= 1; + drawLine(p, Common::Point(n, p.y), _color); + p.x = n; + +doYStep: + READ_BYTE(n); + n += _offset.y; + + _display.putPixel(p, _color); + drawLine(p, Common::Point(p.x, n), _color); + + _display.putPixel(Common::Point(p.x + 1, p.y), _color); + drawLine(Common::Point(p.x + 1, p.y), Common::Point(p.x + 1, n), _color); + + p.y = n; + } +} + +void Graphics_v2::drawRelativeLines(Common::SeekableReadStream &pic) { + Common::Point p1; + + READ_POINT(p1); + _display.putPixel(p1, _color); + + while (true) { + Common::Point p2(p1); + + byte n; + READ_BYTE(n); + + byte h = (n & 0x70) >> 4; + byte l = n & 7; + + if (n & 0x80) + p2.x -= (h << 1); + else + p2.x += (h << 1); + + if (n & 8) + p2.y -= l; + else + p2.y += l; + + drawLine(p1, p2, _color); + p1 = p2; + } +} + +void Graphics_v2::drawAbsoluteLines(Common::SeekableReadStream &pic) { + Common::Point p1; + + READ_POINT(p1); + _display.putPixel(p1, _color); + + while (true) { + Common::Point p2; + + READ_POINT(p2); + drawLine(p1, p2, _color); + p1 = p2; + } +} + +static byte getPatternColor(const Common::Point &p, byte pattern) { + if (pattern >= NUM_PATTERNS) + error("Invalid fill pattern %i encountered in picture", pattern); + + byte offset = (p.y & 1) << 1; + offset += (p.x / 7) & 3; + + return fillPatterns[pattern][offset % PATTERN_LEN]; +} + +void Graphics_v2::fillRow(const Common::Point &p, bool stopBit, byte pattern) { + const byte color = getPatternColor(p, pattern); + _display.setPixelPalette(p, color); + _display.setPixelBit(p, color); + + Common::Point q(p); + byte c = color; + + while (++q.x < DISPLAY_WIDTH) { + if ((q.x % 7) == 0) { + c = getPatternColor(q, pattern); + // Palette is set before the first bit is tested + _display.setPixelPalette(q, c); + } + if (_display.getPixelBit(q) == stopBit) + break; + _display.setPixelBit(q, c); + } + + q = p; + c = color; + while (--q.x >= 0) { + if ((q.x % 7) == 6) { + c = getPatternColor(q, pattern); + _display.setPixelPalette(q, c); + } + if (_display.getPixelBit(q) == stopBit) + break; + _display.setPixelBit(q, c); + } +} + +// Basic flood fill +void Graphics_v2::fill(Common::SeekableReadStream &pic) { + byte pattern; + READ_BYTE(pattern); + + while (true) { + Common::Point p; + READ_POINT(p); + + bool stopBit = !_display.getPixelBit(p); + + while (--p.y >= 0) { + if (_display.getPixelBit(p) == stopBit) + break; + if (_display.getPixelBit(Common::Point(p.x + 1, p.y)) == stopBit) + break; + } + + while (++p.y < DISPLAY_HEIGHT) { + if (_display.getPixelBit(p) == stopBit) + break; + if (_display.getPixelBit(Common::Point(p.x + 1, p.y)) == stopBit) + break; + fillRow(p, stopBit, pattern); + } + } +} + +void Graphics_v2::drawPic(Common::SeekableReadStream &pic, const Common::Point &pos) { + _color = 0; + _offset = pos; + + while (true) { + byte opcode = pic.readByte(); + + if (pic.eos() || pic.err()) + error("Error reading picture"); + + switch (opcode) { + case 0xe0: + drawCorners(pic, false); + break; + case 0xe1: + drawCorners(pic, true); + break; + case 0xe2: + drawRelativeLines(pic); + break; + case 0xe3: + drawAbsoluteLines(pic); + break; + case 0xe4: + fill(pic); + break; + case 0xe5: + clear(); + break; + case 0xf0: + _color = 0x00; + break; + case 0xf1: + _color = 0x2a; + break; + case 0xf2: + _color = 0x55; + break; + case 0xf3: + _color = 0x7f; + break; + case 0xf4: + _color = 0x80; + break; + case 0xf5: + _color = 0xaa; + break; + case 0xf6: + _color = 0xd5; + break; + case 0xf7: + _color = 0xff; + break; + case 0xff: + return; + default: + error("Invalid pic opcode %02x", opcode); + } + } +} + +} // End of namespace Adl diff --git a/engines/adl/hires1.cpp b/engines/adl/hires1.cpp new file mode 100644 index 0000000000..096d8ef496 --- /dev/null +++ b/engines/adl/hires1.cpp @@ -0,0 +1,374 @@ +/* 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/ptr.h" + +#include "adl/hires1.h" +#include "adl/display.h" + +namespace Adl { + +void HiRes1Engine::runIntro() const { + StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_0)); + + stream->seek(IDI_HR1_OFS_LOGO_0); + _display->setMode(DISPLAY_MODE_HIRES); + _display->loadFrameBuffer(*stream); + _display->updateHiResScreen(); + delay(4000); + + if (shouldQuit()) + return; + + _display->setMode(DISPLAY_MODE_TEXT); + + StreamPtr basic(_files->createReadStream(IDS_HR1_LOADER)); + Common::String str; + + str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_0, '"'); + _display->printAsciiString(str + '\r'); + + str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_1, '"'); + _display->printAsciiString(str + "\r\r"); + + str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_2, '"'); + _display->printAsciiString(str + "\r\r"); + + str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_3, '"'); + _display->printAsciiString(str + '\r'); + + inputKey(); + if (g_engine->shouldQuit()) + return; + + _display->setMode(DISPLAY_MODE_MIXED); + + str = readStringAt(*stream, IDI_HR1_OFS_GAME_OR_HELP); + + bool instructions = false; + + while (1) { + _display->printString(str); + Common::String s = inputString(); + + if (g_engine->shouldQuit()) + break; + + if (s.empty()) + continue; + + if (s[0] == APPLECHAR('I')) { + instructions = true; + break; + } else if (s[0] == APPLECHAR('G')) { + break; + } + }; + + if (instructions) { + _display->setMode(DISPLAY_MODE_TEXT); + stream->seek(IDI_HR1_OFS_INTRO_TEXT); + + const uint pages[] = { 6, 6, 4, 5, 8, 7, 0 }; + + uint page = 0; + while (pages[page] != 0) { + _display->home(); + + uint count = pages[page++]; + for (uint i = 0; i < count; ++i) { + str = readString(*stream); + _display->printString(str); + stream->seek(3, SEEK_CUR); + } + + inputString(); + + if (g_engine->shouldQuit()) + return; + + stream->seek(6, SEEK_CUR); + } + } + + _display->printAsciiString("\r"); + + _display->setMode(DISPLAY_MODE_MIXED); + + // Title screen shown during loading + stream.reset(_files->createReadStream(IDS_HR1_EXE_1)); + stream->seek(IDI_HR1_OFS_LOGO_1); + _display->loadFrameBuffer(*stream); + _display->updateHiResScreen(); + delay(2000); +} + +void HiRes1Engine::init() { + if (Common::File::exists("MYSTHOUS.DSK")) { + _files = new Files_DOS33(); + if (!static_cast<Files_DOS33 *>(_files)->open("MYSTHOUS.DSK")) + error("Failed to open MYSTHOUS.DSK"); + } else + _files = new Files_Plain(); + + _graphics = new Graphics_v1(*_display); + + StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1)); + + // Some messages have overrides inside the executable + _gameStrings.cantGoThere = readStringAt(*stream, IDI_HR1_OFS_STR_CANT_GO_THERE); + _gameStrings.dontHaveIt = readStringAt(*stream, IDI_HR1_OFS_STR_DONT_HAVE_IT); + _gameStrings.dontUnderstand = readStringAt(*stream, IDI_HR1_OFS_STR_DONT_UNDERSTAND); + _gameStrings.gettingDark = readStringAt(*stream, IDI_HR1_OFS_STR_GETTING_DARK); + + // Load other strings from executable + _strings.enterCommand = readStringAt(*stream, IDI_HR1_OFS_STR_ENTER_COMMAND); + _strings.verbError = readStringAt(*stream, IDI_HR1_OFS_STR_VERB_ERROR); + _strings.nounError = readStringAt(*stream, IDI_HR1_OFS_STR_NOUN_ERROR); + _strings.playAgain = readStringAt(*stream, IDI_HR1_OFS_STR_PLAY_AGAIN); + _strings.pressReturn = readStringAt(*stream, IDI_HR1_OFS_STR_PRESS_RETURN); + _strings.lineFeeds = readStringAt(*stream, IDI_HR1_OFS_STR_LINE_FEEDS); + + // Set message IDs + _messageIds.cantGoThere = IDI_HR1_MSG_CANT_GO_THERE; + _messageIds.dontUnderstand = IDI_HR1_MSG_DONT_UNDERSTAND; + _messageIds.itemDoesntMove = IDI_HR1_MSG_ITEM_DOESNT_MOVE; + _messageIds.itemNotHere = IDI_HR1_MSG_ITEM_NOT_HERE; + _messageIds.thanksForPlaying = IDI_HR1_MSG_THANKS_FOR_PLAYING; + + // Load message offsets + stream->seek(IDI_HR1_OFS_MSGS); + for (uint i = 0; i < IDI_HR1_NUM_MESSAGES; ++i) + _messages.push_back(_files->getDataBlock(IDS_HR1_MESSAGES, stream->readUint16LE())); + + // Load picture data from executable + stream->seek(IDI_HR1_OFS_PICS); + for (uint i = 1; i <= IDI_HR1_NUM_PICS; ++i) { + byte block = stream->readByte(); + Common::String name = Common::String::format("BLOCK%i", block); + uint16 offset = stream->readUint16LE(); + _pictures[i] = _files->getDataBlock(name, offset); + } + + // Load commands from executable + stream->seek(IDI_HR1_OFS_CMDS_1); + readCommands(*stream, _roomCommands); + + stream->seek(IDI_HR1_OFS_CMDS_0); + readCommands(*stream, _globalCommands); + + // Load dropped item offsets + stream->seek(IDI_HR1_OFS_ITEM_OFFSETS); + for (uint i = 0; i < IDI_HR1_NUM_ITEM_OFFSETS; ++i) { + Common::Point p; + p.x = stream->readByte(); + p.y = stream->readByte(); + _itemOffsets.push_back(p); + } + + // Load right-angle line art + stream->seek(IDI_HR1_OFS_CORNERS); + uint16 cornersCount = stream->readUint16LE(); + for (uint i = 0; i < cornersCount; ++i) + _corners.push_back(_files->getDataBlock(IDS_HR1_EXE_1, IDI_HR1_OFS_CORNERS + stream->readUint16LE())); + + if (stream->eos() || stream->err()) + error("Failed to read game data from '" IDS_HR1_EXE_1 "'"); + + stream->seek(IDI_HR1_OFS_VERBS); + loadWords(*stream, _verbs, _priVerbs); + + stream->seek(IDI_HR1_OFS_NOUNS); + loadWords(*stream, _nouns, _priNouns); +} + +void HiRes1Engine::initGameState() { + _state.vars.resize(IDI_HR1_NUM_VARS); + + StreamPtr stream(_files->createReadStream(IDS_HR1_EXE_1)); + + // Load room data from executable + _roomDesc.clear(); + stream->seek(IDI_HR1_OFS_ROOMS); + for (uint i = 0; i < IDI_HR1_NUM_ROOMS; ++i) { + Room room; + stream->readByte(); + _roomDesc.push_back(stream->readByte()); + for (uint j = 0; j < 6; ++j) + room.connections[j] = stream->readByte(); + room.picture = stream->readByte(); + room.curPicture = stream->readByte(); + _state.rooms.push_back(room); + } + + // Load item data from executable + stream->seek(IDI_HR1_OFS_ITEMS); + byte id; + while ((id = stream->readByte()) != 0xff) { + Item item = Item(); + item.id = id; + item.noun = stream->readByte(); + item.room = stream->readByte(); + item.picture = stream->readByte(); + item.isLineArt = stream->readByte(); + item.position.x = stream->readByte(); + item.position.y = stream->readByte(); + item.state = stream->readByte(); + item.description = stream->readByte(); + + stream->readByte(); + + byte size = stream->readByte(); + + for (uint i = 0; i < size; ++i) + item.roomPictures.push_back(stream->readByte()); + + _state.items.push_back(item); + } +} + +void HiRes1Engine::restartGame() { + _display->printString(_strings.pressReturn); + initState(); + _display->printAsciiString(_strings.lineFeeds); +} + +void HiRes1Engine::printString(const Common::String &str) { + Common::String wrap = str; + wordWrap(wrap); + _display->printString(wrap); + + if (_messageDelay) + delay(14 * 166018 / 1000); +} + +Common::String HiRes1Engine::loadMessage(uint idx) const { + StreamPtr stream(_messages[idx]->createReadStream()); + return readString(*stream, APPLECHAR('\r')) + APPLECHAR('\r'); +} + +void HiRes1Engine::printMessage(uint idx) { + // Messages with hardcoded overrides don't delay after printing. + // It's unclear if this is a bug or not. In some cases the result + // is that these strings will scroll past the four-line text window + // before the user gets a chance to read them. + // NOTE: later games seem to wait for a key when the text window + // overflows and don't use delays. It might be better to use + // that system for this game as well. + switch (idx) { + case IDI_HR1_MSG_CANT_GO_THERE: + _display->printString(_gameStrings.cantGoThere); + return; + case IDI_HR1_MSG_DONT_HAVE_IT: + _display->printString(_gameStrings.dontHaveIt); + return; + case IDI_HR1_MSG_DONT_UNDERSTAND: + _display->printString(_gameStrings.dontUnderstand); + return; + case IDI_HR1_MSG_GETTING_DARK: + _display->printString(_gameStrings.gettingDark); + return; + default: + printString(loadMessage(idx)); + } +} + +void HiRes1Engine::drawItems() { + Common::List<Item>::iterator item; + + uint dropped = 0; + + for (item = _state.items.begin(); item != _state.items.end(); ++item) { + // Skip items not in this room + if (item->room != _state.room) + continue; + + if (item->state == IDI_ITEM_DROPPED) { + // Draw dropped item if in normal view + if (getCurRoom().picture == getCurRoom().curPicture) + drawItem(*item, _itemOffsets[dropped++]); + } else { + // Draw fixed item if current view is in the pic list + Common::Array<byte>::const_iterator pic; + + for (pic = item->roomPictures.begin(); pic != item->roomPictures.end(); ++pic) { + if (*pic == getCurRoom().curPicture) { + drawItem(*item, item->position); + break; + } + } + } + } +} + +void HiRes1Engine::drawItem(Item &item, const Common::Point &pos) { + if (item.isLineArt) { + StreamPtr stream(_corners[item.picture - 1]->createReadStream()); + static_cast<Graphics_v1 *>(_graphics)->drawCorners(*stream, pos); + } else + drawPic(item.picture, pos); +} + +void HiRes1Engine::loadRoom(byte roomNr) { + _roomData.description = loadMessage(_roomDesc[_state.room - 1]); +} + +void HiRes1Engine::showRoom() { + clearScreen(); + loadRoom(_state.room); + + if (!_state.isDark) { + drawPic(getCurRoom().curPicture); + drawItems(); + } + + _display->updateHiResScreen(); + _messageDelay = false; + printString(_roomData.description); + _messageDelay = true; +} + +void HiRes1Engine::wordWrap(Common::String &str) const { + uint end = 39; + + while (1) { + if (str.size() <= end) + return; + + while (str[end] != APPLECHAR(' ')) + --end; + + str.setChar(APPLECHAR('\r'), end); + end += 40; + } +} + +Engine *HiRes1Engine_create(OSystem *syst, const AdlGameDescription *gd) { + return new HiRes1Engine(syst, gd); +} + +} // End of namespace Adl diff --git a/engines/adl/hires1.h b/engines/adl/hires1.h new file mode 100644 index 0000000000..c060bc892e --- /dev/null +++ b/engines/adl/hires1.h @@ -0,0 +1,134 @@ +/* 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. + * + */ + +#ifndef ADL_HIRES1_H +#define ADL_HIRES1_H + +#include "common/str.h" + +#include "adl/adl.h" +#include "adl/graphics.h" +#include "adl/disk.h" + +namespace Common { +class ReadStream; +struct Point; +} + +namespace Adl { + +#define IDS_HR1_EXE_0 "AUTO LOAD OBJ" +#define IDS_HR1_EXE_1 "ADVENTURE" +#define IDS_HR1_LOADER "MYSTERY.HELLO" +#define IDS_HR1_MESSAGES "MESSAGES" + +#define IDI_HR1_NUM_ROOMS 41 +#define IDI_HR1_NUM_PICS 97 +#define IDI_HR1_NUM_VARS 20 +#define IDI_HR1_NUM_ITEM_OFFSETS 21 +#define IDI_HR1_NUM_MESSAGES 168 + +// Messages used outside of scripts +#define IDI_HR1_MSG_CANT_GO_THERE 137 +#define IDI_HR1_MSG_DONT_UNDERSTAND 37 +#define IDI_HR1_MSG_ITEM_DOESNT_MOVE 151 +#define IDI_HR1_MSG_ITEM_NOT_HERE 152 +#define IDI_HR1_MSG_THANKS_FOR_PLAYING 140 +#define IDI_HR1_MSG_DONT_HAVE_IT 127 +#define IDI_HR1_MSG_GETTING_DARK 7 + +#define IDI_HR1_OFS_STR_ENTER_COMMAND 0x5bbc +#define IDI_HR1_OFS_STR_VERB_ERROR 0x5b4f +#define IDI_HR1_OFS_STR_NOUN_ERROR 0x5b8e +#define IDI_HR1_OFS_STR_PLAY_AGAIN 0x5f1e +#define IDI_HR1_OFS_STR_CANT_GO_THERE 0x6c0a +#define IDI_HR1_OFS_STR_DONT_HAVE_IT 0x6c31 +#define IDI_HR1_OFS_STR_DONT_UNDERSTAND 0x6c51 +#define IDI_HR1_OFS_STR_GETTING_DARK 0x6c7c +#define IDI_HR1_OFS_STR_PRESS_RETURN 0x5f68 +#define IDI_HR1_OFS_STR_LINE_FEEDS 0x59d4 + +#define IDI_HR1_OFS_PD_TEXT_0 0x005d +#define IDI_HR1_OFS_PD_TEXT_1 0x012b +#define IDI_HR1_OFS_PD_TEXT_2 0x016d +#define IDI_HR1_OFS_PD_TEXT_3 0x0259 + +#define IDI_HR1_OFS_INTRO_TEXT 0x0066 +#define IDI_HR1_OFS_GAME_OR_HELP 0x000f + +#define IDI_HR1_OFS_LOGO_0 0x1003 +#define IDI_HR1_OFS_LOGO_1 0x1800 + +#define IDI_HR1_OFS_ITEMS 0x0100 +#define IDI_HR1_OFS_ROOMS 0x050a +#define IDI_HR1_OFS_PICS 0x4b03 +#define IDI_HR1_OFS_CMDS_0 0x3c00 +#define IDI_HR1_OFS_CMDS_1 0x3d00 +#define IDI_HR1_OFS_MSGS 0x4d00 + +#define IDI_HR1_OFS_ITEM_OFFSETS 0x68ff +#define IDI_HR1_OFS_CORNERS 0x4f00 + +#define IDI_HR1_OFS_VERBS 0x3800 +#define IDI_HR1_OFS_NOUNS 0x0f00 + +class HiRes1Engine : public AdlEngine { +public: + HiRes1Engine(OSystem *syst, const AdlGameDescription *gd) : + AdlEngine(syst, gd), + _files(nullptr), + _messageDelay(true) { } + ~HiRes1Engine() { delete _files; } + +private: + // AdlEngine + void runIntro() const; + void init(); + void initGameState(); + void restartGame(); + void printString(const Common::String &str); + Common::String loadMessage(uint idx) const; + void printMessage(uint idx); + void drawItems(); + void drawItem(Item &item, const Common::Point &pos); + void loadRoom(byte roomNr); + void showRoom(); + + void wordWrap(Common::String &str) const; + + Files *_files; + Common::File _exe; + Common::Array<DataBlockPtr> _corners; + Common::Array<byte> _roomDesc; + bool _messageDelay; + + struct { + Common::String cantGoThere; + Common::String dontHaveIt; + Common::String dontUnderstand; + Common::String gettingDark; + } _gameStrings; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/hires2.cpp b/engines/adl/hires2.cpp new file mode 100644 index 0000000000..d8e8a65e29 --- /dev/null +++ b/engines/adl/hires2.cpp @@ -0,0 +1,183 @@ +/* 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/hires2.h" +#include "adl/display.h" +#include "adl/graphics.h" +#include "adl/disk.h" + +namespace Adl { + +void HiRes2Engine::runIntro() const { + StreamPtr stream(_disk->createReadStream(0x00, 0xd, 0x17, 1)); + + _display->setMode(DISPLAY_MODE_TEXT); + + Common::String str = readString(*stream); + + if (stream->eos() || stream->err()) + error("Error reading disk image"); + + _display->printString(str); + delay(2000); +} + +void HiRes2Engine::init() { + _graphics = new Graphics_v2(*_display); + + _disk = new DiskImage_DSK(); + if (!_disk->open(IDS_HR2_DISK_IMAGE)) + error("Failed to open disk image '" IDS_HR2_DISK_IMAGE "'"); + + StreamPtr stream(_disk->createReadStream(0x1f, 0x2, 0x00, 4)); + + for (uint i = 0; i < IDI_HR2_NUM_MESSAGES; ++i) + _messages.push_back(readDataBlockPtr(*stream)); + + // Read parser messages + stream.reset(_disk->createReadStream(0x1a, 0x1)); + _strings.verbError = readStringAt(*stream, 0x4f); + _strings.nounError = readStringAt(*stream, 0x8e); + _strings.enterCommand = readStringAt(*stream, 0xbc); + + // Read time string + stream.reset(_disk->createReadStream(0x19, 0x7, 0xd7)); + _strings_v2.time = readString(*stream, 0xff); + + // Read line feeds + stream.reset(_disk->createReadStream(0x19, 0xb, 0xf8, 1)); + _strings.lineFeeds = readString(*stream); + + // Read opcode strings + stream.reset(_disk->createReadStream(0x1a, 0x6, 0x00, 2)); + _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); + _strings.pressReturn = readStringAt(*stream, 0x25f); + + _messageIds.cantGoThere = IDI_HR2_MSG_CANT_GO_THERE; + _messageIds.dontUnderstand = IDI_HR2_MSG_DONT_UNDERSTAND; + _messageIds.itemDoesntMove = IDI_HR2_MSG_ITEM_DOESNT_MOVE; + _messageIds.itemNotHere = IDI_HR2_MSG_ITEM_NOT_HERE; + _messageIds.thanksForPlaying = IDI_HR2_MSG_THANKS_FOR_PLAYING; + + // Load global picture data + stream.reset(_disk->createReadStream(0x19, 0xa, 0x80, 0)); + byte picNr; + while ((picNr = stream->readByte()) != 0xff) { + if (stream->eos() || stream->err()) + error("Error reading global pic list"); + + _pictures[picNr] = readDataBlockPtr(*stream); + } + + // Load item picture data + stream.reset(_disk->createReadStream(0x1e, 0x9, 0x05)); + for (uint i = 0; i < IDI_HR2_NUM_ITEM_PICS; ++i) { + stream->readByte(); // number + _itemPics.push_back(readDataBlockPtr(*stream)); + } + + // Load commands from executable + stream.reset(_disk->createReadStream(0x1d, 0x7, 0x00, 4)); + readCommands(*stream, _roomCommands); + + stream.reset(_disk->createReadStream(0x1f, 0x7, 0x00, 2)); + readCommands(*stream, _globalCommands); + + // Load dropped item offsets + stream.reset(_disk->createReadStream(0x1b, 0x4, 0x15)); + for (uint i = 0; i < IDI_HR2_NUM_ITEM_OFFSETS; ++i) { + Common::Point p; + p.x = stream->readByte(); + p.y = stream->readByte(); + _itemOffsets.push_back(p); + } + + // Load verbs + stream.reset(_disk->createReadStream(0x19, 0x0, 0x00, 3)); + loadWords(*stream, _verbs, _priVerbs); + + // Load nouns + stream.reset(_disk->createReadStream(0x22, 0x2, 0x00, 7)); + loadWords(*stream, _nouns, _priNouns); +} + +void HiRes2Engine::initGameState() { + _state.vars.resize(IDI_HR2_NUM_VARS); + + StreamPtr stream(_disk->createReadStream(0x21, 0x5, 0x0e, 7)); + + for (uint i = 0; i < IDI_HR2_NUM_ROOMS; ++i) { + Room room; + stream->readByte(); // number + for (uint j = 0; j < 6; ++j) + room.connections[j] = stream->readByte(); + room.data = readDataBlockPtr(*stream); + room.picture = stream->readByte(); + room.curPicture = stream->readByte(); + room.isFirstTime = stream->readByte(); + _state.rooms.push_back(room); + } + + stream.reset(_disk->createReadStream(0x21, 0x0, 0x00, 2)); + + byte id; + while ((id = stream->readByte()) != 0xff) { + Item item = Item(); + item.id = id; + item.noun = stream->readByte(); + item.room = stream->readByte(); + item.picture = stream->readByte(); + item.isLineArt = stream->readByte(); // Is this still used in this way? + item.position.x = stream->readByte(); + item.position.y = stream->readByte(); + item.state = stream->readByte(); + item.description = stream->readByte(); + + stream->readByte(); // Struct size + + byte picListSize = stream->readByte(); + + // Flag to keep track of what has been drawn on the screen + stream->readByte(); + + for (uint i = 0; i < picListSize; ++i) + item.roomPictures.push_back(stream->readByte()); + + _state.items.push_back(item); + } +} + +Engine *HiRes2Engine_create(OSystem *syst, const AdlGameDescription *gd) { + return new HiRes2Engine(syst, gd); +} + +} // End of namespace Adl diff --git a/engines/adl/hires2.h b/engines/adl/hires2.h new file mode 100644 index 0000000000..50016725d6 --- /dev/null +++ b/engines/adl/hires2.h @@ -0,0 +1,66 @@ +/* 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. + * + */ + +#ifndef ADL_HIRES2_H +#define ADL_HIRES2_H + +#include "common/str.h" + +#include "adl/adl_v2.h" +#include "adl/disk.h" + +namespace Common { +class ReadStream; +struct Point; +} + +namespace Adl { + +#define IDS_HR2_DISK_IMAGE "WIZARD.DSK" + +#define IDI_HR2_NUM_ROOMS 135 +#define IDI_HR2_NUM_MESSAGES 255 +#define IDI_HR2_NUM_VARS 40 +#define IDI_HR2_NUM_ITEM_PICS 38 +#define IDI_HR2_NUM_ITEM_OFFSETS 16 + +// Messages used outside of scripts +#define IDI_HR2_MSG_CANT_GO_THERE 123 +#define IDI_HR2_MSG_DONT_UNDERSTAND 19 +#define IDI_HR2_MSG_ITEM_DOESNT_MOVE 242 +#define IDI_HR2_MSG_ITEM_NOT_HERE 4 +#define IDI_HR2_MSG_THANKS_FOR_PLAYING 239 + +class HiRes2Engine : public AdlEngine_v2 { +public: + HiRes2Engine(OSystem *syst, const AdlGameDescription *gd) : AdlEngine_v2(syst, gd) { } + +private: + // AdlEngine + void runIntro() const; + void init(); + void initGameState(); +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/hires6.cpp b/engines/adl/hires6.cpp new file mode 100644 index 0000000000..c42b4165a6 --- /dev/null +++ b/engines/adl/hires6.cpp @@ -0,0 +1,444 @@ +/* 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/memstream.h" + +#include "adl/hires6.h" +#include "adl/display.h" +#include "adl/graphics.h" +#include "adl/disk.h" + +namespace Adl { + +static const char *disks[] = { "DARK1A.DSK", "DARK1B.NIB", "DARK2A.NIB", "DARK2B.NIB" }; + +#define SECTORS_PER_TRACK 16 +#define BYTES_PER_SECTOR 256 + +static Common::MemoryReadStream *loadSectors(DiskImage *disk, byte track, byte sector = SECTORS_PER_TRACK - 1, byte count = SECTORS_PER_TRACK) { + const int bufSize = count * BYTES_PER_SECTOR; + byte *const buf = (byte *)malloc(bufSize); + byte *p = buf; + + while (count-- > 0) { + StreamPtr stream(disk->createReadStream(track, sector, 0, 0)); + stream->read(p, BYTES_PER_SECTOR); + + if (stream->err() || stream->eos()) + error("Error loading from disk image"); + + p += BYTES_PER_SECTOR; + if (sector > 0) + --sector; + else { + ++track; + + // Skip VTOC track + if (track == 17) + ++track; + + sector = SECTORS_PER_TRACK - 1; + } + } + + return new Common::MemoryReadStream(buf, bufSize, DisposeAfterUse::YES); +} + +void HiRes6Engine::runIntro() const { + DiskImage_DSK *boot(new DiskImage_DSK()); + + if (!boot->open(disks[0])) + error("Failed to open disk image '%s'", disks[0]); + + StreamPtr stream(loadSectors(boot, 11, 1, 96)); + + _display->setMode(DISPLAY_MODE_HIRES); + _display->loadFrameBuffer(*stream); + _display->updateHiResScreen(); + delay(256 * 8609 / 1000); + + _display->loadFrameBuffer(*stream); + _display->updateHiResScreen(); + delay(256 * 8609 / 1000); + + _display->loadFrameBuffer(*stream); + + delete boot; + + // Load copyright string from boot file + Files_DOS33 *files(new Files_DOS33()); + + if (!files->open(disks[0])) + error("Failed to open disk image '%s'", disks[0]); + + stream.reset(files->createReadStream("\010\010\010\010\010\010")); + Common::String copyright(readStringAt(*stream, 0x103, APPLECHAR('\r'))); + + delete files; + + _display->updateHiResScreen(); + _display->home(); + _display->setMode(DISPLAY_MODE_MIXED); + _display->moveCursorTo(Common::Point(0, 21)); + _display->printString(copyright); + delay(256 * 8609 / 1000); +} + +void HiRes6Engine::init() { + _boot = new DiskImage_DSK(); + _graphics = new Graphics_v2(*_display); + + if (!_boot->open(disks[0])) + error("Failed to open disk image '%s'", disks[0]); + + StreamPtr stream(loadSectors(_boot, 0x7)); + + // Read parser messages + _strings.verbError = readStringAt(*stream, 0x666); + _strings.nounError = readStringAt(*stream, 0x6bd); + _strings.enterCommand = readStringAt(*stream, 0x6e9); + + // Read line feeds + _strings.lineFeeds = readStringAt(*stream, 0x408); + + // Read opcode strings (TODO) + _strings_v2.saveInsert = readStringAt(*stream, 0xad8); + readStringAt(*stream, 0xb95); // Confirm save + // _strings_v2.saveReplace + _strings_v2.restoreInsert = readStringAt(*stream, 0xc07); + // _strings_v2.restoreReplace + _strings.playAgain = readStringAt(*stream, 0xcdf, 0xff); + + _messageIds.cantGoThere = IDI_HR6_MSG_CANT_GO_THERE; + _messageIds.dontUnderstand = IDI_HR6_MSG_DONT_UNDERSTAND; + _messageIds.itemDoesntMove = IDI_HR6_MSG_ITEM_DOESNT_MOVE; + _messageIds.itemNotHere = IDI_HR6_MSG_ITEM_NOT_HERE; + _messageIds.thanksForPlaying = IDI_HR6_MSG_THANKS_FOR_PLAYING; + + // Item descriptions + stream.reset(loadSectors(_boot, 0x6, 0xb, 2)); + stream->seek(0x34); + for (uint i = 0; i < IDI_HR6_NUM_ITEM_DESCS; ++i) + _itemDesc.push_back(readString(*stream, 0xff)); + + // Load dropped item offsets + stream.reset(_boot->createReadStream(0x8, 0x9, 0x16)); + for (uint i = 0; i < IDI_HR6_NUM_ITEM_OFFSETS; ++i) { + Common::Point p; + p.x = stream->readByte(); + p.y = stream->readByte(); + _itemOffsets.push_back(p); + } + + // Location of game data for each disc + stream.reset(_boot->createReadStream(0x5, 0xa, 0x03)); + for (uint i = 0; i < sizeof(disks); ++i) { + DiskDataDesc desc; + desc.track = stream->readByte(); + desc.sector = stream->readByte(); + desc.offset = stream->readByte(); + desc.volume = stream->readByte(); + _diskDataDesc.push_back(desc); + } + + // DataBlockPtr offsets for each disk + stream.reset(_boot->createReadStream(0x3, 0xf, 0x03)); + for (uint i = 0; i < sizeof(disks); ++i) { + DiskOffset offset; + offset.track = stream->readByte(); + offset.sector = stream->readByte(); + _diskOffsets.push_back(offset); + } +} + +void HiRes6Engine::loadDisk(byte disk) { + delete _disk; + _disk = new DiskImage_NIB(); + + if (!_disk->open(disks[disk])) + error("Failed to open disk image '%s'", disks[disk]); + + _curDisk = 0; + + // Load item picture data (indexed on boot disk) + StreamPtr stream(_boot->createReadStream(0xb, 0xd, 0x08)); + _itemPics.clear(); + for (uint i = 0; i < IDI_HR6_NUM_ITEM_PICS; ++i) { + stream->readByte(); + _itemPics.push_back(readDataBlockPtr(*stream)); + } + + _curDisk = disk; + + byte track = _diskDataDesc[disk].track; + byte sector = _diskDataDesc[disk].sector; + uint offset = _diskDataDesc[disk].offset; + + applyDiskOffset(track, sector); + + for (uint block = 0; block < 7; ++block) { + stream.reset(_disk->createReadStream(track, sector, offset, 1)); + + uint16 addr = stream->readUint16LE(); + uint16 size = stream->readUint16LE(); + + stream.reset(_disk->createReadStream(track, sector, offset, size / 256 + 1)); + stream->skip(4); + + switch (addr) { + case 0x9000: { + // Messages + _messages.clear(); + uint count = size / 4; + for (uint i = 0; i < count; ++i) + _messages.push_back(readDataBlockPtr(*stream)); + break; + } + case 0x4a80: { + // Global pics + _pictures.clear(); + byte picNr; + while ((picNr = stream->readByte()) != 0xff) { + if (stream->eos() || stream->err()) + error("Error reading global pic list"); + _pictures[picNr] = readDataBlockPtr(*stream); + } + break; + } + case 0x4000: + // Verbs + loadWords(*stream, _verbs, _priVerbs); + break; + case 0x1800: + // Nouns + loadWords(*stream, _nouns, _priNouns); + break; + case 0x0e00: { + // Rooms + uint count = size / 14 - 1; + stream->skip(14); // Skip invalid room 0 + + _state.rooms.clear(); + for (uint i = 0; i < count; ++i) { + Room room; + stream->readByte(); // number + for (uint j = 0; j < 6; ++j) + room.connections[j] = stream->readByte(); + room.data = readDataBlockPtr(*stream); + room.picture = stream->readByte(); + room.curPicture = stream->readByte(); + room.isFirstTime = stream->readByte(); + _state.rooms.push_back(room); + } + break; + } + case 0x7b00: + // Global commands + readCommands(*stream, _globalCommands); + break; + case 0x9500: + // Room commands + readCommands(*stream, _roomCommands); + break; + default: + error("Unknown data block found (addr %04x; size %04x)", addr, size); + } + + offset += 4 + size; + while (offset >= 256) { + offset -= 256; + ++sector; + if (sector >= 16) { + sector = 0; + ++track; + } + } + } +} + +void HiRes6Engine::initGameState() { + _state.vars.resize(IDI_HR6_NUM_VARS); + + loadDisk(1); + + StreamPtr stream(_boot->createReadStream(0x3, 0xe, 0x03)); + + byte id; + while ((id = stream->readByte()) != 0xff) { + Item item = Item(); + item.id = id; + item.noun = stream->readByte(); + item.room = stream->readByte(); + item.picture = stream->readByte(); + item.isLineArt = stream->readByte(); // Now seems to be disk number + item.position.x = stream->readByte(); + item.position.y = stream->readByte(); + item.state = stream->readByte(); + item.description = stream->readByte(); + + stream->readByte(); // Struct size + + byte picListSize = stream->readByte(); + + // Flag to keep track of what has been drawn on the screen + stream->readByte(); + + for (uint i = 0; i < picListSize; ++i) + item.roomPictures.push_back(stream->readByte()); + + _state.items.push_back(item); + } + + _currVerb = _currNoun = 0; +} + +void HiRes6Engine::showRoom() { + bool redrawPic = false; + + if (getVar(26) == 0xfe) + setVar(26, 0); + else if (getVar(26) != 0xff) + setVar(26, _state.room); + + if (_state.room != _roomOnScreen) { + loadRoom(_state.room); + + if (getVar(26) < 0x80 && getCurRoom().isFirstTime) + setVar(26, 0); + + clearScreen(); + + if (!_state.isDark) + redrawPic = true; + } else { + if (getCurRoom().curPicture != _picOnScreen || _itemRemoved) + redrawPic = true; + } + + if (redrawPic) { + _roomOnScreen = _state.room; + _picOnScreen = getCurRoom().curPicture; + + drawPic(getCurRoom().curPicture); + _itemRemoved = false; + _itemsOnScreen = 0; + + Common::List<Item>::iterator item; + for (item = _state.items.begin(); item != _state.items.end(); ++item) + item->isOnScreen = false; + } + + if (!_state.isDark) + drawItems(); + + _display->updateHiResScreen(); + setVar(2, 0xff); + printString(_roomData.description); + + // FIXME: move to main loop? + _linesPrinted = 0; +} + +Common::String HiRes6Engine::formatVerbError(const Common::String &verb) const { + Common::String err = _strings.verbError; + + for (uint i = 0; i < verb.size(); ++i) + err.setChar(verb[i], i + 24); + + err.setChar(APPLECHAR(' '), 32); + + uint i = 24; + while (err[i] != APPLECHAR(' ')) + ++i; + + err.setChar(APPLECHAR('.'), i); + + return err; +} + +Common::String HiRes6Engine::formatNounError(const Common::String &verb, const Common::String &noun) const { + Common::String err = _strings.nounError; + + for (uint i = 0; i < noun.size(); ++i) + err.setChar(noun[i], i + 24); + + for (uint i = 35; i > 31; --i) + err.setChar(APPLECHAR(' '), i); + + uint i = 24; + while (err[i] != APPLECHAR(' ')) + ++i; + + err.setChar(APPLECHAR('I'), i + 1); + err.setChar(APPLECHAR('S'), i + 2); + err.setChar(APPLECHAR('.'), i + 3); + + return err; +} + +void HiRes6Engine::printString(const Common::String &str) { + Common::String s; + uint found = 0; + + // This does not emulate the corner cases of the original, hence this check + if (getVar(27) > 1) + error("Invalid value %i encountered for variable 27", getVar(27)); + + for (uint i = 0; i < str.size(); ++i) { + if (str[i] == '%') { + ++found; + if (found == 3) + found = 0; + } else { + if (found == 0 || found - 1 == getVar(27)) + s += str[i]; + } + } + + if (getVar(2) != 0xff) { + AdlEngine_v2::printString(s); + } else { + if (getVar(26) == 0) { + if (str.size() != 1 || APPLECHAR(str[0]) != APPLECHAR(' ')) + return AdlEngine_v2::printString(s); + setVar(2, APPLECHAR(' ')); + } else if (getVar(26) != 0xff) { + setVar(2, 'P'); + } else { + setVar(26, _state.room); + setVar(2, 1); + } + + doAllCommands(_globalCommands, _currVerb, _currNoun); + } +} + +Engine *HiRes6Engine_create(OSystem *syst, const AdlGameDescription *gd) { + return new HiRes6Engine(syst, gd); +} + +} // End of namespace Adl diff --git a/engines/adl/hires6.h b/engines/adl/hires6.h new file mode 100644 index 0000000000..4bd2bcc7cc --- /dev/null +++ b/engines/adl/hires6.h @@ -0,0 +1,92 @@ +/* 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. + * + */ + +#ifndef ADL_HIRES6_H +#define ADL_HIRES6_H + +#include "common/str.h" + +#include "adl/adl_v3.h" +#include "adl/disk.h" + +namespace Common { +class ReadStream; +struct Point; +} + +namespace Adl { + +#define IDI_HR6_NUM_ROOMS 35 +#define IDI_HR6_NUM_MESSAGES 256 +#define IDI_HR6_NUM_VARS 40 +#define IDI_HR6_NUM_ITEM_DESCS 15 +#define IDI_HR6_NUM_ITEM_PICS 15 +#define IDI_HR6_NUM_ITEM_OFFSETS 16 + +// Messages used outside of scripts +#define IDI_HR6_MSG_CANT_GO_THERE 249 +#define IDI_HR6_MSG_DONT_UNDERSTAND 247 +#define IDI_HR6_MSG_ITEM_DOESNT_MOVE 253 +#define IDI_HR6_MSG_ITEM_NOT_HERE 254 +#define IDI_HR6_MSG_THANKS_FOR_PLAYING 252 + +struct DiskDataDesc { + byte track; + byte sector; + byte offset; + byte volume; +}; + +class HiRes6Engine : public AdlEngine_v3 { +public: + HiRes6Engine(OSystem *syst, const AdlGameDescription *gd) : + AdlEngine_v3(syst, gd), + _boot(nullptr), + _currVerb(0), + _currNoun(0) { + } + + ~HiRes6Engine() { delete _boot; } + +private: + // AdlEngine + void runIntro() const; + void init(); + void initGameState(); + void printRoomDescription(); + void showRoom(); + Common::String formatVerbError(const Common::String &verb) const; + Common::String formatNounError(const Common::String &verb, const Common::String &noun) const; + + // AdlEngine_v2 + void printString(const Common::String &str); + + void loadDisk(byte disk); + + DiskImage_DSK *_boot; + byte _currVerb, _currNoun; + Common::Array<DiskDataDesc> _diskDataDesc; +}; + +} // End of namespace Adl + +#endif diff --git a/engines/adl/module.mk b/engines/adl/module.mk new file mode 100644 index 0000000000..7ab37efc67 --- /dev/null +++ b/engines/adl/module.mk @@ -0,0 +1,28 @@ +MODULE := engines/adl + +MODULE_OBJS := \ + adl.o \ + adl_v2.o \ + adl_v3.o \ + console.o \ + detection.o \ + disk.o \ + display.o \ + graphics.o \ + graphics_v1.o \ + graphics_v2.o \ + hires1.o \ + hires2.o \ + hires6.o \ + speaker.o + +MODULE_DIRS += \ + engines/adl + +# This module can be built as a plugin +ifeq ($(ENABLE_ADL), DYNAMIC_PLUGIN) +PLUGIN := 1 +endif + +# Include common rules +include $(srcdir)/rules.mk diff --git a/engines/adl/speaker.cpp b/engines/adl/speaker.cpp new file mode 100644 index 0000000000..532d361cd9 --- /dev/null +++ b/engines/adl/speaker.cpp @@ -0,0 +1,94 @@ +/* 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/events.h" + +#include "engines/engine.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" + +#include "adl/speaker.h" +#include "adl/adl.h" + +namespace Adl { + +// Number of times to duplicate each sample +#define SAMPLE_DUP 4 +// Bell frequency in Hz +#define BELL_FREQ 1000 +// Sample rate +#define SAMPLE_RATE (BELL_FREQ * SAMPLE_DUP * 2) +// Number of waves per 0.1 seconds (bell length) +#define BELL_WAVE_COUNT (SAMPLE_RATE / 10 / SAMPLE_DUP / 2) +// Length of bell in samples +#define BELL_LEN (BELL_WAVE_COUNT * SAMPLE_DUP * 2) +// Length of silence in samples +#define SILENCE_LEN (SAMPLE_RATE / 80) + +Speaker::~Speaker() { + delete[] _bell; + delete[] _silence; +} + +Speaker::Speaker() { + _bell = new byte[BELL_LEN]; + + byte *buf = _bell; + for (uint i = 0; i < BELL_WAVE_COUNT; ++i) { + for (uint j = 0; j < SAMPLE_DUP; ++j) + *buf++ = 0x00; + for (uint j = 0; j < SAMPLE_DUP; ++j) + *buf++ = 0xff; + } + + _silence = new byte[SILENCE_LEN]; + + buf = _silence; + for (uint i = 0; i < SILENCE_LEN; ++i) + *buf++ = 0x80; +} + +void Speaker::bell(uint count) { + Audio::QueuingAudioStream *stream = Audio::makeQueuingAudioStream(SAMPLE_RATE, false); + Audio::SoundHandle handle; + + stream->queueBuffer(_bell, BELL_LEN, DisposeAfterUse::NO, Audio::FLAG_UNSIGNED); + + for (uint i = 1; i < count; ++i) { + stream->queueBuffer(_silence, SILENCE_LEN, DisposeAfterUse::NO, Audio::FLAG_UNSIGNED); + stream->queueBuffer(_bell, BELL_LEN, DisposeAfterUse::NO, Audio::FLAG_UNSIGNED); + } + + stream->finish(); + + g_system->getMixer()->playStream(Audio::Mixer::kPlainSoundType, &handle, stream); + + while (!g_engine->shouldQuit() && g_system->getMixer()->isSoundHandleActive(handle)) { + Common::Event event; + static_cast<AdlEngine *>(g_engine)->pollEvent(event); + g_system->delayMillis(16); + } +} + +} // End of namespace Adl diff --git a/engines/adl/speaker.h b/engines/adl/speaker.h new file mode 100644 index 0000000000..31aaac32d2 --- /dev/null +++ b/engines/adl/speaker.h @@ -0,0 +1,49 @@ +/* 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. + * + */ + +#ifndef ADL_SPEAKER_H +#define ADL_SPEAKER_H + +#include "common/types.h" + +#include "audio/mixer.h" + +namespace Audio { +class AudioStream; +} + +namespace Adl { + +class Speaker { +public: + Speaker(); + ~Speaker(); + + void bell(uint count); + +private: + byte *_bell, *_silence; +}; + +} // End of namespace Adl + +#endif |
