mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-21 20:21:38 +00:00
When enabled, the `baseurl` of the session is written to
`workspace:SetAttribute("__Rojo_ConnectionUrl")` so that the test server
can connect to that session automatically.
This works for Play Solo and Local Test Server. It is marked
experimental for now (and disabled by default) since connecting during a
playtest session is... not polished. Rojo may overwrite things and cause
headaches. Further work can be done later.
875 lines
23 KiB
Lua
875 lines
23 KiB
Lua
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
local Players = game:GetService("Players")
|
|
local ServerStorage = game:GetService("ServerStorage")
|
|
local RunService = game:GetService("RunService")
|
|
|
|
local Rojo = script:FindFirstAncestor("Rojo")
|
|
local Plugin = Rojo.Plugin
|
|
local Packages = Rojo.Packages
|
|
|
|
local Roact = require(Packages.Roact)
|
|
local Log = require(Packages.Log)
|
|
|
|
local Assets = require(Plugin.Assets)
|
|
local Version = require(Plugin.Version)
|
|
local Config = require(Plugin.Config)
|
|
local Settings = require(Plugin.Settings)
|
|
local strict = require(Plugin.strict)
|
|
local Dictionary = require(Plugin.Dictionary)
|
|
local ServeSession = require(Plugin.ServeSession)
|
|
local ApiContext = require(Plugin.ApiContext)
|
|
local PatchSet = require(Plugin.PatchSet)
|
|
local PatchTree = require(Plugin.PatchTree)
|
|
local preloadAssets = require(Plugin.preloadAssets)
|
|
local soundPlayer = require(Plugin.soundPlayer)
|
|
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
|
|
local Theme = require(script.Theme)
|
|
|
|
local Page = require(script.Page)
|
|
local Notifications = require(script.Notifications)
|
|
local Tooltip = require(script.Components.Tooltip)
|
|
local StudioPluginAction = require(script.Components.Studio.StudioPluginAction)
|
|
local StudioToolbar = require(script.Components.Studio.StudioToolbar)
|
|
local StudioToggleButton = require(script.Components.Studio.StudioToggleButton)
|
|
local StudioPluginGui = require(script.Components.Studio.StudioPluginGui)
|
|
local StudioPluginContext = require(script.Components.Studio.StudioPluginContext)
|
|
local StatusPages = require(script.StatusPages)
|
|
|
|
local AppStatus = strict("AppStatus", {
|
|
NotConnected = "NotConnected",
|
|
Settings = "Settings",
|
|
Connecting = "Connecting",
|
|
Confirming = "Confirming",
|
|
Connected = "Connected",
|
|
Error = "Error",
|
|
})
|
|
|
|
local e = Roact.createElement
|
|
|
|
local App = Roact.Component:extend("App")
|
|
|
|
function App:init()
|
|
preloadAssets()
|
|
|
|
local priorHost, priorPort = self:getPriorEndpoint()
|
|
self.host, self.setHost = Roact.createBinding(priorHost or "")
|
|
self.port, self.setPort = Roact.createBinding(priorPort or "")
|
|
|
|
self.confirmationBindable = Instance.new("BindableEvent")
|
|
self.confirmationEvent = self.confirmationBindable.Event
|
|
self.knownProjects = {}
|
|
self.notifId = 0
|
|
|
|
self.waypointConnection = ChangeHistoryService.OnUndo:Connect(function(action: string)
|
|
if not string.find(action, "^Rojo: Patch") then
|
|
return
|
|
end
|
|
|
|
local undoConnection, redoConnection = nil, nil
|
|
local function cleanup()
|
|
undoConnection:Disconnect()
|
|
redoConnection:Disconnect()
|
|
end
|
|
|
|
Log.warn(
|
|
string.format(
|
|
"You've undone '%s'.\nIf this was not intended, please Redo in the topbar or with Ctrl/⌘+Y.",
|
|
action
|
|
)
|
|
)
|
|
local dismissNotif = self:addNotification(
|
|
string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
|
|
10,
|
|
{
|
|
Restore = {
|
|
text = "Restore",
|
|
style = "Solid",
|
|
layoutOrder = 1,
|
|
onClick = function(notification)
|
|
cleanup()
|
|
notification:dismiss()
|
|
ChangeHistoryService:Redo()
|
|
end,
|
|
},
|
|
Dismiss = {
|
|
text = "Dismiss",
|
|
style = "Bordered",
|
|
layoutOrder = 2,
|
|
onClick = function(notification)
|
|
cleanup()
|
|
notification:dismiss()
|
|
end,
|
|
},
|
|
}
|
|
)
|
|
|
|
undoConnection = ChangeHistoryService.OnUndo:Once(function()
|
|
-- Our notif is now out of date- redoing will not restore the patch
|
|
-- since we've undone even further. Dismiss the notif.
|
|
cleanup()
|
|
dismissNotif()
|
|
end)
|
|
redoConnection = ChangeHistoryService.OnRedo:Once(function(redoneAction: string)
|
|
if redoneAction == action then
|
|
-- The user has restored the patch, so we can dismiss the notif
|
|
cleanup()
|
|
dismissNotif()
|
|
end
|
|
end)
|
|
end)
|
|
|
|
self:setState({
|
|
appStatus = AppStatus.NotConnected,
|
|
guiEnabled = false,
|
|
confirmData = {},
|
|
patchData = {
|
|
patch = PatchSet.newEmpty(),
|
|
unapplied = PatchSet.newEmpty(),
|
|
timestamp = os.time(),
|
|
},
|
|
notifications = {},
|
|
toolbarIcon = Assets.Images.PluginButton,
|
|
})
|
|
|
|
if
|
|
RunService:IsEdit()
|
|
and self.serveSession == nil
|
|
and Settings:get("syncReminder")
|
|
and self:getLastSyncTimestamp()
|
|
and (self:isSyncLockAvailable())
|
|
then
|
|
self:addNotification("You've previously synced this place. Would you like to reconnect?", 300, {
|
|
Connect = {
|
|
text = "Connect",
|
|
style = "Solid",
|
|
layoutOrder = 1,
|
|
onClick = function(notification)
|
|
notification:dismiss()
|
|
self:startSession()
|
|
end,
|
|
},
|
|
Dismiss = {
|
|
text = "Dismiss",
|
|
style = "Bordered",
|
|
layoutOrder = 2,
|
|
onClick = function(notification)
|
|
notification:dismiss()
|
|
end,
|
|
},
|
|
})
|
|
end
|
|
|
|
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
|
|
|
|
function App:willUnmount()
|
|
self.waypointConnection:Disconnect()
|
|
self.confirmationBindable:Destroy()
|
|
self.autoConnectPlaytestServerListener()
|
|
self:clearRunningConnectionInfo()
|
|
end
|
|
|
|
function App:addNotification(
|
|
text: string,
|
|
timeout: number?,
|
|
actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
|
|
)
|
|
if not Settings:get("showNotifications") then
|
|
return
|
|
end
|
|
|
|
self.notifId += 1
|
|
local id = self.notifId
|
|
|
|
local notifications = table.clone(self.state.notifications)
|
|
notifications[id] = {
|
|
text = text,
|
|
timestamp = DateTime.now().UnixTimestampMillis,
|
|
timeout = timeout or 3,
|
|
actions = actions,
|
|
}
|
|
|
|
self:setState({
|
|
notifications = notifications,
|
|
})
|
|
|
|
return function()
|
|
self:closeNotification(id)
|
|
end
|
|
end
|
|
|
|
function App:closeNotification(id: number)
|
|
if not self.state.notifications[id] then
|
|
return
|
|
end
|
|
|
|
local notifications = table.clone(self.state.notifications)
|
|
notifications[id] = nil
|
|
|
|
self:setState({
|
|
notifications = notifications,
|
|
})
|
|
end
|
|
|
|
function App:getPriorEndpoint()
|
|
local priorEndpoints = Settings:get("priorEndpoints")
|
|
if not priorEndpoints then
|
|
return
|
|
end
|
|
|
|
local id = tostring(game.PlaceId)
|
|
if ignorePlaceIds[id] then
|
|
return
|
|
end
|
|
|
|
local place = priorEndpoints[id]
|
|
if not place then
|
|
return
|
|
end
|
|
|
|
return place.host, place.port
|
|
end
|
|
|
|
function App:getLastSyncTimestamp()
|
|
local priorEndpoints = Settings:get("priorEndpoints")
|
|
if not priorEndpoints then
|
|
return
|
|
end
|
|
|
|
local id = tostring(game.PlaceId)
|
|
if ignorePlaceIds[id] then
|
|
return
|
|
end
|
|
|
|
local place = priorEndpoints[id]
|
|
if not place then
|
|
return
|
|
end
|
|
|
|
return place.timestamp
|
|
end
|
|
|
|
function App:setPriorEndpoint(host: string, port: string)
|
|
local priorEndpoints = Settings:get("priorEndpoints")
|
|
if not priorEndpoints then
|
|
priorEndpoints = {}
|
|
end
|
|
|
|
-- Clear any stale saves to avoid disc bloat
|
|
for placeId, endpoint in priorEndpoints do
|
|
if os.time() - endpoint.timestamp > 12_960_000 then
|
|
priorEndpoints[placeId] = nil
|
|
Log.trace("Cleared stale saved endpoint for {}", placeId)
|
|
end
|
|
end
|
|
|
|
local id = tostring(game.PlaceId)
|
|
if ignorePlaceIds[id] then
|
|
return
|
|
end
|
|
|
|
priorEndpoints[id] = {
|
|
host = if host ~= Config.defaultHost then host else nil,
|
|
port = if port ~= Config.defaultPort then port else nil,
|
|
timestamp = os.time(),
|
|
}
|
|
Log.trace("Saved last used endpoint for {}", game.PlaceId)
|
|
|
|
Settings:set("priorEndpoints", priorEndpoints)
|
|
end
|
|
|
|
function App:getHostAndPort()
|
|
local host = self.host:getValue()
|
|
local port = self.port:getValue()
|
|
|
|
return if #host > 0 then host else Config.defaultHost, if #port > 0 then port else Config.defaultPort
|
|
end
|
|
|
|
function App:isSyncLockAvailable()
|
|
if #Players:GetPlayers() == 0 then
|
|
-- Team Create is not active, so no one can be holding the lock
|
|
return true
|
|
end
|
|
|
|
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
|
if not lock then
|
|
-- No lock is made yet, so it is available
|
|
return true
|
|
end
|
|
|
|
if lock.Value and lock.Value ~= Players.LocalPlayer and lock.Value.Parent then
|
|
-- Someone else is holding the lock
|
|
return false, lock.Value
|
|
end
|
|
|
|
-- The lock exists, but is not claimed
|
|
return true
|
|
end
|
|
|
|
function App:claimSyncLock()
|
|
if #Players:GetPlayers() == 0 then
|
|
Log.trace("Skipping sync lock because this isn't in Team Create")
|
|
return true
|
|
end
|
|
|
|
local isAvailable, priorOwner = self:isSyncLockAvailable()
|
|
if not isAvailable then
|
|
Log.trace("Skipping sync lock because it is already claimed")
|
|
return false, priorOwner
|
|
end
|
|
|
|
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
|
if not lock then
|
|
lock = Instance.new("ObjectValue")
|
|
lock.Name = "__Rojo_SessionLock"
|
|
lock.Archivable = false
|
|
lock.Value = Players.LocalPlayer
|
|
lock.Parent = ServerStorage
|
|
Log.trace("Created and claimed sync lock")
|
|
return true
|
|
end
|
|
|
|
lock.Value = Players.LocalPlayer
|
|
Log.trace("Claimed existing sync lock")
|
|
return true
|
|
end
|
|
|
|
function App:releaseSyncLock()
|
|
local lock = ServerStorage:FindFirstChild("__Rojo_SessionLock")
|
|
if not lock then
|
|
Log.trace("No sync lock found, assumed released")
|
|
return
|
|
end
|
|
|
|
if lock.Value == Players.LocalPlayer then
|
|
lock.Value = nil
|
|
Log.trace("Released sync lock")
|
|
return
|
|
end
|
|
|
|
Log.trace("Could not relase sync lock because it is owned by {}", lock.Value)
|
|
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()
|
|
local claimedLock, priorOwner = self:claimSyncLock()
|
|
if not claimedLock then
|
|
local msg = string.format("Could not sync because user '%s' is already syncing", tostring(priorOwner))
|
|
|
|
Log.warn(msg)
|
|
self:addNotification(msg, 10)
|
|
self:setState({
|
|
appStatus = AppStatus.Error,
|
|
errorMessage = msg,
|
|
toolbarIcon = Assets.Images.PluginButtonWarning,
|
|
})
|
|
|
|
return
|
|
end
|
|
|
|
local host, port = self:getHostAndPort()
|
|
|
|
local sessionOptions = {
|
|
openScriptsExternally = Settings:get("openScriptsExternally"),
|
|
twoWaySync = Settings:get("twoWaySync"),
|
|
}
|
|
|
|
local baseUrl = if string.find(host, "^https?://")
|
|
then string.format("%s:%s", host, port)
|
|
else string.format("http://%s:%s", host, port)
|
|
local apiContext = ApiContext.new(baseUrl)
|
|
|
|
local serveSession = ServeSession.new({
|
|
apiContext = apiContext,
|
|
openScriptsExternally = sessionOptions.openScriptsExternally,
|
|
twoWaySync = sessionOptions.twoWaySync,
|
|
})
|
|
|
|
self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
|
|
-- Build new tree for patch
|
|
self:setState({
|
|
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
|
|
})
|
|
end)
|
|
self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
|
|
-- Update tree with unapplied metadata
|
|
self:setState(function(prevState)
|
|
return {
|
|
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
|
|
}
|
|
end)
|
|
end)
|
|
|
|
serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
|
|
local now = os.time()
|
|
local old = self.state.patchData
|
|
|
|
if PatchSet.isEmpty(patch) then
|
|
-- Ignore empty patch, but update timestamp
|
|
self:setState({
|
|
patchData = {
|
|
patch = old.patch,
|
|
unapplied = old.unapplied,
|
|
timestamp = now,
|
|
},
|
|
})
|
|
return
|
|
end
|
|
|
|
if now - old.timestamp < 2 then
|
|
-- Patches that apply in the same second are
|
|
-- considered to be part of the same change for human clarity
|
|
patch = PatchSet.assign(PatchSet.newEmpty(), old.patch, patch)
|
|
unapplied = PatchSet.assign(PatchSet.newEmpty(), old.unapplied, unapplied)
|
|
end
|
|
|
|
self:setState({
|
|
patchData = {
|
|
patch = patch,
|
|
unapplied = unapplied,
|
|
timestamp = now,
|
|
},
|
|
})
|
|
end)
|
|
|
|
serveSession:onStatusChanged(function(status, details)
|
|
if status == ServeSession.Status.Connecting then
|
|
self:setPriorEndpoint(host, port)
|
|
|
|
self:setState({
|
|
appStatus = AppStatus.Connecting,
|
|
toolbarIcon = Assets.Images.PluginButton,
|
|
})
|
|
self:addNotification("Connecting to session...")
|
|
elseif status == ServeSession.Status.Connected then
|
|
self.knownProjects[details] = true
|
|
self:setRunningConnectionInfo(baseUrl)
|
|
|
|
local address = ("%s:%s"):format(host, port)
|
|
self:setState({
|
|
appStatus = AppStatus.Connected,
|
|
projectName = details,
|
|
address = address,
|
|
toolbarIcon = Assets.Images.PluginButtonConnected,
|
|
})
|
|
self:addNotification(string.format("Connected to session '%s' at %s.", details, address), 5)
|
|
elseif status == ServeSession.Status.Disconnected then
|
|
self.serveSession = nil
|
|
self:releaseSyncLock()
|
|
self:clearRunningConnectionInfo()
|
|
self:setState({
|
|
patchData = {
|
|
patch = PatchSet.newEmpty(),
|
|
unapplied = PatchSet.newEmpty(),
|
|
timestamp = os.time(),
|
|
},
|
|
})
|
|
|
|
-- Details being present indicates that this
|
|
-- disconnection was from an error.
|
|
if details ~= nil then
|
|
Log.warn("Disconnected from an error: {}", details)
|
|
|
|
self:setState({
|
|
appStatus = AppStatus.Error,
|
|
errorMessage = tostring(details),
|
|
toolbarIcon = Assets.Images.PluginButtonWarning,
|
|
})
|
|
self:addNotification(tostring(details), 10)
|
|
else
|
|
self:setState({
|
|
appStatus = AppStatus.NotConnected,
|
|
toolbarIcon = Assets.Images.PluginButton,
|
|
})
|
|
self:addNotification("Disconnected from session.")
|
|
end
|
|
end
|
|
end)
|
|
|
|
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
|
|
if PatchSet.isEmpty(patch) then
|
|
Log.trace("Accepting patch without confirmation because it is empty")
|
|
return "Accept"
|
|
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")
|
|
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
|
|
-- and unnecessary. This special case allows it to be accepted without confirmation.
|
|
if
|
|
PatchSet.hasAdditions(patch) == false
|
|
and PatchSet.hasRemoves(patch) == false
|
|
and PatchSet.containsOnlyInstance(patch, instanceMap, game)
|
|
then
|
|
local datamodelUpdates = PatchSet.getUpdateForInstance(patch, instanceMap, game)
|
|
if
|
|
datamodelUpdates ~= nil
|
|
and next(datamodelUpdates.changedProperties) == nil
|
|
and datamodelUpdates.changedClassName == nil
|
|
then
|
|
Log.trace("Accepting patch without confirmation because it only contains a datamodel name change")
|
|
return "Accept"
|
|
end
|
|
end
|
|
|
|
self:setState({
|
|
appStatus = AppStatus.Confirming,
|
|
confirmData = {
|
|
instanceMap = instanceMap,
|
|
patch = patch,
|
|
serverInfo = serverInfo,
|
|
},
|
|
toolbarIcon = Assets.Images.PluginButton,
|
|
})
|
|
|
|
self:addNotification(
|
|
string.format(
|
|
"Please accept%sor abort the initializing sync session.",
|
|
Settings:get("twoWaySync") and ", reject, " or " "
|
|
),
|
|
7
|
|
)
|
|
|
|
return self.confirmationEvent:Wait()
|
|
end)
|
|
|
|
serveSession:start()
|
|
|
|
self.serveSession = serveSession
|
|
end
|
|
|
|
function App:endSession()
|
|
if self.serveSession == nil then
|
|
return
|
|
end
|
|
|
|
Log.trace("Disconnecting session")
|
|
|
|
self.serveSession:stop()
|
|
self.serveSession = nil
|
|
self:setState({
|
|
appStatus = AppStatus.NotConnected,
|
|
})
|
|
|
|
if self.cleanupPrecommit ~= nil then
|
|
self.cleanupPrecommit()
|
|
end
|
|
if self.cleanupPostcommit ~= nil then
|
|
self.cleanupPostcommit()
|
|
end
|
|
|
|
Log.trace("Session terminated by user")
|
|
end
|
|
|
|
function App:render()
|
|
local pluginName = "Rojo " .. Version.display(Config.version)
|
|
|
|
local function createPageElement(appStatus, additionalProps)
|
|
additionalProps = additionalProps or {}
|
|
|
|
local props = Dictionary.merge(additionalProps, {
|
|
component = StatusPages[appStatus],
|
|
active = self.state.appStatus == appStatus,
|
|
})
|
|
|
|
return e(Page, props)
|
|
end
|
|
|
|
return e(StudioPluginContext.Provider, {
|
|
value = self.props.plugin,
|
|
}, {
|
|
e(Theme.StudioProvider, nil, {
|
|
tooltip = e(Tooltip.Provider, nil, {
|
|
gui = e(StudioPluginGui, {
|
|
id = pluginName,
|
|
title = pluginName,
|
|
active = self.state.guiEnabled,
|
|
isEphemeral = false,
|
|
|
|
initDockState = Enum.InitialDockState.Right,
|
|
overridePreviousState = false,
|
|
floatingSize = Vector2.new(320, 210),
|
|
minimumSize = Vector2.new(300, 210),
|
|
|
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
|
|
|
onInitialState = function(initialState)
|
|
self:setState({
|
|
guiEnabled = initialState,
|
|
})
|
|
end,
|
|
|
|
onClose = function()
|
|
self:setState({
|
|
guiEnabled = false,
|
|
})
|
|
end,
|
|
}, {
|
|
Tooltips = e(Tooltip.Container, nil),
|
|
|
|
NotConnectedPage = createPageElement(AppStatus.NotConnected, {
|
|
host = self.host,
|
|
onHostChange = self.setHost,
|
|
port = self.port,
|
|
onPortChange = self.setPort,
|
|
|
|
onConnect = function()
|
|
self:startSession()
|
|
end,
|
|
|
|
onNavigateSettings = function()
|
|
self.backPage = AppStatus.NotConnected
|
|
self:setState({
|
|
appStatus = AppStatus.Settings,
|
|
})
|
|
end,
|
|
}),
|
|
|
|
ConfirmingPage = createPageElement(AppStatus.Confirming, {
|
|
confirmData = self.state.confirmData,
|
|
createPopup = not self.state.guiEnabled,
|
|
|
|
onAbort = function()
|
|
self.confirmationBindable:Fire("Abort")
|
|
end,
|
|
onAccept = function()
|
|
self.confirmationBindable:Fire("Accept")
|
|
end,
|
|
onReject = function()
|
|
self.confirmationBindable:Fire("Reject")
|
|
end,
|
|
}),
|
|
|
|
Connecting = createPageElement(AppStatus.Connecting),
|
|
|
|
Connected = createPageElement(AppStatus.Connected, {
|
|
projectName = self.state.projectName,
|
|
address = self.state.address,
|
|
patchTree = self.state.patchTree,
|
|
patchData = self.state.patchData,
|
|
serveSession = self.serveSession,
|
|
|
|
onDisconnect = function()
|
|
self:endSession()
|
|
end,
|
|
|
|
onNavigateSettings = function()
|
|
self.backPage = AppStatus.Connected
|
|
self:setState({
|
|
appStatus = AppStatus.Settings,
|
|
})
|
|
end,
|
|
}),
|
|
|
|
Settings = createPageElement(AppStatus.Settings, {
|
|
syncActive = self.serveSession ~= nil
|
|
and self.serveSession:getStatus() == ServeSession.Status.Connected,
|
|
|
|
onBack = function()
|
|
self:setState({
|
|
appStatus = self.backPage or AppStatus.NotConnected,
|
|
})
|
|
end,
|
|
}),
|
|
|
|
Error = createPageElement(AppStatus.Error, {
|
|
errorMessage = self.state.errorMessage,
|
|
|
|
onClose = function()
|
|
self:setState({
|
|
appStatus = AppStatus.NotConnected,
|
|
toolbarIcon = Assets.Images.PluginButton,
|
|
})
|
|
end,
|
|
}),
|
|
}),
|
|
|
|
RojoNotifications = e("ScreenGui", {
|
|
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
|
ResetOnSpawn = false,
|
|
DisplayOrder = 100,
|
|
}, {
|
|
layout = e("UIListLayout", {
|
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
|
VerticalAlignment = Enum.VerticalAlignment.Bottom,
|
|
Padding = UDim.new(0, 5),
|
|
}),
|
|
padding = e("UIPadding", {
|
|
PaddingTop = UDim.new(0, 5),
|
|
PaddingBottom = UDim.new(0, 5),
|
|
PaddingLeft = UDim.new(0, 5),
|
|
PaddingRight = UDim.new(0, 5),
|
|
}),
|
|
notifs = e(Notifications, {
|
|
soundPlayer = self.props.soundPlayer,
|
|
notifications = self.state.notifications,
|
|
onClose = function(id)
|
|
self:closeNotification(id)
|
|
end,
|
|
}),
|
|
}),
|
|
}),
|
|
|
|
toggleAction = e(StudioPluginAction, {
|
|
name = "RojoConnection",
|
|
title = "Rojo: Connect/Disconnect",
|
|
description = "Toggles the server for a Rojo sync session",
|
|
icon = Assets.Images.PluginButton,
|
|
bindable = true,
|
|
onTriggered = function()
|
|
if self.serveSession == nil or self.serveSession:getStatus() == ServeSession.Status.NotStarted then
|
|
self:startSession()
|
|
elseif
|
|
self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected
|
|
then
|
|
self:endSession()
|
|
end
|
|
end,
|
|
}),
|
|
|
|
connectAction = e(StudioPluginAction, {
|
|
name = "RojoConnect",
|
|
title = "Rojo: Connect",
|
|
description = "Connects the server for a Rojo sync session",
|
|
icon = Assets.Images.PluginButton,
|
|
bindable = true,
|
|
onTriggered = function()
|
|
if self.serveSession == nil or self.serveSession:getStatus() == ServeSession.Status.NotStarted then
|
|
self:startSession()
|
|
end
|
|
end,
|
|
}),
|
|
|
|
disconnectAction = e(StudioPluginAction, {
|
|
name = "RojoDisconnect",
|
|
title = "Rojo: Disconnect",
|
|
description = "Disconnects the server for a Rojo sync session",
|
|
icon = Assets.Images.PluginButton,
|
|
bindable = true,
|
|
onTriggered = function()
|
|
if self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected then
|
|
self:endSession()
|
|
end
|
|
end,
|
|
}),
|
|
|
|
toolbar = e(StudioToolbar, {
|
|
name = pluginName,
|
|
}, {
|
|
button = e(StudioToggleButton, {
|
|
name = "Rojo",
|
|
tooltip = "Show or hide the Rojo panel",
|
|
icon = self.state.toolbarIcon,
|
|
active = self.state.guiEnabled,
|
|
enabled = true,
|
|
onClick = function()
|
|
self:setState(function(state)
|
|
return {
|
|
guiEnabled = not state.guiEnabled,
|
|
}
|
|
end)
|
|
end,
|
|
}),
|
|
}),
|
|
}),
|
|
})
|
|
end
|
|
|
|
return function(props)
|
|
local mergedProps = Dictionary.merge(props, {
|
|
soundPlayer = soundPlayer.new(Settings),
|
|
})
|
|
|
|
return e(App, mergedProps)
|
|
end
|