Stylua formatting (#785)

Uses Stylua to format all existing Lua files, and adds a CI check in
`lint` to pin this improvement. Excludes formatting dependencies, of
course.
This commit is contained in:
boatbomber
2023-09-18 18:39:46 -04:00
committed by GitHub
parent 840e9bedb2
commit 0f8e1625d5
52 changed files with 595 additions and 549 deletions

View File

@@ -42,7 +42,7 @@ jobs:
run: cargo test --locked --verbose run: cargo test --locked --verbose
lint: lint:
name: Rustfmt and Clippy name: Rustfmt, Clippy, & Stylua
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -62,6 +62,9 @@ jobs:
with: with:
version: 'v0.2.7' version: 'v0.2.7'
- name: Stylua
run: stylua --check plugin/src
- name: Rustfmt - name: Rustfmt
run: cargo fmt -- --check run: cargo fmt -- --check

View File

@@ -1,4 +1,5 @@
[tools] [tools]
rojo = "rojo-rbx/rojo@7.3.0" rojo = "rojo-rbx/rojo@7.3.0"
selene = "Kampfkarren/selene@0.20.0" selene = "Kampfkarren/selene@0.25.0"
stylua = "JohnnyMorganz/stylua@0.18.2"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"

View File

@@ -24,15 +24,17 @@ end
local function rejectWrongProtocolVersion(infoResponseBody) local function rejectWrongProtocolVersion(infoResponseBody)
if infoResponseBody.protocolVersion ~= Config.protocolVersion then if infoResponseBody.protocolVersion ~= Config.protocolVersion then
local message = ( local message = (
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." .. "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!" .. .. "\nMake sure you have matching versions of both the Rojo plugin and server!"
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." .. .. "\n\nYour client is version %s, with protocol version %s. It expects server version %s."
"\nYour server is version %s, with protocol version %s." .. .. "\nYour server is version %s, with protocol version %s."
"\n\nGo to https://github.com/rojo-rbx/rojo for more details." .. "\n\nGo to https://github.com/rojo-rbx/rojo for more details."
):format( ):format(
Version.display(Config.version), Config.protocolVersion, Version.display(Config.version),
Config.protocolVersion,
Config.expectedServerVersionString, Config.expectedServerVersionString,
infoResponseBody.serverVersion, infoResponseBody.protocolVersion infoResponseBody.serverVersion,
infoResponseBody.protocolVersion
) )
return Promise.reject(message) return Promise.reject(message)
@@ -59,14 +61,11 @@ local function rejectWrongPlaceId(infoResponseBody)
end end
local message = ( local message = (
"Found a Rojo server, but its project is set to only be used with a specific list of places." .. "Found a Rojo server, but its project is set to only be used with a specific list of places."
"\nYour place ID is %s, but needs to be one of these:" .. .. "\nYour place ID is %s, but needs to be one of these:"
"\n%s" .. .. "\n%s"
"\n\nTo change this list, edit 'servePlaceIds' in your .project.json file." .. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
):format( ):format(tostring(game.PlaceId), table.concat(idList, "\n"))
tostring(game.PlaceId),
table.concat(idList, "\n")
)
return Promise.reject(message) return Promise.reject(message)
end end
@@ -141,18 +140,15 @@ end
function ApiContext:read(ids) function ApiContext:read(ids)
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ",")) local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
return Http.get(url) return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
:andThen(rejectFailedRequests) if body.sessionId ~= self.__sessionId then
:andThen(Http.Response.json) return Promise.reject("Server changed ID")
:andThen(function(body) end
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
assert(validateApiRead(body)) assert(validateApiRead(body))
return body return body
end) end)
end end
function ApiContext:write(patch) function ApiContext:write(patch)
@@ -189,28 +185,24 @@ function ApiContext:write(patch)
body = Http.jsonEncode(body) body = Http.jsonEncode(body)
return Http.post(url, body) return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
:andThen(rejectFailedRequests) Log.info("Write response: {:?}", body)
:andThen(Http.Response.json)
:andThen(function(body)
Log.info("Write response: {:?}", body)
return body return body
end) end)
end end
function ApiContext:retrieveMessages() function ApiContext:retrieveMessages()
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor) local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
local function sendRequest() local function sendRequest()
local request = Http.get(url) local request = Http.get(url):catch(function(err)
:catch(function(err) if err.type == Http.Error.Kind.Timeout and self.__connected then
if err.type == Http.Error.Kind.Timeout and self.__connected then return sendRequest()
return sendRequest() end
end
return Promise.reject(err) return Promise.reject(err)
end) end)
Log.trace("Tracking request {}", request) Log.trace("Tracking request {}", request)
self.__activeRequests[request] = true self.__activeRequests[request] = true
@@ -222,35 +214,29 @@ function ApiContext:retrieveMessages()
end) end)
end end
return sendRequest() return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
:andThen(rejectFailedRequests) if body.sessionId ~= self.__sessionId then
:andThen(Http.Response.json) return Promise.reject("Server changed ID")
:andThen(function(body) end
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
assert(validateApiSubscribe(body)) assert(validateApiSubscribe(body))
self:setMessageCursor(body.messageCursor) self:setMessageCursor(body.messageCursor)
return body.messages return body.messages
end) end)
end end
function ApiContext:open(id) function ApiContext:open(id)
local url = ("%s/api/open/%s"):format(self.__baseUrl, id) local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
return Http.post(url, "") return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
:andThen(rejectFailedRequests) if body.sessionId ~= self.__sessionId then
:andThen(Http.Response.json) return Promise.reject("Server changed ID")
:andThen(function(body) end
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
return nil return nil
end) end)
end end
return ApiContext return ApiContext

View File

@@ -23,12 +23,10 @@ end
function Checkbox:didUpdate(lastProps) function Checkbox:didUpdate(lastProps)
if lastProps.active ~= self.props.active then if lastProps.active ~= self.props.active then
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, {
Flipper.Spring.new(self.props.active and 1 or 0, { frequency = 6,
frequency = 6, dampingRatio = 1.1,
dampingRatio = 1.1, }))
})
)
end end
end end
@@ -52,13 +50,14 @@ function Checkbox:render()
BackgroundTransparency = 1, BackgroundTransparency = 1,
[Roact.Event.Activated] = function() [Roact.Event.Activated] = function()
if self.props.locked then return end if self.props.locked then
return
end
self.props.onClick() self.props.onClick()
end, end,
}, { }, {
StateTip = e(Tooltip.Trigger, { StateTip = e(Tooltip.Trigger, {
text = text = (if self.props.locked then "[LOCKED] " else "")
(if self.props.locked then "[LOCKED] " else "")
.. (if self.props.active then "Enabled" else "Disabled"), .. (if self.props.active then "Enabled" else "Disabled"),
}), }),
@@ -89,7 +88,9 @@ function Checkbox:render()
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Inactive, Image = if self.props.locked
then Assets.Images.Checkbox.Locked
else Assets.Images.Checkbox.Inactive,
ImageColor3 = theme.Inactive.IconColor, ImageColor3 = theme.Inactive.IconColor,
ImageTransparency = self.props.transparency, ImageTransparency = self.props.transparency,

View File

@@ -22,7 +22,7 @@ function CodeLabel:didMount()
end end
function CodeLabel:didUpdate() function CodeLabel:didUpdate()
self:updateHighlights() self:updateHighlights()
end end
function CodeLabel:updateHighlights() function CodeLabel:updateHighlights()

View File

@@ -36,12 +36,10 @@ function Dropdown:didUpdate(prevProps)
}) })
end end
self.openMotor:setGoal( self.openMotor:setGoal(Flipper.Spring.new(self.state.open and 1 or 0, {
Flipper.Spring.new(self.state.open and 1 or 0, { frequency = 6,
frequency = 6, dampingRatio = 1.1,
dampingRatio = 1.1, }))
})
)
end end
function Dropdown:render() function Dropdown:render()
@@ -52,10 +50,7 @@ function Dropdown:render()
local width = -1 local width = -1
for i, option in self.props.options do for i, option in self.props.options do
local text = tostring(option or "") local text = tostring(option or "")
local textSize = TextService:GetTextSize( local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
text, 15, Enum.Font.GothamMedium,
Vector2.new(math.huge, 20)
)
if textSize.X > width then if textSize.X > width then
width = textSize.X width = textSize.X
end end
@@ -74,7 +69,9 @@ function Dropdown:render()
Font = Enum.Font.GothamMedium, Font = Enum.Font.GothamMedium,
[Roact.Event.Activated] = function() [Roact.Event.Activated] = function()
if self.props.locked then return end if self.props.locked then
return
end
self:setState({ self:setState({
open = false, open = false,
}) })
@@ -88,7 +85,7 @@ function Dropdown:render()
end end
return e("ImageButton", { return e("ImageButton", {
Size = UDim2.new(0, width+50, 0, 28), Size = UDim2.new(0, width + 50, 0, 28),
Position = self.props.position, Position = self.props.position,
AnchorPoint = self.props.anchorPoint, AnchorPoint = self.props.anchorPoint,
LayoutOrder = self.props.layoutOrder, LayoutOrder = self.props.layoutOrder,
@@ -96,7 +93,9 @@ function Dropdown:render()
BackgroundTransparency = 1, BackgroundTransparency = 1,
[Roact.Event.Activated] = function() [Roact.Event.Activated] = function()
if self.props.locked then return end if self.props.locked then
return
end
self:setState({ self:setState({
open = not self.state.open, open = not self.state.open,
}) })
@@ -136,40 +135,42 @@ function Dropdown:render()
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
}), }),
}), }),
Options = if self.state.open then e(SlicedImage, { Options = if self.state.open
slice = Assets.Slices.RoundedBackground, then e(SlicedImage, {
color = theme.BackgroundColor, slice = Assets.Slices.RoundedBackground,
position = UDim2.new(1, 0, 1, 3), color = theme.BackgroundColor,
size = self.openBinding:map(function(a) position = UDim2.new(1, 0, 1, 3),
return UDim2.new(1, 0, a*math.min(3, #self.props.options), 0) size = self.openBinding:map(function(a)
end), return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
anchorPoint = Vector2.new(1, 0), end),
}, { anchorPoint = Vector2.new(1, 0),
Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = theme.BorderColor,
transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0),
}),
ScrollingFrame = e(ScrollingFrame, {
size = UDim2.new(1, -4, 1, -4),
position = UDim2.new(0, 2, 0, 2),
transparency = self.props.transparency,
contentSize = self.contentSize,
}, { }, {
Layout = e("UIListLayout", { Border = e(SlicedImage, {
VerticalAlignment = Enum.VerticalAlignment.Top, slice = Assets.Slices.RoundedBorder,
FillDirection = Enum.FillDirection.Vertical, color = theme.BorderColor,
SortOrder = Enum.SortOrder.LayoutOrder, transparency = self.props.transparency,
Padding = UDim.new(0, 0), size = UDim2.new(1, 0, 1, 0),
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}), }),
Roact.createFragment(optionButtons), ScrollingFrame = e(ScrollingFrame, {
}), size = UDim2.new(1, -4, 1, -4),
}) else nil, position = UDim2.new(0, 2, 0, 2),
transparency = self.props.transparency,
contentSize = self.contentSize,
}, {
Layout = e("UIListLayout", {
VerticalAlignment = Enum.VerticalAlignment.Top,
FillDirection = Enum.FillDirection.Vertical,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 0),
[Roact.Change.AbsoluteContentSize] = function(object)
self.setContentSize(object.AbsoluteContentSize)
end,
}),
Roact.createFragment(optionButtons),
}),
})
else nil,
}) })
end) end)
end end

View File

@@ -38,15 +38,11 @@ function IconButton:render()
[Roact.Event.Activated] = self.props.onClick, [Roact.Event.Activated] = self.props.onClick,
[Roact.Event.MouseEnter] = function() [Roact.Event.MouseEnter] = function()
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(1, HOVER_SPRING_PROPS))
Flipper.Spring.new(1, HOVER_SPRING_PROPS)
)
end, end,
[Roact.Event.MouseLeave] = function() [Roact.Event.MouseLeave] = function()
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(0, HOVER_SPRING_PROPS))
Flipper.Spring.new(0, HOVER_SPRING_PROPS)
)
end, end,
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {

View File

@@ -42,7 +42,6 @@ local function DisplayValue(props)
Position = UDim2.new(0, 25, 0, 0), Position = UDim2.new(0, 25, 0, 0),
}), }),
}) })
elseif t == "table" then elseif t == "table" then
-- Showing a memory address for tables is useless, so we want to show the best we can -- Showing a memory address for tables is useless, so we want to show the best we can
local textRepresentation = nil local textRepresentation = nil
@@ -62,10 +61,10 @@ local function DisplayValue(props)
-- Wrap strings in quotes -- Wrap strings in quotes
if type(k) == "string" then if type(k) == "string" then
k = "\"" .. k .. "\"" k = '"' .. k .. '"'
end end
if type(v) == "string" then if type(v) == "string" then
v = "\"" .. v .. "\"" v = '"' .. v .. '"'
end end
out[i] = string.format("[%s] = %s", tostring(k), tostring(v)) out[i] = string.format("[%s] = %s", tostring(k), tostring(v))

View File

@@ -43,9 +43,14 @@ end
function PatchVisualizer:render() function PatchVisualizer:render()
local patchTree = self.props.patchTree local patchTree = self.props.patchTree
if patchTree == nil and self.props.patch ~= nil then if patchTree == nil and self.props.patch ~= nil then
patchTree = PatchTree.build(self.props.patch, self.props.instanceMap, self.props.changeListHeaders or { "Property", "Current", "Incoming" }) patchTree = PatchTree.build(
self.props.patch,
self.props.instanceMap,
self.props.changeListHeaders or { "Property", "Current", "Incoming" }
)
if self.props.unappliedPatch then if self.props.unappliedPatch then
patchTree = PatchTree.updateMetadata(patchTree, self.props.patch, self.props.instanceMap, self.props.unappliedPatch) patchTree =
PatchTree.updateMetadata(patchTree, self.props.patch, self.props.instanceMap, self.props.unappliedPatch)
end end
end end

View File

@@ -10,11 +10,15 @@ local StudioPluginContext = require(script.Parent.StudioPluginContext)
local e = Roact.createElement local e = Roact.createElement
local StudioPluginAction = Roact.Component:extend("StudioPluginAction") local StudioPluginAction = Roact.Component:extend("StudioPluginAction")
function StudioPluginAction:init() function StudioPluginAction:init()
self.pluginAction = self.props.plugin:CreatePluginAction( self.pluginAction = self.props.plugin:CreatePluginAction(
self.props.name, self.props.title, self.props.description, self.props.icon, self.props.bindable self.props.name,
self.props.title,
self.props.description,
self.props.icon,
self.props.bindable
) )
self.pluginAction.Triggered:Connect(self.props.onTriggered) self.pluginAction.Triggered:Connect(self.props.onTriggered)
@@ -31,9 +35,12 @@ end
local function StudioPluginActionWrapper(props) local function StudioPluginActionWrapper(props)
return e(StudioPluginContext.Consumer, { return e(StudioPluginContext.Consumer, {
render = function(plugin) render = function(plugin)
return e(StudioPluginAction, Dictionary.merge(props, { return e(
plugin = plugin, StudioPluginAction,
})) Dictionary.merge(props, {
plugin = plugin,
})
)
end, end,
}) })
end end

View File

@@ -38,7 +38,10 @@ function StudioPluginGui:init()
minimumSize.Y minimumSize.Y
) )
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(if self.props.isEphemeral then HttpService:GenerateGUID(false) else self.props.id, dockWidgetPluginGuiInfo) local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(
if self.props.isEphemeral then HttpService:GenerateGUID(false) else self.props.id,
dockWidgetPluginGuiInfo
)
pluginGui.Name = self.props.id pluginGui.Name = self.props.id
pluginGui.Title = self.props.title pluginGui.Title = self.props.title

View File

@@ -18,12 +18,8 @@ StudioToggleButton.defaultProps = {
} }
function StudioToggleButton:init() function StudioToggleButton:init()
local button = self.props.toolbar:CreateButton( local button =
self.props.name, self.props.toolbar:CreateButton(self.props.name, self.props.tooltip, self.props.icon, self.props.text)
self.props.tooltip,
self.props.icon,
self.props.text
)
button.Click:Connect(function() button.Click:Connect(function()
if self.props.onClick then if self.props.onClick then
@@ -61,9 +57,12 @@ end
local function StudioToggleButtonWrapper(props) local function StudioToggleButtonWrapper(props)
return e(StudioToolbarContext.Consumer, { return e(StudioToolbarContext.Consumer, {
render = function(toolbar) render = function(toolbar)
return e(StudioToggleButton, Dictionary.merge(props, { return e(
toolbar = toolbar, StudioToggleButton,
})) Dictionary.merge(props, {
toolbar = toolbar,
})
)
end, end,
}) })
end end

View File

@@ -36,9 +36,12 @@ end
local function StudioToolbarWrapper(props) local function StudioToolbarWrapper(props)
return e(StudioPluginContext.Consumer, { return e(StudioPluginContext.Consumer, {
render = function(plugin) render = function(plugin)
return e(StudioToolbar, Dictionary.merge(props, { return e(
plugin = plugin, StudioToolbar,
})) Dictionary.merge(props, {
plugin = plugin,
})
)
end, end,
}) })
end end

View File

@@ -41,10 +41,8 @@ end
function TextButton:render() function TextButton:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local textSize = TextService:GetTextSize( local textSize =
self.props.text, 18, Enum.Font.GothamSemibold, TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamSemibold, Vector2.new(math.huge, math.huge))
Vector2.new(math.huge, math.huge)
)
local style = self.props.style local style = self.props.style
@@ -124,7 +122,11 @@ function TextButton:render()
Background = style == "Solid" and e(SlicedImage, { Background = style == "Solid" and e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BackgroundColor, theme.Disabled.BackgroundColor), color = bindingUtil.mapLerp(
bindingEnabled,
theme.Enabled.BackgroundColor,
theme.Disabled.BackgroundColor
),
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),

View File

@@ -38,22 +38,22 @@ end
function TextInput:render() function TextInput:render()
return Theme.with(function(theme) return Theme.with(function(theme)
theme = theme.TextInput theme = theme.TextInput
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover") local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled") local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
return e(SlicedImage, { return e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor), color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
transparency = self.props.transparency, transparency = self.props.transparency,
size = self.props.size or UDim2.new(1, 0, 1, 0), size = self.props.size or UDim2.new(1, 0, 1, 0),
position = self.props.position, position = self.props.position,
layoutOrder = self.props.layoutOrder, layoutOrder = self.props.layoutOrder,
anchorPoint = self.props.anchorPoint, anchorPoint = self.props.anchorPoint,
}, { }, {
HoverOverlay = e(SlicedImage, { HoverOverlay = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = theme.ActionFillColor, color = theme.ActionFillColor,
transparency = Roact.joinBindings({ transparency = Roact.joinBindings({
@@ -67,36 +67,40 @@ function TextInput:render()
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
zIndex = -1, zIndex = -1,
}), }),
Input = e("TextBox", { Input = e("TextBox", {
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1), Size = UDim2.fromScale(1, 1),
Text = self.props.text, Text = self.props.text,
PlaceholderText = self.props.placeholder, PlaceholderText = self.props.placeholder,
Font = Enum.Font.GothamMedium, Font = Enum.Font.GothamMedium,
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor), TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
PlaceholderColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.PlaceholderColor, theme.Enabled.PlaceholderColor), PlaceholderColor3 = bindingUtil.mapLerp(
TextSize = 18, bindingEnabled,
TextEditable = self.props.enabled, theme.Disabled.PlaceholderColor,
ClearTextOnFocus = self.props.clearTextOnFocus, theme.Enabled.PlaceholderColor
),
TextSize = 18,
TextEditable = self.props.enabled,
ClearTextOnFocus = self.props.clearTextOnFocus,
[Roact.Event.MouseEnter] = function() [Roact.Event.MouseEnter] = function()
self.motor:setGoal({ self.motor:setGoal({
hover = Flipper.Spring.new(1, SPRING_PROPS), hover = Flipper.Spring.new(1, SPRING_PROPS),
}) })
end, end,
[Roact.Event.MouseLeave] = function() [Roact.Event.MouseLeave] = function()
self.motor:setGoal({ self.motor:setGoal({
hover = Flipper.Spring.new(0, SPRING_PROPS), hover = Flipper.Spring.new(0, SPRING_PROPS),
}) })
end, end,
[Roact.Event.FocusLost] = function(rbx) [Roact.Event.FocusLost] = function(rbx)
self.props.onEntered(rbx.Text) self.props.onEntered(rbx.Text)
end, end,
}), }),
Children = Roact.createFragment(self.props[Roact.Children]), Children = Roact.createFragment(self.props[Roact.Children]),
}) })
end) end)
end end

View File

@@ -22,12 +22,16 @@ local TooltipContext = Roact.createContext({})
local function Popup(props) local function Popup(props)
local textSize = TextService:GetTextSize( local textSize = TextService:GetTextSize(
props.Text, 16, Enum.Font.GothamMedium, Vector2.new(math.min(props.parentSize.X, 160), math.huge) props.Text,
16,
Enum.Font.GothamMedium,
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
) + TEXT_PADDING + (Vector2.one * 2) ) + TEXT_PADDING + (Vector2.one * 2)
local trigger = props.Trigger:getValue() local trigger = props.Trigger:getValue()
local spaceBelow = props.parentSize.Y - (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE) local spaceBelow = props.parentSize.Y
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger -- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
@@ -39,7 +43,10 @@ local function Popup(props)
if displayAbove then if displayAbove then
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0) Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
else else
Y = math.min(trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP, props.parentSize.Y - textSize.Y) Y = math.min(
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
props.parentSize.Y - textSize.Y
)
end end
return Theme.with(function(theme) return Theme.with(function(theme)
@@ -64,17 +71,9 @@ local function Popup(props)
Tail = e("ImageLabel", { Tail = e("ImageLabel", {
ZIndex = 100, ZIndex = 100,
Position = Position = if displayAbove
if displayAbove then then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -1)
UDim2.new( else UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 0, -TAIL_SIZE + 1),
0, math.clamp(props.Position.X - X, 6, textSize.X-6),
1, -1
)
else
UDim2.new(
0, math.clamp(props.Position.X - X, 6, textSize.X-6),
0, -TAIL_SIZE+1
),
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE), Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
AnchorPoint = Vector2.new(0.5, 0), AnchorPoint = Vector2.new(0.5, 0),
Rotation = if displayAbove then 180 else 0, Rotation = if displayAbove then 180 else 0,
@@ -90,7 +89,7 @@ local function Popup(props)
ImageColor3 = theme.BorderedContainer.BorderColor, ImageColor3 = theme.BorderedContainer.BorderColor,
ImageTransparency = props.transparency, ImageTransparency = props.transparency,
}), }),
}) }),
}) })
end) end)
end end
@@ -200,9 +199,10 @@ function Trigger:isHovering()
local size = rbx.AbsoluteSize local size = rbx.AbsoluteSize
local mousePos = self.mousePos local mousePos = self.mousePos
return return mousePos.X >= pos.X
mousePos.X >= pos.X and mousePos.X <= pos.X + size.X and mousePos.X <= pos.X + size.X
and mousePos.Y >= pos.Y and mousePos.Y <= pos.Y + size.Y and mousePos.Y >= pos.Y
and mousePos.Y <= pos.Y + size.Y
end end
return false return false
end end
@@ -236,7 +236,9 @@ end
function Trigger:render() function Trigger:render()
local function recalculate(rbx) local function recalculate(rbx)
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui") local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
if not widget then return end if not widget then
return
end
self.mousePos = widget:GetRelativeMousePosition() self.mousePos = widget:GetRelativeMousePosition()
self:managePopup() self:managePopup()

View File

@@ -24,9 +24,7 @@ function TouchRipple:init()
}) })
self.binding = bindingUtil.fromMotor(self.motor) self.binding = bindingUtil.fromMotor(self.motor)
self.position, self.setPosition = Roact.createBinding( self.position, self.setPosition = Roact.createBinding(Vector2.new(0, 0))
Vector2.new(0, 0)
)
end end
function TouchRipple:reset() function TouchRipple:reset()
@@ -43,10 +41,7 @@ function TouchRipple:calculateRadius(position)
local container = self.ref.current local container = self.ref.current
if container then if container then
local corner = Vector2.new( local corner = Vector2.new(math.floor((1 - position.X) + 0.5), math.floor((1 - position.Y) + 0.5))
math.floor((1 - position.X) + 0.5),
math.floor((1 - position.Y) + 0.5)
)
local size = container.AbsoluteSize local size = container.AbsoluteSize
local ratio = size / math.min(size.X, size.Y) local ratio = size / math.min(size.X, size.Y)
@@ -93,10 +88,7 @@ function TouchRipple:render()
input:GetPropertyChangedSignal("UserInputState"):Connect(function() input:GetPropertyChangedSignal("UserInputState"):Connect(function()
local userInputState = input.UserInputState local userInputState = input.UserInputState
if if userInputState == Enum.UserInputState.Cancel or userInputState == Enum.UserInputState.End then
userInputState == Enum.UserInputState.Cancel
or userInputState == Enum.UserInputState.End
then
self.motor:setGoal({ self.motor:setGoal({
opacity = Flipper.Spring.new(0, { opacity = Flipper.Spring.new(0, {
frequency = 5, frequency = 5,
@@ -127,8 +119,10 @@ function TouchRipple:render()
local containerAspect = containerSize.X / containerSize.Y local containerAspect = containerSize.X / containerSize.Y
return UDim2.new( return UDim2.new(
currentSize / math.max(containerAspect, 1), 0, currentSize / math.max(containerAspect, 1),
currentSize * math.min(containerAspect, 1), 0 0,
currentSize * math.min(containerAspect, 1),
0
) )
end end
end), end),

View File

@@ -37,28 +37,24 @@ function Notification:init()
end end
function Notification:dismiss() function Notification:dismiss()
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(0, {
Flipper.Spring.new(0, { frequency = 5,
frequency = 5, dampingRatio = 1,
dampingRatio = 1, }))
})
)
end end
function Notification:didMount() function Notification:didMount()
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(1, {
Flipper.Spring.new(1, { frequency = 3,
frequency = 3, dampingRatio = 1,
dampingRatio = 1, }))
})
)
self.props.soundPlayer:play(Assets.Sounds.Notification) self.props.soundPlayer:play(Assets.Sounds.Notification)
self.timeout = task.spawn(function() self.timeout = task.spawn(function()
local clock = os.clock() local clock = os.clock()
local seen = false local seen = false
while task.wait(1/10) do while task.wait(1 / 10) do
local now = os.clock() local now = os.clock()
local dt = now - clock local dt = now - clock
clock = now clock = now
@@ -90,12 +86,7 @@ function Notification:render()
return 1 - value return 1 - value
end) end)
local textBounds = TextService:GetTextSize( local textBounds = TextService:GetTextSize(self.props.text, 15, Enum.Font.GothamMedium, Vector2.new(350, 700))
self.props.text,
15,
Enum.Font.GothamMedium,
Vector2.new(350, 700)
)
local actionButtons = {} local actionButtons = {}
local buttonsX = 0 local buttonsX = 0
@@ -116,7 +107,9 @@ function Notification:render()
}) })
buttonsX += TextService:GetTextSize( buttonsX += TextService:GetTextSize(
action.text, 18, Enum.Font.GothamMedium, action.text,
18,
Enum.Font.GothamMedium,
Vector2.new(math.huge, math.huge) Vector2.new(math.huge, math.huge)
).X + 30 ).X + 30
@@ -156,7 +149,7 @@ function Notification:render()
Contents = e("Frame", { Contents = e("Frame", {
Size = UDim2.new(0, 35 + contentX, 1, -paddingY), Size = UDim2.new(0, 35 + contentX, 1, -paddingY),
Position = UDim2.new(0, 0, 0, paddingY / 2), Position = UDim2.new(0, 0, 0, paddingY / 2),
BackgroundTransparency = 1 BackgroundTransparency = 1,
}, { }, {
Logo = e("ImageLabel", { Logo = e("ImageLabel", {
ImageTransparency = transparency, ImageTransparency = transparency,
@@ -181,28 +174,30 @@ function Notification:render()
LayoutOrder = 1, LayoutOrder = 1,
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
Actions = if self.props.actions then e("Frame", { Actions = if self.props.actions
Size = UDim2.new(1, -40, 0, 35), then e("Frame", {
Position = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, -40, 0, 35),
AnchorPoint = Vector2.new(1, 1), Position = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, AnchorPoint = Vector2.new(1, 1),
}, { BackgroundTransparency = 1,
Layout = e("UIListLayout", { }, {
FillDirection = Enum.FillDirection.Horizontal, Layout = e("UIListLayout", {
HorizontalAlignment = Enum.HorizontalAlignment.Right, FillDirection = Enum.FillDirection.Horizontal,
VerticalAlignment = Enum.VerticalAlignment.Center, HorizontalAlignment = Enum.HorizontalAlignment.Right,
SortOrder = Enum.SortOrder.LayoutOrder, VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder,
}), Padding = UDim.new(0, 5),
Buttons = Roact.createFragment(actionButtons), }),
}) else nil, Buttons = Roact.createFragment(actionButtons),
})
else nil,
}), }),
Padding = e("UIPadding", { Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17), PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15), PaddingRight = UDim.new(0, 15),
}), }),
}) }),
}) })
end) end)
end end

View File

@@ -15,7 +15,7 @@ local Page = Roact.Component:extend("Page")
function Page:init() function Page:init()
self:setState({ self:setState({
rendered = self.props.active rendered = self.props.active,
}) })
self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0) self.motor = Flipper.SingleMotor.new(self.props.active and 1 or 0)
@@ -51,20 +51,21 @@ function Page:render()
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, { }, {
Component = e(self.props.component, Dictionary.merge(self.props, { Component = e(
transparency = transparency, self.props.component,
})) Dictionary.merge(self.props, {
transparency = transparency,
})
),
}) })
end end
function Page:didUpdate(lastProps) function Page:didUpdate(lastProps)
if self.props.active ~= lastProps.active then if self.props.active ~= lastProps.active then
self.motor:setGoal( self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, {
Flipper.Spring.new(self.props.active and 1 or 0, { frequency = 6,
frequency = 6, dampingRatio = 1,
dampingRatio = 1, }))
})
)
end end
end end

View File

@@ -85,7 +85,7 @@ function ConfirmingPage:render()
onClick = self.props.onAbort, onClick = self.props.onAbort,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Stop the connection process" text = "Stop the connection process",
}), }),
}), }),
@@ -98,7 +98,7 @@ function ConfirmingPage:render()
onClick = self.props.onReject, onClick = self.props.onReject,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Push Studio changes to the Rojo server" text = "Push Studio changes to the Rojo server",
}), }),
}) })
else nil, else nil,
@@ -111,7 +111,7 @@ function ConfirmingPage:render()
onClick = self.props.onAccept, onClick = self.props.onAccept,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Pull Rojo server changes to Studio" text = "Pull Rojo server changes to Studio",
}), }),
}), }),
@@ -169,7 +169,7 @@ function ConfirmingPage:render()
oldText = self.state.oldSource, oldText = self.state.oldSource,
newText = self.state.newSource, newText = self.state.newSource,
}) }),
}), }),
}), }),
}), }),

View File

@@ -21,7 +21,17 @@ local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
local e = Roact.createElement local e = Roact.createElement
local AGE_UNITS = { {31556909, "year"}, {2629743, "month"}, {604800, "week"}, {86400, "day"}, {3600, "hour"}, {60, "minute"}, } local AGE_UNITS = {
{ 31556909, "year" },
{ 2629743, "month" },
{ 604800, "week" },
{ 86400, "day" },
{ 3600, "hour" },
{
60,
"minute",
},
}
function timeSinceText(elapsed: number): string function timeSinceText(elapsed: number): string
if elapsed < 3 then if elapsed < 3 then
return "just now" return "just now"
@@ -159,16 +169,15 @@ function ConnectedPage:getChangeInfoText()
local elapsed = os.time() - patchData.timestamp local elapsed = os.time() - patchData.timestamp
local unapplied = PatchSet.countChanges(patchData.unapplied) local unapplied = PatchSet.countChanges(patchData.unapplied)
return return "<i>Synced "
"<i>Synced "
.. timeSinceText(elapsed) .. timeSinceText(elapsed)
.. (if unapplied > 0 then .. (if unapplied > 0
string.format( then string.format(
", <font color=\"#FF8E3C\">but %d change%s failed to apply</font>", ', <font color="#FF8E3C">but %d change%s failed to apply</font>',
unapplied, unapplied,
unapplied == 1 and "" or "s" unapplied == 1 and "" or "s"
) )
else "") else "")
.. "</i>" .. "</i>"
end end
@@ -297,7 +306,7 @@ function ConnectedPage:render()
onClick = self.props.onNavigateSettings, onClick = self.props.onNavigateSettings,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "View and modify plugin settings" text = "View and modify plugin settings",
}), }),
}), }),
@@ -309,7 +318,7 @@ function ConnectedPage:render()
onClick = self.props.onDisconnect, onClick = self.props.onDisconnect,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Disconnect from the Rojo sync server" text = "Disconnect from the Rojo sync server",
}), }),
}), }),
@@ -427,7 +436,7 @@ function ConnectedPage:render()
oldText = self.state.oldSource, oldText = self.state.oldSource,
newText = self.state.newSource, newText = self.state.newSource,
}) }),
}), }),
}), }),
}), }),

View File

@@ -50,7 +50,9 @@ function Error:render()
local containerSize = object.AbsoluteSize - ERROR_PADDING * 2 local containerSize = object.AbsoluteSize - ERROR_PADDING * 2
local textBounds = TextService:GetTextSize( local textBounds = TextService:GetTextSize(
self.props.errorMessage, 16, Enum.Font.Code, self.props.errorMessage,
16,
Enum.Font.Code,
Vector2.new(containerSize.X, math.huge) Vector2.new(containerSize.X, math.huge)
) )
@@ -60,12 +62,13 @@ function Error:render()
ErrorMessage = Theme.with(function(theme) ErrorMessage = Theme.with(function(theme)
return e("TextBox", { return e("TextBox", {
[Roact.Event.InputBegan] = function(rbx, input) [Roact.Event.InputBegan] = function(rbx, input)
if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
return
end
rbx.SelectionStart = 0 rbx.SelectionStart = 0
rbx.CursorPosition = #rbx.Text+1 rbx.CursorPosition = #rbx.Text + 1
end, end,
Text = self.props.errorMessage, Text = self.props.errorMessage,
TextEditable = false, TextEditable = false,
Font = Enum.Font.Code, Font = Enum.Font.Code,
@@ -126,7 +129,7 @@ function ErrorPage:render()
onClick = self.props.onClose, onClick = self.props.onClose,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Dismiss message" text = "Dismiss message",
}), }),
}), }),

View File

@@ -46,7 +46,7 @@ local function AddressEntry(props)
if props.onHostChange ~= nil then if props.onHostChange ~= nil then
props.onHostChange(object.Text) props.onHostChange(object.Text)
end end
end end,
}), }),
Port = e("TextBox", { Port = e("TextBox", {
@@ -120,7 +120,7 @@ function NotConnectedPage:render()
onClick = self.props.onNavigateSettings, onClick = self.props.onNavigateSettings,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "View and modify plugin settings" text = "View and modify plugin settings",
}), }),
}), }),
@@ -132,7 +132,7 @@ function NotConnectedPage:render()
onClick = self.props.onConnect, onClick = self.props.onConnect,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Connect to a Rojo sync server" text = "Connect to a Rojo sync server",
}), }),
}), }),

View File

@@ -81,40 +81,39 @@ function Setting:render()
end, end,
}), }),
Input = Input = if self.props.input ~= nil
if self.props.input ~= nil then then self.props.input
self.props.input elseif self.props.options ~= nil then e(Dropdown, {
elseif self.props.options ~= nil then locked = self.props.locked,
e(Dropdown, { options = self.props.options,
locked = self.props.locked, active = self.state.setting,
options = self.props.options, transparency = self.props.transparency,
active = self.state.setting, onClick = function(option)
transparency = self.props.transparency, Settings:set(self.props.id, option)
onClick = function(option) end,
Settings:set(self.props.id, option) })
end, else e(Checkbox, {
}) locked = self.props.locked,
else active = self.state.setting,
e(Checkbox, { transparency = self.props.transparency,
locked = self.props.locked, onClick = function()
active = self.state.setting, local currentValue = Settings:get(self.props.id)
transparency = self.props.transparency, Settings:set(self.props.id, not currentValue)
onClick = function() end,
local currentValue = Settings:get(self.props.id) }),
Settings:set(self.props.id, not currentValue)
end,
}),
Reset = if self.props.onReset then e(IconButton, { Reset = if self.props.onReset
icon = Assets.Images.Icons.Reset, then e(IconButton, {
iconSize = 24, icon = Assets.Images.Icons.Reset,
color = theme.BackButtonColor, iconSize = 24,
transparency = self.props.transparency, color = theme.BackButtonColor,
visible = self.props.showReset, transparency = self.props.transparency,
layoutOrder = -1, visible = self.props.showReset,
layoutOrder = -1,
onClick = self.props.onReset, onClick = self.props.onReset,
}) else nil, })
else nil,
}), }),
Text = e("Frame", { Text = e("Frame", {
@@ -122,7 +121,8 @@ function Setting:render()
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, { }, {
Name = e("TextLabel", { Name = e("TextLabel", {
Text = (if self.props.experimental then "<font color=\"#FF8E3C\">⚠ </font>" else "") .. self.props.name, Text = (if self.props.experimental then '<font color="#FF8E3C">⚠ </font>' else "")
.. self.props.name,
Font = Enum.Font.GothamBold, Font = Enum.Font.GothamBold,
TextSize = 17, TextSize = 17,
TextColor3 = theme.Setting.NameColor, TextColor3 = theme.Setting.NameColor,
@@ -137,7 +137,8 @@ function Setting:render()
}), }),
Description = e("TextLabel", { Description = e("TextLabel", {
Text = (if self.props.experimental then "<font color=\"#FF8E3C\">[Experimental] </font>" else "") .. self.props.description, Text = (if self.props.experimental then '<font color="#FF8E3C">[Experimental] </font>' else "")
.. self.props.description,
Font = Enum.Font.Gotham, Font = Enum.Font.Gotham,
LineHeight = 1.2, LineHeight = 1.2,
TextSize = 14, TextSize = 14,
@@ -151,10 +152,14 @@ function Setting:render()
containerSize = self.containerSize, containerSize = self.containerSize,
inputSize = self.inputSize, inputSize = self.inputSize,
}):map(function(values) }):map(function(values)
local desc = (if self.props.experimental then "[Experimental] " else "") .. self.props.description local desc = (if self.props.experimental then "[Experimental] " else "")
.. self.props.description
local offset = values.inputSize.X + 5 local offset = values.inputSize.X + 5
local textBounds = getTextBounds( local textBounds = getTextBounds(
desc, 14, Enum.Font.Gotham, 1.2, desc,
14,
Enum.Font.Gotham,
1.2,
Vector2.new(values.containerSize.X - offset, math.huge) Vector2.new(values.containerSize.X - offset, math.huge)
) )
return UDim2.new(1, -offset, 0, textBounds.Y) return UDim2.new(1, -offset, 0, textBounds.Y)

View File

@@ -49,7 +49,7 @@ local function Navbar(props)
onClick = props.onBack, onClick = props.onBack,
}, { }, {
Tip = e(Tooltip.Trigger, { Tip = e(Tooltip.Trigger, {
text = "Back" text = "Back",
}), }),
}), }),
@@ -63,7 +63,7 @@ local function Navbar(props)
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}) }),
}) })
end) end)
end end
@@ -138,7 +138,10 @@ function SettingsPage:render()
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999)) Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
else else
-- Force text back to last valid value -- Force text back to last valid value
Settings:set("largeChangesConfirmationThreshold", Settings:get("largeChangesConfirmationThreshold")) Settings:set(
"largeChangesConfirmationThreshold",
Settings:get("largeChangesConfirmationThreshold")
)
end end
end, end,
}), }),

View File

@@ -90,7 +90,7 @@ local lightTheme = strict("LightTheme", {
}, },
AddressEntry = { AddressEntry = {
TextColor = Color3.fromHex("000000"), TextColor = Color3.fromHex("000000"),
PlaceholderColor = Color3.fromHex("8C8C8C") PlaceholderColor = Color3.fromHex("8C8C8C"),
}, },
BorderedContainer = { BorderedContainer = {
BorderColor = Color3.fromHex("CBCBCB"), BorderColor = Color3.fromHex("CBCBCB"),
@@ -200,7 +200,7 @@ local darkTheme = strict("DarkTheme", {
}, },
AddressEntry = { AddressEntry = {
TextColor = Color3.fromHex("FFFFFF"), TextColor = Color3.fromHex("FFFFFF"),
PlaceholderColor = Color3.fromHex("8B8B8B") PlaceholderColor = Color3.fromHex("8B8B8B"),
}, },
BorderedContainer = { BorderedContainer = {
BorderColor = Color3.fromHex("535353"), BorderColor = Color3.fromHex("535353"),
@@ -235,7 +235,7 @@ local darkTheme = strict("DarkTheme", {
}, },
Header = { Header = {
LogoColor = BRAND_COLOR, LogoColor = BRAND_COLOR,
VersionColor = Color3.fromHex("D3D3D3") VersionColor = Color3.fromHex("D3D3D3"),
}, },
Notification = { Notification = {
InfoColor = Color3.fromHex("FFFFFF"), InfoColor = Color3.fromHex("FFFFFF"),

View File

@@ -469,13 +469,17 @@ function App:startSession()
if confirmationBehavior == "Initial" then if confirmationBehavior == "Initial" then
-- Only confirm if we haven't synced this project yet this session -- Only confirm if we haven't synced this project yet this session
if self.knownProjects[serverInfo.projectName] then if self.knownProjects[serverInfo.projectName] then
Log.trace("Accepting patch without confirmation because project has already been connected and behavior is set to Initial") Log.trace(
"Accepting patch without confirmation because project has already been connected and behavior is set to Initial"
)
return "Accept" return "Accept"
end end
elseif confirmationBehavior == "Large Changes" then elseif confirmationBehavior == "Large Changes" then
-- Only confirm if the patch impacts many instances -- Only confirm if the patch impacts many instances
if PatchSet.countInstances(patch) < Settings:get("largeChangesConfirmationThreshold") then if PatchSet.countInstances(patch) < Settings:get("largeChangesConfirmationThreshold") then
Log.trace("Accepting patch without confirmation because patch is small and behavior is set to Large Changes") Log.trace(
"Accepting patch without confirmation because patch is small and behavior is set to Large Changes"
)
return "Accept" return "Accept"
end end
elseif confirmationBehavior == "Unlisted PlaceId" then elseif confirmationBehavior == "Unlisted PlaceId" then
@@ -483,7 +487,9 @@ function App:startSession()
if serverInfo.expectedPlaceIds then if serverInfo.expectedPlaceIds then
local isListed = table.find(serverInfo.expectedPlaceIds, game.PlaceId) ~= nil local isListed = table.find(serverInfo.expectedPlaceIds, game.PlaceId) ~= nil
if isListed then if isListed then
Log.trace("Accepting patch without confirmation because placeId is listed and behavior is set to Unlisted PlaceId") Log.trace(
"Accepting patch without confirmation because placeId is listed and behavior is set to Unlisted PlaceId"
)
return "Accept" return "Accept"
end end
end end
@@ -657,7 +663,8 @@ function App:render()
}), }),
Settings = createPageElement(AppStatus.Settings, { Settings = createPageElement(AppStatus.Settings, {
syncActive = self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected, syncActive = self.serveSession ~= nil
and self.serveSession:getStatus() == ServeSession.Status.Connected,
onBack = function() onBack = function()
self:setState({ self:setState({

View File

@@ -54,7 +54,7 @@ local Assets = {
[32] = "rbxassetid://3088713341", [32] = "rbxassetid://3088713341",
[64] = "rbxassetid://4918677124", [64] = "rbxassetid://4918677124",
[128] = "rbxassetid://2600845734", [128] = "rbxassetid://2600845734",
[500] = "rbxassetid://2609138523" [500] = "rbxassetid://2609138523",
}, },
}, },
Sounds = { Sounds = {

View File

@@ -5,7 +5,7 @@ local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
return strict("Config", { return strict("Config", {
isDevBuild = isDevBuild, isDevBuild = isDevBuild,
codename = "Epiphany", codename = "Epiphany",
version = {7, 3, 0}, version = { 7, 3, 0 },
expectedServerVersionString = "7.2 or newer", expectedServerVersionString = "7.2 or newer",
protocolVersion = 4, protocolVersion = 4,
defaultHost = "localhost", defaultHost = "localhost",

View File

@@ -30,4 +30,4 @@ end
return { return {
None = None, None = None,
merge = merge, merge = merge,
} }

View File

@@ -66,7 +66,7 @@ function InstanceMap:__fmtDebug(output)
for id, instance in pairs(self.fromIds) do for id, instance in pairs(self.fromIds) do
local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName) local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName)
table.insert(entries, {id, label}) table.insert(entries, { id, label })
end end
table.sort(entries, function(a, b) table.sort(entries, function(a, b)

View File

@@ -26,7 +26,9 @@ local function deepEqual(a: any, b: any): boolean
end end
for key, value in b do for key, value in b do
if checkedKeys[key] then continue end if checkedKeys[key] then
continue
end
if deepEqual(value, a[key]) == false then if deepEqual(value, a[key]) == false then
return false return false
end end
@@ -65,9 +67,7 @@ end
Tells whether the given PatchSet is empty. Tells whether the given PatchSet is empty.
]] ]]
function PatchSet.isEmpty(patchSet) function PatchSet.isEmpty(patchSet)
return next(patchSet.removed) == nil and return next(patchSet.removed) == nil and next(patchSet.added) == nil and next(patchSet.updated) == nil
next(patchSet.added) == nil and
next(patchSet.updated) == nil
end end
--[[ --[[
@@ -342,9 +342,15 @@ function PatchSet.humanSummary(instanceMap, patchSet)
end end
end end
table.insert(statements, string.format( table.insert(
"- Add instance %q (ClassName %q) to %s", statements,
virtualInstance.Name, virtualInstance.ClassName, parentDisplayName)) string.format(
"- Add instance %q (ClassName %q) to %s",
virtualInstance.Name,
virtualInstance.ClassName,
parentDisplayName
)
)
end end
for _, update in ipairs(patchSet.updated) do for _, update in ipairs(patchSet.updated) do
@@ -374,9 +380,10 @@ function PatchSet.humanSummary(instanceMap, patchSet)
displayName = "[unknown instance]" displayName = "[unknown instance]"
end end
table.insert(statements, string.format( table.insert(
"- Update properties on %s: %s", statements,
displayName, table.concat(updatedProperties, ","))) string.format("- Update properties on %s: %s", displayName, table.concat(updatedProperties, ","))
)
end end
return table.concat(statements, "\n") return table.concat(statements, "\n")

View File

@@ -401,7 +401,9 @@ end
-- Creates a deep copy of a tree for immutability purposes in Roact -- Creates a deep copy of a tree for immutability purposes in Roact
function PatchTree.clone(tree) function PatchTree.clone(tree)
if not tree then return end if not tree then
return
end
local newTree = Tree.new() local newTree = Tree.new()
tree:forEach(function(node) tree:forEach(function(node)

View File

@@ -83,11 +83,11 @@ return function()
ClassName = "Model", ClassName = "Model",
Name = "Child", Name = "Child",
Parent = "ROOT", Parent = "ROOT",
Children = {"GRANDCHILD"}, Children = { "GRANDCHILD" },
Properties = {}, Properties = {},
} }
patch.added["GRANDCHILD"] = { patch.added["GRANDCHILD"] = {
Id = "GRANDCHILD", Id = "GRANDCHILD",
ClassName = "Part", ClassName = "Part",
Name = "Grandchild", Name = "Grandchild",
@@ -193,4 +193,4 @@ return function()
assert(newChild ~= nil, "expected child to be present") assert(newChild ~= nil, "expected child to be present")
assert(newChild == child, "expected child to be preserved") assert(newChild == child, "expected child to be preserved")
end) end)
end end

View File

@@ -30,10 +30,11 @@ local function decodeValue(encodedValue, instanceMap)
local ok, decodedValue = RbxDom.EncodedValue.decode(encodedValue) local ok, decodedValue = RbxDom.EncodedValue.decode(encodedValue)
if not ok then if not ok then
return false, Error.new(Error.CannotDecodeValue, { return false,
encodedValue = encodedValue, Error.new(Error.CannotDecodeValue, {
innerError = decodedValue, encodedValue = encodedValue,
}) innerError = decodedValue,
})
end end
return true, decodedValue return true, decodedValue

View File

@@ -37,7 +37,9 @@ local function trueEquals(a, b): boolean
end end
end end
for key, value in pairs(b) do for key, value in pairs(b) do
if checkedKeys[key] then continue end if checkedKeys[key] then
continue
end
if not trueEquals(value, a[key]) then if not trueEquals(value, a[key]) then
return false return false
end end
@@ -62,7 +64,7 @@ local function trueEquals(a, b): boolean
-- For CFrames, compare to components with epsilon of 0.0001 to avoid floating point inequality -- For CFrames, compare to components with epsilon of 0.0001 to avoid floating point inequality
elseif typeA == "CFrame" and typeB == "CFrame" then elseif typeA == "CFrame" and typeB == "CFrame" then
local aComponents, bComponents = {a:GetComponents()}, {b:GetComponents()} local aComponents, bComponents = { a:GetComponents() }, { b:GetComponents() }
for i, aComponent in aComponents do for i, aComponent in aComponents do
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
return false return false
@@ -72,7 +74,7 @@ local function trueEquals(a, b): boolean
-- For Vector3s, compare to components with epsilon of 0.0001 to avoid floating point inequality -- For Vector3s, compare to components with epsilon of 0.0001 to avoid floating point inequality
elseif typeA == "Vector3" and typeB == "Vector3" then elseif typeA == "Vector3" and typeB == "Vector3" then
local aComponents, bComponents = {a.X, a.Y, a.Z}, {b.X, b.Y, b.Z} local aComponents, bComponents = { a.X, a.Y, a.Z }, { b.X, b.Y, b.Z }
for i, aComponent in aComponents do for i, aComponent in aComponents do
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
return false return false
@@ -82,14 +84,13 @@ local function trueEquals(a, b): boolean
-- For Vector2s, compare to components with epsilon of 0.0001 to avoid floating point inequality -- For Vector2s, compare to components with epsilon of 0.0001 to avoid floating point inequality
elseif typeA == "Vector2" and typeB == "Vector2" then elseif typeA == "Vector2" and typeB == "Vector2" then
local aComponents, bComponents = {a.X, a.Y}, {b.X, b.Y} local aComponents, bComponents = { a.X, a.Y }, { b.X, b.Y }
for i, aComponent in aComponents do for i, aComponent in aComponents do
if not fuzzyEq(aComponent, bComponents[i], 0.0001) then if not fuzzyEq(aComponent, bComponents[i], 0.0001) then
return false return false
end end
end end
return true return true
end end
return false return false
@@ -154,7 +155,13 @@ local function diff(instanceMap, virtualInstances, rootId)
if ok then if ok then
if not trueEquals(existingValue, decodedValue) then if not trueEquals(existingValue, decodedValue) then
Log.debug("{}.{} changed from '{}' to '{}'", instance:GetFullName(), propertyName, existingValue, decodedValue) Log.debug(
"{}.{} changed from '{}' to '{}'",
instance:GetFullName(),
propertyName,
existingValue,
decodedValue
)
changedProperties[propertyName] = virtualValue changedProperties[propertyName] = virtualValue
end end
else else

View File

@@ -264,7 +264,7 @@ return function()
ClassName = "Folder", ClassName = "Folder",
Name = "Folder", Name = "Folder",
Properties = {}, Properties = {},
Children = {"CHILD"}, Children = { "CHILD" },
}, },
CHILD = { CHILD = {

View File

@@ -14,17 +14,19 @@ local function getProperty(instance, propertyName)
-- A good example of a property like this is `Model.ModelInPrimary`, which -- A good example of a property like this is `Model.ModelInPrimary`, which
-- is serialized but not reflected to Lua. -- is serialized but not reflected to Lua.
if descriptor == nil then if descriptor == nil then
return false, Error.new(Error.UnknownProperty, { return false,
className = instance.ClassName, Error.new(Error.UnknownProperty, {
propertyName = propertyName, className = instance.ClassName,
}) propertyName = propertyName,
})
end end
if descriptor.scriptability == "None" or descriptor.scriptability == "Write" then if descriptor.scriptability == "None" or descriptor.scriptability == "Write" then
return false, Error.new(Error.UnreadableProperty, { return false,
className = instance.ClassName, Error.new(Error.UnreadableProperty, {
propertyName = propertyName, className = instance.ClassName,
}) propertyName = propertyName,
})
end end
local success, valueOrErr = descriptor:read(instance) local success, valueOrErr = descriptor:read(instance)
@@ -35,23 +37,26 @@ local function getProperty(instance, propertyName)
-- If we don't have permission to read a property, we can chalk that up -- If we don't have permission to read a property, we can chalk that up
-- to our database being out of date and the engine being right. -- to our database being out of date and the engine being right.
if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("lacking permission") then if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("lacking permission") then
return false, Error.new(Error.LackingPropertyPermissions, { return false,
className = instance.ClassName, Error.new(Error.LackingPropertyPermissions, {
propertyName = propertyName, className = instance.ClassName,
}) propertyName = propertyName,
})
end end
if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("is not a valid member of") then if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("is not a valid member of") then
return false, Error.new(Error.UnknownProperty, { return false,
Error.new(Error.UnknownProperty, {
className = instance.ClassName,
propertyName = propertyName,
})
end
return false,
Error.new(Error.OtherPropertyError, {
className = instance.ClassName, className = instance.ClassName,
propertyName = propertyName, propertyName = propertyName,
}) })
end
return false, Error.new(Error.OtherPropertyError, {
className = instance.ClassName,
propertyName = propertyName,
})
end end
return true, valueOrErr return true, valueOrErr

View File

@@ -47,4 +47,4 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
end end
end end
return hydrate return hydrate

View File

@@ -91,7 +91,7 @@ return function()
ClassName = "Folder", ClassName = "Folder",
Name = "Root", Name = "Root",
Properties = {}, Properties = {},
Children = {"CHILD1", "CHILD2"}, Children = { "CHILD1", "CHILD2" },
}, },
CHILD1 = { CHILD1 = {
@@ -126,4 +126,4 @@ return function()
expect(knownInstances.fromIds["CHILD1"]).to.equal(child1) expect(knownInstances.fromIds["CHILD1"]).to.equal(child1)
expect(knownInstances.fromIds["CHILD2"]).to.equal(child2) expect(knownInstances.fromIds["CHILD2"]).to.equal(child2)
end) end)
end end

View File

@@ -80,7 +80,7 @@ return function()
ClassName = "Folder", ClassName = "Folder",
Name = "Parent", Name = "Parent",
Properties = {}, Properties = {},
Children = {"CHILD"}, Children = { "CHILD" },
}, },
CHILD = { CHILD = {
@@ -112,7 +112,7 @@ return function()
ClassName = "Folder", ClassName = "Folder",
Name = "Parent", Name = "Parent",
Properties = {}, Properties = {},
Children = {"CHILD"}, Children = { "CHILD" },
}, },
CHILD = { CHILD = {
@@ -147,7 +147,7 @@ return function()
Properties = { Properties = {
Value = { Value = {
Type = "Vector3", Type = "Vector3",
Value = {1, 2, 3}, Value = { 1, 2, 3 },
}, },
}, },
Children = {}, Children = {},
@@ -182,7 +182,7 @@ return function()
ClassName = "Folder", ClassName = "Folder",
Name = "Root", Name = "Root",
Properties = {}, Properties = {},
Children = {"CHILD"}, Children = { "CHILD" },
}, },
CHILD = { CHILD = {
@@ -247,7 +247,7 @@ return function()
ClassName = "Folder", ClassName = "Folder",
Name = "Root", Name = "Root",
Properties = {}, Properties = {},
Children = {"CHILD_A", "CHILD_B"}, Children = { "CHILD_A", "CHILD_B" },
}, },
CHILD_A = { CHILD_A = {
@@ -297,7 +297,7 @@ return function()
Ref = "CHILD", Ref = "CHILD",
}, },
}, },
Children = {"CHILD"}, Children = { "CHILD" },
}, },
CHILD = { CHILD = {

View File

@@ -21,26 +21,29 @@ local function setProperty(instance, propertyName, value)
end end
if descriptor.scriptability == "None" or descriptor.scriptability == "Read" then if descriptor.scriptability == "None" or descriptor.scriptability == "Read" then
return false, Error.new(Error.UnwritableProperty, { return false,
className = instance.ClassName, Error.new(Error.UnwritableProperty, {
propertyName = propertyName, className = instance.ClassName,
}) propertyName = propertyName,
})
end end
local ok, err = descriptor:write(instance, value) local ok, err = descriptor:write(instance, value)
if not ok then if not ok then
if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("lacking permission") then if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("lacking permission") then
return false, Error.new(Error.LackingPropertyPermissions, { return false,
Error.new(Error.LackingPropertyPermissions, {
className = instance.ClassName,
propertyName = propertyName,
})
end
return false,
Error.new(Error.OtherPropertyError, {
className = instance.ClassName, className = instance.ClassName,
propertyName = propertyName, propertyName = propertyName,
}) })
end
return false, Error.new(Error.OtherPropertyError, {
className = instance.ClassName,
propertyName = propertyName,
})
end end
return true return true

View File

@@ -77,15 +77,13 @@ function ServeSession.new(options)
local connections = {} local connections = {}
local connection = StudioService local connection = StudioService:GetPropertyChangedSignal("ActiveScript"):Connect(function()
:GetPropertyChangedSignal("ActiveScript") local activeScript = StudioService.ActiveScript
:Connect(function()
local activeScript = StudioService.ActiveScript
if activeScript ~= nil then if activeScript ~= nil then
self:__onActiveScriptChanged(activeScript) self:__onActiveScriptChanged(activeScript)
end end
end) end)
table.insert(connections, connection) table.insert(connections, connection)
self = { self = {
@@ -139,16 +137,16 @@ end
function ServeSession:start() function ServeSession:start()
self:__setStatus(Status.Connecting) self:__setStatus(Status.Connecting)
self.__apiContext:connect() self.__apiContext
:connect()
:andThen(function(serverInfo) :andThen(function(serverInfo)
self:__applyGameAndPlaceId(serverInfo) self:__applyGameAndPlaceId(serverInfo)
return self:__initialSync(serverInfo) return self:__initialSync(serverInfo):andThen(function()
:andThen(function() self:__setStatus(Status.Connected, serverInfo.projectName)
self:__setStatus(Status.Connected, serverInfo.projectName)
return self:__mainSyncLoop() return self:__mainSyncLoop()
end) end)
end) end)
:catch(function(err) :catch(function(err)
if self.__status ~= Status.Disconnected then if self.__status ~= Status.Disconnected then
@@ -211,97 +209,93 @@ function ServeSession:__onActiveScriptChanged(activeScript)
end end
function ServeSession:__initialSync(serverInfo) function ServeSession:__initialSync(serverInfo)
return self.__apiContext:read({ serverInfo.rootInstanceId }) return self.__apiContext:read({ serverInfo.rootInstanceId }):andThen(function(readResponseBody)
:andThen(function(readResponseBody) -- Tell the API Context that we're up-to-date with the version of
-- Tell the API Context that we're up-to-date with the version of -- the tree defined in this response.
-- the tree defined in this response. self.__apiContext:setMessageCursor(readResponseBody.messageCursor)
self.__apiContext:setMessageCursor(readResponseBody.messageCursor)
-- For any instances that line up with the Rojo server's view, start -- For any instances that line up with the Rojo server's view, start
-- tracking them in the reconciler. -- tracking them in the reconciler.
Log.trace("Matching existing Roblox instances to Rojo IDs") Log.trace("Matching existing Roblox instances to Rojo IDs")
self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game) self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game)
-- Calculate the initial patch to apply to the DataModel to catch us -- Calculate the initial patch to apply to the DataModel to catch us
-- up to what Rojo thinks the place should look like. -- up to what Rojo thinks the place should look like.
Log.trace("Computing changes that plugin needs to make to catch up to server...") Log.trace("Computing changes that plugin needs to make to catch up to server...")
local success, catchUpPatch = self.__reconciler:diff( local success, catchUpPatch =
readResponseBody.instances, self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, game)
serverInfo.rootInstanceId,
game
)
if not success then if not success then
Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch) Log.error("Could not compute a diff to catch up to the Rojo server: {:#?}", catchUpPatch)
end end
for _, update in catchUpPatch.updated do for _, update in catchUpPatch.updated do
if if update.id == self.__instanceMap.fromInstances[game] and update.changedClassName ~= nil then
update.id == self.__instanceMap.fromInstances[game] -- Non-place projects will try to update the classname of game from DataModel to
and update.changedClassName ~= nil -- something like Folder, ModuleScript, etc. This would fail, so we exit with a clear
then -- message instead of crashing.
-- Non-place projects will try to update the classname of game from DataModel to return Promise.reject(
-- something like Folder, ModuleScript, etc. This would fail, so we exit with a clear "Cannot sync a model as a place."
-- 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." .. "\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/" .. "\nSee project file docs: https://rojo.space/docs/v7/project-format/"
) )
end
end
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch))
local userDecision = "Accept"
if self.__userConfirmCallback ~= nil then
userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo)
end
if userDecision == "Abort" then
return Promise.reject("Aborted Rojo sync operation")
elseif userDecision == "Reject" and self.__twoWaySync then
-- The user wants their studio DOM to write back to their Rojo DOM
-- so we will reverse the patch and send it back
local inversePatch = PatchSet.newEmpty()
-- Send back the current properties
for _, change in catchUpPatch.updated do
local instance = self.__instanceMap.fromIds[change.id]
if not instance then
continue
end end
local update = encodePatchUpdate(instance, change.id, change.changedProperties)
table.insert(inversePatch.updated, update)
end
-- Add the removed instances back to Rojo
-- selene:allow(empty_if, unused_variable)
for _, instance in catchUpPatch.removed do
-- TODO: Generate ID for our instance and add it to inversePatch.added
end
-- Remove the additions we've rejected
for id, _change in catchUpPatch.added do
table.insert(inversePatch.removed, id)
end end
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch)) self.__apiContext:write(inversePatch)
elseif userDecision == "Accept" then
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
local userDecision = "Accept" if not PatchSet.isEmpty(unappliedPatch) then
if self.__userConfirmCallback ~= nil then Log.warn(
userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo) "Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
)
end end
end
if userDecision == "Abort" then end)
return Promise.reject("Aborted Rojo sync operation")
elseif userDecision == "Reject" and self.__twoWaySync then
-- The user wants their studio DOM to write back to their Rojo DOM
-- so we will reverse the patch and send it back
local inversePatch = PatchSet.newEmpty()
-- Send back the current properties
for _, change in catchUpPatch.updated do
local instance = self.__instanceMap.fromIds[change.id]
if not instance then continue end
local update = encodePatchUpdate(instance, change.id, change.changedProperties)
table.insert(inversePatch.updated, update)
end
-- Add the removed instances back to Rojo
-- selene:allow(empty_if, unused_variable)
for _, instance in catchUpPatch.removed do
-- TODO: Generate ID for our instance and add it to inversePatch.added
end
-- Remove the additions we've rejected
for id, _change in catchUpPatch.added do
table.insert(inversePatch.removed, id)
end
self.__apiContext:write(inversePatch)
elseif userDecision == "Accept" then
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
if not PatchSet.isEmpty(unappliedPatch) then
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
end
end
end)
end end
function ServeSession:__mainSyncLoop() function ServeSession:__mainSyncLoop()
return Promise.new(function(resolve, reject) return Promise.new(function(resolve, reject)
while self.__status == Status.Connected do while self.__status == Status.Connected do
local success, result = self.__apiContext:retrieveMessages() local success, result = self.__apiContext
:retrieveMessages()
:andThen(function(messages) :andThen(function(messages)
if self.__status == Status.Disconnected then if self.__status == Status.Disconnected then
-- In the time it took to retrieve messages, we disconnected -- In the time it took to retrieve messages, we disconnected
@@ -315,11 +309,14 @@ function ServeSession:__mainSyncLoop()
local unappliedPatch = self.__reconciler:applyPatch(message) local unappliedPatch = self.__reconciler:applyPatch(message)
if not PatchSet.isEmpty(unappliedPatch) then if not PatchSet.isEmpty(unappliedPatch) then
Log.warn("Could not apply all changes requested by the Rojo server:\n{}", Log.warn(
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)) "Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
)
end end
end end
end):await() end)
:await()
if self.__status == Status.Disconnected then if self.__status == Status.Disconnected then
-- If we are no longer connected after applying, we stop silently -- If we are no longer connected after applying, we stop silently

View File

@@ -56,11 +56,7 @@ local ApiSubscribeResponse = t.interface({
}) })
local ApiError = t.interface({ local ApiError = t.interface({
kind = t.union( kind = t.union(t.literal("NotFound"), t.literal("BadRequest"), t.literal("InternalError")),
t.literal("NotFound"),
t.literal("BadRequest"),
t.literal("InternalError")
),
details = t.string, details = t.string,
}) })

View File

@@ -43,4 +43,4 @@ function Version.display(version)
return output return output
end end
return Version return Version

View File

@@ -2,27 +2,27 @@ return function()
local Version = require(script.Parent.Version) local Version = require(script.Parent.Version)
it("should compare equal versions", function() it("should compare equal versions", function()
expect(Version.compare({1, 2, 3}, {1, 2, 3})).to.equal(0) expect(Version.compare({ 1, 2, 3 }, { 1, 2, 3 })).to.equal(0)
expect(Version.compare({0, 4, 0}, {0, 4})).to.equal(0) expect(Version.compare({ 0, 4, 0 }, { 0, 4 })).to.equal(0)
expect(Version.compare({0, 0, 123}, {0, 0, 123})).to.equal(0) expect(Version.compare({ 0, 0, 123 }, { 0, 0, 123 })).to.equal(0)
expect(Version.compare({26}, {26})).to.equal(0) expect(Version.compare({ 26 }, { 26 })).to.equal(0)
expect(Version.compare({26, 42}, {26, 42})).to.equal(0) expect(Version.compare({ 26, 42 }, { 26, 42 })).to.equal(0)
expect(Version.compare({1, 0, 0}, {1})).to.equal(0) expect(Version.compare({ 1, 0, 0 }, { 1 })).to.equal(0)
end) end)
it("should compare newer, older versions", function() it("should compare newer, older versions", function()
expect(Version.compare({1}, {0})).to.equal(1) expect(Version.compare({ 1 }, { 0 })).to.equal(1)
expect(Version.compare({1, 1}, {1, 0})).to.equal(1) expect(Version.compare({ 1, 1 }, { 1, 0 })).to.equal(1)
end) end)
it("should compare different major versions", function() it("should compare different major versions", function()
expect(Version.compare({1, 3, 2}, {2, 2, 1})).to.equal(-1) expect(Version.compare({ 1, 3, 2 }, { 2, 2, 1 })).to.equal(-1)
expect(Version.compare({1, 2}, {2, 1})).to.equal(-1) expect(Version.compare({ 1, 2 }, { 2, 1 })).to.equal(-1)
expect(Version.compare({1}, {2})).to.equal(-1) expect(Version.compare({ 1 }, { 2 })).to.equal(-1)
end) end)
it("should compare different minor versions", function() it("should compare different minor versions", function()
expect(Version.compare({1, 2, 3}, {1, 3, 2})).to.equal(-1) expect(Version.compare({ 1, 2, 3 }, { 1, 3, 2 })).to.equal(-1)
expect(Version.compare({50, 1}, {50, 2})).to.equal(-1) expect(Version.compare({ 50, 1 }, { 50, 2 })).to.equal(-1)
end) end)
end end

View File

@@ -50,4 +50,4 @@ local function createSignal()
} }
end end
return createSignal return createSignal

View File

@@ -5,23 +5,23 @@
--]] --]]
return { return {
["0"] = true, -- Local file ["0"] = true, -- Local file
["95206881"] = true, -- Baseplate ["95206881"] = true, -- Baseplate
["6560363541"] = true, -- Classic Baseplate ["6560363541"] = true, -- Classic Baseplate
["95206192"] = true, -- Flat Terrain ["95206192"] = true, -- Flat Terrain
["13165709401"] = true, -- Modern City ["13165709401"] = true, -- Modern City
["520390648"] = true, -- Village ["520390648"] = true, -- Village
["203810088"] = true, -- Castle ["203810088"] = true, -- Castle
["366130569"] = true, -- Suburban ["366130569"] = true, -- Suburban
["215383192"] = true, -- Racing ["215383192"] = true, -- Racing
["264719325"] = true, -- Pirate Island ["264719325"] = true, -- Pirate Island
["203812057"] = true, -- Obby ["203812057"] = true, -- Obby
["379736082"] = true, -- Starting Place ["379736082"] = true, -- Starting Place
["301530843"] = true, -- Line Runner ["301530843"] = true, -- Line Runner
["92721754"] = true, -- Capture The Flag ["92721754"] = true, -- Capture The Flag
["301529772"] = true, -- Team/FFA Arena ["301529772"] = true, -- Team/FFA Arena
["203885589"] = true, -- Combat ["203885589"] = true, -- Combat
["10275826693"] = true, -- Concert ["10275826693"] = true, -- Concert
["5353920686"] = true, -- Move It Simulator ["5353920686"] = true, -- Move It Simulator
["6936227200"] = true, -- Mansion Of Wonder ["6936227200"] = true, -- Mansion Of Wonder
} }

View File

@@ -17,7 +17,7 @@ Log.setLogLevelThunk(function()
end) end)
local app = Roact.createElement(App, { local app = Roact.createElement(App, {
plugin = plugin plugin = plugin,
}) })
local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI") local tree = Roact.mount(app, game:GetService("CoreGui"), "Rojo UI")

View File

@@ -1,4 +1,3 @@
local Packages = script.Parent.Parent.Packages local Packages = script.Parent.Parent.Packages
local Fmt = require(Packages.Fmt) local Fmt = require(Packages.Fmt)
@@ -17,10 +16,10 @@ else
message = Fmt.fmt(message, ...) message = Fmt.fmt(message, ...)
local fullMessage = string.format( local fullMessage = string.format(
"Rojo detected an invariant violation within itself:\n" .. "Rojo detected an invariant violation within itself:\n"
"%s\n\n" .. .. "%s\n\n"
"This is a bug in Rojo. Please file an issue:\n" .. .. "This is a bug in Rojo. Please file an issue:\n"
"https://github.com/rojo-rbx/rojo/issues", .. "https://github.com/rojo-rbx/rojo/issues",
message message
) )

View File

@@ -2,12 +2,10 @@
local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin") local plugin = plugin or script:FindFirstAncestorWhichIsA("Plugin")
local widget = nil local widget = nil
if plugin then if plugin then
widget = plugin:CreateDockWidgetPluginGui("Rojo_soundPlayer", DockWidgetPluginGuiInfo.new( widget = plugin:CreateDockWidgetPluginGui(
Enum.InitialDockState.Float, "Rojo_soundPlayer",
false, true, DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, true, 10, 10, 10, 10)
10, 10, )
10, 10
))
widget.Name = "Rojo_soundPlayer" widget.Name = "Rojo_soundPlayer"
widget.Title = "Rojo Sound Player" widget.Title = "Rojo Sound Player"
end end
@@ -22,7 +20,9 @@ function SoundPlayer.new(settings)
end end
function SoundPlayer:play(soundId) function SoundPlayer:play(soundId)
if self.settings and self.settings:get("playSounds") == false then return end if self.settings and self.settings:get("playSounds") == false then
return
end
local sound = Instance.new("Sound") local sound = Instance.new("Sound")
sound.SoundId = soundId sound.SoundId = soundId

View File

@@ -21,4 +21,4 @@ return function(nameOrTarget, target)
else else
return strictInner("<unnamed table>", target) return strictInner("<unnamed table>", target)
end end
end end