Локальное хранилище 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 даже в пределах сессии сохраняться не будут.
- Документация по Emscripten File System API.
- Документация на localStorage.