Emscripten logo

Локальное хранилище IDBFS отлично подходит для хранения профайла игры между сессиями. Позволяет хранить относительно большие файлы и с ним удобно работать благодаря реализованным fopen/fread/fwrite/fclose в Emscritpen.

Но из-за политики безопасности браузеров это хранилище не всегда доступно. При попытке выполнить FS.syncfs() из игры, запущенной в iframe на другом домене получим ошибку:

SecurityError: IDBFactory.open() called in an invalid security context

Печально, но решаемо (про исключение чуть ниже). Политики безопасности браузера не запрещают нам использовать window.localStorage.
Объединяем вместе IDBFS и localStorage:

FS.mkdir(profilePath);
FS.mount(IDBFS, {}, profilePath);
FS.syncfs(true, function(err) {
   if (err) {
        var storage = window.localStorage.getItem(profilePath);
        if (storage) {
            Module.ccall('has_data'...);
        } else {
            Module.ccall('empty_data'...);
        }
    } else {
        Module.ccall('idbfs_available'...);
    }
});

Для хранения в localStorage произвольных файлов можно использовать JSON как контейнер key-value. Где key – это произвольное имя файла, а value – произвольное содержимое файла закодированное в BASE64.

{
   "arbitrary_file_name": "file_content_in_base64",
   "another_file_name": "another_file_content_in_base64"
}

Для работы с хранилищем и файлами в моем движке реализованы абстракции.

Интерфейс хранилища выглядит так:

class ageStorage
{
public:
   virtual ~ageStorage() = default;

   enum class State
   {
      InSync,
      Ready,
      NotAvailable
   };

   virtual State getState() const = 0;
   virtual ageInputStream* open(const char* path) = 0;
   virtual ageOutputStream* create(const char* path) = 0;
};

Интерфейс input stream:

class ageInputStream
{
public:
   virtual ~ageInputStream() = default;
   virtual uint32_t getSize() const = 0;
   virtual uint32_t getPosition() const = 0;

   enum class SeekMode
   {
      Begin = 0,
      Current,
      End
   };

   virtual bool seek(SeekMode mode, uint32_t offset) = 0;
   virtual uint32_t read(void* buffer, uint32_t count) = 0;
};

А интерфейс output stream совсем простой:

class ageOutputStream
{
public:
   virtual ~ageOutputStream() = default;
   virtual uint32_t write(const void* buffer, uint32_t count) = 0;
};

Все, что нужно приложению – узнать доступно ли уже хранилище, т.к. хранилище может быть размещено не только локально, но и где-то в облаке. Или синхронизация хранилища может занимать какое-то время. А стандартные файловые операции и вовсе могут отсутствовать.

Для чтения данных из фала на вебе или при работе с удаленной ФС с помощью метода ageStorage::open(…) получаем ageInputMemoryStream, в который за кадром переданы извлеченные и декодированные из JSON данные.

Запись данных в файл выполняется с помощью класса ageOutputMemoryStream. Для его создания есть метод ageStorage::create(…). При разрушении output memory stream будет вызван специальный internalFlush(). На вебе internalFlush() выполнит кодирование данных с помощью BASE64, разместит их в общем JSON и синхронизирует с реальным хранилищем.

ВАЖНО. Этот метод не будет работать в Safari, в случае CORS. Например когда игра грузится с домена game.com внутри iframe в домене portal.com. Вариант с cookies тоже не поможет – Safari все равно будет использовать песочницу. При этом localStorage будет доступен, но содержимое будет потеряно при закрытии вкладки или браузера, а cookies даже в пределах сессии сохраняться не будут.