<?xml version="1.0" encoding="utf-8"?>
<roblox version="4">
<Item class="Script" referent="0">
<Properties>
<string name="Name">MCPPlugin</string>
<token name="RunContext">0</token>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.include.RuntimeLib)
local State = TS.import(script, script, "modules", "State")
local UI = TS.import(script, script, "modules", "UI")
local Communication = TS.import(script, script, "modules", "Communication")
UI.init(plugin)
local elements = UI.getElements()
local toolbar = plugin:CreateToolbar("MCP Integration")
local button = toolbar:CreateButton("MCP Server", "Connect to MCP Server for AI Integration", "rbxassetid://10734944444")
elements.connectButton.Activated:Connect(function()
local conn = State.getActiveConnection()
if conn and conn.isActive then
Communication.deactivatePlugin(State.getActiveTabIndex())
else
Communication.activatePlugin(State.getActiveTabIndex())
end
end)
button.Click:Connect(function()
elements.screenGui.Enabled = not elements.screenGui.Enabled
end)
plugin.Unloading:Connect(function()
Communication.deactivateAll()
end)
UI.updateUIState()
Communication.checkForUpdates()
]]></string>
</Properties>
<Item class="Folder" referent="1">
<Properties>
<string name="Name">modules</string>
</Properties>
<Item class="ModuleScript" referent="2">
<Properties>
<string name="Name">Communication</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.include.RuntimeLib)
local _services = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services")
local HttpService = _services.HttpService
local RunService = _services.RunService
local State = TS.import(script, script.Parent, "State")
local Utils = TS.import(script, script.Parent, "Utils")
local UI = TS.import(script, script.Parent, "UI")
local QueryHandlers = TS.import(script, script.Parent, "handlers", "QueryHandlers")
local PropertyHandlers = TS.import(script, script.Parent, "handlers", "PropertyHandlers")
local InstanceHandlers = TS.import(script, script.Parent, "handlers", "InstanceHandlers")
local ScriptHandlers = TS.import(script, script.Parent, "handlers", "ScriptHandlers")
local MetadataHandlers = TS.import(script, script.Parent, "handlers", "MetadataHandlers")
local TestHandlers = TS.import(script, script.Parent, "handlers", "TestHandlers")
local routeMap = {
["/api/file-tree"] = QueryHandlers.getFileTree,
["/api/search-files"] = QueryHandlers.searchFiles,
["/api/place-info"] = QueryHandlers.getPlaceInfo,
["/api/services"] = QueryHandlers.getServices,
["/api/search-objects"] = QueryHandlers.searchObjects,
["/api/instance-properties"] = QueryHandlers.getInstanceProperties,
["/api/instance-children"] = QueryHandlers.getInstanceChildren,
["/api/search-by-property"] = QueryHandlers.searchByProperty,
["/api/class-info"] = QueryHandlers.getClassInfo,
["/api/project-structure"] = QueryHandlers.getProjectStructure,
["/api/set-property"] = PropertyHandlers.setProperty,
["/api/mass-set-property"] = PropertyHandlers.massSetProperty,
["/api/mass-get-property"] = PropertyHandlers.massGetProperty,
["/api/set-calculated-property"] = PropertyHandlers.setCalculatedProperty,
["/api/set-relative-property"] = PropertyHandlers.setRelativeProperty,
["/api/create-object"] = InstanceHandlers.createObject,
["/api/mass-create-objects"] = InstanceHandlers.massCreateObjects,
["/api/mass-create-objects-with-properties"] = InstanceHandlers.massCreateObjectsWithProperties,
["/api/delete-object"] = InstanceHandlers.deleteObject,
["/api/smart-duplicate"] = InstanceHandlers.smartDuplicate,
["/api/mass-duplicate"] = InstanceHandlers.massDuplicate,
["/api/get-script-source"] = ScriptHandlers.getScriptSource,
["/api/set-script-source"] = ScriptHandlers.setScriptSource,
["/api/edit-script-lines"] = ScriptHandlers.editScriptLines,
["/api/insert-script-lines"] = ScriptHandlers.insertScriptLines,
["/api/delete-script-lines"] = ScriptHandlers.deleteScriptLines,
["/api/get-attribute"] = MetadataHandlers.getAttribute,
["/api/set-attribute"] = MetadataHandlers.setAttribute,
["/api/get-attributes"] = MetadataHandlers.getAttributes,
["/api/delete-attribute"] = MetadataHandlers.deleteAttribute,
["/api/get-tags"] = MetadataHandlers.getTags,
["/api/add-tag"] = MetadataHandlers.addTag,
["/api/remove-tag"] = MetadataHandlers.removeTag,
["/api/get-tagged"] = MetadataHandlers.getTagged,
["/api/get-selection"] = MetadataHandlers.getSelection,
["/api/execute-luau"] = MetadataHandlers.executeLuau,
["/api/start-playtest"] = TestHandlers.startPlaytest,
["/api/stop-playtest"] = TestHandlers.stopPlaytest,
["/api/get-playtest-output"] = TestHandlers.getPlaytestOutput,
}
local function processRequest(request)
local endpoint = request.endpoint
local data = request.data or {}
local handler = routeMap[endpoint]
if handler then
return handler(data)
else
return {
error = `Unknown endpoint: {endpoint}`,
}
end
end
local function sendResponse(conn, requestId, responseData)
pcall(function()
HttpService:RequestAsync({
Url = `{conn.serverUrl}/response`,
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
requestId = requestId,
response = responseData,
}),
})
end)
end
local function getConnectionStatus(connIndex)
local conn = State.getConnection(connIndex)
if not conn or not conn.isActive then
return "disconnected"
end
if conn.consecutiveFailures >= conn.maxFailuresBeforeError then
return "error"
end
if conn.lastHttpOk then
return "connected"
end
return "connecting"
end
local function pollForRequests(connIndex)
local conn = State.getConnection(connIndex)
if not conn or not conn.isActive then
return nil
end
if conn.isPolling then
return nil
end
conn.isPolling = true
local success, result = pcall(function()
return HttpService:RequestAsync({
Url = `{conn.serverUrl}/poll`,
Method = "GET",
Headers = {
["Content-Type"] = "application/json",
},
})
end)
conn.isPolling = false
local ui = UI.getElements()
UI.updateTabDot(connIndex)
if success and (result.Success or result.StatusCode == 503) then
conn.consecutiveFailures = 0
conn.currentRetryDelay = 0.5
conn.lastSuccessfulConnection = tick()
local data = HttpService:JSONDecode(result.Body)
local mcpConnected = data.mcpConnected == true
conn.lastHttpOk = true
if connIndex == State.getActiveTabIndex() then
local el = ui
el.step1Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
el.step1Label.Text = "HTTP server (OK)"
local _condition = mcpConnected
if _condition then
local _value = (string.find(el.statusLabel.Text, "Connected"))
_condition = not (_value ~= 0 and _value == _value and _value)
end
if _condition then
el.statusLabel.Text = "Connected"
el.statusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
el.statusPulse.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
el.statusText.Text = "ONLINE"
el.detailStatusLabel.Text = "HTTP: OK MCP: OK"
el.detailStatusLabel.TextColor3 = Color3.fromRGB(34, 197, 94)
el.step2Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
el.step2Label.Text = "MCP bridge (OK)"
el.step3Dot.BackgroundColor3 = Color3.fromRGB(34, 197, 94)
el.step3Label.Text = "Commands (OK)"
conn.mcpWaitStartTime = nil
el.troubleshootLabel.Visible = false
UI.stopPulseAnimation()
elseif not mcpConnected then
el.statusLabel.Text = "Waiting for MCP server"
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.statusText.Text = "WAITING"
el.detailStatusLabel.Text = "HTTP: OK MCP: ..."
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step2Label.Text = "MCP bridge (waiting...)"
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step3Label.Text = "Commands (waiting...)"
if conn.mcpWaitStartTime == nil then
conn.mcpWaitStartTime = tick()
end
local _exp = tick()
local _condition_1 = conn.mcpWaitStartTime
if _condition_1 == nil then
_condition_1 = tick()
end
local elapsed = _exp - _condition_1
el.troubleshootLabel.Visible = elapsed > 8
UI.startPulseAnimation()
end
end
if data.request and mcpConnected then
task.spawn(function()
local ok, response = pcall(function()
return processRequest(data.request)
end)
if ok then
sendResponse(conn, data.requestId, response)
else
sendResponse(conn, data.requestId, {
error = tostring(response),
})
end
end)
end
elseif conn.isActive then
conn.consecutiveFailures += 1
if conn.consecutiveFailures > 1 then
conn.currentRetryDelay = math.min(conn.currentRetryDelay * conn.retryBackoffMultiplier, conn.maxRetryDelay)
end
if connIndex == State.getActiveTabIndex() then
local el = ui
if conn.consecutiveFailures >= conn.maxFailuresBeforeError then
el.statusLabel.Text = "Server unavailable"
el.statusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
el.statusPulse.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
el.statusText.Text = "ERROR"
el.detailStatusLabel.Text = "HTTP: X MCP: X"
el.detailStatusLabel.TextColor3 = Color3.fromRGB(239, 68, 68)
el.step1Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
el.step1Label.Text = "HTTP server (error)"
el.step2Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
el.step2Label.Text = "MCP bridge (error)"
el.step3Dot.BackgroundColor3 = Color3.fromRGB(239, 68, 68)
el.step3Label.Text = "Commands (error)"
conn.mcpWaitStartTime = nil
el.troubleshootLabel.Visible = false
UI.stopPulseAnimation()
elseif conn.consecutiveFailures > 5 then
local waitTime = math.ceil(conn.currentRetryDelay)
el.statusLabel.Text = `Retrying ({waitTime}s)`
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.statusText.Text = "RETRY"
el.detailStatusLabel.Text = "HTTP: ... MCP: ..."
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
el.step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step1Label.Text = "HTTP server (retrying...)"
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step2Label.Text = "MCP bridge (retrying...)"
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step3Label.Text = "Commands (retrying...)"
conn.mcpWaitStartTime = nil
el.troubleshootLabel.Visible = false
UI.startPulseAnimation()
elseif conn.consecutiveFailures > 1 then
el.statusLabel.Text = `Connecting (attempt {conn.consecutiveFailures})`
el.statusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
el.statusIndicator.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.statusPulse.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.statusText.Text = "CONNECTING"
el.detailStatusLabel.Text = "HTTP: ... MCP: ..."
el.detailStatusLabel.TextColor3 = Color3.fromRGB(245, 158, 11)
el.step1Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step1Label.Text = "HTTP server (connecting...)"
el.step2Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step2Label.Text = "MCP bridge (connecting...)"
el.step3Dot.BackgroundColor3 = Color3.fromRGB(245, 158, 11)
el.step3Label.Text = "Commands (connecting...)"
conn.mcpWaitStartTime = nil
el.troubleshootLabel.Visible = false
UI.startPulseAnimation()
end
end
end
end
local function discoverPort()
for offset = 0, 4 do
local port = State.BASE_PORT + offset
local success, result = pcall(function()
return HttpService:RequestAsync({
Url = `http://localhost:{port}/status`,
Method = "GET",
Headers = {
["Content-Type"] = "application/json",
},
})
end)
if success and result.Success then
local ok, data = pcall(function()
return HttpService:JSONDecode(result.Body)
end)
if ok and data.pluginConnected == false then
return port
end
end
end
return nil
end
local function activatePlugin(connIndex)
local _condition = connIndex
if _condition == nil then
_condition = State.getActiveTabIndex()
end
local idx = _condition
local conn = State.getConnection(idx)
if not conn then
return nil
end
local ui = UI.getElements()
conn.isActive = true
conn.consecutiveFailures = 0
conn.currentRetryDelay = 0.5
ui.screenGui.Enabled = true
if idx == State.getActiveTabIndex() then
conn.serverUrl = ui.urlInput.Text
local portStr = string.match(conn.serverUrl, ":(%d+)$")
if portStr ~= 0 and portStr == portStr and portStr ~= "" and portStr then
local _condition_1 = tonumber(portStr)
if _condition_1 == nil then
_condition_1 = conn.port
end
conn.port = _condition_1
end
UI.updateUIState()
end
UI.updateTabDot(idx)
if not conn.heartbeatConnection then
conn.heartbeatConnection = RunService.Heartbeat:Connect(function()
local now = tick()
local currentInterval = if conn.consecutiveFailures > 5 then conn.currentRetryDelay else conn.pollInterval
if now - conn.lastPoll > currentInterval then
conn.lastPoll = now
pollForRequests(idx)
end
end)
end
task.spawn(function()
local discoveredPort = discoverPort()
if discoveredPort ~= nil then
conn.port = discoveredPort
conn.serverUrl = `http://localhost:{discoveredPort}`
if idx == State.getActiveTabIndex() then
ui.urlInput.Text = conn.serverUrl
end
end
pcall(function()
HttpService:RequestAsync({
Url = `{conn.serverUrl}/ready`,
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
pluginReady = true,
timestamp = tick(),
}),
})
end)
end)
end
local function deactivatePlugin(connIndex)
local _condition = connIndex
if _condition == nil then
_condition = State.getActiveTabIndex()
end
local idx = _condition
local conn = State.getConnection(idx)
if not conn then
return nil
end
conn.isActive = false
if idx == State.getActiveTabIndex() then
UI.updateUIState()
end
UI.updateTabDot(idx)
pcall(function()
HttpService:RequestAsync({
Url = `{conn.serverUrl}/disconnect`,
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
timestamp = tick(),
}),
})
end)
if conn.heartbeatConnection then
conn.heartbeatConnection:Disconnect()
conn.heartbeatConnection = nil
end
conn.consecutiveFailures = 0
conn.currentRetryDelay = 0.5
end
local function deactivateAll()
for i = 0, #State.getConnections() - 1 do
if State.getConnections()[i + 1].isActive then
deactivatePlugin(i)
end
end
end
local function checkForUpdates()
task.spawn(function()
local success, result = pcall(function()
return HttpService:RequestAsync({
Url = "https://registry.npmjs.org/robloxstudio-mcp/latest",
Method = "GET",
Headers = {
Accept = "application/json",
},
})
end)
if success and result.Success then
local ok, data = pcall(function()
return HttpService:JSONDecode(result.Body)
end)
local _condition = ok
if _condition then
local _result = data
if _result ~= nil then
_result = _result.version
end
_condition = _result
end
if _condition ~= "" and _condition then
local latestVersion = data.version
if Utils.compareVersions(State.CURRENT_VERSION, latestVersion) < 0 then
local ui = UI.getElements()
ui.updateBannerText.Text = `v{latestVersion} available - github.com/boshyxd/robloxstudio-mcp`
ui.updateBanner.Visible = true
ui.contentFrame.Position = UDim2.new(0, 8, 0, 92)
ui.contentFrame.Size = UDim2.new(1, -16, 1, -100)
end
end
end
end)
end
return {
getConnectionStatus = getConnectionStatus,
activatePlugin = activatePlugin,
deactivatePlugin = deactivatePlugin,
deactivateAll = deactivateAll,
checkForUpdates = checkForUpdates,
}
]]></string>
</Properties>
</Item>
<Item class="Folder" referent="3">
<Properties>
<string name="Name">handlers</string>
</Properties>
<Item class="ModuleScript" referent="4">
<Properties>
<string name="Name">InstanceHandlers</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
local Utils = TS.import(script, script.Parent.Parent, "Utils")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local _binding = Utils
local getInstancePath = _binding.getInstancePath
local getInstanceByPath = _binding.getInstanceByPath
local convertPropertyValue = _binding.convertPropertyValue
local function createObject(requestData)
local className = requestData.className
local parentPath = requestData.parent
local name = requestData.name
local properties = (requestData.properties) or {}
if not (className ~= "" and className) or not (parentPath ~= "" and parentPath) then
return {
error = "Class name and parent are required",
}
end
local parentInstance = getInstanceByPath(parentPath)
if not parentInstance then
return {
error = `Parent instance not found: {parentPath}`,
}
end
local success, newInstance = pcall(function()
local instance = Instance.new(className)
if name ~= "" and name then
instance.Name = name
end
for propertyName, propertyValue in pairs(properties) do
pcall(function()
instance[propertyName] = propertyValue
end)
end
instance.Parent = parentInstance
ChangeHistoryService:SetWaypoint(`Create {className}`)
return instance
end)
if success and newInstance then
return {
success = true,
className = className,
parent = parentPath,
instancePath = getInstancePath(newInstance),
name = newInstance.Name,
message = "Object created successfully",
}
else
return {
error = `Failed to create object: {newInstance}`,
className = className,
parent = parentPath,
}
end
end
local function deleteObject(requestData)
local instancePath = requestData.instancePath
if not (instancePath ~= "" and instancePath) then
return {
error = "Instance path is required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
if instance == game then
return {
error = "Cannot delete the game instance",
}
end
local success, result = pcall(function()
local name = instance.Name
local className = instance.ClassName
instance:Destroy()
ChangeHistoryService:SetWaypoint(`Delete {className} ({name})`)
return true
end)
if success then
return {
success = true,
instancePath = instancePath,
message = "Object deleted successfully",
}
else
return {
error = `Failed to delete object: {result}`,
instancePath = instancePath,
}
end
end
local function massCreateObjects(requestData)
local objects = requestData.objects
if not objects or not (type(objects) == "table") or #objects == 0 then
return {
error = "Objects array is required",
}
end
local results = {}
local successCount = 0
local failureCount = 0
for _, objData in objects do
local className = objData.className
local parentPath = objData.parent
local name = objData.name
local _condition = className
if _condition ~= "" and _condition then
_condition = parentPath
end
if _condition ~= "" and _condition then
local parentInstance = getInstanceByPath(parentPath)
if parentInstance then
local success, newInstance = pcall(function()
local instance = Instance.new(className)
if name ~= "" and name then
instance.Name = name
end
instance.Parent = parentInstance
return instance
end)
if success and newInstance then
successCount += 1
local _arg0 = {
success = true,
className = className,
parent = parentPath,
instancePath = getInstancePath(newInstance),
name = newInstance.Name,
}
table.insert(results, _arg0)
else
failureCount += 1
local _arg0 = {
success = false,
className = className,
parent = parentPath,
error = tostring(newInstance),
}
table.insert(results, _arg0)
end
else
failureCount += 1
local _arg0 = {
success = false,
className = className,
parent = parentPath,
error = "Parent instance not found",
}
table.insert(results, _arg0)
end
else
failureCount += 1
table.insert(results, {
success = false,
error = "Class name and parent are required",
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Mass create objects")
end
return {
results = results,
summary = {
total = #objects,
succeeded = successCount,
failed = failureCount,
},
}
end
local function massCreateObjectsWithProperties(requestData)
local objects = requestData.objects
if not objects or not (type(objects) == "table") or #objects == 0 then
return {
error = "Objects array is required",
}
end
local results = {}
local successCount = 0
local failureCount = 0
for _, objData in objects do
local className = objData.className
local parentPath = objData.parent
local name = objData.name
local properties = (objData.properties) or {}
local _condition = className
if _condition ~= "" and _condition then
_condition = parentPath
end
if _condition ~= "" and _condition then
local parentInstance = getInstanceByPath(parentPath)
if parentInstance then
local success, newInstance = pcall(function()
local instance = Instance.new(className)
if name ~= "" and name then
instance.Name = name
end
instance.Parent = parentInstance
for propName, propValue in pairs(properties) do
pcall(function()
local converted = convertPropertyValue(instance, propName, propValue)
if converted ~= nil then
instance[propName] = converted
end
end)
end
return instance
end)
if success and newInstance then
successCount += 1
local _arg0 = {
success = true,
className = className,
parent = parentPath,
instancePath = getInstancePath(newInstance),
name = newInstance.Name,
}
table.insert(results, _arg0)
else
failureCount += 1
local _arg0 = {
success = false,
className = className,
parent = parentPath,
error = tostring(newInstance),
}
table.insert(results, _arg0)
end
else
failureCount += 1
local _arg0 = {
success = false,
className = className,
parent = parentPath,
error = "Parent instance not found",
}
table.insert(results, _arg0)
end
else
failureCount += 1
table.insert(results, {
success = false,
error = "Class name and parent are required",
})
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint("Mass create objects with properties")
end
return {
results = results,
summary = {
total = #objects,
succeeded = successCount,
failed = failureCount,
},
}
end
local function smartDuplicate(requestData)
local instancePath = requestData.instancePath
local count = requestData.count
local options = (requestData.options) or {}
if not (instancePath ~= "" and instancePath) or not (count ~= 0 and count == count and count) or count < 1 then
return {
error = "Instance path and count > 0 are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local results = {}
local successCount = 0
local failureCount = 0
do
local _i = 1
local _shouldIncrement = false
while true do
local i = _i
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i <= count) then
break
end
local success, newInstance = pcall(function()
local clone = instance:Clone()
local _value = options.namePattern
if _value ~= 0 and _value == _value and _value ~= "" and _value then
local _exp = (options.namePattern)
local _arg1 = tostring(i)
clone.Name = (string.gsub(_exp, "{n}", _arg1))
else
clone.Name = instance.Name .. tostring(i)
end
local _condition = options.positionOffset
if _condition ~= 0 and _condition == _condition and _condition ~= "" and _condition then
_condition = clone:IsA("BasePart")
end
if _condition ~= 0 and _condition == _condition and _condition ~= "" and _condition then
local offset = options.positionOffset
local currentPos = clone.Position
local _exp = currentPos.X
local _condition_1 = offset[1]
if _condition_1 == nil then
_condition_1 = 0
end
local _exp_1 = _exp + _condition_1 * i
local _exp_2 = currentPos.Y
local _condition_2 = offset[2]
if _condition_2 == nil then
_condition_2 = 0
end
local _exp_3 = _exp_2 + _condition_2 * i
local _exp_4 = currentPos.Z
local _condition_3 = offset[3]
if _condition_3 == nil then
_condition_3 = 0
end
clone.Position = Vector3.new(_exp_1, _exp_3, _exp_4 + _condition_3 * i)
end
local _condition_1 = options.rotationOffset
if _condition_1 ~= 0 and _condition_1 == _condition_1 and _condition_1 ~= "" and _condition_1 then
_condition_1 = clone:IsA("BasePart")
end
if _condition_1 ~= 0 and _condition_1 == _condition_1 and _condition_1 ~= "" and _condition_1 then
local offset = options.rotationOffset
local _cFrame = clone.CFrame
local _condition_2 = offset[1]
if _condition_2 == nil then
_condition_2 = 0
end
local _exp = math.rad(_condition_2 * i)
local _condition_3 = offset[2]
if _condition_3 == nil then
_condition_3 = 0
end
local _exp_1 = math.rad(_condition_3 * i)
local _condition_4 = offset[3]
if _condition_4 == nil then
_condition_4 = 0
end
local _arg0 = CFrame.Angles(_exp, _exp_1, math.rad(_condition_4 * i))
clone.CFrame = _cFrame * _arg0
end
local _condition_2 = options.scaleOffset
if _condition_2 ~= 0 and _condition_2 == _condition_2 and _condition_2 ~= "" and _condition_2 then
_condition_2 = clone:IsA("BasePart")
end
if _condition_2 ~= 0 and _condition_2 == _condition_2 and _condition_2 ~= "" and _condition_2 then
local offset = options.scaleOffset
local currentSize = clone.Size
local _exp = currentSize.X
local _condition_3 = offset[1]
if _condition_3 == nil then
_condition_3 = 1
end
local _exp_1 = _exp * (_condition_3 ^ i)
local _exp_2 = currentSize.Y
local _condition_4 = offset[2]
if _condition_4 == nil then
_condition_4 = 1
end
local _exp_3 = _exp_2 * (_condition_4 ^ i)
local _exp_4 = currentSize.Z
local _condition_5 = offset[3]
if _condition_5 == nil then
_condition_5 = 1
end
clone.Size = Vector3.new(_exp_1, _exp_3, _exp_4 * (_condition_5 ^ i))
end
local _value_1 = options.propertyVariations
if _value_1 ~= 0 and _value_1 == _value_1 and _value_1 ~= "" and _value_1 then
for propName, values in pairs(options.propertyVariations) do
if values and #values > 0 then
local valueIndex = ((i - 1) % #values)
pcall(function()
clone[propName] = values[valueIndex + 1]
end)
end
end
end
local targetParents = options.targetParents
local _value_2 = targetParents and targetParents[i]
if _value_2 ~= "" and _value_2 then
local targetParent = getInstanceByPath(targetParents[i])
clone.Parent = targetParent or instance.Parent
else
clone.Parent = instance.Parent
end
return clone
end)
if success and newInstance then
successCount += 1
local _arg0 = {
success = true,
instancePath = getInstancePath(newInstance),
name = newInstance.Name,
index = i,
}
table.insert(results, _arg0)
else
failureCount += 1
local _arg0 = {
success = false,
index = i,
error = tostring(newInstance),
}
table.insert(results, _arg0)
end
_i = i
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint(`Smart duplicate {instance.Name} ({successCount} copies)`)
end
return {
results = results,
summary = {
total = count,
succeeded = successCount,
failed = failureCount,
},
sourceInstance = instancePath,
}
end
local function massDuplicate(requestData)
local duplications = requestData.duplications
if not duplications or not (type(duplications) == "table") or #duplications == 0 then
return {
error = "Duplications array is required",
}
end
local allResults = {}
local totalSuccess = 0
local totalFailures = 0
for _, duplication in duplications do
local result = smartDuplicate(duplication)
table.insert(allResults, result)
if result.summary then
totalSuccess += result.summary.succeeded
totalFailures += result.summary.failed
end
end
if totalSuccess > 0 then
ChangeHistoryService:SetWaypoint(`Mass duplicate operations ({totalSuccess} objects)`)
end
return {
results = allResults,
summary = {
total = totalSuccess + totalFailures,
succeeded = totalSuccess,
failed = totalFailures,
},
}
end
return {
createObject = createObject,
deleteObject = deleteObject,
massCreateObjects = massCreateObjects,
massCreateObjectsWithProperties = massCreateObjectsWithProperties,
smartDuplicate = smartDuplicate,
massDuplicate = massDuplicate,
}
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="5">
<Properties>
<string name="Name">MetadataHandlers</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
local CollectionService = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services").CollectionService
local Utils = TS.import(script, script.Parent.Parent, "Utils")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local Selection = game:GetService("Selection")
local _binding = Utils
local getInstancePath = _binding.getInstancePath
local getInstanceByPath = _binding.getInstanceByPath
local function serializeValue(value)
local _value = value
local vType = typeof(_value)
if vType == "Vector3" then
local v = value
return {
X = v.X,
Y = v.Y,
Z = v.Z,
_type = "Vector3",
}
elseif vType == "Color3" then
local v = value
return {
R = v.R,
G = v.G,
B = v.B,
_type = "Color3",
}
elseif vType == "CFrame" then
local v = value
return {
Position = {
X = v.Position.X,
Y = v.Position.Y,
Z = v.Position.Z,
},
_type = "CFrame",
}
elseif vType == "UDim2" then
local v = value
return {
X = {
Scale = v.X.Scale,
Offset = v.X.Offset,
},
Y = {
Scale = v.Y.Scale,
Offset = v.Y.Offset,
},
_type = "UDim2",
}
elseif vType == "BrickColor" then
local v = value
return {
Name = v.Name,
_type = "BrickColor",
}
end
return value
end
local function deserializeValue(attributeValue, valueType)
local _attributeValue = attributeValue
if not (type(_attributeValue) == "table") then
return attributeValue
end
local tbl = attributeValue
local _condition = (tbl._type)
if _condition == nil then
_condition = valueType
end
local t = _condition
if t == "Vector3" then
local _condition_1 = (tbl.X)
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (tbl.Y)
if _condition_2 == nil then
_condition_2 = 0
end
local _condition_3 = (tbl.Z)
if _condition_3 == nil then
_condition_3 = 0
end
return Vector3.new(_condition_1, _condition_2, _condition_3)
elseif t == "Color3" then
local _condition_1 = (tbl.R)
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (tbl.G)
if _condition_2 == nil then
_condition_2 = 0
end
local _condition_3 = (tbl.B)
if _condition_3 == nil then
_condition_3 = 0
end
return Color3.new(_condition_1, _condition_2, _condition_3)
elseif t == "UDim2" then
local x = tbl.X
local y = tbl.Y
local _result = x
if _result ~= nil then
_result = _result.Scale
end
local _condition_1 = _result
if _condition_1 == nil then
_condition_1 = 0
end
local _result_1 = x
if _result_1 ~= nil then
_result_1 = _result_1.Offset
end
local _condition_2 = _result_1
if _condition_2 == nil then
_condition_2 = 0
end
local _result_2 = y
if _result_2 ~= nil then
_result_2 = _result_2.Scale
end
local _condition_3 = _result_2
if _condition_3 == nil then
_condition_3 = 0
end
local _result_3 = y
if _result_3 ~= nil then
_result_3 = _result_3.Offset
end
local _condition_4 = _result_3
if _condition_4 == nil then
_condition_4 = 0
end
return UDim2.new(_condition_1, _condition_2, _condition_3, _condition_4)
elseif t == "BrickColor" then
local _condition_1 = (tbl.Name)
if _condition_1 == nil then
_condition_1 = "Medium stone grey"
end
return BrickColor.new(_condition_1)
end
return attributeValue
end
local function getAttribute(requestData)
local instancePath = requestData.instancePath
local attributeName = requestData.attributeName
if not (instancePath ~= "" and instancePath) or not (attributeName ~= "" and attributeName) then
return {
error = "Instance path and attribute name are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local success, result = pcall(function()
local value = instance:GetAttribute(attributeName)
return {
instancePath = instancePath,
attributeName = attributeName,
value = serializeValue(value),
valueType = typeof(value),
exists = value ~= nil,
}
end)
if success then
return result
end
return {
error = `Failed to get attribute: {result}`,
}
end
local function setAttribute(requestData)
local instancePath = requestData.instancePath
local attributeName = requestData.attributeName
local attributeValue = requestData.attributeValue
local valueType = requestData.valueType
if not (instancePath ~= "" and instancePath) or not (attributeName ~= "" and attributeName) then
return {
error = "Instance path and attribute name are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local success, result = pcall(function()
local value = deserializeValue(attributeValue, valueType)
instance:SetAttribute(attributeName, value)
ChangeHistoryService:SetWaypoint(`Set attribute {attributeName} on {instance.Name}`)
return {
success = true,
instancePath = instancePath,
attributeName = attributeName,
value = attributeValue,
message = "Attribute set successfully",
}
end)
if success then
return result
end
return {
error = `Failed to set attribute: {result}`,
}
end
local function getAttributes(requestData)
local instancePath = requestData.instancePath
if not (instancePath ~= "" and instancePath) then
return {
error = "Instance path is required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local success, result = pcall(function()
local attributes = instance:GetAttributes()
local serializedAttributes = {}
local count = 0
for name, value in pairs(attributes) do
serializedAttributes[name] = {
value = serializeValue(value),
type = typeof(value),
}
count += 1
end
return {
instancePath = instancePath,
attributes = serializedAttributes,
count = count,
}
end)
if success then
return result
end
return {
error = `Failed to get attributes: {result}`,
}
end
local function deleteAttribute(requestData)
local instancePath = requestData.instancePath
local attributeName = requestData.attributeName
if not (instancePath ~= "" and instancePath) or not (attributeName ~= "" and attributeName) then
return {
error = "Instance path and attribute name are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local success, result = pcall(function()
local existed = instance:GetAttribute(attributeName) ~= nil
instance:SetAttribute(attributeName, nil)
ChangeHistoryService:SetWaypoint(`Delete attribute {attributeName} from {instance.Name}`)
return {
success = true,
instancePath = instancePath,
attributeName = attributeName,
existed = existed,
message = if existed then "Attribute deleted successfully" else "Attribute did not exist",
}
end)
if success then
return result
end
return {
error = `Failed to delete attribute: {result}`,
}
end
local function getTags(requestData)
local instancePath = requestData.instancePath
if not (instancePath ~= "" and instancePath) then
return {
error = "Instance path is required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local success, result = pcall(function()
local tags = CollectionService:GetTags(instance)
return {
instancePath = instancePath,
tags = tags,
count = #tags,
}
end)
if success then
return result
end
return {
error = `Failed to get tags: {result}`,
}
end
local function addTag(requestData)
local instancePath = requestData.instancePath
local tagName = requestData.tagName
if not (instancePath ~= "" and instancePath) or not (tagName ~= "" and tagName) then
return {
error = "Instance path and tag name are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local success, result = pcall(function()
local alreadyHad = CollectionService:HasTag(instance, tagName)
CollectionService:AddTag(instance, tagName)
ChangeHistoryService:SetWaypoint(`Add tag {tagName} to {instance.Name}`)
return {
success = true,
instancePath = instancePath,
tagName = tagName,
alreadyHad = alreadyHad,
message = if alreadyHad then "Instance already had this tag" else "Tag added successfully",
}
end)
if success then
return result
end
return {
error = `Failed to add tag: {result}`,
}
end
local function removeTag(requestData)
local instancePath = requestData.instancePath
local tagName = requestData.tagName
if not (instancePath ~= "" and instancePath) or not (tagName ~= "" and tagName) then
return {
error = "Instance path and tag name are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local success, result = pcall(function()
local hadTag = CollectionService:HasTag(instance, tagName)
CollectionService:RemoveTag(instance, tagName)
ChangeHistoryService:SetWaypoint(`Remove tag {tagName} from {instance.Name}`)
return {
success = true,
instancePath = instancePath,
tagName = tagName,
hadTag = hadTag,
message = if hadTag then "Tag removed successfully" else "Instance did not have this tag",
}
end)
if success then
return result
end
return {
error = `Failed to remove tag: {result}`,
}
end
local function getTagged(requestData)
local tagName = requestData.tagName
if not (tagName ~= "" and tagName) then
return {
error = "Tag name is required",
}
end
local success, result = pcall(function()
local taggedInstances = CollectionService:GetTagged(tagName)
-- ▼ ReadonlyArray.map ▼
local _newValue = table.create(#taggedInstances)
local _callback = function(instance)
return {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
}
end
for _k, _v in taggedInstances do
_newValue[_k] = _callback(_v, _k - 1, taggedInstances)
end
-- ▲ ReadonlyArray.map ▲
local instances = _newValue
return {
tagName = tagName,
instances = instances,
count = #instances,
}
end)
if success then
return result
end
return {
error = `Failed to get tagged instances: {result}`,
}
end
local function getSelection(_requestData)
local selection = Selection:Get()
if #selection == 0 then
return {
success = true,
selection = {},
count = 0,
message = "No objects selected",
}
end
-- ▼ ReadonlyArray.map ▼
local _newValue = table.create(#selection)
local _callback = function(instance)
return {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
parent = if instance.Parent then getInstancePath(instance.Parent) else nil,
}
end
for _k, _v in selection do
_newValue[_k] = _callback(_v, _k - 1, selection)
end
-- ▲ ReadonlyArray.map ▲
local selectedObjects = _newValue
return {
success = true,
selection = selectedObjects,
count = #selection,
message = `{#selection} object(s) selected`,
}
end
local function executeLuau(requestData)
local code = requestData.code
if not (code ~= "" and code) or code == "" then
return {
error = "Code is required",
}
end
local output = {}
local oldPrint = print
local oldWarn = warn
local env = getfenv(0)
env.print = function(...)
local args = { ... }
local parts = {}
for _, a in args do
local _arg0 = tostring(a)
table.insert(parts, _arg0)
end
local _arg0 = table.concat(parts, "\t")
table.insert(output, _arg0)
oldPrint(unpack(args))
end
env.warn = function(...)
local args = { ... }
local parts = {}
for _, a in args do
local _arg0 = tostring(a)
table.insert(parts, _arg0)
end
local _arg0 = `[warn] {table.concat(parts, "\t")}`
table.insert(output, _arg0)
oldWarn(unpack(args))
end
local success, result = pcall(function()
local fn, compileError = loadstring(code)
if not fn then
error(`Compile error: {compileError}`)
end
return fn()
end)
env.print = oldPrint
env.warn = oldWarn
if success then
return {
success = true,
returnValue = if result ~= nil then tostring(result) else nil,
output = output,
message = "Code executed successfully",
}
else
return {
success = false,
error = tostring(result),
output = output,
message = "Code execution failed",
}
end
end
return {
getAttribute = getAttribute,
setAttribute = setAttribute,
getAttributes = getAttributes,
deleteAttribute = deleteAttribute,
getTags = getTags,
addTag = addTag,
removeTag = removeTag,
getTagged = getTagged,
getSelection = getSelection,
executeLuau = executeLuau,
}
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="6">
<Properties>
<string name="Name">PropertyHandlers</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
local Utils = TS.import(script, script.Parent.Parent, "Utils")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local _binding = Utils
local getInstanceByPath = _binding.getInstanceByPath
local convertPropertyValue = _binding.convertPropertyValue
local evaluateFormula = _binding.evaluateFormula
local function setProperty(requestData)
local instancePath = requestData.instancePath
local propertyName = requestData.propertyName
local propertyValue = requestData.propertyValue
if not (instancePath ~= "" and instancePath) or not (propertyName ~= "" and propertyName) then
return {
error = "Instance path and property name are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local inst = instance
local success, result = pcall(function()
if propertyName == "Parent" or propertyName == "PrimaryPart" then
if type(propertyValue) == "string" then
local refInstance = getInstanceByPath(propertyValue)
if refInstance then
inst[propertyName] = refInstance
else
return {
error = `{propertyName} instance not found: {propertyValue}`,
}
end
end
elseif propertyName == "Name" then
instance.Name = tostring(propertyValue)
elseif propertyName == "Source" and instance:IsA("LuaSourceContainer") then
instance.Source = tostring(propertyValue)
else
local convertedValue = convertPropertyValue(instance, propertyName, propertyValue)
if convertedValue ~= nil then
inst[propertyName] = convertedValue
else
inst[propertyName] = propertyValue
end
end
ChangeHistoryService:SetWaypoint(`Set {propertyName} property`)
return true
end)
if success then
return {
success = true,
instancePath = instancePath,
propertyName = propertyName,
propertyValue = propertyValue,
message = "Property set successfully",
}
else
return {
error = `Failed to set property: {result}`,
instancePath = instancePath,
propertyName = propertyName,
}
end
end
local function massSetProperty(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
local propertyValue = requestData.propertyValue
if not paths or not (type(paths) == "table") or #paths == 0 or not (propertyName ~= "" and propertyName) then
return {
error = "Paths array and property name are required",
}
end
local results = {}
local successCount = 0
local failureCount = 0
for _, path in paths do
local instance = getInstanceByPath(path)
if instance then
local success, err = pcall(function()
instance[propertyName] = propertyValue
end)
if success then
successCount += 1
local _arg0 = {
path = path,
success = true,
propertyName = propertyName,
propertyValue = propertyValue,
}
table.insert(results, _arg0)
else
failureCount += 1
local _arg0 = {
path = path,
success = false,
error = tostring(err),
}
table.insert(results, _arg0)
end
else
failureCount += 1
local _arg0 = {
path = path,
success = false,
error = "Instance not found",
}
table.insert(results, _arg0)
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint(`Mass set {propertyName} property`)
end
return {
results = results,
summary = {
total = #paths,
succeeded = successCount,
failed = failureCount,
},
}
end
local function massGetProperty(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
if not paths or not (type(paths) == "table") or #paths == 0 or not (propertyName ~= "" and propertyName) then
return {
error = "Paths array and property name are required",
}
end
local results = {}
for _, path in paths do
local instance = getInstanceByPath(path)
if instance then
local success, value = pcall(function()
return instance[propertyName]
end)
if success then
local _arg0 = {
path = path,
success = true,
propertyName = propertyName,
propertyValue = value,
}
table.insert(results, _arg0)
else
local _arg0 = {
path = path,
success = false,
error = tostring(value),
}
table.insert(results, _arg0)
end
else
local _arg0 = {
path = path,
success = false,
error = "Instance not found",
}
table.insert(results, _arg0)
end
end
return {
results = results,
propertyName = propertyName,
}
end
local function setCalculatedProperty(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
local formula = requestData.formula
local variables = requestData.variables
if not paths or not (type(paths) == "table") or #paths == 0 or not (propertyName ~= "" and propertyName) or not (formula ~= "" and formula) then
return {
error = "Paths, property name, and formula are required",
}
end
local results = {}
local successCount = 0
local failureCount = 0
for i = 0, #paths - 1 do
local path = paths[i + 1]
local instance = getInstanceByPath(path)
if instance then
local value, evalError = evaluateFormula(formula, variables, instance, i + 1)
if value ~= nil and not (evalError ~= "" and evalError) then
local success, err = pcall(function()
instance[propertyName] = value
end)
if success then
successCount += 1
local _arg0 = {
path = path,
success = true,
propertyName = propertyName,
calculatedValue = value,
formula = formula,
}
table.insert(results, _arg0)
else
failureCount += 1
local _arg0 = {
path = path,
success = false,
error = `Property set failed: {err}`,
}
table.insert(results, _arg0)
end
else
failureCount += 1
local _object = {
path = path,
success = false,
}
local _left = "error"
local _condition = evalError
if _condition == nil then
_condition = "Formula evaluation failed"
end
_object[_left] = _condition
table.insert(results, _object)
end
else
failureCount += 1
local _arg0 = {
path = path,
success = false,
error = "Instance not found",
}
table.insert(results, _arg0)
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint(`Set calculated {propertyName} property`)
end
return {
results = results,
summary = {
total = #paths,
succeeded = successCount,
failed = failureCount,
},
formula = formula,
}
end
local function setRelativeProperty(requestData)
local paths = requestData.paths
local propertyName = requestData.propertyName
local operation = requestData.operation
local value = requestData.value
local component = requestData.component
if not paths or not (type(paths) == "table") or #paths == 0 or not (propertyName ~= "" and propertyName) or not (operation ~= "" and operation) or value == nil then
return {
error = "Paths, property name, operation, and value are required",
}
end
local results = {}
local successCount = 0
local failureCount = 0
local function applyOp(current, op, val)
if op == "add" then
return current + val
end
if op == "subtract" then
return current - val
end
if op == "multiply" then
return current * val
end
if op == "divide" then
return current / val
end
if op == "power" then
return current ^ val
end
return current
end
for _, path in paths do
local instance = getInstanceByPath(path)
if instance then
local success, err = pcall(function()
local currentValue = instance[propertyName]
local newValue
local _condition = component
if _condition ~= "" and _condition then
_condition = typeof(currentValue) == "Vector3"
end
if _condition ~= "" and _condition then
local cv = currentValue
local x = cv.X
local y = cv.Y
local z = cv.Z
if component == "X" then
x = applyOp(x, operation, value)
elseif component == "Y" then
y = applyOp(y, operation, value)
elseif component == "Z" then
z = applyOp(z, operation, value)
end
newValue = Vector3.new(x, y, z)
elseif typeof(currentValue) == "Color3" and typeof(value) == "Color3" then
local cv = currentValue
local v = value
if operation == "add" then
newValue = Color3.new(math.min(1, cv.R + v.R), math.min(1, cv.G + v.G), math.min(1, cv.B + v.B))
elseif operation == "subtract" then
newValue = Color3.new(math.max(0, cv.R - v.R), math.max(0, cv.G - v.G), math.max(0, cv.B - v.B))
elseif operation == "multiply" then
newValue = Color3.new(cv.R * v.R, cv.G * v.G, cv.B * v.B)
end
elseif type(currentValue) == "number" and type(value) == "number" then
newValue = applyOp(currentValue, operation, value)
elseif typeof(currentValue) == "Vector3" and type(value) == "number" then
local cv = currentValue
newValue = Vector3.new(applyOp(cv.X, operation, value), applyOp(cv.Y, operation, value), applyOp(cv.Z, operation, value))
else
local _value = typeof(currentValue) == "UDim2" and type(value) == "number" and component
if _value ~= "" and _value then
local cv = currentValue
local xs = cv.X.Scale
local xo = cv.X.Offset
local ys = cv.Y.Scale
local yo = cv.Y.Offset
if component == "XScale" then
xs = applyOp(xs, operation, value)
elseif component == "XOffset" then
xo = applyOp(xo, operation, value)
elseif component == "YScale" then
ys = applyOp(ys, operation, value)
elseif component == "YOffset" then
yo = applyOp(yo, operation, value)
end
newValue = UDim2.new(xs, xo, ys, yo)
else
error("Unsupported property type or operation")
end
end
instance[propertyName] = newValue
return newValue
end)
if success then
successCount += 1
local _arg0 = {
path = path,
success = true,
propertyName = propertyName,
operation = operation,
value = value,
component = component,
newValue = err,
}
table.insert(results, _arg0)
else
failureCount += 1
local _arg0 = {
path = path,
success = false,
error = tostring(err),
}
table.insert(results, _arg0)
end
else
failureCount += 1
local _arg0 = {
path = path,
success = false,
error = "Instance not found",
}
table.insert(results, _arg0)
end
end
if successCount > 0 then
ChangeHistoryService:SetWaypoint(`Set relative {propertyName} property`)
end
return {
results = results,
summary = {
total = #paths,
succeeded = successCount,
failed = failureCount,
},
operation = operation,
value = value,
}
end
return {
setProperty = setProperty,
massSetProperty = massSetProperty,
massGetProperty = massGetProperty,
setCalculatedProperty = setCalculatedProperty,
setRelativeProperty = setRelativeProperty,
}
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="7">
<Properties>
<string name="Name">QueryHandlers</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
local Utils = TS.import(script, script.Parent.Parent, "Utils")
local _binding = Utils
local getInstancePath = _binding.getInstancePath
local getInstanceByPath = _binding.getInstanceByPath
local readScriptSource = _binding.readScriptSource
local function getFileTree(requestData)
local _condition = (requestData.path)
if _condition == nil then
_condition = ""
end
local path = _condition
local startInstance = getInstanceByPath(path)
if not startInstance then
return {
error = `Path not found: {path}`,
}
end
local function buildTree(instance, depth)
if depth > 10 then
return {
name = instance.Name,
className = instance.ClassName,
children = {},
}
end
local node = {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
children = {},
}
if instance:IsA("LuaSourceContainer") then
node.hasSource = true
node.scriptType = instance.ClassName
end
for _, child in instance:GetChildren() do
local _children = node.children
local _arg0 = buildTree(child, depth + 1)
table.insert(_children, _arg0)
end
return node
end
return {
tree = buildTree(startInstance, 0),
timestamp = tick(),
}
end
local function searchFiles(requestData)
local query = requestData.query
local _condition = (requestData.searchType)
if _condition == nil then
_condition = "name"
end
local searchType = _condition
if not (query ~= "" and query) then
return {
error = "Query is required",
}
end
local results = {}
local function searchRecursive(instance)
local match = false
if searchType == "name" then
local _exp = string.lower(instance.Name)
local _arg0 = string.lower(query)
match = (string.find(_exp, _arg0)) ~= nil
elseif searchType == "type" then
local _exp = string.lower(instance.ClassName)
local _arg0 = string.lower(query)
match = (string.find(_exp, _arg0)) ~= nil
elseif searchType == "content" and instance:IsA("LuaSourceContainer") then
local _exp = string.lower(readScriptSource(instance))
local _arg0 = string.lower(query)
match = (string.find(_exp, _arg0)) ~= nil
end
if match then
local _arg0 = {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
hasSource = instance:IsA("LuaSourceContainer"),
}
table.insert(results, _arg0)
end
for _, child in instance:GetChildren() do
searchRecursive(child)
end
end
searchRecursive(game)
return {
results = results,
query = query,
searchType = searchType,
count = #results,
}
end
local function getPlaceInfo(_requestData)
return {
placeName = game.Name,
placeId = game.PlaceId,
gameId = game.GameId,
jobId = game.JobId,
workspace = {
name = game.Workspace.Name,
className = game.Workspace.ClassName,
},
}
end
local function getServices(requestData)
local serviceName = requestData.serviceName
if serviceName ~= "" and serviceName then
local ok, service = pcall(function()
return game:GetService(serviceName)
end)
if ok and service then
return {
service = {
name = service.Name,
className = service.ClassName,
path = getInstancePath(service),
childCount = #service:GetChildren(),
},
}
else
return {
error = `Service not found: {serviceName}`,
}
end
else
local services = {}
local commonServices = { "Workspace", "Players", "StarterGui", "StarterPack", "StarterPlayer", "ReplicatedStorage", "ServerStorage", "ServerScriptService", "HttpService", "TeleportService", "DataStoreService" }
for _, svcName in commonServices do
local ok, service = pcall(function()
return game:GetService(svcName)
end)
if ok and service then
local _arg0 = {
name = service.Name,
className = service.ClassName,
path = getInstancePath(service),
childCount = #service:GetChildren(),
}
table.insert(services, _arg0)
end
end
return {
services = services,
}
end
end
local function searchObjects(requestData)
local query = requestData.query
local _condition = (requestData.searchType)
if _condition == nil then
_condition = "name"
end
local searchType = _condition
local propertyName = requestData.propertyName
if not (query ~= "" and query) then
return {
error = "Query is required",
}
end
local results = {}
local function searchRecursive(instance)
local match = false
if searchType == "name" then
local _exp = string.lower(instance.Name)
local _arg0 = string.lower(query)
match = (string.find(_exp, _arg0)) ~= nil
elseif searchType == "class" then
local _exp = string.lower(instance.ClassName)
local _arg0 = string.lower(query)
match = (string.find(_exp, _arg0)) ~= nil
else
local _value = searchType == "property" and propertyName
if _value ~= "" and _value then
local success, value = pcall(function()
return tostring(instance[propertyName])
end)
if success then
local _exp = string.lower(value)
local _arg0 = string.lower(query)
match = (string.find(_exp, _arg0)) ~= nil
end
end
end
if match then
local _arg0 = {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
}
table.insert(results, _arg0)
end
for _, child in instance:GetChildren() do
searchRecursive(child)
end
end
searchRecursive(game)
return {
results = results,
query = query,
searchType = searchType,
count = #results,
}
end
local function getInstanceProperties(requestData)
local instancePath = requestData.instancePath
if not (instancePath ~= "" and instancePath) then
return {
error = "Instance path is required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local properties = {}
local success, result = pcall(function()
local basicProps = { "Name", "ClassName", "Parent" }
for _, prop in basicProps do
local propSuccess, propValue = pcall(function()
local val = instance[prop]
local _value = prop == "Parent" and val
if _value ~= 0 and _value == _value and _value ~= "" and _value then
return getInstancePath(val)
end
if val == nil then
return "nil"
end
return tostring(val)
end)
if propSuccess then
properties[prop] = propValue
end
end
local commonProps = { "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide", "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3", "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex", "BorderSizePixel", "BackgroundTransparency", "ImageTransparency", "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows", "Face", "SurfaceType" }
for _, prop in commonProps do
local propSuccess, propValue = pcall(function()
local val = instance[prop]
if typeof(val) == "UDim2" then
local udim = val
return {
X = {
Scale = udim.X.Scale,
Offset = udim.X.Offset,
},
Y = {
Scale = udim.Y.Scale,
Offset = udim.Y.Offset,
},
_type = "UDim2",
}
end
return tostring(val)
end)
if propSuccess then
properties[prop] = propValue
end
end
if instance:IsA("LuaSourceContainer") then
properties.Source = readScriptSource(instance)
if instance:IsA("BaseScript") then
properties.Enabled = tostring(instance.Enabled)
end
end
if instance:IsA("Part") then
properties.Shape = tostring(instance.Shape)
end
if instance:IsA("BasePart") then
properties.TopSurface = tostring(instance.TopSurface)
properties.BottomSurface = tostring(instance.BottomSurface)
end
if instance:IsA("MeshPart") then
properties.MeshId = tostring(instance.MeshId)
properties.TextureID = tostring(instance.TextureID)
end
if instance:IsA("SpecialMesh") then
properties.MeshId = tostring(instance.MeshId)
properties.TextureId = tostring(instance.TextureId)
properties.MeshType = tostring(instance.MeshType)
end
if instance:IsA("Sound") then
properties.SoundId = tostring(instance.SoundId)
properties.TimeLength = tostring(instance.TimeLength)
properties.IsPlaying = tostring(instance.IsPlaying)
end
if instance:IsA("Animation") then
properties.AnimationId = tostring(instance.AnimationId)
end
if instance:IsA("Decal") or instance:IsA("Texture") then
properties.Texture = tostring(instance.Texture)
end
if instance:IsA("Shirt") then
properties.ShirtTemplate = tostring(instance.ShirtTemplate)
elseif instance:IsA("Pants") then
properties.PantsTemplate = tostring(instance.PantsTemplate)
elseif instance:IsA("ShirtGraphic") then
properties.Graphic = tostring(instance.Graphic)
end
properties.ChildCount = tostring(#instance:GetChildren())
end)
if success then
return {
instancePath = instancePath,
className = instance.ClassName,
properties = properties,
}
else
return {
error = `Failed to get properties: {result}`,
}
end
end
local function getInstanceChildren(requestData)
local instancePath = requestData.instancePath
if not (instancePath ~= "" and instancePath) then
return {
error = "Instance path is required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
local children = {}
for _, child in instance:GetChildren() do
local _arg0 = {
name = child.Name,
className = child.ClassName,
path = getInstancePath(child),
hasChildren = #child:GetChildren() > 0,
hasSource = child:IsA("LuaSourceContainer"),
}
table.insert(children, _arg0)
end
return {
instancePath = instancePath,
children = children,
count = #children,
}
end
local function searchByProperty(requestData)
local propertyName = requestData.propertyName
local propertyValue = requestData.propertyValue
if not (propertyName ~= "" and propertyName) or not (propertyValue ~= "" and propertyValue) then
return {
error = "Property name and value are required",
}
end
local results = {}
local function searchRecursive(instance)
local success, value = pcall(function()
return tostring(instance[propertyName])
end)
local _condition = success
if _condition then
local _exp = string.lower(value)
local _arg0 = string.lower(propertyValue)
_condition = (string.find(_exp, _arg0)) ~= nil
end
if _condition then
local _arg0 = {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
propertyValue = value,
}
table.insert(results, _arg0)
end
for _, child in instance:GetChildren() do
searchRecursive(child)
end
end
searchRecursive(game)
return {
propertyName = propertyName,
propertyValue = propertyValue,
results = results,
count = #results,
}
end
local function getClassInfo(requestData)
local className = requestData.className
if not (className ~= "" and className) then
return {
error = "Class name is required",
}
end
local success, tempInstance = pcall(function()
return Instance.new(className)
end)
local isService = false
if not success then
local serviceSuccess, serviceInstance = pcall(function()
return game:GetService(className)
end)
if serviceSuccess and serviceInstance then
success = true
tempInstance = serviceInstance
isService = true
end
end
if not success then
return {
error = `Invalid class name: {className}`,
}
end
local classInfo = {
className = className,
isService = isService,
properties = {},
methods = {},
events = {},
}
local commonProps = { "Name", "ClassName", "Parent", "Size", "Position", "Rotation", "CFrame", "Anchored", "CanCollide", "Transparency", "BrickColor", "Material", "Color", "Text", "TextColor3", "BackgroundColor3", "Image", "ImageColor3", "Visible", "Active", "ZIndex", "BorderSizePixel", "BackgroundTransparency", "ImageTransparency", "TextTransparency", "Value", "Enabled", "Brightness", "Range", "Shadows" }
for _, prop in commonProps do
local propSuccess = pcall(function()
return tempInstance[prop]
end)
if propSuccess then
local _exp = classInfo.properties
table.insert(_exp, prop)
end
end
local commonMethods = { "Destroy", "Clone", "FindFirstChild", "FindFirstChildOfClass", "GetChildren", "IsA", "IsAncestorOf", "IsDescendantOf", "WaitForChild" }
for _, method in commonMethods do
local methodSuccess = pcall(function()
return tempInstance[method]
end)
if methodSuccess then
local _exp = classInfo.methods
table.insert(_exp, method)
end
end
if not isService then
tempInstance:Destroy()
end
return classInfo
end
local function getProjectStructure(requestData)
local _condition = (requestData.path)
if _condition == nil then
_condition = ""
end
local startPath = _condition
local _condition_1 = (requestData.maxDepth)
if _condition_1 == nil then
_condition_1 = 3
end
local maxDepth = _condition_1
local _condition_2 = (requestData.scriptsOnly)
if _condition_2 == nil then
_condition_2 = false
end
local showScriptsOnly = _condition_2
if startPath == "" or startPath == "game" then
local services = {}
local mainServices = { "Workspace", "ServerScriptService", "ServerStorage", "ReplicatedStorage", "StarterGui", "StarterPack", "StarterPlayer", "Players" }
for _, serviceName in mainServices do
local svcOk, service = pcall(function()
return game:GetService(serviceName)
end)
if svcOk and service then
local _arg0 = {
name = service.Name,
className = service.ClassName,
path = getInstancePath(service),
childCount = #service:GetChildren(),
hasChildren = #service:GetChildren() > 0,
}
table.insert(services, _arg0)
end
end
return {
type = "service_overview",
services = services,
timestamp = tick(),
note = "Use path parameter to explore specific locations (e.g., 'game.ServerScriptService')",
}
end
local startInstance = getInstanceByPath(startPath)
if not startInstance then
return {
error = `Path not found: {startPath}`,
}
end
local function getStructure(instance, depth)
if depth > maxDepth then
return {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
childCount = #instance:GetChildren(),
hasMore = true,
note = "Max depth reached - use this path to explore further",
}
end
local node = {
name = instance.Name,
className = instance.ClassName,
path = getInstancePath(instance),
children = {},
}
if instance:IsA("LuaSourceContainer") then
node.hasSource = true
node.scriptType = instance.ClassName
if instance:IsA("BaseScript") then
node.enabled = instance.Enabled
end
end
if instance:IsA("GuiObject") then
node.visible = instance.Visible
if instance:IsA("Frame") or instance:IsA("ScreenGui") then
node.guiType = "container"
elseif instance:IsA("TextLabel") or instance:IsA("TextButton") then
node.guiType = "text"
local textInst = instance
if textInst.Text ~= "" then
node.text = textInst.Text
end
elseif instance:IsA("ImageLabel") or instance:IsA("ImageButton") then
node.guiType = "image"
end
end
local children = instance:GetChildren()
if showScriptsOnly then
-- ▼ ReadonlyArray.filter ▼
local _newValue = {}
local _callback = function(child)
return child:IsA("BaseScript") or child:IsA("Folder") or child:IsA("ModuleScript")
end
local _length = 0
for _k, _v in children do
if _callback(_v, _k - 1, children) == true then
_length += 1
_newValue[_length] = _v
end
end
-- ▲ ReadonlyArray.filter ▲
children = _newValue
end
local nodeChildren = node.children
local childCount = #children
if childCount > 20 and depth < maxDepth then
local classGroups = {}
for _, child in children do
local cn = child.ClassName
if not (classGroups[cn] ~= nil) then
classGroups[cn] = {}
end
local _exp = classGroups[cn]
table.insert(_exp, child)
end
local childSummary = {}
-- ▼ ReadonlyMap.forEach ▼
local _callback = function(classChildren, cn)
local _object = {
className = cn,
count = #classChildren,
}
local _left = "examples"
local _result = classChildren[1]
if _result ~= nil then
_result = _result.Name
end
local _result_1 = classChildren[2]
if _result_1 ~= nil then
_result_1 = _result_1.Name
end
_object[_left] = { _result, _result_1 }
table.insert(childSummary, _object)
end
for _k, _v in classGroups do
_callback(_v, _k, classGroups)
end
-- ▲ ReadonlyMap.forEach ▲
node.childSummary = childSummary
-- ▼ ReadonlyMap.forEach ▼
local _callback_1 = function(classChildren, cn)
local limit = math.min(3, #classChildren)
do
local i = 0
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < limit) then
break
end
local _arg0 = getStructure(classChildren[i + 1], depth + 1)
table.insert(nodeChildren, _arg0)
end
end
if #classChildren > 3 then
local _arg0 = {
name = `... {#classChildren - 3} more {cn} objects`,
className = "MoreIndicator",
path = `{getInstancePath(instance)} [{cn} children]`,
note = "Use specific path to explore these objects",
}
table.insert(nodeChildren, _arg0)
end
end
for _k, _v in classGroups do
_callback_1(_v, _k, classGroups)
end
-- ▲ ReadonlyMap.forEach ▲
else
for _, child in children do
local _arg0 = getStructure(child, depth + 1)
table.insert(nodeChildren, _arg0)
end
end
return node
end
local result = getStructure(startInstance, 0)
result.requestedPath = startPath
result.maxDepth = maxDepth
result.scriptsOnly = showScriptsOnly
result.timestamp = tick()
return result
end
return {
getFileTree = getFileTree,
searchFiles = searchFiles,
getPlaceInfo = getPlaceInfo,
getServices = getServices,
searchObjects = searchObjects,
getInstanceProperties = getInstanceProperties,
getInstanceChildren = getInstanceChildren,
searchByProperty = searchByProperty,
getClassInfo = getClassInfo,
getProjectStructure = getProjectStructure,
}
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="8">
<Properties>
<string name="Name">ScriptHandlers</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
local Utils = TS.import(script, script.Parent.Parent, "Utils")
local ChangeHistoryService = game:GetService("ChangeHistoryService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local _binding = Utils
local getInstancePath = _binding.getInstancePath
local getInstanceByPath = _binding.getInstanceByPath
local readScriptSource = _binding.readScriptSource
local splitLines = _binding.splitLines
local joinLines = _binding.joinLines
local function normalizeEscapes(s)
local result = s
result = (string.gsub(result, "\\n", "\n"))
result = (string.gsub(result, "\\t", "\t"))
result = (string.gsub(result, "\\r", "\r"))
result = (string.gsub(result, "\\\\", "\\"))
return result
end
local function getScriptSource(requestData)
local instancePath = requestData.instancePath
local startLine = requestData.startLine
local endLine = requestData.endLine
if not (instancePath ~= "" and instancePath) then
return {
error = "Instance path is required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
if not instance:IsA("LuaSourceContainer") then
return {
error = `Instance is not a script-like object: {instance.ClassName}`,
}
end
local success, result = pcall(function()
local fullSource = readScriptSource(instance)
local lines, hasTrailingNewline = splitLines(fullSource)
local totalLineCount = #lines
local sourceToReturn = fullSource
local returnedStartLine = 1
local returnedEndLine = totalLineCount
if startLine ~= nil or endLine ~= nil then
local _condition = startLine
if _condition == nil then
_condition = 1
end
local actualStartLine = math.max(1, _condition)
local _exp = #lines
local _condition_1 = endLine
if _condition_1 == nil then
_condition_1 = #lines
end
local actualEndLine = math.min(_exp, _condition_1)
local selectedLines = {}
do
local i = actualStartLine
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i <= actualEndLine) then
break
end
local _condition_2 = lines[i]
if _condition_2 == nil then
_condition_2 = ""
end
table.insert(selectedLines, _condition_2)
end
end
sourceToReturn = table.concat(selectedLines, "\n")
if hasTrailingNewline and actualEndLine == #lines and string.sub(sourceToReturn, -1) ~= "\n" then
sourceToReturn ..= "\n"
end
returnedStartLine = actualStartLine
returnedEndLine = actualEndLine
end
local numberedLines = {}
local linesToNumber = if startLine ~= nil then (splitLines(sourceToReturn)) else lines
local lineOffset = returnedStartLine - 1
for i = 0, #linesToNumber - 1 do
local _arg0 = `{i + 1 + lineOffset}: {linesToNumber[i + 1]}`
table.insert(numberedLines, _arg0)
end
local numberedSource = table.concat(numberedLines, "\n")
local resp = {
instancePath = instancePath,
className = instance.ClassName,
name = instance.Name,
source = sourceToReturn,
numberedSource = numberedSource,
sourceLength = #fullSource,
lineCount = totalLineCount,
startLine = returnedStartLine,
endLine = returnedEndLine,
isPartial = startLine ~= nil or endLine ~= nil,
truncated = false,
}
if startLine == nil and endLine == nil and #fullSource > 50000 then
local truncatedLines = {}
local truncatedNumberedLines = {}
local maxLines = math.min(1000, #lines)
do
local i = 0
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < maxLines) then
break
end
local _arg0 = lines[i + 1]
table.insert(truncatedLines, _arg0)
local _arg0_1 = `{i + 1}: {lines[i + 1]}`
table.insert(truncatedNumberedLines, _arg0_1)
end
end
resp.source = table.concat(truncatedLines, "\n")
resp.numberedSource = table.concat(truncatedNumberedLines, "\n")
resp.truncated = true
resp.endLine = maxLines
resp.note = "Script truncated to first 1000 lines. Use startLine/endLine parameters to read specific sections."
end
if instance:IsA("BaseScript") then
resp.enabled = instance.Enabled
end
return resp
end)
if success then
return result
else
return {
error = `Failed to get script source: {result}`,
}
end
end
local function setScriptSource(requestData)
local instancePath = requestData.instancePath
local newSource = requestData.source
if not (instancePath ~= "" and instancePath) or not (newSource ~= "" and newSource) then
return {
error = "Instance path and source are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
if not instance:IsA("LuaSourceContainer") then
return {
error = `Instance is not a script-like object: {instance.ClassName}`,
}
end
local sourceToSet = normalizeEscapes(newSource)
local updateSuccess, updateResult = pcall(function()
local oldSourceLength = #readScriptSource(instance)
ScriptEditorService:UpdateSourceAsync(instance, function()
return sourceToSet
end)
ChangeHistoryService:SetWaypoint(`Set script source: {instance.Name}`)
return {
success = true,
instancePath = instancePath,
oldSourceLength = oldSourceLength,
newSourceLength = #sourceToSet,
method = "UpdateSourceAsync",
message = "Script source updated successfully (editor-safe)",
}
end)
if updateSuccess then
return updateResult
end
local directSuccess, directResult = pcall(function()
local oldSource = instance.Source
instance.Source = sourceToSet
ChangeHistoryService:SetWaypoint(`Set script source: {instance.Name}`)
return {
success = true,
instancePath = instancePath,
oldSourceLength = #oldSource,
newSourceLength = #sourceToSet,
method = "direct",
message = "Script source updated successfully (direct assignment)",
}
end)
if directSuccess then
return directResult
end
local replaceSuccess, replaceResult = pcall(function()
local parent = instance.Parent
local name = instance.Name
local className = instance.ClassName
local wasBaseScript = instance:IsA("BaseScript")
local enabled = if wasBaseScript then instance.Enabled else nil
local newScript = Instance.new(className)
newScript.Name = name
newScript.Source = sourceToSet
if wasBaseScript and enabled ~= nil then
newScript.Enabled = enabled
end
newScript.Parent = parent
instance:Destroy()
ChangeHistoryService:SetWaypoint(`Replace script: {name}`)
return {
success = true,
instancePath = getInstancePath(newScript),
method = "replace",
message = "Script replaced successfully with new source",
}
end)
if replaceSuccess then
return replaceResult
end
return {
error = `Failed to set script source. UpdateSourceAsync failed: {updateResult}. Direct assignment failed: {directResult}. Replace method failed: {replaceResult}`,
}
end
local function editScriptLines(requestData)
local instancePath = requestData.instancePath
local startLine = requestData.startLine
local endLine = requestData.endLine
local newContent = requestData.newContent
if not (instancePath ~= "" and instancePath) or not (startLine ~= 0 and startLine == startLine and startLine) or not (endLine ~= 0 and endLine == endLine and endLine) or not (newContent ~= "" and newContent) then
return {
error = "Instance path, startLine, endLine, and newContent are required",
}
end
newContent = normalizeEscapes(newContent)
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
if not instance:IsA("LuaSourceContainer") then
return {
error = `Instance is not a script-like object: {instance.ClassName}`,
}
end
local success, result = pcall(function()
local lines, hadTrailingNewline = splitLines(readScriptSource(instance))
local totalLines = #lines
if startLine < 1 or startLine > totalLines then
error(`startLine out of range (1-{totalLines})`)
end
if endLine < startLine or endLine > totalLines then
error(`endLine out of range ({startLine}-{totalLines})`)
end
local newLines = splitLines(newContent)
local resultLines = {}
do
local i = 0
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < startLine - 1) then
break
end
local _arg0 = lines[i + 1]
table.insert(resultLines, _arg0)
end
end
for _, line in newLines do
table.insert(resultLines, line)
end
do
local i = endLine
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < totalLines) then
break
end
local _arg0 = lines[i + 1]
table.insert(resultLines, _arg0)
end
end
local newSource = joinLines(resultLines, hadTrailingNewline)
ScriptEditorService:UpdateSourceAsync(instance, function()
return newSource
end)
ChangeHistoryService:SetWaypoint(`Edit script lines {startLine}-{endLine}: {instance.Name}`)
return {
success = true,
instancePath = instancePath,
editedLines = {
startLine = startLine,
endLine = endLine,
},
linesRemoved = endLine - startLine + 1,
linesAdded = #newLines,
newLineCount = #resultLines,
message = "Script lines edited successfully",
}
end)
if success then
return result
end
return {
error = `Failed to edit script lines: {result}`,
}
end
local function insertScriptLines(requestData)
local instancePath = requestData.instancePath
local _condition = (requestData.afterLine)
if _condition == nil then
_condition = 0
end
local afterLine = _condition
local newContent = requestData.newContent
if not (instancePath ~= "" and instancePath) or not (newContent ~= "" and newContent) then
return {
error = "Instance path and newContent are required",
}
end
newContent = normalizeEscapes(newContent)
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
if not instance:IsA("LuaSourceContainer") then
return {
error = `Instance is not a script-like object: {instance.ClassName}`,
}
end
local success, result = pcall(function()
local lines, hadTrailingNewline = splitLines(readScriptSource(instance))
local totalLines = #lines
if afterLine < 0 or afterLine > totalLines then
error(`afterLine out of range (0-{totalLines})`)
end
local newLines = splitLines(newContent)
local resultLines = {}
do
local i = 0
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < afterLine) then
break
end
local _arg0 = lines[i + 1]
table.insert(resultLines, _arg0)
end
end
for _, line in newLines do
table.insert(resultLines, line)
end
do
local i = afterLine
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < totalLines) then
break
end
local _arg0 = lines[i + 1]
table.insert(resultLines, _arg0)
end
end
local newSource = joinLines(resultLines, hadTrailingNewline)
ScriptEditorService:UpdateSourceAsync(instance, function()
return newSource
end)
ChangeHistoryService:SetWaypoint(`Insert script lines after line {afterLine}: {instance.Name}`)
return {
success = true,
instancePath = instancePath,
insertedAfterLine = afterLine,
linesInserted = #newLines,
newLineCount = #resultLines,
message = "Script lines inserted successfully",
}
end)
if success then
return result
end
return {
error = `Failed to insert script lines: {result}`,
}
end
local function deleteScriptLines(requestData)
local instancePath = requestData.instancePath
local startLine = requestData.startLine
local endLine = requestData.endLine
if not (instancePath ~= "" and instancePath) or not (startLine ~= 0 and startLine == startLine and startLine) or not (endLine ~= 0 and endLine == endLine and endLine) then
return {
error = "Instance path, startLine, and endLine are required",
}
end
local instance = getInstanceByPath(instancePath)
if not instance then
return {
error = `Instance not found: {instancePath}`,
}
end
if not instance:IsA("LuaSourceContainer") then
return {
error = `Instance is not a script-like object: {instance.ClassName}`,
}
end
local success, result = pcall(function()
local lines, hadTrailingNewline = splitLines(readScriptSource(instance))
local totalLines = #lines
if startLine < 1 or startLine > totalLines then
error(`startLine out of range (1-{totalLines})`)
end
if endLine < startLine or endLine > totalLines then
error(`endLine out of range ({startLine}-{totalLines})`)
end
local resultLines = {}
do
local i = 0
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < startLine - 1) then
break
end
local _arg0 = lines[i + 1]
table.insert(resultLines, _arg0)
end
end
do
local i = endLine
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < totalLines) then
break
end
local _arg0 = lines[i + 1]
table.insert(resultLines, _arg0)
end
end
local newSource = joinLines(resultLines, hadTrailingNewline)
ScriptEditorService:UpdateSourceAsync(instance, function()
return newSource
end)
ChangeHistoryService:SetWaypoint(`Delete script lines {startLine}-{endLine}: {instance.Name}`)
return {
success = true,
instancePath = instancePath,
deletedLines = {
startLine = startLine,
endLine = endLine,
},
linesDeleted = endLine - startLine + 1,
newLineCount = #resultLines,
message = "Script lines deleted successfully",
}
end)
if success then
return result
end
return {
error = `Failed to delete script lines: {result}`,
}
end
return {
getScriptSource = getScriptSource,
setScriptSource = setScriptSource,
editScriptLines = editScriptLines,
insertScriptLines = insertScriptLines,
deleteScriptLines = deleteScriptLines,
}
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="9">
<Properties>
<string name="Name">TestHandlers</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
local LogService = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services").LogService
local StudioTestService = game:GetService("StudioTestService")
local ServerScriptService = game:GetService("ServerScriptService")
local ScriptEditorService = game:GetService("ScriptEditorService")
local STOP_SIGNAL = "__MCP_STOP__"
local testRunning = false
local outputBuffer = {}
local logConnection
local testResult
local testError
local stopListenerScript
local function buildStopListenerSource()
return `local LogService = game:GetService("LogService")\
local StudioTestService = game:GetService("StudioTestService")\
LogService.MessageOut:Connect(function(message)\
if message == "{STOP_SIGNAL}" then\
pcall(function() StudioTestService:EndTest("stopped_by_mcp") end)\
end\
end)`
end
local function injectStopListener()
local listener = Instance.new("Script")
listener.Name = "__MCP_StopListener"
listener.Parent = ServerScriptService
local source = buildStopListenerSource()
local seOk = pcall(function()
ScriptEditorService:UpdateSourceAsync(listener, function()
return source
end)
end)
if not seOk then
listener.Source = source
end
stopListenerScript = listener
end
local function cleanupStopListener()
if stopListenerScript then
pcall(function()
return stopListenerScript:Destroy()
end)
stopListenerScript = nil
end
end
local function startPlaytest(requestData)
local mode = requestData.mode
if mode ~= "play" and mode ~= "run" then
return {
error = 'mode must be "play" or "run"',
}
end
if testRunning then
return {
error = "A test is already running",
}
end
testRunning = true
outputBuffer = {}
testResult = nil
testError = nil
cleanupStopListener()
logConnection = LogService.MessageOut:Connect(function(message, messageType)
if message == STOP_SIGNAL then
return nil
end
local _outputBuffer = outputBuffer
local _arg0 = {
message = message,
messageType = messageType.Name,
timestamp = tick(),
}
table.insert(_outputBuffer, _arg0)
end)
local injected, injErr = pcall(function()
return injectStopListener()
end)
if not injected then
warn(`[MCP] Failed to inject stop listener: {injErr}`)
end
task.spawn(function()
local ok, result = pcall(function()
if mode == "play" then
return StudioTestService:ExecutePlayModeAsync({})
end
return StudioTestService:ExecuteRunModeAsync({})
end)
if ok then
testResult = result
else
testError = tostring(result)
end
if logConnection then
logConnection:Disconnect()
logConnection = nil
end
testRunning = false
cleanupStopListener()
end)
return {
success = true,
message = `Playtest started in {mode} mode`,
}
end
local function stopPlaytest(_requestData)
if not testRunning then
return {
error = "No test is currently running",
}
end
warn(STOP_SIGNAL)
local _object = {
success = true,
}
local _left = "output"
local _array = {}
local _length = #_array
table.move(outputBuffer, 1, #outputBuffer, _length + 1, _array)
_object[_left] = _array
_object.outputCount = #outputBuffer
_object.message = "Playtest stop signal sent."
return _object
end
local function getPlaytestOutput(_requestData)
local _object = {
isRunning = testRunning,
}
local _left = "output"
local _array = {}
local _length = #_array
table.move(outputBuffer, 1, #outputBuffer, _length + 1, _array)
_object[_left] = _array
_object.outputCount = #outputBuffer
_object.testResult = if testResult ~= nil then tostring(testResult) else nil
_object.testError = testError
return _object
end
return {
startPlaytest = startPlaytest,
stopPlaytest = stopPlaytest,
getPlaytestOutput = getPlaytestOutput,
}
]]></string>
</Properties>
</Item>
</Item>
<Item class="ModuleScript" referent="10">
<Properties>
<string name="Name">State</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local CURRENT_VERSION = "2.4.0"
local MAX_CONNECTIONS = 5
local BASE_PORT = 58741
local activeTabIndex = 0
local function createConnection(port)
return {
port = port,
serverUrl = `http://localhost:{port}`,
isActive = false,
pollInterval = 0.5,
lastPoll = 0,
consecutiveFailures = 0,
maxFailuresBeforeError = 50,
lastSuccessfulConnection = 0,
currentRetryDelay = 0.5,
maxRetryDelay = 5,
retryBackoffMultiplier = 1.2,
lastHttpOk = false,
mcpWaitStartTime = nil,
isPolling = false,
heartbeatConnection = nil,
}
end
local connections = { createConnection(BASE_PORT) }
local function addConnection(port)
if #connections >= MAX_CONNECTIONS then
return nil
end
local lastPort = connections[#connections].port
local _condition = port
if _condition == nil then
_condition = lastPort + 1
end
local conn = createConnection(_condition)
table.insert(connections, conn)
return #connections - 1
end
local function removeConnection(index)
if #connections <= 1 then
return false
end
if index < 0 or index >= #connections then
return false
end
if connections[index + 1].isActive then
return false
end
local _index = index
table.remove(connections, _index + 1)
if activeTabIndex >= #connections then
activeTabIndex = #connections - 1
elseif activeTabIndex > index then
activeTabIndex -= 1
end
return true
end
local function getActiveConnection()
return connections[activeTabIndex + 1]
end
local function getConnection(index)
return connections[index + 1]
end
local function getActiveTabIndex()
return activeTabIndex
end
local function setActiveTabIndex(index)
activeTabIndex = index
end
local function getConnections()
return connections
end
return {
CURRENT_VERSION = CURRENT_VERSION,
MAX_CONNECTIONS = MAX_CONNECTIONS,
BASE_PORT = BASE_PORT,
connections = connections,
addConnection = addConnection,
removeConnection = removeConnection,
getActiveConnection = getActiveConnection,
getConnection = getConnection,
getActiveTabIndex = getActiveTabIndex,
setActiveTabIndex = setActiveTabIndex,
getConnections = getConnections,
}
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="11">
<Properties>
<string name="Name">UI</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local TS = require(script.Parent.Parent.include.RuntimeLib)
local TweenService = TS.import(script, script.Parent.Parent, "node_modules", "@rbxts", "services").TweenService
local State = TS.import(script, script.Parent, "State")
local elements = nil
local pulseAnimation
local buttonHover = false
local tabButtons = {}
local TWEEN_QUICK = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
local function tweenProp(instance, props)
TweenService:Create(instance, TWEEN_QUICK, props):Play()
end
local C = {
bg = Color3.fromRGB(14, 14, 14),
card = Color3.fromRGB(22, 22, 22),
surface = Color3.fromRGB(30, 30, 30),
border = Color3.fromRGB(38, 38, 38),
subtle = Color3.fromRGB(48, 48, 48),
muted = Color3.fromRGB(100, 100, 100),
dim = Color3.fromRGB(140, 140, 140),
label = Color3.fromRGB(180, 180, 180),
white = Color3.fromRGB(240, 240, 240),
green = Color3.fromRGB(52, 211, 153),
yellow = Color3.fromRGB(251, 191, 36),
red = Color3.fromRGB(248, 113, 113),
gray = Color3.fromRGB(120, 120, 120),
}
local CORNER = UDim.new(0, 4)
local function getStatusDotColor(connIndex)
local conn = State.getConnection(connIndex)
if not conn or not conn.isActive then
return C.red
end
if conn.consecutiveFailures >= conn.maxFailuresBeforeError then
return C.red
end
if conn.lastHttpOk then
return C.green
end
return C.yellow
end
local function setButtonConnect(btn, stroke)
btn.Text = "Connect"
btn.TextColor3 = C.white
btn.BackgroundColor3 = C.surface
stroke.Color = C.subtle
end
local function setButtonDisconnect(btn, stroke)
btn.Text = "Disconnect"
btn.TextColor3 = C.red
btn.BackgroundColor3 = C.bg
stroke.Color = Color3.fromRGB(80, 30, 30)
end
local function stopPulseAnimation()
elements.statusPulse.Size = UDim2.new(0, 10, 0, 10)
elements.statusPulse.Position = UDim2.new(0, 0, 0, 0)
elements.statusPulse.BackgroundTransparency = 0.7
end
local function startPulseAnimation()
elements.statusPulse.Size = UDim2.new(0, 10, 0, 10)
elements.statusPulse.Position = UDim2.new(0, 0, 0, 0)
elements.statusPulse.BackgroundTransparency = 0.7
end
local refreshTabBar
local switchToTab
local function createTabButton(connIndex)
local conn = State.getConnection(connIndex)
if not conn then
return nil
end
local isActive = connIndex == State.getActiveTabIndex()
local tabFrame = Instance.new("Frame")
tabFrame.Size = UDim2.new(0, 58, 1, -6)
tabFrame.Position = UDim2.new(0, 0, 0, 3)
tabFrame.BackgroundColor3 = if isActive then C.surface else C.bg
tabFrame.BackgroundTransparency = if isActive then 0 else 0.5
tabFrame.BorderSizePixel = 0
tabFrame.LayoutOrder = connIndex
local tabCorner = Instance.new("UICorner")
tabCorner.CornerRadius = UDim.new(0, 3)
tabCorner.Parent = tabFrame
local dot = Instance.new("Frame")
dot.Size = UDim2.new(0, 5, 0, 5)
dot.Position = UDim2.new(0, 6, 0.5, -2)
dot.BackgroundColor3 = getStatusDotColor(connIndex)
dot.BorderSizePixel = 0
dot.Parent = tabFrame
local dotCorner = Instance.new("UICorner")
dotCorner.CornerRadius = UDim.new(1, 0)
dotCorner.Parent = dot
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, -26, 1, 0)
label.Position = UDim2.new(0, 14, 0, 0)
label.BackgroundTransparency = 1
label.Text = tostring(conn.port)
label.TextColor3 = if isActive then C.label else C.muted
label.TextSize = 10
label.Font = Enum.Font.GothamMedium
label.TextXAlignment = Enum.TextXAlignment.Left
label.TextTruncate = Enum.TextTruncate.AtEnd
label.Parent = tabFrame
local closeBtn = Instance.new("TextButton")
closeBtn.Size = UDim2.new(0, 12, 0, 12)
closeBtn.Position = UDim2.new(1, -15, 0.5, -6)
closeBtn.BackgroundTransparency = 1
closeBtn.Text = "x"
closeBtn.TextColor3 = C.muted
closeBtn.TextSize = 8
closeBtn.Font = Enum.Font.GothamBold
closeBtn.Parent = tabFrame
local clickBtn = Instance.new("TextButton")
clickBtn.Size = UDim2.new(1, -14, 1, 0)
clickBtn.Position = UDim2.new(0, 0, 0, 0)
clickBtn.BackgroundTransparency = 1
clickBtn.Text = ""
clickBtn.Parent = tabFrame
clickBtn.Activated:Connect(function()
return switchToTab(connIndex)
end)
closeBtn.Activated:Connect(function()
local c = State.getConnection(connIndex)
if c and c.isActive then
return nil
end
if #State.getConnections() <= 1 then
return nil
end
State.removeConnection(connIndex)
refreshTabBar()
switchToTab(State.getActiveTabIndex())
end)
tabFrame.Parent = elements.tabBar
local _tabButtons = tabButtons
local _connIndex = connIndex
local _arg1 = {
frame = tabFrame,
label = label,
dot = dot,
closeBtn = closeBtn,
}
_tabButtons[_connIndex] = _arg1
end
refreshTabBar = function()
-- ▼ ReadonlyMap.forEach ▼
local _callback = function(tb)
if tb.frame then
tb.frame:Destroy()
end
end
for _k, _v in tabButtons do
_callback(_v, _k, tabButtons)
end
-- ▲ ReadonlyMap.forEach ▲
tabButtons = {}
for i = 0, #State.getConnections() - 1 do
createTabButton(i)
end
-- ▼ ReadonlyMap.forEach ▼
local _callback_1 = function(tb, i)
local active = i == State.getActiveTabIndex()
if tb.frame then
tb.frame.BackgroundColor3 = if active then C.surface else C.bg
tb.frame.BackgroundTransparency = if active then 0 else 0.5
end
if tb.label then
tb.label.TextColor3 = if active then C.label else C.muted
end
end
for _k, _v in tabButtons do
_callback_1(_v, _k, tabButtons)
end
-- ▲ ReadonlyMap.forEach ▲
end
local updateUIState
switchToTab = function(index)
if index < 0 or index >= #State.getConnections() then
return nil
end
State.setActiveTabIndex(index)
local conn = State.getActiveConnection()
-- ▼ ReadonlyMap.forEach ▼
local _callback = function(tb, i)
local active = i == index
if tb.frame then
tweenProp(tb.frame, {
BackgroundColor3 = if active then C.surface else C.bg,
BackgroundTransparency = if active then 0 else 0.5,
})
end
if tb.label then
tb.label.TextColor3 = if active then C.label else C.muted
end
end
for _k, _v in tabButtons do
_callback(_v, _k, tabButtons)
end
-- ▲ ReadonlyMap.forEach ▲
elements.urlInput.Text = conn.serverUrl
updateUIState()
end
local function updateTabDot(connIndex)
local _tabButtons = tabButtons
local _connIndex = connIndex
local tb = _tabButtons[_connIndex]
if tb and tb.dot then
tb.dot.BackgroundColor3 = getStatusDotColor(connIndex)
end
end
local function init(pluginRef)
local CURRENT_VERSION = State.CURRENT_VERSION
local screenGui = pluginRef:CreateDockWidgetPluginGuiAsync("MCPServerInterface", DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 300, 260, 260, 200))
screenGui.Title = `MCP Server v{CURRENT_VERSION}`
local mainFrame = Instance.new("Frame")
mainFrame.Size = UDim2.new(1, 0, 1, 0)
mainFrame.BackgroundColor3 = C.bg
mainFrame.BorderSizePixel = 0
mainFrame.Parent = screenGui
local header = Instance.new("Frame")
header.Size = UDim2.new(1, 0, 0, 40)
header.BackgroundColor3 = C.bg
header.BorderSizePixel = 0
header.Parent = mainFrame
local headerLine = Instance.new("Frame")
headerLine.Size = UDim2.new(1, -16, 0, 1)
headerLine.Position = UDim2.new(0, 8, 1, -1)
headerLine.BackgroundColor3 = C.border
headerLine.BorderSizePixel = 0
headerLine.Parent = header
local titleLabel = Instance.new("TextLabel")
titleLabel.Size = UDim2.new(1, -50, 0, 22)
titleLabel.Position = UDim2.new(0, 10, 0, 2)
titleLabel.BackgroundTransparency = 1
titleLabel.RichText = true
titleLabel.Text = `<font color="#F0F0F0">MCP</font> <font color="#646464">v{CURRENT_VERSION}</font>`
titleLabel.TextColor3 = C.white
titleLabel.TextSize = 12
titleLabel.Font = Enum.Font.GothamBold
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
titleLabel.Parent = header
local creditsLabel = Instance.new("TextLabel")
creditsLabel.Size = UDim2.new(1, -20, 0, 12)
creditsLabel.Position = UDim2.new(0, 10, 0, 23)
creditsLabel.BackgroundTransparency = 1
creditsLabel.RichText = true
creditsLabel.Text = '<font color="#999999">by</font> <font color="#CCCCCC">@BoshyDx</font> <font color="#666666">|</font> <font color="#999999">discord</font> <font color="#CCCCCC">boshyz</font>'
creditsLabel.TextColor3 = C.muted
creditsLabel.TextSize = 8
creditsLabel.Font = Enum.Font.GothamMedium
creditsLabel.TextXAlignment = Enum.TextXAlignment.Left
creditsLabel.Parent = header
local statusContainer = Instance.new("Frame")
statusContainer.Size = UDim2.new(0, 20, 0, 22)
statusContainer.Position = UDim2.new(1, -26, 0, 2)
statusContainer.BackgroundTransparency = 1
statusContainer.Parent = header
local statusIndicator = Instance.new("Frame")
statusIndicator.Size = UDim2.new(0, 8, 0, 8)
statusIndicator.Position = UDim2.new(0.5, -4, 0.5, -4)
statusIndicator.BackgroundColor3 = C.red
statusIndicator.BorderSizePixel = 0
statusIndicator.Parent = statusContainer
local statusCorner = Instance.new("UICorner")
statusCorner.CornerRadius = UDim.new(1, 0)
statusCorner.Parent = statusIndicator
local statusPulse = Instance.new("Frame")
statusPulse.Size = UDim2.new(0, 10, 0, 10)
statusPulse.Position = UDim2.new(0, 0, 0, 0)
statusPulse.BackgroundColor3 = C.red
statusPulse.BackgroundTransparency = 0.7
statusPulse.BorderSizePixel = 0
statusPulse.Parent = statusIndicator
local pulseCorner = Instance.new("UICorner")
pulseCorner.CornerRadius = UDim.new(1, 0)
pulseCorner.Parent = statusPulse
local statusText = Instance.new("TextLabel")
statusText.Size = UDim2.new(0, 0, 0, 0)
statusText.BackgroundTransparency = 1
statusText.Text = "OFFLINE"
statusText.TextTransparency = 1
statusText.TextSize = 1
statusText.Font = Enum.Font.GothamMedium
statusText.TextColor3 = C.white
statusText.Parent = statusContainer
local tabBar = Instance.new("Frame")
tabBar.Size = UDim2.new(1, 0, 0, 22)
tabBar.Position = UDim2.new(0, 0, 0, 40)
tabBar.BackgroundColor3 = C.bg
tabBar.BorderSizePixel = 0
tabBar.Parent = mainFrame
local tabBarLayout = Instance.new("UIListLayout")
tabBarLayout.FillDirection = Enum.FillDirection.Horizontal
tabBarLayout.Padding = UDim.new(0, 2)
tabBarLayout.SortOrder = Enum.SortOrder.LayoutOrder
tabBarLayout.VerticalAlignment = Enum.VerticalAlignment.Center
tabBarLayout.Parent = tabBar
local tabBarPadding = Instance.new("UIPadding")
tabBarPadding.PaddingLeft = UDim.new(0, 8)
tabBarPadding.PaddingRight = UDim.new(0, 8)
tabBarPadding.Parent = tabBar
local addTabBtn = Instance.new("TextButton")
addTabBtn.Size = UDim2.new(0, 18, 0, 18)
addTabBtn.BackgroundColor3 = C.surface
addTabBtn.BackgroundTransparency = 0.5
addTabBtn.BorderSizePixel = 0
addTabBtn.Text = "+"
addTabBtn.TextColor3 = C.muted
addTabBtn.TextSize = 12
addTabBtn.Font = Enum.Font.GothamMedium
addTabBtn.LayoutOrder = 999
addTabBtn.Parent = tabBar
local addTabCorner = Instance.new("UICorner")
addTabCorner.CornerRadius = UDim.new(0, 3)
addTabCorner.Parent = addTabBtn
addTabBtn.MouseEnter:Connect(function()
return tweenProp(addTabBtn, {
BackgroundTransparency = 0,
BackgroundColor3 = C.subtle,
})
end)
addTabBtn.MouseLeave:Connect(function()
return tweenProp(addTabBtn, {
BackgroundTransparency = 0.5,
BackgroundColor3 = C.surface,
})
end)
addTabBtn.Activated:Connect(function()
local newIndex = State.addConnection()
if newIndex ~= nil then
refreshTabBar()
switchToTab(newIndex)
end
end)
local updateBanner = Instance.new("Frame")
updateBanner.Size = UDim2.new(1, -16, 0, 24)
updateBanner.Position = UDim2.new(0, 8, 0, 64)
updateBanner.BackgroundColor3 = Color3.fromRGB(40, 32, 10)
updateBanner.BorderSizePixel = 0
updateBanner.Visible = false
updateBanner.Parent = mainFrame
local updateBannerCorner = Instance.new("UICorner")
updateBannerCorner.CornerRadius = UDim.new(0, 3)
updateBannerCorner.Parent = updateBanner
local updateBannerText = Instance.new("TextLabel")
updateBannerText.Size = UDim2.new(1, -16, 1, 0)
updateBannerText.Position = UDim2.new(0, 8, 0, 0)
updateBannerText.BackgroundTransparency = 1
updateBannerText.Text = ""
updateBannerText.TextColor3 = C.yellow
updateBannerText.TextSize = 9
updateBannerText.Font = Enum.Font.GothamMedium
updateBannerText.TextXAlignment = Enum.TextXAlignment.Left
updateBannerText.Parent = updateBanner
local contentY = 66
local contentFrame = Instance.new("ScrollingFrame")
contentFrame.Size = UDim2.new(1, -16, 1, -(contentY + 8))
contentFrame.Position = UDim2.new(0, 8, 0, contentY)
contentFrame.BackgroundTransparency = 1
contentFrame.BorderSizePixel = 0
contentFrame.ScrollBarThickness = 2
contentFrame.ScrollBarImageColor3 = C.subtle
contentFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
contentFrame.AutomaticCanvasSize = Enum.AutomaticSize.Y
contentFrame.Parent = mainFrame
local card = Instance.new("Frame")
card.Size = UDim2.new(1, 0, 0, 0)
card.AutomaticSize = Enum.AutomaticSize.Y
card.BackgroundColor3 = C.card
card.BorderSizePixel = 0
card.LayoutOrder = 1
card.Parent = contentFrame
local cardCorner = Instance.new("UICorner")
cardCorner.CornerRadius = CORNER
cardCorner.Parent = card
local cardPadding = Instance.new("UIPadding")
cardPadding.PaddingLeft = UDim.new(0, 10)
cardPadding.PaddingRight = UDim.new(0, 10)
cardPadding.PaddingTop = UDim.new(0, 8)
cardPadding.PaddingBottom = UDim.new(0, 10)
cardPadding.Parent = card
local cardLayout = Instance.new("UIListLayout")
cardLayout.Padding = UDim.new(0, 6)
cardLayout.SortOrder = Enum.SortOrder.LayoutOrder
cardLayout.Parent = card
local urlInput = Instance.new("TextBox")
urlInput.Size = UDim2.new(1, 0, 0, 26)
urlInput.BackgroundColor3 = C.bg
urlInput.BorderSizePixel = 0
urlInput.Text = "http://localhost:58741"
urlInput.TextColor3 = C.label
urlInput.TextSize = 11
urlInput.Font = Enum.Font.GothamMedium
urlInput.ClearTextOnFocus = false
urlInput.PlaceholderText = "Server URL..."
urlInput.PlaceholderColor3 = C.muted
urlInput.LayoutOrder = 1
urlInput.Parent = card
local urlCorner = Instance.new("UICorner")
urlCorner.CornerRadius = CORNER
urlCorner.Parent = urlInput
local urlPadding = Instance.new("UIPadding")
urlPadding.PaddingLeft = UDim.new(0, 8)
urlPadding.PaddingRight = UDim.new(0, 8)
urlPadding.Parent = urlInput
local statusRow = Instance.new("Frame")
statusRow.Size = UDim2.new(1, 0, 0, 14)
statusRow.BackgroundTransparency = 1
statusRow.LayoutOrder = 2
statusRow.Parent = card
local statusLabel = Instance.new("TextLabel")
statusLabel.Size = UDim2.new(1, 0, 1, 0)
statusLabel.BackgroundTransparency = 1
statusLabel.Text = "Disconnected"
statusLabel.TextColor3 = C.red
statusLabel.TextSize = 10
statusLabel.Font = Enum.Font.GothamBold
statusLabel.TextXAlignment = Enum.TextXAlignment.Left
statusLabel.TextWrapped = true
statusLabel.Parent = statusRow
local detailStatusLabel = Instance.new("TextLabel")
detailStatusLabel.Size = UDim2.new(0.5, 0, 1, 0)
detailStatusLabel.Position = UDim2.new(0.5, 0, 0, 0)
detailStatusLabel.BackgroundTransparency = 1
detailStatusLabel.Text = "HTTP: X MCP: X"
detailStatusLabel.TextColor3 = C.muted
detailStatusLabel.TextSize = 9
detailStatusLabel.Font = Enum.Font.GothamMedium
detailStatusLabel.TextXAlignment = Enum.TextXAlignment.Right
detailStatusLabel.TextWrapped = true
detailStatusLabel.Parent = statusRow
local stepsFrame = Instance.new("Frame")
stepsFrame.Size = UDim2.new(1, 0, 0, 0)
stepsFrame.AutomaticSize = Enum.AutomaticSize.Y
stepsFrame.BackgroundTransparency = 1
stepsFrame.LayoutOrder = 3
stepsFrame.Parent = card
local stepsLayout = Instance.new("UIListLayout")
stepsLayout.Padding = UDim.new(0, 1)
stepsLayout.FillDirection = Enum.FillDirection.Vertical
stepsLayout.SortOrder = Enum.SortOrder.LayoutOrder
stepsLayout.Parent = stepsFrame
local function createStepRow(text, order)
local row = Instance.new("Frame")
row.Size = UDim2.new(1, 0, 0, 13)
row.BackgroundTransparency = 1
row.LayoutOrder = order
local d = Instance.new("Frame")
d.Size = UDim2.new(0, 4, 0, 4)
d.Position = UDim2.new(0, 1, 0, 5)
d.BackgroundColor3 = C.gray
d.BorderSizePixel = 0
d.Parent = row
local dCorner = Instance.new("UICorner")
dCorner.CornerRadius = UDim.new(1, 0)
dCorner.Parent = d
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1, -12, 1, 0)
lbl.Position = UDim2.new(0, 12, 0, 0)
lbl.BackgroundTransparency = 1
lbl.Text = text
lbl.TextColor3 = C.dim
lbl.TextSize = 9
lbl.Font = Enum.Font.GothamMedium
lbl.TextXAlignment = Enum.TextXAlignment.Left
lbl.Parent = row
row.Parent = stepsFrame
return { row, d, lbl }
end
local _binding = createStepRow("HTTP server", 1)
local step1Dot = _binding[2]
local step1Label = _binding[3]
local _binding_1 = createStepRow("MCP bridge", 2)
local step2Dot = _binding_1[2]
local step2Label = _binding_1[3]
local _binding_2 = createStepRow("Commands", 3)
local step3Dot = _binding_2[2]
local step3Label = _binding_2[3]
local troubleshootLabel = Instance.new("TextLabel")
troubleshootLabel.Size = UDim2.new(1, 0, 0, 24)
troubleshootLabel.BackgroundTransparency = 1
troubleshootLabel.TextWrapped = true
troubleshootLabel.Visible = false
troubleshootLabel.Text = "MCP not responding. Close node.exe and restart server."
troubleshootLabel.TextColor3 = C.yellow
troubleshootLabel.TextSize = 9
troubleshootLabel.Font = Enum.Font.GothamMedium
troubleshootLabel.TextXAlignment = Enum.TextXAlignment.Left
troubleshootLabel.LayoutOrder = 4
troubleshootLabel.Parent = card
local connectButton = Instance.new("TextButton")
connectButton.Size = UDim2.new(1, 0, 0, 28)
connectButton.BackgroundColor3 = C.surface
connectButton.BackgroundTransparency = 0
connectButton.BorderSizePixel = 0
connectButton.Text = "Connect"
connectButton.TextColor3 = C.white
connectButton.TextSize = 11
connectButton.Font = Enum.Font.GothamBold
connectButton.LayoutOrder = 5
connectButton.Parent = card
local connectCorner = Instance.new("UICorner")
connectCorner.CornerRadius = CORNER
connectCorner.Parent = connectButton
local connectStroke = Instance.new("UIStroke")
connectStroke.Color = C.subtle
connectStroke.Thickness = 1
connectStroke.Parent = connectButton
connectButton.MouseEnter:Connect(function()
buttonHover = true
local conn = State.getActiveConnection()
if conn and conn.isActive then
tweenProp(connectButton, {
BackgroundColor3 = C.surface,
})
tweenProp(connectStroke, {
Color = Color3.fromRGB(100, 35, 35),
})
else
tweenProp(connectButton, {
BackgroundColor3 = C.subtle,
})
tweenProp(connectStroke, {
Color = C.muted,
})
end
end)
connectButton.MouseLeave:Connect(function()
buttonHover = false
local conn = State.getActiveConnection()
if conn and conn.isActive then
setButtonDisconnect(connectButton, connectStroke)
else
setButtonConnect(connectButton, connectStroke)
end
end)
elements = {
screenGui = screenGui,
mainFrame = mainFrame,
contentFrame = contentFrame,
statusLabel = statusLabel,
detailStatusLabel = detailStatusLabel,
statusIndicator = statusIndicator,
statusPulse = statusPulse,
statusText = statusText,
connectButton = connectButton,
connectStroke = connectStroke,
urlInput = urlInput,
step1Dot = step1Dot,
step1Label = step1Label,
step2Dot = step2Dot,
step2Label = step2Label,
step3Dot = step3Dot,
step3Label = step3Label,
troubleshootLabel = troubleshootLabel,
updateBanner = updateBanner,
updateBannerText = updateBannerText,
tabBar = tabBar,
}
refreshTabBar()
end
function updateUIState()
local conn = State.getActiveConnection()
if not conn then
return nil
end
local el = elements
if conn.isActive then
el.statusLabel.Text = "Connecting..."
el.statusLabel.TextColor3 = C.yellow
el.statusIndicator.BackgroundColor3 = C.yellow
el.statusPulse.BackgroundColor3 = C.yellow
el.statusText.Text = "CONNECTING"
el.detailStatusLabel.Text = if conn.consecutiveFailures == 0 then "..." else "HTTP: X MCP: X"
el.detailStatusLabel.TextColor3 = C.muted
startPulseAnimation()
el.step1Dot.BackgroundColor3 = C.yellow
el.step1Label.Text = "HTTP server (connecting...)"
el.step2Dot.BackgroundColor3 = C.yellow
el.step2Label.Text = "MCP bridge (connecting...)"
el.step3Dot.BackgroundColor3 = C.yellow
el.step3Label.Text = "Commands (connecting...)"
conn.mcpWaitStartTime = nil
el.troubleshootLabel.Visible = false
if not buttonHover then
setButtonDisconnect(el.connectButton, el.connectStroke)
end
el.urlInput.TextEditable = false
el.urlInput.BackgroundColor3 = C.card
else
el.statusLabel.Text = "Disconnected"
el.statusLabel.TextColor3 = C.muted
el.statusIndicator.BackgroundColor3 = C.red
el.statusPulse.BackgroundColor3 = C.red
el.statusText.Text = "OFFLINE"
el.detailStatusLabel.Text = ""
el.detailStatusLabel.TextColor3 = C.muted
stopPulseAnimation()
el.step1Dot.BackgroundColor3 = C.gray
el.step1Label.Text = "HTTP server"
el.step2Dot.BackgroundColor3 = C.gray
el.step2Label.Text = "MCP bridge"
el.step3Dot.BackgroundColor3 = C.gray
el.step3Label.Text = "Commands"
conn.mcpWaitStartTime = nil
el.troubleshootLabel.Visible = false
if not buttonHover then
setButtonConnect(el.connectButton, el.connectStroke)
end
el.urlInput.TextEditable = true
el.urlInput.BackgroundColor3 = C.bg
end
end
return {
elements = nil,
init = init,
updateUIState = updateUIState,
updateTabDot = updateTabDot,
stopPulseAnimation = stopPulseAnimation,
startPulseAnimation = startPulseAnimation,
getElements = function()
return elements
end,
}
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="12">
<Properties>
<string name="Name">Utils</string>
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
local ScriptEditorService = game:GetService("ScriptEditorService")
local function safeCall(func, ...)
local args = { ... }
local success, result = pcall(func, unpack(args))
if success then
return result
else
warn(`MCP Plugin Error: {result}`)
return nil
end
end
local function getInstancePath(instance)
if not instance or instance == game then
return "game"
end
local pathParts = {}
local current = instance
while current and current ~= game do
local _name = current.Name
table.insert(pathParts, 1, _name)
current = current.Parent
end
return `game.{table.concat(pathParts, ".")}`
end
local function getInstanceByPath(path)
if path == "game" or path == "" then
return game
end
local cleaned = (string.gsub(path, "^game%.", ""))
local parts = {}
for part in string.gmatch(cleaned, "[^%.]+") do
table.insert(parts, part)
end
local current = game
for _, part in parts do
local _result = current
if _result ~= nil then
_result = _result:FindFirstChild(part)
end
current = _result
if not current then
return nil
end
end
return current
end
local function splitLines(source)
local _condition = source
if _condition == nil then
_condition = ""
end
local normalized = (string.gsub((string.gsub(_condition, "\r\n", "\n")), "\r", "\n"))
local endsWithNewline = string.sub(normalized, -1) == "\n"
local lines = {}
local start = 1
while true do
local newlinePos = string.find(normalized, "\n", start, true)
if newlinePos ~= nil then
local _arg0 = string.sub(normalized, start, newlinePos - 1)
table.insert(lines, _arg0)
start = newlinePos + 1
else
local remainder = string.sub(normalized, start)
if remainder ~= "" or not endsWithNewline then
table.insert(lines, remainder)
end
break
end
end
if #lines == 0 then
table.insert(lines, "")
end
return lines, endsWithNewline
end
local function joinLines(lines, hadTrailingNewline)
local source = table.concat(lines, "\n")
if hadTrailingNewline and string.sub(source, -1) ~= "\n" then
source ..= "\n"
end
return source
end
local function readScriptSource(instance)
local ok, result = pcall(function()
local doc = ScriptEditorService:FindScriptDocument(instance)
if doc then
return doc:GetText()
end
return nil
end)
local _value = ok and result
if _value ~= "" and _value then
return result
end
return instance.Source
end
local function convertPropertyValue(instance, propertyName, propertyValue)
if propertyValue == nil then
return nil
end
local inst = instance
local _propertyValue = propertyValue
if type(_propertyValue) == "table" then
local arr = propertyValue
local tbl = propertyValue
if type(arr) == "table" and #arr > 0 then
local len = #arr
if len == 3 then
local prop = string.lower(propertyName)
if prop == "position" or prop == "size" or prop == "orientation" or prop == "velocity" or prop == "angularvelocity" then
local _condition = (arr[1])
if _condition == nil then
_condition = 0
end
local _condition_1 = (arr[2])
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (arr[3])
if _condition_2 == nil then
_condition_2 = 0
end
return Vector3.new(_condition, _condition_1, _condition_2)
elseif prop == "color" or prop == "color3" then
local _condition = (arr[1])
if _condition == nil then
_condition = 0
end
local _condition_1 = (arr[2])
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (arr[3])
if _condition_2 == nil then
_condition_2 = 0
end
return Color3.new(_condition, _condition_1, _condition_2)
else
local success, currentVal = pcall(function()
return inst[propertyName]
end)
if success then
if typeof(currentVal) == "Vector3" then
local _condition = (arr[1])
if _condition == nil then
_condition = 0
end
local _condition_1 = (arr[2])
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (arr[3])
if _condition_2 == nil then
_condition_2 = 0
end
return Vector3.new(_condition, _condition_1, _condition_2)
elseif typeof(currentVal) == "Color3" then
local _condition = (arr[1])
if _condition == nil then
_condition = 0
end
local _condition_1 = (arr[2])
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (arr[3])
if _condition_2 == nil then
_condition_2 = 0
end
return Color3.new(_condition, _condition_1, _condition_2)
end
end
end
elseif len == 2 then
local success, currentVal = pcall(function()
return inst[propertyName]
end)
if success and typeof(currentVal) == "Vector2" then
local _condition = (arr[1])
if _condition == nil then
_condition = 0
end
local _condition_1 = (arr[2])
if _condition_1 == nil then
_condition_1 = 0
end
return Vector2.new(_condition, _condition_1)
end
elseif len == 4 then
local success, currentVal = pcall(function()
return inst[propertyName]
end)
if success and typeof(currentVal) == "UDim2" then
local _condition = (arr[1])
if _condition == nil then
_condition = 0
end
local _condition_1 = (arr[2])
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (arr[3])
if _condition_2 == nil then
_condition_2 = 0
end
local _condition_3 = (arr[4])
if _condition_3 == nil then
_condition_3 = 0
end
return UDim2.new(_condition, _condition_1, _condition_2, _condition_3)
end
end
end
if tbl.X ~= nil or tbl.Y ~= nil or tbl.Z ~= nil then
local _x = tbl.X
local _condition = type(_x) == "table"
if _condition then
local _y = tbl.Y
_condition = type(_y) == "table"
end
if _condition then
local xTbl = tbl.X
local yTbl = tbl.Y
local _condition_1 = xTbl.Scale
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = xTbl.Offset
if _condition_2 == nil then
_condition_2 = 0
end
local _condition_3 = yTbl.Scale
if _condition_3 == nil then
_condition_3 = 0
end
local _condition_4 = yTbl.Offset
if _condition_4 == nil then
_condition_4 = 0
end
return UDim2.new(_condition_1, _condition_2, _condition_3, _condition_4)
end
local _condition_1 = (tbl.X)
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (tbl.Y)
if _condition_2 == nil then
_condition_2 = 0
end
local _condition_3 = (tbl.Z)
if _condition_3 == nil then
_condition_3 = 0
end
return Vector3.new(_condition_1, _condition_2, _condition_3)
end
if tbl.R ~= nil or tbl.G ~= nil or tbl.B ~= nil then
local _condition = (tbl.R)
if _condition == nil then
_condition = 0
end
local _condition_1 = (tbl.G)
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = (tbl.B)
if _condition_2 == nil then
_condition_2 = 0
end
return Color3.new(_condition, _condition_1, _condition_2)
end
end
local _propertyValue_1 = propertyValue
if type(_propertyValue_1) == "string" then
local success, currentVal = pcall(function()
return inst[propertyName]
end)
if success and typeof(currentVal) == "EnumItem" then
local enumItem = currentVal
local enumTypeName = tostring(enumItem.EnumType)
local enumSuccess, enumVal = pcall(function()
return Enum[enumTypeName][propertyValue]
end)
if enumSuccess and enumVal then
return enumVal
end
end
if propertyName == "BrickColor" then
return BrickColor.new(propertyValue)
end
if propertyValue == "true" then
return true
end
if propertyValue == "false" then
return false
end
end
return propertyValue
end
local function evaluateFormula(formula, variables, instance, index)
local value = formula
local _value = value
local _arg1 = tostring(index)
value = (string.gsub(_value, "index", _arg1))
if instance and instance:IsA("BasePart") then
local pos = instance.Position
local sz = instance.Size
local _value_1 = value
local _arg1_1 = tostring(pos.X)
value = (string.gsub(_value_1, "Position%.X", _arg1_1))
local _value_2 = value
local _arg1_2 = tostring(pos.Y)
value = (string.gsub(_value_2, "Position%.Y", _arg1_2))
local _value_3 = value
local _arg1_3 = tostring(pos.Z)
value = (string.gsub(_value_3, "Position%.Z", _arg1_3))
local _value_4 = value
local _arg1_4 = tostring(sz.X)
value = (string.gsub(_value_4, "Size%.X", _arg1_4))
local _value_5 = value
local _arg1_5 = tostring(sz.Y)
value = (string.gsub(_value_5, "Size%.Y", _arg1_5))
local _value_6 = value
local _arg1_6 = tostring(sz.Z)
value = (string.gsub(_value_6, "Size%.Z", _arg1_6))
local _value_7 = value
local _arg1_7 = tostring(pos.Magnitude)
value = (string.gsub(_value_7, "magnitude", _arg1_7))
end
if variables then
for k, v in pairs(variables) do
local _value_1 = value
local _arg1_1 = tostring(v)
value = (string.gsub(_value_1, k, _arg1_1))
end
end
value = (string.gsub(value, "sin%(([%d%.%-]+)%)", function(x)
local _condition = tonumber(x)
if _condition == nil then
_condition = 0
end
return tostring(math.sin(_condition))
end))
value = (string.gsub(value, "cos%(([%d%.%-]+)%)", function(x)
local _condition = tonumber(x)
if _condition == nil then
_condition = 0
end
return tostring(math.cos(_condition))
end))
value = (string.gsub(value, "sqrt%(([%d%.%-]+)%)", function(x)
local _condition = tonumber(x)
if _condition == nil then
_condition = 0
end
return tostring(math.sqrt(_condition))
end))
value = (string.gsub(value, "abs%(([%d%.%-]+)%)", function(x)
local _condition = tonumber(x)
if _condition == nil then
_condition = 0
end
return tostring(math.abs(_condition))
end))
value = (string.gsub(value, "floor%(([%d%.%-]+)%)", function(x)
local _condition = tonumber(x)
if _condition == nil then
_condition = 0
end
return tostring(math.floor(_condition))
end))
value = (string.gsub(value, "ceil%(([%d%.%-]+)%)", function(x)
local _condition = tonumber(x)
if _condition == nil then
_condition = 0
end
return tostring(math.ceil(_condition))
end))
local directResult = tonumber(value)
if directResult ~= nil then
return directResult, nil
end
local success, evalResult = pcall(function()
local num = tonumber(value)
if num ~= nil then
return num
end
do
local a, b = string.match(value, "^([%d%.%-]+)%s*%*%s*([%d%.%-]+)$")
local _condition = a
if _condition ~= "" and _condition then
_condition = b
end
if _condition ~= "" and _condition then
local _condition_1 = tonumber(a)
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = tonumber(b)
if _condition_2 == nil then
_condition_2 = 0
end
return _condition_1 * _condition_2
end
end
do
local a, b = string.match(value, "^([%d%.%-]+)%s*%+%s*([%d%.%-]+)$")
local _condition = a
if _condition ~= "" and _condition then
_condition = b
end
if _condition ~= "" and _condition then
local _condition_1 = tonumber(a)
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = tonumber(b)
if _condition_2 == nil then
_condition_2 = 0
end
return _condition_1 + _condition_2
end
end
do
local a, b = string.match(value, "^([%d%.%-]+)%s*%-%s*([%d%.%-]+)$")
local _condition = a
if _condition ~= "" and _condition then
_condition = b
end
if _condition ~= "" and _condition then
local _condition_1 = tonumber(a)
if _condition_1 == nil then
_condition_1 = 0
end
local _condition_2 = tonumber(b)
if _condition_2 == nil then
_condition_2 = 0
end
return _condition_1 - _condition_2
end
end
do
local a, b = string.match(value, "^([%d%.%-]+)%s*/%s*([%d%.%-]+)$")
local _condition = a
if _condition ~= "" and _condition then
_condition = b
end
if _condition ~= "" and _condition then
local _condition_1 = tonumber(b)
if _condition_1 == nil then
_condition_1 = 1
end
local divisor = _condition_1
if divisor ~= 0 then
local _condition_2 = tonumber(a)
if _condition_2 == nil then
_condition_2 = 0
end
return _condition_2 / divisor
end
end
end
error(`Unsupported formula pattern: {value}`)
end)
if success and type(evalResult) == "number" then
return evalResult, nil
else
return index, "Complex formulas not supported - using index value"
end
end
local function compareVersions(v1, v2)
local function parseVersion(v)
local parts = {}
for num in string.gmatch(v, "%d+") do
local _condition = tonumber(num)
if _condition == nil then
_condition = 0
end
table.insert(parts, _condition)
end
return parts
end
local p1 = parseVersion(v1)
local p2 = parseVersion(v2)
local maxLen = math.max(#p1, #p2)
do
local i = 0
local _shouldIncrement = false
while true do
if _shouldIncrement then
i += 1
else
_shouldIncrement = true
end
if not (i < maxLen) then
break
end
local _condition = p1[i + 1]
if _condition == nil then
_condition = 0
end
local n1 = _condition
local _condition_1 = p2[i + 1]
if _condition_1 == nil then
_condition_1 = 0
end
local n2 = _condition_1
if n1 < n2 then
return -1
end
if n1 > n2 then
return 1
end
end
end
return 0
end
return {
safeCall = safeCall,
getInstancePath = getInstancePath,
getInstanceByPath = getInstanceByPath,
splitLines = splitLines,
joinLines = joinLines,
readScriptSource = readScriptSource,
convertPropertyValue = convertPropertyValue,
evaluateFormula = evaluateFormula,
compareVersions = compareVersions,
}
]]></string>
</Properties>
</Item>
</Item>
<Item class="Folder" referent="16">
<Properties>
<string name="Name">include</string>
</Properties>
<Item class="ModuleScript" referent="13">
<Properties>
<string name="Name">Promise</string>
<string name="Source"><![CDATA[--[[
An implementation of Promises similar to Promise/A+.
]]
local ERROR_NON_PROMISE_IN_LIST = "Non-promise value passed into %s at index %s"
local ERROR_NON_LIST = "Please pass a list of promises to %s"
local ERROR_NON_FUNCTION = "Please pass a handler function to %s!"
local MODE_KEY_METATABLE = { __mode = "k" }
local function isCallable(value)
if type(value) == "function" then
return true
end
if type(value) == "table" then
local metatable = getmetatable(value)
if metatable and type(rawget(metatable, "__call")) == "function" then
return true
end
end
return false
end
--[[
Creates an enum dictionary with some metamethods to prevent common mistakes.
]]
local function makeEnum(enumName, members)
local enum = {}
for _, memberName in ipairs(members) do
enum[memberName] = memberName
end
return setmetatable(enum, {
__index = function(_, k)
error(string.format("%s is not in %s!", k, enumName), 2)
end,
__newindex = function()
error(string.format("Creating new members in %s is not allowed!", enumName), 2)
end,
})
end
--[=[
An object to represent runtime errors that occur during execution.
Promises that experience an error like this will be rejected with
an instance of this object.
@class Error
]=]
local Error
do
Error = {
Kind = makeEnum("Promise.Error.Kind", {
"ExecutionError",
"AlreadyCancelled",
"NotResolvedInTime",
"TimedOut",
}),
}
Error.__index = Error
function Error.new(options, parent)
options = options or {}
return setmetatable({
error = tostring(options.error) or "[This error has no error text.]",
trace = options.trace,
context = options.context,
kind = options.kind,
parent = parent,
createdTick = os.clock(),
createdTrace = debug.traceback(),
}, Error)
end
function Error.is(anything)
if type(anything) == "table" then
local metatable = getmetatable(anything)
if type(metatable) == "table" then
return rawget(anything, "error") ~= nil and type(rawget(metatable, "extend")) == "function"
end
end
return false
end
function Error.isKind(anything, kind)
assert(kind ~= nil, "Argument #2 to Promise.Error.isKind must not be nil")
return Error.is(anything) and anything.kind == kind
end
function Error:extend(options)
options = options or {}
options.kind = options.kind or self.kind
return Error.new(options, self)
end
function Error:getErrorChain()
local runtimeErrors = { self }
while runtimeErrors[#runtimeErrors].parent do
table.insert(runtimeErrors, runtimeErrors[#runtimeErrors].parent)
end
return runtimeErrors
end
function Error:__tostring()
local errorStrings = {
string.format("-- Promise.Error(%s) --", self.kind or "?"),
}
for _, runtimeError in ipairs(self:getErrorChain()) do
table.insert(
errorStrings,
table.concat({
runtimeError.trace or runtimeError.error,
runtimeError.context,
}, "\n")
)
end
return table.concat(errorStrings, "\n")
end
end
--[[
Packs a number of arguments into a table and returns its length.
Used to cajole varargs without dropping sparse values.
]]
local function pack(...)
return select("#", ...), { ... }
end
--[[
Returns first value (success), and packs all following values.
]]
local function packResult(success, ...)
return success, select("#", ...), { ... }
end
local function makeErrorHandler(traceback)
assert(traceback ~= nil, "traceback is nil")
return function(err)
-- If the error object is already a table, forward it directly.
-- Should we extend the error here and add our own trace?
if type(err) == "table" then
return err
end
return Error.new({
error = err,
kind = Error.Kind.ExecutionError,
trace = debug.traceback(tostring(err), 2),
context = "Promise created at:\n\n" .. traceback,
})
end
end
--[[
Calls a Promise executor with error handling.
]]
local function runExecutor(traceback, callback, ...)
return packResult(xpcall(callback, makeErrorHandler(traceback), ...))
end
--[[
Creates a function that invokes a callback with correct error handling and
resolution mechanisms.
]]
local function createAdvancer(traceback, callback, resolve, reject)
return function(...)
local ok, resultLength, result = runExecutor(traceback, callback, ...)
if ok then
resolve(unpack(result, 1, resultLength))
else
reject(result[1])
end
end
end
local function isEmpty(t)
return next(t) == nil
end
--[=[
An enum value used to represent the Promise's status.
@interface Status
@tag enum
@within Promise
.Started "Started" -- The Promise is executing, and not settled yet.
.Resolved "Resolved" -- The Promise finished successfully.
.Rejected "Rejected" -- The Promise was rejected.
.Cancelled "Cancelled" -- The Promise was cancelled before it finished.
]=]
--[=[
@prop Status Status
@within Promise
@readonly
@tag enums
A table containing all members of the `Status` enum, e.g., `Promise.Status.Resolved`.
]=]
--[=[
A Promise is an object that represents a value that will exist in the future, but doesn't right now.
Promises allow you to then attach callbacks that can run once the value becomes available (known as *resolving*),
or if an error has occurred (known as *rejecting*).
@class Promise
@__index prototype
]=]
local Promise = {
Error = Error,
Status = makeEnum("Promise.Status", { "Started", "Resolved", "Rejected", "Cancelled" }),
_getTime = os.clock,
_timeEvent = game:GetService("RunService").Heartbeat,
_unhandledRejectionCallbacks = {},
}
Promise.prototype = {}
Promise.__index = Promise.prototype
function Promise._new(traceback, callback, parent)
if parent ~= nil and not Promise.is(parent) then
error("Argument #2 to Promise.new must be a promise or nil", 2)
end
local self = {
-- The executor thread.
_thread = nil,
-- Used to locate where a promise was created
_source = traceback,
_status = Promise.Status.Started,
-- A table containing a list of all results, whether success or failure.
-- Only valid if _status is set to something besides Started
_values = nil,
-- Lua doesn't like sparse arrays very much, so we explicitly store the
-- length of _values to handle middle nils.
_valuesLength = -1,
-- Tracks if this Promise has no error observers..
_unhandledRejection = true,
-- Queues representing functions we should invoke when we update!
_queuedResolve = {},
_queuedReject = {},
_queuedFinally = {},
-- The function to run when/if this promise is cancelled.
_cancellationHook = nil,
-- The "parent" of this promise in a promise chain. Required for
-- cancellation propagation upstream.
_parent = parent,
-- Consumers are Promises that have chained onto this one.
-- We track them for cancellation propagation downstream.
_consumers = setmetatable({}, MODE_KEY_METATABLE),
}
if parent and parent._status == Promise.Status.Started then
parent._consumers[self] = true
end
setmetatable(self, Promise)
local function resolve(...)
self:_resolve(...)
end
local function reject(...)
self:_reject(...)
end
local function onCancel(cancellationHook)
if cancellationHook then
if self._status == Promise.Status.Cancelled then
cancellationHook()
else
self._cancellationHook = cancellationHook
end
end
return self._status == Promise.Status.Cancelled
end
self._thread = coroutine.create(function()
local ok, _, result = runExecutor(self._source, callback, resolve, reject, onCancel)
if not ok then
reject(result[1])
end
end)
task.spawn(self._thread)
return self
end
--[=[
Construct a new Promise that will be resolved or rejected with the given callbacks.
If you `resolve` with a Promise, it will be chained onto.
You can safely yield within the executor function and it will not block the creating thread.
```lua
local myFunction()
return Promise.new(function(resolve, reject, onCancel)
wait(1)
resolve("Hello world!")
end)
end
myFunction():andThen(print)
```
You do not need to use `pcall` within a Promise. Errors that occur during execution will be caught and turned into a rejection automatically. If `error()` is called with a table, that table will be the rejection value. Otherwise, string errors will be converted into `Promise.Error(Promise.Error.Kind.ExecutionError)` objects for tracking debug information.
You may register an optional cancellation hook by using the `onCancel` argument:
* This should be used to abort any ongoing operations leading up to the promise being settled.
* Call the `onCancel` function with a function callback as its only argument to set a hook which will in turn be called when/if the promise is cancelled.
* `onCancel` returns `true` if the Promise was already cancelled when you called `onCancel`.
* Calling `onCancel` with no argument will not override a previously set cancellation hook, but it will still return `true` if the Promise is currently cancelled.
* You can set the cancellation hook at any time before resolving.
* When a promise is cancelled, calls to `resolve` or `reject` will be ignored, regardless of if you set a cancellation hook or not.
:::caution
If the Promise is cancelled, the `executor` thread is closed with `coroutine.close` after the cancellation hook is called.
You must perform any cleanup code in the cancellation hook: any time your executor yields, it **may never resume**.
:::
@param executor (resolve: (...: any) -> (), reject: (...: any) -> (), onCancel: (abortHandler?: () -> ()) -> boolean) -> ()
@return Promise
]=]
function Promise.new(executor)
return Promise._new(debug.traceback(nil, 2), executor)
end
function Promise:__tostring()
return string.format("Promise(%s)", self._status)
end
--[=[
The same as [Promise.new](/api/Promise#new), except execution begins after the next `Heartbeat` event.
This is a spiritual replacement for `spawn`, but it does not suffer from the same [issues](https://eryn.io/gist/3db84579866c099cdd5bb2ff37947cec) as `spawn`.
```lua
local function waitForChild(instance, childName, timeout)
return Promise.defer(function(resolve, reject)
local child = instance:WaitForChild(childName, timeout)
;(child and resolve or reject)(child)
end)
end
```
@param executor (resolve: (...: any) -> (), reject: (...: any) -> (), onCancel: (abortHandler?: () -> ()) -> boolean) -> ()
@return Promise
]=]
function Promise.defer(executor)
local traceback = debug.traceback(nil, 2)
local promise
promise = Promise._new(traceback, function(resolve, reject, onCancel)
local connection
connection = Promise._timeEvent:Connect(function()
connection:Disconnect()
local ok, _, result = runExecutor(traceback, executor, resolve, reject, onCancel)
if not ok then
reject(result[1])
end
end)
end)
return promise
end
-- Backwards compatibility
Promise.async = Promise.defer
--[=[
Creates an immediately resolved Promise with the given value.
```lua
-- Example using Promise.resolve to deliver cached values:
function getSomething(name)
if cache[name] then
return Promise.resolve(cache[name])
else
return Promise.new(function(resolve, reject)
local thing = getTheThing()
cache[name] = thing
resolve(thing)
end)
end
end
```
@param ... any
@return Promise<...any>
]=]
function Promise.resolve(...)
local length, values = pack(...)
return Promise._new(debug.traceback(nil, 2), function(resolve)
resolve(unpack(values, 1, length))
end)
end
--[=[
Creates an immediately rejected Promise with the given value.
:::caution
Something needs to consume this rejection (i.e. `:catch()` it), otherwise it will emit an unhandled Promise rejection warning on the next frame. Thus, you should not create and store rejected Promises for later use. Only create them on-demand as needed.
:::
@param ... any
@return Promise<...any>
]=]
function Promise.reject(...)
local length, values = pack(...)
return Promise._new(debug.traceback(nil, 2), function(_, reject)
reject(unpack(values, 1, length))
end)
end
--[[
Runs a non-promise-returning function as a Promise with the
given arguments.
]]
function Promise._try(traceback, callback, ...)
local valuesLength, values = pack(...)
return Promise._new(traceback, function(resolve)
resolve(callback(unpack(values, 1, valuesLength)))
end)
end
--[=[
Begins a Promise chain, calling a function and returning a Promise resolving with its return value. If the function errors, the returned Promise will be rejected with the error. You can safely yield within the Promise.try callback.
:::info
`Promise.try` is similar to [Promise.promisify](#promisify), except the callback is invoked immediately instead of returning a new function.
:::
```lua
Promise.try(function()
return math.random(1, 2) == 1 and "ok" or error("Oh an error!")
end)
:andThen(function(text)
print(text)
end)
:catch(function(err)
warn("Something went wrong")
end)
```
@param callback (...: T...) -> ...any
@param ... T... -- Additional arguments passed to `callback`
@return Promise
]=]
function Promise.try(callback, ...)
return Promise._try(debug.traceback(nil, 2), callback, ...)
end
--[[
Returns a new promise that:
* is resolved when all input promises resolve
* is rejected if ANY input promises reject
]]
function Promise._all(traceback, promises, amount)
if type(promises) ~= "table" then
error(string.format(ERROR_NON_LIST, "Promise.all"), 3)
end
-- We need to check that each value is a promise here so that we can produce
-- a proper error rather than a rejected promise with our error.
for i, promise in pairs(promises) do
if not Promise.is(promise) then
error(string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.all", tostring(i)), 3)
end
end
-- If there are no values then return an already resolved promise.
if #promises == 0 or amount == 0 then
return Promise.resolve({})
end
return Promise._new(traceback, function(resolve, reject, onCancel)
-- An array to contain our resolved values from the given promises.
local resolvedValues = {}
local newPromises = {}
-- Keep a count of resolved promises because just checking the resolved
-- values length wouldn't account for promises that resolve with nil.
local resolvedCount = 0
local rejectedCount = 0
local done = false
local function cancel()
for _, promise in ipairs(newPromises) do
promise:cancel()
end
end
-- Called when a single value is resolved and resolves if all are done.
local function resolveOne(i, ...)
if done then
return
end
resolvedCount = resolvedCount + 1
if amount == nil then
resolvedValues[i] = ...
else
resolvedValues[resolvedCount] = ...
end
if resolvedCount >= (amount or #promises) then
done = true
resolve(resolvedValues)
cancel()
end
end
onCancel(cancel)
-- We can assume the values inside `promises` are all promises since we
-- checked above.
for i, promise in ipairs(promises) do
newPromises[i] = promise:andThen(function(...)
resolveOne(i, ...)
end, function(...)
rejectedCount = rejectedCount + 1
if amount == nil or #promises - rejectedCount < amount then
cancel()
done = true
reject(...)
end
end)
end
if done then
cancel()
end
end)
end
--[=[
Accepts an array of Promises and returns a new promise that:
* is resolved after all input promises resolve.
* is rejected if *any* input promises reject.
:::info
Only the first return value from each promise will be present in the resulting array.
:::
After any input Promise rejects, all other input Promises that are still pending will be cancelled if they have no other consumers.
```lua
local promises = {
returnsAPromise("example 1"),
returnsAPromise("example 2"),
returnsAPromise("example 3"),
}
return Promise.all(promises)
```
@param promises {Promise<T>}
@return Promise<{T}>
]=]
function Promise.all(promises)
return Promise._all(debug.traceback(nil, 2), promises)
end
--[=[
Folds an array of values or promises into a single value. The array is traversed sequentially.
The reducer function can return a promise or value directly. Each iteration receives the resolved value from the previous, and the first receives your defined initial value.
The folding will stop at the first rejection encountered.
```lua
local basket = {"blueberry", "melon", "pear", "melon"}
Promise.fold(basket, function(cost, fruit)
if fruit == "blueberry" then
return cost -- blueberries are free!
else
-- call a function that returns a promise with the fruit price
return fetchPrice(fruit):andThen(function(fruitCost)
return cost + fruitCost
end)
end
end, 0)
```
@since v3.1.0
@param list {T | Promise<T>}
@param reducer (accumulator: U, value: T, index: number) -> U | Promise<U>
@param initialValue U
]=]
function Promise.fold(list, reducer, initialValue)
assert(type(list) == "table", "Bad argument #1 to Promise.fold: must be a table")
assert(isCallable(reducer), "Bad argument #2 to Promise.fold: must be a function")
local accumulator = Promise.resolve(initialValue)
return Promise.each(list, function(resolvedElement, i)
accumulator = accumulator:andThen(function(previousValueResolved)
return reducer(previousValueResolved, resolvedElement, i)
end)
end):andThen(function()
return accumulator
end)
end
--[=[
Accepts an array of Promises and returns a Promise that is resolved as soon as `count` Promises are resolved from the input array. The resolved array values are in the order that the Promises resolved in. When this Promise resolves, all other pending Promises are cancelled if they have no other consumers.
`count` 0 results in an empty array. The resultant array will never have more than `count` elements.
```lua
local promises = {
returnsAPromise("example 1"),
returnsAPromise("example 2"),
returnsAPromise("example 3"),
}
return Promise.some(promises, 2) -- Only resolves with first 2 promises to resolve
```
@param promises {Promise<T>}
@param count number
@return Promise<{T}>
]=]
function Promise.some(promises, count)
assert(type(count) == "number", "Bad argument #2 to Promise.some: must be a number")
return Promise._all(debug.traceback(nil, 2), promises, count)
end
--[=[
Accepts an array of Promises and returns a Promise that is resolved as soon as *any* of the input Promises resolves. It will reject only if *all* input Promises reject. As soon as one Promises resolves, all other pending Promises are cancelled if they have no other consumers.
Resolves directly with the value of the first resolved Promise. This is essentially [[Promise.some]] with `1` count, except the Promise resolves with the value directly instead of an array with one element.
```lua
local promises = {
returnsAPromise("example 1"),
returnsAPromise("example 2"),
returnsAPromise("example 3"),
}
return Promise.any(promises) -- Resolves with first value to resolve (only rejects if all 3 rejected)
```
@param promises {Promise<T>}
@return Promise<T>
]=]
function Promise.any(promises)
return Promise._all(debug.traceback(nil, 2), promises, 1):andThen(function(values)
return values[1]
end)
end
--[=[
Accepts an array of Promises and returns a new Promise that resolves with an array of in-place Statuses when all input Promises have settled. This is equivalent to mapping `promise:finally` over the array of Promises.
```lua
local promises = {
returnsAPromise("example 1"),
returnsAPromise("example 2"),
returnsAPromise("example 3"),
}
return Promise.allSettled(promises)
```
@param promises {Promise<T>}
@return Promise<{Status}>
]=]
function Promise.allSettled(promises)
if type(promises) ~= "table" then
error(string.format(ERROR_NON_LIST, "Promise.allSettled"), 2)
end
-- We need to check that each value is a promise here so that we can produce
-- a proper error rather than a rejected promise with our error.
for i, promise in pairs(promises) do
if not Promise.is(promise) then
error(string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.allSettled", tostring(i)), 2)
end
end
-- If there are no values then return an already resolved promise.
if #promises == 0 then
return Promise.resolve({})
end
return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
-- An array to contain our resolved values from the given promises.
local fates = {}
local newPromises = {}
-- Keep a count of resolved promises because just checking the resolved
-- values length wouldn't account for promises that resolve with nil.
local finishedCount = 0
-- Called when a single value is resolved and resolves if all are done.
local function resolveOne(i, ...)
finishedCount = finishedCount + 1
fates[i] = ...
if finishedCount >= #promises then
resolve(fates)
end
end
onCancel(function()
for _, promise in ipairs(newPromises) do
promise:cancel()
end
end)
-- We can assume the values inside `promises` are all promises since we
-- checked above.
for i, promise in ipairs(promises) do
newPromises[i] = promise:finally(function(...)
resolveOne(i, ...)
end)
end
end)
end
--[=[
Accepts an array of Promises and returns a new promise that is resolved or rejected as soon as any Promise in the array resolves or rejects.
:::warning
If the first Promise to settle from the array settles with a rejection, the resulting Promise from `race` will reject.
If you instead want to tolerate rejections, and only care about at least one Promise resolving, you should use [Promise.any](#any) or [Promise.some](#some) instead.
:::
All other Promises that don't win the race will be cancelled if they have no other consumers.
```lua
local promises = {
returnsAPromise("example 1"),
returnsAPromise("example 2"),
returnsAPromise("example 3"),
}
return Promise.race(promises) -- Only returns 1st value to resolve or reject
```
@param promises {Promise<T>}
@return Promise<T>
]=]
function Promise.race(promises)
assert(type(promises) == "table", string.format(ERROR_NON_LIST, "Promise.race"))
for i, promise in pairs(promises) do
assert(Promise.is(promise), string.format(ERROR_NON_PROMISE_IN_LIST, "Promise.race", tostring(i)))
end
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
local newPromises = {}
local finished = false
local function cancel()
for _, promise in ipairs(newPromises) do
promise:cancel()
end
end
local function finalize(callback)
return function(...)
cancel()
finished = true
return callback(...)
end
end
if onCancel(finalize(reject)) then
return
end
for i, promise in ipairs(promises) do
newPromises[i] = promise:andThen(finalize(resolve), finalize(reject))
end
if finished then
cancel()
end
end)
end
--[=[
Iterates serially over the given an array of values, calling the predicate callback on each value before continuing.
If the predicate returns a Promise, we wait for that Promise to resolve before moving on to the next item
in the array.
:::info
`Promise.each` is similar to `Promise.all`, except the Promises are ran in order instead of all at once.
But because Promises are eager, by the time they are created, they're already running. Thus, we need a way to defer creation of each Promise until a later time.
The predicate function exists as a way for us to operate on our data instead of creating a new closure for each Promise. If you would prefer, you can pass in an array of functions, and in the predicate, call the function and return its return value.
:::
```lua
Promise.each({
"foo",
"bar",
"baz",
"qux"
}, function(value, index)
return Promise.delay(1):andThen(function()
print(("%d) Got %s!"):format(index, value))
end)
end)
--[[
(1 second passes)
> 1) Got foo!
(1 second passes)
> 2) Got bar!
(1 second passes)
> 3) Got baz!
(1 second passes)
> 4) Got qux!
]]
```
If the Promise a predicate returns rejects, the Promise from `Promise.each` is also rejected with the same value.
If the array of values contains a Promise, when we get to that point in the list, we wait for the Promise to resolve before calling the predicate with the value.
If a Promise in the array of values is already Rejected when `Promise.each` is called, `Promise.each` rejects with that value immediately (the predicate callback will never be called even once). If a Promise in the list is already Cancelled when `Promise.each` is called, `Promise.each` rejects with `Promise.Error(Promise.Error.Kind.AlreadyCancelled`). If a Promise in the array of values is Started at first, but later rejects, `Promise.each` will reject with that value and iteration will not continue once iteration encounters that value.
Returns a Promise containing an array of the returned/resolved values from the predicate for each item in the array of values.
If this Promise returned from `Promise.each` rejects or is cancelled for any reason, the following are true:
- Iteration will not continue.
- Any Promises within the array of values will now be cancelled if they have no other consumers.
- The Promise returned from the currently active predicate will be cancelled if it hasn't resolved yet.
@since 3.0.0
@param list {T | Promise<T>}
@param predicate (value: T, index: number) -> U | Promise<U>
@return Promise<{U}>
]=]
function Promise.each(list, predicate)
assert(type(list) == "table", string.format(ERROR_NON_LIST, "Promise.each"))
assert(isCallable(predicate), string.format(ERROR_NON_FUNCTION, "Promise.each"))
return Promise._new(debug.traceback(nil, 2), function(resolve, reject, onCancel)
local results = {}
local promisesToCancel = {}
local cancelled = false
local function cancel()
for _, promiseToCancel in ipairs(promisesToCancel) do
promiseToCancel:cancel()
end
end
onCancel(function()
cancelled = true
cancel()
end)
-- We need to preprocess the list of values and look for Promises.
-- If we find some, we must register our andThen calls now, so that those Promises have a consumer
-- from us registered. If we don't do this, those Promises might get cancelled by something else
-- before we get to them in the series because it's not possible to tell that we plan to use it
-- unless we indicate it here.
local preprocessedList = {}
for index, value in ipairs(list) do
if Promise.is(value) then
if value:getStatus() == Promise.Status.Cancelled then
cancel()
return reject(Error.new({
error = "Promise is cancelled",
kind = Error.Kind.AlreadyCancelled,
context = string.format(
"The Promise that was part of the array at index %d passed into Promise.each was already cancelled when Promise.each began.\n\nThat Promise was created at:\n\n%s",
index,
value._source
),
}))
elseif value:getStatus() == Promise.Status.Rejected then
cancel()
return reject(select(2, value:await()))
end
-- Chain a new Promise from this one so we only cancel ours
local ourPromise = value:andThen(function(...)
return ...
end)
table.insert(promisesToCancel, ourPromise)
preprocessedList[index] = ourPromise
else
preprocessedList[index] = value
end
end
for index, value in ipairs(preprocessedList) do
if Promise.is(value) then
local success
success, value = value:await()
if not success then
cancel()
return reject(value)
end
end
if cancelled then
return
end
local predicatePromise = Promise.resolve(predicate(value, index))
table.insert(promisesToCancel, predicatePromise)
local success, result = predicatePromise:await()
if not success then
cancel()
return reject(result)
end
results[index] = result
end
resolve(results)
end)
end
--[=[
Checks whether the given object is a Promise via duck typing. This only checks if the object is a table and has an `andThen` method.
@param object any
@return boolean -- `true` if the given `object` is a Promise.
]=]
function Promise.is(object)
if type(object) ~= "table" then
return false
end
local objectMetatable = getmetatable(object)
if objectMetatable == Promise then
-- The Promise came from this library.
return true
elseif objectMetatable == nil then
-- No metatable, but we should still chain onto tables with andThen methods
return isCallable(object.andThen)
elseif
type(objectMetatable) == "table"
and type(rawget(objectMetatable, "__index")) == "table"
and isCallable(rawget(rawget(objectMetatable, "__index"), "andThen"))
then
-- Maybe this came from a different or older Promise library.
return true
end
return false
end
--[=[
Wraps a function that yields into one that returns a Promise.
Any errors that occur while executing the function will be turned into rejections.
:::info
`Promise.promisify` is similar to [Promise.try](#try), except the callback is returned as a callable function instead of being invoked immediately.
:::
```lua
local sleep = Promise.promisify(wait)
sleep(1):andThen(print)
```
```lua
local isPlayerInGroup = Promise.promisify(function(player, groupId)
return player:IsInGroup(groupId)
end)
```
@param callback (...: any) -> ...any
@return (...: any) -> Promise
]=]
function Promise.promisify(callback)
return function(...)
return Promise._try(debug.traceback(nil, 2), callback, ...)
end
end
--[=[
Returns a Promise that resolves after `seconds` seconds have passed. The Promise resolves with the actual amount of time that was waited.
This function is **not** a wrapper around `wait`. `Promise.delay` uses a custom scheduler which provides more accurate timing. As an optimization, cancelling this Promise instantly removes the task from the scheduler.
:::warning
Passing `NaN`, infinity, or a number less than 1/60 is equivalent to passing 1/60.
:::
```lua
Promise.delay(5):andThenCall(print, "This prints after 5 seconds")
```
@function delay
@within Promise
@param seconds number
@return Promise<number>
]=]
do
-- uses a sorted doubly linked list (queue) to achieve O(1) remove operations and O(n) for insert
-- the initial node in the linked list
local first
local connection
function Promise.delay(seconds)
assert(type(seconds) == "number", "Bad argument #1 to Promise.delay, must be a number.")
-- If seconds is -INF, INF, NaN, or less than 1 / 60, assume seconds is 1 / 60.
-- This mirrors the behavior of wait()
if not (seconds >= 1 / 60) or seconds == math.huge then
seconds = 1 / 60
end
return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
local startTime = Promise._getTime()
local endTime = startTime + seconds
local node = {
resolve = resolve,
startTime = startTime,
endTime = endTime,
}
if connection == nil then -- first is nil when connection is nil
first = node
connection = Promise._timeEvent:Connect(function()
local threadStart = Promise._getTime()
while first ~= nil and first.endTime < threadStart do
local current = first
first = current.next
if first == nil then
connection:Disconnect()
connection = nil
else
first.previous = nil
end
current.resolve(Promise._getTime() - current.startTime)
end
end)
else -- first is non-nil
if first.endTime < endTime then -- if `node` should be placed after `first`
-- we will insert `node` between `current` and `next`
-- (i.e. after `current` if `next` is nil)
local current = first
local next = current.next
while next ~= nil and next.endTime < endTime do
current = next
next = current.next
end
-- `current` must be non-nil, but `next` could be `nil` (i.e. last item in list)
current.next = node
node.previous = current
if next ~= nil then
node.next = next
next.previous = node
end
else
-- set `node` to `first`
node.next = first
first.previous = node
first = node
end
end
onCancel(function()
-- remove node from queue
local next = node.next
if first == node then
if next == nil then -- if `node` is the first and last
connection:Disconnect()
connection = nil
else -- if `node` is `first` and not the last
next.previous = nil
end
first = next
else
local previous = node.previous
-- since `node` is not `first`, then we know `previous` is non-nil
previous.next = next
if next ~= nil then
next.previous = previous
end
end
end)
end)
end
end
--[=[
Returns a new Promise that resolves if the chained Promise resolves within `seconds` seconds, or rejects if execution time exceeds `seconds`. The chained Promise will be cancelled if the timeout is reached.
Rejects with `rejectionValue` if it is non-nil. If a `rejectionValue` is not given, it will reject with a `Promise.Error(Promise.Error.Kind.TimedOut)`. This can be checked with [[Error.isKind]].
```lua
getSomething():timeout(5):andThen(function(something)
-- got something and it only took at max 5 seconds
end):catch(function(e)
-- Either getting something failed or the time was exceeded.
if Promise.Error.isKind(e, Promise.Error.Kind.TimedOut) then
warn("Operation timed out!")
else
warn("Operation encountered an error!")
end
end)
```
Sugar for:
```lua
Promise.race({
Promise.delay(seconds):andThen(function()
return Promise.reject(
rejectionValue == nil
and Promise.Error.new({ kind = Promise.Error.Kind.TimedOut })
or rejectionValue
)
end),
promise
})
```
@param seconds number
@param rejectionValue? any -- The value to reject with if the timeout is reached
@return Promise
]=]
function Promise.prototype:timeout(seconds, rejectionValue)
local traceback = debug.traceback(nil, 2)
return Promise.race({
Promise.delay(seconds):andThen(function()
return Promise.reject(rejectionValue == nil and Error.new({
kind = Error.Kind.TimedOut,
error = "Timed out",
context = string.format(
"Timeout of %d seconds exceeded.\n:timeout() called at:\n\n%s",
seconds,
traceback
),
}) or rejectionValue)
end),
self,
})
end
--[=[
Returns the current Promise status.
@return Status
]=]
function Promise.prototype:getStatus()
return self._status
end
--[[
Creates a new promise that receives the result of this promise.
The given callbacks are invoked depending on that result.
]]
function Promise.prototype:_andThen(traceback, successHandler, failureHandler)
self._unhandledRejection = false
-- If we are already cancelled, we return a cancelled Promise
if self._status == Promise.Status.Cancelled then
local promise = Promise.new(function() end)
promise:cancel()
return promise
end
-- Create a new promise to follow this part of the chain
return Promise._new(traceback, function(resolve, reject, onCancel)
-- Our default callbacks just pass values onto the next promise.
-- This lets success and failure cascade correctly!
local successCallback = resolve
if successHandler then
successCallback = createAdvancer(traceback, successHandler, resolve, reject)
end
local failureCallback = reject
if failureHandler then
failureCallback = createAdvancer(traceback, failureHandler, resolve, reject)
end
if self._status == Promise.Status.Started then
-- If we haven't resolved yet, put ourselves into the queue
table.insert(self._queuedResolve, successCallback)
table.insert(self._queuedReject, failureCallback)
onCancel(function()
-- These are guaranteed to exist because the cancellation handler is guaranteed to only
-- be called at most once
if self._status == Promise.Status.Started then
table.remove(self._queuedResolve, table.find(self._queuedResolve, successCallback))
table.remove(self._queuedReject, table.find(self._queuedReject, failureCallback))
end
end)
elseif self._status == Promise.Status.Resolved then
-- This promise has already resolved! Trigger success immediately.
successCallback(unpack(self._values, 1, self._valuesLength))
elseif self._status == Promise.Status.Rejected then
-- This promise died a terrible death! Trigger failure immediately.
failureCallback(unpack(self._values, 1, self._valuesLength))
end
end, self)
end
--[=[
Chains onto an existing Promise and returns a new Promise.
:::warning
Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
:::
You can return a Promise from the success or failure handler and it will be chained onto.
Calling `andThen` on a cancelled Promise returns a cancelled Promise.
:::tip
If the Promise returned by `andThen` is cancelled, `successHandler` and `failureHandler` will not run.
To run code no matter what, use [Promise:finally].
:::
@param successHandler (...: any) -> ...any
@param failureHandler? (...: any) -> ...any
@return Promise<...any>
]=]
function Promise.prototype:andThen(successHandler, failureHandler)
assert(successHandler == nil or isCallable(successHandler), string.format(ERROR_NON_FUNCTION, "Promise:andThen"))
assert(failureHandler == nil or isCallable(failureHandler), string.format(ERROR_NON_FUNCTION, "Promise:andThen"))
return self:_andThen(debug.traceback(nil, 2), successHandler, failureHandler)
end
--[=[
Shorthand for `Promise:andThen(nil, failureHandler)`.
Returns a Promise that resolves if the `failureHandler` worked without encountering an additional error.
:::warning
Within the failure handler, you should never assume that the rejection value is a string. Some rejections within the Promise library are represented by [[Error]] objects. If you want to treat it as a string for debugging, you should call `tostring` on it first.
:::
Calling `catch` on a cancelled Promise returns a cancelled Promise.
:::tip
If the Promise returned by `catch` is cancelled, `failureHandler` will not run.
To run code no matter what, use [Promise:finally].
:::
@param failureHandler (...: any) -> ...any
@return Promise<...any>
]=]
function Promise.prototype:catch(failureHandler)
assert(failureHandler == nil or isCallable(failureHandler), string.format(ERROR_NON_FUNCTION, "Promise:catch"))
return self:_andThen(debug.traceback(nil, 2), nil, failureHandler)
end
--[=[
Similar to [Promise.andThen](#andThen), except the return value is the same as the value passed to the handler. In other words, you can insert a `:tap` into a Promise chain without affecting the value that downstream Promises receive.
```lua
getTheValue()
:tap(print)
:andThen(function(theValue)
print("Got", theValue, "even though print returns nil!")
end)
```
If you return a Promise from the tap handler callback, its value will be discarded but `tap` will still wait until it resolves before passing the original value through.
@param tapHandler (...: any) -> ...any
@return Promise<...any>
]=]
function Promise.prototype:tap(tapHandler)
assert(isCallable(tapHandler), string.format(ERROR_NON_FUNCTION, "Promise:tap"))
return self:_andThen(debug.traceback(nil, 2), function(...)
local callbackReturn = tapHandler(...)
if Promise.is(callbackReturn) then
local length, values = pack(...)
return callbackReturn:andThen(function()
return unpack(values, 1, length)
end)
end
return ...
end)
end
--[=[
Attaches an `andThen` handler to this Promise that calls the given callback with the predefined arguments. The resolved value is discarded.
```lua
promise:andThenCall(someFunction, "some", "arguments")
```
This is sugar for
```lua
promise:andThen(function()
return someFunction("some", "arguments")
end)
```
@param callback (...: any) -> any
@param ...? any -- Additional arguments which will be passed to `callback`
@return Promise
]=]
function Promise.prototype:andThenCall(callback, ...)
assert(isCallable(callback), string.format(ERROR_NON_FUNCTION, "Promise:andThenCall"))
local length, values = pack(...)
return self:_andThen(debug.traceback(nil, 2), function()
return callback(unpack(values, 1, length))
end)
end
--[=[
Attaches an `andThen` handler to this Promise that discards the resolved value and returns the given value from it.
```lua
promise:andThenReturn("some", "values")
```
This is sugar for
```lua
promise:andThen(function()
return "some", "values"
end)
```
:::caution
Promises are eager, so if you pass a Promise to `andThenReturn`, it will begin executing before `andThenReturn` is reached in the chain. Likewise, if you pass a Promise created from [[Promise.reject]] into `andThenReturn`, it's possible that this will trigger the unhandled rejection warning. If you need to return a Promise, it's usually best practice to use [[Promise.andThen]].
:::
@param ... any -- Values to return from the function
@return Promise
]=]
function Promise.prototype:andThenReturn(...)
local length, values = pack(...)
return self:_andThen(debug.traceback(nil, 2), function()
return unpack(values, 1, length)
end)
end
--[=[
Cancels this promise, preventing the promise from resolving or rejecting. Does not do anything if the promise is already settled.
Cancellations will propagate upwards and downwards through chained promises.
Promises will only be cancelled if all of their consumers are also cancelled. This is to say that if you call `andThen` twice on the same promise, and you cancel only one of the child promises, it will not cancel the parent promise until the other child promise is also cancelled.
```lua
promise:cancel()
```
]=]
function Promise.prototype:cancel()
if self._status ~= Promise.Status.Started then
return
end
self._status = Promise.Status.Cancelled
if self._cancellationHook then
self._cancellationHook()
end
coroutine.close(self._thread)
if self._parent then
self._parent:_consumerCancelled(self)
end
for child in pairs(self._consumers) do
child:cancel()
end
self:_finalize()
end
--[[
Used to decrease the number of consumers by 1, and if there are no more,
cancel this promise.
]]
function Promise.prototype:_consumerCancelled(consumer)
if self._status ~= Promise.Status.Started then
return
end
self._consumers[consumer] = nil
if next(self._consumers) == nil then
self:cancel()
end
end
--[[
Used to set a handler for when the promise resolves, rejects, or is
cancelled.
]]
function Promise.prototype:_finally(traceback, finallyHandler)
self._unhandledRejection = false
local promise = Promise._new(traceback, function(resolve, reject, onCancel)
local handlerPromise
onCancel(function()
-- The finally Promise is not a proper consumer of self. We don't care about the resolved value.
-- All we care about is running at the end. Therefore, if self has no other consumers, it's safe to
-- cancel. We don't need to hold out cancelling just because there's a finally handler.
self:_consumerCancelled(self)
if handlerPromise then
handlerPromise:cancel()
end
end)
local finallyCallback = resolve
if finallyHandler then
finallyCallback = function(...)
local callbackReturn = finallyHandler(...)
if Promise.is(callbackReturn) then
handlerPromise = callbackReturn
callbackReturn
:finally(function(status)
if status ~= Promise.Status.Rejected then
resolve(self)
end
end)
:catch(function(...)
reject(...)
end)
else
resolve(self)
end
end
end
if self._status == Promise.Status.Started then
-- The promise is not settled, so queue this.
table.insert(self._queuedFinally, finallyCallback)
else
-- The promise already settled or was cancelled, run the callback now.
finallyCallback(self._status)
end
end)
return promise
end
--[=[
Set a handler that will be called regardless of the promise's fate. The handler is called when the promise is
resolved, rejected, *or* cancelled.
Returns a new Promise that:
- resolves with the same values that this Promise resolves with.
- rejects with the same values that this Promise rejects with.
- is cancelled if this Promise is cancelled.
If the value you return from the handler is a Promise:
- We wait for the Promise to resolve, but we ultimately discard the resolved value.
- If the returned Promise rejects, the Promise returned from `finally` will reject with the rejected value from the
*returned* promise.
- If the `finally` Promise is cancelled, and you returned a Promise from the handler, we cancel that Promise too.
Otherwise, the return value from the `finally` handler is entirely discarded.
:::note Cancellation
As of Promise v4, `Promise:finally` does not count as a consumer of the parent Promise for cancellation purposes.
This means that if all of a Promise's consumers are cancelled and the only remaining callbacks are finally handlers,
the Promise is cancelled and the finally callbacks run then and there.
Cancellation still propagates through the `finally` Promise though: if you cancel the `finally` Promise, it can cancel
its parent Promise if it had no other consumers. Likewise, if the parent Promise is cancelled, the `finally` Promise
will also be cancelled.
:::
```lua
local thing = createSomething()
doSomethingWith(thing)
:andThen(function()
print("It worked!")
-- do something..
end)
:catch(function()
warn("Oh no it failed!")
end)
:finally(function()
-- either way, destroy thing
thing:Destroy()
end)
```
@param finallyHandler (status: Status) -> ...any
@return Promise<...any>
]=]
function Promise.prototype:finally(finallyHandler)
assert(finallyHandler == nil or isCallable(finallyHandler), string.format(ERROR_NON_FUNCTION, "Promise:finally"))
return self:_finally(debug.traceback(nil, 2), finallyHandler)
end
--[=[
Same as `andThenCall`, except for `finally`.
Attaches a `finally` handler to this Promise that calls the given callback with the predefined arguments.
@param callback (...: any) -> any
@param ...? any -- Additional arguments which will be passed to `callback`
@return Promise
]=]
function Promise.prototype:finallyCall(callback, ...)
assert(isCallable(callback), string.format(ERROR_NON_FUNCTION, "Promise:finallyCall"))
local length, values = pack(...)
return self:_finally(debug.traceback(nil, 2), function()
return callback(unpack(values, 1, length))
end)
end
--[=[
Attaches a `finally` handler to this Promise that discards the resolved value and returns the given value from it.
```lua
promise:finallyReturn("some", "values")
```
This is sugar for
```lua
promise:finally(function()
return "some", "values"
end)
```
@param ... any -- Values to return from the function
@return Promise
]=]
function Promise.prototype:finallyReturn(...)
local length, values = pack(...)
return self:_finally(debug.traceback(nil, 2), function()
return unpack(values, 1, length)
end)
end
--[=[
Yields the current thread until the given Promise completes. Returns the Promise's status, followed by the values that the promise resolved or rejected with.
@yields
@return Status -- The Status representing the fate of the Promise
@return ...any -- The values the Promise resolved or rejected with.
]=]
function Promise.prototype:awaitStatus()
self._unhandledRejection = false
if self._status == Promise.Status.Started then
local thread = coroutine.running()
self
:finally(function()
task.spawn(thread)
end)
-- The finally promise can propagate rejections, so we attach a catch handler to prevent the unhandled
-- rejection warning from appearing
:catch(
function() end
)
coroutine.yield()
end
if self._status == Promise.Status.Resolved then
return self._status, unpack(self._values, 1, self._valuesLength)
elseif self._status == Promise.Status.Rejected then
return self._status, unpack(self._values, 1, self._valuesLength)
end
return self._status
end
local function awaitHelper(status, ...)
return status == Promise.Status.Resolved, ...
end
--[=[
Yields the current thread until the given Promise completes. Returns true if the Promise resolved, followed by the values that the promise resolved or rejected with.
:::caution
If the Promise gets cancelled, this function will return `false`, which is indistinguishable from a rejection. If you need to differentiate, you should use [[Promise.awaitStatus]] instead.
:::
```lua
local worked, value = getTheValue():await()
if worked then
print("got", value)
else
warn("it failed")
end
```
@yields
@return boolean -- `true` if the Promise successfully resolved
@return ...any -- The values the Promise resolved or rejected with.
]=]
function Promise.prototype:await()
return awaitHelper(self:awaitStatus())
end
local function expectHelper(status, ...)
if status ~= Promise.Status.Resolved then
error((...) == nil and "Expected Promise rejected with no value." or (...), 3)
end
return ...
end
--[=[
Yields the current thread until the given Promise completes. Returns the values that the promise resolved with.
```lua
local worked = pcall(function()
print("got", getTheValue():expect())
end)
if not worked then
warn("it failed")
end
```
This is essentially sugar for:
```lua
select(2, assert(promise:await()))
```
**Errors** if the Promise rejects or gets cancelled.
@error any -- Errors with the rejection value if this Promise rejects or gets cancelled.
@yields
@return ...any -- The values the Promise resolved with.
]=]
function Promise.prototype:expect()
return expectHelper(self:awaitStatus())
end
-- Backwards compatibility
Promise.prototype.awaitValue = Promise.prototype.expect
--[[
Intended for use in tests.
Similar to await(), but instead of yielding if the promise is unresolved,
_unwrap will throw. This indicates an assumption that a promise has
resolved.
]]
function Promise.prototype:_unwrap()
if self._status == Promise.Status.Started then
error("Promise has not resolved or rejected.", 2)
end
local success = self._status == Promise.Status.Resolved
return success, unpack(self._values, 1, self._valuesLength)
end
function Promise.prototype:_resolve(...)
if self._status ~= Promise.Status.Started then
if Promise.is((...)) then
(...):_consumerCancelled(self)
end
return
end
-- If the resolved value was a Promise, we chain onto it!
if Promise.is((...)) then
-- Without this warning, arguments sometimes mysteriously disappear
if select("#", ...) > 1 then
local message = string.format(
"When returning a Promise from andThen, extra arguments are " .. "discarded! See:\n\n%s",
self._source
)
warn(message)
end
local chainedPromise = ...
local promise = chainedPromise:andThen(function(...)
self:_resolve(...)
end, function(...)
local maybeRuntimeError = chainedPromise._values[1]
-- Backwards compatibility < v2
if chainedPromise._error then
maybeRuntimeError = Error.new({
error = chainedPromise._error,
kind = Error.Kind.ExecutionError,
context = "[No stack trace available as this Promise originated from an older version of the Promise library (< v2)]",
})
end
if Error.isKind(maybeRuntimeError, Error.Kind.ExecutionError) then
return self:_reject(maybeRuntimeError:extend({
error = "This Promise was chained to a Promise that errored.",
trace = "",
context = string.format(
"The Promise at:\n\n%s\n...Rejected because it was chained to the following Promise, which encountered an error:\n",
self._source
),
}))
end
self:_reject(...)
end)
if promise._status == Promise.Status.Cancelled then
self:cancel()
elseif promise._status == Promise.Status.Started then
-- Adopt ourselves into promise for cancellation propagation.
self._parent = promise
promise._consumers[self] = true
end
return
end
self._status = Promise.Status.Resolved
self._valuesLength, self._values = pack(...)
-- We assume that these callbacks will not throw errors.
for _, callback in ipairs(self._queuedResolve) do
coroutine.wrap(callback)(...)
end
self:_finalize()
end
function Promise.prototype:_reject(...)
if self._status ~= Promise.Status.Started then
return
end
self._status = Promise.Status.Rejected
self._valuesLength, self._values = pack(...)
-- If there are any rejection handlers, call those!
if not isEmpty(self._queuedReject) then
-- We assume that these callbacks will not throw errors.
for _, callback in ipairs(self._queuedReject) do
coroutine.wrap(callback)(...)
end
else
-- At this point, no one was able to observe the error.
-- An error handler might still be attached if the error occurred
-- synchronously. We'll wait one tick, and if there are still no
-- observers, then we should put a message in the console.
local err = tostring((...))
coroutine.wrap(function()
Promise._timeEvent:Wait()
-- Someone observed the error, hooray!
if not self._unhandledRejection then
return
end
-- Build a reasonable message
local message = string.format("Unhandled Promise rejection:\n\n%s\n\n%s", err, self._source)
for _, callback in ipairs(Promise._unhandledRejectionCallbacks) do
task.spawn(callback, self, unpack(self._values, 1, self._valuesLength))
end
if Promise.TEST then
-- Don't spam output when we're running tests.
return
end
warn(message)
end)()
end
self:_finalize()
end
--[[
Calls any :finally handlers. We need this to be a separate method and
queue because we must call all of the finally callbacks upon a success,
failure, *and* cancellation.
]]
function Promise.prototype:_finalize()
for _, callback in ipairs(self._queuedFinally) do
-- Purposefully not passing values to callbacks here, as it could be the
-- resolved values, or rejected errors. If the developer needs the values,
-- they should use :andThen or :catch explicitly.
coroutine.wrap(callback)(self._status)
end
self._queuedFinally = nil
self._queuedReject = nil
self._queuedResolve = nil
-- Clear references to other Promises to allow gc
if not Promise.TEST then
self._parent = nil
self._consumers = nil
end
task.defer(coroutine.close, self._thread)
end
--[=[
Chains a Promise from this one that is resolved if this Promise is already resolved, and rejected if it is not resolved at the time of calling `:now()`. This can be used to ensure your `andThen` handler occurs on the same frame as the root Promise execution.
```lua
doSomething()
:now()
:andThen(function(value)
print("Got", value, "synchronously.")
end)
```
If this Promise is still running, Rejected, or Cancelled, the Promise returned from `:now()` will reject with the `rejectionValue` if passed, otherwise with a `Promise.Error(Promise.Error.Kind.NotResolvedInTime)`. This can be checked with [[Error.isKind]].
@param rejectionValue? any -- The value to reject with if the Promise isn't resolved
@return Promise
]=]
function Promise.prototype:now(rejectionValue)
local traceback = debug.traceback(nil, 2)
if self._status == Promise.Status.Resolved then
return self:_andThen(traceback, function(...)
return ...
end)
else
return Promise.reject(rejectionValue == nil and Error.new({
kind = Error.Kind.NotResolvedInTime,
error = "This Promise was not resolved in time for :now()",
context = ":now() was called at:\n\n" .. traceback,
}) or rejectionValue)
end
end
--[=[
Repeatedly calls a Promise-returning function up to `times` number of times, until the returned Promise resolves.
If the amount of retries is exceeded, the function will return the latest rejected Promise.
```lua
local function canFail(a, b, c)
return Promise.new(function(resolve, reject)
-- do something that can fail
local failed, thing = doSomethingThatCanFail(a, b, c)
if failed then
reject("it failed")
else
resolve(thing)
end
end)
end
local MAX_RETRIES = 10
local value = Promise.retry(canFail, MAX_RETRIES, "foo", "bar", "baz") -- args to send to canFail
```
@since 3.0.0
@param callback (...: P) -> Promise<T>
@param times number
@param ...? P
@return Promise<T>
]=]
function Promise.retry(callback, times, ...)
assert(isCallable(callback), "Parameter #1 to Promise.retry must be a function")
assert(type(times) == "number", "Parameter #2 to Promise.retry must be a number")
local args, length = { ... }, select("#", ...)
return Promise.resolve(callback(...)):catch(function(...)
if times > 0 then
return Promise.retry(callback, times - 1, unpack(args, 1, length))
else
return Promise.reject(...)
end
end)
end
--[=[
Repeatedly calls a Promise-returning function up to `times` number of times, waiting `seconds` seconds between each
retry, until the returned Promise resolves.
If the amount of retries is exceeded, the function will return the latest rejected Promise.
@since v3.2.0
@param callback (...: P) -> Promise<T>
@param times number
@param seconds number
@param ...? P
@return Promise<T>
]=]
function Promise.retryWithDelay(callback, times, seconds, ...)
assert(isCallable(callback), "Parameter #1 to Promise.retry must be a function")
assert(type(times) == "number", "Parameter #2 (times) to Promise.retry must be a number")
assert(type(seconds) == "number", "Parameter #3 (seconds) to Promise.retry must be a number")
local args, length = { ... }, select("#", ...)
return Promise.resolve(callback(...)):catch(function(...)
if times > 0 then
Promise.delay(seconds):await()
return Promise.retryWithDelay(callback, times - 1, seconds, unpack(args, 1, length))
else
return Promise.reject(...)
end
end)
end
--[=[
Converts an event into a Promise which resolves the next time the event fires.
The optional `predicate` callback, if passed, will receive the event arguments and should return `true` or `false`, based on if this fired event should resolve the Promise or not. If `true`, the Promise resolves. If `false`, nothing happens and the predicate will be rerun the next time the event fires.
The Promise will resolve with the event arguments.
:::tip
This function will work given any object with a `Connect` method. This includes all Roblox events.
:::
```lua
-- Creates a Promise which only resolves when `somePart` is touched
-- by a part named `"Something specific"`.
return Promise.fromEvent(somePart.Touched, function(part)
return part.Name == "Something specific"
end)
```
@since 3.0.0
@param event Event -- Any object with a `Connect` method. This includes all Roblox events.
@param predicate? (...: P) -> boolean -- A function which determines if the Promise should resolve with the given value, or wait for the next event to check again.
@return Promise<P>
]=]
function Promise.fromEvent(event, predicate)
predicate = predicate or function()
return true
end
return Promise._new(debug.traceback(nil, 2), function(resolve, _, onCancel)
local connection
local shouldDisconnect = false
local function disconnect()
connection:Disconnect()
connection = nil
end
-- We use shouldDisconnect because if the callback given to Connect is called before
-- Connect returns, connection will still be nil. This happens with events that queue up
-- events when there's nothing connected, such as RemoteEvents
connection = event:Connect(function(...)
local callbackValue = predicate(...)
if callbackValue == true then
resolve(...)
if connection then
disconnect()
else
shouldDisconnect = true
end
elseif type(callbackValue) ~= "boolean" then
error("Promise.fromEvent predicate should always return a boolean")
end
end)
if shouldDisconnect and connection then
return disconnect()
end
onCancel(disconnect)
end)
end
--[=[
Registers a callback that runs when an unhandled rejection happens. An unhandled rejection happens when a Promise
is rejected, and the rejection is not observed with `:catch`.
The callback is called with the actual promise that rejected, followed by the rejection values.
@since v3.2.0
@param callback (promise: Promise, ...: any) -- A callback that runs when an unhandled rejection happens.
@return () -> () -- Function that unregisters the `callback` when called
]=]
function Promise.onUnhandledRejection(callback)
table.insert(Promise._unhandledRejectionCallbacks, callback)
return function()
local index = table.find(Promise._unhandledRejectionCallbacks, callback)
if index then
table.remove(Promise._unhandledRejectionCallbacks, index)
end
end
end
return Promise
]]></string>
</Properties>
</Item>
<Item class="ModuleScript" referent="14">
<Properties>
<string name="Name">RuntimeLib</string>
<string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
local RunService = game:GetService("RunService")
local OUTPUT_PREFIX = "roblox-ts: "
local NODE_MODULES = "node_modules"
local DEFAULT_SCOPE = "@rbxts"
local TS = {}
TS.Promise = Promise
local function isPlugin(context)
return RunService:IsStudio() and context:FindFirstAncestorWhichIsA("Plugin") ~= nil
end
function TS.getModule(context, scope, moduleName)
-- legacy call signature
if moduleName == nil then
moduleName = scope
scope = DEFAULT_SCOPE
end
-- ensure modules have fully replicated
if RunService:IsRunning() and RunService:IsClient() and not isPlugin(context) and not game:IsLoaded() then
game.Loaded:Wait()
end
local object = context
repeat
local nodeModulesFolder = object:FindFirstChild(NODE_MODULES)
if nodeModulesFolder then
local scopeFolder = nodeModulesFolder:FindFirstChild(scope)
if scopeFolder then
local module = scopeFolder:FindFirstChild(moduleName)
if module then
return module
end
end
end
object = object.Parent
until object == nil
error(OUTPUT_PREFIX .. "Could not find module: " .. moduleName, 2)
end
-- This is a hash which TS.import uses as a kind of linked-list-like history of [Script who Loaded] -> Library
local currentlyLoading = {}
local registeredLibraries = {}
function TS.import(context, module, ...)
for i = 1, select("#", ...) do
module = module:WaitForChild((select(i, ...)))
end
if module.ClassName ~= "ModuleScript" then
error(OUTPUT_PREFIX .. "Failed to import! Expected ModuleScript, got " .. module.ClassName, 2)
end
currentlyLoading[context] = module
-- Check to see if a case like this occurs:
-- module -> Module1 -> Module2 -> module
-- WHERE currentlyLoading[module] is Module1
-- and currentlyLoading[Module1] is Module2
-- and currentlyLoading[Module2] is module
local currentModule = module
local depth = 0
while currentModule do
depth = depth + 1
currentModule = currentlyLoading[currentModule]
if currentModule == module then
local str = currentModule.Name -- Get the string traceback
for _ = 1, depth do
currentModule = currentlyLoading[currentModule]
str = str .. " ⇒ " .. currentModule.Name
end
error(OUTPUT_PREFIX .. "Failed to import! Detected a circular dependency chain: " .. str, 2)
end
end
if not registeredLibraries[module] then
if _G[module] then
error(
OUTPUT_PREFIX
.. "Invalid module access! Do you have multiple TS runtimes trying to import this? "
.. module:GetFullName(),
2
)
end
_G[module] = TS
registeredLibraries[module] = true -- register as already loaded for subsequent calls
end
local data = require(module)
if currentlyLoading[context] == module then -- Thread-safe cleanup!
currentlyLoading[context] = nil
end
return data
end
function TS.instanceof(obj, class)
-- custom Class.instanceof() check
if type(class) == "table" and type(class.instanceof) == "function" then
return class.instanceof(obj)
end
-- metatable check
if type(obj) == "table" then
obj = getmetatable(obj)
while obj ~= nil do
if obj == class then
return true
end
local mt = getmetatable(obj)
if mt then
obj = mt.__index
else
obj = nil
end
end
end
return false
end
function TS.async(callback)
return function(...)
local n = select("#", ...)
local args = { ... }
return Promise.new(function(resolve, reject)
coroutine.wrap(function()
local ok, result = pcall(callback, unpack(args, 1, n))
if ok then
resolve(result)
else
reject(result)
end
end)()
end)
end
end
function TS.await(promise)
if not Promise.is(promise) then
return promise
end
local status, value = promise:awaitStatus()
if status == Promise.Status.Resolved then
return value
elseif status == Promise.Status.Rejected then
error(value, 2)
else
error("The awaited Promise was cancelled", 2)
end
end
local SIGN = 2 ^ 31
local COMPLEMENT = 2 ^ 32
local function bit_sign(num)
-- Restores the sign after an unsigned conversion according to 2s complement.
if bit32.btest(num, SIGN) then
return num - COMPLEMENT
else
return num
end
end
function TS.bit_lrsh(a, b)
return bit_sign(bit32.arshift(a, b))
end
TS.TRY_RETURN = 1
TS.TRY_BREAK = 2
TS.TRY_CONTINUE = 3
function TS.try(try, catch, finally)
-- execute try
local trySuccess, exitTypeOrTryError, returns = pcall(try)
local exitType, tryError
if trySuccess then
exitType = exitTypeOrTryError
else
tryError = exitTypeOrTryError
end
local catchSuccess = true
local catchError
-- if try block failed, and catch block exists, execute catch
if not trySuccess and catch then
local newExitTypeOrCatchError, newReturns
catchSuccess, newExitTypeOrCatchError, newReturns = pcall(catch, tryError)
local newExitType
if catchSuccess then
newExitType = newExitTypeOrCatchError
else
catchError = newExitTypeOrCatchError
end
if newExitType then
exitType, returns = newExitType, newReturns
end
end
-- execute finally
if finally then
local newExitType, newReturns = finally()
if newExitType then
exitType, returns = newExitType, newReturns
end
end
-- if exit type is a control flow, do not rethrow errors
if exitType ~= TS.TRY_RETURN and exitType ~= TS.TRY_BREAK and exitType ~= TS.TRY_CONTINUE then
-- if catch block threw an error, rethrow it
if not catchSuccess then
error(catchError, 2)
end
-- if try block threw an error and there was no catch block, rethrow it
if not trySuccess and not catch then
error(tryError, 2)
end
end
return exitType, returns
end
function TS.generator(callback)
local co = coroutine.create(callback)
return {
next = function(...)
if coroutine.status(co) == "dead" then
return { done = true }
else
local success, value = coroutine.resume(co, ...)
if success == false then
error(value, 2)
end
return {
value = value,
done = coroutine.status(co) == "dead",
}
end
end,
}
end
return TS
]]></string>
</Properties>
</Item>
</Item>
<Item class="Folder" referent="17">
<Properties>
<string name="Name">node_modules</string>
</Properties>
<Item class="Folder" referent="18">
<Properties>
<string name="Name">@rbxts</string>
</Properties>
<Item class="ModuleScript" referent="15">
<Properties>
<string name="Name">services</string>
<string name="Source"><![CDATA[return setmetatable({}, {
__index = function(self, serviceName)
local service = game:GetService(serviceName)
self[serviceName] = service
return service
end,
})
]]></string>
</Properties>
</Item>
</Item>
</Item>
</Item>
</roblox>