Binary slots and Variant serialization
Save Vector3 and integers with a header, length and type validation.
When binary data helps
FileAccess.StoreVar() preserves Variant types, including Vector3, arrays and dictionaries. GetVar() reads one such record. This suits a local slot; JSON with an explicit schema is usually simpler for a web service or another program. Neither format automatically restores live C# objects or level references.
Complete slot code
Add BinarySlot.cs to the C# project. Call Save("user://binary-slot.dat", new Vector3(2, 1, -3), 80), check Error.Ok, then Load() with the same path. Expect the same coordinates and health 80. The parent directory must already exist; the application creates the user:// root.
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;
}
}The methods assign restored out values only after validation. On failure these are Vector3.Zero and 0; apply them only after Error.Ok. This writes the file directly. Add a temporary file and backup to preserve the last working progress.
File layout
| Offset | Contents |
|---|---|
| 0–3 | Our signature 0x52465331 |
| 4–7 | Schema version 1 |
| 8–11 | The record length added by StoreVar() |
| From 12 | Encoded Variant dictionary |
A new FileAccess uses little-endian for Store32(). Variant encoding has its own rules; BigEndian does not turn it into an arbitrary network protocol. Do not guess its layout or equate the save schema with the engine version.
Checking length before GetVar() avoids reading a huge declared outer block or silently accepting a suffix. Next, validate the dictionary root, required fields, exact types and ranges. This slot's limit is 64 KiB. It is not a general decoder for arbitrary untrusted input; corrupt internal content can produce a decoder diagnostic.
Objects and buffer framing
Keep fullObjects: false and allowObjects: false. Object-enabled variants can instantiate objects while decoding; they are unsuitable for persistent game references or files of unknown origin. Store IDs and resolve references explicitly.
ReckfellApi.VarToBytes() and BytesToVar() handle one in-memory record. StoreVar() prefixes that record with a 4-byte length, so do not pass its whole file to BytesToVar() without removing the framing. The extra 8-byte header above belongs to our slot, not the API.
Do not parse an unsupported version as the current schema. Keep it available for recovery and perform explicit migration. Test loading after a complete process restart, not only immediately after Save().