Skip to content

05 — File-Storage Notepad

TL;DR: A TMP_InputField drives onValueChanged(string) into OnTextChanged(string) via persistent-call rewiring. Save / Load / Clear buttons round-trip the cached text to a FileStorage file. The script requests FileStorageApiAllowed + 64 KB quota on first run, handles denial gracefully, and surfaces the loaded text in a separate TextMeshProUGUI display label. World-only.

TMP_InputField is not in the generated WASM binder surface (see UnityEngine Surface — only 16 TMPro types have binder links, and TMP_InputField isn’t one of them). Setting input.text = "…" from the guest would hit a stub and do nothing. But:

  • Input into the guest works fine because Unity’s UI system (native, not WASM) drives the onValueChanged(string) event. At build time the persistent call is rewritten to TriggerScriptEventString("OnTextChanged") and the string is marshaled correctly.
  • Output to the user on load is shown in a TextMeshProUGUI label — that IS bound, so displayLabel.text = ... works.

If you genuinely need to push text back into the input field on load, put that logic in an editor-side UnityEvent (e.g. a Button that reads displayLabel.text and writes it to inputField.text), since both ends run in native UI code.

  1. World-space Canvas.
  2. TMP_InputField child for text entry. Multi-line optional.
  3. TextMeshProUGUI child for the loaded display.
  4. TextMeshProUGUI child for status messages.
  5. Three Button children: SaveBtn, LoadBtn, ClearBtn.
  6. GameObject with the Notepad script.
    • Display Label → the display TMP.
    • Status Label → the status TMP.
    • File Name → default "notepad.txt".
  7. TMP_InputField wiring:
    • In the inspector, expand its On Value Changed (String) event list.
    • Add a call, target the Notepad, pick OnTextChanged (string). Leave the hard-coded string empty — event-defined mode will pass the current input text each keystroke.
  8. Button wiring: each Button.OnClick () calls OnSaveClicked, OnLoadClicked, OnClearClicked.
  9. CCKWasmProjectDescriptor on the world root.
using System;
using System.Text;
using TMPro;
using UnityEngine;
using WasmScripting;
public partial class Notepad : WasmBehaviour
{
public TextMeshProUGUI displayLabel;
public TextMeshProUGUI statusLabel;
public string fileName = "notepad.txt";
[WasmSerialized] private string pendingText = string.Empty;
[WasmSerialized] private bool permissionsRequested;
void Start()
{
if (!permissionsRequested)
{
WorldPermissions.Request(new WorldPermissions
{
FileStorageApiAllowed = true,
FileStorageStorageLimit = 64 * 1024,
});
permissionsRequested = true;
}
LoadFromDisk();
}
[ExternallyVisible] public void OnTextChanged(string text) { pendingText = text ?? string.Empty; }
[ExternallyVisible] public void OnSaveClicked() { /* WriteFile(fileName, UTF-8(pendingText)) */ }
[ExternallyVisible] public void OnLoadClicked() { LoadFromDisk(); }
[ExternallyVisible] public void OnClearClicked() { /* DeleteFile */ }
public void OnWorldPermissionsChanged() { /* game event — no attribute needed */ }
// ... (LoadFromDisk + SetStatus, see the source file)
}

Source file: examples/05_Notepad.cs.

  1. First run: Start calls WorldPermissions.Request(...). The WasmPermissionsPage UI opens; the user approves, adjusts the quota, or denies.
  2. Result: the host fires the OnWorldPermissionsChanged game event → build-time scanner finds the matching method name on the behaviour → dispatcher invokes OnWorldPermissionsChanged(). We re-read WorldPermissions.CurrentPermissions.FileStorageApiAllowed and update status.
  3. Subsequent runs: permissionsRequested == true is serialized to disk, so we don’t prompt again. The saved permissions are loaded from WasmPermissions.json before the script runs.

A user can change permissions any time via the CVR UI — your script will receive OnPermissionsResult when they do.

%APPDATA%/ChilloutVR/WorldData/{worldId}/
├── WasmPermissions.json
└── {hash-of-"notepad.txt"}.part.wasmdata

The .part.wasmdata file contains [1 byte filename length][N bytes UTF-8 filename][128 bytes random XOR key][encrypted UTF-8 text bytes]. See File Storage for details.

  • FileStorage.*(World, Any, —). All calls throw WasmAccessDeniedException from avatar or prop contexts, regardless of world permissions.
  • WorldPermissions.Request(World, Any, —).
  • Text manipulation, Encoding.UTF8.GetBytes/GetString — no binding involved (pure guest C#).

Every handler re-checks WorldPermissions.CurrentPermissions.FileStorageApiAllowed before touching the file API. If the user denies or later revokes permission, the UI still works; save/load/clear produce a status line explaining what went wrong.

  • Multiple notes — derive fileName from a button that calls public void SetFileName(string) or a TMP_Dropdown wired via a string event.
  • Auto-save — use onValueChanged(string) to update a “dirty” flag plus a timer (Update + Time.time) that saves after 1 s of idle.
  • Quota display — poll FileStorage.GetTotalSize() / FileStorage.GetTotalCapacity() in Update (maybe every N frames) and show "Used: 12.3 KB of 64.0 KB".
  • Binary formats — save a BufferReaderWriter blob (WasmScripting.BufferReaderWriter) instead of plain UTF-8. Good for structured save data.