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