Dominus 3 currentPart end end return false end local clientId = getSetting("Dominus2ClientId", nil) if type(clientId) ~= "string" then clientId = HttpService:GenerateGUID(false) pcall(function() plugin:SetSetting("Dominus2ClientId", clientId) end) end local bridgeToken = getSetting("Dominus2BridgeToken", nil) if type(EmbeddedBridgeToken) == "string" and #EmbeddedBridgeToken >= MIN_BRIDGE_TOKEN_LENGTH then bridgeToken = EmbeddedBridgeToken pcall(function() plugin:SetSetting("Dominus2BridgeToken", bridgeToken) end) end local port = tonumber(getSetting("Dominus2Port", DEFAULT_PORT)) or DEFAULT_PORT local codeExecutionEnabled = PluginSettings.getCodeExecutionEnabled(plugin) local rulesScope = if game.GameId > 0 then "universe-" .. tostring(game.GameId) elseif game.PlaceId > 0 then "place-" .. tostring(game.PlaceId) else "unpublished" local pluginRules = PluginSettings.getRules(plugin, rulesScope) CodeRunner.configure(function() return codeExecutionEnabled end) luauButton:SetActive(codeExecutionEnabled) local function log(message) print("[romcp] " .. message) end local function sendEnvelope(message) local encodedOk, encoded = pcall(Protocol.encode, message) if not encodedOk then return false, tostring(encoded) end if #encoded > MAX_MESSAGE_BYTES then return false, "Message exceeds one MiB" end return WsClient.send(encoded) end local function sendResponse(id, payload) return sendEnvelope({ id = id, type = "studio:response", payload = payload, ts = os.time() }) end local function sendSettings() if not authenticated then return end local sent, sendError = sendEnvelope({ id = Protocol.generateId(), type = "studio:settings", payload = { pluginRules = pluginRules, codeExecutionEnabled = codeExecutionEnabled, }, ts = os.time(), }) if not sent then warn("[romcp] Could not sync plugin settings: " .. tostring(sendError)) end end local function setCodeExecutionEnabled(enabled) local saved, saveError = PluginSettings.setCodeExecutionEnabled(plugin, enabled) if not saved then error("Could not save the Luau execution setting: " .. tostring(saveError)) end codeExecutionEnabled = enabled == true luauButton:SetActive(codeExecutionEnabled) if panelController then panelController:setCodeExecutionEnabled(codeExecutionEnabled) end if codeExecutionEnabled then warn("[romcp] Luau execution ENABLED. Generated code can make arbitrary Studio changes; use Undo if needed.") else log("Luau execution disabled") end sendSettings() end local function setPluginRules(rules) local saved, saveError = PluginSettings.setRules(plugin, rules, rulesScope) if not saved then error("Could not save project rules: " .. tostring(saveError)) end pluginRules = rules sendSettings() end local scheduleReconnect local function stopSession() authenticated = false transportOpen = false Watcher.stop() if panelController then panelController:setConnectionStatus("disconnected", "Waiting for the local bridge") end end local function showUpdateAvailable(payload) if type(payload) ~= "table" or type(payload.currentVersion) ~= "string" or type(payload.latestVersion) ~= "string" then return end local currentVersion = PluginVersion.version if type(currentVersion) ~= "string" or not string.match(currentVersion, "^%d+%.%d+%.%d+$") then currentVersion = payload.currentVersion end if not isNewerVersion(currentVersion, payload.latestVersion) then return end panelController:setUpdateAvailable(currentVersion, payload.latestVersion) warn( string.format( "[romcp] Update available: %s → %s. Run `dominus update`, then reload Studio.", currentVersion, payload.latestVersion ) ) if getSetting("Dominus2LastUpdateNotice", "") ~= payload.latestVersion then pcall(function() plugin:SetSetting("Dominus2LastUpdateNotice", payload.latestVersion) end) panelController.widget.Enabled = true panelButton:SetActive(true) end end local function handleMessage(rawMessage) if #rawMessage > MAX_MESSAGE_BYTES then warn("[romcp] Rejected an oversized server message") WsClient.disconnect() return end local decodedOk, message = pcall(Protocol.decode, rawMessage) if not decodedOk or type(message) ~= "table" then warn("[romcp] Rejected an invalid server message") return end if message.type == "server:authenticated" then local payload = message.payload or {} if payload.protocolVersion ~= PROTOCOL_VERSION or type(payload.connectionId) ~= "string" then warn("[romcp] Server returned an invalid authentication response") WsClient.disconnect() return end authenticated = true if panelController then panelController:setConnectionStatus("connected", game.Name .. " · authenticated") end log("Authenticated as Studio session " .. payload.connectionId) if payload.updateAvailable then showUpdateAvailable(payload.updateAvailable) end Watcher.start(function(entry) if authenticated then sendEnvelope({ id = Protocol.generateId(), type = "studio:output", payload = entry, ts = os.time() }) end end) return end if message.type == "server:auth_rejected" then warn("[romcp] Authentication failed. Run dominus setup to refresh and verify the local plugin.") WsClient.disconnect() return end if message.type == "server:update_available" then if authenticated then showUpdateAvailable(message.payload) end return end if not authenticated then warn("[romcp] Ignored a command before authentication") return end CommandRouter.handle(message, sendResponse) end local function connect() if shuttingDown or transportOpen then return end if connecting then if shouldReconnect then scheduleReconnect() end return end connecting = true reconnectScheduled = false local success = WsClient.connect("ws://127.0.0.1:" .. port, { onOpen = function() transportOpen = true reconnectDelay = 1 if panelController then panelController:setConnectionStatus("connecting", "Authenticating with the local bridge") end local sent, sendErr = sendEnvelope({ id = Protocol.generateId(), type = "studio:hello", payload = { protocolVersion = PROTOCOL_VERSION, clientId = clientId, token = bridgeToken, studioVersion = version(), placeId = game.PlaceId, universeId = game.GameId, placeName = game.Name, pluginRules = pluginRules, codeExecutionEnabled = codeExecutionEnabled, }, ts = os.time(), }) if not sent then warn("[romcp] Could not send handshake: " .. tostring(sendErr)) stopSession() WsClient.disconnect() end end, onMessage = function(message) task.spawn(handleMessage, message) end, onClose = function() local wasOpen = transportOpen stopSession() if wasOpen and not shuttingDown then log("Studio bridge disconnected") end if shouldReconnect then scheduleReconnect() end end, onError = function(statusCode, errorMessage) if transportOpen then warn("[romcp] Bridge error " .. tostring(statusCode) .. ": " .. tostring(errorMessage)) stopSession() WsClient.disconnect() if shouldReconnect then scheduleReconnect() end end end, }, 4) connecting = false if not success then stopSession() if shouldReconnect then scheduleReconnect() end end end scheduleReconnect = function() if reconnectScheduled or shuttingDown or not shouldReconnect then return end reconnectScheduled = true local delaySeconds = reconnectDelay reconnectDelay = math.min(reconnectDelay * 2, 30) task.delay(delaySeconds, function() reconnectScheduled = false if shouldReconnect and not shuttingDown then task.spawn(connect) end end) end local function forceReconnect() shouldReconnect = true reconnectDelay = 1 stopSession() WsClient.disconnect() log("Reconnecting to the romcp MCP server") task.spawn(function() while connecting and not shuttingDown do task.wait() end connect() end) end panelController = PluginPanel.create(plugin, { initialCodeExecutionEnabled = codeExecutionEnabled, initialRules = pluginRules, onCodeExecutionChanged = setCodeExecutionEnabled, onReconnect = forceReconnect, onRulesChanged = setPluginRules, }) panelButton.Click:Connect(function() panelController:toggle() end) panelController.widget:GetPropertyChangedSignal("Enabled"):Connect(function() panelButton:SetActive(panelController.widget.Enabled) end) luauButton.Click:Connect(function() local ok, toggleError = pcall(setCodeExecutionEnabled, not codeExecutionEnabled) if not ok then warn("[romcp] " .. tostring(toggleError)) end end) task.delay(1, function() task.spawn(connect) end) plugin.Unloading:Connect(function() shuttingDown = true shouldReconnect = false stopSession() WsClient.disconnect() end) ]]> AssetResolver 0 then textureId = value break end end end -- LoadAsset returns an unparented Model; drop it so nothing leaks into the place. pcall(function() loaded:Destroy() end) if not textureId then return { success = false, assetId = assetId, error = "Loaded asset contained no image-bearing instance", found = found, } end return { success = true, assetId = assetId, textureId = textureId } end return AssetResolver ]]> BridgeConfig Building = minimum and value <= maximum, label .. " must be from " .. minimum .. " to " .. maximum) return value end local function vector3(value, label, default) if value == nil then return default end assert(type(value) == "table", label .. " must be a Vector3 object") return Vector3.new( finiteNumber(value.X or value.x or value[1], label .. ".x"), finiteNumber(value.Y or value.y or value[2], label .. ".y"), finiteNumber(value.Z or value.z or value[3], label .. ".z") ) end local function boundedVector3(value, label, default, minimum, maximum) local result = vector3(value, label, default) assert(result ~= nil, label .. " is required") assert( result.X >= minimum and result.X <= maximum and result.Y >= minimum and result.Y <= maximum and result.Z >= minimum and result.Z <= maximum, label .. " components must be from " .. minimum .. " to " .. maximum ) return result end local function rotation(value, label) local degrees = vector3(value, label, Vector3.zero) return CFrame.fromOrientation(math.rad(degrees.X), math.rad(degrees.Y), math.rad(degrees.Z)) end local function setProperties(instance, properties) if properties == nil then return {} end assert(type(properties) == "table", "properties must be an object") local count = 0 local applied = {} for propertyName, value in properties do count += 1 if count > MAX_PROPERTIES then error("At most " .. MAX_PROPERTIES .. " properties are allowed per item") end applied[propertyName] = ValueCodec.setProperty(instance, propertyName, value) end return applied end local function validateItemCount(items, label, maximum) assert(type(items) == "table" and #items > 0, label .. " must contain at least one item") assert(#items <= maximum, label .. " is limited to " .. maximum .. " items") end local function assertMovable(instance, label) assert(instance ~= game and instance.Parent ~= game, label .. " cannot be a DataModel service") end function Building.createParts(spec) validateItemCount(spec.parts, "parts", MAX_ITEMS) local parent = spec.parent and resolve(spec.parent, "parent") or workspace local group = nil local created = {} return record("Dominus 2: Build parts", function() if spec.groupName ~= nil then assert(type(spec.groupName) == "string" and #spec.groupName > 0, "groupName must be a non-empty string") local groupClass = spec.groupClass or "Model" assert(groupClass == "Model" or groupClass == "Folder", "groupClass must be Model or Folder") group = Instance.new(groupClass) group.Name = spec.groupName table.insert(created, group) end local parts = {} for index, item in spec.parts do assert(type(item) == "table", "Part " .. index .. " must be an object") assert( type(item.name) == "string" and #item.name > 0, "Part " .. index .. ".name must be a non-empty string" ) local className = item.className or "Part" if item.shape == "Wedge" then className = "WedgePart" elseif item.shape == "CornerWedge" then className = "CornerWedgePart" end assert( ALLOWED_PART_CLASSES[className] == true, "Part " .. index .. " uses an unsupported class: " .. tostring(className) ) local part = Instance.new(className) table.insert(created, part) part.Name = item.name if part:IsA("BasePart") then part.Anchored = item.anchored ~= false end if item.position then part.Position = vector3(item.position, "Part " .. index .. ".position") end if item.orientation then part.Orientation = vector3(item.orientation, "Part " .. index .. ".orientation") end if item.size then part.Size = vector3(item.size, "Part " .. index .. ".size") end if item.color then ValueCodec.setProperty(part, "Color", item.color) end if item.material then ValueCodec.setProperty(part, "Material", item.material) end if item.transparency ~= nil then part.Transparency = finiteNumber(item.transparency, "Part " .. index .. ".transparency") end if item.canCollide ~= nil then assert(type(item.canCollide) == "boolean", "Part " .. index .. ".canCollide must be a boolean") part.CanCollide = item.canCollide end if item.shape and className == "Part" then ValueCodec.setProperty(part, "Shape", item.shape) end setProperties(part, item.properties) part.Parent = group or parent table.insert(parts, part) end if group then group.Parent = parent end local refs = {} for _, part in parts do table.insert(refs, InstanceRegistry.toRef(part)) end return { created = refs, group = group and InstanceRegistry.toRef(group) or nil, count = #refs, } end, created) end local function boundedInteger(value, label, minimum, maximum) assert( type(value) == "number" and value % 1 == 0 and value >= minimum and value <= maximum, label .. " must be an integer from " .. minimum .. " to " .. maximum ) return value end local function programPartCount(generator, label) assert(type(generator) == "table", label .. " must be an object") assert(PROGRAM_KINDS[generator.kind] == true, label .. ".kind is not supported") if generator.kind == "grid" then assert(type(generator.counts) == "table", label .. ".counts must be an object") local x = boundedInteger(generator.counts.x, label .. ".counts.x", 1, 50) local y = boundedInteger(generator.counts.y, label .. ".counts.y", 1, 50) local z = boundedInteger(generator.counts.z, label .. ".counts.z", 1, 50) local count = x * y * z assert(count <= 500, label .. " is limited to 500 grid parts") return count end local minimum = generator.kind == "line" and 1 or 2 return boundedInteger(generator.count, label .. ".count", minimum, 500) end local function mapFromYAxis(value, axis, label) axis = axis or "Y" if axis == "Y" then return value elseif axis == "X" then return Vector3.new(value.Y, value.Z, value.X) elseif axis == "Z" then return Vector3.new(value.Z, value.X, value.Y) end error(label .. ".axis must be X, Y, or Z") end local function lerpVector(left, right, alpha) return left + (right - left) * alpha end local function safeDirection(direction, fallback) if direction.Magnitude > 0.000001 then return direction.Unit end return fallback.Unit end local function placementCFrame(generator, index, position, tangent, outward) local mode = generator.orientationMode or "fixed" local base if mode == "fixed" then base = CFrame.new(position) else local direction if mode == "tangent" then direction = tangent elseif mode == "outward" then direction = outward elseif mode == "inward" then direction = -outward else error("orientationMode must be fixed, tangent, outward, or inward") end direction = safeDirection(direction, Vector3.zAxis) local up = math.abs(direction:Dot(Vector3.yAxis)) > 0.98 and Vector3.zAxis or Vector3.yAxis base = CFrame.lookAt(position, position + direction, up) end local template = generator.part local orientationOffset = vector3(template.orientationOffset, "part.orientationOffset", Vector3.zero) local rotationPerStep = vector3(template.rotationPerStep, "part.rotationPerStep", Vector3.zero) local step = index - 1 return base * CFrame.fromOrientation( math.rad(orientationOffset.X + rotationPerStep.X * step), math.rad(orientationOffset.Y + rotationPerStep.Y * step), math.rad(orientationOffset.Z + rotationPerStep.Z * step) ) end local function programPoint(generator, index, count, label) local zeroIndex = index - 1 local alpha = count == 1 and 0 or zeroIndex / (count - 1) if generator.kind == "line" then local start = boundedVector3(generator.start, label .. ".start", nil, -MAX_PROGRAM_COORDINATE, MAX_PROGRAM_COORDINATE) local finish = boundedVector3(generator["end"], label .. ".end", nil, -MAX_PROGRAM_COORDINATE, MAX_PROGRAM_COORDINATE) local position = lerpVector(start, finish, alpha) local tangent = safeDirection(finish - start, Vector3.yAxis) return position, tangent, safeDirection(position - (start + finish) / 2, tangent), alpha elseif generator.kind == "helix" then local origin = boundedVector3(generator.origin, label .. ".origin", nil, -MAX_PROGRAM_COORDINATE, MAX_PROGRAM_COORDINATE) local radiusStart = boundedNumber(generator.radiusStart, label .. ".radiusStart", 0, MAX_PROGRAM_DIMENSION) local radiusEnd = boundedNumber(generator.radiusEnd or radiusStart, label .. ".radiusEnd", 0, MAX_PROGRAM_DIMENSION) local height = boundedNumber(generator.height, label .. ".height", -MAX_PROGRAM_DIMENSION, MAX_PROGRAM_DIMENSION) local turns = finiteNumber(generator.turns, label .. ".turns") assert(math.abs(turns) > 0.0001 and math.abs(turns) <= 50, label .. ".turns must be non-zero and at most 50") local startAngle = math.rad(finiteNumber(generator.startAngleDegrees or 0, label .. ".startAngleDegrees")) local angleDelta = math.pi * 2 * turns local angle = startAngle + angleDelta * alpha local radius = radiusStart + (radiusEnd - radiusStart) * alpha local localPosition = Vector3.new(math.cos(angle) * radius, height * alpha, math.sin(angle) * radius) local localTangent = Vector3.new( -math.sin(angle) * radius * angleDelta + math.cos(angle) * (radiusEnd - radiusStart), height, math.cos(angle) * radius * angleDelta + math.sin(angle) * (radiusEnd - radiusStart) ) local localOutward = Vector3.new(math.cos(angle), 0, math.sin(angle)) return origin + mapFromYAxis(localPosition, generator.axis, label), mapFromYAxis(localTangent, generator.axis, label), mapFromYAxis(localOutward, generator.axis, label), alpha elseif generator.kind == "ring" then local origin = boundedVector3(generator.origin, label .. ".origin", nil, -MAX_PROGRAM_COORDINATE, MAX_PROGRAM_COORDINATE) local radius = boundedNumber(generator.radius, label .. ".radius", 0.000001, MAX_PROGRAM_DIMENSION) local divisor = generator.closed == false and math.max(1, count - 1) or count local progress = zeroIndex / divisor local arc = math.rad(boundedNumber(generator.arcDegrees or 360, label .. ".arcDegrees", -3600, 3600)) local angle = math.rad(finiteNumber(generator.startAngleDegrees or 0, label .. ".startAngleDegrees")) + arc * progress local localPosition = Vector3.new(math.cos(angle) * radius, 0, math.sin(angle) * radius) local localTangent = Vector3.new(-math.sin(angle), 0, math.cos(angle)) * arc local localOutward = Vector3.new(math.cos(angle), 0, math.sin(angle)) return origin + mapFromYAxis(localPosition, generator.axis, label), mapFromYAxis(localTangent, generator.axis, label), mapFromYAxis(localOutward, generator.axis, label), progress elseif generator.kind == "grid" then local origin = boundedVector3(generator.origin, label .. ".origin", nil, -MAX_PROGRAM_COORDINATE, MAX_PROGRAM_COORDINATE) local spacing = boundedVector3(generator.spacing, label .. ".spacing", nil, -MAX_PROGRAM_DIMENSION, MAX_PROGRAM_DIMENSION) local xCount = generator.counts.x local yCount = generator.counts.y local zCount = generator.counts.z local xIndex = zeroIndex % xCount local yIndex = math.floor(zeroIndex / xCount) % yCount local zIndex = math.floor(zeroIndex / (xCount * yCount)) local centerOffset = generator.centered == false and Vector3.zero or Vector3.new((xCount - 1) / 2, (yCount - 1) / 2, (zCount - 1) / 2) local gridOffset = Vector3.new(xIndex, yIndex, zIndex) - centerOffset local position = origin + Vector3.new(gridOffset.X * spacing.X, gridOffset.Y * spacing.Y, gridOffset.Z * spacing.Z) return position, Vector3.yAxis, safeDirection(position - origin, Vector3.zAxis), alpha elseif generator.kind == "fibonacciSphere" then local origin = boundedVector3(generator.origin, label .. ".origin", nil, -MAX_PROGRAM_COORDINATE, MAX_PROGRAM_COORDINATE) local radii = boundedVector3(generator.radii, label .. ".radii", nil, 0.000001, MAX_PROGRAM_DIMENSION) assert(radii.X > 0 and radii.Y > 0 and radii.Z > 0, label .. ".radii components must be positive") local normalizedY = 1 - 2 * ((zeroIndex + 0.5) / count) local horizontal = math.sqrt(math.max(0, 1 - normalizedY * normalizedY)) local goldenAngle = math.pi * (3 - math.sqrt(5)) local angle = math.rad(finiteNumber(generator.startAngleDegrees or 0, label .. ".startAngleDegrees")) + goldenAngle * zeroIndex local unit = Vector3.new(math.cos(angle) * horizontal, normalizedY, math.sin(angle) * horizontal) local position = origin + Vector3.new(unit.X * radii.X, unit.Y * radii.Y, unit.Z * radii.Z) local tangent = Vector3.new(-math.sin(angle), 0, math.cos(angle)) return position, tangent, unit, alpha end error(label .. ".kind must be line, helix, ring, grid, or fibonacciSphere") end local function generatedPart(template, name, cframe, size, created) assert(type(template) == "table", "generator.part must be an object") local className = template.className or "Part" if template.shape == "Wedge" then className = "WedgePart" elseif template.shape == "CornerWedge" then className = "CornerWedgePart" end assert(ALLOWED_PART_CLASSES[className] == true, "generator.part uses an unsupported class: " .. tostring(className)) assert( size.X > 0 and size.X <= MAX_PROGRAM_DIMENSION and size.Y > 0 and size.Y <= MAX_PROGRAM_DIMENSION and size.Z > 0 and size.Z <= MAX_PROGRAM_DIMENSION, "generated part sizes must be positive and at most " .. MAX_PROGRAM_DIMENSION ) local part = Instance.new(className) table.insert(created, part) part.Name = name part.Anchored = template.anchored ~= false part.Size = size part.CFrame = cframe if template.color then ValueCodec.setProperty(part, "Color", template.color) end if template.material then ValueCodec.setProperty(part, "Material", template.material) end if template.transparency ~= nil then part.Transparency = boundedNumber(template.transparency, "generator.part.transparency", 0, 1) end if template.canCollide ~= nil then assert(type(template.canCollide) == "boolean", "generator.part.canCollide must be a boolean") part.CanCollide = template.canCollide end if template.shape and className == "Part" then ValueCodec.setProperty(part, "Shape", template.shape) end setProperties(part, template.properties) return part end function Building.runBuildProgram(spec) validateItemCount(spec.generators, "generators", MAX_PROGRAM_GENERATORS) assert( type(spec.groupName) == "string" and #spec.groupName > 0 and #spec.groupName <= 100, "groupName must be a non-empty string of at most 100 characters" ) local totalCount = 0 local counts = {} for index, generator in spec.generators do assert( type(generator.namePrefix) == "string" and #generator.namePrefix > 0 and #generator.namePrefix <= 80, "Generator " .. index .. ".namePrefix must be 1 to 80 characters" ) assert(type(generator.part) == "table", "Generator " .. index .. ".part must be an object") local count = programPartCount(generator, "Generator " .. index) totalCount += count assert(totalCount <= MAX_PROGRAM_PARTS, "A build program is limited to " .. MAX_PROGRAM_PARTS .. " parts") counts[index] = count end local parent = spec.parent and resolve(spec.parent, "parent") or workspace local group = Instance.new("Model") group.Name = spec.groupName local createdForCleanup = { group } local result = record("Dominus 2: Run build program", function() local returnedRefs = {} local generatorReports = {} local generatedCount = 0 for generatorIndex, generator in spec.generators do local count = counts[generatorIndex] local template = generator.part local startSize = vector3(template.size, "Generator " .. generatorIndex .. ".part.size") local endSize = vector3(template.endSize, "Generator " .. generatorIndex .. ".part.endSize", startSize) local firstRef = nil local lastRef = nil for itemIndex = 1, count do local position, tangent, outward, alpha = programPoint(generator, itemIndex, count, "Generator " .. generatorIndex) local size = lerpVector(startSize, endSize, alpha) local cframe = placementCFrame(generator, itemIndex, position, tangent, outward) local name = generator.namePrefix .. " " .. string.format("%03d", itemIndex) local part = generatedPart(template, name, cframe, size, createdForCleanup) part.Parent = group generatedCount += 1 local needsReturnedRef = #returnedRefs < MAX_PROGRAM_RETURNED_REFS local partRef = nil if itemIndex == 1 or itemIndex == count or needsReturnedRef then partRef = InstanceRegistry.toRef(part) end firstRef = firstRef or partRef if itemIndex == count then lastRef = partRef end if needsReturnedRef then table.insert(returnedRefs, partRef) end end table.insert(generatorReports, { kind = generator.kind, namePrefix = generator.namePrefix, count = count, first = firstRef, last = lastRef, }) end group.Parent = parent return { group = InstanceRegistry.toRef(group), generatedCount = generatedCount, returnedRefs = returnedRefs, returnedRefsTruncated = generatedCount > #returnedRefs, generators = generatorReports, } end, createdForCleanup) if result.success ~= true or spec.review == false then return result end local reviewOk, review = pcall(function() return Spatial.review({ targets = { result.group }, screenshot = spec.reviewScreenshot or "auto", view = spec.reviewView or "isometric", maxParts = 100, maxRelationships = 30, maxImageSize = spec.reviewMaxImageSize or 640, keepCamera = false, }) end) if reviewOk then if review.imageBase64 then result.imageBase64 = review.imageBase64 review.imageBase64 = nil end result.review = review else result.reviewError = tostring(review) end return result end function Building.cloneInstances(spec) validateItemCount(spec.requests, "requests", 100) local created = {} return record("Dominus 2: Clone instances", function() local clones = {} local totalCopies = 0 for requestIndex, request in spec.requests do assert(type(request) == "table", "Clone request " .. requestIndex .. " must be an object") local source = resolve(request.target, "Clone request " .. requestIndex .. " target") assertMovable(source, "Clone source") local parent = request.parent and resolve(request.parent, "Clone request " .. requestIndex .. " parent") or source.Parent assert(parent ~= nil, "Clone request " .. requestIndex .. " source has no parent") local count = request.count or 1 assert( type(count) == "number" and count % 1 == 0 and count >= 1 and count <= 50, "count must be an integer from 1 to 50" ) totalCopies += count assert(totalCopies <= MAX_ITEMS, "A clone batch is limited to " .. MAX_ITEMS .. " copies") local offset = vector3(request.offset, "Clone request " .. requestIndex .. ".offset", Vector3.zero) local stepOffset = vector3(request.stepOffset, "Clone request " .. requestIndex .. ".stepOffset", Vector3.zero) for copyIndex = 1, count do local clone = source:Clone() assert(clone ~= nil, "Clone request " .. requestIndex .. " target is not Archivable") table.insert(created, clone) if request.name then clone.Name = count == 1 and request.name or (request.name .. " " .. copyIndex) end setProperties(clone, request.properties) if clone:IsA("PVInstance") then local totalOffset = offset + stepOffset * (copyIndex - 1) clone:PivotTo(clone:GetPivot() + totalOffset) elseif offset.Magnitude > 0 or stepOffset.Magnitude > 0 then error("Clone request " .. requestIndex .. " cannot offset a non-PVInstance") end clone.Parent = parent table.insert(clones, InstanceRegistry.toRef(clone)) end end return { clones = clones, count = #clones } end, created) end local function validateIndependentTargets(targets) for leftIndex, left in targets do for rightIndex = leftIndex + 1, #targets do local right = targets[rightIndex] assert( not left:IsAncestorOf(right) and not right:IsAncestorOf(left), "Targets " .. leftIndex .. " and " .. rightIndex .. " overlap" ) end end end function Building.groupInstances(spec) validateItemCount(spec.targets, "targets", 100) assert(type(spec.name) == "string" and #spec.name > 0, "name must be a non-empty string") local className = spec.className or "Model" assert(className == "Model" or className == "Folder", "className must be Model or Folder") local targets = {} for index, targetRef in spec.targets do local target = resolve(targetRef, "Target " .. index) assertMovable(target, "Target " .. index) table.insert(targets, target) end validateIndependentTargets(targets) local parent = spec.parent and resolve(spec.parent, "parent") or targets[1].Parent assert(parent ~= nil, "The first target has no parent") local created = {} return record("Dominus 2: Group instances", function() local group = Instance.new(className) table.insert(created, group) group.Name = spec.name group.Parent = parent for _, target in targets do assert(not target:IsAncestorOf(group), "Cannot group an ancestor into its descendant") target.Parent = group end return { group = InstanceRegistry.toRef(group), count = #targets } end, created) end function Building.ungroupInstances(spec) assert(spec.confirm == true, "confirm must be true") validateItemCount(spec.targets, "targets", 20) return record("Dominus 2: Ungroup instances", function() local moved = {} local removed = {} for index, targetRef in spec.targets do local group = resolve(targetRef, "Group " .. index) assert(group:IsA("Model") or group:IsA("Folder"), "Group " .. index .. " must be a Model or Folder") assertMovable(group, "Group " .. index) local parent = spec.parent and resolve(spec.parent, "parent") or group.Parent assert(parent ~= nil, "Group " .. index .. " has no parent") for _, child in group:GetChildren() do child.Parent = parent table.insert(moved, InstanceRegistry.toRef(child)) end table.insert(removed, { name = group.Name, className = group.ClassName }) group:Destroy() end return { moved = moved, removedGroups = removed, count = #moved } end) end local function absolutePivot(current, operation) if operation.cframe then return ValueCodec.decodeForCurrent(current, operation.cframe) end local position = vector3(operation.position, "position", current.Position) local x, y, z = current:ToOrientation() if operation.orientation then local degrees = vector3(operation.orientation, "orientation") x, y, z = math.rad(degrees.X), math.rad(degrees.Y), math.rad(degrees.Z) end return CFrame.new(position) * CFrame.fromOrientation(x, y, z) end function Building.transformInstances(spec) validateItemCount(spec.operations, "operations", 100) return record("Dominus 2: Transform instances", function() local results = {} for index, operation in spec.operations do local target = resolve(operation.target, "Operation " .. index .. " target") assert(target:IsA("PVInstance"), "Operation " .. index .. " target must be a Model or BasePart") local current = target:GetPivot() local mode = operation.mode or "absolute" local nextPivot if mode == "absolute" then nextPivot = absolutePivot(current, operation) elseif mode == "relative" then local offset = vector3(operation.offset, "Operation " .. index .. ".offset", Vector3.zero) local rotate = rotation(operation.rotationOffset, "Operation " .. index .. ".rotationOffset") if operation.space == "local" then nextPivot = current * CFrame.new(offset) * rotate else nextPivot = CFrame.new(offset) * current * rotate end else error("Operation " .. index .. ".mode must be absolute or relative") end target:PivotTo(nextPivot) table.insert( results, { target = InstanceRegistry.toRef(target), pivot = ValueCodec.encode(target:GetPivot()) } ) end return { transformed = results, count = #results } end) end function Building.createWelds(spec) validateItemCount(spec.welds, "welds", MAX_ITEMS) local created = {} return record("Dominus 2: Create welds", function() local welds = {} for index, item in spec.welds do local part0 = resolve(item.part0, "Weld " .. index .. " part0") local part1 = resolve(item.part1, "Weld " .. index .. " part1") assert(part0:IsA("BasePart") and part1:IsA("BasePart"), "Weld " .. index .. " endpoints must be BaseParts") assert(part0 ~= part1, "Weld " .. index .. " endpoints must be different") local parent = item.parent and resolve(item.parent, "Weld " .. index .. " parent") or part0 local weld = Instance.new("WeldConstraint") table.insert(created, weld) weld.Name = item.name or (part0.Name .. "_to_" .. part1.Name) weld.Part0 = part0 weld.Part1 = part1 weld.Parent = parent table.insert(welds, InstanceRegistry.toRef(weld)) end return { welds = welds, count = #welds } end, created) end local COLLISION_FIDELITIES = { Default = Enum.CollisionFidelity.Default, Hull = Enum.CollisionFidelity.Hull, Box = Enum.CollisionFidelity.Box, PreciseConvexDecomposition = Enum.CollisionFidelity.PreciseConvexDecomposition, } local RENDER_FIDELITIES = { Automatic = Enum.RenderFidelity.Automatic, Precise = Enum.RenderFidelity.Precise, Performance = Enum.RenderFidelity.Performance, } local UNION_ROOT_PROPERTIES = { "Anchored", "CanCollide", "CanQuery", "CanTouch", "CastShadow", "CollisionGroup", "Color", "Massless", "Material", "MaterialVariant", "Reflectance", "Transparency", } local function assertUnionPart(instance, label) assert(instance:IsA("BasePart"), label .. " must be a BasePart") assert( instance:IsA("Part") or instance:IsA("WedgePart") or instance:IsA("CornerWedgePart") or instance:IsA("MeshPart") or instance:IsA("PartOperation"), label .. " is not supported by GeometryService:UnionAsync" ) end local function preserveUnionConstraints(sources, results) local preserved = 0 for _, source in sources do local ok, recommendations = pcall(function() return GeometryService:CalculateConstraintsToPreserve(source, results) end) if ok and type(recommendations) == "table" then for _, recommendation in recommendations do if recommendation.Constraint and recommendation.ConstraintParent then recommendation.Constraint.Parent = recommendation.ConstraintParent preserved += 1 end end end end return preserved end local function copyUnionRootProperties(source, result) for _, propertyName in UNION_ROOT_PROPERTIES do pcall(function() result[propertyName] = source[propertyName] end) end if source:IsA("PartOperation") and result:IsA("PartOperation") then result.UsePartColor = source.UsePartColor end end function Building.unionParts(spec) validateItemCount(spec.others, "others", 99) local main = resolve(spec.main, "main") assertUnionPart(main, "main") assertMovable(main, "main") local sources = { main } local seen = { [main] = true } for index, targetRef in spec.others do local part = resolve(targetRef, "Other " .. index) assertUnionPart(part, "Other " .. index) assertMovable(part, "Other " .. index) assert(not seen[part], "Union inputs must be unique") seen[part] = true table.insert(sources, part) end local parent = spec.parent and resolve(spec.parent, "parent") or main.Parent assert(parent ~= nil, "main has no parent") local collisionFidelity = COLLISION_FIDELITIES[spec.collisionFidelity or "Default"] local renderFidelity = RENDER_FIDELITIES[spec.renderFidelity or "Automatic"] assert(collisionFidelity ~= nil, "collisionFidelity is invalid") assert(renderFidelity ~= nil, "renderFidelity is invalid") local created = {} return record("Dominus 2: Union parts", function() local otherParts = {} for index = 2, #sources do table.insert(otherParts, sources[index]) end local results = GeometryService:UnionAsync(main, otherParts, { CollisionFidelity = collisionFidelity, RenderFidelity = renderFidelity, SplitApart = spec.splitApart == true, }) assert(type(results) == "table" and #results > 0, "UnionAsync returned no geometry") local refs = {} for index, result in results do assert(result:IsA("BasePart"), "UnionAsync returned an invalid result") table.insert(created, result) result.Name = spec.name and (#results == 1 and spec.name or (spec.name .. " " .. index)) or (#results == 1 and main.Name or (main.Name .. " " .. index)) copyUnionRootProperties(main, result) setProperties(result, spec.properties) result.Parent = parent table.insert(refs, InstanceRegistry.toRef(result)) end local constraintsPreserved = preserveUnionConstraints(sources, results) for _, source in sources do source:Destroy() end return { results = refs, count = #refs, replacedCount = #sources, constraintsPreserved = constraintsPreserved, } end, created) end local function overlapParams(spec) local params = OverlapParams.new() local maxParts = spec.maxParts or 200 assert( type(maxParts) == "number" and maxParts % 1 == 0 and maxParts >= 1 and maxParts <= 500, "maxParts must be an integer from 1 to 500" ) params.MaxParts = maxParts params.RespectCanCollide = spec.respectCanCollide == true if spec.filter and #spec.filter > 0 then local instances = {} for index, targetRef in spec.filter do table.insert(instances, resolve(targetRef, "Filter " .. index)) end params.FilterDescendantsInstances = instances params.FilterType = spec.filterType == "Include" and Enum.RaycastFilterType.Include or Enum.RaycastFilterType.Exclude end return params end function Building.queryParts(spec) local root = spec.root and resolve(spec.root, "root") or workspace assert(root:IsA("WorldRoot"), "root must be Workspace or a WorldModel") local mode = spec.mode or "box" local found if mode == "box" then local position = vector3(spec.position, "position") local size = vector3(spec.size, "size") assert(size.X > 0 and size.Y > 0 and size.Z > 0, "size components must be greater than zero") local box = CFrame.new(position) * rotation(spec.orientation, "orientation") found = root:GetPartBoundsInBox(box, size, overlapParams(spec)) elseif mode == "radius" then local position = vector3(spec.position, "position") local radius = finiteNumber(spec.radius, "radius") assert(radius > 0, "radius must be greater than zero") found = root:GetPartBoundsInRadius(position, radius, overlapParams(spec)) else error("mode must be box or radius") end table.sort(found, function(left, right) return InstanceRegistry.getDisplayPath(left) < InstanceRegistry.getDisplayPath(right) end) local parts = {} for _, part in found do table.insert(parts, { name = part.Name, className = part.ClassName, ref = InstanceRegistry.toRef(part), position = ValueCodec.encode(part.Position), size = ValueCodec.encode(part.Size), }) end return { success = true, parts = parts, count = #parts, mode = mode } end function Building.setSelection(spec) assert(type(spec.targets) == "table", "targets must be an array") assert(#spec.targets <= 200, "At most 200 instances can be selected") local targets = {} for index, targetRef in spec.targets do table.insert(targets, resolve(targetRef, "Target " .. index)) end Selection:Set(targets) local selected = {} for _, target in targets do table.insert(selected, InstanceRegistry.toRef(target)) end return { success = true, selection = selected, count = #selected } end return Building ]]> CodeRunner 5 or state.count >= MAX_RESULT_ITEMS then return end seen[value] = true for _, child in value do state.count += 1 if state.count > MAX_RESULT_ITEMS then break end collectReturnedTargets(child, targets, seen, depth + 1, state) end seen[value] = nil end local function compile(source, chunkName) if type(loadstring) ~= "function" then return nil, "Luau execution is unavailable in this Studio plugin context." end local ok, chunk, compileError = pcall(loadstring, source, "=" .. chunkName) if not ok then return nil, tostring(chunk) end if type(chunk) ~= "function" then return nil, tostring(compileError or "Luau compilation failed") end return chunk, nil end function CodeRunner.configure(provider) assert(type(provider) == "function", "CodeRunner.configure requires an enabled provider") enabledProvider = provider end function CodeRunner.execute(spec) assert(type(spec) == "table", "Luau execution payload must be an object") assert(spec.confirm == true, "Luau execution requires confirm=true") local enabledOk, enabled = pcall(enabledProvider) assert( enabledOk and enabled == true, "Luau execution is disabled. Use the Dominus panel or Luau toolbar button to opt in." ) assert(type(spec.source) == "string" and #spec.source > 0, "source must be a non-empty Luau string") assert(#spec.source <= MAX_SOURCE_BYTES, "source is limited to " .. MAX_SOURCE_BYTES .. " bytes") local chunkName = spec.chunkName or "Dominus generated Luau" assert( type(chunkName) == "string" and #chunkName > 0 and #chunkName <= 100, "chunkName must be 1 to 100 characters" ) local chunk, compileError = compile(spec.source, chunkName) if not chunk then return { success = false, error = compileError, phase = "compile" } end local reviewTargets = {} local dominus = { game = game, workspace = workspace, } function dominus.track(first, second) local instance = first == dominus and second or first assert(typeof(instance) == "Instance", "dominus.track expects an Instance") return addReviewTarget(reviewTargets, instance) end function dominus.trackAll(first, second) local instances = first == dominus and second or first assert(type(instances) == "table", "dominus.trackAll expects an array of Instances") for _, instance in instances do dominus.track(instance) end return instances end local environment = setmetatable({ dominus = dominus }, { __index = getfenv(chunk) }) setfenv(chunk, environment) local recording = ChangeHistoryService:TryBeginRecording("Dominus 2: Run Luau") if not recording then return { success = false, error = "Another plugin recording is already active", phase = "execute" } end local startedAt = os.clock() local packed = table.pack(pcall(chunk)) local duration = os.clock() - startedAt ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) local executionSuccess = packed[1] == true local returnValues = {} if executionSuccess then local returnCount = math.min(packed.n - 1, MAX_RETURN_VALUES) for index = 1, returnCount do local value = packed[index + 1] collectReturnedTargets(value, reviewTargets, {}, 0, { count = 0 }) table.insert(returnValues, ValueCodec.encodeTyped(value, MAX_RESULT_ITEMS)) end end local targetRefs = {} for _, target in reviewTargets do local hasGeometry = target:IsA("BasePart") or target:FindFirstChildWhichIsA("BasePart", true) ~= nil if target:IsDescendantOf(game) and hasGeometry then table.insert(targetRefs, InstanceRegistry.toRef(target)) end end local result = { success = true, executionSuccess = executionSuccess, durationSeconds = duration, undoable = true, chunkName = chunkName, returnValues = returnValues, returnValuesTruncated = executionSuccess and packed.n - 1 > MAX_RETURN_VALUES or false, reviewTargets = targetRefs, } if not executionSuccess then result.runtimeError = tostring(packed[2]) end if spec.review ~= false and #targetRefs > 0 then local reviewOk, review = pcall(Spatial.review, { targets = targetRefs, screenshot = spec.reviewScreenshot or "auto", view = spec.reviewView or "isometric", maxImageSize = spec.reviewMaxImageSize or 640, }) if reviewOk then if review.imageBase64 then result.imageBase64 = review.imageBase64 review.imageBase64 = nil end result.review = review else result.reviewError = tostring(review) end elseif spec.review ~= false then result.reviewHint = "Return the built Model/part or call dominus.track(instance) to enable automatic review." end return result end return CodeRunner ]]> CommandRouter 128 then return false, "Invalid Dominus request envelope" end if type(message.type) ~= "string" or type(message.payload) ~= "table" then return false, "Invalid Dominus request type or payload" end local handler = handlers[message.type] if not handler then sendResponse(message.id, { success = false, error = "Command is not allowed by the Dominus 2 plugin: " .. message.type, }) return true end task.spawn(function() local ok, result = pcall(handler, message.payload) if not ok then result = { success = false, error = "Command failed: " .. tostring(result) } elseif type(result) ~= "table" then result = { success = false, error = "Command returned an invalid result" } end sendResponse(message.id, result) end) return true end return CommandRouter ]]> DeviceSimulator Explorer maxDepth then return nil end local node = { name = instance.Name, className = instance.ClassName, path = Explorer.getPath(instance), } local children = {} for _, child in instance:GetChildren() do local childNode = serialize(child, depth + 1) if childNode then table.insert(children, childNode) end end if #children > 0 then node.children = children end return node end if root == game then local topLevel = {} for _, service in game:GetChildren() do local ok, node = pcall(serialize, service, 1) if ok and node then table.insert(topLevel, node) end end return topLevel else local node = serialize(root, 0) return node and { node } or {} end end function Explorer.getScriptSource(path) local instance = Explorer.resolveInstance(path) if not instance then return { success = false, error = "Instance not found: " .. path } end if not SCRIPT_CLASSES[instance.ClassName] then return { success = false, error = "Not a script: " .. path } end local ok, source = pcall(function() return ScriptEditorService:GetEditorSource(instance) end) if not ok then -- Fallback to .Source property ok, source = pcall(function() return instance.Source end) end if not ok then return { success = false, error = "Could not read source: " .. tostring(source) } end local lines = select(2, source:gsub("\n", "\n")) + 1 return { path = path, className = instance.ClassName, source = source, lineCount = lines, } end function Explorer.setScriptSource(path, newSource) local instance = Explorer.resolveInstance(path) if not instance then return { success = false, error = "Instance not found: " .. path } end if not SCRIPT_CLASSES[instance.ClassName] then return { success = false, error = "Not a script: " .. path } end local ok, err = pcall(function() ScriptEditorService:UpdateSourceAsync(instance, function() return newSource end) end) if not ok then return { success = false, error = "Failed to update source: " .. tostring(err) } end return { success = true } end function Explorer.insertInstance(className, parentPath, name, properties) local parent = Explorer.resolveInstance(parentPath) if not parent then return { success = false, error = "Parent not found: " .. parentPath } end local ok, instance = pcall(function() local obj = Instance.new(className) obj.Name = name if properties then for key, value in properties do pcall(function() obj[key] = value end) end end obj.Parent = parent return obj end) if not ok then return { success = false, error = "Failed to create instance: " .. tostring(instance) } end return { success = true, path = Explorer.getPath(instance) } end function Explorer.deleteInstance(path) local instance = Explorer.resolveInstance(path) if not instance then return { success = false, error = "Instance not found: " .. path } end local ok, err = pcall(function() instance:Destroy() end) if not ok then return { success = false, error = "Failed to delete: " .. tostring(err) } end return { success = true } end function Explorer.searchScripts(query, maxResults) maxResults = maxResults or 20 local results = {} local queryLower = string.lower(query) local function searchIn(instance) if #results >= maxResults then return end if SCRIPT_CLASSES[instance.ClassName] then local ok, source = pcall(function() return ScriptEditorService:GetEditorSource(instance) end) if not ok then ok, source = pcall(function() return instance.Source end) end if ok and source then local matches = {} local lineNum = 0 for line in source:gmatch("[^\n]+") do lineNum = lineNum + 1 if string.find(string.lower(line), queryLower, 1, true) then table.insert(matches, { line = lineNum, text = line }) if #matches >= 5 then break end end end if #matches > 0 then table.insert(results, { path = Explorer.getPath(instance), className = instance.ClassName, matches = matches, }) end end end for _, child in instance:GetChildren() do if #results >= maxResults then return end pcall(searchIn, child) end end pcall(searchIn, game) return { results = results } end function Explorer.findReplaceScripts(spec) local find = spec.find local replace = spec.replace local usePattern = spec.usePattern or false local dryRun = spec.dryRun or false if not find or find == "" then return { success = false, error = "Missing 'find' parameter" } end local root = game if spec.root and spec.root ~= "" then root = Explorer.resolveInstance(spec.root) if not root then local ok, svc = pcall(function() return game:GetService(spec.root) end) if ok then root = svc else return { success = false, error = "Root not found: " .. spec.root } end end end local ChangeHistoryService = game:GetService("ChangeHistoryService") local recording = nil if not dryRun then recording = ChangeHistoryService:TryBeginRecording("Dominus: Find/replace in scripts") end local modified = {} local totalMatches = 0 local totalFiles = 0 local function processScript(instance) if not SCRIPT_CLASSES[instance.ClassName] then return end local ok, source = pcall(function() return ScriptEditorService:GetEditorSource(instance) end) if not ok then ok, source = pcall(function() return instance.Source end) end if not ok or not source then return end local count = 0 if usePattern then _, count = source:gsub(find, "") else _, count = source:gsub(find, "", nil) -- plain text: count occurrences with plain find count = 0 local startPos = 1 while true do local foundPos = source:find(find, startPos, true) if not foundPos then break end count = count + 1 startPos = foundPos + #find end end if count > 0 then totalMatches = totalMatches + count totalFiles = totalFiles + 1 local path = Explorer.getPath(instance) table.insert(modified, { path = path, matches = count }) if not dryRun then local newSource if usePattern then newSource = source:gsub(find, replace) else newSource = source:gsub(find:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"), replace:gsub("%%", "%%%%")) end pcall(function() ScriptEditorService:UpdateSourceAsync(instance, function() return newSource end) end) end end end local function walk(instance) processScript(instance) for _, child in instance:GetChildren() do pcall(walk, child) end end pcall(walk, root) if recording then if totalFiles > 0 then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) else ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) end end return { success = true, modified = modified, totalMatches = totalMatches, totalFiles = totalFiles, } end local function sourceHash(source) local hash = 5381 for index = 1, #source do hash = (hash * 33 + string.byte(source, index)) % 4294967296 end return string.format("%d:%08x", #source, hash) end function Explorer.getTreeV2(spec) local root = game if spec.root then local resolved, err = InstanceRegistry.resolve(spec.root) if not resolved then return { success = false, error = err } end root = resolved end local maxDepth = math.clamp(spec.maxDepth or 3, 0, 12) local maxNodes = math.clamp(spec.maxNodes or 1000, 1, 5000) local nodeCount = 0 local truncated = false local function serialize(instance, depth) if depth > maxDepth or nodeCount >= maxNodes then truncated = true return nil end nodeCount += 1 local node = { name = instance.Name, className = instance.ClassName, ref = InstanceRegistry.toRef(instance), } local children = instance:GetChildren() table.sort(children, function(a, b) if a.Name == b.Name then return a.ClassName < b.ClassName end return a.Name < b.Name end) for childIndex, child in children do local childNode = serialize(child, depth + 1) if childNode then node.children = node.children or {} table.insert(node.children, childNode) end if nodeCount >= maxNodes then if childIndex < #children then truncated = true end break end end return node end if root == game then local roots = {} local services = game:GetChildren() table.sort(services, function(a, b) return a.Name < b.Name end) for serviceIndex, service in services do local node = serialize(service, 0) if node then table.insert(roots, node) end if nodeCount >= maxNodes then if serviceIndex < #services then truncated = true end break end end return { success = true, roots = roots, nodeCount = nodeCount, truncated = truncated } end return { success = true, roots = { serialize(root, 0) }, nodeCount = nodeCount, truncated = truncated } end function Explorer.readScriptV2(spec) local instance, err = InstanceRegistry.resolve(spec.target) if not instance then return { success = false, error = err } end if not SCRIPT_CLASSES[instance.ClassName] then return { success = false, error = "Target is not a LuaSourceContainer" } end local ok, source = pcall(function() return ScriptEditorService:GetEditorSource(instance) end) if not ok then return { success = false, error = "Could not read script source: " .. tostring(source) } end return { success = true, target = InstanceRegistry.toRef(instance), className = instance.ClassName, source = source, lineCount = select(2, source:gsub("\n", "\n")) + 1, revision = sourceHash(source), } end function Explorer.updateScriptV2(spec) local instance, err = InstanceRegistry.resolve(spec.target) if not instance then return { success = false, error = err } end if not SCRIPT_CLASSES[instance.ClassName] then return { success = false, error = "Target is not a LuaSourceContainer" } end if type(spec.source) ~= "string" then return { success = false, error = "source must be a string" } end if #spec.source > 750000 then return { success = false, error = "source exceeds 750,000 characters" } end if type(spec.expectedRevision) ~= "string" then return { success = false, error = "expectedRevision is required; call studio_read_script first" } end local conflictRevision = nil local ok, updateErr = pcall(function() ScriptEditorService:UpdateSourceAsync(instance, function(oldSource) local currentRevision = sourceHash(oldSource) if currentRevision ~= spec.expectedRevision then conflictRevision = currentRevision return nil end return spec.source end) end) if conflictRevision then return { success = false, conflict = true, error = "Script changed since it was read", currentRevision = conflictRevision, } end if not ok then return { success = false, error = "Script update failed: " .. tostring(updateErr) } end local readback = Explorer.readScriptV2({ target = InstanceRegistry.toRef(instance) }) return { success = true, target = readback.target, revision = readback.revision, lineCount = readback.lineCount, } end return Explorer ]]> Generation Game Settings > Security. Beta limit is 5 generations per minute.", } end function Generation.generateModel(spec) local generation, unavailable = service() if not generation then return { success = false, error = unavailable } end local prompt = spec.prompt if type(prompt) ~= "string" or #prompt == 0 or #prompt > 500 then return { success = false, error = "prompt must be a string of 1 to 500 characters" } end local inputs = { Prompt = prompt } local schema = type(spec.schema) == "table" and spec.schema or {} local options = type(spec.options) == "table" and spec.options or nil local ok, result, extra = pcall(function() return generation:GenerateModelAsync(inputs, schema, options) end) if not ok then return { success = false, error = "GenerateModelAsync failed: " .. tostring(result), hint = "Confirm the DynamicGeneration capability and the Editable Mesh/Image security " .. "setting are enabled. This API may not be callable from edit-mode plugins.", } end return { success = true, prompt = prompt, result = tostring(result), extra = extra ~= nil and tostring(extra) or nil, } end return Generation ]]> Input , y=}" end local x, y = tonumber(value.x), tonumber(value.y) if x == nil or y == nil then return nil, label .. " requires numeric x and y" end return Vector2.new(x, y), nil end local function resolveKeyCode(name) if type(name) ~= "string" then return nil, "key must be a string KeyCode name" end local ok, keyCode = pcall(function() return Enum.KeyCode[name] end) if not ok or keyCode == nil then return nil, "Unknown KeyCode: " .. tostring(name) end return keyCode, nil end local function resolveMouseButton(name) local mapping = { left = Enum.UserInputType.MouseButton1, right = Enum.UserInputType.MouseButton2, middle = Enum.UserInputType.MouseButton3, } local resolved = mapping[string.lower(tostring(name or "left"))] if not resolved then return nil, "button must be left, right, or middle" end return resolved, nil end -- Each action is applied in order. Anything unrecognized aborts the batch -- rather than silently skipping, so a typo cannot look like a passing run. local function applyAction(virtualInput, action, index, held) local label = "Action " .. index if type(action) ~= "table" or type(action.type) ~= "string" then return false, label .. " requires a type" end local kind = string.lower(action.type) if kind == "wait" then local seconds = math.clamp(tonumber(action.seconds) or 0.1, 0, MAX_WAIT_SECONDS) task.wait(seconds) return true, nil end if kind == "keydown" or kind == "keyup" or kind == "keypress" then local keyCode, err = resolveKeyCode(action.key) if not keyCode then return false, label .. ": " .. err end if kind == "keydown" then held.keys[keyCode] = true virtualInput:SendKey(true, keyCode, action.repeated == true) elseif kind == "keyup" then virtualInput:SendKey(false, keyCode, false) held.keys[keyCode] = nil else held.keys[keyCode] = true virtualInput:SendKey(true, keyCode, false) task.wait(math.clamp(tonumber(action.holdSeconds) or 0.05, 0, 2)) virtualInput:SendKey(false, keyCode, false) held.keys[keyCode] = nil end return true, nil end if kind == "text" then local text = tostring(action.text or "") if #text > MAX_TEXT_LENGTH then return false, label .. ": text exceeds " .. MAX_TEXT_LENGTH .. " characters" end virtualInput:SendTextInput(text) return true, nil end if kind == "mousemove" then local position, err = toVector2(action.position, label .. " position") if not position then return false, err end virtualInput:SendMousePosition(position) held.position = position return true, nil end if kind == "mousedelta" then local delta, err = toVector2(action.delta, label .. " delta") if not delta then return false, err end virtualInput:SendMouseDelta(delta) return true, nil end if kind == "mousedown" or kind == "mouseup" or kind == "click" then local position, positionErr = toVector2(action.position, label .. " position") if not position then return false, positionErr end local button, buttonErr = resolveMouseButton(action.button) if not button then return false, label .. ": " .. buttonErr end if kind == "mousedown" then held.buttons[button] = position virtualInput:SendMouseButton(position, button, true, 0) elseif kind == "mouseup" then virtualInput:SendMouseButton(position, button, false, 0) held.buttons[button] = nil else virtualInput:SendMousePosition(position) held.buttons[button] = position virtualInput:SendMouseButton(position, button, true, 0) task.wait(math.clamp(tonumber(action.holdSeconds) or 0.05, 0, 2)) virtualInput:SendMouseButton(position, button, false, 0) held.buttons[button] = nil end held.position = position return true, nil end return false, label .. ": unsupported action type '" .. kind .. "'" end local function matchesRun(runId) local ok, context = pcall(function() return require(script.Parent.TestRunner).context() end) return ok and type(context) == "table" and context.success == true and context.role == "client" and context.runId == runId end function Input.sendGuarded(spec) if type(spec.runId) ~= "string" or #spec.runId == 0 or #spec.runId > 100 then return { success = false, error = "A managed run ID is required", applied = 0 } end return Input.send(spec, spec.runId) end function Input.send(spec, runId) if busy then return { success = false, error = "Another input batch is running", applied = 0 } end if runId and not matchesRun(runId) then return { success = false, error = "Input requires the matching managed client runtime", applied = 0 } end local virtualInput, unavailable = service() if not virtualInput then return { success = false, error = unavailable } end local actions = spec.actions if type(actions) ~= "table" or #actions == 0 then return { success = false, error = "actions must be a non-empty array" } end if #actions > MAX_ACTIONS then return { success = false, error = "At most " .. MAX_ACTIONS .. " actions per request" } end -- Validate every action before delivering any input. local plannedSeconds = 0 for index, action in actions do if type(action) ~= "table" or type(action.type) ~= "string" then return { success = false, error = "Invalid action " .. index, applied = 0 } end local kind = string.lower(action.type) local duration = 0 if kind == "wait" then duration = action.seconds or 0.1 elseif kind == "keypress" or kind == "click" then duration = action.holdSeconds or 0.05 end local limit = kind == "wait" and MAX_WAIT_SECONDS or 2 if type(duration) ~= "number" or duration ~= duration or duration < 0 or duration > limit then return { success = false, error = "Invalid duration in action " .. index, applied = 0 } end plannedSeconds += duration local valid = false if kind == "wait" then valid = true elseif kind == "keydown" or kind == "keyup" or kind == "keypress" then valid = resolveKeyCode(action.key) ~= nil elseif kind == "text" then valid = type(action.text) == "string" and #action.text <= MAX_TEXT_LENGTH elseif kind == "mousemove" or kind == "mousedelta" or kind == "mousedown" or kind == "mouseup" or kind == "click" then local point = kind == "mousedelta" and action.delta or action.position valid = type(point) == "table" and type(point.x) == "number" and type(point.y) == "number" and math.abs(point.x) < math.huge and math.abs(point.y) < math.huge if kind == "mousedown" or kind == "mouseup" or kind == "click" then valid = valid and resolveMouseButton(action.button) ~= nil end end if not valid then return { success = false, error = "Invalid input action " .. index, applied = 0 } end end if plannedSeconds > MAX_BATCH_SECONDS then return { success = false, error = "Input waits and holds exceed the 30 second budget", applied = 0 } end busy = true local held = { keys = {}, buttons = {} } local applied = 0 local failure = nil local started = os.clock() for index, action in actions do if os.clock() - started > MAX_BATCH_SECONDS + 5 then failure = "Input execution exceeded its deadline" break end if runId and not matchesRun(runId) then failure = "Managed client context changed during input" break end local ran, ok, err = pcall(applyAction, virtualInput, action, index, held) if not ran or not ok then failure = not ran and tostring(ok) or err break end applied += 1 end local cleanupErrors = {} for keyCode in held.keys do local ok = pcall(function() virtualInput:SendKey(false, keyCode, false) end) if not ok then table.insert(cleanupErrors, "Could not release key " .. keyCode.Name) end end for button, position in held.buttons do local ok = pcall(function() virtualInput:SendMouseButton(held.position or position, button, false, 0) end) if not ok then table.insert(cleanupErrors, "Could not release " .. button.Name) end end -- The final action or cleanup can yield too; a pre-action check alone cannot -- establish that the batch finished in its requested runtime and deadline. if failure == nil and runId and not matchesRun(runId) then failure = "Managed client context changed before input completion" end if failure == nil and os.clock() - started > MAX_BATCH_SECONDS + 5 then failure = "Input execution exceeded its deadline" end busy = false return { success = failure == nil and #cleanupErrors == 0, error = failure or (#cleanupErrors > 0 and "Input cleanup failed" or nil), applied = applied, cleanupErrors = cleanupErrors, elapsedSeconds = os.clock() - started, } end return Input ]]> InstanceRegistry 1 then return nil, "Instance path is ambiguous at duplicate sibling name: " .. segment end current = match end return current end function InstanceRegistry.resolve(target) if typeof(target) == "Instance" then return target end if type(target) ~= "table" then return nil, "Target must be an instance reference" end if type(target.instanceId) == "string" then local instance = idToInstance[target.instanceId] if instance then return instance end return nil, "Instance reference is stale: " .. target.instanceId end if target.pathSegments then local instance, err = resolveSegments(target.pathSegments) if instance then register(instance) end return instance, err end return nil, "Target requires instanceId or pathSegments" end function InstanceRegistry.register(instance) return register(instance) end return InstanceRegistry ]]> Metadata 0 and #value <= 100, label .. " must contain 1 to 100 characters") assert(not value:find("[%c]"), label .. " cannot contain control characters") return value end local function validateTag(value) return validateName(value, "Tag") end local function validateAttributeName(value) return validateName(value, "Attribute name") end local function prepareAttributeChanges(set, remove, label) set = set or {} remove = remove or {} assert(type(set) == "table" and type(remove) == "table", "set and remove must be arrays") assert(#set + #remove > 0, label .. " has no attribute updates") assert(#set + #remove <= MAX_ITEMS_PER_TARGET, "Too many attribute updates in " .. label) local changes = {} local seen = {} for itemIndex, item in set do assert(type(item) == "table" and type(item.value) == "table", "Set item " .. itemIndex .. " is invalid") local name = validateAttributeName(item.name) assert(not seen[name], "Attribute " .. name .. " appears more than once") local typeName = item.value.type assert(ATTRIBUTE_TYPES[typeName], "Unsupported attribute type: " .. tostring(typeName)) seen[name] = true table.insert(changes, { name = name, value = ValueCodec.decodeTyped(typeName, item.value.value) }) end for _, nameValue in remove do local name = validateAttributeName(nameValue) assert(not seen[name], "Attribute " .. name .. " cannot be set and removed together") seen[name] = true table.insert(changes, { name = name, remove = true }) end return changes end local function applyPreparedAttributeChanges(target, changes) for _, change in changes do if change.remove then target:SetAttribute(change.name, nil) else target:SetAttribute(change.name, change.value) end end end local function prepareTagChanges(add, remove, label) add = add or {} remove = remove or {} assert(type(add) == "table" and type(remove) == "table", "add and remove must be arrays") assert(#add + #remove > 0, label .. " has no tag updates") assert(#add + #remove <= MAX_ITEMS_PER_TARGET, "Too many tag updates in " .. label) local seen = {} for _, tagValue in add do local tag = validateTag(tagValue) assert(not seen[tag], "Tag " .. tag .. " appears more than once") seen[tag] = "add" end for _, tagValue in remove do local tag = validateTag(tagValue) assert(not seen[tag], "Tag " .. tag .. " cannot be added and removed together") seen[tag] = "remove" end return add, remove end local function applyPreparedTagChanges(target, add, remove) for _, tag in add do target:AddTag(tag) end for _, tag in remove do target:RemoveTag(tag) end end local function sortedTags(instance) local tags = instance:GetTags() table.sort(tags) return tags end local function encodedAttributes(instance) local attributes = instance:GetAttributes() local names = {} for name in attributes do table.insert(names, name) end table.sort(names) local encoded = {} for _, name in names do table.insert(encoded, { name = name, value = ValueCodec.encodeTyped(attributes[name]), }) end return encoded end local function metadataFor(instance) return { ref = InstanceRegistry.toRef(instance), name = instance.Name, className = instance.ClassName, tags = sortedTags(instance), attributes = encodedAttributes(instance), } end local function beginRecording(name) local recording = ChangeHistoryService:TryBeginRecording(name) if not recording then error("Another plugin recording is already active") end return recording end function Metadata.get(spec) assert(type(spec.targets) == "table" and #spec.targets > 0, "targets must contain at least one instance reference") assert(#spec.targets <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " instances can be inspected at once") local results = {} for index, targetRef in spec.targets do table.insert(results, metadataFor(resolve(targetRef, "Target " .. index))) end return { success = true, results = results } end function Metadata.setAttributes(spec) assert( type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one attribute update" ) assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " attribute operations are allowed") local prepared = {} for index, operation in spec.operations do assert(type(operation) == "table", "Operation " .. index .. " must be an object") table.insert(prepared, { target = resolve(operation.target, "Operation " .. index .. " target"), changes = prepareAttributeChanges(operation.set, operation.remove, "Operation " .. index), }) end local recording = beginRecording("Dominus 2: Update attributes") local ok, err = pcall(function() for _, operation in prepared do applyPreparedAttributeChanges(operation.target, operation.changes) end end) if not ok then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) return { success = false, error = tostring(err), rolledBack = true } end ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) local results = {} for _, operation in prepared do table.insert(results, metadataFor(operation.target)) end return { success = true, operationCount = #prepared, results = results } end function Metadata.updateTags(spec) assert(type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one tag update") assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " tag operations are allowed") local prepared = {} for index, operation in spec.operations do assert(type(operation) == "table", "Operation " .. index .. " must be an object") local add, remove = prepareTagChanges(operation.add, operation.remove, "Operation " .. index) table.insert(prepared, { target = resolve(operation.target, "Operation " .. index .. " target"), add = add, remove = remove, }) end local recording = beginRecording("Dominus 2: Update tags") local ok, err = pcall(function() for _, operation in prepared do applyPreparedTagChanges(operation.target, operation.add, operation.remove) end end) if not ok then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) return { success = false, error = tostring(err), rolledBack = true } end ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) local results = {} for _, operation in prepared do table.insert(results, metadataFor(operation.target)) end return { success = true, operationCount = #prepared, results = results } end function Metadata.applyAttributeChangesWithinRecording(target, set, remove) local changes = prepareAttributeChanges(set, remove, "Mutation operation") applyPreparedAttributeChanges(target, changes) return encodedAttributes(target) end function Metadata.applyTagChangesWithinRecording(target, add, remove) local preparedAdd, preparedRemove = prepareTagChanges(add, remove, "Mutation operation") applyPreparedTagChanges(target, preparedAdd, preparedRemove) return sortedTags(target) end local function publicPolicy(methodName, policy) local params = {} for _, param in policy.params do table.insert(params, { type = param.type, optional = param.optional or false, }) end return { name = methodName, kind = policy.kind, className = policy.className, parameters = params, } end function Metadata.listCallableMethods(spec) local target = resolve(spec.target, "Target") local methods = {} for methodName, policy in METHOD_POLICY do if target:IsA(policy.className) then table.insert(methods, publicPolicy(methodName, policy)) end end table.sort(methods, function(left, right) return left.name < right.name end) return { success = true, target = InstanceRegistry.toRef(target), methods = methods, blockedByDefault = true, } end local function decodeArgument(argument, parameter, label) assert(type(argument) == "table" and type(argument.type) == "string", label .. " must be a typed value") if parameter.type == "Attribute" then if argument.type == "nil" and parameter.allowNil then return nil end assert(ATTRIBUTE_TYPES[argument.type], label .. " must be a supported Roblox attribute type") return ValueCodec.decodeTyped(argument.type, argument.value) end assert(argument.type == parameter.type, label .. " must have type " .. parameter.type) local decoded = ValueCodec.decodeTyped(argument.type, argument.value) if parameter.tag then validateTag(decoded) elseif parameter.attributeName then validateAttributeName(decoded) end return decoded end function Metadata.invoke(spec) assert(type(spec.calls) == "table" and #spec.calls > 0, "calls must contain at least one method call") assert(#spec.calls <= MAX_METHOD_CALLS, "At most " .. MAX_METHOD_CALLS .. " method calls are allowed") local prepared = {} local hasWrites = false for index, call in spec.calls do assert(type(call) == "table", "Call " .. index .. " must be an object") local methodName = call.method local policy = METHOD_POLICY[methodName] assert(policy, "Method is not allowed by Dominus: " .. tostring(methodName)) local target = resolve(call.target, "Call " .. index .. " target") assert( target:IsA(policy.className), methodName .. " requires " .. policy.className .. ", got " .. target.ClassName ) local arguments = call.arguments or {} assert(type(arguments) == "table", "Call " .. index .. " arguments must be an array") local minimum = 0 for _, parameter in policy.params do if not parameter.optional then minimum += 1 end end assert( #arguments >= minimum and #arguments <= #policy.params, methodName .. " received the wrong number of arguments" ) local decoded = {} for argumentIndex, argument in arguments do decoded[argumentIndex] = decodeArgument(argument, policy.params[argumentIndex], "Argument " .. argumentIndex) end table.insert(prepared, { target = target, method = methodName, policy = policy, arguments = decoded, argumentCount = #arguments, }) hasWrites = hasWrites or policy.kind == "write" end local recording = hasWrites and beginRecording("Dominus 2: Invoke safe instance methods") or nil local results = {} local ok, err = pcall(function() for _, call in prepared do local method = call.target[call.method] assert(type(method) == "function", call.method .. " is unavailable on " .. call.target.ClassName) local returned = table.pack(method(call.target, table.unpack(call.arguments, 1, call.argumentCount))) local values = {} for resultIndex = 1, returned.n do table.insert(values, ValueCodec.encodeTyped(returned[resultIndex])) end table.insert(results, { target = InstanceRegistry.toRef(call.target), method = call.method, kind = call.policy.kind, results = values, }) end end) if not ok then if recording then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) end return { success = false, error = tostring(err), rolledBack = recording ~= nil } end if recording then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) end return { success = true, callCount = #results, mutationRecorded = recording ~= nil, results = results } end return Metadata ]]> Mutation MAX_PROPERTIES then error("Too many properties in one operation") end applied[propertyName] = ValueCodec.setProperty(instance, propertyName, value) end return applied end function Mutation.apply(spec) local operations = spec.operations if type(operations) ~= "table" or #operations == 0 then return { success = false, error = "operations must contain at least one mutation" } end if #operations > MAX_OPERATIONS then return { success = false, error = "A mutation batch is limited to " .. MAX_OPERATIONS .. " operations" } end local recording = ChangeHistoryService:TryBeginRecording("Dominus 2: Apply mutation batch") if not recording then return { success = false, error = "Another plugin recording is already active" } end local results = {} local created = {} local ok, err = pcall(function() for index, operation in operations do assert(type(operation) == "table", "Operation " .. index .. " must be an object") if operation.op == "setProperties" then local target = resolve(operation.target, "Operation " .. index .. " target") table.insert(results, { op = operation.op, target = InstanceRegistry.toRef(target), properties = setProperties(target, operation.properties), }) elseif operation.op == "setAttributes" then local target = resolve(operation.target, "Operation " .. index .. " target") table.insert(results, { op = operation.op, target = InstanceRegistry.toRef(target), attributes = Metadata.applyAttributeChangesWithinRecording(target, operation.set, operation.remove), }) elseif operation.op == "updateTags" then local target = resolve(operation.target, "Operation " .. index .. " target") table.insert(results, { op = operation.op, target = InstanceRegistry.toRef(target), tags = Metadata.applyTagChangesWithinRecording(target, operation.add, operation.remove), }) elseif operation.op == "create" then local parent = resolve(operation.parent, "Operation " .. index .. " parent") assert(type(operation.className) == "string", "create.className must be a string") assert(type(operation.name) == "string", "create.name must be a string") local instance = Instance.new(operation.className) table.insert(created, instance) instance.Name = operation.name if operation.properties then setProperties(instance, operation.properties) end instance.Parent = parent table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(instance) }) elseif operation.op == "clone" then local target = resolve(operation.target, "Operation " .. index .. " target") assert(target ~= game and target.Parent ~= game, "Services cannot be cloned") local parent = operation.parent and resolve(operation.parent, "Operation " .. index .. " parent") or target.Parent assert(parent ~= nil, "Clone target has no parent") local clone = target:Clone() assert(clone ~= nil, "Clone target is not Archivable") table.insert(created, clone) if operation.name then clone.Name = operation.name end if operation.properties then setProperties(clone, operation.properties) end if operation.offset then assert(clone:IsA("PVInstance"), "Only Models and BaseParts can be spatially offset") local offset = ValueCodec.decodeForCurrent(Vector3.zero, operation.offset) clone:PivotTo(clone:GetPivot() + offset) end clone.Parent = parent table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(clone) }) elseif operation.op == "move" then local target = resolve(operation.target, "Operation " .. index .. " target") local parent = resolve(operation.parent, "Operation " .. index .. " parent") assert(target ~= game and target.Parent ~= game, "Services cannot be moved") target.Parent = parent table.insert(results, { op = operation.op, target = InstanceRegistry.toRef(target) }) else error("Unsupported mutation operation: " .. tostring(operation.op)) end end end) if not ok then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) for _, instance in created do pcall(function() if instance.Parent == nil then instance:Destroy() end end) end return { success = false, error = tostring(err), rolledBack = true } end ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) return { success = true, operationCount = #results, results = results } end function Mutation.delete(spec) if spec.confirm ~= true then return { success = false, error = "confirm must be true" } end if type(spec.targets) ~= "table" or #spec.targets == 0 then return { success = false, error = "targets must contain at least one instance reference" } end if #spec.targets > 50 then return { success = false, error = "At most 50 instances can be deleted at once" } end local targets = {} local seen = {} for index, targetRef in spec.targets do local target = resolve(targetRef, "Target " .. index) assert(target ~= game and target.Parent ~= game, "Services cannot be deleted") local id = InstanceRegistry.register(target) if not seen[id] then seen[id] = true table.insert(targets, target) end end local recording = ChangeHistoryService:TryBeginRecording("Dominus 2: Delete instances") if not recording then return { success = false, error = "Another plugin recording is already active" } end local deleted = {} local ok, err = pcall(function() for _, target in targets do table.insert(deleted, { name = target.Name, className = target.ClassName, pathSegments = InstanceRegistry.getPathSegments(target), }) target:Destroy() end end) if not ok then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) return { success = false, error = tostring(err), rolledBack = true } end ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) return { success = true, deleted = deleted } end return Mutation ]]> PluginPanel PluginSettings MAX_RULE_BYTES then return decoded:sub(1, MAX_RULE_BYTES) end return decoded end function PluginSettings.getCodeExecutionEnabled(pluginObject) return getSetting(pluginObject, CODE_EXECUTION_KEY, false) == true end function PluginSettings.setCodeExecutionEnabled(pluginObject, enabled) return setSetting(pluginObject, CODE_EXECUTION_KEY, enabled == true) end function PluginSettings.getRules(pluginObject, scope) local scoped = getSetting(pluginObject, rulesSettingKey(scope), nil) if scoped ~= nil then return decodeRules(scoped) end -- Migrate the short-lived global 2.7 preview setting if it exists. return decodeRules(getSetting(pluginObject, RULES_KEY, "")) end function PluginSettings.setRules(pluginObject, rules, scope) assert(type(rules) == "string", "Plugin rules must be a string") assert(#rules <= MAX_RULE_BYTES, "Plugin rules are limited to " .. MAX_RULE_BYTES .. " bytes") return setSetting(pluginObject, rulesSettingKey(scope), encodeRules(rules)) end return PluginSettings ]]> PluginVersion Profiler frame.finish or state:IsFrameStitching() or state:ThreadStackIsUnderflowed() or state:ThreadStackIsOverflowed() then clearStacks() rejected += 1 elseif state:IsEnter() or state:IsExit() then assert(finite(threadId) and finite(timerId), "Invalid scope identity") local stack = stacks[threadId] or {} stacks[threadId] = stack if state:IsEnter() then if #stack >= 128 then error("Scope nesting exceeds 128") end table.insert(stack, { timer = timerId, tick = tick }) else local entry = table.remove(stack) if not entry or entry.timer ~= timerId or tick < entry.tick then abandoned += #stack table.clear(stack) rejected += 1 else local duration = (tick - entry.tick) * clock assert(finite(duration), "Scope duration overflow") local timer = timers[timerId] if not timer then timerCount += 1 assert(timerCount <= 512, "Scope timer count exceeds 512") timer = { timerId = timerId, calls = 0, inclusiveMs = 0, maxMs = 0 } timers[timerId] = timer end timer.calls += 1 timer.inclusiveMs += duration assert(finite(timer.inclusiveMs), "Scope total overflow") timer.maxMs = math.max(timer.maxMs, duration) paired += 1 end end end end clearStacks() local ranked = {} for _, timer in timers do table.insert(ranked, timer) end table.sort(ranked, function(a, b) return a.inclusiveMs == b.inclusiveMs and a.timerId < b.timerId or a.inclusiveMs > b.inclusiveMs end) while #ranked > 20 do table.remove(ranked) end for _, timer in ranked do local desc = session:FetchTimerDesc(timer.timerId) if type(desc.TimerName) == "string" then local boundary = utf8.offset(desc.TimerName, 201) timer.name = boundary and string.sub(desc.TimerName, 1, boundary - 1) or desc.TimerName end end return { status = "analyzed", steps = steps, completePairs = paired, rejectedEntries = rejected, abandonedEnters = abandoned, limitReached = steps == 50000, timers = ranked, omittedTimers = timerCount - #ranked, method = "Inclusive CPU durations of complete enter/exit pairs within individual valid frames; nested and parallel durations overlap. Not per-script or exclusive CPU cost.", } end) local disposed = not iterator or pcall(function() iterator:Dispose() end) if not ok then return { status = "unavailable", reason = tostring(result), cleanupRequired = not disposed } end result.cleanupRequired = not disposed return result end function Profiler.analyzeSession(session, maxFrames) assert(finite(maxFrames) and maxFrames % 1 == 0 and maxFrames >= 1 and maxFrames <= 256, "Invalid frame limit") local first, last = session:GetFrameIdMin(), session:GetFrameIdMax() assert( finite(first) and finite(last) and first % 1 == 0 and last % 1 == 0 and first >= 0 and last >= first, "Invalid frame range" ) local global = session:FetchGlobalDesc() assert(finite(global.TickToMsCpu) and global.TickToMsCpu > 0, "Invalid CPU clock conversion") local gpuClock = finite(global.TickToMsGpu) and global.TickToMsGpu > 0 local cpu, gpu, worst = {}, {}, {} local validFrames = {} local excluded = { paused = 0, incomplete = 0, invalidCpu = 0, missingGpu = 0 } local gaps, previousAbsolute, considered = 0, nil, 0 local start = math.max(first, last - maxFrames + 1) if first == 0 and last == 0 then start = 1 end for frameId = start, last do local frame = session:FetchFrameDesc(frameId) considered += 1 if finite(frame.FrameAbsoluteId) and previousAbsolute then gaps += math.max(0, frame.FrameAbsoluteId - previousAbsolute - 1) end previousAbsolute = finite(frame.FrameAbsoluteId) and frame.FrameAbsoluteId or nil if frame.IsPaused then excluded.paused += 1 elseif frame.IsIncomplete then excluded.incomplete += 1 elseif not finite(frame.TickStartCpu) or not finite(frame.TickEndCpu) or frame.TickEndCpu < frame.TickStartCpu then excluded.invalidCpu += 1 else local duration = (frame.TickEndCpu - frame.TickStartCpu) * global.TickToMsCpu if not finite(duration) then error("Frame duration overflow") end table.insert(cpu, duration) validFrames[frameId] = { start = frame.TickStartCpu, finish = frame.TickEndCpu } table.insert(worst, { frameId = frameId, absoluteId = frame.FrameAbsoluteId, cpuMs = duration }) if gpuClock and finite(frame.TickStartGpu) and finite(frame.TickEndGpu) and frame.TickStartGpu > 0 and frame.TickEndGpu >= frame.TickStartGpu then local gpuDuration = (frame.TickEndGpu - frame.TickStartGpu) * global.TickToMsGpu if finite(gpuDuration) then table.insert(gpu, gpuDuration) else excluded.missingGpu += 1 end else excluded.missingGpu += 1 end end end table.sort(worst, function(a, b) return a.cpuMs == b.cpuMs and a.frameId < b.frameId or a.cpuMs > b.cpuMs end) while #worst > 8 do table.remove(worst) end return { success = true, source = "microprofiler-buffer", frameIdMin = start, frameIdMax = last, consideredFrames = considered, omittedEarlierFrames = considered > 0 and math.max(0, start - first) or 0, frameGaps = gaps, excluded = excluded, cpu = distribution(cpu), gpu = distribution(gpu), worstFrames = worst, scopes = considered > 0 and Profiler.analyzeScopes(session, start, last, validFrames, global.TickToMsCpu) or { status = "unavailable", reason = "No frames available" }, percentileMethod = "nearest-rank", dataFormatVersion = session:GetDataFormatVersion(), warnings = { "Frame intervals are not per-script CPU costs. CPU and GPU intervals may overlap.", "This is the existing rolling buffer, not a controlled benchmark. Profiler overhead is not measured.", "Scope summaries include only complete within-frame CPU pairs. Counter trends, raw artifact retrieval and scenario comparison are not implemented yet.", }, } end function Profiler.snapshot(library, maxFrames, captureBuffer) if busy then return { success = false, error = "A romcp profiler snapshot is already running" } end busy = true local session local ok, result = pcall(function() assert(library.Control:IsBackendReady(), "LibMP backend is unavailable or incompatible") local captured = captureBuffer and captureBuffer() or library.Control:CaptureToBufferSync() assert( typeof(captured) == "buffer" and buffer.len(captured) <= 16 * 1024 * 1024, "Invalid or oversized profiler buffer" ) session = library.Session.OpenFromBuffer(captured) assert(session and session:IsValid(), "Could not open profiler snapshot") local report = Profiler.analyzeSession(session, maxFrames) report.captureBytes = buffer.len(captured) return report end) local removed, cleanupError = true, nil if session then removed, cleanupError = pcall(function() session:Dispose() end) end busy = false if not ok then return { success = false, error = tostring(result), cleanupRequired = not removed } end result.cleanupRequired = not removed or (result.scopes and result.scopes.cleanupRequired == true) if not removed then result.cleanupError = tostring(cleanupError) end return result end function Profiler.run(spec) local dependency = script.Parent:FindFirstChild("LibMP") if not dependency or not dependency:IsA("ModuleScript") then return { success = spec.action == "status", available = false, reason = "Official LibMP dependency is not installed alongside the romcp plugin modules", error = spec.action ~= "status" and "Official LibMP dependency is not installed" or nil, } end local ok, library = pcall(function() cachedLibrary = cachedLibrary or require(dependency) return cachedLibrary end) if not ok then return { success = false, available = false, error = "LibMP could not initialize in this Studio context" } end if spec.action == "status" then local ready, available = pcall(function() return library.Control:IsBackendReady() end) return { success = true, available = ready and available == true, dependencyPresent = true, mode = "existing-buffer", controlsModified = false, } end if spec.action ~= "snapshot" then return { success = false, error = "Unknown profiler action" } end -- Allocate only the checked size. If the engine changes its buffer during -- the read, parsing can fail; do not retry against a different sample silently. local sizeOk, size = pcall(function() return game:GetService("MicroProfilerService"):GetDataSize(0) end) if not sizeOk or not finite(size) or size <= 0 or size > 16 * 1024 * 1024 then return { success = false, error = "Profiler buffer is empty, inaccessible or exceeds 16 MiB" } end return Profiler.snapshot(library, spec.maxFrames or 64, function() local captured = buffer.create(size) game:GetService("MicroProfilerService"):GetDataInRange(0, 0, size, captured, 0) return captured end) end return Profiler ]]> Properties 1 or value[2] > 1 or value[3] > 1 then return Color3.fromRGB(value[1], value[2], value[3]) end return Color3.new(value[1], value[2], value[3]) end end end -- ═══════════════════════════════════════════ -- BrickColor -- ═══════════════════════════════════════════ if expectedType == "BrickColor" then if type(value) == "string" then return BrickColor.new(value) end if type(value) == "number" then return BrickColor.new(value) end end -- ═══════════════════════════════════════════ -- CFrame -- ═══════════════════════════════════════════ if expectedType == "CFrame" then if type(value) == "table" then if value.Position or value.position then local pos = value.Position or value.position local cf = CFrame.new( pos.x or pos.X or pos[1] or 0, pos.y or pos.Y or pos[2] or 0, pos.z or pos.Z or pos[3] or 0 ) if value.Rotation or value.rotation then local rot = value.Rotation or value.rotation cf = cf * CFrame.Angles( math.rad(rot.x or rot.X or rot[1] or 0), math.rad(rot.y or rot.Y or rot[2] or 0), math.rad(rot.z or rot.Z or rot[3] or 0) ) end return cf end -- Simple {x, y, z} if value[1] then return CFrame.new(value[1], value[2] or 0, value[3] or 0) end end end -- ═══════════════════════════════════════════ -- Rect (for GUI ImageRectOffset, etc.) -- ═══════════════════════════════════════════ if expectedType == "Rect" then if type(value) == "table" then return Rect.new( value[1] or value.Min and value.Min[1] or 0, value[2] or value.Min and value.Min[2] or 0, value[3] or value.Max and value.Max[1] or 0, value[4] or value.Max and value.Max[2] or 0 ) end end -- ═══════════════════════════════════════════ -- NumberRange -- ═══════════════════════════════════════════ if expectedType == "NumberRange" then if type(value) == "table" then return NumberRange.new(value.Min or value[1] or 0, value.Max or value[2] or 1) end if type(value) == "number" then return NumberRange.new(value) end end -- ═══════════════════════════════════════════ -- NumberSequence (for transparency gradients, etc.) -- ═══════════════════════════════════════════ if expectedType == "NumberSequence" then if type(value) == "table" then if value[1] and type(value[1]) == "table" then local keypoints = {} for _, kp in value do table.insert( keypoints, NumberSequenceKeypoint.new( kp.Time or kp[1] or 0, kp.Value or kp[2] or 0, kp.Envelope or kp[3] or 0 ) ) end return NumberSequence.new(keypoints) end if #value == 2 and type(value[1]) == "number" then return NumberSequence.new(value[1], value[2]) end end if type(value) == "number" then return NumberSequence.new(value) end end -- ═══════════════════════════════════════════ -- ColorSequence -- ═══════════════════════════════════════════ if expectedType == "ColorSequence" then if type(value) == "table" then if value[1] and type(value[1]) == "table" then local keypoints = {} for _, kp in value do local color = Color3.new(1, 1, 1) if kp.Color then if type(kp.Color) == "string" and kp.Color:sub(1, 1) == "#" then color = Color3.fromHex(kp.Color) elseif type(kp.Color) == "table" then color = Color3.new(kp.Color[1] or 0, kp.Color[2] or 0, kp.Color[3] or 0) end end table.insert(keypoints, ColorSequenceKeypoint.new(kp.Time or kp[1] or 0, color)) end return ColorSequence.new(keypoints) end end end -- ═══════════════════════════════════════════ -- Font -- ═══════════════════════════════════════════ if expectedType == "Font" then if type(value) == "table" then local family = value.Family or value.family or "rbxasset://fonts/families/SourceSansPro.json" local weight = Enum.FontWeight.Regular local style = Enum.FontStyle.Normal if value.Weight or value.weight then pcall(function() weight = Enum.FontWeight[value.Weight or value.weight] end) end if value.Style or value.style then pcall(function() style = Enum.FontStyle[value.Style or value.style] end) end return Font.new(family, weight, style) end if type(value) == "string" then return Font.new(value) end end -- ═══════════════════════════════════════════ -- EnumItem (any enum property) -- ═══════════════════════════════════════════ if type(value) == "string" then local currentVal = nil pcall(function() currentVal = instance[key] end) if typeof(currentVal) == "EnumItem" then local enumType = tostring(currentVal.EnumType) local enumOk, enumVal = pcall(function() return Enum[enumType][value] end) if enumOk then return enumVal end end end -- ═══════════════════════════════════════════ -- Fallback: type-agnostic guesses (no expectedType known) -- ═══════════════════════════════════════════ if type(value) == "table" and not expectedType then -- UDim2 guess: has XScale/YScale keys if value.XScale or value.xScale then return UDim2.new( value.XScale or value.xScale or 0, value.XOffset or value.xOffset or 0, value.YScale or value.yScale or 0, value.YOffset or value.yOffset or 0 ) end -- Vector3 guess: 3 number elements or X/Y/Z keys if (value.X or value.x) and (value.Y or value.y) and (value.Z or value.z) then return Vector3.new(value.X or value.x, value.Y or value.y, value.Z or value.z) end if value[1] and value[2] and value[3] and not value[4] then return Vector3.new(value[1], value[2], value[3]) end -- Color3 guess: R/G/B keys if value.R or value.r then return Color3.new(value.R or value.r or 0, value.G or value.g or 0, value.B or value.b or 0) end -- Vector2 guess: 2 number elements if value[1] and value[2] and not value[3] then return Vector2.new(value[1], value[2]) end end -- String-based color on color properties (fallback) if type(value) == "string" and (key == "Color" or key == "Color3" or key:match("Color$")) then if value:sub(1, 1) == "#" then return Color3.fromHex(value) end local ok, bc = pcall(BrickColor.new, value) if ok then return bc.Color end end if type(value) == "string" and key == "BrickColor" then return BrickColor.new(value) end return value end function Properties.set(path, propertiesToSet) local instance = Explorer.resolveInstance(path) if not instance then return { success = false, error = "Instance not found: " .. path } end local errors = {} local set = {} for key, value in propertiesToSet do local coerced = coerceValue(instance, key, value) local ok, err = pcall(function() instance[key] = coerced end) if ok then table.insert(set, key) else table.insert(errors, key .. ": " .. tostring(err)) end end if #errors > 0 then return { success = false, error = table.concat(errors, "; "), set = set } end return { success = true, set = set } end function Properties.bulkSet(spec) local ChangeHistoryService = game:GetService("ChangeHistoryService") local recording = ChangeHistoryService:TryBeginRecording("Dominus: Bulk set properties") local modified = 0 local errors = {} if spec.targets and type(spec.targets) == "table" then -- Explicit mode: array of {path, properties} for _, target in ipairs(spec.targets) do local instance = Explorer.resolveInstance(target.path) if not instance then table.insert(errors, "Not found: " .. tostring(target.path)) else for key, value in target.properties do local coerced = coerceValue(instance, key, value) local ok, err = pcall(function() instance[key] = coerced end) if not ok then table.insert(errors, target.path .. "." .. key .. ": " .. tostring(err)) end end modified = modified + 1 end end elseif spec.root and (spec.properties or spec.addChildren) then -- Filter mode: apply to all matching descendants local root = Explorer.resolveInstance(spec.root) if not root then if recording then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) end return { success = false, error = "Root not found: " .. tostring(spec.root) } end local function processInstance(inst) if spec.className and inst.ClassName ~= spec.className then return end if spec.properties then for key, value in spec.properties do local coerced = coerceValue(inst, key, value) local ok, err = pcall(function() inst[key] = coerced end) if not ok then table.insert(errors, Explorer.getPath(inst) .. "." .. key .. ": " .. tostring(err)) end end end -- addChildren: insert new child instances (e.g. UIStroke, UICorner) if spec.addChildren and type(spec.addChildren) == "table" then for _, childSpec in ipairs(spec.addChildren) do local childClass = childSpec.ClassName or childSpec.className if childClass then -- Skip if child of this class already exists (idempotent) local skipIfExists = childSpec.skipIfExists ~= false if skipIfExists then local existing = inst:FindFirstChildOfClass(childClass) if existing then continue end end local ok2, child = pcall(Instance.new, childClass) if ok2 and child then if childSpec.Name or childSpec.name then child.Name = childSpec.Name or childSpec.name end -- Set props on the new child local props = childSpec.properties or childSpec.Properties or childSpec.props or childSpec.Props or {} -- Also check flattened keys for k, v in childSpec do if k ~= "ClassName" and k ~= "className" and k ~= "Name" and k ~= "name" and k ~= "properties" and k ~= "Properties" and k ~= "props" and k ~= "Props" and k ~= "skipIfExists" and k ~= "Children" and k ~= "children" then props[k] = v end end for pk, pv in props do local coerced = coerceValue(child, pk, pv) pcall(function() child[pk] = coerced end) end child.Parent = inst else table.insert( errors, Explorer.getPath(inst) .. ": failed to create " .. tostring(childClass) ) end end end end modified = modified + 1 end -- Include root itself if it matches processInstance(root) for _, desc in root:GetDescendants() do processInstance(desc) end else if recording then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) end return { success = false, error = "Provide either 'targets' array or 'root' + 'properties'/'addChildren'" } end if recording then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) end return { success = true, modified = modified, errors = errors } end function Properties.inspectV2(spec) if type(spec.targets) ~= "table" or #spec.targets == 0 then return { success = false, error = "targets must contain at least one instance reference" } end if #spec.targets > 20 then return { success = false, error = "At most 20 instances can be inspected at once" } end local compact = spec.compact ~= false if spec.properties ~= nil then if type(spec.properties) ~= "table" or #spec.properties > 32 then return { success = false, error = "At most 32 requested properties are allowed" } end for _, name in spec.properties do if type(name) ~= "string" or not name:match("^[%a][%w_]*$") or name == "Source" then return { success = false, error = "Invalid requested property" } end end end local results = {} for index, targetRef in spec.targets do local instance, err = InstanceRegistry.resolve(targetRef) if not instance then table.insert(results, { success = false, index = index, error = err }) continue end local metadata = Reflection.getPropertyMetadata(instance.ClassName) local propertyNames = {} local requested = metadata if spec.properties ~= nil then requested = {} for _, name in spec.properties do if metadata[name] then requested[name] = true end end end for propertyName in requested do if not IGNORED_PROPERTIES[propertyName] and (not compact or not COMPACT_SKIP[propertyName]) then table.insert(propertyNames, propertyName) end end table.sort(propertyNames) local properties = {} for _, propertyName in propertyNames do local readOk, value = pcall(function() return instance[propertyName] end) if readOk and value ~= nil then table.insert(properties, { name = propertyName, type = metadata[propertyName].type, category = metadata[propertyName].category, writable = metadata[propertyName].writable, value = ValueCodec.encode(value), }) end end local children = spec.includeChildren == false and {} or instance:GetChildren() table.sort(children, function(a, b) if a.Name == b.Name then return a.ClassName < b.ClassName end return a.Name < b.Name end) local childRefs = {} for _, child in children do table.insert(childRefs, { name = child.Name, className = child.ClassName, ref = InstanceRegistry.toRef(child), }) end table.insert(results, { success = true, name = instance.Name, className = instance.ClassName, ref = InstanceRegistry.toRef(instance), properties = properties, children = childRefs, }) end local response = { success = true, results = results } if spec.includeTestContext == true then response.testContext = require(script.Parent.TestRunner).context() end return response end return Properties ]]> Protocol Reflection 0, className = className, properties = properties, methods = methods, events = events, } if classData then result.superclass = classData.Superclass and tostring(classData.Superclass) or nil result.newPermission = permitString(classData.Permits, "New") result.creatable = classData.Permits ~= nil and classData.Permits.New ~= nil end if classErr and #properties == 0 then result.error = "Could not reflect class: " .. tostring(classErr) end return result end function Reflection.getPropertyTypes(className) local properties = safeCall(function() return ReflectionService:GetPropertiesOfClass(className) end) if not properties then return {} end local result = {} for _, property in properties do result[property.Name] = typeName(property.Type) end return result end function Reflection.getPropertyMetadata(className) local properties = safeCall(function() return ReflectionService:GetPropertiesOfClass(className) end) if not properties then return {} end local result = {} for _, property in properties do result[property.Name] = { type = typeName(property.Type), category = displayCategory(property), writable = property.Permits == nil or property.Permits.Write ~= nil, } end return result end function Reflection.listClasses() local classes = safeCall(function() return ReflectionService:GetClasses() end) if not classes then return {} end local result = {} for _, classData in classes do table.insert(result, { name = classData.Name, superclass = classData.Superclass and tostring(classData.Superclass) or nil, creatable = classData.Permits ~= nil and classData.Permits.New ~= nil, }) end table.sort(result, function(a, b) return a.name < b.name end) return result end return Reflection ]]> Spatial 0 then local accumulator = {} includeBox(accumulator, cframe, size) local _, _, minimum, maximum = finishBounds(accumulator) return cframe, size, minimum, maximum end end local accumulator = {} for _, descendant in instance:GetDescendants() do if descendant:IsA("BasePart") then includeBox(accumulator, descendant.CFrame, descendant.Size) end end local cframe, size, minimum, maximum = finishBounds(accumulator) return cframe, size, minimum, maximum end local function validateTargets(targets) assert(type(targets) == "table" and #targets > 0, "targets must contain at least one instance") assert(#targets <= MAX_TARGETS, "targets is limited to " .. MAX_TARGETS .. " instances") for leftIndex, left in targets do for rightIndex = leftIndex + 1, #targets do local right = targets[rightIndex] assert( not left:IsAncestorOf(right) and not right:IsAncestorOf(left), "Targets " .. leftIndex .. " and " .. rightIndex .. " overlap" ) end end end local function isAxisAligned(orientation) for _, angle in { orientation.X, orientation.Y, orientation.Z } do if math.abs(angle - math.round(angle / 90) * 90) > 0.05 then return false end end return true end local function partAabb(part) local accumulator = {} includeBox(accumulator, part.CFrame, part.Size) local _, _, minimum, maximum = finishBounds(accumulator) return minimum, maximum end local function collectParts(targets, aggregateCenter, maxParts) local entries = {} local seen = {} local classCounts = {} local complexPartCount = 0 local rotatedPartCount = 0 local totalVolume = 0 local partCount = 0 local candidateLimit = math.max(200, maxParts * 4) local function addPart(part) if seen[part] then return end seen[part] = true partCount += 1 classCounts[part.ClassName] = (classCounts[part.ClassName] or 0) + 1 if part:IsA("MeshPart") or part:IsA("PartOperation") then complexPartCount += 1 end if not isAxisAligned(part.Orientation) then rotatedPartCount += 1 end local volume = part.Size.X * part.Size.Y * part.Size.Z totalVolume += volume if #entries >= candidateLimit then return end local minimum, maximum = partAabb(part) local path = InstanceRegistry.getDisplayPath(part) local output = { name = part.Name, className = part.ClassName, path = path, ref = InstanceRegistry.toRef(part), cframe = ValueCodec.encode(part.CFrame), position = ValueCodec.encode(part.Position), relativePosition = ValueCodec.encode(part.Position - aggregateCenter), orientation = ValueCodec.encode(part.Orientation), size = ValueCodec.encode(part.Size), color = ValueCodec.encode(part.Color), material = part.Material.Name, surfaces = { TopSurface = part.TopSurface.Name, BottomSurface = part.BottomSurface.Name, LeftSurface = part.LeftSurface.Name, RightSurface = part.RightSurface.Name, FrontSurface = part.FrontSurface.Name, BackSurface = part.BackSurface.Name, }, transparency = rounded(part.Transparency), anchored = part.Anchored, canCollide = part.CanCollide, } if part:IsA("Part") then output.shape = part.Shape.Name elseif part:IsA("MeshPart") then output.meshId = tostring(part.MeshId) output.textureId = tostring(part.TextureID) elseif part:IsA("PartOperation") then output.usePartColor = part.UsePartColor end table.insert(entries, { instance = part, path = path, volume = volume, minimum = minimum, maximum = maximum, output = output, }) end for _, target in targets do if target:IsA("BasePart") then addPart(target) end for _, descendant in target:GetDescendants() do if descendant:IsA("BasePart") then addPart(descendant) end end end return entries, { partCount = partCount, candidatePartCount = #entries, classCounts = classCounts, complexPartCount = complexPartCount, rotatedPartCount = rotatedPartCount, totalPartVolume = rounded(totalVolume), } end local function sampleParts(entries, maximum) if #entries <= maximum then table.sort(entries, function(left, right) return left.path < right.path end) return entries end local byVolume = table.clone(entries) table.sort(byVolume, function(left, right) if left.volume == right.volume then return left.path < right.path end return left.volume > right.volume end) local selected = {} local selectedInstances = {} local largestCount = math.max(1, math.floor(maximum * 0.6)) for index = 1, largestCount do local entry = byVolume[index] table.insert(selected, entry) selectedInstances[entry.instance] = true end local byPath = table.clone(entries) table.sort(byPath, function(left, right) return left.path < right.path end) for _, entry in byPath do if #selected >= maximum then break end if not selectedInstances[entry.instance] then table.insert(selected, entry) selectedInstances[entry.instance] = true end end table.sort(selected, function(left, right) return left.path < right.path end) return selected end local function edgeGap(left, right) local dx = math.max(0, left.minimum.X - right.maximum.X, right.minimum.X - left.maximum.X) local dy = math.max(0, left.minimum.Y - right.maximum.Y, right.minimum.Y - left.maximum.Y) local dz = math.max(0, left.minimum.Z - right.maximum.Z, right.minimum.Z - left.maximum.Z) return math.sqrt(dx * dx + dy * dy + dz * dz) end local function buildRelationships(entries, maximum) if maximum <= 0 or #entries < 2 then return {} end local relationships = {} local seenPairs = {} for leftIndex, left in entries do local nearest = nil local nearestDistance = math.huge for rightIndex, right in entries do if leftIndex ~= rightIndex then local distance = edgeGap(left, right) if distance < nearestDistance then nearest = right nearestDistance = distance end end end if nearest then local firstPath, secondPath = left.path, nearest.path if secondPath < firstPath then firstPath, secondPath = secondPath, firstPath end local pairKey = firstPath .. "\0" .. secondPath if not seenPairs[pairKey] then seenPairs[pairKey] = true local offset = nearest.instance.Position - left.instance.Position table.insert(relationships, { from = left.path, to = nearest.path, offset = ValueCodec.encode(offset), centerDistance = rounded(offset.Magnitude), surfaceGap = rounded(nearestDistance), touchingOrOverlapping = nearestDistance <= 0.05, }) end end end table.sort(relationships, function(left, right) if left.surfaceGap == right.surfaceGap then return left.centerDistance < right.centerDistance end return left.surfaceGap < right.surfaceGap end) while #relationships > maximum do table.remove(relationships) end return relationships end local function captureErrors(capture) local ok, errors = pcall(function() return capture:GetErrors() end) if not ok or type(errors) ~= "table" or #errors == 0 then return "Studio screenshot capture failed" end local messages = {} for _, item in errors do table.insert(messages, tostring(item)) end return table.concat(messages, "; ") end local function waitForCapture(capture) local deadline = os.clock() + CAPTURE_TIMEOUT_SECONDS while os.clock() < deadline do if capture.BufferStatus == Enum.StudioCaptureBufferStatus.Ready then return true end if capture.BufferStatus == Enum.StudioCaptureBufferStatus.Error then return false, captureErrors(capture) end task.wait(0.03) end return false, "Studio screenshot capture timed out" end local function projectedCrop(camera, boundsCFrame, boundsSize) local viewport = camera.ViewportSize local minimum = Vector2.new(viewport.X, viewport.Y) local maximum = Vector2.zero local visible = false for _, point in corners(boundsCFrame, boundsSize) do local projected = camera:WorldToViewportPoint(point) if projected.Z > 0 then visible = true minimum = Vector2.new(math.min(minimum.X, projected.X), math.min(minimum.Y, projected.Y)) maximum = Vector2.new(math.max(maximum.X, projected.X), math.max(maximum.Y, projected.Y)) end end if not visible then return Vector2.zero, viewport end local margin = math.max(8, math.min(viewport.X, viewport.Y) * 0.025) minimum = Vector2.new(math.max(0, minimum.X - margin), math.max(0, minimum.Y - margin)) maximum = Vector2.new(math.min(viewport.X, maximum.X + margin), math.min(viewport.Y, maximum.Y + margin)) local resolution = maximum - minimum if resolution.X < 32 or resolution.Y < 32 then return Vector2.zero, viewport end return Vector2.new(math.floor(minimum.X), math.floor(minimum.Y)), Vector2.new(math.ceil(resolution.X), math.ceil(resolution.Y)) end local function scaleCapture(capture, maxImageSize) local current = capture local currentSize = current.Resolution local largest = math.max(currentSize.X, currentSize.Y) if largest > maxImageSize then local scale = maxImageSize / largest local nextSize = Vector2.new(math.max(1, math.round(currentSize.X * scale)), math.max(1, math.round(currentSize.Y * scale))) current = current:ScaleAsync(Enum.ResamplerMode.Default, nextSize) local ready, err = waitForCapture(current) assert(ready, err) end return current end local function takeScreenshot(boundsCFrame, boundsSize, spec) local serviceOk, StudioCaptureService = pcall(function() return game:GetService("StudioCaptureService") end) if not serviceOk then return nil, { captured = false, error = "StudioCaptureService is unavailable in this Studio version" } end local capabilityOk, canCapture = pcall(function() return StudioCaptureService:CanCaptureScreenshot() end) if not capabilityOk then return nil, { captured = false, error = "Studio screenshot capture is unavailable in this Studio version" } end if not canCapture then local permissionOk, granted = pcall(function() return StudioCaptureService:RequestScreenshotPermissionAsync() end) local recheckOk, allowed = pcall(function() return StudioCaptureService:CanCaptureScreenshot() end) if not permissionOk or not granted or not recheckOk or not allowed then return nil, { captured = false, error = "Studio screenshot permission was not granted" } end end local camera = workspace.CurrentCamera if not camera then return nil, { captured = false, error = "Workspace has no current camera" } end local savedCFrame = camera.CFrame local savedFocus = camera.Focus local savedCameraType = camera.CameraType local captures = {} local ok, imageOrError, metadata = pcall(function() local direction = VIEW_DIRECTIONS[spec.view or "isometric"] assert(direction ~= nil, "view is invalid") local center = boundsCFrame.Position local distance = math.max(boundsSize.Magnitude, 4) local up = math.abs(direction.Unit.Y) > 0.95 and Vector3.zAxis or Vector3.yAxis camera.CFrame = CFrame.lookAt(center + direction.Unit * distance, center, up) camera.Focus = CFrame.new(center) local padding = finiteNumber(spec.padding or 0.15, "padding") assert(padding >= 0.05 and padding <= 0.5, "padding must be from 0.05 to 0.5") camera:ZoomToExtents(boundsCFrame, boundsSize * (1 + padding * 2)) task.wait(0.08) local cropPosition, cropResolution = projectedCrop(camera, boundsCFrame, boundsSize) local function startCapture(useCrop) local options = { BufferFormat = Enum.StudioCaptureScreenshotFormat.PNG, UICaptureMode = Enum.UICaptureMode.None, } if useCrop then options.Position = cropPosition options.Resolution = cropResolution end local capture = StudioCaptureService:CaptureScreenshot(options) table.insert(captures, capture) return capture end local cropOk, capture = pcall(startCapture, true) if not cropOk then capture = startCapture(false) end local ready, captureErr = waitForCapture(capture) if not ready then capture = startCapture(false) ready, captureErr = waitForCapture(capture) end assert(ready, captureErr) assert( capture.BufferFormat == Enum.StudioCaptureScreenshotFormat.PNG, "Studio returned a non-PNG screenshot buffer" ) local maxImageSize = math.floor(finiteNumber(spec.maxImageSize or 640, "maxImageSize")) assert(maxImageSize >= 256 and maxImageSize <= 768, "maxImageSize must be from 256 to 768") capture = scaleCapture(capture, maxImageSize) if captures[#captures] ~= capture then table.insert(captures, capture) end local raw = capture:GetBuffer() assert(typeof(raw) == "buffer", "Studio returned an invalid screenshot buffer") while buffer.len(raw) > MAX_SCREENSHOT_BYTES and math.max(capture.Resolution.X, capture.Resolution.Y) > 192 do local nextSize = Vector2.new( math.max(1, math.round(capture.Resolution.X * 0.75)), math.max(1, math.round(capture.Resolution.Y * 0.75)) ) capture = capture:ScaleAsync(Enum.ResamplerMode.Default, nextSize) table.insert(captures, capture) local scaledReady, scaledErr = waitForCapture(capture) assert(scaledReady, scaledErr) raw = capture:GetBuffer() end assert(buffer.len(raw) <= MAX_SCREENSHOT_BYTES, "Screenshot is too large for the Dominus bridge") local EncodingService = game:GetService("EncodingService") local encoded = EncodingService:Base64Encode(raw) return buffer.tostring(encoded), { captured = true, mimeType = "image/png", view = spec.view or "isometric", position = ValueCodec.encode(capture.Position), resolution = ValueCodec.encode(capture.Resolution), originalSize = ValueCodec.encode(capture.OriginalSize), byteLength = buffer.len(raw), cameraCFrame = ValueCodec.encode(camera.CFrame), } end) if spec.keepCamera ~= true then camera.CameraType = savedCameraType camera.CFrame = savedCFrame camera.Focus = savedFocus end for _, capture in captures do pcall(function() capture:Destroy() end) end if not ok then return nil, { captured = false, error = tostring(imageOrError) } end return imageOrError, metadata end function Spatial.review(spec) local targets = {} for index, targetRef in spec.targets do table.insert(targets, resolve(targetRef, "Target " .. index)) end validateTargets(targets) local aggregate = {} local targetReports = {} for _, target in targets do local cframe, size, minimum, maximum = getBounds(target) includeBox(aggregate, cframe, size) local report = { name = target.Name, className = target.ClassName, ref = InstanceRegistry.toRef(target), bounds = { cframe = ValueCodec.encode(cframe), center = ValueCodec.encode(cframe.Position), size = ValueCodec.encode(size), minimum = ValueCodec.encode(minimum), maximum = ValueCodec.encode(maximum), }, } if target:IsA("PVInstance") then report.pivot = ValueCodec.encode(target:GetPivot()) end table.insert(targetReports, report) end local boundsCFrame, boundsSize, minimum, maximum = finishBounds(aggregate) local maxParts = spec.maxParts or 80 assert( type(maxParts) == "number" and maxParts % 1 == 0 and maxParts >= 1 and maxParts <= MAX_PARTS, "maxParts must be an integer from 1 to " .. MAX_PARTS ) local maxRelationships = spec.maxRelationships or 24 assert( type(maxRelationships) == "number" and maxRelationships % 1 == 0 and maxRelationships >= 0 and maxRelationships <= 50, "maxRelationships must be an integer from 0 to 50" ) local candidateParts, metrics = collectParts(targets, boundsCFrame.Position, maxParts) local sampled = sampleParts(candidateParts, maxParts) local partReports = {} for _, entry in sampled do table.insert(partReports, entry.output) end local relationships = buildRelationships(sampled, maxRelationships) local touchingPairCount = 0 for _, relationship in relationships do if relationship.touchingOrOverlapping then touchingPairCount += 1 end end local boundsVolume = boundsSize.X * boundsSize.Y * boundsSize.Z metrics.boundsVolume = rounded(boundsVolume) metrics.approximateOccupancy = boundsVolume > 0 and rounded(metrics.totalPartVolume / boundsVolume) or 0 metrics.touchingOrOverlappingRelationshipCount = touchingPairCount local report = { success = true, targetCount = #targets, partCount = metrics.partCount, includedPartCount = #sampled, truncated = #sampled < metrics.partCount, bounds = { cframe = ValueCodec.encode(boundsCFrame), center = ValueCodec.encode(boundsCFrame.Position), size = ValueCodec.encode(boundsSize), minimum = ValueCodec.encode(minimum), maximum = ValueCodec.encode(maximum), }, metrics = metrics, targets = targetReports, parts = partReports, relationships = relationships, relationshipMethod = "nearest sampled parts using world-axis-aligned bounds; rotated-part overlap is approximate", } local screenshotMode = spec.screenshot or "auto" assert(screenshotMode == "auto" or screenshotMode == "always" or screenshotMode == "never", "screenshot is invalid") local shouldCapture = screenshotMode == "always" local captureReason = screenshotMode == "always" and "explicitly requested" or nil if screenshotMode == "auto" then if metrics.partCount >= 8 then shouldCapture = true captureReason = "target contains at least 8 parts" elseif metrics.complexPartCount > 0 then shouldCapture = true captureReason = "target contains mesh or CSG geometry" elseif metrics.rotatedPartCount >= 3 then shouldCapture = true captureReason = "target contains several rotated parts" elseif #targets > 1 then shouldCapture = true captureReason = "review spans multiple targets" end end if shouldCapture then local imageBase64, screenshot = takeScreenshot(boundsCFrame, boundsSize, spec) screenshot.mode = screenshotMode screenshot.reason = captureReason report.screenshot = screenshot if imageBase64 then report.imageBase64 = imageBase64 end else report.screenshot = { captured = false, mode = screenshotMode, reason = screenshotMode == "never" and "disabled" or "simple geometry did not require a visual pass", } end return report end return Spatial ]]> TestRunner 600000 then return { success = false, error = "Invalid test timeout" } end if type(players) ~= "number" or players % 1 ~= 0 or players < 1 or players > 8 then return { success = false, error = "Invalid player count" } end local id = HttpService:GenerateGUID(false) local run = { id = id, mode = mode, phase = "starting", startedAt = os.clock(), timedOut = false } activeRunId = id managedRuns[id] = run table.insert(managedOrder, id) while #managedOrder > 10 do managedRuns[table.remove(managedOrder, 1)] = nil end -- A server-side copy owns the deadline. Client LeaveTest is not a test-stop API. local harness local installed, installError = pcall(function() harness = Instance.new("Script") harness.Name = "DominusTestDeadline_" .. id harness:SetAttribute("DominusRunId", id) harness.Source = [[ local RunService = game:GetService("RunService") if not RunService:IsStudio() or not RunService:IsServer() then return end local service = game:GetService("StudioTestService") local ok, args = pcall(function() return service:GetTestArgs() end) if not ok or type(args) ~= "table" or type(args.__dominusManaged) ~= "table" then return end local run = args.__dominusManaged if run.runId ~= script:GetAttribute("DominusRunId") then return end -- Publish only in the runtime DataModel, never in the saved edit place. -- A replication failure must not disable the server deadline below. local published, publishError = pcall(function() game:GetService("ReplicatedStorage"):SetAttribute("RomcpManagedRunId", run.runId) end) if not published then warn("Romcp client test identity could not be published: " .. tostring(publishError)) end task.delay(run.timeoutMs / 1000, function() local currentOk, current = pcall(function() return service:GetTestArgs() end) if not currentOk or type(current) ~= "table" or type(current.__dominusManaged) ~= "table" or current.__dominusManaged.runId ~= run.runId then return end local ended, err = pcall(function() service:EndTest({ __dominusOutcome = { runId = run.runId, timedOut = true } }) end) if not ended then warn("Dominus managed test deadline could not end the test: " .. tostring(err)) end end) ]] harness.Parent = game:GetService("ServerScriptService") end) if not installed then local removed = pcall(function() if harness then harness:Destroy() end end) run.cleanupRequired = not removed activeRunId = nil run.phase = "failed" run.error = tostring(installError) run.finishedAt = os.clock() return snapshot(run) end run.timer = task.delay(timeoutMs / 1000 + 5, function() run.timer = nil if activeRunId == id then run.timedOut = true run.phase = "timeout-pending" end end) task.defer(function() run.phase = "executing" local args = { __dominusManaged = { runId = id, timeoutMs = timeoutMs }, args = spec.args } local ok, result = pcall(function() if mode == "run" then return StudioTestService:ExecuteRunModeAsync(args) end if mode == "play" then return StudioTestService:ExecutePlayModeAsync(args) end return StudioTestService:ExecuteMultiplayerTestAsync(players, args) end) local removed, removeError = pcall(function() harness:Destroy() end) run.cleanupRequired = not removed if run.timer then pcall(task.cancel, run.timer) run.timer = nil end run.finishedAt = os.clock() if activeRunId == id then activeRunId = nil end local outcome = type(result) == "table" and result.__dominusOutcome run.timedOut = run.timedOut or (type(outcome) == "table" and outcome.runId == id and outcome.timedOut == true) run.phase = not ok and "failed" or run.timedOut and "timed-out" or (type(outcome) == "table" and outcome.runId == id and outcome.stopped == true) and "stopped" or "completed" if ok and result ~= nil then local encoded local serialized, text = pcall(function() encoded = ValueCodec.encodeSerializable(result, 500) return HttpService:JSONEncode(encoded) end) if serialized and #text <= 65536 then run.result = encoded else run.result = { truncated = true, reason = "Test result exceeds the inline result budget or cannot be encoded" } end end run.error = not ok and tostring(result) or not removed and tostring(removeError) or nil end) return snapshot(run) end function TestRunner.status(spec) local run = managedRuns[spec.runId] if not run then return { success = false, error = "Unknown managed run on this connection" } end return snapshot(run) end function TestRunner.stop(spec) -- EndTest is supported only in the server DataModel. Never replace it with -- RunService:Stop(), which can retain simulation changes in the place. if StudioTestService.EditModeActive or not RunService:IsServer() then return { success = false, error = "Select this test's server runtime connection to stop it", requiresServerConnection = true, } end local ok, args = pcall(function() return StudioTestService:GetTestArgs() end) if not ok or type(args) ~= "table" or type(args.__dominusManaged) ~= "table" or args.__dominusManaged.runId ~= spec.runId then return { success = false, error = "This server does not belong to the requested managed run" } end local ended, err = pcall(function() StudioTestService:EndTest({ __dominusOutcome = { runId = spec.runId, stopped = true } }) end) return ended and { success = true, runId = spec.runId, phase = "stop-requested" } or { success = false, error = tostring(err) } end function TestRunner.context() local role = StudioTestService.EditModeActive and "edit" or RunService:IsServer() and "server" or "client" local runId = role == "edit" and activeRunId or nil if role == "server" then local ok, args = pcall(function() return StudioTestService:GetTestArgs() end) if ok and type(args) == "table" and type(args.__dominusManaged) == "table" then runId = args.__dominusManaged.runId end elseif role == "client" and RunService:IsStudio() then local marker = ReplicatedStorage:GetAttribute(RUN_ATTRIBUTE) if type(marker) == "string" and #marker > 0 and #marker <= 100 then runId = marker end end return { success = true, role = role, runId = runId } end function TestRunner.executeV2(spec) if activeRunId then return { success = false, error = "A Dominus Studio test is already running" } end local mode = spec.mode or "run" if mode ~= "run" and mode ~= "play" and mode ~= "multiplayer" then return { success = false, error = "mode must be run, play, or multiplayer" } end local timeoutSeconds = math.clamp((spec.timeoutMs or 120000) / 1000, 5, 600) local players = math.clamp(spec.players or 1, 1, 8) local runId = tostring(os.clock()) .. ":" .. mode activeRunId = runId local timedOut = false task.delay(timeoutSeconds, function() if activeRunId ~= runId then return end timedOut = true -- Legacy calls cannot end the server test from this edit context. -- Managed sessions install a server-side deadline helper instead. end) local startedAt = os.clock() local ok, result = pcall(function() if mode == "run" then return StudioTestService:ExecuteRunModeAsync(spec.args) elseif mode == "play" then return StudioTestService:ExecutePlayModeAsync(spec.args) end return StudioTestService:ExecuteMultiplayerTestAsync(players, spec.args) end) local duration = os.clock() - startedAt if activeRunId == runId then activeRunId = nil end if timedOut then return { success = false, timedOut = true, error = "Studio test exceeded its timeout", duration = duration } end if not ok then return { success = false, error = tostring(result), duration = duration } end return { success = true, mode = mode, players = mode == "multiplayer" and players or nil, result = result, duration = duration, } end function TestRunner.endTest(value) local ok, err = pcall(function() StudioTestService:EndTest(value) end) return ok and { success = true } or { success = false, error = tostring(err) } end return TestRunner ]]> Theme Studio style guide color. These follow Studio's own -- widgets, so the panel matches Explorer and Properties in either theme. local GUIDE = { background = Enum.StudioStyleGuideColor.MainBackground, card = Enum.StudioStyleGuideColor.Item, input = Enum.StudioStyleGuideColor.InputFieldBackground, inputLine = Enum.StudioStyleGuideColor.InputFieldBorder, ink = Enum.StudioStyleGuideColor.MainText, muted = Enum.StudioStyleGuideColor.SubText, dimmed = Enum.StudioStyleGuideColor.DimmedText, line = Enum.StudioStyleGuideColor.Border, button = Enum.StudioStyleGuideColor.Button, buttonText = Enum.StudioStyleGuideColor.ButtonText, buttonLine = Enum.StudioStyleGuideColor.ButtonBorder, bright = Enum.StudioStyleGuideColor.BrightText, warning = Enum.StudioStyleGuideColor.WarningText, error = Enum.StudioStyleGuideColor.ErrorText, scrollBar = Enum.StudioStyleGuideColor.ScrollBar, } -- Colors Studio has no style guide entry for. Brand accent is deliberately -- constant across themes; success needs a lighter value on dark backgrounds -- to stay legible. local STATIC = { accent = { light = BRAND_ACCENT, dark = BRAND_ACCENT }, onAccent = { light = Color3.fromRGB(255, 253, 246), dark = Color3.fromRGB(255, 253, 246) }, success = { light = Color3.fromRGB(92, 143, 34), dark = Color3.fromRGB(150, 202, 92) }, } local function guideColor(guide, modifier) local ok, value = pcall(function() return studio.Theme:GetColor(guide, modifier or Enum.StudioStyleGuideModifier.Default) end) if ok and typeof(value) == "Color3" then return value end return nil end -- Derived from the resolved background rather than Theme.Name so custom or -- future Studio themes still pick the correct static variants. function Theme.isDark() local background = guideColor(GUIDE.background) if not background then return false end local luminance = 0.299 * background.R + 0.587 * background.G + 0.114 * background.B return luminance < 0.5 end function Theme.color(key, modifier) local guide = GUIDE[key] if guide then local resolved = guideColor(guide, modifier) if resolved then return resolved end end local static = STATIC[key] if static then return Theme.isDark() and static.dark or static.light end return FALLBACK end local bindings = {} local listeners = {} local function applyBinding(binding) pcall(function() binding.instance[binding.property] = Theme.color(binding.key, binding.modifier) end) end -- Binds a property to a semantic color for the lifetime of the instance. function Theme.bind(instance, property, key, modifier) local binding = { instance = instance, property = property, key = key, modifier = modifier, } table.insert(bindings, binding) applyBinding(binding) instance.Destroying:Connect(function() binding.dead = true end) return instance end -- Registers a callback for colors that depend on runtime state (connection -- status, toggle position) and therefore cannot be a static binding. function Theme.onChanged(callback) table.insert(listeners, callback) return function() local index = table.find(listeners, callback) if index then table.remove(listeners, index) end end end function Theme.refresh() for index = #bindings, 1, -1 do local binding = bindings[index] if binding.dead then table.remove(bindings, index) else applyBinding(binding) end end for _, listener in listeners do local ok, err = pcall(listener) if not ok then warn("[Dominus 2] Theme listener failed: " .. tostring(err)) end end end local connection = nil function Theme.start() if connection then return end local ok, err = pcall(function() connection = studio.ThemeChanged:Connect(Theme.refresh) end) if not ok then warn("[Dominus 2] Could not observe Studio theme changes: " .. tostring(err)) end end function Theme.stop() if connection then connection:Disconnect() connection = nil end end return Theme ]]> UIBuilder 1 or value[2] > 1 or value[3] > 1 then return Color3.fromRGB(value[1], value[2], value[3]) end return Color3.new(value[1], value[2], value[3]) end if value.R or value.r then return Color3.new(value.R or value.r or 0, value.G or value.g or 0, value.B or value.b or 0) end end return Color3.new(1, 1, 1) end local function resolveUDim2(value) if type(value) == "table" then if value.XScale or value.xScale then return UDim2.new( value.XScale or value.xScale or 0, value.XOffset or value.xOffset or 0, value.YScale or value.yScale or 0, value.YOffset or value.yOffset or 0 ) end if value.X and type(value.X) == "table" then return UDim2.new( value.X.Scale or value.X[1] or 0, value.X.Offset or value.X[2] or 0, value.Y and (value.Y.Scale or value.Y[1]) or 0, value.Y and (value.Y.Offset or value.Y[2]) or 0 ) end if value[1] ~= nil then return UDim2.new(value[1] or 0, value[2] or 0, value[3] or 0, value[4] or 0) end end if type(value) == "string" then local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)") if a then return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d)) end end return UDim2.new(0, 0, 0, 0) end local function resolveUDim(value) if type(value) == "table" then return UDim.new(value.Scale or value[1] or 0, value.Offset or value[2] or 0) end if type(value) == "number" then return UDim.new(0, value) end return UDim.new(0, 0) end local function resolveVector2(value) if type(value) == "table" then return Vector2.new(value.X or value.x or value[1] or 0, value.Y or value.y or value[2] or 0) end return Vector2.new(0, 0) end local function resolveEnum(instance, key, value) if type(value) ~= "string" then return value end local curOk, curVal = pcall(function() return instance[key] end) if curOk and typeof(curVal) == "EnumItem" then local enumType = tostring(curVal.EnumType) local ok, enumVal = pcall(function() return Enum[enumType][value] end) if ok then return enumVal end end return value end local function resolveFont(value) if type(value) == "string" then local ok, font = pcall(function() return Enum.Font[value] end) if ok then return font end end return value end local UDIM2_PROPS = { Size = true, Position = true, CellSize = true, CellPadding = true, } local COLOR3_PROPS = { BackgroundColor3 = true, BorderColor3 = true, TextColor3 = true, ImageColor3 = true, PlaceholderColor3 = true, TextStrokeColor3 = true, ScrollBarImageColor3 = true, } local function resolveColorSequence(value) if type(value) == "table" then -- Array of keypoints: {{time, color}, ...} if type(value[1]) == "table" then local keypoints = {} for _, kp in ipairs(value) do local t = kp[1] or kp.Time or 0 local c = resolveColor(kp[2] or kp.Color or kp) table.insert(keypoints, ColorSequenceKeypoint.new(t, c)) end return ColorSequence.new(keypoints) end -- Single color as array [r,g,b] → uniform ColorSequence if type(value[1]) == "number" and #value == 3 then local c = resolveColor(value) return ColorSequence.new(c) end end -- String hex/rgb → uniform ColorSequence if type(value) == "string" then return ColorSequence.new(resolveColor(value)) end return ColorSequence.new(Color3.new(1, 1, 1)) end local function resolveNumberSequence(value) if type(value) == "table" then -- Array of keypoints: {{time, value, envelope?}, ...} if type(value[1]) == "table" then local keypoints = {} for _, kp in ipairs(value) do local t = kp[1] or kp.Time or 0 local v = kp[2] or kp.Value or 0 local e = kp[3] or kp.Envelope or 0 table.insert(keypoints, NumberSequenceKeypoint.new(t, v, e)) end return NumberSequence.new(keypoints) end end if type(value) == "number" then return NumberSequence.new(value) end return NumberSequence.new(0) end local VECTOR2_PROPS = { AnchorPoint = true, CanvasSize_v2 = true, AbsoluteSize = true, AbsolutePosition = true, } local UDIM_PROPS = { CornerRadius = true, Padding = true, PaddingTop = true, PaddingBottom = true, PaddingLeft = true, PaddingRight = true, FillDirection = false, -- not UDim } local NUMBER_RANGE_PROPS = { Range = true, RotationRange = true, Size_NR = true, -- particle size range alias } local RECT_PROPS = { SliceCenter = true, ImageRectOffset_Rect = true, } local function resolveNumberRange(value) if type(value) == "table" then return NumberRange.new(value[1] or 0, value[2] or value[1] or 0) end if type(value) == "number" then return NumberRange.new(value) end return NumberRange.new(0) end local function resolveRect(value) if type(value) == "table" then if #value == 4 then return Rect.new(value[1], value[2], value[3], value[4]) end return Rect.new( value.Min and value.Min[1] or value.MinX or 0, value.Min and value.Min[2] or value.MinY or 0, value.Max and value.Max[1] or value.MaxX or 0, value.Max and value.Max[2] or value.MaxY or 0 ) end return Rect.new(0, 0, 0, 0) end -- Disambiguate 2-element arrays: check actual property type on the instance local function resolve2ElementArray(instance, key, value) -- First, check our known lookup tables if VECTOR2_PROPS[key] then return resolveVector2(value) end if UDIM_PROPS[key] then return resolveUDim(value) end if NUMBER_RANGE_PROPS[key] then return resolveNumberRange(value) end -- Fall back to runtime type introspection local ok, curVal = pcall(function() return instance[key] end) if ok and curVal ~= nil then local t = typeof(curVal) if t == "Vector2" then return resolveVector2(value) end if t == "UDim" then return resolveUDim(value) end if t == "NumberRange" then return resolveNumberRange(value) end end -- Default: assume UDim for 2-element (more common in UI) return resolveUDim(value) end local function applyProperty(instance, key, value) -- Special handling by property name if key == "Size" or key == "Position" then if instance:IsA("GuiObject") or instance:IsA("UIBase") then instance[key] = resolveUDim2(value) return end end if key == "AnchorPoint" then if instance:IsA("GuiObject") then instance[key] = resolveVector2(value) return end end if key == "CanvasSize" and instance:IsA("ScrollingFrame") then instance[key] = resolveUDim2(value) return end if UDIM_PROPS[key] then instance[key] = resolveUDim(value) return end -- Use reflection to distinguish Color3 vs ColorSequence, NumberSequence, etc. local propType = getPropertyType(instance.ClassName, key) if propType == "ColorSequence" then instance[key] = resolveColorSequence(value) return end if propType == "NumberSequence" then instance[key] = resolveNumberSequence(value) return end if COLOR3_PROPS[key] or propType == "Color3" then instance[key] = resolveColor(value) return end -- Reflection-based coercion for types not in hardcoded tables if propType == "UDim2" and type(value) == "table" then instance[key] = resolveUDim2(value) return end if propType == "UDim" then instance[key] = resolveUDim(value) return end if propType == "Vector2" then instance[key] = resolveVector2(value) return end if propType == "NumberRange" then instance[key] = resolveNumberRange(value) return end if propType == "Rect" then instance[key] = resolveRect(value) return end -- Rect properties (4-element arrays like SliceCenter) if RECT_PROPS[key] then instance[key] = resolveRect(value) return end -- NumberRange properties if NUMBER_RANGE_PROPS[key] then instance[key] = resolveNumberRange(value) return end if key == "Font" and (instance:IsA("TextLabel") or instance:IsA("TextButton") or instance:IsA("TextBox")) then instance[key] = resolveFont(value) return end if key == "FontFace" then if type(value) == "table" then local family = value.Family or "rbxasset://fonts/families/SourceSansPro.json" local weight = Enum.FontWeight.Regular local style = Enum.FontStyle.Normal pcall(function() weight = Enum.FontWeight[value.Weight or "Regular"] end) pcall(function() style = Enum.FontStyle[value.Style or "Normal"] end) instance.FontFace = Font.new(family, weight, style) end return end -- Enum properties (string values) if type(value) == "string" then local resolved = resolveEnum(instance, key, value) local ok, err = pcall(function() instance[key] = resolved end) if not ok then -- Maybe it's a direct value pcall(function() instance[key] = value end) end return end if UDIM2_PROPS[key] then pcall(function() instance[key] = resolveUDim2(value) end) return end -- Smart disambiguation for 2-element table arrays if type(value) == "table" and #value == 2 then local resolved = resolve2ElementArray(instance, key, value) pcall(function() instance[key] = resolved end) return end -- 4-element table arrays: try Rect if not already handled as UDim2 if type(value) == "table" and #value == 4 then -- Check actual property type local ok, curVal = pcall(function() return instance[key] end) if ok and curVal ~= nil then local t = typeof(curVal) if t == "Rect" then pcall(function() instance[key] = resolveRect(value) end) return elseif t == "UDim2" then pcall(function() instance[key] = resolveUDim2(value) end) return end end -- Default 4-element to UDim2 pcall(function() instance[key] = resolveUDim2(value) end) return end -- Direct assignment for numbers, booleans, etc. pcall(function() instance[key] = value end) end local RESERVED_KEYS = { ClassName = true, className = true, Name = true, name = true, Children = true, children = true, properties = true, Properties = true, props = true, Props = true, } local function buildNode(spec, parent, animate) local className = spec.ClassName or spec.className or "Frame" local name = spec.Name or spec.name or className local instance = Instance.new(className) instance.Name = name -- Apply flattened top-level properties for key, value in spec do if not RESERVED_KEYS[key] then applyProperty(instance, key, value) end end -- Also accept a nested "properties" / "Properties" / "props" bag for convenience. -- This lets callers group properties explicitly without the key being misread -- as an instance property name. local propBag = spec.properties or spec.Properties or spec.props or spec.Props if type(propBag) == "table" then for key, value in propBag do if not RESERVED_KEYS[key] then applyProperty(instance, key, value) end end end -- Set parent after properties to avoid unnecessary layout computations instance.Parent = parent -- Build children recursively local children = spec.Children or spec.children if children then for _, childSpec in children do if animate then task.wait(0.05) end buildNode(childSpec, instance, animate) end end return instance end function UIBuilder.build(spec) local parentPath = spec.parent or "StarterGui" local parent = Explorer.resolveInstance(parentPath) if not parent then -- Try as a service local ok ok, parent = pcall(function() return game:GetService(parentPath) end) if not ok then return { success = false, error = "Parent not found: " .. parentPath } end end -- Clean up existing instance with same name local existingName = spec.tree and (spec.tree.Name or spec.tree.name) or nil if existingName then local existing = parent:FindFirstChild(existingName) if existing then existing:Destroy() end end local tree = spec.tree if not tree then return { success = false, error = "No 'tree' provided in spec" } end local animate = false if spec.animate ~= nil then animate = spec.animate end local ok, result = pcall(function() return buildNode(tree, parent, animate) end) if not ok then return { success = false, error = "Build failed: " .. tostring(result) } end local path = Explorer.getPath(result) local count = 0 local function countDescendants(inst) count = count + 1 for _, child in inst:GetChildren() do countDescendants(child) end end countDescendants(result) return { success = true, path = path, instanceCount = count } end --[[ Serialize: convert an existing instance tree into create_ui-compatible JSON. Reads properties via ReflectionService, converts Roblox types to JSON-friendly values, and skips default/unchanged values to keep output minimal. ]] local Reflection = require(script.Parent.Reflection) local ReflectionService = game:GetService("ReflectionService") -- Convert a Roblox value to a JSON-friendly representation local function valueToJson(value, propType) local t = typeof(value) if t == "UDim2" then return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset } end if t == "UDim" then return { value.Scale, value.Offset } end if t == "Vector2" then return { value.X, value.Y } end if t == "Vector3" then return { value.X, value.Y, value.Z } end if t == "Color3" then return "#" .. value:ToHex() end if t == "BrickColor" then return value.Name end if t == "ColorSequence" then local kps = {} for _, kp in value.Keypoints do table.insert(kps, { kp.Time, "#" .. kp.Value:ToHex() }) end return kps end if t == "NumberSequence" then local kps = {} for _, kp in value.Keypoints do table.insert(kps, { kp.Time, kp.Value, kp.Envelope }) end return kps end if t == "NumberRange" then return { value.Min, value.Max } end if t == "Rect" then return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y } end if t == "CFrame" then return { value:GetComponents() } end if t == "EnumItem" then return value.Name end if t == "Font" then return { Family = value.Family, Weight = value.Weight.Name, Style = value.Style.Name, } end if t == "boolean" or t == "number" or t == "string" then return value end -- Fallback: tostring return tostring(value) end local SERIALIZE_SKIP = { Parent = true, ClassName = true, Name = true, Archivable = true, AbsolutePosition = true, AbsoluteSize = true, AbsoluteRotation = true, IsLoaded = true, } local function serializeNode(instance, maxDepth, depth) if depth > maxDepth then return nil end local node = { ClassName = instance.ClassName, Name = instance.Name, } -- Get non-default properties via reflection local ok, propData = pcall(function() return ReflectionService:GetPropertiesOfClass(instance.ClassName) end) if ok and propData then -- Create a default instance to compare against local defaultInstance = nil pcall(function() defaultInstance = Instance.new(instance.ClassName) end) for _, prop in propData do if not SERIALIZE_SKIP[prop.Name] and not prop.Name:match("^Absolute") then local valOk, value = pcall(function() return instance[prop.Name] end) if valOk and value ~= nil then -- Skip default values local isDefault = false if defaultInstance then local defOk, defVal = pcall(function() return defaultInstance[prop.Name] end) if defOk and defVal == value then isDefault = true end end if not isDefault then local jsonVal = valueToJson(value, prop.Type and tostring(prop.Type)) if jsonVal ~= nil then node[prop.Name] = jsonVal end end end end end if defaultInstance then pcall(function() defaultInstance:Destroy() end) end end -- Serialize children local children = instance:GetChildren() if #children > 0 then node.Children = {} for _, child in children do local childNode = serializeNode(child, maxDepth, depth + 1) if childNode then table.insert(node.Children, childNode) end end if #node.Children == 0 then node.Children = nil end end return node end function UIBuilder.serialize(spec) local path = spec.path if not path then return { success = false, error = "Missing 'path' parameter" } end local instance = Explorer.resolveInstance(path) if not instance then return { success = false, error = "Instance not found: " .. path } end local maxDepth = spec.maxDepth or 50 local ok, tree = pcall(function() return serializeNode(instance, maxDepth, 0) end) if not ok then return { success = false, error = "Serialization failed: " .. tostring(tree) } end return { success = true, tree = tree } end local function buildStrictNode(spec, state, depth) assert(type(spec) == "table", "Every UI node must be an object") if depth > state.maxDepth then error("UI tree exceeds maximum depth of " .. state.maxDepth) end state.count += 1 if state.count > state.maxNodes then error("UI tree exceeds maximum node count of " .. state.maxNodes) end local className = spec.ClassName or spec.className assert(type(className) == "string", "Every UI node requires ClassName") local instance = Instance.new(className) instance.Name = spec.Name or spec.name or className for key, value in spec do if not RESERVED_KEYS[key] then ValueCodec.setProperty(instance, key, value) end end local properties = spec.properties or spec.Properties or spec.props or spec.Props if properties ~= nil then assert(type(properties) == "table", "UI node properties must be an object") for key, value in properties do ValueCodec.setProperty(instance, key, value) end end local children = spec.Children or spec.children if children ~= nil then assert(type(children) == "table", "UI node Children must be an array") for _, childSpec in children do local child = buildStrictNode(childSpec, state, depth + 1) child.Parent = instance end end return instance end function UIBuilder.buildV2(spec) if type(spec.tree) ~= "table" then return { success = false, error = "tree is required" } end local parentRef = spec.parent or { pathSegments = { "StarterGui" } } local parent, parentErr = InstanceRegistry.resolve(parentRef) if not parent then return { success = false, error = "Parent: " .. tostring(parentErr) } end local state = { count = 0, maxDepth = math.clamp(spec.maxDepth or 30, 1, 50), maxNodes = math.clamp(spec.maxNodes or 1000, 1, 3000), } local builtOk, rootOrError = pcall(function() return buildStrictNode(spec.tree, state, 0) end) if not builtOk then return { success = false, error = "UI validation failed: " .. tostring(rootOrError) } end local root = rootOrError local existing = nil local duplicateCount = 0 for _, child in parent:GetChildren() do if child.Name == root.Name then existing = child duplicateCount += 1 end end if duplicateCount > 1 then root:Destroy() return { success = false, error = "Replacement target is ambiguous because multiple children share the name " .. root.Name, } end if existing and spec.replaceExisting ~= true then root:Destroy() return { success = false, error = "A child named " .. root.Name .. " already exists; set replaceExisting=true to replace it", } end local recording = ChangeHistoryService:TryBeginRecording("Dominus 2: Build UI " .. root.Name) if not recording then root:Destroy() return { success = false, error = "Another plugin recording is already active" } end local commitOk, commitErr = pcall(function() if existing then existing:Destroy() end root.Parent = parent end) if not commitOk then ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel) if root.Parent == nil then root:Destroy() end return { success = false, error = "UI commit failed: " .. tostring(commitErr), rolledBack = true } end ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit) return { success = true, root = InstanceRegistry.toRef(root), instanceCount = state.count, replaced = existing ~= nil, } end function UIBuilder.snapshotV2(spec) local instance, err = InstanceRegistry.resolve(spec.target) if not instance then return { success = false, error = err } end local maxDepth = math.clamp(spec.maxDepth or 30, 0, 50) local ok, tree = pcall(function() return serializeNode(instance, maxDepth, 0) end) if not ok then return { success = false, error = "UI serialization failed: " .. tostring(tree) } end return { success = true, target = InstanceRegistry.toRef(instance), tree = tree } end return UIBuilder ]]> UIExport maxDepth or count >= maxNodes then warning("Snapshot limit omitted a subtree") return nil end if not supported[instance.ClassName] then warning("Unsupported class omitted: " .. instance.ClassName) return nil end count += 1 local node = { className = instance.ClassName, name = instance.Name, properties = {}, children = {} } local default = Instance.new(instance.ClassName) local ok, failure = pcall(function() cache[instance.ClassName] = cache[instance.ClassName] or Reflection.getClassInfo(instance.ClassName) local info = cache[instance.ClassName] if not info.success then error("Reflection unavailable for " .. instance.ClassName) end for _, property in info.properties do local name = property.name -- Nonserialized members include deprecated aliases (Transparency, -- TextColor, FontSize) that can overwrite the canonical properties. if not skip[name] and property.writable and property.serialized ~= false then local read, value = pcall(function() return instance:GetStyled(name) end) if not read then read, value = pcall(function() return instance[name] end) warning("Styled value unavailable: " .. instance.ClassName .. "." .. name) end if read and value ~= nil then local kind = typeof(value) if types[kind] then local writable = pcall(function() default[name] = value end) if writable then table.insert(node.properties, { name = name, type = kind, value = Codec.encode(value) }) else warning("Property could not be reproduced: " .. instance.ClassName .. "." .. name) end else warning( "Unsupported property type: " .. instance.ClassName .. "." .. name .. " (" .. kind .. ")" ) end end end end end) default:Destroy() if not ok then error(failure) end for _, child in instance:GetChildren() do local item = visit(child, depth + 1) if item then table.insert(node.children, item) end end return node end local ok, tree = pcall(visit, root, 0) if not ok or not tree then return { success = false, error = tostring(tree or "Unsupported UI root") } end return { success = true, version = 1, tree = tree, nodeCount = count, warnings = warnings, omittedCount = omitted } end return UIExport ]]> ValueCodec 1 or g > 1 or b > 1 then return Color3.fromRGB(r, g, b) end return Color3.new(r, g, b) end local function decodeEnum(current, value) if typeof(value) == "EnumItem" then return value end if type(value) ~= "string" then error("Enum value must be a string") end local itemName = value:match("Enum%.[^.]+%.(.+)") or value for _, item in current.EnumType:GetEnumItems() do if item.Name == itemName then return item end end error("Invalid enum value " .. value .. " for " .. tostring(current.EnumType)) end function ValueCodec.decodeForCurrent(current, value) local expectedType = typeof(current) if expectedType == "UDim2" then assert(type(value) == "table", "UDim2 must be an array or object") return UDim2.new( component(value, { "XScale", "xScale" }, 1, 0), component(value, { "XOffset", "xOffset" }, 2, 0), component(value, { "YScale", "yScale" }, 3, 0), component(value, { "YOffset", "yOffset" }, 4, 0) ) elseif expectedType == "UDim" then assert(type(value) == "table", "UDim must be an array or object") return UDim.new(component(value, { "Scale", "scale" }, 1, 0), component(value, { "Offset", "offset" }, 2, 0)) elseif expectedType == "Vector2" then assert(type(value) == "table", "Vector2 must be an array or object") return Vector2.new(component(value, { "X", "x" }, 1, 0), component(value, { "Y", "y" }, 2, 0)) elseif expectedType == "Vector3" then assert(type(value) == "table", "Vector3 must be an array or object") return Vector3.new( component(value, { "X", "x" }, 1, 0), component(value, { "Y", "y" }, 2, 0), component(value, { "Z", "z" }, 3, 0) ) elseif expectedType == "Color3" then return decodeColor3(value) elseif expectedType == "BrickColor" then return BrickColor.new(value) elseif expectedType == "EnumItem" then return decodeEnum(current, value) elseif expectedType == "Rect" then assert(type(value) == "table", "Rect must be a four-number array") return Rect.new(number(value[1], "1"), number(value[2], "2"), number(value[3], "3"), number(value[4], "4")) elseif expectedType == "NumberRange" then if type(value) == "number" then return NumberRange.new(value) end assert(type(value) == "table", "NumberRange must be a number or two-number array") return NumberRange.new(number(value[1], "1"), number(value[2] or value[1], "2")) elseif expectedType == "CFrame" then assert(type(value) == "table", "CFrame must be an array") if #value == 3 then return CFrame.new(value[1], value[2], value[3]) elseif #value == 12 then return CFrame.new(table.unpack(value)) end error("CFrame requires 3 or 12 numbers") elseif expectedType == "NumberSequence" then if type(value) == "number" then return NumberSequence.new(value) end assert(type(value) == "table", "NumberSequence must be a number or keypoint array") local keypoints = {} for _, keypoint in value do table.insert(keypoints, NumberSequenceKeypoint.new(keypoint[1], keypoint[2], keypoint[3] or 0)) end return NumberSequence.new(keypoints) elseif expectedType == "ColorSequence" then if type(value) == "string" then return ColorSequence.new(decodeColor3(value)) end assert(type(value) == "table", "ColorSequence must be a color or keypoint array") local keypoints = {} for _, keypoint in value do table.insert(keypoints, ColorSequenceKeypoint.new(keypoint[1], decodeColor3(keypoint[2]))) end return ColorSequence.new(keypoints) elseif expectedType == "Font" then assert(type(value) == "table", "Font must be an object") local weight = Enum.FontWeight[value.Weight or value.weight or "Regular"] local style = Enum.FontStyle[value.Style or value.style or "Normal"] assert(weight and style, "Invalid Font weight or style") return Font.new(value.Family or value.family, weight, style) elseif expectedType == "Instance" then local resolved, err = InstanceRegistry.resolve(value) if not resolved then error(err) end return resolved elseif expectedType == "number" then return number(value, "Property") elseif expectedType == "boolean" then assert(type(value) == "boolean", "Property must be a boolean") return value elseif expectedType == "string" then assert(type(value) == "string", "Property must be a string") return value end return value end function ValueCodec.encode(value) local valueType = typeof(value) if valueType == "UDim2" then return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset } end if valueType == "UDim" then return { value.Scale, value.Offset } end if valueType == "Vector2" then return { value.X, value.Y } end if valueType == "Vector3" then return { value.X, value.Y, value.Z } end if valueType == "Color3" then return "#" .. value:ToHex() end if valueType == "BrickColor" then return value.Name end if valueType == "EnumItem" then return tostring(value) end if valueType == "Rect" then return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y } end if valueType == "NumberRange" then return { value.Min, value.Max } end if valueType == "CFrame" then return { value:GetComponents() } end if valueType == "NumberSequence" then local result = {} for _, keypoint in value.Keypoints do table.insert(result, { keypoint.Time, keypoint.Value, keypoint.Envelope }) end return result end if valueType == "ColorSequence" then local result = {} for _, keypoint in value.Keypoints do table.insert(result, { keypoint.Time, "#" .. keypoint.Value:ToHex() }) end return result end if valueType == "Font" then return { Family = value.Family, Weight = value.Weight.Name, Style = value.Style.Name } end if valueType == "Instance" then return InstanceRegistry.toRef(value) end if valueType == "boolean" or valueType == "number" or valueType == "string" then return value end return tostring(value) end local TYPED_DEFAULTS = { UDim = function() return UDim.new() end, UDim2 = function() return UDim2.new() end, Vector2 = function() return Vector2.zero end, Vector3 = function() return Vector3.zero end, Color3 = function() return Color3.new() end, BrickColor = function() return BrickColor.new("Medium stone grey") end, Rect = function() return Rect.new() end, NumberRange = function() return NumberRange.new(0) end, CFrame = function() return CFrame.new() end, NumberSequence = function() return NumberSequence.new(0) end, ColorSequence = function() return ColorSequence.new(Color3.new()) end, Font = function() return Font.new("rbxasset://fonts/families/SourceSansPro.json") end, } function ValueCodec.decodeTyped(typeName, value) assert(type(typeName) == "string", "Typed value requires a type") if typeName == "nil" then return nil end if typeName == "string" or typeName == "number" or typeName == "boolean" then return ValueCodec.decodeForCurrent(typeName == "string" and "" or typeName == "number" and 0 or false, value) end if typeName == "Instance" then local resolved, err = InstanceRegistry.resolve(value) if not resolved then error(err) end return resolved end local createDefault = TYPED_DEFAULTS[typeName] if not createDefault then error("Unsupported typed value: " .. typeName) end return ValueCodec.decodeForCurrent(createDefault(), value) end local function encodeSerializable(value, depth, state, seen) if depth > 5 then return "" end if type(value) ~= "table" then return ValueCodec.encode(value) end if seen[value] then return "" end seen[value] = true local count = 0 local maxIndex = 0 local isArray = true for key in value do count += 1 state.count += 1 if state.count > state.limit then seen[value] = nil return "" end if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then isArray = false else maxIndex = math.max(maxIndex, key) end end local result = {} if isArray and maxIndex == count then for index = 1, maxIndex do result[index] = encodeSerializable(value[index], depth + 1, state, seen) end else for key, child in value do result[tostring(key)] = encodeSerializable(child, depth + 1, state, seen) end end seen[value] = nil return result end function ValueCodec.encodeSerializable(value, limit) return encodeSerializable(value, 0, { count = 0, limit = limit or 500 }, {}) end function ValueCodec.encodeTyped(value, limit) local encoded = { type = typeof(value) } if value ~= nil then encoded.value = ValueCodec.encodeSerializable(value, limit) end return encoded end function ValueCodec.setProperty(instance, propertyName, value) if propertyName == "Parent" or propertyName == "ClassName" or propertyName == "Source" then error("Property cannot be changed through setProperties: " .. propertyName) end local readOk, current = pcall(function() return instance[propertyName] end) if not readOk then error("Unknown or unreadable property: " .. propertyName) end local decoded = ValueCodec.decodeForCurrent(current, value) local writeOk, writeErr = pcall(function() instance[propertyName] = decoded end) if not writeOk then error("Failed to set " .. propertyName .. ": " .. tostring(writeErr)) end return ValueCodec.encode(instance[propertyName]) end return ValueCodec ]]> Watcher MAX_BUFFER then table.remove(outputBuffer, 1) end -- Forward to CLI if outputCallback then outputCallback(entry) end end) end function Watcher.stop() if connection then connection:Disconnect() connection = nil end outputCallback = nil end function Watcher.getRecentOutput(limit, levelFilter) limit = limit or 50 local result = {} for i = math.max(1, #outputBuffer - limit + 1), #outputBuffer do local entry = outputBuffer[i] if entry then if not levelFilter or entry.level == levelFilter then table.insert(result, entry) end end end return { entries = result } end function Watcher.clearBuffer() outputBuffer = {} end return Watcher ]]> WsClient