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.
Prerequisites
Section titled “Prerequisites”- Script must run in world ObjectContext. Avatars and props get
WasmAccessDeniedExceptionon everyFileStorage_*call. - The world permission
FileStorageApiAllowedmust be approved. See World Permissions. - Total bytes written must fit under
FileStorageStorageLimit(default 4 MB).
Permission request
Section titled “Permission request”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();}API surface
Section titled “API surface”Source: CVR-GameFiles/WasmScripting/FileStorageLinks.cs. Eleven host functions in the CVR import module:
| Function | Shape | Notes |
|---|---|---|
FileStorage_ReadInternal_Full(name, nameLen, outBufPtrPtr, outBufLenPtr) | reads all bytes | host 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 file | quota 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) -> i32 | 1 if exists, 0 otherwise | |
FileStorage_GetFilesInternal(outNamesPtr, outLengthsPtr, outCountPtr) | lists all filenames | host allocates guest memory for all three |
FileStorage_GetFileSizeInternal(name, nameLen) -> i32 | byte count | |
FileStorage_GetTotalSizeInternal() -> i64 | total bytes used by this world | |
FileStorage_GetTotalCapacityInternal() -> i64 | current quota limit |
All names are UTF-16 — the host reads them via data.Memory.ReadString(namePtr, nameLength, Encoding.Unicode).
On-disk layout
Section titled “On-disk layout”%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).
Addressing
Section titled “Addressing”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_304LinCVR-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) + bufferSizeabove_maxSizeLimitthrow —FileStorageManager.CheckCanWritewalks the directory contents and refuses. - Creating a new
.wasmdatawhen 1000 already exist throws — see theMaxFileLimit = 1000cap. FileStorage_GetTotalCapacityInternal()returns the current cap;FileStorage_GetTotalSizeInternal()the current usage.
Raw files
Section titled “Raw files”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.
Atomicity
Section titled “Atomicity”- 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.
Security
Section titled “Security”- 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.
Working with binary data
Section titled “Working with binary data”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/BinaryReaderorMemoryMarshal.AsBytes. - Arrays of primitives → just
MemoryMarshal.AsBytesfor 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.
Source
Section titled “Source”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.
Related
Section titled “Related”- World Permissions — the gating flag and quota slider.
- Permissions — ObjectContext.World enforcement.
- Examples → 05 File-Storage Notepad.