/* 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/player.h" #include "alcachofa/script.h" #include "alcachofa/alcachofa.h" #include "alcachofa/menu.h" using namespace Common; namespace Alcachofa { Player::Player() : _activeCharacter(&g_engine->world().mortadelo()) , _semaphore("player") { const auto &cursorPath = g_engine->world().getGlobalAnimationName(GlobalAnimationKind::Cursor); _cursorAnimation.reset(new Animation(cursorPath)); _cursorAnimation->load(); } void Player::preUpdate() { _selectedObject = nullptr; _cursorFrameI = 0; } void Player::postUpdate() { if (g_engine->input().wasAnyMouseReleased()) _pressedObject = nullptr; } void Player::resetCursor() { _cursorFrameI = 0; } void Player::updateCursor() { if (g_engine->menu().isOpen() || !_isGameLoaded) _cursorFrameI = 0; else if (_selectedObject == nullptr) _cursorFrameI = !g_engine->input().isMouseLeftDown() || _pressedObject != nullptr ? 6 : 7; else { auto type = _selectedObject->cursorType(); switch (type) { case CursorType::LeaveUp: _cursorFrameI = 8; break; case CursorType::LeaveRight: _cursorFrameI = 10; break; case CursorType::LeaveDown: _cursorFrameI = 12; break; case CursorType::LeaveLeft: _cursorFrameI = 14; break; case CursorType::WalkTo: _cursorFrameI = 6; break; case CursorType::Point: default: _cursorFrameI = 0; break; } if (_cursorFrameI != 0) { if (g_engine->input().isAnyMouseDown() && _pressedObject == _selectedObject) _cursorFrameI++; } else if (g_engine->input().isMouseLeftDown()) _cursorFrameI = 2; else if (g_engine->input().isMouseRightDown()) _cursorFrameI = 4; } } void Player::drawCursor(bool forceDefaultCursor) { Point cursorPos = g_engine->input().mousePos2D(); if (_heldItem == nullptr || forceDefaultCursor) { if (forceDefaultCursor) _cursorFrameI = 0; g_engine->drawQueue().add(_cursorAnimation.get(), _cursorFrameI, as2D(cursorPos), -10); } else { auto itemGraphic = _heldItem->graphic(); assert(itemGraphic != nullptr); auto &animation = itemGraphic->animation(); auto frameOffset = animation.totalFrameOffset(0); auto imageSize = animation.imageSize(animation.imageIndex(0, 0)); cursorPos -= frameOffset + imageSize / 2; g_engine->drawQueue().add(&animation, 0, as2D(cursorPos), -kForegroundOrderCount); } } void Player::changeRoom(const Common::String &targetRoomName, bool resetCamera, bool isTemporary) { debugC(1, kDebugGameplay, "Change room to %s", targetRoomName.c_str()); // original would be to always free all resources from globalRoom, inventory, GlobalUI // We don't do that, it is unnecessary, all resources would be loaded right after // Instead we just keep resources loaded for all global rooms and during inventory/room transitions if (targetRoomName.equalsIgnoreCase("SALIR")) { _currentRoom = nullptr; return; // exiting game entirely } if (_currentRoom != nullptr) { g_engine->scheduler().killProcessByName("ACTUALIZAR_" + _currentRoom->name()); bool keepResources = _currentRoom->name().equalsIgnoreCase(targetRoomName) || _currentRoom->name().equalsIgnoreCase("inventario"); if (targetRoomName.equalsIgnoreCase("inventario")) { keepResources = true; if (!_isInTemporaryRoom) _roomBeforeInventory = _currentRoom; } if (!keepResources) _currentRoom->freeResources(); } // this fixes a bug with all original games where changing the room in the inventory (e.g. iFOTO in aventura de cine) // would overwrite the actual game room thus returning from the inventory one would be stuck in the temporary room // If we know that a transition is temporary we prevent that and only remember the real game room _isInTemporaryRoom = isTemporary; _currentRoom = g_engine->world().getRoomByName(targetRoomName.c_str()); if (_currentRoom == nullptr) // no good way to recover, leaving-the-room actions might already prevent further progress error("Invalid room name: %s", targetRoomName.c_str()); if (!_didLoadGlobalRooms) { _didLoadGlobalRooms = true; g_engine->world().inventory().loadResources(); g_engine->world().globalRoom().loadResources(); } _currentRoom->loadResources(); // if we kept resources we loop over a couple noops, that is fine. if (resetCamera) g_engine->camera().resetRotationAndScale(); WalkingCharacter *followTarget = g_engine->camera().followTarget(); if (followTarget != nullptr) g_engine->camera().setFollow(followTarget, true); _pressedObject = _selectedObject = nullptr; } void Player::changeRoomToBeforeInventory() { assert(_roomBeforeInventory != nullptr); changeRoom(_roomBeforeInventory->name(), true); _roomBeforeInventory = nullptr; } MainCharacter *Player::inactiveCharacter() const { if (_activeCharacter == nullptr) return nullptr; return &g_engine->world().getOtherMainCharacterByKind(activeCharacterKind()); } FakeSemaphore &Player::semaphoreFor(MainCharacterKind kind) { static FakeSemaphore dummySemaphore("dummy"); switch (kind) { case MainCharacterKind::None: return _semaphore; case MainCharacterKind::Mortadelo: return g_engine->world().mortadelo().semaphore(); case MainCharacterKind::Filemon: return g_engine->world().filemon().semaphore(); default: assert(false && "Invalid main character kind"); return dummySemaphore; } } void Player::triggerObject(ObjectBase *object, const char *action) { assert(object != nullptr && action != nullptr); if (_activeCharacter->isBusy() || _activeCharacter->currentlyUsing() != nullptr) return; debugC(1, kDebugGameplay, "Trigger object %s %s with %s", object->typeName(), object->name().c_str(), action); if (strcmp(action, "MIRAR") == 0 || inactiveCharacter()->currentlyUsing() == object) { action = "MIRAR"; _activeCharacter->currentlyUsing() = nullptr; } else _activeCharacter->currentlyUsing() = object; auto &script = g_engine->script(); if (script.createProcess(activeCharacterKind(), object->name(), action, ScriptFlags::AllowMissing) != nullptr) return; _activeCharacter->currentlyUsing() = nullptr; if (scumm_stricmp(action, "MIRAR") == 0) script.createProcess(activeCharacterKind(), "DefectoMirar"); //else if (action[0] == 'i' && object->name()[0] == 'i') // This case can happen if you combine two objects without procedure, the original engine // would attempt to start the procedure "DefectoObjeto" which does not exist // (this should be revised when working on further games) else script.createProcess(activeCharacterKind(), "DefectoUsar"); } struct DoorTask final : public Task { DoorTask(Process &process, const Door *door, FakeLock &&lock) : Task(process) , _lock(move(lock)) , _sourceDoor(door) , _character(g_engine->player().activeCharacter()) , _player(g_engine->player()) , _targetObject(nullptr) , _targetDirection(Direction::Invalid) { findTarget(); process.name() = String::format("Door to %s %s", _targetRoom == nullptr ? "" : _targetRoom->name().c_str(), _targetObject == nullptr ? "" : _targetObject->name().c_str()); } DoorTask(Process &process, Serializer &s) : Task(process) , _player(g_engine->player()) { DoorTask::syncGame(s); } TaskReturn run() override { TASK_BEGIN; if (_targetRoom == nullptr || _targetObject == nullptr) return TaskReturn::finish(1); _musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore()); if (g_engine->sounds().musicID() != _targetRoom->musicID()) g_engine->sounds().fadeMusic(); TASK_WAIT(1, fade(process(), FadeType::ToBlack, 0, 1, 500, EasingType::Out, -5)); _player.changeRoom(_targetRoom->name(), true); //-V779 if (_targetRoom->fixedCameraOnEntering()) g_engine->camera().setPosition(as2D(_targetObject->interactionPoint())); else { _character->room() = _targetRoom; _character->setPosition(_targetObject->interactionPoint()); _character->stopWalking(_targetDirection); g_engine->camera().setFollow(_character, true); } g_engine->sounds().setMusicToRoom(_targetRoom->musicID()); _musicLock.release(); if (g_engine->script().createProcess(_character->kind(), "ENTRAR_" + _targetRoom->name(), ScriptFlags::AllowMissing)) TASK_YIELD(2); else TASK_WAIT(3, fade(process(), FadeType::ToBlack, 1, 0, 500, EasingType::Out, -5)); TASK_END; } void debugPrint() override { g_engine->console().debugPrintf("%s\n", process().name().c_str()); } void syncGame(Serializer &s) override { assert(s.isSaving() || (_lock.isReleased() && _musicLock.isReleased())); Task::syncGame(s); syncObjectAsString(s, _sourceDoor); syncObjectAsString(s, _character); bool hasMusicLock = !_musicLock.isReleased(); s.syncAsByte(hasMusicLock); if (s.isLoading() && hasMusicLock) _musicLock = FakeLock("door-music", g_engine->sounds().musicSemaphore()); _lock = FakeLock("door", _character->semaphore()); findTarget(); } const char *taskName() const override; private: void findTarget() { _targetRoom = g_engine->world().getRoomByName(_sourceDoor->targetRoom().c_str()); if (_targetRoom == nullptr) { g_engine->game().unknownDoorTargetRoom(_sourceDoor->targetRoom()); return; } _targetObject = dynamic_cast(_targetRoom->getObjectByName(_sourceDoor->targetObject().c_str())); if (_targetObject == nullptr) { g_engine->game().unknownDoorTargetDoor(_sourceDoor->targetRoom(), _sourceDoor->targetObject()); return; } _targetDirection = _sourceDoor->characterDirection(); } FakeLock _lock, _musicLock; const Door *_sourceDoor = nullptr; const InteractableObject *_targetObject = nullptr; Direction _targetDirection = {}; Room *_targetRoom = nullptr; MainCharacter *_character = nullptr; Player &_player; }; DECLARE_TASK(DoorTask) void Player::triggerDoor(const Door *door) { _heldItem = nullptr; if (g_engine->game().shouldTriggerDoor(door)) { FakeLock lock("door", _activeCharacter->semaphore()); g_engine->scheduler().createProcess(activeCharacterKind(), door, move(lock)); } } // the last dialog character mechanic seems like a hack in the original engine // all talking characters (see SayText kernel call) are added to a fixed-size // rolling queue and stopped upon killProcesses void Player::addLastDialogCharacter(Character *character) { auto lastDialogCharactersEnd = _lastDialogCharacters + kMaxLastDialogCharacters; if (Common::find(_lastDialogCharacters, lastDialogCharactersEnd, character) != lastDialogCharactersEnd) return; _lastDialogCharacters[_nextLastDialogCharacter++] = character; _nextLastDialogCharacter %= kMaxLastDialogCharacters; } void Player::stopLastDialogCharacters() { // originally only the isTalking flag is reset, but this seems a bit safer so unless we find a bug for (int i = 0; i < kMaxLastDialogCharacters; i++) { auto character = _lastDialogCharacters[i]; if (character != nullptr) character->resetTalking(); } } void Player::setActiveCharacter(MainCharacterKind kind) { scumm_assert(kind == MainCharacterKind::Mortadelo || kind == MainCharacterKind::Filemon); _activeCharacter = &g_engine->world().getMainCharacterByKind(kind); } bool Player::isAllowedToOpenMenu() { return isGameLoaded() && !g_engine->menu().isOpen() && g_engine->sounds().musicSemaphore().isReleased() && !g_engine->script().variable("prohibirESC") && !_isInTemporaryRoom; // we cannot reliably store this state across multiple room changes } void Player::syncGame(Serializer &s) { auto characterKind = activeCharacterKind(); syncEnum(s, characterKind); switch (characterKind) { case MainCharacterKind::None: _activeCharacter = nullptr; break; case MainCharacterKind::Mortadelo: case MainCharacterKind::Filemon: _activeCharacter = &g_engine->world().getMainCharacterByKind(characterKind); break; default: error("Invalid character kind in savestate: %d", (int)characterKind); } FakeSemaphore::sync(s, _semaphore); String roomName; if (s.isSaving()) { roomName = g_engine->menu().isOpen() ? g_engine->menu().previousRoom()->name() // save from in-game menu : _roomBeforeInventory != nullptr ? _roomBeforeInventory->name() // save from ScummVM while in inventory : currentRoom()->name(); // save from ScumnmVM global menu or autosave in normal gameplay } s.syncString(roomName); if (s.isLoading()) { _selectedObject = nullptr; _pressedObject = nullptr; _heldItem = nullptr; _nextLastDialogCharacter = 0; _isGameLoaded = true; _roomBeforeInventory = nullptr; _isInTemporaryRoom = false; fill(_lastDialogCharacters, _lastDialogCharacters + kMaxLastDialogCharacters, nullptr); changeRoom(roomName, true); } } }