Skip to content

Serialization

Ryan edited this page May 9, 2019 · 12 revisions

Summary

SKSESerializationInterface can be accessed from SKSEInterface by calling SKSEInterface::QueryInterface(kInterface_Serialization). The serialization interface allows plugin authors to serialize data to the SKSE co-save. This can be useful if the author wishes to persist data between runs of the executable.

Class Interface

  • version: This is the version of the exported interface. Plugin authors should assert on this field if they require a certain version.
  • SetUniqueID: This sets a unique signature for your plugin, which SKSE will use to call your plugin when serializing to/from the co-save. Give it a four letter signature that's a shorthand for your plugin name (i.e. 'PLGN').
  • SetRevertCallback:
  • SetSaveCallback: This assigns the function that will be called whenever the game saves.
  • SetLoadCallback: This assigns the function that will be called whenever the game loads.
  • SetFormDeleteCallback: This assigns the function that will be called whenever a form is deleted.
  • WriteRecord: This writes the buffer buf with the number of bytes length to the co-save under the signature type with the version version.
  • OpenRecord: This opens a record in the co-save with the given signature type and the version version. It returns a boolean indicating success.
  • WriteRecordData: This writes the buffer buf with the number of bytes length to the co-save. It returns a boolean indicating success.
  • GetNextRecordInfo: This reads the next record's info from the co-save, storing the signature in type, the version in version, and the number of bytes in length. It returns a boolean indicating success.
  • ReadRecordData: This reads the specified number of bytes length into the given buffer buf from the co-save. It returns the number of bytes actually read.
  • ResolveHandle: This takes a virtual machine handle handle as it was when the save was made and writes the handle as it is when the save is loaded into handleOut. It returns a boolean indicating success.
  • ResolveFormId: This takes a formID formId as it was when the save was made and writes the formID as it is when the save is loaded into formIdOut. It returns a boolean indicating success.

Usage

Authors should define one function for each callback they wish to register, matching the declared typedef for EventCallback. In these callbacks, authors can use the passed SKSESerializationInterface* to perform the serialization tasks that are required for their plugin.

  • The Save Callback
#include "skse64/PluginAPI.h"  // SKSESerializationInterface
#include <vector>  // vector

void SaveCallback(SKSESerializationInterface* a_intfc)
{
    int num = 42;
    std::vector<int> arr;
    for (int i = 0; i < 10; ++i) {
        arr.push_back(i);
    }
    
    if (!a_intfc->WriteRecord('NUM_', 1, &num, sizeof(num))) {
        _ERROR("Failed to serialize num!");
    }
    
    if (!a_intfc->OpenRecord('ARR_', 1)) {
        _ERROR("Failed to open record for arr!");
    } else {
        UInt32 size = arr.size();
        if (!a_intfc->WriteRecordData(&size, sizeof(size))) {
            _ERROR("Failed to write size of arr!");
        } else {
            for (auto& elem : arr) {
                if (!a_intfc->WriteRecordData(&elem, sizeof(elem))) {
                    _ERROR("Failed to write data for elem!");
                    break;
                }
            }
        }
    }
}
  • The Load Callback
#include "skse64/PluginAPI.h"  // SKSESerializationInterface
#include <vector>  // vector

void LoadCallback(SKSESerializationInterface* a_intfc)
{
    int num;
    std::vector<int> arr;

    UInt32 type;
    UInt32 version;
    UInt32 length;
    while (a_intfc->GetNextRecordInfo(&type, &version, &length)) {
        switch (type) {
        case 'NUM_':
            if (!a_intfc->ReadRecordData(&num, sizeof(num))) {
                _ERROR("Failed to load num!");
            }
            break;
        case 'ARR_':
            length = a_intfc->ReadRecordData(&length, sizeof(length));
            for (UInt32 i = 0; i < length; ++i) {
                int elem;
                if (!a_intfc->ReadRecordData(&elem, sizeof(elem))) {
                    _ERROR("Failed to load elem!");
                    break;
                } else {
                    arr.push_back(elem);
                }
            }
            break;
        default:
            _ERROR("Unrecognized signature type!");
            break;
        }
    }
}

You'll notice in these callbacks, that we aren't doing something like a_intfc->WriteRecordData(&arr, arr.size());. The problem with this statement is that the pointer we passed as the buffer points to the std::vector structure and not the underlying data itself, where the int's are stored. This approach also doesn't take into account the size of an int. Remember that we're telling the interface to serialize a number of bytes from the provided buffer, so we must convert out int into a size using sizeof and multiply that by the number of elements in the container. A correct way to serialize the vector in a single statement would be a_intfc->WriteRecordData(arr.data(), arr.size() * sizeof(int));, however, this approach constrains the layout of our data. Consider a structure struct Data { int num1, num2, num3; };, which we directly serialize using the preceding statement. This would work fine, until we wanted to reorder our member variables so that our structure now looks like struct Data { int num3, num2, num1; };. Now when we go to load our data, we will be loading what should be num1 into num3 and vice-versa. Thus, it's better to serialize your data one element at a time, so as to give yourself the flexibility to adjust the layout of your structures.

Clone this wiki locally