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.
Why a separate display label
Section titled “Why a separate display label”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 toTriggerScriptEventString("OnTextChanged")and the string is marshaled correctly. - Output to the user on load is shown in a
TextMeshProUGUIlabel — that IS bound, sodisplayLabel.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.
Scene setup (World context)
Section titled “Scene setup (World context)”- World-space
Canvas. TMP_InputFieldchild for text entry. Multi-line optional.TextMeshProUGUIchild for the loaded display.TextMeshProUGUIchild for status messages.- Three
Buttonchildren:SaveBtn,LoadBtn,ClearBtn. - GameObject with the
Notepadscript.Display Label→ the display TMP.Status Label→ the status TMP.File Name→ default"notepad.txt".
TMP_InputFieldwiring:- In the inspector, expand its
On Value Changed (String)event list. - Add a call, target the
Notepad, pickOnTextChanged (string). Leave the hard-coded string empty — event-defined mode will pass the current input text each keystroke.
- In the inspector, expand its
- Button wiring: each
Button.OnClick ()callsOnSaveClicked,OnLoadClicked,OnClearClicked. CCKWasmProjectDescriptoron 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.
Permission flow
Section titled “Permission flow”- First run:
StartcallsWorldPermissions.Request(...). TheWasmPermissionsPageUI opens; the user approves, adjusts the quota, or denies. - Result: the host fires the
OnWorldPermissionsChangedgame event → build-time scanner finds the matching method name on the behaviour → dispatcher invokesOnWorldPermissionsChanged(). We re-readWorldPermissions.CurrentPermissions.FileStorageApiAllowedand update status. - Subsequent runs:
permissionsRequested == trueis serialized to disk, so we don’t prompt again. The saved permissions are loaded fromWasmPermissions.jsonbefore the script runs.
A user can change permissions any time via the CVR UI — your script will receive OnPermissionsResult when they do.
File layout on disk
Section titled “File layout on disk”%APPDATA%/ChilloutVR/WorldData/{worldId}/├── WasmPermissions.json└── {hash-of-"notepad.txt"}.part.wasmdataThe .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.
Permission model (access axes)
Section titled “Permission model (access axes)”FileStorage.*—(World, Any, —). All calls throwWasmAccessDeniedExceptionfrom avatar or prop contexts, regardless of world permissions.WorldPermissions.Request—(World, Any, —).- Text manipulation,
Encoding.UTF8.GetBytes/GetString— no binding involved (pure guest C#).
Graceful degradation
Section titled “Graceful degradation”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.
Extensions to try
Section titled “Extensions to try”- Multiple notes — derive
fileNamefrom a button that callspublic void SetFileName(string)or aTMP_Dropdownwired 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()inUpdate(maybe every N frames) and show"Used: 12.3 KB of 64.0 KB". - Binary formats — save a
BufferReaderWriterblob (WasmScripting.BufferReaderWriter) instead of plain UTF-8. Good for structured save data.
Related
Section titled “Related”- File Storage — the full API.
- World Permissions — what the user sees during the prompt.
- Unity Events Rewiring — how the string event reaches us.