mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-15 06:08:35 +00:00
1019 lines
29 KiB
C++
1019 lines
29 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
#include "mads/conversations.h"
|
|
#include "mads/mads.h"
|
|
#include "mads/compression.h"
|
|
#include "common/file.h"
|
|
#include "common/util.h" // for Common::hexdump
|
|
|
|
namespace MADS {
|
|
|
|
GameConversations::GameConversations(MADSEngine *vm) : _vm(vm) {
|
|
_runningConv = nullptr;
|
|
_restoreRunning = 0;
|
|
_playerEnabled = false;
|
|
_inputMode = kInputBuildingSentences;
|
|
_startFrameNumber = 0;
|
|
_speakerVal = 0;
|
|
_currentMode = CONVMODE_NONE;
|
|
_priorMode = CONVMODE_NONE;
|
|
_popupVisible = false;
|
|
_verbId = 0;
|
|
_vars = _nextStartNode = nullptr;
|
|
_heroTrigger = 0;
|
|
_heroTriggerMode = SEQUENCE_TRIGGER_PARSER;
|
|
_interlocutorTrigger = 0;
|
|
_interlocutorTriggerMode = SEQUENCE_TRIGGER_PARSER;
|
|
_currentNode = 0;
|
|
_dialogNodeOffset = _dialogNodeSize = 0;
|
|
_dialog = nullptr;
|
|
_dialogAltFlag = false;
|
|
_personSpeaking = 0;
|
|
|
|
// Mark all conversation slots as empty
|
|
for (int idx = 0; idx < MAX_CONVERSATIONS; ++idx)
|
|
_conversations[idx]._convId = -1;
|
|
}
|
|
|
|
GameConversations::~GameConversations() {
|
|
}
|
|
|
|
void GameConversations::load(int id) {
|
|
// Scan through the conversation list for a free slot
|
|
int slotIndex = -1;
|
|
for (int idx = 0; idx < MAX_CONVERSATIONS && slotIndex == -1; ++idx) {
|
|
if (_conversations[idx]._convId == -1)
|
|
slotIndex = idx;
|
|
}
|
|
if (slotIndex == -1)
|
|
error("Too many conversations loaded");
|
|
|
|
// Set the conversation the slot will contain
|
|
_conversations[slotIndex]._convId = id;
|
|
|
|
// Load the conversation data
|
|
Common::String cnvFilename = Common::String::format("CONV%03d.CNV", id);
|
|
_conversations[slotIndex]._data.load(cnvFilename);
|
|
|
|
// Load the conversation's CND data
|
|
Common::String cndFilename = Common::String::format("CONV%03d.CND", id);
|
|
_conversations[slotIndex]._cnd.load(cndFilename);
|
|
}
|
|
|
|
ConversationEntry *GameConversations::getConv(int convId) {
|
|
for (uint idx = 0; idx < MAX_CONVERSATIONS; ++idx) {
|
|
if (_conversations[idx]._convId == convId)
|
|
return &_conversations[idx];
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void GameConversations::run(int id) {
|
|
// If another conversation is running, then stop it first
|
|
if (_runningConv)
|
|
stop();
|
|
|
|
// Get the next conversation to run
|
|
_runningConv = getConv(id);
|
|
if (!_runningConv)
|
|
error("Specified conversation %d not loaded", id);
|
|
|
|
// Initialize needed fields
|
|
_startFrameNumber = _vm->_events->getFrameCounter();
|
|
_playerEnabled = _vm->_game->_player._stepEnabled;
|
|
_inputMode = _vm->_game->_screenObjects._inputMode;
|
|
_heroTrigger = 0;
|
|
_interlocutorTrigger = 0;
|
|
_popupVisible = false;
|
|
_currentMode = CONVMODE_0;
|
|
_verbId = -1;
|
|
_speakerVal = 1;
|
|
_personSpeaking = 1;
|
|
|
|
// Initialize speaker arrays
|
|
Common::fill(&_speakerActive[0], &_speakerActive[MAX_SPEAKERS], false);
|
|
Common::fill(&_speakerSeries[0], &_speakerSeries[MAX_SPEAKERS], -1);
|
|
Common::fill(&_speakerFrame[0], &_speakerFrame[MAX_SPEAKERS], 1);
|
|
Common::fill(&_popupX[0], &_popupX[MAX_SPEAKERS], POPUP_CENTER);
|
|
Common::fill(&_popupY[0], &_popupY[MAX_SPEAKERS], POPUP_CENTER);
|
|
Common::fill(&_popupMaxLen[0], &_popupMaxLen[MAX_SPEAKERS], 30);
|
|
|
|
// Start the conversation
|
|
start();
|
|
|
|
// Setup variables to point to data in the speaker arrays
|
|
setVariable(2, &_speakerVal);
|
|
for (int idx = 0; idx < MAX_SPEAKERS; ++idx) {
|
|
setVariable(3 + idx, &_speakerFrame[idx]);
|
|
setVariable(8 + idx, &_popupX[idx]);
|
|
setVariable(13 + idx, &_popupY[idx]);
|
|
setVariable(18 + idx, &_popupMaxLen[idx]);
|
|
}
|
|
|
|
// Load sprite data for speaker portraits
|
|
for (uint idx = 0; idx < _runningConv->_data._speakerCount; ++idx) {
|
|
const Common::String &portraitName = _runningConv->_data._portraits[idx];
|
|
_speakerSeries[idx] = _vm->_game->_scene._sprites.addSprites(portraitName, PALFLAG_RESERVED);
|
|
|
|
if (_speakerSeries[idx] > 0) {
|
|
_speakerActive[idx] = true;
|
|
_speakerFrame[idx] = _runningConv->_data._speakerFrame[idx];
|
|
}
|
|
}
|
|
|
|
// Refresh colors if needed
|
|
if (_vm->_game->_kernelMode == KERNEL_ACTIVE_CODE)
|
|
_vm->_palette->refreshSceneColors();
|
|
}
|
|
|
|
void GameConversations::start() {
|
|
assert(_runningConv->_cnd._vars.size() >= 2);
|
|
_vars = &_runningConv->_cnd._vars[0];
|
|
_nextStartNode = &_runningConv->_cnd._vars[1];
|
|
|
|
_runningConv->_cnd._currentNode = -1;
|
|
_runningConv->_cnd._numImports = 0;
|
|
_runningConv->_cnd._vars[0].setValue(_nextStartNode->_val);
|
|
|
|
// Store a reference to the variables list in the script handler for later reference
|
|
ScriptEntry::Conditional::_vars = &_runningConv->_cnd._vars;
|
|
}
|
|
|
|
void GameConversations::setVariable(uint idx, int val) {
|
|
if (active())
|
|
_runningConv->_cnd._vars[idx].setValue(val);
|
|
}
|
|
|
|
void GameConversations::setVariable(uint idx, int *val) {
|
|
if (active())
|
|
_runningConv->_cnd._vars[idx].setValue(val);
|
|
}
|
|
|
|
void GameConversations::setStartNode(uint nodeIndex) {
|
|
assert(_nextStartNode && _nextStartNode->_isPtr == false);
|
|
_nextStartNode->_val = nodeIndex;
|
|
}
|
|
|
|
void GameConversations::stop() {
|
|
// Only need to proceed if there is an active conversation
|
|
if (!active())
|
|
return;
|
|
|
|
// Reset player enabled state if needed
|
|
if (_vm->_game->_kernelMode == KERNEL_ACTIVE_CODE)
|
|
_vm->_game->_player._stepEnabled = _playerEnabled;
|
|
|
|
// Remove any visible dialog window
|
|
removeActiveWindow();
|
|
|
|
// Release any sprites used for character portraits
|
|
for (int idx = 0; idx < _runningConv->_data._speakerCount; ++idx) {
|
|
if (_speakerActive[idx])
|
|
_vm->_game->_scene._sprites.remove(_speakerSeries[idx]);
|
|
}
|
|
|
|
// Flag conversation as no longer running
|
|
_runningConv = nullptr;
|
|
|
|
if (_inputMode == kInputConversation)
|
|
_vm->_game->_scene._userInterface.emptyConversationList();
|
|
|
|
_vm->_game->_scene._userInterface.setup(_inputMode);
|
|
}
|
|
|
|
void GameConversations::exportPointer(int *ptr) {
|
|
// Only need to proceed if there is an active conversation
|
|
if (!active())
|
|
return;
|
|
|
|
// Also don't proceed if the number of allowed imports has already been reached
|
|
if (_runningConv->_cnd._numImports >= _runningConv->_data._maxImports)
|
|
return;
|
|
|
|
// Get the variable to use for this next import and set it's value
|
|
int variableIndex = _runningConv->_cnd._importVariables[
|
|
_runningConv->_cnd._numImports++];
|
|
setVariable(variableIndex, ptr);
|
|
}
|
|
|
|
void GameConversations::exportValue(int val) {
|
|
// Only need to proceed if there is an active conversation
|
|
if (!active())
|
|
return;
|
|
|
|
// Also don't proceed if the number of allowed imports has already been reached
|
|
if (_runningConv->_cnd._numImports >= _runningConv->_data._maxImports)
|
|
return;
|
|
|
|
// Get the variable to use for this next import and set it's value
|
|
int variableIndex = _runningConv->_cnd._importVariables[
|
|
_runningConv->_cnd._numImports++];
|
|
setVariable(variableIndex, val);
|
|
}
|
|
|
|
void GameConversations::setHeroTrigger(int val) {
|
|
_heroTrigger = val;
|
|
_heroTriggerMode = _vm->_game->_triggerSetupMode;
|
|
}
|
|
|
|
void GameConversations::setInterlocutorTrigger(int val) {
|
|
_interlocutorTrigger = val;
|
|
_interlocutorTriggerMode = _vm->_game->_triggerSetupMode;
|
|
}
|
|
|
|
int *GameConversations::getVariable(int idx) {
|
|
assert(idx >= 0); // TODO: Some negative values are allowed? Investigate
|
|
return _vars[idx].getValue();
|
|
}
|
|
|
|
void GameConversations::hold() {
|
|
if (_currentMode != CONVMODE_NONE) {
|
|
_priorMode = _currentMode;
|
|
_currentMode = CONVMODE_NONE;
|
|
}
|
|
}
|
|
|
|
void GameConversations::release() {
|
|
if (_currentMode == CONVMODE_NONE) {
|
|
_currentMode = _priorMode;
|
|
if (_currentMode == 1 || _currentMode == 2)
|
|
update(true);
|
|
}
|
|
}
|
|
|
|
void GameConversations::flagEntry(DialogCommand mode, int entryIndex) {
|
|
assert(_runningConv);
|
|
uint &flags = _runningConv->_cnd._entryFlags[entryIndex];
|
|
|
|
switch (mode) {
|
|
case CMD_1:
|
|
flags |= ENTRYFLAG_4000;
|
|
flags &= ~ENTRYFLAG_8000;
|
|
break;
|
|
|
|
case CMD_HIDE:
|
|
flags &= ~ENTRYFLAG_8000;
|
|
break;
|
|
|
|
case CMD_UNHIDE:
|
|
if (!(flags & ENTRYFLAG_4000))
|
|
flags |= ENTRYFLAG_8000;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GameConversations::reset(int id) {
|
|
warning("TODO: GameConversations::reset");
|
|
}
|
|
|
|
void GameConversations::update(bool flag) {
|
|
// Only need to proceed if there is an active conversation
|
|
if (!active())
|
|
return;
|
|
|
|
ConversationVar &var0 = _runningConv->_cnd._vars[0];
|
|
|
|
switch (_currentMode) {
|
|
case CONVMODE_0:
|
|
assert(var0.isNumeric());
|
|
if (var0._val < 0) {
|
|
if (_vm->_game->_scene._frameStartTime >= _startFrameNumber) {
|
|
removeActiveWindow();
|
|
if (_heroTrigger) {
|
|
_vm->_game->_scene._action._activeAction._verbId = _verbId;
|
|
_vm->_game->_trigger = _heroTrigger;
|
|
_vm->_game->_triggerMode = _heroTriggerMode;
|
|
_heroTrigger = 0;
|
|
}
|
|
|
|
_currentMode = CONVMODE_STOP;
|
|
}
|
|
} else {
|
|
bool isActive = nextNode();
|
|
_currentNode = var0._val;
|
|
|
|
if (isActive) {
|
|
_verbId = _runningConv->_data._nodes[_currentNode]._index;
|
|
_vm->_game->_scene._action._activeAction._verbId = _verbId;
|
|
_vm->_game->_scene._action._inProgress = true;
|
|
_vm->_game->_scene._action._savedFields._commandError = false;
|
|
_currentMode = CONVMODE_1;
|
|
} else {
|
|
_currentMode = generateMenu();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CONVMODE_1:
|
|
if (flag)
|
|
_currentMode = CONVMODE_3;
|
|
break;
|
|
|
|
case CONVMODE_2:
|
|
if (flag) {
|
|
_vm->_game->_player._stepEnabled = false;
|
|
_verbId = _vm->_game->_scene._action._activeAction._verbId;
|
|
|
|
if (!(_runningConv->_cnd._entryFlags[_verbId] & ENTRYFLAG_2))
|
|
flagEntry(CMD_HIDE, _verbId);
|
|
|
|
removeActiveWindow();
|
|
_vm->_game->_scene._userInterface.emptyConversationList();
|
|
_vm->_game->_scene._userInterface.setup(kInputConversation);
|
|
_personSpeaking = 0;
|
|
executeEntry(_verbId);
|
|
|
|
ConvDialog &dialog = _runningConv->_data._dialogs[_verbId];
|
|
if (dialog._speechIndex) {
|
|
_runningConv->_cnd._messageList3.clear();
|
|
_runningConv->_cnd._messageList3.push_back(dialog._speechIndex);
|
|
}
|
|
|
|
generateText(dialog._textLineIndex, _runningConv->_cnd._messageList3);
|
|
_currentMode = CONVMODE_0;
|
|
|
|
if (_heroTrigger) {
|
|
_vm->_game->_scene._action._activeAction._verbId = _verbId;
|
|
_vm->_game->_trigger = _heroTrigger;
|
|
_vm->_game->_triggerMode = _heroTriggerMode;
|
|
_heroTrigger = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CONVMODE_3:
|
|
if (_vm->_game->_scene._frameStartTime >= _startFrameNumber) {
|
|
removeActiveWindow();
|
|
_personSpeaking = 0;
|
|
executeEntry(_verbId);
|
|
generateMessage(_runningConv->_cnd._messageList1, _runningConv->_cnd._messageList3);
|
|
|
|
if (_heroTrigger && _popupVisible) {
|
|
_vm->_game->_scene._action._activeAction._verbId = _verbId;
|
|
_vm->_game->_trigger = _heroTrigger;
|
|
_vm->_game->_triggerMode = _heroTriggerMode;
|
|
_heroTrigger = 0;
|
|
}
|
|
|
|
_currentMode = CONVMODE_4;
|
|
}
|
|
break;
|
|
|
|
case CONVMODE_4:
|
|
if (_vm->_game->_scene._frameStartTime >= _startFrameNumber) {
|
|
removeActiveWindow();
|
|
_personSpeaking = _speakerVal;
|
|
|
|
generateMessage(_runningConv->_cnd._messageList2, _runningConv->_cnd._messageList4);
|
|
|
|
if (_interlocutorTrigger && _popupVisible) {
|
|
_vm->_game->_scene._action._activeAction._verbId = _verbId;
|
|
_vm->_game->_trigger = _interlocutorTrigger;
|
|
_vm->_game->_triggerMode = _interlocutorTriggerMode;
|
|
_interlocutorTrigger = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CONVMODE_STOP:
|
|
stop();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
warning("TODO: GameConversations::update");
|
|
}
|
|
|
|
void GameConversations::removeActiveWindow() {
|
|
warning("TODO: GameConversations::removeActiveWindow");
|
|
}
|
|
|
|
ConversationMode GameConversations::generateMenu() {
|
|
error("TODO: GameConversations::generateMenu");
|
|
}
|
|
|
|
void GameConversations::generateText(int textLineIndex, Common::Array<int> &messages) {
|
|
_dialogAltFlag = true;
|
|
|
|
error("TODO: GameConversations::generateText");
|
|
}
|
|
|
|
void GameConversations::generateMessage(Common::Array<int> &messageList, Common::Array<int> &voiceList) {
|
|
_dialogAltFlag = false;
|
|
if (messageList.size() == 0)
|
|
return;
|
|
|
|
if (_dialog)
|
|
delete _dialog;
|
|
|
|
// Get the speaker portrait
|
|
SpriteAsset &sprites = *_vm->_game->_scene._sprites[_speakerSeries[_personSpeaking]];
|
|
MSprite *portrait = sprites.getFrame(_speakerFrame[_personSpeaking]);
|
|
|
|
// Create the new text dialog
|
|
_dialog = new TextDialog(_vm, FONT_INTERFACE,
|
|
Common::Point(_popupX[_personSpeaking], _popupY[_personSpeaking]),
|
|
portrait, _popupMaxLen[_personSpeaking]);
|
|
|
|
// Add in the lines
|
|
for (uint msgNum = 0; msgNum < messageList.size(); ++msgNum) {
|
|
ConvMessage &msg = _runningConv->_data._messages[messageList[msgNum]];
|
|
uint stringIndex = msg._stringIndex;
|
|
|
|
for (uint strNum = 0; strNum < msg._count; ++strNum, ++stringIndex) {
|
|
Common::String textLine = _runningConv->_data._textLines[stringIndex];
|
|
textLine.trim();
|
|
_dialog->addLine(textLine);
|
|
}
|
|
}
|
|
|
|
// Show the dialog
|
|
_popupVisible = true;
|
|
_dialog->show();
|
|
|
|
// Play the speech if one was provided
|
|
if (voiceList.size() > 0) {
|
|
_vm->_audio->setSoundGroup(_runningConv->_data._speechFile);
|
|
_vm->_audio->playSound(voiceList[0] - 1);
|
|
}
|
|
}
|
|
|
|
bool GameConversations::nextNode() {
|
|
ConversationVar &var0 = _runningConv->_cnd._vars[0];
|
|
_runningConv->_cnd._currentNode = var0._val;
|
|
return _runningConv->_data._nodes[var0._val]._active;
|
|
}
|
|
|
|
int GameConversations::executeEntry(int index) {
|
|
ConvDialog &dlg = _runningConv->_data._dialogs[index];
|
|
ConversationVar &var0 = _runningConv->_cnd._vars[0];
|
|
|
|
_runningConv->_cnd._messageList1.clear();
|
|
_runningConv->_cnd._messageList2.clear();
|
|
_runningConv->_cnd._messageList3.clear();
|
|
_runningConv->_cnd._messageList4.clear();
|
|
_nextStartNode->_val = var0._val;
|
|
|
|
bool flag = true;
|
|
for (uint scriptIdx = 0; scriptIdx < dlg._script.size() && flag; ) {
|
|
ScriptEntry &scrEntry = dlg._script[scriptIdx];
|
|
if (scrEntry._command == CMD_END)
|
|
break;
|
|
|
|
switch (scrEntry._command) {
|
|
case CMD_1:
|
|
case CMD_HIDE:
|
|
case CMD_UNHIDE:
|
|
for (uint idx = 0; scrEntry._entries.size(); ++idx)
|
|
flagEntry(scrEntry._command, scrEntry._entries[idx]);
|
|
break;
|
|
|
|
case CMD_MESSAGE1:
|
|
case CMD_MESSAGE2:
|
|
scriptMessage(scrEntry);
|
|
break;
|
|
|
|
case CMD_ERROR:
|
|
error("Conversation script generated error");
|
|
break;
|
|
|
|
case CMD_NODE:
|
|
flag = !scriptNode(scrEntry);
|
|
break;
|
|
|
|
case CMD_GOTO: {
|
|
bool gotoFlag = scrEntry._conditionals[0].evaluate();
|
|
if (gotoFlag) {
|
|
scriptIdx = scrEntry._index;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CMD_ASSIGN: {
|
|
bool setFlag = scrEntry._conditionals[0].evaluate();
|
|
if (setFlag) {
|
|
int *ptr = _runningConv->_cnd._vars[scrEntry._index].getValue();
|
|
*ptr = scrEntry._conditionals[1].evaluate();
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
error("Unknown script opcode");
|
|
}
|
|
|
|
++scriptIdx;
|
|
}
|
|
|
|
if (flag) {
|
|
var0._val = -1;
|
|
}
|
|
|
|
return var0._val;
|
|
}
|
|
|
|
void GameConversations::scriptMessage(ScriptEntry &scrEntry) {
|
|
// Check whether this operation should be done
|
|
bool doFlag = scrEntry._conditionals[0].evaluate();
|
|
if (!doFlag)
|
|
return;
|
|
|
|
// Figure out the entire range that messages can be selected from
|
|
int total = 0;
|
|
for (uint idx = 0; idx < scrEntry._entries2.size(); ++idx)
|
|
total += scrEntry._entries2[idx]._size;
|
|
|
|
// Choose a random entry from the list of possible values
|
|
int randomVal = _vm->getRandomNumber(1, total);
|
|
int randomIndex = -1;
|
|
while (randomVal > 0 && randomIndex < (int)scrEntry._entries2.size()) {
|
|
++randomIndex;
|
|
randomVal -= scrEntry._entries2[randomIndex]._size;
|
|
}
|
|
if (randomIndex == (int)scrEntry._entries2.size())
|
|
randomIndex = 0;
|
|
int entryVal = scrEntry._entries2[randomIndex]._v2;
|
|
|
|
if (scrEntry._command == CMD_MESSAGE1) {
|
|
_runningConv->_cnd._messageList2.push_back(entryVal);
|
|
|
|
if (scrEntry._entries2.size() <= 1) {
|
|
for (uint idx = 0; idx < scrEntry._entries.size(); ++idx)
|
|
_runningConv->_cnd._messageList4.push_back(scrEntry._entries[idx]);
|
|
}
|
|
else if (scrEntry._entries.size() > 0 && randomIndex < (int)scrEntry._entries.size()) {
|
|
_runningConv->_cnd._messageList4.push_back(entryVal);
|
|
}
|
|
} else {
|
|
_runningConv->_cnd._messageList1.push_back(entryVal);
|
|
|
|
if (scrEntry._entries2.size() <= 1) {
|
|
for (uint idx = 0; idx < scrEntry._entries.size(); ++idx)
|
|
_runningConv->_cnd._messageList3.push_back(scrEntry._entries[idx]);
|
|
} else if (scrEntry._entries.size() > 0 && randomIndex < (int)scrEntry._entries.size()) {
|
|
_runningConv->_cnd._messageList3.push_back(entryVal);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool GameConversations::scriptNode(ScriptEntry &scrEntry) {
|
|
bool doFlag = scrEntry._conditionals[0].evaluate();
|
|
if (!doFlag)
|
|
return false;
|
|
|
|
ConversationVar &var0 = _runningConv->_cnd._vars[0];
|
|
int val1 = scrEntry._conditionals[1].evaluate();
|
|
int val2 = scrEntry._conditionals[2].evaluate();
|
|
|
|
var0._val = val1;
|
|
if (val1 >= 0)
|
|
_nextStartNode->_val = val1;
|
|
else if (val2 >= 0)
|
|
_nextStartNode->_val = val2;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
void ConversationData::load(const Common::String &filename) {
|
|
Common::File inFile;
|
|
char buffer[16];
|
|
|
|
inFile.open(filename);
|
|
MadsPack convFileUnpacked(&inFile);
|
|
|
|
// **** Section 0: Header *************************************************
|
|
Common::SeekableReadStream *convFile = convFileUnpacked.getItemStream(0);
|
|
|
|
_nodeCount = convFile->readUint16LE();
|
|
_dialogCount = convFile->readUint16LE();
|
|
_messageCount = convFile->readUint16LE();
|
|
_textLineCount = convFile->readUint16LE();
|
|
_unk2 = convFile->readUint16LE();
|
|
_maxImports = convFile->readUint16LE();
|
|
_speakerCount = convFile->readUint16LE();
|
|
|
|
for (uint idx = 0; idx < MAX_SPEAKERS; ++idx) {
|
|
convFile->read(buffer, 16);
|
|
_portraits[idx] = buffer;
|
|
}
|
|
|
|
for (uint idx = 0; idx < MAX_SPEAKERS; ++idx) {
|
|
_speakerFrame[idx] = convFile->readUint16LE();
|
|
}
|
|
|
|
convFile->read(buffer, 14);
|
|
_speechFile = Common::String(buffer);
|
|
|
|
// Total text length in section 5
|
|
_textSize = convFile->readUint32LE();
|
|
_commandsSize = convFile->readUint32LE();
|
|
|
|
// The rest of the section 0 is padding to allow room for a set of pointers
|
|
// to the contents of the remaining sections loaded into memory as a
|
|
// continuous data block containing both the header and the sections
|
|
delete convFile;
|
|
|
|
// **** Section 1: Nodes **************************************************
|
|
convFile = convFileUnpacked.getItemStream(1);
|
|
|
|
_nodes.clear();
|
|
for (uint i = 0; i < _nodeCount; i++) {
|
|
ConvNode node;
|
|
node._index = convFile->readUint16LE();
|
|
node._dialogCount = convFile->readUint16LE();
|
|
node._unk1 = convFile->readSint16LE(); // TODO
|
|
node._active = convFile->readSint16LE() != 0;
|
|
node._unk3 = convFile->readSint16LE(); // TODO
|
|
_nodes.push_back(node);
|
|
}
|
|
|
|
delete convFile;
|
|
|
|
// **** Section 2: Dialogs ************************************************
|
|
convFile = convFileUnpacked.getItemStream(2);
|
|
assert(convFile->size() == _dialogCount * 8);
|
|
|
|
_dialogs.resize(_dialogCount);
|
|
for (uint idx = 0; idx < _dialogCount; ++idx) {
|
|
_dialogs[idx]._textLineIndex = convFile->readSint16LE();
|
|
_dialogs[idx]._speechIndex = convFile->readSint16LE();
|
|
_dialogs[idx]._scriptOffset = convFile->readUint16LE();
|
|
_dialogs[idx]._scriptSize = convFile->readUint16LE();
|
|
}
|
|
|
|
delete convFile;
|
|
|
|
// **** Section 3: Messages ***********************************************
|
|
convFile = convFileUnpacked.getItemStream(3);
|
|
assert(convFile->size() == _messageCount * 4);
|
|
|
|
_messages.resize(_messageCount);
|
|
for (uint idx = 0; idx < _messageCount; ++idx) {
|
|
_messages[idx]._stringIndex = convFile->readUint16LE();
|
|
_messages[idx]._count = convFile->readUint16LE();
|
|
}
|
|
|
|
delete convFile;
|
|
|
|
// **** Section 4: Text line offsets **************************************
|
|
convFile = convFileUnpacked.getItemStream(4);
|
|
assert(convFile->size() == _textLineCount * 2);
|
|
|
|
uint16 *textLineOffsets = new uint16[_textLineCount]; // deleted below in section 5
|
|
for (uint16 i = 0; i < _textLineCount; i++)
|
|
textLineOffsets[i] = convFile->readUint16LE();
|
|
|
|
delete convFile;
|
|
|
|
// **** Section 5: Text lines *********************************************
|
|
convFile = convFileUnpacked.getItemStream(5);
|
|
assert(convFile->size() == _textSize);
|
|
|
|
Common::String textLine;
|
|
_textLines.resize(_textLineCount);
|
|
char textLineBuffer[256];
|
|
uint16 nextOffset;
|
|
for (uint16 i = 0; i < _textLineCount; i++) {
|
|
nextOffset = (i != _textLineCount - 1) ? textLineOffsets[i + 1] : convFile->size();
|
|
convFile->read(textLineBuffer, nextOffset - textLineOffsets[i]);
|
|
_textLines[i] = Common::String(textLineBuffer);
|
|
}
|
|
|
|
delete[] textLineOffsets;
|
|
delete convFile;
|
|
|
|
// **** Section 6: Scripts ************************************************
|
|
convFile = convFileUnpacked.getItemStream(6);
|
|
assert(convFile->size() == _commandsSize);
|
|
|
|
for (uint idx = 0; idx < _dialogs.size(); ++idx) {
|
|
// Move to the correct position for the dialog's script, and create
|
|
// a memory stream to represent the data for just that script
|
|
convFile->seek(_dialogs[idx]._scriptOffset);
|
|
Common::SeekableReadStream *scriptStream = convFile->readStream(_dialogs[idx]._scriptSize);
|
|
|
|
// Pass it to the dialog's script set class to parse into commands
|
|
_dialogs[idx]._script.load(*scriptStream, _dialogs[idx]._scriptOffset);
|
|
delete scriptStream;
|
|
}
|
|
|
|
delete convFile;
|
|
inFile.close();
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
ConversationConditionals::ConversationConditionals() : _numImports(0) {
|
|
_currentNode = -1;
|
|
}
|
|
|
|
void ConversationConditionals::load(const Common::String &filename) {
|
|
Common::File inFile;
|
|
Common::SeekableReadStream *convFile;
|
|
|
|
// Open up the file for access
|
|
inFile.open(filename);
|
|
MadsPack convFileUnpacked(&inFile);
|
|
|
|
// **** Section 0: Header *************************************************
|
|
convFile = convFileUnpacked.getItemStream(0);
|
|
|
|
_currentNode = convFile->readUint16LE();
|
|
int entryFlagsCount = convFile->readUint16LE();
|
|
int varsCount = convFile->readUint16LE();
|
|
int importsCount = convFile->readUint16LE();
|
|
|
|
convFile->skip(4);
|
|
|
|
_messageList1.resize(convFile->readUint16LE());
|
|
_messageList2.resize(convFile->readUint16LE());
|
|
_messageList3.resize(convFile->readUint16LE());
|
|
_messageList4.resize(convFile->readUint16LE());
|
|
convFile->skip(20);
|
|
|
|
for (uint idx = 0; idx < 10; ++idx) {
|
|
int v = convFile->readUint16LE();
|
|
if (idx < _messageList1.size())
|
|
_messageList1[idx] = v;
|
|
}
|
|
for (uint idx = 0; idx < 10; ++idx) {
|
|
int v = convFile->readUint16LE();
|
|
if (idx < _messageList2.size())
|
|
_messageList2[idx] = v;
|
|
}
|
|
for (uint idx = 0; idx < 10; ++idx) {
|
|
int v = convFile->readUint16LE();
|
|
if (idx < _messageList3.size())
|
|
_messageList3[idx] = v;
|
|
}
|
|
for (uint idx = 0; idx < 10; ++idx) {
|
|
int v = convFile->readUint16LE();
|
|
if (idx < _messageList4.size())
|
|
_messageList4[idx] = v;
|
|
}
|
|
|
|
delete convFile;
|
|
|
|
// **** Section: Imports *************************************************
|
|
int streamNum = 1;
|
|
|
|
_importVariables.resize(importsCount);
|
|
if (importsCount > 0) {
|
|
convFile = convFileUnpacked.getItemStream(streamNum++);
|
|
|
|
// Read in the variable indexes that each import value will be stored in
|
|
for (int idx = 0; idx < importsCount; ++idx)
|
|
_importVariables[idx] = convFile->readUint16LE();
|
|
|
|
delete convFile;
|
|
}
|
|
|
|
// **** Section: Entry Flags *********************************************
|
|
convFile = convFileUnpacked.getItemStream(streamNum++);
|
|
assert(convFile->size() == (entryFlagsCount * 2));
|
|
|
|
_entryFlags.resize(entryFlagsCount);
|
|
for (int idx = 0; idx < entryFlagsCount; ++idx)
|
|
_entryFlags[idx] = convFile->readUint16LE();
|
|
|
|
delete convFile;
|
|
|
|
// **** Section: Variables ***********************************************
|
|
convFile = convFileUnpacked.getItemStream(streamNum);
|
|
assert(convFile->size() == (varsCount * 6));
|
|
|
|
_vars.resize(varsCount);
|
|
for (int idx = 0; idx < varsCount; ++idx) {
|
|
convFile->skip(2); // Loaded values are never pointers, so don't need this
|
|
_vars[idx]._isPtr = false;
|
|
_vars[idx]._val = convFile->readSint16LE();
|
|
convFile->skip(2); // Unused segment selector for pointer values
|
|
}
|
|
|
|
delete convFile;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
void ConversationVar::setValue(int val) {
|
|
_isPtr = false;
|
|
_valPtr = nullptr;
|
|
_val = val;
|
|
}
|
|
|
|
void ConversationVar::setValue(int *val) {
|
|
_isPtr = true;
|
|
_valPtr = val;
|
|
_val = 0;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
void DialogScript::load(Common::SeekableReadStream &s, uint startingOffset) {
|
|
clear();
|
|
Common::HashMap<uint, uint> instructionOffsets;
|
|
|
|
// Iterate getting each instruction in turn
|
|
while (s.pos() < s.size()) {
|
|
// Create a new entry for the next script command
|
|
instructionOffsets[startingOffset + s.pos()] = size();
|
|
push_back(ScriptEntry());
|
|
ScriptEntry &se = (*this)[size() - 1];
|
|
|
|
// Load the instruction
|
|
se.load(s);
|
|
}
|
|
|
|
// Do a final iteration over the loaded instructions to convert
|
|
// any GOTO instructions from original offsets to instruction indexes
|
|
for (uint idx = 0; idx < size(); ++idx) {
|
|
ScriptEntry &se = (*this)[idx];
|
|
|
|
if (se._command == CMD_GOTO)
|
|
se._index = instructionOffsets[se._index];
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
void ScriptEntry::load(Common::SeekableReadStream &s) {
|
|
// Get the command byte
|
|
_command = (DialogCommand)s.readByte();
|
|
|
|
if (!(_command == CMD_DIALOG_END || (_command >= CMD_END && _command <= CMD_ASSIGN))) {
|
|
warning("unknown opcode - %d", _command);
|
|
s.seek(0, SEEK_END);
|
|
return;
|
|
}
|
|
|
|
// Get in the conditional values
|
|
int numConditionals = 1;
|
|
if (_command == CMD_NODE)
|
|
numConditionals = 3;
|
|
else if (_command == CMD_ASSIGN)
|
|
numConditionals = 2;
|
|
else if (_command == CMD_ERROR)
|
|
numConditionals = 0;
|
|
|
|
for (int idx = 0; idx < numConditionals; ++idx)
|
|
_conditionals[idx].load(s);
|
|
|
|
// Get further parameters
|
|
switch (_command) {
|
|
case CMD_1:
|
|
case CMD_HIDE:
|
|
case CMD_UNHIDE: {
|
|
// Read in the list of entries whose flags are to be updated
|
|
int count = s.readByte();
|
|
for (int idx = 0; idx < count; ++idx)
|
|
_entries.push_back(s.readSint16LE());
|
|
break;
|
|
}
|
|
|
|
case CMD_MESSAGE1:
|
|
case CMD_MESSAGE2: {
|
|
int count2 = s.readByte();
|
|
int count1 = s.readByte();
|
|
_entries2.resize(count2);
|
|
_entries.resize(count1);
|
|
|
|
for (uint idx = 0; idx < _entries2.size(); ++idx) {
|
|
int v = s.readByte();
|
|
if (idx < 10)
|
|
_entries2[idx]._size = v;
|
|
}
|
|
for (uint idx = 0; idx < _entries2.size(); ++idx) {
|
|
int v = s.readUint16LE();
|
|
if (idx < 10)
|
|
_entries2[idx]._v2 = v;
|
|
}
|
|
for (uint idx = 0; idx < _entries.size(); ++idx) {
|
|
int v = s.readUint16LE();
|
|
if (idx < 10)
|
|
_entries[idx] = v;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CMD_ERROR:
|
|
case CMD_NODE:
|
|
// These opcodes have no extra parameters
|
|
break;
|
|
|
|
case CMD_GOTO:
|
|
case CMD_ASSIGN:
|
|
// Goto has a single extra parameter for the destination
|
|
// Assign has a single extra parameter for the variable index
|
|
// that the value resulting from the condition will be set to
|
|
_index = s.readUint16LE();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
Common::Array<ConversationVar> *ScriptEntry::Conditional::_vars = nullptr;
|
|
|
|
void ScriptEntry::Conditional::load(Common::SeekableReadStream &s) {
|
|
_operation = (ConditionalOperation)s.readUint16LE();
|
|
|
|
if (_operation == CONDOP_ABORT) {
|
|
_param1._isVariable = false;
|
|
_param1._val = 0;
|
|
} else {
|
|
_param1._isVariable = s.readByte() != 0;
|
|
_param1._val = s.readSint16LE();
|
|
}
|
|
|
|
if (_operation == CONDOP_ABORT || _operation == CONDOP_VALUE) {
|
|
_param2._isVariable = false;
|
|
_param2._val = 0;
|
|
} else {
|
|
_param2._isVariable = s.readByte() != 0;
|
|
_param2._val = s.readSint16LE();
|
|
}
|
|
}
|
|
|
|
int ScriptEntry::Conditional::evaluate() const {
|
|
if (_operation == CONDOP_NONE)
|
|
return -1;
|
|
|
|
int param1 = get(1);
|
|
if (_operation == CONDOP_VALUE)
|
|
return param1;
|
|
int param2 = get(2);
|
|
|
|
switch (_operation) {
|
|
case CONDOP_ADD:
|
|
return param1 + param2;
|
|
case CONDOP_SUBTRACT:
|
|
return param1 - param2;
|
|
case CONDOP_MULTIPLY:
|
|
return param1 * param2;
|
|
case CONDOP_DIVIDE:
|
|
return param1 / param2;
|
|
case CONDOP_MODULUS:
|
|
return param1 % param2;
|
|
case CONDOP_LTEQ:
|
|
return (param1 <= param2) ? 1 : 0;
|
|
case CONDOP_GTEQ:
|
|
return (param1 < param2) ? 1 : 0;
|
|
case CONDOP_LT:
|
|
return (param1 < param2) ? 1 : 0;
|
|
case CONDOP_GT:
|
|
return (param1 > param2) ? 1 : 0;
|
|
case CONDOP_NEQ:
|
|
return (param1 != param2) ? 1 : 0;
|
|
case CONDOP_EQ:
|
|
return (param1 == param2) ? 1 : 0;
|
|
case CONDOP_AND:
|
|
return (param1 || param2) ? 1 : 0;
|
|
case CONDOP_OR:
|
|
return (param1 && param2) ? 1 : 0;
|
|
default:
|
|
error("Unknown conditional operation");
|
|
}
|
|
}
|
|
|
|
int ScriptEntry::Conditional::get(int paramNum) const {
|
|
const CondtionalParamEntry &p = (paramNum == 1) ? _param1 : _param2;
|
|
return p._isVariable ? *(*_vars)[p._val].getValue() : p._val;
|
|
}
|
|
|
|
/*------------------------------------------------------------------------*/
|
|
|
|
|
|
} // End of namespace MADS
|