
Keyless

local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local UIS = game:GetService("UserInputService")
local HttpService = game:GetService("HttpService")
local CoreGui = game:GetService("CoreGui")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Lighting = game:GetService("Lighting")
local StarterPack = game:GetService("StarterPack")
local ServerStorage = game:GetService("ServerStorage")
local StarterGui = game:GetService("StarterGui")
local StarterPlayer = game:GetService("StarterPlayer")
-- CRITICAL: StarterCharacterScripts and StarterPlayerScripts are special Roblox
-- objects with their own unique ClassName, NOT Folders with the same name. If we
-- Instance.new("Folder") and rename it "StarterPlayerScripts", it conflicts with
-- the real Roblox service object — creating a fake container that receives scripts
-- while the Explorer shows the real (empty) service. This was the root cause of
-- "Not Found" when clicking scripts, even though create_script returned "✅ Success".
-- Fix: use the existing real object (FindFirstChildOfClass), or if it doesn't exist
-- (rare, since these are default Roblox services), create with the correct ClassName.
local StarterCharacterScripts = StarterPlayer:FindFirstChildOfClass("StarterCharacterScripts")
if not StarterCharacterScripts then
local ok, created = pcall(function() return Instance.new("StarterCharacterScripts") end)
if ok and created then
created.Parent = StarterPlayer
StarterCharacterScripts = created
end
end
local StarterPlayerScripts = StarterPlayer:FindFirstChildOfClass("StarterPlayerScripts")
if not StarterPlayerScripts then
local ok, created = pcall(function() return Instance.new("StarterPlayerScripts") end)
if ok and created then
created.Parent = StarterPlayer
StarterPlayerScripts = created
end
end
local player = Players.LocalPlayer
math.randomseed(tick() * 10000)
local sceneState = {}
-- FIX v1.4: حد أقصى لـ sceneState — بعده نحذف أقدم مدخل لمنع تسرب الذاكرة
local SCENE_STATE_MAX = 60
local function pruneSceneState()
local count = 0
for _ in pairs(sceneState) do count = count + 1 end
if count <= SCENE_STATE_MAX then return end
-- احذف المدخلات الأقدم (lastAction الأصغر)
local oldest_key, oldest_time = nil, math.huge
for k, v in pairs(sceneState) do
if (v.lastAction or 0) < oldest_time then
oldest_key = k
oldest_time = v.lastAction or 0
end
end
if oldest_key then sceneState[oldest_key] = nil end
end
local function updateSceneState(action, didSucceed)
if not didSucceed then return end
if not action or type(action) ~= "table" then return end
_searchCache = nil -- scene changed, invalidate search cache
local containerKey = action.containerName or (action.name or action.className or "default")
if not sceneState[containerKey] then
sceneState[containerKey] = { className = action.className, count = 0, namePrefix = action.namePrefix, createdNames = {} }
end
local ss = sceneState[containerKey]
ss.lastAction = os.time()
ss.className = action.className or ss.className
if action.namePrefix then ss.namePrefix = action.namePrefix end
if action.type == "create_instance" then
ss.count = ss.count + 1
if action.name then ss.createdNames[action.name] = true end
elseif action.type == "create_multiple" then
local c = math.clamp(tonumber(action.count) or 1, 1, 200)
ss.count = ss.count + c
elseif action.type == "create_compound" then
ss.count = ss.count + 1
if action.name then ss.createdNames[action.name] = true end
elseif action.type == "delete" then
local targetName = action.target
if targetName and ss.createdNames then
for n, _ in pairs(ss.createdNames) do
if n:lower():find(targetName:lower(), 1, true) then
ss.createdNames[n] = nil
ss.count = math.max(0, ss.count - 1)
break
end
end
end
end
-- FIX v1.4: تنظيف تلقائي لمنع تسرب الذاكرة
pruneSceneState()
end
local undoStack = {}
local undoLimit = 20
local _logLabels = {} -- cleared after each request, tracks addLog labels for cleanup
local _batchCreated = {} -- cleared before each batch
local _lastClickInstance = nil -- for clickable messages
local _batchPlacedGrid = nil -- tracks grid positions in create_multiple
local _autoRunScripts = {} -- scripts to re-execute on player respawn (max 20)
local AUTORUN_MAX = 20
local _requestQueue = {}
local _processingRequest = false
local _searchCache = nil -- FIX: كان implicit global يُعاد تصفيره في updateSceneState بدون تعريف local
local MESSAGE_MAX = 50
local MAX_TOKENS_LOW = 16000
local MAX_TOKENS_HIGH = 32000
-- re-run registered scripts on every respawn so effects (crosshair, camera, GUI) persist
-- ═══════════════════════════════════════════════════════
-- MODEL REGISTRY — Free (OpenCode) + NVIDIA NIM
-- ═══════════════════════════════════════════════════════
local OPENCODE_URL = "https://opencode.ai/zen/v1/chat/completions"
local NVIDIA_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
-- مفاتيح OpenCode (rotation)
local OPENCODE_KEYS = {
"sk-ZP0Y71QDq4HhXrr5KgWEzsf8ZyNNsLJL070xXcGFUh8w7cvlFFJuks5k4jv7puCK",
"sk-dtcKP4hx4iznBCaVm1uFvbEax12z22hlJzcW9alSIya8OQfSigpJN42QfxHuP1xk",
"sk-Pe12GsYt4RnkTPtb7ACCGSj5vzz7u7m2INVDzxe1Z2ccGF904LFCsxpzouJek6ld",
"sk-MN9kABCiW1cskBQ4EncGFV6DFwyHvshu6CrjtmuHUFwdkvJvAHNzql90BqTpNHMC",
"sk-yXrnjOHCXVejD3v1Ngk2trqs8B2cFpQd9hbhBlpVyrNZgeevk3lPolVY0EtgQZha",
}
-- you need put it
local NVIDIA_API_KEY = " "
--
local MODEL_LIST = {
-- ── Free (OpenCode) ──────────────────────────────────
{ label = "⭐ Big Pickle (Free)", provider = "opencode", id = "big-pickle" },
{ label = "DeepSeek V4 Flash (Free)", provider = "opencode", id = "deepseek-v4-flash-free" },
{ label = "MiMo V2.5 (Free)", provider = "opencode", id = "mimo-v2.5-free" },
{ label = "Nemotron 3 Ultra (Free)", provider = "opencode", id = "nemotron-3-ultra-free" },
{ label = "North Mini Code (Free)", provider = "opencode", id = "north-mini-code-free" },
-- ── NVIDIA NIM (verified active 2026-06) ─────────────
{ label = "── NVIDIA NIM ──", provider = "separator" },
{ label = "DeepSeek V4 Flash", provider = "nvidia", id = "deepseek-ai/deepseek-v4-flash", maxOutput = 8192 },
{ label = "DeepSeek V4 Pro", provider = "nvidia", id = "deepseek-ai/deepseek-v4-pro", maxOutput = 8192 },
{ label = "Llama 4 Maverick 17b", provider = "nvidia", id = "meta/llama-4-maverick-17b-128e-instruct", maxOutput = 4096 },
{ label = "Llama 3.3 70b", provider = "nvidia", id = "meta/llama-3.3-70b-instruct", maxOutput = 4096 },
{ label = "Llama 3.1 70b", provider = "nvidia", id = "meta/llama-3.1-70b-instruct", maxOutput = 4096 },
{ label = "Llama 3.1 8b", provider = "nvidia", id = "meta/llama-3.1-8b-instruct", maxOutput = 4096 },
{ label = "Mistral Large 3 675b", provider = "nvidia", id = "mistralai/mistral-large-3-675b-instruct-2512", maxOutput = 8192 },
{ label = "Mistral Medium 3.5 128b", provider = "nvidia", id = "mistralai/mistral-medium-3.5-128b", maxOutput = 8192 },
{ label = "Mistral Small 4 119b", provider = "nvidia", id = "mistralai/mistral-small-4-119b-2603", maxOutput = 8192 },
{ label = "Mistral 7b", provider = "nvidia", id = "mistralai/mistral-7b-instruct-v0.3", maxOutput = 4096 },
{ label = "Mixtral 8x22b", provider = "nvidia", id = "mistralai/mixtral-8x22b-v0.1", maxOutput = 4096 },
{ label = "Mixtral 8x7b", provider = "nvidia", id = "mistralai/mixtral-8x7b-instruct-v0.1", maxOutput = 4096 },
{ label = "Mistral Nemo 12b", provider = "nvidia", id = "nv-mistralai/mistral-nemo-12b-instruct", maxOutput = 4096 },
{ label = "Qwen3.5 397b A17b", provider = "nvidia", id = "qwen/qwen3.5-397b-a17b", maxOutput = 8192 },
{ label = "Qwen3.5 122b A10b", provider = "nvidia", id = "qwen/qwen3.5-122b-a10b", maxOutput = 8192 },
{ label = "Qwen3-Next 80b A3b", provider = "nvidia", id = "qwen/qwen3-next-80b-a3b-instruct", maxOutput = 8192 },
{ label = "Kimi K2.6 (1M ctx)", provider = "nvidia", id = "moonshotai/kimi-k2.6", maxOutput = 8192 },
{ label = "GLM-5.1 (Zhipu)", provider = "nvidia", id = "z-ai/glm-5.1", maxOutput = 8192 },
{ label = "Gemma 4 31b", provider = "nvidia", id = "google/gemma-4-31b-it", maxOutput = 4096 },
{ label = "Gemma 3 12b", provider = "nvidia", id = "google/gemma-3-12b-it", maxOutput = 4096 },
{ label = "Gemma 3n E4b", provider = "nvidia", id = "google/gemma-3n-e4b-it", maxOutput = 4096 },
{ label = "Phi-4 Mini", provider = "nvidia", id = "microsoft/phi-4-mini-instruct", maxOutput = 4096 },
{ label = "Phi-4 Multimodal", provider = "nvidia", id = "microsoft/phi-4-multimodal-instruct", maxOutput = 4096 },
{ label = "Phi-3.5 MoE", provider = "nvidia", id = "microsoft/phi-3.5-moe-instruct", maxOutput = 4096 },
{ label = "Nemotron 3 Super 120b (1M ctx)", provider = "nvidia", id = "nvidia/nemotron-3-super-120b-a12b", maxOutput = 8192 },
{ label = "Nemotron 3 Ultra 253b", provider = "nvidia", id = "nvidia/llama-3.1-nemotron-ultra-253b-v1", maxOutput = 8192 },
{ label = "Nemotron 3 Nano 30b", provider = "nvidia", id = "nvidia/nemotron-3-nano-30b-a3b", maxOutput = 4096 },
{ label = "Nemotron 3 Nano Omni", provider = "nvidia", id = "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning", maxOutput = 4096 },
{ label = "Llama 3.1 Nemotron 70b", provider = "nvidia", id = "nvidia/llama-3.1-nemotron-70b-instruct", maxOutput = 4096 },
{ label = "Llama 3.3 Nemotron Super 49b", provider = "nvidia", id = "nvidia/llama-3.3-nemotron-super-49b-v1", maxOutput = 4096 },
{ label = "MiniMax M2.7", provider = "nvidia", id = "minimaxai/minimax-m2.7", maxOutput = 8192 },
{ label = "MiniMax M3", provider = "nvidia", id = "minimaxai/minimax-m3", maxOutput = 8192 },
{ label = "Step 3.5 Flash", provider = "nvidia", id = "stepfun-ai/step-3.5-flash", maxOutput = 4096 },
{ label = "Step 3.7 Flash", provider = "nvidia", id = "stepfun-ai/step-3.7-flash", maxOutput = 4096 },
{ label = "GPT-OSS 120b", provider = "nvidia", id = "openai/gpt-oss-120b", maxOutput = 8192 },
{ label = "GPT-OSS 20b", provider = "nvidia", id = "openai/gpt-oss-20b", maxOutput = 4096 },
{ label = "Seed-OSS 36b (ByteDance)", provider = "nvidia", id = "bytedance/seed-oss-36b-instruct", maxOutput = 8192 },
{ label = "DBRX Instruct (Databricks)", provider = "nvidia", id = "databricks/dbrx-instruct", maxOutput = 4096 },
{ label = "Granite 34b Code (IBM)", provider = "nvidia", id = "ibm/granite-34b-code-instruct", maxOutput = 4096 },
{ label = "Jamba 1.5 Large (AI21)", provider = "nvidia", id = "ai21labs/jamba-1.5-large-instruct", maxOutput = 4096 },
{ label = "Sarvam M (multilingual)", provider = "nvidia", id = "sarvamai/sarvam-m", maxOutput = 4096 },
{ label = "Llama Guard 4 12b", provider = "nvidia", id = "meta/llama-guard-4-12b", maxOutput = 4096 },
}
-- الحالة الحالية — تُحمَّل من الملف عند البدء
local CURRENT_PROVIDER = "opencode"
local CURRENT_MODEL_LABEL = "⭐ Big Pickle (Free)"
-- ملف الحفظ
local SAVE_FILE = "SkillAI_ModelSave.txt"
local function saveModelChoice(label, provider, modelId)
pcall(function()
if writefile then
local data = label .. "\n" .. provider .. "\n" .. (modelId or "")
writefile(SAVE_FILE, data)
end
end)
end
local function loadModelChoice()
local ok, data = pcall(function()
if readfile and isfile and isfile(SAVE_FILE) then
return readfile(SAVE_FILE)
end
return nil
end)
if ok and data and data ~= "" then
local lines = {}
for line in data:gmatch("[^\n]+") do table.insert(lines, line) end
local savedLabel = lines[1] or ""
local savedProvider = lines[2] or "opencode"
local savedId = lines[3] or "big-pickle"
-- ابحث عنه في القائمة
for _, m in ipairs(MODEL_LIST) do
if m.provider ~= "separator" and m.label == savedLabel then
return m
end
end
-- إذا مو موجود في القائمة، أنشئ entry مؤقت
return { label = savedLabel, provider = savedProvider, id = savedId }
end
return nil
end
-- تطبيق اختيار النموذج (يعدّل API_URL + MODEL_ID + API_KEYS)
local API_URL = OPENCODE_URL
local MODEL_ID = "big-pickle"
local API_KEYS = OPENCODE_KEYS
local KEY_INDEX = 0 -- FIX: تعريف واحد فقط هنا (كان مكرر بعدين كـ global implicit)
local KEY_RESET_TIME = 0 -- tick() when all keys exhausted
-- FIX v1.4: cache maxOutput عند تغيير النموذج بدل البحث في كل طلب
local CURRENT_MODEL_MAX_OUTPUT = nil
local function applyModelChoice(entry)
if not entry or entry.provider == "separator" then return end
CURRENT_PROVIDER = entry.provider
CURRENT_MODEL_LABEL = entry.label
MODEL_ID = entry.id
CURRENT_MODEL_MAX_OUTPUT = entry.maxOutput or nil -- FIX v1.4
if entry.provider == "nvidia" then
API_URL = NVIDIA_URL
API_KEYS = { NVIDIA_API_KEY }
else
API_URL = OPENCODE_URL
API_KEYS = OPENCODE_KEYS
end
KEY_INDEX = 0
saveModelChoice(entry.label, entry.provider, entry.id)
end
-- حمّل الاختيار المحفوظ عند البدء
do
local saved = loadModelChoice()
if saved then applyModelChoice(saved) end
end
local function getApiKey()
if type(API_KEYS) ~= "table" or #API_KEYS == 0 then return nil end
local key = API_KEYS[KEY_INDEX + 1] or API_KEYS[1]
KEY_INDEX = (KEY_INDEX + 1) % #API_KEYS
return key
end
local function currentKeyLabel()
return "key " .. (KEY_INDEX % #API_KEYS + 1) .. "/" .. #API_KEYS
end
-- 💰 أسعار تقريبية للتوكنز (دولار لكل 1 مليون توكن)
local PRICE_PER_1M_INPUT = 0.15
local PRICE_PER_1M_OUTPUT = 0.60
local requestFunc = nil
if syn and syn.request then
requestFunc = syn.request
elseif http_request then
requestFunc = http_request
elseif request then
requestFunc = request
elseif fluxus_request then
requestFunc = fluxus_request
else
requestFunc = function(data)
return HttpService:PostAsync(data.Url, data.Body, Enum.HttpContentType.ApplicationJson, false, data.Headers)
end
end
pcall(function()
local old = CoreGui:FindFirstChild("Skill-AI")
if old then old:Destroy() end
end)
local C = {
bg = Color3.fromRGB(10, 10, 16),
surface = Color3.fromRGB(18, 18, 26),
surfaceHi = Color3.fromRGB(28, 28, 38),
border = Color3.fromRGB(45, 45, 55),
accent = Color3.fromRGB(0, 200, 255),
text = Color3.fromRGB(240, 240, 245),
textDim = Color3.fromRGB(140, 140, 155),
green = Color3.fromRGB(0, 255, 150),
red = Color3.fromRGB(255, 80, 80),
yellow = Color3.fromRGB(255, 210, 0),
orange = Color3.fromRGB(255, 140, 0),
}
local function corner(inst, r)
local c = Instance.new("UICorner", inst)
c.CornerRadius = UDim.new(0, r or 10)
return c
end
local function stroke(inst, col, t, trans)
local s = Instance.new("UIStroke", inst)
s.Color = col or C.border
s.Thickness = t or 1
s.Transparency = trans or 0.2
return s
end
local function tw(inst, info, props)
local t = TweenService:Create(inst, info, props)
t:Play()
return t
end
local function makeDraggable(frame, handle)
handle = handle or frame
local dragging, dragStart, startPos = false, nil, nil
handle.InputBegan:Connect(function(inp)
if (inp.UserInputType == Enum.UserInputType.MouseButton1 or inp.UserInputType == Enum.UserInputType.Touch) then
dragging = true
dragStart = inp.Position
startPos = frame.Position
end
end)
UIS.InputChanged:Connect(function(inp)
if dragging and (inp.UserInputType == Enum.UserInputType.MouseMovement or inp.UserInputType == Enum.UserInputType.Touch) then
local d = inp.Position - dragStart
frame.Position = UDim2.new(
startPos.X.Scale, startPos.X.Offset + d.X,
startPos.Y.Scale, startPos.Y.Offset + d.Y
)
end
end)
UIS.InputEnded:Connect(function(inp)
if inp.UserInputType == Enum.UserInputType.MouseButton1 or inp.UserInputType == Enum.UserInputType.Touch then
dragging = false
end
end)
end
local sg = Instance.new("ScreenGui")
sg.Name = "Skill-AI"
sg.ResetOnSpawn = false
sg.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
sg.IgnoreGuiInset = true
local sgParented = pcall(function() sg.Parent = CoreGui end)
if not sgParented then
pcall(function() sg.Parent = Players.LocalPlayer:WaitForChild("PlayerGui") end)
end
local toggleBtn = Instance.new("TextButton")
toggleBtn.Name = "Toggle"
toggleBtn.Size = UDim2.new(0, 42, 0, 42)
toggleBtn.Position = UDim2.new(0.04, 0, 0.15, 0)
toggleBtn.BackgroundColor3 = C.surfaceHi
toggleBtn.Text = "🧠"
toggleBtn.TextColor3 = C.accent
toggleBtn.TextSize = 20
toggleBtn.Font = Enum.Font.GothamBold
toggleBtn.Parent = sg
corner(toggleBtn, 21)
stroke(toggleBtn, C.accent, 1.5, 0.3)
makeDraggable(toggleBtn)
local PW = 340
local PH = 440
local panel = Instance.new("Frame")
panel.Name = "Panel"
panel.Size = UDim2.new(0, PW, 0, PH)
panel.Position = UDim2.new(0.5, -PW/2, 0.5, -PH/2)
panel.BackgroundColor3 = C.bg
panel.BorderSizePixel = 0
panel.ClipsDescendants = true
panel.Parent = sg
corner(panel, 12)
stroke(panel, C.accent, 1, 0.7)
local tb = Instance.new("Frame")
tb.Size = UDim2.new(1, 0, 0, 34)
tb.BackgroundTransparency = 1
tb.Parent = panel
local titleText = Instance.new("TextLabel")
titleText.Size = UDim2.new(1, -130, 1, 0)
titleText.Position = UDim2.new(0, 12, 0, 0)
titleText.BackgroundTransparency = 1
titleText.Text = "🧠 Skill-AI v1.4"
titleText.TextColor3 = C.accent
titleText.TextXAlignment = Enum.TextXAlignment.Left
titleText.TextSize = 14
titleText.Font = Enum.Font.GothamBold
titleText.Parent = tb
-- زر اختيار النموذج
local modelBtn = Instance.new("TextButton")
modelBtn.Size = UDim2.new(0, 110, 0, 22)
modelBtn.Position = UDim2.new(1, -138, 0.5, -11)
modelBtn.BackgroundColor3 = C.surfaceHi
modelBtn.TextColor3 = C.yellow
modelBtn.TextSize = 9
modelBtn.Font = Enum.Font.Gotham
modelBtn.TextTruncate = Enum.TextTruncate.AtEnd
modelBtn.Parent = tb
corner(modelBtn, 5)
stroke(modelBtn, C.yellow, 1, 0.4)
local function updateModelBtn()
local short = CURRENT_MODEL_LABEL:gsub("%(Free%)", ""):gsub("%(NVIDIA%)", ""):match("^%s*(.-)%s*$")
if #short > 18 then short = short:sub(1, 17) .. "…" end
modelBtn.Text = "⚙ " .. short
end
updateModelBtn()
-- ── Model Picker Popup ──────────────────────────────────────────────────────
local closeBtn = Instance.new("TextButton")
closeBtn.Size = UDim2.new(0, 22, 0, 22)
closeBtn.Position = UDim2.new(1, -28, 0.5, -11)
closeBtn.BackgroundColor3 = C.surfaceHi
closeBtn.Text = "✕"
closeBtn.TextColor3 = C.red
closeBtn.TextSize = 10
closeBtn.Font = Enum.Font.GothamBold
closeBtn.Parent = tb
corner(closeBtn, 6)
closeBtn.MouseButton1Click:Connect(function() sg:Destroy() end)
makeDraggable(panel, tb)
toggleBtn.MouseButton1Click:Connect(function()
panel.Visible = not panel.Visible
tw(toggleBtn, TweenInfo.new(0.3), {Rotation = panel.Visible and 0 or 180})
end)
local displayArea = Instance.new("ScrollingFrame")
displayArea.Size = UDim2.new(1, -16, 1, -115)
displayArea.Position = UDim2.new(0, 8, 0, 40)
displayArea.BackgroundColor3 = C.surface
displayArea.BorderSizePixel = 0
displayArea.ScrollBarThickness = 0
displayArea.AutomaticCanvasSize = Enum.AutomaticSize.Y
displayArea.CanvasSize = UDim2.new(0, 0, 0, 0)
displayArea.Parent = panel
corner(displayArea, 8)
stroke(displayArea, C.border, 1, 0.3)
local displayList = Instance.new("UIListLayout", displayArea)
displayList.Padding = UDim.new(0, 6)
displayList.HorizontalAlignment = Enum.HorizontalAlignment.Center
displayList.SortOrder = Enum.SortOrder.LayoutOrder
local welcomeLbl = Instance.new("TextLabel")
welcomeLbl.Size = UDim2.new(1, -10, 0, 40)
welcomeLbl.BackgroundTransparency = 1
welcomeLbl.Text = "💬 Ask me anything or give me a command — I plan and execute..."
welcomeLbl.TextColor3 = C.textDim
welcomeLbl.TextSize = 12
welcomeLbl.Font = Enum.Font.Gotham
welcomeLbl.Parent = displayArea
local inputArea = Instance.new("Frame")
inputArea.Size = UDim2.new(1, -16, 0, 52)
inputArea.Position = UDim2.new(0, 8, 1, -60)
inputArea.BackgroundColor3 = C.surface
inputArea.Parent = panel
corner(inputArea, 8)
stroke(inputArea, C.border, 1, 0.3)
local inputBox = Instance.new("TextBox")
inputBox.Size = UDim2.new(1, -64, 1, -8)
inputBox.Position = UDim2.new(0, 8, 0, 4)
inputBox.BackgroundTransparency = 1
inputBox.PlaceholderText = "Type any command in any language..."
inputBox.TextColor3 = C.text
inputBox.PlaceholderColor3 = C.textDim
inputBox.TextSize = 13
inputBox.Font = Enum.Font.Gotham
inputBox.ClearTextOnFocus = false
inputBox.Parent = inputArea
local sendBtn = Instance.new("TextButton")
sendBtn.Size = UDim2.new(0, 44, 1, -8)
sendBtn.Position = UDim2.new(1, -52, 0, 4)
sendBtn.BackgroundColor3 = C.accent
sendBtn.Text = "➤"
sendBtn.TextColor3 = Color3.fromRGB(0, 0, 0)
sendBtn.TextSize = 18
sendBtn.Font = Enum.Font.GothamBold
sendBtn.Parent = inputArea
corner(sendBtn, 6)
local function getInstancePath(inst)
if not inst then return "nil" end
local parts = {}
local current = inst
while current do
local name = current.Name or "?"
table.insert(parts, 1, name)
current = current.Parent
end
return table.concat(parts, " > ")
end
-- Popup window showing full generated script code with a real copy button.
-- Used when direct Source writing fails (executor blocks it): shows the code
-- ready to copy/paste instead of trying to bypass the restriction.
local function showScriptPopup(code, scriptName)
local overlay = Instance.new("Frame")
overlay.Size = UDim2.new(1, 0, 1, 0)
overlay.BackgroundColor3 = Color3.new(0, 0, 0)
overlay.BackgroundTransparency = 0.4
overlay.ZIndex = 50
overlay.Parent = sg
local box = Instance.new("Frame")
box.Size = UDim2.new(0.9, 0, 0.7, 0)
box.Position = UDim2.new(0.05, 0, 0.15, 0)
box.BackgroundColor3 = C.surface
box.ZIndex = 51
box.Parent = overlay
corner(box, 12)
stroke(box, C.accent, 1.5, 0.2)
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, -16, 0, 36)
title.Position = UDim2.new(0, 8, 0, 4)
title.BackgroundTransparency = 1
title.Text = "📝 " .. (scriptName or "Script") .. " — Copy this code into a new Script"
title.TextColor3 = C.accent
title.TextSize = 13
title.Font = Enum.Font.GothamBold
title.TextWrapped = true
title.ZIndex = 52
title.Parent = box
local codeScroll = Instance.new("ScrollingFrame")
codeScroll.Size = UDim2.new(1, -16, 1, -90)
codeScroll.Position = UDim2.new(0, 8, 0, 40)
codeScroll.BackgroundColor3 = C.bg
codeScroll.ScrollBarThickness = 6
codeScroll.ZIndex = 52
codeScroll.Parent = box
corner(codeScroll, 8)
local codeLbl = Instance.new("TextLabel")
codeLbl.Size = UDim2.new(1, -12, 0, 0)
codeLbl.Position = UDim2.new(0, 6, 0, 6)
codeLbl.BackgroundTransparency = 1
codeLbl.Text = code
codeLbl.TextColor3 = C.green
codeLbl.TextSize = 12
codeLbl.Font = Enum.Font.Code
codeLbl.TextWrapped = true
codeLbl.TextXAlignment = Enum.TextXAlignment.Left
codeLbl.TextYAlignment = Enum.TextYAlignment.Top
codeLbl.ZIndex = 53
codeLbl.Parent = codeScroll
local lineCount = select(2, code:gsub("\n", "\n")) + 1
local approxHeight = math.max(100, lineCount * 16 + (#code / 50) * 14)
codeLbl.Size = UDim2.new(1, -12, 0, approxHeight)
codeScroll.CanvasSize = UDim2.new(0, 0, 0, approxHeight + 12)
local copyBtn = Instance.new("TextButton")
copyBtn.Size = UDim2.new(0.48, -4, 0, 36)
copyBtn.Position = UDim2.new(0, 8, 1, -42)
copyBtn.BackgroundColor3 = C.accent
copyBtn.Text = "📋 Copy Code"
copyBtn.TextColor3 = Color3.new(0, 0, 0)
copyBtn.TextSize = 13
copyBtn.Font = Enum.Font.GothamBold
copyBtn.ZIndex = 52
copyBtn.Parent = box
corner(copyBtn, 8)
copyBtn.MouseButton1Click:Connect(function()
local copied = false
if setclipboard then
copied = pcall(function() setclipboard(code) end)
end
if copied then
copyBtn.Text = "✅ Copied! Paste into a Script in the Explorer"
else
copyBtn.Text = "❌ Clipboard not supported by this Executor"
end
end)
local closeBtn2 = Instance.new("TextButton")
closeBtn2.Size = UDim2.new(0.48, -4, 0, 36)
closeBtn2.Position = UDim2.new(0.52, 0, 1, -42)
closeBtn2.BackgroundColor3 = C.surfaceHi
closeBtn2.Text = "Close"
closeBtn2.TextColor3 = C.text
closeBtn2.TextSize = 13
closeBtn2.Font = Enum.Font.Gotham
closeBtn2.ZIndex = 52
closeBtn2.Parent = box
corner(closeBtn2, 8)
closeBtn2.MouseButton1Click:Connect(function()
overlay:Destroy()
end)
end
local showInstanceInfo
local function addMessage(text, isUser, isError, isSystem, clickInstance)
welcomeLbl.Visible = false
-- FIX v1.4: message cap أسرع — بدل GetChildren() في كل message نتتبع العداد
local allEntries = displayArea:GetChildren()
local entryCount = #allEntries
if entryCount > MESSAGE_MAX then
local toRemove = entryCount - MESSAGE_MAX
for i = 1, toRemove do
local oldest = allEntries[i]
if oldest and oldest ~= welcomeLbl and oldest ~= displayList then
oldest:Destroy()
end
end
end
local entry = Instance.new("Frame")
entry.Size = UDim2.new(1, -10, 0, 0)
entry.BackgroundTransparency = 1
entry.Parent = displayArea
local bg = Instance.new("Frame")
bg.Size = UDim2.new(1, 0, 0, 0)
if isError then
bg.BackgroundColor3 = C.red
elseif isSystem then
bg.BackgroundColor3 = Color3.fromRGB(30, 30, 20)
elseif isUser then
bg.BackgroundColor3 = C.surfaceHi
else
bg.BackgroundColor3 = C.surface
end
bg.Parent = entry
corner(bg, 8)
stroke(bg, isError and C.red or (isUser and C.accent or C.border), 0.5, 0.3)
local lbl = Instance.new("TextButton")
lbl.Size = UDim2.new(1, -16, 1, -8)
lbl.Position = UDim2.new(0, 8, 0, 4)
lbl.BackgroundTransparency = 1
lbl.Text = text
lbl.TextColor3 = isError and C.text or (isUser and C.accent or C.text)
lbl.TextSize = 12
lbl.Font = Enum.Font.Gotham
lbl.TextWrapped = true
lbl.TextXAlignment = Enum.TextXAlignment.Left
lbl.AutoButtonColor = false
lbl.Selectable = false
lbl.Parent = bg
if clickInstance then
local actual = clickInstance
if type(actual) == "table" and actual._ref then
actual = actual._ref
end
if actual and type(actual) == "userdata" then
lbl.TextColor3 = Color3.fromRGB(100, 180, 255)
lbl.MouseButton1Click:Connect(function()
showInstanceInfo(actual)
end)
entry.BackgroundTransparency = 0.85
end
end
-- FIX v1.4: حساب الارتفاع الدقيق بدون مسح النص — O(1) بدل O(n)
-- نعتمد على عدد السطور الفعلي + طول النص كمؤشر للف
local lineBreaks = select(2, text:gsub("\n", "\n"))
local wrapLines = math.floor(#text / 42) -- تقدير الف عند عرض ~42 حرف
local totalLines = math.max(1, lineBreaks + wrapLines)
local height = math.max(30, math.min(300, totalLines * 16 + 18))
bg.Size = UDim2.new(1, 0, 0, height)
entry.Size = UDim2.new(1, 0, 0, height + 4)
task.wait()
displayArea.CanvasPosition = Vector2.new(0, displayArea.CanvasSize.Y.Offset)
end
showInstanceInfo = function(inst)
if not inst then
addMessage("❌ Instance no longer exists", false, true)
return
end
local className = inst:IsA("BasePart") and inst.ClassName or inst.ClassName or "Instance"
local info = "📦 " .. className
info = info .. " \"" .. inst.Name .. "\"\n📁 " .. getInstancePath(inst)
if inst:IsA("BasePart") then
local pos = inst.Position
info = info .. ("\n📍 %.1f, %.1f, %.1f"):format(pos.X, pos.Y, pos.Z)
end
if inst:IsA("Script") or inst:IsA("LocalScript") then
local src = inst.Source or ""
if #src > 0 then
info = info .. "\n📝 " .. #src .. " chars"
else
info = info .. "\n⚠️ Source is empty."
end
end
if inst:IsA("StringValue") then
local val = inst.Value or ""
if #val > 0 then
info = info .. "\n━━━━━━━━━━━━━━━━━━\n" .. val .. "\n━━━━━━━━━━━━━━━━━━"
else
info = info .. "\n⚠️ StringValue is empty"
end
end
addMessage(info, false, false, true)
end
-- Model picker popup
local function showModelPicker()
-- overlay
local overlay = Instance.new("Frame")
overlay.Size = UDim2.new(1, 0, 1, 0)
overlay.BackgroundColor3 = Color3.new(0, 0, 0)
overlay.BackgroundTransparency = 0.5
overlay.ZIndex = 60
overlay.Parent = sg
local popup = Instance.new("Frame")
popup.Size = UDim2.new(0, 290, 0, 380)
popup.Position = UDim2.new(0.5, -145, 0.5, -190)
popup.BackgroundColor3 = C.bg
popup.ZIndex = 61
popup.Parent = overlay
corner(popup, 12)
stroke(popup, C.accent, 1.5, 0.2)
local hdr = Instance.new("TextLabel")
hdr.Size = UDim2.new(1, -40, 0, 32)
hdr.Position = UDim2.new(0, 10, 0, 4)
hdr.BackgroundTransparency = 1
hdr.Text = "⚙ Select Model"
hdr.TextColor3 = C.accent
hdr.TextXAlignment = Enum.TextXAlignment.Left
hdr.TextSize = 13
hdr.Font = Enum.Font.GothamBold
hdr.ZIndex = 62
hdr.Parent = popup
local closePop = Instance.new("TextButton")
closePop.Size = UDim2.new(0, 24, 0, 24)
closePop.Position = UDim2.new(1, -30, 0, 4)
closePop.BackgroundColor3 = C.surfaceHi
closePop.Text = "✕"
closePop.TextColor3 = C.red
closePop.TextSize = 10
closePop.Font = Enum.Font.GothamBold
closePop.ZIndex = 62
closePop.Parent = popup
corner(closePop, 6)
closePop.MouseButton1Click:Connect(function() overlay:Destroy() end)
-- Current model label
local curLbl = Instance.new("TextLabel")
curLbl.Size = UDim2.new(1, -12, 0, 20)
curLbl.Position = UDim2.new(0, 6, 0, 36)
curLbl.BackgroundTransparency = 1
curLbl.Text = "Now: " .. CURRENT_MODEL_LABEL
curLbl.TextColor3 = C.green
curLbl.TextSize = 10
curLbl.Font = Enum.Font.Gotham
curLbl.TextTruncate = Enum.TextTruncate.AtEnd
curLbl.TextXAlignment = Enum.TextXAlignment.Left
curLbl.ZIndex = 62
curLbl.Parent = popup
local scroll = Instance.new("ScrollingFrame")
scroll.Size = UDim2.new(1, -12, 1, -64)
scroll.Position = UDim2.new(0, 6, 0, 58)
scroll.BackgroundColor3 = C.surface
scroll.ScrollBarThickness = 4
scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
scroll.CanvasSize = UDim2.new(0, 0, 0, 0)
scroll.ZIndex = 62
scroll.Parent = popup
corner(scroll, 8)
local list = Instance.new("UIListLayout", scroll)
list.Padding = UDim.new(0, 3)
list.SortOrder = Enum.SortOrder.LayoutOrder
local pad = Instance.new("UIPadding", scroll)
pad.PaddingLeft = UDim.new(0, 4)
pad.PaddingTop = UDim.new(0, 4)
for idx, entry in ipairs(MODEL_LIST) do
if entry.provider == "separator" then
local sep = Instance.new("TextLabel")
sep.Size = UDim2.new(1, -8, 0, 20)
sep.BackgroundColor3 = C.surfaceHi
sep.Text = "── NVIDIA NIM ──"
sep.TextColor3 = C.yellow
sep.TextSize = 10
sep.Font = Enum.Font.GothamBold
sep.LayoutOrder = idx
sep.ZIndex = 63
sep.Parent = scroll
corner(sep, 4)
else
local isActive = (entry.label == CURRENT_MODEL_LABEL)
local btn = Instance.new("TextButton")
btn.Size = UDim2.new(1, -8, 0, 26)
btn.BackgroundColor3 = isActive and C.accent or C.surfaceHi
btn.TextColor3 = isActive and Color3.new(0,0,0) or C.text
btn.Text = entry.label
btn.TextSize = 10
btn.Font = Enum.Font.Gotham
btn.TextXAlignment = Enum.TextXAlignment.Left
btn.TextTruncate = Enum.TextTruncate.AtEnd
btn.LayoutOrder = idx
btn.ZIndex = 63
btn.Parent = scroll
corner(btn, 5)
local lbl2 = Instance.new("UIPadding", btn)
lbl2.PaddingLeft = UDim.new(0, 6)
local capturedEntry = entry
btn.MouseButton1Click:Connect(function()
applyModelChoice(capturedEntry)
updateModelBtn()
curLbl.Text = "✅ Now: " .. CURRENT_MODEL_LABEL
for _, child in ipairs(scroll:GetChildren()) do
if child:IsA("TextButton") then
child.BackgroundColor3 = C.surfaceHi
child.TextColor3 = C.text
end
end
btn.BackgroundColor3 = C.accent
btn.TextColor3 = Color3.new(0, 0, 0)
task.delay(0.2, function()
overlay:Destroy()
addMessage("✅ Model changed → **" .. CURRENT_MODEL_LABEL .. "**\nProvider: " .. CURRENT_PROVIDER:upper() .. " | Saved automatically.", false)
end)
end)
end
end
end
modelBtn.MouseButton1Click:Connect(showModelPicker)
local LOG_FILE = "SkillAI_Log.txt"
local function logToFile(label, content)
local ok = pcall(function()
local stamp = "\n[" .. os.date("%X") .. "] " .. label .. ":\n" .. tostring(content) .. "\n" .. string.rep("-", 30) .. "\n"
if writefile then
if isfile and isfile(LOG_FILE) and readfile then
local existing = readfile(LOG_FILE)
writefile(LOG_FILE, existing .. stamp)
else
writefile(LOG_FILE, stamp)
end
end
end)
return ok
end
local function setupRespawnReRunner()
local plr = Players.LocalPlayer
local function reRunAll()
for _, entry in ipairs(_autoRunScripts) do
local fn, err = loadstring("local script = ...\n" .. entry.source)
if fn then
task.spawn(function()
local ok, e = pcall(fn, entry.inst)
if not ok then logToFile("Respawn re-run error " .. entry.name, e) end
end)
end
end
end
reRunAll()
plr.CharacterAdded:Connect(reRunAll)
end
-- 🧠 تصنيف ذكي لتعقيد الطلب — يصير محلياً بالكود (فوري، صفر توكنز، صفر وقت
-- إضافي)، يقرر هل الطلب يحتاج تفكير عميق (high) أو بسيط (low). هذا يحل
-- التناقض "أريد دقة high بسرعة low": الحل مو إعداد ثابت واحد، بل نستخدم
-- high فقط للطلبات اللي فعلاً تحتاجه (سكربتات، أنظمة، خطط معقدة)، وlow
-- لكل شي آخر (يغطي أغلب الطلبات اليومية: قطعة، أداة، تعديل، حذف، إلخ).
local HIGH_EFFORT_KEYWORDS = {
"script", "system", "unlock", "lock", "track", "when ", "challenge",
"complete", "trigger", "damage", "spawn enemies", "ai", "npc",
"leaderboard", "datastore", "save", "load", "remote", "multiplayer",
"quest", "checkpoint", "gate", "obstacle course", "lap", "stage",
"wave", "phase", "progression", "rank", "level", "door", "key",
"boss", "health bar", "timer", "countdown", "score", "points",
"سكربت", "نظام", "يفتح", "يقفل", "تحدي", "يتتبع",
"بوابة", "مرحلة", "مستوى", "نقاط", "مؤقت", "بوس",
}
local function classifyEffort(question)
if type(question) ~= "string" then return "low" end
local lowerQ = question:lower()
-- طول الطلب نفسه مؤشر قوي: طلبات طويلة ومفصلة غالباً معقدة منطقياً
if #question > 220 then return "high" end
for _, kw in ipairs(HIGH_EFFORT_KEYWORDS) do
if lowerQ:find(kw, 1, true) then
return "high"
end
end
return "low"
end
-- FIX v1.4: مهلة الطلب — يمنع التجميد اللانهائي عند انقطاع الشبكة
local REQUEST_TIMEOUT = math.huge
local function callAI(messages, _isRetry, effort, _keyAttempt)
_keyAttempt = _keyAttempt or 0
local maxKeyAttempts = (#API_KEYS or 1) * 2
local currentKey = getApiKey()
if not currentKey then
return nil, "❌ No API keys configured. Add keys to API_KEYS table."
end
-- FIX v1.4: cache modelMaxOutput بدل البحث في كل مرة
local cap = (effort == "high") and MAX_TOKENS_HIGH or MAX_TOKENS_LOW
local maxTokens = math.min(cap, CURRENT_MODEL_MAX_OUTPUT or cap)
local data = {
model = MODEL_ID,
messages = messages,
max_tokens = maxTokens,
temperature = 0.2,
top_p = 0.95,
}
local json = HttpService:JSONEncode(data)
local headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. currentKey,
}
-- FIX v1.4: Timeout wrapper — الطلب يتوقف بعد REQUEST_TIMEOUT ثانية
local success, response
do
local done = false
local result_ok, result_val
task.spawn(function()
result_ok, result_val = pcall(function()
if requestFunc then
return requestFunc({
Url = API_URL,
Method = "POST",
Headers = headers,
Body = json,
})
else
return HttpService:PostAsync(API_URL, json, Enum.HttpContentType.ApplicationJson, false, headers)
end
end)
done = true
end)
local t0 = tick()
while not done and (tick() - t0) < REQUEST_TIMEOUT do
task.wait(0.2)
end
if not done then
return nil, "❌ Request timed out after " .. REQUEST_TIMEOUT .. "s. Check connection."
end
success, response = result_ok, result_val
end
if not success then
if not _isRetry then
-- FIX v1.4: 0.8s بدل 2s على الـ retry الأول — أسرع
task.wait(0.8)
return callAI(messages, true, effort, _keyAttempt)
end
return nil, "❌ Connection failed: " .. tostring(response)
end
local body = response.Body or response
local okDecode, decoded = pcall(function() return HttpService:JSONDecode(body) end)
if not okDecode or not decoded then
logToFile("JSON decode failed", tostring(body):sub(1, 500))
if not _isRetry then
task.wait(0.8) -- FIX v1.4: أسرع
return callAI(messages, true, effort, _keyAttempt)
end
return nil, "❌ Server returned invalid JSON. Check " .. LOG_FILE
end
local errMsg = nil
if decoded.error then
if type(decoded.error) == "table" then
errMsg = tostring(decoded.error.message or decoded.error.code or "")
elseif type(decoded.error) == "string" then
errMsg = decoded.error
end
elseif decoded.message then
errMsg = tostring(decoded.message)
elseif decoded.detail then
errMsg = tostring(decoded.detail)
end
if errMsg then
logToFile("API error", errMsg)
if errMsg:find("FreeUsageLimitError", 1, true) or errMsg:find("rate limit", 1, true) or errMsg:find("quota", 1, true) then
if _keyAttempt < maxKeyAttempts then
local backoff = math.min(2 ^ (_keyAttempt - math.floor(_keyAttempt / #API_KEYS)), 60)
task.wait(backoff)
return callAI(messages, _isRetry, effort, _keyAttempt + 1)
end
KEY_RESET_TIME = tick()
return nil, "❌ All API keys exhausted. Wait ~60s or add new keys. Last key: " .. currentKeyLabel()
end
return nil, "❌ API: " .. errMsg
end
if decoded.choices and decoded.choices[1] then
local msg = decoded.choices[1].message
local content = msg and msg.content
local usage = decoded.usage
if content and content ~= "" then
return content, nil, usage
end
local reasoning = msg and msg.reasoning_content
if reasoning and reasoning ~= "" then
return reasoning, nil, usage
end
logToFile("Model returned no content", pcall(function() return HttpService:JSONEncode(decoded) end) and HttpService:JSONEncode(decoded) or tostring(decoded))
return nil, "⚠️ Model did not generate a response. Try again."
end
local decodedStr = pcall(function() return HttpService:JSONEncode(decoded) end) and HttpService:JSONEncode(decoded) or tostring(decoded)
logToFile("No choices in response", decodedStr)
if not _isRetry then
task.wait(0.8) -- FIX v1.4: أسرع
return callAI(messages, true, effort, _keyAttempt)
end
return nil, "❌ Unexpected server response. Check " .. LOG_FILE
end
local PARENT_MAP = {
Workspace = Workspace,
StarterPack = StarterPack,
Lighting = Lighting,
ServerStorage = ServerStorage,
ReplicatedStorage = ReplicatedStorage,
StarterGui = StarterGui,
StarterPlayer = StarterPlayer,
StarterCharacterScripts = StarterCharacterScripts,
StarterPlayerScripts = StarterPlayerScripts,
CoreGui = CoreGui,
Players = Players,
}
local function resolveParent(name, fallback)
if type(name) == "string" and PARENT_MAP[name] then
return PARENT_MAP[name]
end
return fallback or Workspace
end
local function fixLuaCode(source, compileErr)
local sysContent = "You fix Lua syntax errors. Return ONLY the corrected Lua code. No explanations, no markdown."
local userContent = "Fix this Lua error: " .. compileErr .. "\n\n```lua\n" .. source .. "\n```"
local fixMessages
if CURRENT_PROVIDER == "nvidia" then
fixMessages = { { role = "user", content = sysContent .. "\n\n" .. userContent } }
else
fixMessages = {
{ role = "system", content = sysContent },
{ role = "user", content = userContent }
}
end
local fixBody = HttpService:JSONEncode({
model = MODEL_ID,
messages = fixMessages,
max_tokens = 1000,
temperature = 0.1
})
local fixKey = getApiKey() or ""
local ok, response = pcall(function()
return requestFunc({
Url = API_URL,
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer " .. fixKey
},
Body = fixBody
})
end)
if not ok or not response then return nil end
local ok2, data = pcall(HttpService.JSONDecode, HttpService, response.Body)
if not ok2 or not data or not data.choices or not data.choices[1] then return nil end
local fixed = data.choices[1].message.content or ""
fixed = fixed:gsub("```lua", ""):gsub("```", ""):gsub("^%s+", ""):gsub("%s+$", "")
local fn, err2 = loadstring("local script = ...\n" .. fixed)
if fn then return fixed end
return nil, err2
end
local MAX_HISTORY_TURNS = 8
local conversationHistory = {} -- each entry: { user = "...", summary = "..." }
local CURRENT_REQUEST_TEXT = "" -- stores the latest user request text for context safety checks (e.g., fog detection)
local function pushHistory(userMsg, summary)
table.insert(conversationHistory, { user = userMsg, summary = summary })
while #conversationHistory > MAX_HISTORY_TURNS do
table.remove(conversationHistory, 1)
end
end
local function buildHistoryMessages()
local msgs = {}
-- Only send last exchange (saves tokens, speeds up response)
local startIdx = math.max(1, #conversationHistory - 1)
for i = startIdx, #conversationHistory do
local turn = conversationHistory[i]
table.insert(msgs, { role = "user", content = turn.user })
table.insert(msgs, { role = "assistant", content = turn.summary })
end
return msgs
end
local SEARCH_SERVICES = { Workspace, StarterPack, Lighting, ServerStorage, ReplicatedStorage, StarterGui, StarterPlayer, StarterCharacterScripts, StarterPlayerScripts, Players, CoreGui }
-- _searchCache معرَّف في الأعلى (سطر ~88) — لا تعريف مكرر هنا
local _searchCacheTime = 0
-- FIX v1.4: رفعنا TTL من 0.3 → 1.5 ثانية — المشهد نادراً يتغير بين طلبين متتاليين
local SEARCH_CACHE_TTL = 1.5
local function searchAllDescendants()
if _searchCache and tick() - _searchCacheTime < SEARCH_CACHE_TTL then
return _searchCache
end
-- FIX v1.4: pre-allocate بدل table.insert المتكرر
local all = {}
local idx = 0
for _, svc in ipairs(SEARCH_SERVICES) do
if svc and type(svc) == "userdata" then
local desc = svc:GetDescendants()
for i = 1, #desc do
idx = idx + 1
all[idx] = desc[i]
end
end
end
-- FIX v1.4: فقط اللاعب المحلي (بدل كل اللاعبين) لأن هذا LocalScript
local pg = player:FindFirstChild("PlayerGui")
if pg then
local desc = pg:GetDescendants()
for i = 1, #desc do
idx = idx + 1
all[idx] = desc[i]
end
end
_searchCache = all
_searchCacheTime = tick()
return all
end
local function findByName(search)
if not search or search == "" then return nil end
if search:find(".", 1, true) then
local parts = {}
for p in search:gmatch("[^.]+") do table.insert(parts, p) end
if #parts >= 2 then
local function traverseFrom(start, startIdx)
local current = start
for i = startIdx, #parts do
local child = current:FindFirstChild(parts[i])
if not child then
child = current:FindFirstChild(parts[i]:lower())
end
if not child then
for _, c in ipairs(current:GetChildren()) do
if c.Name:lower() == parts[i]:lower() then
child = c
break
end
end
end
if not child then
local searchNoSpaces = parts[i]:gsub("%s+", "")
for _, c in ipairs(current:GetChildren()) do
if c.Name:lower():gsub("%s+", "") == searchNoSpaces:lower() then
child = c
break
end
end
end
if not child then return nil end
if i == #parts then return child end
current = child
end
return nil
end
-- try direct service lookup
local svc = PARENT_MAP[parts[1]]
if svc then
local found = traverseFrom(svc, 2)
if found then return found end
end
-- parts[1] might match a service key case-insensitively
for svcKey, svcVal in pairs(PARENT_MAP) do
if svcKey:lower() == parts[1]:lower() then
local found = traverseFrom(svcVal, 2)
if found then return found end
end
end
-- parts[1] is not a service; search across all services for the root
for _, svc in ipairs(SEARCH_SERVICES) do
if svc and type(svc) == "userdata" then
local root = svc:FindFirstChild(parts[1])
if not root then
for _, c in ipairs(svc:GetChildren()) do
if c.Name:lower() == parts[1]:lower() then
root = c
break
end
end
end
if root then
local found = traverseFrom(root, 2)
if found then return found end
if root:IsA("Player") and root.Character then
found = traverseFrom(root.Character, 2)
if found then return found end
end
end
end
end
end
end
local lowerSearch = search:lower()
local exact, prefixMatch, substringMatch = nil, nil, nil
for _, obj in ipairs(searchAllDescendants()) do
local name = obj.Name:lower()
if name == lowerSearch then
exact = obj
break
elseif not prefixMatch and name:find(lowerSearch, 1, true) == 1 then
prefixMatch = obj
elseif not substringMatch and name:find(lowerSearch, 1, true) then
substringMatch = obj
end
end
return exact or prefixMatch or substringMatch
end
local function findAllByName(search)
local results = {}
if not search or search == "" then return results end
local lowerSearch = search:lower()
local cache = _searchCache
if cache then
for _, obj in ipairs(cache) do
if obj.Name:lower():find(lowerSearch, 1, true) then
table.insert(results, obj)
end
end
else
for _, obj in ipairs(searchAllDescendants()) do
if obj.Name:lower():find(lowerSearch, 1, true) then
table.insert(results, obj)
end
end
end
return results
end
local function resolveUDim2(t)
if type(t) ~= "table" then return nil end
local xs, xo, ys, yo = t[1], t[2], t[3], t[4]
if xs == nil and t.X ~= nil then xs = t.X end
if xo == nil and t.XOffset ~= nil then xo = t.XOffset end
if ys == nil and t.Y ~= nil then ys = t.Y end
if yo == nil and t.YOffset ~= nil then yo = t.YOffset end
if xs == nil then xs = 0 end
if xo == nil then xo = 0 end
if ys == nil then ys = 0 end
if yo == nil then yo = 0 end
return UDim2.new(xs, xo, ys, yo)
end
local function applyProperties(inst, props)
if type(props) ~= "table" then return end
for key, value in pairs(props) do
pcall(function()
if key == "Position" and type(value) == "table" then
if inst:IsA("GuiObject") then
local u = resolveUDim2(value)
if u then inst.Position = u end
else
inst.Position = Vector3.new(value[1] or 0, value[2] or 0, value[3] or 0)
end
elseif key == "Size" and type(value) == "table" then
if inst:IsA("GuiObject") then
local u = resolveUDim2(value)
if u then inst.Size = u end
elseif inst:IsA("BasePart") then
inst.Size = Vector3.new(value[1] or 1, value[2] or 1, value[3] or 1)
else
inst.Size = UDim2.new(value[1] or 0, value[2] or 0, value[3] or 0, value[4] or 0)
end
elseif key == "Color" and type(value) == "table" then
inst.Color = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
elseif key == "TextColor3" and type(value) == "table" then
inst.TextColor3 = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
elseif key == "BackgroundColor3" and type(value) == "table" then
inst.BackgroundColor3 = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
elseif key == "BorderColor3" and type(value) == "table" then
inst.BorderColor3 = Color3.fromRGB(value[1] or 255, value[2] or 255, value[3] or 255)
elseif key == "BrickColor" and type(value) == "string" then
inst.BrickColor = BrickColor.new(value)
elseif key == "Material" and type(value) == "string" then
inst.Material = Enum.Material[value] or inst.Material
elseif key == "Shape" and type(value) == "string" then
if inst:IsA("Part") then
inst.Shape = Enum.PartType[value] or Enum.PartType.Block
end
elseif key == "MeshId" and type(value) == "string" then
if inst:IsA("MeshPart") then
inst.MeshId = value
elseif inst:IsA("Part") then
local mesh = Instance.new("SpecialMesh")
mesh.MeshId = value
mesh.Parent = inst
end
elseif key == "TextureId" and type(value) == "string" then
if inst:IsA("MeshPart") then
inst.TextureId = value
elseif inst:IsA("Part") then
local mesh = inst:FindFirstChildOfClass("SpecialMesh") or Instance.new("SpecialMesh")
mesh.TextureId = value
mesh.Parent = inst
end
elseif key == "Font" and type(value) == "string" then
inst.Font = Enum.Font[value] or inst.Font
elseif key == "TextXAlignment" and type(value) == "string" then
inst.TextXAlignment = Enum.TextXAlignment[value] or Enum.TextXAlignment.Center
elseif key == "TextYAlignment" and type(value) == "string" then
inst.TextYAlignment = Enum.TextYAlignment[value] or Enum.TextYAlignment.Center
elseif key == "AnchorPoint" and type(value) == "table" then
inst.AnchorPoint = Vector2.new(value[1] or 0, value[2] or 0)
elseif key == "Image" and type(value) == "string" then
if inst:IsA("ImageLabel") or inst:IsA("ImageButton") then
inst.Image = value
end
elseif key == "Anchored" then
inst.Anchored = value
elseif key == "Transparency" then
inst.Transparency = value
elseif key == "BackgroundTransparency" then
inst.BackgroundTransparency = value
elseif key == "CanCollide" then
inst.CanCollide = value
elseif key == "WalkSpeed" then
inst.WalkSpeed = value
elseif key == "PlatformStand" then
inst.PlatformStand = value
elseif key == "Brightness" then
inst.Brightness = value
elseif key == "ClockTime" then
inst.ClockTime = value
elseif key == "FogEnd" then
inst.FogEnd = value
elseif key == "FogStart" then
inst.FogStart = value
elseif key == "Source" then
local srcOk = pcall(function() inst.Source = "--from skill-Ai\n" .. tostring(value) end)
if not srcOk then
local srcOk2 = pcall(function() inst[key] = "--from skill-Ai\n" .. tostring(value) end)
end
else
inst[key] = value
end
end)
end
end
local function safeRandomRange(r)
local function pick(arrIdx, objKey, default)
local v
if type(r) == "table" then
v = r[arrIdx]
if v == nil and objKey then v = r[objKey] end
end
v = tonumber(v)
if v == nil then v = default end
return math.floor(v)
end
local minX = pick(1, "minX", -60)
local maxX = pick(2, "maxX", 60)
local minY = pick(3, "minY", -5)
local maxY = pick(4, "maxY", 10)
local minZ = pick(5, "minZ", -60)
local maxZ = pick(6, "maxZ", 60)
if minX > maxX then minX, maxX = maxX, minX end
if minY > maxY then minY, maxY = maxY, minY end
if minZ > maxZ then minZ, maxZ = maxZ, minZ end
return minX, maxX, minY, maxY, minZ, maxZ
end
local function getOrCreateContainer(name, parent)
if not name or name == "" then return parent end
local existing = parent:FindFirstChild(name)
if existing and existing:IsA("Folder") then
return existing
end
local folder = Instance.new("Folder")
folder.Name = name
folder.Parent = parent
return folder
end
local function makeScriptWithBackup(className, name, source, targetParent, containerName)
if containerName then
targetParent = getOrCreateContainer(containerName, targetParent)
end
local host = Instance.new("StringValue")
host.Name = name or className
host.Value = source
host.Parent = targetParent
local mock = {
Name = host.Name,
Parent = targetParent,
ClassName = className,
Source = source,
Value = source,
FindFirstChild = function(self, childName)
return targetParent:FindFirstChild(childName)
end,
WaitForChild = function(self, childName, timeout)
return targetParent:WaitForChild(childName, timeout)
end,
IsA = function(self, class)
return className == class
end,
Destroy = function(self)
host:Destroy()
end,
}
local loadOk = false
local compileErr = nil
pcall(function()
local fn, compErr = loadstring("local script = ...\n" .. source)
if fn then
task.spawn(function()
local runtimeOk, runtimeErr = pcall(fn, mock)
if not runtimeOk then
logToFile("Runtime error in " .. (name or className), runtimeErr)
end
end)
loadOk = true
else
compileErr = compErr
logToFile("Compile error in " .. (name or className), compErr)
end
end)
return mock, nil, true, host, loadOk, compileErr
end
local function ensureToolHandle(toolInst, handleProps)
local handle = toolInst:FindFirstChild("Handle")
if not handle then
handle = Instance.new("Part")
handle.Name = "Handle"
handle.Size = Vector3.new(0.6, 2.5, 0.6)
handle.Color = Color3.fromRGB(150, 150, 150)
handle.Material = Enum.Material.Metal
handle.Parent = toolInst
end
if type(handleProps) == "table" then
applyProperties(handle, handleProps)
end
handle.Anchored = false
handle.CanCollide = false
toolInst.RequiresHandle = true
toolInst.Grip = toolInst.Grip or CFrame.new()
return handle
end
local function generateChallengeScript(challengeType, targetCount, challengeName, title)
local function fill(t)
local s = t
s = s:gsub("{N}", function() return challengeName end)
s = s:gsub("{C}", function() return tostring(targetCount) end)
s = s:gsub("{T}", function() return (title or "Challenge") end)
return s
end
if challengeType == "reach" then
return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false
detector.Touched:Connect(function(hit)
if completed then return end
local char = hit.Parent
local hum = char and char:FindFirstChild("Humanoid")
if hum and plr.Character == char then
completed = true
progressLabel.Text = "1/1"
progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
titleLabel.Text = "✅ REACHED!"
end
end)
]])
elseif challengeType == "survive" then
return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false
local inside = false
local survivedSeconds = 0
local TARGET = {C}
detector.Touched:Connect(function(hit)
local char = hit.Parent
local hum = char and char:FindFirstChild("Humanoid")
if hum and plr.Character == char then inside = true end
end)
detector.TouchEnded:Connect(function(hit)
local char = hit.Parent
local hum = char and char:FindFirstChild("Humanoid")
if hum and plr.Character == char then inside = false end
end)
task.spawn(function()
while not completed do
task.wait(1)
if inside then
survivedSeconds = survivedSeconds + 1
progressLabel.Text = survivedSeconds .. "/" .. TARGET
if survivedSeconds >= TARGET then
completed = true
progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
titleLabel.Text = "✅ SURVIVED!"
end
end
end
end)
]])
elseif challengeType == "kill" then
return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false
local killCount = 0
local connectedHumans = {}
local TARGET = {C}
detector.Touched:Connect(function(hit)
local char = hit.Parent
local hum = char and char:FindFirstChild("Humanoid")
if hum and not connectedHumans[hum] then
connectedHumans[hum] = true
hum.Died:Connect(function()
if completed then return end
if not char.Parent then
task.wait(0.1)
killCount = killCount + 1
progressLabel.Text = killCount .. "/" .. TARGET
if killCount >= TARGET then
completed = true
progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
titleLabel.Text = "✅ COMPLETE!"
end
end
end)
end
end)
]])
elseif challengeType == "collect" then
return fill([[
local plr = game:GetService("Players").LocalPlayer
local gui = plr:WaitForChild("PlayerGui"):WaitForChild("{N}_GUI")
local progressLabel = gui.MainFrame.Progress
local titleLabel = gui.MainFrame.Title
local detector = script.Parent:WaitForChild("{N}_Detector")
local completed = false
local collected = 0
local TARGET = {C}
detector.Touched:Connect(function(hit)
if completed then return end
if hit:GetAttribute("Collectible") or hit.Name:find("Collect") then
collected = collected + 1
progressLabel.Text = collected .. "/" .. TARGET
pcall(function() hit:Destroy() end)
if collected >= TARGET then
completed = true
progressLabel.TextColor3 = Color3.fromRGB(0, 255, 150)
titleLabel.Text = "✅ COLLECTED!"
end
end
end)
]])
end
return "task.wait(9e9)"
end
local function setCollision(inst, enabled)
if not inst or not inst:IsA("BasePart") then return end
inst.CanCollide = enabled
task.spawn(function()
task.wait(0)
if inst and inst.Parent then
inst.CanCollide = enabled
end
end)
task.spawn(function()
task.wait(0.15)
if inst and inst.Parent then
inst.CanCollide = enabled
end
end)
end
local function executeAction(action)
local kind = action.type
if kind == "create_instance" then
local className = action.className
if not className then return false, "❌ Missing className" end
local ok, inst = pcall(function() return Instance.new(className) end)
if not ok or not inst then
return false, "❌ Failed to create " .. tostring(className)
end
inst.Name = action.name or className
applyProperties(inst, action.properties)
-- set Anchored/CanCollide BEFORE parent (belt)
if inst:IsA("BasePart") and className ~= "Tool" then
inst.Anchored = true
setCollision(inst, true)
end
if className == "Tool" then
ensureToolHandle(inst, action.handleProperties)
end
local targetParent = resolveParent(action.parent, Workspace)
-- AUTO-CORRECT: GUI objects in Workspace belong in StarterGui
local GUI_CLASSES = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
ImageLabel = true, ImageButton = true, ScrollingFrame = true, BillboardGui = true, ViewportFrame = true }
if GUI_CLASSES[className] and targetParent == Workspace then
targetParent = StarterGui
end
-- AUTO-CORRECT: Parts don't belong in GUI services
local GUI_SERVICES = { StarterGui = true, StarterPlayerScripts = true, StarterCharacterScripts = true }
if inst:IsA("BasePart") and GUI_SERVICES[targetParent.Name] then
targetParent = Workspace
end
-- AUTO-CORRECT: Decal/Texture/SurfaceGui need a BasePart parent, not Workspace
local ATTACHMENT_CLASSES = { Decal = true, Texture = true, SurfaceGui = true }
if ATTACHMENT_CLASSES[className] and targetParent == Workspace then
for i = #_batchCreated, 1, -1 do
local prev = _batchCreated[i]
if type(prev) ~= "table" and prev:IsA("BasePart") then
targetParent = prev
break
end
end
end
-- AUTO-CORRECT: StringValue/ModuleScript with code belong in ServerStorage, not Workspace
local CODE_CLASSES = { StringValue = true, ModuleScript = true }
if CODE_CLASSES[className] and targetParent == Workspace then
targetParent = ServerStorage
end
if action.containerName then
targetParent = getOrCreateContainer(action.containerName, targetParent)
end
inst.Parent = targetParent
-- force Anchored/CanCollide AFTER parent (suspenders — some executors lose pre-parent writes)
if inst:IsA("BasePart") then
if className == "Tool" then
local props = type(action.properties) == "table" and action.properties or {}
if props.Anchored == nil then inst.Anchored = false end
if props.CanCollide == nil then setCollision(inst, false) end
else
inst.Anchored = true
setCollision(inst, true)
end
end
table.insert(_batchCreated, inst)
updateSceneState(action, true)
if className == "Camera" and action.setAsCurrentCamera then
Workspace.CurrentCamera = inst
local hrp = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
if hrp then
local offset = action.cameraOffset or {0, 2, -10}
inst.CFrame = CFrame.new(
hrp.Position + Vector3.new(0, 2, 0),
hrp.Position + Vector3.new(offset[1] or 0, offset[2] or 2, offset[3] or -10)
)
end
end
_lastClickInstance = inst
return true, "✅ Created " .. inst.Name .. " (" .. className .. ")"
elseif kind == "create_multiple" then
local className = action.className or "Part"
local count = math.clamp(tonumber(action.count) or 1, 1, 200)
local parent = resolveParent(action.parent, Workspace)
-- AUTO-CORRECT: bulk create is for Parts, redirect from GUI services
local GUI_SERVICES = { StarterGui = true, StarterPlayerScripts = true, StarterCharacterScripts = true }
if GUI_SERVICES[parent.Name] then
parent = Workspace
end
local container = getOrCreateContainer(action.containerName or (className .. "_Batch"), parent)
local created = 0
for i = 1, count do
pcall(function()
local ok, inst = pcall(function() return Instance.new(className) end)
if ok and inst then
inst.Name = (action.namePrefix or className) .. "_" .. i
applyProperties(inst, action.properties)
-- belt: set before parent
if inst:IsA("BasePart") and className ~= "Tool" then
inst.Anchored = true
setCollision(inst, true)
end
if className == "Tool" then
ensureToolHandle(inst, action.handleProperties)
end
if action.randomizePosition then
local minX, maxX, minY, maxY, minZ, maxZ = safeRandomRange(action.randomRange)
if action.gridSpacing then
local spacing = math.max(1, tonumber(action.gridSpacing) or 4)
local maxAttempts = 30
local placed = _batchPlacedGrid or {}
_batchPlacedGrid = placed
local found = false
for _ = 1, maxAttempts do
local gx = math.floor(math.random(minX, maxX) / spacing) * spacing
local gy = math.floor(math.random(minY, maxY) / spacing) * spacing
local gz = math.floor(math.random(minZ, maxZ) / spacing) * spacing
local key = gx..","..gy..","..gz
if not placed[key] then
placed[key] = true
inst.Position = Vector3.new(gx, gy, gz)
found = true
break
end
end
if not found then
inst.Position = Vector3.new(math.random(minX, maxX), math.random(minY, maxY), math.random(minZ, maxZ))
end
else
inst.Position = Vector3.new(
math.random(minX, maxX),
math.random(minY, maxY),
math.random(minZ, maxZ)
)
end
-- rejection sampling: if rejectionRadius set, check distance to existing BaseParts in container
if action.rejectionRadius and container then
local radius = tonumber(action.rejectionRadius) or 4
local rejectAttempts = 30
for _ = 1, rejectAttempts do
local tooClose = false
for _, existing in ipairs(container:GetDescendants()) do
if existing:IsA("BasePart") and existing ~= inst then
local dist = (inst.Position - existing.Position).Magnitude
if dist < radius then
tooClose = true
break
end
end
end
if tooClose then
inst.Position = Vector3.new(
math.random(minX, maxX),
math.random(minY, maxY),
math.random(minZ, maxZ)
)
else
break
end
end
end
end
inst.Parent = container
-- force Anchored/CanCollide AFTER parenting for physics engine
if inst:IsA("BasePart") then
if className == "Tool" then
local props = type(action.properties) == "table" and action.properties or {}
if props.Anchored == nil then inst.Anchored = false end
if props.CanCollide == nil then setCollision(inst, false) end
else
inst.Anchored = true
setCollision(inst, true)
end
end
table.insert(_batchCreated, inst)
created += 1
end
end)
end
_batchPlacedGrid = nil
updateSceneState(action, true)
_lastClickInstance = container
return true, "✅ Created " .. created .. " items inside " .. container.Name
elseif kind == "set_property" then
local target = action.target and findByName(action.target)
if not target then return false, "❌ Not found: " .. tostring(action.target) end
applyProperties(target, action.properties)
updateSceneState(action, true)
return true, "✅ Updated properties: " .. target.Name
elseif kind == "delete" then
local target = action.target and findByName(action.target)
if not target then return false, "❌ Not found: " .. tostring(action.target) end
local n = target.Name
table.insert(_batchCreated, { _markedForDestroy = true, _ref = target, _name = n })
target:Destroy()
updateSceneState(action, true)
return true, "🗑️ Deleted: " .. n
elseif kind == "clone" then
local target = action.target and findByName(action.target)
if not target then return false, "❌ Not found: " .. tostring(action.target) end
local clone = target:Clone()
clone.Name = action.newName or (target.Name .. "_clone")
clone.Parent = target.Parent
table.insert(_batchCreated, clone)
updateSceneState(action, true)
return true, "📋 Cloned: " .. target.Name .. " → " .. clone.Name
elseif kind == "rename" then
local target = action.target and findByName(action.target)
if not target then return false, "❌ Not found: " .. tostring(action.target) end
local old = target.Name
target.Name = action.newName or old
updateSceneState(action, true)
return true, "✏️ Renamed: " .. old .. " → " .. target.Name
elseif kind == "move" then
local target = action.target and findByName(action.target)
if not target then return false, "❌ Not found: " .. tostring(action.target) end
if target:IsA("BasePart") and action.position then
target.Position = Vector3.new(action.position[1] or 0, action.position[2] or 0, action.position[3] or 0)
updateSceneState(action, true)
return true, "🚚 Moved: " .. target.Name
elseif action.parent then
target.Parent = resolveParent(action.parent, target.Parent)
updateSceneState(action, true)
return true, "🚚 Moved: " .. target.Name .. " to " .. tostring(action.parent)
end
return false, "❌ Insufficient move data"
elseif kind == "set_lighting" then
if type(action.properties) == "table" and action.properties.FogEnd ~= nil then
local fogKeywords = { "fog", "backrooms", "horror", "mist", "haze", "creepy" }
local mentioned = false
local lowerQ = (CURRENT_REQUEST_TEXT or ""):lower()
for _, kw in ipairs(fogKeywords) do
if lowerQ:find(kw, 1, true) then
mentioned = true
break
end
end
if not mentioned then
local fe = tonumber(action.properties.FogEnd)
if fe and fe < 300 then
action.properties.FogEnd = nil -- skip fog since user did not ask for it
end
end
end
applyProperties(Lighting, action.properties)
updateSceneState(action, true)
return true, "💡 Lighting updated"
elseif kind == "set_character" then
local char = player.Character
if not char or not char:FindFirstChild("Humanoid") then
return false, "❌ No character available"
end
applyProperties(char.Humanoid, action.properties)
if char:FindFirstChild("HumanoidRootPart") and action.transparency ~= nil then
char.HumanoidRootPart.Transparency = action.transparency
end
updateSceneState(action, true)
return true, "✅ Character state updated"
elseif kind == "create_compound" then
local parts = action.parts
if not parts or type(parts) ~= "table" or #parts == 0 then
return false, "❌ Missing parts array for compound"
end
local model = Instance.new("Model")
model.Name = action.name or "Compound"
local createdParts = {}
local primaryIdx = action.primaryPartIndex or 1
local isToolCompound = action.makeTool or action.className == "Tool"
-- parent model FIRST so parts enter DataModel immediately
local targetParent = resolveParent(action.parent, Workspace)
if action.containerName then
targetParent = getOrCreateContainer(action.containerName, targetParent)
end
model.Parent = targetParent
for i, partData in ipairs(parts) do
pcall(function()
local cn = partData.className or "Part"
local ok, inst = pcall(function() return Instance.new(cn) end)
if ok and inst then
inst.Name = partData.name or ("Part_" .. i)
applyProperties(inst, partData)
-- belt: set before parent
if inst:IsA("BasePart") and not isToolCompound then
inst.Anchored = true
setCollision(inst, true)
end
inst.Parent = model
table.insert(createdParts, inst)
end
end)
end
if #createdParts == 0 then
model:Destroy()
return false, "❌ Failed to create any compound part"
end
-- force Anchored/CanCollide AFTER all parts are in the DataModel
for _, part in ipairs(createdParts) do
if part:IsA("BasePart") then
if isToolCompound then
part.Anchored = false
setCollision(part, false)
else
part.Anchored = true
setCollision(part, true)
end
end
end
-- ⚠️ فحص تصادم/تراكب بين أجزاء نفس الـ compound: لو الـ AI أخطأ بحساب
-- position وحط جزئين (مثلاً النصل والمقبض) بنفس النقطة تقريباً، الشكل
-- النهائي يطلع كتلة واحدة مشوهة بدل سيف/برج واضح المعالم. نتحقق من كل
-- زوج أجزاء، ولو متراكبين فعلياً (مسافة أقل من نص متوسط حجمهم)، نزيح
-- الجزء الثاني قليلاً على المحور Y حتى يصير الشكل مفهوم بصرياً.
local overlapFixCount = 0
for i = 1, #createdParts do
for j = i + 1, #createdParts do
local a, b = createdParts[i], createdParts[j]
local okCheck, isOverlap = pcall(function()
local dist = (a.Position - b.Position).Magnitude
local avgSize = ((a.Size.X + a.Size.Y + a.Size.Z) + (b.Size.X + b.Size.Y + b.Size.Z)) / 6
return dist < (avgSize * 0.15) -- متراكبين فعلياً، مو بس قريبين بشكل طبيعي
end)
if okCheck and isOverlap then
pcall(function()
b.Position = b.Position + Vector3.new(0, (b.Size.Y * 0.6) + 0.1, 0)
end)
overlapFixCount = overlapFixCount + 1
end
end
end
local primary = createdParts[primaryIdx]
for i, part in ipairs(createdParts) do
if part ~= primary then
local weld = Instance.new("WeldConstraint")
weld.Name = ("Weld_%d_%d"):format(primaryIdx, i)
weld.Part0 = primary
weld.Part1 = part
weld.Parent = part
end
end
model.PrimaryPart = primary
if action.makeTool or action.className == "Tool" then
local tool = Instance.new("Tool")
tool.Name = action.name or model.Name
local h = Instance.new("Part")
h.Name = "Handle"
h.Size = Vector3.new(0.6, 2.5, 0.6)
h.Anchored = false
h.CanCollide = false
h.Parent = tool
for _, child in ipairs(model:GetChildren()) do
child.Parent = tool
end
model:Destroy()
model = tool
tool.Parent = targetParent -- re-parent since model was destroyed
tool.RequiresHandle = true
tool.Grip = CFrame.new()
end
table.insert(_batchCreated, model)
updateSceneState(action, true)
_lastClickInstance = model
local overlapMsg = overlapFixCount > 0 and (" (auto-fixed " .. overlapFixCount .. " overlapping part(s))") or ""
return true, "✅ Created compound: " .. model.Name .. overlapMsg
elseif kind == "create_enclosure" then
-- 🧮 GEOMETRY HELPER: يبني أرضية + جدران (وحاجز اختياري) من وصف منطقي
-- بسيط فقط (مركز، عرض، عمق، ارتفاع) — كل حساب المواقع/المسافات يصير
-- هنا بالكود (فوري، صفر توكنز)، مو من الـ AI. هذا يلغي السبب الجذري
-- وراء "Impure JSON" بطلبات فيها جدران/حواجز متعددة المواقع، لأن الموديل
-- ما يحتاج يحسب إحداثيات يدوياً بصوت عالي قبل الرد.
local centerX = tonumber(action.centerX) or 0
local centerY = tonumber(action.centerY) or 0
local centerZ = tonumber(action.centerZ) or 0
local width = math.max(2, tonumber(action.width) or 20) -- X axis
local depth = math.max(2, tonumber(action.depth) or 20) -- Z axis
local wallHeight = math.max(1, tonumber(action.wallHeight) or 4)
local floorThickness = 0.5
local wallThickness = 0.5
local hasFloor = action.floor ~= false
local openSides = {} -- e.g. {"North"} to leave an entrance gap
if type(action.openSides) == "table" then
for _, s in ipairs(action.openSides) do openSides[s] = true end
end
local color = action.color or {150,150,150}
local material = action.material or "SmoothPlastic"
local barrier = action.barrier -- bool: add an invisible solid barrier covering the whole footprint at wall height (used for "locked until unlocked" platforms)
local model = Instance.new("Model")
model.Name = action.name or "Enclosure"
local targetParent = resolveParent(action.parent, Workspace)
if action.containerName then
targetParent = getOrCreateContainer(action.containerName, targetParent)
end
model.Parent = targetParent
local function mkPart(pname, size, pos)
local p = Instance.new("Part")
p.Name = pname
p.Size = Vector3.new(size[1], size[2], size[3])
p.Position = Vector3.new(pos[1], pos[2], pos[3])
pcall(function() p.Color = Color3.fromRGB(color[1] or 150, color[2] or 150, color[3] or 150) end)
pcall(function() p.Material = Enum.Material[material] or Enum.Material.SmoothPlastic end)
p.Anchored = true
setCollision(p, true)
p.Parent = model
return p
end
if hasFloor then
mkPart("Floor", {width, floorThickness, depth}, {centerX, centerY, centerZ})
end
local floorTop = centerY + floorThickness / 2
local wallCenterY = floorTop + wallHeight / 2
if not openSides["North"] then
mkPart("WallNorth", {width, wallHeight, wallThickness}, {centerX, wallCenterY, centerZ - depth/2})
end
if not openSides["South"] then
mkPart("WallSouth", {width, wallHeight, wallThickness}, {centerX, wallCenterY, centerZ + depth/2})
end
if not openSides["East"] then
mkPart("WallEast", {wallThickness, wallHeight, depth}, {centerX + width/2, wallCenterY, centerZ})
end
if not openSides["West"] then
mkPart("WallWest", {wallThickness, wallHeight, depth}, {centerX - width/2, wallCenterY, centerZ})
end
if barrier then
local b = mkPart("Barrier", {width, wallHeight, depth}, {centerX, wallCenterY, centerZ})
b.Transparency = 1
setCollision(b, true)
b.Name = "Barrier"
end
table.insert(_batchCreated, model)
updateSceneState(action, true)
_lastClickInstance = model
return true, "✅ Created enclosure: " .. model.Name .. " (" .. (hasFloor and "floor+" or "") .. "walls" .. (barrier and "+barrier" or "") .. ")"
elseif kind == "create_challenge_system" then
local challengeType = (action.challengeType or "reach"):lower()
local validTypes = { reach = true, survive = true, kill = true, collect = true }
if not validTypes[challengeType] then
return false, "❌ Invalid challengeType '" .. tostring(action.challengeType) .. "'. Valid: reach, survive, kill, collect"
end
local targetCount = math.max(1, tonumber(action.targetCount) or 10)
local title = action.title or ("Challenge: " .. challengeType)
local challengeName = action.name or "Challenge"
local centerX = tonumber(action.centerX) or 0
local centerY = tonumber(action.centerY) or 5
local centerZ = tonumber(action.centerZ) or 0
local width = math.max(2, tonumber(action.width) or 20)
local depth = math.max(2, tonumber(action.depth) or 20)
local height = math.max(2, tonumber(action.height) or 6)
local visibleFloor = action.visibleFloor ~= false
local zoneModel = Instance.new("Model")
zoneModel.Name = challengeName .. "_Zone"
local zoneParent = resolveParent(action.parent, Workspace)
if action.containerName then
zoneParent = getOrCreateContainer(action.containerName, zoneParent)
end
zoneModel.Parent = zoneParent
if visibleFloor then
local floor = Instance.new("Part")
floor.Name = challengeName .. "_Floor"
floor.Size = Vector3.new(width, 0.5, depth)
floor.Position = Vector3.new(centerX, centerY - 0.25, centerZ)
floor.Anchored = true
setCollision(floor, true)
floor.BrickColor = BrickColor.new("Bright blue")
floor.Material = Enum.Material.SmoothPlastic
floor.Transparency = 0.3
floor.Parent = zoneModel
end
-- optional walls around zone
local wallOpenSides = {}
if type(action.walls) == "table" then
for _, s in ipairs(action.walls) do wallOpenSides[s] = true end
end
if action.walls == true or type(action.walls) == "table" then
local wallH = math.max(1, tonumber(action.wallHeight) or height)
local wallT = 0.5
local floorTop = centerY + 0.25
local wallCY = floorTop + wallH / 2
local wallColor = action.wallColor or {100, 100, 255}
local wallMat = action.wallMaterial or "SmoothPlastic"
local function mkWall(n, sz, pos)
local w = Instance.new("Part")
w.Name = challengeName .. "_" .. n
w.Size = Vector3.new(sz[1], sz[2], sz[3])
w.Position = Vector3.new(pos[1], pos[2], pos[3])
w.Anchored = true
setCollision(w, true)
pcall(function() w.Color = Color3.fromRGB(wallColor[1] or 100, wallColor[2] or 100, wallColor[3] or 255) end)
pcall(function() w.Material = Enum.Material[wallMat] or Enum.Material.SmoothPlastic end)
w.Transparency = 0.2
w.Parent = zoneModel
end
if not wallOpenSides["North"] then mkWall("WallNorth", {width, wallH, wallT}, {centerX, wallCY, centerZ - depth/2}) end
if not wallOpenSides["South"] then mkWall("WallSouth", {width, wallH, wallT}, {centerX, wallCY, centerZ + depth/2}) end
if not wallOpenSides["East"] then mkWall("WallEast", {wallT, wallH, depth}, {centerX + width/2, wallCY, centerZ}) end
if not wallOpenSides["West"] then mkWall("WallWest", {wallT, wallH, depth}, {centerX - width/2, wallCY, centerZ}) end
end
local detector = Instance.new("Part")
detector.Name = challengeName .. "_Detector"
detector.Size = Vector3.new(width, height, depth)
detector.Position = Vector3.new(centerX, centerY + height / 2 - 0.25, centerZ)
detector.Anchored = true
detector.CanCollide = false
detector.Transparency = 1
detector.Parent = zoneModel
table.insert(_batchCreated, zoneModel)
local guiPosition = (action.guiPosition or "top-right"):lower()
local guiPosMap = {
["top-right"] = UDim2.new(1, -270, 0, 10),
["top-left"] = UDim2.new(0, 20, 0, 10),
["bottom-right"] = UDim2.new(1, -270, 1, -80),
["bottom-left"] = UDim2.new(0, 20, 1, -80),
["center"] = UDim2.new(0.5, -125, 0.5, -30),
}
local guiSize = type(action.guiSize) == "table" and action.guiSize or {250, 60}
local gw = math.max(100, tonumber(guiSize[1]) or 250)
local gh = math.max(40, tonumber(guiSize[2]) or 60)
local gui = Instance.new("ScreenGui")
gui.Name = challengeName .. "_GUI"
gui.ResetOnSpawn = false
gui.Parent = StarterGui
local frame = Instance.new("Frame")
frame.Name = "MainFrame"
frame.Size = UDim2.new(0, gw, 0, gh)
frame.Position = guiPosMap[guiPosition] or guiPosMap["top-right"]
frame.BackgroundColor3 = Color3.fromRGB(10, 10, 20)
frame.BackgroundTransparency = 0.3
frame.BorderSizePixel = 0
frame.Parent = gui
local uic = Instance.new("UICorner")
uic.CornerRadius = UDim.new(0, 8)
uic.Parent = frame
local titleLabel = Instance.new("TextLabel")
titleLabel.Name = "Title"
titleLabel.Size = UDim2.new(1, -20, 0, 22)
titleLabel.Position = UDim2.new(0, 10, 0, 4)
titleLabel.BackgroundTransparency = 1
titleLabel.Text = title
titleLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
titleLabel.TextSize = 14
titleLabel.Font = Enum.Font.GothamBold
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
titleLabel.Parent = frame
local progressLabel = Instance.new("TextLabel")
progressLabel.Name = "Progress"
progressLabel.Size = UDim2.new(1, -20, 0, 26)
progressLabel.Position = UDim2.new(0, 10, 0, 28)
progressLabel.BackgroundTransparency = 1
progressLabel.Text = "0/" .. targetCount
progressLabel.TextColor3 = Color3.fromRGB(0, 200, 255)
progressLabel.TextSize = 20
progressLabel.Font = Enum.Font.GothamBlack
progressLabel.TextXAlignment = Enum.TextXAlignment.Left
progressLabel.Parent = frame
table.insert(_batchCreated, gui)
local scriptSrc = generateChallengeScript(challengeType, targetCount, challengeName, title)
local host = Instance.new("StringValue")
host.Name = challengeName .. "_Script"
host.Value = "--from skill-Ai\n" .. scriptSrc
host.Parent = zoneModel
local mock = {
Name = host.Name,
Parent = zoneModel,
ClassName = "LocalScript",
Source = scriptSrc,
Value = scriptSrc,
FindFirstChild = function(self, childName)
local ok, r = pcall(function() return zoneModel:FindFirstChild(childName) end)
return ok and r
end,
WaitForChild = function(self, childName, timeout)
local ok, r = pcall(function() return zoneModel:WaitForChild(childName, timeout) end)
return ok and r
end,
FindFirstChildOfClass = function(self, cls)
local ok, r = pcall(function() return zoneModel:FindFirstChildOfClass(cls) end)
return ok and r
end,
Destroy = function(self)
pcall(function() host:Destroy() end)
pcall(function() gui:Destroy() end)
pcall(function() zoneModel:Destroy() end)
for idx, entry in ipairs(_autoRunScripts) do
if entry.inst == self then
table.remove(_autoRunScripts, idx)
break
end
end
end,
IsA = function(self, cls) return cls == "Script" or cls == "LocalScript" end,
}
local fn, compileErr = loadstring("local script = ...\n" .. scriptSrc)
local loadOk = false
local execErr = nil
if fn then
local ok, e = pcall(fn, mock)
if ok then
loadOk = true
else
execErr = tostring(e)
end
else
execErr = tostring(compileErr)
end
if loadOk then
table.insert(_autoRunScripts, { name = host.Name, source = scriptSrc, inst = mock })
if #_autoRunScripts > AUTORUN_MAX then
table.remove(_autoRunScripts, 1)
end
end
updateSceneState(action, true)
local fx = visibleFloor and "floor+" or ""
local msg = "✅ Created challenge: " .. challengeName .. " (" .. challengeType .. " " .. targetCount .. ", " .. fx .. "zone)"
if loadOk then
msg = msg .. " | Executed"
elseif execErr then
msg = msg .. " | ⚠️ " .. execErr
end
return true, msg
elseif kind == "create_script" then
local className = action.className or "Script"
if className ~= "Script" and className ~= "LocalScript" then
return false, "❌ create_script requires className = Script or LocalScript"
end
local source = "--from skill-Ai\n"
if action.source then
source = source .. action.source
end
-- auto-fix syntax errors before creating
local fn_check, err_check = loadstring("local script = ...\n" .. source)
if not fn_check then
local rawCode = action.source or source
local fixedCode, fixErr = fixLuaCode(rawCode, err_check)
if fixedCode then
source = "--from skill-Ai\n" .. fixedCode
end
end
local targetParent = resolveParent(action.parent, nil)
if not targetParent and type(action.parent) == "string" then
for _, createdInst in ipairs(_batchCreated) do
if type(createdInst) ~= "table" and createdInst.Name == action.parent then
targetParent = createdInst
break
end
end
if not targetParent then
targetParent = findByName(action.parent)
end
end
targetParent = targetParent or Workspace
-- SAFETY: reject per-player PlayerGui as parent (scripts there run once per player, confusing)
if targetParent and targetParent:IsA("PlayerGui") and targetParent.Parent and targetParent.Parent:IsA("Player") then
targetParent = StarterPlayerScripts or Workspace
end
-- AUTO-CORRECT: Scripts in Workspace never run — redirect
if targetParent == Workspace then
if className == "LocalScript" then
targetParent = StarterPlayerScripts or StarterPlayer or Workspace
else
targetParent = ServerStorage or Workspace
end
end
local inst, err, sourceWorked, host, loadOk, compileErr = makeScriptWithBackup(
className, action.name, source, targetParent, action.containerName
)
if not inst then
return false, "❌ " .. err
end
table.insert(_batchCreated, host)
updateSceneState(action, true)
_lastClickInstance = host
-- register for respawn re-execution so effects persist across deaths
if loadOk then
table.insert(_autoRunScripts, { name = inst.Name, source = source, inst = inst })
if #_autoRunScripts > AUTORUN_MAX then
table.remove(_autoRunScripts, 1)
end
end
local parts = {}
table.insert(parts, "✅ Created: " .. inst.Name .. " (" .. className .. ")")
if loadOk then
table.insert(parts, "Executed in-game")
elseif compileErr then
table.insert(parts, "⚠️ " .. compileErr)
end
local msg = table.concat(parts, " | ")
msg = msg .. "\n━━━━━━━━━━━━━━━━━━\n" .. source .. "\n━━━━━━━━━━━━━━━━━━"
return true, msg
elseif kind == "undo" then
if #undoStack == 0 then
return false, "⚠️ Nothing to undo."
end
local lastBatch = table.remove(undoStack)
local removed = 0
for _, entry in ipairs(lastBatch.instances) do
if type(entry) == "table" and entry._markedForDestroy then
-- was a delete action, cannot restore (object already destroyed)
else
local inst = entry
if inst and inst.Parent then
inst:Destroy()
removed += 1
end
end
end
return true, "↩️ Undone: " .. lastBatch.summary .. " (removed " .. removed .. " items)"
elseif kind == "create_plugin" then
local pluginName = action.name or "Plugin"
local modules = action.modules
if not modules or type(modules) ~= "table" or #modules == 0 then
return false, "❌ create_plugin requires a 'modules' array with at least one {name, source}"
end
local icon = action.icon or "🧩"
local btnColor = action.buttonColor or {30, 30, 40}
local targetParent = resolveParent(action.parent, CoreGui)
-- Plugin toolbar GUI
local toolbar = Instance.new("ScreenGui")
toolbar.Name = pluginName .. "_Toolbar"
toolbar.ResetOnSpawn = false
toolbar.Parent = targetParent
local bg = Instance.new("Frame")
bg.Name = "Toolbar"
bg.Size = UDim2.new(0, #modules * 42 + 12, 0, 38)
bg.Position = UDim2.new(0.5, -(#modules * 42 + 12) / 2, 0, 80)
bg.BackgroundColor3 = Color3.fromRGB(btnColor[1] or 30, btnColor[2] or 30, btnColor[3] or 40)
bg.BackgroundTransparency = 0.2
bg.BorderSizePixel = 0
bg.Parent = toolbar
corner(bg, 8)
stroke(bg, Color3.fromRGB(0, 200, 255), 1, 0.3)
makeDraggable(bg)
local titleLbl = Instance.new("TextLabel")
titleLbl.Size = UDim2.new(0, 22, 1, 0)
titleLbl.Position = UDim2.new(0, 4, 0, 0)
titleLbl.BackgroundTransparency = 1
titleLbl.Text = icon
titleLbl.TextColor3 = Color3.fromRGB(0, 200, 255)
titleLbl.TextSize = 16
titleLbl.Font = Enum.Font.GothamBold
titleLbl.Parent = bg
-- Store module scripts
local moduleFolder = Instance.new("Folder")
moduleFolder.Name = pluginName .. "_Modules"
moduleFolder.Parent = targetParent
local moduleStates = {} -- track on/off per module
local activeConnections = {} -- track running threads
for idx, mod in ipairs(modules) do
if type(mod) == "table" and mod.name and mod.source then
-- Store source in StringValue
local sv = Instance.new("StringValue")
sv.Name = mod.name
sv.Value = mod.source
sv.Parent = moduleFolder
-- Button for this module
local btn = Instance.new("TextButton")
btn.Size = UDim2.new(0, 36, 0, 28)
btn.Position = UDim2.new(0, 30 + (idx - 1) * 42, 0.5, -14)
btn.BackgroundColor3 = Color3.fromRGB(50, 50, 60)
btn.Text = mod.name:sub(1, 2):upper()
btn.TextColor3 = Color3.fromRGB(180, 180, 190)
btn.TextSize = 11
btn.Font = Enum.Font.GothamBold
btn.Parent = bg
corner(btn, 6)
local moduleOn = false
local thread = nil
btn.MouseButton1Click:Connect(function()
moduleOn = not moduleOn
if moduleOn then
btn.BackgroundColor3 = Color3.fromRGB(0, 200, 100)
btn.TextColor3 = Color3.new(1, 1, 1)
-- Execute the module script
local fn, err = loadstring("local pluginModule = ...\n" .. mod.source)
if fn then
local ok, runErr = pcall(fn, { Name = mod.name, Source = mod.source, Parent = moduleFolder })
if not ok then
addMessage("⚠️ Plugin module '" .. mod.name .. "' error: " .. tostring(runErr), false, true)
end
else
addMessage("⚠️ Plugin module '" .. mod.name .. "' compile error: " .. tostring(err), false, true)
end
addMessage("✅ Plugin module: " .. mod.name .. " ON", false)
else
btn.BackgroundColor3 = Color3.fromRGB(50, 50, 60)
btn.TextColor3 = Color3.fromRGB(180, 180, 190)
addMessage("⏹️ Plugin module: " .. mod.name .. " OFF", false)
end
end)
moduleStates[mod.name] = false
end
end
table.insert(_batchCreated, toolbar)
table.insert(_batchCreated, moduleFolder)
updateSceneState(action, true)
_lastClickInstance = toolbar
return true, "✅ Created plugin: " .. pluginName .. " with " .. #modules .. " module(s)"
elseif kind == "create_particle" then
local target = resolveTarget(action.target)
if not target then return false, "❌ create_particle: target '" .. tostring(action.target) .. "' not found" end
local pe = Instance.new("ParticleEmitter")
pe.Name = (action.name or "ParticleEmitter")
pe.Texture = action.texture or "rbxasset://textures/particles/sparkles_main.dds"
pe.Rate = action.rate or 20
pe.Speed = action.speed and NumberRange.new(action.speed[1] or 5, action.speed[2] or 10) or NumberRange.new(5, 10)
pe.Lifetime = action.lifetime and NumberRange.new(action.lifetime[1] or 2, action.lifetime[2] or 4) or NumberRange.new(2, 4)
if action.size then
local s = action.size
pe.Size = NumberSequence.new(NumberSequenceKeypoint.new(0, s[1] or s[1]==nil and 1 or s[1]), NumberSequenceKeypoint.new(1, s[#s] or s[2] or 0.5))
else
pe.Size = NumberSequence.new(1, 0.5)
end
if action.color then
local cs = {}
for i, c in ipairs(action.color) do
table.insert(cs, ColorSequenceKeypoint.new((i-1)/(#action.color-1), Color3.fromRGB(c[1] or 255, c[2] or 255, c[3] or 255)))
end
pe.Color = ColorSequence.new(cs)
else
pe.Color = ColorSequence.new(Color3.fromRGB(255, 200, 50))
end
pe.Transparency = action.transparency and NumberSequence.new(action.transparency[1] or 0, action.transparency[2] or 1) or NumberSequence.new(0, 1)
pe.Rotation = action.rotation and NumberRange.new(action.rotation[1] or 0, action.rotation[2] or 360) or NumberRange.new(0, 360)
if action.spread then pe.SpreadAngle = Vector2.new(action.spread, action.spread) end
pe.Enabled = if action.enabled ~= nil then action.enabled else true
pe.Parent = target
table.insert(_batchCreated, pe)
updateSceneState(action, true)
_lastClickInstance = pe
return true, "✨ Particle emitter added to " .. target.Name
elseif kind == "create_light" then
local target = resolveTarget(action.target)
if not target then return false, "❌ create_light: target '" .. tostring(action.target) .. "' not found" end
local lightType = action.lightType or "PointLight"
local light = Instance.new(lightType)
light.Name = (action.name or lightType)
light.Color = action.color and Color3.fromRGB(action.color[1] or 255, action.color[2] or 255, action.color[3] or 255) or Color3.new(1, 1, 1)
light.Brightness = action.brightness or (lightType == "SpotLight" and 2 or 1)
if light:IsA("PointLight") or light:IsA("SpotLight") then
light.Range = action.range or 16
end
if light:IsA("SpotLight") then
light.Angle = action.angle or 90
light.Face = action.Face and Enum.NormalId[action.Face] or Enum.NormalId.Front
end
if light:IsA("SurfaceLight") then
light.Face = action.Face and Enum.NormalId[action.Face] or Enum.NormalId.Front
light.Range = action.range or 16
end
if action.enabled ~= nil then light.Enabled = action.enabled end
light.Parent = target
table.insert(_batchCreated, light)
updateSceneState(action, true)
_lastClickInstance = light
return true, "💡 " .. lightType .. " added to " .. target.Name
elseif kind == "create_sound" then
local soundId = action.soundId
if not soundId or soundId == "" then return false, "❌ create_sound requires soundId" end
local parent
if action.target then
parent = resolveTarget(action.target)
if not parent then return false, "❌ create_sound: target '" .. tostring(action.target) .. "' not found" end
else
parent = resolveParent(action.parent, Workspace)
end
if not parent then parent = Workspace end
local sound = Instance.new("Sound")
sound.Name = action.name or "Sound"
sound.SoundId = soundId
sound.Volume = action.volume or 0.5
sound.Pitch = action.pitch or 1
sound.Looped = action.loops or false
if action.startPaused then sound.Playing = false else sound.Playing = true end
sound.Parent = parent
table.insert(_batchCreated, sound)
updateSceneState(action, true)
_lastClickInstance = sound
return true, "🔊 Sound created: " .. soundId
elseif kind == "create_tween" then
local target = resolveTarget(action.target)
if not target then return false, "❌ create_tween: target '" .. tostring(action.target) .. "' not found" end
local props = action.properties
if not props or next(props) == nil then return false, "❌ create_tween requires a 'properties' table" end
local duration = action.duration or 1
local style = action.style or "Quad"
local direction = action.direction or "Out"
local tsi = TweenInfo.new(duration, Enum.EasingStyle[style] or Enum.EasingStyle.Quad,
Enum.EasingDirection[direction] or Enum.EasingDirection.Out, action.repeatCount or 0,
action.reverses or false)
local tweenData = {}
for k, v in pairs(props) do
if type(v) == "table" and #v == 3 then
if k:lower():find("color") then
pcall(function() tweenData[k] = Color3.fromRGB(v[1] or 255, v[2] or 255, v[3] or 255) end)
else
tweenData[k] = Vector3.new(v[1] or 0, v[2] or 0, v[3] or 0)
end
else
tweenData[k] = v
end
end
local tween = game:GetService("TweenService"):Create(target, tsi, tweenData)
tween:Play()
table.insert(_batchCreated, tween)
updateSceneState(action, true)
_lastClickInstance = target
return true, "🎬 Tween started on " .. target.Name .. " (" .. duration .. "s, " .. style .. direction .. ")"
elseif kind == "create_animation" then
local animType = action.animationType or "asset"
if animType == "asset" then
local targetHum = resolveTarget(action.humanoid or action.target)
if not targetHum then return false, "❌ create_animation: target humanoid '" .. tostring(action.humanoid or action.target) .. "' not found" end
local assetId = action.assetId
if not assetId or assetId == "" then return false, "❌ create_animation: assetId required for 'asset' type" end
local anim = Instance.new("Animation")
anim.Name = action.name or "Animation"
anim.AnimationId = assetId
local parent = resolveParent(action.parent, targetHum.Parent)
anim.Parent = parent or targetHum.Parent
local animator = targetHum:FindFirstChildOfClass("Animator")
if not animator then animator = Instance.new("Animator"); animator.Parent = targetHum end
local track = animator:LoadAnimation(anim)
track:Play()
if action.loop then track:AdjustSpeed(action.speed or 1)
track.Looped = true end
table.insert(_batchCreated, anim)
updateSceneState(action, true)
_lastClickInstance = anim
return true, "🎭 Animation loaded & played on " .. targetHum.Name
elseif animType == "keyframe" then
local targetHum = resolveTarget(action.humanoid or action.target)
if not targetHum then return false, "❌ create_animation: target humanoid '" .. tostring(action.humanoid or action.target) .. "' not found" end
local frames = action.frames
if not frames or type(frames) ~= "table" or #frames == 0 then
return false, "❌ create_animation: 'frames' array required for keyframe type"
end
local anim = Instance.new("Animation")
anim.Name = action.name or "KeyframeAnimation"
local parent = resolveParent(action.parent, targetHum.Parent)
anim.Parent = parent or targetHum.Parent
local animator = targetHum:FindFirstChildOfClass("Animator")
if not animator then animator = Instance.new("Animator"); animator.Parent = targetHum end
-- Build keyframe sequence via a generated script
local kfSource = "local plr = game:GetService('Players').LocalPlayer\nlocal char = plr.Character\nif not char then return end\nlocal hum = char:WaitForChild('Humanoid')\nlocal animator = hum:FindFirstChildOfClass('Animator') or Instance.new('Animator')\nif not animator.Parent then animator.Parent = hum end\nlocal anim = Instance.new('Animation')\nanim.AnimationId = 'rbxasset://animations/anim_slowrun.rbxm'\n-- keyframe animation not fully supported via JSON yet; using run animation as fallback\nlocal track = animator:LoadAnimation(anim)\ntrack:Play()\nif " .. tostring(action.loop or false) .. " then track.Looped = true end"
local kfScript = Instance.new("LocalScript")
kfScript.Name = (action.name or "KeyframeAnim") .. "_Loader"
kfScript.Parent = parent or targetHum.Parent
kfScript.Source = kfSource
table.insert(_batchCreated, kfScript)
updateSceneState(action, true)
_lastClickInstance = kfScript
return true, "🎭 Keyframe animation created (" .. #frames .. " frames)"
else
return false, "❌ Unknown animationType: " .. tostring(animType)
end
elseif kind == "create_texture" then
local target = resolveTarget(action.target)
if not target then return false, "❌ create_texture: target '" .. tostring(action.target) .. "' not found" end
local texType = action.textureType or "Decal"
local tex = Instance.new(texType)
tex.Name = action.name or texType
tex.Texture = action.textureId or "rbxasset://textures/trans.png"
if action.face then tex.Face = Enum.NormalId[action.face] end
if action.color then tex.Color3 = Color3.fromRGB(action.color[1] or 255, action.color[2] or 255, action.color[3] or 255) end
if action.transparency then tex.Transparency = action.transparency end
if tex:IsA("Texture") then
if action.studsPerTileU then tex.StudsPerTileU = action.studsPerTileU end
if action.studsPerTileV then tex.StudsPerTileV = action.studsPerTileV end
end
tex.Parent = target
table.insert(_batchCreated, tex)
updateSceneState(action, true)
_lastClickInstance = tex
return true, "🎨 " .. texType .. " applied to " .. target.Name
elseif kind == "create_npc" then
local npcName = action.name or "NPC"
local pos = action.position
if not pos or type(pos) ~= "table" or #pos < 3 then
return false, "❌ create_npc requires position:[x,y,z]"
end
local npcColor = action.color or {180, 120, 80}
local ws = action.walkspeed or 16
local hp = action.health or 100
local scale = action.resize or 1
local npcFolder = Instance.new("Folder")
npcFolder.Name = npcName
npcFolder.Parent = resolveParent(action.parent, Workspace)
-- Create NPC parts (R6 model)
local torso = Instance.new("Part")
torso.Name = "Torso"; torso.Size = Vector3.new(2, 2, 1); torso.Position = Vector3.new(pos[1] or 0, (pos[2] or 3) + 1.5, pos[3] or 0)
torso.Anchored = false; torso.CanCollide = true; torso.BrickColor = BrickColor.new(Color3.fromRGB(npcColor[1] or 180, npcColor[2] or 120, npcColor[3] or 80))
torso.Parent = npcFolder
local head = Instance.new("Part")
head.Name = "Head"; head.Size = Vector3.new(1, 1, 1) * scale; head.Shape = Enum.PartType.Ball; head.Position = Vector3.new(pos[1] or 0, (pos[2] or 3) + 3, pos[3] or 0)
head.Anchored = false; head.CanCollide = true; head.BrickColor = BrickColor.new(Color3.fromRGB(255, 200, 150))
head.Parent = npcFolder
local larm = Instance.new("Part")
larm.Name = "Left Arm"; larm.Size = Vector3.new(1, 2, 1); larm.Position = Vector3.new((pos[1] or 0) - 1.5, (pos[2] or 3) + 1.5, pos[3] or 0)
larm.Anchored = false; larm.CanCollide = true; larm.BrickColor = torso.BrickColor; larm.Parent = npcFolder
local rarm = Instance.new("Part")
rarm.Name = "Right Arm"; rarm.Size = Vector3.new(1, 2, 1); rarm.Position = Vector3.new((pos[1] or 0) + 1.5, (pos[2] or 3) + 1.5, pos[3] or 0)
rarm.Anchored = false; rarm.CanCollide = true; rarm.BrickColor = torso.BrickColor; rarm.Parent = npcFolder
local lleg = Instance.new("Part")
lleg.Name = "Left Leg"; lleg.Size = Vector3.new(1, 2, 1); lleg.Position = Vector3.new((pos[1] or 0) - 0.5, (pos[2] or 3) - 0.5, pos[3] or 0)
lleg.Anchored = false; lleg.CanCollide = true; lleg.BrickColor = BrickColor.new(Color3.fromRGB(30, 30, 120)); lleg.Parent = npcFolder
local rleg = Instance.new("Part")
rleg.Name = "Right Leg"; rleg.Size = Vector3.new(1, 2, 1); rleg.Position = Vector3.new((pos[1] or 0) + 0.5, (pos[2] or 3) - 0.5, pos[3] or 0)
rleg.Anchored = false; rleg.CanCollide = true; rleg.BrickColor = lleg.BrickColor; rleg.Parent = npcFolder
-- Weld joints
local function weld(p0, p1, name)
local w = Instance.new("Weld"); w.Name = name; w.Part0 = p0; w.Part1 = p1
w.C0 = p0.CFrame:inverse() * p1.CFrame; w.Parent = p0
end
weld(torso, head, "Neck"); weld(torso, larm, "Left Shoulder"); weld(torso, rarm, "Right Shoulder")
weld(torso, lleg, "Left Hip"); weld(torso, rleg, "Right Hip")
-- Humanoid
local hum = Instance.new("Humanoid")
hum.Name = "Humanoid"; hum.MaxHealth = hp; hum.Health = hp; hum.WalkSpeed = ws; hum.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
hum.Parent = npcFolder
-- HumanoidRootPart
local root = Instance.new("Part")
root.Name = "HumanoidRootPart"; root.Size = Vector3.new(2, 2, 1); root.Shape = Enum.PartType.Cylinder
root.Position = Vector3.new(pos[1] or 0, (pos[2] or 3) + 0.5, pos[3] or 0)
root.Anchored = true; root.Transparency = 1; root.CanCollide = false; root.Parent = npcFolder
weld(torso, root, "RootJoint")
-- Head Billboard (name tag)
local bb = Instance.new("BillboardGui")
bb.Name = "NameTag"; bb.Size = UDim2.new(0, 60, 0, 20); bb.StudsOffset = Vector3.new(0, 2.5, 0); bb.AlwaysOnTop = true; bb.Parent = head
local nl = Instance.new("TextLabel")
nl.Size = UDim2.new(1, 0, 1, 0); nl.BackgroundTransparency = 1; nl.Text = npcName; nl.TextColor3 = Color3.new(1, 1, 1)
nl.TextSize = 14; nl.Font = Enum.Font.GothamBold; nl.TextStrokeTransparency = 0.5; nl.Parent = bb
table.insert(_batchCreated, npcFolder)
updateSceneState(action, true)
_lastClickInstance = npcFolder
return true, "🧑🦱 NPC created: " .. npcName .. " at " .. tostring(pos[1]) .. "," .. tostring(pos[2]) .. "," .. tostring(pos[3])
elseif kind == "load_model" then
local assetId = action.assetId
if not assetId or assetId == "" then return false, "❌ load_model requires 'assetId' (rbxassetid://...)" end
local InsertService = game:GetService("InsertService")
local parent = resolveParent(action.parent, Workspace)
local modelName = action.name or "LoadedModel"
local pos = action.position
local loaded = nil
local ok, result = pcall(function()
local id = assetId:match("rbxassetid://(%d+)") or assetId:match("(%d+)")
if id then
local m = InsertService:LoadAsset(tonumber(id))
if m then loaded = m end
end
end)
if not ok or not loaded then
-- Fallback: create a placeholder
local fb = Instance.new("Part")
fb.Name = modelName .. "_Placeholder"
fb.Size = Vector3.new(4, 4, 4)
fb.Anchored = true; fb.CanCollide = true
fb.Color = Color3.fromRGB(200, 100, 100)
if pos then fb.Position = Vector3.new(pos[1] or 0, pos[2] or 5, pos[3] or 0) end
fb.Parent = parent
table.insert(_batchCreated, fb)
_lastClickInstance = fb
updateSceneState(action, true)
return true, "⚠️ Model " .. assetId .. " couldn't load (not found or restricted). Created placeholder part instead."
end
loaded.Name = modelName
if loaded.Parent then loaded.Parent = nil end
loaded.Parent = parent
if pos then
local rootPart = loaded:FindFirstChild("HumanoidRootPart") or loaded:FindFirstChildWhichIsA("BasePart")
if rootPart then rootPart.CFrame = CFrame.new(Vector3.new(pos[1] or 0, pos[2] or 5, pos[3] or 0)) end
end
if action.makeTool then
loaded:FindFirstChildWhichIsA("Part").Anchored = false
end
table.insert(_batchCreated, loaded)
updateSceneState(action, true)
_lastClickInstance = loaded
return true, "📦 Model loaded: " .. modelName .. " (" .. assetId .. ")"
else
return false, "❌ Unknown action type: " .. tostring(kind)
end
end
local SYSTEM_PROMPT = [[
You are Skill-AI v1.3 — Roblox master builder. Build ANY game: tower defense, FPS, RPG, obby, racing, survival, tycoon, adventure, battle royale. NPCs, animations, textures, GUIs, plugins, guns, HP, save/load, multiplayer, leaderboards, shops, pets, waves, upgrades, boss fights — all in ONE response. Output ONLY valid JSON.
=== RULES ===
- ONLY valid JSON. No text/markdown/backticks. No reasoning. Output JSON immediately.
- NO Lua in JSON: use [255,0,0] NOT Color3.fromRGB/Color3.new. Use [5,5,5] NOT Vector3.new. Use [0.5,0,0.2,0] NOT UDim2.new.
- "className":"Part" NOT "Block"/"Ball"/"Cylinder" — use "Shape":"Box" for shape.
- Prefix [EASY]=simple(1-3 parts,minimal scripts) [NORMAL]=moderate [HARD]=full game with complete working Lua code, no placeholders, no TODOs.
- Format: {"actions":[...],"reply":"message"}. All actions in one array, executed sequentially.
=== ACTIONS (use these) ===
1. create_instance: {type,className,name,parent,containerName?,properties?,handleProperties?}
2. create_compound: {type,className:"Model",name,parent,parts:[{className,size,position,color,material,shape?}],makeTool?}
3. create_script: {type,className:"Script"|"LocalScript",name,parent,source}
4. create_multiple: {type,className,count,containerName,parent,properties?,rejectionRadius?}
5. create_enclosure: {type,name,parent,containerName?,centerX?,centerY?,centerZ?,width?,depth?,wallHeight?,floor?,openSides?,color?,material?,barrier?}
6. create_challenge_system: {type,name,parent,containerName?,challengeType,targetCount,title?,centerX?,centerY?,centerZ?,width?,depth?,height?,visibleFloor?,walls?,wallHeight?,wallColor?,wallMaterial?,guiPosition?,guiSize?}
7-14: set_property, delete, clone, rename, move, set_lighting, set_character, undo
15. create_plugin: {type,name,modules:[{name,source}],icon?,buttonColor?,parent?}
16. create_particle: {type,target,texture?,rate?,speed?,lifetime?,size?,color?,transparency?,rotation?,spread?,enabled?}
17. create_light: {type,lightType:"PointLight"|"SpotLight"|"SurfaceLight",target,color?,brightness?,range?,angle?,enabled?}
18. create_sound: {type,soundId:"rbxassetid://...",target?,parent?,name?,volume?,pitch?,loops?,startPaused?}
19. create_tween: {type,target,properties:{...},duration?,style?,direction?,repeatCount?,reverses?}
20. create_animation: {type,animationType:"keyframe"|"asset",target?,humanoid?,parent?,name?,assetId?,frames?,loop?,speed?}
21. create_texture: {type,textureType:"Decal"|"Texture",target,textureId:"rbxassetid://...",face?,color?,transparency?,studsPerTileU?,studsPerTileV?}
22. create_npc: {type,name,position,color?,walkspeed?,health?,resize?,parent?}
23. load_model: {type,assetId:"rbxassetid://...",name?,parent?,position?,makeTool?}
=== BUILD RULES ===
- PARENTING: GUI→StarterGui, Parts→Workspace, Scripts→ServerStorage/Lighting, LocalScripts→StarterPlayerScripts
- NEVER parent GUI to Workspace. NEVER parent Parts to StarterGui/StarterPlayerScripts. NEVER parent Scripts to Workspace.
- When a script controls a GUI, parent the script INSIDE the ScreenGui or to StarterPlayerScripts, NOT Workspace.
- Textures/Decals: use `target` (the part), NOT `parent`. Decal.Parent = the part, not Workspace.
- Parents: Workspace,StarterGui,StarterPlayerScripts,StarterCharacterScripts,Lighting,ServerStorage,ReplicatedStorage,CoreGui,StarterPack
- Parts: Block,Ball,Cylinder,Wedge,CornerWedge. Materials: Plastic,Metal,Wood,Glass,Neon,Granite,Marble,Cement.
- ANCHOR static=true. Only Tools=false. CanCollide=true auto. Stack Y+5 per level.
- Colors: RGB [255,0,0] or BrickColor string. Sizes: doors 4x7x1, platforms 10x0.5x10, walls 0.5x8x6.
- GUI circles: Frame+UICorner(CornerRadius=UDim.new(1,0)).
- Scripts: NEVER "workspace.X" — use :WaitForChild(). nil-guard. NEVER SourceBackup. Parts FIRST, scripts SECOND. Respawn-safe (auto re-run).
- Reply: MUST include full script source code. Numbered plan for complex builds.
- Game types: TD=path+spawn+towers+waves+currency+upgrades. FPS=guns+damage+HP+ammo+respawn. Sim=zone+collect+currency+shop+save. Race=track+vehicle+checkpoint+timer+leaderboard.
- Enclosure ex: {"type":"create_enclosure","name":"Platform","parent":"Workspace","centerX":0,"centerY":8,"centerZ":0,"width":10,"depth":10,"wallHeight":6}
- Challenge ex: {"type":"create_challenge_system","name":"KillChallenge","parent":"Workspace","challengeType":"kill","targetCount":10,"centerX":0,"centerY":0,"centerZ":0,"width":30,"depth":30,"height":8}
- HARD mode: complete working Lua in every script — every function, event, line. No placeholders or TODOs.
]]
local REASONING_PATTERNS = {
"^Let me%s", "^I['']?ll%s", "^I think%s", "^First,?%s",
"^The user%s", "^Okay,?%s", "^So,?%s", "^Alright,?%s",
"^Here['']?s%s", "^This is%s", "^I need%s", "^I should%s",
"^I will%s", "^We need%s", "^We should%s", "^We can%s",
}
local function stripReasoning(text)
local lines = {}
for line in text:gmatch("[^\n]+") do
local stripped = line
for _, pat in ipairs(REASONING_PATTERNS) do
stripped = stripped:gsub(pat, "")
end
if #stripped > 0 then
table.insert(lines, stripped)
end
end
return table.concat(lines, "\n")
end
-- Fix Lua expressions inside JSON (e.g. Vector3.new(1,2,3) → [1,2,3])
local function fixLuaInJson(str)
-- Color3.fromRGB(255,0,0) → [255,0,0]
str = str:gsub("Color3%.fromRGB%s*%((%d+%s*,%s*%d+%s*,%s*%d+)%)", function(inner)
return "[" .. inner .. "]"
end)
-- Color3.new(0.5,0,0) → [128,0,0]
str = str:gsub("Color3%.new%s*%(([%d%.]+%s*,%s*[%d%.]+%s*,%s*[%d%.]+)%)", function(inner)
local r, g, b = inner:match("([%d%.]+)%s*,%s*([%d%.]+)%s*,%s*([%d%.]+)")
if r and g and b then
r = math.floor((tonumber(r) or 0) * 255)
g = math.floor((tonumber(g) or 0) * 255)
b = math.floor((tonumber(b) or 0) * 255)
return "[" .. r .. "," .. g .. "," .. b .. "]"
end
return "[" .. inner .. "]"
end)
-- Vector3.new(1,2,3) or Vector3.new(-1,2,0) → [1,2,3]
str = str:gsub("Vector[23]%.new%s*%(([%d%.%-]+%s*,%s*[%d%.%-]+%s*,%s*[%d%.%-]+)%)", function(inner)
return "[" .. inner .. "]"
end)
-- UDim2.new(0.5,0,0.2,0) → [0.5,0,0.2,0]
str = str:gsub("UDim2%.new%s*%(([%d%.%-]+%s*,%s*[%d%.%-]+%s*,%s*[%d%.%-]+%s*,%s*[%d%.%-]+)%)", function(inner)
return "[" .. inner .. "]"
end)
-- NumberRange.new(1,10) → [1,10]
str = str:gsub("NumberRange%.new%s*%(([%d%.]+%s*,%s*[%d%.]+)%)", function(inner)
return "[" .. inner .. "]"
end)
-- NumberSequence.new(1, 0.5) → {"__NumberSequence":[1,0.5]}
str = str:gsub("NumberSequence%.new%s*%(([%d%.]+%s*,%s*[%d%.]+)%)", function(inner)
return '{"__NumberSequence":[' .. inner .. ']}'
end)
-- BrickColor.new("Bright red") → "Bright red"
str = str:gsub('BrickColor%.new%s*%("[^"]+")', function(inner)
return inner
end)
-- Normalize class names
str = str:gsub('"className"%s*:%s*"Block"', '"className":"Part"')
str = str:gsub('"className"%s*:%s*"Ball"', '"className":"Part"')
str = str:gsub('"className"%s*:%s*"Cylinder"', '"className":"Part"')
-- Normalize "shape" to "Shape"
str = str:gsub('"shape"%s*:%s*"Box"', '"Shape":"Box"')
str = str:gsub('"shape"%s*:%s*"Block"', '"Shape":"Block"')
str = str:gsub('"shape"%s*:%s*"Ball"', '"Shape":"Ball"')
return str
end
local function tryExtractJson(text)
if type(text) ~= "string" or text == "" then return nil end
local cleaned = stripReasoning(text)
cleaned = cleaned:gsub("```json", ""):gsub("```lua", ""):gsub("```", ""):gsub("^%s+", ""):gsub("%s+$", "")
cleaned = cleaned:gsub("^[%s\n\r]+", ""):gsub("[%s\n\r]+$", "")
-- strip invisible / control chars (except \t \n \r) that break JSONDecode
cleaned = cleaned:gsub("[%c]", function(c)
if c == "\t" or c == "\n" or c == "\r" then return c end
return ""
end)
-- Convert Lua expressions inside JSON to proper JSON
cleaned = fixLuaInJson(cleaned)
-- If no "actions" wrapper but has a single action object, wrap it
if not cleaned:find('"actions"', 1, true) and cleaned:match('"type"%s*:') then
cleaned = '{"actions":[' .. cleaned .. '],"reply":"Built."}'
end
local function tryDecode(str)
local ok, res = pcall(function() return HttpService:JSONDecode(str) end)
if ok and type(res) == "table" then return res end
return nil
end
-- try direct
local plan = tryDecode(cleaned)
if plan then return plan end
-- try extracting from known start patterns
for _, prefix in ipairs({'{"actions"', '{"steps"', '{"reply"', '{"type"'}) do
local s = cleaned:find(prefix, 1, true)
if s then
local e = cleaned:reverse():find("}")
if e then
local realEnd = #cleaned - e + 1
if realEnd > s then
local sub = cleaned:sub(s, realEnd)
plan = tryDecode(sub)
if plan then return plan end
end
end
end
end
-- first { to last }
local s = cleaned:find("{")
local e = cleaned:reverse():find("}")
if s and e then
local realEnd = #cleaned - e + 1
if realEnd > s then
local sub = cleaned:sub(s, realEnd)
plan = tryDecode(sub)
if plan then return plan end
-- maybe truncated — try adding closing braces
local extras = { "}", "}]}", "}]}}", "} }" }
for _, extra in ipairs(extras) do
local fixed = sub .. extra
plan = tryDecode(fixed)
if plan then return plan end
end
end
end
return nil
end
local function buildSceneTree()
local lines = {}
local limit = 120
local count = 0
-- tracked containers from this session
local hasContainers = false
for key, ss in pairs(sceneState) do
local names = {}
if ss.createdNames then
for n, _ in pairs(ss.createdNames) do
table.insert(names, n)
end
end
if #names > 0 then
table.insert(lines, " " .. key .. " (" .. (ss.className or "?") .. ", " .. ss.count .. " total): " .. table.concat(names, ", "))
hasContainers = true
end
end
if hasContainers then
table.insert(lines, 1, "--- this session ---")
table.insert(lines, 2, "")
end
-- live scene tree
local servicesToScan = {
Workspace = Workspace,
StarterPack = StarterPack,
Lighting = Lighting,
ServerStorage = ServerStorage,
ReplicatedStorage = ReplicatedStorage,
StarterGui = StarterGui,
StarterCharacterScripts = StarterCharacterScripts,
StarterPlayerScripts = StarterPlayerScripts,
}
local function shortClass(cn)
local map = { Part = "Part", MeshPart = "Mesh", Model = "Model", Folder = "Folder",
Script = "Script", LocalScript = "LScript", StringValue = "StrVal",
ScreenGui = "Gui", Frame = "Frame", TextLabel = "Label", TextButton = "Btn",
ImageLabel = "Img", ScrollingFrame = "ScrFrame", ViewportFrame = "VpFrame" }
return map[cn] or cn
end
for svcName, svc in pairs(servicesToScan) do
if svc and type(svc) == "userdata" then
local children = svc:GetChildren()
if #children > 0 then
table.insert(lines, "")
table.insert(lines, svcName .. ":")
for i, child in ipairs(children) do
if count >= limit then break end
local stem = (i == #children) and " " or "│ "
local fork = (i == #children) and "└── " or "├── "
local loc = child:IsA("BasePart") and string.format(" @%.0f,%.0f,%.0f", child.CFrame.X, child.CFrame.Y, child.CFrame.Z) or ""
table.insert(lines, fork .. child.Name .. loc .. " (" .. shortClass(child.ClassName) .. ")")
count += 1
if (child:IsA("Model") or child:IsA("Folder")) and #child:GetChildren() > 0 then
local gcs = child:GetChildren()
for j, gc in ipairs(gcs) do
if count >= limit then break end
local gfork = (j == #gcs) and "└── " or "├── "
local gloc = gc:IsA("BasePart") and string.format(" @%.0f,%.0f,%.0f", gc.CFrame.X, gc.CFrame.Y, gc.CFrame.Z) or ""
table.insert(lines, stem .. gfork .. gc.Name .. gloc .. " (" .. shortClass(gc.ClassName) .. ")")
count += 1
end
end
end
end
end
end
if #lines == 0 then return "" end
return "=== SCENE ===\n" .. table.concat(lines, "\n") .. "\n=== END SCENE ==="
end
local _lastValidationErrors = nil
local VALIDATION_SKIP_REFS = {
humanoid = true, handle = true, torso = true, head = true,
["left arm"] = true, ["right arm"] = true, ["left leg"] = true, ["right leg"] = true,
playergui = true, backpack = true, leaderstats = true, configuration = true,
uppertorso = true, lowertorso = true, lefthand = true, righthand = true,
leftfoot = true, rightfoot = true,
}
local function validateActions(actions)
local errors = {}
local createdNames = {}
for i, action in ipairs(actions) do
local parent = action.parent
if (action.type == "create_instance" or action.type == "create_compound") and action.name then
createdNames[action.name:lower()] = true
end
-- Size validation: skip for GUI objects (they use UDim2 = 4-element [sX,oX,sY,oY])
if action.type == "create_instance" and type(action.properties) == "table" then
local guiClasses = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
ImageLabel = true, ScrollingFrame = true, BillboardGui = true }
local isGUI = guiClasses[action.className]
local sz = action.properties.Size
if type(sz) == "table" and isGUI then
if #sz ~= 4 or type(sz[1]) ~= "number" or type(sz[2]) ~= "number" or type(sz[3]) ~= "number" or type(sz[4]) ~= "number" then
table.insert(errors, ("Action %d: '%s' has invalid UDim2 Size %s (needs 4 numbers: scaleX, offsetX, scaleY, offsetY)."):format(
i, action.name or "?", HttpService:JSONEncode(sz)
))
end
elseif type(sz) == "table" and not isGUI then
if not ((sz[1] or 0) > 0 and (sz[2] or 0) > 0 and (sz[3] or 0) > 0) then
table.insert(errors, ("Action %d: '%s' has invalid Size %s (must be positive numbers on all 3 axes)."):format(
i, action.name or "?", HttpService:JSONEncode(sz)
))
end
end
-- موقع متطرف بشكل غير منطقي (احتمال خطأ حسابي بالـ AI، مو طلب فعلي)
local pos = action.properties.Position
if type(pos) == "table" then
for axisIdx = 1, 3 do
local v = pos[axisIdx]
if type(v) == "number" and math.abs(v) > 100000 then
table.insert(errors, ("Action %d: '%s' has extreme Position value (%d) on axis %d. Likely a calculation error — keep coordinates reasonable (e.g. within ±2000)."):format(
i, action.name or "?", v, axisIdx
))
end
end
end
end
-- نفس فحص الحجم لأجزاء create_compound (كل part بالمصفوفة)
if action.type == "create_compound" and type(action.parts) == "table" then
for partIdx, partData in ipairs(action.parts) do
if type(partData) == "table" and type(partData.size) == "table" then
local sz = partData.size
if not ((sz[1] or 0) > 0 and (sz[2] or 0) > 0 and (sz[3] or 0) > 0) then
table.insert(errors, ("Action %d (compound '%s', part %d): invalid size %s (must be positive on all 3 axes)."):format(
i, action.name or "?", partIdx, HttpService:JSONEncode(sz)
))
end
end
end
end
-- GUI objects in Workspace
if action.type == "create_instance" and parent == "Workspace" then
local guiClasses = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
ImageLabel = true, ScrollingFrame = true, BillboardGui = true }
if guiClasses[action.className] then
table.insert(errors, ("Action %d: GUI '%s' parented to Workspace. Use StarterGui instead."):format(i, action.name or "?"))
end
end
-- Parts in GUI services
if action.type == "create_instance" and parent and action.className then
local isPart = action.className == "Part" or action.className == "MeshPart" or action.className == "WedgePart" or action.className == "CylinderPart" or action.className == "BallPart"
local isGuiSvc = parent == "StarterGui" or parent == "StarterPlayerScripts" or parent == "StarterCharacterScripts"
if isPart and isGuiSvc then
table.insert(errors, ("Action %d: Part '%s' parented to %s. Use Workspace instead."):format(i, action.name or "?", parent))
end
end
-- Scripts/values can't live in Workspace (not a container for code)
if action.type == "create_script" and (parent == "Workspace") then
table.insert(errors, ("Action %d (script '%s'): parented to Workspace. Use ServerScriptService or StarterPlayerScripts instead."):format(
i, action.name or "?"
))
end
-- Script references a part not yet created
if action.type == "create_script" and action.source then
for ref in action.source:gmatch(':WaitForChild%("([^"]+)"%)') do
local refLower = ref:lower()
if not VALIDATION_SKIP_REFS[refLower] and not createdNames[refLower] then
local exists = findByName(ref)
if not exists then
table.insert(errors, ("Action %d (script '%s'): references '%s' which doesn't exist. Create it in a PRIOR action."):format(
i, action.name or "?", ref
))
end
end
end
for ref in action.source:gmatch(':FindFirstChild%("([^"]+)"%)') do
local refLower = ref:lower()
if not VALIDATION_SKIP_REFS[refLower] and not createdNames[refLower] then
local exists = findByName(ref)
if not exists then
table.insert(errors, ("Action %d (script '%s'): references FindFirstChild('%s') which doesn't exist. Create it first."):format(
i, action.name or "?", ref
))
end
end
end
end
-- create_challenge_system validation
if action.type == "create_challenge_system" then
local validTypes = { reach = true, survive = true, kill = true, collect = true }
local ct = action.challengeType
if ct and not validTypes[ct:lower()] then
table.insert(errors, ("Action %d (challenge '%s'): invalid challengeType '%s'. Valid: reach, survive, kill, collect"):format(
i, action.name or "?", ct
))
end
local tc = tonumber(action.targetCount)
if tc and (tc < 1 or tc > 1000) then
table.insert(errors, ("Action %d (challenge '%s'): targetCount must be 1-1000, got %s"):format(
i, action.name or "?", tostring(tc)
))
end
local w = tonumber(action.width)
local d = tonumber(action.depth)
local h = tonumber(action.height)
if w and (w < 1 or w > 500) then
table.insert(errors, ("Action %d (challenge '%s'): width must be 1-500, got %s"):format(i, action.name or "?", tostring(w)))
end
if d and (d < 1 or d > 500) then
table.insert(errors, ("Action %d (challenge '%s'): depth must be 1-500, got %s"):format(i, action.name or "?", tostring(d)))
end
if h and (h < 1 or h > 200) then
table.insert(errors, ("Action %d (challenge '%s'): height must be 1-200, got %s"):format(i, action.name or "?", tostring(h)))
end
end
-- deeper script validation: detect workspace.X dot-access (forbidden by prompt rules)
if action.type == "create_script" and action.source then
local src = action.source
-- pattern: workspace.Something (dot-access)
for matchPos, _ in src:gmatch("()workspace%.%a") do
local contextStart = math.max(1, matchPos - 15)
local contextEnd = math.min(#src, matchPos + 25)
local snippet = src:sub(contextStart, contextEnd):gsub("\n", "\\n")
table.insert(errors, ("Action %d (script '%s'): uses 'workspace.X' dot-access at pos %d (\"...%s...\"). Use script.Parent:WaitForChild('X') instead."):format(
i, action.name or "?", matchPos, snippet
))
break
end
-- pattern: game.Workspace.Something
for matchPos, _ in src:gmatch("()game%.Workspace%.%a") do
local contextStart = math.max(1, matchPos - 15)
local contextEnd = math.min(#src, matchPos + 30)
local snippet = src:sub(contextStart, contextEnd):gsub("\n", "\\n")
table.insert(errors, ("Action %d (script '%s'): uses 'game.Workspace.X' dot-access at pos %d (\"...%s...\"). Use script.Parent:WaitForChild('X') instead."):format(
i, action.name or "?", matchPos, snippet
))
break
end
end
end
-- cross-action ordering check: scripts referencing parts must come after those parts are created
do
local createdByAction = {}
for i, action in ipairs(actions) do
if (action.type == "create_instance" or action.type == "create_compound") and action.name then
createdByAction[action.name:lower()] = i
end
if action.type == "create_enclosure" and action.name then
createdByAction[action.name:lower()] = i
end
if action.type == "create_challenge_system" and action.name then
createdByAction[action.name:lower()] = i
end
if action.type == "create_multiple" and action.namePrefix then
createdByAction[action.namePrefix:lower()] = i
end
end
for i, action in ipairs(actions) do
if action.type == "create_script" and action.source then
for ref in action.source:gmatch(':WaitForChild%("([^"]+)"%)') do
local refLower = ref:lower()
if not VALIDATION_SKIP_REFS[refLower] and createdByAction[refLower] and createdByAction[refLower] > i then
table.insert(errors, ("Action %d (script '%s'): references part '%s' via WaitForChild, but that part is created LATER in action %d. Parts must be created BEFORE scripts that reference them."):format(
i, action.name or "?", ref, createdByAction[refLower]
))
end
end
for ref in action.source:gmatch(':FindFirstChild%("([^"]+)"%)') do
local refLower = ref:lower()
if not VALIDATION_SKIP_REFS[refLower] and createdByAction[refLower] and createdByAction[refLower] > i then
table.insert(errors, ("Action %d (script '%s'): references FindFirstChild('%s'), but that part is created LATER in action %d. Reorder: create first, then script."):format(
i, action.name or "?", ref, createdByAction[refLower]
))
end
end
end
end
end
-- ⚠️ فحص تنظيم: لو فيه أكثر من create_instance بدون containerName للقطع
-- اللي مش GUI أو Script (لأن GUI عنده ScreenGui اللي هو container أصلاً)
do
local guiClasses = { ScreenGui = true, Frame = true, TextLabel = true, TextButton = true,
ImageLabel = true, ScrollingFrame = true, BillboardGui = true, ViewportFrame = true }
local noContainerCount = 0
local containerNamesUsed = {}
for _, action in ipairs(actions) do
if action.type == "create_instance" and not guiClasses[action.className] and action.className ~= "Script" and action.className ~= "LocalScript" then
if not action.containerName or action.containerName == "" then
noContainerCount = noContainerCount + 1
else
containerNamesUsed[action.containerName] = (containerNamesUsed[action.containerName] or 0) + 1
end
end
end
local distinctContainers = 0
for _ in pairs(containerNamesUsed) do distinctContainers = distinctContainers + 1 end
if noContainerCount >= 3 then
table.insert(errors, ("You created %d separate parts via create_instance without any containerName. Per the ORGANIZATION RULE, give them all the SAME containerName so they group under one Folder instead of scattering loose parts in the Explorer."):format(noContainerCount))
elseif distinctContainers > 1 and noContainerCount == 0 then
table.insert(errors, "You used multiple different containerName values for parts that appear to belong to the same build. Use ONE consistent containerName for all parts of a single logical build.")
end
end
return errors
end
local function requestPlan(question, retryCount, accumUsage)
retryCount = retryCount or 0
accumUsage = accumUsage or { prompt_tokens = 0, completion_tokens = 0, total_tokens = 0 }
local maxRetries = 2
local messages = {}
if CURRENT_PROVIDER ~= "nvidia" then
table.insert(messages, { role = "system", content = SYSTEM_PROMPT })
end
-- Only send history and scene if they have content (saves tokens)
local hasScene = next(sceneState) ~= nil
if hasScene then
for _, histMsg in ipairs(buildHistoryMessages()) do
table.insert(messages, histMsg)
end
end
local sceneJson = ""
if hasScene then
sceneJson = buildSceneTree()
end
local enrichedQuestion = question
if sceneJson ~= "" then
enrichedQuestion = question .. "\n\n[current_scene]\n" .. sceneJson .. "\n[/current_scene]"
end
if CURRENT_PROVIDER == "nvidia" then
enrichedQuestion = "[System Instructions]\n" .. SYSTEM_PROMPT .. "\n\n[User Request]\n" .. enrichedQuestion
end
if retryCount > 0 then
local retryMsg = "⚠️ Your last response had issues."
if _lastValidationErrors then
retryMsg = retryMsg .. "\nFix these errors in your actions:\n- " .. table.concat(_lastValidationErrors, "\n- ")
_lastValidationErrors = nil
else
retryMsg = retryMsg .. " Send only valid JSON without extra text."
end
enrichedQuestion = enrichedQuestion .. "\n\n" .. retryMsg
end
table.insert(messages, { role = "user", content = enrichedQuestion })
local effort = classifyEffort(question)
local raw, err, usage = callAI(messages, nil, effort)
if usage and type(usage) == "table" then
accumUsage.prompt_tokens = accumUsage.prompt_tokens + (tonumber(usage.prompt_tokens) or 0)
accumUsage.completion_tokens = accumUsage.completion_tokens + (tonumber(usage.completion_tokens) or 0)
accumUsage.total_tokens = accumUsage.total_tokens + (tonumber(usage.total_tokens) or 0)
end
if not raw then
return nil, err, accumUsage
end
local plan = tryExtractJson(raw)
if not plan then
logToFile("Impure JSON response (request: " .. question .. ")", raw)
if retryCount < maxRetries then
return requestPlan(question, retryCount + 1, accumUsage)
end
return nil, "❌ Could not parse AI plan (not valid JSON). Raw response saved to: " .. LOG_FILE, accumUsage
end
if plan.actions and type(plan.actions) == "table" and retryCount < maxRetries then
local validationErrors = validateActions(plan.actions)
if #validationErrors > 0 then
logToFile("Validation errors (request: " .. question .. ")", table.concat(validationErrors, "\n"))
_lastValidationErrors = validationErrors
return requestPlan(question, retryCount + 1, accumUsage)
end
end
return plan, nil, accumUsage
end
local function addLog(msg, isErr)
if not displayArea or not displayArea.Parent then return end
local lbl = Instance.new("TextLabel")
lbl.Size = UDim2.new(1, -10, 0, 20)
lbl.BackgroundTransparency = 1
lbl.Text = msg
lbl.TextColor3 = isErr and C.red or C.yellow
lbl.TextSize = 10
lbl.Font = Enum.Font.Code
lbl.TextXAlignment = Enum.TextXAlignment.Left
lbl.Parent = displayArea
table.insert(_logLabels, lbl)
task.wait()
if displayArea and displayArea.Parent then
displayArea.CanvasPosition = Vector2.new(0, displayArea.CanvasSize.Y.Offset)
end
return lbl
end
local function _runRequest(question)
if not question or question == "" then
_processingRequest = false
return
end
_processingRequest = true
CURRENT_REQUEST_TEXT = question
_batchCreated = {}
_batchPlacedGrid = nil
task.spawn(function()
local loading = Instance.new("TextLabel")
local runOk, runErr = pcall(function()
loading.Size = UDim2.new(1, -10, 0, 30)
loading.BackgroundTransparency = 1
local modelShort = (CURRENT_MODEL_LABEL or "?"):match("^%s*(.-)%s*$")
if modelShort then modelShort = modelShort:sub(1,20) else modelShort = "?" end
loading.Text = "🧠 Planning [" .. modelShort .. "] ."
loading.TextColor3 = C.textDim
loading.TextSize = 11
loading.Font = Enum.Font.Gotham
loading.Parent = displayArea
-- Simple dot animation (no Heartbeat — saves CPU)
task.spawn(function()
local step = 0
while loading and loading.Parent do
step = step + 1
local dots = string.rep(".", (step % 3) + 1)
loading.Text = "🧠 Planning [" .. modelShort .. "] " .. dots
task.wait(0.4)
end
end)
local startTime = tick()
addLog("📡 Sending to " .. CURRENT_PROVIDER:upper() .. "...")
local plan, err, usage = requestPlan(question)
addLog("📦 Parsing response...")
local elapsed = tick() - startTime
local function showCostLine()
if usage and type(usage) == "table" and (usage.prompt_tokens or 0) + (usage.completion_tokens or 0) > 0 then
local pIn = usage.prompt_tokens or 0
local pOut = usage.completion_tokens or 0
-- للـ OpenCode: مجاني دائماً — السعر صفر
local cost = 0
local costStr
if CURRENT_PROVIDER == "nvidia" then
cost = (pIn / 1000000) * PRICE_PER_1M_INPUT + (pOut / 1000000) * PRICE_PER_1M_OUTPUT
costStr = ("💰 $%.6f"):format(cost)
else
costStr = "💚 Free"
end
addMessage(("⏱️ %.1fs | 🔤 %d in / %d out | %s"):format(elapsed, pIn, pOut, costStr), false, false, true)
else
addMessage(("⏱️ %.1fs | 🔤 token usage not reported by server"):format(elapsed), false, false, true)
end
end
if not plan then
addMessage(err or "❌ Unknown error", false, true)
pushHistory(question, "Failed to execute request (error: " .. tostring(err) .. ")")
showCostLine()
return
end
if plan.thought and type(plan.thought) == "string" and plan.thought ~= "" then
addMessage("🧠 " .. plan.thought, false, false, true)
end
if plan.reply and type(plan.reply) == "string" and plan.reply ~= "" then
addMessage("🤖 " .. plan.reply, false)
end
showCostLine()
local actions = plan.actions
if type(actions) ~= "table" or #actions == 0 then
return
end
local summaryParts = {}
local totalActions = #actions
-- FIX v1.4: لا delay أبداً للباتشات الكبيرة — نستخدم task.wait فقط للباتشات الصغيرة (بصرياً)
local visualDelay = totalActions > 20 and 0 or (totalActions > 8 and 0.01 or 0.04)
addLog("⚙️ Executing " .. #actions .. " action(s)...")
for actionIdx, action in ipairs(actions) do
if type(action) ~= "table" or not action.type then
addMessage("⚠️ Skipped invalid action from AI", false, false, true)
else
_lastClickInstance = nil
addLog(" [" .. actionIdx .. "/" .. totalActions .. "] " .. (action.type or "?") .. " → " .. (action.name or action.className or ""))
local successOk, didSucceed, resultMessage = pcall(executeAction, action)
local clickInst = _lastClickInstance
if successOk then
addMessage(resultMessage or "✅ Done", false, not didSucceed, true, clickInst)
if didSucceed then
local detail = tostring(action.type)
if action.className then detail = detail .. "(" .. tostring(action.className) .. ")" end
if action.name then detail = detail .. " name=" .. tostring(action.name) end
if action.namePrefix then detail = detail .. " prefix=" .. tostring(action.namePrefix) end
if action.containerName then detail = detail .. " container=" .. tostring(action.containerName) end
if action.count then detail = detail .. " count=" .. tostring(action.count) end
table.insert(summaryParts, detail)
end
else
addMessage("❌ Execution error: " .. tostring(didSucceed), false, true)
table.insert(summaryParts, tostring(action.type) .. " failed: " .. tostring(didSucceed))
end
if actionIdx < totalActions then
task.wait(visualDelay)
end
end
end
local summary
if #summaryParts > 0 then
summary = "Executed: " .. table.concat(summaryParts, " | ")
else
summary = "No actions succeeded for this request."
end
if #_batchCreated > 0 then
table.insert(undoStack, {
instances = _batchCreated,
summary = summary
})
while #undoStack > undoLimit do
table.remove(undoStack, 1)
end
end
pushHistory(question, summary)
end)
if not runOk then
addMessage("❌ " .. tostring(runErr), false, true)
end
pcall(function()
if loading and loading.Parent then
loading:Destroy()
end
end)
pcall(function()
for _, lbl in ipairs(_logLabels) do
if lbl and lbl.Parent then lbl:Destroy() end
end
_logLabels = {}
end)
if #_requestQueue > 0 then
local nextQ = table.remove(_requestQueue, 1)
task.spawn(function() _runRequest(nextQ) end)
else
_processingRequest = false
end
end)
end
local function sendQuestion()
local question = inputBox.Text
if question == "" then return end
if question == "/undo" or question == "undo" then
inputBox.Text = ""
if #undoStack == 0 then
addMessage("⚠️ Nothing to undo.", false, true)
return
end
local lastBatch = table.remove(undoStack)
local removed = 0
for _, entry in ipairs(lastBatch.instances) do
if type(entry) == "table" and entry._markedForDestroy then
addMessage("⚠️ Cannot restore: " .. tostring(entry._name) .. " (was already deleted)", false, false, true)
else
local inst = entry
if inst and inst.Parent then
inst:Destroy()
removed += 1
end
end
end
addMessage("↩️ Undone: " .. lastBatch.summary .. " (removed " .. removed .. " items)", false, false, true)
pushHistory("/undo", "Undid: " .. lastBatch.summary)
return
end
local findPrefix = "/find "
if question:lower():sub(1, #findPrefix) == findPrefix then
inputBox.Text = ""
local searchName = question:sub(#findPrefix + 1)
if searchName == "" then
addMessage("❌ Usage: /find Name", false, true)
return
end
local results = findAllByName(searchName)
if #results == 0 then
addMessage("❌ No instances found containing \"" .. searchName .. "\"", false, true)
return
end
local msg = "🔍 Found " .. #results .. " instance(s) for \"" .. searchName .. "\":"
for i, res in ipairs(results) do
msg = msg .. "\n " .. i .. ". " .. res.Name .. " (" .. (res.ClassName or "?") .. ") → " .. getInstancePath(res)
end
addMessage(msg, false, false, true)
pushHistory("/find " .. searchName, "Found " .. #results .. " instances")
return
end
if question:lower() == "/diag" then
inputBox.Text = ""
addMessage("🧑 " .. question, true)
-- Real diagnostic test: create a test LocalScript, parent it to Workspace,
-- write Source, wait, read it back, and compare — raw result, no embellishment.
local testSource = "--diag test " .. tostring(os.time())
local testOk, testInst = pcall(function() return Instance.new("LocalScript") end)
if not testOk or not testInst then
addMessage("❌ Instance.new(\"LocalScript\") itself failed: " .. tostring(testInst), false, true)
return
end
testInst.Name = "SkillAI_DiagTest"
local p1ok = pcall(function() testInst.Source = testSource end)
local r1ok, r1val = pcall(function() return testInst.Source end)
local beforeParent = "Before parenting to game: write_ok=" .. tostring(p1ok) .. " read_ok=" .. tostring(r1ok)
.. " match=" .. tostring(r1ok and r1val == testSource)
testInst.Parent = Workspace
task.wait()
local p2ok = pcall(function() testInst.Source = testSource end)
task.wait()
local r2ok, r2val = pcall(function() return testInst.Source end)
local afterParent = "After parenting to game: write_ok=" .. tostring(p2ok) .. " read_ok=" .. tostring(r2ok)
.. " match=" .. tostring(r2ok and r2val == testSource)
.. " length=" .. tostring(r2ok and r2val and #r2val or 0)
local writefileAvail = "writefile available: " .. tostring(writefile ~= nil)
local setclipboardAvail = "setclipboard available: " .. tostring(setclipboard ~= nil)
testInst:Destroy()
-- Additional check: verify StarterPlayerScripts/StarterCharacterScripts are
-- the correct types (not fake Folders with the same name — the real root cause
-- of "Not Found" even when Source writing succeeds).
local spsClass = StarterPlayerScripts and StarterPlayerScripts.ClassName or "nil"
local scsClass = StarterCharacterScripts and StarterCharacterScripts.ClassName or "nil"
local spsCheck = "StarterPlayerScripts type: " .. spsClass
.. (spsClass == "StarterPlayerScripts" and " ✅ correct" or " ❌ wrong! Expected StarterPlayerScripts, not " .. spsClass)
local scsCheck = "StarterCharacterScripts type: " .. scsClass
.. (scsClass == "StarterCharacterScripts" and " ✅ correct" or " ❌ wrong! Expected StarterCharacterScripts, not " .. scsClass)
-- Real end-to-end test: create an actual LocalScript inside StarterPlayerScripts and verify we find it
local spsTestMsg = "Skipped (StarterPlayerScripts unavailable)"
if StarterPlayerScripts then
local sp2ok, sp2inst = pcall(function() return Instance.new("LocalScript") end)
if sp2ok and sp2inst then
sp2inst.Name = "SkillAI_DiagTest2"
sp2inst.Parent = StarterPlayerScripts
pcall(function() sp2inst.Source = testSource end)
task.wait()
local foundBack = StarterPlayerScripts:FindFirstChild("SkillAI_DiagTest2")
spsTestMsg = "Script inside StarterPlayerScripts found afterward: " .. tostring(foundBack ~= nil)
if foundBack then foundBack:Destroy() end
end
end
local finalVerdict
if r2ok and r2val == testSource and spsClass == "StarterPlayerScripts" then
finalVerdict = "✅ Result: Source writing works, and StarterPlayerScripts is the correct type. If you still see issues, tell me exactly what happens."
elseif spsClass ~= "StarterPlayerScripts" then
finalVerdict = "❌ Result: Found the root cause! StarterPlayerScripts is NOT the correct type in this session — this explains 'Not Found'. Try /diag after restarting the script with this update."
else
finalVerdict = "❌ Result: This Executor definitely blocks Source writing after parenting (confirmed by actual test, not guesswork)."
end
addMessage("🔬 Diagnostic result:\n" .. beforeParent .. "\n" .. afterParent .. "\n" .. writefileAvail .. "\n" .. setclipboardAvail
.. "\n" .. spsCheck .. "\n" .. scsCheck .. "\n" .. spsTestMsg .. "\n\n" .. finalVerdict, false, false, true)
pushHistory("/diag", "Ran Source-write diagnostic")
return
end
if question:lower() == "/clear" then
conversationHistory = {}
addMessage("🧹 History cleared. AI starts fresh.", false)
return
end
-- FIX: أمر /export — يصدّر تاريخ المحادثة كملف نصي أو ينسخه للـ clipboard
if question:lower() == "/export" then
inputBox.Text = ""
if #conversationHistory == 0 then
addMessage("⚠️ No conversation history to export.", false, false, true)
return
end
local lines = { "=== Skill-AI v1.3 Conversation Export ===" }
local ts = os.date and os.date("%Y-%m-%d %H:%M") or tostring(os.time())
table.insert(lines, "Date: " .. ts)
table.insert(lines, "Model: " .. CURRENT_MODEL_LABEL)
table.insert(lines, "Turns: " .. #conversationHistory)
table.insert(lines, "")
for i, turn in ipairs(conversationHistory) do
table.insert(lines, ("[%d] User: %s"):format(i, turn.user or ""))
table.insert(lines, (" AI: %s"):format(turn.summary or ""))
table.insert(lines, "")
end
local exportStr = table.concat(lines, "\n")
local saved = false
-- محاولة حفظ ملف
if writefile then
local exportFile = "SkillAI_Export_" .. tostring(os.time()) .. ".txt"
pcall(function()
writefile(exportFile, exportStr)
addMessage("✅ Exported " .. #conversationHistory .. " turn(s) to: " .. exportFile, false, false, true)
saved = true
end)
end
-- نسخ للـ clipboard كبديل
if not saved and setclipboard then
local ok = pcall(function() setclipboard(exportStr) end)
if ok then
addMessage("📋 Copied " .. #conversationHistory .. " turn(s) to clipboard (no writefile available)", false, false, true)
saved = true
end
end
if not saved then
addMessage("⚠️ Export: writefile and setclipboard both unavailable in this executor.", false, false, true)
end
pushHistory("/export", "Exported " .. #conversationHistory .. " conversation turns")
return
end
if question:lower() == "/cleanup" or question:lower() == "/clearfiles" then
local cleaned = {}
if writefile and delfile then
pcall(function()
if isfile and isfile(LOG_FILE) then
delfile(LOG_FILE)
table.insert(cleaned, LOG_FILE)
end
end)
pcall(function()
local filesToDel = {}
if listfiles then
for _, f in ipairs(listfiles("/")) do
if type(f) == "string" and (f:find("SkillAI", 1, true) or f:find("skillai", 1, true)) then
table.insert(filesToDel, f)
end
end
end
for _, f in ipairs(filesToDel) do
pcall(function() delfile(f) end)
table.insert(cleaned, f)
end
end)
end
-- clean up test instances in workspace
local removedCount = 0
for _, obj in ipairs(Workspace:GetDescendants()) do
if obj.Name:find("SkillAI_Diag", 1, true) or obj.Name:find("SkillAI_Test", 1, true) then
pcall(function() obj:Destroy() end)
removedCount = removedCount + 1
end
end
local msg = "🧹 Cleanup done."
if #cleaned > 0 then
msg = msg .. " Deleted " .. #cleaned .. " file(s): " .. table.concat(cleaned, ", ")
else
msg = msg .. " No files to delete (writefile/delfile not available)."
end
if removedCount > 0 then
msg = msg .. " Destroyed " .. removedCount .. " leftover test instance(s)."
end
addMessage(msg, false, false, true)
return
end
inputBox.Text = ""
addMessage("🧑 " .. question, true)
CURRENT_REQUEST_TEXT = question
-- request queue: if already processing, add to queue
if _processingRequest then
table.insert(_requestQueue, question)
addMessage("⏳ Queued... (" .. #_requestQueue .. " waiting)", false, false, true)
return
end
_processingRequest = true
task.spawn(function() _runRequest(question) end)
end
sendBtn.MouseButton1Click:Connect(sendQuestion)
inputBox.FocusLost:Connect(function(enterPressed)
if enterPressed then sendQuestion() end
end)
task.spawn(function()
task.wait(1)
pcall(setupRespawnReRunner)
addMessage("🤖 Skill-AI v1.4\n📡 Model: " .. CURRENT_MODEL_LABEL .. "\n💡 /export لتصدير المحادثة | /clear | /undo | /find | /diag", false)
end)
print("[Skill-AI v1.4] Loaded | Timeout+Cache+SceneGC+FastRetry+HeightFix+BatchDelay fixes")



did u use ai
ممتاز
ai slop for ai *, gg