Бинарный слот и сериализация Variant
Сохранение Vector3 и целых чисел с заголовком, проверкой длины и типов.
Когда нужен бинарный формат
FileAccess.StoreVar() сохраняет типы Variant, включая Vector3, массивы и словари. GetVar() читает одну такую запись. Это удобно для локального слота; для обмена с веб-сервисом или другой программой обычно проще JSON с явной схемой. Ни один формат сам по себе не восстанавливает живые C#-объекты и ссылки уровня.
Полный код слота
Добавьте BinarySlot.cs в C#-проект. Вызовите Save("user://binary-slot.dat", new Vector3(2, 1, -3), 80), проверьте Error.Ok, затем Load() с тем же путём. Ожидаются те же координаты и здоровье 80. Родительская папка должна уже существовать; сам корень user:// создаёт приложение.
BinarySlot.cs
using Reckfell;
public static class BinarySlot
{
private const uint Magic = 0x52465331;
private const uint Format = 1;
private const ulong MaxBytes = 64 * 1024;
public static Error Save(string path, Vector3 position, int health)
{
if (!position.IsFinite() || health < 0 || health > 100)
return Error.InvalidData;
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
if (file is null)
return FileAccess.GetOpenError();
var payload = new Reckfell.Collections.Dictionary
{
["position"] = position,
["health"] = health
};
return file.Store32(Magic) && file.Store32(Format) &&
file.StoreVar(payload, fullObjects: false)
? file.GetError() : Error.FileCantWrite;
}
public static Error Load(string path, out Vector3 position, out int health)
{
position = Vector3.Zero;
health = 0;
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
if (file is null)
return FileAccess.GetOpenError();
if (file.GetLength() < 12 || file.GetLength() > MaxBytes)
return Error.FileCorrupt;
if (file.Get32() != Magic || file.Get32() != Format)
return Error.InvalidData;
uint payloadLength = file.Get32();
if (payloadLength != file.GetLength() - 12)
return Error.FileCorrupt;
file.Seek(8);
Variant value = file.GetVar(allowObjects: false);
if (value.VariantType != Variant.Type.Dictionary)
return Error.InvalidData;
var data = value.AsReckfellDictionary();
if (!data.TryGetValue("position", out Variant p) ||
!data.TryGetValue("health", out Variant h) ||
p.VariantType != Variant.Type.Vector3 || h.VariantType != Variant.Type.Int)
return Error.InvalidData;
Vector3 loadedPosition = p.AsVector3();
long loadedHealth = h.AsInt64();
if (!loadedPosition.IsFinite() || loadedHealth < 0 || loadedHealth > 100)
return Error.InvalidData;
position = loadedPosition;
health = (int)loadedHealth;
return Error.Ok;
}
}Методы возвращают значения через out только после проверки. При ошибке это Vector3.Zero и 0; применять их к персонажу можно только после Error.Ok. Код записывает файл напрямую: для последнего рабочего прогресса добавьте временный файл и резервную копию.
Что лежит в файле
| Смещение | Содержимое |
|---|---|
| 0–3 | Собственная сигнатура 0x52465331 |
| 4–7 | Версия схемы 1 |
| 8–11 | Длина записи, которую добавляет StoreVar() |
| С 12 | Закодированный словарь Variant |
Новый FileAccess использует little-endian для Store32(). Внутренний формат Variant имеет собственные правила; BigEndian не превращает его в произвольный сетевой протокол. Не разбирайте его по догадкам и не считайте номер схемы равным версии движка.
Проверка длины перед GetVar() не даёт прочитать заявленный огромный внешний блок или незаметно принять добавленный хвост. Затем проверяются корневой словарь, поля, точные типы и диапазоны. Лимит этого слота — 64 КиБ. Это не общий декодер произвольных недоверенных файлов: повреждённое внутреннее содержимое может вызвать диагностическое сообщение самого декодера.
Объекты и формат буфера
Оставьте fullObjects: false и allowObjects: false. Варианты с объектами могут создавать экземпляры при декодировании; они не подходят для сохранения постоянных ссылок игры или чтения файла неизвестного происхождения. Сохраняйте ID и восстанавливайте ссылки явно.
ReckfellApi.VarToBytes() и BytesToVar() работают с одной записью в памяти. У StoreVar() перед этой записью есть 4-байтовая длина; поэтому нельзя скормить BytesToVar() весь файл StoreVar() без снятия обрамления. Дополнительные 8 байт заголовка выше принадлежат нашему слоту, а не API.
При несовпадении версии не пытайтесь читать файл как текущую схему. Сохраните его для восстановления и выполните отдельную миграцию. Проверяйте загрузку после полного перезапуска процесса, а не только сразу после Save().