Revision control

++ // Copyright (C) 2016 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
++
++ /*!
++ \example serialization/savegame
++ \examplecategory {Data Processing & I/O}
++ \title Saving and Loading a Game
++ \ingroup corelib_examples
++
++ \brief How to save and load a game using Qt's JSON or CBOR classes.
++
++ Many games provide save functionality, so that the player's progress through
++ the game can be saved and loaded at a later time. The process of saving a
++ game generally involves serializing each game object's member variables to a
++ file. Many formats can be used for this purpose, one of which is JSON. With
++ QJsonDocument, you also have the ability to serialize a document in a \l
++ {RFC 7049} {CBOR} format, which is great if you don't want the save file to
++ be easy to read (but see \l {Parsing and displaying CBOR data} for how it \e
++ can be read), or if you need to keep the file size down.
++
++ In this example, we'll demonstrate how to save and load a simple game to
++ and from JSON and binary formats.
++
++ \section1 The Character Class
++
++ The Character class represents a non-player character (NPC) in our game, and
++ stores the player's name, level, and class type.
++
++ It provides static fromJson() and non-static toJson() functions to
++ serialise itself.
++
++ \note This pattern (fromJson()/toJson()) works because QJsonObjects can be
++ constructed independent of an owning QJsonDocument, and because the data
++ types being (de)serialized here are value types, so can be copied. When
++ serializing to another format — for example XML or QDataStream, which require passing
++ a document-like object — or when the object identity is important (QObject
++ subclasses, for example), other patterns may be more suitable. See the
++ \l{dombookmarks} example for XML, and the implementation of
++ \l QListWidgetItem::read() and \l QListWidgetItem::write()
++ for idiomatic QDataStream serialization. The \c{print()} functions in this example
++ are good examples of QTextStream serialization, even though they, of course, lack
++ the deserialization side.
++
++ \snippet serialization/savegame/character.h 0
++
++ Of particular interest to us are the fromJson() and toJson() function
++ implementations:
++
++ \snippet serialization/savegame/character.cpp fromJson
++
++ In the fromJson() function, we construct a local \c result Character object
++ and assign \c{result}'s members values from the QJsonObject argument. You
++ can use either \l QJsonObject::operator[]() or QJsonObject::value() to
++ access values within the JSON object; both are const functions and return
++ QJsonValue::Undefined if the key is invalid. In particular, the \c{is...}
++ functions (for example \l QJsonValue::isString(), \l
++ QJsonValue::isDouble()) return \c false for QJsonValue::Undefined, so we
++ can check for existence as well as the correct type in a single lookup.
++
++ If a value does not exist in the JSON object, or has the wrong type, we
++ don't write to the corresponding \c result member, either, thereby
++ preserving any values the default constructor may have set. This means
++ default values are centrally defined in one location (the default
++ constructor) and need not be repeated in serialisation code
++
++ Observe the use of
++ {C++17 if-with-initializer} to separate scoping and checking of the variable \c v.
++ This means we can keep the variable name short, because its scope is limited.
++
++ Compare that to the naïve approach using \c QJsonObject::contains():
++
++ \badcode
++ if (json.contains("name") && json["name"].isString())
++ result.mName = json["name"].toString();
++ \endcode
++
++ which, beside being less readable, requires a total of three lookups (no,
++ the compiler will \e not optimize these into one), so is three times
++ slower and repeats \c{"name"} three times (violating the DRY principle).
++
++ \snippet serialization/savegame/character.cpp toJson
++
++ In the toJson() function, we do the reverse of the fromJson() function;
++ assign values from the Character object to a new JSON object we then
++ return. As with accessing values, there are two ways to set values on a
++ QJsonObject: \l QJsonObject::operator[]() and \l QJsonObject::insert().
++ Both will override any existing value at the given key.
++
++ \section1 The Level Class
++
++ \snippet serialization/savegame/level.h 0
++
++ We want the levels in our game to each have several NPCs, so we keep a QList
++ of Character objects. We also provide the familiar fromJson() and toJson()
++ functions.
++
++ \snippet serialization/savegame/level.cpp fromJson
++
++ Containers can be written to and read from JSON using QJsonArray. In our
++ case, we construct a QJsonArray from the value associated with the key
++ \c "npcs". Then, for each QJsonValue element in the array, we call
++ toObject() to get the Character's JSON object. Character::fromJson() can
++ then turn that QJSonObject into a Character object to append to our NPC array.
++
++ \note \l{Container Classes}{Associate containers} can be written by storing
++ the key in each value object (if it's not already). With this approach, the
++ container is stored as a regular array of objects, but the index of each
++ element is used as the key to construct the container when reading it back
++ in.
++
++ \snippet serialization/savegame/level.cpp toJson
++
++ Again, the toJson() function is similar to the fromJson() function, except
++ reversed.
++
++ \section1 The Game Class
++
++ Having established the Character and Level classes, we can move on to
++ the Game class:
++
++ \snippet serialization/savegame/game.h 0
++
++ First of all, we define the \c SaveFormat enum. This will allow us to
++ specify the format in which the game should be saved: \c Json or \c Binary.
++
++ Next, we provide accessors for the player and levels. We then expose three
++ functions: newGame(), saveGame() and loadGame().
++
++ The read() and toJson() functions are used by saveGame() and loadGame().
++
++ \div{class="admonition note"}\b{Note:}
++ Despite \c Game being a value class, we assume that the author wants a game to have
++ identity, much like your main window would have. We therefore don't use a
++ static fromJson() function, which would create a new object, but a read()
++ function we can call on existing objects. There's a 1:1 correspondence
++ between read() and fromJson(), in that one can be implemented in terms of
++ the other:
++
++ \code
++ void read(const QJsonObject &json) { *this = fromJson(json); }
++ static Game fromObject(const QJsonObject &json) { Game g; g.read(json); return g; }
++ \endcode
++
++ We just use what's more convenient for callers of the functions.
++ \enddiv
++
++ \snippet serialization/savegame/game.cpp newGame
++
++ To setup a new game, we create the player and populate the levels and their
++ NPCs.
++
++ \snippet serialization/savegame/game.cpp read
++
++ The read() function starts by replacing the player with the
++ one read from JSON. We then clear() the level array so that calling
++ loadGame() on the same Game object twice doesn't result in old levels
++ hanging around.
++
++ We then populate the level array by reading each Level from a QJsonArray.
++
++ \snippet serialization/savegame/game.cpp toJson
++
++ Writing the game to JSON is similar to writing a level.
++
++ \snippet serialization/savegame/game.cpp loadGame
++
++ When loading a saved game in loadGame(), the first thing we do is open the
++ save file based on which format it was saved to; \c "save.json" for JSON,
++ and \c "save.dat" for CBOR. We print a warning and return \c false if the
++ file couldn't be opened.
++
++ Since \l QJsonDocument::fromJson() and \l QCborValue::fromCbor() both take
++ a QByteArray, we can read the entire contents of the save file into one,
++ regardless of the save format.
++
++ After constructing the QJsonDocument, we instruct the Game object to read
++ itself and then return \c true to indicate success.
++
++ \snippet serialization/savegame/game.cpp saveGame
++
++ Not surprisingly, saveGame() looks very much like loadGame(). We determine
++ the file extension based on the format, print a warning and return \c false
++ if the opening of the file fails. We then write the Game object to a
++ QJsonObject. To save the game in the format that was specified, we
++ convert the JSON object into either a QJsonDocument for a subsequent
++ QJsonDocument::toJson() call, or a QCborValue for QCborValue::toCbor().
++
++ \section1 Tying It All Together
++
++ We are now ready to enter main():
++
++ \snippet serialization/savegame/main.cpp 0
++
++ Since we're only interested in demonstrating \e serialization of a game with
++ JSON, our game is not actually playable. Therefore, we only need
++ QCoreApplication and have no event loop. On application start-up we parse
++ the command-line arguments to decide how to start the game. For the first
++ argument the options "new" (default) and "load" are available. When "new"
++ is specified a new game will be generated, and when "load" is specified a
++ previously saved game will be loaded in. For the second argument
++ "json" (default) and "binary" are available as options. This argument will
++ decide which file is saved to and/or loaded from. We then move ahead and
++ assume that the player had a great time and made lots of progress, altering
++ the internal state of our Character, Level and Game objects.
++
++ \snippet serialization/savegame/main.cpp 1
++
++ When the player has finished, we save their game. For demonstration
++ purposes, we can serialize to either JSON or CBOR. You can examine the
++ contents of the files in the same directory as the executable (or re-run
++ the example, making sure to also specify the "load" option), although the
++ binary save file will contain some garbage characters (which is normal).
++
++ That concludes our example. As you can see, serialization with Qt's JSON
++ classes is very simple and convenient. The advantages of using QJsonDocument
++ and friends over QDataStream, for example, is that you not only get
++ human-readable JSON files, but you also have the option to use a binary
++ format if it's required, \e without rewriting any code.
++
++ \sa {JSON Support in Qt}, {CBOR Support in Qt}, {Data Input Output}
++ */