/* 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 "twine/twine.h" #include "common/config-manager.h" #include "common/debug.h" #include "common/error.h" #include "common/savefile.h" #include "common/scummsys.h" #include "common/str.h" #include "common/stream.h" #include "common/system.h" #include "common/text-to-speech.h" #include "common/textconsole.h" #include "engines/metaengine.h" #include "engines/util.h" #include "graphics/cursorman.h" #include "graphics/font.h" #include "graphics/fontman.h" #include "graphics/managed_surface.h" #include "graphics/pixelformat.h" #include "graphics/surface.h" #include "twine/audio/music.h" #include "twine/audio/sound.h" #include "twine/debugger/console.h" #include "twine/debugger/debug_state.h" #include "twine/detection.h" #include "twine/holomap_v1.h" #include "twine/holomap_v2.h" #include "twine/input.h" #include "twine/menu/interface.h" #include "twine/menu/menu.h" #include "twine/menu/menuoptions.h" #include "twine/movies.h" #include "twine/renderer/redraw.h" #include "twine/renderer/renderer.h" #include "twine/renderer/screens.h" #include "twine/resources/hqr.h" #include "twine/resources/resources.h" #include "twine/scene/actor.h" #include "twine/scene/animations.h" #include "twine/scene/buggy.h" #include "twine/scene/collision.h" #include "twine/scene/dart.h" #include "twine/scene/extra.h" #include "twine/scene/gamestate.h" #include "twine/scene/grid.h" #include "twine/scene/movements.h" #include "twine/scene/rain.h" #include "twine/scene/scene.h" #include "twine/scene/wagon.h" #include "twine/script/script_life_v1.h" #include "twine/script/script_life_v2.h" #include "twine/script/script_move_v1.h" #include "twine/script/script_move_v2.h" #include "twine/shared.h" #include "twine/slideshow.h" #include "twine/text.h" #ifdef USE_IMGUI #include "twine/debugger/debugtools.h" #endif namespace TwinE { ScopedCursor::ScopedCursor(TwinEEngine *engine) : _engine(engine) { _engine->pushMouseCursorVisible(); } ScopedCursor::~ScopedCursor() { _engine->popMouseCursorVisible(); _engine->_input->resetLastHoveredMousePosition(); } FrameMarker::FrameMarker(TwinEEngine *engine, uint32 fps) : _engine(engine), _fps(fps) { _start = g_system->getMillis(); ++_engine->_frameCounter; } FrameMarker::~FrameMarker() { _engine->_frontVideoBuffer.update(); if (_fps == 0) { return; } const uint32 end = g_system->getMillis(); const uint32 frameTime = end - _start; const uint32 maxDelay = 1000 / _fps; const int32 waitMillis = (int32)maxDelay - (int32)frameTime; _engine->_debugState->addFrameData(frameTime, waitMillis, maxDelay); if (waitMillis < 0) { debug(5, "Frame took longer than the max allowed time: %u (max is %u)", frameTime, maxDelay); return; } g_system->delayMillis((uint)waitMillis); } TwineScreen::TwineScreen(TwinEEngine *engine) : _engine(engine) { } void TwineScreen::update() { if (_lastFrame == _engine->_frameCounter) { return; } _lastFrame = _engine->_frameCounter; if (_engine->_redraw->_flagMCGA) { markAllDirty(); Graphics::ManagedSurface zoomWorkVideoBuffer; zoomWorkVideoBuffer.copyFrom(_engine->_workVideoBuffer); const int maxW = zoomWorkVideoBuffer.w; const int maxH = zoomWorkVideoBuffer.h; const int left = CLIP(_engine->_redraw->_sceneryViewX - maxW / 4, 0, maxW / 2); const int top = CLIP(_engine->_redraw->_sceneryViewY - maxH / 4, 0, maxH / 2); const Common::Rect srcRect(left, top, left + maxW / 2, top + maxH / 2); const Common::Rect &destRect = zoomWorkVideoBuffer.getBounds(); zoomWorkVideoBuffer.blitFrom(*this, srcRect, destRect); blitFrom(zoomWorkVideoBuffer); // TODO: we need to redraw everything because we just modified the screen buffer itself _engine->_redraw->_firstTime = true; } Super::update(); } int32 boundRuleThree(int32 val1, int32 val2, int32 nbstep, int32 step) { // BoundRegleTrois if (step <= 0) { return val1; } if (step >= nbstep) { return val2; } return val1 + (((val2 - val1) * step) / nbstep); } int32 ruleThree32(int32 val1, int32 val2, int32 nbstep, int32 step) { // RegleTrois32 if (nbstep < 0) { return val2; } return (((val2 - val1) * step) / nbstep) + val1; } TwinEEngine::TwinEEngine(OSystem *system, Common::Language language, uint32 flags, Common::Platform platform, TwineGameType gameType) : Engine(system), _gameType(gameType), _gameLang(language), _frontVideoBuffer(this), _gameFlags(flags), _platform(platform), _rnd("twine") { // Add default file directories const Common::FSNode gameDataDir(ConfMan.getPath("path")); SearchMan.addSubDirectoryMatching(gameDataDir, "fla"); SearchMan.addSubDirectoryMatching(gameDataDir, "vox"); if (isLBA2()) { SearchMan.addSubDirectoryMatching(gameDataDir, "video"); SearchMan.addSubDirectoryMatching(gameDataDir, "music"); } if (isLba1Classic()) { SearchMan.addSubDirectoryMatching(gameDataDir, "common"); SearchMan.addSubDirectoryMatching(gameDataDir, "commonclassic"); SearchMan.addSubDirectoryMatching(gameDataDir, "common/fla"); SearchMan.addSubDirectoryMatching(gameDataDir, "common/vox"); SearchMan.addSubDirectoryMatching(gameDataDir, "common/music"); SearchMan.addSubDirectoryMatching(gameDataDir, "common/midi"); SearchMan.addSubDirectoryMatching(gameDataDir, "commonclassic/images"); SearchMan.addSubDirectoryMatching(gameDataDir, "commonclassic/voices/de_voice"); SearchMan.addSubDirectoryMatching(gameDataDir, "commonclassic/voices/en_voice"); SearchMan.addSubDirectoryMatching(gameDataDir, "commonclassic/voices/fr_voice"); } if (isDotEmuEnhanced()) { SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/hqr"); SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/fla"); SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/vox/de_voice"); SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/vox/en_voice"); SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/vox/fr_voice"); SearchMan.addSubDirectoryMatching(gameDataDir, "resources"); #ifdef USE_MAD SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/music"); SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/midi_mp3"); #endif #ifdef USE_VORBIS SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/music/ogg"); SearchMan.addSubDirectoryMatching(gameDataDir, "resources/lba_files/midi_mp3/ogg"); #endif } if (isLBA2()) { LBAAngles::lba2(); } else { LBAAngles::lba1(); } _actor = new Actor(this); _animations = new Animations(this); _collision = new Collision(this); _extra = new Extra(this); _gameState = new GameState(this); _grid = new Grid(this); _movements = new Movements(this); _interface = new Interface(this); _menu = new Menu(this); _movie = new Movies(this); _menuOptions = new MenuOptions(this); _music = new Music(this); _redraw = new Redraw(this); _renderer = new Renderer(this); _resources = new Resources(this); _scene = new Scene(this); _screens = new Screens(this); if (isLBA1()) { _scriptLife = new ScriptLifeV1(this); _scriptMove = new ScriptMoveV1(this); _buggy = nullptr; _dart = nullptr; _rain = nullptr; _wagon = nullptr; _holomap = new HolomapV1(this); } else { _scriptLife = new ScriptLifeV2(this); _scriptMove = new ScriptMoveV2(this); _buggy = new Buggy(this); _dart = new Dart(this); _rain = new Rain(this); _wagon = new Wagon(this); _holomap = new HolomapV2(this); } _sound = new Sound(this); _text = new Text(this); _input = new Input(this); _debugState = new DebugState(this); setDebugger(new TwinEConsole(this)); } TwinEEngine::~TwinEEngine() { ConfMan.flushToDisk(); delete _actor; delete _animations; delete _collision; delete _extra; delete _gameState; delete _grid; delete _movements; delete _interface; delete _menu; delete _movie; delete _menuOptions; delete _music; delete _redraw; delete _renderer; delete _resources; delete _scene; delete _screens; delete _scriptLife; delete _scriptMove; delete _buggy; delete _dart; delete _rain; delete _wagon; delete _holomap; delete _sound; delete _text; delete _input; delete _debugState; } void TwinEEngine::pushMouseCursorVisible() { ++_mouseCursorState; if (!_cfgfile.Mouse) { return; } if (_mouseCursorState == 1) { CursorMan.showMouse(_cfgfile.Mouse); } } void TwinEEngine::popMouseCursorVisible() { --_mouseCursorState; if (_mouseCursorState == 0) { CursorMan.showMouse(false); } } Common::Error TwinEEngine::run() { #ifdef USE_IMGUI ImGuiCallbacks callbacks; bool drawImGui = debugChannelSet(-1, kDebugImGui); callbacks.init = TwinE::onImGuiInit; callbacks.render = drawImGui ? TwinE::onImGuiRender : nullptr; callbacks.cleanup = TwinE::onImGuiCleanup; _system->setImGuiCallbacks(callbacks); #endif debug("Based on TwinEngine v0.2.2"); debug("(c) 2002 The TwinEngine team."); debug("(c) 2020-2022 The ScummVM team."); debug("Refer to the credits for further details."); debug("The original Little Big Adventure game is:"); debug("(c) 1994 by Adeline Software International, All Rights Reserved."); ConfMan.registerDefault("usehighres", false); const Common::String &gameTarget = ConfMan.getActiveDomainName(); AchMan.setActiveDomain(getMetaEngine()->getAchievementsInfo(gameTarget)); syncSoundSettings(); int32 w = originalWidth(); int32 h = originalHeight(); const bool highRes = ConfMan.getBool("usehighres"); if (highRes) { w = 1024; h = 768; } initGraphics(w, h); allocVideoMemory(w, h); if (isLBASlideShow()) { playSlideShow(this); return Common::kNoError; } _renderer->init(w, h); _grid->init(w, h); initAll(); introduction(); _sound->stopSamples(); saveFrontBuffer(); _menu->init(); _holomap->loadLocations(); if (ConfMan.hasKey("save_slot")) { const int saveSlot = ConfMan.getInt("save_slot"); if (saveSlot >= 0 && saveSlot <= 999) { Common::Error state = loadGameState(saveSlot); if (state.getCode() != Common::kNoError) { #ifdef USE_IMGUI _system->setImGuiCallbacks(ImGuiCallbacks()); #endif return state; } } } if (ConfMan.hasKey("boot_param")) { const int sceneIndex = ConfMan.getInt("boot_param"); if (sceneIndex < 0 || sceneIndex >= LBA1SceneId::SceneIdMax) { warning("Scene index out of bounds"); } else { debug("Boot parameter: %i", sceneIndex); _gameState->initEngineVars(); _text->normalWinDial(); _text->_flagMessageShade = true; _text->_renderTextTriangle = false; _scene->_newCube = sceneIndex; _scene->_flagChgCube = ScenePositionType::kScene; _state = EngineState::GameLoop; } } bool quitGame = false; while (!quitGame && !shouldQuit()) { readKeys(); switch (_state) { case EngineState::QuitGame: { quitGame = true; break; } case EngineState::LoadedGame: debug("Loaded game"); if (_scene->_sceneStart.x == -1) { _scene->_flagChgCube = ScenePositionType::kNoPosition; } _text->_renderTextTriangle = false; _text->normalWinDial(); _text->_flagMessageShade = true; _state = EngineState::GameLoop; break; case EngineState::GameLoop: if (mainLoop()) { _menuOptions->showCredits(); _menuOptions->showEndSequence(); } _state = EngineState::Menu; break; case EngineState::Menu: #if 0 // this will enter the game and execute the commands in the file "debug" _gameState->initEngineVars(); _text->textClipSmall(); _text->_drawTextBoxBackground = true; _text->_renderTextTriangle = false; if (!((TwinEConsole*)getDebugger())->exec("debug")) { debug("Failed to execute debug file before entering the scene"); } gameEngineLoop(); #else _state = _menu->run(); #endif break; } #ifdef USE_IMGUI // For performance reasons, disable the renderer callback if the ImGui debug flag isn't set if (debugChannelSet(-1, kDebugImGui) != drawImGui) { drawImGui = !drawImGui; callbacks.render = drawImGui ? TwinE::onImGuiRender : nullptr; _system->setImGuiCallbacks(callbacks); } #endif } ConfMan.setBool("combatauto", _actor->_combatAuto); ConfMan.setInt("shadow", _cfgfile.ShadowMode); ConfMan.setBool("scezoom", _cfgfile.SceZoom); ConfMan.setInt("polygondetails", _cfgfile.PolygonDetails); _sound->stopSamples(); _music->stopMusic(); #ifdef USE_IMGUI _system->setImGuiCallbacks(ImGuiCallbacks()); #endif return Common::kNoError; } bool TwinEEngine::hasFeature(EngineFeature f) const { switch (f) { case EngineFeature::kSupportsReturnToLauncher: case EngineFeature::kSupportsLoadingDuringRuntime: case EngineFeature::kSupportsSavingDuringRuntime: case EngineFeature::kSupportsChangingOptionsDuringRuntime: return true; default: break; } return false; } SaveStateList TwinEEngine::getSaveSlots() const { return getMetaEngine()->listSaves(_targetName.c_str()); } void TwinEEngine::wipeSaveSlot(int slot) { Common::SaveFileManager *saveFileMan = getSaveFileManager(); const Common::String &saveFile = getSaveStateName(slot); saveFileMan->removeSavefile(saveFile); } bool TwinEEngine::canSaveGameStateCurrently(Common::U32String *msg) { return _scene->isGameRunning(); } Common::Error TwinEEngine::loadGameStream(Common::SeekableReadStream *stream) { debug("load game stream"); if (!_gameState->loadGame(stream)) { return Common::Error(Common::kReadingFailed); } _state = EngineState::LoadedGame; return Common::Error(Common::kNoError); } Common::Error TwinEEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) { if (!_gameState->saveGame(stream)) { return Common::Error(Common::kWritingFailed); } return Common::Error(Common::kNoError); } void TwinEEngine::autoSave() { debug("Autosave %s", _gameState->_sceneName); saveGameState(getAutosaveSlot(), _gameState->_sceneName, true); } void TwinEEngine::allocVideoMemory(int32 w, int32 h) { const Graphics::PixelFormat format = Graphics::PixelFormat::createFormatCLUT8(); // original resolution of the game _imageBuffer.create(originalWidth(), originalHeight(), format); _workVideoBuffer.create(w, h, format); _frontVideoBuffer.create(w, h, format); } static int getLanguageTypeIndex(const char *languageName) { char buffer[256]; Common::strlcpy(buffer, languageName, sizeof(buffer)); char *ptr = strchr(buffer, ' '); if (ptr != nullptr) { *ptr = '\0'; } const int32 length = ARRAYSIZE(ListLanguage); for (int32 i = 0; i < length; i++) { if (!strcmp(ListLanguage[i].name, buffer)) { return i; } } debug("Failed to detect language %s - falling back to english", languageName); // select english for the fan translations return 0; // English } #define ConfGetOrDefault(key, defaultVal) (ConfMan.hasKey(key) ? ConfMan.get(key) : Common::String(defaultVal)) #define ConfGetIntOrDefault(key, defaultVal) (ConfMan.hasKey(key) ? atoi(ConfMan.get(key).c_str()) : (defaultVal)) #define ConfGetBoolOrDefault(key, defaultVal) (ConfMan.hasKey(key) ? ConfMan.get(key) == "true" || atoi(ConfMan.get(key).c_str()) == 1 : (defaultVal)) void TwinEEngine::initConfigurations() { // TODO: use existing entries for some of the settings - like volume and so on. ConfMan.registerDefault("wallcollision", false); const char *lng = Common::getLanguageDescription(_gameLang); _cfgfile._languageId = getLanguageTypeIndex(lng); ConfMan.registerDefault("audio_language", ListLanguage[_cfgfile._languageId].voice); _cfgfile.FlagDisplayText = ConfGetBoolOrDefault("displaytext", true); // TODO: use subtitles const Common::String midiType = ConfGetOrDefault("miditype", "auto"); if (midiType == "None") { _cfgfile.MidiType = MIDIFILE_NONE; } else { Common::File midiHqr; if (midiHqr.exists(Resources::HQR_MIDI_MI_WIN_FILE)) { _cfgfile.MidiType = MIDIFILE_WIN; debug("Use %s for midi", Resources::HQR_MIDI_MI_WIN_FILE); } else if (midiHqr.exists(Resources::HQR_MIDI_MI_DOS_FILE)) { _cfgfile.MidiType = MIDIFILE_DOS; debug("Use %s for midi", Resources::HQR_MIDI_MI_DOS_FILE); } else { _cfgfile.MidiType = MIDIFILE_NONE; debug("Could not find midi hqr file"); } } if (_gameFlags & TwineFeatureFlags::TF_VERSION_EUROPE) { _cfgfile.Version = EUROPE_VERSION; } else if (_gameFlags & TwineFeatureFlags::TF_VERSION_USA) { _cfgfile.Version = USA_VERSION; } else if (_gameFlags & TwineFeatureFlags::TF_VERSION_CUSTOM) { _cfgfile.Version = MODIFICATION_VERSION; } if (_gameFlags & TwineFeatureFlags::TF_USE_GIF) { _cfgfile.Movie = CONF_MOVIE_FLAGIF; } _cfgfile.Sound = ConfGetBoolOrDefault("sound", true); _cfgfile.Fps = ConfGetIntOrDefault("fps", DEFAULT_FRAMES_PER_SECOND); _cfgfile.Mouse = ConfGetBoolOrDefault("mouse", true); _cfgfile.UseAutoSaving = ConfGetBoolOrDefault("useautosaving", false); _cfgfile.WallCollision = ConfGetBoolOrDefault("wallcollision", false); _actor->_combatAuto = ConfGetBoolOrDefault("combatauto", true); _cfgfile.ShadowMode = ConfGetIntOrDefault("shadow", 2); _cfgfile.SceZoom = ConfGetBoolOrDefault("scezoom", false); _cfgfile.PolygonDetails = ConfGetIntOrDefault("polygondetails", 2); Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager(); if (ttsMan != nullptr) ttsMan->enable(ConfGetBoolOrDefault("tts_narrator", false)); debug(1, "Sound: %s", (_cfgfile.Sound ? "true" : "false")); debug(1, "Movie: %i", _cfgfile.Movie); debug(1, "Fps: %i", _cfgfile.Fps); debug(1, "UseAutoSaving: %s", (_cfgfile.UseAutoSaving ? "true" : "false")); debug(1, "WallCollision: %s", (_cfgfile.WallCollision ? "true" : "false")); debug(1, "AutoAggressive: %s", (_actor->_combatAuto ? "true" : "false")); debug(1, "ShadowMode: %i", _cfgfile.ShadowMode); debug(1, "PolygonDetails: %i", _cfgfile.PolygonDetails); debug(1, "SceZoom: %s", (_cfgfile.SceZoom ? "true" : "false")); } void TwinEEngine::queueMovie(const char *filename) { _queuedFlaMovie = filename; } void TwinEEngine::adjustScreenMax(Common::Rect &rect, int16 x, int16 y) { if (x < rect.left) { rect.left = x; } if (x > rect.right) { rect.right = x; } if (y < rect.top) { rect.top = y; } if (y > rect.bottom) { rect.bottom = y; } } void TwinEEngine::clearScreenMinMax(Common::Rect &rect) { rect.left = 0x7D00; // SCENE_SIZE_MAX rect.right = -0x7D00; rect.top = 0x7D00; rect.bottom = -0x7D00; } void TwinEEngine::introduction() { _input->enableKeyMap(cutsceneKeyMapId); // Display company logo bool abort = false; if (isLBA2()) { // abort |= _screens->loadImageDelay(_resources->activisionLogo(), 7); abort |= _screens->loadImageDelay(_resources->eaLogo(), 7); } if (isLba1Classic()) { abort |= _screens->loadBitmapDelay("Logo2Point21_640_480_256.bmp", 3); if (!abort) { abort |= _screens->adelineLogo(); } if (!abort) { abort |= _screens->loadBitmapDelay("TLBA1C_640_480_256.bmp", 3); } } else { if (isDotEmuEnhanced()) { abort |= _screens->loadBitmapDelay("splash_1.png", 3); } abort |= _screens->adelineLogo(); if (isLBA1()) { // verify game version screens if (!abort && _cfgfile.Version == EUROPE_VERSION) { // Little Big Adventure screen abort |= _screens->loadImageDelay(_resources->lbaLogo(), 3); if (!abort && !isDotEmuEnhanced()) { // Electronic Arts Logo abort |= _screens->loadImageDelay(_resources->eaLogo(), 2); } } else if (!abort && _cfgfile.Version == USA_VERSION) { // Relentless screen abort |= _screens->loadImageDelay(_resources->relentLogo(), 3); if (!abort && !isDotEmuEnhanced()) { // Electronic Arts Logo abort |= _screens->loadImageDelay(_resources->eaLogo(), 2); } } else if (!abort && _cfgfile.Version == MODIFICATION_VERSION) { // Modification screen abort |= _screens->loadImageDelay(_resources->relentLogo(), 2); } } } if (isLBA1()) { _movie->playMovie(FLA_DRAGON3); } else { _movie->playMovie("INTRO"); } } void TwinEEngine::extInitMcga() { _redraw->_flagMCGA = true; if (_screens->_flagPalettePcx) setPalette(_screens->_palettePcx); else setPalette(_screens->_ptrPal); } void TwinEEngine::extInitSvga() { _redraw->_flagMCGA = false; if (_screens->_flagPalettePcx) setPalette(_screens->_palettePcx); else setPalette(_screens->_ptrPal); } void TwinEEngine::testRestoreModeSVGA(bool redraw) { if (_redraw->_flagMCGA) { extInitSvga(); if (redraw) { _redraw->drawScene(redraw); } } } void TwinEEngine::initAll() { _scene->_sceneHero = _scene->getActor(OWN_ACTOR_SCENE_INDEX); // Set clip to fullscreen by default, allows main menu to render properly after load _interface->unsetClip(); // getting configuration file initConfigurations(); _resources->initResources(); extInitSvga(); _screens->clearScreen(); // Check if LBA CD-Rom is on drive _music->initCdrom(); } int TwinEEngine::getRandomNumber(uint max) { if (max == 0) { return 0; } return _rnd.getRandomNumber(max - 1); } void TwinEEngine::saveTimer(bool pause) { if (_isTimeFreezed == 0) { _saveFreezedTime = timerRef; debugC(3, kDebugLevels::kDebugTimers, "freezeTime: timer %i", timerRef); if (pause) _pauseToken = pauseEngine(); } _isTimeFreezed++; debugC(3, kDebugLevels::kDebugTimers, "freezeTime: %i", _isTimeFreezed); } void TwinEEngine::restoreTimer() { --_isTimeFreezed; debugC(3, kDebugLevels::kDebugTimers, "unfreezeTime: %i", _isTimeFreezed); if (_isTimeFreezed == 0) { timerRef = _saveFreezedTime; debugC(3, kDebugLevels::kDebugTimers, "unfreezeTime: time %i", timerRef); if (_pauseToken.isActive()) { _pauseToken.clear(); } } } void TwinEEngine::processActorSamplePosition(int32 actorIdx) { const ActorStruct *actor = _scene->getActor(actorIdx); const int32 channelIdx = _sound->getActorChannel(actorIdx); _sound->setChannelPosition(channelIdx, actor->posObj()); } void TwinEEngine::processBookOfBu() { _screens->fadeToBlack(_screens->_ptrPal); _screens->loadImage(TwineImage(Resources::HQR_RESS_FILE, 15, 16)); _screens->fadeToPal(_screens->_palettePcx); _text->initDial(TextBankId::Inventory_Intro_and_Holomap); _text->_flagMessageShade = false; _text->bigWinDial(); _text->setFontCrossColor(COLOR_WHITE); const bool tmpFlagDisplayText = _cfgfile.FlagDisplayText; _cfgfile.FlagDisplayText = true; _text->drawTextProgressive(TextId::kBookOfBu); _cfgfile.FlagDisplayText = tmpFlagDisplayText; _text->normalWinDial(); _text->_flagMessageShade = true; _text->initSceneTextBank(); _screens->fadeToBlack(_screens->_palettePcx); _screens->clearScreen(); _screens->_flagFade = true; } void TwinEEngine::processBonusList() { _text->initDial(TextBankId::Inventory_Intro_and_Holomap); _text->bigWinDial(); _text->setFontCrossColor(COLOR_WHITE); const bool tmpFlagDisplayText = _cfgfile.FlagDisplayText; _cfgfile.FlagDisplayText = true; _text->drawTextProgressive(TextId::kBonusList); _cfgfile.FlagDisplayText = tmpFlagDisplayText; _text->normalWinDial(); _text->initSceneTextBank(); } void TwinEEngine::processInventoryAction() { saveTimer(false); testRestoreModeSVGA(true) ; _menu->inventory(); switch (_loopInventoryItem) { case kiHolomap: _holomap->holoMap(); _screens->_flagFade = true; break; case kiMagicBall: if (_gameState->_weapon) { _actor->initBody(BodyType::btNormal, OWN_ACTOR_SCENE_INDEX); } _gameState->_weapon = false; break; case kiUseSabre: if (_scene->_sceneHero->_genBody != BodyType::btSabre) { if (_actor->_heroBehaviour == HeroBehaviourType::kProtoPack) { _actor->setBehaviour(HeroBehaviourType::kNormal); } _actor->initBody(BodyType::btSabre, OWN_ACTOR_SCENE_INDEX); _animations->initAnim(AnimationTypes::kSabreUnknown, AnimType::kAnimationThen, AnimationTypes::kStanding, OWN_ACTOR_SCENE_INDEX); _gameState->_weapon = true; } break; case kiBookOfBu: { processBookOfBu(); break; } case kiProtoPack: if (_gameState->hasItem(InventoryItems::kSendellsMedallion)) { _scene->_sceneHero->_genBody = BodyType::btNormal; } else { _scene->_sceneHero->_genBody = BodyType::btTunic; } if (_actor->_heroBehaviour == HeroBehaviourType::kProtoPack) { _actor->setBehaviour(HeroBehaviourType::kNormal); } else { _actor->setBehaviour(HeroBehaviourType::kProtoPack); } break; case kiPenguin: { ActorStruct *penguin = _scene->getActor(_scene->_mecaPenguinIdx); const IVec2 &destPos = _renderer->rotate(0, 800, _scene->_sceneHero->_beta); penguin->_posObj = _scene->_sceneHero->posObj(); penguin->_posObj.x += destPos.x; penguin->_posObj.z += destPos.y; // TODO: HACK for https://bugs.scummvm.org/ticket/13731 // The movement of the meca penguin is different from dos version // the problem is that the value set to 1 even if the penguin is not yet spawned // this might either be a problem with initObject() not being called for the penguin // or some other flaw that doesn't ignore the penguin until spawned penguin->_workFlags.bIsFalling = 0; penguin->_beta = _scene->_sceneHero->_beta; debug("penguin angle: %i", penguin->_beta); if (_collision->checkValidObjPos(_scene->_mecaPenguinIdx)) { penguin->setLife(getMaxLife()); penguin->_genBody = BodyType::btNone; _actor->initBody(BodyType::btNormal, _scene->_mecaPenguinIdx); penguin->_workFlags.bIsDead = 0; penguin->setCollision(ShapeType::kNone); _movements->initRealAngleConst(penguin->_beta, penguin->_beta, penguin->_srot, &penguin->realAngle); _gameState->removeItem(InventoryItems::kiPenguin); penguin->_delayInMillis = timerRef + toSeconds(30); } break; } case kiBonusList: { restoreTimer(); _redraw->drawScene(true); saveTimer(false); processBonusList(); break; } case kiCloverLeaf: if (_scene->_sceneHero->_lifePoint < getMaxLife()) { if (_gameState->_inventoryNumLeafs > 0) { _scene->_sceneHero->setLife(getMaxLife()); _gameState->setMagicPoints(_gameState->_magicLevelIdx * 20); _gameState->addLeafs(-1); _redraw->addOverlay(OverlayType::koInventoryItem, InventoryItems::kiCloverLeaf, 0, 0, 0, OverlayPosType::koNormal, 3); } } break; } restoreTimer(); _redraw->drawScene(true); } int32 TwinEEngine::toSeconds(int x) const { if (isLBA1()) { return DEFAULT_HZ * x; } return x * 1000; } void TwinEEngine::processOptionsMenu() { saveTimer(false); testRestoreModeSVGA(true) ; _sound->pauseSamples(); _menu->inGameOptionsMenu(); _scene->playSceneMusic(); _sound->resumeSamples(); restoreTimer(); _redraw->drawScene(true); } bool TwinEEngine::runGameEngine() { // mainLoopInteration g_system->delayMillis(2); FrameMarker frame(this, 60); _input->enableKeyMap(mainKeyMapId); readKeys(); if (!_queuedFlaMovie.empty()) { _movie->playMovie(_queuedFlaMovie.c_str()); _queuedFlaMovie.clear(); } if (_scene->_newCube > -1) { if (!isMod() && isDemo() && isLBA1()) { // the demo only has these scenes if (_scene->_newCube != LBA1SceneId::Citadel_Island_Prison && _scene->_newCube != LBA1SceneId::Citadel_Island_outside_the_citadel && _scene->_newCube != LBA1SceneId::Citadel_Island_near_the_tavern) { _music->playMidiFile(6); return true; } } _scene->changeCube(); } _movements->update(); _debugState->update(); if (_menuOptions->flagCredits) { if (isLBA1()) { if (isCDROM()) { if (_music->getMusicCD() != 8) { _music->playCdTrack(8); } } else if (!_music->isMidiPlaying()) { _music->playMidiFile(9); } } if (_input->toggleAbortAction()) { return true; } } else if (!_screens->_flagFade) { // Process give up menu - Press ESC if (_input->toggleAbortAction() && _scene->_sceneHero->_lifePoint > 0 && _scene->_sceneHero->_body != -1 && !_scene->_sceneHero->_flags.bIsInvisible) { saveTimer(false); testRestoreModeSVGA(true) ; const int giveUp = _menu->quitMenu(); if (giveUp == kQuitEngine) { return false; } if (giveUp == 1) { restoreTimer(); _redraw->drawScene(true); _sceneLoopState = SceneLoopState::ReturnToMenu; return false; } restoreTimer(); _redraw->drawScene(true); } if (_input->toggleActionIfActive(TwinEActionType::OptionsMenu)) { processOptionsMenu(); } // inventory menu _loopInventoryItem = -1; if (_input->isActionActive(TwinEActionType::InventoryMenu) && _scene->_sceneHero->_body != -1 && _scene->_sceneHero->_move == ControlMode::kManual) { processInventoryAction(); } if (_input->toggleActionIfActive(TwinEActionType::ChangeBehaviourNormal)) { _actor->setBehaviour(HeroBehaviourType::kNormal); } else if (_input->toggleActionIfActive(TwinEActionType::ChangeBehaviourAthletic)) { _actor->setBehaviour(HeroBehaviourType::kAthletic); } else if (_input->toggleActionIfActive(TwinEActionType::ChangeBehaviourAggressive)) { _actor->setBehaviour(HeroBehaviourType::kAggressive); } else if (_input->toggleActionIfActive(TwinEActionType::ChangeBehaviourDiscreet)) { _actor->setBehaviour(HeroBehaviourType::kDiscrete); } // Process behaviour menu const bool behaviourMenu = _input->isActionActive(TwinEActionType::BehaviourMenu, false); if ((behaviourMenu || _input->isActionActive(TwinEActionType::QuickBehaviourNormal, false) || _input->isActionActive(TwinEActionType::QuickBehaviourAthletic, false) || _input->isActionActive(TwinEActionType::QuickBehaviourAggressive, false) || _input->isActionActive(TwinEActionType::QuickBehaviourDiscreet, false)) && _scene->_sceneHero->_body != -1 && _scene->_sceneHero->_move == ControlMode::kManual) { if (_input->isActionActive(TwinEActionType::QuickBehaviourNormal, false)) { _actor->_heroBehaviour = HeroBehaviourType::kNormal; } else if (_input->isActionActive(TwinEActionType::QuickBehaviourAthletic, false)) { _actor->_heroBehaviour = HeroBehaviourType::kAthletic; } else if (_input->isActionActive(TwinEActionType::QuickBehaviourAggressive, false)) { _actor->_heroBehaviour = HeroBehaviourType::kAggressive; } else if (_input->isActionActive(TwinEActionType::QuickBehaviourDiscreet, false)) { _actor->_heroBehaviour = HeroBehaviourType::kDiscrete; } saveTimer(false); testRestoreModeSVGA(true); _menu->processBehaviourMenu(behaviourMenu); restoreTimer(); _redraw->drawScene(true); } // use Proto-Pack if (_input->toggleActionIfActive(TwinEActionType::UseProtoPack) && _gameState->hasItem(InventoryItems::kiProtoPack)) { if (_gameState->hasItem(InventoryItems::kiBookOfBu)) { _scene->_sceneHero->_genBody = BodyType::btNormal; } else { _scene->_sceneHero->_genBody = BodyType::btTunic; } if (_actor->_heroBehaviour == HeroBehaviourType::kProtoPack) { _actor->setBehaviour(HeroBehaviourType::kNormal); } else { _actor->setBehaviour(HeroBehaviourType::kProtoPack); } } // Recenter Screen if (_input->toggleActionIfActive(TwinEActionType::RecenterScreenOnTwinsen) && !_cameraZone) { const ActorStruct *currentlyFollowedActor = _scene->getActor(_scene->_numObjFollow); _grid->centerOnActor(currentlyFollowedActor); } // Draw holomap if (_input->toggleActionIfActive(TwinEActionType::OpenHolomap) && _gameState->hasItem(InventoryItems::kiHolomap) && !_gameState->inventoryDisabled()) { testRestoreModeSVGA(true); saveTimer(false); _holomap->holoMap(); // unfreeze here - the redrawEngineActions is also doing a freeze // see https://bugs.scummvm.org/ticket/14808 restoreTimer(); _screens->_flagFade = true; _redraw->drawScene(true); } // Process Pause if (_input->toggleActionIfActive(TwinEActionType::Pause)) { saveTimer(true); const char *PauseString = "Pause"; _text->setFontColor(COLOR_WHITE); if (_redraw->_flagMCGA) { _text->drawText(_redraw->_sceneryViewX + 5, _redraw->_sceneryViewY, PauseString); } else { const int width = _text->sizeFont(PauseString); const int bottom = height() - _text->lineHeight; _text->drawText(5, bottom, PauseString); copyBlockPhys(5, bottom, 5 + width, bottom + _text->lineHeight); } do { FrameMarker frameWait(this); readKeys(); if (shouldQuit()) { break; } } while (!_input->toggleActionIfActive(TwinEActionType::Pause)); restoreTimer(); _redraw->drawScene(true); } if (_input->toggleActionIfActive(TwinEActionType::SceneryZoom)) { _redraw->_flagMCGA ^= true; if (_redraw->_flagMCGA) { extInitMcga(); } else { extInitSvga(); _redraw->_firstTime = true; } } } _stepFalling = _realFalling.getRealValueFromTime(timerRef); if (!_stepFalling) { _stepFalling = 1; } _movements->initRealValue(LBAAngles::ANGLE_0, -LBAAngles::ANGLE_90, LBAAngles::ANGLE_1, &_realFalling); _cameraZone = false; _scene->processEnvironmentSound(); // Reset HitBy state for (int32 a = 0; a < _scene->_nbObjets; a++) { _scene->getActor(a)->_hitBy = -1; } _extra->gereExtras(); for (int32 a = 0; a < _scene->_nbObjets; a++) { ActorStruct *actor = _scene->getActor(a); if (actor->_workFlags.bIsDead) { continue; } if (actor->_lifePoint == 0) { if (IS_HERO(a)) { _animations->initAnim(AnimationTypes::kLandDeath, AnimType::kAnimationSet, AnimationTypes::kStanding, OWN_ACTOR_SCENE_INDEX); actor->_move = ControlMode::kNoMove; #if 0 // TODO: enable me - found in the lba1 community release source code // Disable collisions on Twinsen to allow other objects to continue their tracks // while the death animation is playing actor->_flags.bObjFallable = 1; actor->_flags.bCheckZone = 0; actor->_flags.bComputeCollisionWithObj = 0; actor->_flags.bComputeCollisionWithBricks = 0; actor->_flags.bCanDrown = 1; actor->_workFlags.bIsHitting = 0; #endif } else { const uint16 pitchBend = 0x1000 + getRandomNumber(2000) - (2000 / 2); _sound->mixSample3D(Samples::Explode, pitchBend, 1, actor->posObj(), a); if (a == _scene->_mecaPenguinIdx) { _extra->extraExplo(actor->posObj()); } } if (!actor->_bonusParameter.givenNothing && (actor->_bonusParameter.cloverleaf || actor->_bonusParameter.kashes || actor->_bonusParameter.key || actor->_bonusParameter.lifepoints || actor->_bonusParameter.magicpoints)) { _actor->giveExtraBonus(a); } } _movements->doDir(a); actor->_oldPos = actor->posObj(); if (actor->_offsetTrack != -1) { _scriptMove->doTrack(a); } _animations->doAnim(a); if (actor->_flags.bCheckZone) { _scene->checkZoneSce(a); } if (actor->_offsetLife != -1) { _scriptLife->doLife(a); } if (_debugState->_playFoundItemAnimation) { _debugState->_playFoundItemAnimation = false; auto tmp = _scene->_sceneHero->_offsetLife; LifeScriptContext fakeCtx(0, _scene->_sceneHero); fakeCtx.stream.writeByte(InventoryItems::kiHolomap); fakeCtx.stream.seek(0); _scriptLife->lFOUND_OBJECT(this, fakeCtx); _scene->_sceneHero->_offsetLife = tmp; } processActorSamplePosition(a); if (_sceneLoopState != SceneLoopState::Continue) { return _sceneLoopState == SceneLoopState::Finished; } if (actor->_flags.bCanDrown) { const uint8 brickSound = _grid->worldCodeBrick(actor->_posObj.x, actor->_posObj.y - 1, actor->_posObj.z); actor->_brickSound = brickSound; if (brickSound == WATER_BRICK) { if (IS_HERO(a)) { // we are dying if we aren't using the protopack to fly over water if (_actor->_heroBehaviour != HeroBehaviourType::kProtoPack || actor->_genAnim != AnimationTypes::kForward) { if (!_actor->_cropBottomScreen) { _animations->initAnim(AnimationTypes::kDrawn, AnimType::kAnimationSet, AnimationTypes::kStanding, OWN_ACTOR_SCENE_INDEX); } const IVec3 &projPos = _renderer->projectPoint(actor->posObj() - _grid->_worldCube); actor->_move = ControlMode::kNoMove; actor->setLife(-1); _actor->_cropBottomScreen = projPos.y; actor->_flags.bNoShadow = 1; } } else { const uint16 pitchBend = 0x1000 + getRandomNumber(2000) - (2000 / 2); _sound->mixSample3D(Samples::Explode, pitchBend, 1, actor->posObj(), a); if (actor->_bonusParameter.cloverleaf || actor->_bonusParameter.kashes || actor->_bonusParameter.key || actor->_bonusParameter.lifepoints || actor->_bonusParameter.magicpoints) { if (!actor->_bonusParameter.givenNothing) { _actor->giveExtraBonus(a); } actor->setLife(0); } } } } if (actor->_lifePoint <= 0) { if (IS_HERO(a)) { if (actor->_workFlags.bAnimEnded) { if (_gameState->_inventoryNumLeafs > 0) { // use clover leaf automaticaly _scene->_sceneHero->_posObj = _scene->_sceneStart; _scene->_newCube = _scene->_numCube; _gameState->setMaxMagicPoints(); _grid->centerOnActor(_scene->_sceneHero); _scene->_flagChgCube = ScenePositionType::kReborn; _scene->_sceneHero->setLife(getMaxLife()); _redraw->_firstTime = true; _screens->_flagFade = true; _gameState->addLeafs(-1); _actor->_cropBottomScreen = 0; } else { // game over _gameState->setLeafBoxes(2); _gameState->setLeafs(1); _gameState->setMaxMagicPoints(); _actor->_heroBehaviour = _actor->_previousHeroBehaviour; actor->_beta = _actor->_previousHeroAngle; actor->setLife(getMaxLife()); if (_scene->_oldcube != _scene->_numCube) { _scene->_sceneStart.x = -1; _scene->_sceneStart.y = -1; _scene->_sceneStart.z = -1; _scene->_numCube = _scene->_oldcube; _scene->stopRunningGame(); } autoSave(); _gameState->processGameoverAnimation(); _sceneLoopState = SceneLoopState::ReturnToMenu; return false; } } } else { _actor->checkCarrier(a); actor->_workFlags.bIsDead = 1; actor->_body = -1; actor->_zoneSce = -1; } } if (_scene->_newCube != -1) { return false; } } _grid->centerScreenOnActor(); _redraw->drawScene(_redraw->_firstTime); // workaround to fix hero redraw after drowning if (_actor->_cropBottomScreen && _redraw->_firstTime) { _scene->_sceneHero->_flags.bIsInvisible = 1; _redraw->drawScene(true); _scene->_sceneHero->_flags.bIsInvisible = 0; } _scene->_newCube = SCENE_CEILING_GRID_FADE_1; _redraw->_firstTime = false; return false; } bool TwinEEngine::mainLoop() { _redraw->_firstTime = true; _screens->_flagFade = true; _movements->initRealValue(LBAAngles::ANGLE_0, -LBAAngles::ANGLE_90, LBAAngles::ANGLE_1, &_realFalling); while (_sceneLoopState == SceneLoopState::Continue) { if (runGameEngine()) { return true; } timerRef++; if (shouldQuit()) { break; } } return false; } bool TwinEEngine::delaySkip(uint32 time) { uint32 startTicks = _system->getMillis(); uint32 stopTicks = 0; do { FrameMarker frame(this); readKeys(); if (_input->toggleAbortAction()) { return true; } if (shouldQuit()) { return true; } stopTicks = _system->getMillis() - startTicks; //_lbaTime++; } while (stopTicks <= time); return false; } void TwinEEngine::saveFrontBuffer() { // CopyScreen(Log, Screen) _screens->copyScreen(_frontVideoBuffer, _workVideoBuffer); } void TwinEEngine::restoreFrontBuffer() { // CopyScreen _screens->copyScreen(_workVideoBuffer, _frontVideoBuffer); } void TwinEEngine::blitWorkToFront(const Common::Rect &rect) { _interface->blitBox(rect, _workVideoBuffer, _frontVideoBuffer); copyBlockPhys(rect); } void TwinEEngine::copyBlock(const Common::Rect &rect) { _interface->blitBox(rect, _frontVideoBuffer, _workVideoBuffer); } void TwinEEngine::setPalette(const Graphics::Palette &palette, uint startColor) { debugC(1, TwinE::kDebugPalette, "Change palette (%i colors, starting at %i)", (int)palette.size(), (int)startColor); _frontVideoBuffer.setPalette(palette, startColor); } void TwinEEngine::setPalette(uint startColor, uint numColors, const byte *palette) { if (numColors == 0 || palette == nullptr) { warning("Could not set palette"); return; } debugC(1, TwinE::kDebugPalette, "Change palette (%i colors, starting at %i)", (int)numColors, (int)startColor); _frontVideoBuffer.setPalette(palette, startColor, numColors); } void TwinEEngine::copyBlockPhys(const Common::Rect &rect) { copyBlockPhys(rect.left, rect.top, rect.right, rect.bottom); } void TwinEEngine::copyBlockPhys(int32 left, int32 top, int32 right, int32 bottom) { assert(left <= right); assert(top <= bottom); int32 width = right - left + 1; int32 height = bottom - top + 1; if (left + width > this->width()) { width = this->width() - left; } if (top + height > this->height()) { height = this->height() - top; } if (width <= 0 || height <= 0) { return; } _frontVideoBuffer.addDirtyRect(Common::Rect(left, top, right, bottom)); } void TwinEEngine::readKeys() { _input->readKeys(); } void TwinEEngine::drawText(int32 x, int32 y, const Common::String &text, bool center, bool bigFont, int width) { const Graphics::Font *font = FontMan.getFontByUsage(bigFont ? Graphics::FontManager::kBigGUIFont : Graphics::FontManager::kGUIFont); if (!font) { return; } font->drawString(&_frontVideoBuffer, text, x, y, width, _frontVideoBuffer.format.RGBToColor(255, 255, 255), center ? Graphics::kTextAlignCenter : Graphics::kTextAlignLeft, 0, true); } Common::Language TwinEEngine::getGameLang() const { return _gameLang; } const char *TwinEEngine::getGameId() const { if (isLBA1()) { return "lba"; } assert(isLBA2()); return "lba2"; } bool TwinEEngine::unlockAchievement(const Common::String &id) { return AchMan.setAchievement(id); } Common::Rect TwinEEngine::centerOnScreenX(int32 w, int32 y, int32 h) const { const int32 left = width() / 2 - w / 2; const int32 right = left + w; const int32 top = y; const int32 bottom = top + h; return Common::Rect(left, top, right, bottom); } Common::Rect TwinEEngine::centerOnScreen(int32 w, int32 h) const { const int32 left = width() / 2 - w / 2; const int32 right = left + w; const int32 top = height() / 2 - h / 2; const int32 bottom = top + h; return Common::Rect(left, top, right, bottom); } } // namespace TwinE