389 lines
9.9 KiB
C++
389 lines
9.9 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 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, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "glk/advsys/game.h"
|
|
#include "glk/advsys/definitions.h"
|
|
#include "common/memstream.h"
|
|
|
|
namespace Glk {
|
|
namespace AdvSys {
|
|
|
|
void Decrypter::decrypt(byte *data, size_t size) {
|
|
for (size_t idx = 0; idx < size; ++idx, ++data)
|
|
*data = ~(*data + 30);
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
#define HEADER_SIZE 62
|
|
|
|
bool Header::init(Common::SeekableReadStream *s) {
|
|
_valid = false;
|
|
byte data[HEADER_SIZE];
|
|
|
|
// Read in the data
|
|
if (s->read(data, HEADER_SIZE) != HEADER_SIZE)
|
|
return false;
|
|
decrypt(data, HEADER_SIZE);
|
|
Common::MemoryReadStream ms(data, HEADER_SIZE, DisposeAfterUse::NO);
|
|
|
|
// Validate the header
|
|
_valid = !strncmp((const char *)data + 2, "ADVSYS", 6);
|
|
if (!_valid)
|
|
return false;
|
|
|
|
_size = ms.readUint16LE();
|
|
ms.skip(6);
|
|
_headerVersion = ms.readUint16LE();
|
|
_name = Common::String((const char *)data + 10, (const char *)data + 28);
|
|
ms.skip(18);
|
|
_version = ms.readUint16LE();
|
|
_wordTableOffset = ms.readUint16LE();
|
|
_wordTypeTableOffset = ms.readUint16LE();
|
|
_objectTableOffset = ms.readUint16LE();
|
|
_actionTableOffset = ms.readUint16LE();
|
|
_variableTableOffset = ms.readUint16LE();
|
|
_dataSpaceOffset = ms.readUint16LE();
|
|
_codeSpaceOffset = ms.readUint16LE();
|
|
_dataBlockOffset = ms.readUint16LE();
|
|
_messageBlockOffset = ms.readUint16LE();
|
|
_initCodeOffset = ms.readUint16LE();
|
|
_updateCodeOffset = ms.readUint16LE();
|
|
_beforeOffset = ms.readUint16LE();
|
|
_afterOffset = ms.readUint16LE();
|
|
_errorHandlerOffset = ms.readUint16LE();
|
|
_saveAreaOffset = ms.readUint16LE();
|
|
_saveSize = ms.readUint16LE();
|
|
|
|
return true;
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
#define MAX_VERSION 102
|
|
#define WORD_SIZE 6
|
|
|
|
/**
|
|
* Property flags
|
|
*/
|
|
enum PropertyFlag {
|
|
P_CLASS = 0x8000
|
|
};
|
|
|
|
/**
|
|
* Link fields
|
|
*/
|
|
enum LinkField {
|
|
L_DATA = 0,
|
|
L_NEXT = 2,
|
|
L_SIZE = 4
|
|
};
|
|
|
|
Game::Game() : Header(), _stream(nullptr), _restartFlag(false), _residentOffset(0), _wordCount(0),
|
|
_objectCount(0), _actionCount(0), _variableCount(0), _wordTable(nullptr), _wordTypeTable(nullptr),
|
|
_objectTable(nullptr), _actionTable(nullptr), _variableTable(nullptr), _saveArea(nullptr),
|
|
_msgBlockNum(-1), _msgBlockOffset(0) {
|
|
_msgCache.resize(MESSAGE_CACHE_SIZE);
|
|
for (int idx = 0; idx < MESSAGE_CACHE_SIZE; ++idx)
|
|
_msgCache[idx] = new CacheEntry();
|
|
}
|
|
|
|
Game::~Game() {
|
|
for (int idx = 0; idx < MESSAGE_CACHE_SIZE; ++idx)
|
|
delete _msgCache[idx];
|
|
}
|
|
|
|
bool Game::init(Common::SeekableReadStream *s) {
|
|
// Store a copy of the game file stream
|
|
_stream = s;
|
|
|
|
// Load the header
|
|
s->seek(0);
|
|
if (!Header::init(s))
|
|
return false;
|
|
|
|
if (_headerVersion < 101 || _headerVersion > MAX_VERSION)
|
|
error("Wrong version number");
|
|
|
|
// Load the needed resident game data and decrypt it
|
|
_residentOffset = _dataBlockOffset * 512;
|
|
s->seek(_residentOffset);
|
|
|
|
_data.resize(_size);
|
|
if (!s->read(&_data[0], _size))
|
|
return false;
|
|
decrypt(&_data[0], _size);
|
|
|
|
_wordTable = &_data[_wordTableOffset];
|
|
_wordTypeTable = &_data[_wordTypeTableOffset - 1];
|
|
_objectTable = &_data[_objectTableOffset];
|
|
_actionTable = &_data[_actionTableOffset];
|
|
_variableTable = &_data[_variableTableOffset];
|
|
_saveArea = &_data[_saveAreaOffset];
|
|
_dataSpace = &_data[_dataSpaceOffset];
|
|
_codeSpace = &_data[_codeSpaceOffset];
|
|
|
|
_wordCount = READ_LE_UINT16(_wordTable);
|
|
_objectCount = READ_LE_UINT16(_objectTable);
|
|
_actionCount = READ_LE_UINT16(_actionTable);
|
|
_variableCount = READ_LE_UINT16(_variableTable);
|
|
|
|
setVariable(V_OCOUNT, _objectCount);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Game::restart() {
|
|
_stream->seek(_residentOffset + _saveAreaOffset);
|
|
_stream->read(_saveArea, _saveSize);
|
|
decrypt(_saveArea, _saveSize);
|
|
|
|
setVariable(V_OCOUNT, _objectCount);
|
|
_restartFlag = true;
|
|
}
|
|
|
|
bool Game::shouldRestart() {
|
|
bool result = _restartFlag;
|
|
_restartFlag = false;
|
|
return result;
|
|
}
|
|
|
|
void Game::saveGameData(Common::WriteStream &ws) {
|
|
ws.write(_saveArea, _saveSize);
|
|
}
|
|
|
|
void Game::loadGameData(Common::ReadStream &rs) {
|
|
rs.read(_saveArea, _saveSize);
|
|
}
|
|
|
|
int Game::findWord(const Common::String &word) const {
|
|
// Limit the word to the maximum allowable size
|
|
Common::String w(word.c_str(), MIN(word.size(), (uint)WORD_SIZE));
|
|
|
|
// Iterate over the dictionary for the word
|
|
for (int idx = 1; idx <= _wordCount; ++idx) {
|
|
int wordOffset = READ_LE_UINT16(_wordTable + idx * 2);
|
|
if (w == (const char *)_dataSpace + wordOffset + 2)
|
|
return readWord(wordOffset);
|
|
}
|
|
|
|
return NIL;
|
|
}
|
|
|
|
int Game::checkVerb(const Common::Array<int> &verbs) {
|
|
// Iterate through the actions
|
|
for (int idx = 1; idx <= _actionCount; ++idx) {
|
|
if (hasVerb(idx, verbs))
|
|
return idx;
|
|
}
|
|
|
|
return NIL;
|
|
}
|
|
|
|
int Game::findAction(const Common::Array<int> &verbs, int preposition, int flag) {
|
|
// Iterate through the actions
|
|
for (int idx = 1; idx <= _actionCount; ++idx) {
|
|
if ((preposition && !hasPreposition(idx, preposition)) || !hasVerb(idx, verbs))
|
|
continue;
|
|
|
|
int mask = ~getActionByte(idx, A_MASK);
|
|
if ((flag & mask) == (getActionByte(idx, A_FLAG) & mask))
|
|
return idx;
|
|
}
|
|
|
|
return NIL;
|
|
}
|
|
|
|
int Game::getObjectProperty(int obj, int prop) {
|
|
int field;
|
|
|
|
for (; obj; obj = getObjectField(obj, O_CLASS)) {
|
|
if ((field = findProperty(obj, prop)) != 0)
|
|
return getObjectField(obj, field);
|
|
}
|
|
|
|
return NIL;
|
|
}
|
|
|
|
int Game::setObjectProperty(int obj, int prop, int val) {
|
|
int field;
|
|
|
|
for (; obj; obj = getObjectField(obj, O_CLASS)) {
|
|
if ((field = findProperty(obj, prop)) != 0)
|
|
return setObjectField(obj, field, val);
|
|
}
|
|
|
|
return NIL;
|
|
}
|
|
|
|
int Game::getObjectLocation(int obj) const {
|
|
if (obj < 1 || obj > _objectCount)
|
|
error("Invalid object number %d", obj);
|
|
|
|
return READ_LE_UINT16(_objectTable + obj * 2);
|
|
}
|
|
|
|
int Game::getActionLocation(int action) const {
|
|
if (action < 1 || action > _actionCount)
|
|
error("Invalid action number %d", action);
|
|
|
|
return READ_LE_UINT16(_actionTable + action * 2);
|
|
}
|
|
|
|
int Game::getVariable(int variableNum) {
|
|
if (variableNum < 1 || variableNum > _variableCount)
|
|
error("Invalid ariable number %d", variableNum);
|
|
|
|
return READ_LE_UINT16(_variableTable + variableNum * 2);
|
|
}
|
|
|
|
void Game::setVariable(int variableNum, int value) {
|
|
if (variableNum < 1 || variableNum > _variableCount)
|
|
error("Invalid ariable number %d", variableNum);
|
|
|
|
WRITE_LE_UINT16(_variableTable + variableNum * 2, value);
|
|
}
|
|
|
|
int Game::findProperty(int obj, int prop) const {
|
|
int nProp = getObjectField(obj, O_NPROPERTIES);
|
|
|
|
for (int idx = 0, p = 0; idx < nProp; ++idx, p += 4) {
|
|
if ((getObjectField(obj, O_PROPERTIES + p) & ~P_CLASS) == prop)
|
|
return O_PROPERTIES + p + 2;
|
|
}
|
|
|
|
return NIL;
|
|
}
|
|
|
|
bool Game::hasNoun(int obj, int noun) const {
|
|
for (; obj; obj = getObjectField(obj, O_CLASS)) {
|
|
if (inList(getObjectField(obj, O_NOUNS), noun))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Game::hasAdjective(int obj, int adjective) const {
|
|
for (; obj; obj = getObjectField(obj, O_CLASS)) {
|
|
if (inList(getObjectField(obj, O_ADJECTIVES), adjective))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Game::hasVerb(int act, const Common::Array<int> &verbs) const {
|
|
// Get the list of verbs
|
|
int link = getActionField(act, A_VERBS);
|
|
|
|
// Look for the verb
|
|
for (; link; link = readWord(link + L_NEXT)) {
|
|
Common::Array<int>::const_iterator verb = verbs.begin();
|
|
int word = readWord(link + L_DATA);
|
|
|
|
for (; verb < verbs.end() && word; ++verb, word = readWord(word + L_NEXT)) {
|
|
if (*verb != readWord(word + L_DATA))
|
|
break;
|
|
}
|
|
|
|
if (verb == verbs.end() && !word)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Game::inList(int link, int word) const {
|
|
for (; link; link = readWord(link + L_NEXT)) {
|
|
if (word == readWord(link + L_DATA))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Common::String Game::readString(int msg) {
|
|
// Get the block to use, and ensure it's loaded
|
|
_msgBlockNum = msg >> 7;
|
|
_msgBlockOffset = (msg & 0x7f) << 2;
|
|
readMsgBlock();
|
|
|
|
// Read the string
|
|
Common::String result;
|
|
char c;
|
|
|
|
while ((c = readMsgChar()) != '\0')
|
|
result += c;
|
|
|
|
return result;
|
|
}
|
|
|
|
char Game::readMsgChar() {
|
|
if (_msgBlockOffset >= MESSAGE_BLOCK_SIZE) {
|
|
// Move to the next block
|
|
++_msgBlockNum;
|
|
_msgBlockOffset = 0;
|
|
readMsgBlock();
|
|
}
|
|
|
|
// Return next character
|
|
return _msgCache[0]->_data[_msgBlockOffset++];
|
|
}
|
|
|
|
void Game::readMsgBlock() {
|
|
CacheEntry *ce;
|
|
|
|
// Check to see if the specified block is in the cache
|
|
for (int idx = 0; idx < MESSAGE_CACHE_SIZE; ++idx) {
|
|
if (_msgCache[idx]->_blockNum == _msgBlockNum) {
|
|
// If it's not already at the top of the list, move it there to ensure
|
|
// it'll be last to be unloaded as new blocks are loaded in
|
|
if (idx != 0) {
|
|
ce = _msgCache[idx];
|
|
_msgCache.remove_at(idx);
|
|
_msgCache.insert_at(0, ce);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// At this point we need to load a new block in. Discard the block at the end
|
|
// and move it to the start for storing the new block to load
|
|
ce = _msgCache.back();
|
|
_msgCache.remove_at(_msgCache.size() - 1);
|
|
_msgCache.insert_at(0, ce);
|
|
|
|
// Load the new block
|
|
ce->_blockNum = _msgBlockNum;
|
|
_stream->seek((_messageBlockOffset + _msgBlockNum) << 9);
|
|
if (_stream->read(&ce->_data[0], MESSAGE_BLOCK_SIZE) != MESSAGE_BLOCK_SIZE)
|
|
error("Error reading message block");
|
|
|
|
// Decode the loaded block
|
|
for (int idx = 0; idx < MESSAGE_BLOCK_SIZE; ++idx)
|
|
ce->_data[idx] = (ce->_data[idx] + 30) & 0xff;
|
|
}
|
|
|
|
} // End of namespace AdvSys
|
|
} // End of namespace Glk
|