game_save_buffer(buffer)

From ENIGMA
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Description

Serialize the game state (objects, backgrounds and room index) into a buffer

This function has two options of serialization formats: a binary format and a JSON format, the JSON format is the default. If you choose the binary format, the function will serialize (in the following order):

  • The number of active objects
  • The active objects themselves
  • The number of inactive objects
  • The inactive objects themselves
  • A header byte for the enigma::backgrounds AssetArray
  • enigma::backgrounds itself
  • A footer byte for the enigma::backgrounds AssetArray
  • The current room index
  • A SHA1-checksum

The serialization for objects works in the following manner: for each class in the object hierarchy, a leading byte is emitted to identify the object and verify that the data following it is valid. The bytes are written as follows:

  • object_basic: 0xAA
  • object_planar: 0xAB
  • object_timelines: 0xAC
  • object_graphics: 0xAD
  • object_transform: 0xAE
  • object_collisions: 0xAF
  • Any user defined object : 0xBB

These leading bytes are then followed by the data of each class. While the data that is serialized for the inner classes does not change over time in terms of the contained variables, modifications to the user defined objects can lead to generation of classes whose contents do not remain stable over time.

To ensure backwards compatibility of previously saved game state with a future revision of the user-defined object, a symbol table is emitted before each object's data block which maps the name of the serialized variable to the offset from the end of the symbol table where its data is stored. On the C++ side, the compiler emits a table for every object which maps the name of each variable in the object to a function which is responsible for deserializing its state from a byte stream. Then, the intersection of these two tables are taken at runtime, which gives the set of variables that exist both in the current revision of the object and the one serialized as part of the older game state, along with deserialization routines for each variable.

As a proof of concept of implementing serialization and deserialization for an object type, the required routines are defined for the AssetArray and the Background classes to allow serializing and deserializing enigma::backgrounds, which is a AssetArray<Background>. The methods are:

  • std::size_t byte_size() const noexcept - Get the size (in bytes) of the object
  • std::vector<std::size_t> serialize() - Serialize the object into a byte stream
  • std::size_t deserialize_self(std::byte *iter) - Deserialize this from the byte stream and return the total number of bytes read
  • static std::pair<std::size_t, T> deserialize(std::byte *iter) - Deserialize an object from the given byte stream and return it along with its size

Out of these four, byte_size() and deserialize() are automatically picked up by the routines defined in serialization.h. The other methods have to be detected at compile time by the caller, using code like the following:

if constexpr (has_serialize_method_v<T>) {
  for (std::size_t i = 0; i < assets_.size(); i++) {
    std::vector<std::byte> serialized = assets_[i].serialize();
    // ...
  }
} else if constexpr (HAS_INTERNAL_SERIALIZE_FUNCTION()) {
  for (std::size_t i = 0; i < assets_.size(); i++) {
    enigma::bytes_serialization::enigma_serialize(operator[](i), len, result);
    // ...
  }
}

Both of the functions used in the if constexpr blocks are defined in detect_serialization.h. The first one, has_serialize_method_v<T>, checks if the object has a method of the form object.serialize() following the type given before. The second one, HAS_INTERNAL_SERIALIZE_FUNCTION(), checks if the enigma::serialize function is invocable with the object's type. Together, these allow handling both cases of object.serialize() and serialize(object).

After all the state is serialized, the SHA-1 checksum of the buffer's data is calculated and written at the end.

An example of the serialized state may look like the following:

┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 00 00 00 00 00 00 00 01 ┊ aa 00 01 86 a1 00 00 00 │0000000•┊×0•××000│
│00000010│ 00 ab 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │0×000000┊00000000│
│00000020│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │00000000┊00000000│
│*       │                         ┊                         │        ┊        │
│00000060│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 40 │00000000┊0000000@│
│00000070│ 70 e0 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 ac │p×000000┊0000000×│
│00000080│ 00 00 00 00 00 00 00 00 ┊ ff ff ff ff 00 3f 80 00 │00000000┊××××0?×0│
│00000090│ 00 00 00 00 00 00 ad ff ┊ ff ff ff 00 00 00 00 3f │000000××┊×××0000?│
│000000a0│ 80 00 00 00 bf f0 00 00 ┊ 00 00 00 00 00 00 00 00 │×000××00┊00000000│
│000000b0│ 00 00 00 00 00 01 3f 80 ┊ 00 00 3f 80 00 00 00 00 │00000•?×┊00?×0000│
│000000c0│ 00 00 ae 3f f0 00 00 00 ┊ 00 00 00 00 ff ff ff af │00×?×000┊0000××××│
│000000d0│ ff ff ff ff 00 ff ff ff ┊ ff 3f 80 00 00 3f 80 00 │××××0×××┊×?×00?×0│
│000000e0│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 bb 00 00 │00000000┊00000×00│
│000000f0│ 00 00 00 00 00 02 00 00 ┊ 00 00 00 00 00 06 66 6f │00000•00┊00000•fo│
│00000100│ 6f 62 61 72 00 00 00 00 ┊ 00 00 00 39 00 00 00 00 │obar0000┊00090000│
│00000110│ 00 00 00 06 62 61 72 66 ┊ 6f 6f 00 00 00 00 00 00 │000•barf┊oo000000│
│00000120│ 00 00 00 00 00 00 00 00 ┊ 00 72 00 40 20 00 00 00 │00000000┊0r0@ 000│
│00000130│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │00000000┊00000000│
│*       │                         ┊                         │        ┊        │
│00000160│ 00 00 00 00 40 18 00 00 ┊ 00 00 00 00 00 00 00 00 │0000@•00┊00000000│
│00000170│ 00 00 00 00 00 00 00 00 ┊ 00 00 00 00 00 00 00 00 │00000000┊00000000│
│*       │                         ┊                         │        ┊        │
│000001a0│ 00 00 00 00 ee 00 00 00 ┊ 00 00 00 00 00 ef 00 00 │0000×000┊00000×00│
│000001b0│ 00 00 df f2 d6 04 a3 26 ┊ f5 ff 44 d2 0e 25 34 ec │00××ו×&┊××Dו%4×│
│000001c0│ 85 c7 7f c5 91 0c       ┊                         │×ו××_  ┊        │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

If you choose the JSON format, the function will serialize (in the following order):

  • The active objects
  • The inactive objects
  • enigma::backgrounds
  • The current room index
  • A SHA1-checksum

The serialization for objects works in the following manner: for each class in the object hierarchy, a leading key-value pair is emitted to identify the object and verify that the data following it is valid. The key is "object_type" and the values are:

  • object_basic: "object_basic"
  • object_planar: "object_planar"
  • object_timelines: "object_timelines"
  • object_graphics: "object_graphics"
  • object_transform: "object_transform"
  • object_collisions: "object_collisions"
  • Any user defined object: "locals", but this one is the key for the object's local variables

These leading key-value pairs are then followed by the data of each class.

An example of the serialized state may look like the following:

{
"instance_list": [
  {
    "obj": {
      "event_parent": {
        "object_type": "object_collisions",
        "parent": {
          "object_type": "object_transform",
          "parent": {
            "object_type": "object_graphics",
            "parent": {
              "object_type": "object_timelines",
              "parent": {
                "object_type": "object_planar",
                "parent": {
                  "object_type": "object_basic",
                  "id": 100001,
                  "object_index": 0
                },
                "x": 0,
                "y": 0,
                "xprevious": 0,
                "yprevious": 0,
                "xstart": 0,
                "ystart": 0,
                "persistent": "false",
                "direction": {
                  "type": "real",
                  "value": 0
                },
                "speed": {
                  "type": "real",
                  "value": 0
                },
                "hspeed": {
                  "type": "real",
                  "value": 0
                },
                "vspeed": {
                  "type": "real",
                  "value": 0
                },
                "gravity": 0,
                "gravity_direction": 270,
                "friction": 0
              },
              "timeline_moments_maps": [],
              "timeline_index": -1,
              "timeline_running": "false",
              "timeline_speed": 1,
              "timeline_position": 0,
              "timeline_loop": "false"
            },
            "sprite_index": -1,
            "image_index": 0,
            "image_speed": 1,
            "image_single": {
              "type": "real",
              "value": -1
            },
            "depth": {
              "type": "real",
              "value": 0
            },
            "visible": "true",
            "image_xscale": 1,
            "image_yscale": 1,
            "image_angle": 0
          },
          "image_alpha": 1,
          "image_blend": 16777215
        },
        "mask_index": -1,
        "solid": "false",
        "polygon_index": -1,
        "polygon_xscale": 1,
        "polygon_yscale": 1,
        "polygon_angle": 0
      },
      "vmap": {}
    },
    "locals": [
      {
        "name": "mydouble",
        "data": {
          "variant": {
            "type": "real",
            "value": 1.2345
          },
          "array1d": [],
          "array2d": []
        }
      },
      {
        "name": "myuint8_t",
        "data": {
          "variant": {
            "type": "real",
            "value": 255
          },
          "array1d": [],
          "array2d": []
        }
      }
    ]
  }
],
"instance_deactivated_list": [],
"backgrounds": [],
"room_index": 0
}

Note: It is only to make it simple for beginners to add saving and loading to their game but since it dumps the entire game state and all variables it can be rather inefficient.

Also see:

Parameters

Parameter Data Type Description
buffer buffer_t The buffer to store the game state into
backend enum SerializationBackend The format to store the game state with

Return Values

void: This function does not return anything.

Example Call

// demonstrates saving the game state to a buffer with JSON format
game_save_buffer(mybuffer, SerializationBackend::JSON);

// demonstrates saving the game state to a buffer with the Bytes format
game_save_buffer(mybuffer, SerializationBackend::Binary);