Skip to content

Commit

Permalink
Add docs for persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
N3X15 committed Nov 27, 2024
1 parent 7a28acc commit 3db2648
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 3 deletions.
3 changes: 2 additions & 1 deletion docs/cck/lua/api/.pages
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ nav:
- instances-api.md
- player-api.md
- avatar-api.md
- lua-behaviour.md
- lua-behaviour.md
- storage.md
3 changes: 2 additions & 1 deletion docs/cck/lua/api/globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ Most MonoBehaviour [Events](events.md) are also available.
| `IsWornByMe` | Defined when running on an Avatar. True for the avatar wearer. False for others. |
| `IsSpawnedByMe` | Defined when running on a Prop. True for the spawner. False for others. |
| `BoundObjects` | Access to the Bound Objects assigned on the CVRLuaClientBehaviour script in editor. |
| [Script](lua-behaviour.md) | Reference to the currently running script. **Example:** Script.Destroy("boom") |
| [`Script`](lua-behaviour.md) | Reference to the currently running script. **Example:** Script.Destroy("boom") |
| [`Storage`](storage.md) | Access to the data persistence system. |

## API Access

Expand Down
65 changes: 65 additions & 0 deletions docs/cck/lua/api/storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Storage

The `Storage` global provides access to the script's associated Persistence object. The Persistence object is where your script can store simple forms of data on a user's computer.

## Bins

We provide two "storage bins:" The `Public` bin and the `Private` bin. Each bin is a property of the Storage global and are almost identical in terms of API. Each bin has a set amount of data that they can store, as well, to prevent malicious scripts from filling up the user's hard drive.

### Public

The Public bin is accessible as `Storage.Public`. This bin's corresponding file on disk is user-editable, as it is stored as clear-text JSON. This file is intended to be used for things the user may need to edit, such as configuration or tuning values.

#### Properties

| Name | Type | Notes |
| --- | --- | --- |
| `BytesAllowed` | `int` | <p>How many bytes are allowed for this storage bin, in total. Currently set to `4194304`, which corresponds to 4MiB.</p><p>Does not include data outside of your control that we add during the serialization process, such as encryption stuff, headers, section data, etc.</p> |
| `CurrentSize` | `int` | <p>Current reported size of this storage bin, in bytes.</p><p>Does not include data outside of your control that we add during the serialization process, such as encryption stuff, headers, section data, etc. </p> |
| `IsEncrypted` | `bool` | Whether this storage bin is encrypted on disk. Always `false` for Public bins. |
| `Path` | `string` | Path of the file on disk. |

#### Methods

| Name | Notes |
| --- | --- |
| `GetBoolean(string key) : bool?` | Get the value of a key as a `boolean`, or `nil` if the key is `nil` or not present. |
| `GetNumber(string key) : number?` | Get the value of a key as a `number`, or `nil` if the key is `nil` or missing. |
| `GetString(string key) : string?` | Get the value of a key as a `string`, or `nil` if the key is `nil` or missing. |
| `GetTable(string key) : table?` | Get the value of a key as a `table`, or `nil` if the key is `nil` or missing. |
| `HasValue(string key) : bool` | Determine if the given named `key` is present in the underlying `table`. |
| `Load() : void` | Loads data from disk, if present. |
| `Save() : void` | <p>If anything has changed (if the storage is marked dirty), save to disk.</p><p>A successful save will clear dirtiness.</p> |
| `SetBoolean(string key, bool? value) : void` | <p>Set the value of a key to a `boolean` or `nil`.</p><p>You will need to call `Save()` to commit this change to disk.</p> |
| `SetNumber(string key, number? value) : void` | <p>Set the value of a key to a `number` or `nil`.</p><p>You will need to call `Save()` to commit this change to disk.</p> |
| `SetString(string key, string? value) : void` | <p>Set the value of a key to a `string` or `nil`.</p><p>You will need to call `Save()` to commit this change to disk.</p> |
| `SetTable(string key, table? value) : void` | <p>Set the value of a key to a `table` or `nil`.</p><p>**IMPORTANT:** Any values in the provided table or subtables that are not `nil`, `number`, `boolean`, `table`, or `string` will be silently stripped during serialization!</p><p>You will need to call `Save()` to commit this change to disk.</p> |

### Private

The Private bin is accessible as `Storage.Private`. This bin's corresponding file on disk is encrypted and stored as a binary format, as it is intended to be used for storing deliberately opaque data, such as a user's score in a game, character levels, *et cetera*.

#### Properties

| Name | Type | Notes |
| --- | --- | --- |
| `BytesAllowed` | `int` | <p>How many bytes are allowed for this storage bin, in total. Currently set to `4194304`, which corresponds to 4MiB.</p><p>Does not include data outside of your control that we add during the serialization process, such as encryption stuff, headers, section data, etc.</p> |
| `CurrentSize` | `int` | <p>Current reported size of this storage bin, in bytes.</p><p>Does not include data outside of your control that we add during the serialization process, such as encryption stuff, headers, section data, etc.</p> |
| `IsEncrypted` | `bool` | Whether this storage bin is encrypted on disk. Always `false` for Private bins. |
| `Path` | `string` | Path of the file on disk. |

#### Methods

| Name | Notes |
| --- | --- |
| `GetBoolean(string key) : bool?` | Get the value of a key as a `boolean`, or `nil` if the key is `nil` or not present. |
| `GetNumber(string key) : number?` | Get the value of a key as a `number`, or `nil` if the key is `nil` or missing. |
| `GetString(string key) : string?` | Get the value of a key as a `string`, or `nil` if the key is `nil` or missing. |
| `GetTable(string key) : table?` | Get the value of a key as a `table`, or `nil` if the key is `nil` or missing. |
| `HasValue(string key) : bool` | Determine if the given named `key` is present in the underlying `table`. |
| `Load() : void` | Loads data from disk, if present. |
| `Save() : void` | <p>If anything has changed (if the storage is marked dirty), save to disk.</p><p>A successful save will clear dirtiness.</p> |
| `SetBoolean(string key, bool? value) : void` | <p>Set the value of a key to a `boolean` or `nil`.</p><p>You will need to call `Save()` to commit this change to disk.</p> |
| `SetNumber(string key, number? value) : void` | <p>Set the value of a key to a `number` or `nil`.</p><p>You will need to call `Save()` to commit this change to disk.</p> |
| `SetString(string key, string? value) : void` | <p>Set the value of a key to a `string` or `nil`.</p><p>You will need to call `Save()` to commit this change to disk.</p> |
| `SetTable(string key, table? value) : void` | <p>Set the value of a key to a `table` or `nil`.</p><p>**IMPORTANT:** Any values in the provided table or subtables that are not `nil`, `number`, `boolean`, `table`, or `string` will be silently stripped during serialization!</p><p>You will need to call `Save()` to commit this change to disk.</p> |
3 changes: 2 additions & 1 deletion docs/cck/lua/examples/.pages
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ nav:
- player-profile-picture.md
- player-thumbs-up-launch.md
- player-teleport-away-from-water.md
- viewpoint-raycast.md
- viewpoint-raycast.md
- persistence.md
1 change: 1 addition & 0 deletions docs/cck/lua/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Here are some example scripts that can be used as an example to learn about Lua.
* [Thumbs Up Player Launcher](player-thumbs-up-launch.md) - Launch upward when doing 2x thumbs up gesture.
* [Viewpoint Raycast](viewpoint-raycast.md) - Perform a raycast from the player's viewpoint.
* [Player Profile / Avatar Picture](player-profile-picture.md) - Get Player Profile and Avatar pictures.
* [Persistence](persistence.md) - Store all kinds of data on the user's PC.
105 changes: 105 additions & 0 deletions docs/cck/lua/examples/persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Persistence

The following script is a short demonstration of a common use case for storing persistence data: A hypothetical RPG game! (We've stripped out the actual RPG code to make things simpler to understand.)

For more information on the Storage API, please read our [Storage API Reference](../api/storage.md).

**This is stored on the user's computer, so it's not entirely secure!**

```lua
-- This is a world script, but you can use persistence on worlds, avatars, and props!

-- A table of the players in our game. In our case, we'll use a {user guid => data} structure.
PLAYERS = {}

-- Version of our game's public storage scheme, explanation below
LATEST_PUBLIC_VERSION = 5

-- Same for private storage
LATEST_PRIVATE_VERSION = 13

-- Game difficulty (you'd use constants here, like DIFFICULTY_EASY=1, _MEDIUM=2, _HARD=3; but let's be short and sweet for this example)
DIFFICULTY=2

-- Turn off Krampus mobs by defaults because they scared the American playtesters
KRAMPUS_ENABLED=false
function Start()
print("HYPOTHETICAL-RPG V1.0")

-- Print out some helpful debug info
print("[Public Storage]")
print("Path..........: " .. Storage.Public.Path)
print("Bytes Allowed.: " .. tostring(Storage.Public.BytesAllowed))
print("Bytes In Use..: " .. tostring(Storage.Public.CurrentSize))
print("[Private Storage]")
print("Path..........: " .. Storage.Private.Path)
print("Bytes Allowed.: " .. tostring(Storage.Private.BytesAllowed))
print("Bytes In Use..: " .. tostring(Storage.Private.CurrentSize))

-- First, we'll load up our public configuration data.
local public = Storage.Public
--[[ WALL OF TEXT WARNING
At this point, if there's an existing file, it's already Load()'d, so we don't need to call it here. It should already be loaded into memory if it's there on disk. If it's NOT present on disk, we're just basically working with a blank file with no data. We can add this later.
So, a good way to check for file presence while also tracking changes to the data structure we make over time (like adding new stats or renaming our 'spel' list to 'spell'), we try to access a field called version.
Version is a number because we can just increment it one to say we're on a new version. If the save is less than the latest version, we know it's old and can throw it out, or manually upgrade it.
]]--
local publicVersion = public.GetNumber("version")

-- If we're starting a new save, it's simply nil, because the file is blank.
if publicVersion == nil then
-- Cool, new save! Let's fill in some defaults for the user.
-- Tell the game this is the latest structure of our save file, since we're making it from scratch.
public.SetNumber("version", LATEST_PUBLIC_VERSION)
-- Set our defaults
public.SetBoolean("krampus_enabled", KRAMPUS_ENABLED)
public.SetNumber("difficulty", DIFFICULTY)
-- And any other stuff you'd like can go here.
-- Now we need to Save() to actually save these changes to disk.
public.Save()
elseif publicVersion < LATEST_PUBLIC_VERSION then
-- We could either reset here as above, or make an if-tree handling and upgrading every prior version of this save. This is outside the scope of this example.
print('ERROR: Public data is out of date! FIXME!')
end

-- So we have data. Let's load it into our game configuration:
DIFFICULTY = public.GetNumber("difficulty")
KRAMPUS_ENABLED = public.GetBoolean("krampus")

-- Let's load up our private storage now.
-- This is where we're going to store more sensitive things, like player stats, save state, monster positions, quests, etc.
local private = Storage.Private
-- Check our version info
local privateVersion = private.GetNumber("version")
if privateVersion == nil then
-- New save! Build defaults and save. Don't do class selection or anything here, this is just scaffolding for later.
-- Ideally, this would be player GUID instead of name, and a full-featured "class" with functions, and then you'd just simplify it in SaveGame(), but we're getting ahead of ourselves.
PLAYER["Example"] = {
level=0,
hp=0,
mana=0,
inventory=["sword", "rat flail"],
gold=10
}
SaveGame()
elseif privateVersion < LATEST_PRIVATE_VERSION then
-- Upgrade saves here
print('ERROR: Private data is out of date! FIXME!')
end

-- And load it all into memory.
PLAYERS = private.GetTable("players")

-- Other game startup stuff
end

-- ... Other game stuff ...

-- A function to just dump the game state into our save.
function SaveGame()
private.SetNumber("version", LATEST_PRIVATE_VERSION)
-- Remember that if you feed SetTable a table with functions/coroutines/etc, it will silently eat them and you won't get them back when it loads the table later. Allowing Lua to store arbitrary code to a user's computer would be... bad, so we had to make some compromises.
private.SetTable("players", PLAYERS)
end
```

0 comments on commit 3db2648

Please sign in to comment.