From 5143d824680197eaf47838b325946372970c9bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaakko=20Kera=CC=88nen?= Date: Thu, 8 Nov 2018 18:46:06 +0200 Subject: [PATCH] SaveGame|UI: Custom profiles have their own save folders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each custom profile is assigned a unique empty save folder when the profile is created. SaveGames can be queried for the current game profile’s save path. IssueID #2177 --- doomsday/apps/client/src/dd_main.cpp | 2 +- .../src/ui/dialogs/createprofiledialog.cpp | 21 ++--- .../client/src/ui/home/gamecolumnwidget.cpp | 86 +++++++++++++++---- .../src/ui/home/gamepanelbuttonwidget.cpp | 6 ++ .../include/doomsday/gameprofiles.h | 7 ++ doomsday/apps/libdoomsday/src/doomsdayapp.cpp | 15 ++-- .../src/filesys/idgamespackageinfofile.cpp | 2 +- .../apps/libdoomsday/src/gameprofiles.cpp | 84 +++++++++++++++++- .../plugins/common/src/game/gamesession.cpp | 3 +- 9 files changed, 184 insertions(+), 42 deletions(-) diff --git a/doomsday/apps/client/src/dd_main.cpp b/doomsday/apps/client/src/dd_main.cpp index 36f1c1aa85..b03b9c9e36 100644 --- a/doomsday/apps/client/src/dd_main.cpp +++ b/doomsday/apps/client/src/dd_main.cpp @@ -1360,7 +1360,7 @@ static dint DD_StartupWorker(void * /*context*/) #endif ); DoomsdayApp::bundles().waitForEverythingIdentified();*/ - FS::get().waitForIdle(); + FS::waitForIdle(); /*String foundPath = App_FileSystem().findPath(de::Uri("doomsday.pk3", RC_PACKAGE), RLF_DEFAULT, App_ResourceClass(RC_PACKAGE)); diff --git a/doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp b/doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp index b1c66a88b9..54b3043368 100644 --- a/doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp +++ b/doomsday/apps/client/src/ui/dialogs/createprofiledialog.cpp @@ -20,7 +20,6 @@ #include "ui/widgets/packagesbuttonwidget.h" #include "ui/widgets/packageswidget.h" #include "ui/dialogs/datafilesettingsdialog.h" -//#include "ui/widgets/nativepathwidget.h" #include #include @@ -37,14 +36,11 @@ using namespace de; DENG_GUI_PIMPL(CreateProfileDialog) -//, DENG2_OBSERVES(NativePathWidget, UserChange) { ChoiceWidget *gameChoice; PackagesButtonWidget *packages; ChoiceWidget *autoStartMap; ChoiceWidget *autoStartSkill; - //NativePathWidget *customDataFile; - //String customDataFile; AuxButtonWidget *customDataFileName; ui::ListData customDataFileActions; SafeWidgetPtr customPicker; @@ -157,17 +153,6 @@ DENG_GUI_PIMPL(CreateProfileDialog) autoStartMap->setSelected(pos != ui::Data::InvalidPos ? pos : 0); } -// void pathChangedByUser(NativePathWidget &) override -// { -// setCustomDataFile(customDataFile->path()); -// } - -// void setCustomDataFile(const NativePath &path) -// { -// tempProfile->setUseGameRequirements(path.isEmpty()); -// tempProfile->setCustomDataFile(path.toString()); -// } - void updateDataFile() { if (tempProfile->customDataFile().isEmpty()) @@ -341,6 +326,10 @@ GameProfile *CreateProfileDialog::makeProfile() const auto *prof = new GameProfile(profileName()); prof->setUserCreated(true); applyTo(*prof); + if (!prof->saveLocationId()) + { + prof->createSaveLocation(); + } return prof; } @@ -353,6 +342,7 @@ void CreateProfileDialog::fetchFrom(GameProfile const &profile) d->updateDataFile(); d->autoStartMap->setSelected(d->autoStartMap->items().findData(profile.autoStartMap())); d->autoStartSkill->setSelected(d->autoStartSkill->items().findData(profile.autoStartSkill())); + d->tempProfile->setSaveLocationId(profile.saveLocationId()); } void CreateProfileDialog::applyTo(GameProfile &profile) const @@ -367,6 +357,7 @@ void CreateProfileDialog::applyTo(GameProfile &profile) const profile.setPackages(d->packages->packages()); profile.setAutoStartMap(d->autoStartMap->selectedItem().data().toString()); profile.setAutoStartSkill(d->autoStartSkill->selectedItem().data().toInt()); + profile.setSaveLocationId(profile.saveLocationId()); } String CreateProfileDialog::profileName() const diff --git a/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp b/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp index d744940423..ec58cfacd4 100644 --- a/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp +++ b/doomsday/apps/client/src/ui/home/gamecolumnwidget.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,8 @@ #include #include +#include + using namespace de; const String GameColumnWidget::SORT_GAME_ID("game"); @@ -147,7 +150,7 @@ DENG_GUI_PIMPL(GameColumnWidget) if (dlg->exec(root())) { // Adding the profile has the side effect that a widget is - // created for it. + // created for it in the menu. auto *added = dlg->makeProfile(); DoomsdayApp::gameProfiles().add(added); } @@ -380,7 +383,7 @@ DENG_GUI_PIMPL(GameColumnWidget) { cmp = -1; } - else + else if (prof2.lastPlayedAt().isValid()) { cmp = +1; } @@ -509,9 +512,10 @@ DENG_GUI_PIMPL(GameColumnWidget) button->clearPackages(); })) << new ui::ActionItem( - tr("Duplicate"), new CallbackAction([this, profileItem]() { + tr("Duplicate"), new CallbackAction([profileItem]() { GameProfile *dup = new GameProfile(*profileItem->profile); dup->setUserCreated(true); + dup->createSaveLocation(); // Generate a unique name. for (int attempt = 1;; ++attempt) @@ -530,24 +534,76 @@ DENG_GUI_PIMPL(GameColumnWidget) } })); + if (const auto *loc = FS::tryLocate(profileItem->profile->savePath())) + { + popup->items() << new ui::Item(ui::Item::Separator) + << new ui::ActionItem( + "Show Save Folder", new CallbackAction([loc]() { + QDesktopServices::openUrl( + QUrl::fromLocalFile(loc->correspondingNativePath())); + })); + } + if (isUserProfile) { auto *deleteSub = new ui::SubmenuItem(style().images().image("close.ring"), tr("Delete"), ui::Left); deleteSub->items() << new ui::Item(ui::Item::Separator, tr("Are you sure?")) - << new ui::ActionItem(tr("Delete Profile"), - new CallbackAction([this, button, profileItem, popup] () - { - popup->detachAnchor(); - // Animate the widget to fade it away. - TimeSpan const SPAN = 0.2; - button->setOpacity(0, SPAN); - Loop::get().timer(SPAN, [profileItem] () - { - delete profileItem->profile; - }); - })) + << new ui::ActionItem( + tr("Delete Profile"), + new CallbackAction([this, button, profileItem, popup]() { + if (profileItem->profile->saveLocationId()) + { + const Folder *saveFolder = + FS::tryLocate(profileItem->profile->savePath()); + + if (saveFolder && !profileItem->profile->isSaveLocationEmpty()) + { + // What to do with the savegames? + auto *question = new MessageDialog; + question->setDeleteAfterDismissed(true); + question->title().setText("Delete Saved Games?"); + question->title().setStyleImage("alert"); + question->message().setText( + "The profile " _E(b) + profileItem->profile->name() + + _E(.) " that is being deleted has saved games. " + "Do you wish to delete the save files as well?"); + const NativePath savePath = + saveFolder->correspondingNativePath(); + question->buttons() + << new DialogButtonItem(DialogWidget::Accept, + "Delete All") + << new DialogButtonItem(DialogWidget::Reject | + DialogWidget::Default, + "Cancel") + << new DialogButtonItem( + DialogWidget::Action, + "Show Folder", + new CallbackAction([savePath]() { + QDesktopServices::openUrl( + QUrl::fromLocalFile(savePath)); + })); + if (!question->exec(root())) + { + // Cancelled. + return; + } + } + if (saveFolder) + { + profileItem->profile->destroySaveLocation(); + } + } + + // Animate the widget to fade it away. + const TimeSpan SPAN = 0.2; + button->setOpacity(0, SPAN); + popup->detachAnchor(); + popup->close(); + Loop::get().timer(SPAN, + [profileItem]() { delete profileItem->profile; }); + })) << new ui::ActionItem(tr("Cancel"), new Action); popup->items() diff --git a/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp b/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp index 8bd55d54ca..c1d7230553 100644 --- a/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp +++ b/doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp @@ -78,6 +78,12 @@ DENG_GUI_PIMPL(GamePanelButtonWidget) if (!gameProfile.isPlayable()) return false; + // The file must be in the right save folder. + if (item.savePath().fileNamePath().compareWithoutCase(gameProfile.savePath())) + { + return false; + } + StringList const savePacks = item.loadedPackages(); // Fallback for older saves without package metadata. diff --git a/doomsday/apps/libdoomsday/include/doomsday/gameprofiles.h b/doomsday/apps/libdoomsday/include/doomsday/gameprofiles.h index ed86ca3417..7d84d01c58 100644 --- a/doomsday/apps/libdoomsday/include/doomsday/gameprofiles.h +++ b/doomsday/apps/libdoomsday/include/doomsday/gameprofiles.h @@ -57,6 +57,7 @@ class LIBDOOMSDAY_PUBLIC GameProfiles : public de::Profiles void setAutoStartMap(de::String const &map); void setAutoStartSkill(int level); void setLastPlayedAt(const de::Time &at = de::Time()); + void setSaveLocationId(de::duint32 saveLocationId); bool appendPackage(de::String const &id); @@ -69,8 +70,14 @@ class LIBDOOMSDAY_PUBLIC GameProfiles : public de::Profiles de::String autoStartMap() const; int autoStartSkill() const; de::Time lastPlayedAt() const; + de::duint32 saveLocationId() const; de::String savePath() const; + void createSaveLocation(); + void destroySaveLocation(); + void checkSaveLocation() const; + bool isSaveLocationEmpty() const; + /** * Returns a list of the game's packages in addition to the profile's * configured packages. diff --git a/doomsday/apps/libdoomsday/src/doomsdayapp.cpp b/doomsday/apps/libdoomsday/src/doomsdayapp.cpp index 7b9ec7fd97..59ce8093cf 100644 --- a/doomsday/apps/libdoomsday/src/doomsdayapp.cpp +++ b/doomsday/apps/libdoomsday/src/doomsdayapp.cpp @@ -549,13 +549,13 @@ void DoomsdayApp::initialize() void DoomsdayApp::initWadFolders() { - FS::get().waitForIdle(); + FS::waitForIdle(); d->initWadFolders(); } void DoomsdayApp::initPackageFolders() { - FS::get().waitForIdle(); + FS::waitForIdle(); d->initPackageFolders(); } @@ -864,9 +864,9 @@ void DoomsdayApp::setGame(Game const &game) app().d->currentGame = const_cast(&game); } -void DoomsdayApp::makeGameCurrent(GameProfile const &profile) +void DoomsdayApp::makeGameCurrent(const GameProfile &profile) { - auto const &newGame = profile.game(); + const auto &newGame = profile.game(); if (!newGame.isNull()) { @@ -889,7 +889,8 @@ void DoomsdayApp::makeGameCurrent(GameProfile const &profile) // This is now the current game. setGame(newGame); d->currentProfile = &profile; - //AbstractSession::profile().gameId = newGame.id(); + + profile.checkSaveLocation(); // in case it's gone missing if (!newGame.isNull()) { @@ -921,9 +922,9 @@ bool DoomsdayApp::changeGame(GameProfile const &profile, std::function gameActivationFunc, Behaviors behaviors) { - auto const &newGame = profile.game(); + const auto &newGame = profile.game(); - bool const arePackagesDifferent = + const bool arePackagesDifferent = !GameProfiles::arePackageListsCompatible(DoomsdayApp::app().loadedPackagesAffectingGameplay(), profile.packagesAffectingGameplay()); diff --git a/doomsday/apps/libdoomsday/src/filesys/idgamespackageinfofile.cpp b/doomsday/apps/libdoomsday/src/filesys/idgamespackageinfofile.cpp index 2e433a2d42..1ab15a15d8 100644 --- a/doomsday/apps/libdoomsday/src/filesys/idgamespackageinfofile.cpp +++ b/doomsday/apps/libdoomsday/src/filesys/idgamespackageinfofile.cpp @@ -107,7 +107,7 @@ DENG2_PIMPL(IdgamesPackageInfoFile) } return LoopContinue; }); - FS::get().waitForIdle(); + FS::waitForIdle(); StringList components; foreach (String path, dataFiles) diff --git a/doomsday/apps/libdoomsday/src/gameprofiles.cpp b/doomsday/apps/libdoomsday/src/gameprofiles.cpp index b47c0f0ed9..c0c7b7fe58 100644 --- a/doomsday/apps/libdoomsday/src/gameprofiles.cpp +++ b/doomsday/apps/libdoomsday/src/gameprofiles.cpp @@ -22,6 +22,7 @@ #include "doomsday/GameStateFolder" #include +#include #include #include @@ -37,6 +38,7 @@ static String const VAR_USE_GAME_REQUIREMENTS("useGameRequirements"); static String const VAR_AUTO_START_MAP ("autoStartMap"); static String const VAR_AUTO_START_SKILL("autoStartSkill"); static String const VAR_LAST_PLAYED ("lastPlayed"); +static String const VAR_SAVE_LOCATION_ID("saveLocationId"); static String const VAR_VALUES ("values"); static int const DEFAULT_SKILL = 3; // Normal skill level (1-5) @@ -191,6 +193,10 @@ Profiles::AbstractProfile *GameProfiles::profileFromInfoBlock(Info::BlockElement { prof->setAutoStartSkill(block.keyValue(VAR_AUTO_START_SKILL).text.toInt()); } + if (block.contains(VAR_SAVE_LOCATION_ID)) + { + prof->setSaveLocationId(block.keyValue(VAR_SAVE_LOCATION_ID).text.toUInt32(nullptr, 16)); + } if (block.contains(VAR_LAST_PLAYED)) { prof->setLastPlayedAt(Time::fromText(block.keyValue(VAR_LAST_PLAYED).text)); @@ -215,6 +221,7 @@ DENG2_PIMPL_NOREF(GameProfiles::Profile) String autoStartMap; int autoStartSkill = DEFAULT_SKILL; Time lastPlayedAt = Time::invalidTime(); + duint32 saveLocationId = 0; Record values; Impl() {} @@ -228,6 +235,7 @@ DENG2_PIMPL_NOREF(GameProfiles::Profile) , autoStartMap (other.autoStartMap) , autoStartSkill (other.autoStartSkill) , lastPlayedAt (other.lastPlayedAt) + , saveLocationId (other.saveLocationId) , values (other.values) {} }; @@ -254,6 +262,7 @@ GameProfiles::Profile &GameProfiles::Profile::operator=(const Profile &other) d->autoStartMap = other.d->autoStartMap; d->autoStartSkill = other.d->autoStartSkill; d->lastPlayedAt = other.d->lastPlayedAt; + d->saveLocationId = other.d->saveLocationId; d->values = other.d->values; return *this; } @@ -332,6 +341,15 @@ void GameProfiles::Profile::setLastPlayedAt(const Time &at) } } +void GameProfiles::Profile::setSaveLocationId(const duint32 saveLocationId) +{ + if (d->saveLocationId != saveLocationId) + { + d->saveLocationId = saveLocationId; + notifyChange(); + } +} + bool GameProfiles::Profile::appendPackage(String const &id) { if (!d->packages.contains(id)) @@ -393,11 +411,69 @@ Time GameProfiles::Profile::lastPlayedAt() const return d->lastPlayedAt; } +duint32 GameProfiles::Profile::saveLocationId() const +{ + return d->saveLocationId; +} + +static const String PATH_SAVEGAMES = "/home/savegames"; + String GameProfiles::Profile::savePath() const { - /// @todo If the profile has a custom save path, use that instead! + /// If the profile has a custom save location, use that instead. + if (d->saveLocationId) + { + return PATH_SAVEGAMES / String::format("profile-%08x", d->saveLocationId); + } - return "/home/savegames" / gameId(); + return PATH_SAVEGAMES / gameId(); +} + + +bool GameProfiles::Profile::isSaveLocationEmpty() const +{ + FS::waitForIdle(); + if (const auto *loc = FS::tryLocate(savePath())) + { + return loc->contents().size() == 0; + } + return true; +} + +void GameProfiles::Profile::createSaveLocation() +{ + FS::waitForIdle(); + do + { + d->saveLocationId = randui32(); + } while (FS::exists(savePath())); + Folder &loc = FS::get().makeFolder(savePath()); + LOG_MSG("Created save location %s") << loc.description(); +} + +void GameProfiles::Profile::destroySaveLocation() +{ + if (d->saveLocationId) + { + FS::waitForIdle(); + if (auto *loc = FS::tryLocate(savePath())) + { + LOG_NOTE("Destroying save location %s") << loc->description(); + loc->destroyAllFiles(); + loc->correspondingNativePath().destroy(); + loc->parent()->populate(); + } + d->saveLocationId = 0; + } +} + +void GameProfiles::Profile::checkSaveLocation() const +{ + if (d->saveLocationId && !FS::exists(savePath())) + { + Folder &loc = FS::get().makeFolder(savePath()); + LOG_MSG("Created missing save location %s") << loc.description(); + } } StringList GameProfiles::Profile::allRequiredPackages() const @@ -510,6 +586,10 @@ String GameProfiles::Profile::toInfoSource() const { os << "\n" << VAR_LAST_PLAYED << ": " << d->lastPlayedAt.asText(); } + if (d->saveLocationId) + { + os << "\n" << VAR_SAVE_LOCATION_ID << ": " << String::format("%08x", d->saveLocationId); + } // Additional configuration values (e.g., config for the game to use). if (!d->values.isEmpty()) { diff --git a/doomsday/apps/plugins/common/src/game/gamesession.cpp b/doomsday/apps/plugins/common/src/game/gamesession.cpp index beb4148dc7..9b0b379bbb 100644 --- a/doomsday/apps/plugins/common/src/game/gamesession.cpp +++ b/doomsday/apps/plugins/common/src/game/gamesession.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include "acs/system.h" #include "api_gl.h" @@ -121,7 +122,7 @@ DENG2_PIMPL(GameSession), public GameStateFolder::IMapStateReaderFactory inline String userSavePath(String const &fileName) { DENG_ASSERT(DoomsdayApp::currentGameProfile()); - return DoomsdayApp::currentGameProfile()->savePath() / fileName + ".save"; + return SaveGames::savePath() / fileName + ".save"; } void cleanupInternalSave()