/* 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