/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "alcachofa/script.h" #include "alcachofa/rooms.h" #include "alcachofa/global-ui.h" #include "alcachofa/alcachofa.h" #include "alcachofa/script-debug.h" #include "common/file.h" using namespace Common; using namespace Math; namespace Alcachofa { enum ScriptDebugLevel { SCRIPT_DEBUG_LVL_NONE = 0, SCRIPT_DEBUG_LVL_TASKS = 1, SCRIPT_DEBUG_LVL_KERNELCALLS = 2, SCRIPT_DEBUG_LVL_INSTRUCTIONS = 3 }; ScriptInstruction::ScriptInstruction(ReadStream &stream) : _op(stream.readSint32LE()) , _arg(stream.readSint32LE()) {} Script::Script() { File file; if (!file.open("script/SCRIPT.COD")) error("Could not open script"); uint32 stringBlobSize = file.readUint32LE(); uint32 memorySize = file.readUint32LE(); _strings = SpanOwner>({ new char[stringBlobSize], stringBlobSize }); if (file.read(&_strings[0], stringBlobSize) != stringBlobSize) error("Could not read script string blob"); if (_strings[stringBlobSize - 1] != 0) error("String blob does not end with null terminator"); if (memorySize % sizeof(int32) != 0) error("Unexpected size of script memory"); _variables.resize(memorySize / sizeof(int32), 0); uint32 variableCount = file.readUint32LE(); for (uint32 i = 0; i < variableCount; i++) { String name = readVarString(file); uint32 offset = file.readUint32LE(); if (offset % sizeof(int32) != 0) error("Unaligned variable offset"); _variableNames[name] = offset / 4; } uint32 procedureCount = file.readUint32LE(); for (uint32 i = 0; i < procedureCount; i++) { String name = readVarString(file); uint32 offset = file.readUint32LE(); file.skip(sizeof(uint32)); _procedures[name] = offset - 1; // originally one-based, but let's not. } uint32 behaviorCount = file.readUint32LE(); for (uint32 i = 0; i < behaviorCount; i++) { String behaviorName = readVarString(file) + '/'; variableCount = file.readUint32LE(); // not used by the original game assert(variableCount == 0); procedureCount = file.readUint32LE(); for (uint32 j = 0; j < procedureCount; j++) { String name = behaviorName + readVarString(file); uint32 offset = file.readUint32LE(); file.skip(sizeof(uint32)); _procedures[name] = offset - 1; } } uint32 instructionCount = file.readUint32LE(); _instructions.reserve(instructionCount); for (uint32 i = 0; i < instructionCount; i++) _instructions.push_back(ScriptInstruction(file)); } static void syncAsSint32LE(Serializer &s, int32 &value) { s.syncAsSint32LE(value); } void Script::syncGame(Serializer &s) { s.syncArray(_variables.data(), _variables.size(), syncAsSint32LE); } int32 Script::variable(const char *name) const { uint32 index; if (_variableNames.tryGetVal(name, index)) return _variables[index]; g_engine->game().unknownVariable(name); return 0; } int32 &Script::variable(const char *name) { uint32 index; if (_variableNames.tryGetVal(name, index)) return _variables[index]; g_engine->game().unknownVariable(name); static int32 dummy = 0; return dummy; } bool Script::hasProcedure(const Common::String &behavior, const Common::String &action) const { return hasProcedure(behavior + '/' + action); } bool Script::hasProcedure(const Common::String &procedure) const { return _procedures.contains(procedure); } struct ScriptTimerTask final : public Task { ScriptTimerTask(Process &process, int32 durationSec) : Task(process) , _durationSec(durationSec) {} ScriptTimerTask(Process &process, Serializer &s) : Task(process) { ScriptTimerTask::syncGame(s); } TaskReturn run() override { TASK_BEGIN; { uint32 timeSinceTimer = g_engine->script()._scriptTimer == 0 ? 0 : (g_engine->getMillis() - g_engine->script()._scriptTimer) / 1000; if (_durationSec >= (int32)timeSinceTimer) _result = g_engine->script().variable("SeHaPulsadoRaton") ? 0 : 2; else _result = 1; g_engine->player().drawCursor(); } TASK_YIELD(1); // Wait a frame to not produce an endless loop TASK_RETURN(_result); //-V779 TASK_END; } void debugPrint() override { g_engine->getDebugger()->debugPrintf("Check input timer for %dsecs", _durationSec); } void syncGame(Serializer &s) override { Task::syncGame(s); s.syncAsSint32LE(_durationSec); s.syncAsSint32LE(_result); } const char *taskName() const override; private: int32 _durationSec = 0; int32 _result = 1; }; DECLARE_TASK(ScriptTimerTask) enum class StackEntryType { Number, Variable, String, Instruction }; struct StackEntry { StackEntry(StackEntryType type, int32 number) : _type(type), _number(number) {} StackEntry(StackEntryType type, uint32 index) : _type(type), _index(index) {} StackEntry(Serializer &s) : _type(), _number(0) { syncGame(s); } void syncGame(Serializer &s) { syncEnum(s, _type); if (_type == StackEntryType::Number) s.syncAsSint32LE(_number); else s.syncAsUint32LE(_index); } StackEntryType _type; union { int32 _number; uint32 _index; }; }; struct ScriptTask final : public Task { ScriptTask(Process &process, const String &name, uint32 pc, FakeLock &&lock) : Task(process) , _script(g_engine->script()) , _name(name) , _pc(pc) , _lock(Common::move(lock)) { pushInstruction(UINT_MAX); debugC(SCRIPT_DEBUG_LVL_TASKS, kDebugScript, "%u: Script start at %u", process.pid(), pc); } ScriptTask(Process &process, const ScriptTask &forkParent) : Task(process) , _script(g_engine->script()) , _name(forkParent._name + " FORKED") , _pc(forkParent._pc) , _lock(forkParent._lock) { for (uint i = 0; i < forkParent._stack.size(); i++) _stack.push(forkParent._stack[i]); pushNumber(1); // this task is the forked one debugC(SCRIPT_DEBUG_LVL_TASKS, kDebugScript, "%u: Script fork from %u at %u", process.pid(), forkParent.process().pid(), _pc); } ScriptTask(Process &process, Serializer &s) : Task(process) , _script(g_engine->script()) { ScriptTask::syncGame(s); } TaskReturn run() override { if (_isFirstExecution || _returnsFromKernelCall) setCharacterVariables(); if (_returnsFromKernelCall) { handleReturnFromKernelCall(process().returnValue()); } _isFirstExecution = _returnsFromKernelCall = false; auto opMap = g_engine->game().getScriptOpMap(); while (true) { if (_pc >= _script._instructions.size()) error("Script process reached instruction out-of-bounds"); const auto &instruction = _script._instructions[_pc++]; if (debugChannelSet(SCRIPT_DEBUG_LVL_INSTRUCTIONS, kDebugScript)) { debugN("%u: %5u %-12s %8d Stack: ", process().pid(), _pc - 1, ScriptOpNames[(int)instruction._op], instruction._arg); if (_stack.empty()) debug("empty"); else { const auto &top = _stack.top(); switch (top._type) { case StackEntryType::Number: debug("Number %d", top._number); break; case StackEntryType::Variable: debug("Var %u (%d)", top._index, _script._variables[top._index]); break; case StackEntryType::Instruction: debug("Instr %u", top._index); break; case StackEntryType::String: debug("String %u (\"%s\")", top._index, getStringArg(0)); break; default: debug("INVALID"); break; } } } if (instruction._op < 0 || (uint32)instruction._op >= opMap.size()) { g_engine->game().unknownInstruction(instruction); continue; } switch (opMap[instruction._op]) { case ScriptOp::Nop: break; case ScriptOp::Dup: if (_stack.empty()) error("Script tried to duplicate stack top, but stack is empty"); _stack.push(_stack.top()); break; case ScriptOp::PushAddr: pushVariable(instruction._arg); break; case ScriptOp::PushValue: pushNumber(instruction._arg); break; case ScriptOp::Deref: pushNumber(popVariable()); break; case ScriptOp::PopN: popN(instruction._arg); break; case ScriptOp::Store: { int32 value = popNumber(); popVariable() = value; pushNumber(value); }break; case ScriptOp::LoadString: pushString(popNumber()); break; case ScriptOp::ScriptCall: pushInstruction(_pc); _pc = instruction._arg - 1; break; case ScriptOp::KernelCall: { TaskReturn kernelReturn = kernelCall(instruction._arg); if (kernelReturn.type() == TaskReturnType::Waiting) { _returnsFromKernelCall = true; return kernelReturn; } else handleReturnFromKernelCall(kernelReturn.returnValue()); }break; case ScriptOp::JumpIfFalse: if (popNumber() == 0) _pc = _pc - 1 + instruction._arg; break; case ScriptOp::JumpIfTrue: if (popNumber() != 0) _pc = _pc - 1 + instruction._arg; break; case ScriptOp::Jump: _pc = _pc - 1 + instruction._arg; break; case ScriptOp::Negate: pushNumber(-popNumber()); break; case ScriptOp::BooleanNot: pushNumber(popNumber() == 0 ? 1 : 0); break; case ScriptOp::Mul: pushNumber(popNumber() * popNumber()); break; case ScriptOp::Add: pushNumber(popNumber() + popNumber()); break; // flipped operators to not use a temporary case ScriptOp::Sub: pushNumber(-popNumber() + popNumber()); break; case ScriptOp::Less: pushNumber(popNumber() > popNumber()); //-V501 break; case ScriptOp::Greater: pushNumber(popNumber() < popNumber()); //-V501 break; case ScriptOp::LessEquals: pushNumber(popNumber() >= popNumber()); //-V501 break; case ScriptOp::GreaterEquals: pushNumber(popNumber() <= popNumber()); //-V501 break; case ScriptOp::Equals: pushNumber(popNumber() == popNumber()); //-V501 break; case ScriptOp::NotEquals: pushNumber(popNumber() != popNumber()); //-V501 break; case ScriptOp::BitAnd: pushNumber(popNumber() & popNumber()); //-V501 break; case ScriptOp::BitOr: pushNumber(popNumber() | popNumber()); //-V501 break; case ScriptOp::ReturnValue: { int32 returnValue = popNumber(); _pc = popInstruction(); if (_pc == UINT_MAX) return TaskReturn::finish(returnValue); else pushNumber(returnValue); }break; default: g_engine->game().unknownInstruction(instruction); break; } } } void debugPrint() override { g_engine->getDebugger()->debugPrintf("\"%s\" at %u\n", _name.c_str(), _pc); } void syncGame(Serializer &s) override { assert(s.isSaving() || (_lock.isReleased() && _stack.empty())); s.syncString(_name); s.syncAsUint32LE(_pc); s.syncAsByte(_returnsFromKernelCall); s.syncAsByte(_isFirstExecution); uint count = _stack.size(); s.syncAsUint32LE(count); if (s.isLoading()) { for (uint i = 0; i < count; i++) _stack.push(StackEntry(s)); } else { for (uint i = 0; i < count; i++) _stack[i].syncGame(s); } bool hasLock = !_lock.isReleased(); s.syncAsByte(hasLock); if (s.isLoading() && hasLock) _lock = FakeLock("script", g_engine->player().semaphoreFor(process().character())); } const char *taskName() const override; private: void setCharacterVariables() { _script.variable("m_o_f") = (int32)process().character(); _script.variable("m_o_f_real") = (int32)g_engine->player().activeCharacterKind(); } void handleReturnFromKernelCall(int32 returnValue) { // this is also original done, every KernelCall is followed by a PopN of the arguments // only *after* the PopN the return value is pushed so that the following script can use it scumm_assert( _pc < _script._instructions.size() && g_engine->game().getScriptOpMap()[_script._instructions[_pc]._op] == ScriptOp::PopN); popN(_script._instructions[_pc++]._arg); pushNumber(returnValue); } void pushNumber(int32 value) { _stack.push({ StackEntryType::Number, value }); } // For the following methods error recovery is not really viable void pushVariable(uint32 offset) { uint32 index = offset / sizeof(int32); if (offset % sizeof(int32) != 0 || index >= _script._variables.size()) error("Script tried to push invalid variable offset"); _stack.push({ StackEntryType::Variable, index }); } void pushString(uint32 offset) { if (offset >= _script._strings->size()) error("Script tried to push invalid string offset"); _stack.push({ StackEntryType::String, offset }); } void pushInstruction(uint32 pc) { _stack.push({ StackEntryType::Instruction, pc }); } StackEntry pop() { if (_stack.empty()) error("Script tried to pop empty stack"); return _stack.pop(); } int32 popNumber() { auto entry = pop(); if (entry._type != StackEntryType::Number) error("Script tried to pop, but top of stack is not a number"); return entry._number; } int32 &popVariable() { auto entry = pop(); if (entry._type != StackEntryType::Variable) error("Script tried to pop, but top of stack is not a variable"); return _script._variables[entry._index]; } const char *popString() { auto entry = pop(); if (entry._type != StackEntryType::String) error("Script tried to pop, but top of stack is not a string"); return _script._strings->data() + entry._index; } uint32 popInstruction() { auto entry = pop(); if (entry._type != StackEntryType::Instruction) error("Script tried to pop but top of stack is not an instruction"); return entry._index; } void popN(int32 count) { if (count < 0 || (uint)count > _stack.size()) error("Script tried to pop more entries than are available on the stack"); for (int32 i = 0; i < count; i++) _stack.pop(); } StackEntry getArg(uint argI) { if (_stack.size() < argI + 1) error("Script did not supply enough arguments for kernel call"); return _stack[_stack.size() - 1 - argI]; } int32 getNumberArg(uint argI) { auto entry = getArg(argI); if (entry._type != StackEntryType::Number) error("Expected number in argument %u for kernel call", argI); return entry._number; } const char *getStringArg(uint argI) { auto entry = getArg(argI); if (entry._type != StackEntryType::String) error("Expected string in argument %u for kernel call", argI); return &_script._strings[entry._index]; } int32 getNumberOrStringArg(uint argI) { // Original inconsistency: sometimes a string is passed instead of a number // as it will be interpreted as a boolean we only care about == 0 / != 0 auto entry = getArg(argI); if (entry._type != StackEntryType::Number && entry._type != StackEntryType::String) error("Expected number or string in argument %u for kernel call", argI); return entry._number; } const char *getOptionalStringArg(uint argI) { // another special case: a string that may be zero which is passed as number auto entry = getArg(argI); if (entry._type == StackEntryType::String) return &_script._strings[entry._index]; if (entry._type == StackEntryType::Number && entry._number == 0) return nullptr; error("Expected optional string in argument %u for kernel call", argI); } template TObject *getObjectArg(uint argI) { const char *const name = getStringArg(argI); auto *object = g_engine->world().getObjectByName(process().character(), name); return dynamic_cast(object); } MainCharacter &relatedCharacter() { if (process().character() == MainCharacterKind::None) error("Script tried to use character from non-character-related process"); return g_engine->world().getMainCharacterByKind(process().character()); } bool shouldSkipCutscene() { return process().character() != MainCharacterKind::None && g_engine->player().activeCharacterKind() != process().character(); } TaskReturn kernelCall(int32 taskI) { const auto taskMap = g_engine->game().getScriptKernelTaskMap(); if (taskI < 0 || (uint32)taskI >= taskMap.size()) { g_engine->game().unknownKernelTask(taskI); return TaskReturn::finish(-1); } const auto task = taskMap[taskI]; debugC(SCRIPT_DEBUG_LVL_KERNELCALLS, kDebugScript, "%u: %5u Kernel %-25s", process().pid(), _pc - 1, KernelCallNames[(int)task]); switch (task) { // sound/video case ScriptKernelTask::PlayVideo: g_engine->playVideo(getNumberArg(0)); return TaskReturn::finish(0); case ScriptKernelTask::PlaySound: { auto soundID = g_engine->sounds().playSFX(getStringArg(0)); g_engine->sounds().setAppropriateVolume(soundID, process().character(), nullptr); return getNumberArg(1) == 0 ? TaskReturn::waitFor(new PlaySoundTask(process(), soundID)) : TaskReturn::finish(1); } case ScriptKernelTask::PlayMusic: if (process().isActiveForPlayer()) g_engine->sounds().startMusic((int)getNumberArg(0)); return TaskReturn::finish(0); case ScriptKernelTask::StopMusic: if (process().isActiveForPlayer()) g_engine->sounds().fadeMusic(); return TaskReturn::finish(0); case ScriptKernelTask::WaitForMusicToEnd: warning("STUB KERNEL CALL: WaitForMusicToEnd"); return TaskReturn::finish(0); // Misc / control flow case ScriptKernelTask::ShowCenterBottomText: return TaskReturn::waitFor(showCenterBottomText(process(), getNumberArg(0), (uint32)getNumberArg(1))); case ScriptKernelTask::Delay: return getNumberArg(0) <= 0 ? TaskReturn::finish(0) : TaskReturn::waitFor(delay((uint32)getNumberArg(0))); case ScriptKernelTask::HadNoMousePressFor: return TaskReturn::waitFor(new ScriptTimerTask(process(), getNumberArg(0))); case ScriptKernelTask::Fork: g_engine->scheduler().createProcess(process().character(), *this); return TaskReturn::finish(0); // 0 means this is the forking process case ScriptKernelTask::KillProcesses: killProcessesFor((MainCharacterKind)getNumberArg(0)); return TaskReturn::finish(1); // player/world state changes case ScriptKernelTask::ChangeCharacter: { MainCharacterKind kind = (MainCharacterKind)getNumberArg(0); killProcessesFor(MainCharacterKind::None); // yes, kill for all characters auto &camera = g_engine->camera(); auto &player = g_engine->player(); camera.resetRotationAndScale(); camera.backup(0); if (kind != MainCharacterKind::None) { player.setActiveCharacter(kind); player.heldItem() = nullptr; camera.setFollow(player.activeCharacter()); camera.backup(0); } process().character() = MainCharacterKind::None; assert(player.semaphore().isReleased()); _lock = FakeLock("script", player.semaphore()); return TaskReturn::finish(1); } case ScriptKernelTask::ChangeRoom: if (scumm_stricmp(getStringArg(0), "SALIR") == 0) { g_engine->quitGame(); g_engine->player().changeRoom("SALIR", true); } else if (scumm_stricmp(getStringArg(0), "MENUPRINCIPALINICIO") == 0) warning("STUB: change room to MenuPrincipalInicio special case"); else { auto targetRoom = g_engine->world().getRoomByName(getStringArg(0)); if (targetRoom == nullptr) error("Invalid room name: %s\n", getStringArg(0)); if (process().isActiveForPlayer()) { g_engine->player().heldItem() = nullptr; bool isTemporaryRoom = false; if (g_engine->player().currentRoom() == &g_engine->world().inventory()) { isTemporaryRoom = true; // see changeRoom, this fixes a bug on looking at items in the inventory // this is also why we do not exit the inventory room here (like when the user closes the inventory) g_engine->world().inventory().close(); } if (targetRoom == &g_engine->world().inventory()) g_engine->world().inventory().open(); else g_engine->player().changeRoom(targetRoom->name(), true, isTemporaryRoom); g_engine->sounds().setMusicToRoom(targetRoom->musicID()); } g_engine->script().createProcess(process().character(), "ENTRAR_" + targetRoom->name(), ScriptFlags::AllowMissing); } return TaskReturn::finish(1); case ScriptKernelTask::ToggleRoomFloor: if (process().character() == MainCharacterKind::None) { if (g_engine->player().currentRoom() != nullptr) g_engine->player().currentRoom()->toggleActiveFloor(); } else g_engine->world().getMainCharacterByKind(process().character()).room()->toggleActiveFloor(); return TaskReturn::finish(1); // object control / animation case ScriptKernelTask::On: g_engine->world().toggleObject(process().character(), getStringArg(0), true); return TaskReturn::finish(0); case ScriptKernelTask::Off: g_engine->world().toggleObject(process().character(), getStringArg(0), false); return TaskReturn::finish(0); case ScriptKernelTask::Animate: { auto graphicObject = getObjectArg(0); if (graphicObject == nullptr) { g_engine->game().unknownAnimateObject(getStringArg(0)); return TaskReturn::finish(1); } if (getNumberOrStringArg(1)) { graphicObject->toggle(true); graphicObject->graphic()->start(false); return TaskReturn::finish(1); } else return TaskReturn::waitFor(graphicObject->animate(process())); } // character control / animation case ScriptKernelTask::StopAndTurn: { auto character = getObjectArg(0); if (character == nullptr) g_engine->game().unknownScriptCharacter("stop-and-turn", getStringArg(0)); else character->stopWalking((Direction)getNumberArg(1)); return TaskReturn::finish(1); } case ScriptKernelTask::StopAndTurnMe: { relatedCharacter().stopWalking((Direction)getNumberArg(0)); return TaskReturn::finish(1); } case ScriptKernelTask::Go: { auto character = getObjectArg(0); if (character == nullptr) { g_engine->game().unknownScriptCharacter("go", getStringArg(0)); return TaskReturn::finish(1); } auto target = getObjectArg(1); if (target == nullptr) target = g_engine->game().unknownGoPutTarget(process(), "go", getStringArg(1)); if (target == nullptr) return TaskReturn::finish(0); character->walkTo(target->position()); if (getNumberArg(2) & 2) g_engine->camera().setFollow(nullptr); return (getNumberArg(2) & 1) ? TaskReturn::finish(1) : TaskReturn::waitFor(character->waitForArrival(process())); } case ScriptKernelTask::Put: { auto character = getObjectArg(0); if (character == nullptr) { g_engine->game().unknownScriptCharacter("put", getStringArg(0)); return TaskReturn::finish(1); } auto target = getObjectArg(1); if (target == nullptr) target = g_engine->game().unknownGoPutTarget(process(), "put", getStringArg(1)); if (target == nullptr) return TaskReturn::finish(0); character->setPosition(target->position()); return TaskReturn::finish(1); } case ScriptKernelTask::ChangeCharacterRoom: { auto *character = getObjectArg(0); if (character == nullptr) { g_engine->game().unknownScriptCharacter("change character room", getStringArg(0)); return TaskReturn::finish(1); } auto *targetRoom = g_engine->world().getRoomByName(getStringArg(1)); if (targetRoom == nullptr) { g_engine->game().unknownChangeCharacterRoom(getStringArg(1)); return TaskReturn::finish(1); } character->resetTalking(); character->room() = targetRoom; return TaskReturn::finish(1); } case ScriptKernelTask::LerpCharacterLodBias: { auto *character = getObjectArg(0); if (character == nullptr) { g_engine->game().unknownScriptCharacter("lerp character LOD bias", getStringArg(0)); return TaskReturn::finish(1); } float targetLodBias = getNumberArg(1) * 0.01f; int32 durationMs = getNumberArg(2); if (durationMs <= 0) { character->lodBias() = targetLodBias; return TaskReturn::finish(1); } else return TaskReturn::waitFor(character->lerpLodBias(process(), targetLodBias, durationMs)); } case ScriptKernelTask::AnimateCharacter: { auto *character = getObjectArg(0); if (character == nullptr) { g_engine->game().unknownScriptCharacter("animate character", getStringArg(0)); return TaskReturn::finish(1); } auto *animObject = getObjectArg(1); if (animObject == nullptr) { g_engine->game().unknownAnimateCharacterObject(getStringArg(1)); return TaskReturn::finish(1); } return TaskReturn::waitFor(character->animate(process(), animObject)); } case ScriptKernelTask::AnimateTalking: { auto *character = getObjectArg(0); if (character == nullptr) { g_engine->game().unknownScriptCharacter("talk", getStringArg(0)); return TaskReturn::finish(1); } ObjectBase *talkObject = getObjectArg(1); if (talkObject == nullptr && *getStringArg(1) != '\0') { g_engine->game().unknownAnimateTalkingObject(getStringArg(1)); return TaskReturn::finish(1); } character->talkUsing(talkObject); return TaskReturn::finish(1); } case ScriptKernelTask::SayText: { const char *characterName = getStringArg(0); int32 dialogId = getNumberArg(1); if (strncmp(characterName, "MENU_", 5) == 0) { relatedCharacter().addDialogLine(dialogId); return TaskReturn::finish(1); } Character *_character = strcmp(characterName, "AMBOS") == 0 ? &relatedCharacter() : getObjectArg(0); if (_character == nullptr) { g_engine->game().unknownSayTextCharacter(characterName, dialogId); return TaskReturn::finish(1); } return TaskReturn::waitFor(_character->sayText(process(), dialogId)); }; case ScriptKernelTask::SetDialogLineReturn: relatedCharacter().setLastDialogReturnValue(getNumberArg(0)); return TaskReturn::finish(0); case ScriptKernelTask::DialogMenu: return TaskReturn::waitFor(relatedCharacter().dialogMenu(process())); // Inventory control case ScriptKernelTask::Pickup: relatedCharacter().pickup(getStringArg(0), !getNumberArg(1)); return TaskReturn::finish(1); case ScriptKernelTask::CharacterPickup: { auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1)); character.pickup(getStringArg(0), !getNumberArg(2)); return TaskReturn::finish(1); } case ScriptKernelTask::Drop: relatedCharacter().drop(getStringArg(0)); return TaskReturn::finish(1); case ScriptKernelTask::CharacterDrop: { auto &character = g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(1)); character.drop(getOptionalStringArg(0)); return TaskReturn::finish(1); } case ScriptKernelTask::ClearInventory: switch ((MainCharacterKind)getNumberArg(0)) { case MainCharacterKind::Mortadelo: g_engine->world().mortadelo().clearInventory(); break; case MainCharacterKind::Filemon: g_engine->world().filemon().clearInventory(); break; default: g_engine->game().unknownClearInventoryTarget(getNumberArg(0)); break; } return TaskReturn::finish(1); // Camera tasks case ScriptKernelTask::WaitCamStopping: return TaskReturn::waitFor(g_engine->camera().waitToStop(process())); case ScriptKernelTask::CamFollow: { WalkingCharacter *target = nullptr; if (getNumberArg(0) != 0) target = &g_engine->world().getMainCharacterByKind((MainCharacterKind)getNumberArg(0)); g_engine->camera().setFollow(target, getNumberArg(1) != 0); return TaskReturn::finish(1); } case ScriptKernelTask::CamShake: return TaskReturn::waitFor(g_engine->camera().shake(process(), Vector2d(getNumberArg(1), getNumberArg(2)), Vector2d(getNumberArg(3), getNumberArg(4)), getNumberArg(0))); case ScriptKernelTask::LerpCamXY: return TaskReturn::waitFor(g_engine->camera().lerpPos(process(), Vector2d(getNumberArg(0), getNumberArg(1)), getNumberArg(2), (EasingType)getNumberArg(3))); case ScriptKernelTask::LerpCamXYZ: return TaskReturn::waitFor(g_engine->camera().lerpPos(process(), Vector3d(getNumberArg(0), getNumberArg(1), getNumberArg(2)), getNumberArg(3), (EasingType)getNumberArg(4))); case ScriptKernelTask::LerpCamZ: return TaskReturn::waitFor(g_engine->camera().lerpPosZ(process(), getNumberArg(0), getNumberArg(1), (EasingType)getNumberArg(2))); case ScriptKernelTask::LerpCamScale: return TaskReturn::waitFor(g_engine->camera().lerpScale(process(), getNumberArg(0) * 0.01f, getNumberArg(1), (EasingType)getNumberArg(2))); case ScriptKernelTask::LerpCamRotation: return TaskReturn::waitFor(g_engine->camera().lerpRotation(process(), getNumberArg(0), getNumberArg(1), (EasingType)getNumberArg(2))); case ScriptKernelTask::LerpCamToObjectKeepingZ: { if (!process().isActiveForPlayer()) return TaskReturn::finish(0); // contrary to ...ResettingZ this one does not delay if not active auto pointObject = getObjectArg(0); if (pointObject == nullptr) { g_engine->game().unknownCamLerpTarget("LerpCamToObjectKeepingZ", getStringArg(0)); return TaskReturn::finish(1); } return TaskReturn::waitFor(g_engine->camera().lerpPos(process(), as2D(pointObject->position()), getNumberArg(1), EasingType::Linear)); } case ScriptKernelTask::LerpCamToObjectResettingZ: { if (!process().isActiveForPlayer()) return TaskReturn::waitFor(delay(getNumberArg(1))); auto pointObject = getObjectArg(0); if (pointObject == nullptr) { g_engine->game().unknownCamLerpTarget("LerpCamToObjectResettingZ", getStringArg(0)); return TaskReturn::finish(1); } return TaskReturn::waitFor(g_engine->camera().lerpPos(process(), as3D(pointObject->position()), getNumberArg(1), (EasingType)getNumberArg(2))); } case ScriptKernelTask::LerpCamToObjectWithScale: { float targetScale = getNumberArg(1) * 0.01f; if (!process().isActiveForPlayer()) // the scale will wait then snap the scale return TaskReturn::waitFor(g_engine->camera().lerpScale(process(), targetScale, getNumberArg(2), EasingType::Linear)); auto pointObject = getObjectArg(0); if (pointObject == nullptr) { g_engine->game().unknownCamLerpTarget("LerpCamToObjectWithScale", getStringArg(0)); return TaskReturn::finish(1); } return TaskReturn::waitFor(g_engine->camera().lerpPosScale(process(), as3D(pointObject->position()), targetScale, getNumberArg(2), (EasingType)getNumberArg(3), (EasingType)getNumberArg(4))); } // Fades case ScriptKernelTask::FadeType0: return TaskReturn::waitFor(fade(process(), FadeType::ToBlack, getNumberArg(0) * 0.01f, getNumberArg(1) * 0.01f, getNumberArg(2), (EasingType)getNumberArg(4), getNumberArg(3))); case ScriptKernelTask::FadeType1: return TaskReturn::waitFor(fade(process(), FadeType::ToWhite, getNumberArg(0) * 0.01f, getNumberArg(1) * 0.01f, getNumberArg(2), (EasingType)getNumberArg(4), getNumberArg(3))); case ScriptKernelTask::FadeIn: return TaskReturn::waitFor(fade(process(), FadeType::ToBlack, 1.0f, 0.0f, getNumberArg(0), EasingType::Out, -5, PermanentFadeAction::UnsetFaded)); case ScriptKernelTask::FadeOut: return TaskReturn::waitFor(fade(process(), FadeType::ToBlack, 0.0f, 1.0f, getNumberArg(0), EasingType::Out, -5, PermanentFadeAction::SetFaded)); case ScriptKernelTask::FadeIn2: return TaskReturn::waitFor(fade(process(), FadeType::ToBlack, 0.0f, 1.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5, PermanentFadeAction::UnsetFaded)); case ScriptKernelTask::FadeOut2: return TaskReturn::waitFor(fade(process(), FadeType::ToBlack, 1.0f, 0.0f, getNumberArg(0), (EasingType)getNumberArg(1), -5, PermanentFadeAction::SetFaded)); // Unused and/or useless case ScriptKernelTask::SetMaxCamSpeedFactor: warning("STUB KERNEL CALL: SetMaxCamSpeedFactor"); return TaskReturn::finish(0); case ScriptKernelTask::LerpWorldLodBias: warning("STUB KERNEL CALL: LerpWorldLodBias"); return TaskReturn::finish(0); case ScriptKernelTask::SetActiveTextureSet: // Fortunately this seems to be unused. warning("STUB KERNEL CALL: SetActiveTextureSet"); return TaskReturn::finish(0); case ScriptKernelTask::FadeType2: warning("STUB KERNEL CALL: FadeType2"); // Crossfade, unused from script return TaskReturn::finish(0); case ScriptKernelTask::Nop: return TaskReturn::finish(0); default: g_engine->game().unknownKernelTask(taskI); return TaskReturn::finish(0); } } void killProcessesFor(MainCharacterKind kind) { if (kind == MainCharacterKind::None) { killProcessesFor(MainCharacterKind::Mortadelo); killProcessesFor(MainCharacterKind::Filemon); g_engine->scheduler().killAllProcessesFor(kind); return; } g_engine->scheduler().killAllProcessesFor(kind); g_engine->sounds().fadeOutVoiceAndSFX(200); g_engine->player().stopLastDialogCharacters(); _lock.release(); // yes this seems dangerous, but it is original.. auto &character = g_engine->world().getMainCharacterByKind(kind); character.resetUsingObjectAndDialogMenu(); assert(character.semaphore().isReleased()); // this process should be the last to hold a lock if at all... } Script &_script; Stack _stack; String _name; uint32 _pc = 0; bool _returnsFromKernelCall = false; bool _isFirstExecution = true; FakeLock _lock; }; DECLARE_TASK(ScriptTask) Process *Script::createProcess(MainCharacterKind character, const String &behavior, const String &action, ScriptFlags flags) { return createProcess(character, behavior + '/' + action, flags); } Process *Script::createProcess(MainCharacterKind character, const String &procedure, ScriptFlags flags) { uint32 offset; if (!_procedures.tryGetVal(procedure, offset)) { if (flags & ScriptFlags::AllowMissing) return nullptr; // it is currently unnecessary but we could return an empty process to avoid returning nullptr here g_engine->game().unknownScriptProcedure(procedure); return nullptr; } FakeLock lock; if (!(flags & ScriptFlags::IsBackground)) lock = FakeLock("script", g_engine->player().semaphoreFor(character)); Process *process = g_engine->scheduler().createProcess(character, procedure, offset, Common::move(lock)); process->name() = procedure; return process; } void Script::setScriptTimer(bool reset) { // Used for the V3 exclusive kernel task HadNoMousePressFor if (reset) _scriptTimer = 0; else if (_scriptTimer == 0) _scriptTimer = g_engine->getMillis(); } }