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

This commit is contained in:
boatbomber 2024-01-31 18:41:24 -08:00
commit 9397dbaf78
7 changed files with 191 additions and 45 deletions

View file

@ -4,6 +4,7 @@
* Added headless API for Studio companion plugins. ([#631]) * Added headless API for Studio companion plugins. ([#631])
* Added popout diff visualizer for table properties like Attributes and Tags ([#834]) * Added popout diff visualizer for table properties like Attributes and Tags ([#834])
* Updated Theme to use Studio colors ([#838]) * Updated Theme to use Studio colors ([#838])
* Added experimental setting for Auto Connect in playtests ([#840])
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813]) * Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
This is specified via a new field on project files, `syncRules`: This is specified via a new field on project files, `syncRules`:
@ -55,6 +56,7 @@
[#813]: https://github.com/rojo-rbx/rojo/pull/813 [#813]: https://github.com/rojo-rbx/rojo/pull/813
[#834]: https://github.com/rojo-rbx/rojo/pull/834 [#834]: https://github.com/rojo-rbx/rojo/pull/834
[#838]: https://github.com/rojo-rbx/rojo/pull/838 [#838]: https://github.com/rojo-rbx/rojo/pull/838
[#840]: https://github.com/rojo-rbx/rojo/pull/840
## [7.4.0] - January 16, 2024 ## [7.4.0] - January 16, 2024
* Improved the visualization for array properties like Tags ([#829]) * Improved the visualization for array properties like Tags ([#829])

View file

@ -76,6 +76,12 @@ function SettingsPage:init()
end end
function SettingsPage:render() function SettingsPage:render()
local layoutOrder = 0
local function layoutIncrement()
layoutOrder += 1
return layoutOrder
end
return Theme.with(function(theme) return Theme.with(function(theme)
theme = theme.Settings theme = theme.Settings
@ -87,7 +93,7 @@ function SettingsPage:render()
Navbar = e(Navbar, { Navbar = e(Navbar, {
onBack = self.props.onBack, onBack = self.props.onBack,
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 0, layoutOrder = layoutIncrement(),
}), }),
ShowNotifications = e(Setting, { ShowNotifications = e(Setting, {
@ -95,7 +101,7 @@ function SettingsPage:render()
name = "Show Notifications", name = "Show Notifications",
description = "Popup notifications in viewport", description = "Popup notifications in viewport",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 1, layoutOrder = layoutIncrement(),
}), }),
SyncReminder = e(Setting, { SyncReminder = e(Setting, {
@ -104,7 +110,7 @@ function SettingsPage:render()
description = "Notify to sync when opening a place that has previously been synced", description = "Notify to sync when opening a place that has previously been synced",
transparency = self.props.transparency, transparency = self.props.transparency,
visible = Settings:getBinding("showNotifications"), visible = Settings:getBinding("showNotifications"),
layoutOrder = 2, layoutOrder = layoutIncrement(),
}), }),
ConfirmationBehavior = e(Setting, { ConfirmationBehavior = e(Setting, {
@ -112,7 +118,7 @@ function SettingsPage:render()
name = "Confirmation Behavior", name = "Confirmation Behavior",
description = "When to prompt for confirmation before syncing", description = "When to prompt for confirmation before syncing",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 3, layoutOrder = layoutIncrement(),
options = confirmationBehaviors, options = confirmationBehaviors,
}), }),
@ -122,7 +128,7 @@ function SettingsPage:render()
name = "Confirmation Threshold", name = "Confirmation Threshold",
description = "How many modified instances to be considered a large change", description = "How many modified instances to be considered a large change",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 4, layoutOrder = layoutIncrement(),
visible = Settings:getBinding("confirmationBehavior"):map(function(value) visible = Settings:getBinding("confirmationBehavior"):map(function(value)
return value == "Large Changes" return value == "Large Changes"
end), end),
@ -153,7 +159,16 @@ function SettingsPage:render()
name = "Play Sounds", name = "Play Sounds",
description = "Toggle sound effects", description = "Toggle sound effects",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 5, layoutOrder = layoutIncrement(),
}),
AutoConnectPlaytestServer = e(Setting, {
id = "autoConnectPlaytestServer",
name = "Auto Connect Playtest Server",
description = "Automatically connect game server to Rojo when playtesting while connected in Edit",
experimental = true,
transparency = self.props.transparency,
layoutOrder = layoutIncrement(),
}), }),
Permissions = e(Setting, { Permissions = e(Setting, {
@ -203,7 +218,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 = 7, layoutOrder = layoutIncrement(),
}), }),
TwoWaySync = e(Setting, { TwoWaySync = e(Setting, {
@ -213,7 +228,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 = 8, layoutOrder = layoutIncrement(),
}), }),
LogLevel = e(Setting, { LogLevel = e(Setting, {
@ -221,7 +236,7 @@ function SettingsPage:render()
name = "Log Level", name = "Log Level",
description = "Plugin output verbosity level", description = "Plugin output verbosity level",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 100, layoutOrder = layoutIncrement(),
options = invertedLevels, options = invertedLevels,
showReset = Settings:getBinding("logLevel"):map(function(value) showReset = Settings:getBinding("logLevel"):map(function(value)
@ -237,7 +252,7 @@ function SettingsPage:render()
name = "Typechecking", name = "Typechecking",
description = "Toggle typechecking on the API surface", description = "Toggle typechecking on the API surface",
transparency = self.props.transparency, transparency = self.props.transparency,
layoutOrder = 101, layoutOrder = layoutIncrement(),
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {

View file

@ -214,11 +214,29 @@ function App:init()
}, },
}) })
end end
if self:isAutoConnectPlaytestServerAvailable() then
self:useRunningConnectionInfo()
self:startSession()
end
self.autoConnectPlaytestServerListener = Settings:onChanged("autoConnectPlaytestServer", function(enabled)
if enabled then
if self:isAutoConnectPlaytestServerWriteable() and self.serveSession ~= nil then
-- Write the existing session
local baseUrl = self.serveSession.__apiContext.__baseUrl
self:setRunningConnectionInfo(baseUrl)
end
else
self:clearRunningConnectionInfo()
end
end)
end end
function App:willUnmount() function App:willUnmount()
self.waypointConnection:Disconnect() self.waypointConnection:Disconnect()
self.confirmationBindable:Destroy() self.confirmationBindable:Destroy()
self.autoConnectPlaytestServerListener()
self:clearRunningConnectionInfo()
end end
function App:addNotification( function App:addNotification(
@ -368,10 +386,7 @@ function App:getHostAndPort(): (string, string)
local host = self.host:getValue() local host = self.host:getValue()
local port = self.port:getValue() local port = self.port:getValue()
local host = if #host > 0 then host else Config.defaultHost return if #host > 0 then host else Config.defaultHost, if #port > 0 then port else Config.defaultPort
local port = if #port > 0 then port else Config.defaultPort
return host, port
end end
function App:isSyncLockAvailable() function App:isSyncLockAvailable()
@ -493,6 +508,49 @@ function App:requestPermission(
return response return response
end end
function App:isAutoConnectPlaytestServerAvailable()
return RunService:IsRunMode()
and RunService:IsServer()
and Settings:get("autoConnectPlaytestServer")
and workspace:GetAttribute("__Rojo_ConnectionUrl")
end
function App:isAutoConnectPlaytestServerWriteable()
return RunService:IsEdit() and Settings:get("autoConnectPlaytestServer")
end
function App:setRunningConnectionInfo(baseUrl: string)
if not self:isAutoConnectPlaytestServerWriteable() then
return
end
Log.trace("Setting connection info for play solo auto-connect")
workspace:SetAttribute("__Rojo_ConnectionUrl", baseUrl)
end
function App:clearRunningConnectionInfo()
if not RunService:IsEdit() then
-- Only write connection info from edit mode
return
end
Log.trace("Clearing connection info for play solo auto-connect")
workspace:SetAttribute("__Rojo_ConnectionUrl", nil)
end
function App:useRunningConnectionInfo()
local connectionInfo = workspace:GetAttribute("__Rojo_ConnectionUrl")
if not connectionInfo then
return
end
Log.trace("Using connection info for play solo auto-connect")
local host, port = string.match(connectionInfo, "^(.+):(.-)$")
self.setHost(host)
self.setPort(port)
end
function App:startSession(host: string?, port: string?) function App:startSession(host: string?, port: string?)
local claimedLock, priorOwner = self:claimSyncLock() local claimedLock, priorOwner = self:claimSyncLock()
if not claimedLock then if not claimedLock then
@ -592,6 +650,7 @@ function App:startSession(host: string?, port: string?)
self.headlessAPI:_updateProperty("ProjectName", details) self.headlessAPI:_updateProperty("ProjectName", details)
self.knownProjects[details] = true self.knownProjects[details] = true
self:setRunningConnectionInfo(baseUrl)
self:setState({ self:setState({
appStatus = AppStatus.Connected, appStatus = AppStatus.Connected,
@ -603,6 +662,7 @@ function App:startSession(host: string?, port: string?)
elseif status == ServeSession.Status.Disconnected then elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil self.serveSession = nil
self:releaseSyncLock() self:releaseSyncLock()
self:clearRunningConnectionInfo()
self:setState({ self:setState({
patchData = { patchData = {
patch = PatchSet.newEmpty(), patch = PatchSet.newEmpty(),
@ -644,6 +704,12 @@ function App:startSession(host: string?, port: string?)
return "Accept" return "Accept"
end end
-- Play solo auto-connect does not require confirmation
if self:isAutoConnectPlaytestServerAvailable() then
Log.trace("Accepting patch without confirmation because play solo auto-connect is enabled")
return "Accept"
end
local confirmationBehavior = Settings:get("confirmationBehavior") local confirmationBehavior = Settings:get("confirmationBehavior")
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
@ -779,7 +845,7 @@ function App:render()
value = self.props.plugin, value = self.props.plugin,
}, { }, {
e(Theme.StudioProvider, nil, { e(Theme.StudioProvider, nil, {
e(Tooltip.Provider, nil, { tooltip = e(Tooltip.Provider, nil, {
popups = Roact.createFragment(popups), popups = Roact.createFragment(popups),
gui = e(StudioPluginGui, { gui = e(StudioPluginGui, {

View file

@ -113,27 +113,29 @@ end
function InstanceMap:destroyInstance(instance) function InstanceMap:destroyInstance(instance)
local id = self.fromInstances[instance] local id = self.fromInstances[instance]
local descendants = instance:GetDescendants()
instance:Destroy()
-- After the instance is successfully destroyed,
-- we can remove all the id mappings
if id ~= nil then if id ~= nil then
self:removeId(id) self:removeId(id)
end end
for _, descendantInstance in ipairs(instance:GetDescendants()) do for _, descendantInstance in descendants do
self:removeInstance(descendantInstance) self:removeInstance(descendantInstance)
end end
instance:Destroy()
end end
function InstanceMap:destroyId(id) function InstanceMap:destroyId(id)
local instance = self.fromIds[id] local instance = self.fromIds[id]
self:removeId(id)
if instance ~= nil then if instance ~= nil then
for _, descendantInstance in ipairs(instance:GetDescendants()) do self:destroyInstance(instance)
self:removeInstance(descendantInstance) else
end -- There is no instance with this id, so we can just remove the id
-- without worrying about instance destruction
instance:Destroy() self:removeId(id)
end end
end end

View file

@ -426,22 +426,71 @@ function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
-- Update isWarning metadata -- Update isWarning metadata
for _, failedChange in unappliedPatch.updated do for _, failedChange in unappliedPatch.updated do
local node = tree:getNode(failedChange.id) local node = tree:getNode(failedChange.id)
if node then if not node then
node.isWarning = true continue
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if node.changeList then
for _, change in node.changeList do
if failedChange.changedProperties[change[1]] then
Log.trace(" Marked property as warning: {}", change[1])
if change[4] == nil then
change[4] = {}
end
change[4].isWarning = true
end
end
end
end end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if not node.changeList then
continue
end
for _, change in node.changeList do
local property = change[1]
local propertyFailedToApply = if property == "Name"
then failedChange.changedName ~= nil -- Name is not in changedProperties, so it needs a special case
else failedChange.changedProperties[property] ~= nil
if not propertyFailedToApply then
-- This change didn't fail, no need to mark
continue
end
if change[4] == nil then
change[4] = { isWarning = true }
else
change[4].isWarning = true
end
Log.trace(" Marked property as warning: {}.{}", node.name, property)
end
end
for failedAdditionId in unappliedPatch.added do
local node = tree:getNode(failedAdditionId)
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
if not node.changeList then
continue
end
for _, change in node.changeList do
-- Failed addition means that all properties failed to be added
if change[4] == nil then
change[4] = { isWarning = true }
else
change[4].isWarning = true
end
Log.trace(" Marked property as warning: {}.{}", node.name, change[1])
end
end
for _, failedRemovalIdOrInstance in unappliedPatch.removed do
local failedRemovalId = if Types.RbxId(failedRemovalIdOrInstance)
then failedRemovalIdOrInstance
else instanceMap.fromInstances[failedRemovalIdOrInstance]
if not failedRemovalId then
continue
end
local node = tree:getNode(failedRemovalId)
if not node then
continue
end
node.isWarning = true
Log.trace("Marked node as warning: {} {}", node.id, node.name)
end end
-- Update if instances exist -- Update if instances exist

View file

@ -25,10 +25,15 @@ local function applyPatch(instanceMap, patch)
local unappliedPatch = PatchSet.newEmpty() local unappliedPatch = PatchSet.newEmpty()
for _, removedIdOrInstance in ipairs(patch.removed) do for _, removedIdOrInstance in ipairs(patch.removed) do
if Types.RbxId(removedIdOrInstance) then local ok = pcall(function()
instanceMap:destroyId(removedIdOrInstance) if Types.RbxId(removedIdOrInstance) then
else instanceMap:destroyId(removedIdOrInstance)
instanceMap:destroyInstance(removedIdOrInstance) else
instanceMap:destroyInstance(removedIdOrInstance)
end
end)
if not ok then
table.insert(unappliedPatch.removed, removedIdOrInstance)
end end
end end
@ -170,7 +175,13 @@ local function applyPatch(instanceMap, patch)
end end
if update.changedName ~= nil then if update.changedName ~= nil then
instance.Name = update.changedName local ok = pcall(function()
instance.Name = update.changedName
end)
if not ok then
unappliedUpdate.changedName = update.changedName
partiallyApplied = true
end
end end
if update.changedMetadata ~= nil then if update.changedMetadata ~= nil then

View file

@ -14,6 +14,7 @@ local defaultSettings = {
twoWaySync = false, twoWaySync = false,
showNotifications = true, showNotifications = true,
syncReminder = true, syncReminder = true,
autoConnectPlaytestServer = false,
confirmationBehavior = "Initial", confirmationBehavior = "Initial",
largeChangesConfirmationThreshold = 5, largeChangesConfirmationThreshold = 5,
playSounds = true, playSounds = true,