Skip to content

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

Value.Get()  T

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

Value.Set(value: T, replicate: boolean?)  T

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: #arr stops at the gap and every replicated index below it shifts, so use Value.Remove (clearing the tail is fine)
  • a declared, non-optional field of an element record, whose field set is closed: wrap it in Scribe.Optional if 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

Value.Update(transform: (current: T) -> T, replicate: boolean?)  T

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

Value.Clone()  T

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

Value.Default()  T

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

Value.Observe(callback: (value: T) -> ())  () -> ()

Calls callback immediately with the current value, then again on every change. Returns a disconnect function. The go-to for driving UI.

.Changed

Value.Changed(callback: (new: T, old: T) -> ())  () -> ()

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

Value.OnChildChanged(callback: (key: K, new: V?, old: V?) -> ())  () -> ()

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

Value.Increment(amount: number, meta: EconomyMeta?)  number

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

Value.Decrement(amount: number, meta: EconomyMeta?)  number

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

Value.Min()  number?

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

Value.Max()  number?

Number fields only. The declared maximum, or nil if unbounded. See Value.Min.

Booleans

.Toggle

Value.Toggle(nameOrReplicate: (string | boolean)?, replicate: boolean?)  boolean

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

Value.Enable(name: string, replicate: boolean?)

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

Value.Disable(name: string, replicate: boolean?)

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.

data.Settings.Disable("Music")
data.Settings.Has("Music")     --> false

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

Value.Add(item: T, replicate: boolean?)  boolean

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

Value.Multiply(factor: number | string | BigValue, replicate: boolean?)  BigValue

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

Value.Divide(divisor: number | string | BigValue, replicate: boolean?)  BigValue

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.

data.Essence.Divide(2)   --> 8.625e99, halved on a prestige reset

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

Value.Insert(value: T, index: number?, replicate: boolean?)

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

Value.Remove(indexOrKey: (number | string)?, replicate: boolean?)  (T | boolean)?

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

Value.RemoveValue(value: T, replicate: boolean?)  (T?, number?)

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

Value.Find(value: T)  number?

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

Value.Has(value: T)  boolean

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

Value.OnInsert(callback: (value: T, index: number) -> ())  () -> ()

Array fields only. Fires when an element is inserted, with the value and the index it landed at. Returns a disconnect function.

.OnRemove

Value.OnRemove(callback: (value: T, index: number) -> ())  () -> ()

Array fields only. Fires when an element is removed, with the removed value and its former index. Returns a disconnect function.

.OnKeyAdded

Value.OnKeyAdded(callback: (key: K, value: T) -> ())  () -> ()

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

Value.OnKeyRemoved(callback: (key: K, value: T) -> ())  () -> ()

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

Value.Count()  number

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

Value.Clear(replicate: boolean?)

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

Value.SetTimed(value: T, seconds: number, replicate: boolean?)

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

Value.ExtendTimed(seconds: number)

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

Value.Active()  (boolean, number?)

Scribe.Timed fields only. Whether the timer is active, and the seconds remaining.