Skip to content

File Storage

TL;DR: World scripts (only) can read and write files in a sandboxed per-world directory under %APPDATA%/ChilloutVR/WorldData/{worldId}/. Each file is XOR-encrypted with a per-file random 128-byte key. Default quota is 4 MB, adjustable by the user when the world requests permission. Files are addressed by UTF-16 name; the on-disk name is a hash, so the storage is effectively a key-value store.

  • Script must run in world ObjectContext. Avatars and props get WasmAccessDeniedException on every FileStorage_* call.
  • The world permission FileStorageApiAllowed must be approved. See World Permissions.
  • Total bytes written must fit under FileStorageStorageLimit (default 4 MB).

WasmScripting.FileStorage in the current CCK stub (CVR.CCK.Wasm/Scripting/Links/APIs/CCKStubs/FileStorageCCK.cs) is a thin wrapper that forwards every call straight to FileStorageManager — it does not carry CanUseFileStorage or RequestUseFileStorage helpers. Call WorldPermissions.Request yourself:

if (!WorldPermissions.CurrentPermissions.FileStorageApiAllowed)
{
WorldPermissions.Request(new WorldPermissions
{
FileStorageApiAllowed = true,
FileStorageStorageLimit = 4_194_304,
});
return;
}
var file = FileStorage.ReadFile("save.dat");

Subscribe to OnWorldPermissionsChanged to resume once the user accepts:

void OnWorldPermissionsChanged()
{
if (WorldPermissions.CurrentPermissions.FileStorageApiAllowed) LoadFromDisk();
}

Source: CVR-GameFiles/WasmScripting/FileStorageLinks.cs. Eleven host functions in the CVR import module:

FunctionShapeNotes
FileStorage_ReadInternal_Full(name, nameLen, outBufPtrPtr, outBufLenPtr)reads all byteshost allocates guest memory for result
FileStorage_ReadInternal_Partial(name, nameLen, offset, length, outBufPtrPtr, outBufLenPtr)reads a range
FileStorage_WriteInternal_Full(name, nameLen, bufPtr, bufLen)overwrites whole filequota enforced
FileStorage_WriteInternal_Partial(name, nameLen, bufPtr, bufLen, offset)partial write / insert
FileStorage_DeleteFileInternal(name, nameLen)deletes the file
FileStorage_RenameFileInternal(oldName, oldLen, newName, newLen)renames within the same world
FileStorage_FileExistsInternal(name, nameLen) -> i321 if exists, 0 otherwise
FileStorage_GetFilesInternal(outNamesPtr, outLengthsPtr, outCountPtr)lists all filenameshost allocates guest memory for all three
FileStorage_GetFileSizeInternal(name, nameLen) -> i32byte count
FileStorage_GetTotalSizeInternal() -> i64total bytes used by this world
FileStorage_GetTotalCapacityInternal() -> i64current quota limit

All names are UTF-16 — the host reads them via data.Memory.ReadString(namePtr, nameLength, Encoding.Unicode).

%LocalAppData%/ChilloutVR/WorldData/{worldId}/
├── WasmPermissions.json # permission decisions (see World Permissions)
└── LocalStorage/
├── index.wasmdata # per-world hash ↔ original-name map
├── {hash1}.wasmdata # committed entries (one per file)
└── ...

Each .wasmdata file contains (from CVR-GameFiles/WasmScripting/FileStorageManager.cs):

[ 1 byte ] filename length (N), limit 100 chars
[ N bytes ] UTF-8 filename
[ 128 bytes ] random XOR key (one per file)
[ remaining ] data XOR-masked with the 128-byte key (repeating)

Writes go to {hash}.part.wasmdata first; on successful stream close the temp is atomic-renamed to {hash}.wasmdata. If the old file exists, it is replaced. An interrupted write leaves the original committed file and a .part.wasmdata dangler for the next session to overwrite.

Raw files that a user drops into LocalStorage/ are recognised by FileStorageManager.GetFiles only when FileStorageReadRawFiles is granted — they must not end in .wasmdata / .part.wasmdata and count as IsUserFile (no XOR, no quota).

You pass logical names like "player_stats.bin" or "session/last-run.json". The hash step means slashes in the name do not create subdirectories — the on-disk filename is always a single hash. Keep a flat namespace and embed grouping in the name if you want.

  • Default: 4 MB (WorldPermissions.FileStorageStorageLimit = 4_194_304L in CVR-GameFiles/WasmScripting/WorldPermissions.cs).
  • User may adjust at first permission prompt or later from the CVR UI (46-step palette 1 KB → 16 GB).
  • Writes that would push sum(file sizes) + bufferSize above _maxSizeLimit throw — FileStorageManager.CheckCanWrite walks the directory contents and refuses.
  • Creating a new .wasmdata when 1000 already exist throws — see the MaxFileLimit = 1000 cap.
  • FileStorage_GetTotalCapacityInternal() returns the current cap; FileStorage_GetTotalSizeInternal() the current usage.

A second flag, FileStorageReadRawFiles, lets the world read user-supplied raw files (not written by this script) and bypass the quota. Use this for large read-only content packs the user deliberately dropped into the folder. Writes still go through the quota system.

  • Full writes: temp file + atomic rename. Interrupted mid-write → the old file survives, the temp is garbage.
  • Partial writes: not atomic by themselves — if you need atomicity across a partial write, combine it with a rename or keep a version index in a separate file.
  • Not cryptographically secure. XOR with a known-layout key is obfuscation, not encryption. Anyone with filesystem access can read the data.
  • Do not store secrets (credentials, tokens). Treat this as persistent application state, not a safe.
  • Contents are tied to the {worldId} directory — switching worlds gives you a fresh quota and a fresh namespace.

Values are untyped byte blobs. Pick a serialization format that suits your data:

  • Small structured state → JSON (available via .NET System.Text.Json).
  • Fixed binary → BinaryWriter / BinaryReader or MemoryMarshal.AsBytes.
  • Arrays of primitives → just MemoryMarshal.AsBytes for fast reuse.

The CCKWasmModule shim is expected to provide idiomatic C# wrappers (e.g. FileStorage.WriteAllBytes(name, bytes)). Call those rather than the raw bindings where available.

  • CVR-GameFiles/WasmScripting/FileStorageManager.cs — on-disk XOR format, quota enforcement, 1000-file cap, 100-char filename cap.
  • CVR-GameFiles/WasmScripting/FileStorageLinks.cs — host bindings (11 functions).
  • CVR-GameFiles/WasmScripting/CVRFile.cs — guest return wrapper (byte[] Bytes, int Length).
  • CVR.CCK.Wasm/Scripting/Links/APIs/CCKStubs/FileStorageCCK.cs — guest-facing shim.