scummvm/engines/nancy/action/actionmanager.cpp
2021-04-13 20:49:46 +03:00

464 lines
14 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 "common/serializer.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/input.h"
#include "engines/nancy/sound.h"
#include "engines/nancy/action/actionmanager.h"
#include "engines/nancy/action/actionrecord.h"
#include "engines/nancy/state/scene.h"
namespace Nancy {
namespace Action {
void ActionManager::handleInput(NancyInput &input) {
for (auto &rec : _records) {
if (rec->_isActive) {
// Send input to all active records
rec->handleInput(input);
}
if (rec->_isActive && rec->_hasHotspot && NancySceneState.getViewport().convertViewportToScreen(rec->_hotspot).contains(input.mousePos)) {
g_nancy->_cursorManager->setCursorType(rec->getHoverCursor());
if (input.input & NancyInput::kLeftMouseButtonUp) {
input.input &= ~NancyInput::kLeftMouseButtonUp;
bool shouldTrigger = false;
int16 heldItem = NancySceneState.getHeldItem();
if (rec->_itemRequired != -1) {
if (heldItem == -1 && rec->_itemRequired == -2) {
shouldTrigger = true;
} else {
if (rec->_itemRequired <= 100) {
if (heldItem == rec->_itemRequired) {
shouldTrigger = true;
}
} else if (rec->_itemRequired <= 110 && rec->_itemRequired - 100 != heldItem) {
// IDs 100 - 110 mean the record will activate when the object is _not_ the specified one
shouldTrigger = true;
}
}
if (!shouldTrigger) {
g_nancy->_sound->playSound("CANT");
}
} else {
shouldTrigger = true;
}
if (shouldTrigger) {
rec->_state = ActionRecord::ExecutionState::kActionTrigger;
if (rec->_itemRequired > 100 && rec->_itemRequired <= 110) {
rec->_itemRequired -= 100;
}
// Re-add the object to the inventory unless it's marked as a one-time use
if (rec->_itemRequired == heldItem && rec->_itemRequired != -1) {
if (NancySceneState.getInventoryBox().getItemDescription(heldItem).oneTimeUse != 0) {
NancySceneState.getInventoryBox().addItem(heldItem);
}
NancySceneState.setHeldItem(-1);
}
}
break;
}
}
}
}
bool ActionManager::addNewActionRecord(Common::SeekableReadStream &inputData) {
inputData.seek(0x30);
byte ARType = inputData.readByte();
ActionRecord *newRecord = createActionRecord(ARType);
inputData.seek(0);
char descBuf[0x30];
inputData.read(descBuf, 0x30);
descBuf[0x2F] = '\0';
newRecord->_description = descBuf;
newRecord->_type = inputData.readByte(); // redundant
newRecord->_execType = (ActionRecord::ExecutionType)inputData.readByte();
uint16 localChunkSize = inputData.pos();
newRecord->readData(inputData);
localChunkSize = inputData.pos() - localChunkSize;
localChunkSize += 0x32;
// If the localChunkSize is less than the total data, there must be dependencies at the end of the chunk
uint16 depsDataSize = (uint16)inputData.size() - localChunkSize;
if (depsDataSize > 0) {
// Each dependency is 0x0C bytes long (in v1)
uint numDependencies = depsDataSize / 0xC;
if (depsDataSize % 0xC) {
error("Action record type %s has incorrect read size", newRecord->getRecordTypeName().c_str());;
}
// Initialize the dependencies data
inputData.seek(localChunkSize);
newRecord->_dependencies.reserve(numDependencies);
for (uint16 i = 0; i < numDependencies; ++i) {
newRecord->_dependencies.push_back(DependencyRecord());
DependencyRecord &dep = newRecord->_dependencies.back();
dep.type = (DependencyType)inputData.readByte();
dep.label = inputData.readByte();
dep.condition = inputData.readByte();
dep.orFlag = inputData.readByte();
dep.hours = inputData.readSint16LE();
dep.minutes = inputData.readSint16LE();
dep.seconds = inputData.readSint16LE();
dep.milliseconds = inputData.readSint16LE();
if (dep.type != DependencyType::kSceneCount || dep.hours != -1 || dep.minutes != -1 || dep.seconds != -1) {
dep.timeData = ((dep.hours * 60 + dep.minutes) * 60 + dep.seconds) * 1000 + dep.milliseconds;
}
}
} else {
// Set new record to active if it doesn't depend on anything
newRecord->_isActive = true;
}
_records.push_back(newRecord);
debugC(1, kDebugActionRecord, "Loaded action record %i, type %s, typeID %i, description \"%s\", execType == %s",
_records.size() - 1,
newRecord->getRecordTypeName().c_str(),
newRecord->_type,
newRecord->_description.c_str(),
newRecord->_execType == ActionRecord::kRepeating ? "kRepeating" : "kOneShot");
for (uint i = 0; i < newRecord->_dependencies.size(); ++i) {
debugCN(1, kDebugActionRecord, "\tDependency %i: type ", i);
DependencyRecord &dep = newRecord->_dependencies[i];
switch (dep.type) {
case DependencyType::kNone :
debugCN(1, kDebugActionRecord, "kNone");
break;
case DependencyType::kInventory :
debugCN(1, kDebugActionRecord, "kInventory, item ID %i %s",
dep.label,
dep.condition == kTrue ? "is in possession" : "is not in possession");
break;
case DependencyType::kEventFlag :
debugCN(1, kDebugActionRecord, "kEventFlag, flag ID %i == %s",
dep.label,
dep.condition == kTrue ? "true" : "false");
break;
case DependencyType::kLogicCondition :
debugCN(1, kDebugActionRecord, "kLogicCondition, logic condition ID %i == %s",
dep.label,
dep.condition == kTrue ? "true" : "false");
break;
case DependencyType::kTotalTime :
debugCN(1, kDebugActionRecord, "kTotalTime, %i hours, %i minutes, %i seconds, %i milliseconds",
dep.hours,
dep.minutes,
dep.seconds,
dep.milliseconds);
break;
case DependencyType::kSceneTime :
debugCN(1, kDebugActionRecord, "kSceneTime, %i hours, %i minutes, %i seconds, %i milliseconds",
dep.hours,
dep.minutes,
dep.seconds,
dep.milliseconds);
break;
case DependencyType::kPlayerTime :
debugCN(1, kDebugActionRecord, "kPlayerTime, %i days, %i hours, %i minutes, %i seconds",
dep.hours,
dep.minutes,
dep.seconds,
dep.milliseconds);
break;
case DependencyType::kSceneCount :
debugCN(1, kDebugActionRecord, "kSceneCount, scene ID %i, hit count %s %i",
dep.hours,
dep.milliseconds == 1 ? ">" : dep.milliseconds == 2 ? "<" : "==",
dep.seconds);
break;
case DependencyType::kResetOnNewDay :
debugCN(1, kDebugActionRecord, "kResetOnNewDay");
break;
case DependencyType::kUseItem :
debugCN(1, kDebugActionRecord, "kUseItem, item ID %i %s",
dep.label,
dep.condition == kTrue ? "is held" : "is not held");
break;
case DependencyType::kTimeOfDay :
debugCN(1, kDebugActionRecord, "kTimeOfDay, %s",
dep.label == 0 ? "day" : dep.label == 1 ? "night" : "dusk/dawn");
break;
case DependencyType::kTimerNotDone :
debugCN(1, kDebugActionRecord, "kTimerNotDone");
break;
case DependencyType::kTimerDone :
debugCN(1, kDebugActionRecord, "kTimerDone");
break;
case DependencyType::kDifficultyLevel :
debugCN(1, kDebugActionRecord, "kDifficultyLevel, level %i", dep.condition);
break;
default:
debugCN(1, kDebugActionRecord, "unknown");
break;
}
debugC(1, kDebugActionRecord, ", orFlag == %s", dep.orFlag == true ? "true" : "false");
}
return true;
}
void ActionManager::processActionRecords() {
for (auto record : _records) {
if (record->_isDone) {
continue;
}
if (!record->_isActive) {
for (uint i = 0; i < record->_dependencies.size(); ++i) {
DependencyRecord &dep = record->_dependencies[i];
if (!dep.satisfied) {
switch (dep.type) {
case DependencyType::kNone:
dep.satisfied = true;
break;
case DependencyType::kInventory:
switch (dep.condition) {
case kFalse:
// Item not in possession or held
if (NancySceneState._flags.items[dep.label] == kFalse &&
dep.label != NancySceneState._flags.heldItem) {
dep.satisfied = true;
}
break;
case kTrue:
if (NancySceneState._flags.items[dep.label] == kTrue ||
dep.label == NancySceneState._flags.heldItem) {
dep.satisfied = true;
}
break;
default:
break;
}
break;
case DependencyType::kEventFlag:
if (NancySceneState.getEventFlag(dep.label, (NancyFlag)dep.condition)) {
// nancy1 has code for some timer array that never gets used
// and is discarded from nancy2 onward
dep.satisfied = true;
}
break;
case DependencyType::kLogicCondition:
if (NancySceneState._flags.logicConditions[dep.label].flag == dep.condition) {
// Wait for specified time before satisfying dependency condition
Time elapsed = NancySceneState._timers.lastTotalTime - NancySceneState._flags.logicConditions[dep.label].timestamp;
if (elapsed >= dep.timeData) {
dep.satisfied = true;
}
}
break;
case DependencyType::kTotalTime:
if (NancySceneState._timers.lastTotalTime >= dep.timeData) {
dep.satisfied = true;
}
break;
case DependencyType::kSceneTime:
if (NancySceneState._timers.sceneTime >= dep.timeData) {
dep.satisfied = true;
}
break;
case DependencyType::kPlayerTime:
// TODO almost definitely wrong, as the original engine treats player time differently
if (NancySceneState._timers.playerTime >= dep.timeData) {
dep.satisfied = true;
}
break;
case DependencyType::kUnknownType7:
warning("Unknown Dependency type 7");
break;
case DependencyType::kUnknownType8:
warning("Unknown Dependency type 8");
break;
case DependencyType::kSceneCount:
// This dependency type keeps its data in the time variables
// Also, I'm pretty sure it never gets used
switch (dep.milliseconds) {
case 1:
if (dep.seconds < NancySceneState._flags.sceneHitCount[dep.hours]) {
dep.satisfied = true;
}
break;
case 2:
if (dep.seconds > NancySceneState._flags.sceneHitCount[dep.hours]) {
dep.satisfied = true;
}
break;
case 3:
if (dep.seconds == NancySceneState._flags.sceneHitCount[dep.hours]) {
dep.satisfied = true;
}
break;
}
break;
case DependencyType::kResetOnNewDay:
if (record->_days == -1) {
record->_days = NancySceneState._timers.playerTime.getDays();
dep.satisfied = true;
break;
}
if (record->_days < NancySceneState._timers.playerTime.getDays()) {
record->_days = NancySceneState._timers.playerTime.getDays();
for (uint j = 0; j < record->_dependencies.size(); ++j) {
if (record->_dependencies[j].type == DependencyType::kPlayerTime) {
record->_dependencies[j].satisfied = false;
}
}
}
break;
case DependencyType::kUseItem: {
bool hasUnsatisfiedDeps = false;
for (uint j = 0; j < record->_dependencies.size(); ++j) {
if (j != i && record->_dependencies[j].satisfied == false) {
hasUnsatisfiedDeps = true;
}
}
if (hasUnsatisfiedDeps) {
break;
}
record->_itemRequired = dep.label;
if (dep.condition == 1) {
record->_itemRequired += 100;
}
dep.satisfied = true;
break;
}
case DependencyType::kTimeOfDay:
if (dep.label == (byte)NancySceneState._timers.timeOfDay) {
dep.satisfied = true;
}
break;
case DependencyType::kTimerNotDone:
if (NancySceneState._timers.timerTime <= dep.timeData) {
dep.satisfied = true;
}
break;
case DependencyType::kTimerDone:
if (NancySceneState._timers.timerTime > dep.timeData) {
dep.satisfied = true;
}
break;
case DependencyType::kDifficultyLevel:
if (dep.condition == NancySceneState._difficulty) {
dep.satisfied = true;
}
break;
default:
warning("Unknown Dependency type %i", (int)dep.type);
break;
}
}
}
// An orFlag marks that its corresponding dependency and the one after it
// mutually satisfy each other; if one is satisfied, so is the other
for (uint i = 1; i < record->_dependencies.size(); ++i) {
if (record->_dependencies[i - 1].orFlag) {
if (record->_dependencies[i - 1].satisfied)
record->_dependencies[i].satisfied = true;
if (record->_dependencies[i].satisfied)
record->_dependencies[i - 1].satisfied = true;
}
}
// Check if all dependencies have been satisfied, and activate the record if they have
uint satisfied = 0;
for (uint i = 0; i < record->_dependencies.size(); ++i) {
if (record->_dependencies[i].satisfied)
++satisfied;
}
if (satisfied == record->_dependencies.size())
record->_isActive = true;
}
if (record->_isActive) {
record->execute();
}
}
}
void ActionManager::clearActionRecords() {
for (auto &r : _records) {
delete r;
}
_records.clear();
}
void ActionManager::onPause(bool pause) {
for (auto &r : _records) {
if (r->_isActive && !r->_isDone) {
r->onPause(pause);
}
}
}
void ActionManager::synchronize(Common::Serializer &ser) {
// When loading, the records should already have been initialized by scene
for (auto &rec : _records) {
ser.syncAsByte(rec->_isActive);
ser.syncAsByte(rec->_isDone);
}
}
} // End of namespace Action
} // End of namespace Nancy