Skip to content

Commit

Permalink
Game Save|Resources|libdoomsday: Savegames know which packages were i…
Browse files Browse the repository at this point in the history
…n use

The packages used when a savegame is created are included in the
save metadata. The Home UI can then match these against the game
to see which saves are usable with which profiles.

IssueID #916
  • Loading branch information
skyjake committed Jul 6, 2016
1 parent c29619d commit cfc26f6
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 127 deletions.
1 change: 1 addition & 0 deletions doomsday/apps/client/include/ui/savedsessionlistdata.h
Expand Up @@ -42,6 +42,7 @@ class SavedSessionListData : public de::ui::ListData
de::String gameId() const;
de::String savePath() const;
de::String name() const;
de::StringList loadedPackages() const;

void fileBeingDeleted(de::File const &);
};
Expand Down
27 changes: 23 additions & 4 deletions doomsday/apps/client/src/ui/home/gamepanelbuttonwidget.cpp
Expand Up @@ -59,12 +59,31 @@ DENG_GUI_PIMPL(GamePanelButtonWidget)
// Only show the savegames relevant for this game.
savedItems.setFilter([this] (ui::Item const &it)
{
// User-created profiles currently have no saves associated with them.
if (gameProfile.isUserCreated()) return false;

// Only saved sessions for this game are to be included.
auto const &item = it.as<SavedSessionListData::SaveItem>();
return item.gameId() == gameProfile.game();
if (item.gameId() != gameProfile.game())
{
return false;
}

StringList const savePacks = item.loadedPackages();

// Fallback for older saves without package metadata.
if (savePacks.isEmpty())
{
// Show under the built-in profile.
return !gameProfile.isUserCreated();
}

// Check if the packages used in the savegame are in conflict with the
// profile's packages.
if (!gameProfile.isCompatibleWithPackages(savePacks))
{
return false;
}

// Yep, seems fine.
return true;
});

packagesButton = new PackagesButtonWidget;
Expand Down
17 changes: 17 additions & 0 deletions doomsday/apps/client/src/ui/savedsessionlistdata.cpp
Expand Up @@ -138,6 +138,23 @@ String SavedSessionListData::SaveItem::name() const
return "";
}

StringList SavedSessionListData::SaveItem::loadedPackages() const
{
StringList ids;
if (session)
{
Record const &meta = session->metadata();
if (meta.has("packages"))
{
for (Value const *pkg : meta.geta("packages").elements())
{
ids << pkg->asText();
}
}
}
return ids;
}

void SavedSessionListData::SaveItem::fileBeingDeleted(File const &)
{
session = nullptr;
Expand Down
1 change: 1 addition & 0 deletions doomsday/apps/libdoomsday/include/doomsday/DataBundle
@@ -0,0 +1 @@
#include "resource/databundle.h"
4 changes: 4 additions & 0 deletions doomsday/apps/libdoomsday/include/doomsday/gameprofiles.h
Expand Up @@ -54,6 +54,10 @@ class LIBDOOMSDAY_PUBLIC GameProfiles : public de::Profiles
*/
de::StringList allRequiredPackages() const;

de::StringList packagesIncludedInSavegames() const;

bool isCompatibleWithPackages(de::StringList const &ids) const;

bool isPlayable() const;

void loadPackages() const;
Expand Down
1 change: 1 addition & 0 deletions doomsday/apps/libdoomsday/include/doomsday/res/Bundles
@@ -0,0 +1 @@
#include "../resource/bundles.h"
12 changes: 12 additions & 0 deletions doomsday/apps/libdoomsday/include/doomsday/savedsession.h
Expand Up @@ -196,6 +196,18 @@ class LIBDOOMSDAY_PUBLIC SavedSession : public de::ArchiveFolder
}

public:
/**
* Determines if informatino about a package should be included in savegame files.
* Packages that alter gameplay or game objects must be included, while purely visual
* content does not.
*
* @param packageId Package identifier.
*
* @return @c true, if the package must be included in the list of packages in
* savegame metadata. @c false, if it is omitted.
*/
static bool isIncludedInSavegames(de::String const &packageId);

/**
* Utility for composing the full path of a state data file in the saved session.
*
Expand Down
2 changes: 1 addition & 1 deletion doomsday/apps/libdoomsday/src/doomsdayapp.cpp
Expand Up @@ -658,7 +658,7 @@ void DoomsdayApp::makeGameCurrent(GameProfile const &profile)
if (!newGame.isNull())
{
// Remember what was loaded beforehand.
d->preGamePackages = PackageLoader::get().loadedPackagesInOrder();
d->preGamePackages = PackageLoader::get().loadedPackagesInOrder(PackageLoader::NonVersioned);
}

profile.loadPackages();
Expand Down
32 changes: 32 additions & 0 deletions doomsday/apps/libdoomsday/src/gameprofiles.cpp
Expand Up @@ -19,6 +19,7 @@
#include "doomsday/GameProfiles"
#include "doomsday/Games"
#include "doomsday/DoomsdayApp"
#include "doomsday/SavedSession"

#include <de/App>
#include <de/PackageLoader>
Expand Down Expand Up @@ -169,6 +170,37 @@ StringList GameProfiles::Profile::allRequiredPackages() const
return DoomsdayApp::games()[d->gameId].requiredPackages() + d->packages;
}

StringList GameProfiles::Profile::packagesIncludedInSavegames() const
{
StringList ids = allRequiredPackages();
QMutableListIterator<String> iter(ids);
while (iter.hasNext())
{
if (!SavedSession::isIncludedInSavegames(iter.next()))
{
iter.remove();
}
}
return ids;
}

bool GameProfiles::Profile::isCompatibleWithPackages(StringList const &ids) const
{
StringList packs = packagesIncludedInSavegames();
if (packs.size() != ids.size()) return false;

// The package lists must match order and IDs, but currently we ignore the
// versions.
for (int i = 0; i < packs.size(); ++i)
{
if (Package::split(packs.at(i)).first != Package::split(ids.at(i)).first)
{
return false;
}
}
return true;
}

bool GameProfiles::Profile::isPlayable() const
{
for (String const &pkg : allRequiredPackages())
Expand Down
49 changes: 39 additions & 10 deletions doomsday/apps/libdoomsday/src/savedsession.cpp
Expand Up @@ -18,6 +18,7 @@

#include "doomsday/savedsession.h"
#include "doomsday/Session"
#include "doomsday/DataBundle"

#include <de/App>
#include <de/ArrayValue>
Expand Down Expand Up @@ -133,11 +134,28 @@ void SavedSession::Metadata::parse(String const &source)
}
}

if (info.root().contains("packages"))
{
Info::ListElement const &list = info.root().find("packages")->as<Info::ListElement>();
auto *pkgs = new ArrayValue;
for (auto const &value : list.values())
{
*pkgs << new TextValue(value.text);
}
set("packages", pkgs);
}
else
{
set("packages", new ArrayValue);
}

// Ensure we have a valid description.
if (gets("userDescription").isEmpty())
{
set("userDescription", "UNNAMED");
}

//qDebug() << "Parsed save metadata:\n" << asText();
}
catch (Error const &er)
{
Expand Down Expand Up @@ -198,6 +216,10 @@ String SavedSession::Metadata::asTextWithInfoSyntax() const
os.setCodec("UTF-8");

if (has("gameIdentityKey")) os << "gameIdentityKey: " << gets("gameIdentityKey");
if (has("packages"))
{
os << "\npackages " << geta("packages").asTextUsingInfoSyntax();
}
if (has("episode")) os << "\nepisode: " << gets("episode");
if (has("mapTime")) os << "\nmapTime: " << String::number(geti("mapTime"));
if (has("mapUri")) os << "\nmapUri: " << gets("mapUri");
Expand All @@ -215,15 +237,7 @@ String SavedSession::Metadata::asTextWithInfoSyntax() const
}
if (has("visitedMaps"))
{
os << "\nvisitedMaps <";
ArrayValue const &visitedMapsArray = geta("visitedMaps");
DENG2_FOR_EACH_CONST(ArrayValue::Elements, i, visitedMapsArray.elements())
{
Value const *value = *i;
if (i != visitedMapsArray.elements().begin()) os << ", ";
os << "\"" << String(value->as<TextValue>()) << "\"";
}
os << ">";
os << "\nvisitedMaps " << geta("visitedMaps").asTextUsingInfoSyntax();
}
if (has("sessionId")) os << "\nsessionId: " << String::number(geti("sessionId"));
if (has("userDescription")) os << "\nuserDescription: " << gets("userDescription");
Expand All @@ -242,7 +256,7 @@ String SavedSession::Metadata::asTextWithInfoSyntax() const
valueAsText = "\"" + valueAsText.replace("\"", "''") + "\"";
}
os << "\n " << BLOCK_GAMERULE << " \"" << i.key() << "\""
<< " { value= " << valueAsText << " }";
<< " { value = " << valueAsText << " }";
}

os << "\n}";
Expand Down Expand Up @@ -385,6 +399,20 @@ String SavedSession::stateFilePath(String const &path) //static
return "";
}

bool SavedSession::isIncludedInSavegames(String const &packageId) // static
{
/**
* @todo The rules here could be more sophisticated when it comes to checking what
* exactly does a data bundle contain. Also, packages should be checked for any
* gameplay-affecting assets. (2016-07-06: Currently there are none.)
*/
if (DataBundle::bundleForPackage(packageId))
{
return true;
}
return false;
}

File *SavedSession::Interpreter::interpretFile(File *sourceData) const
{
try
Expand Down Expand Up @@ -415,3 +443,4 @@ File *SavedSession::Interpreter::interpretFile(File *sourceData) const
}
return nullptr;
}

0 comments on commit cfc26f6

Please sign in to comment.