scummvm/engines/m4/converse.cpp
2010-11-19 17:03:07 +00:00

1240 lines
42 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.
*
* $URL$
* $Id$
*
*/
#include "common/array.h"
#include "common/hashmap.h"
#include "common/substream.h"
#include "m4/converse.h"
#include "m4/resource.h"
#include "m4/globals.h"
#include "m4/m4_views.h"
#include "m4/compression.h"
namespace M4 {
#define CONV_ENTRIES_X_OFFSET 20
#define CONV_ENTRIES_Y_OFFSET 4
#define CONV_ENTRIES_HEIGHT 20
#define CONV_MAX_SHOWN_ENTRIES 5
#define CONVERSATION_ENTRY_HIGHLIGHTED 22
#define CONVERSATION_ENTRY_NORMAL 3
// Conversation chunks
// Header
#define HEAD_CONV MKID_BE('CONV') // conversation
#define CHUNK_DECL MKID_BE('DECL') // declaration
#define CHUNK_NODE MKID_BE('NODE') // node
#define CHUNK_LNOD MKID_BE('LNOD') // linear node
#define CHUNK_ETRY MKID_BE('ETRY') // entry
#define CHUNK_TEXT MKID_BE('TEXT') // text
#define CHUNK_MESG MKID_BE('MESG') // message
// Conversation chunks - entry related (unconditional)
#define CHUNK_RPLY MKID_BE('RPLY') // reply
#define CHUNK_HIDE MKID_BE('HIDE') // hide entry
#define CHUNK_UHID MKID_BE('UHID') // unhide entry
#define CHUNK_DSTR MKID_BE('DSTR') // destroy entry
// Conversation chunks - entry related (conditional)
#define CHUNK_CRPL MKID_BE('CRPL') // reply
#define CHUNK_CHDE MKID_BE('CHDE') // hide entry
#define CHUNK_CUHD MKID_BE('CUHD') // unhide entry
#define CHUNK_CDST MKID_BE('DDTS') // destroy entry
// Conversation chunks - branching and logic (unconditional)
#define CHUNK_ASGN MKID_BE('ASGN') // assign
#define CHUNK_GOTO MKID_BE('GOTO') // goto chunk
#define CHUNK_EXIT MKID_BE('EXIT') // exit/return from goto
// Conversation chunks - branching and logic (conditional)
#define CHUNK_CASN MKID_BE('CASN') // assign
#define CHUNK_CCGO MKID_BE('CCGO') // goto chunk
#define CHUNK_CEGO MKID_BE('CEGO') // exit/return from goto
// Others
#define CHUNK_FALL MKID_BE('FALL') // fallthrough
#define CHUNK_WRPL MKID_BE('WRPL') // weighted reply chunk
#define CHUNK_WPRL MKID_BE('WPRL') // weighted preply chunk
ConversationView::ConversationView(MadsM4Engine *vm): View(vm, Common::Rect(0,
vm->_screen->height() - INTERFACE_HEIGHT, vm->_screen->width(), vm->_screen->height())) {
_screenType = VIEWID_CONVERSATION;
_screenFlags.layer = LAYER_INTERFACE;
_screenFlags.visible = false;
_screenFlags.get = SCREVENT_MOUSE;
_conversationState = kNoConversation;
_currentHandle = NULL;
}
ConversationView::~ConversationView() {
_activeItems.clear();
}
void ConversationView::setNode(int32 nodeIndex) {
_highlightedIndex = -1;
_xEnd = CONV_ENTRIES_X_OFFSET;
_vm->_font->setFont(FONT_CONVERSATION);
// TODO: Conversation styles and colors
_vm->_font->current()->setColours(2, 1, 3);
_currentNodeIndex = nodeIndex;
_activeItems.clear();
if (nodeIndex != -1) {
ConvEntry *node = _m4Vm->_converse->getNode(nodeIndex);
for (uint i = 0; i < node->entries.size(); ++i) {
if (!node->entries[i]->visible)
continue;
if ((int)_activeItems.size() > CONV_MAX_SHOWN_ENTRIES) {
warning("TODO: scrolling. Max shown entries are %i, skipping entry %i",
CONV_MAX_SHOWN_ENTRIES, i);
}
// Add node to active items list
_activeItems.push_back(node->entries[i]);
if (node->entries[i]->autoSelect || strlen(node->entries[i]->text) == 0) {
//warning(kDebugConversations, "Auto selecting entry %i of node %i\n", i, nodeIndex);
selectEntry(i);
return;
}
// Figure out the longest string to determine where option highlighting ends
int tempX = _vm->_font->current()->getWidth(node->entries[i]->text, 0) +
CONV_ENTRIES_X_OFFSET + 10;
_xEnd = MAX(_xEnd, tempX);
}
// Make sure that there aren't too many entries
//assert((int)_activeItems.size() < (height() - CONV_ENTRIES_Y_OFFSET) / CONV_ENTRIES_HEIGHT);
// Fallthrough
if (node->fallthroughMinEntries >= 0 && node->fallthroughOffset >= 0) {
//warning(kDebugConversations, "Current node falls through node at offset %i when entries are less or equal than %i\n",
// node->fallthroughOffset, node->fallthroughMinEntries);
if (_activeItems.size() <= (uint32)node->fallthroughMinEntries) {
const EntryInfo *entryInfo = _m4Vm->_converse->getEntryInfo(node->fallthroughOffset);
//warning(kDebugConversations, "Entries are less than or equal to %i, falling through to node at offset %i, index %i\n",
// node->fallthroughMinEntries, node->fallthroughOffset, entryInfo->nodeIndex);
setNode(entryInfo->nodeIndex);
return;
}
}
_entriesShown = true;
_conversationState = kConversationOptionsShown;
}
}
void ConversationView::onRefresh(RectList *rects, M4Surface *destSurface) {
//if (!this->isVisible())
// return;
clear();
if (_entriesShown) {
// Write out the conversation options
_vm->_font->setFont(FONT_CONVERSATION);
for (int i = 0; i < (int)_activeItems.size(); ++i) {
// TODO: scrolling
if (i > CONV_MAX_SHOWN_ENTRIES - 1)
break;
_vm->_font->current()->setColour((_highlightedIndex == i) ? CONVERSATION_ENTRY_HIGHLIGHTED :
CONVERSATION_ENTRY_NORMAL);
_vm->_font->current()->writeString(this, _activeItems[i]->text, CONV_ENTRIES_X_OFFSET,
CONV_ENTRIES_Y_OFFSET + CONV_ENTRIES_HEIGHT * i, 0, 0);
}
}
View::onRefresh(rects, destSurface);
}
bool ConversationView::onEvent(M4EventType eventType, int32 param, int x, int y, bool &captureEvents) {
//if (!this->isVisible())
// return false;
if (!_entriesShown)
return false;
if (eventType == KEVENT_KEY)
return false;
int localY = y - _coords.top;
int selectedIndex = _highlightedIndex;
switch (eventType) {
case MEVENT_MOVE:
if ((x < CONV_ENTRIES_X_OFFSET) || (x >= _xEnd) || (localY < CONV_ENTRIES_Y_OFFSET))
_highlightedIndex = -1;
else {
int index = (localY - CONV_ENTRIES_Y_OFFSET) / CONV_ENTRIES_HEIGHT;
_highlightedIndex = (index >= (int)_activeItems.size()) ? -1 : index;
}
break;
case MEVENT_LEFT_RELEASE:
if (_highlightedIndex != -1) {
selectEntry(selectedIndex);
}
break;
default:
break;
}
return true;
}
void ConversationView::selectEntry(int entryIndex) {
char buffer[20];
sprintf(buffer, "%s.raw", _activeItems[entryIndex]->voiceFile);
_entriesShown = false;
_conversationState = kEntryIsActive;
_vm->_player->setCommandsAllowed(false);
// Necessary, as entries can be selected programmatically
_highlightedIndex = entryIndex;
// Play the selected entry's voice
if (strlen(_activeItems[entryIndex]->voiceFile) > 0) {
_currentHandle = _vm->_sound->getHandle();
_vm->_sound->playVoice(buffer, 255);
} else {
_currentHandle = NULL;
}
// Hide selected entry, unless it has a persistent flag set
if (!(_activeItems[entryIndex]->flags & kEntryPersists)) {
//debugCN(kDebugConversations, "Hiding selected entry\n");
_m4Vm->_converse->getNode(_currentNodeIndex)->entries[entryIndex]->visible = false;
} else {
//debugCN(kDebugConversations, "Selected entry is persistent, not hiding it\n");
}
}
void ConversationView::updateState() {
switch (_conversationState) {
case kConversationOptionsShown:
return;
case kEntryIsActive:
case kReplyIsActive:
// FIXME: for now, we determine whether a conversation entry is
// finished by waiting for its associated speech file to finish playing
if (_currentHandle != NULL && _vm->_sound->isHandleActive(_currentHandle)) {
return;
} else {
playNextReply();
} // end else
break;
case kNoConversation:
return;
default:
error("Unknown converstation state");
break;
}
}
void ConversationView::playNextReply() {
char buffer[20];
assert(_highlightedIndex >= 0);
// Start playing the first reply
for (uint32 i = 0; i < _activeItems[_highlightedIndex]->entries.size(); i++) {
ConvEntry *currentEntry = _activeItems[_highlightedIndex]->entries[i];
if (currentEntry->isConditional) {
if (!_m4Vm->_converse->evaluateCondition(
_m4Vm->_converse->getValue(currentEntry->condition.offset),
currentEntry->condition.op, currentEntry->condition.val))
continue; // don't play this reply
}
if (currentEntry->entryType != kWeightedReply) {
sprintf(buffer, "%s.raw", currentEntry->voiceFile);
if (strlen(currentEntry->voiceFile) > 0) {
_currentHandle = _vm->_sound->getHandle();
_vm->_sound->playVoice(buffer, 255);
// Remove reply from the list of replies
_activeItems[_highlightedIndex]->entries.remove_at(i);
_conversationState = kReplyIsActive;
return;
} else {
_currentHandle = NULL;
}
} else {
int selectedWeight = _vm->_random->getRandomNumber(currentEntry->totalWeight - 1) + 1;
//debugCN(kDebugConversations, "Selected weight: %i\n", selectedWeight);
int previousWeight = 1;
int currentWeight = 0;
for (uint32 j = 0; j < currentEntry->entries.size(); j++) {
currentWeight += currentEntry->entries[j]->weight;
if (selectedWeight >= previousWeight && selectedWeight <= currentWeight) {
sprintf(buffer, "%s.raw", currentEntry->entries[j]->voiceFile);
if (strlen(currentEntry->entries[j]->voiceFile) > 0) {
_currentHandle = _vm->_sound->getHandle();
_vm->_sound->playVoice(buffer, 255);
// Remove reply from the list of replies
_activeItems[_highlightedIndex]->entries.remove_at(i);
_conversationState = kReplyIsActive;
return;
} else {
_currentHandle = NULL;
}
break;
}
previousWeight += currentWeight;
} // end for j
} // end if
} // end for i
// If we reached here, there are no more replies, so perform the active entry's actions
//debugCN(kDebugConversations, "Current selection does %i actions\n", _activeItems[entryIndex]->actions.size());
for (uint32 i = 0; i < _activeItems[_highlightedIndex]->actions.size(); i++) {
if (!_m4Vm->_converse->performAction(_activeItems[_highlightedIndex]->actions[i]))
break;
} // end for
// Refresh the conversation node, in case it hasn't changed
setNode(_currentNodeIndex);
_entriesShown = true;
_vm->_player->setCommandsAllowed(true);
// Check if the conversation has been ended
if (_currentNodeIndex == -1) {
_conversationState = kNoConversation;
}
}
//--------------------------------------------------------------------------
void Converse::startConversation(const char *convName, bool showConverseBox, ConverseStyle style) {
if (_vm->isM4())
loadConversation(convName);
else
loadConversationMads(convName);
if (!_vm->isM4()) showConverseBox = false; // TODO: remove
_playerCommandsAllowed = _vm->_player->commandsAllowed;
if (_vm->isM4()) // TODO: remove (interface not implemented yet in MADS games)
_interfaceWasVisible = _m4Vm->scene()->getInterface()->isVisible();
_vm->_player->setCommandsAllowed(false);
_style = style;
if (showConverseBox) {
_vm->_conversationView->show();
_vm->_mouse->lockCursor(CURSOR_ARROW);
if (_interfaceWasVisible)
_m4Vm->scene()->getInterface()->hide();
_vm->_conversationView->setNode(0);
_vm->_conversationView->show();
}
}
void Converse::endConversation() {
_vm->_conversationView->setNode(-1);
_vm->_conversationView->hide();
// TODO: do a more proper cleanup here
_convNodes.clear();
_variables.clear();
_offsetMap.clear();
_vm->_player->setCommandsAllowed(_playerCommandsAllowed);
if (_interfaceWasVisible)
_m4Vm->scene()->getInterface()->show();
}
void Converse::loadConversation(const char *convName) {
char name[40];
char buffer[256];
sprintf(name, "%s.chk", convName);
Common::SeekableReadStream *convS = _vm->res()->get(name);
uint32 header = convS->readUint32LE();
uint32 size;
uint32 chunk;
uint32 data = 0;
uint32 i = 0;
ConvEntry* curEntry = NULL;
ConvEntry* replyEntry = NULL;
int32 currentWeightedEntry = -1;
EntryAction* curAction = NULL;
uint32 curNode = 0;
uint32 chunkPos = 0;
uint32 val;
int32 autoSelectIndex = -1;
int returnAddress = -1;
bool debugFlag = false; // set to true for debug messages
// Conversation *.chk files contain a 'CONV' header in LE format
if (header != HEAD_CONV) {
warning("Unexpected conversation file external header");
return;
}
size = convS->readUint32LE(); // is this used at all?
if (debugFlag) debugCN(kDebugConversations, "Conv chunk size (external header): %i\n", size);
// Conversation name, which is the conversation file's name
// without the extension
convS->read(buffer, 8);
if (debugFlag) debugCN(kDebugConversations, "Conversation name: %s\n", buffer);
while (true) {
chunkPos = convS->pos();
chunk = convS->readUint32LE(); // read chunk
if (convS->eos()) break;
if (debugFlag) debugC(kDebugConversations, "***** Pos: %i -> ", chunkPos);
switch (chunk) {
case CHUNK_DECL: // Declare
if (debugFlag) debugCN(kDebugConversations, "DECL chunk\n");
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Tag: %i\n", data);
if (data > 0)
warning("Tag > 0 in DECL chunk, value is: %i", data); // TODO
val = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Value: %i\n", val);
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Flags: %i\n", data);
if (data > 0)
warning("Flags != 0 in DECL chunk, value is %i", data); // TODO
setValue(chunkPos, val);
break;
// ----------------------------------------------------------------------------
case CHUNK_NODE: // Node
case CHUNK_LNOD: // Linear node
// Create new node
curEntry = new ConvEntry();
curEntry->offset = chunkPos;
curEntry->entryType = (chunk == CHUNK_NODE) ? kNode : kLinearNode;
curEntry->fallthroughMinEntries = -1;
curEntry->fallthroughOffset = -1;
if (chunk == CHUNK_NODE) {
if (debugFlag) debugCN(kDebugConversations, "NODE chunk\n");
} else {
if (debugFlag) debugCN(kDebugConversations, "LNOD chunk\n");
}
curNode = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Node number: %i\n", curNode);
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Tag: %i\n", data);
if (chunk == CHUNK_LNOD) {
autoSelectIndex = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Autoselect entry number: %i\n", autoSelectIndex);
}
size = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Number of entries: %i\n", size);
for (i = 0; i < size; i++) {
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Entry %i: %i\n", i + 1, data);
}
_convNodes.push_back(curEntry);
setEntryInfo(curEntry->offset, curEntry->entryType, curNode, -1);
break;
case CHUNK_ETRY: // Entry
// Create new entry
curEntry = new ConvEntry();
curEntry->offset = chunkPos;
curEntry->entryType = kEntry;
strcpy(curEntry->voiceFile, "");
strcpy(curEntry->text, "");
if (debugFlag) debugCN(kDebugConversations, "ETRY chunk\n");
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Unknown (attributes perhaps?): %i\n", data);
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Entry flags: ");
if (debugFlag) if (data & kEntryInitial) debugCN(kDebugConversations, "Initial ");
if (debugFlag) if (data & kEntryPersists) debugCN(kDebugConversations, "Persists ");
if (debugFlag) debugCN(kDebugConversations, "\n");
curEntry->flags = data;
curEntry->visible = (curEntry->flags & kEntryInitial) ? true : false;
if (autoSelectIndex >= 0) {
if (_convNodes[curNode]->entries.size() == (uint32)autoSelectIndex) {
curEntry->autoSelect = true;
autoSelectIndex = -1;
} else {
curEntry->autoSelect = false;
}
} else {
curEntry->autoSelect = false;
}
_convNodes[curNode]->entries.push_back(curEntry);
setEntryInfo(curEntry->offset, curEntry->entryType,
curNode, _convNodes[curNode]->entries.size() - 1);
replyEntry = NULL;
break;
case CHUNK_WPRL: // Weighted preply
// WPRL chunks are random entries that the character would say, not an NPC
// They don't seem to be used in Orion Burger
warning("WPRL chunk - treating as WRPL chunk");
case CHUNK_WRPL: // Weighted reply
case CHUNK_CRPL: // Conditional reply
case CHUNK_RPLY: // Reply
{
ConvEntry* weightedEntry = NULL;
// Create new reply
replyEntry = new ConvEntry();
replyEntry->offset = chunkPos;
strcpy(replyEntry->voiceFile, "");
// Conditional part
if (chunk == CHUNK_CRPL) {
replyEntry->isConditional = true;
replyEntry->condition.offset = convS->readUint32LE();
replyEntry->condition.op = convS->readUint32LE();
replyEntry->condition.val = convS->readUint32LE();
} else {
replyEntry->isConditional = false;
}
if (chunk == CHUNK_WPRL || chunk == CHUNK_WRPL) {
replyEntry->entryType = kWeightedReply;
replyEntry->totalWeight = 0;
if (debugFlag) debugCN(kDebugConversations, "WRPL chunk\n");
size = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Weighted reply %i - %i entries:\n", i, size);
for (i = 0; i < size; i++) {
weightedEntry = new ConvEntry();
weightedEntry->offset = chunkPos;
strcpy(weightedEntry->voiceFile, "");
weightedEntry->entryType = kWeightedReply;
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "- Weight: %i ", data);
weightedEntry->weight = data;
replyEntry->totalWeight += weightedEntry->weight;
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "offset: %i\n", data);
replyEntry->entries.push_back(weightedEntry);
}
currentWeightedEntry = 0;
} else {
replyEntry->entryType = kReply;
if (debugFlag) debugCN(kDebugConversations, "RPLY chunk\n");
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Reply data offset: %i\n", data);
}
if (!curEntry)
error("Converse::loadConversation(): curEntry is NULL while adding a reply");
curEntry->entries.push_back(replyEntry);
setEntryInfo(replyEntry->offset, replyEntry->entryType,
curNode, _convNodes[curNode]->entries.size() - 1);
// Seek to chunk data (i.e. TEXT/MESG tag, which is usually right
// after this chunk but it can be further on in conditional reply chunks
assert((int)data >= convS->pos());
// If the entry's data is not right after the entry, remember the position
// to return to after the data is read
if (chunk == CHUNK_CRPL && (int)data != convS->pos())
returnAddress = convS->pos();
convS->seek(data, SEEK_SET);
}
break;
// ----------------------------------------------------------------------------
case CHUNK_TEXT: // Text (contains text and voice)
case CHUNK_MESG: // Message (contains voice only)
{
ConvEntry* parentEntry = NULL;
if (chunk == CHUNK_TEXT) {
if (debugFlag) debugCN(kDebugConversations, "TEXT chunk\n");
} else {
if (debugFlag) debugCN(kDebugConversations, "MESG chunk\n");
}
if (replyEntry == NULL) {
parentEntry = curEntry;
} else if (replyEntry != NULL && replyEntry->entryType != kWeightedReply) {
parentEntry = replyEntry;
} else if (replyEntry != NULL && replyEntry->entryType == kWeightedReply) {
parentEntry = replyEntry->entries[currentWeightedEntry];
currentWeightedEntry++;
} else {
error("Converse::loadConversation(): Unexpected reply entry while processing TEXT/MESG chunk");
}
size = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Entry data size: %i\n", size);
convS->read(buffer, 8);
size -= 8; // subtract maximum length of voice file name
// If the file name is 8 characters, it will not be 0-terminated, so use strncpy
strncpy(parentEntry->voiceFile, buffer, 8);
parentEntry->voiceFile[8] = '\0';
if (debugFlag) debugCN(kDebugConversations, "Voice file: %s\n", parentEntry->voiceFile);
if (chunk == CHUNK_TEXT) {
convS->read(buffer, size);
if (debugFlag) debugCN(kDebugConversations, "Text: %s\n", buffer);
sprintf(parentEntry->text, "%s", buffer);
} else {
while (size > 0) {
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Unknown: %i\n", data); // TODO
size -= 4;
}
}
// Now that the data chunk has been read, if we skipped a reply node,
// jump back to it
if (returnAddress != -1) {
convS->seek(returnAddress, SEEK_SET);
returnAddress = -1;
}
}
break;
// ----------------------------------------------------------------------------
// Entry action chunks
case CHUNK_CASN: // Conditional assign
case CHUNK_ASGN: // Assign
curAction = new EntryAction();
if (debugFlag) debugCN(kDebugConversations, "ASGN chunk\n");
curAction->actionType = kAssignValue;
// Conditional part
if (chunk == CHUNK_CASN) {
curAction->isConditional = true;
curAction->condition.offset = convS->readUint32LE();
curAction->condition.op = convS->readUint32LE();
curAction->condition.val = convS->readUint32LE();
} else {
curAction->isConditional = false;
}
curAction->targetOffset = convS->readUint32LE();
assert(convS->readUint32LE() == kOpAssign);
curAction->value = convS->readUint32LE();
break;
case CHUNK_CCGO: // Conditional go to entry
case CHUNK_CHDE: // Conditional hide entry
case CHUNK_CUHD: // Conditional unhide entry
case CHUNK_CDST: // Conditional destroy entry
case CHUNK_CEGO: // Conditional exit conversation / go to
case CHUNK_GOTO: // Go to entry
case CHUNK_HIDE: // Hide entry
case CHUNK_UHID: // Unhide entry
case CHUNK_DSTR: // Destroy entry
case CHUNK_EXIT: // Exit conversation
curAction = new EntryAction();
// Conditional part
if (chunk == CHUNK_CCGO || chunk == CHUNK_CHDE || chunk == CHUNK_CUHD ||
chunk == CHUNK_CDST || chunk == CHUNK_CEGO) {
curAction->isConditional = true;
curAction->condition.offset = convS->readUint32LE();
curAction->condition.op = convS->readUint32LE();
curAction->condition.val = convS->readUint32LE();
} else {
curAction->isConditional = false;
}
if (chunk == CHUNK_GOTO || chunk == CHUNK_CCGO) {
curAction->actionType = kGotoEntry;
if (debugFlag) debugCN(kDebugConversations, "GOTO chunk\n");
} else if (chunk == CHUNK_HIDE || chunk == CHUNK_CHDE) {
curAction->actionType = kHideEntry;
if (debugFlag) debugCN(kDebugConversations, "HIDE chunk\n");
} else if (chunk == CHUNK_UHID || chunk == CHUNK_CUHD) {
curAction->actionType = kUnhideEntry;
if (debugFlag) debugCN(kDebugConversations, "UHID chunk\n");
} else if (chunk == CHUNK_DSTR || chunk == CHUNK_CDST) {
curAction->actionType = kDestroyEntry;
if (debugFlag) debugCN(kDebugConversations, "DSTR chunk\n");
} else if (chunk == CHUNK_EXIT || chunk == CHUNK_CEGO) {
curAction->actionType = kExitConv;
if (debugFlag) debugCN(kDebugConversations, "EXIT chunk\n");
}
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Offset: %i\n", data);
curAction->targetOffset = data;
curEntry->actions.push_back(curAction);
break;
case CHUNK_FALL: // Fallthrough
if (debugFlag) debugCN(kDebugConversations, "FALL chunk\n");
size = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Minimum nodes: %i\n", size);
_convNodes[curNode]->fallthroughMinEntries = size;
data = convS->readUint32LE();
if (debugFlag) debugCN(kDebugConversations, "Offset: %i\n", data);
_convNodes[curNode]->fallthroughOffset = data;
break;
// ----------------------------------------------------------------------------
default:
// Should never happen
error("Unknown conversation chunk");
break;
}
}
_vm->res()->toss(name);
}
void Converse::loadConversationMads(const char *convName) {
char name[40];
char buffer[256];
char *buf;
Common::SeekableReadStream *convS;
int curPos = 0;
int unk = 0;
uint32 stringIndex = 0;
uint32 stringCount = 0;
int flags = 0;
int count = 0;
uint32 i, j;
ConvEntry* curEntry = NULL;
MessageEntry *curMessage;
Common::Array<char *> messageList;
// TODO
// CND file
sprintf(name, "%s.cnd", convName);
MadsPack convDataD(name, _vm);
// ------------------------------------------------------------
// Chunk 0
convS = convDataD.getItemStream(0);
debugCN(kDebugConversations, "Chunk 0\n");
debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
while (!convS->eos()) { // FIXME (eos changed)
debugCN(kDebugConversations, "%i ", convS->readByte());
}
debugCN(kDebugConversations, "\n");
// ------------------------------------------------------------
// Chunk 1
convS = convDataD.getItemStream(1);
debugCN(kDebugConversations, "Chunk 1\n");
debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
while (!convS->eos()) { // FIXME (eos changed)
debugCN(kDebugConversations, "%i ", convS->readByte());
}
debugCN(kDebugConversations, "\n");
// ------------------------------------------------------------
// Chunk 2
convS = convDataD.getItemStream(2);
debugCN(kDebugConversations, "Chunk 2\n");
debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
while (!convS->eos()) { // FIXME (eos changed)
debugCN(kDebugConversations, "%i ", convS->readByte());
}
debugCN(kDebugConversations, "\n");
// ------------------------------------------------------------
// CNV file
sprintf(name, "%s.cnv", convName);
MadsPack convData(name, _vm);
// *.cnv files have 7 chunks
// Here is the chunk output of conv001.cnv (from the dump_file command)
/*
Dumping conv001.cnv, size: 3431
Dumping compressed chunk 1 of 7, size is 150
Dumping compressed chunk 2 of 7, size is 130
Dumping compressed chunk 3 of 7, size is 224
Dumping compressed chunk 4 of 7, size is 92
Dumping compressed chunk 5 of 7, size is 168
Dumping compressed chunk 6 of 7, size is 4064
Dumping compressed chunk 7 of 7, size is 2334
*/
// ------------------------------------------------------------
// TODO: finish this
// Chunk 0
convS = convData.getItemStream(0);
debugCN(kDebugConversations, "Chunk 0\n");
debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
debugCN(kDebugConversations, "%i ", convS->readUint16LE());
debugCN(kDebugConversations, "%i ", convS->readUint16LE());
debugCN(kDebugConversations, "%i ", convS->readUint16LE());
debugCN(kDebugConversations, "%i ", convS->readUint16LE());
debugCN(kDebugConversations, "%i ", convS->readUint16LE());
debugCN(kDebugConversations, "%i ", convS->readUint16LE());
debugCN(kDebugConversations, "\n");
count = convS->readUint16LE(); // conversation face count (usually 2)
debugCN(kDebugConversations, "Conversation faces: %i\n", count);
for (i = 0; i < 5; i++) {
convS->read(buffer, 16);
debugCN(kDebugConversations, "Face %i: %s ", i + 1, buffer);
}
debugCN(kDebugConversations, "\n");
// 5 face slots
// 1 = face slot has a face (with the filename specified above)
// 0 = face slot doesn't contain face data
for (i = 0; i < 5; i++) {
debugCN(kDebugConversations, "%i ", convS->readUint16LE());
}
debugCN(kDebugConversations, "\n");
convS->read(buffer, 14); // speech file
debugCN(kDebugConversations, "Speech file: %s\n", buffer);
while (!convS->eos()) { // FIXME: eos changed
debugCN(kDebugConversations, "%i ", convS->readByte());
}
debugCN(kDebugConversations, "\n");
delete convS;
// ------------------------------------------------------------
// Chunk 1: Conversation nodes
convS = convData.getItemStream(1);
debugCN(kDebugConversations, "Chunk 1: conversation nodes\n");
debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
while (true) {
uint16 id = convS->readUint16LE();
if (convS->eos()) break;
curEntry = new ConvEntry();
curEntry->id = id;
curEntry->entryCount = convS->readUint16LE();
curEntry->flags = convS->readUint16LE();
if (curEntry->entryCount == 1 && curEntry->flags != 65535) {
warning("Entry count is 1 and flags is not 65535 (it's %i)", flags);
} else if (curEntry->entryCount != 1 && flags != 0) {
warning("Entry count > 1 and flags is not 0 (it's %i)", flags);
}
unk = convS->readUint16LE();
assert (unk == 65535);
unk = convS->readUint16LE();
assert (unk == 65535);
_convNodes.push_back(curEntry);
debugCN(kDebugConversations, "Node %i, ID %i, entries %i\n", _convNodes.size(), curEntry->id, curEntry->entryCount);
// flags = 0: node has more than 1 entry
// flags = 65535: node has 1 entry
}
debugCN(kDebugConversations, "Conversation has %i nodes\n", _convNodes.size());
debugCN(kDebugConversations, "\n");
delete convS;
// ------------------------------------------------------------
// Chunk 4 contains the conversation string offsets of chunk 5
// (unused, as it's unneeded - we find the offsets ourselves)
// ------------------------------------------------------------
// Chunk 5 contains the conversation strings
convS = convData.getItemStream(5);
//debugCN(kDebugConversations, "Chunk 5: conversation strings\n");
//debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
*buffer = 0;
while (true) {
//if (curPos == 0)
// debugCN(kDebugConversations, "%i: Offset %i: ", _convStrings.size(), convS->pos());
uint8 b = convS->readByte();
if (convS->eos()) break;
buffer[curPos++] = b;
if (buffer[curPos - 1] == '~') { // filter out special characters
curPos--;
continue;
}
if (buffer[curPos - 1] == '\0') {
// end of string
//debugCN(kDebugConversations, "%s\n", buffer);
buf = new char[strlen(buffer) + 1];
strcpy(buf, buffer);
_convStrings.push_back(buf);
curPos = 0;
*buffer = 0;
}
}
delete convS;
// ------------------------------------------------------------
// Chunk 2: entry data
convS = convData.getItemStream(2);
//debugCN(kDebugConversations, "Chunk 2 - entry data\n");
//debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
for (i = 0; i < _convNodes.size(); i++) {
for (j = 0; j < _convNodes[i]->entryCount; j++) {
curEntry = new ConvEntry();
stringIndex = convS->readUint16LE();
if (stringIndex != 65535)
sprintf(curEntry->text, "%s", _convStrings[stringIndex]);
else
*curEntry->text = 0;
curEntry->id = convS->readUint16LE();
curEntry->offset = convS->readUint16LE();
curEntry->size = convS->readUint16LE();
_convNodes[i]->entries.push_back(curEntry);
//debugCN(kDebugConversations, "Node %i, entry %i, id %i, offset %i, size %i, text: %s\n",
// i, j, curEntry->id, curEntry->offset, curEntry->size, curEntry->text);
}
}
delete convS;
// ------------------------------------------------------------
// Chunk 3: message (MESG) chunks, created from the strings of chunk 5
convS = convData.getItemStream(3);
//debugCN(kDebugConversations, "Chunk 3 - MESG chunk data\n");
//debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
while (true) {
uint16 index = convS->readUint16LE();
if (convS->eos()) break;
curMessage = new MessageEntry();
stringIndex = index;
stringCount = convS->readUint16LE();
*buffer = 0;
//debugCN(kDebugConversations, "Message: %i\n", _madsMessageList.size());
for (i = stringIndex; i < stringIndex + stringCount; i++) {
//debugCN(kDebugConversations, "%i: %s\n", i, _convStrings[i]);
curMessage->messageStrings.push_back(_convStrings[i]);
}
_madsMessageList.push_back(curMessage);
//debugCN(kDebugConversations, "----------\n");
}
//debugCN(kDebugConversations, "\n");
delete convS;
// ------------------------------------------------------------
// TODO: finish this
// Chunk 6: conversation script
convS = convData.getItemStream(6);
debugCN(kDebugConversations, "Chunk 6\n");
debugCN(kDebugConversations, "Conv stream size: %i\n", convS->size());
/*while (!convS->eos()) { // FIXME (eos changed)
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "%i ", convS->readByte());
debugCN(kDebugConversations, "\n");
}
return;*/
for (i = 0; i < _convNodes.size(); i++) {
for (j = 0; j < _convNodes[i]->entryCount; j++) {
debugCN(kDebugConversations, "*** Node %i entry %i data size %i\n", i, j, _convNodes[i]->entries[j]->size);
debugCN(kDebugConversations, "Entry ID %i, text %s\n", _convNodes[i]->entries[j]->id, _convNodes[i]->entries[j]->text);
Common::ReadStream *entryStream = new Common::SubReadStream(convS, _convNodes[i]->entries[j]->size);
readConvEntryActions(entryStream, _convNodes[i]->entries[j]);
delete entryStream;
debugCN(kDebugConversations, "--------------------\n");
}
}
delete convS;
}
void Converse::readConvEntryActions(Common::ReadStream *convS, ConvEntry *curEntry) {
uint8 chunk;
uint8 type; // 255: normal, 11: conditional
uint8 hasText1, hasText2;
int target;
int count = 0;
int var, val;
int messageIndex = 0;
int unk = 0;
while (true) {
chunk = convS->readByte();
if (convS->eos()) break;
type = convS->readByte();
switch (chunk) {
case 1:
debugCN(kDebugConversations, "TODO: chunk type %i\n", chunk);
break;
case 2:
debugCN(kDebugConversations, "HIDE\n");
convS->readByte();
count = convS->readByte();
debugCN(kDebugConversations, "%i entries: ", count);
for (int i = 0; i < count; i++)
debugCN(kDebugConversations, "%i %d", i, convS->readUint16LE());
debugCN(kDebugConversations, "\n");
break;
case 3:
debugCN(kDebugConversations, "UNHIDE\n");
convS->readByte();
count = convS->readByte();
debugCN(kDebugConversations, "%i entries: ", count);
for (int i = 0; i < count; i++)
debugCN(kDebugConversations, "%i %d", i, convS->readUint16LE());
debugCN(kDebugConversations, "\n");
break;
case 4: // MESSAGE
debugCN(kDebugConversations, "MESSAGE\n");
if (type == 255) {
//debugCN(kDebugConversations, "unconditional\n");
} else if (type == 11) {
//debugCN(kDebugConversations, "conditional\n");
} else {
debugCN(kDebugConversations, "unknown type: %i\n", type);
}
// Conditional part
if (type == 11) {
unk = convS->readUint16LE(); // 1
if (unk != 1)
debugCN(kDebugConversations, "Message: unk != 1 (it's %i)\n", unk);
var = convS->readUint16LE();
val = convS->readUint16LE();
debugCN(kDebugConversations, "Var %i == %i\n", var, val);
}
unk = convS->readUint16LE(); // 256
if (unk != 256)
debugCN(kDebugConversations, "Message: unk != 256 (it's %i)\n", unk);
// it seems that the first text entry is set when the message
// chunk is supposed to be shown unconditionally, whereas the second text
// entry is set when the message is supposed to be shown conditionally
hasText1 = convS->readByte();
hasText2 = convS->readByte();
if (hasText1 == 1) {
messageIndex = convS->readUint16LE();
debugCN(kDebugConversations, "Message 1 index: %i, text:\n", messageIndex);
for (uint32 i = 0; i < _madsMessageList[messageIndex]->messageStrings.size(); i++) {
debugCN(kDebugConversations, "%s\n", _madsMessageList[messageIndex]->messageStrings[i]);
}
}
if (hasText2 == 1) {
messageIndex = convS->readUint16LE();
if (hasText1 == 0) {
debugCN(kDebugConversations, "Message 2 index: %i, text:\n", messageIndex);
for (uint32 i = 0; i < _madsMessageList[messageIndex]->messageStrings.size(); i++) {
debugCN(kDebugConversations, "%s\n", _madsMessageList[messageIndex]->messageStrings[i]);
}
}
}
break;
case 5: // AUTO
debugCN(kDebugConversations, "AUTO\n");
for (int k = 0; k < 4; k++)
convS->readByte();
messageIndex = convS->readUint16LE();
debugCN(kDebugConversations, "Message index: %i, text:\n", messageIndex);
for (uint32 i = 0; i < _madsMessageList[messageIndex]->messageStrings.size(); i++) {
debugCN(kDebugConversations, "%s\n", _madsMessageList[messageIndex]->messageStrings[i]);
}
convS->readUint16LE();
break;
case 6:
debugCN(kDebugConversations, "TODO: chunk type %i\n", chunk);
break;
case 7: // GOTO
unk = convS->readUint32LE(); // 0
if (unk != 0 && unk != 1)
debugCN(kDebugConversations, "Goto: unk != 0 or 1 (it's %i)\n", unk);
target = convS->readUint16LE();
convS->readUint16LE(); // 255
if (unk != 0)
debugCN(kDebugConversations, "Goto: unk != 0 (it's %i)\n", unk);
if (target != 65535)
debugCN(kDebugConversations, "GOTO node %i\n", target);
else
debugCN(kDebugConversations, "GOTO exit\n");
break;
case 8:
debugCN(kDebugConversations, "TODO: chunk type %i\n", chunk);
break;
case 9: // ASSIGN
//debugCN(kDebugConversations, "ASSIGN\n");
unk = convS->readUint32LE(); // 0
if (unk != 0)
debugCN(kDebugConversations, "Assign: unk != 0 (it's %i)\n", unk);
val = convS->readUint16LE();
var = convS->readUint16LE();
//debugCN(kDebugConversations, "Var %i = %i\n", var, val);
break;
default:
debugCN(kDebugConversations, "Unknown chunk type! (%i)\n", chunk);
break;
}
}
debugCN(kDebugConversations, "\n");
}
void Converse::setEntryInfo(int32 offset, EntryType type, int32 nodeIndex, int32 entryIndex) {
char hashOffset[10];
sprintf(hashOffset, "%i", offset);
EntryInfo info;
info.targetType = type;
info.nodeIndex = nodeIndex;
info.entryIndex = entryIndex;
_offsetMap[hashOffset] = info;
//debugCN(kDebugConversations, "Set entry info: offset %i, type %i, node %i, entry %i\n", offset, type, nodeIndex, entryIndex);
}
const EntryInfo* Converse::getEntryInfo(int32 offset) {
char hashOffset[10];
sprintf(hashOffset, "%i", offset);
OffsetHashMap::const_iterator entry = _offsetMap.find(hashOffset);
if (entry != _offsetMap.end())
return &(entry->_value);
else
error("Undeclared entry offset: %i", offset);
}
void Converse::setValue(int32 offset, int32 value) {
char hashOffset[10];
sprintf(hashOffset, "%i", offset);
_variables[hashOffset] = value;
}
int32 Converse::getValue(int32 offset) {
char hashOffset[10];
sprintf(hashOffset, "%i", offset);
ConvVarHashMap::const_iterator entry = _variables.find(hashOffset);
if (entry != _variables.end())
return entry->_value;
else
error("Undeclared variable offset: %i", offset);
}
bool Converse::evaluateCondition(int32 leftVal, int32 op, int32 rightVal) {
switch (op) {
case kOpPercent:
return (leftVal % rightVal == 0);
case kOpGreaterOrEqual:
return leftVal >= rightVal;
case kOpLessOrEqual:
return leftVal <= rightVal;
case kOpGreaterThan:
return leftVal > rightVal;
case kOpLessThan:
return leftVal < rightVal;
case kOpNotEqual:
case kOpCondNotEqual:
return leftVal != rightVal;
case kOpAssign:
return leftVal == rightVal;
case kOpAnd:
return leftVal && rightVal;
case kOpOr:
return leftVal || rightVal;
default:
error("Unknown conditional operator: %i", op);
}
}
bool Converse::performAction(EntryAction *action) {
if (action->isConditional) {
if (!evaluateCondition(getValue(action->condition.offset),
action->condition.op, action->condition.val))
return true; // don't perform this action
}
if (action->actionType == kAssignValue) {
//debugCN(kDebugConversations, "Assigning variable at offset %i to value %i\n",
// action->targetOffset, action->value);
setValue(action->targetOffset, action->value);
return true; // nothing else to do in an assignment action
}
const EntryInfo *entryInfo = getEntryInfo(action->targetOffset);
ConvEntry *targetEntry;
if (entryInfo->nodeIndex >= 0 && entryInfo->entryIndex >= 0)
targetEntry = getNode(entryInfo->nodeIndex)->entries[entryInfo->entryIndex];
else if (entryInfo->nodeIndex >= 0)
targetEntry = getNode(entryInfo->nodeIndex);
else
error("Target node id is negative");
switch (action->actionType) {
case kGotoEntry:
//debugCN(kDebugConversations, "Goto entry at offset %i. Associated node is %i, entry %i\n",
// action->targetOffset, entryInfo->nodeIndex, entryInfo->entryIndex);
_vm->_conversationView->setNode(entryInfo->nodeIndex);
if (entryInfo->entryIndex >= 0)
_vm->_conversationView->selectEntry(entryInfo->entryIndex);
return false;
case kHideEntry:
//debugCN(kDebugConversations, "Hide entry at offset %i. Associated node is %i, entry %i\n",
// targetEntry->offset, entryInfo->nodeIndex, entryInfo->entryIndex);
targetEntry->visible = false;
return true;
case kUnhideEntry:
//debugCN(kDebugConversations, "Show entry at offset %i. Associated node is %i, entry %i\n",
// targetEntry->offset, entryInfo->nodeIndex, entryInfo->entryIndex);
targetEntry->visible = true;
return true;
case kDestroyEntry:
//debugCN(kDebugConversations, "Destroy entry at offset %i. Associated node is %i, entry %i\n",
// targetEntry->offset, entryInfo->nodeIndex, entryInfo->entryIndex);
if (entryInfo->entryIndex >= 0)
getNode(entryInfo->nodeIndex)->entries.remove_at(entryInfo->entryIndex);
else
warning("Target entry is a node, not destroying it");
targetEntry->visible = true;
return true;
case kExitConv:
//debugCN(kDebugConversations, "Exit conversation\n");
endConversation();
return false;
default:
warning("Unknown entry action");
return false;
} // end switch
}
/*--------------------------------------------------------------------------*/
MadsConversation::MadsConversation() {
for (int i = 0; i < MADS_TALK_SIZE; ++i) {
_talkList[i].desc = NULL;
_talkList[i].id = 0;
}
}
} // End of namespace M4