DevexDefiantDevexDefiantfalse0) and savedURL or 'https://devexdefiant.com' urlBox.Position=UDim2.new(0,16,0,360);urlBox.Visible=false label('Get a connection code at devexdefiant.com/connect-studio. Paste it below.',50,44) local tokenBox=input('Connection code (ABCDE-23456)',104) local toggle=Instance.new('TextButton',frame) toggle.Position=UDim2.new(0,16,0,152);toggle.Size=UDim2.new(1,-32,0,40);toggle.BackgroundColor3=Color3.fromRGB(30,107,255);toggle.TextColor3=Color3.new(1,1,1);toggle.Text='Connect';toggle.Font=Enum.Font.GothamBold;toggle.TextSize=15 local allowTests=false local testToggle=Instance.new('TextButton',frame) testToggle.Position=UDim2.new(0,16,0,282);testToggle.Size=UDim2.new(1,-32,0,32);testToggle.BackgroundColor3=Color3.fromRGB(18,31,61);testToggle.TextColor3=Color3.new(1,1,1);testToggle.TextSize=13;testToggle.Text='Allow AI playtests: OFF' testToggle.MouseButton1Click:Connect(function() allowTests=not allowTests;testToggle.Text='Allow AI playtests: '..(allowTests and 'ON' or 'OFF') end) local status=label('Ready to connect. Paste the code from the website, then click Connect.',202,72) tokenBox:GetPropertyChangedSignal('Text'):Connect(function() local text=tokenBox.Text if #text>80 or string.find(text,'[^%w%s%-]') or string.find(text,'[\r\n]') then tokenBox.Text='' status.Text='That was not a connection code. In the app, click Get connection code, then Copy code. Paste only that short code here.' end end) local advanced=Instance.new('TextButton',frame) advanced.Position=UDim2.new(0,16,0,322);advanced.Size=UDim2.new(1,-32,0,26);advanced.BackgroundTransparency=1;advanced.TextColor3=Color3.fromRGB(137,172,220);advanced.TextSize=12;advanced.Text='Advanced: website address' advanced.MouseButton1Click:Connect(function() urlBox.Visible=not urlBox.Visible end) local pairingBusy=false local connected=false local baseURL='' local secret='' local generation=0 local cache={} local roots={Workspace=true,ReplicatedStorage=true,ServerScriptService=true,ServerStorage=true,StarterGui=true,StarterPack=true,StarterPlayer=true,Lighting=true,SoundService=true} local function resolve(path) assert(type(path)=='string','Path must be a string') local parts=string.split(path,'/');assert(roots[parts[1]],'Unsupported root service') local current=game:GetService(parts[1]) for i=2,#parts do local found=nil for _,child in current:GetChildren() do if child.Name==parts[i] then assert(not found,'Ambiguous path; duplicate names');found=child end end assert(found,'Path not found: '..path);current=found end return current end local function vec(v) return Vector3.new(v[1],v[2],v[3]) end local function rgb(v) return Color3.fromRGB(v[1],v[2],v[3]) end local function newParent(args) local parent=resolve(args.parent);assert(not parent:FindFirstChild(args.name),'Name already exists under parent');return parent end local allowed={Name=true,Position=true,Size=true,Color=true,Material=true,Anchored=true,Transparency=true,CanCollide=true,Reflectance=true,CastShadow=true,Value=true,Text=true,Enabled=true,Brightness=true,ClockTime=true,WalkSpeed=true,JumpPower=true} local request local function execute(command) local a=command.args;local tool=command.tool if tool=='run_playtest' then assert(allowTests,'Enable Allow AI playtests in the plugin first. Playtesting runs the scripts in your place.') assert(not RunService:IsRunning(),'Stop the current Play session first') local tests=game:GetService('StudioTestService') local testScript=Instance.new('Script') testScript.Name='DevexDefiantTest_'..Http:GenerateGUID(false) testScript.Source=[==[ local tests=game:GetService('StudioTestService') local args=tests:GetTestArgs() if type(args)~='table' or args.kind~='DevexDefiantSmokeTest' then return end local errors={} local connection=game:GetService('LogService').MessageOut:Connect(function(message,kind) if kind==Enum.MessageType.MessageError and #errors<12 then table.insert(errors,string.sub(message,1,600)) end end) task.wait(math.clamp(args.seconds or 3,1,5)) local checks={} for _,path in args.paths or {} do local current=game for _,name in string.split(path,'/') do current=current and current:FindFirstChild(name) end table.insert(checks,{path=path,exists=current~=nil}) end connection:Disconnect() tests:EndTest({checks=checks,serverErrors=errors,note='Short server smoke test only. Player input, client UI, appearance, purchases and overall game quality were not verified.'}) ]==] testScript.Parent=game:GetService('ServerScriptService') local ok,result=pcall(function() return tests:ExecutePlayModeAsync({kind='DevexDefiantSmokeTest',seconds=math.clamp(a.seconds,1,5),paths=a.paths}) end) testScript:Destroy() assert(ok,'Studio playtest failed: '..tostring(result)) return Http:JSONEncode(result) elseif tool=='get_tree' then local count=0 local function tree(obj,path,depth) count+=1 local node={name=obj.Name,className=obj.ClassName,path=path,children={}} if depth>0 then for _,child in obj:GetChildren() do if count>=250 then node.truncated=true;break end table.insert(node.children,tree(child,path..'/'..child.Name,depth-1)) end end return node end local root=tree(resolve(a.path),a.path,math.clamp(a.depth,0,4)) local encoded=Http:JSONEncode(root) while #encoded>17000 and #root.children>0 do table.remove(root.children);root.truncated=true;encoded=Http:JSONEncode(root) end return encoded elseif tool=='insert_asset' then local parent=newParent(a) local metadata=game:GetService('MarketplaceService'):GetProductInfo(a.assetId,Enum.InfoType.Asset) assert(metadata.AssetTypeId==10,'Choose a Roblox model asset, not a plugin or another asset type') local ok,loaded=pcall(function() return game:GetService('AssetService'):LoadAssetAsync(a.assetId) end) if not ok then ok,loaded=pcall(function() return game:GetService('InsertService'):LoadAsset(a.assetId) end) end if not ok then ok,loaded=pcall(function() local objects=game:GetObjects('rbxassetid://'..a.assetId) assert(#objects>0,'Asset was empty') local holder=Instance.new('Model');holder.Name=a.name for _,obj in objects do obj.Parent=holder end return holder end) end assert(ok and typeof(loaded)=='Instance','Roblox could not load that asset. Check its availability and your asset permissions. For third-party models, Roblox may require Allow Loading Third Party Assets in Experience Settings.') local success,result=pcall(function() assert(loaded:IsA('Model'),'The asset is not a model') local descendants=loaded:GetDescendants();assert(#descendants<=5000,'That model is too large. Choose a smaller model.') local count=0;local removed=0 for i,obj in descendants do if obj:IsA('LuaSourceContainer') or obj:IsA('PackageLink') then obj:Destroy();removed+=1 elseif obj:IsA('BasePart') then obj.Anchored=true;count+=1 end if i%200==0 then task.wait() end end assert(count>0 and count<=2000,'Choose a model containing between 1 and 2,000 parts') loaded.Name=a.name local bounds=loaded:GetBoundingBox();loaded:PivotTo(CFrame.new(vec(a.position)-bounds.Position)*loaded:GetPivot()) loaded.Parent=parent return 'Inserted '..a.name..' at '..a.parent..'/'..a.name..' ('..count..' anchored parts; '..removed..' scripts/package links removed)' end) if not success then loaded:Destroy();error(result) end return result elseif tool=='import_blender_mesh' then local parent=newParent(a) local mesh=Http:JSONDecode(request('GET','/api/plugin/mesh?id='..a.previewId..'&index='..a.meshIndex,nil,secret,baseURL)) assert(mesh.name==a.name and #mesh.vertices<=24000 and #mesh.triangles<=24000,'Invalid approved mesh') local assets=game:GetService('AssetService') local editable=assets:CreateEditableMesh();assert(editable,'Studio could not allocate mesh memory') local part=nil local ok,result=pcall(function() local vertices={};local normals={} for i,v in mesh.vertices do vertices[i]=editable:AddVertex(vec(v));if mesh.normals then normals[i]=editable:AddNormal(vec(mesh.normals[i])) end;if i%300==0 then task.wait() end end for i,t in mesh.triangles do local face=editable:AddTriangle(vertices[t[1]+1],vertices[t[2]+1],vertices[t[3]+1]) if mesh.normals then editable:SetFaceNormals(face,{normals[t[1]+1],normals[t[2]+1],normals[t[3]+1]}) end if i%300==0 then task.wait() end end local outcome,id=assets:CreateAssetAsync(editable,Enum.AssetType.Mesh,{Name=a.name,Description='Approved DevexDefiant Blender model geometry'}) assert(outcome==Enum.CreateAssetResult.Success,'Roblox did not accept the mesh upload: '..tostring(outcome)..'. Check your account upload permissions and try again.') part=assets:CreateMeshPartAsync(Content.fromUri('rbxassetid://'..id),{CollisionFidelity=Enum.CollisionFidelity.Box}) part.Name=a.name;part.Size=vec(mesh.size);part.CFrame=CFrame.new(vec(mesh.position));part.Color=rgb(mesh.color);part.Material=Enum.Material[mesh.material];part.Anchored=true;part.Parent=parent return 'Imported Blender mesh '..a.parent..'/'..a.name..' (Roblox mesh asset '..id..')' end) editable:Destroy() if not ok then if part then part:Destroy() end;error(tostring(result)..' Any mesh already uploaded remains in your Roblox inventory.') end return result elseif tool=='create_part' then local parent=newParent(a) local p=Instance.new(a.shape=='Wedge' and 'WedgePart' or 'Part') local ok,err=pcall(function() p.Name=a.name;p.Position=vec(a.position);p.Size=vec(a.size);p.Color=rgb(a.color);p.Material=Enum.Material[a.material];p.Anchored=a.anchored if p:IsA('Part') then p.Shape=Enum.PartType[a.shape] end p.Parent=parent end) if not ok then p:Destroy();error(err) end return "Created Part '"..a.name.."' at "..a.parent..'/'..a.name elseif tool=='create_model' then local parent=newParent(a);local m=Instance.new('Model');m.Name=a.name;m.Parent=parent;return 'Created Model '..a.name elseif tool=='create_script' then assert(a.scriptType=='Script' or a.scriptType=='LocalScript' or a.scriptType=='ModuleScript','Invalid script type') local parent=newParent(a);local s=Instance.new(a.scriptType);s.Name=a.name local ok,err=pcall(function() Editor:UpdateSourceAsync(s,function() return a.source end);s.Parent=parent end) if not ok then s:Destroy();error(err) end return 'Created '..a.scriptType..' '..a.name elseif tool=='set_property' then assert(allowed[a.property],'Property is not allowed') local obj=resolve(a.path);local v=a.value if a.property=='Position' or a.property=='Size' then v=vec(v) elseif a.property=='Color' then v=rgb(v) elseif a.property=='Material' then v=Enum.Material[v] elseif a.property=='Name' then assert(obj.Parent~=game,'Cannot rename root services');assert(not string.find(v,'/'),'Name cannot contain slash');assert(not obj.Parent:FindFirstChild(v) or obj.Name==v,'Name exists') end obj[a.property]=v;return 'Set '..a.path..'.'..a.property elseif tool=='delete_instance' then local obj=resolve(a.path);assert(obj.Parent~=game,'Cannot delete root services');obj:Destroy();return 'Deleted '..a.path end error('Unknown tool: '..tostring(tool)) end request=function(method,path,body,token,url) local response=Http:RequestAsync({Url=url..path,Method=method,Headers={['x-plugin-token']=token,['Content-Type']='application/json'},Body=body and Http:JSONEncode(body) or nil}) if not response.Success then error('HTTP '..response.StatusCode,0) end return response.Body end button.Click:Connect(function() widget.Enabled=not widget.Enabled end) toggle.MouseButton1Click:Connect(function() if connected then connected=false;generation+=1;secret='';tokenBox.Text='';toggle.Text='Connect';status.Text='Disconnected';return end if pairingBusy then return end baseURL=string.gsub(urlBox.Text,'/+$','') if not string.match(baseURL,'^https://') and not string.match(baseURL,'^http://localhost:%d+$') and not string.match(baseURL,'^http://127%.0%.0%.1:%d+$') then status.Text='Use an HTTPS URL (or localhost for development).';return end if RunService:IsRunning() then status.Text='Click Stop in Studio before connecting. Stay in Edit mode.';return end local entered=string.gsub(tokenBox.Text,'[%s%-]','') if #entered==64 and string.match(entered,'^[a-f0-9]+$') then secret=entered elseif #entered==10 then pairingBusy=true;toggle.Text='Connecting…';status.Text='Checking your code… Allow website access if Studio asks.' local ok,result=pcall(function() return Http:JSONDecode(request('POST','/api/plugin/pair',{code=string.upper(entered)},nil,baseURL)) end) pairingBusy=false;toggle.Text='Connect' if not ok then if string.find(tostring(result),'HTTP 401',1,true) then status.Text='That code expired or was already used. Click Get connection code in the app for a new one.' elseif string.find(tostring(result),'HTTP 429',1,true) then status.Text='Too many attempts. Wait one minute, then get a new connection code.' else status.Text='Could not reach the website. Check your internet and allow devexdefiant.com if Studio asks, then try again.' end return end if type(result.token)~='string' or #result.token~=64 then status.Text='Connection failed. Get a new code and try again.';return end secret=result.token else status.Text='Paste the 10-character connection code from the website, then click Connect.';return end plugin:SetSetting('DevexDefiantURL',baseURL);tokenBox.Text='';connected=true;generation+=1 local mine=generation;local sessionToken=secret;local sessionURL=baseURL toggle.Text='Disconnect';status.Text='Connecting…' task.spawn(function() while connected and generation==mine do local ok,err=pcall(function() assert(not RunService:IsRunning(),'Stop Play mode to resume building') local data=Http:JSONDecode(request('GET','/api/plugin/commands',nil,sessionToken,sessionURL)) if not connected or mine~=generation then return end status.Text='Connected! Go to the DevexDefiant website or app and tell the AI what you want to create. Keep Studio open.' local commands=data.commands or {} if #commands==0 then return end local results={} for _,command in commands do local cached=cache[command.id] if not cached then local recording=nil if command.tool~='run_playtest' and command.tool~='get_tree' then recording=History:TryBeginRecording('DevexDefiantBuild','DevexDefiant AI build') assert(recording,'Cannot record changes. Finish any active Studio operation.') end local success,result=pcall(function() return execute(command) end) if recording then History:FinishRecording(recording,Enum.FinishRecordingOperation.Commit) end cached={id=command.id,success=success,result=string.sub(tostring(result),1,17500)};cache[command.id]=cached end table.insert(results,cached) end for _,result in results do local sent,sendError=pcall(function() request('POST','/api/plugin/results',result,sessionToken,sessionURL) end) if not sent then status.Text='Result delivery failed: '..tostring(sendError) end end -- Results are at-most-once; never automatically replay mutations after a lost response. end) if not ok then local message=tostring(err) if string.find(message,'HTTP 401',1,true) then connected=false;generation+=1;secret='';toggle.Text='Connect';status.Text='Connection expired. Get a new code at devexdefiant.com/connect-studio.' elseif string.find(message,'HTTP 429',1,true) then status.Text='Too many requests. Waiting for the website…' else status.Text='Studio could not reach the website. Check your internet and allow devexdefiant.com if Studio asks, then reconnect.' end end task.wait(1.5) end end) end) plugin.Unloading:Connect(function() connected=false;generation+=1;secret='' end) ]]>