mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Merge branch 'master' of https://github.com/rojo-rbx/rojo into headless-api
This commit is contained in:
commit
9397dbaf78
7 changed files with 191 additions and 45 deletions
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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", {
|
||||||
|
|
|
||||||
|
|
@ -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, {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue