Files
scummvm/engines/nancy/state/loadsave.cpp
T

637 lines
20 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "engines/nancy/nancy.h"
#include "engines/nancy/cursor.h"
#include "engines/nancy/sound.h"
#include "engines/nancy/input.h"
#include "engines/nancy/util.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/state/loadsave.h"
#include "engines/nancy/state/scene.h"
#include "engines/nancy/ui/button.h"
#include "common/config-manager.h"
namespace Common {
DECLARE_SINGLETON(Nancy::State::LoadSaveMenu);
}
namespace Nancy {
namespace State {
LoadSaveMenu::~LoadSaveMenu() {
for (auto *tb : _textboxes) {
delete tb;
}
for (auto *button : _loadButtons) {
delete button;
}
for (auto *button : _saveButtons) {
delete button;
}
for (auto *overlay : _cancelButtonOverlays) {
delete overlay;
}
delete _exitButton;
delete _cancelButton;
g_nancy->_input->setVKEnabled(false);
}
void LoadSaveMenu::process() {
if (g_nancy->_sound->isSoundPlaying("BUOK") || g_nancy->_sound->isSoundPlaying("BULS") || g_nancy->_sound->isSoundPlaying("BUDE")) {
return;
}
switch (_state) {
case kInit:
init();
// fall through
case kRun:
run();
break;
case kEnterFilename:
enterFilename();
break;
case kSave:
save();
break;
case kLoad:
load();
break;
case kSuccess:
success();
break;
default:
break;
}
// Make sure stop runs on the same frame
if (_state == kStop) {
stop();
}
g_nancy->_cursor->setCursorType(CursorManager::kNormalArrow);
}
void LoadSaveMenu::onStateEnter(const NancyState::NancyState prevState) {
if (_state == kEnterFilename) {
g_nancy->_input->setVKEnabled(true);
}
registerGraphics();
}
bool LoadSaveMenu::onStateExit(const NancyState::NancyState nextState) {
g_nancy->_input->setVKEnabled(false);
return _destroyOnExit;
}
void LoadSaveMenu::registerGraphics() {
_background.registerGraphics();
for (auto *button : _loadButtons) {
button->registerGraphics();
}
for (auto *button : _saveButtons) {
button->registerGraphics();
}
for (auto *overlay : _cancelButtonOverlays) {
overlay->registerGraphics();
}
for (auto *tb : _textboxes) {
tb->registerGraphics();
}
if (_exitButton) {
_exitButton->registerGraphics();
}
if (_cancelButton) {
_cancelButton->registerGraphics();
}
_blinkingCursorOverlay.registerGraphics();
_successOverlay.registerGraphics();
g_nancy->_graphics->redrawAll();
}
void LoadSaveMenu::init() {
_loadSaveData = GetEngineData(LOAD);
assert(_loadSaveData);
_background.init(_loadSaveData->_image1Name);
_baseFont = g_nancy->_graphics->getFont(_loadSaveData->_mainFontID);
if (_loadSaveData->_highlightFontID != -1) {
_highlightFont = g_nancy->_graphics->getFont(_loadSaveData->_highlightFontID);
_disabledFont = g_nancy->_graphics->getFont(_loadSaveData->_disabledFontID);
} else {
_highlightFont = _disabledFont = _baseFont;
}
_filenameStrings.resize(_loadSaveData->_textboxBounds.size());
_saveExists.resize(_filenameStrings.size(), false);
_textboxes.resize(_filenameStrings.size());
for (uint i = 0; i < _textboxes.size(); ++i) {
// Load textbox objects
RenderObject *newTb = new RenderObject(5);
_textboxes[i] = newTb;
const Common::Rect &bounds = _loadSaveData->_textboxBounds[i];
newTb->_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getScreenPixelFormat());
newTb->_drawSurface.clear(g_nancy->_graphics->getTransColor());
newTb->moveTo(bounds);
newTb->setTransparent(true);
newTb->setVisible(true);
newTb->init();
// Check for valid save and write its name in the textbox
SaveStateDescriptor desc = g_nancy->getMetaEngine()->querySaveMetaInfos(ConfMan.getActiveDomainName().c_str(), i + 1);
if (desc.isValid()) {
_saveExists[i] = true;
_filenameStrings[i] = desc.getDescription();
} else {
// If no valid save, copy over the empty save string
if (_loadSaveData->_emptySaveText.size()) {
_filenameStrings[i] = _loadSaveData->_emptySaveText;
} else {
_filenameStrings[i] = g_nancy->getStaticData().emptySaveText;
}
}
}
bool hasHighlights = _loadSaveData->_loadButtonHighlightSrcs.size();
_loadButtons.resize(_textboxes.size());
_saveButtons.resize(_textboxes.size());
_cancelButtonOverlays.resize(_textboxes.size());
for (uint i = 0; i < _loadButtons.size(); ++i) {
// Load Save and Load buttons, and Cancel overlays
_loadButtons[i] = new UI::Button(1, _background._drawSurface,
_loadSaveData->_loadButtonDownSrcs[i], _loadSaveData->_loadButtonDests[i],
hasHighlights ? _loadSaveData->_loadButtonHighlightSrcs[i] : Common::Rect(),
hasHighlights ? _loadSaveData->_loadButtonDisabledSrcs[i] : Common::Rect());
_saveButtons[i] = new UI::Button(1, _background._drawSurface,
_loadSaveData->_saveButtonDownSrcs[i], _loadSaveData->_saveButtonDests[i],
hasHighlights ? _loadSaveData->_saveButtonHighlightSrcs[i] : Common::Rect(),
hasHighlights ? _loadSaveData->_saveButtonDisabledSrcs[i] : Common::Rect());
_cancelButtonOverlays[i] = new RenderObject(2, _background._drawSurface,
_loadSaveData->_cancelButtonSrcs[i], _loadSaveData->_cancelButtonDests[i]);
_loadButtons[i]->init();
_saveButtons[i]->init();
_cancelButtonOverlays[i]->init();
}
// Load exit button
_exitButton = new UI::Button(3, _background._drawSurface,
_loadSaveData->_doneButtonDownSrc, _loadSaveData->_doneButtonDest,
hasHighlights ? _loadSaveData->_doneButtonHighlightSrc : Common::Rect(),
hasHighlights ? _loadSaveData->_doneButtonDisabledSrc : Common::Rect());
// Load Cancel button that activates when typing a filename
// Note: this is only responsible for the hover/mouse down/disabled graphic;
// the graphics that replace the Save buttons with Cancel are their own RenderObject.
// We also make sure this has an invalid position until we need it.
Common::Rect pos = _loadSaveData->_cancelButtonDests[0];
pos.moveTo(-500, 0);
_cancelButton = new UI::Button(3, _background._drawSurface,
_loadSaveData->_cancelButtonDownSrc, Common::Rect(),
_loadSaveData->_cancelButtonHighlightSrc, _loadSaveData->_cancelButtonDisabledSrc);
// Load the blinking cursor graphic that appears while typing a filename
_blinkingCursorOverlay._drawSurface.create(_loadSaveData->_blinkingCursorSrc.width(),
_loadSaveData->_blinkingCursorSrc.height(),
g_nancy->_graphics->getScreenPixelFormat());
_blinkingCursorOverlay.setTransparent(true);
_blinkingCursorOverlay.setVisible(false);
_blinkingCursorOverlay._drawSurface.clear(_blinkingCursorOverlay._drawSurface.getTransparentColor());
_blinkingCursorOverlay._drawSurface.transBlitFrom(_highlightFont->getImageSurface(), _loadSaveData->_blinkingCursorSrc,
Common::Point(), g_nancy->_graphics->getTransColor());
// Load the "Your game has been saved" popup graphic
if (!_loadSaveData->_gameSavedPopup.empty()) {
g_nancy->_resource->loadImage(_loadSaveData->_gameSavedPopup, _successOverlay._drawSurface);
Common::Rect destBounds = Common::Rect(0,0, _successOverlay._drawSurface.w, _successOverlay._drawSurface.h);
destBounds.moveTo(640 / 2 - destBounds.width() / 2,
480 / 2 - destBounds.height() / 2);
_successOverlay.moveTo(destBounds);
_successOverlay.setVisible(false);
}
registerGraphics();
_state = kRun;
_enteringNewState = true;
}
void LoadSaveMenu::run() {
if (_enteringNewState) {
// State has changed, revert all relevant objects to an appropriate state
for (uint i = 0; i < _textboxes.size(); ++i) {
writeToTextbox(i, _filenameStrings[i], Nancy::State::Scene::hasInstance() ? _baseFont : _disabledFont);
// Set load button state depending on whether there exists a save in the corresponding slot
// Save buttons are always active
_loadButtons[i]->setDisabled(_saveExists[i] == false);
_saveButtons[i]->setDisabled(!Nancy::State::Scene::hasInstance());
_cancelButtonOverlays[i]->setVisible(false);
_loadButtons[i]->_isClicked = false;
_saveButtons[i]->_isClicked = false;
}
if (_cancelButton) {
_cancelButton->_isClicked = false;
_cancelButton->setDisabled(true);
_cancelButton->moveTo(Common::Point(-500, 0));
}
_blinkingCursorOverlay.setVisible(false);
_exitButton->setDisabled(false);
_enteredString.clear();
_successOverlay.setVisible(false);
_selectedSave = -1;
_enteringNewState = false;
}
// Handle input
NancyInput input = g_nancy->_input->getInput();
for (uint i = 0; i < _loadButtons.size(); ++i) {
_loadButtons[i]->handleInput(input);
if (_loadButtons[i]->_isClicked) {
if (_saveExists[i]) {
_state = kLoad;
_enteringNewState = true;
_selectedSave = i;
g_nancy->_sound->playSound("BULS");
} else if (!_loadSaveData->_saveButtonHighlightSrcs.size()) {
// TVD and nancy1 buttons get pushed and play error sound
_loadButtons[i]->setVisible(true);
g_nancy->_sound->playSound("BUDE");
_enteringNewState = true;
}
return;
}
}
for (uint i = 0; i < _saveButtons.size(); ++i) {
_saveButtons[i]->handleInput(input);
if (_saveButtons[i]->_isClicked) {
if (Nancy::State::Scene::hasInstance()) {
_state = kSave;
_enteringNewState = true;
_selectedSave = i;
g_nancy->_sound->playSound("BULS");
} else if (!_loadSaveData->_saveButtonHighlightSrcs.size()) {
// TVD and nancy1 buttons get pushed and play error sound
_saveButtons[i]->setVisible(true);
g_nancy->_sound->playSound("BUDE");
_enteringNewState = true;
}
return;
}
}
// Handle textbox hovering
bool hoversOverTextbox = false;
for (int i = 0; i < (int)_textboxes.size(); ++i) {
if (_textboxes[i]->getScreenPosition().contains(input.mousePos)) {
if (Nancy::State::Scene::hasInstance()) {
hoversOverTextbox = true;
if (_selectedSave != i) {
if (_selectedSave != -1) {
writeToTextbox(_selectedSave, _filenameStrings[_selectedSave], _baseFont);
}
_selectedSave = i;
writeToTextbox(_selectedSave, _filenameStrings[_selectedSave], _highlightFont);
}
if (input.input & NancyInput::kLeftMouseButtonUp) {
_state = kEnterFilename;
_enteringNewState = true;
g_nancy->_sound->playSound("BUOK");
_selectedSave = i;
return;
}
break;
} else if (!_loadSaveData->_saveButtonHighlightSrcs.size()) {
if (input.input & NancyInput::kLeftMouseButtonUp) {
// TVD and nancy1 textboxes play error sound when no Scene is active
g_nancy->_sound->playSound("BUDE");
}
}
}
}
if (!hoversOverTextbox && _selectedSave != -1) {
writeToTextbox(_selectedSave, _filenameStrings[_selectedSave], _baseFont);
_selectedSave = -1;
}
// Check Done button
if (_exitButton) {
_exitButton->handleInput(input);
if (_exitButton->_isClicked) {
_state = kStop;
g_nancy->_sound->playSound("BUOK");
return;
}
}
}
void LoadSaveMenu::enterFilename() {
if (_enteringNewState) {
// State has changed, revert all relevant objects to an appropriate state
if (_cancelButton) {
_cancelButton->setDisabled(false);
_cancelButton->moveTo(_loadSaveData->_cancelButtonDests[_selectedSave]);
}
for (int i = 0; i < (int)_textboxes.size(); ++i) {
bool sel = i == _selectedSave;
writeToTextbox(i, sel ? Common::String() : _filenameStrings[i], sel ? _highlightFont : _disabledFont);
_loadButtons[i]->setDisabled(true);
if (i != _selectedSave) {
_saveButtons[i]->setDisabled(true);
}
}
_exitButton->setDisabled(true);
_cancelButtonOverlays[_selectedSave]->setVisible(true);
// Set up blinking cursor (doesn't blink in TVD)
Common::Rect tbPosition = _textboxes[_selectedSave]->getScreenPosition();
Common::Rect cursorRect = _blinkingCursorOverlay._drawSurface.getBounds();
cursorRect.moveTo(tbPosition.left, tbPosition.bottom - _blinkingCursorOverlay._drawSurface.h + _loadSaveData->_fontYOffset);
_blinkingCursorOverlay.moveTo(cursorRect);
_blinkingCursorOverlay.setVisible(true);
_nextBlink = g_nancy->getTotalPlayTime() + _loadSaveData->_blinkingTimeDelay;
_enteringNewState = false;
g_nancy->_input->setVKEnabled(true);
}
// Perform cursor blinking
uint32 gameTime = g_nancy->getTotalPlayTime();
if (_loadSaveData->_blinkingTimeDelay != 0 && gameTime > _nextBlink) {
_blinkingCursorOverlay.setVisible(!_blinkingCursorOverlay.isVisible());
_nextBlink = gameTime + _loadSaveData->_blinkingTimeDelay;
}
// Handle input
NancyInput input = g_nancy->_input->getInput();
// Improvement: we allow the enter key to sumbit
bool enterKeyPressed = false;
for (uint i = 0; i < input.otherKbdInput.size(); ++i) {
Common::KeyState &key = input.otherKbdInput[i];
if (key.keycode == Common::KEYCODE_BACKSPACE) {
if (_enteredString.size()) {
_enteredString.deleteLastChar();
}
} else if (key.keycode == Common::KEYCODE_RETURN || key.keycode == Common::KEYCODE_KP_ENTER) {
enterKeyPressed = true;
} else if (Common::isAlnum(key.ascii) || Common::isSpace(key.ascii)) {
_enteredString += key.ascii;
}
uint16 textWidthInPixels = writeToTextbox(_selectedSave, _enteredString, _highlightFont);
Common::Rect tbPosition = _textboxes[_selectedSave]->getScreenPosition();
_blinkingCursorOverlay.moveTo(Common::Point(tbPosition.left + textWidthInPixels,
tbPosition.bottom - _blinkingCursorOverlay._drawSurface.h + _loadSaveData->_fontYOffset));
}
_cancelButton->handleInput(input);
if (_cancelButton->_isClicked) {
_state = kRun;
_enteringNewState = true;
g_nancy->_sound->playSound("BULS");
g_nancy->_input->setVKEnabled(false);
return;
}
_saveButtons[_selectedSave]->handleInput(input);
if (_saveButtons[_selectedSave]->_isClicked || enterKeyPressed) {
_state = kSave;
_enteringNewState = true;
g_nancy->_sound->playSound("BULS");
g_nancy->_input->setVKEnabled(false);
return;
}
}
void LoadSaveMenu::save() {
auto *sdlg = GetEngineData(SDLG);
if (sdlg && sdlg->dialogs.size() > 1) {
// nancy6 added a "Do you want to overwrite this save" dialog.
// First, check if we are actually overwriting
SaveStateDescriptor desc = g_nancy->getMetaEngine()->querySaveMetaInfos(ConfMan.getActiveDomainName().c_str(), _selectedSave + 1);
if (desc.isValid()) {
if (!ConfMan.hasKey("sdlg_return", Common::ConfigManager::kTransientDomain)) {
// Request the dialog
ConfMan.setInt("sdlg_id", 1, Common::ConfigManager::kTransientDomain);
_destroyOnExit = false;
g_nancy->setState(NancyState::kSaveDialog);
return;
} else {
// Dialog has returned
g_nancy->_graphics->suppressNextDraw();
_destroyOnExit = true;
uint ret = ConfMan.getInt("sdlg_return", Common::ConfigManager::kTransientDomain);
ConfMan.removeKey("sdlg_return", Common::ConfigManager::kTransientDomain);
switch (ret) {
case 1 :
// "No" keeps us in the LoadSave state but doesn't save
_state = kRun;
return;
case 2 :
// "Cancel" returns to the main menu
g_nancy->setState(NancyState::kMainMenu);
return;
default:
// "Yes" actually saves
break;
}
}
}
}
// Improvement: not providing a name doesn't result in the
// savefile being named "--- Empty ---" or "Nothing Saved Here".
// Instead, we use ScummVM's built-in save name generator
// This does not apply to nancy7, where a default name is provided in
// the LOAD chunk, and has a number appended to the end
Common::String finalDesc = _enteredString;
if (!finalDesc.size()) {
if (_loadSaveData->_defaultSaveNamePrefix.size()) {
if (_filenameStrings[_selectedSave].equals(_loadSaveData->_emptySaveText)) {
uint suffixNum = 1;
for (int i = 1; i < g_nancy->getMetaEngine()->getMaximumSaveSlot(); ++i) {
if (i == _selectedSave + 1) {
continue;
}
SaveStateDescriptor desc = g_nancy->getMetaEngine()->querySaveMetaInfos(ConfMan.getActiveDomainName().c_str(), i);
if (desc.getDescription().substr(0, _loadSaveData->_defaultSaveNamePrefix.size()).equals(Common::U32String(_loadSaveData->_defaultSaveNamePrefix))) {
if (desc.getDescription().substr(_loadSaveData->_defaultSaveNamePrefix.size(), 1).asUint64() == suffixNum) {
++suffixNum;
} else {
break;
}
}
}
finalDesc = _loadSaveData->_defaultSaveNamePrefix + ('0' + suffixNum);
} else {
finalDesc = _filenameStrings[_selectedSave];
}
} else {
if (!_filenameStrings[_selectedSave].equals(g_nancy->getStaticData().emptySaveText)) {
finalDesc = _filenameStrings[_selectedSave];
}
}
}
g_nancy->saveGameState(_selectedSave + 1, finalDesc, false);
// Feed the new name back into the list of saves
SaveStateDescriptor desc = g_nancy->getMetaEngine()->querySaveMetaInfos(ConfMan.getActiveDomainName().c_str(), _selectedSave + 1);
if (desc.isValid()) {
_filenameStrings[_selectedSave] = desc.getDescription();
}
if (_successOverlay._drawSurface.empty()) {
_state = kRun;
_enteringNewState = true;
} else {
_state = kSuccess;
_enteringNewState = true;
}
_saveExists[_selectedSave] = true;
g_nancy->_hasJustSaved = true;
}
void LoadSaveMenu::load() {
auto *sdlg = GetEngineData(SDLG);
if (sdlg && sdlg->dialogs.size() > 1 && Nancy::State::Scene::hasInstance() && !g_nancy->_hasJustSaved) {
// nancy6 added a "Do you want load without saving" dialog.
if (!ConfMan.hasKey("sdlg_return", Common::ConfigManager::kTransientDomain)) {
// Request the dialog
ConfMan.setInt("sdlg_id", 2, Common::ConfigManager::kTransientDomain);
_destroyOnExit = false;
g_nancy->setState(NancyState::kSaveDialog);
return;
} else {
// Dialog has returned
_destroyOnExit = true;
g_nancy->_graphics->suppressNextDraw();
uint ret = ConfMan.getInt("sdlg_return", Common::ConfigManager::kTransientDomain);
ConfMan.removeKey("sdlg_return", Common::ConfigManager::kTransientDomain);
switch (ret) {
case 1 :
// "No" keeps us in the LoadSave state but doesn't load
_state = kRun;
return;
case 2 :
// "Cancel" returns to the main menu
g_nancy->setState(NancyState::kMainMenu);
return;
default:
// "Yes" actually loads
break;
}
}
}
if (Nancy::State::Scene::hasInstance()) {
Nancy::State::Scene::destroy();
}
ConfMan.setInt("save_slot", _selectedSave + 1, Common::ConfigManager::kTransientDomain);
_state = kStop;
_enteringNewState = true;
}
void LoadSaveMenu::success() {
// The original engine still lets the cursor blink in the background, but implementing that is completely unnecessary
if (_enteringNewState) {
_nextBlink = g_nancy->getTotalPlayTime() + 2000; // Hardcoded
_successOverlay.setVisible(true);
_enteringNewState = false;
}
if (g_nancy->getTotalPlayTime() > _nextBlink) {
_state = kRun;
_enteringNewState = true;
}
}
void LoadSaveMenu::stop() {
if (_selectedSave != -1) {
g_nancy->setState(NancyState::kScene);
} else {
g_nancy->setState(NancyState::kMainMenu);
}
}
uint16 LoadSaveMenu::writeToTextbox(uint textboxID, const Common::String &text, const Font *font) {
assert(font);
_textboxes[textboxID]->_drawSurface.clear(g_nancy->_graphics->getTransColor());
Common::Point destPoint(_loadSaveData->_fontXOffset, _loadSaveData->_fontYOffset + _textboxes[textboxID]->_drawSurface.h - font->getFontHeight());
font->drawString(&_textboxes[textboxID]->_drawSurface, text, destPoint.x, destPoint.y, _textboxes[textboxID]->_drawSurface.w, 0);
_textboxes[textboxID]->setVisible(true);
return font->getStringWidth(text);
}
} // End of namespace State
} // End of namespace Nancy