Refactor and rebrand Rojo to Atlas

- Renamed the project from Rojo to Atlas, updating all relevant files, scripts, and documentation to reflect the new branding.
- Bumped version to 7.7.0-rc.2 and updated protocol version from 5 to 6.
- Modified installation scripts and CI workflows to use the new binary name and paths.
- Adjusted plugin settings and internal identifiers to prevent conflicts with the original Rojo.
- Enhanced session management and API response handling to support the new server fork identifier.
- Updated project templates and README files to guide users on using Atlas.
This commit is contained in:
Joe 2026-02-07 13:21:52 -06:00
parent 0e263789b0
commit 50b9e18514
54 changed files with 496 additions and 218 deletions

View file

@ -1 +1 @@
7.7.0-rc.1
7.7.0-rc.2

View file

@ -27,11 +27,10 @@ end
local function rejectWrongProtocolVersion(infoResponseBody)
if infoResponseBody.protocolVersion ~= Config.protocolVersion then
local message = (
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible."
.. "\nMake sure you have matching versions of both the Rojo plugin and server!"
"Found a server, but it's using a different protocol version, and is incompatible."
.. "\nMake sure you have matching versions of both the Atlas plugin and server!"
.. "\n\nYour client is version %s, with protocol version %s. It expects server version %s."
.. "\nYour server is version %s, with protocol version %s."
.. "\n\nGo to https://github.com/rojo-rbx/rojo for more details."
):format(
Version.display(Config.version),
Config.protocolVersion,
@ -46,6 +45,20 @@ local function rejectWrongProtocolVersion(infoResponseBody)
return Promise.resolve(infoResponseBody)
end
local function rejectWrongFork(infoResponseBody)
if infoResponseBody.serverFork ~= "atlas" then
local message = (
"Found a Rojo server, but this plugin requires an Atlas server."
.. "\nThe stock Rojo server is not compatible with the Atlas plugin."
.. "\nMake sure you are running 'atlas serve' instead of 'rojo serve'."
)
return Promise.reject(message)
end
return Promise.resolve(infoResponseBody)
end
local function rejectWrongPlaceId(infoResponseBody)
if infoResponseBody.expectedPlaceIds ~= nil then
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId)
@ -147,6 +160,7 @@ function ApiContext:connect()
:andThen(rejectFailedRequests)
:andThen(Http.Response.msgpack)
:andThen(rejectWrongProtocolVersion)
:andThen(rejectWrongFork)
:andThen(function(body)
assert(validateApiInfo(body))

View file

@ -81,25 +81,65 @@ function SelectionOption:render()
local buttonWidth = if props.text == "Studio" then 44 else 36
-- Transparency logic with hover effect
-- Combines real hover state with subtree highlight binding so the matching
-- button on child nodes looks hovered when a parent's button is hovered.
local bgTransparency, txtTransparency
if isParentOnly then
-- Parent-only: partially transparent, less so when hovering the button
bgTransparency = props.transparency:map(function(t)
return if isHovered then 0.2 + (0.8 * t) else 0.5 + (0.5 * t)
end)
txtTransparency = props.transparency:map(function(t)
return if isHovered then 0 + t else 0.3 + (0.7 * t)
end)
else
-- Normal buttons: show hover effect by reducing transparency
bgTransparency = props.transparency:map(function(t)
if isSelected then
return if isHovered then 0 + (1 * t) else 0.1 + (0.9 * t)
local subtreeBinding = props.subtreeHighlightBinding
local ancestorIds = props.ancestorIds
if subtreeBinding and ancestorIds then
local optionType = props.optionType
local joined = Roact.joinBindings({
transparency = props.transparency,
highlight = subtreeBinding,
})
bgTransparency = joined:map(function(values)
local t = values.transparency
local info = values.highlight
local subtreeHov = info ~= nil and ancestorIds[info.nodeId] == true and info.optionType == optionType
local hov = isHovered or subtreeHov
if isParentOnly then
return if hov then 0.2 + (0.8 * t) else 0.5 + (0.5 * t)
elseif isSelected then
return if hov then 0 + (1 * t) else 0.1 + (0.9 * t)
else
return if isHovered then 0.5 + (0.5 * t) else 0.7 + (0.3 * t)
return if hov then 0.5 + (0.5 * t) else 0.7 + (0.3 * t)
end
end)
txtTransparency = props.transparency
txtTransparency = joined:map(function(values)
local t = values.transparency
local info = values.highlight
local subtreeHov = info ~= nil and ancestorIds[info.nodeId] == true and info.optionType == optionType
local hov = isHovered or subtreeHov
if isParentOnly then
return if hov then 0 + t else 0.3 + (0.7 * t)
else
return t
end
end)
else
-- Fallback: no subtree highlight available
if isParentOnly then
bgTransparency = props.transparency:map(function(t)
return if isHovered then 0.2 + (0.8 * t) else 0.5 + (0.5 * t)
end)
txtTransparency = props.transparency:map(function(t)
return if isHovered then 0 + t else 0.3 + (0.7 * t)
end)
else
bgTransparency = props.transparency:map(function(t)
if isSelected then
return if isHovered then 0 + (1 * t) else 0.1 + (0.9 * t)
else
return if isHovered then 0.5 + (0.5 * t) else 0.7 + (0.3 * t)
end
end)
txtTransparency = props.transparency
end
end
return e("TextButton", {
@ -118,7 +158,7 @@ function SelectionOption:render()
self:setState({ isHovered = true })
-- Trigger subtree highlight when this button would apply to children
if (isParentOnly or (isSelected and hasChildren)) and props.onSubtreeHoverStart then
props.onSubtreeHoverStart()
props.onSubtreeHoverStart(props.optionType)
end
end,
[Roact.Event.MouseLeave] = function()
@ -180,6 +220,8 @@ local function SelectionRadio(props)
layoutOrder = 1,
hasChildren = hasChildren,
isParentOnly = isParentOnly,
subtreeHighlightBinding = props.subtreeHighlightBinding,
ancestorIds = props.ancestorIds,
onSubtreeHoverStart = props.onSubtreeHoverStart,
onSubtreeHoverEnd = props.onSubtreeHoverEnd,
onClick = function()
@ -201,6 +243,8 @@ local function SelectionRadio(props)
layoutOrder = 2,
hasChildren = hasChildren,
isParentOnly = isParentOnly,
subtreeHighlightBinding = props.subtreeHighlightBinding,
ancestorIds = props.ancestorIds,
onSubtreeHoverStart = props.onSubtreeHoverStart,
onSubtreeHoverEnd = props.onSubtreeHoverEnd,
onClick = function()
@ -222,6 +266,8 @@ local function SelectionRadio(props)
layoutOrder = 3,
hasChildren = hasChildren,
isParentOnly = isParentOnly,
subtreeHighlightBinding = props.subtreeHighlightBinding,
ancestorIds = props.ancestorIds,
onSubtreeHoverStart = props.onSubtreeHoverStart,
onSubtreeHoverEnd = props.onSubtreeHoverEnd,
onClick = function()
@ -337,17 +383,6 @@ function DomLabel:render()
local props = self.props
local depth = props.depth or 1
-- Derive subtree highlight state from binding (no re-renders needed)
local ancestorIds = props.ancestorIds
local isSubtreeHighlighted = if props.subtreeHighlightNodeId
then props.subtreeHighlightNodeId:map(function(highlightId)
if highlightId == nil then
return false
end
return ancestorIds[highlightId] == true
end)
else nil
return Theme.with(function(theme)
local color = if props.isWarning
then theme.Diff.Warning
@ -544,17 +579,6 @@ function DomLabel:render()
})
else nil,
}),
SubtreeHighlight = if isSubtreeHighlighted
then e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundColor3 = theme.Diff.SubtreeHighlight,
BackgroundTransparency = isSubtreeHighlighted:map(function(highlighted)
return if highlighted then 0.92 else 1
end),
BorderSizePixel = 0,
ZIndex = 0,
})
else nil,
SelectionRadio = e(SelectionRadio, {
-- Visible for nodes with patchType, or for parent-only nodes on hover
visible = (props.patchType ~= nil and props.onSelectionChange ~= nil)
@ -572,9 +596,12 @@ function DomLabel:render()
isParentOnly = props.patchType == nil,
onSelectionChange = props.onSelectionChange,
onSubtreeSelectionChange = props.onSubtreeSelectionChange,
onSubtreeHoverStart = function()
-- Subtree hover highlight: pass binding and ancestors to child buttons
subtreeHighlightBinding = props.subtreeHighlightNodeId,
ancestorIds = props.ancestorIds,
onSubtreeHoverStart = function(optionType)
if props.setSubtreeHighlightNodeId then
props.setSubtreeHighlightNodeId(props.nodeId)
props.setSubtreeHighlightNodeId({ nodeId = props.nodeId, optionType = optionType })
end
end,
onSubtreeHoverEnd = function()

View file

@ -245,7 +245,7 @@ function ConfirmingPage:render()
}),
StringDiff = e(StudioPluginGui, {
id = "Rojo_ConfirmingStringDiff",
id = "Atlas_ConfirmingStringDiff",
title = "String diff",
active = self.state.showingStringDiff,
isEphemeral = true,
@ -283,7 +283,7 @@ function ConfirmingPage:render()
}),
TableDiff = e(StudioPluginGui, {
id = "Rojo_ConfirmingTableDiff",
id = "Atlas_ConfirmingTableDiff",
title = "Table diff",
active = self.state.showingTableDiff,
isEphemeral = true,
@ -322,7 +322,7 @@ function ConfirmingPage:render()
-- Accept confirmation popup for unselected items
AcceptConfirm = e(StudioPluginGui, {
id = "Rojo_AcceptConfirm",
id = "Atlas_AcceptConfirm",
title = "Unreviewed Items",
active = self.state.showingAcceptConfirm,
isEphemeral = true,
@ -400,7 +400,7 @@ function ConfirmingPage:render()
if self.props.createPopup then
return e(StudioPluginGui, {
id = "Rojo_DiffSync",
id = "Atlas_DiffSync",
title = string.format(
"Confirm sync for project '%s':",
self.props.confirmData.serverInfo.projectName or "UNKNOWN"

View file

@ -481,7 +481,7 @@ function ConnectedPage:render()
}),
ChangesViewer = e(StudioPluginGui, {
id = "Rojo_ChangesViewer",
id = "Atlas_ChangesViewer",
title = "View changes",
active = self.state.renderChanges,
isEphemeral = true,
@ -536,7 +536,7 @@ function ConnectedPage:render()
}),
StringDiff = e(StudioPluginGui, {
id = "Rojo_ConnectedStringDiff",
id = "Atlas_ConnectedStringDiff",
title = "String diff",
active = self.state.showingStringDiff,
isEphemeral = true,
@ -574,7 +574,7 @@ function ConnectedPage:render()
}),
TableDiff = e(StudioPluginGui, {
id = "Rojo_ConnectedTableDiff",
id = "Atlas_ConnectedTableDiff",
title = "Table diff",
active = self.state.showingTableDiff,
isEphemeral = true,

View file

@ -237,7 +237,6 @@ function StudioProvider:updateTheme()
Edit = Color3.fromRGB(255, 0, 255),
Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
SubtreeHighlight = if isDark then Color3.fromRGB(255, 255, 255) else Color3.fromRGB(0, 0, 0),
Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
WhitespaceOnly = if isDark then Color3.fromRGB(140, 140, 140) else Color3.fromRGB(110, 110, 110),
Changes = if isDark then Color3.fromRGB(100, 140, 220) else Color3.fromRGB(40, 100, 180),

View file

@ -63,7 +63,7 @@ function App:init()
self.notifId = 0
self.waypointConnection = ChangeHistoryService.OnUndo:Connect(function(action: string)
if not string.find(action, "^Rojo: Patch") then
if not string.find(action, "^Atlas: Patch") then
return
end
@ -318,7 +318,7 @@ function App:isSyncLockAvailable()
return true
end
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
local lock = ServerStorage:FindFirstChild("__Atlas_SessionLock")
if not lock then
-- No lock is made yet, so it is available
return true
@ -345,10 +345,10 @@ function App:claimSyncLock()
return false, priorOwner
end
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
local lock = ServerStorage:FindFirstChild("__Atlas_SessionLock")
if not lock then
lock = Instance.new("ObjectValue")
lock.Name = "__Rojo_SessionLock"
lock.Name = "__Atlas_SessionLock"
lock.Archivable = false
lock.Value = Players.LocalPlayer
lock.Parent = ServerStorage
@ -362,7 +362,7 @@ function App:claimSyncLock()
end
function App:releaseSyncLock()
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
local lock = ServerStorage:FindFirstChild("__Atlas_SessionLock")
if not lock then
Log.trace("No sync lock found, assumed released")
return
@ -450,7 +450,7 @@ function App:checkSyncReminder()
local timeSinceSync = timeUtil.elapsedToText(os.time() - priorSyncInfo.timestamp)
self:sendSyncReminder(
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'rojo serve' and then connect?`
`You synced project '{priorSyncInfo.projectName}' to this place {timeSinceSync}.\nDid you mean to run 'atlas serve' and then connect?`
)
end
end)
@ -531,7 +531,7 @@ function App:isAutoConnectPlaytestServerAvailable()
and RunService:IsStudio()
and RunService:IsServer()
and Settings:get("autoConnectPlaytestServer")
and workspace:GetAttribute("__Rojo_ConnectionUrl")
and workspace:GetAttribute("__Atlas_ConnectionUrl")
end
function App:isAutoConnectPlaytestServerWriteable()
@ -544,7 +544,7 @@ function App:setRunningConnectionInfo(baseUrl: string)
end
Log.trace("Setting connection info for play solo auto-connect")
workspace:SetAttribute("__Rojo_ConnectionUrl", baseUrl)
workspace:SetAttribute("__Atlas_ConnectionUrl", baseUrl)
end
function App:clearRunningConnectionInfo()
@ -554,11 +554,11 @@ function App:clearRunningConnectionInfo()
end
Log.trace("Clearing connection info for play solo auto-connect")
workspace:SetAttribute("__Rojo_ConnectionUrl", nil)
workspace:SetAttribute("__Atlas_ConnectionUrl", nil)
end
function App:useRunningConnectionInfo()
local connectionInfo = workspace:GetAttribute("__Rojo_ConnectionUrl")
local connectionInfo = workspace:GetAttribute("__Atlas_ConnectionUrl")
if not connectionInfo then
return
end
@ -890,7 +890,8 @@ function App:endSession()
-- but the UI is still stuck on Connected/Connecting, force reset to NotConnected.
-- This handles race conditions where onStatusChanged(Connected) fires after
-- onStatusChanged(Disconnected) due to a coroutine resuming on a dead session.
if self.state.appStatus ~= AppStatus.NotConnected
if
self.state.appStatus ~= AppStatus.NotConnected
and self.state.appStatus ~= AppStatus.Error
and self.state.appStatus ~= AppStatus.Settings
then

View file

@ -27,8 +27,8 @@ return strict("Config", {
codename = "Epiphany",
version = realVersion,
expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]),
protocolVersion = 5,
protocolVersion = 6,
defaultHost = "localhost",
defaultPort = "34872",
defaultPort = "34873",
ignoredClassNames = IgnoredClassNames,
})

View file

@ -38,11 +38,11 @@ Settings._bindings = {}
if plugin then
for name, defaultValue in pairs(Settings._values) do
local savedValue = plugin:GetSetting("Rojo_" .. name)
local savedValue = plugin:GetSetting("Atlas_" .. name)
if savedValue == nil then
-- plugin:SetSetting hits disc instead of memory, so it can be slow. Spawn so we don't hang.
task.spawn(plugin.SetSetting, plugin, "Rojo_" .. name, defaultValue)
task.spawn(plugin.SetSetting, plugin, "Atlas_" .. name, defaultValue)
Settings._values[name] = defaultValue
else
Settings._values[name] = savedValue
@ -67,7 +67,7 @@ function Settings:set(name, value)
if plugin then
-- plugin:SetSetting hits disc instead of memory, so it can be slow. Spawn so we don't hang.
task.spawn(plugin.SetSetting, plugin, "Rojo_" .. name, value)
task.spawn(plugin.SetSetting, plugin, "Atlas_" .. name, value)
end
if self._updateListeners[name] then

View file

@ -39,6 +39,7 @@ local ApiInfoResponse = t.interface({
sessionId = t.string,
serverVersion = t.string,
protocolVersion = t.number,
serverFork = t.string,
expectedPlaceIds = t.optional(t.array(t.number)),
rootInstanceId = RbxId,
})