true TeamPresence --!strict -- Plugin entry point: wires the toolbar button, settings and the publish/poll loops. local Auth = require(script.Auth) local Coedit = require(script.Coedit) local Capture = require(script.Capture) local Catchup = require(script.Catchup) local Crypto = require(script.Crypto) local Defaults =require(script.Defaults) local DiffWindow = require(script.DiffWindow) local Markers = require(script.Markers) local Relay = require(script.Relay) local Schema = require(script.Schema) local Secret = require(script.Secret) local Widget = require(script.Widget) local Players = game:GetService("Players") local HEARTBEAT = 5 -- How often the last-seen time is saved while connected: what arrives while -- connected is seen live, so the next catch-up starts after it. local LAST_SEEN_EVERY = 30 local TICK = 0.2 local BACKOFF_MIN = 1 local BACKOFF_MAX = 30 local toolbar = plugin:CreateToolbar("rbx-native") local button = toolbar:CreateButton("Presence", "Show rbx-native team presence", "") button.ClickableWhenViewportHidden = true local coeditButton = toolbar:CreateButton("Co-edit", "Live co-editing (beta): share edits with rbx-native", "") coeditButton.ClickableWhenViewportHidden = true local generation = 0 local coedit: any = nil local coeditEnabled = plugin:GetSetting("coeditEnabled") == true local capture: any = nil local markers: any = nil local relay: any = nil local widget: any = nil local diffWindow: any = nil -- The journal the last "While you were away" came from, for View changes. local lastEntries: { any } = {} local function userId(): number local id = Players.LocalPlayer and Players.LocalPlayer.UserId or 0 if id == 0 then local ok, studioId = pcall(function() return game:GetService("StudioService"):GetUserId() end) if ok and typeof(studioId) == "number" then id = studioId end end return id end local function lastSeenKey(): string return "LastSeen_" .. tostring(game.PlaceId) end local function saveLastSeen() if game.PlaceId ~= 0 then plugin:SetSetting(lastSeenKey(), DateTime.now().UnixTimestampMillis) end end local function stop() if relay then saveLastSeen() end generation += 1 if coedit then coedit:stop() coedit = nil end if relay then relay:leave() relay = nil end if capture then capture:destroy() capture = nil end if markers then markers:destroy() markers = nil end if widget then widget:setCatchup(nil) end end local function nextBackoff(current: number): number if current == 0 then return BACKOFF_MIN end return math.min(current * 2, BACKOFF_MAX) end local function start() stop() if game.PlaceId == 0 then widget:setStatus("Publish the place first") return end -- Zero-config: the first connector mints the place's secret, everyone -- else reads it. Writing it needs a Save/Publish to reach collaborators. local secret = Secret.read() if not secret then secret = Secret.create() widget:setStatus("Shared a key — Save or Publish once to share it") end -- The place names the relay; collaborators read it. A place that names -- none falls back to the best-effort community instance. local placeRelay = Secret.readRelay() local kind = if placeRelay then placeRelay.kind else Defaults.communityKind local url = if placeRelay then placeRelay.url else Defaults.communityUrl widget:setRelay(kind, url, placeRelay == nil) local key = Auth.deriveKey(secret :: string, game.PlaceId) local encKey = Crypto.encKey(secret :: string, game.PlaceId) local ownId = userId() local myRelay = Relay.new({ kind = kind, url = url, key = key, encKey = encKey, room = Auth.roomId(key), userId = ownId, }) local myCapture = Capture.new() local myMarkers = Markers.new() relay, capture, markers = myRelay, myCapture, myMarkers generation += 1 local myGen = generation widget:setStatus("Connecting…") if coeditEnabled then local myCoedit = Coedit.new({ relay = myRelay, userId = ownId, room = Auth.roomId(key), encKey = encKey, name = if Players.LocalPlayer then Players.LocalPlayer.Name else tostring(ownId), onNote = function(note: string) warn("[Co-edit] " .. note) if generation == myGen then widget:setStatus("Co-edit: " .. note) end end, }) coedit = myCoedit myCoedit:start() end -- "While you were away": fetch the value-less journal once, scoped to -- edits since this client last left this place (kept in plugin settings -- per place, mirroring rbx-native's on-disk last-seen). task.spawn(function() local since = tonumber(plugin:GetSetting(lastSeenKey())) or 0 local entries = myRelay:journal(since) if generation ~= myGen then return end if entries then lastEntries = entries widget:setCatchup(Catchup.summarize(entries)) end -- From here on, changes arrive live; keep the mark moving. while generation == myGen do saveLastSeen() task.wait(LAST_SEEN_EVERY) end end) task.spawn(function() local lastPublish = 0.0 local backoff = 0 while generation == myGen do if myCapture:isDirty() or os.clock() - lastPublish >= HEARTBEAT then myCapture:clearDirty() local ok, err = myRelay:publish(myCapture:state()) if generation ~= myGen then return end if ok then lastPublish = os.clock() backoff = 0 else widget:setStatus(err or "Publish failed", not myRelay:waiting()) backoff = nextBackoff(backoff) lastPublish = os.clock() task.wait(backoff) end end task.wait(TICK) end end) task.spawn(function() local cursor = 0 local backoff = 0 while generation == myGen do local snapshot, err = myRelay:poll(cursor) if generation ~= myGen then return end if snapshot then backoff = 0 cursor = snapshot.cursor local ids = { ownId } for _, user in snapshot.users do table.insert(ids, user.userId) end local colors = Schema.assignColors(ids) for _, user in snapshot.users do user.color = colors[user.userId] or user.color end local native = {} for _, user in snapshot.users do if user.client == "rbx-native" then if user.userId == ownId then user.name = `{user.name} (you, in rbx-native)` end table.insert(native, user) end end myMarkers:render(snapshot.users) widget:setUsers(native) widget:setStatus(`Connected · {#native} rbx-native`) -- ntfy polls return at once; pace them so we don't spin. task.wait(myRelay:pollDelay()) else widget:setStatus(err or "Poll failed", not myRelay:waiting()) backoff = nextBackoff(backoff) task.wait(backoff) end end end) end -- Presence on or off is the plugin setting, apart from whether the widget -- is showing: closing the widget keeps presence running. local enabled = false local function apply(on: boolean) enabled = on plugin:SetSetting("Enabled", on) widget:setEnabled(on) if on then if not plugin:GetSetting("SeenHttpNote") then widget:showHttpNote(true) plugin:SetSetting("SeenHttpNote", true) end start() else stop() widget:setStatus("Off") end end widget = Widget.new(plugin, { onToggle = function() apply(not enabled) end, onViewChanges = function() if not diffWindow then diffWindow = DiffWindow.new(plugin) end diffWindow:show(lastEntries) end, onSaveRelay = function(kind: string, url: string) -- Owner setup: write the relay into the place (empty URL clears it). if url == "" then Secret.clearRelay() else Secret.writeRelay(kind, url) end if enabled then start() end end, }) button.Click:Connect(function() widget.gui.Enabled = not widget.gui.Enabled end) coeditButton:SetActive(coeditEnabled) coeditButton.Click:Connect(function() coeditEnabled = not coeditEnabled plugin:SetSetting("coeditEnabled", coeditEnabled) coeditButton:SetActive(coeditEnabled) if coeditEnabled and enabled then start() elseif coedit then coedit:stop() coedit = nil end end) widget.gui:GetPropertyChangedSignal("Enabled"):Connect(function() button:SetActive(widget.gui.Enabled) end) plugin.Unloading:Connect(stop) local savedRelay = Secret.readRelay() if savedRelay then widget:setRelay(savedRelay.kind, savedRelay.url, false) else widget:setRelay(Defaults.communityKind, Defaults.communityUrl, true) end apply(plugin:GetSetting("Enabled") == true) Announce --!strict --! Where the room's host is, mirrored from crates/rbx_presence/src/announce.rs. --! In a "hosted" room an rbx-native collaborator runs the relay behind a --! Cloudflare quick tunnel and posts its address, signed with the place key, --! to the room's announce topic on ntfy; the plugin reads the newest genuine --! one and talks to that relay. An empty url is the host saying goodbye. --! Pure (no HttpService): the Luau tests check it against the Rust encoder. local Auth = require(script.Parent.Auth) local Json = require(script.Parent.Json) local Ntfy = require(script.Parent.Ntfy) local Announce = {} Announce.MAX_AGE_MS = 12 * 60 * 60 * 1000 Announce.CLOCK_SKEW_MS = 60000 function Announce.topic(room: string): string return Ntfy.topic(room) .. "-host" end local function canonical(room: string, url: string, at: number): string return string.format("rbx-presence/announce/v1\n%s\n%s\n%.0f", room, url, at) end -- Whether `url` may be connected to: public HTTPS, or this machine. function Announce.allowedUrl(url: string): boolean return string.sub(url, 1, 8) == "https://" or string.sub(url, 1, 17) == "http://127.0.0.1:" end -- `text` decoded if it is a genuine, current announcement for `room`. function Announce.verify(key: string, room: string, now: number, text: string): any? local ok, message = pcall(Json.decode, text) if not ok or type(message) ~= "table" then return nil end local url, at, sig = message.url, message.at, message.sig if message.v ~= 1 or type(url) ~= "string" or type(at) ~= "number" or type(sig) ~= "string" then return nil end if at > now + Announce.CLOCK_SKEW_MS or now - at > Announce.MAX_AGE_MS then return nil end if url ~= "" and not Announce.allowedUrl(url) then return nil end if Auth.sign(key, canonical(room, url, at)) ~= sig then return nil end return { url = url, at = at } end -- The host's URL from a run of announcement texts: the newest genuine one, -- nil when there is none or the newest says the host left. function Announce.newest(key: string, room: string, now: number, texts: { string }): string? local best: any = nil for _, text in texts do local message = Announce.verify(key, room, now, text) if message and (best == nil or message.at > best.at) then best = message end end if best == nil or best.url == "" then return nil end return best.url end return Announce Auth --!strict -- Key derivation and request signing for the presence relay protocol. local Sha256 = require(script.Parent.Sha256) local Hmac = require(script.Parent.Hmac) local Auth = {} local function int(n: number): string return string.format("%.0f", n) end function Auth.deriveKey(secret: string, placeId: number): string return Hmac.sha256Hex(secret, "rbx-presence/key/v1/" .. int(placeId)) end function Auth.roomId(key: string): string return string.sub(Sha256.hex("rbx-presence/room/v1/" .. key), 1, 32) end function Auth.canonical(method: string, path: string, userId: number, ts: number, body: string): string return method .. "\n" .. path .. "\n" .. int(userId) .. "\n" .. int(ts) .. "\n" .. Sha256.hex(body) end function Auth.sign(key: string, canonical: string): string return Hmac.sha256Hex(key, canonical) end function Auth.headers( key: string, method: string, path: string, userId: number, ts: number, body: string, withKey: boolean ): { [string]: string } local headers: { [string]: string } = { ["X-Presence-User"] = int(userId), ["X-Presence-Time"] = int(ts), ["X-Presence-Sig"] = Auth.sign(key, Auth.canonical(method, path, userId, ts, body)), } if withKey then headers["X-Presence-Key"] = key end return headers end return Auth Capture --!strict --! Collects the local user's presence (camera, selection, open script, recent edits) and tracks when it changed. local Players = game:GetService("Players") local RunService = game:GetService("RunService") local Selection = game:GetService("Selection") local StudioService = game:GetService("StudioService") local ChangeHistoryService = game:GetService("ChangeHistoryService") local Edits = require(script.Parent.Edits) local Paths = require(script.Parent.Paths) local Schema = require(script.Parent.Schema) local SAMPLE_INTERVAL = 0.2 local MOVE_STUDS = 0.05 local TURN_RADIANS = math.rad(0.5) local MAX_WATCHED = 50 local VALUE_CUT = 80 local Capture = {} Capture.__index = Capture local function nowMs(): number return DateTime.now().UnixTimestampMillis end function Capture.new() local userId = StudioService:GetUserId() local name = "User " .. tostring(userId) local ok, fetched = pcall(function() return Players:GetNameFromUserIdAsync(userId) end) if ok and type(fetched) == "string" and fetched ~= "" then name = fetched end local self = setmetatable({ userId = userId, name = name, color = Schema.colorFor(userId), dirty = true, camera = nil :: { number }?, lastCFrame = nil :: CFrame?, lastSample = 0, tracker = Edits.new(), watchers = {} :: { RBXScriptConnection }, connections = {} :: { RBXScriptConnection }, }, Capture) local function markDirty() self.dirty = true end table.insert( self.connections, Selection.SelectionChanged:Connect(function() self:_watchSelection() markDirty() end) ) table.insert(self.connections, StudioService:GetPropertyChangedSignal("ActiveScript"):Connect(markDirty)) table.insert( self.connections, ChangeHistoryService.OnRecordingStarted:Connect(function() self.tracker:recordingStarted(nowMs()) end) ) table.insert( self.connections, ChangeHistoryService.OnRecordingFinished:Connect( function(_name: string, _displayName: string?, _identifier: string?, operation: Enum.FinishRecordingOperation) self.tracker:recordingFinished(operation ~= Enum.FinishRecordingOperation.Cancel, nowMs()) markDirty() end ) ) local function onUndoRedo() self.tracker:undoRedo(nowMs()) markDirty() end table.insert(self.connections, ChangeHistoryService.OnUndo:Connect(onUndoRedo)) table.insert(self.connections, ChangeHistoryService.OnRedo:Connect(onUndoRedo)) table.insert( self.connections, RunService.Heartbeat:Connect(function() self:_sampleCamera() end) ) self:_watchSelection() self:_sampleCamera() return self end function Capture:_sampleCamera() local clock = os.clock() if clock - self.lastSample < SAMPLE_INTERVAL then return end self.lastSample = clock local camera = workspace.CurrentCamera if camera == nil then return end local cf = camera.CFrame local last: CFrame? = self.lastCFrame local moved = last == nil if last ~= nil then local _, angle = last:ToObjectSpace(cf):ToAxisAngle() moved = (cf.Position - last.Position).Magnitude > MOVE_STUDS or math.abs(angle) > TURN_RADIANS end if moved then self.lastCFrame = cf self.camera = { cf:GetComponents() } self.dirty = true end end function Capture:_disconnectWatchers() for _, connection in self.watchers do connection:Disconnect() end table.clear(self.watchers) end function Capture:_recordEdit(instance: Instance, property: string) local path = Paths.pathOf(instance) if path == nil then return end local ok, raw = pcall(function() return (instance :: any)[property] end) if not ok then return end local value = string.sub(tostring(raw), 1, VALUE_CUT) self.tracker:changed(path, property, value, nowMs()) end function Capture:_watchSelection() self:_disconnectWatchers() for index, instance in Selection:Get() do if index > MAX_WATCHED then break end table.insert( self.watchers, instance.Changed:Connect(function(property: string) self:_recordEdit(instance, property) end) ) end end function Capture:state() local selection = {} for _, instance in Selection:Get() do if #selection >= Schema.MAX_SELECTION then break end local path = Paths.pathOf(instance) if path ~= nil then table.insert(selection, path) end end local edits = self.tracker:recent(nowMs()) local scriptPath: string? = nil local active = StudioService.ActiveScript if active ~= nil then scriptPath = Paths.pathOf(active) end return { v = Schema.VERSION, userId = self.userId, name = self.name, color = self.color, client = "studio", camera = self.camera, selection = selection, script = scriptPath, edits = edits, seen = 0, } end function Capture:isDirty(): boolean return self.dirty end function Capture:clearDirty() self.dirty = false end function Capture:destroy() self:_disconnectWatchers() for _, connection in self.connections do connection:Disconnect() end table.clear(self.connections) end return Capture Catchup --!strict --! The offline catch-up summary, mirrored from --! crates/rbx_presence/src/catchup.rs: a value-less journal of property --! edits, aggregated per author and in total, shown as "While you were --! away" when a collaborator reconnects. Pure (no HttpService), so it runs --! in the shared Luau golden tests and is pinned by the same --! fixtures/catchup.json the Rust side uses. --! --! Neither transport carries edit *values*, and neither records instance --! add/remove: a worker journal returns the entries directly, while ntfy's --! retained messages are the peers' states, whose edits are lifted out here. local Schema = require(script.Parent.Schema) local Catchup = {} export type JournalEntry = { userId: number, name: string, path: string, property: string, -- The edited value as display text ("" from an older peer). value: string?, at: number, } export type AuthorChange = { userId: number, name: string, instances: number, properties: number, edits: number, } export type Summary = { authors: { AuthorChange }, instances: number, properties: number, edits: number, } -- Aggregates `entries`. Authors are ordered by edits descending, then name -- case-insensitively, then user id; an author's name is the one on their -- newest entry. `instances` counts distinct paths, `properties` distinct -- (path, property) pairs, `edits` the entry count. function Catchup.summarize(entries: { JournalEntry }): Summary local paths: { [string]: boolean } = {} local props: { [string]: boolean } = {} local pathCount, propCount = 0, 0 type Tally = { name: string, latest: number, paths: { [string]: boolean }, pathN: number, props: { [string]: boolean }, propN: number, edits: number, } local byUser: { [number]: Tally } = {} local total = 0 for _, entry in entries do total += 1 if not paths[entry.path] then paths[entry.path] = true pathCount += 1 end local pair = entry.path .. "\0" .. entry.property if not props[pair] then props[pair] = true propCount += 1 end local tally = byUser[entry.userId] if not tally then tally = { name = entry.name, latest = entry.at, paths = {}, pathN = 0, props = {}, propN = 0, edits = 0 } byUser[entry.userId] = tally end if tally.edits == 0 or entry.at >= tally.latest then tally.name = entry.name tally.latest = entry.at end if not tally.paths[entry.path] then tally.paths[entry.path] = true tally.pathN += 1 end if not tally.props[pair] then tally.props[pair] = true tally.propN += 1 end tally.edits += 1 end local authors: { AuthorChange } = {} for userId, tally in byUser do table.insert(authors, { userId = userId, name = tally.name, instances = tally.pathN, properties = tally.propN, edits = tally.edits, }) end table.sort(authors, function(a, b) if a.edits ~= b.edits then return a.edits > b.edits end local an, bn = string.lower(a.name), string.lower(b.name) if an ~= bn then return an < bn end return a.userId < b.userId end) return { authors = authors, instances = pathCount, properties = propCount, edits = total } end -- The journal entries lifted out of a list of peer states (the -- ntfy transport's catch-up: retained messages are the peers' states). function Catchup.fromStates(states: { any }): { JournalEntry } local entries: { JournalEntry } = {} for _, state in states do for _, edit in state.edits or {} do table.insert(entries, { userId = state.userId, name = state.name, path = edit.path, property = edit.property, value = edit.value, at = edit.at, }) end end return entries end -- The journal entries a relay's sealed journal reply holds, opened; entries -- that fail to authenticate are dropped. function Catchup.fromSealed(encKey: string, room: string, entries: { any }): { JournalEntry } local opened: { JournalEntry } = {} for _, entry in entries do local plain = Schema.openJournal(encKey, room, entry) if plain ~= nil then table.insert(opened, plain) end end return opened end -- One author's line: "Ada made 3 edits to 2 properties across 1 instance", -- with singulars, mirroring the rbx-native dock's wording. function Catchup.authorLine(author: AuthorChange): string local function plural(n: number, one: string, many: string): string return if n == 1 then one else many end return string.format( "%s made %d %s to %d %s across %d %s", author.name, author.edits, plural(author.edits, "edit", "edits"), author.properties, plural(author.properties, "property", "properties"), author.instances, plural(author.instances, "instance", "instances") ) end return Catchup Changes --!strict --! The catch-up journal as the diff window lists it: one change per --! instance, its edited properties under it, each with who edited it and --! when last. Pure (no Studio services), so it runs in the Luau golden tests. --! --! The journal records property edits only, so every change here is an --! update; each property keeps the value its newest edit set (display text, --! "" from an older peer, when the window reads the place instead). local Paths = require(script.Parent.Paths) local Changes = {} export type PropertyEdit = { property: string, -- Distinct author names, most recent editor first. authors: { string }, edits: number, at: number, value: string, } export type Change = { path: string, name: string, -- The parent's path, `game` for a service. parent: string, properties: { PropertyEdit }, at: number, } -- Groups `entries` (Catchup.JournalEntry) by instance path. Changes are -- ordered by their newest edit, newest first; properties by name. function Changes.fromJournal(entries: { any }): { Change } local byPath: { [string]: Change } = {} local order: { Change } = {} type Tally = { edit: PropertyEdit, latest: { [string]: number } } local tallies: { [string]: { [string]: Tally } } = {} for _, entry in entries do local change = byPath[entry.path] if not change then local names = Paths.split(entry.path) or { entry.path } local name = names[#names] or entry.path local parentNames = table.clone(names) table.remove(parentNames) change = { path = entry.path, name = name, parent = if #parentNames == 0 then "game" else Paths.join(parentNames), properties = {}, at = 0, } byPath[entry.path] = change tallies[entry.path] = {} table.insert(order, change) end local c = change :: Change c.at = math.max(c.at, entry.at) local tally = tallies[entry.path][entry.property] if not tally then tally = { edit = { property = entry.property, authors = {}, edits = 0, at = 0, value = "" }, latest = {}, } tallies[entry.path][entry.property] = tally table.insert(c.properties, tally.edit) end local t = tally :: Tally t.edit.edits += 1 if entry.at >= t.edit.at then t.edit.at = entry.at t.edit.value = entry.value or "" end t.latest[entry.name] = math.max(t.latest[entry.name] or 0, entry.at) end for path, byProperty in tallies do for _, tally in byProperty do local names: { string } = {} for name in tally.latest do table.insert(names, name) end table.sort(names, function(a, b) if tally.latest[a] ~= tally.latest[b] then return tally.latest[a] > tally.latest[b] end return a < b end) tally.edit.authors = names end table.sort(byPath[path].properties, function(a, b) return a.property < b.property end) end table.sort(order, function(a, b) if a.at ~= b.at then return a.at > b.at end return a.path < b.path end) return order end -- "just now", "4 min ago", "2 h ago", "3 d ago". function Changes.ago(at: number, now: number): string local seconds = math.max(0, (now - at) // 1000) if seconds < 60 then return "just now" elseif seconds < 3600 then return `{seconds // 60} min ago` elseif seconds < 86400 then return `{seconds // 3600} h ago` end return `{seconds // 86400} d ago` end return Changes Coedit -- Live co-editing in Studio: captures local edits under the tracked roots -- (CoeditCollect turns them into ops), sends them to the relay op log and -- applies every batch of the log, our own included, in log order through -- CoeditApply (CoeditSync does the traffic). local Apply = require(script.Parent.CoeditApply) local Collect = require(script.Parent.CoeditCollect) local CoeditCore = require(script.Parent.CoeditCore) local Kept = require(script.Parent.CoeditKept) local Paths = require(script.Parent.Paths) local Sync = require(script.Parent.CoeditSync) local FLUSH_EVERY = 0.3 local POLL_EVERY = 0.5 local ROOTS = { "Workspace", "Lighting", "ReplicatedFirst", "ReplicatedStorage", "ServerScriptService", "ServerStorage", "StarterGui", "StarterPack", "StarterPlayer", "SoundService", "Teams", "Chat", "TextChatService", "MaterialService", } export type Options = { relay: any, userId: number, room: string, encKey: string, name: string, onNote: (string) -> (), } local Coedit = {} Coedit.__index = Coedit Coedit.collect = Collect.collect Coedit.send = Sync.send Coedit.flush = Sync.flush Coedit.authorOf = Sync.authorOf Coedit.poll = Sync.poll function Coedit.new(options: Options) local actor = string.format("%.0f", options.userId) .. ":studio:" .. CoeditCore.session() local state = { idOf = setmetatable({}, { __mode = "k" }), byId = setmetatable({}, { __mode = "v" }), registers = CoeditCore.Registers.new(), log = CoeditCore.Log.new(), marks = setmetatable({}, { __mode = "k" }), ignore = setmetatable({}, { __mode = "k" }), counter = 0, } return setmetatable({ options = options, actor = actor, state = state, apply = Apply.new(state), running = false, connections = {} :: { RBXScriptConnection }, watched = {} :: { [Instance]: { RBXScriptConnection } }, tagsHooked = {} :: { [string]: boolean }, roots = {} :: { [Instance]: boolean }, secret = nil :: Instance?, added = {} :: { [Instance]: boolean }, dirty = {} :: { [Instance]: { [string]: boolean } }, dirtySrc = {} :: { [Instance]: boolean }, removed = {} :: { { inst: Instance, id: string, p: string } }, resend = {} :: { any }, paths = setmetatable({}, { __mode = "k" }), moved = setmetatable({}, { __mode = "k" }), since = 0, kept = Kept.new(), batchCount = 0, }, Coedit) end function Coedit.note(self: any, text: string) pcall(self.options.onNote, text) end function Coedit.isTracked(self: any, inst: Instance): boolean local current: Instance? = inst while current ~= nil do -- The camera is each editor's own view, and holds the presence -- markers: none of it is shared. if current == self.secret or current:IsA("Camera") then return false end if self.roots[current] then return current ~= inst end current = current.Parent end return false end function Coedit.scriptKey(self: any, inst: Instance): string return Apply.keyOf(Apply.targetOf(self.state, inst)) end function Coedit.onChanged(self: any, inst: Instance, prop: string) if not self.running or self.state.ignore[inst] then return end if prop == "Name" or prop == "Parent" then -- Peers find an instance whose id they don't know by its path, so a -- rename or move is sent at the path it had before. if self.moved[inst] == nil then self.moved[inst] = self.paths[inst] end if inst.Parent ~= nil then self.paths[inst] = Paths.pathOf(inst) end end if prop == "Source" then self.dirtySrc[inst] = true return end if prop ~= "Name" and prop ~= "Parent" and Apply.valueType(inst.ClassName, prop) == nil then return end local marks = self.state.marks[inst] if marks ~= nil and marks[prop] ~= nil then local mark = marks[prop] marks[prop] = nil if Apply.encodeProp(self.state, inst, prop) == mark then return end end if prop == "Parent" and (inst.Parent == nil or not self:isTracked(inst)) then return end local props = self.dirty[inst] if props == nil then props = {} self.dirty[inst] = props end props[prop] = true end function Coedit.watch(self: any, inst: Instance) if self.watched[inst] ~= nil or not self:isTracked(inst) then return end self.watched[inst] = { inst.Changed:Connect(function(prop: string) self:onChanged(inst, prop) end), inst.AttributeChanged:Connect(function(name: string) self:onChanged(inst, "@" .. name) end), } self.paths[inst] = Paths.pathOf(inst) if inst:IsA("LuaSourceContainer") then self.state.log:hash(self:scriptKey(inst), Apply.editorSource(inst)) end end function Coedit.unwatch(self: any, inst: Instance) local conns = self.watched[inst] if conns ~= nil then for _, conn in conns do conn:Disconnect() end self.watched[inst] = nil end end function Coedit.onAdded(self: any, inst: Instance) if self.state.ignore[inst] then self.state.ignore[inst] = nil self:watch(inst) return end if not self:isTracked(inst) then return end self:watch(inst) self.added[inst] = true end function Coedit.onRemoving(self: any, inst: Instance) if self.state.ignore[inst] then self.state.ignore[inst] = nil self:unwatch(inst) return end if self.watched[inst] == nil then return end if self.added[inst] then self.added[inst] = nil else table.insert(self.removed, { inst = inst, id = self.state.idOf[inst] or "", p = self.paths[inst] or Paths.pathOf(inst) or "", }) end self.dirty[inst] = nil self.dirtySrc[inst] = nil self:unwatch(inst) end function Coedit.onDocumentChanged(self: any, document: any) local ok, inst = pcall(function() if document:IsCommandBar() then return nil end return document:GetScript() end) if ok and inst ~= nil and self.watched[inst] ~= nil then self.dirtySrc[inst] = true end end function Coedit.clock(self: any): { any } self.state.counter += 1 return { self.state.counter, self.actor } end function Coedit.start(self: any) if self.running then return end self.running = true self.reassembler = CoeditCore.Reassembler.new() local ok, secret = pcall(function() return game:GetService("ServerStorage"):FindFirstChild("RbxNativePresence") end) self.secret = if ok then secret else nil for _, name in ROOTS do local okRoot, root = pcall(function() return game:GetService(name) end) if okRoot and root ~= nil then self.roots[root] = true end end for root in self.roots do table.insert(self.connections, root.DescendantAdded:Connect(function(d) if self.running then self:onAdded(d) end end)) table.insert(self.connections, root.DescendantRemoving:Connect(function(d) if self.running then self:onRemoving(d) end end)) for _, d in root:GetDescendants() do self:watch(d) end end if self.roots[game:GetService("ServerStorage")] then table.insert(self.connections, game:GetService("ServerStorage").ChildAdded:Connect(function(child) if child.Name == "RbxNativePresence" and self.secret == nil then self.secret = child self:unwatch(child) self.added[child] = nil end end)) end local tags = game:GetService("CollectionService") local function hookTag(tag: string) if self.tagsHooked[tag] then return nil end self.tagsHooked[tag] = true local function changed(inst: Instance) if self.running and self.watched[inst] ~= nil then self:onChanged(inst, "#" .. tag) end end table.insert(self.connections, tags:GetInstanceAddedSignal(tag):Connect(changed)) table.insert(self.connections, tags:GetInstanceRemovedSignal(tag):Connect(changed)) return changed end for _, tag in tags:GetAllTags() do hookTag(tag) end -- A tag's first use fires TagAdded only after the instance has it, so -- the instances already tagged are picked up here. table.insert(self.connections, tags.TagAdded:Connect(function(tag: string) local changed = hookTag(tag) if changed ~= nil then for _, inst in tags:GetTagged(tag) do changed(inst) end end end)) pcall(function() local editor = game:GetService("ScriptEditorService") table.insert(self.connections, editor.TextDocumentDidChange:Connect(function(document) if self.running then self:onDocumentChanged(document) end end)) end) task.spawn(function() while self.running do task.wait(FLUSH_EVERY) if self.running then local okFlush, flushErr = pcall(self.flush, self) if not okFlush then self:note("Co-edit: " .. tostring(flushErr)) end end end end) task.spawn(function() while self.running do local okPoll, pollErr = pcall(self.poll, self) if not okPoll then self:note("Co-edit: " .. tostring(pollErr)) end task.wait(POLL_EVERY) end end) end function Coedit.stop(self: any) self.running = false for _, conn in self.connections do conn:Disconnect() end self.connections = {} for _, conns in self.watched do for _, conn in conns do conn:Disconnect() end end self.watched = {} self.tagsHooked = {} self.roots = {} self.added = {} self.dirty = {} self.dirtySrc = {} self.removed = {} end return Coedit CoeditApply -- Applies co-edit batches from the relay log to the place, in log order, -- inside one undo recording per batch. Shares `state` with Coedit.luau: -- idOf/byId identity maps, LWW registers, the script text log, echo marks -- and the set of instances whose add/remove events must be ignored. local CoeditClasses = require(script.Parent.CoeditClasses) local CoeditCore = require(script.Parent.CoeditCore) local CoeditValue = require(script.Parent.CoeditValue) local Json = require(script.Parent.Json) local Paths = require(script.Parent.Paths) local EXISTS = "\0exists" local Apply = {} Apply.__index = Apply -- Attributes travel as `@<name>` and tags as `#<tag>` (one property each, -- see crates/rbx_presence/src/coedit/attrs.rs); an absent one is `null`. function Apply.isEntry(prop: string): boolean local first = string.sub(prop, 1, 1) return first == "@" or first == "#" end function Apply.valueType(className: string, prop: string): string? if Apply.isEntry(prop) then return "Attribute" end local cls = CoeditClasses[className] while cls ~= nil do local t = cls.p and cls.p[prop] if t ~= nil then return t end cls = cls.s and CoeditClasses[cls.s] or nil end if prop == "Parent" then return "Ref" elseif prop == "Name" then return "String" end return nil end -- Every scripting property of the class, inherited ones included. function Apply.propsOf(className: string): { [string]: string } local out: { [string]: string } = {} local cls = CoeditClasses[className] while cls ~= nil do if cls.p then for prop, t in cls.p do if out[prop] == nil then out[prop] = t end end end cls = cls.s and CoeditClasses[cls.s] or nil end return out end function Apply.targetOf(state: any, inst: Instance): { id: string, p: string } return { id = state.idOf[inst] or "", p = Paths.pathOf(inst) or "" } end function Apply.resolve(state: any, target: any): Instance? if type(target) ~= "table" then return nil end local id = type(target.id) == "string" and target.id or "" local inst = if id ~= "" then state.byId[id] else nil if inst == nil and type(target.p) == "string" and target.p ~= "" then inst = Paths.resolve(game, target.p) end if inst ~= nil and id ~= "" then state.idOf[inst] = id state.byId[id] = inst end return inst end function Apply.keyOf(target: any): string local id = type(target.id) == "string" and target.id or "" if id ~= "" then return id end return "p:" .. tostring(target.p or "") end -- The property's current value as a JSON string, the form echo marks hold. local function read(inst: Instance, prop: string): (boolean, any) local first, rest = string.sub(prop, 1, 1), string.sub(prop, 2) if first == "@" then return pcall(inst.GetAttribute, inst, rest) elseif first == "#" then return true, if inst:HasTag(rest) then true else nil end return pcall(function() return (inst :: any)[prop] end) end function Apply.encodeProp(state: any, inst: Instance, prop: string): string? local ok, value = read(inst, prop) if not ok then return nil end if value == nil and Apply.isEntry(prop) then return "null" end local encoded = CoeditValue.encode(value, Apply.valueType(inst.ClassName, prop), function(i: Instance) return Apply.targetOf(state, i) end) if encoded == nil then return nil end local okJson, text = pcall(Json.encode, encoded) return if okJson then text else nil end function Apply.mark(state: any, inst: Instance, prop: string, encoded: string) local marks = state.marks[inst] if marks == nil then marks = {} state.marks[inst] = marks end marks[prop] = encoded end function Apply.editorSource(inst: Instance): string local ok, text = pcall(function() return game:GetService("ScriptEditorService"):GetEditorSource(inst) end) if ok and type(text) == "string" then return text end local okSource, source = pcall(function() return (inst :: any).Source end) return if okSource and type(source) == "string" then source else "" end function Apply.writeSource(state: any, inst: Instance, text: string) Apply.mark(state, inst, "Source", text) local ok = pcall(function() game:GetService("ScriptEditorService"):UpdateSourceAsync(inst, function() return text end) end) if not ok then pcall(function() (inst :: any).Source = text end) end end function Apply.new(state: any) return setmetatable({ state = state }, Apply) end local function assign(inst: Instance, prop: string, value: any): boolean local first, rest = string.sub(prop, 1, 1), string.sub(prop, 2) if first == "@" then return pcall(inst.SetAttribute, inst, rest, value) elseif first == "#" then return pcall(if value then inst.AddTag else inst.RemoveTag, inst, rest) end return pcall(function() (inst :: any)[prop] = value end) end local function writeProp(state: any, inst: Instance, prop: string, json: any, notes: { string }): boolean local _, current = read(inst, prop) local ok, value = true, nil if not (Apply.isEntry(prop) and json == Json.null) then ok, value = CoeditValue.decode(json, current, Apply.valueType(inst.ClassName, prop), function(t) return Apply.resolve(state, t) end) end if not ok then table.insert(notes, string.format("Co-edit: could not read %s.%s", inst:GetFullName(), prop)) return false end local okSet = assign(inst, prop, value) if not okSet then return false end local encoded = Apply.encodeProp(state, inst, prop) if encoded ~= nil then Apply.mark(state, inst, prop, encoded) end return true end local function ignoreSubtree(state: any, inst: Instance) state.ignore[inst] = true for _, d in inst:GetDescendants() do state.ignore[d] = true end end function Apply.batch(self: any, batch: any, own: boolean, authorName: string) local state = self.state local notes: { string } = {} local resend: { any } = {} local applied = 0 local history = game:GetService("ChangeHistoryService") local okRec, recording = pcall(function() return history:TryBeginRecording("Co-edit", "Co-edit from " .. authorName) end) if not okRec then recording = nil end for _, op in (type(batch.ops) == "table" and batch.ops or {}) do if type(op) ~= "table" or type(op.c) ~= "table" or type(op.c[1]) ~= "number" then continue end state.counter = math.max(state.counter or 0, op.c[1]) local target = { id = op.id or "", p = op.p or "" } local key = Apply.keyOf(target) local inst = Apply.resolve(state, target) if op.k == "set" and type(op.n) == "string" then if state.registers:claim(key .. "\0" .. op.n, op.c) and not own then if inst == nil then table.insert(notes, "Co-edit: no instance at " .. tostring(op.p)) elseif op.v ~= nil and writeProp(state, inst, op.n, op.v, notes) then applied += 1 end end elseif op.k == "add" and type(op.cls) == "string" then if state.registers:claim(key .. "\0" .. EXISTS, op.c) and not own then local exists = target.id ~= "" and state.byId[target.id] ~= nil local parent = if type(op.parent) == "table" then Apply.resolve(state, op.parent) else nil if not exists and parent ~= nil then local okNew, created = pcall(Instance.new, op.cls) if okNew then state.ignore[created] = true if target.id ~= "" then state.idOf[created] = target.id state.byId[target.id] = created end local source: string? = nil for prop, value in (type(op.props) == "table" and op.props or {}) do if prop == "Parent" then continue elseif prop == "Source" and type(value) == "table" and type(value.v) == "string" then source = value.v end writeProp(state, created, prop, value, notes) end if source ~= nil then state.log:hash(key, source) end local okParent = pcall(function() created.Parent = parent end) if okParent then applied += 1 else state.ignore[created] = nil created:Destroy() table.insert(notes, "Co-edit: could not add " .. op.cls .. " at " .. tostring(op.p)) end else table.insert(notes, "Co-edit: Studio cannot create " .. op.cls) end elseif not exists then table.insert(notes, "Co-edit: no parent for " .. tostring(op.p)) end end elseif op.k == "del" then if state.registers:claim(key .. "\0" .. EXISTS, op.c) and not own and inst ~= nil then ignoreSubtree(state, inst) if pcall(inst.Destroy, inst) then applied += 1 end end elseif op.k == "src" and type(op.text) == "string" and type(op.base) == "string" then if inst ~= nil and inst:IsA("LuaSourceContainer") then local current = Apply.editorSource(inst) local result = state.log:src(key, current, op, own) if result.write ~= nil then Apply.writeSource(state, inst, result.write) applied += 1 end if result.conflicted then table.insert(notes, "Co-edit: conflicting edits merged with markers in " .. inst:GetFullName()) end if result.resend ~= nil then local logged = state.log.texts[key] or current table.insert(resend, { k = "src", id = target.id, p = target.p, base = CoeditCore.textHash(logged), text = result.resend, }) end elseif inst == nil then table.insert(notes, "Co-edit: no script at " .. tostring(op.p)) end end end if recording ~= nil then pcall(function() history:FinishRecording( recording, if applied > 0 then Enum.FinishRecordingOperation.Commit else Enum.FinishRecordingOperation.Cancel ) end) end return { applied = applied, notes = notes, resend = resend } end return Apply CoeditClasses --!strict -- Generated by build_presence_plugin from the Roblox API dump; do not edit. return { Accessory = { s = "Accoutrement", p = { AccessoryType = "AccessoryType", } }, AccessoryDescription = { s = "Instance", p = { AccessoryType = "AccessoryType", AssetId = "int64", Instance = "Instance", IsLayered = "bool", Order = "int", Position = "Vector3", Rotation = "Vector3", Scale = "Vector3", } }, AccountService = { s = "Instance", p = { } }, Accoutrement = { s = "Instance", p = { AttachmentPoint = "CFrame", } }, AchievementService = { s = "Instance", p = { } }, ActivityHistoryEventService = { s = "Instance", p = { } }, Actor = { s = "Model", p = { } }, AdGui = { s = "SurfaceGuiBase", p = { AdShape = "AdShape", EnableVideoAds = "bool", FallbackImage = "ContentId", FallbackImageContent = "Content", } }, AdPlacement = { s = "Instance", p = { ActivationInstance = "Instance", AdFormat = "AdFormat", PlacementId = "int64", } }, AdPortal = { s = "Instance", p = { } }, AdService = { s = "Instance", p = { } }, AdvancedDragger = { s = "Instance", p = { } }, AirController = { s = "ControllerBase", p = { BalanceMaxTorque = "float", BalanceSpeed = "float", MaintainAngularMomentum = "bool", MaintainLinearMomentum = "bool", MoveMaxForce = "float", TurnMaxTorque = "float", TurnSpeedFactor = "float", } }, AlignOrientation = { s = "Constraint", p = { AlignType = "AlignType", CFrame = "CFrame", LookAtPosition = "Vector3", MaxAngularVelocity = "float", MaxTorque = "float", Mode = "OrientationAlignmentMode", PrimaryAxis = "Vector3", PrimaryAxisOnly = "bool", ReactionTorqueEnabled = "bool", Responsiveness = "float", RigidityEnabled = "bool", SecondaryAxis = "Vector3", } }, AlignPosition = { s = "Constraint", p = { ApplyAtCenterOfMass = "bool", ForceLimitMode = "ForceLimitMode", ForceRelativeTo = "ActuatorRelativeTo", MaxAxesForce = "Vector3", MaxForce = "float", MaxVelocity = "float", Mode = "PositionAlignmentMode", Position = "Vector3", ReactionForceEnabled = "bool", Responsiveness = "float", RigidityEnabled = "bool", } }, AnalyticsService = { s = "Instance", p = { } }, AngularVelocity = { s = "Constraint", p = { AngularVelocity = "Vector3", MaxTorque = "float", ReactionTorqueEnabled = "bool", RelativeTo = "ActuatorRelativeTo", } }, AnimatedImageService = { s = "Instance", p = { UserCreatedTracks = "BinaryString", } }, Animation = { s = "Instance", p = { AnimationContent = "Content", AnimationId = "ContentId", } }, AnimationClip = { s = "Instance", p = { Loop = "bool", Priority = "AnimationPriority", } }, AnimationClipProvider = { s = "Instance", p = { } }, AnimationConstraint = { s = "Constraint", p = { AngularDamping = "float", AngularStrength = "float", IsKinematic = "bool", LinearDamping = "float", LinearStrength = "float", MaxForce = "float", MaxTorque = "float", Transform = "CFrame", } }, AnimationController = { s = "Instance", p = { } }, AnimationFromVideoCreatorService = { s = "Instance", p = { } }, AnimationFromVideoCreatorStudioService = { s = "Instance", p = { } }, AnimationGraphDefinition = { s = "AnimationClip", p = { } }, AnimationImportData = { s = "BaseImportData", p = { ForceNewVersion = "bool", VersionedAssetId = "int64", } }, AnimationNode = { s = "Object", p = { } }, AnimationNodeDefinition = { s = "Instance", p = { NodeId = "string", NodeType = "AnimationNodeType", } }, AnimationPlayer = { s = "Instance", p = { Track = "string", } }, AnimationRigData = { s = "Instance", p = { } }, AnimationStreamTrack = { s = "Instance", p = { } }, AnimationTrack = { s = "Instance", p = { Priority = "AnimationPriority", TimePosition = "float", } }, AnimationValueNodeDefinition = { s = "Instance", p = { NodeId = "string", NodeType = "AnimationValueNodeType", } }, AnimationValueOutputDefinition = { s = "Instance", p = { } }, Animator = { s = "Instance", p = { PreferLodEnabled = "bool", } }, Annotation = { s = "Instance", p = { } }, AnnotationsService = { s = "Instance", p = { } }, AppAgeSignalsService = { s = "Instance", p = { } }, AppLifecycleObserverService = { s = "Instance", p = { } }, AppRatingPromptService = { s = "Instance", p = { } }, AppStorageService = { s = "LocalStorageService", p = { } }, AppUpdateService = { s = "Instance", p = { } }, ArcHandles = { s = "HandlesBase", p = { Axes = "Axes", } }, AssetCounterService = { s = "Instance", p = { } }, AssetDeliveryProxy = { s = "Instance", p = { Interface = "string", Port = "int", StartServer = "bool", } }, AssetImportService = { s = "Instance", p = { } }, AssetImportSession = { s = "ImportSession", p = { } }, AssetManagerService = { s = "Instance", p = { } }, AssetPatchSettings = { s = "Instance", p = { ContentId = "string", OutputPath = "string", PatchId = "string", } }, AssetQualityService = { s = "Instance", p = { } }, AssetService = { s = "Instance", p = { AllowInsertFreeAssets = "bool", } }, AssetSoundEffect = { s = "CustomSoundEffect", p = { } }, Atmosphere = { s = "Instance", p = { Color = "Color3", Decay = "Color3", Density = "float", Glare = "float", Haze = "float", Offset = "float", } }, AtmosphereSensor = { s = "SensorBase", p = { } }, Attachment = { s = "Instance", p = { Axis = "Vector3", CFrame = "CFrame", SecondaryAxis = "Vector3", Visible = "bool", WorldAxis = "Vector3", WorldCFrame = "CFrame", WorldSecondaryAxis = "Vector3", } }, AudioAnalyzer = { s = "Instance", p = { SpectrumEnabled = "bool", WindowSize = "AudioWindowSize", } }, AudioChannelMixer = { s = "Instance", p = { Layout = "AudioChannelLayout", } }, AudioChannelSplitter = { s = "Instance", p = { Layout = "AudioChannelLayout", } }, AudioChorus = { s = "Instance", p = { Bypass = "bool", Depth = "float", Mix = "float", Rate = "float", } }, AudioCompressor = { s = "Instance", p = { Attack = "float", Bypass = "bool", Editor = "bool", MakeupGain = "float", Ratio = "float", Release = "float", Threshold = "float", } }, AudioDeviceInput = { s = "Instance", p = { AccessType = "AccessModifierType", Active = "bool", Muted = "bool", Player = "Player", Volume = "float", } }, AudioDeviceOutput = { s = "Instance", p = { Player = "Player", } }, AudioDistortion = { s = "Instance", p = { Bypass = "bool", Level = "float", } }, AudioEcho = { s = "Instance", p = { Bypass = "bool", DelayTime = "float", DryLevel = "float", Feedback = "float", RampTime = "float", WetLevel = "float", } }, AudioEmitter = { s = "Instance", p = { AcousticSimulationEnabled = "bool", AngleAttenuation = "BinaryString", AudioInteractionGroup = "string", DiffractionEnabled = "SimulationMode", DistanceAttenuation = "BinaryString", DistanceAttenuationBounds = "NumberRange", DistanceAttenuationMode = "DistanceAttenuationMode", OcclusionEnabled = "SimulationMode", PositionInstance = "Instance", PositionType = "EmitterPositionType", ReverbEnabled = "SimulationMode", } }, AudioEqualizer = { s = "Instance", p = { Bypass = "bool", Editor = "bool", HighGain = "float", LowGain = "float", MidGain = "float", MidRange = "NumberRange", } }, AudioFader = { s = "Instance", p = { Bypass = "bool", Volume = "float", } }, AudioFilter = { s = "Instance", p = { Bypass = "bool", Editor = "bool", FilterType = "AudioFilterType", Frequency = "float", Gain = "float", Q = "float", } }, AudioFlanger = { s = "Instance", p = { Bypass = "bool", Depth = "float", Mix = "float", Rate = "float", } }, AudioFocusService = { s = "Instance", p = { } }, AudioGate = { s = "Instance", p = { Attack = "float", Bypass = "bool", Release = "float", Threshold = "NumberRange", } }, AudioLimiter = { s = "Instance", p = { Bypass = "bool", Editor = "bool", MaxLevel = "float", Release = "float", } }, AudioListener = { s = "Instance", p = { AcousticSimulationEnabled = "bool", AngleAttenuation = "BinaryString", AudioInteractionGroup = "string", DiffractionEnabled = "SimulationMode", DistanceAttenuation = "BinaryString", OcclusionEnabled = "SimulationMode", PositionInstance = "Instance", PositionType = "ListenerPositionType", ReverbEnabled = "SimulationMode", } }, AudioPages = { s = "Pages", p = { } }, AudioPitchShifter = { s = "Instance", p = { Bypass = "bool", Pitch = "float", WindowSize = "AudioWindowSize", } }, AudioPlayer = { s = "Instance", p = { Asset = "ContentId", AutoLoad = "bool", AutoPlay = "bool", IsPlaying = "bool", LoopRegion = "NumberRange", Looping = "bool", PlaybackRegion = "NumberRange", PlaybackSpeed = "double", TimePosition = "double", Volume = "float", } }, AudioRecorder = { s = "Instance", p = { IsRecording = "bool", } }, AudioReverb = { s = "Instance", p = { Bypass = "bool", DecayRatio = "float", DecayTime = "float", Density = "float", Diffusion = "float", DryLevel = "float", EarlyDelayTime = "float", HighCutFrequency = "float", LateDelayTime = "float", LowShelfFrequency = "float", LowShelfGain = "float", ReferenceFrequency = "float", WetLevel = "float", } }, AudioSearchParams = { s = "Instance", p = { Album = "string", Artist = "string", AudioSubType = "AudioSubType", MaxDuration = "int", MinDuration = "int", SearchKeyword = "string", Tag = "string", Title = "string", } }, AudioSpeechToText = { s = "Instance", p = { Enabled = "bool", Text = "string", } }, AudioTextToSpeech = { s = "Instance", p = { AutoLocalize = "bool", IsPlaying = "bool", Looping = "bool", Pitch = "float", PlaybackSpeed = "float", Speed = "float", Text = "string", TimePosition = "double", VoiceId = "string", Volume = "float", } }, AudioTremolo = { s = "Instance", p = { Bypass = "bool", Depth = "float", Duty = "float", Frequency = "float", Shape = "float", Skew = "float", Square = "float", } }, AudioWindSynthesizer = { s = "Instance", p = { Enabled = "bool", PositionInstance = "Instance", PositionType = "AudioPositionType", Profile = "WindSoundProfile", Volume = "float", } }, AuroraScriptObject = { s = "Instance", p = { BehaviorWeak = "AuroraScript", BoundInstanceWeak = "Instance", FrameId = "int", LODLevel = "int", PriorFrameInvoked = "int", } }, AvatarAbilityRules = { s = "Instance", p = { CharacterControllerMode = "AvatarSettingsCharacterControllerMode", EnableClimbing = "bool", EnableCrouching = "bool", EnableFallingDown = "bool", EnableGettingUp = "bool", EnableHolding = "bool", EnableJumping = "bool", EnableReaching = "bool", EnableRunning = "bool", EnableSitting = "bool", EnableSprinting = "bool", EnableSwimming = "bool", EnableTurning = "bool", } }, AvatarAccessoryRules = { s = "Instance", p = { AccessoryMode = "AvatarSettingsAccessoryMode", BackAccessoryOverride = "int64", EnableEmissives = "bool", EnableSound = "bool", EnableVFX = "bool", FaceAccessoryOverride = "int64", FrontAccessoryOverride = "int64", HairAccessoryOverride = "int64", HeadAccessoryOverride = "int64", LimitBounds = "Vector3", LimitMethod = "AvatarSettingsAccessoryLimitMethod", NeckAccessoryOverride = "int64", ShoulderAccessoryOverride = "int64", WaistAccessoryOverride = "int64", } }, AvatarAnimationRules = { s = "Instance", p = { AnimationPacksMode = "AvatarSettingsAnimationPacksMode", ClimbAnimationOverride = "int64", FallAnimationOverride = "int64", IdleAltAnimationOverride = "int64", IdleAnimationOverride = "int64", JumpAnimationOverride = "int64", MoodAnimationOverride = "int64", RunAnimationOverride = "int64", SwimAnimationOverride = "int64", SwimIdleAnimationOverride = "int64", WalkAnimationOverride = "int64", } }, AvatarBodyRules = { s = "Instance", p = { AvatarType = "GameAvatarType", BuildMode = "AvatarSettingsBuildMode", CustomBuildBodyType = "NumberRange", CustomBuildHead = "NumberRange", CustomBuildHeight = "NumberRange", CustomBuildProportions = "NumberRange", CustomBuildWidth = "NumberRange", CustomWorldHeight = "NumberRange", HeadOverride = "int64", LeftArmOverride = "int64", LeftLegOverride = "int64", RightArmOverride = "int64", RightLegOverride = "int64", ScaleMode = "AvatarSettingsScaleMode", TorsoOverride = "int64", } }, AvatarChatService = { s = "Instance", p = { } }, AvatarClothingRules = { s = "Instance", p = { ClassicPantsAccessoryOverride = "int64", ClassicShirtsAccessoryOverride = "int64", ClassicTShirtsAccessoryOverride = "int64", ClothingMode = "AvatarSettingsClothingMode", DressSkirtAccessoryOverride = "int64", JacketAccessoryOverride = "int64", LeftShoesAccessoryOverride = "int64", LimitBounds = "Vector3", PantsAccessoryOverride = "int64", RightShoesAccessoryOverride = "int64", ShirtAccessoryOverride = "int64", ShortsAccessoryOverride = "int64", SweaterAccessoryOverride = "int64", TShirtAccessoryOverride = "int64", } }, AvatarCollisionRules = { s = "Instance", p = { CollisionMode = "AvatarSettingsCollisionMode", HitAndTouchDetectionMode = "AvatarSettingsHitAndTouchDetectionMode", SingleColliderSize = "Vector3", } }, AvatarCreationService = { s = "Instance", p = { } }, AvatarEditorService = { s = "Instance", p = { } }, AvatarImportService = { s = "Instance", p = { } }, AvatarRules = { s = "Instance", p = { } }, AvatarSettings = { s = "Instance", p = { RulesSchemaVersion = "int", } }, BackendReplicatedStorage = { s = "Instance", p = { } }, BackendServerScriptService = { s = "Instance", p = { } }, BackendServerStorage = { s = "Instance", p = { } }, Backpack = { s = "Instance", p = { } }, BackpackItem = { s = "Model", p = { TextureContent = "Content", TextureId = "ContentId", } }, BadgeService = { s = "Instance", p = { } }, BallSocketConstraint = { s = "Constraint", p = { LimitsEnabled = "bool", MaxFrictionTorque = "float", Radius = "float", Restitution = "float", TwistLimitsEnabled = "bool", TwistLowerAngle = "float", TwistUpperAngle = "float", UpperAngle = "float", } }, BanHistoryPages = { s = "Pages", p = { } }, BaseCoreGuiConfiguration = { s = "Instance", p = { Enabled = "bool", } }, BaseImportData = { s = "Instance", p = { ImportName = "string", ShouldImport = "bool", } }, BasePart = { s = "PVInstance", p = { Anchored = "bool", AssemblyAngularVelocity = "Vector3", AssemblyLinearVelocity = "Vector3", AudioCanCollide = "bool", BackSurface = "SurfaceType", BottomSurface = "SurfaceType", BrickColor = "BrickColor", CFrame = "CFrame", CanCollide = "bool", CanQuery = "bool", CanTouch = "bool", CastShadow = "bool", CollisionGroup = "string", Color = "Color3", CustomPhysicalProperties = "PhysicalProperties", EnableFluidForces = "bool", FrontSurface = "SurfaceType", LeftSurface = "SurfaceType", Locked = "bool", Massless = "bool", Material = "Material", MaterialVariant = "string", PivotOffset = "CFrame", Reflectance = "float", RightSurface = "SurfaceType", RootPriority = "int", Rotation = "Vector3", Size = "Vector3", TopSurface = "SurfaceType", Transparency = "float", } }, BasePlayerGui = { s = "Instance", p = { } }, BaseRemoteEvent = { s = "Instance", p = { } }, BaseScript = { s = "LuaSourceContainer", p = { Disabled = "bool", Enabled = "bool", RunContext = "RunContext", } }, BaseWrap = { s = "Instance", p = { CageMeshContent = "Content", CageMeshId = "ContentId", CageOrigin = "CFrame", HSRAssetId = "ContentId", HSRContent = "Content", ImportOrigin = "CFrame", } }, Beam = { s = "Instance", p = { Attachment0 = "Attachment", Attachment1 = "Attachment", Brightness = "float", Color = "ColorSequence", CurveSize0 = "float", CurveSize1 = "float", Enabled = "bool", FaceCamera = "bool", LightEmission = "float", LightInfluence = "float", Segments = "int", Texture = "ContentId", TextureContent = "Content", TextureLength = "float", TextureMode = "TextureMode", TextureSpeed = "float", Transparency = "NumberSequence", Width0 = "float", Width1 = "float", ZOffset = "float", } }, BevelMesh = { s = "DataModelMesh", p = { } }, BillboardGui = { s = "LayerCollector", p = { Active = "bool", Adornee = "Instance", AlwaysOnTop = "bool", Brightness = "float", ClipsDescendants = "bool", DistanceStep = "float", ExtentsOffset = "Vector3", ExtentsOffsetWorldSpace = "Vector3", LightInfluence = "float", MaxDistance = "float", PlayerToHideFrom = "Instance", Size = "UDim2", SizeOffset = "Vector2", StudsOffset = "Vector3", StudsOffsetWorldSpace = "Vector3", } }, BinaryStringValue = { s = "ValueBase", p = { } }, BindableEvent = { s = "Instance", p = { } }, BindableFunction = { s = "Instance", p = { } }, BlockMesh = { s = "BevelMesh", p = { } }, BloomEffect = { s = "PostEffect", p = { Intensity = "float", Size = "float", Threshold = "float", } }, BlurEffect = { s = "PostEffect", p = { Size = "float", } }, BodyAngularVelocity = { s = "BodyMover", p = { AngularVelocity = "Vector3", MaxTorque = "Vector3", P = "float", } }, BodyColors = { s = "CharacterAppearance", p = { HeadColor = "BrickColor", HeadColor3 = "Color3", LeftArmColor = "BrickColor", LeftArmColor3 = "Color3", LeftLegColor = "BrickColor", LeftLegColor3 = "Color3", RightArmColor = "BrickColor", RightArmColor3 = "Color3", RightLegColor = "BrickColor", RightLegColor3 = "Color3", TorsoColor = "BrickColor", TorsoColor3 = "Color3", } }, BodyForce = { s = "BodyMover", p = { Force = "Vector3", } }, BodyGyro = { s = "BodyMover", p = { CFrame = "CFrame", D = "float", MaxTorque = "Vector3", P = "float", } }, BodyMover = { s = "Instance", p = { } }, BodyPartDescription = { s = "Instance", p = { AssetId = "int64", BodyPart = "BodyPart", Color = "Color3", HeadShape = "string", Instance = "Instance", } }, BodyPosition = { s = "BodyMover", p = { D = "float", MaxForce = "Vector3", P = "float", Position = "Vector3", } }, BodyThrust = { s = "BodyMover", p = { Force = "Vector3", Location = "Vector3", } }, BodyVelocity = { s = "BodyMover", p = { MaxForce = "Vector3", P = "float", Velocity = "Vector3", } }, Bone = { s = "Attachment", p = { Transform = "CFrame", } }, BoolValue = { s = "ValueBase", p = { Value = "bool", } }, BoxHandleAdornment = { s = "HandleAdornment", p = { Shading = "AdornShading", Size = "Vector3", } }, BranchService = { s = "Instance", p = { } }, Breakpoint = { s = "Instance", p = { } }, BrickColorValue = { s = "ValueBase", p = { Value = "BrickColor", } }, BrowserService = { s = "Instance", p = { } }, BubbleChatConfiguration = { s = "TextChatConfigurations", p = { AdorneeName = "string", BackgroundColor3 = "Color3", BackgroundTransparency = "double", BubbleDuration = "float", BubblesSpacing = "float", Enabled = "bool", FontFace = "Font", LocalPlayerStudsOffset = "Vector3", MaxBubbles = "float", MaxDistance = "float", MinimizeDistance = "float", TailVisible = "bool", TextColor3 = "Color3", TextSize = "int64", VerticalStudsOffset = "float", } }, BubbleChatMessageProperties = { s = "TextChatMessageProperties", p = { BackgroundColor3 = "Color3", BackgroundTransparency = "double", FontFace = "Font", TailVisible = "bool", TextColor3 = "Color3", TextSize = "int64", } }, BugReporterService = { s = "Instance", p = { } }, BulkImportService = { s = "Instance", p = { } }, BuoyancySensor = { s = "SensorBase", p = { FullySubmerged = "bool", TouchingSurface = "bool", } }, CFrameValue = { s = "ValueBase", p = { Value = "CFrame", } }, CSGDictionaryService = { s = "FlyweightService", p = { } }, CacheableContentProvider = { s = "Instance", p = { } }, CallingService = { s = "Instance", p = { } }, CalloutService = { s = "Instance", p = { } }, Camera = { s = "PVInstance", p = { CFrame = "CFrame", CameraSubject = "Instance", CameraType = "CameraType", DiagonalFieldOfView = "float", FieldOfView = "float", FieldOfViewMode = "FieldOfViewMode", Focus = "CFrame", HeadLocked = "bool", HeadScale = "float", MaxAxisFieldOfView = "float", OrthographicMode = "OrthographicMode", OrthographicSize = "float", ProjectionType = "ProjectionType", VRTiltAndRollEnabled = "bool", ViewStretch = "float", } }, CanvasGroup = { s = "GuiObject", p = { GroupColor3 = "Color3", GroupTransparency = "float", } }, Capture = { s = "Object", p = { } }, CaptureService = { s = "Instance", p = { } }, CapturesPages = { s = "Pages", p = { } }, CapturesViewConfiguration = { s = "BaseCoreGuiConfiguration", p = { Open = "bool", } }, CatalogPages = { s = "Pages", p = { } }, ChangeHistoryService = { s = "Instance", p = { } }, ChangeHistoryStreamingService = { s = "Instance", p = { } }, ChannelSelectorSoundEffect = { s = "CustomSoundEffect", p = { Channel = "int", } }, ChannelTabsConfiguration = { s = "TextChatConfigurations", p = { BackgroundColor3 = "Color3", BackgroundTransparency = "double", Enabled = "bool", FontFace = "Font", HoverBackgroundColor3 = "Color3", SelectedTabTextColor3 = "Color3", TextColor3 = "Color3", TextSize = "int64", TextStrokeColor3 = "Color3", TextStrokeTransparency = "double", } }, CharacterAppearance = { s = "Instance", p = { } }, CharacterMesh = { s = "CharacterAppearance", p = { BaseTextureContent = "Content", BaseTextureId = "int64", BodyPart = "BodyPart", MeshContent = "Content", MeshId = "int64", OverlayTextureContent = "Content", OverlayTextureId = "int64", } }, Chat = { s = "Instance", p = { BubbleChatEnabled = "bool", LoadDefaultChat = "bool", } }, ChatInputBarConfiguration = { s = "TextChatConfigurations", p = { AutocompleteEnabled = "bool", BackgroundColor3 = "Color3", BackgroundTransparency = "double", Enabled = "bool", FontFace = "Font", KeyboardKeyCode = "KeyCode", PlaceholderColor3 = "Color3", TargetTextChannel = "TextChannel", TextColor3 = "Color3", TextSize = "int64", TextStrokeColor3 = "Color3", TextStrokeTransparency = "double", } }, ChatWindowConfiguration = { s = "TextChatConfigurations", p = { BackgroundColor3 = "Color3", BackgroundTransparency = "double", Enabled = "bool", FontFace = "Font", HeightScale = "float", HorizontalAlignment = "HorizontalAlignment", TextChannelDisplayMode = "TextChannelDisplayMode", TextColor3 = "Color3", TextSize = "int64", TextStrokeColor3 = "Color3", TextStrokeTransparency = "double", VerticalAlignment = "VerticalAlignment", WidthScale = "float", } }, ChatWindowMessageProperties = { s = "TextChatMessageProperties", p = { FontFace = "Font", PrefixTextProperties = "ChatWindowMessageProperties", TextColor3 = "Color3", TextSize = "int", TextStrokeColor3 = "Color3", TextStrokeTransparency = "double", } }, ChorusSoundEffect = { s = "SoundEffect", p = { Depth = "float", Mix = "float", Rate = "float", } }, ClickDetector = { s = "Instance", p = { CursorIcon = "ContentId", CursorIconContent = "Content", MaxActivationDistance = "float", } }, ClientReplicator = { s = "NetworkReplicator", p = { } }, ClientStorageService = { s = "Instance", p = { } }, ClimbController = { s = "ControllerBase", p = { AccelerationTime = "float", BalanceMaxTorque = "float", BalanceSpeed = "float", MoveMaxForce = "float", } }, Clothing = { s = "CharacterAppearance", p = { Color3 = "Color3", } }, CloudCRUDService = { s = "Instance", p = { } }, CloudExecutionService = { s = "Instance", p = { } }, CloudLocalizationTable = { s = "LocalizationTable", p = { } }, Clouds = { s = "Instance", p = { Color = "Color3", Cover = "float", Density = "float", Enabled = "bool", } }, ClusterPacketCache = { s = "Instance", p = { } }, Collaborator = { s = "Instance", p = { } }, CollaboratorsService = { s = "Instance", p = { } }, CollectionService = { s = "Instance", p = { } }, Color3Value = { s = "ValueBase", p = { Value = "Color3", } }, ColorCorrectionEffect = { s = "PostEffect", p = { Brightness = "float", Contrast = "float", Saturation = "float", TintColor = "Color3", } }, ColorGradingEffect = { s = "PostEffect", p = { TonemapperPreset = "TonemapperPreset", } }, CommerceService = { s = "Instance", p = { } }, CompositeValueCurve = { s = "Instance", p = { CurveType = "CompositeValueCurveType", } }, CompressorSoundEffect = { s = "SoundEffect", p = { Attack = "float", GainMakeup = "float", Ratio = "float", Release = "float", SideChain = "Instance", Threshold = "float", } }, ConeHandleAdornment = { s = "HandleAdornment", p = { Height = "float", Hollow = "bool", Radius = "float", Shading = "AdornShading", } }, ConfigService = { s = "Instance", p = { } }, ConfigSnapshot = { s = "Object", p = { } }, Configuration = { s = "Instance", p = { } }, ConfigureServerService = { s = "Instance", p = { } }, ConnectivityService = { s = "Instance", p = { } }, Constraint = { s = "Instance", p = { Attachment0 = "Attachment", Attachment1 = "Attachment", Color = "BrickColor", Enabled = "bool", Visible = "bool", } }, ContentProvider = { s = "Instance", p = { } }, ContextActionService = { s = "Instance", p = { } }, ControlState = { s = "Instance", p = { Owner = "Player", } }, Controller = { s = "Instance", p = { } }, ControllerBase = { s = "Instance", p = { BalanceRigidityEnabled = "bool", MoveSpeedFactor = "float", } }, ControllerManager = { s = "Instance", p = { ActiveController = "ControllerBase", BaseMoveSpeed = "float", BaseTurnSpeed = "float", ClimbSensor = "ControllerSensor", FacingDirection = "Vector3", GroundSensor = "ControllerSensor", MovingDirection = "Vector3", RootPart = "BasePart", UpDirection = "Vector3", } }, ControllerPartSensor = { s = "ControllerSensor", p = { HitFrame = "CFrame", HitNormal = "Vector3", LadderSearchHeight = "float", LadderSearchOffset = "float", SearchDistance = "float", SensedMaterial = "Material", SensedPart = "BasePart", SensorMode = "SensorMode", } }, ControllerSensor = { s = "SensorBase", p = { } }, ControllerService = { s = "Instance", p = { } }, CookiesService = { s = "Instance", p = { } }, CoreGui = { s = "BasePlayerGui", p = { SelectionImageObject = "GuiObject", } }, CoreGuiConfiguration = { s = "Instance", p = { CapturesViewConfiguration = "CapturesViewConfiguration", PlayerListConfiguration = "PlayerListConfiguration", SelfViewConfiguration = "SelfViewConfiguration", } }, CorePackages = { s = "Instance", p = { } }, CoreScript = { s = "BaseScript", p = { } }, CoreScriptDebuggingManagerHelper = { s = "Instance", p = { } }, CoreScriptSyncService = { s = "Instance", p = { } }, CornerWedgePart = { s = "BasePart", p = { } }, CreationDBService = { s = "Instance", p = { } }, CreatorStoreService = { s = "Instance", p = { } }, CrossDMScriptChangeListener = { s = "Instance", p = { } }, CurveAnimation = { s = "AnimationClip", p = { } }, CustomEvent = { s = "Instance", p = { } }, CustomEventReceiver = { s = "Instance", p = { Source = "Instance", } }, CustomLog = { s = "Instance", p = { } }, CustomSoundEffect = { s = "SoundEffect", p = { } }, CylinderHandleAdornment = { s = "HandleAdornment", p = { Angle = "float", Height = "float", InnerRadius = "float", Radius = "float", Shading = "AdornShading", } }, CylinderMesh = { s = "BevelMesh", p = { } }, CylindricalConstraint = { s = "SlidingBallConstraint", p = { AngularActuatorType = "ActuatorType", AngularLimitsEnabled = "bool", AngularResponsiveness = "float", AngularRestitution = "float", AngularSpeed = "float", AngularVelocity = "float", InclinationAngle = "float", LowerAngle = "float", MotorMaxAngularAcceleration = "float", MotorMaxTorque = "float", RotationAxisVisible = "bool", ServoMaxTorque = "float", TargetAngle = "float", UpperAngle = "float", } }, DataModel = { s = "ServiceProvider", p = { } }, DataModelDiff = { s = "Object", p = { } }, DataModelMesh = { s = "Instance", p = { Offset = "Vector3", Scale = "Vector3", VertexColor = "Vector3", } }, DataModelSession = { s = "Instance", p = { } }, DataStore = { s = "GlobalDataStore", p = { } }, DataStoreGetOptions = { s = "Instance", p = { UseCache = "bool", } }, DataStoreIncrementOptions = { s = "Instance", p = { } }, DataStoreInfo = { s = "Instance", p = { } }, DataStoreKey = { s = "Instance", p = { } }, DataStoreKeyInfo = { s = "Instance", p = { } }, DataStoreKeyPages = { s = "Pages", p = { } }, DataStoreListingPages = { s = "Pages", p = { } }, DataStoreObjectVersionInfo = { s = "Instance", p = { } }, DataStoreOptions = { s = "Instance", p = { AllScopes = "bool", } }, DataStorePages = { s = "Pages", p = { } }, DataStoreService = { s = "Instance", p = { AutomaticRetry = "bool", } }, DataStoreSetOptions = { s = "Instance", p = { } }, DataStoreVersionPages = { s = "Pages", p = { } }, Debris = { s = "Instance", p = { } }, DebugSettings = { s = "Instance", p = { IsScriptStackTracingEnabled = "bool", ReportSoundWarnings = "bool", TickCountPreciseOverride = "TickCountSampleMethod", } }, DebuggablePluginWatcher = { s = "Instance", p = { } }, DebuggerBreakpoint = { s = "Instance", p = { Condition = "string", ContinueExecution = "bool", IsEnabled = "bool", LogExpression = "string", isContextDependentBreakpoint = "bool", } }, DebuggerConnection = { s = "Instance", p = { } }, DebuggerConnectionManager = { s = "Instance", p = { } }, DebuggerLuaResponse = { s = "Instance", p = { } }, DebuggerManager = { s = "Instance", p = { } }, DebuggerUIService = { s = "Instance", p = { } }, DebuggerVariable = { s = "Instance", p = { } }, DebuggerWatch = { s = "Instance", p = { Expression = "string", } }, Decal = { s = "FaceInstance", p = { AutoLocalize = "bool", Color3 = "Color3", ColorMap = "ContentId", ColorMapContent = "Content", EmissiveMaskContent = "Content", EmissiveStrength = "float", EmissiveTint = "Color3", LocalizedTextureContent = "Content", MetalnessMapContent = "Content", NormalMapContent = "Content", ResampleMode = "ResamplerMode", Rotation = "float", RoughnessMapContent = "Content", Texture = "ContentId", TextureContent = "Content", TexturePack = "ContentId", TexturePackContent = "Content", Transparency = "float", UVOffset = "Vector2", UVScale = "Vector2", ZIndex = "int", } }, DeferredAssetManagerService = { s = "Instance", p = { JoiningPlaceId = "int64", JoiningUniverseId = "int64", PregameLoadingScreenOnly = "bool", } }, DepthOfFieldEffect = { s = "PostEffect", p = { FarIntensity = "float", FocusDistance = "float", InFocusRadius = "float", NearIntensity = "float", } }, DesignFoundationsService = { s = "Instance", p = { } }, DeviceDisplayService = { s = "Instance", p = { } }, DeviceIdService = { s = "Instance", p = { } }, Dialog = { s = "Instance", p = { BehaviorType = "DialogBehaviorType", ConversationDistance = "float", GoodbyeChoiceActive = "bool", GoodbyeDialog = "string", InitialPrompt = "string", Purpose = "DialogPurpose", Tone = "DialogTone", TriggerDistance = "float", TriggerOffset = "Vector3", } }, DialogChoice = { s = "Instance", p = { GoodbyeChoiceActive = "bool", GoodbyeDialog = "string", ResponseDialog = "string", UserDialog = "string", } }, DigitsRigDescription = { s = "Instance", p = { Index1 = "Instance", Index1TposeAdjustment = "CFrame", Index2 = "Instance", Index2TposeAdjustment = "CFrame", Index3 = "Instance", Index3TposeAdjustment = "CFrame", IndexRange = "Vector3", IndexSize = "float", Middle1 = "Instance", Middle1TposeAdjustment = "CFrame", Middle2 = "Instance", Middle2TposeAdjustment = "CFrame", Middle3 = "Instance", Middle3TposeAdjustment = "CFrame", MiddleRange = "Vector3", MiddleSize = "float", Pinky1 = "Instance", Pinky1TposeAdjustment = "CFrame", Pinky2 = "Instance", Pinky2TposeAdjustment = "CFrame", Pinky3 = "Instance", Pinky3TposeAdjustment = "CFrame", PinkyRange = "Vector3", PinkySize = "float", Ring1 = "Instance", Ring1TposeAdjustment = "CFrame", Ring2 = "Instance", Ring2TposeAdjustment = "CFrame", Ring3 = "Instance", Ring3TposeAdjustment = "CFrame", RingRange = "Vector3", RingSize = "float", Side = "DigitsRigDescriptionSide", Thumb1 = "Instance", Thumb1TposeAdjustment = "CFrame", Thumb2 = "Instance", Thumb2TposeAdjustment = "CFrame", Thumb3 = "Instance", Thumb3TposeAdjustment = "CFrame", ThumbRange = "Vector3", ThumbSize = "float", } }, DisplayWakeLock = { s = "Instance", p = { } }, DistortionSoundEffect = { s = "SoundEffect", p = { Level = "float", } }, DockWidgetPluginGui = { s = "PluginGui", p = { } }, DoubleConstrainedValue = { s = "ValueBase", p = { MaxValue = "double", MinValue = "double", Value = "double", } }, DraftsService = { s = "Instance", p = { } }, DragDetector = { s = "ClickDetector", p = { ActivatedCursorIcon = "ContentId", ActivatedCursorIconContent = "Content", ApplyAtCenterOfMass = "bool", Axis = "Vector3", DragFrame = "CFrame", DragStyle = "DragDetectorDragStyle", Enabled = "bool", GamepadModeSwitchKeyCode = "KeyCode", KeyboardModeSwitchKeyCode = "KeyCode", MaxDragAngle = "float", MaxDragTranslation = "Vector3", MaxForce = "float", MaxTorque = "float", MinDragAngle = "float", MinDragTranslation = "Vector3", Orientation = "Vector3", PermissionPolicy = "DragDetectorPermissionPolicy", ReferenceInstance = "Instance", ResponseStyle = "DragDetectorResponseStyle", Responsiveness = "float", RunLocally = "bool", SecondaryAxis = "Vector3", TrackballRadialPullFactor = "float", TrackballRollFactor = "float", VRSwitchKeyCode = "KeyCode", WorldAxis = "Vector3", WorldSecondaryAxis = "Vector3", } }, Dragger = { s = "Instance", p = { } }, DraggerService = { s = "Instance", p = { AlignDraggedObjects = "bool", AngleSnapEnabled = "bool", AngleSnapIncrement = "float", AnimateHover = "bool", CollisionsEnabled = "bool", DraggerCoordinateSpace = "DraggerCoordinateSpace", DraggerMovementMode = "DraggerMovementMode", GeometrySnapColor = "Color3", HoverAnimateFrequency = "float", HoverThickness = "float", JointsEnabled = "bool", LinearSnapEnabled = "bool", LinearSnapIncrement = "float", PartSnapEnabled = "bool", PivotSnapToGeometry = "bool", ShowHover = "bool", ShowPivotIndicator = "bool", UseBoundingBoxes = "bool", } }, DynamicRotate = { s = "JointInstance", p = { BaseAngle = "float", } }, EchoSoundEffect = { s = "SoundEffect", p = { Delay = "float", DryLevel = "float", Feedback = "float", WetLevel = "float", } }, EditableImage = { s = "Object", p = { } }, EditableMesh = { s = "Object", p = { } }, EditableService = { s = "Instance", p = { } }, EditorSourceService = { s = "Instance", p = { } }, EncodingService = { s = "Instance", p = { } }, EqualizerSoundEffect = { s = "SoundEffect", p = { HighGain = "float", LowGain = "float", MidGain = "float", } }, EulerRotationCurve = { s = "Instance", p = { RotationOrder = "RotationOrder", } }, EventIngestService = { s = "Instance", p = { } }, ExampleV2Service = { s = "Instance", p = { } }, ExecutedRemoteCommand = { s = "Object", p = { } }, ExperienceAuthService = { s = "Instance", p = { } }, ExperienceInviteOptions = { s = "Instance", p = { InviteMessageId = "string", InviteUser = "int64", LaunchData = "string", PromptMessage = "string", } }, ExperienceNotificationService = { s = "Instance", p = { } }, ExperienceService = { s = "Instance", p = { } }, ExperienceStateCaptureService = { s = "Instance", p = { } }, ExperienceStateRecordingService = { s = "Instance", p = { } }, ExplorerFilter = { s = "Instance", p = { } }, ExplorerFilterAutocompleter = { s = "Instance", p = { } }, ExplorerServiceVisibilityService = { s = "Instance", p = { } }, Explosion = { s = "Instance", p = { BlastPressure = "float", BlastRadius = "float", DestroyJointRadiusPercent = "float", ExplosionType = "ExplosionType", Position = "Vector3", TimeScale = "float", Visible = "bool", } }, ExternalIdentityService = { s = "Instance", p = { } }, FaceAnimatorService = { s = "Instance", p = { } }, FaceControls = { s = "Instance", p = { ChinRaiser = "float", ChinRaiserUpperLip = "float", Corrugator = "float", EyesLookDown = "float", EyesLookLeft = "float", EyesLookRight = "float", EyesLookUp = "float", FlatPucker = "float", Funneler = "float", JawDrop = "float", JawLeft = "float", JawRight = "float", LeftBrowLowerer = "float", LeftCheekPuff = "float", LeftCheekRaiser = "float", LeftDimpler = "float", LeftEyeClosed = "float", LeftEyeUpperLidRaiser = "float", LeftInnerBrowRaiser = "float", LeftLipCornerDown = "float", LeftLipCornerPuller = "float", LeftLipStretcher = "float", LeftLowerLipDepressor = "float", LeftNoseWrinkler = "float", LeftOuterBrowRaiser = "float", LeftUpperLipRaiser = "float", LipPresser = "float", LipsTogether = "float", LowerLipSuck = "float", MouthLeft = "float", MouthRight = "float", Pucker = "float", RightBrowLowerer = "float", RightCheekPuff = "float", RightCheekRaiser = "float", RightDimpler = "float", RightEyeClosed = "float", RightEyeUpperLidRaiser = "float", RightInnerBrowRaiser = "float", RightLipCornerDown = "float", RightLipCornerPuller = "float", RightLipStretcher = "float", RightLowerLipDepressor = "float", RightNoseWrinkler = "float", RightOuterBrowRaiser = "float", RightUpperLipRaiser = "float", TongueDown = "float", TongueOut = "float", TongueUp = "float", UpperLipSuck = "float", } }, FaceInstance = { s = "Instance", p = { Face = "NormalId", } }, FacialAgeEstimationService = { s = "Instance", p = { } }, FacialAnimationRecordingService = { s = "Instance", p = { } }, FacialAnimationStreamingServiceStats = { s = "Instance", p = { } }, FacialAnimationStreamingServiceV2 = { s = "Instance", p = { } }, FacialAnimationStreamingSubsessionStats = { s = "Instance", p = { } }, FacsImportData = { s = "BaseImportData", p = { } }, Feature = { s = "Instance", p = { FaceId = "NormalId", InOut = "InOut", LeftRight = "LeftRight", TopBottom = "TopBottom", } }, FeatureMatchingTable = { s = "Instance", p = { } }, FeatureRestrictionManager = { s = "Instance", p = { } }, File = { s = "Instance", p = { } }, FileManagerService = { s = "Instance", p = { } }, FileMesh = { s = "DataModelMesh", p = { MeshContent = "Content", MeshId = "ContentId", TextureContent = "Content", TextureId = "ContentId", } }, FileSyncReplicationService = { s = "Instance", p = { } }, Fire = { s = "Instance", p = { Color = "Color3", Enabled = "bool", Heat = "float", SecondaryColor = "Color3", Size = "float", TimeScale = "float", } }, Flag = { s = "Tool", p = { TeamColor = "BrickColor", } }, FlagStand = { s = "Part", p = { TeamColor = "BrickColor", } }, FlagStandService = { s = "Instance", p = { } }, FlangeSoundEffect = { s = "SoundEffect", p = { Depth = "float", Mix = "float", Rate = "float", } }, FloatCurve = { s = "Instance", p = { } }, FloorWire = { s = "GuiBase3d", p = { CycleOffset = "float", From = "BasePart", StudsBetweenTextures = "float", Texture = "ContentId", TextureSize = "Vector2", To = "BasePart", Velocity = "float", WireRadius = "float", } }, FluidForceSensor = { s = "SensorBase", p = { } }, FlyweightService = { s = "Instance", p = { } }, Folder = { s = "Instance", p = { IconTint = "Color3", } }, ForceField = { s = "Instance", p = { Visible = "bool", } }, FormFactorPart = { s = "BasePart", p = { } }, Frame = { s = "GuiObject", p = { Style = "FrameStyle", } }, FriendPages = { s = "Pages", p = { } }, FriendService = { s = "Instance", p = { } }, FriendsCallingInstance = { s = "Instance", p = { } }, FriendsCallingParticipant = { s = "Instance", p = { } }, FriendsCallingService = { s = "Instance", p = { } }, FunctionalTest = { s = "Instance", p = { Description = "string", } }, GamePassService = { s = "Instance", p = { } }, GameSettings = { s = "Instance", p = { } }, GamepadService = { s = "Instance", p = { GamepadCursorEnabled = "bool", } }, GeneratedFolder = { s = "Folder", p = { } }, GenerationService = { s = "Instance", p = { } }, GenericChallengeService = { s = "Instance", p = { } }, GenericSettings = { s = "ServiceProvider", p = { } }, Geometry = { s = "Instance", p = { } }, GeometryService = { s = "Instance", p = { } }, GetTextBoundsParams = { s = "Instance", p = { Font = "Font", RichText = "bool", Size = "float", Text = "string", Width = "float", } }, GlobalDataStore = { s = "Instance", p = { } }, GlobalSettings = { s = "GenericSettings", p = { } }, Glue = { s = "JointInstance", p = { F0 = "Vector3", F1 = "Vector3", F2 = "Vector3", F3 = "Vector3", } }, GongService = { s = "Instance", p = { } }, GroundController = { s = "ControllerBase", p = { AccelerationLean = "float", AccelerationTime = "float", BalanceMaxTorque = "float", BalanceSpeed = "float", DecelerationTime = "float", Friction = "float", FrictionWeight = "float", GroundOffset = "float", StandForce = "float", StandSpeed = "float", TurnSpeedFactor = "float", } }, GroupImportData = { s = "BaseImportData", p = { Anchored = "bool", ImportAsModelAsset = "bool", InsertInWorkspace = "bool", } }, GroupService = { s = "Instance", p = { } }, GuiBase = { s = "Instance", p = { } }, GuiBase2d = { s = "GuiBase", p = { AutoLocalize = "bool", RootLocalizationTable = "LocalizationTable", SelectionBehaviorDown = "SelectionBehavior", SelectionBehaviorLeft = "SelectionBehavior", SelectionBehaviorRight = "SelectionBehavior", SelectionBehaviorUp = "SelectionBehavior", SelectionGroup = "bool", } }, GuiBase3d = { s = "GuiBase", p = { Color3 = "Color3", Transparency = "float", Visible = "bool", } }, GuiButton = { s = "GuiObject", p = { AutoButtonColor = "bool", HoverHapticEffect = "HapticEffect", Modal = "bool", PressHapticEffect = "HapticEffect", Selected = "bool", Style = "ButtonStyle", } }, GuiLabel = { s = "GuiObject", p = { } }, GuiMain = { s = "ScreenGui", p = { } }, GuiObject = { s = "GuiBase2d", p = { Active = "bool", AnchorPoint = "Vector2", AutomaticSize = "AutomaticSize", BackgroundColor3 = "Color3", BackgroundTransparency = "float", BorderColor3 = "Color3", BorderMode = "BorderMode", BorderSizePixel = "int", ClipsDescendants = "bool", InputSink = "InputSink", Interactable = "bool", LayoutOrder = "int", NextSelectionDown = "GuiObject", NextSelectionLeft = "GuiObject", NextSelectionRight = "GuiObject", NextSelectionUp = "GuiObject", Position = "UDim2", Rotation = "float", Selectable = "bool", SelectionImageObject = "GuiObject", SelectionOrder = "int", Size = "UDim2", SizeConstraint = "SizeConstraint", Visible = "bool", ZIndex = "int", } }, GuiService = { s = "Instance", p = { AutoSelectGuiEnabled = "bool", GuiNavigationEnabled = "bool", SelectedObject = "GuiObject", } }, GuidRegistryService = { s = "Instance", p = { } }, HSRDataContentProvider = { s = "CacheableContentProvider", p = { } }, HandleAdornment = { s = "PVAdornment", p = { AdornCullingMode = "AdornCullingMode", AlwaysOnTop = "bool", CFrame = "CFrame", SizeRelativeOffset = "Vector3", ZIndex = "int", } }, Handles = { s = "HandlesBase", p = { Faces = "Faces", Style = "HandlesStyle", } }, HandlesBase = { s = "PartAdornment", p = { } }, HapticEffect = { s = "Instance", p = { Looped = "bool", Position = "Vector3", Radius = "float", Type = "HapticEffectType", } }, HapticService = { s = "Instance", p = { } }, HarmonyService = { s = "Instance", p = { } }, Hat = { s = "Accoutrement", p = { } }, HeapProfilerService = { s = "Instance", p = { } }, HeatmapQueryService = { s = "Instance", p = { } }, HeatmapService = { s = "Instance", p = { } }, HeightmapImporterService = { s = "Instance", p = { } }, HiddenPlayerService = { s = "Instance", p = { } }, HiddenSurfaceRemovalAsset = { s = "Instance", p = { } }, Highlight = { s = "Instance", p = { Adornee = "Instance", DepthMode = "HighlightDepthMode", Enabled = "bool", FillColor = "Color3", FillTransparency = "float", OutlineColor = "Color3", OutlineTransparency = "float", } }, HingeConstraint = { s = "Constraint", p = { ActuatorType = "ActuatorType", AngularResponsiveness = "float", AngularSpeed = "float", AngularVelocity = "float", LimitsEnabled = "bool", LowerAngle = "float", MotorMaxAcceleration = "float", MotorMaxTorque = "float", Radius = "float", Restitution = "float", ServoMaxTorque = "float", TargetAngle = "float", UpperAngle = "float", } }, Hint = { s = "Message", p = { } }, Hole = { s = "Feature", p = { } }, Hopper = { s = "Instance", p = { } }, HopperBin = { s = "BackpackItem", p = { Active = "bool", BinType = "BinType", } }, HttpRbxApiService = { s = "Instance", p = { } }, HttpRequest = { s = "Instance", p = { } }, HttpService = { s = "Instance", p = { HttpEnabled = "bool", } }, Humanoid = { s = "Instance", p = { AutoJumpEnabled = "bool", AutoRotate = "bool", AutomaticScalingEnabled = "bool", BreakJointsOnDeath = "bool", DisplayDistanceType = "HumanoidDisplayDistanceType", DisplayName = "string", EvaluateStateMachine = "bool", Health = "float", HealthDisplayDistance = "float", HealthDisplayType = "HumanoidHealthDisplayType", HipHeight = "float", Jump = "bool", JumpHeight = "float", JumpPower = "float", MaxHealth = "float", MaxSlopeAngle = "float", NameDisplayDistance = "float", NameOcclusion = "NameOcclusion", PlatformStand = "bool", RequiresNeck = "bool", RigType = "HumanoidRigType", Sit = "bool", TargetPoint = "Vector3", UseJumpPower = "bool", WalkSpeed = "float", WalkToPart = "BasePart", WalkToPoint = "Vector3", } }, HumanoidController = { s = "Controller", p = { } }, HumanoidDescription = { s = "Instance", p = { BackAccessory = "string", BodyTypeScale = "float", ClimbAnimation = "int64", DepthScale = "float", Face = "int64", FaceAccessory = "string", FallAnimation = "int64", FrontAccessory = "string", GraphicTShirt = "int64", HairAccessory = "string", HatAccessory = "string", Head = "int64", HeadColor = "Color3", HeadScale = "float", HeightScale = "float", IdleAnimation = "int64", JumpAnimation = "int64", LeftArm = "int64", LeftArmColor = "Color3", LeftLeg = "int64", LeftLegColor = "Color3", MoodAnimation = "int64", NeckAccessory = "string", Pants = "int64", ProportionScale = "float", RightArm = "int64", RightArmColor = "Color3", RightLeg = "int64", RightLegColor = "Color3", RunAnimation = "int64", Shirt = "int64", ShouldersAccessory = "string", StaticFacialAnimation = "bool", SwimAnimation = "int64", Torso = "int64", TorsoColor = "Color3", UseAvatarSettings = "bool", WaistAccessory = "string", WalkAnimation = "int64", WidthScale = "float", } }, HumanoidRigDescription = { s = "Instance", p = { Chest = "Instance", ChestRangeMax = "Vector3", ChestRangeMin = "Vector3", ChestSize = "float", ChestTposeAdjustment = "CFrame", HeadBase = "Instance", HeadBaseRangeMax = "Vector3", HeadBaseRangeMin = "Vector3", HeadBaseSize = "float", HeadBaseTposeAdjustment = "CFrame", LeftAnkle = "Instance", LeftAnkleRangeMax = "Vector3", LeftAnkleRangeMin = "Vector3", LeftAnkleSize = "float", LeftAnkleTposeAdjustment = "CFrame", LeftClavicle = "Instance", LeftClavicleRangeMax = "Vector3", LeftClavicleRangeMin = "Vector3", LeftClavicleSize = "float", LeftClavicleTposeAdjustment = "CFrame", LeftElbow = "Instance", LeftElbowRangeMax = "Vector3", LeftElbowRangeMin = "Vector3", LeftElbowSize = "float", LeftElbowTposeAdjustment = "CFrame", LeftHip = "Instance", LeftHipRangeMax = "Vector3", LeftHipRangeMin = "Vector3", LeftHipSize = "float", LeftHipTposeAdjustment = "CFrame", LeftKnee = "Instance", LeftKneeRangeMax = "Vector3", LeftKneeRangeMin = "Vector3", LeftKneeSize = "float", LeftKneeTposeAdjustment = "CFrame", LeftShoulder = "Instance", LeftShoulderRangeMax = "Vector3", LeftShoulderRangeMin = "Vector3", LeftShoulderSize = "float", LeftShoulderTposeAdjustment = "CFrame", LeftToeBase = "Instance", LeftToeBaseRangeMax = "Vector3", LeftToeBaseRangeMin = "Vector3", LeftToeBaseSize = "float", LeftToeBaseTposeAdjustment = "CFrame", LeftWrist = "Instance", LeftWristRangeMax = "Vector3", LeftWristRangeMin = "Vector3", LeftWristSize = "float", LeftWristTposeAdjustment = "CFrame", Neck = "Instance", NeckRangeMax = "Vector3", NeckRangeMin = "Vector3", NeckSize = "float", NeckTposeAdjustment = "CFrame", OriginOffset = "CFrame", RightAnkle = "Instance", RightAnkleRangeMax = "Vector3", RightAnkleRangeMin = "Vector3", RightAnkleSize = "float", RightAnkleTposeAdjustment = "CFrame", RightClavicle = "Instance", RightClavicleRangeMax = "Vector3", RightClavicleRangeMin = "Vector3", RightClavicleSize = "float", RightClavicleTposeAdjustment = "CFrame", RightElbow = "Instance", RightElbowRangeMax = "Vector3", RightElbowRangeMin = "Vector3", RightElbowSize = "float", RightElbowTposeAdjustment = "CFrame", RightHip = "Instance", RightHipRangeMax = "Vector3", RightHipRangeMin = "Vector3", RightHipSize = "float", RightHipTposeAdjustment = "CFrame", RightKnee = "Instance", RightKneeRangeMax = "Vector3", RightKneeRangeMin = "Vector3", RightKneeSize = "float", RightKneeTposeAdjustment = "CFrame", RightShoulder = "Instance", RightShoulderRangeMax = "Vector3", RightShoulderRangeMin = "Vector3", RightShoulderSize = "float", RightShoulderTposeAdjustment = "CFrame", RightToeBase = "Instance", RightToeBaseRangeMax = "Vector3", RightToeBaseRangeMin = "Vector3", RightToeBaseSize = "float", RightToeBaseTposeAdjustment = "CFrame", RightWrist = "Instance", RightWristRangeMax = "Vector3", RightWristRangeMin = "Vector3", RightWristSize = "float", RightWristTposeAdjustment = "CFrame", Root = "Instance", RootRangeMax = "Vector3", RootRangeMin = "Vector3", RootSize = "float", RootTposeAdjustment = "CFrame", Spine = "Instance", SpineRangeMax = "Vector3", SpineRangeMin = "Vector3", SpineSize = "float", SpineTposeAdjustment = "CFrame", Waist = "Instance", WaistRangeMax = "Vector3", WaistRangeMin = "Vector3", WaistSize = "float", WaistTposeAdjustment = "CFrame", } }, IKControl = { s = "Instance", p = { ChainRoot = "Instance", Enabled = "bool", EndEffector = "Instance", EndEffectorOffset = "CFrame", Offset = "CFrame", Pole = "Instance", Priority = "int", SmoothTime = "float", Target = "Instance", Type = "IKControlType", Weight = "float", } }, ILegacyStudioBridge = { s = "Instance", p = { } }, IXPService = { s = "Instance", p = { } }, ImageButton = { s = "GuiButton", p = { HoverImage = "ContentId", HoverImageContent = "Content", Image = "ContentId", ImageColor3 = "Color3", ImageContent = "Content", ImageRectOffset = "Vector2", ImageRectSize = "Vector2", ImageTransparency = "float", LocalizedImageContent = "Content", PressedImage = "ContentId", PressedImageContent = "Content", ResampleMode = "ResamplerMode", ScaleType = "ScaleType", SliceCenter = "Rect", SliceScale = "float", TileSize = "UDim2", } }, ImageHandleAdornment = { s = "HandleAdornment", p = { Image = "ContentId", ImageContent = "Content", Size = "Vector2", } }, ImageLabel = { s = "GuiLabel", p = { Image = "ContentId", ImageColor3 = "Color3", ImageContent = "Content", ImageRectOffset = "Vector2", ImageRectSize = "Vector2", ImageTransparency = "float", LocalizedImageContent = "Content", ResampleMode = "ResamplerMode", ScaleType = "ScaleType", SliceCenter = "Rect", SliceScale = "float", TileSize = "UDim2", } }, ImageScreenCaptureService = { s = "Instance", p = { } }, ImportSession = { s = "Instance", p = { } }, IncrementalPatchBuilder = { s = "Instance", p = { AddPathsToBundle = "bool", BuildDebouncePeriod = "double", HighCompression = "bool", SerializePatch = "bool", UseFileLevelCompressionInsteadOfChunk = "bool", ZstdCompression = "bool", } }, InputAction = { s = "Instance", p = { DisplayName = "string", Enabled = "bool", Type = "InputActionType", } }, InputActionLabel = { s = "GuiObject", p = { FontFace = "Font", ImageColor3 = "Color3", ImageTransparency = "float", InputAction = "InputAction", TextColor3 = "Color3", TextSize = "float", TextTransparency = "float", TextWrapped = "bool", TextXAlignment = "TextXAlignment", TextYAlignment = "TextYAlignment", } }, InputBinding = { s = "Instance", p = { Backward = "KeyCode", ClampMagnitudeToOne = "bool", Deadzone = "NumberRange", DisplayImage = "Content", DisplayName = "string", Down = "KeyCode", Forward = "KeyCode", KeyCode = "KeyCode", Left = "KeyCode", PointerIndex = "int", PressedThreshold = "float", PrimaryModifier = "KeyCode", ReleasedThreshold = "float", ResponseCurve = "float", Right = "KeyCode", Scale = "float", SecondaryModifier = "KeyCode", Type = "InputBindingType", UIButton = "GuiButton", UIModifier = "GuiButton", Up = "KeyCode", Vector2Scale = "Vector2", Vector3Scale = "Vector3", } }, InputContext = { s = "Instance", p = { Enabled = "bool", Priority = "int", Sink = "bool", } }, InputObject = { s = "Instance", p = { UserInputState = "UserInputState", } }, InputReplayService = { s = "Instance", p = { } }, InsertService = { s = "Instance", p = { } }, Instance = { s = "Object", p = { Capabilities = "SecurityCapabilities", Sandboxed = "bool", UniqueId = "UniqueId", } }, InstanceAdornment = { s = "GuiBase3d", p = { Adornee = "Instance", } }, InstanceExtensionsService = { s = "Instance", p = { } }, InstanceFileSyncService = { s = "Instance", p = { } }, InstanceRegistryService = { s = "Instance", p = { } }, IntConstrainedValue = { s = "ValueBase", p = { MaxValue = "int64", MinValue = "int64", Value = "int64", } }, IntValue = { s = "ValueBase", p = { Value = "int64", } }, IntentService = { s = "Instance", p = { } }, InternalMessagingService = { s = "Instance", p = { } }, InternalMessagingServiceVerifier = { s = "Instance", p = { } }, IntersectOperation = { s = "PartOperation", p = { } }, InventoryPages = { s = "Pages", p = { } }, JointImportData = { s = "BaseImportData", p = { } }, JointInstance = { s = "Instance", p = { C0 = "CFrame", C1 = "CFrame", Enabled = "bool", Part0 = "BasePart", Part1 = "BasePart", } }, JointsService = { s = "Instance", p = { } }, KeyboardService = { s = "Instance", p = { } }, Keyframe = { s = "Instance", p = { Time = "float", } }, KeyframeMarker = { s = "Instance", p = { Value = "string", } }, KeyframeSequence = { s = "AnimationClip", p = { } }, KeyframeSequenceProvider = { s = "Instance", p = { } }, LanguageService = { s = "Instance", p = { } }, LayerCollector = { s = "GuiBase2d", p = { Enabled = "bool", ResetOnSpawn = "bool", ZIndexBehavior = "ZIndexBehavior", } }, LegacyStudioBridge = { s = "ILegacyStudioBridge", p = { } }, Light = { s = "Instance", p = { Brightness = "float", Color = "Color3", Enabled = "bool", Shadows = "bool", } }, Lighting = { s = "Instance", p = { Ambient = "Color3", Brightness = "float", ClockTime = "float", ColorShift_Bottom = "Color3", ColorShift_Top = "Color3", EnvironmentDiffuseScale = "float", EnvironmentSpecularScale = "float", ExposureCompensation = "float", FogColor = "Color3", FogEnd = "float", FogStart = "float", GeographicLatitude = "float", GlobalShadows = "bool", LightingStyle = "LightingStyle", OutdoorAmbient = "Color3", PrioritizeLightingQuality = "bool", ShadowSoftness = "float", Technology = "Technology", TimeOfDay = "string", } }, LineForce = { s = "Constraint", p = { ApplyAtCenterOfMass = "bool", InverseSquareLaw = "bool", Magnitude = "float", MaxForce = "float", ReactionForceEnabled = "bool", } }, LineHandleAdornment = { s = "HandleAdornment", p = { Length = "float", Thickness = "float", } }, LinearVelocity = { s = "Constraint", p = { ForceLimitMode = "ForceLimitMode", ForceLimitsEnabled = "bool", LineDirection = "Vector3", LineVelocity = "float", MaxAxesForce = "Vector3", MaxForce = "float", MaxPlanarAxesForce = "Vector2", PlaneVelocity = "Vector2", PrimaryTangentAxis = "Vector3", ReactionForceEnabled = "bool", RelativeTo = "ActuatorRelativeTo", SecondaryTangentAxis = "Vector3", VectorVelocity = "Vector3", VelocityConstraintMode = "VelocityConstraintMode", } }, LinkingService = { s = "Instance", p = { } }, LiveScriptingService = { s = "Instance", p = { } }, LiveSyncService = { s = "Instance", p = { } }, LocalDebuggerConnection = { s = "DebuggerConnection", p = { } }, LocalScript = { s = "Script", p = { } }, LocalStorageService = { s = "Instance", p = { } }, LocalizationService = { s = "Instance", p = { } }, LocalizationTable = { s = "Instance", p = { SourceLocaleId = "string", } }, LodDataEntity = { s = "Instance", p = { } }, LodDataService = { s = "Instance", p = { } }, LogReporterService = { s = "Instance", p = { } }, LogService = { s = "Instance", p = { } }, Logger = { s = "Object", p = { } }, LoginService = { s = "Instance", p = { } }, LuaSettings = { s = "Instance", p = { } }, LuaSourceContainer = { s = "Instance", p = { } }, LuaWebService = { s = "Instance", p = { } }, LuauExpression = { s = "Object", p = { } }, LuauExpressionService = { s = "Instance", p = { } }, LuauScriptAnalyzerService = { s = "Instance", p = { } }, MLModelDeliveryService = { s = "Instance", p = { } }, MLService = { s = "Instance", p = { } }, MLSession = { s = "Object", p = { } }, MakeupDescription = { s = "Instance", p = { AssetId = "int64", Instance = "Instance", MakeupType = "MakeupType", Order = "int", } }, ManualGlue = { s = "ManualSurfaceJointInstance", p = { } }, ManualSurfaceJointInstance = { s = "JointInstance", p = { } }, ManualWeld = { s = "ManualSurfaceJointInstance", p = { } }, MarkerCurve = { s = "Instance", p = { } }, MarketplaceService = { s = "Instance", p = { } }, MatchmakingService = { s = "Instance", p = { } }, MaterialGenerationService = { s = "Instance", p = { } }, MaterialImportData = { s = "BaseImportData", p = { DiffuseFilePath = "string", DiffuseVersionedAssetId = "int64", EmissiveFilePath = "string", EmissiveVersionedAssetId = "int64", MetalnessFilePath = "string", MetalnessVersionedAssetId = "int64", NormalFilePath = "string", NormalVersionedAssetId = "int64", RoughnessFilePath = "string", RoughnessVersionedAssetId = "int64", } }, MaterialService = { s = "Instance", p = { Use2022Materials = "bool", } }, MaterialVariant = { s = "Instance", p = { BaseMaterial = "Material", ColorMap = "ContentId", ColorMapContent = "Content", CustomPhysicalProperties = "PhysicalProperties", EmissiveMaskContent = "Content", EmissiveStrength = "float", EmissiveTint = "Color3", MaterialPattern = "MaterialPattern", MetalnessMap = "ContentId", MetalnessMapContent = "Content", NormalMap = "ContentId", NormalMapContent = "Content", RoughnessMap = "ContentId", RoughnessMapContent = "Content", StudsPerTile = "float", } }, MemStorageConnection = { s = "Instance", p = { } }, MemStorageService = { s = "Instance", p = { } }, MemoryStoreDistributedCounter = { s = "Instance", p = { } }, MemoryStoreHashMap = { s = "Instance", p = { } }, MemoryStoreHashMapPages = { s = "Pages", p = { } }, MemoryStoreQueue = { s = "Instance", p = { } }, MemoryStoreService = { s = "Instance", p = { } }, MemoryStoreSortedMap = { s = "Instance", p = { } }, MeshContentProvider = { s = "CacheableContentProvider", p = { } }, MeshImportData = { s = "BaseImportData", p = { Anchored = "bool", CageMeshIntersectedPreview = "bool", CageNonManifoldPreview = "bool", CageOverlappingVerticesPreview = "bool", CageUVMisMatchedPreview = "bool", DoubleSided = "bool", IgnoreVertexColors = "bool", IrrelevantCageModifiedPreview = "bool", MeshHoleDetectedPreview = "bool", OuterCageFarExtendedFromMeshPreview = "bool", UseImportedPivot = "bool", VersionedAssetId = "int64", } }, MeshPart = { s = "TriangleMeshPart", p = { DoubleSided = "bool", MeshContent = "Content", MeshId = "ContentId", RenderFidelity = "RenderFidelity", TextureContent = "Content", TextureID = "ContentId", } }, Message = { s = "Instance", p = { Text = "string", } }, MessageBusConnection = { s = "Instance", p = { } }, MessageBusService = { s = "Instance", p = { } }, MessagingService = { s = "Instance", p = { } }, MetaBreakpoint = { s = "Instance", p = { } }, MetaBreakpointContext = { s = "Instance", p = { } }, MetaBreakpointManager = { s = "Instance", p = { } }, MicroProfilerService = { s = "Instance", p = { ContextLabel = "string", } }, Model = { s = "PVInstance", p = { LevelOfDetail = "ModelLevelOfDetail", ModelStreamingMode = "ModelStreamingMode", PrimaryPart = "BasePart", WorldPivot = "CFrame", } }, ModerationService = { s = "Instance", p = { } }, ModuleScript = { s = "LuaSourceContainer", p = { Source = "ProtectedString", } }, MomentsService = { s = "Instance", p = { } }, Motor = { s = "JointInstance", p = { CurrentAngle = "float", DesiredAngle = "float", MaxVelocity = "float", } }, Motor6D = { s = "Motor", p = { } }, MotorFeature = { s = "Feature", p = { } }, Mouse = { s = "Instance", p = { Icon = "ContentId", IconContent = "Content", TargetFilter = "Instance", } }, MouseService = { s = "Instance", p = { } }, MultipleDocumentInterfaceInstance = { s = "Instance", p = { } }, NegateOperation = { s = "PartOperation", p = { } }, NetworkClient = { s = "NetworkPeer", p = { } }, NetworkMarker = { s = "Instance", p = { } }, NetworkPeer = { s = "Instance", p = { } }, NetworkReplicator = { s = "Instance", p = { } }, NetworkServer = { s = "NetworkPeer", p = { } }, NetworkSettings = { s = "Instance", p = { HttpProxyEnabled = "bool", HttpProxyURL = "string", InboundNetworkJitterMs = "float", InboundNetworkLossPercent = "float", InboundNetworkMinDelayMs = "float", IncomingReplicationLag = "double", OutboundNetworkJitterMs = "float", OutboundNetworkLossPercent = "float", OutboundNetworkMinDelayMs = "float", PrintJoinSizeBreakdown = "bool", PrintPhysicsErrors = "bool", PrintStreamInstanceQuota = "bool", RandomizeJoinInstanceOrder = "bool", RenderStreamedRegions = "bool", ShowActiveAnimationAsset = "bool", } }, NoCollisionConstraint = { s = "Instance", p = { Enabled = "bool", Part0 = "BasePart", Part1 = "BasePart", } }, Noise = { s = "Instance", p = { NoiseType = "NoiseType", Seed = "int", } }, NonReplicatedCSGDictionaryService = { s = "FlyweightService", p = { } }, NotificationService = { s = "Instance", p = { SelectedTheme = "string", } }, NumberPose = { s = "PoseBase", p = { Value = "double", } }, NumberValue = { s = "ValueBase", p = { Value = "double", } }, Object = { p = { } }, ObjectValue = { s = "ValueBase", p = { Value = "Instance", } }, OmniRecommendationsService = { s = "Instance", p = { } }, OpenCloudApiV1 = { s = "Instance", p = { } }, OpenCloudService = { s = "Instance", p = { } }, OperationGraph = { s = "Instance", p = { } }, OrderedDataStore = { s = "GlobalDataStore", p = { } }, OutfitPages = { s = "Pages", p = { } }, OutputLink = { s = "Object", p = { } }, PVAdornment = { s = "GuiBase3d", p = { Adornee = "PVInstance", } }, PVInstance = { s = "Instance", p = { } }, PackageLink = { s = "Instance", p = { AutoUpdate = "bool", DefaultName = "string", SerializedDefaultAttributes = "BinaryString", VersionNumber = "int64", } }, PackageService = { s = "Instance", p = { } }, PackageUIService = { s = "Instance", p = { } }, Packages = { s = "Instance", p = { } }, Pages = { s = "Instance", p = { } }, Pants = { s = "Clothing", p = { PantsTemplate = "ContentId", PantsTemplateContent = "Content", } }, ParabolaAdornment = { s = "PVAdornment", p = { } }, Part = { s = "FormFactorPart", p = { Shape = "PartType", } }, PartAdornment = { s = "GuiBase3d", p = { Adornee = "BasePart", } }, PartOperation = { s = "TriangleMeshPart", p = { RenderFidelity = "RenderFidelity", SmoothingAngle = "float", TriangleCount = "int", UsePartColor = "bool", } }, PartOperationAsset = { s = "Instance", p = { } }, ParticleEmitter = { s = "Instance", p = { Acceleration = "Vector3", Brightness = "float", Color = "ColorSequence", Drag = "float", EmissionDirection = "NormalId", Enabled = "bool", FlipbookBlendFrames = "bool", FlipbookFramerate = "NumberRange", FlipbookIncompatible = "string", FlipbookLayout = "ParticleFlipbookLayout", FlipbookMode = "ParticleFlipbookMode", FlipbookSizeX = "int", FlipbookSizeY = "int", FlipbookStartRandom = "bool", Lifetime = "NumberRange", LightEmission = "float", LightInfluence = "float", LockedToPart = "bool", Orientation = "ParticleOrientation", Rate = "float", RotSpeed = "NumberRange", Rotation = "NumberRange", Shape = "ParticleEmitterShape", ShapeInOut = "ParticleEmitterShapeInOut", ShapePartial = "float", ShapeStyle = "ParticleEmitterShapeStyle", Size = "NumberSequence", Speed = "NumberRange", SpreadAngle = "Vector2", Squash = "NumberSequence", Texture = "ContentId", TextureContent = "Content", TimeScale = "float", Transparency = "NumberSequence", VelocityInheritance = "float", WindAffectsDrag = "bool", ZOffset = "float", } }, ParticleEmitter2D = { s = "Instance", p = { Acceleration = "Vector2", Blend = "float", Brightness = "float", Color = "ColorSequence", Drag = "float", EmissionAngle = "float", EmissionMode = "ParticleEmitter2DEmissionMode", EmissionOffset = "Vector2", EmissionSpread = "Vector2", Enabled = "bool", FlipbookBlendFrames = "bool", FlipbookFramerate = "NumberRange", FlipbookLayout = "ParticleFlipbookLayout", FlipbookMode = "ParticleFlipbookMode", FlipbookSizeX = "int", FlipbookSizeY = "int", FlipbookStartRandom = "bool", Lifetime = "NumberRange", LightEmission = "float", LockedToParent = "bool", Rate = "float", RotSpeed = "NumberRange", Rotation = "NumberRange", Size = "NumberSequence", Speed = "NumberRange", SpreadAngle = "float", Squash = "NumberSequence", TextureContent = "Content", TimeScale = "float", Transparency = "NumberSequence", ZIndex = "int", } }, PartyEmulatorService = { s = "Instance", p = { } }, PatchBundlerFileWatch = { s = "Instance", p = { } }, PatchMapping = { s = "Instance", p = { FlattenTree = "bool", PatchId = "string", TargetPath = "string", } }, Path = { s = "Instance", p = { } }, Path2D = { s = "GuiBase", p = { Closed = "bool", Color3 = "Color3", SelectedControlPoint = "int", SelectedControlPointData = "Path2DControlPoint", Thickness = "float", Transparency = "float", Visible = "bool", ZIndex = "int", } }, Path3D = { s = "Instance", p = { } }, PathfindingLink = { s = "Instance", p = { Attachment0 = "Attachment", Attachment1 = "Attachment", IsBidirectional = "bool", Label = "string", } }, PathfindingModifier = { s = "Instance", p = { Label = "string", PassThrough = "bool", } }, PathfindingService = { s = "Instance", p = { } }, PausedState = { s = "Instance", p = { } }, PausedStateBreakpoint = { s = "PausedState", p = { } }, PausedStateException = { s = "PausedState", p = { } }, PerformanceControlService = { s = "Instance", p = { } }, PermissionsService = { s = "Instance", p = { } }, PhysicsActivitySensor = { s = "SensorBase", p = { Position = "Vector3", Size = "Vector3", } }, PhysicsService = { s = "Instance", p = { } }, PhysicsSettings = { s = "Instance", p = { AllowSleep = "bool", AreAnchorsShown = "bool", AreAssembliesShown = "bool", AreAssemblyCentersOfMassShown = "bool", AreAwakePartsHighlighted = "bool", AreBodyTypesShown = "bool", AreCollisionCostsShown = "bool", AreConstraintForcesShownForSelectedOrHoveredInstances = "bool", AreConstraintTorquesShownForSelectedOrHoveredInstances = "bool", AreContactForcesShownForSelectedOrHoveredAssemblies = "bool", AreContactIslandsShown = "bool", AreContactPointsShown = "bool", AreGravityForcesShownForSelectedOrHoveredAssemblies = "bool", AreJointCoordinatesShown = "bool", AreMagnitudesShownForDrawnForcesAndTorques = "bool", AreMechanismsShown = "bool", AreModelCoordsShown = "bool", AreNonAnchorsShown = "bool", AreOwnersShown = "bool", ArePartCoordsShown = "bool", AreRegionsShown = "bool", AreSolverIslandsShown = "bool", AreTerrainReplicationRegionsShown = "bool", AreTimestepsShown = "bool", AreUnalignedPartsShown = "bool", AreWorldCoordsShown = "bool", CollisionGeomDrawOriginalParts = "bool", CollisionGeomMatchPartTransparency = "bool", CollisionGeomOverlayTransparency = "float", CollisionGeomShowCollidableParts = "bool", CollisionGeomShowCollisionGroup = "string", CollisionGeomShowQueryableParts = "bool", CollisionGeomShowTouchableParts = "bool", DisableCSGv2 = "bool", DisableCSGv3ForPlugins = "bool", DrawConstraintsNetForce = "bool", DrawContactsNetForce = "bool", DrawShapecast = "bool", DrawTotalNetForce = "bool", EnableForceVisualizationSmoothing = "bool", FluidForceDrawScale = "float", ForceDrawScale = "float", ForceVisualizationSmoothingSteps = "int", IsInterpolationThrottleShown = "bool", IsReceiveAgeShown = "bool", IsTreeShown = "bool", PhysicsEnvironmentalThrottle = "EnviromentalPhysicsThrottle", ShowDecompositionGeometry = "bool", ShowFluidForcesForSelectedOrHoveredMechanisms = "bool", ShowInstanceNamesForDrawnForcesAndTorques = "bool", SolverConvergenceMetricType = "SolverConvergenceMetricType", SolverConvergenceVisualizationMode = "SolverConvergenceVisualizationMode", ThrottleAdjustTime = "double", TorqueDrawScale = "float", UseCSGv2 = "bool", } }, PinShortcutService = { s = "Instance", p = { } }, PitchShiftSoundEffect = { s = "SoundEffect", p = { Octave = "float", } }, PlaceAssetIdsService = { s = "Instance", p = { } }, PlaceStatsService = { s = "Instance", p = { } }, PlacesService = { s = "Instance", p = { } }, Plane = { s = "PlaneConstraint", p = { } }, PlaneConstraint = { s = "Constraint", p = { } }, Platform = { s = "Part", p = { } }, PlatformCloudStorageService = { s = "Instance", p = { } }, PlatformFriendsService = { s = "Instance", p = { } }, PlatformLibraries = { s = "Instance", p = { } }, Player = { s = "Instance", p = { AutoJumpEnabled = "bool", CameraMaxZoomDistance = "float", CameraMinZoomDistance = "float", CameraMode = "CameraMode", CanLoadCharacterAppearance = "bool", Character = "Model", CharacterAppearanceId = "int64", DevCameraOcclusionMode = "DevCameraOcclusionMode", DevComputerCameraMode = "DevComputerCameraMovementMode", DevComputerMovementMode = "DevComputerMovementMode", DevEnableMouseLock = "bool", DevTouchCameraMode = "DevTouchCameraMovementMode", DevTouchMovementMode = "DevTouchMovementMode", HealthDisplayDistance = "float", InputLatency = "int", NameDisplayDistance = "float", Neutral = "bool", ReplicationFocus = "Instance", RespawnLocation = "SpawnLocation", StepIdOffset = "int", Team = "Team", TeamColor = "BrickColor", } }, PlayerData = { s = "Instance", p = { } }, PlayerDataRecord = { s = "Instance", p = { } }, PlayerDataRecordConfig = { s = "Instance", p = { } }, PlayerDataService = { s = "Instance", p = { LoadFailureBehavior = "PlayerDataLoadFailureBehavior", } }, PlayerEmulatorService = { s = "Instance", p = { } }, PlayerGui = { s = "BasePlayerGui", p = { ScreenOrientation = "ScreenOrientation", SelectionImageObject = "GuiObject", } }, PlayerHydrationService = { s = "Instance", p = { } }, PlayerListConfiguration = { s = "BaseCoreGuiConfiguration", p = { Open = "bool", } }, PlayerMouse = { s = "Mouse", p = { } }, PlayerScripts = { s = "Instance", p = { } }, PlayerViewService = { s = "Instance", p = { } }, Players = { s = "Instance", p = { CharacterAutoLoads = "bool", RespawnTime = "float", } }, Plugin = { s = "Instance", p = { DisableUIDragDetectorDrags = "bool", IsDebuggable = "bool", } }, PluginAction = { s = "Instance", p = { Text = "string", } }, PluginCapabilities = { s = "Instance", p = { Manifest = "string", } }, PluginConnection = { s = "Object", p = { } }, PluginConnectionService = { s = "Instance", p = { } }, PluginDebugService = { s = "Instance", p = { } }, PluginDragEvent = { s = "Instance", p = { } }, PluginGui = { s = "LayerCollector", p = { Title = "string", } }, PluginGuiService = { s = "Instance", p = { } }, PluginManagementService = { s = "Instance", p = { } }, PluginManager = { s = "Instance", p = { } }, PluginManagerInterface = { s = "Instance", p = { } }, PluginMenu = { s = "Instance", p = { Icon = "string", Title = "string", Visible = "bool", } }, PluginMouse = { s = "Mouse", p = { } }, PluginPolicyService = { s = "Instance", p = { } }, PluginToolbar = { s = "Instance", p = { } }, PluginToolbarButton = { s = "Instance", p = { ClickableWhenViewportHidden = "bool", Enabled = "bool", Icon = "ContentId", IconContent = "Content", } }, PointLight = { s = "Light", p = { Range = "float", } }, PointsService = { s = "Instance", p = { } }, PolicyService = { s = "Instance", p = { } }, PopLatencyService = { s = "Instance", p = { } }, Pose = { s = "PoseBase", p = { CFrame = "CFrame", } }, PoseBase = { s = "Instance", p = { EasingDirection = "PoseEasingDirection", EasingStyle = "PoseEasingStyle", Weight = "float", } }, PostEffect = { s = "Instance", p = { Enabled = "bool", } }, Preloaded = { s = "Instance", p = { } }, PrismaticConstraint = { s = "SlidingBallConstraint", p = { } }, ProceduralBehaviorSchedulerService = { s = "Instance", p = { } }, ProceduralModel = { s = "Model", p = { Generator = "ModuleScript", Size = "Vector3", } }, ProcessInstancePhysicsService = { s = "Instance", p = { } }, ProjectService = { s = "Instance", p = { } }, ProximityPrompt = { s = "Instance", p = { ActionText = "string", AutoLocalize = "bool", ClickablePrompt = "bool", Enabled = "bool", Exclusivity = "ProximityPromptExclusivity", GamepadKeyCode = "KeyCode", HoldDuration = "float", KeyboardKeyCode = "KeyCode", MaxActivationDistance = "float", MaxIndicatorDistance = "float", ObjectText = "string", RequiresLineOfSight = "bool", RootLocalizationTable = "LocalizationTable", Style = "ProximityPromptStyle", UIOffset = "Vector2", } }, ProximityPromptService = { s = "Instance", p = { Enabled = "bool", MaxIndicatorsVisible = "int", MaxPromptsVisible = "int", } }, PublishService = { s = "Instance", p = { } }, PyramidHandleAdornment = { s = "HandleAdornment", p = { Height = "float", Shading = "AdornShading", Sides = "int", Size = "float", } }, QWidgetPluginGui = { s = "PluginGui", p = { } }, QueueService = { s = "Instance", p = { } }, RTAnimationTracker = { s = "Instance", p = { } }, RayValue = { s = "ValueBase", p = { Value = "Ray", } }, RbxAnalyticsService = { s = "Instance", p = { } }, RealtimeMedia = { s = "Instance", p = { ForwardInput = "bool", } }, RecommendationPages = { s = "Pages", p = { } }, RecommendationService = { s = "Instance", p = { } }, ReflectionMetadata = { s = "Instance", p = { } }, ReflectionMetadataCallbacks = { s = "Instance", p = { } }, ReflectionMetadataClass = { s = "ReflectionMetadataItem", p = { ExplorerImageIndex = "int", ExplorerOrder = "int", Insertable = "bool", PreferredParent = "string", } }, ReflectionMetadataClasses = { s = "Instance", p = { } }, ReflectionMetadataEnum = { s = "ReflectionMetadataItem", p = { } }, ReflectionMetadataEnumItem = { s = "ReflectionMetadataItem", p = { } }, ReflectionMetadataEnums = { s = "Instance", p = { } }, ReflectionMetadataEvents = { s = "Instance", p = { } }, ReflectionMetadataFunctions = { s = "Instance", p = { } }, ReflectionMetadataItem = { s = "Instance", p = { Browsable = "bool", ClassCategory = "string", ClientOnly = "bool", Constraint = "string", Deprecated = "bool", EditingDisabled = "bool", EditorType = "string", FFlag = "string", IsBackend = "bool", PropertyOrder = "int", ScriptContext = "string", ServerOnly = "bool", SliderScaling = "string", UIMaximum = "double", UIMinimum = "double", UINumTicks = "double", } }, ReflectionMetadataMember = { s = "ReflectionMetadataItem", p = { } }, ReflectionMetadataProperties = { s = "Instance", p = { } }, ReflectionMetadataYieldFunctions = { s = "Instance", p = { } }, ReflectionService = { s = "Instance", p = { } }, RelativeGui = { s = "GuiObject", p = { } }, RemoteCommandService = { s = "Instance", p = { } }, RemoteCursorService = { s = "Instance", p = { } }, RemoteDebuggerServer = { s = "Instance", p = { } }, RemoteEvent = { s = "BaseRemoteEvent", p = { } }, RemoteFunction = { s = "Instance", p = { } }, RenderSettings = { s = "Instance", p = { AutoFRMLevel = "int", EagerBulkExecution = "bool", EditQualityLevel = "QualityLevel", ["Enable VR Mode"] = "bool", ExportMergeByMaterial = "bool", FrameRateManager = "FramerateManagerMode", GraphicsMode = "GraphicsMode", MeshCacheSize = "int", MeshPartDetailLevel = "MeshPartDetailLevel", QualityLevel = "QualityLevel", ReloadAssets = "bool", RenderCSGTrianglesDebug = "bool", ShowBoundingBoxes = "bool", ViewMode = "ViewMode", } }, RenderingTest = { s = "Instance", p = { CFrame = "CFrame", ComparisonDiffThreshold = "int", ComparisonMethod = "RenderingTestComparisonMethod", ComparisonPsnrThreshold = "float", Description = "string", FieldOfView = "float", PerfTest = "bool", QualityAuto = "bool", QualityLevel = "int", RenderingTestFrameCount = "int", ShouldSkip = "bool", Ticket = "string", Timeout = "int", } }, ReplicatedFirst = { s = "Instance", p = { } }, ReplicatedStorage = { s = "Instance", p = { } }, RequestOrchestratorService = { s = "Instance", p = { } }, ReverbSoundEffect = { s = "SoundEffect", p = { DecayTime = "float", Density = "float", Diffusion = "float", DryLevel = "float", WetLevel = "float", } }, RibbonNotificationService = { s = "Instance", p = { } }, RigidConstraint = { s = "Constraint", p = { } }, RobloxPluginGuiService = { s = "Instance", p = { } }, RobloxReplicatedStorage = { s = "Instance", p = { } }, RobloxSerializableInstance = { s = "Instance", p = { Data = "BinaryString", } }, RobloxServerStorage = { s = "Instance", p = { } }, RocketPropulsion = { s = "BodyMover", p = { CartoonFactor = "float", MaxSpeed = "float", MaxThrust = "float", MaxTorque = "Vector3", Target = "BasePart", TargetOffset = "Vector3", TargetRadius = "float", ThrustD = "float", ThrustP = "float", TurnD = "float", TurnP = "float", } }, RodConstraint = { s = "Constraint", p = { Length = "float", LimitAngle0 = "float", LimitAngle1 = "float", LimitsEnabled = "bool", Thickness = "float", } }, RolloutValidation = { s = "Instance", p = { } }, RolloutValidationService = { s = "Instance", p = { } }, RomarkRbxAnalyticsService = { s = "Instance", p = { } }, RomarkService = { s = "Instance", p = { } }, RootImportData = { s = "BaseImportData", p = { AddModelToInventory = "bool", Anchored = "bool", AnimationIdForRestPose = "float", ExistingPackageId = "string", ImportAsModelAsset = "bool", ImportAsPackage = "bool", InsertInWorkspace = "bool", InsertWithScenePosition = "bool", InvertNegativeFaces = "bool", KeepZeroInfluenceBones = "bool", MergeMeshes = "bool", PhysicalConstraintType = "PhysicalConstraintType", PreferredUploadId = "int64", RestPose = "RestPose", RigScale = "RigScale", RigType = "RigType", RigVisualization = "bool", ScaleFactor = "float", ScaleUnit = "MeshScaleUnit", UseSceneOriginAsPivot = "bool", UsesCages = "bool", VersionedAssetId = "int64", WorldForward = "NormalId", WorldUp = "NormalId", } }, RopeConstraint = { s = "Constraint", p = { Length = "float", Restitution = "float", Thickness = "float", WinchEnabled = "bool", WinchForce = "float", WinchResponsiveness = "float", WinchSpeed = "float", WinchTarget = "float", } }, Rotate = { s = "JointInstance", p = { } }, RotateP = { s = "DynamicRotate", p = { } }, RotateV = { s = "DynamicRotate", p = { } }, RotationCurve = { s = "Instance", p = { } }, RtMessagingService = { s = "Instance", p = { } }, RunService = { s = "Instance", p = { RunState = "RunState", } }, RunningAverageItemDouble = { s = "StatsItem", p = { } }, RunningAverageItemInt = { s = "StatsItem", p = { } }, RunningAverageTimeIntervalItem = { s = "StatsItem", p = { } }, RuntimeContentService = { s = "Instance", p = { } }, RuntimeScriptService = { s = "Instance", p = { } }, SafetyService = { s = "Instance", p = { IsCaptureModeForReport = "bool", } }, SceneAnalysisService = { s = "Instance", p = { } }, ScreenGui = { s = "LayerCollector", p = { ClipToDeviceSafeArea = "bool", DisplayOrder = "int", IgnoreGuiInset = "bool", SafeAreaCompatibility = "SafeAreaCompatibility", ScreenInsets = "ScreenInsets", } }, ScreenshotCapture = { s = "Capture", p = { } }, ScreenshotHud = { s = "Instance", p = { CameraButtonIcon = "ContentId", CameraButtonIconContent = "Content", CameraButtonPosition = "UDim2", CloseButtonPosition = "UDim2", CloseWhenScreenshotTaken = "bool", HideCoreGuiForCaptures = "bool", HidePlayerGuiForCaptures = "bool", Visible = "bool", } }, Script = { s = "BaseScript", p = { Source = "ProtectedString", } }, ScriptBuilder = { s = "Instance", p = { } }, ScriptChangeService = { s = "Instance", p = { } }, ScriptCloneWatcher = { s = "Instance", p = { } }, ScriptCloneWatcherHelper = { s = "Instance", p = { } }, ScriptCommitService = { s = "Instance", p = { } }, ScriptContext = { s = "Instance", p = { } }, ScriptDebugger = { s = "Instance", p = { } }, ScriptDebuggerService = { s = "Instance", p = { } }, ScriptDocument = { s = "Instance", p = { } }, ScriptEditorService = { s = "Instance", p = { } }, ScriptProfilerService = { s = "Instance", p = { } }, ScriptRegistrationService = { s = "Instance", p = { } }, ScriptRuntime = { s = "Instance", p = { } }, ScriptScannerService = { s = "Instance", p = { } }, ScriptService = { s = "Instance", p = { } }, ScrollingFrame = { s = "GuiObject", p = { AutomaticCanvasSize = "AutomaticSize", BottomImage = "ContentId", BottomImageContent = "Content", CanvasPosition = "Vector2", CanvasSize = "UDim2", ElasticBehavior = "ElasticBehavior", HorizontalScrollBarInset = "ScrollBarInset", MidImage = "ContentId", MidImageContent = "Content", ScrollBarImageColor3 = "Color3", ScrollBarImageTransparency = "float", ScrollBarThickness = "int", ScrollingDirection = "ScrollingDirection", ScrollingEnabled = "bool", TopImage = "ContentId", TopImageContent = "Content", VerticalScrollBarInset = "ScrollBarInset", VerticalScrollBarPosition = "VerticalScrollBarPosition", } }, Seat = { s = "Part", p = { Disabled = "bool", } }, Selection = { s = "Instance", p = { } }, SelectionBox = { s = "InstanceAdornment", p = { LineThickness = "float", SurfaceColor3 = "Color3", SurfaceTransparency = "float", } }, SelectionHighlightManager = { s = "Instance", p = { } }, SelectionLasso = { s = "GuiBase3d", p = { Humanoid = "Humanoid", } }, SelectionPartLasso = { s = "SelectionLasso", p = { Part = "BasePart", } }, SelectionPointLasso = { s = "SelectionLasso", p = { Point = "Vector3", } }, SelectionSphere = { s = "PVAdornment", p = { SurfaceColor3 = "Color3", SurfaceTransparency = "float", } }, SelfViewConfiguration = { s = "BaseCoreGuiConfiguration", p = { Open = "bool", } }, SensorBase = { s = "Instance", p = { UpdateType = "SensorUpdateType", } }, SerializationService = { s = "Instance", p = { } }, ServerReplicator = { s = "NetworkReplicator", p = { } }, ServerScriptService = { s = "Instance", p = { } }, ServerStorage = { s = "Instance", p = { } }, ServiceProvider = { s = "Instance", p = { } }, ServiceVisibilityService = { s = "Instance", p = { HiddenServices = "BinaryString", VisibleServices = "BinaryString", } }, SessionCheckService = { s = "Instance", p = { } }, SessionService = { s = "Instance", p = { } }, SharedTableRegistry = { s = "Instance", p = { } }, Shirt = { s = "Clothing", p = { ShirtTemplate = "ContentId", ShirtTemplateContent = "Content", } }, ShirtGraphic = { s = "CharacterAppearance", p = { Color3 = "Color3", Graphic = "ContentId", TextureContent = "Content", } }, SkateboardController = { s = "Controller", p = { } }, SkateboardPlatform = { s = "Part", p = { Steer = "int", StickyWheels = "bool", Throttle = "int", } }, Skin = { s = "CharacterAppearance", p = { SkinColor = "BrickColor", } }, Sky = { s = "Instance", p = { CelestialBodiesShown = "bool", MoonAngularSize = "float", MoonTextureContent = "Content", MoonTextureId = "ContentId", SkyboxBackContent = "Content", SkyboxBk = "ContentId", SkyboxDn = "ContentId", SkyboxDownContent = "Content", SkyboxFrontContent = "Content", SkyboxFt = "ContentId", SkyboxLeftContent = "Content", SkyboxLf = "ContentId", SkyboxOrientation = "Vector3", SkyboxRightContent = "Content", SkyboxRt = "ContentId", SkyboxUp = "ContentId", SkyboxUpContent = "Content", StarCount = "int", SunAngularSize = "float", SunTextureContent = "Content", SunTextureId = "ContentId", } }, SlidingBallConstraint = { s = "Constraint", p = { ActuatorType = "ActuatorType", LimitsEnabled = "bool", LinearResponsiveness = "float", LowerLimit = "float", MotorMaxAcceleration = "float", MotorMaxForce = "float", Restitution = "float", ServoMaxForce = "float", Size = "float", Speed = "float", TargetPosition = "float", UpperLimit = "float", Velocity = "float", } }, SlimAnimationDataEntity = { s = "Instance", p = { } }, SlimAnimationReplicationService = { s = "Instance", p = { } }, SlimContentProvider = { s = "CacheableContentProvider", p = { } }, SlimDebugSettings = { s = "Instance", p = { } }, SlimReplicationService = { s = "Instance", p = { } }, SlimService = { s = "Instance", p = { } }, Smoke = { s = "Instance", p = { Color = "Color3", Enabled = "bool", Opacity = "float", RiseVelocity = "float", Size = "float", TimeScale = "float", } }, SmoothVoxelsUpgraderService = { s = "Instance", p = { } }, Snap = { s = "JointInstance", p = { } }, SocialService = { s = "Instance", p = { } }, SolidModelContentProvider = { s = "CacheableContentProvider", p = { } }, Sound = { s = "Instance", p = { AcousticSimulationEnabled = "bool", LoopRegion = "NumberRange", Looped = "bool", PlayOnRemove = "bool", PlaybackRegion = "NumberRange", PlaybackRegionsEnabled = "bool", PlaybackSpeed = "float", Playing = "bool", RollOffMaxDistance = "float", RollOffMinDistance = "float", RollOffMode = "RollOffMode", SoundGroup = "SoundGroup", SoundId = "ContentId", TimePosition = "double", Volume = "float", } }, SoundEffect = { s = "Instance", p = { Enabled = "bool", Priority = "int", } }, SoundGroup = { s = "Instance", p = { Volume = "float", } }, SoundService = { s = "Instance", p = { AcousticSimulationEnabled = "bool", AmbientReverb = "ReverbType", CharacterSoundsUseNewApi = "RolloutState", DefaultListenerLocation = "ListenerLocation", DiffractionEnabled = "bool", DistanceFactor = "float", DopplerScale = "float", ListenerCFrame = "CFrame", ListenerObject = "Instance", ListenerType = "ListenerType", OcclusionEnabled = "bool", RespectFilteringEnabled = "bool", ReverbEnabled = "bool", RolloffScale = "float", } }, SoundShimService = { s = "Instance", p = { } }, Sparkles = { s = "Instance", p = { Enabled = "bool", SparkleColor = "Color3", TimeScale = "float", } }, SpawnLocation = { s = "Part", p = { AllowTeamChangeOnTouch = "bool", Duration = "int", Enabled = "bool", Neutral = "bool", TeamColor = "BrickColor", } }, SpawnerService = { s = "Instance", p = { } }, SpecialMesh = { s = "FileMesh", p = { MeshType = "MeshType", } }, SphereHandleAdornment = { s = "HandleAdornment", p = { Radius = "float", Shading = "AdornShading", } }, SpotLight = { s = "Light", p = { Angle = "float", Face = "NormalId", Range = "float", } }, SpringConstraint = { s = "Constraint", p = { Coils = "float", Damping = "float", FreeLength = "float", LimitsEnabled = "bool", MaxForce = "float", MaxLength = "float", MinLength = "float", Radius = "float", Stiffness = "float", Thickness = "float", } }, StackFrame = { s = "Instance", p = { } }, StandalonePluginScripts = { s = "Instance", p = { } }, StandardPages = { s = "Pages", p = { } }, StandardQueue = { s = "Instance", p = { } }, StartPageService = { s = "Instance", p = { } }, StarterCharacterScripts = { s = "StarterPlayerScripts", p = { } }, StarterGear = { s = "Instance", p = { } }, StarterGui = { s = "BasePlayerGui", p = { ScreenOrientation = "ScreenOrientation", ShowDevelopmentGui = "bool", } }, StarterPack = { s = "Instance", p = { } }, StarterPlayer = { s = "Instance", p = { AutoJumpEnabled = "bool", AvatarJointUpgrade = "RolloutState", CameraMaxZoomDistance = "float", CameraMinZoomDistance = "float", CameraMode = "CameraMode", CharacterBreakJointsOnDeath = "bool", CharacterJumpHeight = "float", CharacterJumpPower = "float", CharacterMaxSlopeAngle = "float", CharacterUseJumpPower = "bool", CharacterWalkSpeed = "float", ClassicDeath = "bool", CreateDefaultPlayerModule = "bool", DevCameraOcclusionMode = "DevCameraOcclusionMode", DevComputerCameraMovementMode = "DevComputerCameraMovementMode", DevComputerMovementMode = "DevComputerMovementMode", DevTouchCameraMovementMode = "DevTouchCameraMovementMode", DevTouchMovementMode = "DevTouchMovementMode", EnableMouseLockOption = "bool", HealthDisplayDistance = "float", LoadCharacterAppearance = "bool", LuaCharacterController = "CharacterControlMode", NameDisplayDistance = "float", UserEmotesEnabled = "bool", } }, StarterPlayerScripts = { s = "Instance", p = { } }, StartupMessageService = { s = "Instance", p = { } }, StateMachineDefinition = { s = "Instance", p = { NodeId = "string", } }, StateMachineTransitionDefinition = { s = "Instance", p = { From = "Instance", Priority = "int", To = "Instance", TransitionId = "string", } }, Stats = { s = "Instance", p = { } }, StatsItem = { s = "Instance", p = { } }, Status = { s = "Model", p = { } }, StopWatchReporter = { s = "Instance", p = { } }, StringValue = { s = "ValueBase", p = { Value = "string", } }, Studio = { s = "Instance", p = { ["\"TODO\" Color"] = "Color3", ["\"function\" Color"] = "Color3", ["\"local\" Color"] = "Color3", ["\"nil\" Color"] = "Color3", ["\"self\" Color"] = "Color3", ActionOnAutoResumeSync = "ActionOnAutoResumeSync", ActionOnStopSync = "ActionOnStopSync", ["Active Color"] = "Color3", ["Active Hover Over Color"] = "Color3", ["Always Save Script Changes"] = "bool", ["Animate Hover Over"] = "bool", ["Animation Skeleton Scale"] = "float", ["Animation Skeleton Transparency"] = "float", ["Auto Clean Empty Line"] = "bool", ["Auto Closing Brackets"] = "bool", ["Auto Closing Quotes"] = "bool", ["Auto Delete Closing Brackets and Quotes"] = "bool", ["Auto Indent Rule"] = "AutoIndentRule", ["Auto-Recovery Enabled"] = "bool", ["Auto-Recovery Interval (Minutes)"] = "int", AutoResumeSyncOnPlaceOpen = "bool", AutoUpdateEnabled = "bool", AutocompleteAcceptanceBehavior = "CompletionAcceptanceBehavior", ["Automatically trigger AI Code Completion"] = "bool", ["Background Color"] = "Color3", ["Basic Objects Display Mode"] = "ListDisplayMode", ["Bool Color"] = "Color3", ["Bracket Color"] = "Color3", ["Built-in Function Color"] = "Color3", ["Camera Mouse Wheel Speed"] = "float", ["Camera Pan Speed"] = "float", ["Camera Shift Speed"] = "float", ["Camera Speed"] = "float", ["Camera Speed Adjust Binding"] = "CameraSpeedAdjustBinding", ["Camera Zoom to Mouse Position"] = "bool", CameraAltLeftMouseToRotate = "bool", CameraKeyMoveSmoothing = "bool", CameraMouseMultiplier = "float", CameraNavigationModel = "CameraNavigationModel", CameraRotateShiftFactor = "float", CameraShiftFactor = "float", CameraTweenFocus = "bool", CameraZoomToMousePosition = "bool", ["Clear Output On Start"] = "bool", CommandBarEnterExec = "bool", CommandBarFont = "QFont", CommandBarHistoryLen = "int", CommandBarLocalState = "bool", ["Comment Color"] = "Color3", ["Current Line Highlight Color"] = "Color3", ["Debugger Current Line Color"] = "Color3", ["Debugger Error Line Color"] = "Color3", DefaultInstancesDir = "QDir", DefaultScriptSyncFileType = "DefaultScriptSyncFileType", DeprecatedObjectsShown = "bool", DisplayLanguage = "string", DraggerActiveColor = "Color3", DraggerLengthFactor = "float", DraggerMajorGridIncrement = "int", DraggerMaxSoftSnaps = "int", DraggerPassiveColor = "Color3", DraggerScaleFactor = "float", DraggerShowAxisTicks = "bool", DraggerShowDraggedPoint = "bool", DraggerShowHoverRuler = "bool", DraggerShowMeasurement = "bool", DraggerShowNegativeAxes = "bool", DraggerShowPlanes = "bool", DraggerShowTargetSnap = "bool", DraggerShowTrackball = "bool", DraggerShowWhileDragging = "bool", DraggerSoftSnapMarginFactor = "float", DraggerSummonMarginFactor = "float", DraggerTiltRotateDuration = "float", ["Enable Autocomplete"] = "bool", ["Enable Autocomplete Doc View"] = "bool", ["Enable Client/Server MDI"] = "bool", ["Enable CoreScript Debugger"] = "bool", ["Enable Http Sandboxing"] = "bool", ["Enable Internal Beta Features"] = "bool", ["Enable Internal Features"] = "bool", ["Enable Script Analysis"] = "bool", ["Enable Scrollbar Markers"] = "bool", ["Enable Signature Help"] = "bool", ["Enable Signature Help Doc View"] = "bool", ["Enable Temporary Tabs"] = "bool", ["Enable Temporary Tabs In Explorer"] = "bool", ["Enable Type Hover"] = "bool", EnableCodeAssist = "bool", EnableFindOnType = "bool", EnableIndentationRulers = "bool", EnableOvertypeMode = "bool", EnableSelectionTooltips = "bool", EnableStudioStreaming = "bool", ["Error Color"] = "Color3", ExternalEditorMode = "ExternalEditorMode", ExternalEditorSelection = "QDir", ["Find Selection Background Color"] = "Color3", Font = "QFont", ["Format On Paste"] = "bool", ["Format On Type"] = "bool", ["Function Name Color"] = "Color3", ["Highlight Current Line"] = "bool", ["Highlight Occurances"] = "bool", HintColor = "Color3", ["Hover Animate Speed"] = "HoverAnimateSpeed", ["Hover Box Thickness"] = "float", ["Hover Line Thickness"] = "int", ["Hover Over Color"] = "Color3", IconOverrideDir = "QDir", ["Indent Using Spaces"] = "bool", IndentationRulerColor = "Color3", InformationColor = "Color3", ["Keyword Color"] = "Color3", LargeFileLineCountThreshold = "int", LargeFileThreshold = "int", ["Line Thickness"] = "float", LoadAllBuiltinPluginsInRunModes = "bool", LoadInternalPlugins = "bool", LoadUserPluginsInRunModes = "bool", LocalAssetsFolder = "QDir", LuaDebuggerEnabled = "bool", ["Luau Keyword Color"] = "Color3", ["Main Volume"] = "float", ["Matching Word Background Color"] = "Color3", MaxFindReplaceAllResults = "int", ["Maximum Output Lines"] = "int", ["Menu Item Background Color"] = "Color3", ["Method Color"] = "Color3", ["Number Color"] = "Color3", ["Only Play Audio from Window in Focus"] = "bool", ["Operator Color"] = "Color3", ["Output Font"] = "QFont", ["Output Layout Mode"] = "OutputLayoutMode", PermissionLevelShown = "PermissionLevelShown", ["Physical Draggers Select Scope By Default"] = "bool", ["Pivot Snap To Geometry Color"] = "Color3", PluginDebuggingEnabled = "bool", PluginsDir = "QDir", PreferredTextSize = "PreferredTextSize", ["Primary Text Color"] = "Color3", ["Property Color"] = "Color3", ReloadBuiltinPluginsOnChange = "bool", ReloadLocalPluginsOnChange = "bool", ["Respect Studio shortcuts when game has focus"] = "bool", ["Ruler Color"] = "Color3", Rulers = "string", RuntimeUndoBehavior = "RuntimeUndoBehavior", ["Script Editor Color Preset"] = "StudioScriptEditorColorPresets", ["Script Editor Scrollbar Background Color"] = "Color3", ["Script Editor Scrollbar Handle Color"] = "Color3", ScriptTimeoutLength = "int", ["Scroll Past Last Line"] = "bool", ["Secondary Text Color"] = "Color3", ["Select Color"] = "Color3", ["Select/Hover Color"] = "Color3", ["Selected Menu Item Background Color"] = "Color3", ["Selected Text Color"] = "Color3", ["Selection Background Color"] = "Color3", ["Selection Box Thickness"] = "float", ["Selection Color"] = "Color3", ["Selection Line Thickness"] = "int", ["Set Pivot of Imported Parts"] = "bool", ["Show Animation Skeleton"] = "bool", ["Show Animation Skeleton Attachments"] = "bool", ["Show Animation Skeleton Axes"] = "bool", ["Show Animation Skeleton Rotations"] = "bool", ["Show Animation Skeleton Text"] = "bool", ["Show Core GUI in Explorer while Playing"] = "bool", ["Show Diagnostics Bar"] = "bool", ["Show FileSyncService"] = "bool", ["Show Hidden Objects in Explorer"] = "bool", ["Show Hover Over"] = "bool", ["Show Light Guides"] = "bool", ["Show Navigation Labels"] = "bool", ["Show Navigation Mesh"] = "bool", ["Show Pathfinding Links"] = "bool", ["Show Plugin GUI Service in Explorer"] = "bool", ["Show Singly Selected Attachment Parent Frame"] = "bool", ["Show Whitespace"] = "bool", ["Show plus button on hover in Explorer"] = "bool", ShowCorePackagesInExplorer = "bool", ["Skip Closing Brackets and Quotes"] = "bool", ["String Color"] = "Color3", ["Tab Width"] = "int", ["Text Color"] = "Color3", ["Text Wrapping"] = "bool", Theme = "Instance", TypeColor = "Color3", ["Use Bounding Box Move Handles"] = "bool", UseDefaultExternalEditor = "bool", VAxisColor = "Color3", ["Warning Color"] = "Color3", ["Whitespace Color"] = "Color3", XAxisColor = "Color3", YAxisColor = "Color3", ZAxisColor = "Color3", } }, StudioActionOverride = { s = "Object", p = { Enabled = "bool", } }, StudioAssetService = { s = "Instance", p = { } }, StudioAttachment = { s = "Instance", p = { AutoHideParent = "bool", IsArrowVisible = "bool", Offset = "Vector2", SourceAnchorPoint = "Vector2", TargetAnchorPoint = "Vector2", } }, StudioCallout = { s = "Instance", p = { } }, StudioCameraService = { s = "Instance", p = { FocusDistance = "float", LoggingEnabled = "bool", } }, StudioCaptureService = { s = "Instance", p = { } }, StudioData = { s = "Instance", p = { } }, StudioDeviceEmulatorService = { s = "Instance", p = { } }, StudioDeviceSimulatorService = { s = "Instance", p = { } }, StudioObjectBase = { s = "Instance", p = { } }, StudioPublishService = { s = "Instance", p = { PublishLocked = "bool", } }, StudioScreenshotCapture = { s = "Instance", p = { } }, StudioScriptDebugEventListener = { s = "Instance", p = { } }, StudioSdkService = { s = "Instance", p = { } }, StudioService = { s = "Instance", p = { Secrets = "string", UseLocalSpace = "bool", } }, StudioTestService = { s = "Instance", p = { EditModeActive = "bool", } }, StudioTheme = { s = "Instance", p = { } }, StudioUserService = { s = "Instance", p = { } }, StudioWidget = { s = "StudioObjectBase", p = { } }, StudioWidgetsService = { s = "Instance", p = { } }, StyleBase = { s = "Instance", p = { } }, StyleDerive = { s = "Instance", p = { Priority = "int", StyleSheet = "StyleSheet", } }, StyleLink = { s = "Instance", p = { StyleSheet = "StyleSheet", } }, StyleQuery = { s = "Instance", p = { } }, StyleRule = { s = "StyleBase", p = { Priority = "int", Selector = "string", } }, StyleSheet = { s = "StyleBase", p = { } }, StylingService = { s = "Instance", p = { } }, SunRaysEffect = { s = "PostEffect", p = { Intensity = "float", Spread = "float", } }, SurfaceAppearance = { s = "Instance", p = { AlphaMode = "AlphaMode", Color = "Color3", ColorMap = "ContentId", ColorMapContent = "Content", EmissiveMaskContent = "Content", EmissiveStrength = "float", EmissiveTint = "Color3", MetalnessMap = "ContentId", MetalnessMapContent = "Content", NormalMap = "ContentId", NormalMapContent = "Content", ResampleMode = "ResamplerMode", RoughnessMap = "ContentId", RoughnessMapContent = "Content", SurfaceSlot = "int", TexturePack = "ContentId", TexturePackContent = "Content", } }, SurfaceGui = { s = "SurfaceGuiBase", p = { AlwaysOnTop = "bool", Brightness = "float", CanvasSize = "Vector2", ClipsDescendants = "bool", LightInfluence = "float", MaxDistance = "float", PixelsPerStud = "float", SizingMode = "SurfaceGuiSizingMode", ToolPunchThroughDistance = "float", ZOffset = "float", } }, SurfaceGuiBase = { s = "LayerCollector", p = { Active = "bool", Adornee = "Instance", Face = "NormalId", } }, SurfaceLight = { s = "Light", p = { Angle = "float", Face = "NormalId", Range = "float", } }, SurfaceSelection = { s = "PartAdornment", p = { TargetSurface = "NormalId", } }, SwimController = { s = "ControllerBase", p = { AccelerationTime = "float", PitchMaxTorque = "float", PitchSpeedFactor = "float", RollMaxTorque = "float", RollSpeedFactor = "float", } }, SyncScriptBuilder = { s = "ScriptBuilder", p = { CompileTarget = "CompileTarget", CoverageInfo = "bool", DebugInfo = "bool", PackAsSource = "bool", } }, SystemThemeService = { s = "Instance", p = { } }, TaskScheduler = { s = "Instance", p = { ThreadPoolConfig = "ThreadPoolConfig", } }, Team = { s = "Instance", p = { AutoAssignable = "bool", TeamColor = "BrickColor", } }, TeamCreateData = { s = "Instance", p = { } }, TeamCreatePublishService = { s = "Instance", p = { } }, TeamCreateService = { s = "Instance", p = { } }, Teams = { s = "Instance", p = { } }, TelemetryService = { s = "Instance", p = { } }, TeleportAsyncResult = { s = "Instance", p = { } }, TeleportOptions = { s = "Instance", p = { ReservedServerAccessCode = "string", ReservedServerId = "string", ServerInstanceId = "string", ShouldReserveServer = "bool", VipServerId = "string", } }, TeleportService = { s = "Instance", p = { } }, TemporaryCageMeshProvider = { s = "Instance", p = { } }, TemporaryScriptService = { s = "Instance", p = { } }, Terrain = { s = "BasePart", p = { WaterColor = "Color3", WaterReflectance = "float", WaterTransparency = "float", WaterWaveSize = "float", WaterWaveSpeed = "float", } }, TerrainDetail = { s = "Instance", p = { ColorMap = "ContentId", ColorMapContent = "Content", EmissiveMaskContent = "Content", EmissiveStrength = "float", EmissiveTint = "Color3", Face = "TerrainFace", MaterialPattern = "MaterialPattern", MetalnessMap = "ContentId", MetalnessMapContent = "Content", NormalMap = "ContentId", NormalMapContent = "Content", RoughnessMap = "ContentId", RoughnessMapContent = "Content", StudsPerTile = "float", } }, TerrainIterateOperation = { s = "Object", p = { } }, TerrainModifyOperation = { s = "Object", p = { } }, TerrainReadOperation = { s = "Object", p = { } }, TerrainRegion = { s = "Instance", p = { } }, TerrainWriteOperation = { s = "Object", p = { } }, TestCase = { s = "Object", p = { } }, TestExecutionContext = { s = "Object", p = { } }, TestFailure = { s = "Object", p = { } }, TestResult = { s = "Object", p = { } }, TestRunResult = { s = "Object", p = { } }, TestService = { s = "Instance", p = { AutoRuns = "bool", Description = "string", ExecuteWithStudioRun = "bool", IsPhysicsEnvironmentalThrottled = "bool", IsSleepAllowed = "bool", NumberOfPlayers = "int", SimulateSecondsLag = "double", ThrottlePhysicsToRealtime = "bool", Timeout = "double", } }, TestSuite = { s = "Object", p = { } }, TextBox = { s = "GuiObject", p = { ClearTextOnFocus = "bool", FontFace = "Font", LineHeight = "float", MaxVisibleGraphemes = "int", MultiLine = "bool", OpenTypeFeatures = "string", PlaceholderColor3 = "Color3", PlaceholderText = "string", RichText = "bool", ShowNativeInput = "bool", Text = "string", TextColor3 = "Color3", TextDirection = "TextDirection", TextEditable = "bool", TextScaled = "bool", TextSize = "float", TextStrokeColor3 = "Color3", TextStrokeTransparency = "float", TextTransparency = "float", TextTruncate = "TextTruncate", TextWrapped = "bool", TextXAlignment = "TextXAlignment", TextYAlignment = "TextYAlignment", } }, TextBoxService = { s = "Instance", p = { } }, TextButton = { s = "GuiButton", p = { FontFace = "Font", LineHeight = "float", MaxVisibleGraphemes = "int", OpenTypeFeatures = "string", RichText = "bool", Text = "string", TextColor3 = "Color3", TextDirection = "TextDirection", TextScaled = "bool", TextSize = "float", TextStrokeColor3 = "Color3", TextStrokeTransparency = "float", TextTransparency = "float", TextTruncate = "TextTruncate", TextWrapped = "bool", TextXAlignment = "TextXAlignment", TextYAlignment = "TextYAlignment", } }, TextChannel = { s = "Instance", p = { AddPlayersOnJoin = "bool", } }, TextChannelWindow = { s = "GuiObject", p = { FontFace = "Font", Target = "TextChannel", UseDefaultFont = "bool", } }, TextChatCommand = { s = "Instance", p = { AutocompleteVisible = "bool", Enabled = "bool", PrimaryAlias = "string", SecondaryAlias = "string", } }, TextChatConfigurations = { s = "Instance", p = { } }, TextChatMessage = { s = "Instance", p = { BubbleChatMessageProperties = "BubbleChatMessageProperties", ChatWindowMessageProperties = "ChatWindowMessageProperties", TextChannel = "TextChannel", TextSource = "TextSource", } }, TextChatMessageProperties = { s = "Instance", p = { } }, TextChatService = { s = "Instance", p = { ChatTranslationEnabled = "bool", ChatVersion = "ChatVersion", CreateDefaultCommands = "bool", CreateDefaultTextChannels = "bool", PlatformIntegratedChat = "RolloutState", } }, TextDocument = { s = "Instance", p = { TextContent = "Content", } }, TextFilterResult = { s = "Instance", p = { } }, TextFilterTranslatedResult = { s = "Instance", p = { } }, TextGenerator = { s = "Instance", p = { Seed = "int", SystemPrompt = "string", Temperature = "float", TopP = "float", } }, TextLabel = { s = "GuiLabel", p = { FontFace = "Font", LineHeight = "float", MaxVisibleGraphemes = "int", OpenTypeFeatures = "string", RichText = "bool", Text = "string", TextColor3 = "Color3", TextDirection = "TextDirection", TextScaled = "bool", TextSize = "float", TextStrokeColor3 = "Color3", TextStrokeTransparency = "float", TextTransparency = "float", TextTruncate = "TextTruncate", TextWrapped = "bool", TextXAlignment = "TextXAlignment", TextYAlignment = "TextYAlignment", } }, TextService = { s = "Instance", p = { } }, TextSource = { s = "Instance", p = { CanSend = "bool", } }, Texture = { s = "Decal", p = { OffsetStudsU = "float", OffsetStudsV = "float", StudsPerTileU = "float", StudsPerTileV = "float", } }, TextureGenerationPartGroup = { s = "Instance", p = { } }, TextureGenerationService = { s = "Instance", p = { } }, TextureGenerationUnwrappingRequest = { s = "Instance", p = { } }, ThirdPartyUserService = { s = "Instance", p = { } }, ThreadState = { s = "Instance", p = { } }, TimerService = { s = "Instance", p = { } }, ToastNotificationService = { s = "Instance", p = { } }, Tool = { s = "BackpackItem", p = { CanBeDropped = "bool", Enabled = "bool", Grip = "CFrame", ManualActivationOnly = "bool", RequiresHandle = "bool", ToolTip = "string", } }, Torque = { s = "Constraint", p = { RelativeTo = "ActuatorRelativeTo", Torque = "Vector3", } }, TorsionSpringConstraint = { s = "Constraint", p = { Coils = "float", Damping = "float", LimitsEnabled = "bool", MaxAngle = "float", MaxTorque = "float", Radius = "float", Restitution = "float", Stiffness = "float", } }, TotalCountTimeIntervalItem = { s = "StatsItem", p = { } }, TouchInputService = { s = "Instance", p = { } }, TouchTransmitter = { s = "Instance", p = { } }, TraceRouteService = { s = "Instance", p = { } }, TracerService = { s = "Instance", p = { } }, TrackerLodController = { s = "Instance", p = { AudioMode = "TrackerLodFlagMode", VideoExtrapolationMode = "TrackerExtrapolationFlagMode", VideoLodMode = "TrackerLodValueMode", VideoMode = "TrackerLodFlagMode", } }, TrackerStreamAnimation = { s = "Instance", p = { } }, Trail = { s = "Instance", p = { Attachment0 = "Attachment", Attachment1 = "Attachment", Brightness = "float", Color = "ColorSequence", Enabled = "bool", FaceCamera = "bool", Lifetime = "float", LightEmission = "float", LightInfluence = "float", MaxLength = "float", MinLength = "float", Texture = "ContentId", TextureContent = "Content", TextureLength = "float", TextureMode = "TextureMode", Transparency = "NumberSequence", WidthScale = "NumberSequence", } }, Translator = { s = "Instance", p = { } }, TremoloSoundEffect = { s = "SoundEffect", p = { Depth = "float", Duty = "float", Frequency = "float", } }, TriangleMeshPart = { s = "BasePart", p = { CollisionFidelity = "CollisionFidelity", CollisionPrecision = "float", FluidFidelity = "FluidFidelity", } }, TrussPart = { s = "BasePart", p = { Style = "Style", } }, TutorialService = { s = "Instance", p = { } }, Tween = { s = "TweenBase", p = { } }, TweenBase = { s = "Instance", p = { } }, TweenService = { s = "Instance", p = { } }, UGCAvatarService = { s = "Instance", p = { } }, UGCValidationService = { s = "Instance", p = { } }, UIAspectRatioConstraint = { s = "UIConstraint", p = { AspectRatio = "float", AspectType = "AspectType", DominantAxis = "DominantAxis", } }, UIBase = { s = "Instance", p = { } }, UIBlur = { s = "UIComponent", p = { Color = "Color3", Enabled = "bool", Strength = "UIBlurStrength", Transparency = "float", } }, UIComponent = { s = "UIBase", p = { } }, UIConstraint = { s = "UIComponent", p = { } }, UICorner = { s = "UIComponent", p = { BottomLeftRadius = "UDim", BottomRightRadius = "UDim", CornerRadius = "UDim", TopLeftRadius = "UDim", TopRightRadius = "UDim", } }, UIDragDetector = { s = "UIComponent", p = { ActivatedCursorIcon = "ContentId", ActivatedCursorIconContent = "Content", BoundingBehavior = "UIDragDetectorBoundingBehavior", BoundingUI = "GuiBase2d", CursorIcon = "ContentId", CursorIconContent = "Content", DragAxis = "Vector2", DragRelativity = "UIDragDetectorDragRelativity", DragRotation = "float", DragSpace = "UIDragDetectorDragSpace", DragStyle = "UIDragDetectorDragStyle", DragUDim2 = "UDim2", Enabled = "bool", MaxDragAngle = "float", MaxDragTranslation = "UDim2", MinDragAngle = "float", MinDragTranslation = "UDim2", ReferenceUIInstance = "GuiObject", ResponseStyle = "UIDragDetectorResponseStyle", Selectable = "bool", SelectionModeDragSpeed = "UDim2", SelectionModeRotateSpeed = "float", UIDragSpeedAxisMapping = "UIDragSpeedAxisMapping", } }, UIDragDetectorService = { s = "Instance", p = { } }, UIFlexItem = { s = "UIComponent", p = { FlexMode = "UIFlexMode", GrowRatio = "float", ItemLineAlignment = "ItemLineAlignment", ShrinkRatio = "float", } }, UIGradient = { s = "UIComponent", p = { Color = "ColorSequence", Enabled = "bool", Offset = "Vector2", Rotation = "float", Transparency = "NumberSequence", } }, UIGridLayout = { s = "UIGridStyleLayout", p = { CellPadding = "UDim2", CellSize = "UDim2", FillDirectionMaxCells = "int", StartCorner = "StartCorner", } }, UIGridStyleLayout = { s = "UILayout", p = { FillDirection = "FillDirection", HorizontalAlignment = "HorizontalAlignment", SortOrder = "SortOrder", VerticalAlignment = "VerticalAlignment", } }, UILayout = { s = "UIComponent", p = { } }, UIListLayout = { s = "UIGridStyleLayout", p = { HorizontalFlex = "UIFlexAlignment", ItemLineAlignment = "ItemLineAlignment", Padding = "UDim", VerticalFlex = "UIFlexAlignment", Wraps = "bool", } }, UIPadding = { s = "UIComponent", p = { PaddingBottom = "UDim", PaddingLeft = "UDim", PaddingRight = "UDim", PaddingTop = "UDim", } }, UIPageLayout = { s = "UIGridStyleLayout", p = { Animated = "bool", Circular = "bool", EasingDirection = "EasingDirection", EasingStyle = "EasingStyle", GamepadInputEnabled = "bool", Padding = "UDim", ScrollWheelInputEnabled = "bool", TouchInputEnabled = "bool", TweenTime = "float", } }, UIScale = { s = "UIComponent", p = { Scale = "float", } }, UIShadow = { s = "UIComponent", p = { BlurRadius = "UDim", Color = "Color3", Enabled = "bool", Inset = "bool", Mode = "ApplyShadowMode", Offset = "UDim2", ShowBehindParent = "bool", Spread = "UDim2", Transparency = "float", ZIndex = "int", } }, UISizeConstraint = { s = "UIConstraint", p = { MaxSize = "Vector2", MinSize = "Vector2", } }, UIStroke = { s = "UIComponent", p = { ApplyStrokeMode = "ApplyStrokeMode", BorderOffset = "UDim", BorderStrokePosition = "BorderStrokePosition", Color = "Color3", Enabled = "bool", LineJoinMode = "LineJoinMode", StrokeSizingMode = "StrokeSizingMode", Thickness = "float", Transparency = "float", ZIndex = "int", } }, UITableLayout = { s = "UIGridStyleLayout", p = { FillEmptySpaceColumns = "bool", FillEmptySpaceRows = "bool", MajorAxis = "TableMajorAxis", Padding = "UDim2", } }, UITextSizeConstraint = { s = "UIConstraint", p = { MaxTextSize = "int", MinTextSize = "int", } }, UnionOperation = { s = "PartOperation", p = { } }, UniqueIdLookupService = { s = "Instance", p = { } }, UniversalConstraint = { s = "Constraint", p = { LimitsEnabled = "bool", MaxAngle = "float", Radius = "float", Restitution = "float", } }, UnreliableRemoteEvent = { s = "BaseRemoteEvent", p = { } }, UnvalidatedAssetService = { s = "Instance", p = { } }, UserGameSettings = { s = "Instance", p = { AllTutorialsDisabled = "bool", BadgeVisible = "bool", CameraMode = "CustomCameraMode", ChatVisible = "bool", ComputerCameraMovementMode = "ComputerCameraMovementMode", ComputerMovementMode = "ComputerMovementMode", ControlMode = "ControlMode", Fullscreen = "bool", GamepadCameraSensitivity = "float", GraphicsOptimizationMode = "GraphicsOptimizationMode", GraphicsQualityLevel = "int", HasEverUsedVR = "bool", MasterVolume = "float", MasterVolumeStudio = "float", MaxQualityEnabled = "bool", MouseSensitivity = "float", PartyVoiceVolume = "float", PeoplePageLayout = "PeoplePageLayout", PlayerListVisible = "bool", PlayerNamesEnabled = "bool", RCCProfilerRecordFrameRate = "int", RCCProfilerRecordTimeFrame = "int", RotationType = "RotationType", SavedQualityLevel = "SavedQualitySetting", TouchCameraMovementMode = "TouchCameraMovementMode", TouchMovementMode = "TouchMovementMode", UsedCoreGuiIsVisibleToggle = "bool", UsedCustomGuiIsVisibleToggle = "bool", UsedHideHudShortcut = "bool", VREnabled = "bool", VRRotationIntensity = "int", VRSmoothRotationEnabled = "bool", VignetteEnabled = "bool", VoiceChatVolume = "float", } }, UserInputService = { s = "Instance", p = { MouseBehavior = "MouseBehavior", MouseDeltaSensitivity = "float", MouseIcon = "ContentId", MouseIconContent = "Content", MouseIconEnabled = "bool", } }, UserService = { s = "Instance", p = { } }, UserSettings = { s = "GenericSettings", p = { } }, UserStorageService = { s = "LocalStorageService", p = { } }, VRService = { s = "Instance", p = { AutomaticScaling = "VRScaling", AvatarGestures = "bool", ControllerModels = "VRControllerModelMode", FadeOutViewOnCollision = "bool", GuiInputUserCFrame = "UserCFrame", LaserPointer = "VRLaserPointerMode", } }, VRStatusService = { s = "Instance", p = { } }, ValueBase = { s = "Instance", p = { } }, ValueCurve = { s = "Instance", p = { } }, Vector3Curve = { s = "Instance", p = { } }, Vector3Value = { s = "ValueBase", p = { Value = "Vector3", } }, VectorForce = { s = "Constraint", p = { ApplyAtCenterOfMass = "bool", Force = "Vector3", RelativeTo = "ActuatorRelativeTo", } }, VehicleController = { s = "Controller", p = { } }, VehicleSeat = { s = "BasePart", p = { Disabled = "bool", HeadsUpDisplay = "bool", MaxSpeed = "float", Steer = "int", SteerFloat = "float", Throttle = "int", ThrottleFloat = "float", Torque = "float", TurnSpeed = "float", } }, VelocityMotor = { s = "JointInstance", p = { CurrentAngle = "float", DesiredAngle = "float", Hole = "Hole", MaxVelocity = "float", } }, VersionControlService = { s = "Instance", p = { } }, VideoCapture = { s = "Capture", p = { } }, VideoCaptureService = { s = "Instance", p = { } }, VideoDeviceInput = { s = "Instance", p = { Active = "bool", CameraId = "string", CaptureQuality = "VideoDeviceCaptureQuality", } }, VideoDisplay = { s = "GuiObject", p = { ResampleMode = "ResamplerMode", ScaleType = "ScaleType", TileSize = "UDim2", VideoColor3 = "Color3", VideoRectOffset = "Vector2", VideoRectSize = "Vector2", VideoTransparency = "float", } }, VideoFrame = { s = "GuiObject", p = { Looped = "bool", Playing = "bool", RollOffMaxDistance = "float", RollOffMinDistance = "float", RollOffMode = "RollOffMode", TimePosition = "double", Video = "ContentId", VideoContent = "Content", Volume = "float", } }, VideoPlayer = { s = "Instance", p = { Looping = "bool", PlaybackSpeed = "float", TimePosition = "double", VideoContent = "Content", Volume = "float", } }, VideoSampler = { s = "Object", p = { } }, VideoScreenCaptureService = { s = "Instance", p = { } }, VideoService = { s = "Instance", p = { } }, ViewportCamera = { s = "Camera", p = { } }, ViewportFrame = { s = "GuiObject", p = { Ambient = "Color3", CurrentCamera = "Camera", ImageColor3 = "Color3", ImageTransparency = "float", LightColor = "Color3", LightDirection = "Vector3", } }, VirtualInput = { s = "Object", p = { } }, VirtualInputManager = { s = "Instance", p = { } }, VirtualUser = { s = "Instance", p = { } }, VisibilityCheckDispatcher = { s = "Instance", p = { } }, Visit = { s = "Instance", p = { } }, VisualizationMode = { s = "Instance", p = { Enabled = "bool", Title = "string", ToolTip = "string", } }, VisualizationModeCategory = { s = "Instance", p = { Enabled = "bool", Title = "string", } }, VisualizationModeService = { s = "Instance", p = { } }, VoiceChatInternal = { s = "Instance", p = { } }, VoiceChatService = { s = "Instance", p = { DefaultDistanceAttenuation = "VoiceChatDistanceAttenuationType", EnableDefaultVoice = "bool", EnableVoiceVolumeControls = "RolloutState", UseAudioApi = "AudioApiRollout", } }, VoxelBuffer = { s = "Object", p = { } }, WebSocketClient = { s = "Instance", p = { } }, WebSocketService = { s = "Instance", p = { } }, WebStreamClient = { s = "Object", p = { } }, WebViewService = { s = "Instance", p = { } }, WedgePart = { s = "FormFactorPart", p = { } }, Weld = { s = "JointInstance", p = { } }, WeldConstraint = { s = "Instance", p = { Enabled = "bool", Part0 = "BasePart", Part1 = "BasePart", } }, WindowProtocolService = { s = "Instance", p = { } }, Wire = { s = "Instance", p = { SourceInstance = "Instance", SourceName = "string", TargetInstance = "Instance", TargetName = "string", } }, WireframeHandleAdornment = { s = "HandleAdornment", p = { Scale = "Vector3", Thickness = "float", } }, Workspace = { s = "WorldRoot", p = { AirDensity = "float", AirTurbulenceIntensity = "float", AllowThirdPartySales = "bool", AuthorityMode = "AuthorityMode", ClientAnimatorThrottling = "ClientAnimatorThrottlingMode", CurrentCamera = "Camera", DistributedGameTime = "double", FallHeightEnabled = "bool", FallenPartsDestroyHeight = "float", GlobalWind = "Vector3", Gravity = "float", InsertPoint = "Vector3", LuauTypeCheckMode = "LuauTypeCheckMode", PlayerScriptsUseInputActionSystem = "RolloutState", ReplicationFrequency = "ReplicationMode", ReplicationThroughput = "ReplicationMode", Retargeting = "AnimatorRetargetingMode", StreamingAdaptiveRadius = "bool", StreamingEnabled = "bool", } }, WorkspaceAnnotation = { s = "Annotation", p = { } }, WorldModel = { s = "WorldRoot", p = { UseWorkspaceCollisionGroups = "bool", } }, WorldRoot = { s = "Model", p = { } }, WrapContentProvider = { s = "CacheableContentProvider", p = { } }, WrapDeformMeshProvider = { s = "Instance", p = { } }, WrapDeformer = { s = "BaseWrap", p = { } }, WrapLayer = { s = "BaseWrap", p = { AutoSkin = "WrapLayerAutoSkin", BindOffset = "CFrame", Enabled = "bool", Order = "int", ReferenceMeshContent = "Content", ReferenceMeshId = "ContentId", ReferenceOrigin = "CFrame", } }, WrapTarget = { s = "BaseWrap", p = { } }, WrapTextureTransfer = { s = "Instance", p = { ReferenceCageMeshContent = "Content", UVMaxBound = "Vector2", UVMinBound = "Vector2", } }, } CoeditCollect -- Turns what changed in Studio since the last flush into ops: deletes, -- adds (only what differs from a fresh instance of the class), property -- sets (renames and moves at the path peers still know) and script text. local Apply = require(script.Parent.CoeditApply) local Json = require(script.Parent.Json) local Paths = require(script.Parent.Paths) local Collect = {} local function freshId(): string return string.lower((string.gsub(game:GetService("HttpService"):GenerateGUID(false), "-", ""))) end local function depth(inst: Instance): number local n = 0 local current = inst.Parent while current ~= nil do n += 1 current = current.Parent end return n end -- Each class's scripting properties as a fresh `Instance.new` has them -- (encoded), so an add only carries what differs: a part is a handful of -- properties instead of a hundred, which matters when thousands are added. local defaults: { [string]: { [string]: string } } = {} local function defaultsOf(state: any, className: string): { [string]: string } local found = defaults[className] if found ~= nil then return found end local made: { [string]: string } = {} local ok, fresh = pcall(Instance.new, className) if ok then for prop in Apply.propsOf(className) do local okEnc, encoded = pcall(Apply.encodeProp, state, fresh, prop) if okEnc and encoded ~= nil then made[prop] = encoded end end fresh:Destroy() end defaults[className] = made return made end function Collect.collect(self: any): { any } local state = self.state local ops: { any } = {} local targetOf = function(i: Instance) return Apply.targetOf(state, i) end for _, entry in self.removed do local inst = entry.inst if inst.Parent ~= nil and self:isTracked(inst) then -- Moved between tracked roots: a reparent, not a delete. self:watch(inst) local props = self.dirty[inst] or {} props.Parent = true self.dirty[inst] = props else table.insert(ops, { k = "del", id = entry.id, p = entry.p, c = self:clock() }) end end self.removed = {} local adds: { Instance } = {} for inst in self.added do if inst.Parent ~= nil and self:isTracked(inst) then table.insert(adds, inst) end end self.added = {} table.sort(adds, function(a, b) return depth(a) < depth(b) end) for _, inst in adds do local id = state.idOf[inst] if id == nil then id = freshId() state.idOf[inst] = id state.byId[id] = inst end local props = {} local default = defaultsOf(state, inst.ClassName) for prop in Apply.propsOf(inst.ClassName) do if prop ~= "Parent" then local okEnc, encoded = pcall(Apply.encodeProp, state, inst, prop) if okEnc and encoded ~= nil and encoded ~= default[prop] then props[prop] = Json.decode(encoded) end end end for name in inst:GetAttributes() do local encoded = Apply.encodeProp(state, inst, "@" .. name) if encoded ~= nil and encoded ~= "null" then props["@" .. name] = Json.decode(encoded) end end for _, tag in inst:GetTags() do props["#" .. tag] = { t = "Bool", v = true } end props.Name = { t = "String", v = inst.Name } if inst:IsA("LuaSourceContainer") then local text = Apply.editorSource(inst) props.Source = { t = "String", v = text } state.log:hash(id, text) end self.dirty[inst] = nil self.dirtySrc[inst] = nil self.paths[inst] = Paths.pathOf(inst) table.insert(ops, { k = "add", id = id, p = Paths.pathOf(inst) or "", cls = inst.ClassName, parent = targetOf(inst.Parent :: Instance), props = props, c = self:clock(), }) end for inst, props in self.dirty do if inst.Parent == nil or not self:isTracked(inst) then continue end local at = targetOf(inst) local function emit(prop: string, path: string) local encoded = Apply.encodeProp(state, inst, prop) if encoded ~= nil then table.insert(ops, { k = "set", id = at.id, p = path, n = prop, v = Json.decode(encoded), c = self:clock() }) end end -- Name, then Parent, each at the path the peers still have. local before = self.moved[inst] if before ~= nil then if props.Name then props.Name = nil emit("Name", before) local names = Paths.split(before) if names ~= nil and #names > 0 then names[#names] = inst.Name before = Paths.join(names) end end if props.Parent then props.Parent = nil emit("Parent", before) end end for prop in props do emit(prop, at.p) end end self.dirty = {} self.moved = setmetatable({}, { __mode = "k" }) for inst in self.dirtySrc do if inst.Parent == nil or not self:isTracked(inst) then continue end local text = Apply.editorSource(inst) local marks = state.marks[inst] if marks ~= nil and marks.Source ~= nil then local mark = marks.Source marks.Source = nil if mark == text then continue end end local at = targetOf(inst) local key = Apply.keyOf(at) local logged = state.log.texts[key] if logged ~= nil and logged == text then continue end local base = state.log:hash(key, text) table.insert(ops, { k = "src", id = at.id, p = at.p, base = base, text = text, c = self:clock() }) end self.dirtySrc = {} for _, op in self.resend do op.c = self:clock() table.insert(ops, op) end self.resend = {} return ops end return Collect CoeditCore --!strict -- The pure parts of live co-editing, mirroring crates/rbx_presence/src/coedit -- (chunk.rs, merge.rs and the engine's script-text rules). Nothing here touches -- a Roblox service, so it runs in plain Luau and is pinned by -- fixtures/coedit_vectors.json. local Crypto = require(script.Parent.Crypto) local Sha256 = require(script.Parent.Sha256) local CHUNK_BYTES = 7000 local STALE_AFTER = 4096 local MAX_KNOWN_TEXTS = 64 local CoeditCore = {} function CoeditCore.textHash(text: string): string return string.sub(Sha256.hex(text), 1, 16) end -- Three-way merge ------------------------------------------------------------- local function splitLines(text: string): {string} local lines: {string} = {} local pos = 1 local len = #text while pos <= len do local nl = string.find(text, "\n", pos, true) if nl == nil then table.insert(lines, string.sub(text, pos)) break end table.insert(lines, string.sub(text, pos, nl)) pos = nl + 1 end return lines end -- For each line of `a`, the index of the line of `b` it is matched with in a -- longest common subsequence, or nil. local function matchLines(a: {string}, b: {string}): {[number]: number} local map: {[number]: number} = {} local na, nb = #a, #b local lo = 0 while lo < na and lo < nb and a[lo + 1] == b[lo + 1] do lo += 1 map[lo] = lo end local ea, eb = na, nb while ea > lo and eb > lo and a[ea] == b[eb] do map[ea] = eb ea -= 1 eb -= 1 end local rows = ea - lo local cols = eb - lo if rows == 0 or cols == 0 then return map end local table2: {{number}} = {} for i = 0, rows do local row: {number} = table.create(cols + 1, 0) table2[i + 1] = row end for i = rows - 1, 0, -1 do local row, below = table2[i + 1], table2[i + 2] local ai = a[lo + i + 1] for j = cols - 1, 0, -1 do if ai == b[lo + j + 1] then row[j + 1] = below[j + 2] + 1 else row[j + 1] = math.max(below[j + 1], row[j + 2]) end end end local i, j = 0, 0 while i < rows and j < cols do if a[lo + i + 1] == b[lo + j + 1] then map[lo + i + 1] = lo + j + 1 i += 1 j += 1 elseif table2[i + 2][j + 1] >= table2[i + 1][j + 2] then i += 1 else j += 1 end end return map end local function sameLines(a: {string}, b: {string}): boolean if #a ~= #b then return false end for i = 1, #a do if a[i] ~= b[i] then return false end end return true end local function slice(lines: {string}, from: number, to: number): {string} local out: {string} = {} for i = from, to do table.insert(out, lines[i]) end return out end local function emit(out: {string}, lines: {string}) for _, line in lines do table.insert(out, line) end end local function emitTerminated(out: {string}, lines: {string}) emit(out, lines) local last = lines[#lines] if last ~= nil and string.sub(last, -1) ~= "\n" then table.insert(out, "\n") end end function CoeditCore.merge3(base: string, ours: string, theirs: string): {text: string, conflicted: boolean} local b, o, t = splitLines(base), splitLines(ours), splitLines(theirs) local mapO, mapT = matchLines(b, o), matchLines(b, t) local out: {string} = {} local conflicted = false local prevB, prevO, prevT = 0, 0, 0 local function region(nextB: number, nextO: number, nextT: number) local baseChunk = slice(b, prevB + 1, nextB - 1) local oursChunk = slice(o, prevO + 1, nextO - 1) local theirsChunk = slice(t, prevT + 1, nextT - 1) if sameLines(oursChunk, baseChunk) then emit(out, theirsChunk) elseif sameLines(theirsChunk, baseChunk) or sameLines(oursChunk, theirsChunk) then emit(out, oursChunk) else conflicted = true table.insert(out, "<<<<<<< ours\n") emitTerminated(out, oursChunk) table.insert(out, "=======\n") emitTerminated(out, theirsChunk) table.insert(out, ">>>>>>> theirs\n") end end for i = 1, #b do local oi, ti = mapO[i], mapT[i] if oi ~= nil and ti ~= nil then region(i, oi, ti) table.insert(out, o[oi]) prevB, prevO, prevT = i, oi, ti end end region(#b + 1, #o + 1, #t + 1) return { text = table.concat(out), conflicted = conflicted } end -- Chunks ---------------------------------------------------------------------- export type Chunk = { b: string, i: number, n: number, d: string } function CoeditCore.split(id: string, batchJson: string): {Chunk} local len = #batchJson if len == 0 then return { { b = id, i = 0, n = 1, d = "" } } end local n = math.ceil(len / CHUNK_BYTES) local chunks: {Chunk} = {} for i = 0, n - 1 do local part = string.sub(batchJson, i * CHUNK_BYTES + 1, (i + 1) * CHUNK_BYTES) chunks[i + 1] = { b = id, i = i, n = n, d = Crypto.b64encode(part) } end return chunks end local Reassembler = {} Reassembler.__index = Reassembler function Reassembler.new() return setmetatable({ pending = {} :: {[string]: any} }, Reassembler) end function Reassembler.push(self: any, seq: number, sender: string, chunk: Chunk): string? local pending = self.pending :: {[string]: any} for key, entry in pending do if seq - entry.first >= STALE_AFTER then pending[key] = nil end end local n, i = chunk.n, chunk.i if type(n) ~= "number" or type(i) ~= "number" or n % 1 ~= 0 or i % 1 ~= 0 then return nil end if n == 0 or n > STALE_AFTER or i < 0 or i >= n then return nil end local data = Crypto.b64decode(chunk.d) if data == nil then return nil end local key = sender .. "\0" .. tostring(chunk.b) local entry = pending[key] if entry == nil then entry = { first = seq, n = n, slots = {}, filled = 0 } pending[key] = entry end if entry.n ~= n then return nil end if entry.slots[i + 1] == nil then entry.filled += 1 end entry.slots[i + 1] = data if entry.filled < n then return nil end pending[key] = nil local text = table.concat(entry.slots) if utf8.len(text) == nil then return nil end return text end CoeditCore.Reassembler = Reassembler -- Clocks and registers -------------------------------------------------------- export type Clock = { any } function CoeditCore.clockLess(a: Clock, b: Clock): boolean local ca, cb = a[1] :: number, b[1] :: number if ca ~= cb then return ca < cb end return (a[2] :: string) < (b[2] :: string) end local Registers = {} Registers.__index = Registers function Registers.new() return setmetatable({ clocks = {} :: {[string]: Clock} }, Registers) end function Registers.claim(self: any, key: string, clock: Clock): boolean local clocks = self.clocks :: {[string]: Clock} local stored = clocks[key] if stored ~= nil and not CoeditCore.clockLess(stored, clock) then return false end clocks[key] = clock return true end CoeditCore.Registers = Registers -- Script text log ------------------------------------------------------------- local Log = {} Log.__index = Log function Log.new() return setmetatable({ texts = {} :: {[string]: string}, known = {} :: {[string]: {[string]: string}} }, Log) end local function remember(self: any, key: string, text: string): string local hash = CoeditCore.textHash(text) local known = self.known[key] if known == nil then known = { count = 0 } self.known[key] = known end if known[hash] == nil then if known.count >= MAX_KNOWN_TEXTS then known = { count = 0 } self.known[key] = known end known[hash] = text known.count += 1 end return hash end local function logText(self: any, key: string, current: string): string local text = self.texts[key] if text == nil then text = current self.texts[key] = current remember(self, key, current) end return text end -- Hash of the log's text for `key` (seeded from `current` when nothing has -- been logged yet): the `base` a new Src op names. function Log.hash(self: any, key: string, current: string): string return remember(self, key, logText(self, key, current)) end export type SrcResult = { accepted: boolean, write: string?, conflicted: boolean, resend: string? } -- Applies a Src op in log order. `current` is the editor's text now. function Log.src(self: any, key: string, current: string, op: { base: string, text: string }, own: boolean): SrcResult local old = logText(self, key, current) if op.base ~= CoeditCore.textHash(old) then -- Our own op on text the log has moved past. What moved it was merged -- into the editor's text when it arrived (or was our own earlier op), -- so the editor already holds both: resend that, never merge twice. if own then local resend: string? = if current ~= old then current else nil return { accepted = false, write = nil, conflicted = false, resend = resend } end return { accepted = false, write = nil, conflicted = false, resend = nil } end self.texts[key] = op.text remember(self, key, op.text) if own or current == op.text then return { accepted = true, write = nil, conflicted = false, resend = nil } end if current == old then return { accepted = true, write = op.text, conflicted = false, resend = nil } end local merged = CoeditCore.merge3(old, current, op.text) local write: string? = nil if merged.text ~= current then write = merged.text end return { accepted = true, write = write, conflicted = merged.conflicted, resend = nil } end CoeditCore.Log = Log function CoeditCore.session(): string local digits: {string} = {} for i = 1, 8 do digits[i] = string.format("%x", math.random(0, 15)) end return table.concat(digits) end return CoeditCore CoeditKept --!strict -- What this peer keeps of the op log it reads, mirroring -- crates/rbx_presence/src/coedit/keep.rs: entries exactly as logged, the -- log's id, so a relay that lacks the log (a new host) is handed it and -- everyone carries on at the same seqs. Pure Luau, run by the test suite. -- Most ciphertext kept, the relay's own cap; the oldest goes first. local KEPT_BYTES = 64 * 1024 * 1024 -- Most ciphertext per request when posting or filling. local REQUEST_CT_BYTES = 512 * 1024 export type Entry = { seq: number, userId: number, client: string, ct: string } local Kept = {} Kept.__index = Kept Kept.REQUEST_CT_BYTES = REQUEST_CT_BYTES function Kept.new(cap: number?) return setmetatable({ log = "", entries = {} :: { [number]: Entry }, first = 1, last = 0, bytes = 0, cap = cap or KEPT_BYTES }, Kept) end -- The newest seq kept; 0 for none. function Kept.head(self: any): number -- Kept as a window `first..last` over the table, so trimming the front -- never leaves holes for `#` to trip on. return if self.last >= self.first then self.entries[self.last].seq else 0 end function Kept.restart(self: any, log: string) self.log = log self.entries = {} self.first = 1 self.last = 0 self.bytes = 0 end -- Keeps an entry of log `log` (the reply's), in order. function Kept.record(self: any, log: string, entry: Entry) if self.log ~= log then self:restart(log) end self.last += 1 self.entries[self.last] = { seq = entry.seq, userId = entry.userId, client = entry.client, ct = entry.ct } self.bytes += #entry.ct while self.bytes > self.cap and self.first <= self.last do self.bytes -= #self.entries[self.first].ct self.entries[self.first] = nil :: any self.first += 1 end end local function from(self: any, after: number): { Entry } local out = {} for i = self.first, self.last do local entry = self.entries[i] if entry.seq > after then table.insert(out, entry) end end return out end -- What to do with a reply: "read" its entries, "fill" the relay with the -- returned entries it lacks, or "restart" on the other log it runs. function Kept.step(self: any, reply: any): (string, { Entry }?) local log = if type(reply.log) == "string" then reply.log else "" local head = tonumber(reply.head) or 0 if log == "" then if head == 0 and self:head() > 0 then return "fill", from(self, 0) end return "read", nil end if self.log == "" then return "read", nil end if log ~= self.log then return "restart", nil end local tail = from(self, head) if tail[1] ~= nil and tail[1].seq == head + 1 then return "fill", tail end return "read", nil end -- `items` cut into runs of at most `max` bytes each (by `size`), never -- empty, so one request each. function Kept.groups<T>(items: { T }, max: number, size: (T) -> number): { { T } } local out: { { T } } = {} local used = 0 for _, item in items do local n = size(item) local last = out[#out] if last ~= nil and used + n <= max then table.insert(last, item) else table.insert(out, { item }) used = 0 end used += n end return out end return Kept CoeditSync -- Co-editing's traffic with the relay: sends batches to the op log (sealed -- chunks, as many per request as fit: Studio allows a plugin 500 HTTP -- requests a minute) and reads the log back, keeping a copy of it -- (CoeditKept) that a relay lacking the log is handed. local CoeditCore = require(script.Parent.CoeditCore) local Crypto = require(script.Parent.Crypto) local Json = require(script.Parent.Json) local Kept = require(script.Parent.CoeditKept) local POST_TRIES = 5 local Sync = {} local rng = Random.new() -- Plugins have no CSPRNG; a v4 GUID mixed with a Random stream gives a nonce -- that does not repeat. local function nonceFor(): string local hex = string.gsub(game:GetService("HttpService"):GenerateGUID(false), "[^%x]", "") local bytes = table.create(12) for i = 1, 12 do local guidByte = tonumber(string.sub(hex, i * 2 - 1, i * 2), 16) or 0 bytes[i] = string.char(bit32.bxor(guidByte, rng:NextInteger(0, 255))) end return table.concat(bytes) end -- Posts `body` to the op log (or its fill route), retrying with backoff. local function post(self: any, body: string, fill: boolean): (boolean, string?) local relay = self.options.relay local lastError: string? = nil for attempt = 1, POST_TRIES do if not self.running then return false, nil end local okPost, ok, err = pcall(relay.postOps, relay, body, fill) if okPost and ok then return true, nil end lastError = if okPost then err else tostring(ok) task.wait(0.5 * 2 ^ (attempt - 1)) end return false, lastError end function Sync.send(self: any, ops: { any }): boolean local options = self.options local okJson, batchJson = pcall(Json.encode, { a = self.actor, ops = ops }) if not okJson then self:note("Co-edit: could not encode an edit batch") return false end self.batchCount += 1 local id = CoeditCore.session() .. string.format("%x", self.batchCount) local aad = Crypto.opsAad(options.room, options.userId, "studio") local sealed: { string } = {} for _, chunk in CoeditCore.split(id, batchJson) do table.insert(sealed, Crypto.sealB64(options.encKey, nonceFor(), aad, Json.encode(chunk))) end for _, cts in Kept.groups(sealed, Kept.REQUEST_CT_BYTES, string.len) do local body = Json.encode({ v = 2, userId = options.userId, client = "studio", cts = cts }) local sent, why = post(self, body, false) if not sent then if why ~= nil then self:note("Co-edit: could not send edits (" .. why .. ")") end return false end end return true end -- Hands the relay entries of the kept log it lacks. local function handOver(self: any, entries: { any }): boolean for _, group in Kept.groups(entries, Kept.REQUEST_CT_BYTES, function(e) return #e.ct end) do local body = Json.encode({ v = 2, log = self.kept.log, entries = group }) local sent, why = post(self, body, true) if not sent then if why ~= nil then self:note("Co-edit: could not hand the edit log to the new host (" .. why .. ")") end return false end end return true end function Sync.flush(self: any) local ops = self:collect() -- Unsent edits go out again with the next flush, to whichever host the -- room has by then. if #ops > 0 and not self:send(ops) then for _, op in ops do table.insert(self.resend, op) end end end function Sync.authorOf(_self: any, actor: any): string if type(actor) ~= "string" then return "a collaborator" end local userId, client = string.match(actor, "^(%d+):([^:]+)") if userId == nil then return actor end return "user " .. userId .. " (" .. client .. ")" end function Sync.poll(self: any) local options = self.options local okPoll, result, err = pcall(options.relay.pollOps, options.relay, self.since) if not okPoll or result == nil then if not okPoll or err ~= nil then self:note("Co-edit: could not read edits (" .. tostring(if okPoll then err else result) .. ")") end return end -- A relay may carry on the log we read, lack some or all of it (a new -- host: hand it over), or run another (read that from its start; what -- the old one decided, last writer per property, is in the place). local step, missing = self.kept:step(result) if step == "fill" then handOver(self, missing :: { any }) return elseif step == "restart" then self.kept:restart(result.log) self.since = 0 self.reassembler = CoeditCore.Reassembler.new() self.state.registers = CoeditCore.Registers.new() return end local log = if type(result.log) == "string" then result.log else "" local entries = type(result.entries) == "table" and result.entries or {} table.sort(entries, function(a, b) return (a.seq or 0) < (b.seq or 0) end) if type(result.floor) == "number" and result.floor > self.since + 1 and self.since > 0 then self:note("Co-edit: the relay dropped older edits; some changes may be missing") end for _, entry in entries do if type(entry) ~= "table" or type(entry.seq) ~= "number" or type(entry.ct) ~= "string" then continue end if entry.seq <= self.since then continue end self.since = entry.seq local client = tostring(entry.client) local userId = tonumber(entry.userId) or 0 self.kept:record(log, { seq = entry.seq, userId = userId, client = client, ct = entry.ct }) local plain = Crypto.openB64(options.encKey, Crypto.opsAad(options.room, userId, client), entry.ct) if plain == nil then continue end local okChunk, chunk = pcall(Json.decode, plain) if not okChunk or type(chunk) ~= "table" then continue end local text = self.reassembler:push(entry.seq, string.format("%.0f", userId) .. ":" .. client, chunk) if text == nil then continue end local okBatch, batch = pcall(Json.decode, text) if okBatch and type(batch) == "table" then local outcome = self.apply:batch(batch, batch.a == self.actor, self:authorOf(batch.a)) for _, line in outcome.notes do self:note(line) end for _, op in outcome.resend do table.insert(self.resend, op) end end end end return Sync CoeditValue --!strict -- Roblox values <-> value.rs JSON (`{t = "<Type>", v = ...}`). A JSON null -- `v` arrives in Luau as a missing key, so an absent `v` reads as null. local CoeditValue = {} type TargetJson = { id: string, p: string } local function wrap(t: string, v: any): any return { t = t, v = v } end local function seqNums(ks: { any }, f: (any) -> { number }): { { number } } local out = {} for _, k in ks do table.insert(out, f(k)) end return out end local function encodeInner(value: any, valueType: string?, targetOf: (Instance) -> TargetJson): any? local kind = typeof(value) if value == nil then if valueType == "Ref" or valueType == "Instance" then return { t = "Ref" } elseif valueType == "PhysicalProperties" then return wrap("PhysicalProperties", "Default") elseif valueType == "OptionalCFrame" then return { t = "OptionalCFrame" } end return nil elseif kind == "string" then return wrap("String", value) elseif kind == "boolean" then return wrap("Bool", value) elseif kind == "number" then if valueType == "float" or valueType == "Float32" then return wrap("Float32", value) elseif valueType == "int" or valueType == "Int32" then return wrap("Int32", math.floor(value)) elseif valueType == "int64" or valueType == "Int64" then return wrap("Int64", math.floor(value)) end return wrap("Float64", value) elseif kind == "EnumItem" then if valueType == "Attribute" then return wrap("EnumItem", { tostring(value.EnumType), value.Value }) end return wrap("Enum", value.Value) elseif kind == "BrickColor" then return wrap("BrickColor", value.Number) elseif kind == "Color3" then if valueType == "Color3uint8" then return wrap("Color3uint8", { math.clamp(math.floor(value.R * 255 + 0.5), 0, 255), math.clamp(math.floor(value.G * 255 + 0.5), 0, 255), math.clamp(math.floor(value.B * 255 + 0.5), 0, 255), }) end return wrap("Color3", { value.R, value.G, value.B }) elseif kind == "Vector2" then return wrap("Vector2", { value.X, value.Y }) elseif kind == "Vector3" then return wrap("Vector3", { value.X, value.Y, value.Z }) elseif kind == "Vector3int16" then return wrap("Vector3int16", { value.X, value.Y, value.Z }) elseif kind == "Ray" then local o, d = value.Origin, value.Direction return wrap("Ray", { o.X, o.Y, o.Z, d.X, d.Y, d.Z }) elseif kind == "Faces" then return wrap("Faces", { value.Front, value.Bottom, value.Left, value.Back, value.Top, value.Right }) elseif kind == "Axes" then return wrap("Axes", { value.X, value.Y, value.Z }) elseif kind == "CFrame" then return wrap(if valueType == "OptionalCFrame" then "OptionalCFrame" else "CFrame", { value:GetComponents() }) elseif kind == "NumberSequence" then return wrap("NumberSequence", seqNums(value.Keypoints, function(k) return { k.Time, k.Value, k.Envelope } end)) elseif kind == "ColorSequence" then return wrap("ColorSequence", seqNums(value.Keypoints, function(k) return { k.Time, k.Value.R, k.Value.G, k.Value.B, 0 } end)) elseif kind == "NumberRange" then return wrap("NumberRange", { value.Min, value.Max }) elseif kind == "Rect" then return wrap("Rect", { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }) elseif kind == "PhysicalProperties" then return wrap("PhysicalProperties", { value.Density, value.Friction, value.Elasticity, value.FrictionWeight, value.ElasticityWeight, }) elseif kind == "UDim" then return wrap("UDim", { value.Scale, value.Offset }) elseif kind == "UDim2" then return wrap("UDim2", { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }) elseif kind == "Font" then return wrap("Font", { family = value.Family, weight = value.Weight.Value, style = value.Style.Value }) elseif kind == "Instance" then local t = targetOf(value) return wrap("Ref", { id = t.id, p = t.p }) end return nil end function CoeditValue.encode(value: any, valueType: string?, targetOf: (Instance) -> TargetJson): any? local ok, result = pcall(encodeInner, value, valueType, targetOf) if ok then return result end return nil end local function enumByValue(enum: any, n: number): EnumItem? for _, item in enum:GetEnumItems() do if item.Value == n then return item end end return nil end local function decodeInner(json: any, current: any, valueType: string?, resolve: (TargetJson) -> Instance?): (boolean, any) if type(json) ~= "table" or type(json.t) ~= "string" then return false, nil end local t, v = json.t, json.v if t == "Ref" then if type(v) ~= "table" then return true, nil end return true, resolve({ id = tostring(v.id or ""), p = tostring(v.p or "") }) elseif t == "OptionalCFrame" and v == nil then return true, nil elseif t == "String" then return type(v) == "string", if type(v) == "string" then v else nil elseif t == "Bool" then return type(v) == "boolean", if type(v) == "boolean" then v else nil elseif t == "Int32" or t == "Int64" or t == "Float32" or t == "Float64" then return type(v) == "number", if type(v) == "number" then v else nil elseif t == "Enum" then local enum: any = nil if typeof(current) == "EnumItem" then enum = current.EnumType elseif valueType and string.sub(valueType, 1, 5) == "Enum:" then enum = (Enum :: any)[string.sub(valueType, 6)] end if enum == nil or type(v) ~= "number" then return false, nil end local item = enumByValue(enum, v) return item ~= nil, item elseif t == "EnumItem" then local item = if type(v) == "table" then enumByValue((Enum :: any)[v[1]], v[2]) else nil return item ~= nil, item elseif t == "BrickColor" then return true, BrickColor.new(v) elseif t == "PhysicalProperties" then if v == "Default" then return true, nil end return true, PhysicalProperties.new(v[1], v[2], v[3], v[4], v[5]) elseif t == "Font" then local weight = enumByValue(Enum.FontWeight, v.weight) local style = enumByValue(Enum.FontStyle, v.style) if weight == nil or style == nil then return false, nil end return true, Font.new(v.family, weight :: any, style :: any) end if type(v) ~= "table" then return false, nil end if t == "Color3" then return true, Color3.new(v[1], v[2], v[3]) elseif t == "Color3uint8" then return true, Color3.fromRGB(v[1], v[2], v[3]) elseif t == "Vector2" then return true, Vector2.new(v[1], v[2]) elseif t == "Vector3" then return true, Vector3.new(v[1], v[2], v[3]) elseif t == "Vector3int16" then return true, Vector3int16.new(v[1], v[2], v[3]) elseif t == "Ray" then return true, Ray.new(Vector3.new(v[1], v[2], v[3]), Vector3.new(v[4], v[5], v[6])) elseif t == "Faces" then local ids = {} local names = { "Front", "Bottom", "Left", "Back", "Top", "Right" } for i, name in names do if v[i] == true then table.insert(ids, (Enum.NormalId :: any)[name]) end end return true, Faces.new(table.unpack(ids)) elseif t == "Axes" then local ids = {} local names = { "X", "Y", "Z" } for i, name in names do if v[i] == true then table.insert(ids, (Enum.Axis :: any)[name]) end end return true, Axes.new(table.unpack(ids)) elseif t == "CFrame" or t == "OptionalCFrame" then if #v ~= 12 then return false, nil end return true, CFrame.new(table.unpack(v, 1, 12)) elseif t == "NumberSequence" then local ks = {} for _, k in v do table.insert(ks, NumberSequenceKeypoint.new(k[1], k[2], k[3] or 0)) end return true, NumberSequence.new(ks) elseif t == "ColorSequence" then local ks = {} for _, k in v do table.insert(ks, ColorSequenceKeypoint.new(k[1], Color3.new(k[2], k[3], k[4]))) end return true, ColorSequence.new(ks) elseif t == "NumberRange" then return true, NumberRange.new(v[1], v[2]) elseif t == "Rect" then return true, Rect.new(v[1], v[2], v[3], v[4]) elseif t == "UDim" then return true, UDim.new(v[1], v[2]) elseif t == "UDim2" then return true, UDim2.new(v[1], v[2], v[3], v[4]) end return false, nil end function CoeditValue.decode(json: any, current: any, valueType: string?, resolve: (TargetJson) -> Instance?): (boolean, any) local ok, good, result = pcall(decodeInner, json, current, valueType, resolve) if ok and good then return true, result end return false, nil end return CoeditValue Crypto --!strict -- Payload encryption, mirroring crates/rbx_presence/src/crypt.rs: -- HKDF-SHA256 key derivation and ChaCha20-Poly1305 (RFC 8439), using only -- bit32 so it runs in Studio and in plain Luau alike. Both sides are pinned -- by fixtures/crypto_vectors.json. local Sha256 = require(script.Parent.Sha256) local Crypto = {} local NONCE_LEN = 12 local TAG_LEN = 16 local BLOCK = 64 local function hmacRaw(key: string, msg: string): string if #key > BLOCK then key = Sha256.raw(key) end key = key .. string.rep("\0", BLOCK - #key) local inner: {string} = table.create(BLOCK) local outer: {string} = table.create(BLOCK) for i = 1, BLOCK do local b = string.byte(key, i) inner[i] = string.char(bit32.bxor(b, 0x36)) outer[i] = string.char(bit32.bxor(b, 0x5c)) end return Sha256.raw(table.concat(outer) .. Sha256.raw(table.concat(inner) .. msg)) end function Crypto.hex(s: string): string return (string.gsub(s, ".", function(c: string) return string.format("%02x", string.byte(c)) end)) end function Crypto.unhex(s: string): string? if #s % 2 ~= 0 or string.find(s, "[^0-9a-fA-F]") then return nil end return (string.gsub(s, "..", function(pair: string) return string.char(tonumber(pair, 16) :: number) end)) end function Crypto.hkdf(ikm: string, salt: string, info: string, len: number): string if salt == "" then salt = string.rep("\0", 32) end local prk = hmacRaw(salt, ikm) local out: {string} = {} local prev = "" local have = 0 local i = 1 while have < len do prev = hmacRaw(prk, prev .. info .. string.char(i)) table.insert(out, prev) have += #prev i += 1 end return string.sub(table.concat(out), 1, len) end function Crypto.encKey(secret: string, placeId: number): string return Crypto.hkdf(secret, "", "rbx-presence/enc/v1/" .. string.format("%.0f", placeId), 32) end -- ChaCha20 ------------------------------------------------------------------- local function leWord(s: string, i: number): number local a, b, c, d = string.byte(s, i, i + 3) return a + b * 0x100 + c * 0x10000 + d * 0x1000000 end local function chachaBlock(key: string, counter: number, nonce: string): {number} local s: {number} = { 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, leWord(key, 1), leWord(key, 5), leWord(key, 9), leWord(key, 13), leWord(key, 17), leWord(key, 21), leWord(key, 25), leWord(key, 29), counter, leWord(nonce, 1), leWord(nonce, 5), leWord(nonce, 9), } local x0, x1, x2, x3, x4, x5, x6, x7 = s[1], s[2], s[3], s[4], s[5], s[6], s[7], s[8] local x8, x9, x10, x11, x12, x13, x14, x15 = s[9], s[10], s[11], s[12], s[13], s[14], s[15], s[16] local band, bxor, rot = bit32.band, bit32.bxor, bit32.lrotate local M = 0xffffffff for _ = 1, 10 do -- columns x0 = band(x0 + x4, M); x12 = rot(bxor(x12, x0), 16); x8 = band(x8 + x12, M); x4 = rot(bxor(x4, x8), 12) x0 = band(x0 + x4, M); x12 = rot(bxor(x12, x0), 8); x8 = band(x8 + x12, M); x4 = rot(bxor(x4, x8), 7) x1 = band(x1 + x5, M); x13 = rot(bxor(x13, x1), 16); x9 = band(x9 + x13, M); x5 = rot(bxor(x5, x9), 12) x1 = band(x1 + x5, M); x13 = rot(bxor(x13, x1), 8); x9 = band(x9 + x13, M); x5 = rot(bxor(x5, x9), 7) x2 = band(x2 + x6, M); x14 = rot(bxor(x14, x2), 16); x10 = band(x10 + x14, M); x6 = rot(bxor(x6, x10), 12) x2 = band(x2 + x6, M); x14 = rot(bxor(x14, x2), 8); x10 = band(x10 + x14, M); x6 = rot(bxor(x6, x10), 7) x3 = band(x3 + x7, M); x15 = rot(bxor(x15, x3), 16); x11 = band(x11 + x15, M); x7 = rot(bxor(x7, x11), 12) x3 = band(x3 + x7, M); x15 = rot(bxor(x15, x3), 8); x11 = band(x11 + x15, M); x7 = rot(bxor(x7, x11), 7) -- diagonals x0 = band(x0 + x5, M); x15 = rot(bxor(x15, x0), 16); x10 = band(x10 + x15, M); x5 = rot(bxor(x5, x10), 12) x0 = band(x0 + x5, M); x15 = rot(bxor(x15, x0), 8); x10 = band(x10 + x15, M); x5 = rot(bxor(x5, x10), 7) x1 = band(x1 + x6, M); x12 = rot(bxor(x12, x1), 16); x11 = band(x11 + x12, M); x6 = rot(bxor(x6, x11), 12) x1 = band(x1 + x6, M); x12 = rot(bxor(x12, x1), 8); x11 = band(x11 + x12, M); x6 = rot(bxor(x6, x11), 7) x2 = band(x2 + x7, M); x13 = rot(bxor(x13, x2), 16); x8 = band(x8 + x13, M); x7 = rot(bxor(x7, x8), 12) x2 = band(x2 + x7, M); x13 = rot(bxor(x13, x2), 8); x8 = band(x8 + x13, M); x7 = rot(bxor(x7, x8), 7) x3 = band(x3 + x4, M); x14 = rot(bxor(x14, x3), 16); x9 = band(x9 + x14, M); x4 = rot(bxor(x4, x9), 12) x3 = band(x3 + x4, M); x14 = rot(bxor(x14, x3), 8); x9 = band(x9 + x14, M); x4 = rot(bxor(x4, x9), 7) end local x = { x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15 } for i = 1, 16 do x[i] = band(x[i] + s[i], M) end return x end local function chachaXor(key: string, counter: number, nonce: string, data: string): string local out: {string} = {} local bytes: {number} = table.create(BLOCK, 0) local n = #data local pos = 1 while pos <= n do local ks = chachaBlock(key, counter, nonce) local count = math.min(BLOCK, n - pos + 1) for j = 0, count - 1 do local w = ks[bit32.rshift(j, 2) + 1] bytes[j + 1] = bit32.bxor(string.byte(data, pos + j), bit32.extract(w, (j % 4) * 8, 8)) end table.insert(out, string.char(table.unpack(bytes, 1, count))) pos += BLOCK counter += 1 end return table.concat(out) end -- Poly1305 with ten 13-bit limbs (130 bits), so every product sum stays far -- below 2^53 in a double. local LIMB = 8192 local function limbsOf(bytes: {number}): {number} local limbs: {number} = table.create(10, 0) for i = 0, 9 do local off = i * 13 local bi = bit32.rshift(off, 3) + 1 local v = (bytes[bi] or 0) + (bytes[bi + 1] or 0) * 256 + (bytes[bi + 2] or 0) * 65536 limbs[i + 1] = bit32.extract(v, off % 8, 13) end return limbs end local function poly1305(key: string, msg: string): string local rb = { string.byte(key, 1, 16) } rb[4] = bit32.band(rb[4], 15) rb[8] = bit32.band(rb[8], 15) rb[12] = bit32.band(rb[12], 15) rb[16] = bit32.band(rb[16], 15) rb[5] = bit32.band(rb[5], 252) rb[9] = bit32.band(rb[9], 252) rb[13] = bit32.band(rb[13], 252) local r = limbsOf(rb) local r5: {number} = table.create(10, 0) for i = 1, 10 do r5[i] = r[i] * 5 end local h: {number} = table.create(10, 0) local d: {number} = table.create(10, 0) local n = #msg local pos = 1 while pos <= n do local count = math.min(16, n - pos + 1) local blk = { string.byte(msg, pos, pos + count - 1) } blk[count + 1] = 1 for k = count + 2, 17 do blk[k] = 0 end local m = limbsOf(blk) for i = 1, 10 do h[i] += m[i] end for k = 0, 9 do local sum = 0 for i = 0, k do sum += h[i + 1] * r[k - i + 1] end for i = k + 1, 9 do sum += h[i + 1] * r5[k - i + 11] end d[k + 1] = sum end local c = 0 for i = 1, 10 do local v = d[i] + c c = math.floor(v / LIMB) h[i] = v - c * LIMB end h[1] += c * 5 c = math.floor(h[1] / LIMB) h[1] -= c * LIMB h[2] += c pos += 16 end -- Fully carry, then subtract p once if h >= p. for _ = 1, 2 do local c = 0 for i = 1, 10 do local v = h[i] + c c = math.floor(v / LIMB) h[i] = v - c * LIMB end h[1] += c * 5 end local g: {number} = table.create(10, 0) local c = 5 for i = 1, 10 do local v = h[i] + c c = math.floor(v / LIMB) g[i] = v - c * LIMB end if c > 0 then h = g end local hb: {number} = {} local acc, bits = 0, 0 for i = 1, 10 do acc += h[i] * 2 ^ bits bits += 13 while bits >= 8 do table.insert(hb, acc % 256) acc = math.floor(acc / 256) bits -= 8 end end local out: {number} = table.create(16, 0) local carry = 0 for i = 1, 16 do local v = hb[i] + string.byte(key, 16 + i) + carry out[i] = v % 256 carry = math.floor(v / 256) end return string.char(table.unpack(out)) end local function pad16(len: number): string return string.rep("\0", (16 - len % 16) % 16) end local function le64(n: number): string local t: {number} = table.create(8, 0) for i = 1, 8 do t[i] = n % 256 n = math.floor(n / 256) end return string.char(table.unpack(t)) end local function tagFor(key: string, nonce: string, aad: string, ct: string): string local polyKey = string.sub(chachaXor(key, 0, nonce, string.rep("\0", 32)), 1, 32) return poly1305(polyKey, aad .. pad16(#aad) .. ct .. pad16(#ct) .. le64(#aad) .. le64(#ct)) end function Crypto.seal(key: string, nonce: string, aad: string, plaintext: string): string local ct = chachaXor(key, 1, nonce, plaintext) return nonce .. ct .. tagFor(key, nonce, aad, ct) end function Crypto.open(key: string, aad: string, sealed: string): string? if #sealed < NONCE_LEN + TAG_LEN then return nil end local nonce = string.sub(sealed, 1, NONCE_LEN) local ct = string.sub(sealed, NONCE_LEN + 1, #sealed - TAG_LEN) local tag = string.sub(sealed, #sealed - TAG_LEN + 1) local expect = tagFor(key, nonce, aad, ct) local diff = 0 for i = 1, TAG_LEN do diff = bit32.bor(diff, bit32.bxor(string.byte(tag, i), string.byte(expect, i))) end if diff ~= 0 then return nil end return chachaXor(key, 1, nonce, ct) end -- Base64 --------------------------------------------------------------------- local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" local DECODE: {[number]: number} = {} for i = 1, 64 do DECODE[string.byte(ALPHABET, i)] = i - 1 end function Crypto.b64encode(s: string): string local out: {string} = {} for i = 1, #s, 3 do local a, b, c = string.byte(s, i, i + 2) local v = a * 65536 + (b or 0) * 256 + (c or 0) local q1 = bit32.extract(v, 18, 6) + 1 local q2 = bit32.extract(v, 12, 6) + 1 local q3 = bit32.extract(v, 6, 6) + 1 local q4 = bit32.extract(v, 0, 6) + 1 table.insert( out, string.sub(ALPHABET, q1, q1) .. string.sub(ALPHABET, q2, q2) .. (if b then string.sub(ALPHABET, q3, q3) else "=") .. (if c then string.sub(ALPHABET, q4, q4) else "=") ) end return table.concat(out) end function Crypto.b64decode(s: string): string? local n = #s if n % 4 ~= 0 then return nil end local out: {string} = {} for i = 1, n, 4 do local c1, c2, c3, c4 = string.byte(s, i, i + 3) local last = i + 3 == n local v1, v2 = DECODE[c1], DECODE[c2] if v1 == nil or v2 == nil then return nil end local pad = 0 local v3, v4 if last and c4 == 61 then pad = 1 v4 = 0 if c3 == 61 then pad = 2 v3 = 0 else v3 = DECODE[c3] end else v3, v4 = DECODE[c3], DECODE[c4] end if v3 == nil or v4 == nil then return nil end -- Non-zero bits under the padding are rejected, as the Rust decoder does. if (pad == 2 and v2 % 16 ~= 0) or (pad == 1 and v3 % 4 ~= 0) then return nil end local v = v1 * 262144 + v2 * 4096 + v3 * 64 + v4 local a, b, c = bit32.extract(v, 16, 8), bit32.extract(v, 8, 8), bit32.extract(v, 0, 8) if pad == 0 then table.insert(out, string.char(a, b, c)) elseif pad == 1 then table.insert(out, string.char(a, b)) else table.insert(out, string.char(a)) end end return table.concat(out) end function Crypto.sealB64(key: string, nonce: string, aad: string, plaintext: string): string return Crypto.b64encode(Crypto.seal(key, nonce, aad, plaintext)) end function Crypto.openB64(key: string, aad: string, b64: string): string? local bytes = Crypto.b64decode(b64) if bytes == nil then return nil end return Crypto.open(key, aad, bytes) end -- AADs and journal tag ------------------------------------------------------- local function int(n: number): string return string.format("%.0f", n) end function Crypto.stateAad(room: string, userId: number, client: string): string return "rbx-presence/state/v2\n" .. room .. "\n" .. int(userId) .. "\n" .. client end function Crypto.journalAad(room: string, userId: number, at: number): string return "rbx-presence/journal/v2\n" .. room .. "\n" .. int(userId) .. "\n" .. int(at) end function Crypto.opsAad(room: string, userId: number, client: string): string return "rbx-presence/ops/v2\n" .. room .. "\n" .. int(userId) .. "\n" .. client end function Crypto.journalTag(key: string, path: string, property: string, at: number): string return string.sub(Crypto.hex(hmacRaw(key, path .. "\n" .. property .. "\n" .. int(at))), 1, 16) end return Crypto Defaults --!strict -- Generated by build_presence_plugin; do not edit. return { communityUrl = "https://ntfy.sh", communityKind = "hosted", communityLabel = "hosted by an rbx-native collaborator through a Cloudflare quick tunnel; no account needed", } DiffWindow --!strict --// "Changes while you were away", drawn like rbx-native's diff window: a --// header with the change counts as filters and a search, the changed --// instances on the left (their Explorer icons, grouped as rbx-native --// groups them), and the selected one on the right, its class, its path, --// and a table of the properties collaborators edited, with each one's --// current value, who edited it and when. --// --// "Value" is the one the newest edit set, as the editor showed it (sealed --// in the journal), so an edit a collaborator never saved still shows what --// they set; an older peer's entry has none, and the place's own value is --// shown. An instance that no longer resolves (renamed or deleted since) is --// listed as "Not found". local Changes = require(script.Parent.Changes) local Paths = require(script.Parent.Paths) local Ui = require(script.Parent.Ui) local DiffWindow = {} DiffWindow.__index = DiffWindow local LIST_WIDTH = 300 local FOOTER_NOTE = "Values are the ones collaborators set; who edited what comes from the presence journal." type Filter = "all" | "updated" | "missing" export type DiffWindow = typeof(setmetatable({} :: { gui: DockWidgetPluginGui, changes: { Changes.Change }, found: { [string]: Instance }, filter: Filter, query: string, selected: string?, subtitle: TextLabel, segments: { [Filter]: TextButton }, counts: { [Filter]: TextLabel }, searchBox: TextBox, list: ScrollingFrame, detail: Frame, }, DiffWindow)) -- A value as the Properties panel words it, and a swatch for a colour. local function format(value: any): (string, Color3?) local kind = typeof(value) if value == nil then return "—", nil elseif kind == "boolean" then return tostring(value), nil elseif kind == "number" then local text = string.format("%.3f", value) text = string.gsub(text, "0+$", "") text = string.gsub(text, "%.$", "") return text, nil elseif kind == "string" then local first = string.match(value, "^[^\n]*") or "" if #first > 60 or #first < #value then return `"{string.sub(first, 1, 60)}…"`, nil end return `"{value}"`, nil elseif kind == "Vector3" then local v = value :: Vector3 return `({format(v.X)}, {format(v.Y)}, {format(v.Z)})`, nil elseif kind == "Vector2" then local v = value :: Vector2 return `({format(v.X)}, {format(v.Y)})`, nil elseif kind == "CFrame" then local p = (value :: CFrame).Position return `pos=({format(p.X)}, {format(p.Y)}, {format(p.Z)})`, nil elseif kind == "Color3" then local c = value :: Color3 return `({math.round(c.R * 255)}, {math.round(c.G * 255)}, {math.round(c.B * 255)})`, c elseif kind == "BrickColor" then return (value :: BrickColor).Name, (value :: BrickColor).Color elseif kind == "EnumItem" then return (value :: EnumItem).Name, nil elseif kind == "Instance" then return (value :: Instance).Name, nil end return tostring(value), nil end local function read(instance: Instance?, property: string): (boolean, any) if not instance then return false, nil end return pcall(function() return (instance :: any)[property] end) end local function sectionLabel(parent: Instance, text: string, count: number?, order: number, ink: string?) local row = Ui.frame(parent, order) row.Size = UDim2.new(1, 0, 0, 14) row.AutomaticSize = Enum.AutomaticSize.None Ui.list(row, 6, true) if ink then Ui.dot(row, Ui.color(ink)).LayoutOrder = 0 end local label = Ui.text(row, text, { size = Ui.TEXT_XXS, color = "text3", font = Ui.FONT_BOLD, wrap = false, order = 1 }) label.AutomaticSize = Enum.AutomaticSize.X label.Size = UDim2.fromOffset(0, 14) if count then local n = Ui.text(row, tostring(count), { size = Ui.TEXT_XXS, color = "text3", font = Ui.FONT_MONO, wrap = false, order = 2 }) n.AutomaticSize = Enum.AutomaticSize.X n.Size = UDim2.fromOffset(0, 14) end end local function hairline(parent: Instance, vertical: boolean, position: UDim2, inset: number?) local line = Instance.new("Frame") line.Size = if vertical then UDim2.new(0, 1, 1, -(inset or 0)) else UDim2.new(1, 0, 0, 1) line.Position = position Ui.fill(line, "border") line.Parent = parent end function DiffWindow.new(plugin: Plugin): DiffWindow local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, 1000, 640, 560, 420) local gui = plugin:CreateDockWidgetPluginGuiAsync("RbxNativePresenceChanges", info) gui.Title = "Changes while you were away" gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling local root = Instance.new("Frame") root.Size = UDim2.fromScale(1, 1) Ui.fill(root, "dock") root.Parent = gui -- Header: tile, title and subtitle, the filters, the search. 61 tall. local header = Instance.new("Frame") header.Size = UDim2.new(1, 0, 0, 60) header.BackgroundTransparency = 1 header.Parent = root hairline(root, false, UDim2.fromOffset(0, 60)) Ui.pad(header, 12, 16) local tile = Instance.new("TextLabel") tile.Size = UDim2.fromOffset(28, 28) tile.Position = UDim2.new(0, 0, 0.5, -14) Ui.fill(tile, "accent_soft") Ui.corner(tile, Ui.RADIUS_TILE) tile.FontFace = Ui.FONT_BOLD tile.TextSize = 15 tile.TextColor3 = Ui.color("check_on") tile.Text = "Δ" tile.Parent = header local titles = Instance.new("Frame") titles.BackgroundTransparency = 1 titles.Position = UDim2.fromOffset(38, 0) titles.Size = UDim2.new(1, -38 - 470, 1, 0) titles.Parent = header Ui.list(titles, 1) local titleList = titles:FindFirstChildOfClass("UIListLayout") :: UIListLayout titleList.VerticalAlignment = Enum.VerticalAlignment.Center Ui.text(titles, "Review changes", { size = Ui.TEXT_LG, font = Ui.FONT_BOLD, wrap = false, order = 1 }) local subtitle = Ui.text(titles, "", { size = Ui.TEXT_SM, color = "text2", wrap = false, order = 2 }) local controls = Instance.new("Frame") controls.BackgroundTransparency = 1 controls.AnchorPoint = Vector2.new(1, 0.5) controls.Position = UDim2.fromScale(1, 0.5) controls.Size = UDim2.fromOffset(460, 28) controls.Parent = header local controlsList = Ui.list(controls, 12, true) controlsList.HorizontalAlignment = Enum.HorizontalAlignment.Right local segmentsFrame = Instance.new("Frame") segmentsFrame.LayoutOrder = 1 segmentsFrame.AutomaticSize = Enum.AutomaticSize.X segmentsFrame.Size = UDim2.fromOffset(0, 26) Ui.fill(segmentsFrame, "field_select") Ui.corner(segmentsFrame) Ui.pad(segmentsFrame, 2) Ui.list(segmentsFrame, 2, true) segmentsFrame.Parent = controls local search = Instance.new("Frame") search.LayoutOrder = 2 search.Size = UDim2.fromOffset(200, 28) Ui.fill(search, "field_select") Ui.corner(search) Ui.stroke(search, "border") search.Parent = controls local searchBox = Instance.new("TextBox") searchBox.BackgroundTransparency = 1 searchBox.Position = UDim2.fromOffset(10, 0) searchBox.Size = UDim2.new(1, -20, 1, 0) searchBox.ClearTextOnFocus = false searchBox.PlaceholderText = "Filter changes" searchBox.PlaceholderColor3 = Ui.color("text3") searchBox.Text = "" searchBox.TextXAlignment = Enum.TextXAlignment.Left searchBox.FontFace = Ui.FONT searchBox.TextSize = Ui.TEXT_SM searchBox.TextColor3 = Ui.color("text") searchBox.Parent = search -- Body: the list pane and the detail. local list = Instance.new("ScrollingFrame") list.Position = UDim2.fromOffset(0, 61) list.Size = UDim2.new(0, LIST_WIDTH, 1, -61 - 57) list.BackgroundTransparency = 1 list.BorderSizePixel = 0 list.CanvasSize = UDim2.new() list.AutomaticCanvasSize = Enum.AutomaticSize.Y list.ScrollBarThickness = 6 list.ScrollBarImageColor3 = Ui.color("text3") list.ScrollingDirection = Enum.ScrollingDirection.Y list.Parent = root -- Between the header's hairline and the footer's. hairline(root, true, UDim2.new(0, LIST_WIDTH, 0, 61), 61 + 57) local detail = Instance.new("Frame") detail.Position = UDim2.fromOffset(LIST_WIDTH + 1, 61) detail.Size = UDim2.new(1, -LIST_WIDTH - 1, 1, -61 - 57) detail.BackgroundTransparency = 1 detail.Parent = root -- Footer: the note and Close. 57 tall with its hairline. local footer = Instance.new("Frame") footer.AnchorPoint = Vector2.new(0, 1) footer.Position = UDim2.fromScale(0, 1) footer.Size = UDim2.new(1, 0, 0, 56) footer.BackgroundTransparency = 1 footer.Parent = root hairline(root, false, UDim2.new(0, 0, 1, -57)) Ui.pad(footer, 0, 16) local note = Ui.text(footer, FOOTER_NOTE, { size = Ui.TEXT_SM, color = "text3", wrap = false }) note.Size = UDim2.new(1, -120, 1, 0) local close = Ui.button(footer, "Close", true) close.Size = UDim2.fromOffset(96, 34) close.AnchorPoint = Vector2.new(1, 0.5) close.Position = UDim2.fromScale(1, 0.5) close.Activated:Connect(function() gui.Enabled = false end) local self: DiffWindow = setmetatable({ gui = gui, changes = {}, found = {}, filter = "all" :: Filter, query = "", selected = nil, subtitle = subtitle, segments = {}, counts = {}, searchBox = searchBox, list = list, detail = detail, }, DiffWindow) for index, pair in { { "all", "All" }, { "updated", "Updated" }, { "missing", "Not found" } } do local filter, label = pair[1] :: Filter, pair[2] local button = Instance.new("TextButton") button.AutoButtonColor = false button.LayoutOrder = index button.AutomaticSize = Enum.AutomaticSize.X button.Size = UDim2.fromOffset(0, 22) button.Text = "" Ui.corner(button, 3) Ui.pad(button, 0, 10, 0, 10) Ui.list(button, 5, true) local name = Ui.text(button, label, { size = Ui.TEXT_XS, wrap = false, order = 1 }) name.AutomaticSize = Enum.AutomaticSize.X name.Size = UDim2.fromOffset(0, 22) name.Name = "Label" local count = Ui.text(button, "0", { size = Ui.TEXT_XXS, font = Ui.FONT_MONO, color = "text3", wrap = false, order = 2 }) count.AutomaticSize = Enum.AutomaticSize.X count.Size = UDim2.fromOffset(0, 22) button.Parent = segmentsFrame button.Activated:Connect(function() self.filter = filter self:render() end) self.segments[filter] = button self.counts[filter] = count end searchBox:GetPropertyChangedSignal("Text"):Connect(function() self.query = string.lower(searchBox.Text) self:render() end) return self end -- Shows `entries` (a journal) and raises the window. function DiffWindow.show(self: DiffWindow, entries: { any }) self.changes = Changes.fromJournal(entries) table.clear(self.found) for _, change in self.changes do local instance = Paths.resolve(game, change.path) if instance then self.found[change.path] = instance end end self.selected = nil self.gui.Enabled = true self:render() end function DiffWindow.visible(self: DiffWindow): { Changes.Change } local out = {} for _, change in self.changes do local missing = self.found[change.path] == nil local passes = self.filter == "all" or (self.filter == "updated" and not missing) or (self.filter == "missing" and missing) local matches = self.query == "" or string.find(string.lower(change.path), self.query, 1, true) ~= nil if passes and matches then table.insert(out, change) end end return out end function DiffWindow.render(self: DiffWindow) local updated, missing = 0, 0 for _, change in self.changes do if self.found[change.path] then updated += 1 else missing += 1 end end local total = updated + missing self.subtitle.Text = `{total} {if total == 1 then "change" else "changes"} by collaborators since you were last here` local counts: { [Filter]: number } = { all = total, updated = updated, missing = missing } for filter, button in self.segments do local active = self.filter == filter local enabled = counts[filter] > 0 Ui.fill(button, if active then "accent_soft" else "field_select") local label = button:FindFirstChild("Label") :: TextLabel label.TextColor3 = Ui.color(if active then "check_on" elseif enabled then "text2" else "text3") label.FontFace = if active then Ui.FONT_SEMIBOLD else Ui.FONT self.counts[filter].Text = tostring(counts[filter]) self.counts[filter].TextColor3 = Ui.color(if active then "check_on" else "text3") end local visible = self:visible() local stillVisible = false for _, change in visible do if change.path == self.selected then stillVisible = true end end if not stillVisible then self.selected = if #visible > 0 then visible[1].path else nil end self:renderList(visible) self:renderDetail() end function DiffWindow.renderList(self: DiffWindow, visible: { Changes.Change }) for _, child in self.list:GetChildren() do child:Destroy() end Ui.pad(self.list, 10, 8, 12, 8) Ui.list(self.list, 2) local groups = { { title = "UPDATES", ink = "check_on", marker = "~", missing = false }, { title = "NOT FOUND", ink = "text_error", marker = "−", missing = true }, } local order = 0 for _, group in groups do local members = {} for _, change in visible do if (self.found[change.path] == nil) == group.missing then table.insert(members, change) end end if #members > 0 then order += 1 local head = Ui.frame(self.list, order) Ui.pad(head, 6, 6, 4, 8) sectionLabel(head, group.title, #members, 1, group.ink) for _, change in members do order += 1 self:listRow(change, group.marker, order) end end end if order == 0 then Ui.text(self.list, "No changes match.", { size = Ui.TEXT_SM, color = "text3" }) end end function DiffWindow.listRow(self: DiffWindow, change: Changes.Change, marker: string, order: number) local selected = self.selected == change.path local row = Instance.new("TextButton") row.AutoButtonColor = false row.Text = "" row.LayoutOrder = order row.Size = UDim2.new(1, 0, 0, 40) Ui.fill(row, if selected then "ribbon_tab_active" else "dock") if not selected then row.BackgroundTransparency = 1 end Ui.corner(row) row.Parent = self.list local mark = Ui.text(row, marker, { size = Ui.TEXT_SM, color = "text3", font = Ui.FONT_MONO, wrap = false }) mark.Position = UDim2.fromOffset(10, 0) mark.Size = UDim2.new(0, 10, 1, 0) local instance = self.found[change.path] local icon = Ui.classIcon(row, if instance then instance.ClassName else "Folder", 16) icon.Position = UDim2.new(0, 26, 0.5, -8) local name = Ui.text(row, change.name, { size = Ui.TEXT_SM, font = if selected then Ui.FONT_SEMIBOLD else Ui.FONT, wrap = false }) name.Position = UDim2.fromOffset(50, 4) name.Size = UDim2.new(1, -120, 0, 16) local path = Ui.text(row, change.parent, { size = Ui.TEXT_XS, color = "text3", wrap = false }) path.Position = UDim2.fromOffset(50, 21) path.Size = UDim2.new(1, -120, 0, 14) local n = #change.properties local count = Ui.text(row, `{n} {if n == 1 then "prop" else "props"}`, { size = Ui.TEXT_XS, color = "text3", font = Ui.FONT_MONO, wrap = false }) count.AnchorPoint = Vector2.new(1, 0) count.Position = UDim2.new(1, -10, 0, 0) count.Size = UDim2.new(0, 60, 1, 0) count.TextXAlignment = Enum.TextXAlignment.Right row.MouseEnter:Connect(function() if self.selected ~= change.path then Ui.fill(row, "hover") end end) row.MouseLeave:Connect(function() if self.selected ~= change.path then row.BackgroundTransparency = 1 end end) row.Activated:Connect(function() self.selected = change.path self:render() end) end function DiffWindow.renderDetail(self: DiffWindow) for _, child in self.detail:GetChildren() do child:Destroy() end local change: Changes.Change? = nil for _, c in self.changes do if c.path == self.selected then change = c end end if not change then local empty = Ui.text(self.detail, "No change selected", { size = Ui.TEXT_MD, color = "text3", wrap = false }) empty.Size = UDim2.fromScale(1, 1) empty.TextXAlignment = Enum.TextXAlignment.Center return end local c = change :: Changes.Change local instance = self.found[c.path] -- Header: icon tile, name, class badge, kind pill, stats; the path under it. local head = Instance.new("Frame") head.Size = UDim2.new(1, 0, 0, 74) head.BackgroundTransparency = 1 head.Parent = self.detail hairline(self.detail, false, UDim2.fromOffset(0, 74)) Ui.pad(head, 16, 20, 14, 20) local top = Instance.new("Frame") top.Size = UDim2.new(1, 0, 0, 28) top.BackgroundTransparency = 1 top.Parent = head Ui.list(top, 10, true) local tile = Instance.new("Frame") tile.Size = UDim2.fromOffset(28, 28) tile.LayoutOrder = 1 Ui.fill(tile, "field_select") Ui.corner(tile, Ui.RADIUS_TILE) tile.Parent = top local icon = Ui.classIcon(tile, if instance then instance.ClassName else "Folder", 16) icon.Position = UDim2.fromOffset(6, 6) local name = Ui.text(top, c.name, { size = 15, font = Ui.FONT_BOLD, wrap = false, order = 2 }) name.AutomaticSize = Enum.AutomaticSize.X name.Size = UDim2.fromOffset(0, 28) if instance then local badge = Instance.new("Frame") badge.LayoutOrder = 3 badge.AutomaticSize = Enum.AutomaticSize.X badge.Size = UDim2.fromOffset(0, 20) badge.BackgroundTransparency = 1 Ui.corner(badge, Ui.RADIUS_BADGE) Ui.stroke(badge, "border2") Ui.pad(badge, 0, 6, 0, 6) local cls = Ui.text(badge, instance.ClassName, { size = Ui.TEXT_XS, color = "text2", font = Ui.FONT_MONO, wrap = false }) cls.AutomaticSize = Enum.AutomaticSize.X cls.Size = UDim2.fromOffset(0, 20) badge.Parent = top Ui.pill(top, "Updated", "accent_soft", "check_on", 4) else Ui.pill(top, "Not found", "diff_remove_pill", "text_error", 4) end local n = #c.properties local stats = Ui.text(head, `{n} {if n == 1 then "property" else "properties"}`, { size = Ui.TEXT_XS, color = "text3", font = Ui.FONT_MONO, wrap = false }) stats.AnchorPoint = Vector2.new(1, 0) stats.Position = UDim2.fromScale(1, 0) stats.Size = UDim2.fromOffset(120, 28) stats.TextXAlignment = Enum.TextXAlignment.Right local path = Ui.text(head, c.path, { size = Ui.TEXT_XS, color = "text3", font = Ui.FONT_MONO, wrap = false }) path.Position = UDim2.fromOffset(38, 34) path.Size = UDim2.new(1, -38 - 80, 0, 16) if instance then local select = Ui.link(head, "Select") select.AnchorPoint = Vector2.new(1, 0) select.Position = UDim2.new(1, 0, 0, 34) select.Activated:Connect(function() game:GetService("Selection"):Set({ instance :: Instance }) end) end -- Body: PROPERTIES, then the table, scrolling when long. local body = Instance.new("ScrollingFrame") body.Position = UDim2.fromOffset(0, 75) body.Size = UDim2.new(1, 0, 1, -75) body.BackgroundTransparency = 1 body.BorderSizePixel = 0 body.CanvasSize = UDim2.new() body.AutomaticCanvasSize = Enum.AutomaticSize.Y body.ScrollBarThickness = 6 body.ScrollBarImageColor3 = Ui.color("text3") body.ScrollingDirection = Enum.ScrollingDirection.Y body.Parent = self.detail Ui.pad(body, 16, 20, 20, 20) Ui.list(body, 10) sectionLabel(body, "PROPERTIES", nil, 1) local tableFrame = Ui.frame(body, 2) Ui.corner(tableFrame) Ui.stroke(tableFrame, "border") Ui.list(tableFrame, 0) local function cells(row: Frame, height: number) local columns = { { UDim2.new(0, 170, 0, height), 0 }, { UDim2.new(1, -170 - 150 - 90, 0, height), 1 }, { UDim2.new(0, 150, 0, height), 2 }, { UDim2.new(0, 90, 0, height), 3 }, } local out = {} for _, col in columns do local cell = Instance.new("Frame") cell.BackgroundTransparency = 1 cell.Size = col[1] cell.LayoutOrder = col[2] cell.Parent = row local layout = Ui.list(cell, 0, true) layout.VerticalAlignment = Enum.VerticalAlignment.Center table.insert(out, cell) end return out end local headRow = Instance.new("Frame") headRow.Size = UDim2.new(1, 0, 0, 28) headRow.BackgroundTransparency = 1 headRow.LayoutOrder = 0 headRow.Parent = tableFrame Ui.pad(headRow, 0, 12) Ui.list(headRow, 0, true) local headCells = cells(headRow, 28) for i, title in { "PROPERTY", "VALUE", "EDITED BY", "WHEN" } do Ui.text(headCells[i], title, { size = Ui.TEXT_XXS, color = "text3", font = Ui.FONT_BOLD, wrap = false }) end local now = DateTime.now().UnixTimestampMillis for index, edit in c.properties do local line = Instance.new("Frame") line.Size = UDim2.new(1, 0, 0, 1) line.LayoutOrder = index * 2 - 1 Ui.fill(line, "border") line.Parent = tableFrame local row = Instance.new("Frame") row.Size = UDim2.new(1, 0, 0, 36) row.BackgroundTransparency = 1 row.LayoutOrder = index * 2 row.Parent = tableFrame Ui.pad(row, 0, 12) Ui.list(row, 0, true) local cols = cells(row, 36) Ui.text(cols[1], edit.property, { size = Ui.TEXT_SM, wrap = false }) local ok, value = read(instance, edit.property) if edit.value ~= "" then Ui.chip(cols[2], edit.value) elseif ok then local text, swatch = format(value) Ui.chip(cols[2], text, swatch) else Ui.text(cols[2], "—", { size = Ui.TEXT_SM, color = "text3", wrap = false }) end local by = table.concat(edit.authors, ", ") if edit.edits > 1 then by ..= ` ({edit.edits} edits)` end Ui.text(cols[3], by, { size = Ui.TEXT_XS, color = "text2", wrap = false }) Ui.text(cols[4], Changes.ago(edit.at, now), { size = Ui.TEXT_XS, color = "text3", font = Ui.FONT_MONO, wrap = false }) end end function DiffWindow.destroy(self: DiffWindow) self.gui:Destroy() end return DiffWindow Edits --!strict --! Tracks which property changes were made by the local user. Studio fires --! property signals for replicated changes too, so only changes inside a --! committed ChangeHistory recording (or just after an undo/redo) count. --! Times are passed in, which keeps the logic testable without Studio. local Schema = require(script.Parent.Schema) local Edits = {} Edits.UNDO_WINDOW_MS = 250 Edits.STALE_RECORDING_MS = 10000 Edits.KEEP_MS = 60000 Edits.MERGE_MS = 2000 local MAX_VALUE = 80 local Tracker = {} Tracker.__index = Tracker function Edits.new(): any return setmetatable({ depth = 0, startedAt = 0, buffer = {}, undoUntil = -math.huge, loose = {}, noted = {}, }, Tracker) end local function dropStale(self: any, now: number) if self.depth > 0 and now - self.startedAt > Edits.STALE_RECORDING_MS then self.depth = 0 self.buffer = {} end end local function note(self: any, edit: any) local value = edit.value if type(value) ~= "string" then value = tostring(value) end local text: string = value if #text > MAX_VALUE then text = text:sub(1, MAX_VALUE) end for _, existing in ipairs(self.noted) do if existing.path == edit.path and existing.property == edit.property and edit.at - existing.at <= Edits.MERGE_MS then existing.value = text existing.at = edit.at return end end table.insert(self.noted, { path = edit.path, property = edit.property, value = text, at = edit.at }) while #self.noted > Schema.MAX_EDITS do table.remove(self.noted, 1) end end function Tracker.recordingStarted(self: any, now: number) dropStale(self, now) if self.depth == 0 then self.startedAt = now self.buffer = {} end self.depth += 1 end function Tracker.recordingFinished(self: any, committed: boolean, now: number) dropStale(self, now) if self.depth == 0 then return end self.depth -= 1 if self.depth > 0 then return end local buffered = self.buffer self.buffer = {} if committed then for _, edit in ipairs(buffered) do note(self, edit) end end end -- Undo and redo apply their changes around the moment OnUndo/OnRedo fires, -- and the order of the two signals is not documented, so changes from just -- before it count as well as changes just after. function Tracker.undoRedo(self: any, now: number) self.undoUntil = now + Edits.UNDO_WINDOW_MS for _, edit in ipairs(self.loose) do if now - edit.at <= Edits.UNDO_WINDOW_MS then note(self, edit) end end self.loose = {} end function Tracker.changed(self: any, path: string, property: string, value: any, now: number) dropStale(self, now) local edit = { path = path, property = property, value = value, at = now } if self.depth > 0 then table.insert(self.buffer, edit) elseif now <= self.undoUntil then note(self, edit) else -- Unattributed for now; an undo or redo signalled just after claims it. local kept = {} for _, old in ipairs(self.loose) do if now - old.at <= Edits.UNDO_WINDOW_MS then table.insert(kept, old) end end table.insert(kept, edit) self.loose = kept end end function Tracker.recent(self: any, now: number): {any} local out: {any} = {} for _, edit in ipairs(self.noted) do if now - edit.at < Edits.KEEP_MS then table.insert(out, { path = edit.path, property = edit.property, value = edit.value, at = edit.at }) end end return out end return Edits Hmac --!strict -- HMAC-SHA256 (RFC 2104) with the 64-byte SHA-256 block size. local Sha256 = require(script.Parent.Sha256) local BLOCK = 64 local Hmac = {} function Hmac.sha256Hex(key: string, msg: string): string if #key > BLOCK then key = Sha256.raw(key) end key = key .. string.rep("\0", BLOCK - #key) local inner: {string} = {} local outer: {string} = {} for i = 1, BLOCK do local b = string.byte(key, i) inner[i] = string.char(bit32.bxor(b, 0x36)) outer[i] = string.char(bit32.bxor(b, 0x5c)) end local innerHash = Sha256.raw(table.concat(inner) .. msg) return Sha256.hex(table.concat(outer) .. innerHash) end return Hmac Json --!strict --! Minimal deterministic JSON codec: sorted object keys and canonical number --! formatting so both ends of the relay produce byte-identical text. local Json = {} Json.null = setmetatable({}, { __tostring = function() return "null" end, }) local arrays: {[any]: boolean} = setmetatable({}, { __mode = "k" }) :: any function Json.array(t: {any}?): {any} local result: {any} = t or {} arrays[result] = true return result end function Json.isArray(t: any): boolean if arrays[t] then return true end local n = #t if n == 0 then return false end local count = 0 for _ in pairs(t) do count += 1 end return count == n end local ESCAPES: {[string]: string} = { ['"'] = '\\"', ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t", } local function encodeString(s: string): string local escaped = s:gsub('[%c"\\]', function(c: string): string return ESCAPES[c] or string.format("\\u%04x", string.byte(c)) end) return '"' .. escaped .. '"' end local function encodeNumber(n: number): string if n ~= n or n == math.huge or n == -math.huge then error("cannot encode non-finite number") end if n == 0 then return "0" end if n % 1 == 0 and math.abs(n) < 2 ^ 53 then return string.format("%.0f", n) end local short = string.format("%.14g", n) if tonumber(short) == n then return short end return string.format("%.17g", n) end local function encodeValue(value: any, out: {string}) local kind = type(value) if value == Json.null or value == nil then table.insert(out, "null") elseif kind == "boolean" then table.insert(out, if value then "true" else "false") elseif kind == "number" then table.insert(out, encodeNumber(value)) elseif kind == "string" then table.insert(out, encodeString(value)) elseif kind == "table" then if Json.isArray(value) then table.insert(out, "[") for i = 1, #value do if i > 1 then table.insert(out, ",") end encodeValue(value[i], out) end table.insert(out, "]") else local keys: {string} = {} for k in pairs(value) do if type(k) ~= "string" then error("object keys must be strings") end table.insert(keys, k) end table.sort(keys) table.insert(out, "{") for i, k in ipairs(keys) do if i > 1 then table.insert(out, ",") end table.insert(out, encodeString(k)) table.insert(out, ":") encodeValue(value[k], out) end table.insert(out, "}") end else error("cannot encode value of type " .. kind) end end function Json.encode(value: any): string local out: {string} = {} encodeValue(value, out) return table.concat(out) end local function fail(text: string, pos: number, what: string): never error(string.format("json: %s at byte %d", what, pos), 0) end local function skip(text: string, pos: number): number local _, e = text:find("^[ \t\r\n]*", pos) return (e :: number) + 1 end local SIMPLE: {[string]: string} = { ['"'] = '"', ["\\"] = "\\", ["/"] = "/", b = "\b", f = "\f", n = "\n", r = "\r", t = "\t", } local function parseHex4(text: string, pos: number): number local hex = text:match("^%x%x%x%x", pos) if not hex then fail(text, pos, "bad unicode escape") end return tonumber(hex, 16) :: number end local function parseString(text: string, pos: number): (string, number) local parts: {string} = {} local i = pos + 1 while true do local s, e = text:find('^[^"\\%c]+', i) if s then table.insert(parts, text:sub(s, e)) i = (e :: number) + 1 end local c = text:sub(i, i) if c == '"' then return table.concat(parts), i + 1 elseif c == "\\" then local esc = text:sub(i + 1, i + 1) if esc == "u" then local code = parseHex4(text, i + 2) i += 6 if code >= 0xD800 and code <= 0xDBFF then if text:sub(i, i + 1) ~= "\\u" then fail(text, i, "lone surrogate") end local low = parseHex4(text, i + 2) if low < 0xDC00 or low > 0xDFFF then fail(text, i, "bad surrogate pair") end code = 0x10000 + (code - 0xD800) * 0x400 + (low - 0xDC00) i += 6 elseif code >= 0xDC00 and code <= 0xDFFF then fail(text, i, "lone surrogate") end table.insert(parts, utf8.char(code)) else local simple = SIMPLE[esc] if not simple then fail(text, i, "bad escape") end table.insert(parts, simple) i += 2 end else fail(text, i, "unterminated or invalid string") end end end local parseValue local function parseNumber(text: string, pos: number): (number, number) local literal = text:match("^-?%d+%.?%d*[eE]?[+-]?%d*", pos) local n = literal and tonumber(literal) if not literal or not n then fail(text, pos, "bad number") end return n :: number, pos + #(literal :: string) end function parseValue(text: string, pos: number): (any, number) pos = skip(text, pos) local c = text:sub(pos, pos) if c == "{" then local obj: {[string]: any} = {} pos = skip(text, pos + 1) if text:sub(pos, pos) == "}" then return obj, pos + 1 end while true do pos = skip(text, pos) if text:sub(pos, pos) ~= '"' then fail(text, pos, "expected object key") end local key key, pos = parseString(text, pos) pos = skip(text, pos) if text:sub(pos, pos) ~= ":" then fail(text, pos, "expected ':'") end local value value, pos = parseValue(text, pos + 1) obj[key] = value pos = skip(text, pos) local d = text:sub(pos, pos) if d == "}" then return obj, pos + 1 elseif d ~= "," then fail(text, pos, "expected ',' or '}'") end pos += 1 end elseif c == "[" then local arr = Json.array() pos = skip(text, pos + 1) if text:sub(pos, pos) == "]" then return arr, pos + 1 end while true do local value value, pos = parseValue(text, pos) table.insert(arr, value) pos = skip(text, pos) local d = text:sub(pos, pos) if d == "]" then return arr, pos + 1 elseif d ~= "," then fail(text, pos, "expected ',' or ']'") end pos += 1 end elseif c == '"' then return parseString(text, pos) elseif text:sub(pos, pos + 3) == "true" then return true, pos + 4 elseif text:sub(pos, pos + 4) == "false" then return false, pos + 5 elseif text:sub(pos, pos + 3) == "null" then return Json.null, pos + 4 elseif c == "-" or c:match("%d") then return parseNumber(text, pos) end fail(text, pos, "unexpected character") end function Json.decode(text: string): any local value, pos = parseValue(text, 1) pos = skip(text, pos) if pos <= #text then fail(text, pos, "trailing data") end return value end return Json Markers --!strict --// Renders rbx-native collaborators' selections and cameras inside Studio. --// Only "rbx-native" users are drawn: Studio already shows Team Create peers. local Paths = require(script.Parent.Paths) local FOLDER_NAME = "RbxNativePresence" local Markers = {} Markers.__index = Markers export type Markers = typeof(setmetatable({} :: { folder: Folder?, camera: Camera?, items: { [string]: Instance }, }, Markers)) local function parseColor(hex: string): Color3 local r, g, b = string.match(hex, "^#(%x%x)(%x%x)(%x%x)$") if not r then return Color3.new(1, 1, 1) end return Color3.fromRGB(tonumber(r, 16) :: number, tonumber(g, 16) :: number, tonumber(b, 16) :: number) end local function tag<T>(instance: T & Instance): T instance.Archivable = false return instance end function Markers.new(): Markers return setmetatable({ folder = nil, camera = nil, items = {} }, Markers) end function Markers.ensureFolder(self: Markers): Folder? local camera = workspace.CurrentCamera if not camera then return nil end if self.camera ~= camera or not self.folder or self.folder.Parent ~= camera then self:clear() local folder = Instance.new("Folder") folder.Name = FOLDER_NAME folder.Archivable = false folder.Parent = camera self.folder = folder self.camera = camera end return self.folder end function Markers.clear(self: Markers) if self.folder then self.folder:Destroy() end self.folder = nil self.camera = nil table.clear(self.items) end local function makeCamera(folder: Folder, user: any): Part local part = Instance.new("Part") part.Archivable = false part.Anchored = true part.CanCollide = false part.CanQuery = false part.CanTouch = false part.Locked = true part.Size = Vector3.new(1, 0.6, 1.5) part.Material = Enum.Material.Neon part.Transparency = 0.3 part.Name = "Camera" local gui = Instance.new("BillboardGui") gui.Archivable = false gui.Size = UDim2.fromOffset(120, 24) gui.StudsOffset = Vector3.new(0, 2, 0) gui.AlwaysOnTop = true gui.Adornee = part gui.Parent = part local label = Instance.new("TextLabel") label.Archivable = false label.Name = "Label" label.Size = UDim2.fromScale(1, 1) label.BackgroundTransparency = 1 label.Font = Enum.Font.BuilderSansBold label.TextSize = 16 label.TextStrokeTransparency = 0.5 label.Parent = gui part.Parent = folder return part end function Markers.render(self: Markers, users: { any }) local folder = self:ensureFolder() if not folder then return end local wanted: { [string]: boolean } = {} for _, user in users do -- Only rbx-native senders: Studio draws its own collaborators. The -- signed-in user in rbx-native is drawn too (another machine). if user.client == "rbx-native" then local color = parseColor(user.color) for index, path in user.selection do local target = Paths.resolve(game, path) if target then local key = `{user.userId}/sel/{index}` wanted[key] = true local box = self.items[key] :: SelectionBox? if not box then local created = Instance.new("SelectionBox") created.Archivable = false created.LineThickness = 0.05 created.SurfaceTransparency = 1 created.Parent = folder self.items[key] = created box = created end local b = box :: SelectionBox b.Adornee = target b.Color3 = color end end if user.camera then local key = `{user.userId}/cam` wanted[key] = true local part = self.items[key] :: Part? if not part then part = makeCamera(folder, user) self.items[key] = part :: Part end local p = part :: Part p.Color = color p.CFrame = CFrame.new(table.unpack(user.camera)) local label = p:FindFirstChild("Label", true) :: TextLabel? if label then label.Text = user.name label.TextColor3 = color end end end end for key, instance in self.items do if not wanted[key] then instance:Destroy() self.items[key] = nil end end end function Markers.destroy(self: Markers) self:clear() end return Markers Ntfy --!strict --! The ntfy wire format, mirrored from crates/rbx_presence/src/ntfy.rs: one --! user's signed presence, wrapped for a plain ntfy pub/sub server and --! verified by peers. ntfy does no auth of its own, so trust is entirely the --! HMAC key both sides derive from the place secret. A receiver signs over --! the message text exactly as received, so Studio and rbx-native senders --! interoperate without agreeing on JSON byte order. Pure (no HttpService), --! so it runs in the shared Luau golden tests. local Auth = require(script.Parent.Auth) local Json = require(script.Parent.Json) local Schema = require(script.Parent.Schema) local Ntfy = {} Ntfy.CLOCK_SKEW_MS = 60000 -- The canonical path a topic's messages are signed under. function Ntfy.signPath(topic: string): string return "/" .. topic end -- A signed envelope string for `state` published to `topic`. function Ntfy.encode( key: string, encKey: string, room: string, topic: string, userId: number, ts: number, state: any, nonceFor: () -> string ): string local d = Schema.sealState(encKey, room, state, nonceFor) local sig = Auth.sign(key, Auth.canonical("POST", Ntfy.signPath(topic), userId, ts, d)) return Json.encode({ u = userId, t = ts, s = sig, d = d }) end Ntfy.MAX_BODY = 4096 -- Like encode, but sheds detail until the envelope fits MAX_BODY: oldest -- edits, then selection, then camera, then script. If even the identity is -- too big, the smallest encoding is returned. function Ntfy.fitEncode( key: string, encKey: string, room: string, topic: string, userId: number, ts: number, state: any, nonceFor: () -> string ): string local copy = table.clone(state) copy.edits = table.clone(state.edits or {}) copy.selection = table.clone(state.selection or {}) while true do local body = Ntfy.encode(key, encKey, room, topic, userId, ts, copy, nonceFor) if #body <= Ntfy.MAX_BODY then return body end if #copy.edits > 0 then table.remove(copy.edits, 1) elseif #copy.selection > 0 then table.remove(copy.selection) elseif copy.camera ~= nil then copy.camera = nil elseif copy.script ~= nil then copy.script = nil else return body end end end -- Verifies and decodes `body`, returning the UserState or nil for anything -- that fails a check (bad JSON, stale time, bad signature, id mismatch). function Ntfy.decode(key: string, encKey: string, room: string, topic: string, nowMs: number, body: string): any? local ok, envelope = pcall(Json.decode, body) if not ok or type(envelope) ~= "table" then return nil end local u = envelope.u local t = envelope.t local s = envelope.s local d = envelope.d if type(u) ~= "number" or type(t) ~= "number" or type(s) ~= "string" or type(d) ~= "string" then return nil end if math.abs(nowMs - t) > Ntfy.CLOCK_SKEW_MS then return nil end if Auth.sign(key, Auth.canonical("POST", Ntfy.signPath(topic), u, t, d)) ~= s then return nil end local sealed ok, sealed = pcall(Json.decode, d) if not ok then return nil end local decoded = Schema.openState(encKey, room, sealed) if decoded == nil or decoded.userId ~= u then return nil end return decoded end -- The ntfy topic for a room: short, unguessable, derived from the room id. function Ntfy.topic(room: string): string return "rbxnp-" .. room end return Ntfy Paths --!strict --! Instance paths: names from the service down, joined by "." with "\" and --! "." escaped inside a name, so a path is unambiguous and round-trips. local Paths = {} function Paths.escape(name: string): string local escaped = name:gsub("[\\.]", function(c: string): string return "\\" .. c end) return escaped end function Paths.join(names: {string}): string local parts: {string} = {} for _, name in ipairs(names) do table.insert(parts, Paths.escape(name)) end return table.concat(parts, ".") end function Paths.split(path: string): {string}? if path == "" then return nil end local names: {string} = {} local current: {string} = {} local i = 1 local n = #path while i <= n do local c = path:sub(i, i) if c == "\\" then local nextChar = path:sub(i + 1, i + 1) if nextChar ~= "\\" and nextChar ~= "." then return nil end table.insert(current, nextChar) i += 2 elseif c == "." then table.insert(names, table.concat(current)) current = {} i += 1 else table.insert(current, c) i += 1 end end table.insert(names, table.concat(current)) return names end function Paths.pathOf(instance: any, root: any?): string? local top: any = root if top == nil then top = (game :: any) end if instance == top then return nil end local names: {string} = {} local current: any = instance while current ~= top do if current == nil then return nil end table.insert(names, current.Name) current = current.Parent end local ordered: {string} = {} for i = #names, 1, -1 do table.insert(ordered, names[i]) end return Paths.join(ordered) end function Paths.resolve(root: any, path: string): any? local names = Paths.split(path) if not names then return nil end local current: any = root for _, name in ipairs(names) do current = current:FindFirstChild(name) if current == nil then return nil end end return current end return Paths Relay --!strict --! The presence transport. Three kinds, all official: --! - "hosted" (the default): an rbx-native collaborator runs the relay --! behind a Cloudflare quick tunnel and announces its address on ntfy --! (see Announce); we read the address, then speak "worker" to it. Studio --! cannot host, so with nobody in rbx-native we wait. --! - "worker": the signed-WebSocket relay in relay/ (here over HTTP long-poll, --! since Studio's HttpService cannot hold a socket), which keeps the room --! and answers with a snapshot. --! - "ntfy": a plain ntfy pub/sub server, which keeps nothing for us: we --! publish our signed state to the room's topic, poll the topic for the --! others, and rebuild the room here. Peers trust each other by HMAC. local HttpService = game:GetService("HttpService") local Announce = require(script.Parent.Announce) local Auth = require(script.Parent.Auth) local Ntfy = require(script.Parent.Ntfy) local Schema = require(script.Parent.Schema) export type Config = { kind: string, url: string, key: string, encKey: string, room: string, userId: number } local VERSION_MISMATCH = "The relay runs a different Team Presence version; update the plugin, rbx-native and the relay together." -- Plugins have no CSPRNG API. A v4 GUID is 122 random bits, so 12 of its 16 -- bytes, mixed with a Random stream, make a nonce that does not repeat. local rng = Random.new() local function nonceFor(): string local hex = string.gsub(HttpService:GenerateGUID(false), "[^%x]", "") local bytes = table.create(12) for i = 1, 12 do local guidByte = tonumber(string.sub(hex, i * 2 - 1, i * 2), 16) or 0 bytes[i] = string.char(bit32.bxor(guidByte, rng:NextInteger(0, 255))) end return table.concat(bytes) end local EXPIRY_MS = 15000 -- How often a hosted room looks for its host while it has none. local RESOLVE_EVERY = 15 local WAITING = "Waiting for an rbx-native collaborator to open the place" local Relay = {} Relay.__index = Relay function Relay.new(config: Config) local url = string.gsub(config.url, "/+$", "") local hosted = config.kind == "hosted" local self = { kind = config.kind, -- hosted: the announce server; `base` is the host, once found. announceBase = if hosted then url else nil, base = if hosted then nil else url, lastResolve = -math.huge, key = config.key, encKey = config.encKey, room = config.room, topic = Ntfy.topic(config.room), userId = config.userId, sendKey = true, -- ntfy only: the room rebuilt from the stream, and the newest id seen. peers = {} :: { [number]: { state: any, seen: number } }, cursor = 0, lastId = nil :: string?, } return setmetatable(self, Relay) end local function nowMs(): number return DateTime.now().UnixTimestampMillis end -- A short wait between ntfy polls, so the poll loop does not spin; a -- hosted room with no host yet waits until it looks again. function Relay:pollDelay(): number if self.kind == "hosted" and self.base == nil then return RESOLVE_EVERY end return if self.kind == "ntfy" then 1 else 0 end -- Whether a hosted room is waiting for an rbx-native user to host it (not -- an error: nothing is wrong, nobody is there yet). function Relay:waiting(): boolean return self.kind == "hosted" and self.base == nil end -- hosted: finds the host from the newest announcement, at most once every -- RESOLVE_EVERY seconds. True when there is one to talk to. function Relay:_resolve(): (boolean, string?) if self.base ~= nil then return true, nil end if os.clock() - self.lastResolve < RESOLVE_EVERY then return false, WAITING end self.lastResolve = os.clock() local url = string.format("%s/%s/json?poll=1&since=12h", self.announceBase, Announce.topic(self.room)) local ok, res = pcall(function() return HttpService:RequestAsync({ Url = url, Method = "GET" }) end) if not ok then return false, tostring(res) end if not res.Success then return false, string.format("announce HTTP %d", res.StatusCode) end local texts = {} for line in string.gmatch(res.Body or "", "[^\n]+") do local decoded, parsed = pcall(function() return HttpService:JSONDecode(line) end) if decoded and type(parsed) == "table" and parsed.event == "message" and type(parsed.message) == "string" then table.insert(texts, parsed.message) end end local host = Announce.newest(self.key, self.room, nowMs(), texts) if not host then return false, WAITING end self.base = string.gsub(host, "/+$", "") self.sendKey = true return true, nil end -- hosted: the host stopped answering (left, crashed, or handed over); look -- for it again on the next call. function Relay:_lost() if self.kind == "hosted" then self.base = nil self.lastResolve = -math.huge end end -- ===== Worker (signed HTTP) ===== function Relay:_request(method: string, suffix: string, body: string): (any?, string?, any?) local path = "/v1/rooms/" .. self.room .. suffix local headers = Auth.headers(self.key, method, path, self.userId, nowMs(), body, self.sendKey) if body ~= "" then headers["Content-Type"] = "application/json" end local request: { [string]: any } = { Url = self.base .. path, Method = method, Headers = headers } if body ~= "" then request.Body = body end local ok, res = pcall(function() return HttpService:RequestAsync(request) end) if not ok then return nil, tostring(res) end if not res.Success then if res.StatusCode == 400 and string.find(res.Body or "", "version", 1, true) then return nil, VERSION_MISMATCH, res end return nil, string.format("HTTP %d: %s", res.StatusCode, string.sub(res.Body or "", 1, 120)), res end self.sendKey = false return res, nil end local function needsKey(res: any): boolean if res == nil or res.StatusCode ~= 401 then return false end local ok, decoded = pcall(function() return HttpService:JSONDecode(res.Body) end) return ok and type(decoded) == "table" and decoded.error == "key" end function Relay:_send(method: string, suffix: string, body: string): (any?, string?) if self.kind == "hosted" then local found, why = self:_resolve() if not found then return nil, why end end local res, err, raw = self:_request(method, suffix, body) if res == nil and raw == nil and self.kind == "hosted" then -- No HTTP answer at all: the host may be gone, or its address not -- reachable yet; look again shortly. self:_lost() err = `Can't reach the rbx-native host yet ({err})` elseif res == nil and raw and self.kind == "hosted" and (raw.StatusCode == 404 or raw.StatusCode >= 500) then -- A closed tunnel answers 404/502/530 from Cloudflare's edge (an -- HTML page: keep it out of the status line). self:_lost() err = `Can't reach the rbx-native host yet (HTTP {raw.StatusCode})` end if res == nil and needsKey(raw) and not self.sendKey then self.sendKey = true res, err = self:_request(method, suffix, body) end return res, err end -- ===== ntfy (plain pub/sub) ===== function Relay:_ntfyPublish(state: any): (boolean, string?) local body = Ntfy.fitEncode(self.key, self.encKey, self.room, self.topic, self.userId, nowMs(), state, nonceFor) local ok, res = pcall(function() return HttpService:RequestAsync({ Url = self.base .. "/" .. self.topic, Method = "POST", Headers = { ["Content-Type"] = "text/plain" }, Body = body, }) end) if not ok then return false, tostring(res) end if not res.Success then return false, string.format("ntfy publish HTTP %d", res.StatusCode) end return true, nil end function Relay:_ntfyPoll(): (any?, string?) local since = self.lastId or "10s" local url = string.format("%s/%s/json?poll=1&since=%s", self.base, self.topic, HttpService:UrlEncode(since)) local ok, res = pcall(function() return HttpService:RequestAsync({ Url = url, Method = "GET" }) end) if not ok then return nil, tostring(res) end if not res.Success then return nil, string.format("ntfy poll HTTP %d", res.StatusCode) end local now = nowMs() local changed = false for line in string.gmatch(res.Body or "", "[^\n]+") do local parsed local decoded = pcall(function() parsed = HttpService:JSONDecode(line) end) if decoded and type(parsed) == "table" then if type(parsed.id) == "string" then self.lastId = parsed.id end if parsed.event == "message" and type(parsed.message) == "string" then local state = Ntfy.decode(self.key, self.encKey, self.room, self.topic, now, parsed.message) -- Our own echo is this user in Studio; the same user in -- rbx-native is another sender, keyed apart. if state and not (state.userId == self.userId and state.client == "studio") then state.seen = now self.peers[`{state.userId}/{state.client}`] = { state = state, seen = now } changed = true end end end end for id, peer in self.peers do if now - peer.seen > EXPIRY_MS then self.peers[id] = nil changed = true end end if changed then self.cursor += 1 end local users = {} for _, peer in self.peers do table.insert(users, peer.state) end return { cursor = self.cursor, users = users }, nil end -- ===== Shared surface ===== function Relay:publish(state: any): (boolean, string?) if self.kind == "ntfy" then return self:_ntfyPublish(state) end local res, err = self:_send("POST", "/state", Schema.sealState(self.encKey, self.room, state, nonceFor)) if res == nil then return false, err end return true, nil end function Relay:poll(cursor: number): (any?, string?) if self.kind == "ntfy" then return self:_ntfyPoll() end local res, err = self:_send("GET", string.format("/poll?since=%.0f", cursor), "") if res == nil then return nil, err end local ok, snapshot = pcall(Schema.openSnapshot, self.encKey, self.room, res.Body) if not ok then return nil, tostring(snapshot) end return snapshot, nil end -- The value-less edit journal since `sinceMs`, for the "While you were -- away" catch-up. Worker: a signed GET of its bounded journal. ntfy: the -- topic's retained messages, verified against each message's own time -- (they predate now by more than the signing skew), edits lifted out, -- filtered to after `sinceMs` and de-duplicated. Mirrors -- crates/rbx_presence/src/catchup.rs. function Relay:journal(sinceMs: number): (any?, string?) if self.kind == "ntfy" then local url = string.format("%s/%s/json?poll=1&since=12h", self.base, self.topic) local ok, res = pcall(function() return HttpService:RequestAsync({ Url = url, Method = "GET" }) end) if not ok then return nil, tostring(res) end if not res.Success then return nil, string.format("ntfy journal HTTP %d", res.StatusCode) end local now = nowMs() local seen: { [string]: boolean } = {} local entries = {} for line in string.gmatch(res.Body or "", "[^\n]+") do local parsed local decoded = pcall(function() parsed = HttpService:JSONDecode(line) end) if decoded and type(parsed) == "table" and parsed.event == "message" and type(parsed.message) == "string" then local checkAt = if type(parsed.time) == "number" then parsed.time * 1000 else now local state = Ntfy.decode(self.key, self.encKey, self.room, self.topic, checkAt, parsed.message) if state then for _, edit in state.edits or {} do if edit.at > sinceMs then local dedupe = string.format("%.0f\0%s\0%s\0%.0f", state.userId, edit.path, edit.property, edit.at) if not seen[dedupe] then seen[dedupe] = true table.insert(entries, { userId = state.userId, name = state.name, path = edit.path, property = edit.property, at = edit.at, }) end end end end end end return entries, nil end local res, err = self:_send("GET", string.format("/journal?since=%.0f", sinceMs), "") if res == nil then return nil, err end local parsed local ok = pcall(function() parsed = HttpService:JSONDecode(res.Body) end) if not ok or type(parsed) ~= "table" then return nil, "journal reply" end local opened = {} local raw = parsed.entries if type(raw) == "table" then for _, entry in raw do local plain = Schema.openJournal(self.encKey, self.room, entry) if plain ~= nil then table.insert(opened, plain) end end end return opened, nil end local NEEDS_HOSTED = "Co-editing needs the hosted relay or a Worker" -- `fill`: entries of the log handed to a relay that lacks them. function Relay:postOps(body: string, fill: boolean?): (boolean, string?) if self.kind ~= "hosted" and self.kind ~= "worker" then return false, NEEDS_HOSTED end local res, err = self:_send("POST", if fill then "/ops/fill" else "/ops", body) if res == nil then return false, err end return true, nil end function Relay:pollOps(since: number): (any?, string?) if self.kind ~= "hosted" and self.kind ~= "worker" then return nil, NEEDS_HOSTED end local res, err = self:_send("GET", string.format("/ops?since=%.0f", since), "") if res == nil then return nil, err end local parsed local ok = pcall(function() parsed = HttpService:JSONDecode(res.Body) end) if not ok or type(parsed) ~= "table" then return nil, "ops reply" end return parsed, nil end function Relay:leave() if self.kind == "ntfy" or self:waiting() then -- Nothing to delete on a dumb pub/sub (peers expire us), or no host. return end self:_send("DELETE", "/state", "") end return Relay Schema --!strict --! UserState / Snapshot shapes shared with the relay, including the size --! limits the relay enforces so a state is never rejected for being too big. local Json = require(script.Parent.Json) local Crypto = require(script.Parent.Crypto) local Schema = {} Schema.VERSION = 2 Schema.MAX_SELECTION = 100 Schema.MAX_EDITS = 20 Schema.MAX_TEXT = 512 Schema.PALETTE = { "#e8590c", "#1c7ed6", "#2f9e44", "#ae3ec9", "#f08c00", "#0c8599", "#e03131", "#7048e8" } -- FNV-1a over the decimal digits; the prime 16777619 is 2^24 + 403, which -- keeps every intermediate product exact in a double. function Schema.colorSlot(userId: number): number local h = 2166136261 for _, byte in ipairs({ string.byte(string.format("%.0f", userId), 1, -1) }) do h = bit32.bxor(h, byte) h = (bit32.lshift(h, 24) + h * 403) % 4294967296 end return h % 8 end function Schema.colorFor(userId: number): string return Schema.PALETTE[Schema.colorSlot(userId) + 1] end function Schema.assignColors(room: {number}, extra: {number}?): {[number]: string} local ordered: {number} = {} local seen: {[number]: boolean} = {} local function add(ids: {number}) local sorted = table.clone(ids) table.sort(sorted) for _, id in ipairs(sorted) do if not seen[id] then seen[id] = true table.insert(ordered, id) end end end add(room) if extra then add(extra) end local taken: {[number]: boolean} = {} local count = 0 local colors: {[number]: string} = {} for _, id in ipairs(ordered) do local slot = Schema.colorSlot(id) if count < 8 then while taken[slot] do slot = (slot + 1) % 8 end taken[slot] = true count += 1 end colors[id] = Schema.PALETTE[slot + 1] end return colors end local function clampText(s: string): string if #s <= Schema.MAX_TEXT then return s end local cut = Schema.MAX_TEXT -- Step back so a multi-byte character is never split. while cut > 0 do local b = string.byte(s, cut + 1) if b == nil or b < 0x80 or b >= 0xC0 then break end cut -= 1 end return s:sub(1, cut) end local function clampList(list: {any}?, max: number, map: (any) -> any): {any} local out = Json.array() if list then for i = 1, math.min(#list, max) do table.insert(out, map(list[i])) end end return out end function Schema.encode(state: any): string local out: {[string]: any} = { v = Schema.VERSION, userId = state.userId, name = clampText(state.name), color = state.color, client = state.client, selection = clampList(state.selection, Schema.MAX_SELECTION, clampText), edits = clampList(state.edits, Schema.MAX_EDITS, function(edit: any): any local value = edit.value if type(value) == "string" then value = clampText(value) end return { path = clampText(edit.path), property = clampText(edit.property), value = value, at = edit.at, } end), seen = state.seen or 0, } if state.camera ~= nil then out.camera = Json.array(table.clone(state.camera)) end if state.script ~= nil then out.script = clampText(state.script) end return Json.encode(out) end local function present(value: any): any if value == Json.null then return nil end return value end local function toState(obj: any): any if type(obj) ~= "table" then error("state must be an object") end local state: {[string]: any} = { v = obj.v or Schema.VERSION, userId = obj.userId, name = obj.name, color = obj.color, client = obj.client, selection = Json.array(), edits = Json.array(), seen = obj.seen or 0, } local selection = present(obj.selection) if selection then for _, path in ipairs(selection) do table.insert(state.selection, path) end end local edits = present(obj.edits) if edits then for _, edit in ipairs(edits) do table.insert(state.edits, edit) end end state.camera = present(obj.camera) state.script = present(obj.script) return state end function Schema.decodeState(text: string): any return toState(Json.decode(text)) end function Schema.decodeSnapshot(text: string): any local obj = Json.decode(text) if type(obj) ~= "table" then error("snapshot must be an object") end local users: {any} = {} local list = present(obj.users) if list then for _, user in ipairs(list) do table.insert(users, toState(user)) end end return { cursor = obj.cursor or 0, users = users } end -- The state as the relay sees it: `ct` is the state JSON, `j` one sealed -- journal entry per edit (its display value included). `nonceFor` returns 12 raw bytes -- and is called once per ciphertext. function Schema.sealState(encKey: string, room: string, state: any, nonceFor: () -> string): string local client: string = state.client local ct = Crypto.sealB64(encKey, nonceFor(), Crypto.stateAad(room, state.userId, client), Schema.encode(state)) local name = clampText(state.name) local j = Json.array() local edits: {any} = state.edits or {} for i = 1, math.min(#edits, Schema.MAX_EDITS) do local edit = edits[i] local path, property = clampText(edit.path), clampText(edit.property) local plain = Json.encode({ name = name, path = path, property = property, value = clampText(edit.value or "") }) table.insert(j, { at = edit.at, tag = Crypto.journalTag(encKey, path, property, edit.at), ct = Crypto.sealB64(encKey, nonceFor(), Crypto.journalAad(room, state.userId, edit.at), plain), }) end return Json.encode({ v = Schema.VERSION, userId = state.userId, client = client, ct = ct, j = j, seen = 0, }) end function Schema.openState(encKey: string, room: string, sealed: any): any? if type(sealed) ~= "table" then return nil end local userId, client, ct = sealed.userId, sealed.client, sealed.ct if type(userId) ~= "number" or type(client) ~= "string" or type(ct) ~= "string" then return nil end local plain = Crypto.openB64(encKey, Crypto.stateAad(room, userId, client), ct) if plain == nil then return nil end local ok, state = pcall(Schema.decodeState, plain) if not ok or type(state) ~= "table" then return nil end if state.v ~= Schema.VERSION or state.userId ~= userId or state.client ~= client then return nil end local seen = sealed.seen state.seen = if type(seen) == "number" then seen else 0 return state end function Schema.openSnapshot(encKey: string, room: string, text: string): any local obj = Json.decode(text) if type(obj) ~= "table" then error("snapshot must be an object") end local users: {any} = {} local list = present(obj.users) if list then for _, sealed in ipairs(list) do local state = Schema.openState(encKey, room, sealed) if state ~= nil then table.insert(users, state) end end end return { cursor = obj.cursor or 0, users = users } end function Schema.openJournal(encKey: string, room: string, entry: any): any? if type(entry) ~= "table" then return nil end local userId, at, ct = entry.userId, entry.at, entry.ct if type(userId) ~= "number" or type(at) ~= "number" or type(ct) ~= "string" then return nil end local plain = Crypto.openB64(encKey, Crypto.journalAad(room, userId, at), ct) if plain == nil then return nil end local ok, obj = pcall(Json.decode, plain) if not ok or type(obj) ~= "table" then return nil end if type(obj.name) ~= "string" or type(obj.path) ~= "string" or type(obj.property) ~= "string" then return nil end local value = if type(obj.value) == "string" then obj.value else "" return { userId = userId, name = obj.name, path = obj.path, property = obj.property, value = value, at = at } end return Schema Secret --!strict --! Shared presence secret stored as an attribute under ServerStorage, so Team Create replicates it to collaborators. local ChangeHistoryService = game:GetService("ChangeHistoryService") local HttpService = game:GetService("HttpService") local ServerStorage = game:GetService("ServerStorage") local FOLDER = "RbxNativePresence" local ATTRIBUTE = "PresenceSecret" local RELAY_KIND = "RelayKind" local RELAY_URL = "RelayUrl" local Secret = {} local function ensureFolder(): Folder local folder = ServerStorage:FindFirstChild(FOLDER) if folder == nil then local created = Instance.new("Folder") created.Name = FOLDER created.Parent = ServerStorage folder = created end return folder :: Folder end function Secret.read(): string? local folder = ServerStorage:FindFirstChild(FOLDER) if folder == nil then return nil end local value = folder:GetAttribute(ATTRIBUTE) if type(value) == "string" and #value == 64 and string.match(value, "^[0-9a-f]+$") then return value end return nil end function Secret.create(): string local secret = string.lower( string.gsub(HttpService:GenerateGUID(false), "-", "") .. string.gsub(HttpService:GenerateGUID(false), "-", "") ) local id = ChangeHistoryService:TryBeginRecording("Create presence key") ensureFolder():SetAttribute(ATTRIBUTE, secret) if id then ChangeHistoryService:FinishRecording(id, Enum.FinishRecordingOperation.Commit) end return secret end -- The relay the place names, if any: a known kind and a non-empty URL. function Secret.readRelay(): { kind: string, url: string }? local folder = ServerStorage:FindFirstChild(FOLDER) if folder == nil then return nil end local kind = folder:GetAttribute(RELAY_KIND) local url = folder:GetAttribute(RELAY_URL) if (kind == "hosted" or kind == "worker" or kind == "ntfy") and type(url) == "string" and url ~= "" then return { kind = kind :: string, url = url } end return nil end -- Owner setup: writes the relay into the place for every collaborator. function Secret.writeRelay(kind: string, url: string) local id = ChangeHistoryService:TryBeginRecording("Set presence relay") local folder = ensureFolder() folder:SetAttribute(RELAY_KIND, kind) folder:SetAttribute(RELAY_URL, url) if id then ChangeHistoryService:FinishRecording(id, Enum.FinishRecordingOperation.Commit) end end function Secret.clearRelay() local folder = ServerStorage:FindFirstChild(FOLDER) if folder == nil then return end local id = ChangeHistoryService:TryBeginRecording("Clear presence relay") folder:SetAttribute(RELAY_KIND, nil) folder:SetAttribute(RELAY_URL, nil) if id then ChangeHistoryService:FinishRecording(id, Enum.FinishRecordingOperation.Commit) end end return Secret Sha256 --!strict -- SHA-256 (FIPS 180-4) over a string's bytes, using only bit32 so it runs in -- Studio and in plain Luau alike. local K: {number} = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, } local Sha256 = {} local function digestWords(msg: string): {number} local len = #msg local padLen = (55 - len) % 64 local hi = math.floor(len / 0x20000000) local lo = (len * 8) % 0x100000000 local tail = "\128" .. string.rep("\0", padLen) .. string.char( bit32.rshift(hi, 24) % 256, bit32.rshift(hi, 16) % 256, bit32.rshift(hi, 8) % 256, hi % 256, bit32.rshift(lo, 24), bit32.rshift(lo, 16) % 256, bit32.rshift(lo, 8) % 256, lo % 256 ) local data = msg .. tail local h: {number} = { 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19, } local w: {number} = table.create(64, 0) for base = 1, #data, 64 do for i = 1, 16 do local a, b, c, d = string.byte(data, base + (i - 1) * 4, base + (i - 1) * 4 + 3) w[i] = a * 0x1000000 + b * 0x10000 + c * 0x100 + d end for i = 17, 64 do local x = w[i - 15] local y = w[i - 2] local s0 = bit32.bxor(bit32.rrotate(x, 7), bit32.rrotate(x, 18), bit32.rshift(x, 3)) local s1 = bit32.bxor(bit32.rrotate(y, 17), bit32.rrotate(y, 19), bit32.rshift(y, 10)) w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % 0x100000000 end local a, b, c, d, e, f, g, hh = h[1], h[2], h[3], h[4], h[5], h[6], h[7], h[8] for i = 1, 64 do local s1 = bit32.bxor(bit32.rrotate(e, 6), bit32.rrotate(e, 11), bit32.rrotate(e, 25)) local ch = bit32.bxor(bit32.band(e, f), bit32.band(bit32.bnot(e), g)) local t1 = (hh + s1 + ch + K[i] + w[i]) % 0x100000000 local s0 = bit32.bxor(bit32.rrotate(a, 2), bit32.rrotate(a, 13), bit32.rrotate(a, 22)) local maj = bit32.bxor(bit32.band(a, b), bit32.band(a, c), bit32.band(b, c)) local t2 = (s0 + maj) % 0x100000000 hh = g g = f f = e e = (d + t1) % 0x100000000 d = c c = b b = a a = (t1 + t2) % 0x100000000 end h[1] = (h[1] + a) % 0x100000000 h[2] = (h[2] + b) % 0x100000000 h[3] = (h[3] + c) % 0x100000000 h[4] = (h[4] + d) % 0x100000000 h[5] = (h[5] + e) % 0x100000000 h[6] = (h[6] + f) % 0x100000000 h[7] = (h[7] + g) % 0x100000000 h[8] = (h[8] + hh) % 0x100000000 end return h end function Sha256.hex(msg: string): string local h = digestWords(msg) local parts: {string} = {} for i = 1, 8 do parts[i] = string.format("%08x", h[i]) end return table.concat(parts) end function Sha256.raw(msg: string): string local h = digestWords(msg) local parts: {string} = {} for i = 1, 8 do local v = h[i] parts[i] = string.char( bit32.rshift(v, 24), bit32.rshift(v, 16) % 256, bit32.rshift(v, 8) % 256, v % 256 ) end return table.concat(parts) end return Sha256 Theme --!strict -- Generated by build_presence_plugin from assets/themes/default/theme.json; do not edit. -- Colours are { r, g, b, alpha } with r, g, b in 0-255 and alpha in 0-1. return { colors = { accent_hover = { 115, 133, 221, 1.000 }, accent_line = { 108, 127, 219, 0.550 }, accent_soft = { 108, 127, 219, 0.120 }, avatar = { 44, 48, 80, 1.000 }, avatar_on_accent = { 42, 45, 74, 1.000 }, axis_x = { 255, 143, 143, 1.000 }, axis_y = { 147, 224, 143, 1.000 }, axis_z = { 143, 184, 255, 1.000 }, black = { 10, 10, 11, 1.000 }, border = { 255, 255, 255, 0.059 }, border2 = { 255, 255, 255, 0.110 }, check_off_border = { 138, 138, 138, 1.000 }, check_on = { 108, 127, 219, 1.000 }, chrome = { 18, 18, 19, 1.000 }, danger_hover = { 138, 90, 85, 1.000 }, diff_add = { 116, 201, 143, 1.000 }, diff_add_gutter = { 116, 201, 143, 0.161 }, diff_add_pill = { 116, 201, 143, 0.122 }, diff_add_soft = { 116, 201, 143, 0.102 }, diff_remove_gutter = { 224, 108, 108, 0.161 }, diff_remove_pill = { 224, 108, 108, 0.122 }, diff_remove_soft = { 224, 108, 108, 0.102 }, dock = { 18, 18, 19, 1.000 }, error_soft = { 224, 108, 108, 0.120 }, field_select = { 25, 26, 28, 1.000 }, hover = { 255, 255, 255, 0.051 }, hover_faint = { 255, 255, 255, 0.016 }, hover_subtle = { 255, 255, 255, 0.031 }, knob = { 255, 255, 255, 1.000 }, menu_bar = { 10, 10, 11, 1.000 }, output_client = { 93, 169, 246, 1.000 }, output_info = { 111, 177, 252, 1.000 }, output_server = { 108, 203, 126, 1.000 }, ribbon_tab_active = { 255, 255, 255, 0.102 }, secondary_hover = { 32, 33, 35, 1.000 }, selection = { 108, 127, 219, 0.722 }, shadow = { 0, 0, 0, 0.400 }, slide_backdrop = { 19, 19, 20, 1.000 }, tab_active_bar = { 108, 127, 219, 1.000 }, text = { 226, 226, 230, 1.000 }, text2 = { 143, 143, 151, 1.000 }, text3 = { 92, 92, 99, 1.000 }, text_disabled = { 92, 92, 99, 1.000 }, text_error = { 224, 108, 108, 1.000 }, text_full = { 226, 226, 230, 1.000 }, text_label = { 143, 143, 151, 1.000 }, text_muted = { 143, 143, 151, 1.000 }, text_placeholder = { 92, 92, 99, 1.000 }, text_strong = { 226, 226, 230, 1.000 }, tile = { 18, 18, 19, 1.000 }, tool_align = { 201, 168, 255, 1.000 }, tool_local = { 143, 224, 216, 1.000 }, tool_move = { 143, 224, 176, 1.000 }, tool_pivot = { 198, 242, 143, 1.000 }, tool_rotate = { 255, 201, 143, 1.000 }, tool_scale = { 255, 155, 155, 1.000 }, tool_select = { 143, 184, 255, 1.000 }, tool_sun = { 255, 232, 143, 1.000 }, tool_transform = { 255, 155, 224, 1.000 }, track = { 255, 255, 255, 0.102 }, veil = { 0, 0, 0, 0.549 }, viewport_axis_x = { 230, 41, 41, 1.000 }, viewport_axis_y = { 41, 199, 56, 1.000 }, viewport_axis_z = { 51, 92, 242, 1.000 }, viewport_background = { 28, 29, 32, 1.000 }, viewport_badge = { 20, 21, 26, 0.878 }, viewport_badge_text = { 228, 229, 233, 1.000 }, viewport_bone = { 206, 206, 206, 0.902 }, viewport_bone_selected = { 111, 173, 249, 1.000 }, viewport_brush_add = { 137, 206, 255, 1.000 }, viewport_brush_region = { 249, 225, 124, 1.000 }, viewport_brush_subtract = { 255, 137, 124, 1.000 }, viewport_grid_axis_x = { 221, 69, 80, 1.000 }, viewport_grid_axis_z = { 89, 144, 234, 1.000 }, viewport_grid_major = { 82, 82, 82, 1.000 }, viewport_grid_minor = { 66, 66, 66, 1.000 }, viewport_guide = { 255, 255, 255, 1.000 }, viewport_guide_active = { 255, 255, 0, 1.000 }, viewport_hover = { 250, 166, 39, 0.549 }, viewport_joint = { 255, 173, 63, 1.000 }, viewport_joint_selected = { 255, 239, 89, 1.000 }, viewport_measure_lit = { 0, 139, 234, 1.000 }, viewport_orient_body = { 91, 143, 212, 0.302 }, viewport_orient_rim = { 157, 196, 255, 0.400 }, viewport_preview = { 144, 226, 161, 0.502 }, viewport_readout = { 37, 37, 37, 1.000 }, viewport_readout_border = { 0, 0, 0, 1.000 }, viewport_readout_text = { 255, 255, 255, 1.000 }, viewport_selection = { 92, 181, 245, 1.000 }, warning = { 217, 165, 91, 1.000 }, wash_faint = { 255, 255, 255, 0.039 }, }, sizes = { checkbox_target = 26, chevron_slot = 11, dock_tab_add_width = 36, dock_tabs_height = 38, dock_width = 314, field_min_width = 76, group_gap = 16, header_gap = 4, input_height = 31, input_padding = 8, label_gap = 4, line_action = 17, line_badge = 16, line_ghost = 15, line_lg = 18, line_md = 16, line_md_tall = 18, line_sm = 16, line_xs = 14, line_xxs = 14, menu_bar_height = 30, panel_padding = 20, radius = 5, radius_badge = 4, radius_container = 8, radius_row = 4, radius_segment = 3, radius_tile = 6, radius_tiny = 1, ribbon_height = 80, ribbon_tabs_height = 28, row_gap = 7, row_height = 28, row_label_width = 150, row_padding = 4, section_gap = 7, section_height = 14, select_inset = 5.5, separator_height = 58, slider_min_width = 60, slider_rail = 6, slider_thumb = 13, stack_width = 100, strip_field_height = 27, tab_add_width = 42, tabs_height = 42, text_action = 12.5, text_badge = 11, text_ghost = 11, text_lg = 13, text_md = 12, text_sm = 11.5, text_xs = 10.5, text_xxs = 10, tile_width = 56, toggle_height = 17, toggle_thumb = 13, toggle_width = 30, topbar_height = 34, tree_row_height = 24, value_width = 130, }, } Ui --!strict --// The plugin's look: rbx-native's own palette (the generated Theme module, --// written from assets/themes/default/theme.json) and the few pieces the --// editor's docks are built from, so the widget and the diff window read as --// part of rbx-native rather than as stock Studio UI. local Theme = require(script.Parent.Theme) local Ui = {} -- Studio has neither Manrope nor JetBrains Mono; Builder Sans and Builder -- Mono are its closest. Ui.FONT = Font.new("rbxasset://fonts/families/BuilderSans.json") Ui.FONT_SEMIBOLD = Font.new("rbxasset://fonts/families/BuilderSans.json", Enum.FontWeight.SemiBold) Ui.FONT_BOLD = Font.new("rbxasset://fonts/families/BuilderSans.json", Enum.FontWeight.Bold) Ui.FONT_MONO = Font.new("rbxasset://fonts/families/BuilderMono.json") -- Studio sizes text in whole points; these round rbx-native's sizes. Ui.TEXT_LG = 14 Ui.TEXT_MD = 13 Ui.TEXT_SM = 12 Ui.TEXT_XS = 11 Ui.TEXT_XXS = 10 -- The spacing step: edge insets, gaps between blocks, card padding. Ui.GAP = 8 Ui.RADIUS = Theme.sizes.radius or 5 Ui.RADIUS_BADGE = Theme.sizes.radius_badge or 4 Ui.RADIUS_TILE = Theme.sizes.radius_tile or 6 -- A theme colour as Color3 and as the Transparency that carries its alpha. function Ui.color(name: string): Color3 local c = Theme.colors[name] if not c then return Color3.new(1, 0, 1) end return Color3.fromRGB(c[1], c[2], c[3]) end function Ui.alpha(name: string): number local c = Theme.colors[name] return if c then 1 - c[4] else 0 end -- `#rrggbb` as Color3 (a collaborator's colour). function Ui.hex(hex: string): Color3 local r, g, b = string.match(hex, "^#(%x%x)(%x%x)(%x%x)$") if not r then return Ui.color("text2") end return Color3.fromRGB(tonumber(r, 16) :: number, tonumber(g, 16) :: number, tonumber(b, 16) :: number) end function Ui.fill(object: GuiObject, name: string) object.BackgroundColor3 = Ui.color(name) object.BackgroundTransparency = Ui.alpha(name) object.BorderSizePixel = 0 end function Ui.corner(parent: Instance, radius: number?) local corner = Instance.new("UICorner") corner.CornerRadius = UDim.new(0, radius or Ui.RADIUS) corner.Parent = parent end function Ui.stroke(parent: Instance, name: string?) local stroke = Instance.new("UIStroke") stroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border stroke.Color = Ui.color(name or "border") stroke.Transparency = Ui.alpha(name or "border") stroke.Thickness = 1 stroke.Parent = parent return stroke end function Ui.pad(parent: Instance, top: number, right: number?, bottom: number?, left: number?) local padding = Instance.new("UIPadding") padding.PaddingTop = UDim.new(0, top) padding.PaddingRight = UDim.new(0, right or top) padding.PaddingBottom = UDim.new(0, bottom or top) padding.PaddingLeft = UDim.new(0, left or right or top) padding.Parent = parent end function Ui.list(parent: Instance, gap: number, horizontal: boolean?): UIListLayout local layout = Instance.new("UIListLayout") layout.SortOrder = Enum.SortOrder.LayoutOrder layout.Padding = UDim.new(0, gap) if horizontal then layout.FillDirection = Enum.FillDirection.Horizontal layout.VerticalAlignment = Enum.VerticalAlignment.Center end layout.Parent = parent return layout end -- A transparent box that sizes to its content (vertical stack by default). function Ui.frame(parent: Instance?, order: number?): Frame local frame = Instance.new("Frame") frame.BackgroundTransparency = 1 frame.BorderSizePixel = 0 frame.LayoutOrder = order or 0 frame.Size = UDim2.fromScale(1, 0) frame.AutomaticSize = Enum.AutomaticSize.Y frame.Parent = parent return frame end export type TextOptions = { size: number?, color: string?, font: Font?, order: number?, wrap: boolean?, width: UDim?, } -- A label: full width and wrapping by default, sized to its text. function Ui.text(parent: Instance?, text: string, options: TextOptions?): TextLabel local o: TextOptions = options or {} local label = Instance.new("TextLabel") label.BackgroundTransparency = 1 label.BorderSizePixel = 0 label.LayoutOrder = o.order or 0 label.FontFace = o.font or Ui.FONT label.TextSize = o.size or Ui.TEXT_SM label.TextColor3 = Ui.color(o.color or "text") label.TextXAlignment = Enum.TextXAlignment.Left label.TextYAlignment = Enum.TextYAlignment.Center label.RichText = false label.Text = text if o.wrap == false then label.TextWrapped = false label.TextTruncate = Enum.TextTruncate.AtEnd label.Size = UDim2.new(o.width or UDim.new(1, 0), UDim.new(0, label.TextSize + 4)) else label.TextWrapped = true label.Size = UDim2.new(o.width or UDim.new(1, 0), UDim.new(0, 0)) label.AutomaticSize = Enum.AutomaticSize.Y end label.Parent = parent return label end -- A text link: `accent_line` until hovered. function Ui.link(parent: Instance?, text: string, order: number?): TextButton local button = Instance.new("TextButton") button.AutoButtonColor = false button.BackgroundTransparency = 1 button.BorderSizePixel = 0 button.LayoutOrder = order or 0 button.FontFace = Ui.FONT_SEMIBOLD button.TextSize = Ui.TEXT_XS button.TextColor3 = Ui.color("check_on") button.TextXAlignment = Enum.TextXAlignment.Left button.Text = text button.AutomaticSize = Enum.AutomaticSize.XY button.Size = UDim2.fromOffset(0, 16) button.Parent = parent button.MouseEnter:Connect(function() button.TextColor3 = Ui.color("accent_hover") end) button.MouseLeave:Connect(function() button.TextColor3 = Ui.color("check_on") end) return button end -- rbx-native's buttons: primary is `check_on` with dark bold text, secondary -- is `field_select` on a `border2` stroke. function Ui.button(parent: Instance?, text: string, primary: boolean, order: number?): TextButton local button = Instance.new("TextButton") button.AutoButtonColor = false button.LayoutOrder = order or 0 button.Size = UDim2.new(0, 96, 0, 30) button.FontFace = if primary then Ui.FONT_BOLD else Ui.FONT_SEMIBOLD button.TextSize = Ui.TEXT_SM button.Text = text local idle = if primary then "check_on" else "field_select" local hover = if primary then "accent_hover" else "secondary_hover" Ui.fill(button, idle) button.TextColor3 = Ui.color(if primary then "black" else "text") Ui.corner(button) if not primary then Ui.stroke(button, "border2") end button.MouseEnter:Connect(function() Ui.fill(button, hover) end) button.MouseLeave:Connect(function() Ui.fill(button, idle) end) button.Parent = parent return button end -- A bordered rounded box with its own vertical stack (`gap` apart). function Ui.card(parent: Instance?, order: number?, gap: number?): Frame local card = Ui.frame(parent, order) Ui.corner(card) Ui.stroke(card, "border") Ui.pad(card, Ui.GAP) Ui.list(card, gap or 4) return card end -- The 6px (or `size`) round dot a kind or a collaborator is marked with. function Ui.dot(parent: Instance?, color: Color3, size: number?): Frame local dot = Instance.new("Frame") dot.Size = UDim2.fromOffset(size or 6, size or 6) dot.BackgroundColor3 = color dot.BorderSizePixel = 0 Ui.corner(dot, 999) dot.Parent = parent return dot end export type Switch = { frame: TextButton, set: (boolean) -> (), } -- The dock's on/off switch: a 28x16 track, `check_on` when on, with a white -- knob; clicking calls `onClick` (the caller sets the state). function Ui.switch(parent: Instance?, onClick: () -> (), order: number?): Switch local track = Instance.new("TextButton") track.AutoButtonColor = false track.Text = "" track.LayoutOrder = order or 0 track.Size = UDim2.fromOffset(28, 16) Ui.corner(track, 999) local knob = Instance.new("Frame") knob.Size = UDim2.fromOffset(12, 12) knob.AnchorPoint = Vector2.new(0, 0.5) knob.BorderSizePixel = 0 knob.BackgroundColor3 = Ui.color("knob") Ui.corner(knob, 999) knob.Parent = track local function set(on: boolean) Ui.fill(track, if on then "check_on" else "track") knob.Position = if on then UDim2.new(1, -14, 0.5, 0) else UDim2.new(0, 2, 0.5, 0) end set(false) track.Activated:Connect(onClick) track.Parent = parent return { frame = track, set = set } end -- A mono value chip, as the diff table's: `diff_add_soft` behind `text`, a -- 10x10 swatch first when `swatch` is given. function Ui.chip(parent: Instance?, text: string, swatch: Color3?, order: number?): Frame local chip = Instance.new("Frame") chip.LayoutOrder = order or 0 chip.AutomaticSize = Enum.AutomaticSize.X chip.Size = UDim2.fromOffset(0, 22) Ui.fill(chip, "diff_add_soft") Ui.corner(chip, Ui.RADIUS_BADGE) Ui.pad(chip, 0, 7, 0, 7) Ui.list(chip, 6, true) if swatch then local box = Instance.new("Frame") box.Size = UDim2.fromOffset(10, 10) box.BackgroundColor3 = swatch box.BorderSizePixel = 0 Ui.corner(box, 2) Ui.stroke(box, "border2") box.Parent = chip end local label = Ui.text(chip, text, { font = Ui.FONT_MONO, size = Ui.TEXT_SM, wrap = false, order = 1 }) label.AutomaticSize = Enum.AutomaticSize.X label.Size = UDim2.fromOffset(0, 22) label.TextTruncate = Enum.TextTruncate.None chip.Parent = parent return chip end -- A small rounded pill: `bg` behind `ink` text, a dot first. function Ui.pill(parent: Instance?, text: string, bg: string, ink: string, order: number?): Frame local pill = Instance.new("Frame") pill.LayoutOrder = order or 0 pill.AutomaticSize = Enum.AutomaticSize.X pill.Size = UDim2.fromOffset(0, 20) Ui.fill(pill, bg) Ui.corner(pill, Ui.RADIUS_BADGE) Ui.pad(pill, 0, 8, 0, 8) Ui.list(pill, 6, true) Ui.dot(pill, Ui.color(ink)) local label = Ui.text(pill, text, { font = Ui.FONT_SEMIBOLD, size = Ui.TEXT_XS, color = ink, wrap = false, order = 1 }) label.AutomaticSize = Enum.AutomaticSize.X label.Size = UDim2.fromOffset(0, 20) label.TextTruncate = Enum.TextTruncate.None pill.Parent = parent return pill end -- A collaborator as Studio shows one in a Team Create session: their round -- Roblox headshot in a ring of their presence colour (the colour behind it -- until the headshot loads). function Ui.avatar(parent: Instance?, userId: number, color: Color3, size: number?, order: number?): ImageLabel local s = size or 22 local image = Instance.new("ImageLabel") image.LayoutOrder = order or 0 image.Size = UDim2.fromOffset(s, s) image.BackgroundColor3 = color image.BorderSizePixel = 0 image.Image = string.format("rbxthumb://type=AvatarHeadShot&id=%.0f&w=48&h=48", userId) Ui.corner(image, 999) local ring = Instance.new("UIStroke") ring.Color = color ring.Thickness = 2 ring.ApplyStrokeMode = Enum.ApplyStrokeMode.Border ring.Parent = image image.Parent = parent return image end -- The Explorer's own icon for `class`, from StudioService. function Ui.classIcon(parent: Instance?, class: string, size: number?): ImageLabel local image = Instance.new("ImageLabel") image.BackgroundTransparency = 1 image.Size = UDim2.fromOffset(size or 16, size or 16) local ok, icon = pcall(function() return game:GetService("StudioService"):GetClassIcon(class) end) if ok and type(icon) == "table" then for key, value in icon :: any do pcall(function() (image :: any)[key] = value end) end end image.Parent = parent return image end return Ui Widget --!strict --// Dock widget, laid out like rbx-native's own Collaborators dock and drawn --// in its palette (see Ui): the note, a "Share my presence" switch, the --// status line, an "Advanced" reveal holding the owner's relay setup, the --// "While you were away" card, then one row per rbx-native collaborator. local Catchup = require(script.Parent.Catchup) local Ui = require(script.Parent.Ui) local Widget = {} Widget.__index = Widget local NOTE = "Presence only works for collaborators who have Studio open with the rbx-native " .. "presence plugin running, or rbx-native itself." local HTTP_NOTE = "Presence sends your selection, camera and open script to the relay. " .. "Studio asks once whether this plugin may reach that domain; allow it." type Callbacks = { onToggle: () -> (), onSaveRelay: (string, string) -> (), onViewChanges: () -> (), } export type Widget = typeof(setmetatable({} :: { gui: DockWidgetPluginGui, enabled: boolean, relayKind: string, switch: Ui.Switch, status: TextLabel, faces: Frame, advanced: TextButton, owner: Frame, kindHosted: TextButton, kindWorker: TextButton, kindNtfy: TextButton, urlBox: TextBox, resolved: TextLabel, httpNote: Frame, catchup: Frame, catchupBody: Frame, rows: Frame, onViewChanges: () -> (), connections: { RBXScriptConnection }, }, Widget)) -- One half of the Worker/ntfy segmented control. local function segment(parent: Instance, text: string, order: number): TextButton local button = Instance.new("TextButton") button.AutoButtonColor = false button.LayoutOrder = order button.Size = UDim2.fromOffset(0, 22) button.AutomaticSize = Enum.AutomaticSize.X button.FontFace = Ui.FONT button.TextSize = Ui.TEXT_XS button.Text = text Ui.pad(button, 0, 10, 0, 10) Ui.corner(button, 3) button.Parent = parent return button end function Widget.new(plugin: Plugin, callbacks: Callbacks): Widget local info = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 300, 420, 240, 260) local gui = plugin:CreateDockWidgetPluginGuiAsync("RbxNativePresence", info) gui.Title = "Collaborators" gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling local scroll = Instance.new("ScrollingFrame") scroll.Size = UDim2.fromScale(1, 1) scroll.CanvasSize = UDim2.new() scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y scroll.ScrollBarThickness = 6 scroll.ScrollBarImageColor3 = Ui.color("text3") scroll.ScrollingDirection = Enum.ScrollingDirection.Y Ui.fill(scroll, "dock") scroll.Parent = gui -- One 8 px step everywhere: the edge inset, the gap between blocks and -- the cards' own padding, as the editor's docks are spaced. The bar sits -- in its own lane so it never covers the content. scroll.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar Ui.pad(scroll, Ui.GAP) Ui.list(scroll, Ui.GAP) -- The note. local noteCard = Ui.card(scroll, 1) Ui.text(noteCard, NOTE, { size = Ui.TEXT_XS, color = "text2" }) -- "Share my presence", with the status under its label: the switch, then -- a column holding the label and what the switch is doing. local shareRow = Ui.frame(scroll, 2) Ui.list(shareRow, Ui.GAP, true).VerticalAlignment = Enum.VerticalAlignment.Top local self: Widget -- The switch is centred on the label's 18 px line. local switchLane = Instance.new("Frame") switchLane.BackgroundTransparency = 1 switchLane.Size = UDim2.fromOffset(28, 18) switchLane.LayoutOrder = 1 switchLane.Parent = shareRow local switch = Ui.switch(switchLane, function() callbacks.onToggle() end) switch.frame.AnchorPoint = Vector2.new(0, 0.5) switch.frame.Position = UDim2.fromScale(0, 0.5) local shareText = Ui.frame(shareRow, 2) shareText.Size = UDim2.new(1, -(28 + Ui.GAP), 0, 0) Ui.list(shareText, 2) local toggleLabel = Ui.text(shareText, "Share my presence", { size = Ui.TEXT_SM, order = 1, wrap = false }) toggleLabel.Size = UDim2.new(1, 0, 0, 18) local status = Ui.text(shareText, "Off", { size = Ui.TEXT_XS, color = "text2", order = 2 }) -- Everyone in rbx-native, as faces, like Studio's own row of collaborators. local faces = Ui.frame(scroll, 3) faces.Visible = false local facesList = Ui.list(faces, 4, true) facesList.Padding = UDim.new(0, -4) local advanced = Ui.link(scroll, "Show advanced", 4) advanced.TextColor3 = Ui.color("text3") advanced.FontFace = Ui.FONT -- Owner setup, behind the reveal: the transport, its URL, and saving both -- into the place for every collaborator. local owner = Ui.frame(scroll, 5) owner.Visible = false Ui.list(owner, 6) Ui.text(owner, "Owner setup — saved into the place for everyone", { size = Ui.TEXT_XS, color = "text3", order = 1 }) local kinds = Instance.new("Frame") kinds.LayoutOrder = 2 kinds.AutomaticSize = Enum.AutomaticSize.X kinds.Size = UDim2.fromOffset(0, 26) Ui.fill(kinds, "field_select") Ui.corner(kinds) Ui.pad(kinds, 2) Ui.list(kinds, 2, true) kinds.Parent = owner local kindHosted = segment(kinds, "Auto", 1) local kindWorker = segment(kinds, "Worker", 2) local kindNtfy = segment(kinds, "ntfy", 3) local urlBox = Instance.new("TextBox") urlBox.LayoutOrder = 3 urlBox.Size = UDim2.new(1, 0, 0, 28) urlBox.ClearTextOnFocus = false urlBox.PlaceholderText = "Relay URL (blank = Auto, no account needed)" urlBox.PlaceholderColor3 = Ui.color("text3") urlBox.Text = "" urlBox.TextXAlignment = Enum.TextXAlignment.Left urlBox.FontFace = Ui.FONT urlBox.TextSize = Ui.TEXT_SM urlBox.TextColor3 = Ui.color("text") urlBox.TextTruncate = Enum.TextTruncate.AtEnd Ui.fill(urlBox, "field_select") Ui.corner(urlBox) local urlStroke = Ui.stroke(urlBox, "border") Ui.pad(urlBox, 0, 9, 0, 9) urlBox.Parent = owner urlBox.Focused:Connect(function() urlStroke.Color = Ui.color("check_on") urlStroke.Transparency = 0.45 end) urlBox.FocusLost:Connect(function() urlStroke.Color = Ui.color("border") urlStroke.Transparency = Ui.alpha("border") end) local saveButton = Ui.button(owner, "Save to place", false, 4) saveButton.Size = UDim2.fromOffset(110, 28) local resolved = Ui.text(owner, "", { size = Ui.TEXT_XS, color = "text3", order = 5 }) local httpNote = Ui.card(scroll, 6) httpNote.Visible = false Ui.text(httpNote, HTTP_NOTE, { size = Ui.TEXT_XS, color = "text2" }) -- "While you were away", hidden until something changed. local catchup = Ui.card(scroll, 7, 4) catchup.Name = "Catchup" catchup.Visible = false local head = Ui.frame(catchup, 1) local title = Ui.text(head, "While you were away", { size = Ui.TEXT_XS, font = Ui.FONT_SEMIBOLD, wrap = false }) title.Size = UDim2.new(1, -60, 0, 16) local dismiss = Ui.link(head, "Dismiss") dismiss.TextColor3 = Ui.color("text3") dismiss.FontFace = Ui.FONT dismiss.AnchorPoint = Vector2.new(1, 0) dismiss.Position = UDim2.fromScale(1, 0) local catchupBody = Ui.frame(catchup, 2) Ui.list(catchupBody, 3) local rows = Ui.frame(scroll, 8) Ui.list(rows, Ui.GAP) self = setmetatable({ gui = gui, enabled = false, relayKind = "hosted", switch = switch, status = status, faces = faces, advanced = advanced, owner = owner, kindHosted = kindHosted, kindWorker = kindWorker, kindNtfy = kindNtfy, urlBox = urlBox, resolved = resolved, httpNote = httpNote, catchup = catchup, catchupBody = catchupBody, rows = rows, onViewChanges = callbacks.onViewChanges, connections = {}, }, Widget) self:showKind() table.insert( self.connections, advanced.Activated:Connect(function() owner.Visible = not owner.Visible advanced.Text = if owner.Visible then "Hide advanced" else "Show advanced" end) ) table.insert( self.connections, kindHosted.Activated:Connect(function() self.relayKind = "hosted" self:showKind() end) ) table.insert( self.connections, kindWorker.Activated:Connect(function() self.relayKind = "worker" self:showKind() end) ) table.insert( self.connections, kindNtfy.Activated:Connect(function() self.relayKind = "ntfy" self:showKind() end) ) table.insert( self.connections, saveButton.Activated:Connect(function() callbacks.onSaveRelay(self.relayKind, (string.gsub(urlBox.Text, "^%s*(.-)%s*$", "%1"))) end) ) table.insert( self.connections, dismiss.Activated:Connect(function() self.catchup.Visible = false end) ) return self end -- The segmented control's active half: `accent_soft` behind `check_on`. function Widget.showKind(self: Widget) for _, pair in { { self.kindHosted, "hosted" }, { self.kindWorker, "worker" }, { self.kindNtfy, "ntfy" } } do local button, kind = pair[1] :: TextButton, pair[2] :: string local active = self.relayKind == kind Ui.fill(button, if active then "accent_soft" else "field_select") button.TextColor3 = Ui.color(if active then "check_on" else "text2") button.FontFace = if active then Ui.FONT_SEMIBOLD else Ui.FONT end end function Widget.setEnabled(self: Widget, on: boolean) self.enabled = on self.switch.set(on) end function Widget.setStatus(self: Widget, text: string, failed: boolean?) self.status.Text = text self.status.TextColor3 = Ui.color(if failed then "text_error" else "text2") end -- Owner setup: show the kind, prefill the URL, and say which relay is in use. function Widget.setRelay(self: Widget, kind: string, url: string, isCommunity: boolean) self.relayKind = kind self:showKind() if not isCommunity then self.urlBox.Text = url self.resolved.Text = `Relay: {url} ({kind})` else self.resolved.Text = "Relay: Auto — hosted by whoever opens the place in rbx-native, no account needed" end end function Widget.showHttpNote(self: Widget, show: boolean) self.httpNote.Visible = show end -- "While you were away": one line per author (value-less, as rbx-native's -- dock words it), then how many instances changed and "View changes" when -- there is anything to show. `nil` or an empty summary hides the card. function Widget.setCatchup(self: Widget, summary: any?) for _, child in self.catchupBody:GetChildren() do if not child:IsA("UIListLayout") then child:Destroy() end end if not summary or #summary.authors == 0 then self.catchup.Visible = false return end self.catchup.Visible = true local order = 0 for _, author in summary.authors do order += 1 Ui.text(self.catchupBody, Catchup.authorLine(author), { size = Ui.TEXT_XS, color = "text2", order = order }) end local view = Ui.link(self.catchupBody, "View changes", order + 1) view.Activated:Connect(function() self.onViewChanges() end) end local function detailLines(user: any): { string } local lines: { string } = {} if #user.selection > 0 then table.insert(lines, `Selected: {#user.selection}`) end if user.script then table.insert(lines, `Has {user.script} open`) end local newest: any = nil for _, edit in user.edits do if not newest or edit.at >= newest.at then newest = edit end end if newest then table.insert(lines, `{newest.property} of {newest.path} -> {tostring(newest.value)}`) end return lines end -- One row per rbx-native collaborator: their colour's dot, the name, "Sending -- presence", then what they have selected, open and last edited. function Widget.setUsers(self: Widget, users: { any }) for _, holder in { self.rows, self.faces } do for _, child in holder:GetChildren() do if not child:IsA("UIListLayout") then child:Destroy() end end end local shown = 0 for _, user in users do if user.client == "rbx-native" and shown < 8 then shown += 1 local face = Ui.avatar(self.faces, user.userId, Ui.hex(user.color), 26, shown) face.Name = user.name end end self.faces.Visible = shown > 0 for index, user in users do if user.client == "rbx-native" then local row = Ui.frame(self.rows, index) Ui.list(row, 3) local line = Ui.frame(row, 1) Ui.list(line, 8, true) Ui.avatar(line, user.userId, Ui.hex(user.color), 22, 1) local name = Ui.text(line, user.name, { size = Ui.TEXT_XS, order = 2, wrap = false }) name.AutomaticSize = Enum.AutomaticSize.X name.Size = UDim2.fromOffset(0, 16) local state = Ui.text(line, "Sending presence", { size = Ui.TEXT_XS, color = "text3", order = 3, wrap = false }) state.AutomaticSize = Enum.AutomaticSize.X state.Size = UDim2.fromOffset(0, 16) for i, text in detailLines(user) do local detail = Ui.text(row, text, { size = Ui.TEXT_XS, color = "text2", order = 1 + i }) local pad = Instance.new("UIPadding") pad.PaddingLeft = UDim.new(0, 30) pad.Parent = detail end end end end function Widget.destroy(self: Widget) for _, connection in self.connections do connection:Disconnect() end table.clear(self.connections) self.gui:Destroy() end return Widget