Lua Learning — Networking Layer¶
This document maps the entire client/server networking surface of Lua Learning: every RemoteEvent and RemoteFunction, the data that flows through it, who fires it, who receives it, and why.
Companion doc: the overall source layout (features vs. kernel/foundation, aliases, the build pipeline) is in
architecture.md. This doc assumes that layout.Scope: Roblox/Luau project. All remotes are wrapped by the
vorlias/netlibrary (Net, v3.0.6). Remotes are declared per-feature and aggregated centrally; handlers and callers live across the feature slices (src/features/<X>/{server,client}), the server kernel (src/kernel), the context-split data modules (src/shared/Data), and the client app shell / shared components (src/client/{app,ui}).Module references below use aliases and omit line numbers (which drift on every edit). To find a remote's exact call sites, grep its name —
Remotes.Server:Create("Name")/Remotes.Client:Get("Name").
1. Architecture overview¶
1.1 Per-feature declaration, central aggregation¶
There is no single Remotes file anymore. Each feature declares the remotes it owns in its
own shared/remotes.luau, returning a plain { name = Net.Definitions.<Kind>() } table:
-- src/features/Quests/shared/remotes.luau
local Net = require("@pkg/Net")
return {
GetQuestContent = Net.Definitions.ServerAsyncFunction(),
UpdateQuestMetadatas = Net.Definitions.ServerToClientEvent(),
-- ...
}
@shared/Remotes/Core holds the app-wide remotes that
belong to no single feature (app info, content, economy, notifications, bookmarks).
@shared/Remotes then merges the Core group plus
every feature's shared/remotes into the single flat table that Net.Definitions.Create expects:
local definitionGroups = {
require("@shared/Remotes/Core"),
require("@features/Analytics/shared/remotes"),
require("@features/Classroom/shared/remotes"),
-- ...one per feature...
}
-- merged into one table; errors on a duplicate remote name
local Remotes = Net.Definitions.Create(declarations)
return Remotes
It errors on a duplicate remote name across groups, so two features can't silently claim the
same remote. Consumers are unchanged: server side Remotes.Server:Create(name), client side
Remotes.Client:Get(name).
When the game is not running (Fusion "story"/preview mode), @shared/Remotes short-circuits
and returns a mock with no-op Client:Get / Server:Create so UI stories render in isolation
without a live server.
1.2 The three remote kinds in use¶
Although vorlias/net supports more, this codebase uses exactly three definition kinds:
| Definition kind | Underlying instance | Direction | Server API | Client API |
|---|---|---|---|---|
ServerAsyncFunction() |
RemoteFunction | Client → Server → (response back) | :SetCallback(fn(player, ...)) returns value |
:CallServerAsync(...) returns a Promise |
ServerToClientEvent() |
RemoteEvent | Server → Client(s) | :SendToPlayer(p, ...), :SendToAllPlayers(...), :SendToPlayers({p}, ...), :SendToAllPlayersExcept({p}, ...) |
:Connect(fn(...)) |
ClientToServerEvent() |
RemoteEvent | Client → Server (fire-and-forget) | :Connect(fn(player, ...)) |
:SendToServer(...) |
Key behavioral notes:
- ServerAsyncFunction is the workhorse. Most remotes are request/response RPCs. The client
gets a Promise from :CallServerAsync() and uses :andThen() / :catch() / :await(). The
server callback's return value becomes the resolved value. Callbacks frequently return a
{ success = boolean, msg = string?, ... } envelope.
- Events carry no acknowledgement. ClientToServerEvent and ServerToClientEvent are one-way;
reliability/ordering is Roblox's default RemoteEvent behavior.
- Server handles are obtained with Remotes.Server:Create(name); client handles with
Remotes.Client:Get(name).
1.3 EncryptedNet — end-to-end encrypted remotes¶
A small subset of security-sensitive remotes are wrapped with EncryptedNet
(@pkg/EncryptedNet, boatbomber/encryptednet@1.0.4). It
performs an ECC (elliptic-curve) Diffie-Hellman handshake over a Handshake RemoteFunction at
require time, then transparently encrypts (ChaCha20) and signs every payload. The wrapper
exposes the same :SendToServer / :Connect / :CallServerAsync / :SetCallback API, so call
sites look identical — only the wrapping differs.
local EncryptedNet = require("@pkg/EncryptedNet")
local AttemptLogin = EncryptedNet(Remotes.Server:Create("AttemptLogin")) -- server
local AttemptLogin = EncryptedNet(Remotes.Client:Get("AttemptLogin")) -- client
The encrypted (credential-bearing) remotes are: AttemptLogin, AttemptRegister (Moderation),
AttemptClassroomLogin, RegisterClassroomPassword (Classroom).
1.4 Idiomatic patterns¶
Request/response (ServerAsyncFunction):
-- server (in a feature's server slice)
local GetProfile = Remotes.Server:Create("GetProfile")
GetProfile:SetCallback(function(Player, userId) return profileFor(userId) end)
-- client
Remotes.Client:Get("GetProfile"):CallServerAsync(userId):andThen(function(profile) ... end)
Server broadcast (ServerToClientEvent):
local ReceiveCoins = Remotes.Server:Create("ReceiveCoins") -- server
ReceiveCoins:SendToPlayer(Player, amount)
Remotes.Client:Get("ReceiveCoins"):Connect(function(amount) ... end) -- client
Client signal (ClientToServerEvent):
Remotes.Client:Get("HaltCode"):SendToServer() -- client
Remotes.Server:Create("HaltCode"):Connect(function(Player) ... end) -- server
1.5 Directory map of networking code¶
src/shared/Remotes/
init.luau ← aggregates Core + every feature's remotes into one Net.Definitions.Create
Core.luau ← app-wide remotes (app info, content, economy, notifications, bookmarks)
src/features/<X>/shared/remotes.luau ← each feature's own remote declarations
src/features/<X>/server/ ← :Create + :SetCallback / :Connect / :Send*
src/features/<X>/client/ ← :Get + :CallServerAsync / :Connect / :SendToServer
src/kernel/ ← PlayerData, FFlags, MessagesHandler (Core remote handlers)
src/shared/Data/{Content,Gamepasses}/ ← context-split data modules that own a few remotes
src/client/{app,ui}/ ← app-shell + shared-component callers
Packages/Net ← vorlias/net@3.0.6
Packages/EncryptedNet ← boatbomber/encryptednet@1.0.4 (ECC-encrypted wrapper)
1.6 Remote count by owning module¶
| Group | Owner | Count |
|---|---|---|
| Core (app info, content, economy, notifications, bookmarks) | @shared/Remotes/Core |
15 |
| Analytics | @features/Analytics |
2 |
| Lessons | @features/Lessons |
2 |
| Tutorials (images, tutorials, comments, submissions) | @features/Tutorials |
22 |
| Quests | @features/Quests |
9 |
| Code Running (shared, quest, lesson) | @features/CodeRunning |
15 |
| AI Tutor | @features/TutorAI |
6 |
| Profile | @features/Profile |
1 |
| Settings | @features/Settings |
2 |
| Onboarding | @features/Onboarding |
2 |
| Monetization | @features/Monetization |
6 |
| Feedback | @features/Feedback |
2 |
| IDE | @features/IDE |
1 |
| Deeplinking | @features/Deeplinking |
1 |
| Moderation | @features/Moderation |
10 |
| Classroom (security, discussions, comments, assignments) | @features/Classroom |
22 |
| Total | 118 |
Some remotes are declared in
Corebut handled outside the kernel, and a few feature remotes are handled in shared data modules — the count above is by declaring module. Handler locations are given per-remote below.
2. Core — app-wide¶
Declared in @shared/Remotes/Core. These belong to no single
feature; most are handled by the server kernel (@kernel/PlayerData, @kernel/FFlags,
@kernel/MessagesHandler) or by the context-split @shared/Data/Content module.
2.1 App info¶
GetServerType — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: the app loader at startup to decide which app variant to mount —
@app/AppLoader. - Received by:
@kernel/PlayerData. - Request payload: none
- Response: a
string— one of: "Private"—game.PrivateServerId ~= ""andPrivateServerOwnerId ~= 0(a school/classroom private server)"Reserved"— reserved server with no owner (temporary lobby used during shutdown cycling)"Standard"— public server- Why:
AppLoadermaps"Reserved" → ShutdownApp,"Private" → Classroom*App, otherwise the normal Desktop/Mobile/Console app. This is the first decision in client bootstrap.
GetFFlags — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: the shared settings module during settings load —
@ui/settings. - Received by:
@kernel/FFlags. - Request payload: none
- Response: the feature-flag table sourced from a Google Sheet via
SheetValues(each flag keyed by name). - Why: Pulls the current set of feature flags (betas) on startup so the client can gate
experimental features. Stored into
settings.betas.
BroadcastFFlags — ServerToClientEvent¶
- Direction: Server → all Clients
- Fired by:
@kernel/FFlags(:SendToAllPlayers(...), triggered by the sheet'sChanged). - Received by:
@ui/settings. - Payload: the full feature-flags table (same shape as
GetFFlags' response). - Why: Live-updates feature flags to every connected client whenever the backing Google Sheet changes, without requiring a rejoin.
ShutdownWarning — ServerToClientEvent¶
- Direction: Server → all Clients
- Fired by:
@kernel/MessagesHandler(:SendToAllPlayers(message, dismissDelay)), driven by a cross-serverMessagingService"Shutdown" message. Counts down minutes then seconds. - Received by: the app shells —
@app/DesktopApp,@app/MobileApp,@app/ClassroomDesktopApp. - Payload:
(message: string, dismissDelay: number) - Why: Warns players of an imminent server restart/update. Rendered as a
Toastthat auto-dismisses afterdismissDelayseconds.
2.2 Content¶
The Content system streams the game's lesson/tutorial/quest content (sourced from the
LuaLearningRBLX/Content GitHub repo) to clients on demand. Its server/client live in
@shared/Data/Content.
RetrieveContent — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@shared/Data/Content/Client— lazily fetches a content key the first time it's accessed. - Received by:
@shared/Data/Content/Server. - Request payload:
(key: string?)— a top-level content section name (e.g."Lessons","Tutorials"). Ifnil, the entire content table is returned. - Response:
Server.Value[key](or the whole table). The server waits up to 1 second for content to finish loading before responding. - Why: Pulls large content blobs only when a section is opened, rather than replicating everything at join.
BroadcastContent — ServerToClientEvent¶
- Direction: Server → all Clients
- Fired by:
@shared/Data/Content/Server—:SendToAllPlayers(key)for each content key whose value changed after a re-download (triggered by aMessagingService"ContentRefresh"message). - Received by:
@shared/Data/Content/Client— re-fetches that key if it was already loaded. - Payload:
(key: string)— the name of the content section that changed. - Why: Live content updates without a rejoin — when the content repo changes, clients refresh just the affected sections.
FilterText — ServerAsyncFunction¶
Not to be confused with the server-only
FilterTextmodule (the actual filtering implementation). This remote is the client-facing wrapper around Roblox's text filter. - Direction: Client → Server (RPC) - Fired by: UIs that filter user text before previewing —@features/Quests/client/Desktop/QuestDiscover, the ClassroomAssignmentsBrowse/{StudentBrowser,TeacherBrowser}andDiscussionsSection/DiscussionDiscover(under@app/ClassroomDesktopApp). - Received by:@kernel/PlayerData. - Request payload:(Text: string | { [any]: string })— a single string or a map of strings to filter in one round-trip. - Response: the filtered string, or a same-keyed table of filtered strings (or"Filter Error"if the input was neither). Filtering is scoped toPlayer.UserId. - Why: Lets client UIs display Roblox-filtered versions of user-entered text (search queries, previews) consistent with Roblox moderation rules.
2.3 Economy¶
Currency/reputation values pushed to the client whenever the server's backing values change. Both
are driven by GlobalStorage key-changed signals in @kernel/PlayerData.
ReceiveCoins — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@kernel/PlayerData(onCoinskey change, plus the initial value at join, default100). - Received by:
@app/DesktopApp,@app/MobileApp→sharedState.coins:set(amount). - Payload:
(amount: number) - Why: Keeps the client's coin balance in sync with the server-authoritative value.
ReceiveReputation — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@kernel/PlayerData(onReputationkey change, plus the initial value at join, default0). - Received by:
@app/DesktopApp,@app/MobileApp→sharedState.reputation:set(amount). - Payload:
(amount: number) - Why: Keeps the client's reputation total in sync with the server-authoritative value.
2.4 Notifications¶
In-app notification inbox. Notifications are stored as a map keyed by id in the player's
GlobalStorage "Notifications" value. All handled by @kernel/PlayerData.
Notifications are classroom-aware: in a classroom server,
RequestNotificationsreads from a<classroomId>/<userId>store; otherwise from the globalPlayerGlobal<userId>store.
RequestNotifications — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by: the notification popups —
@app/{DesktopApp,MobileApp,ClassroomDesktopApp}/AppComponents/PopupMenus/Notifications. - Received by:
@kernel/PlayerData— also lazily wires up aNotificationskey-changed connection that pushes future changes viaReceiveNotifications. - Payload: none
- Why: Asks for the current notifications and subscribes the player to live updates.
ReadNotification — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by: the notification popups, when a notification scrolls into view or is opened.
- Received by:
@kernel/PlayerData— setsnotifs[notifId].Unread = falseand echoes back viaReceiveNotifications. - Payload:
(notifId)— the id of a single notification. - Why: Marks one notification as read.
ReadAllNotifications — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by: (no client caller in the current source) — defined and handled, but not currently invoked; kept available for a "mark all read" action.
- Received by:
@kernel/PlayerData— setsUnread = falseon every notification, then echoes back viaReceiveNotifications. - Payload: none
- Why: Marks the entire inbox as read in one shot.
ReceiveNotifications — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@kernel/PlayerDataat multiple points: on theNotificationskey change, on initial join, and afterReadNotification/ReadAllNotifications. - Received by: the notification popups.
- Payload:
(notifications: { [id]: { Unread: boolean, ... } })— the full notifications map (defaults{}). - Why: Delivers the player's notification set (initial + live updates + post-read echoes).
2.5 Bookmarks¶
Per-player cross-content bookmarks, backed by the "bookmarks" DataStore2 store and handled by
@kernel/PlayerData.
GetBookmarks — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: the app shells at load —
@app/DesktopApp,@app/MobileApp→ setssharedState.bookmarks. - Received by:
@kernel/PlayerData. - Request payload: none
- Response: the player's bookmarks table (defaults
{}). - Why: Loads saved bookmarks on startup.
SetBookmarks — ServerAsyncFunction¶
A
ServerAsyncFunctiondespite the "Set" name — the client awaits a success/failure envelope. - Direction: Client → Server (RPC) - Fired by: the app shells, wheneversharedState.bookmarkschanges (wired up only after the initialGetBookmarksload) —@app/DesktopApp,@app/MobileApp. - Received by:@kernel/PlayerData. - Request payload:(newBookmarks: table)— rejected if not a table. - Response:{ success, msg }(success = false, msg = "Invalid bookmarks"on bad input; otherwisesuccess = true, msg = "Bookmarks saved"). - Why: Persists bookmark changes.
3. Analytics — @features/Analytics¶
Telemetry channels: one reports how long a player dwells in each section; the other mirrors server-side errors to clients for live diagnostics.
SectionTimeAnalytics — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by:
@features/Analytics/client/SectionTrackingwhenever the open section changes. - Received by (two independent handlers):
@features/Analytics/server/SectionTracking— commits the time spent in the previous section and starts a new timer.@features/TutorAI/server/Util— records the player's current section into the AI tutor's player-context.- Payload:
(sectionName: string)— validated server-side against an allow-list (Tutorials, Lessons, Quests, TutorAI, Multiplayer, Other, Discussions, Assignments, Gradebook, Dashboard). - Why: Drives GameAnalytics
SectionTracking:TimeSpent:<section>design events (only if ≥1 minute, not in Studio) and keeps the AI tutor aware of the player's location.
ShowServerError — ServerToClientEvent¶
- Direction: Server → Client(s)
- Fired by:
@features/Analytics/server/ErrorSharing—:SendToAllPlayers(...)on every new server warning/error (LogService.MessageOut), and:SendToPlayer(...)to replay log history on join. - Received by:
@features/Analytics/client/ErrorSharing— surfaces into the in-app developer console/output. - Payload:
(formattedMessage: string)— pre-formatted as"[server err|info|output|warn]: <message>". Only warnings/errors are shared, and never in Studio. - Why: Mirrors server-side errors into the client's output so creators/players can see server faults that would otherwise be invisible client-side.
4. Lessons — @features/Lessons¶
Per-player lesson position tracking. Handled by @features/Lessons/server/init, backed by the
"state" DataStore2 store. (Lesson code execution is the CodeRunning feature, §7.)
ViewLesson — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by:
@features/Lessons/client/Desktopwhen a lesson is opened. - Received by (two handlers):
@features/Lessons/server/init— persistsstate.lastLesson = { chapter, lesson, timestamp }.@features/TutorAI/server/Util— caches the current lesson's metadata into the AI tutor's player context.- Payload:
(chapter: number, lesson: number) - Why: Records the user's most recent lesson (for "resume where you left off") and informs the AI tutor of the lesson the player is viewing.
GetLastLesson — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/DesktopAppat load → setssharedState.lastLesson. - Received by:
@features/Lessons/server/init. - Request payload: none
- Response:
{ chapter, lesson, timestamp }from the"state"store (defaults{ 1, 1, 0 }). - Why: Lets the client restore the "continue learning" pointer on startup.
5. Tutorials — @features/Tutorials¶
Community tutorials: image helpers, chunked metadata browsing, full content fetch, voting,
coin-funded "awards", a threaded comment system, and the author-side submission/draft lifecycle.
Server logic spans @features/Tutorials/server/{init,Comments,Submissions}. Most responses use a
{ success, data | msg } envelope.
AI-tutor side channel:
ServerAsyncFunctions only allow one callback, so the AI tutor "hacks in" an extraOnServerEventlistener on the underlying remote instances ofGetTutorialContentandGetQuestContent(in@features/TutorAI/server/Util) to learn what the player is reading. (This works because the net library implements async functions over RemoteEvents.)
5.1 Images — @features/Tutorials/server/init¶
GetImageSize — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: markdown image rendering and image inputs —
@ui/Components/Markdown,@app/ClassroomDesktopApp/AppComponents/AssignmentsSection/AssignmentsEditor/InputComponent/Image. - Received by:
@features/Tutorials/server/init(server-cached). - Request payload:
(id: string)— an asset id (digits extracted viastring.match). - Response: success
{ success = true, data = { width, height, ... } }(queried fromapi.boatbomber.com/roblox/image-size); failure{ success = false, msg }. - Why: Lets the client compute correct aspect ratio for embedded images before they load (avoiding layout jumps). Cached server-side.
GetImageFromDecal — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@ui/Components/Markdown/getImageId— when a referenced asset is a Decal (AssetTypeId 13). - Received by:
@features/Tutorials/server/init(server-cached). - Request payload:
(id: string)— a decal asset id. - Response: success
{ success = true, data = { imageId } }— resolved viaInsertService:LoadAsset, reading the Decal'sTexture; failure{ success = false, msg }. - Why: Markdown image references may point at a Decal rather than the raw Image; this resolves
the real
rbxassetid://.
5.2 Tutorials — @features/Tutorials/server/init¶
GetTutorialMetadatas — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@shared/Data/Content/Client(loadTutorialChunk), called repeatedly with increasingchunkuntilendChunks. - Received by:
@features/Tutorials/server/init. - Request payload:
(chunk: number)— chunk index into the tutorial store. - Response: end of data
{ success = false, msg = "End of chunks" }; success{ success = true, data = { [key] = { Key, Title, Desc, AuthorId, Date, Votes, Awards } } }— slimmed metadata only (no content body). - Why: Lets clients page through tutorial metadata for the browse view without downloading full bodies.
GetTutorialContent — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Tutorials/client/{Desktop,Mobile}/TutorialReader. - Received by:
@features/Tutorials/server/init(plus the AI-tutor side-channel in@features/TutorAI/server/Util). - Request payload:
(key: string) - Response:
{ success = true, data = <tutorial.Content> }or{ success = false, msg = "Tutorial not found" }. Side effect: marks the tutorial read in the player's"stats"store and awards badge2124484900once 75 tutorials are read. - Why: Fetches the full tutorial body on open, and tracks reading progress / read-count badges.
GetPersonalTutorialVote — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Tutorials/client/{Desktop,Mobile}/TutorialReader. - Received by:
@features/Tutorials/server/init. - Request payload:
(key: string) - Response:
{ success = true, data = <vote> }wherevote ∈ {-1, 0, 1}from the"votes"store. - Why: Shows the user their current vote state on a tutorial.
SetPersonalTutorialVote — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by:
@features/Tutorials/client/{Desktop,Mobile}/TutorialReader. - Received by:
@features/Tutorials/server/init— pushes(key, value)into a per-playerTutorialVoteQueue, flushed in batches. - Payload:
(key: string, value: number)— the new vote (-1 | 0 | 1). - Why: Records the user's up/down vote; queued/batched to avoid hammering the vote store.
GiveTutorialAward — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Tutorials/client/{Desktop,Mobile}/TutorialReader/AwardPanel. - Received by:
@features/Tutorials/server/init. - Request payload:
(tutorialId: string, awardName: string) - Response:
{ success, data }. Failuredata:"Invalid award","Not enough coins","Tutorial not found","Can't give yourself an award". On success, deducts the award's coinPriceand incrementstutorial.Awards[awardName]. - Why: Lets a reader spend coins to grant a tutorial author an award (coin sink + author reward).
NavigateToTutorial — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/TutorAI/server/Tools— an AI-tutor "tool" that opens a tutorial for the player. - Received by:
@features/Tutorials/client/{Desktop,Mobile}— opens the named tutorial in the reader. - Payload:
(tutorialId: string) - Why: Allows the AI tutor to navigate the user directly to a relevant tutorial.
5.3 Tutorial Comments — @features/Tutorials/server/Comments¶
A threaded comment system on tutorials, stored as an MPTT (modified-preorder tree-traversal) tree
per tutorial. All client usage is in the Desktop/Mobile TutorialReader.
Trees are stored oldest-first, so posting a comment appends at the end of its sibling group and no
write ever has to reorder the tree. The reader's chosen order is applied when a window is packed, so
a response only ever carries the slice being displayed, in the order it displays in. The orders on
offer live in @features/Tutorials/shared/CommentSorts, which both sides share so the client can
place a merged page exactly where the server would have put it. Best is the considered one, ranking
by the Wilson lower bound on a comment's votes so a couple of upvotes counts as weak evidence, plus a
bounded award bonus that saturates rather than letting awards run away with the ranking. The other
three are plain numeric orders, a straight vote tally for Top and posting time for Newest and
Oldest. Anything unrecognized falls back to the default, Best.
Two-phase loading: the fetch remotes return a windowed tree skeleton with
Content,AuthorId, and individual awards stripped, carrying only what ordering and rendering need. The client then callsPopulateTutorialCommentper node to fetch the filtered content lazily.
GetTutorialComments — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Tutorials/client/{Desktop,Mobile}/TutorialReader→@features/Tutorials/server/Comments. - Request payload:
(key: string, sortName: string)— one ofCommentSorts.List. - Response:
{ success = true, data = <packed window> }— the first five top-level comments in the requested order with one reply previewed under each, plus the requester's own comments and the ancestors that connect them. The payload is parallel arrays ofIds,Depths,CreationTimes,TotalVotes,Upvotes, sparseAwardTotalsandDeletedmaps, andChildCountstelling the client how much more sits under each node. Content, author, and which awards a comment holds are populated on demand. - Why: Loads just the comments the reader can see, ordered the way they asked for.
GetMoreTutorialComments — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialReader→@features/Tutorials/server/Comments. - Request payload:
(key: string, parentId: string, haveIds: {string}, count: number, sortName: string)—haveIdsis the set of that parent's children the client already holds, which keeps paging gapless even while votes reshuffle the ordering between requests.countis clamped to 12 andhaveIdsto 100 entries. - Response:
{ success = true, data = <packed window> }in the same shape asGetTutorialComments, or{ success = false, data = "Comment not found" }for an unknown parent. Slices under the root preview one reply per comment, while deeper slices carry no previews since those replies stay collapsed until expanded. - Why: Pages the next batch of comments or replies in without resending what's already loaded.
PopulateTutorialComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialReader→@features/Tutorials/server/Comments. - Request payload:
(key: string, id: string) - Response:
{ Content, AuthorId, CreationTime, Deleted, Awards, PersonalVote }(ornil).Contentis Roblox-text-filtered for the requester;PersonalVoteis the requester's own vote. - Why: Fetches the full, filtered details for a single comment when it scrolls into view.
SetTutorialCommentVote — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by / Received by:
TutorialReader→@features/Tutorials/server/Comments— enqueues into a per-playerCommentVoteQueue(batched). - Payload:
(key: string, id: string, value: number)— tutorial key, comment id, vote (-1 | 0 | 1). - Why: Records a vote on an individual comment; batched to limit store writes.
GiveCommentAward — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Tutorials/client/{Desktop,Mobile}/TutorialReader/AwardPanel. - Received by:
@features/Tutorials/server/Comments. - Request payload:
(key: string, id: string, awardName: string) - Response:
{ success, data }. Failuredata:"Invalid award","Not enough coins","Cannot award that user". On success, deducts the award price and incrementsnode.Awards[awardName]. The stored order doesn't change, so the new award only moves the comment the next time a window is packed under a sort that counts awards. - Why: Lets readers spend coins to award a specific comment's author.
PostTutorialComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialReader→@features/Tutorials/server/Comments. - Request payload:
(key: string, id: string, content: string)—idis the parent node ("_ROOT"for a top-level comment). - Validation / failure
data:"Invalid content","Content too short"(<20),"Content too long"(>600), email/Discord-handle guards,"Content was rejected by the filter"(Damerau- Levenshtein vs filtered >25%),"Tutorial not found","Couldn't find target node". - Response:
{ success = true, data = "Posted successfully!", id = <newId> }. Also notifies the parent node's author with aClickInfodeep-link{ Type = "TutorialLink", TutorialId, CommentId }. - Why: Adds a new comment/reply to a tutorial's comment tree and notifies the author.
EditTutorialComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialReader→@features/Tutorials/server/Comments. - Request payload:
(key: string, id: string, content: string) - Validation: identical content checks to
PostTutorialComment. Only the node's own author may edit. - Response:
{ success = true, data = "Edited successfully!" }(failures mirror Post). - Why: Lets an author edit the text of their own comment.
DeleteTutorialComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialReader→@features/Tutorials/server/Comments. - Request payload:
(key: string, id: string) - Response:
{ success, data }.data:"Comment not found","Already deleted","You do not have permission to delete this comment"(must be the author or holdPermissions.Comments), or"Deleted successfully!". - Why: Removes a comment from the tree (author self-delete or moderator delete).
5.4 Submissions (tutorial drafts) — @features/Tutorials/server/Submissions¶
"Submissions" are user-authored tutorial drafts moving through the moderation pipeline (draft →
submitted/pending → accepted → published; the moderator side is §16). Each user's drafts live in an
InfStore keyed by UserId. All client usage is in @features/Tutorials/client/Desktop/TutorialCreate.
GetSubmissionMetadatas — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialCreate→@features/Tutorials/server/Submissions. - Request payload: none
- Response:
{ success = true, data = { [key] = { Key, Title, Desc, Date, Status } } }— slimmed metadata of the caller's own submissions. - Why: Lists the author's drafts/submissions in the tutorial creator.
GetSubmissionContent — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialCreate→@features/Tutorials/server/Submissions. - Request payload:
(key: string) - Response:
{ success = true, data = <submission.Content> }or{ success = false, msg = "Tutorial not found" }. - Why: Loads the full draft body for editing.
CreateSubmission — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialCreate→@features/Tutorials/server/Submissions. - Request payload: none
- Response:
{ success = true, data = <key> }. Adds a blank"Untitled"draft (Status = "Unsubmitted Draft"). - Why: Starts a new tutorial draft.
SaveSubmission — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialCreate→@features/Tutorials/server/Submissions. - Request payload:
(key: string, data: { Content, Title, Desc })— each field type-validated. - Response:
{ success, msg?, validatedSubmission? }. Skips writing if unchanged. Editing resets a draft's status to"Unsubmitted Draft"; re-queues a Pending item for re-audit. - Why: Persists edits to a draft and keeps the moderation queue consistent.
SubmitSubmission — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialCreate→@features/Tutorials/server/Submissions. - Request payload:
(key: string) - Response:
{ success, msg, ... }. Validation: content ≥50, title 5–40, description 10–140. Costs 25 coins for a brand-new submission (edits to an already-published tutorial are free). - Why: Submits a draft into the moderation queue (coin-gated for new tutorials).
UnsubmitSubmission — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialCreate→@features/Tutorials/server/Submissions. - Request payload:
(key: string) - Response:
{ success, msg, validatedSubmission? }. Resets status and removes the item from both moderation queues. - Why: Lets an author pull a submission back out of the moderation queue.
DeleteSubmission — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
TutorialCreate→@features/Tutorials/server/Submissions. - Request payload:
(key: string) - Response:
{ success, msg }("Tutorial not found"or"Deleted successfully"). Removes the draft from the user store plus both moderation queues. - Why: Deletes a draft submission entirely.
6. Quests — @features/Quests¶
Covers both playing quests (a grid/board puzzle) and authoring them. Browsing/playing is in
@features/Quests/server/init; the create/edit/publish lifecycle is in @features/Quests/server/Authoring.
Published quests are in questsStore; per-user drafts in a FreedumbStore keyed by UserId.
(Quest code execution is the CodeRunning feature, §7.)
Quest locator (
QuestLoc) — most quest reads take a locator table:{ scope = "official" | "global" | "player", id = number | string, chunkIndex = number? }.officialquests come from the GitHub Content (numeric ids);globalquests are user-created (string GUID ids, paged bychunkIndex).
GetQuestMetadatas — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@shared/Data/Content/Client— paged with{ scope = "global", chunkIndex }untilendChunks. - Received by:
@features/Quests/server/init. - Request payload:
(QuestLoc)— usesscope+chunkIndex. - Response:
{ success, message, data?, endChunks? }. Forglobal,datais a chunk of per-quest metadata. Theofficialbranch is deprecated (Content handles official quests automatically). - Why: Pages through user-created quest metadata for the quest browser.
UpdateQuestMetadatas — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/Quests/server/init—OnChunkChangedpushes freshly processed chunk metadata to each player. - Received by:
@shared/Data/Content/Client— merges the updated chunk into the client's quest metadata. - Payload:
(processedChunk)— a map of quest metadata. - Why: Live-updates quest metadata when the global quests store changes, without a re-fetch.
GetQuestContent — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Quests/client/Desktop. - Received by:
@features/Quests/server/init(plus the AI-tutor side-channel in@features/TutorAI/server/Util). - Request payload:
(QuestLoc)—scope+id. - Response:
{ success, message, data? }wheredatais the full quest object (board, goals, instructions). - Why: Loads a full quest to play it.
ReportQuest — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Quests/client/Desktop→@features/Quests/server/init. - Request payload:
(QuestId: string)— a global quest GUID. - Response:
{ success, message }. RecordsQuest.Reports[<reporterUserId>] = true. - Why: Lets players flag a user-created quest for moderation.
GetUserQuests — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Quests/client/Desktop/QuestCreate. - Received by:
@features/Quests/server/init. - Request payload:
(userId?)— defaults to the caller. - Response:
{ success, message, data? }— all of that user's quests from theirFreedumbStore. - Why: Lists a user's own authored quests in the quest creator.
CreateQuest — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Quests/client/Desktop/QuestCreate. - Received by:
@features/Quests/server/Authoring. - Request payload: none
- Response:
{ success = true, data = <key> }. Creates a blank 5×5 quest draft (Status = "Unsubmitted Draft"). - Why: Starts a new quest draft and returns its id.
SaveQuest — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Quests/client/Desktop/QuestCreate. - Received by:
@features/Quests/server/Authoring. - Request payload:
(QuestId: string, Quest: table) - Response:
{ success, message?, err?, extraData?, validatedSubmission? }. RunsQuestData.Validation.ValidateSubmission; skips writing if unchanged. - Why: Persists edits to a quest draft (with validation).
DeleteQuest — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Quests/client/Desktop/QuestCreate. - Received by:
@features/Quests/server/Authoring. - Request payload:
(QuestId: string) - Response:
{ success, message }. Removes the quest from both the draft store andquestsStore. - Why: Deletes a user's quest (draft + published copies).
PublishQuest — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Quests/client/Desktop/QuestCreate. - Received by:
@features/Quests/server/Authoring. - Request payload:
(QuestId: string) - Response:
{ success, message?, err?, extraData? }. Requires: passes validation, a valid playtest signature exists, caller is the author, and ≥25 coins (charges 25 on publish). - Why: Publishes a validated, playtested quest to the public board (coin-gated).
7. Code Running — @features/CodeRunning¶
The heart of the learning experience: the player writes Luau code that the server executes in a
sandbox ("Job") with a time limit, streaming output/current-line/status back live, then evaluating
success. Two flavors — Lessons (run code, check a condition) and Quests (drive a grid-board
puzzle) — plus shared infrastructure. Orchestration is in @features/CodeRunning/server/init; the
per-job streaming helpers are in @features/CodeRunning/server/Runner/JobInjections/{Quest,Lesson}.
On an execution Error, the server kicks off an AI-tutor "debug help" conversation (
AIDebugHelp), budget-permitting (see §8).Output
Typeis a string tag ("Normal","Special","Error", …) used by the client console to style each output line.
7.1 Shared¶
HaltCode — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by: the stop buttons —
@features/Quests/client/Desktop/QuestWorkspace,@features/Lessons/client/Desktop/Workspace/Lesson. - Received by:
@features/CodeRunning/server/init— halts all of the player's active jobs. - Payload: none
- Why: Lets the user stop a running/looping program.
7.2 Quest code running¶
Handlers in @features/CodeRunning/server/init; per-action streaming from
@features/CodeRunning/server/Runner/JobInjections/Quest. Client side: the quest workspace
(@features/Quests/client/Desktop/QuestWorkspace) and quests section (@features/Quests/client/Desktop).
RunQuestCode — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by:
@features/Quests/client/Desktop/QuestWorkspace. - Received by:
@features/CodeRunning/server/init. - Payload:
(QuestLoc, Code: string) - Behavior: Loads the quest (official from Content / global from
QuestsStore/ player draft), restores collected-coin state, runs the code as a "Quest" sandbox job (180s limit), and on completion evaluates goals →QuestEvaluationResult. First-time coin collection awards +1 coin; beating a global quest increments itsCompletions; beating a player draft records a validated signature (enables publishing). - Why: Executes the player's quest solution and drives the whole quest play/eval loop.
QuestCodeStatus — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/CodeRunning/server/init—trueat start,falseon completion/error/invalid. - Received by:
@features/Quests/client/Desktop/QuestWorkspace. - Payload:
(isRunning: boolean) - Why: Toggles the running/stop UI state for quests.
QuestOutput — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/CodeRunning/server/init(status/halt messages) and the job injection in.../JobInjections/Quest(per-action output). - Received by:
@features/Quests/client/Desktop/QuestWorkspace. - Payload:
(Text: string, Type: string) - Why: Streams console output lines from the running quest program.
QuestLine — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
.../JobInjections/Questduring execution;-1on finish clears the highlight. - Received by:
@features/Quests/client/Desktop/QuestWorkspace. - Payload:
(lineNumber: number)— currently executing line (-1= none). - Why: Highlights the line of code currently running in the quest editor.
QuestSync — ServerToClientEvent¶
- Direction: Server → Client
- Fired by: quest job injections whenever the board state changes (move/interact/rotate). Created
eagerly in
@features/CodeRunning/server/initso clients can connect. - Received by:
@features/Quests/client/Desktop. - Payload:
(Board)— the full board state (player position/rotation, tiles, floors, metadata, goals-met). - Why: Keeps the client's visual board in sync with the authoritative simulation as the code runs.
QuestAnimate — ServerToClientEvent¶
- Direction: Server → Client
- Fired by: the move injection's
Animatecallback (.../JobInjections/Quest). Created eagerly in@features/CodeRunning/server/init. - Received by: passed as the
animateEventprop into the quest board visual in@features/Quests/client/Desktop/QuestWorkspace. - Payload:
(id: string, duration: number, dontFocus: boolean?) - Why: Triggers a board animation for a duration, optionally without focusing the camera.
QuestGoalMet — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
Injections.checkGoalswhen a goal first becomes satisfied (.../JobInjections/Quest). - Received by:
@features/Quests/client/Desktop/QuestWorkspace(onGoalMet). - Payload:
(goalIndex: number) - Why: Lets the UI tick off each quest goal as it's achieved mid-run.
QuestEvaluationResult — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/CodeRunning/server/init— once the job finishes and goals are evaluated. - Received by:
@features/Quests/client/Desktop/QuestWorkspace. - Payload:
(isSuccess: boolean)— all non-hint goals met. - Why: Tells the client whether the quest was solved (success → completion flow).
GetQuestCoinState — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Quests/client/Desktop. - Received by:
@features/CodeRunning/server/init. - Request payload:
(questId) - Response:
{ ["x,y"] = true, ... }— which coin tiles the player has already collected (from the"quest_coins"store; defaults{}). - Why: Lets the client render already-collected coins as collected (so they're not re-awarded/re-shown).
7.3 Lesson code running¶
Handlers in @features/CodeRunning/server/init; per-action streaming from
@features/CodeRunning/server/Runner/JobInjections/Lesson. Client side: @features/Lessons/client/Desktop/Workspace/Lesson.
RunLessonCode — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by:
@features/Lessons/client/Desktop/Workspace/Lesson. - Received by:
@features/CodeRunning/server/init. - Payload:
(Code: string, ChapterNum: number, LessonNum: number) - Behavior: Validates chapter/lesson, runs the code in a "Lesson" sandbox (100s limit), then runs
the lesson's
Condition(Code, EnvTrack)to evaluate success →LessonEvaluationResult, logging a GameAnalytics progression event (Complete/Fail). - Why: Executes the learner's code for a lesson and checks the lesson's pass condition.
LessonCodeStatus — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/CodeRunning/server/init—trueat start,falseon finish/invalid. - Received by:
@features/Lessons/client/Desktop/Workspace/Lesson. - Payload:
(isRunning: boolean) - Why: Toggles the lesson run/stop UI state.
LessonOutput — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/CodeRunning/server/initandInjections.LessonOutput(.../JobInjections/Lesson). - Received by:
@features/Lessons/client/Desktop/Workspace/Lesson. - Payload:
(Text: string, Type: string) - Why: Streams console output from the running lesson program.
LessonLine — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
Injections.LessonLineRepl(.../JobInjections/Lesson);-1on finish. - Received by:
@features/Lessons/client/Desktop/Workspace/Lesson. - Payload:
(lineNumber: number)— currently executing line (-1= none). - Why: Highlights the active line in the lesson editor.
LessonEvaluationResult — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/CodeRunning/server/init(and the error fallback). - Received by:
@features/Lessons/client/Desktop/Workspace/Lesson. - Payload:
(result: boolean, message: string?) - Why: Tells the client whether the lesson passed (with an optional explanation); success triggers the confetti/completion celebration.
8. AI Tutor — @features/TutorAI¶
The conversational AI tutor. The client sends user inputs (start/message/rate) as
ClientToServerEvents; the server streams back the conversation snapshot, individual AI replies, and
a typing indicator via ServerToClientEvents. Server logic is in
@features/TutorAI/server/{init,ConversationManager}; the UIs are the Desktop/Mobile TutorAI sections
(@features/TutorAI/client/{Desktop,Mobile}).
Shared message shape (used by ReceiveTutorConvo.initialMessages and ReceiveTutorMessage):
{ role = "user" | "ai", content = string, metadata = { id = string?, ... }, temporary = boolean? }
StartTutorConversation — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by: the tutor UI on open and on "new conversation" —
@features/TutorAI/client/{Desktop,Mobile}. - Received by:
@features/TutorAI/server/init→startConversation(player, "user"). - Payload: none
- Why: Creates/resets the player's
ConversationManager. The server replies viaReceiveTutorConvo.
SendTutorMessage — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by: the tutor UI when the user sends a message —
@features/TutorAI/client/{Desktop,Mobile}(also optimistically appends the user's message locally). - Received by:
@features/TutorAI/server/init→sendUserMessage(player, content). - Payload:
(content: string) - Why: Feeds the prompt into the LLM conversation. The server toggles
ReceiveTutorTypingand streams the reply viaReceiveTutorMessage.
RateTutorMessage — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by: the message bubble thumbs up/down —
@ui/Components/TutorMessages(and required by@ui/Components/MessageBubble). - Received by:
@features/TutorAI/server/init→Conversation:voteOnMessage. - Payload:
(message_id: string, vote: number) - Why: Captures per-message quality feedback for the AI tutor (used in analytics).
ReceiveTutorTyping — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/TutorAI/server/init(truebefore generating,falseon exit/finish). - Received by:
@features/TutorAI/client/{Desktop,Mobile}. - Payload:
(typing: boolean) - Why: Drives the "AI is typing…" indicator while the LLM generates a response.
ReceiveTutorMessage — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/TutorAI/server/ConversationManager(real AI reply withmetadata, andreplicateClientOnlyMessagefor errors/system notices). - Received by:
@features/TutorAI/client/{Desktop,Mobile}→appendMessage(message). - Payload: a single message table
{ role = "ai", content, metadata, temporary? }. Content is Roblox-text-filtered server-side; if >40% gets filtered, a cannedAI_ROBLOX_FILTER_REPLYis sent instead. - Why: Streams each AI/system message to the player's tutor view as it's produced.
ReceiveTutorConvo — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/TutorAI/server/ConversationManager(after the access/subscription check, with a fallback if validation fails). - Received by:
@features/TutorAI/client/{Desktop,Mobile}. - Payload:
{ initialMessages = { <message>, ... }, -- user + non-empty AI messages hasAccess = boolean, -- allowed to use the tutor at all isSubscribed = boolean, -- premium subscription state } - Why: Sends the full conversation snapshot plus access/subscription gating when a conversation starts (or resumes), so the client can render history and gate UI.
9. Profile — @features/Profile¶
GetProfile — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: anywhere a profile is viewed —
@features/Profile/client/{Desktop,Mobile,ClassroomDesktop}/ProfileView,@features/Multiplayer/client/Desktop/Invites. - Received by:
@features/Profile/server/init. - Request payload:
(ProfileId: number)— a target player'sUserId; returns{}if not a number. - Response:
Built from a
{ bio = string, -- the target's bio, Roblox-text-filtered for the requester pose = any, -- avatar pose cosmetic frame = any, -- avatar frame cosmetic reputation = number, -- target's reputation total }DataStore2.ReadOnly("settings", ProfileId)read plusGlobalStoragereputation; the read-only stores are cleaned up 60s later (a short cache). - Why: Lets any player view another player's public profile, with the bio filtered against the viewer's moderation context.
10. Settings — @features/Settings¶
Per-player client settings (theme, toggles, …) persisted to the "settings" DataStore2 store.
Handled by @features/Settings/server/init; the client side is the shared settings module @ui/settings.
GetSavedSettings — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@ui/settings(Load()). - Received by:
@features/Settings/server/init. - Request payload: none
- Response: the saved settings table (defaults
{}). - Why: Restores the player's saved settings on startup.
SetSavedSettings — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by:
@ui/settings(Save()) — serializes the reactive settings into a plain table. - Received by:
@features/Settings/server/init. - Payload:
(Settings: table)— rejected if not a table. - Why: Persists settings changes.
11. Onboarding — @features/Onboarding¶
Out-of-box-experience (OOBE) onboarding-tip tracking, backed by the "oobe" DataStore2 store.
GetViewedOOBE — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Onboarding/client/Desktop. - Received by:
@features/Onboarding/server/init. - Request payload: none
- Response: a
{ [key: string]: boolean }map (defaults{}), describing which onboarding tips/tooltips the player has already seen. - Why: Lets the OOBE handler skip tips the player has already dismissed.
SetViewedOOBE — ClientToServerEvent¶
- Direction: Client → Server (fire-and-forget)
- Fired by:
@features/Onboarding/client/Desktop— on change of each OOBE state (easter-egg states are not saved). - Received by:
@features/Onboarding/server/init. - Payload:
(key: string, value: boolean) - Why: Marks an onboarding tip as viewed so it won't show again next session.
12. Monetization — @features/Monetization¶
Robux donations (with a live feed), gamepass state, and coin-purchased cosmetic assets (avatar
poses/frames). Donations/assets in @features/Monetization/server/init; gamepasses in the context-split
@shared/Data/Gamepasses.
DonationFeedEvent — ServerToClientEvent¶
- Direction: Server → Client(s)
- Fired by:
@features/Monetization/server/init(replays the stored feed to a player on join) and@kernel/MessagesHandler(:SendToAllPlayers(...)from a cross-serverMessagingService"MessageFeed"). - Received by:
@features/Monetization/client/{Desktop,Mobile}/DonateMenu. - Payload: a feed
packet—{ Type = "DonationFeed", Username, Amount, Time }. - Why: Drives the live donation feed/leaderboard (and replays recent donations to joiners).
DonationComplete — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/Monetization/server/init— after a donation product receipt is processed. - Received by:
@features/Monetization/client/{Desktop,Mobile}/DonateMenu. - Payload: none
- Why: Signals the donate UI that the player's donation went through (thank-you/celebration).
GetGamepasses — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@shared/Data/Gamepasses/Client(loadGamePasses, retries on failure). - Received by:
@shared/Data/Gamepasses/Server. - Request payload: none
- Response: a
{ [passName] = true }map for the gamepasses the player owns (defaults{}). - Why: Loads owned gamepasses on startup (gates premium features like "Beta Tester").
UpdateGamepasses — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@shared/Data/Gamepasses/Server(initial computed set, and after a purchase is detected). - Received by:
@shared/Data/Gamepasses/Client. - Payload:
(PlayerPasses)— the{ [passName] = true }map. - Why: Pushes gamepass ownership changes live (e.g. right after a purchase).
GetOwnedAssets — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: the app shells —
@app/{DesktopApp,MobileApp,ClassroomDesktopApp}. - Received by:
@features/Monetization/server/init. - Request payload: none
- Response: the player's
"owned_assets"store —{ [assetId] = true }(defaults{}). - Why: Loads which cosmetic assets (poses/frames) the player owns on startup.
PurchaseAsset — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: the profile cosmetic UI —
@features/Settings/client/{Desktop,Mobile}/Profile. - Received by:
@features/Monetization/server/init. - Request payload:
(assetId)— a pose or frame id (looked up inPoses.Dict/Frames.Dict). - Response:
{ success, data }. Failuredata:"Invalid asset ID","Not enough coins". On success, deducts the asset'sCost, marks it owned, returns"Purchase successful". - Why: Buys a cosmetic avatar asset with coins.
13. Feedback — @features/Feedback¶
User feedback / bug reports relayed to a Slack workspace via a SlackBot, with per-user threading.
Server logic in @features/Feedback/server/init; UI in @features/Feedback/client/{Desktop,Mobile}.
Channels are "Feedback" and "Bug Report".
GetThread — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Feedback/client/{Desktop,Mobile}. - Received by:
@features/Feedback/server/init. - Request payload:
(channel: string)—"Feedback"or"Bug Report"(else"Invalid channel"). - Response: the Slack thread's messages array (the player's prior conversation),
{}if none yet, or an error string. Cached bythread_ts. - Why: Loads the player's existing feedback/bug-report conversation so they can see replies.
SendMessage — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@features/Feedback/client/{Desktop,Mobile}(with a"Desktop"/"Mobile"platform tag). - Received by:
@features/Feedback/server/init. - Request payload:
(message: string, channel: string, platform: string) - Validation / returns (status string): cooldown,
>600chars,<15chars / no word chars, duplicate, email/Discord-handle guards, else"Message sent!"/"Message failed to send...". Posts to Slack (with the user's profile link + platform + Beta Tester tag), threaded per user. - Why: Sends a feedback/bug-report message to the team's Slack, threaded per user.
14. IDE — @features/IDE¶
GetLoadstringErrors — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@ui/Components/IDE/ErrorHighlighter— debounced, on the editor's current text. - Received by:
@features/IDE/server/init. - Request payload:
(code: string) - Response:
falseif the code compiles, otherwise theloadstringerror string. - Why: Powers live syntax-error squiggles in the code editor (server-side
loadstring, since the client can'tloadstring).
15. Deeplinking — @features/Deeplinking¶
GetDeeplink — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by: the app shells ~2s after load —
@app/DesktopApp,@app/MobileApp(the Classroom app has this commented out). - Received by:
@features/Deeplinking/server/init. - Request payload: none
- Response: the player's launch data table (or
{}). Decoded server-side fromPlayer:GetJoinData().LaunchData(JSON) on join. Shape:{ section: string?, id: string? }whereidfor"Lessons"is"<chapter>,<lesson>"and for"Tutorials"is a tutorial id. - Why: Lets external links (Roblox launch data) deep-link a player straight into a specific
section / tutorial / lesson. The client sets
sharedState.openSection,openTutorial, orcurrentLesson.
16. Moderation — @features/Moderation¶
The in-app moderation console (for staff with Permissions.Submission / Permissions.Admin): a
password-gated login, then a queue to accept/reject/publish user tutorial submissions. The two
credential remotes are EncryptedNet-wrapped. Privileged calls carry a sessionKey (issued by
AttemptLogin) that the server re-validates. Server logic in @features/Moderation/server/init; UI in
@features/Moderation/client/Desktop ({init, LoginScreen, RegisterScreen, ModTools}).
Submission queue stores:
unsortedStore(awaiting review) →acceptableStore(accepted, awaiting publish) →tutorialStore(published). Accept/Reject are queued and flushed in batches (~6s), which also notifies authors and re-broadcasts the queue to moderators.
GetLoginStatus — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Moderation/client/Desktop→@features/Moderation/server/init. - Request payload: none
- Response:
""(no permission),"Login"(registered, needs to log in), or"Register"(has permission but no password set yet). - Why: Decides whether to show the moderator the login, the register, or nothing.
ReceiveLoginStatus — ServerToClientEvent¶
- Direction: Server → Client
- Fired by:
@features/Moderation/server/init("Login"after register,"LoggedIn"after successful login). - Received by:
@features/Moderation/client/Desktop. - Payload:
(status: string)—"Login"|"LoggedIn". - Why: Advances the moderator UI's auth state.
AttemptRegister — ServerAsyncFunction (EncryptedNet)¶
- Direction: Client → Server (RPC, encrypted+signed)
- Fired by:
@features/Moderation/client/Desktop/RegisterScreen. - Received by:
@features/Moderation/server/init. - Request payload:
(Password: string) - Response:
{ success, msg }. On success, sendsReceiveLoginStatus("Login"). - Why: Sets a moderator's password for the first time.
AttemptLogin — ServerAsyncFunction (EncryptedNet)¶
- Direction: Client → Server (RPC, encrypted+signed)
- Fired by:
@features/Moderation/client/Desktop/LoginScreen. - Received by:
@features/Moderation/server/init. - Request payload:
(Password: string) - Response:
{ success, msg, sessionKey? }. On success, sendsReceiveLoginStatus("LoggedIn")and returns asessionKey(used by all subsequent privileged calls). After 5 incorrect attempts the player is kicked. - Why: Authenticates a moderator and issues their session key.
GetUnsortedSubmissions — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Moderation/client/Desktop/ModTools→@features/Moderation/server/init. - Request payload:
(sessionKey: string)— requires"Submission"permission (returns{}otherwise). - Response: the full
unsortedStoremap of submissions awaiting review. - Why: Loads the queue of un-reviewed tutorial submissions.
GetAcceptableSubmissions — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Moderation/client/Desktop/ModTools→@features/Moderation/server/init. - Request payload:
(sessionKey: string)— requires"Submission"permission. - Response: the
acceptableStoremap (accepted submissions awaiting publish). - Why: Loads the queue of accepted-but-not-yet-published submissions.
BroadcastSubmissions — ServerToClientEvent¶
- Direction: Server → Clients (logged-in moderators)
- Fired by:
@features/Moderation/server/init—:SendToPlayers(moderators, {...})after each accept/reject flush. - Received by:
@features/Moderation/client/Desktop/ModTools. - Payload:
{ Unsorted = <unsortedStore set>, Acceptable = <acceptableStore set> } - Why: Live-syncs the moderation queues across all logged-in moderators after changes.
AcceptSubmission — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Moderation/client/Desktop/ModTools→@features/Moderation/server/init. - Request payload:
(sessionKey: string, tutorialId: string)— requires"Submission"permission. - Response:
true/false. Queues the submission for acceptance (unsorted → acceptable on next flush). - Why: Approves a submission into the "acceptable" pool.
RejectSubmission — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Moderation/client/Desktop/ModTools→@features/Moderation/server/init. - Request payload:
(sessionKey: string, tutorialId: string)— requires"Submission"permission. - Response:
true/false. Queues a rejection; on flush, sets the author's submission status to"Rejected Draft"and notifies them (with aSubmissionLinkdeep-link). - Why: Rejects a submission and informs the author.
PublishSubmission — ServerAsyncFunction (Admin-only)¶
- Direction: Client → Server (RPC)
- Fired by / Received by:
@features/Moderation/client/Desktop/ModTools→@features/Moderation/server/init. - Request payload:
(sessionKey: string, tutorialId: string)— requires"Admin"permission. - Response:
true/false. Moves the submission into the livetutorialStore(preserving existing awards if already published), removing it from both pending queues. - Why: Publishes an accepted submission live as a real tutorial (admin gate).
17. Classroom — @features/Classroom¶
Classroom mode runs in private servers for schools: a password login gate, an activity log, a
discussion board, and assignments/gradebook. Every classroom callback first checks
Security:IsLoggedIn(Player), and actions are written to the ActivityLog. Server logic is split
across @features/Classroom/server/{Security,ActivityLog,Discussions,Assignments}. The UI lives in
the @app/ClassroomDesktopApp shell (the classroom apps are app-shell variants, not a client slice).
17.1 Security & activity log¶
The two credential-bearing remotes are EncryptedNet-wrapped (see §1.3).
GetClassroomOwner — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp. - Received by:
@features/Classroom/server/Security. - Request payload: none
- Response: the classroom owner's UserId.
- Why: Lets the client know who the teacher/owner is (drives teacher-vs-student UI).
GetClassroomRegistered — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp. - Received by:
@features/Classroom/server/Security. - Request payload: none
- Response:
{ success = true, isRegistered = boolean }— whether this classroom has a password set yet. - Why: Decides whether to show the teacher the "set a password" setup or the student login.
AttemptClassroomLogin — ServerAsyncFunction (EncryptedNet)¶
- Direction: Client → Server (RPC, encrypted+signed)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/StartupScreen/{StudentSetup,SecurityLock}. - Received by:
@features/Classroom/server/Security. - Request payload:
(Password: string) - Response:
{ msg = <result>, sessionKey = <key>? }. On success logs aPlayerJoinedactivity and returns a session key. - Why: Authenticates a student into the classroom with the teacher-set password.
RegisterClassroomPassword — ServerAsyncFunction (EncryptedNet)¶
- Direction: Client → Server (RPC, encrypted+signed)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/StartupScreen/TeacherSetup. - Received by:
@features/Classroom/server/Security. - Request payload:
(Password: string) - Response:
{ success, msg }. Only the server owner may set it; logs aClassroomPasswordSetactivity on success. - Why: Lets the teacher (server owner) set the classroom's login password.
GetClassroomActivityLogs — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/DashboardSection. - Received by:
@features/Classroom/server/ActivityLog. - Request payload:
(showingTimespan: "Hour" | "Day" | "Week" | "Month" | "All") - Response:
{ success, data?, message? }. The owner gets all logs; a student gets only their own entries (server-side filtered byuserId). - Why: Powers the teacher dashboard's activity feed (with student-scoped privacy).
17.2 Discussions¶
A classroom discussion board (posts + threaded comments). Structurally mirrors the public
tutorial+comment system, but classroom-scoped (login required) and activity-logged. All in
@features/Classroom/server/Discussions; UI in @app/ClassroomDesktopApp/AppComponents/DiscussionsSection.
Responses use { success, message, data? }.
GetDiscussionMetadatas — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/DiscussionsSection. - Received by:
@features/Classroom/server/Discussions. - Request payload: none
- Response: per-post metadata
{ Key, AuthorId, Title, OriginalPostDate, LatestReplyDate }(no body). Requires login. - Why: Lists discussion threads in the board.
UpdateDiscussionsMetadatas — ServerToClientEvent¶
- Direction: Server → Client (logged-in classroom members)
- Fired by:
@features/Classroom/server/Discussions— onOnChunkChanged, pushed to each player with an active session. - Received by:
@app/ClassroomDesktopApp/AppComponents/DiscussionsSection. - Payload:
(processedChunk)— post metadata map. - Why: Live-updates the discussion list as posts change.
GetDiscussionContent — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/DiscussionsSection(+ itsDiscussionsReader). - Received by:
@features/Classroom/server/Discussions. - Request payload:
(discussionId: string) - Response:
{ success, message, data? }— the Roblox-text-filtered postContent. Logs aViewedDiscussionactivity. Requires login. - Why: Opens a discussion post's body.
PostDiscussion — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/DiscussionsSection. - Received by:
@features/Classroom/server/Discussions. - Request payload:
(discussionId: string, post: { Title, Content })— creates a new post or edits an existing one (author-only edit). - Response:
{ success, message, data? }. LogsPostedDiscussion. Requires login. - Why: Creates or edits a discussion thread.
DeleteDiscussion — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/DiscussionsSection. - Received by:
@features/Classroom/server/Discussions. - Request payload:
(discussionId: string) - Response:
{ success, message }. Only the author or classroom owner may delete. Removes both the post and its comment tree; logsDeletedDiscussion. Requires login. - Why: Deletes a discussion thread (author or teacher).
17.3 Discussion comments¶
Threaded comments on classroom discussions — the same two-phase MPTT loading and validation as §5.3,
but classroom-gated and activity-logged. All in @features/Classroom/server/Discussions; UI in
@app/ClassroomDesktopApp/AppComponents/DiscussionsSection/DiscussionsReader.
GetDiscussionComments — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
DiscussionsReader→@features/Classroom/server/Discussions. - Request payload:
(key: string)— the discussion id. - Response:
{ success = true, data = <MPTT set> }with each node'sContent/AuthorIdstripped. Requires login. - Why: Loads a discussion's comment tree skeleton.
PopulateDiscussionComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
DiscussionsReader→@features/Classroom/server/Discussions. - Request payload:
(key: string, nodeId: string) - Response:
{ success = true, data = { Content, AuthorId } }—Contentfiltered for the requester. Requires login. - Why: Lazily fetches the full content of one comment node.
PostDiscussionComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
DiscussionsReader→@features/Classroom/server/Discussions. - Request payload:
(discussionId: string, targetId: string, content: string) - Validation: content 20–800 chars, plus the same email/Discord/filter guards as tutorial comments. Requires login.
- Response:
{ success, message, ... }. - Why: Adds a comment/reply to a discussion thread.
EditDiscussionComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
DiscussionsReader→@features/Classroom/server/Discussions. - Request payload:
(discussionId: string, targetId: string, content: string)— author-only, same validation. - Response:
{ success, message }. Requires login. - Why: Edits one's own discussion comment.
DeleteDiscussionComment — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
DiscussionsReader→@features/Classroom/server/Discussions. - Request payload:
(discussionId: string, nodeId: string) - Response:
{ success, message }.message:"Comment not found","Already deleted","You do not have permission to delete this comment"(author orPermissions.Comments), or"Deleted successfully!". LogsDeleteDiscussionComment. Requires login. - Why: Removes a comment from a discussion thread.
17.4 Assignments¶
Classroom assignments / gradebook. An assignment has shared Info (teacher-authored) and per-student
UserInfo. Every callback requires Security:IsLoggedIn; teacher-only actions additionally require
Player.UserId == ClassroomUtil.GetClassroomOwner(). Server logic in
@features/Classroom/server/Assignments; UI in the AssignmentsSection (student) and
GradebookSection (teacher) under @app/ClassroomDesktopApp.
ListAssignments — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp. - Received by:
@features/Classroom/server/Assignments. - Request payload: none
- Response:
{ success, message, data? }wheredata = { [id] = { ID, Info, UserInfo } }.Hiddenassignments are omitted for non-owners. - Why: Loads the student's (or teacher's) assignment list with their progress.
GetAssignmentInfo — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/{AssignmentsSection,GradebookSection}. - Received by:
@features/Classroom/server/Assignments. - Request payload:
(assignmentId: string) - Response:
{ success, message, data? }— the shared assignmentInfo. Logs aViewedAssignmentactivity. - Why: Loads a single assignment's shared details (title, blocks, deadlines).
GetAssignmentUserInfo — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/{AssignmentsSection,GradebookSection}. - Received by:
@features/Classroom/server/Assignments. - Request payload:
(assignmentId: string, userId?)— defaults to the caller; viewing another user requires being the owner. - Response:
{ success, message, data? }— the per-user assignment data (may benilif nothing saved yet). - Why: Loads a student's submission/marks (own data, or any student's for the teacher).
EditAssignmentInfo — ServerAsyncFunction (teacher-only)¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/AssignmentsSection. - Received by:
@features/Classroom/server/Assignments. - Request payload:
(assignmentId: string, editedInfo: table) - Response:
{ success, message, ... }. Requires being the classroom owner. - Why: Lets the teacher edit an assignment's shared content/settings.
EditAssignmentUserInfo — ServerAsyncFunction¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/AssignmentsSection(student saving own work) and.../GradebookSection/GradeBreakdown/AssignmentBreakdown(teacher grading). - Received by:
@features/Classroom/server/Assignments. - Request payload:
(assignmentId: string, userId?, editedUserInfo: table)— a student may only edit their own; the teacher may edit any. Typechecked againstTYPE_EDITABLE_USER_INFO. - Response:
{ success, message, ... }. - Why: Saves a student's work, or the teacher's grades/feedback, on an assignment.
CreateAssignment — ServerAsyncFunction (teacher-only)¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/AssignmentsSection. - Received by:
@features/Classroom/server/Assignments. - Request payload: none
- Response:
{ success, message, id? }. Owner-only; creates a hidden"Untitled Assignment"and logsCreatedAssignment. - Why: Lets the teacher create a new (hidden) assignment to fill in.
DeleteAssignment — ServerAsyncFunction (teacher-only)¶
- Direction: Client → Server (RPC)
- Fired by:
@app/ClassroomDesktopApp/AppComponents/AssignmentsSection. - Received by:
@features/Classroom/server/Assignments. - Request payload:
(assignmentId: string) - Response:
{ success, message }. Owner-only. - Why: Lets the teacher delete an assignment.
Appendix — notes & caveats¶
ReadAllNotifications(§2.4) is defined and handled server-side but currently has no active client caller; treat it as reserved.- The AI tutor reads
GetTutorialContent/GetQuestContenttraffic via an extraOnServerEventlistener on the remotes' underlying instances (@features/TutorAI/server/Util) — a second consumer of thoseServerAsyncFunctions beyond theirSetCallback. This works becausevorlias/netimplements async functions over RemoteEvents. SectionTimeAnalytics(§3) andViewLesson(§4) each have two independent server listeners (their feature handler + the AI-tutor context tracker in@features/TutorAI/server/Util).DonationFeedEvent(§12) is sent from two places: the Monetization server (join replay) and@kernel/MessagesHandler(cross-server feed).GetServerType,FilterText, the notification remotes, and the economy/bookmarks remotes are declared inCorebut handled by@kernel/PlayerData.- Encrypted remotes (
AttemptLogin,AttemptRegister,AttemptClassroomLogin,RegisterClassroomPassword) are the only ones wrapped withEncryptedNet; everything else is plainvorlias/net.