Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api

This commit is contained in:
boatbomber 2023-07-04 13:45:36 -07:00
commit 1ff018a2ce
35 changed files with 571 additions and 212 deletions

View file

@ -93,6 +93,7 @@ function ApiContext.new(baseUrl)
__sessionId = nil,
__messageCursor = -1,
__connected = true,
__activeRequests = {},
}
return setmetatable(self, ApiContext)
@ -113,6 +114,11 @@ end
function ApiContext:disconnect()
self.__connected = false
for request in self.__activeRequests do
Log.trace("Cancelling request {}", request)
request:cancel()
end
self.__activeRequests = {}
end
function ApiContext:setMessageCursor(index)
@ -204,7 +210,7 @@ function ApiContext:retrieveMessages()
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
local function sendRequest()
return Http.get(url)
local request = Http.get(url)
:catch(function(err)
if err.type == Http.Error.Kind.Timeout then
if self.__connected then
@ -216,6 +222,15 @@ function ApiContext:retrieveMessages()
return Promise.reject(err)
end)
Log.trace("Tracking request {}", request)
self.__activeRequests[request] = true
return request:finally(function(...)
Log.trace("Cleaning up request {}", request)
self.__activeRequests[request] = nil
return ...
end)
end
return sendRequest()

View file

@ -40,11 +40,6 @@ end
local DomLabel = Roact.Component:extend("DomLabel")
function DomLabel:init()
self.maxElementHeight = 0
if self.props.changeList then
self.maxElementHeight = math.clamp(#self.props.changeList * 30, 30, 30 * 6)
end
local initHeight = self.props.elementHeight:getValue()
self.expanded = initHeight > 30
@ -118,7 +113,8 @@ function DomLabel:render()
Size = UDim2.new(1, 0, 1, 0),
[Roact.Event.Activated] = function()
self.expanded = not self.expanded
self.motor:setGoal(Flipper.Spring.new((self.expanded and self.maxElementHeight or 0) + 30, {
local goalHeight = 30 + (if self.expanded then math.clamp(#self.props.changeList * 30, 30, 30 * 6) else 0)
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
frequency = 5,
dampingRatio = 1,
}))

View file

@ -99,18 +99,30 @@ function VirtualScroller:refresh()
})
end
function VirtualScroller:didUpdate(previousProps)
if self.props.count ~= previousProps.count then
-- Items have changed, so we need to refresh
self:refresh()
end
end
function VirtualScroller:render()
local props, state = self.props, self.state
local items = {}
for i = state.Start, state.End do
local content = props.render(i)
if content == nil then
continue
end
items["Item" .. i] = e("Frame", {
LayoutOrder = i,
Size = props.getHeightBinding(i):map(function(height)
return UDim2.new(1, 0, 0, height)
end),
BackgroundTransparency = 1,
}, props.render(i))
}, content)
end
return Theme.with(function(theme)

View file

@ -7,6 +7,7 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local Log = require(Packages.Log)
local bindingUtil = require(script.Parent.bindingUtil)
@ -14,6 +15,7 @@ local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local TextButton = require(Plugin.App.Components.TextButton)
local baseClock = DateTime.now().UnixTimestampMillis
@ -28,10 +30,8 @@ function Notification:init()
self.lifetime = self.props.timeout
self.motor:onStep(function(value)
if value <= 0 then
if self.props.onClose then
self.props.onClose()
end
if value <= 0 and self.props.onClose then
self.props.onClose()
end
end)
end
@ -86,10 +86,9 @@ function Notification:willUnmount()
end
function Notification:render()
local time = DateTime.fromUnixTimestampMillis(self.props.timestamp)
local infoText =
time:FormatLocalTime("LTS", "en-us") ..
(if self.props.source then " • From: " .. self.props.source else "")
local transparency = self.binding:map(function(value)
return 1 - value
end)
local textBounds = TextService:GetTextSize(
self.props.text,
@ -98,23 +97,43 @@ function Notification:render()
Vector2.new(350, 700)
)
local infoTextBounds = TextService:GetTextSize(
infoText,
12,
Enum.Font.Gotham,
Vector2.new(350, 14)
)
local actionButtons = {}
local buttonsX = 0
if self.props.actions then
local count = 0
for key, action in self.props.actions do
actionButtons[key] = e(TextButton, {
text = action.text,
style = action.style,
onClick = function()
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end,
layoutOrder = -action.layoutOrder,
transparency = transparency,
})
local textWidth = math.max(infoTextBounds.X, textBounds.X)
buttonsX += TextService:GetTextSize(
action.text, 18, Enum.Font.GothamMedium,
Vector2.new(math.huge, math.huge)
).X + 30
local transparency = self.binding:map(function(value)
return 1 - value
end)
count += 1
end
buttonsX += (count - 1) * 5
end
local paddingY, logoSize = 20, 32
local actionsY = if self.props.actions then 35 else 0
local contentX = math.max(textBounds.X, buttonsX)
local size = self.binding:map(function(value)
return UDim2.fromOffset(
(35+40+textWidth)*value,
math.max(16+20+textBounds.Y, 54)
(35 + 40 + contentX) * value,
5 + actionsY + paddingY + math.max(logoSize, textBounds.Y)
)
end)
@ -134,18 +153,18 @@ function Notification:render()
transparency = transparency,
size = UDim2.new(1, 0, 1, 0),
}, {
TextContainer = e("Frame", {
Size = UDim2.new(1, 0, 1, -20),
Position = UDim2.new(0, 0, 0, 10),
Contents = e("Frame", {
Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
Position = UDim2.new(0, 0, 0, paddingY / 2),
BackgroundTransparency = 1
}, {
Logo = e("ImageLabel", {
ImageTransparency = transparency,
Image = if self.props.thirdParty then Assets.Images.ThirdPartyPlugin else Assets.Images.PluginButton,
BackgroundTransparency = 1,
Size = UDim2.new(0, 32, 0, 32),
Position = UDim2.new(0, 0, 0.5, 0),
AnchorPoint = Vector2.new(0, 0.5),
Size = UDim2.new(0, logoSize, 0, logoSize),
Position = UDim2.new(0, 0, 0, 0),
AnchorPoint = Vector2.new(0, 0),
}),
Message = e("TextLabel", {
Text = self.props.text,
@ -162,20 +181,21 @@ function Notification:render()
LayoutOrder = 1,
BackgroundTransparency = 1,
}),
Info = e("TextLabel", {
Text = infoText,
Font = Enum.Font.Gotham,
TextSize = 12,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = transparency,
TextXAlignment = Enum.TextXAlignment.Left,
Size = UDim2.new(1, -38, 0, 14),
Position = UDim2.new(0, 38, 1, -14),
LayoutOrder = 1,
Actions = if self.props.actions then e("Frame", {
Size = UDim2.new(1, -40, 0, 35),
Position = UDim2.new(1, 0, 1, 0),
AnchorPoint = Vector2.new(1, 1),
BackgroundTransparency = 1,
}),
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
}),
Buttons = Roact.createFragment(actionButtons),
}) else nil,
}),
Padding = e("UIPadding", {
@ -192,19 +212,20 @@ local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local notifs = {}
for index, notif in ipairs(self.props.notifications) do
local props = {
for id, notif in self.props.notifications do
notifs["NotifID_" .. id] = e(Notification, {
soundPlayer = self.props.soundPlayer,
text = notif.text,
timestamp = notif.timestamp,
timeout = notif.timeout,
actions = notif.actions,
source = notif.source,
thirdParty = notif.thirdParty,
layoutOrder = (notif.timestamp - baseClock),
onClose = function()
self.props.onClose(index)
self.props.onClose(id)
end,
}
for key, value in notif do
props[key] = value
end
notifs[notif] = e(Notification, props)
})
end
return Roact.createFragment(notifs)

View file

@ -26,11 +26,11 @@ function timeSinceText(elapsed: number): string
local ageText = string.format("%d seconds ago", elapsed)
for _,UnitData in ipairs(AGE_UNITS) do
for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds, UnitName = UnitData[1], UnitData[2]
if elapsed > UnitSeconds then
local c = math.floor(elapsed/UnitSeconds)
ageText = string.format("%d %s%s ago", c, UnitName, c>1 and "s" or "")
local c = math.floor(elapsed / UnitSeconds)
ageText = string.format("%d %s%s ago", c, UnitName, c > 1 and "s" or "")
break
end
end
@ -38,45 +38,53 @@ function timeSinceText(elapsed: number): string
return ageText
end
local function ChangesDrawer(props)
if props.rendered == false then
local ChangesDrawer = Roact.Component:extend("ConnectedPage")
function ChangesDrawer:init()
-- Hold onto the serve session during the lifecycle of this component
-- so that it can still render during the fade out after disconnecting
self.serveSession = self.props.serveSession
end
function ChangesDrawer:render()
if self.props.rendered == false or self.serveSession == nil then
return nil
end
return Theme.with(function(theme)
return e(BorderedContainer, {
transparency = props.transparency,
size = props.height:map(function(y)
transparency = self.props.transparency,
size = self.props.height:map(function(y)
return UDim2.new(1, 0, y, -180 * y)
end),
position = UDim2.new(0, 0, 1, 0),
anchorPoint = Vector2.new(0, 1),
layoutOrder = props.layoutOrder,
layoutOrder = self.props.layoutOrder,
}, {
Close = e(IconButton, {
icon = Assets.Images.Icons.Close,
iconSize = 24,
color = theme.ConnectionDetails.DisconnectColor,
transparency = props.transparency,
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0, 0),
anchorPoint = Vector2.new(1, 0),
onClick = props.onClose,
onClick = self.props.onClose,
}, {
Tip = e(Tooltip.Trigger, {
text = "Close the patch visualizer"
text = "Close the patch visualizer",
}),
}),
PatchVisualizer = e(PatchVisualizer, {
size = UDim2.new(1, 0, 1, 0),
transparency = props.transparency,
transparency = self.props.transparency,
layoutOrder = 3,
columnVisibility = {true, false, true},
patch = props.patchInfo:getValue().patch,
instanceMap = props.serveSession.__instanceMap,
columnVisibility = { true, false, true },
patch = self.props.patch,
instanceMap = self.serveSession.__instanceMap,
}),
})
end)
@ -91,7 +99,7 @@ local function ConnectionDetails(props)
}, {
TextContainer = e("Frame", {
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1
BackgroundTransparency = 1,
}, {
ProjectName = e("TextLabel", {
Text = props.projectName,
@ -141,7 +149,7 @@ local function ConnectionDetails(props)
onClick = props.onDisconnect,
}, {
Tip = e(Tooltip.Trigger, {
text = "Disconnect from the Rojo sync server"
text = "Disconnect from the Rojo sync server",
}),
}),
@ -155,6 +163,18 @@ end
local ConnectedPage = Roact.Component:extend("ConnectedPage")
function ConnectedPage:getChangeInfoText()
local patchData = self.props.patchData
if patchData == nil then
return ""
end
local elapsed = os.time() - patchData.timestamp
local changes = PatchSet.countChanges(patchData.patch)
return string.format("<i>Synced %d change%s %s</i>", changes, changes == 1 and "" or "s", timeSinceText(elapsed))
end
function ConnectedPage:init()
self.changeDrawerMotor = Flipper.SingleMotor.new(0)
self.changeDrawerHeight = bindingUtil.fromMotor(self.changeDrawerMotor)
@ -176,6 +196,34 @@ function ConnectedPage:init()
self:setState({
renderChanges = false,
})
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
self.changeInfoTextUpdater = task.defer(function()
while true do
self.setChangeInfoText(self:getChangeInfoText())
local elapsed = os.time() - self.props.patchData.timestamp
local updateInterval = 1
-- Update timestamp text as frequently as currently needed
for _, UnitData in ipairs(AGE_UNITS) do
local UnitSeconds = UnitData[1]
if elapsed >= UnitSeconds then
updateInterval = UnitSeconds
break
end
end
task.wait(updateInterval)
end
end)
end
function ConnectedPage:willUnmount()
if self.changeInfoTextUpdater then
task.cancel(self.changeInfoTextUpdater)
end
end
function ConnectedPage:render()
@ -208,15 +256,7 @@ function ConnectedPage:render()
}),
ChangeInfo = e("TextButton", {
Text = self.props.patchInfo:map(function(info)
local changes = PatchSet.countChanges(info.patch)
return string.format(
"<i>Synced %d change%s %s</i>",
changes,
changes == 1 and "" or "s",
timeSinceText(os.time() - info.timestamp)
)
end),
Text = self.changeInfoText,
Font = Enum.Font.Gotham,
TextSize = 14,
TextWrapped = true,
@ -249,7 +289,7 @@ function ConnectedPage:render()
ChangesDrawer = e(ChangesDrawer, {
rendered = self.state.renderChanges,
transparency = self.props.transparency,
patchInfo = self.props.patchInfo,
patch = self.props.patchData.patch,
serveSession = self.props.serveSession,
height = self.changeDrawerHeight,
layoutOrder = 4,

View file

@ -62,6 +62,7 @@ function Setting:render()
LayoutOrder = self.props.layoutOrder,
ZIndex = -self.props.layoutOrder,
BackgroundTransparency = 1,
Visible = self.props.visible,
[Roact.Change.AbsoluteSize] = function(object)
self.setContainerSize(object.AbsoluteSize)

View file

@ -88,19 +88,36 @@ function SettingsPage:render()
layoutOrder = 0,
}),
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
description = "Attempt to open scripts in an external editor",
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
transparency = self.props.transparency,
layoutOrder = 1,
}),
SyncReminder = e(Setting, {
id = "syncReminder",
name = "Sync Reminder",
description = "Notify to sync when opening a place that has previously been synced",
transparency = self.props.transparency,
visible = Settings:getBinding("showNotifications"),
layoutOrder = 2,
}),
PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency,
layoutOrder = 3,
}),
Permissions = e(Setting, {
name = "Third Party Permissions",
description = "Manage permissions for third party plugins",
transparency = self.props.transparency,
layoutOrder = 2,
layoutOrder = 4,
customInput = e("TextButton", {
Text = "",
BackgroundTransparency = 1,
@ -136,20 +153,12 @@ function SettingsPage:render()
}),
}),
ShowNotifications = e(Setting, {
id = "showNotifications",
name = "Show Notifications",
description = "Popup notifications in viewport",
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
description = "EXPERIMENTAL! Attempt to open scripts in an external editor",
transparency = self.props.transparency,
layoutOrder = 3,
}),
PlaySounds = e(Setting, {
id = "playSounds",
name = "Play Sounds",
description = "Toggle sound effects",
transparency = self.props.transparency,
layoutOrder = 4,
layoutOrder = 5,
}),
TwoWaySync = e(Setting, {
@ -157,7 +166,7 @@ function SettingsPage:render()
name = "Two-Way Sync",
description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
transparency = self.props.transparency,
layoutOrder = 5,
layoutOrder = 6,
}),
LogLevel = e(Setting, {
@ -165,7 +174,7 @@ function SettingsPage:render()
name = "Log Level",
description = "Plugin output verbosity level",
transparency = self.props.transparency,
layoutOrder = 6,
layoutOrder = 100,
options = invertedLevels,
showReset = Settings:getBinding("logLevel"):map(function(value)
@ -181,7 +190,7 @@ function SettingsPage:render()
name = "Typechecking",
description = "Toggle typechecking on the API surface",
transparency = self.props.transparency,
layoutOrder = 7,
layoutOrder = 101,
}),
Layout = e("UIListLayout", {

View file

@ -1,5 +1,6 @@
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
@ -20,6 +21,7 @@ local HeadlessAPI = require(Plugin.HeadlessAPI)
local PatchSet = require(Plugin.PatchSet)
local preloadAssets = require(Plugin.preloadAssets)
local soundPlayer = require(Plugin.soundPlayer)
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
local Theme = require(script.Theme)
local Page = require(script.Page)
@ -55,17 +57,18 @@ function App:init()
self.host, self.setHost = Roact.createBinding(priorHost or "")
self.port, self.setPort = Roact.createBinding(priorPort or "")
self.patchInfo, self.setPatchInfo = Roact.createBinding({
patch = PatchSet.newEmpty(),
timestamp = os.time(),
})
self.confirmationBindable = Instance.new("BindableEvent")
self.confirmationEvent = self.confirmationBindable.Event
self.notifId = 0
self:setState({
appStatus = AppStatus.NotConnected,
guiEnabled = false,
confirmData = {},
patchData = {
patch = PatchSet.newEmpty(),
timestamp = os.time(),
},
notifications = {},
toolbarIcon = Assets.Images.PluginButton,
popups = {},
@ -114,47 +117,90 @@ function App:init()
-- selene: allow(global_usage)
_G.Rojo = self.readOnlyHeadlessAPI -- Expose headless to other plugins and command bar
end
if
RunService:IsEdit()
and self.serveSession == nil
and Settings:get("syncReminder")
and self:getLastSyncTimestamp()
then
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
Connect = {
text = "Connect",
style = "Solid",
layoutOrder = 1,
onClick = function(notification)
notification:dismiss()
self:startSession()
end
},
Dismiss = {
text = "Dismiss",
style = "Bordered",
layoutOrder = 2,
onClick = function(notification)
notification:dismiss()
end,
},
})
end
end
function App:addNotification(text: string, timeout: number?)
function App:addNotification(text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?)
if not Settings:get("showNotifications") then
return
end
self.notifId += 1
local id = self.notifId
local notifications = table.clone(self.state.notifications)
table.insert(notifications, {
notifications[id] = {
text = text,
timestamp = DateTime.now().UnixTimestampMillis,
timeout = timeout or 3,
})
actions = actions,
}
self:setState({
notifications = notifications,
})
return function()
self:closeNotification(id)
end
end
function App:addThirdPartyNotification(source: string, text: string, timeout: number?)
function App:addThirdPartyNotification(source: string, text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?)
if not Settings:get("showNotifications") then
return
end
self.notifId += 1
local id = self.notifId
local notifications = table.clone(self.state.notifications)
table.insert(notifications, {
notifications[id] = {
text = text,
timestamp = DateTime.now().UnixTimestampMillis,
timeout = timeout or 3,
actions = actions,
thirdParty = true,
source = source,
})
}
self:setState({
notifications = notifications,
})
return function()
self:closeNotification(id)
end
end
function App:closeNotification(index: number)
function App:closeNotification(id: number)
local notifications = table.clone(self.state.notifications)
table.remove(notifications, index)
notifications[id] = nil
self:setState({
notifications = notifications,
@ -165,12 +211,28 @@ function App:getPriorEndpoint()
local priorEndpoints = Settings:get("priorEndpoints")
if not priorEndpoints then return end
local place = priorEndpoints[tostring(game.PlaceId)]
local id = tostring(game.PlaceId)
if ignorePlaceIds[id] then return end
local place = priorEndpoints[id]
if not place then return end
return place.host, place.port
end
function App:getLastSyncTimestamp()
local priorEndpoints = Settings:get("priorEndpoints")
if not priorEndpoints then return end
local id = tostring(game.PlaceId)
if ignorePlaceIds[id] then return end
local place = priorEndpoints[id]
if not place then return end
return place.timestamp
end
function App:setPriorEndpoint(host: string, port: string)
local priorEndpoints = Settings:get("priorEndpoints")
if not priorEndpoints then
@ -185,17 +247,16 @@ function App:setPriorEndpoint(host: string, port: string)
end
end
if host == Config.defaultHost and port == Config.defaultPort then
-- Don't save default
priorEndpoints[tostring(game.PlaceId)] = nil
else
priorEndpoints[tostring(game.PlaceId)] = {
host = host ~= Config.defaultHost and host or nil,
port = port ~= Config.defaultPort and port or nil,
timestamp = os.time(),
}
Log.trace("Saved last used endpoint for {}", game.PlaceId)
end
local id = tostring(game.PlaceId)
if ignorePlaceIds[id] then return end
priorEndpoints[id] = {
host = if host ~= Config.defaultHost then host else nil,
port = if port ~= Config.defaultPort then port else nil,
timestamp = os.time(),
}
Log.trace("Saved last used endpoint for {}", game.PlaceId)
Settings:set("priorEndpoints", priorEndpoints)
end
@ -330,21 +391,25 @@ function App:startSession(host: string?, port: string?)
local now = os.time()
local old = self.patchInfo:getValue()
local old = self.state.patchData
if now - old.timestamp < 2 then
-- Patches that apply in the same second are
-- considered to be part of the same change for human clarity
local merged = PatchSet.newEmpty()
PatchSet.assign(merged, old.patch, patch)
self.setPatchInfo({
patch = merged,
timestamp = now,
self:setState({
patchData = {
patch = merged,
timestamp = now,
},
})
else
self.setPatchInfo({
patch = patch,
timestamp = now,
self:setState({
patchData = {
patch = patch,
timestamp = now,
},
})
end
end)
@ -404,9 +469,28 @@ function App:startSession(host: string?, port: string?)
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
if PatchSet.isEmpty(patch) then
Log.trace("Accepting patch without confirmation because it is empty")
return "Accept"
end
-- The datamodel name gets overwritten by Studio, making confirmation of it intrusive
-- and unnecessary. This special case allows it to be accepted without confirmation.
if
PatchSet.hasAdditions(patch) == false
and PatchSet.hasRemoves(patch) == false
and PatchSet.containsOnlyInstance(patch, instanceMap, game)
then
local datamodelUpdates = PatchSet.getUpdateForInstance(patch, instanceMap, game)
if
datamodelUpdates ~= nil
and next(datamodelUpdates.changedProperties) == nil
and datamodelUpdates.changedClassName == nil
then
Log.trace("Accepting patch without confirmation because it only contains a datamodel name change")
return "Accept"
end
end
self:setState({
appStatus = AppStatus.Confirming,
confirmData = {
@ -431,16 +515,6 @@ function App:startSession(host: string?, port: string?)
serveSession:start()
self.serveSession = serveSession
task.defer(function()
while self.serveSession == serveSession do
-- Trigger rerender to update timestamp text
local patchInfo = table.clone(self.patchInfo:getValue())
self.setPatchInfo(patchInfo)
local elapsed = os.time() - patchInfo.timestamp
task.wait(elapsed < 60 and 1 or elapsed / 5)
end
end)
end
function App:endSession()
@ -574,7 +648,7 @@ function App:render()
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,
address = self.state.address,
patchInfo = self.patchInfo,
patchData = self.state.patchData,
serveSession = self.serveSession,
onDisconnect = function()
@ -631,7 +705,11 @@ function App:render()
}),
}),
RojoNotifications = e("ScreenGui", {}, {
RojoNotifications = e("ScreenGui", {
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
ResetOnSpawn = false,
DisplayOrder = 100,
}, {
layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
@ -647,8 +725,8 @@ function App:render()
notifs = e(Notifications, {
soundPlayer = self.props.soundPlayer,
notifications = self.state.notifications,
onClose = function(index)
self:closeNotification(index)
onClose = function(id)
self:closeNotification(id)
end,
}),
}),

View file

@ -91,6 +91,113 @@ function PatchSet.hasUpdates(patchSet)
return next(patchSet.updated) ~= nil
end
--[[
Tells whether the given PatchSet contains changes to the given instance id
]]
function PatchSet.containsId(patchSet, instanceMap, id)
if patchSet.added[id] ~= nil then
return true
end
for _, idOrInstance in patchSet.removed do
local removedId = if Types.RbxId(idOrInstance) then idOrInstance else instanceMap.fromInstances[idOrInstance]
if removedId == id then
return true
end
end
for _, update in patchSet.updated do
if update.id == id then
return true
end
end
return false
end
--[[
Tells whether the given PatchSet contains changes to the given instance.
If the given InstanceMap does not contain the instance, this function always returns false.
]]
function PatchSet.containsInstance(patchSet, instanceMap, instance)
local id = instanceMap.fromInstances[instance]
if id == nil then
return false
end
return PatchSet.containsId(patchSet, instanceMap, id)
end
--[[
Tells whether the given PatchSet contains changes to nothing but the given instance id
]]
function PatchSet.containsOnlyId(patchSet, instanceMap, id)
if not PatchSet.containsId(patchSet, instanceMap, id) then
-- Patch doesn't contain the id at all
return false
end
for addedId in patchSet.added do
if addedId ~= id then
return false
end
end
for _, idOrInstance in patchSet.removed do
local removedId = if Types.RbxId(idOrInstance) then idOrInstance else instanceMap.fromInstances[idOrInstance]
if removedId ~= id then
return false
end
end
for _, update in patchSet.updated do
if update.id ~= id then
return false
end
end
return true
end
--[[
Tells whether the given PatchSet contains changes to nothing but the given instance.
If the given InstanceMap does not contain the instance, this function always returns false.
]]
function PatchSet.containsOnlyInstance(patchSet, instanceMap, instance)
local id = instanceMap.fromInstances[instance]
if id == nil then
return false
end
return PatchSet.containsOnlyId(patchSet, instanceMap, id)
end
--[[
Returns the update to the given instance id, or nil if there aren't any
]]
function PatchSet.getUpdateForId(patchSet, id)
for _, update in patchSet.updated do
if update.id == id then
return update
end
end
return nil
end
--[[
Returns the update to the given instance, or nil if there aren't any.
If the given InstanceMap does not contain the instance, this function always returns nil.
]]
function PatchSet.getUpdateForInstance(patchSet, instanceMap, instance)
local id = instanceMap.fromInstances[instance]
if id == nil then
return nil
end
return PatchSet.getUpdateForId(patchSet, id)
end
--[[
Tells whether the given PatchSets are equal.
]]
@ -150,7 +257,7 @@ function PatchSet.humanSummary(instanceMap, patchSet)
for _, idOrInstance in ipairs(patchSet.removed) do
local instance, id
if type(idOrInstance) == "string" then
if Types.RbxId(idOrInstance) then
id = idOrInstance
instance = instanceMap.fromIds[id]
else

View file

@ -232,6 +232,22 @@ function ServeSession:__initialSync(serverInfo)
Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch)
end
for _, update in catchUpPatch.updated do
if
update.id == self.__instanceMap.fromInstances[game]
and update.changedClassName ~= nil
then
-- Non-place projects will try to update the classname of game from DataModel to
-- something like Folder, ModuleScript, etc. This would fail, so we exit with a clear
-- message instead of crashing.
return Promise.reject(
"Cannot sync a model as a place."
.. "\nEnsure Rojo is serving a project file that has a DataModel at the root of its tree and try again."
.. "\nSee project file docs: https://rojo.space/docs/v7/project-format/"
)
end
end
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch))
local userDecision = "Accept"
@ -285,6 +301,8 @@ end
function ServeSession:__mainSyncLoop()
return self.__apiContext:retrieveMessages()
:andThen(function(messages)
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
for _, message in ipairs(messages) do
local unappliedPatch = self.__reconciler:applyPatch(message)

View file

@ -13,6 +13,7 @@ local defaultSettings = {
openScriptsExternally = false,
twoWaySync = false,
showNotifications = true,
syncReminder = true,
playSounds = true,
typecheckingEnabled = false,
logLevel = "Info",

View file

@ -0,0 +1,27 @@
--[[
These are place ids that will not have metadata saved for them,
such as last sync address or time. This is because they are not unique
so storing metadata for them does not make sense as these ids are reused.
--]]
return {
["0"] = true, -- Local file
["95206881"] = true, -- Baseplate
["6560363541"] = true, -- Classic Baseplate
["95206192"] = true, -- Flat Terrain
["13165709401"] = true, -- Modern City
["520390648"] = true, -- Village
["203810088"] = true, -- Castle
["366130569"] = true, -- Suburban
["215383192"] = true, -- Racing
["264719325"] = true, -- Pirate Island
["203812057"] = true, -- Obby
["379736082"] = true, -- Starting Place
["301530843"] = true, -- Line Runner
["92721754"] = true, -- Capture The Flag
["301529772"] = true, -- Team/FFA Arena
["203885589"] = true, -- Combat
["10275826693"] = true, -- Concert
["5353920686"] = true, -- Move It Simulator
["6936227200"] = true, -- Mansion Of Wonder
}