Types¶
The exported shapes that the signatures on the other API pages name.
Every one of them is reachable through the module you required, so you can annotate
your own locals with Scribe.LeaderboardEntry and get the same checking Scribe uses
internally. Nothing here is something you construct by hand unless the entry says so.
Setup¶
Bundle¶
| Field | Type | What it is |
|---|---|---|
Server |
Server |
the server-side data API (use on the server) |
Client |
Client |
the client-side data API (use on the client) |
What Scribe(options) returns. The same shared module is required on both
sides; use .Server on the server and .Client on the client.
ScribeOptions¶
| Field | Type | What it is |
|---|---|---|
Template |
T |
your data shape, built from plain values and declarators |
ProfileStoreIndex |
string |
the DataStore name, which has no default |
ProfileKeyPrefix |
string |
per-player key prefix such as "PLAYER_"; "" means bare user-id keys |
The table you hand Scribe(options). Only the three fields above are
required, and every other option has a working default, so the smallest real
configuration is three lines long.
Roughly fifty further options cover persistence, monetization, gifting, limits, integrity and diagnostics. Each is listed with its default, its accepted values and what happens when it is wrong in the Configuration reference, which is the source of truth for all of them. This entry deliberately does not restate that page.
Declarators¶
Timed<T>¶
What Scribe.Timed returns, and the type a cooldown field carries in your
template. You never build one of these yourself. Emberfall declares
LastDaily = Scribe.Timed(86400), arms it with Value.SetTimed, and asks
about it with Server.OnCooldown and Server.PeekCooldown. See
Timers & Cooldowns.
Flags¶
What a Scribe.Flags field reads back: the member names currently enabled,
not the full set of declared members. Emberfall's
Settings = Scribe.Flags({ "Music", "Sfx", "Tips" }) reads back as
{ "Music", "Tips" } for a player who turned Sfx off. Flip one member with
Value.Enable, Value.Disable or Value.Toggle, and test one with
Value.Has.
SetOf<T>¶
What a Scribe.SetOf field reads back: its members, deduplicated and in
sorted order, so two profiles holding the same members hold the same table.
A set has no positions, so you change it with Value.Add and Value.Remove
rather than by index, and both return false when they would be a no-op.
See Containers.
ServerOnly<T>¶
Wraps a field so it never leaves the server. A Scribe.ServerOnly field is
absent from the replicated tree entirely, so no part of it reaches the wire
even inside a whole-subtree read, and a client cannot ask for it. Use it for
anything an exploiter must not see, such as Emberfall's anti-cheat counters.
See Replication & Visibility.
Derived<T>¶
What Scribe.Derived returns. A derived field is computed from other fields
rather than stored, so it is read-only and Value.Set on one throws.
Emberfall's Level derives from Xp, recomputes whenever Xp changes, and
replicates like any other field. See Derived Fields.
MapOf<K,V>¶
What a Scribe.MapOf field reads back: the whole map, keyed by the type you
declared. Unlike a dictionary the key set is open, so Emberfall's
Friends = Scribe.MapOf("integer", { ... }) accepts any user id. Add and
remove keys with Value.Set and Value.Remove, and watch them with
Value.OnKeyAdded and Value.OnKeyRemoved.
MapKeyType¶
The key type you name in the first argument to Scribe.MapOf. It is a
declaration rather than a hint. A DataStore serializes every object key to a
string, so an integer map would come back holding "123" where it stored
123; the declared type is what lets Scribe convert the keys back
unambiguously on load.
BigValue¶
What a Scribe.Big field reads back, and what its arithmetic returns. It is
an object rather than a number, so data.Get().Wealth hands you something
carrying .M, .E and a handful of methods. Its surface has its own page:
see BigValue and Big Numbers.
Declarator options¶
FloatMeta¶
| Field | Type | What it is |
|---|---|---|
Min |
number? |
lowest accepted value; a write below it clamps or rejects per BoundsPolicy |
Max |
number? |
highest accepted value |
Precision |
(number | "f32")? |
opt in to a narrower wire form; omit to keep the full f64 |
The options table for Scribe.Number. Precision is the only thing that
narrows the field on the wire, and declaring Min and Max alone does not imply
it. Scribe.Int takes Min and Max but refuses Precision, because a bounded
integer already packs from its own bounds and there would be nothing left for
Precision to say.
BigMeta¶
| Field | Type | What it is |
|---|---|---|
Min |
(number | string)? |
lowest accepted value |
Max |
(number | string)? |
highest accepted value |
The options table for Scribe.Big. A bound past 1.8e308 has to be written as
a numeric string, because a larger Lua literal is already math.huge and the
template validator rejects that. Emberfall caps prestige currency with
Scribe.Big(0, { Max = "1e400" }).
CFrameMeta¶
| Field | Type | What it is |
|---|---|---|
Precision |
"exact"? |
store and send the full-fidelity CFrame instead of the packed one |
The options table for Scribe.CFrame. A CFrame packs lossily by default,
which is what you want for a saved checkpoint or camera. "exact" is the only
other value, because a CFrame has no intermediate widths to offer the way a
float does.
ArrayOpts¶
| Field | Type | What it is |
|---|---|---|
MaxItems |
number? |
cap on length; growth past it is refused unless Evict is set |
Evict |
("Front" | "Back")? |
turn the cap into a rolling window, dropping from that end |
The options table for Scribe.ArrayOf, and the only container options that
include Evict. Without MaxItems the array grows unbounded, which is how a
profile quietly outgrows its DataStore budget. Emberfall keeps a rolling
combat log with Scribe.ArrayOf(Scribe.String(""), { MaxItems = 50, Evict = "Front" }).
See Containers.
SetOpts¶
| Field | Type | What it is |
|---|---|---|
MaxItems |
number? |
cap on membership; growth past it is refused |
The options table for Scribe.SetOf. The cap always refuses rather than
evicting, and Evict itself is rejected as an unknown option here, because a
set has no oldest member to drop.
DictOpts¶
| Field | Type | What it is |
|---|---|---|
MaxKeys |
number? |
cap on how many keys the container may hold |
MaxKeyLength |
number? |
cap on the byte length of any one key, string keys only |
The options table for Scribe.DictOf and Scribe.MapOf. Both caps matter
because keys are attacker-controlled the moment a client command can name
one, and an unbounded key set is the quickest way past a profile's size
limit. MaxKeyLength counts bytes rather than characters, and an integer
Scribe.MapOf refuses it outright, where it would read as a cap that does
nothing.
Monetization¶
PurchaseSpec¶
| Field | Type | What it is |
|---|---|---|
Cost |
{ Path: string, Amount: number } |
the numeric field to debit, and by how much |
ItemId |
string |
what is being bought; recorded on the log entry |
Category |
string? |
a grouping label of your choosing, for filtering later |
Grant |
((data) -> ())? |
runs inside the same transaction as the debit |
Meta |
{ [string]: any }? |
extra fields stored with the log entry |
IdempotencyKey |
string? |
max 64 bytes; two calls carrying the same key apply once |
The table you hand Server.Purchase for a soft-currency purchase. The debit
and the Grant either both happen or neither does, so a Grant that throws
leaves the balance exactly where it was. A refused purchase claims nothing,
which is why retrying after one still works even with a key.
See Monetization.
PurchaseFilter¶
| Field | Type | What it is |
|---|---|---|
Kind |
("Robux" | "InGame")? |
Robux receipts, soft-currency purchases, or both when omitted |
Category |
string? |
match the Category recorded on the purchase |
ItemId |
string? |
match a single item id |
Since |
number? |
only entries at or after this Unix timestamp, in seconds |
Limit |
number? |
the most recent N matches |
The optional filter you pass Server.GetPurchases or Client.GetPurchases.
The fields combine with AND, so omitting all of them returns the whole
retained log, which is itself capped. See Monetization.
EconomyMeta¶
| Field | Type | What it is |
|---|---|---|
Flow |
("Source" | "Sink")? |
default: Increment is a Source, Decrement is a Sink |
TransactionType |
(Enum.AnalyticsEconomyTransactionType | string)? |
default "Gameplay" |
ItemSku |
string? |
what was bought or awarded |
Currency |
string? |
override the logged currency label; defaults to the field's name |
Fields |
{ [string]: any }? |
values for the custom fields you declared in Economy |
The tag you pass Value.Increment or Value.Decrement to have Scribe emit a
Roblox economy event alongside the write. Every field is optional and an
untagged Increment emits nothing, so you opt in per call rather than per
field. Source and Item are still accepted as aliases of TransactionType
and ItemSku. See Economy Analytics.
Leaderboards¶
LeaderboardEntry¶
| Field | Type | What it is |
|---|---|---|
Rank |
number |
1 for the top entry |
UserId |
number |
the player's user id |
Name |
string |
the username recorded when the score was written |
Score |
(number | BigScore) |
a big board hands back the big itself, not the packed key |
One row of the array Server.GetLeaderboard and Client.GetLeaderboard
return. Name is a snapshot rather than a live lookup, so a player who has
since changed their username shows the old one until their score is written
again. See Leaderboards.
Diagnostics¶
SaveInfo¶
| Field | Type | What it is |
|---|---|---|
Dirty |
boolean |
true when there are writes that have not reached the DataStore |
LastSaveAt |
number? |
Unix timestamp of the last successful save, in seconds; nil before the first |
LastResult |
("Ok" | "Fail")? |
how the last save attempt ended |
Size |
number? |
approximate bytes written by the last successful save |
What Server.GetSaveInfo and Client.GetSaveInfo return. Size is the field
worth watching: a profile creeping toward the DataStore limit shows up here
long before a save actually starts failing. See
Diagnostics.
BudgetSnapshot¶
| Field | Type | What it is |
|---|---|---|
Available |
boolean |
false when the engine could not be asked at all |
Reason |
string? |
why not, when Available is false |
Budgets |
{ [string]: number } |
remaining requests, keyed by DataStoreRequestType name |
At |
number |
Unix timestamp when the snapshot was taken, in seconds |
What Scribe.GetBudgetSnapshot returns. It is resolved per call and never
cached, and a server with no DataStoreService at all, such as a simulated
one, reports Available false rather than raising. See
Diagnostics.
LogEntry¶
| Field | Type | What it is |
|---|---|---|
At |
number |
Unix timestamp when the entry was recorded, in seconds |
Level |
LogLevel |
Debug through Fatal |
Category |
LogCategory |
which subsystem emitted it |
Code |
LogCode |
the stable identifier you branch on |
Message |
string |
human-readable text, not stable across versions |
Context |
{ [string]: any }? |
structured detail; the keys vary by Code |
One record from Scribe.GetRecentLogs, and the single argument every sink
registered with Scribe.AddLogSink receives. Branch on Code, never on
Message. See Diagnostics.
LogLevel¶
How severe a LogEntry is. The LogLevel option sets the floor, and
anything below it is never recorded at all, so raising it is the cheapest way
to quieten a noisy production log. See
Configuration.
LogCategory¶
type LogCategory = "Persistence" | "Replication" | "Transport" | "Commands" | "Leaderboards" | "Monetization" | "Gifting" | "Integrity" | "Lifecycle" | "Derived"
Which subsystem emitted a LogEntry. A sink added with Scribe.AddLogSink
usually filters on this rather than on individual codes, so you can route
Persistence somewhere loud and Replication somewhere quiet.
LogCode¶
The stable identifier on a LogEntry, such as PROFILE_LOAD_FAILED. There
are over 150 of them, and every one is listed with its meaning, its severity
and what to do about it in the Log Code Reference. This is
the field to branch on. Message is prose and is free to change between
versions.
Reasons¶
LifecycleReason¶
type LifecycleReason = "load-failed" | "migration-failed" | "session-ended" | "player-left" | "shutdown" | "still-loading" | "timeout"
Why a data session ended, or why waiting for data produced no tree. There is
one value per state so you can branch exhaustively. It is the second return
of Server.WaitForData, the argument Server.SessionEnded fires with, and
the reason Server.GetState stores. The names are also on Scribe.Reason if
you would rather not spell the strings. See
Session Lifecycle.
RequestReason¶
type RequestReason = "rate-limited" | "not-ready" | "unknown-command" | "bad-args" | "bad-idempotency-key" | "error" | "reply-encode-failed" | "timeout" | "send-failed" | "edit-mode" | "mock-error"
Why Scribe itself refused a Client.Request. The last four are produced on
the client and never reach the server. A handler that throws yields "error",
and its traceback goes to the COMMAND_ERROR log rather than to the client, so
an exploiter learns nothing from a crash. The names are also on
Scribe.RequestReason. See Commands & Requests.
PurchaseReason¶
type PurchaseReason = "player data not loaded" | "invalid Cost spec" | "invalid Cost amount" | "invalid cost path" | "cost path is not a spendable number" | "insufficient funds"
The fixed refusals of Server.Purchase. They are not exhaustive over the
second return: a Grant that throws passes its own error text through instead.
Test for "insufficient funds" specifically rather than assuming any other
string is one of these. The names are also on Scribe.PurchaseReason.
GiftReason¶
The fixed refusals of Server.PromptGift. Each one is a finished sentence you
can show a player unaltered: "buyer data not loaded",
"invalid recipient", "cannot gift yourself", "gift cooldown",
"too many pending gifts", "recipient already owns this",
"data services are experiencing issues; try again later",
"could not reserve gift credit; try again later",
"could not deliver gift; try again later",
"gift delivery could not be confirmed; do not send it again",
"could not record gift intent; try again later", and
"a gift of this item is already pending; try again shortly". Two further
refusals interpolate a value and so cannot be members. The names are also on
Scribe.GiftReason. See Gifting.
The last two delivery refusals are not interchangeable. DeliveryFailed means
the gift provably did not go out and a spent credit has been handed back, so a
retry is the right advice. DeliveryUnconfirmed means the delivery write may
have landed with only its answer lost: the credit stays spent on purpose, and a
retry would send a SECOND gift under an id the recipient cannot dedupe. Show
them as different outcomes.