/* 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 . * */ /* * This code is based on the CRAB engine * * Copyright (c) Arvind Raja Yadav * * Licensed under MIT * */ #include "graphics/managed_surface.h" #include "graphics/screen.h" #include "crab/backInserter.h" #include "crab/game.h" #include "crab/LoadingScreen.h" #include "crab/input/cursor.h" #include "crab/rapidxml/rapidxml_print.hpp" namespace Crab { using namespace pyrodactyl::image; using namespace pyrodactyl::ui; using namespace pyrodactyl::input; //------------------------------------------------------------------------ // Purpose: Loading stuff //------------------------------------------------------------------------ void Game::startNewGame() { init(g_engine->_filePath->_modCur); loadLevel(_info.curLocID()); _info.ironMan(g_engine->_tempData->_ironman); _savefile._ironman = g_engine->_tempData->_filename; _clock.start(); _hud._pause.updateMode(_info.ironMan()); createSaveGame(SAVEGAME_EVENT); } void Game::loadGame() { init(g_engine->_filePath->_modCur); } void Game::init(const Common::Path &filename) { g_engine->_loadingScreen->dim(); g_engine->_eventStore->clear(); _gameOver.clear(false); _state = STATE_GAME; _savefile._autoSlot = false; _gem.init(); _info.Init(); XMLDoc conf(filename); if (conf.ready()) { rapidxml::xml_node *node = conf.doc()->first_node("config"); _info.load(node); Common::Path path; if (nodeValid("level", node)) { loadPath(path, "list", node->first_node("level")); g_engine->_filePath->loadLevel(path); } if (nodeValid("hud", node)) { loadPath(path, "layout", node->first_node("hud")); _hud.load(path, _level._talkNotify, _level._destMarker); } if (nodeValid("sprite", node)) { loadPath(path, "animation", node->first_node("sprite")); _level.loadMoves(path); loadPath(path, "constant", node->first_node("sprite")); _level.loadConst(path); } if (nodeValid("event", node)) { _gem.load(node->first_node("event"), _popDefault); loadPath(path, "store", node->first_node("event")); g_engine->_eventStore->load(path); } if (nodeValid("map", node)) { loadPath(path, "layout", node->first_node("map")); _map.load(path, _info); } if (nodeValid("save", node)) _savefile.load(node->first_node("save")); if (nodeValid("debug", node)) { loadPath(path, "layout", node->first_node("debug")); _debugConsole.load(path); } } _isInited = true; } bool Game::loadLevel(const Common::String &id, int playerX, int playerY) { if (g_engine->_filePath->_level.contains(id)) { g_engine->_loadingScreen->draw(); // Load the assets local to this level // If the filename is same as the previous one, skip loading if (g_engine->_filePath->_currentR != g_engine->_filePath->_level[id]._asset) { g_engine->_filePath->_currentR = g_engine->_filePath->_level[id]._asset; g_engine->_imageManager->loadMap(g_engine->_filePath->_level[id]._asset); } // Load the level itself _level._pop = _popDefault; _level.load(g_engine->_filePath->_level[id]._layout, _info, _gameOver, playerX, playerY); // Set the current location _info.curLocID(id); _info.curLocName(g_engine->_filePath->_level[id]._name); _map._playerPos = _level._mapLoc; // Update and center the world map to the player current position _map.update(_info); _map.center(_map._playerPos); // If this is our first time visiting a level, reveal the associated area on the world map _map.revealAdd(_level._mapClip._id, _level._mapClip._rect); // Initialize inventory _info._inv.init(_level.playerId()); // Initialize journal _info._journal.init(_level.playerId()); _level.preDraw(); return true; } return false; } //------------------------------------------------------------------------ // Purpose: Handle events //------------------------------------------------------------------------ void Game::handleEvents(Common::Event &event, bool &shouldChangeState, GameStateID &newStateId) { g_engine->_mouse->handleEvents(event); // if (GameDebug) // debug_console.handleEvents(Event); if (!_debugConsole.restrictInput()) { if (_state == STATE_LOSE_MENU) { switch (_hud._gom.handleEvents(event)) { case 0: _state = STATE_LOSE_LOAD; break; case 1: quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU); break; default: break; } } else if (_state == STATE_LOSE_LOAD) { if (g_engine->_loadMenu->handleEvents(event)) { shouldChangeState = true; newStateId = GAMESTATE_LOAD_GAME; return; } if (_hud._pausekey.handleEvents(event) || _hud._back.handleEvents(event) == BUAC_LCLICK) _state = STATE_LOSE_MENU; } else { if (!_gem.eventInProgress() && !_hud._pause.disableHotkeys()) { switch (_hud.handleEvents(_info, event)) { case HS_MAP: toggleState(STATE_MAP); break; case HS_PAUSE: g_engine->_thumbnail->copyFrom(g_engine->_screen->rawSurface()); toggleState(STATE_PAUSE); break; case HS_CHAR: toggleState(STATE_CHARACTER); _gem._per.Cache(_info, _level.playerId(), _level); break; case HS_JOURNAL: toggleState(STATE_JOURNAL); break; case HS_INV: toggleState(STATE_INVENTORY); break; default: break; } } if (_state == STATE_GAME) { if (_gem.eventInProgress()) { _gem.handleEvents(_info, _level.playerId(), event, _hud, _level, _eventRes); if (applyResult()) quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU); } else { // Update the talk key state _info._talkKeyDown = g_engine->_inputManager->state(IG_TALK) || _level.containsClick(_info.lastPerson(), event); _level.handleEvents(_info, event); if (!_gameOver.empty() && _gameOver.evaluate(_info)) { _state = STATE_LOSE_MENU; _hud._gom.reset(); return; } #if 0 if (g_engine->_inputManager->Equals(IG_QUICKSAVE, Event) == SDL_RELEASED) { CreateSaveGame(SAVEGAME_QUICK); return; } else if (g_engine->_inputManager->Equals(IG_QUICKLOAD, Event) == SDL_RELEASED && !info.IronMan()) { ShouldChangeState = true; NewStateID = GAMESTATE_LOAD_GAME; g_engine->_loadMenu->SelectedPath(FullPath(savefile.quick)); return; } #endif if (_hud._pausekey.handleEvents(event)) toggleState(STATE_PAUSE); } } else if (_state == STATE_PAUSE) { switch (_hud._pause.handleEvents(event, _hud._back)) { case PS_RESUME: toggleState(STATE_GAME); _hud.setTooltip(); break; case PS_SAVE: createSaveGame(SAVEGAME_NORMAL); toggleState(STATE_GAME); _hud.setTooltip(); break; case PS_LOAD: //ShouldChangeState = true; //NewStateID = GAMESTATE_LOAD_GAME; return; case PS_HELP: toggleState(STATE_HELP); break; case PS_QUIT_MENU: createSaveGame(SAVEGAME_EXIT); quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU); break; case PS_QUIT_GAME: createSaveGame(SAVEGAME_EXIT); quit(shouldChangeState, newStateId, GAMESTATE_EXIT); break; default: break; } } else { if (_hud._back.handleEvents(event) == BUAC_LCLICK) toggleState(STATE_GAME); switch (_state) { case STATE_MAP: if (_map.handleEvents(_info, event)) { // We need to load the new level loadLevel(_map._curLoc); toggleState(STATE_GAME); } break; case STATE_JOURNAL: if (_info._journal.handleEvents(_level.playerId(), event)) { // This means we selected the "find on map" button, so we need to: // switch to the world map, and highlight the appropriate quest marker _map.selectDest(_info._journal._markerTitle); toggleState(STATE_MAP); } break; case STATE_CHARACTER: _gem._per.handleEvents(_info, _level.playerId(), event); break; case STATE_INVENTORY: _info._inv.handleEvents(_level.playerId(), event); break; case STATE_HELP: g_engine->_helpScreen->handleEvents(event); default: break; } } } } } #if 0 //------------------------------------------------------------------------ // Purpose: Handle events //------------------------------------------------------------------------ void Game::handleEvents(SDL_Event &Event, bool &ShouldChangeState, GameStateID &NewStateID) { g_engine->_mouse->handleEvents(Event); if (GameDebug) debug_console.handleEvents(Event); if (!debug_console.RestrictInput()) { if (state == STATE_LOSE_MENU) { switch (hud.gom.handleEvents(Event)) { case 0: state = STATE_LOSE_LOAD; break; case 1: Quit(ShouldChangeState, NewStateID, GAMESTATE_MAIN_MENU); break; default: break; } } else if (state == STATE_LOSE_LOAD) { if (g_engine->_loadMenu->handleEvents(Event)) { ShouldChangeState = true; NewStateID = GAMESTATE_LOAD_GAME; return; } if (hud.pausekey.handleEvents(Event) || hud.back.handleEvents(Event) == BUAC_LCLICK) state = STATE_LOSE_MENU; } else { if (!gem.EventInProgress() && !hud.pause.DisableHotkeys()) { switch (hud.handleEvents(info, Event)) { case HS_MAP: ToggleState(STATE_MAP); break; case HS_PAUSE: ToggleState(STATE_PAUSE); break; case HS_CHAR: ToggleState(STATE_CHARACTER); gem.per.Cache(info, level.PlayerID(), level); break; case HS_JOURNAL: ToggleState(STATE_JOURNAL); break; case HS_INV: ToggleState(STATE_INVENTORY); break; default: break; } } if (state == STATE_GAME) { if (gem.EventInProgress()) { gem.handleEvents(info, level.PlayerID(), Event, hud, level, event_res); if (ApplyResult()) Quit(ShouldChangeState, NewStateID, GAMESTATE_MAIN_MENU); } else { // Update the talk key state info.TalkKeyDown = g_engine->_inputManager->State(IG_TALK) || level.ContainsClick(info.LastPerson(), Event); level.handleEvents(info, Event); if (!game_over.Empty() && game_over.Evaluate(info)) { state = STATE_LOSE_MENU; hud.gom.reset(); return; } if (g_engine->_inputManager->Equals(IG_QUICKSAVE, Event) == SDL_RELEASED) { CreateSaveGame(SAVEGAME_QUICK); return; } else if (g_engine->_inputManager->Equals(IG_QUICKLOAD, Event) == SDL_RELEASED && !info.IronMan()) { ShouldChangeState = true; NewStateID = GAMESTATE_LOAD_GAME; g_engine->_loadMenu->SelectedPath(FullPath(savefile.quick)); return; } if (hud.pausekey.handleEvents(Event)) ToggleState(STATE_PAUSE); } } else if (state == STATE_PAUSE) { switch (hud.pause.handleEvents(Event, hud.back)) { case PS_RESUME: ToggleState(STATE_GAME); hud.SetTooltip(); break; case PS_SAVE: CreateSaveGame(SAVEGAME_NORMAL); ToggleState(STATE_GAME); hud.SetTooltip(); break; case PS_LOAD: ShouldChangeState = true; NewStateID = GAMESTATE_LOAD_GAME; return; case PS_HELP: ToggleState(STATE_HELP); break; case PS_QUIT_MENU: CreateSaveGame(SAVEGAME_EXIT); Quit(ShouldChangeState, NewStateID, GAMESTATE_MAIN_MENU); break; case PS_QUIT_GAME: CreateSaveGame(SAVEGAME_EXIT); Quit(ShouldChangeState, NewStateID, GAMESTATE_EXIT); break; default: break; } } else { if (hud.back.handleEvents(Event) == BUAC_LCLICK) ToggleState(STATE_GAME); switch (state) { case STATE_MAP: if (map.handleEvents(info, Event)) { // We need to load the new level LoadLevel(map.cur_loc); ToggleState(STATE_GAME); } break; case STATE_JOURNAL: if (info.journal.handleEvents(level.PlayerID(), Event)) { // This means we selected the "find on map" button, so we need to: // switch to the world map, and highlight the appropriate quest marker map.SelectDest(info.journal.marker_title); ToggleState(STATE_MAP); } break; case STATE_CHARACTER: gem.per.handleEvents(info, level.PlayerID(), Event); break; case STATE_INVENTORY: info.inv.handleEvents(level.PlayerID(), Event); break; case STATE_HELP: g_engine->_helpScreen->handleEvents(Event); default: break; } } } } } #endif //------------------------------------------------------------------------ // Purpose: InternalEvents //------------------------------------------------------------------------ void Game::internalEvents(bool &shouldChangeState, GameStateID &newStateId) { switch (_state) { case STATE_GAME: _hud.internalEvents(_level.showMap()); _eventRes.clear(); { // HACK: Since sequences can only be ended in GameEventManager, we use this empty array // to get effects to work for levels Common::Array endSeq; applyResult(_level.internalEvents(_info, _eventRes, endSeq, _gem.eventInProgress())); } _gem.internalEvents(_info, _level, _eventRes); _info._talkKeyDown = false; if (applyResult()) quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU); break; case STATE_MAP: _map.internalEvents(_info); break; case STATE_CHARACTER: _gem._per.internalEvents(); break; default: break; } } //------------------------------------------------------------------------ // Purpose: Draw //------------------------------------------------------------------------ void Game::draw() { if (_gem._drawGame) _level.draw(_info); else g_engine->_imageManager->blackScreen(); switch (_state) { case STATE_GAME: if (_gem.eventInProgress()) _gem.draw(_info, _hud, _level); else _hud.draw(_info, _level.playerId()); break; case STATE_PAUSE: g_engine->_imageManager->dimScreen(); _hud._pause.draw(_hud._back); _hud.draw(_info, _level.playerId()); break; case STATE_MAP: g_engine->_imageManager->dimScreen(); _map.draw(_info); _hud.draw(_info, _level.playerId()); _hud._back.draw(); break; case STATE_JOURNAL: g_engine->_imageManager->dimScreen(); _info._journal.draw(_level.playerId()); _hud.draw(_info, _level.playerId()); _hud._back.draw(); break; case STATE_CHARACTER: g_engine->_imageManager->dimScreen(); _gem._per.draw(_info, _level.playerId()); _hud.draw(_info, _level.playerId()); _hud._back.draw(); break; case STATE_INVENTORY: g_engine->_imageManager->dimScreen(); _info.invDraw(_level.playerId()); _hud.draw(_info, _level.playerId()); _hud._back.draw(); break; case STATE_HELP: g_engine->_imageManager->dimScreen(); g_engine->_helpScreen->draw(); _hud._back.draw(); _hud.draw(_info, _level.playerId()); break; case STATE_LOSE_MENU: _hud._gom.draw(); break; case STATE_LOSE_LOAD: g_engine->_loadMenu->draw(); _hud._back.draw(); break; default: break; } if (GameDebug) _debugConsole.draw(_info); g_engine->_mouse->draw(); } //------------------------------------------------------------------------ // Purpose: Apply results of events and levels //------------------------------------------------------------------------ bool Game::applyResult() { using namespace pyrodactyl::event; for (const auto &i : _eventRes) { switch (i._type) { case ER_MAP: if (i._val == "img") _map.setImage(i._y); else if (i._val == "pos") { _map._playerPos.x = i._x; _map._playerPos.y = i._y; } break; case ER_DEST: if (i._x < 0 || i._y < 0) { _info._journal.marker(_level.playerId(), i._val, false); _map.destDel(i._val); } else { _map.destAdd(i._val, i._x, i._y); _info._journal.marker(_level.playerId(), i._val, true); _info._unread._map = true; } break; case ER_IMG: playerImg(); break; case ER_TRAIT: if (i._x == 42) _info.traitDel(i._val, i._y); else _info.traitAdd(i._val, i._y); break; case ER_LEVEL: if (i._val == "Map") toggleState(STATE_MAP); else loadLevel(i._val, i._x, i._y); break; case ER_MOVE: for (auto &o : _level._objects) { if (i._val == o.id()) { o.x(i._x); o.y(i._y); break; } } break; case ER_PLAYER: // First stop the movement of the current player sprite _level.playerStop(); // Then swap to the new id _level.playerId(i._val, i._x, i._y); // Stop the new player sprite's movement as well _level.playerStop(); break; case ER_SAVE: createSaveGame(SAVEGAME_EVENT); break; case ER_SYNC: _level.calcProperties(_info); _map.update(_info); break; case ER_QUIT: g_engine->_tempData->_credits = (i._val == "credits"); return true; default: break; } } _gem._per.Cache(_info, _level.playerId(), _level); _eventRes.clear(); return false; } void Game::applyResult(LevelResult result) { switch (result._type) { case LR_LEVEL: if (result._val == "Map") toggleState(STATE_MAP); else loadLevel(result._val, result._x, result._y); return; case LR_GAMEOVER: _state = STATE_LOSE_MENU; _hud._gom.reset(); break; default: break; } } //------------------------------------------------------------------------ // Purpose: Save/load game //------------------------------------------------------------------------ bool Game::loadState(Common::SeekableReadStream *stream) { Common::String data = stream->readString(); uint64 end = data.findLastOf(">"); if (end == Common::String::npos) // invalid save file, returning false from here will display "Reading data failed" dialogbox return false; // +1 to include > as well end++; uint8 *dataC = new uint8[end + 1]; dataC[end] = '\0'; memcpy(dataC, data.c_str(), end); XMLDoc conf(dataC); if (!_isInited) loadGame(); if (conf.ready()) { rapidxml::xml_node *node = conf.doc()->first_node("save"); if (nodeValid(node)) { _info.loadIronMan(node); loadStr(_savefile._ironman, "file", node); _hud._pause.updateMode(_info.ironMan()); if (nodeValid("events", node)) _gem.loadState(node->first_node("events")); if (nodeValid("info", node)) _info.loadState(node->first_node("info")); if (nodeValid("map", node)) _map.loadState(node->first_node("map")); playerImg(); Common::String loc; loadStr(loc, "loc_id", node); loadLevel(loc); if (nodeValid("level", node)) _level.loadState(node->first_node("level")); _gem._per.Cache(_info, _level.playerId(), _level); Common::String playtime; loadStr(playtime, "time", node); _clock.start(playtime); } } return true; } //------------------------------------------------------------------------ // Purpose: Write game state to file //------------------------------------------------------------------------ void Game::saveState(Common::SeekableWriteStream *stream) { rapidxml::xml_document doc; // xml declaration rapidxml::xml_node *decl = doc.allocate_node(rapidxml::node_declaration); decl->append_attribute(doc.allocate_attribute("version", "1.0")); decl->append_attribute(doc.allocate_attribute("encoding", "utf-8")); doc.append_node(decl); // root node rapidxml::xml_node *root = doc.allocate_node(rapidxml::node_element, "save"); doc.append_node(root); // Save location id Common::String loc = _info.curLocID(); root->append_attribute(doc.allocate_attribute("loc_id", loc.c_str())); // Save location name Common::String locName = _info.curLocName(); root->append_attribute(doc.allocate_attribute("loc_name", locName.c_str())); // Save player character name Common::String charName; if (_info.personValid(_level.playerId())) charName = _info.personGet(_level.playerId())._name; root->append_attribute(doc.allocate_attribute("char_name", charName.c_str())); // Difficulty Common::String diff = "Normal"; if (_info.ironMan()) diff = "Iron Man"; root->append_attribute(doc.allocate_attribute("diff", diff.c_str())); // Save file used if iron man root->append_attribute(doc.allocate_attribute("file", _savefile._ironman.c_str())); // Preview image used root->append_attribute(doc.allocate_attribute("preview", _level._previewPath.toString('/').c_str())); // Time played Common::String playtime = _clock.getTime(); root->append_attribute(doc.allocate_attribute("time", playtime.c_str())); rapidxml::xml_node *childGem = doc.allocate_node(rapidxml::node_element, "events"); _gem.saveState(doc, childGem); root->append_node(childGem); rapidxml::xml_node *childInfo = doc.allocate_node(rapidxml::node_element, "info"); _info.saveState(doc, childInfo); root->append_node(childInfo); rapidxml::xml_node *childMap = doc.allocate_node(rapidxml::node_element, "map"); _map.saveState(doc, childMap); root->append_node(childMap); rapidxml::xml_node *childLevel = doc.allocate_node(rapidxml::node_element, "level"); _level.saveState(doc, childLevel); root->append_node(childLevel); Common::String xmlAsString; rapidxml::print(Crab::backInserter(xmlAsString), doc); stream->writeString(xmlAsString); } //------------------------------------------------------------------------ // Purpose: Quit the game //------------------------------------------------------------------------ void Game::quit(bool &shouldChangeState, GameStateID &newStateId, const GameStateID &newStateVal) { shouldChangeState = true; newStateId = newStateVal; g_engine->_imageManager->loadMap(g_engine->_filePath->_mainmenuR); } //------------------------------------------------------------------------ // Purpose: Change our internal state //------------------------------------------------------------------------ void Game::toggleState(const State &s) { if (_state != s) _state = s; else _state = STATE_GAME; // If we are in game state switch to KBM_GAME if (_state == STATE_GAME && g_engine->_inputManager->getKeyBindingMode() != KBM_GAME) { g_engine->_inputManager->setKeyBindingMode(KBM_GAME); } // This is because game is the first state, the rest are in order _hud.State(_state - 1); _hud._pause.reset(); // Only load help screen image if we have to if (_state == STATE_HELP) g_engine->_helpScreen->refresh(); else g_engine->_helpScreen->clear(); } //------------------------------------------------------------------------ // Purpose: Use this function to actually save your games //------------------------------------------------------------------------ void Game::createSaveGame(const SaveGameType &savetype) { #if 0 // Disregard type in iron man mode, we only save to one file if (info.IronMan()) saveState(savefile.ironman, true); else { switch (savetype) { case SAVEGAME_NORMAL: saveState(hud.pause.SaveFile(), false); break; case SAVEGAME_EVENT: if (savefile.auto_slot) saveState(savefile.auto_2, true); else saveState(savefile.auto_1, true); savefile.auto_slot = !savefile.auto_slot; break; case SAVEGAME_EXIT: saveState(savefile.auto_quit, true); break; case SAVEGAME_QUICK: saveState(savefile.quick, true); break; default: break; } } #endif } void Game::setUI() { _map.setUI(); _hud.setUI(); g_engine->_loadMenu->setUI(); g_engine->_optionMenu->setUI(); _gem.setUI(); _info.setUI(); _level.setUI(); } } // End of namespace Crab