Value¶
A node in the accessor tree: what you get from indexing your data, e.g.
Data[player].Coins or data.Inventory.Sword. Every field is a Value;
container fields also expose their children as Values.
The method set is contextual: all fields have Get/Set/Update/
Observe/Changed; number fields add Increment/Decrement; boolean fields
add Toggle; array fields add Insert/Remove/…; dictionary fields add
Remove/Count/…; Scribe.Flags fields add Enable/Disable; Scribe.SetOf
fields add Add; Scribe.Big fields add Multiply/Divide and read back as
BigValue; Scribe.Timed fields add
SetTimed/ExtendTimed/Active.
On the client, Set writes are local-only (optimistic UI).
Reserved names
Because method names live on the node, a template field named Get, Set,
Count, Remove, etc. is unreachable through the typed accessor. Scribe
warns about this in Studio.
Scribe's own _Scribe root is read-only to game code
Every mutator is refused on a path inside the reserved _Scribe root, and a
read of it hands back a detached copy rather than the live table. That root is
Scribe's ledger, not your data. Value.Set says what it holds and which APIs
own it.
The callable shorthand is not API
A node is callable at runtime, so data.Coins() reads, data.Coins(5) writes
and data.Coins(fn) updates. The generated accessor type does not carry it, so
every such call site is a type error, and nothing tests it. Treat it as an
implementation detail and call Get, Set and Update by name.
Core¶
.Get¶
Returns the field's current value. A datatype field comes back as the real
datatype, unpacked from its stored buffer, and a Scribe.Big field comes back
as a BigValue object rather than a number.
A table field returns the stored table, not a copy, so mutating what Get
hands you edits authoritative state with no validation, no replication and no
Changed. Treat it as read-only, use Value.Clone for a copy you may edit, and
write through the accessor at an index, not the plain entry a read handed back.
Two reads are special. Data[player].Get(), with no field in front of it, returns
a frozen table of exactly the roots your template declares. Scribe's own
_Scribe root returns a detached deep copy, since that root refuses every write.
An unmaterialized field reads nil: a Scribe.DictOf key nothing has written,
or a Scribe.Optional field. Value.Increment and Value.Toggle start from the
element's declared default on such a key, not from that nil.
Reading & Writing Values covers the live-table rule in full.
.Set¶
Writes a new value (validated and clamped per the declarator). Fires Changed,
replicates, and returns the stored value, which is not always the value you
passed. Pass replicate = false to keep the write server-only.
Set(nil) removes the field, except where that would store an unreadable shape:
- a middle index of a
Scribe.ArrayOf, which punches a hole:#arrstops at the gap and every replicated index below it shifts, so useValue.Remove(clearing the tail is fine) - a declared, non-optional field of an element record, whose field set is closed:
wrap it in
Scribe.Optionalif absence is legitimate
A whole-container Set on a Scribe.ArrayOf or Scribe.DictOf must match the
declared key shape: contiguous integer keys from 1 for an array, string keys
within MaxKeyLength for a dictionary, and no growth past MaxItems or
MaxKeys. Omitted declared element fields fill from their defaults.
Like every other mutator, Set is refused on any path inside Scribe's own
_Scribe root, with an error naming the path; see the caution on this page.
.Update¶
Reads the current value, passes it to transform, and writes the result: a
convenient read-modify-write. Pass replicate = false to keep the write
server-only.
On a table field transform receives exactly what Value.Get returns, which is
the stored table, not a copy. Mutating it in place edits authoritative state
directly, and a transform that throws after mutating leaves that edit in the
profile with no Changed fired and no replication op emitted, so it saves and
every client mirror stays behind it. Build a new table and return that, or start
from Value.Clone. A scalar field has none of this.
Reading & Writing Values has the WRONG and RIGHT pair.
.Clone¶
A deep copy of the value (for tables/buffers you intend to mutate locally).
On the root accessor the copy holds exactly your declared template roots, the
same shape Value.Get returns, and unlike Get it is not frozen. Scribe's
_Scribe root is left out on purpose, because it is the copy that tends to
leave the profile it came from. Carrying it into another profile grants that
player every perk and gift credit the first one owned, and stamps them with a
migration Version they never ran, which makes Scribe skip those migrations
permanently.
.Default¶
The field's declared default (a deep copy for tables/buffers, the real datatype for datatype fields). Read-only schema metadata, so it works on the client and server and before data loads. Handy for "reset to default" flows and diffing against the current value.
.Observe¶
Calls callback immediately with the current value, then again on every
change. Returns a disconnect function. The go-to for driving UI.
.Changed¶
Like Value.Observe but fires only on change, with no initial call. Returns a
disconnect function.
On a leaf the callback gets the new and old values and fires once per write.
On a container it reports state rather than a transition: old is the same
reference as new, because the fire happens after the write and Scribe does not
snapshot a container's prior contents. A container fire is also coalesced,
one per Server.Batch and one per client replication frame, however many
children moved. Use Value.OnChildChanged to learn which child moved.
The container key argument was removed
A container listener declaring a third parameter errors at connect time: one
fire now covers every child a batch or a frame touched, so naming one of them
would imply the others did not change. A node beneath an untyped {} subtree is
classified by what is stored there now, so connect after the container exists.
Reading & Writing Values puts the three container listeners side by side.
Containers¶
.OnChildChanged¶
Fires once for every write to a direct child of this container, with that
child's key and both sides of the change. new is nil when the child was
removed, old is nil when it was just added.
old is a real prior value when the child is a leaf. When the child is itself
a container, old is the same reference as new, the same caveat
Value.Changed carries.
Unlike Changed this is never coalesced: a batch writing three children fires it
three times, and a child written twice fires it twice. Every ancestor of a write
receives its own immediate child, so a write to State.Inner.A reports A to
State.Inner and Inner to State. All of a container's OnChildChanged fires
arrive before that container's Changed, batched or not.
This is the only way to learn that an existing Scribe.DictOf key's value
moved: Value.OnKeyAdded and Value.OnKeyRemoved report a key appearing or
disappearing, never one changing.
Numbers¶
.Increment¶
Types: EconomyMeta
Number fields only. Adds amount and returns the new value. Pass false
instead of meta to keep the write server-only.
Pass an EconomyMeta table to auto-emit an economy event
(AnalyticsService:LogEconomyEvent): the currency is this field's name, the
ending balance is the post-change value, and TransactionType, ItemSku and
per-currency custom Fields come from the table. Increment defaults the flow
to Source, and the amount logged is the effective change after clamping.
On a fresh container key the increment starts from the element's declared
default, not from zero. Emberfall declares its Inventory element as
Qty = Scribe.Int(1, { Min = 1, Max = 999 }), so
data.Inventory.Emberblade.Qty.Increment(2) materializes the entry and returns
3 rather than 2. A Scribe.Optional leaf has no default to start from, so
Increment on an absent one is an error, as it is on a non-number amount and
on a field whose current value is not a number.
.Decrement¶
Types: EconomyMeta
Number fields only. Subtracts amount and returns the new value. Like
Value.Increment but the economy flow defaults to Sink. See the
Economy Analytics guide.
.Min¶
Number fields only. The declared minimum (Scribe.Int(0, { Min = 0 })), or
nil if unbounded. Read-only metadata from the shared schema, so it works on
both the client and the server, and even before data has loaded. Handy for
driving slider ranges or validation off the single source of truth.
.Max¶
Number fields only. The declared maximum, or nil if unbounded. See
Value.Min.
Booleans¶
.Toggle¶
Flips a boolean and returns its new value. Which argument it takes depends on the kind of field.
On a plain boolean field it takes no name, and the optional first argument is
replicate, so Toggle(false) flips the value and keeps the write server-only.
On a Scribe.Flags field it takes the member name to flip, because the field
holds a set of enabled names rather than one boolean:
data.Settings.Toggle("Sfx"). Passing no name there is an error, since there is
no single value to flip, and so is a name the template never declared.
On a fresh container key the flip starts from the element's declared default,
not from false, exactly as Value.Increment does for numbers. See
Reading & Writing Values for the whole flags surface.
.Enable¶
Scribe.Flags fields only. Turns the member called name on. Returns nothing.
Pass replicate = false to keep the write server-only.
A flags field is one leaf holding the set of enabled member names, not a
container of booleans, so you address it by name and Settings.Music.Set(true)
reaches for a child that does not exist. The stored value lists the enabled names
in declaration order, not the order you enabled them, so two profiles holding
the same flags hold the same table and diff cleanly.
Enabling a member that is already on writes nothing at all: no Changed, no
replication op, nothing to save. Enabling a name the template never declared is
an error naming the field, and so is calling Enable on a field that is not a
Scribe.Flags.
See Reading & Writing Values for the whole flags surface.
.Disable¶
Scribe.Flags fields only. Turns the member called name off. Returns
nothing. The mirror of Value.Enable, and it refuses the same two things: a
name the template never declared, and a field that is not a Scribe.Flags.
Disabling a member that is already off writes nothing: no Changed, no
replication op, nothing to save. That comes up more often than it looks,
because every member of a flags field starts off, so a Disable on a brand
new profile is always a no-op. Value.Clear turns every member off at once.
Sets¶
.Add¶
Scribe.SetOf fields only. Adds item to the set and returns true. If the set
already holds it, nothing happens at all and you get false back: no write, no
Changed, no replication op. That return value is the cheap way to answer "was
this the first time", which is what a discovery reward needs.
The stored form is kept deduplicated and sorted into a canonical order, so the value does not depend on the order members were added in.
Add(nil) is an error, and so is Add on a field that is not a Scribe.SetOf.
Growing past MaxItems is refused with an error before anything is written; a
duplicate at the cap is still the ordinary no-op returning false. There is no
Value.Insert on a set, and Value.Remove takes the member rather than an index.
Big Numbers¶
.Multiply¶
Types: BigValue
Scribe.Big fields only. Multiplies the field by factor and returns the new
stored value. factor may be a plain number, a numeric string like "1.5e100",
or another big value. Pass replicate = false to keep the write server-only.
It reads, multiplies and writes the whole value back on the ordinary write
path, so a declared Min or Max clamps the result, Changed fires, and the
write replicates like any other. There is no economy-meta slot: the second
argument is replicate, so a meta table passed there is read as that and ignored.
Errors on a field that is not a Scribe.Big, on a factor that is not numeric,
and on an absent Scribe.Optional big, which has no value to multiply.
See Big Numbers for the value object and Reading & Writing Values for why this exists.
.Divide¶
Types: BigValue
Scribe.Big fields only. Divides the field by divisor and returns the new
stored value. The mirror of Value.Multiply: the same accepted operand types,
the same write path, the same clamping and replication.
The quotient is not rounded, so a big field legitimately holds a fractional
value. Divide(3) on a field holding 10 stores 3.33333333333333, and that
is what Get() and tostring give back.
Dividing by zero is an error naming the field, and nothing is written.
Storing the infinity a plain number would produce is worse than throwing,
because an infinity does not survive the DataStore round trip and would come
back as a corrupt profile. The other refusals match Value.Multiply: a field
that is not a Scribe.Big, a non-numeric divisor, and an absent
Scribe.Optional big.
Arrays & Dictionaries¶
.Insert¶
Array fields only. Inserts value at index, or appends it. index is clamped
to the array's contiguous length plus one, and must be an integer: a fractional
or NaN position is refused rather than silently writing a hash-part key. value
may not be nil.
On a Scribe.ArrayOf the entry is validated against the element shape, and
exceeding MaxItems is refused before anything is written, so a rejected
Insert never leaves a phantom container behind. For a record element shape an
undeclared field is an error, and a declared field the caller omitted fills from
its default (Scribe.Optional fields have none and stay absent).
Calling Insert on a Scribe.DictOf is an error: write the key instead.
.Remove¶
Three operations, chosen by the kind of field and then the argument's type.
On an array, a number (or nothing) removes and returns the element at
that index, or the last one, or nil if the array is empty. On a
Scribe.DictOf, the argument is the key to delete, and its former value
comes back. On a Scribe.SetOf the argument is the member itself, since a
set has no positions, and the return is a boolean: true if it was a
member, false if it was not, in which case nothing is written and no
Changed fires.
data.Unlocked.Remove("AshfallRidge") --> true, it was a member
data.Unlocked.Remove("AshfallRidge") --> false, nothing to do
data.Unlocked.Remove(1) --> false, an index is not a member
Outside the set case the returned value is unpacked, so a datatype element comes back as the real datatype rather than its stored buffer.
.RemoveValue¶
Array fields only. Removes the first element matching value, returning it
unpacked along with its former index. Matching follows Value.Find. Errors on a
Scribe.DictOf: use Remove(key). On a Scribe.SetOf, Remove(member) is
already the by-value removal, so reach for that instead.
.Find¶
Array and Scribe.SetOf fields. The index of the first matching element, or
nil.
A Scribe.ArrayOf element is compared structurally, by value, since entries
reach you unpacked and copied and an identity search would never match what
Get() just handed you. An untyped {} container keeps table.find's identity
comparison. Errors on a Scribe.DictOf, which is keyed: read the key directly.
.Has¶
Whether value is present. Matching follows Value.Find, including its error
on a Scribe.DictOf.
This is the membership test for every list-shaped field, not just arrays:
data.Unlocked.Has("AshfallRidge") --> true, a Scribe.SetOf member
data.Settings.Has("Music") --> true, a Scribe.Flags member
.OnInsert¶
Array fields only. Fires when an element is inserted, with the value and the index it landed at. Returns a disconnect function.
.OnRemove¶
Array fields only. Fires when an element is removed, with the removed value and its former index. Returns a disconnect function.
.OnKeyAdded¶
Fires when a key is added, with the key and its new value. Returns a disconnect
function. Mainly for dictionaries, but an array fires it too, with the numeric
index as the key, when an index is written directly with Set rather than
Insert.
.OnKeyRemoved¶
Fires when a key is removed, with the key and its former value. Returns a
disconnect function. As with OnKeyAdded, an array fires it for a direct
Set(nil) on an index, with the numeric index as the key.
.Count¶
Array, dictionary and Scribe.SetOf fields. The number of elements, keys or
members. A Scribe.Flags field answers with the number of members currently
enabled. A field holding nothing yet counts 0 rather than erroring.
.Clear¶
Removes every entry, leaving the container empty. Works on arrays,
dictionaries and Scribe.SetOf fields, and on a Scribe.Flags field it turns
every member off in one write.
Timed¶
.SetTimed¶
Scribe.Timed fields only. Sets value for seconds, after which it
auto-clears back to the declared default (firing Changed). Durations floor
to 1 second (a once-per-second sweep checks expiries) and cap at a finite
~126 years, so SetTimed(value, math.huge) means "effectively permanent".
Note that a later plain Set does NOT cancel the running timer: the field
still resets to its default when the timer lapses. To make a timed value
permanent, re-issue SetTimed(value, math.huge).
.ExtendTimed¶
Scribe.Timed fields only. Adds time to a running timer.
With no timer running it arms a fresh one from now, so a field that was
never SetTimed starts counting down and will revert to its declared default.
Check Active() first if that is not what you want. The value itself is never
written, so unlike SetTimed there is nothing here to replicate.
.Active¶
Scribe.Timed fields only. Whether the timer is active, and the seconds
remaining.