Merge branch 'master' into headless-api

This commit is contained in:
boatbomber 2023-08-20 21:58:03 -07:00 • committed by GitHub
commit 4ab51e1906
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 293 additions and 58 deletions

View file

@ -28,6 +28,7 @@
* Don't override the initial enabled state for source diffing ([#760]) * Don't override the initial enabled state for source diffing ([#760])
* Added support for `Terrain.MaterialColors` ([#770]) * Added support for `Terrain.MaterialColors` ([#770])
* Allow `Terrain` to be specified without a classname ([#771]) * Allow `Terrain` to be specified without a classname ([#771])
* Add Confirmation Behavior setting ([#774])
[#631]: https://github.com/rojo-rbx/rojo/pull/631 [#631]: https://github.com/rojo-rbx/rojo/pull/631
[#761]: https://github.com/rojo-rbx/rojo/pull/761 [#761]: https://github.com/rojo-rbx/rojo/pull/761
@ -56,6 +57,7 @@
[#760]: https://github.com/rojo-rbx/rojo/pull/760 [#760]: https://github.com/rojo-rbx/rojo/pull/760
[#770]: https://github.com/rojo-rbx/rojo/pull/770 [#770]: https://github.com/rojo-rbx/rojo/pull/770
[#771]: https://github.com/rojo-rbx/rojo/pull/771 [#771]: https://github.com/rojo-rbx/rojo/pull/771
[#774]: https://github.com/rojo-rbx/rojo/pull/774
## [7.3.0] - April 22, 2023 ## [7.3.0] - April 22, 2023
* Added `$attributes` to project format. ([#574]) * Added `$attributes` to project format. ([#574])

View file

@ -282,6 +282,7 @@ types = {
math.round(math.clamp(color.B, 0, 1) * 255) math.round(math.clamp(color.B, 0, 1) * 255)
} }
end end
return pod
end, end,
}, },

View file

@ -1,6 +1,6 @@
return function() return function()
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local EncodedValue = require(script.Parent.EncodedValue) local EncodedValue = require(script.Parent.EncodedValue)
local allValues = require(script.Parent.allValues) local allValues = require(script.Parent.allValues)
@ -13,16 +13,16 @@ return function()
if ty == "table" then if ty == "table" then
local visited = {} local visited = {}
for key, valueA in pairs(a) do for key, valueA in a do
visited[key] = true visited[key] = true
if not deepEq(valueA, b[key]) then if not deepEq(valueA, b[key]) then
return false return false
end end
end end
for key, valueB in pairs(b) do for key, valueB in b do
if visited[key] then if visited[key] then
continue continue
end end
@ -44,28 +44,23 @@ return function()
end, end,
} }
for testName, testEntry in pairs(allValues) do for testName, testEntry in allValues do
it("round trip " .. testName, function() it("round trip " .. testName, function()
local ok, decoded = EncodedValue.decode(testEntry.value) local decodeOk, decoded = EncodedValue.decode(testEntry.value)
assert(ok, decoded) assert(decodeOk, decoded)
if extraAssertions[testName] ~= nil then if extraAssertions[testName] ~= nil then
extraAssertions[testName](decoded) extraAssertions[testName](decoded)
end end
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty) local encodeOk, encoded = EncodedValue.encode(decoded, testEntry.ty)
assert(ok, encoded) assert(encodeOk, encoded)
if not deepEq(encoded, testEntry.value) then if not deepEq(encoded, testEntry.value) then
local expected = HttpService:JSONEncode(testEntry.value) local expected = HttpService:JSONEncode(testEntry.value)
local actual = HttpService:JSONEncode(encoded) local actual = HttpService:JSONEncode(encoded)
local message = string.format( error(`Round-trip results did not match.\nExpected:\n{expected}\nActual:\n{actual}`)
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
expected, actual
)
error(message)
end end
end) end)
end end

View file

@ -0,0 +1,103 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Flipper = require(Packages.Flipper)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local bindingUtil = require(Plugin.App.bindingUtil)
local SlicedImage = require(script.Parent.SlicedImage)
local SPRING_PROPS = {
frequency = 5,
dampingRatio = 1,
}
local e = Roact.createElement
local TextInput = Roact.Component:extend("TextInput")
function TextInput:init()
self.motor = Flipper.GroupMotor.new({
hover = 0,
enabled = self.props.enabled and 1 or 0,
})
self.binding = bindingUtil.fromMotor(self.motor)
end
function TextInput:didUpdate(lastProps)
if lastProps.enabled ~= self.props.enabled then
self.motor:setGoal({
enabled = Flipper.Spring.new(self.props.enabled and 1 or 0),
})
end
end
function TextInput:render()
return Theme.with(function(theme)
theme = theme.TextInput
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
return e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
transparency = self.props.transparency,
size = self.props.size or UDim2.new(1, 0, 1, 0),
position = self.props.position,
layoutOrder = self.props.layoutOrder,
anchorPoint = self.props.anchorPoint,
}, {
HoverOverlay = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground,
color = theme.ActionFillColor,
transparency = Roact.joinBindings({
hover = bindingHover:map(function(value)
return 1 - value
end),
transparency = self.props.transparency,
}):map(function(values)
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
end),
size = UDim2.new(1, 0, 1, 0),
zIndex = -1,
}),
Input = e("TextBox", {
BackgroundTransparency = 1,
Size = UDim2.fromScale(1, 1),
Text = self.props.text,
PlaceholderText = self.props.placeholder,
Font = Enum.Font.GothamMedium,
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
PlaceholderColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.PlaceholderColor, theme.Enabled.PlaceholderColor),
TextSize = 18,
TextEditable = self.props.enabled,
ClearTextOnFocus = self.props.clearTextOnFocus,
[Roact.Event.MouseEnter] = function()
self.motor:setGoal({
hover = Flipper.Spring.new(1, SPRING_PROPS),
})
end,
[Roact.Event.MouseLeave] = function()
self.motor:setGoal({
hover = Flipper.Spring.new(0, SPRING_PROPS),
})
end,
[Roact.Event.FocusLost] = function(rbx)
self.props.onEntered(rbx.Text)
end,
}),
Children = Roact.createFragment(self.props[Roact.Children]),
})
end)
end
return TextInput

View file

@ -32,6 +32,7 @@ local Setting = Roact.Component:extend("Setting")
function Setting:init() function Setting:init()
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
self.inputSize, self.setInputSize = Roact.createBinding(Vector2.new(0, 0))
if self.props.id then if self.props.id then
self:setState({ self:setState({
@ -68,45 +69,56 @@ function Setting:render()
self.setContainerSize(object.AbsoluteSize) self.setContainerSize(object.AbsoluteSize)
end, end,
}, { }, {
Input = if self.props.customInput then RightAligned = Roact.createElement("Frame", {
self.props.customInput BackgroundTransparency = 1,
elseif self.props.options ~= nil then Size = UDim2.new(1, 0, 1, 0),
e(Dropdown, { }, {
locked = self.props.locked, Layout = e("UIListLayout", {
options = self.props.options, VerticalAlignment = Enum.VerticalAlignment.Center,
active = self.state.setting, HorizontalAlignment = Enum.HorizontalAlignment.Right,
transparency = self.props.transparency, FillDirection = Enum.FillDirection.Horizontal,
position = UDim2.new(1, 0, 0.5, 0), SortOrder = Enum.SortOrder.LayoutOrder,
anchorPoint = Vector2.new(1, 0.5), Padding = UDim.new(0, 2),
onClick = function(option) [Roact.Change.AbsoluteContentSize] = function(rbx)
Settings:set(self.props.id, option) self.setInputSize(rbx.AbsoluteContentSize)
end,
})
else
e(Checkbox, {
locked = self.props.locked,
active = self.state.setting,
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0.5, 0),
anchorPoint = Vector2.new(1, 0.5),
onClick = function()
local currentValue = Settings:get(self.props.id)
Settings:set(self.props.id, not currentValue)
end, end,
}), }),
Reset = if self.props.onReset then e(IconButton, { Input =
icon = Assets.Images.Icons.Reset, if self.props.input ~= nil then
iconSize = 24, self.props.input
color = theme.BackButtonColor, elseif self.props.options ~= nil then
transparency = self.props.transparency, e(Dropdown, {
visible = self.props.showReset, locked = self.props.locked,
options = self.props.options,
active = self.state.setting,
transparency = self.props.transparency,
onClick = function(option)
Settings:set(self.props.id, option)
end,
})
else
e(Checkbox, {
locked = self.props.locked,
active = self.state.setting,
transparency = self.props.transparency,
onClick = function()
local currentValue = Settings:get(self.props.id)
Settings:set(self.props.id, not currentValue)
end,
}),
position = UDim2.new(1, -32 - (self.props.options ~= nil and 120 or 40), 0.5, 0), Reset = if self.props.onReset then e(IconButton, {
anchorPoint = Vector2.new(0, 0.5), icon = Assets.Images.Icons.Reset,
iconSize = 24,
color = theme.BackButtonColor,
transparency = self.props.transparency,
visible = self.props.showReset,
layoutOrder = -1,
onClick = self.props.onReset, onClick = self.props.onReset,
}) else nil, }) else nil,
}),
Text = e("Frame", { Text = e("Frame", {
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
@ -138,12 +150,15 @@ function Setting:render()
TextWrapped = true, TextWrapped = true,
RichText = true, RichText = true,
Size = self.containerSize:map(function(value) Size = Roact.joinBindings({
containerSize = self.containerSize,
inputSize = self.inputSize,
}):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 = (self.props.onReset and 34 or 0) + (self.props.options ~= nil and 120 or 40) 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(value.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)
end), end),

View file

@ -13,6 +13,7 @@ local IconButton = require(Plugin.App.Components.IconButton)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local Tooltip = require(Plugin.App.Components.Tooltip) local Tooltip = require(Plugin.App.Components.Tooltip)
local SlicedImage = require(Plugin.App.Components.SlicedImage) local SlicedImage = require(Plugin.App.Components.SlicedImage)
local TextInput = require(Plugin.App.Components.TextInput)
local Setting = require(script.Setting) local Setting = require(script.Setting)
local e = Roact.createElement local e = Roact.createElement
@ -26,6 +27,7 @@ local function invertTbl(tbl)
end end
local invertedLevels = invertTbl(Log.Level) local invertedLevels = invertTbl(Log.Level)
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId" }
local function Navbar(props) local function Navbar(props)
return Theme.with(function(theme) return Theme.with(function(theme)
@ -105,19 +107,57 @@ function SettingsPage:render()
layoutOrder = 2, layoutOrder = 2,
}), }),
ConfirmationBehavior = e(Setting, {
id = "confirmationBehavior",
name = "Confirmation Behavior",
description = "When to prompt for confirmation before syncing",
transparency = self.props.transparency,
layoutOrder = 3,
options = confirmationBehaviors,
}),
LargeChangesConfirmationThreshold = e(Setting, {
id = "largeChangesConfirmationThreshold",
name = "Confirmation Threshold",
description = "How many modified instances to be considered a large change",
transparency = self.props.transparency,
layoutOrder = 4,
visible = Settings:getBinding("confirmationBehavior"):map(function(value)
return value == "Large Changes"
end),
input = e(TextInput, {
size = UDim2.new(0, 40, 0, 28),
text = Settings:getBinding("largeChangesConfirmationThreshold"):map(function(value)
return tostring(value)
end),
transparency = self.props.transparency,
enabled = true,
onEntered = function(text)
local number = tonumber(string.match(text, "%d+"))
if number then
Settings:set("largeChangesConfirmationThreshold", math.clamp(number, 1, 999))
else
-- Force text back to last valid value
Settings:set("largeChangesConfirmationThreshold", Settings:get("largeChangesConfirmationThreshold"))
end
end,
}),
}),
PlaySounds = e(Setting, { PlaySounds = e(Setting, {
id = "playSounds", id = "playSounds",
name = "Play Sounds", name = "Play Sounds",
description = "Toggle sound effects", description = "Toggle sound effects",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 3, layoutOrder = 5,
}), }),
Permissions = e(Setting, { Permissions = e(Setting, {
name = "Third Party Permissions", name = "Third Party Permissions",
description = "Manage permissions for third party plugins", description = "Manage permissions for third party plugins",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 4, layoutOrder = 6,
customInput = e("TextButton", { customInput = e("TextButton", {
Text = "", Text = "",
BackgroundTransparency = 1, BackgroundTransparency = 1,
@ -160,7 +200,7 @@ function SettingsPage:render()
locked = self.props.syncActive, locked = self.props.syncActive,
experimental = true, experimental = true,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 5, layoutOrder = 7,
}), }),
TwoWaySync = e(Setting, { TwoWaySync = e(Setting, {
@ -170,7 +210,7 @@ function SettingsPage:render()
locked = self.props.syncActive, locked = self.props.syncActive,
experimental = true, experimental = true,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 6, layoutOrder = 8,
}), }),
LogLevel = e(Setting, { LogLevel = e(Setting, {

View file

@ -74,6 +74,20 @@ local lightTheme = strict("LightTheme", {
IconColor = Color3.fromHex("EEEEEE"), IconColor = Color3.fromHex("EEEEEE"),
}, },
}, },
TextInput = {
Enabled = {
TextColor = Color3.fromHex("000000"),
PlaceholderColor = Color3.fromHex("8C8C8C"),
BorderColor = Color3.fromHex("ACACAC"),
},
Disabled = {
TextColor = Color3.fromHex("393939"),
PlaceholderColor = Color3.fromHex("8C8C8C"),
BorderColor = Color3.fromHex("AFAFAF"),
},
ActionFillColor = Color3.fromHex("000000"),
ActionFillTransparency = 0.9,
},
AddressEntry = { AddressEntry = {
TextColor = Color3.fromHex("000000"), TextColor = Color3.fromHex("000000"),
PlaceholderColor = Color3.fromHex("8C8C8C") PlaceholderColor = Color3.fromHex("8C8C8C")
@ -170,6 +184,20 @@ local darkTheme = strict("DarkTheme", {
IconColor = Color3.fromHex("484848"), IconColor = Color3.fromHex("484848"),
}, },
}, },
TextInput = {
Enabled = {
TextColor = Color3.fromHex("FFFFFF"),
PlaceholderColor = Color3.fromHex("8B8B8B"),
BorderColor = Color3.fromHex("535353"),
},
Disabled = {
TextColor = Color3.fromHex("484848"),
PlaceholderColor = Color3.fromHex("8B8B8B"),
BorderColor = Color3.fromHex("5A5A5A"),
},
ActionFillColor = Color3.fromHex("FFFFFF"),
ActionFillTransparency = 0.9,
},
AddressEntry = { AddressEntry = {
TextColor = Color3.fromHex("FFFFFF"), TextColor = Color3.fromHex("FFFFFF"),
PlaceholderColor = Color3.fromHex("8B8B8B") PlaceholderColor = Color3.fromHex("8B8B8B")

View file

@ -61,6 +61,7 @@ function App:init()
self.confirmationBindable = Instance.new("BindableEvent") self.confirmationBindable = Instance.new("BindableEvent")
self.confirmationEvent = self.confirmationBindable.Event self.confirmationEvent = self.confirmationBindable.Event
self.knownProjects = {}
self.notifId = 0 self.notifId = 0
self.waypointConnection = ChangeHistoryService.OnUndo:Connect(function(action: string) self.waypointConnection = ChangeHistoryService.OnUndo:Connect(function(action: string)
@ -542,6 +543,8 @@ function App:startSession(host: string?, port: string?)
self.headlessAPI:_updateProperty("Address", address) self.headlessAPI:_updateProperty("Address", address)
self.headlessAPI:_updateProperty("ProjectName", details) self.headlessAPI:_updateProperty("ProjectName", details)
self.knownProjects[details] = true
self:setState({ self:setState({
appStatus = AppStatus.Connected, appStatus = AppStatus.Connected,
projectName = details, projectName = details,
@ -593,6 +596,30 @@ function App:startSession(host: string?, port: string?)
return "Accept" return "Accept"
end end
local confirmationBehavior = Settings:get("confirmationBehavior")
if confirmationBehavior == "Initial" then
-- Only confirm if we haven't synced this project yet this session
if self.knownProjects[serverInfo.projectName] then
Log.trace("Accepting patch without confirmation because project has already been connected and behavior is set to Initial")
return "Accept"
end
elseif confirmationBehavior == "Large Changes" then
-- Only confirm if the patch impacts many instances
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")
return "Accept"
end
elseif confirmationBehavior == "Unlisted PlaceId" then
-- Only confirm if the current placeId is not in the servePlaceIds allowlist
if serverInfo.expectedPlaceIds then
local isListed = table.find(serverInfo.expectedPlaceIds, game.PlaceId) ~= nil
if isListed then
Log.trace("Accepting patch without confirmation because placeId is listed and behavior is set to Unlisted PlaceId")
return "Accept"
end
end
end
-- The datamodel name gets overwritten by Studio, making confirmation of it intrusive -- The datamodel name gets overwritten by Studio, making confirmation of it intrusive
-- and unnecessary. This special case allows it to be accepted without confirmation. -- and unnecessary. This special case allows it to be accepted without confirmation.
if if

View file

@ -116,7 +116,7 @@ function PatchSet.containsId(patchSet, instanceMap, id)
end end
--[[ --[[
Tells whether the given PatchSet contains changes to the given instance. 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. If the given InstanceMap does not contain the instance, this function always returns false.
]] ]]
function PatchSet.containsInstance(patchSet, instanceMap, instance) function PatchSet.containsInstance(patchSet, instanceMap, instance)
@ -235,6 +235,28 @@ function PatchSet.countChanges(patch)
return count return count
end end
--[[
Count the number of instances affected by the given PatchSet.
]]
function PatchSet.countInstances(patch)
local count = 0
-- Added instances
for _ in patch.added do
count += 1
end
-- Removed instances
for _ in patch.removed do
count += 1
end
-- Updated instances
for _ in patch.updated do
count += 1
end
return count
end
--[[ --[[
Merge multiple PatchSet objects into the given PatchSet. Merge multiple PatchSet objects into the given PatchSet.
]] ]]

View file

@ -14,6 +14,8 @@ local defaultSettings = {
twoWaySync = false, twoWaySync = false,
showNotifications = true, showNotifications = true,
syncReminder = true, syncReminder = true,
confirmationBehavior = "Initial",
largeChangesConfirmationThreshold = 5,
playSounds = true, playSounds = true,
typecheckingEnabled = false, typecheckingEnabled = false,
logLevel = "Info", logLevel = "Info",