@adiholick/nvstudio-mcp 1.0.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-plugin.js +115 -0
- package/dist/index.js +50 -11
- package/dist/luau-validator.js +191 -0
- package/dist/roblox-bridge.js +27 -1
- package/dist/task-queue.js +4 -1
- package/package.json +5 -3
- package/studio-plugin/Main.server.lua +302 -0
- package/studio-plugin/create_instance.lua +24 -0
- package/studio-plugin/delete_instance.lua +31 -0
- package/studio-plugin/execute_luau.lua +71 -0
- package/studio-plugin/generate_terrain.lua +23 -0
- package/studio-plugin/get_children.lua +7 -0
- package/studio-plugin/get_logs.lua +7 -0
- package/studio-plugin/get_properties.lua +52 -0
- package/studio-plugin/get_script_source.lua +7 -0
- package/studio-plugin/insert_asset.lua +18 -0
- package/studio-plugin/nvstudio_mcp.rbxmx +1420 -0
- package/studio-plugin/patch_script_source.lua +97 -0
- package/studio-plugin/rollback_script.lua +13 -0
- package/studio-plugin/script_grep.lua +63 -0
- package/studio-plugin/search_instance.lua +20 -0
- package/studio-plugin/ui_manager.lua +443 -0
- package/studio-plugin/update_script_source.lua +16 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
local HttpService = game:GetService("HttpService")
|
|
2
|
+
|
|
3
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
4
|
+
if not targetInstance:IsA("LuaSourceContainer") then
|
|
5
|
+
return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
local payload = nil
|
|
9
|
+
pcall(function()
|
|
10
|
+
payload = HttpService:JSONDecode(data)
|
|
11
|
+
end)
|
|
12
|
+
|
|
13
|
+
if not payload then
|
|
14
|
+
return { status = "error", error = "Payload data harus berupa JSON valid." }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
local currentSource = targetInstance.Source
|
|
18
|
+
local newSource = currentSource
|
|
19
|
+
local affectedLines = 0
|
|
20
|
+
|
|
21
|
+
-- Mode 1: Search & Replace Exact Chunk
|
|
22
|
+
if payload.target_content and payload.replacement_content then
|
|
23
|
+
local targetContent = payload.target_content
|
|
24
|
+
local replacementContent = payload.replacement_content
|
|
25
|
+
local allowMultiple = payload.allow_multiple or false
|
|
26
|
+
|
|
27
|
+
-- Escape magic characters for string.gsub if needed, or use string.find manually.
|
|
28
|
+
-- string.gsub doesn't do plain text replacement directly without escaping.
|
|
29
|
+
-- We'll write a plain text replace function.
|
|
30
|
+
|
|
31
|
+
local function plainReplace(str, pattern, repl, max)
|
|
32
|
+
local res = ""
|
|
33
|
+
local startIdx = 1
|
|
34
|
+
local count = 0
|
|
35
|
+
while true do
|
|
36
|
+
if max and count >= max then break end
|
|
37
|
+
local findStart, findEnd = string.find(str, pattern, startIdx, true) -- true for plain match
|
|
38
|
+
if not findStart then break end
|
|
39
|
+
|
|
40
|
+
res = res .. string.sub(str, startIdx, findStart - 1) .. repl
|
|
41
|
+
startIdx = findEnd + 1
|
|
42
|
+
count = count + 1
|
|
43
|
+
end
|
|
44
|
+
res = res .. string.sub(str, startIdx)
|
|
45
|
+
return res, count
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
local _, occurrences = plainReplace(currentSource, targetContent, replacementContent)
|
|
49
|
+
|
|
50
|
+
if occurrences == 0 then
|
|
51
|
+
return { status = "error", error = "target_content tidak ditemukan di dalam script." }
|
|
52
|
+
elseif occurrences > 1 and not allowMultiple then
|
|
53
|
+
return { status = "error", error = "target_content ditemukan " .. occurrences .. " kali. Tolong sertakan baris konteks lebih banyak agar unik, atau set allow_multiple = true." }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
newSource, affectedLines = plainReplace(currentSource, targetContent, replacementContent)
|
|
57
|
+
affectedLines = affectedLines -- not exact line count, but occurrences count
|
|
58
|
+
|
|
59
|
+
-- Mode 2: Line Range Replace
|
|
60
|
+
elseif payload.start_line and payload.end_line and payload.replacement_content then
|
|
61
|
+
local lines = string.split(currentSource, "\n")
|
|
62
|
+
local startLine = tonumber(payload.start_line)
|
|
63
|
+
local endLine = tonumber(payload.end_line)
|
|
64
|
+
|
|
65
|
+
if not startLine or not endLine or startLine < 1 or endLine > #lines or startLine > endLine then
|
|
66
|
+
return { status = "error", error = "Range baris tidak valid. Jumlah baris script: " .. #lines }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
local newLines = {}
|
|
70
|
+
for i = 1, startLine - 1 do
|
|
71
|
+
table.insert(newLines, lines[i])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
table.insert(newLines, payload.replacement_content)
|
|
75
|
+
|
|
76
|
+
for i = endLine + 1, #lines do
|
|
77
|
+
table.insert(newLines, lines[i])
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
newSource = table.concat(newLines, "\n")
|
|
81
|
+
affectedLines = (endLine - startLine) + 1
|
|
82
|
+
else
|
|
83
|
+
return { status = "error", error = "Payload tidak lengkap. Butuh (target_content & replacement_content) atau (start_line, end_line & replacement_content)." }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
-- Simpan backup snapshot sebelum mengaplikasikan patch
|
|
87
|
+
ctx.scriptHistory[targetPath] = currentSource
|
|
88
|
+
|
|
89
|
+
-- Terapkan perubahan
|
|
90
|
+
targetInstance.Source = newSource
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
status = "success",
|
|
94
|
+
result = "Patch berhasil diterapkan pada '" .. targetInstance.Name .. "'. Perubahan diaplikasikan.",
|
|
95
|
+
affected_lines = affectedLines
|
|
96
|
+
}
|
|
97
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
if targetInstance:IsA("LuaSourceContainer") then
|
|
3
|
+
local prevSource = ctx.scriptHistory[targetPath]
|
|
4
|
+
if prevSource then
|
|
5
|
+
targetInstance.Source = prevSource
|
|
6
|
+
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil di-rollback ke versi sebelumnya." }
|
|
7
|
+
else
|
|
8
|
+
return { status = "error", error = "Tidak ada riwayat backup sebelumnya di sesi Studio ini untuk: " .. targetPath }
|
|
9
|
+
end
|
|
10
|
+
else
|
|
11
|
+
return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local query = tostring(data)
|
|
3
|
+
if not query or query == "" then
|
|
4
|
+
return { status = "error", error = "Query pencarian tidak boleh kosong." }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
local matches = {}
|
|
8
|
+
local matchCount = 0
|
|
9
|
+
local MAX_MATCHES = 50
|
|
10
|
+
|
|
11
|
+
local searchableServices = {
|
|
12
|
+
game:GetService("Workspace"),
|
|
13
|
+
game:GetService("ServerScriptService"),
|
|
14
|
+
game:GetService("ReplicatedStorage"),
|
|
15
|
+
game:GetService("StarterPlayer"),
|
|
16
|
+
game:GetService("StarterGui")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
local function searchInScript(scriptObj)
|
|
20
|
+
local success, source = pcall(function() return scriptObj.Source end)
|
|
21
|
+
if not success or not source then return end
|
|
22
|
+
|
|
23
|
+
local lines = string.split(source, "\n")
|
|
24
|
+
for i, line in ipairs(lines) do
|
|
25
|
+
-- Coba cari dengan plain text dulu, jika gagal coba pattern
|
|
26
|
+
local found = string.find(line, query, 1, true)
|
|
27
|
+
if not found then
|
|
28
|
+
pcall(function() found = string.find(line, query) end)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if found then
|
|
32
|
+
table.insert(matches, {
|
|
33
|
+
script = scriptObj:GetFullName(),
|
|
34
|
+
line = i,
|
|
35
|
+
content = string.match(line, "^%s*(.-)%s*$") or line
|
|
36
|
+
})
|
|
37
|
+
matchCount = matchCount + 1
|
|
38
|
+
if matchCount >= MAX_MATCHES then
|
|
39
|
+
return true
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
return false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
for _, service in ipairs(searchableServices) do
|
|
47
|
+
pcall(function()
|
|
48
|
+
local descendants = service:GetDescendants()
|
|
49
|
+
for _, desc in ipairs(descendants) do
|
|
50
|
+
if desc:IsA("LuaSourceContainer") then
|
|
51
|
+
if searchInScript(desc) then
|
|
52
|
+
break
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end)
|
|
57
|
+
if matchCount >= MAX_MATCHES then
|
|
58
|
+
break
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return { status = "success", matches = matches, count = matchCount, limitReached = (matchCount >= MAX_MATCHES) }
|
|
63
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
local searchName = tostring(data)
|
|
3
|
+
local results = {}
|
|
4
|
+
|
|
5
|
+
local searchableServices = {game:GetService("Workspace"), game:GetService("ReplicatedStorage"), game:GetService("ServerScriptService"), game:GetService("StarterGui")}
|
|
6
|
+
|
|
7
|
+
for _, service in ipairs(searchableServices) do
|
|
8
|
+
for _, desc in ipairs(service:GetDescendants()) do
|
|
9
|
+
if desc.Name == searchName then
|
|
10
|
+
table.insert(results, desc:GetFullName())
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if #results > 0 then
|
|
16
|
+
return { status = "success", result = results }
|
|
17
|
+
else
|
|
18
|
+
return { status = "success", result = "Tidak ditemukan instance dengan nama: " .. searchName }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
local UIManager = {}
|
|
2
|
+
local widget = nil
|
|
3
|
+
|
|
4
|
+
function UIManager.init(pluginInstance, studioId)
|
|
5
|
+
-- Membuat Widget Panel yang bisa di-dock (menempel)
|
|
6
|
+
local widgetInfo = DockWidgetPluginGuiInfo.new(
|
|
7
|
+
Enum.InitialDockState.Right,
|
|
8
|
+
false, -- Initially hidden until toolbar button is clicked
|
|
9
|
+
false, -- Override enabled
|
|
10
|
+
380, -- Default width
|
|
11
|
+
520, -- Default height
|
|
12
|
+
280, -- Min width
|
|
13
|
+
320 -- Min height
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
widget = pluginInstance:CreateDockWidgetPluginGui("NVStudioMCP_GUI_V3", widgetInfo)
|
|
17
|
+
widget.Title = "NVStudio MCP Dashboard"
|
|
18
|
+
|
|
19
|
+
-- ═══════════════════════════════════════════
|
|
20
|
+
-- ROOT FRAME (Non-scrolling, fills entire widget)
|
|
21
|
+
-- ═══════════════════════════════════════════
|
|
22
|
+
local rootFrame = Instance.new("Frame")
|
|
23
|
+
rootFrame.Size = UDim2.new(1, 0, 1, 0)
|
|
24
|
+
rootFrame.BackgroundColor3 = Color3.fromRGB(22, 22, 26)
|
|
25
|
+
rootFrame.BorderSizePixel = 0
|
|
26
|
+
rootFrame.Parent = widget
|
|
27
|
+
|
|
28
|
+
local rootPadding = Instance.new("UIPadding")
|
|
29
|
+
rootPadding.PaddingTop = UDim.new(0, 8)
|
|
30
|
+
rootPadding.PaddingLeft = UDim.new(0, 10)
|
|
31
|
+
rootPadding.PaddingRight = UDim.new(0, 10)
|
|
32
|
+
rootPadding.PaddingBottom = UDim.new(0, 8)
|
|
33
|
+
rootPadding.Parent = rootFrame
|
|
34
|
+
|
|
35
|
+
local rootLayout = Instance.new("UIListLayout")
|
|
36
|
+
rootLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
37
|
+
rootLayout.Padding = UDim.new(0, 6)
|
|
38
|
+
rootLayout.Parent = rootFrame
|
|
39
|
+
|
|
40
|
+
-- ═══════════════════════════════════════════
|
|
41
|
+
-- SECTION 1: HEADER CARD
|
|
42
|
+
-- ═══════════════════════════════════════════
|
|
43
|
+
local headerFrame = Instance.new("Frame")
|
|
44
|
+
headerFrame.AutomaticSize = Enum.AutomaticSize.Y
|
|
45
|
+
headerFrame.Size = UDim2.new(1, 0, 0, 0)
|
|
46
|
+
headerFrame.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
|
|
47
|
+
headerFrame.BorderSizePixel = 0
|
|
48
|
+
headerFrame.LayoutOrder = 1
|
|
49
|
+
headerFrame.Parent = rootFrame
|
|
50
|
+
|
|
51
|
+
local hCorner = Instance.new("UICorner")
|
|
52
|
+
hCorner.CornerRadius = UDim.new(0, 6)
|
|
53
|
+
hCorner.Parent = headerFrame
|
|
54
|
+
|
|
55
|
+
local hPadding = Instance.new("UIPadding")
|
|
56
|
+
hPadding.PaddingTop = UDim.new(0, 10)
|
|
57
|
+
hPadding.PaddingLeft = UDim.new(0, 12)
|
|
58
|
+
hPadding.PaddingRight = UDim.new(0, 12)
|
|
59
|
+
hPadding.PaddingBottom = UDim.new(0, 10)
|
|
60
|
+
hPadding.Parent = headerFrame
|
|
61
|
+
|
|
62
|
+
local hLayout = Instance.new("UIListLayout")
|
|
63
|
+
hLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
64
|
+
hLayout.Padding = UDim.new(0, 2)
|
|
65
|
+
hLayout.Parent = headerFrame
|
|
66
|
+
|
|
67
|
+
-- Title Row (Name + Status side by side)
|
|
68
|
+
local titleRow = Instance.new("Frame")
|
|
69
|
+
titleRow.AutomaticSize = Enum.AutomaticSize.Y
|
|
70
|
+
titleRow.Size = UDim2.new(1, 0, 0, 0)
|
|
71
|
+
titleRow.BackgroundTransparency = 1
|
|
72
|
+
titleRow.LayoutOrder = 1
|
|
73
|
+
titleRow.Parent = headerFrame
|
|
74
|
+
|
|
75
|
+
local titleLabel = Instance.new("TextLabel")
|
|
76
|
+
titleLabel.Text = "NVStudio MCP"
|
|
77
|
+
titleLabel.Font = Enum.Font.GothamBold
|
|
78
|
+
titleLabel.TextSize = 18
|
|
79
|
+
titleLabel.TextColor3 = Color3.fromRGB(240, 240, 245)
|
|
80
|
+
titleLabel.BackgroundTransparency = 1
|
|
81
|
+
titleLabel.Size = UDim2.new(0.6, 0, 0, 22)
|
|
82
|
+
titleLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
83
|
+
titleLabel.Parent = titleRow
|
|
84
|
+
|
|
85
|
+
local statusLabel = Instance.new("TextLabel")
|
|
86
|
+
statusLabel.Text = "● Disconnected"
|
|
87
|
+
statusLabel.Font = Enum.Font.GothamBold
|
|
88
|
+
statusLabel.TextSize = 12
|
|
89
|
+
statusLabel.TextColor3 = Color3.fromRGB(255, 100, 100)
|
|
90
|
+
statusLabel.BackgroundTransparency = 1
|
|
91
|
+
statusLabel.Size = UDim2.new(0.4, 0, 0, 22)
|
|
92
|
+
statusLabel.Position = UDim2.new(0.6, 0, 0, 0)
|
|
93
|
+
statusLabel.TextXAlignment = Enum.TextXAlignment.Right
|
|
94
|
+
statusLabel.Parent = titleRow
|
|
95
|
+
|
|
96
|
+
-- Version
|
|
97
|
+
local versionLabel = Instance.new("TextLabel")
|
|
98
|
+
versionLabel.Text = "v1.2.0 · Hybrid Agent"
|
|
99
|
+
versionLabel.Font = Enum.Font.Gotham
|
|
100
|
+
versionLabel.TextSize = 11
|
|
101
|
+
versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)
|
|
102
|
+
versionLabel.BackgroundTransparency = 1
|
|
103
|
+
versionLabel.Size = UDim2.new(1, 0, 0, 14)
|
|
104
|
+
versionLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
105
|
+
versionLabel.LayoutOrder = 2
|
|
106
|
+
versionLabel.Parent = headerFrame
|
|
107
|
+
|
|
108
|
+
-- Thin separator
|
|
109
|
+
local sep1 = Instance.new("Frame")
|
|
110
|
+
sep1.Size = UDim2.new(1, 0, 0, 1)
|
|
111
|
+
sep1.BackgroundColor3 = Color3.fromRGB(50, 50, 58)
|
|
112
|
+
sep1.BorderSizePixel = 0
|
|
113
|
+
sep1.LayoutOrder = 3
|
|
114
|
+
sep1.Parent = headerFrame
|
|
115
|
+
|
|
116
|
+
-- Place info
|
|
117
|
+
local placeLabel = Instance.new("TextLabel")
|
|
118
|
+
placeLabel.Text = "📁 " .. (game.Name ~= "" and game.Name or "Untitled Baseplate")
|
|
119
|
+
placeLabel.Font = Enum.Font.Gotham
|
|
120
|
+
placeLabel.TextSize = 11
|
|
121
|
+
placeLabel.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
122
|
+
placeLabel.BackgroundTransparency = 1
|
|
123
|
+
placeLabel.Size = UDim2.new(1, 0, 0, 16)
|
|
124
|
+
placeLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
125
|
+
placeLabel.LayoutOrder = 4
|
|
126
|
+
placeLabel.Parent = headerFrame
|
|
127
|
+
|
|
128
|
+
-- Studio ID
|
|
129
|
+
local idLabel = Instance.new("TextLabel")
|
|
130
|
+
idLabel.Text = "🔗 " .. string.sub(tostring(studioId), 1, 18) .. "..."
|
|
131
|
+
idLabel.Font = Enum.Font.Code
|
|
132
|
+
idLabel.TextSize = 10
|
|
133
|
+
idLabel.TextColor3 = Color3.fromRGB(90, 180, 230)
|
|
134
|
+
idLabel.BackgroundTransparency = 1
|
|
135
|
+
idLabel.Size = UDim2.new(1, 0, 0, 14)
|
|
136
|
+
idLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
137
|
+
idLabel.LayoutOrder = 5
|
|
138
|
+
idLabel.Parent = headerFrame
|
|
139
|
+
|
|
140
|
+
-- Server URL
|
|
141
|
+
local serverLabel = Instance.new("TextLabel")
|
|
142
|
+
serverLabel.Text = "🌐 localhost:3055"
|
|
143
|
+
serverLabel.Font = Enum.Font.Code
|
|
144
|
+
serverLabel.TextSize = 10
|
|
145
|
+
serverLabel.TextColor3 = Color3.fromRGB(140, 140, 150)
|
|
146
|
+
serverLabel.BackgroundTransparency = 1
|
|
147
|
+
serverLabel.Size = UDim2.new(1, 0, 0, 14)
|
|
148
|
+
serverLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
149
|
+
serverLabel.LayoutOrder = 6
|
|
150
|
+
serverLabel.Parent = headerFrame
|
|
151
|
+
|
|
152
|
+
-- ═══════════════════════════════════════════
|
|
153
|
+
-- SECTION 2: CONTROLS ROW
|
|
154
|
+
-- ═══════════════════════════════════════════
|
|
155
|
+
local controlsFrame = Instance.new("Frame")
|
|
156
|
+
controlsFrame.Size = UDim2.new(1, 0, 0, 32)
|
|
157
|
+
controlsFrame.BackgroundTransparency = 1
|
|
158
|
+
controlsFrame.LayoutOrder = 2
|
|
159
|
+
controlsFrame.Parent = rootFrame
|
|
160
|
+
|
|
161
|
+
-- Connect / Disconnect Button
|
|
162
|
+
local connectBtn = Instance.new("TextButton")
|
|
163
|
+
connectBtn.Size = UDim2.new(0.52, -4, 1, 0)
|
|
164
|
+
connectBtn.Position = UDim2.new(0, 0, 0, 0)
|
|
165
|
+
connectBtn.Text = " Connect"
|
|
166
|
+
connectBtn.Font = Enum.Font.GothamBold
|
|
167
|
+
connectBtn.TextSize = 13
|
|
168
|
+
connectBtn.TextColor3 = Color3.fromRGB(255, 255, 255)
|
|
169
|
+
connectBtn.BackgroundColor3 = Color3.fromRGB(40, 160, 80)
|
|
170
|
+
connectBtn.BorderSizePixel = 0
|
|
171
|
+
connectBtn.AutoButtonColor = false
|
|
172
|
+
connectBtn.ZIndex = 2
|
|
173
|
+
connectBtn.Parent = controlsFrame
|
|
174
|
+
|
|
175
|
+
local cCorner = Instance.new("UICorner")
|
|
176
|
+
cCorner.CornerRadius = UDim.new(0, 5)
|
|
177
|
+
cCorner.Parent = connectBtn
|
|
178
|
+
|
|
179
|
+
-- Hover Effects for Connect Button (menggunakan stored base color, bukan text check)
|
|
180
|
+
local _btnBaseColor = Color3.fromRGB(40, 160, 80)
|
|
181
|
+
local _isHovering = false
|
|
182
|
+
|
|
183
|
+
connectBtn.MouseEnter:Connect(function()
|
|
184
|
+
_isHovering = true
|
|
185
|
+
connectBtn.BackgroundColor3 = Color3.fromRGB(
|
|
186
|
+
math.min(255, math.floor(_btnBaseColor.R * 255 + 25)),
|
|
187
|
+
math.min(255, math.floor(_btnBaseColor.G * 255 + 25)),
|
|
188
|
+
math.min(255, math.floor(_btnBaseColor.B * 255 + 25))
|
|
189
|
+
)
|
|
190
|
+
end)
|
|
191
|
+
connectBtn.MouseLeave:Connect(function()
|
|
192
|
+
_isHovering = false
|
|
193
|
+
connectBtn.BackgroundColor3 = _btnBaseColor
|
|
194
|
+
end)
|
|
195
|
+
|
|
196
|
+
-- Auto-Connect Checkbox Container
|
|
197
|
+
local autoConnectContainer = Instance.new("Frame")
|
|
198
|
+
autoConnectContainer.Size = UDim2.new(0.48, -4, 1, 0)
|
|
199
|
+
autoConnectContainer.Position = UDim2.new(0.52, 4, 0, 0)
|
|
200
|
+
autoConnectContainer.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
|
|
201
|
+
autoConnectContainer.BorderSizePixel = 0
|
|
202
|
+
autoConnectContainer.Parent = controlsFrame
|
|
203
|
+
|
|
204
|
+
local acCorner = Instance.new("UICorner")
|
|
205
|
+
acCorner.CornerRadius = UDim.new(0, 5)
|
|
206
|
+
acCorner.Parent = autoConnectContainer
|
|
207
|
+
|
|
208
|
+
-- Checkbox visual (outer box)
|
|
209
|
+
local checkboxOuter = Instance.new("Frame")
|
|
210
|
+
checkboxOuter.Size = UDim2.new(0, 16, 0, 16)
|
|
211
|
+
checkboxOuter.Position = UDim2.new(0, 10, 0.5, -8)
|
|
212
|
+
checkboxOuter.BackgroundColor3 = Color3.fromRGB(55, 55, 65)
|
|
213
|
+
checkboxOuter.BorderSizePixel = 0
|
|
214
|
+
checkboxOuter.Parent = autoConnectContainer
|
|
215
|
+
|
|
216
|
+
local cbCorner = Instance.new("UICorner")
|
|
217
|
+
cbCorner.CornerRadius = UDim.new(0, 3)
|
|
218
|
+
cbCorner.Parent = checkboxOuter
|
|
219
|
+
|
|
220
|
+
local cbStroke = Instance.new("UIStroke")
|
|
221
|
+
cbStroke.Color = Color3.fromRGB(80, 80, 95)
|
|
222
|
+
cbStroke.Thickness = 1
|
|
223
|
+
cbStroke.Parent = checkboxOuter
|
|
224
|
+
|
|
225
|
+
-- Checkbox inner (check mark)
|
|
226
|
+
local checkboxInner = Instance.new("Frame")
|
|
227
|
+
checkboxInner.Size = UDim2.new(0, 10, 0, 10)
|
|
228
|
+
checkboxInner.Position = UDim2.new(0.5, -5, 0.5, -5)
|
|
229
|
+
checkboxInner.BackgroundColor3 = Color3.fromRGB(80, 170, 255)
|
|
230
|
+
checkboxInner.Visible = false
|
|
231
|
+
checkboxInner.Parent = checkboxOuter
|
|
232
|
+
|
|
233
|
+
local cbiCorner = Instance.new("UICorner")
|
|
234
|
+
cbiCorner.CornerRadius = UDim.new(0, 2)
|
|
235
|
+
cbiCorner.Parent = checkboxInner
|
|
236
|
+
|
|
237
|
+
-- Label
|
|
238
|
+
local autoConnectLabel = Instance.new("TextLabel")
|
|
239
|
+
autoConnectLabel.Text = "Auto-Connect"
|
|
240
|
+
autoConnectLabel.Font = Enum.Font.Gotham
|
|
241
|
+
autoConnectLabel.TextSize = 11
|
|
242
|
+
autoConnectLabel.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
243
|
+
autoConnectLabel.BackgroundTransparency = 1
|
|
244
|
+
autoConnectLabel.Position = UDim2.new(0, 32, 0, 0)
|
|
245
|
+
autoConnectLabel.Size = UDim2.new(1, -36, 1, 0)
|
|
246
|
+
autoConnectLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
247
|
+
autoConnectLabel.Parent = autoConnectContainer
|
|
248
|
+
|
|
249
|
+
-- Invisible overlay button for checkbox hit area
|
|
250
|
+
local autoConnectBtn = Instance.new("TextButton")
|
|
251
|
+
autoConnectBtn.Size = UDim2.new(1, 0, 1, 0)
|
|
252
|
+
autoConnectBtn.BackgroundTransparency = 1
|
|
253
|
+
autoConnectBtn.Text = ""
|
|
254
|
+
autoConnectBtn.ZIndex = 3 -- Di atas elemen visual agar klik selalu terdeteksi
|
|
255
|
+
autoConnectBtn.Parent = autoConnectContainer
|
|
256
|
+
|
|
257
|
+
-- Hover effect for checkbox container
|
|
258
|
+
autoConnectBtn.MouseEnter:Connect(function()
|
|
259
|
+
autoConnectContainer.BackgroundColor3 = Color3.fromRGB(42, 42, 50)
|
|
260
|
+
end)
|
|
261
|
+
autoConnectBtn.MouseLeave:Connect(function()
|
|
262
|
+
autoConnectContainer.BackgroundColor3 = Color3.fromRGB(32, 32, 38)
|
|
263
|
+
end)
|
|
264
|
+
|
|
265
|
+
-- ═══════════════════════════════════════════
|
|
266
|
+
-- SECTION 3: TERMINAL LOG (fills remaining space)
|
|
267
|
+
-- ═══════════════════════════════════════════
|
|
268
|
+
-- Terminal Label
|
|
269
|
+
local termLabel = Instance.new("TextLabel")
|
|
270
|
+
termLabel.Text = "TERMINAL"
|
|
271
|
+
termLabel.Font = Enum.Font.GothamBold
|
|
272
|
+
termLabel.TextSize = 10
|
|
273
|
+
termLabel.TextColor3 = Color3.fromRGB(90, 90, 100)
|
|
274
|
+
termLabel.BackgroundTransparency = 1
|
|
275
|
+
termLabel.Size = UDim2.new(1, 0, 0, 14)
|
|
276
|
+
termLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
277
|
+
termLabel.LayoutOrder = 3
|
|
278
|
+
termLabel.Parent = rootFrame
|
|
279
|
+
|
|
280
|
+
-- Terminal container: Fills ALL remaining vertical space
|
|
281
|
+
local terminalContainer = Instance.new("Frame")
|
|
282
|
+
terminalContainer.BackgroundColor3 = Color3.fromRGB(14, 14, 18)
|
|
283
|
+
terminalContainer.BorderSizePixel = 0
|
|
284
|
+
terminalContainer.LayoutOrder = 4
|
|
285
|
+
terminalContainer.ClipsDescendants = true
|
|
286
|
+
terminalContainer.Parent = rootFrame
|
|
287
|
+
|
|
288
|
+
local termCorner = Instance.new("UICorner")
|
|
289
|
+
termCorner.CornerRadius = UDim.new(0, 5)
|
|
290
|
+
termCorner.Parent = terminalContainer
|
|
291
|
+
|
|
292
|
+
local termStroke = Instance.new("UIStroke")
|
|
293
|
+
termStroke.Color = Color3.fromRGB(40, 40, 48)
|
|
294
|
+
termStroke.Thickness = 1
|
|
295
|
+
termStroke.Parent = terminalContainer
|
|
296
|
+
|
|
297
|
+
-- Scrolling log area inside terminal
|
|
298
|
+
local scrollingFrame = Instance.new("ScrollingFrame")
|
|
299
|
+
scrollingFrame.Size = UDim2.new(1, -8, 1, -4)
|
|
300
|
+
scrollingFrame.Position = UDim2.new(0, 4, 0, 2)
|
|
301
|
+
scrollingFrame.BackgroundTransparency = 1
|
|
302
|
+
scrollingFrame.BorderSizePixel = 0
|
|
303
|
+
scrollingFrame.ScrollBarThickness = 4
|
|
304
|
+
scrollingFrame.ScrollBarImageColor3 = Color3.fromRGB(70, 70, 85)
|
|
305
|
+
scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
|
|
306
|
+
scrollingFrame.Parent = terminalContainer
|
|
307
|
+
|
|
308
|
+
local listLayout = Instance.new("UIListLayout")
|
|
309
|
+
listLayout.Padding = UDim.new(0, 1)
|
|
310
|
+
listLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
311
|
+
listLayout.Parent = scrollingFrame
|
|
312
|
+
|
|
313
|
+
-- ═══════════════════════════════════════════
|
|
314
|
+
-- RESPONSIVITY: Dynamically size terminal to fill remaining space
|
|
315
|
+
-- ═══════════════════════════════════════════
|
|
316
|
+
local function updateLayout()
|
|
317
|
+
-- Calculate how much space header + controls + termLabel use
|
|
318
|
+
local usedHeight = rootLayout.AbsoluteContentSize.Y - terminalContainer.AbsoluteSize.Y
|
|
319
|
+
local availableHeight = rootFrame.AbsoluteSize.Y - 16 -- rootPadding top+bottom
|
|
320
|
+
local termHeight = math.max(100, availableHeight - usedHeight)
|
|
321
|
+
terminalContainer.Size = UDim2.new(1, 0, 0, termHeight)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
-- Fire on widget resize
|
|
325
|
+
widget:GetPropertyChangedSignal("AbsoluteSize"):Connect(updateLayout)
|
|
326
|
+
-- Also fire once after initial render
|
|
327
|
+
task.defer(updateLayout)
|
|
328
|
+
|
|
329
|
+
-- ═══════════════════════════════════════════
|
|
330
|
+
-- Store references for external access
|
|
331
|
+
-- ═══════════════════════════════════════════
|
|
332
|
+
UIManager.widget = widget
|
|
333
|
+
UIManager.connectBtn = connectBtn
|
|
334
|
+
UIManager.autoConnectBtn = autoConnectBtn
|
|
335
|
+
UIManager.checkboxInner = checkboxInner
|
|
336
|
+
UIManager.checkboxStroke = cbStroke
|
|
337
|
+
UIManager.statusLabel = statusLabel
|
|
338
|
+
UIManager.scrollingFrame = scrollingFrame
|
|
339
|
+
UIManager.listLayout = listLayout
|
|
340
|
+
UIManager.terminalContainer = terminalContainer
|
|
341
|
+
UIManager.termStroke = termStroke
|
|
342
|
+
UIManager.logsCount = 0
|
|
343
|
+
|
|
344
|
+
-- Simpan referensi hover state agar setStatus bisa update base color
|
|
345
|
+
UIManager._btnBaseColor = _btnBaseColor
|
|
346
|
+
UIManager._isHovering = _isHovering
|
|
347
|
+
UIManager._updateBtnBaseColor = function(newColor)
|
|
348
|
+
_btnBaseColor = newColor
|
|
349
|
+
if not _isHovering then
|
|
350
|
+
connectBtn.BackgroundColor3 = newColor
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
return UIManager
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
-- ═══════════════════════════════════════════
|
|
358
|
+
-- Menambahkan baris log baru ke terminal GUI
|
|
359
|
+
-- ═══════════════════════════════════════════
|
|
360
|
+
function UIManager:addLog(text, color)
|
|
361
|
+
local timestamp = os.date("%H:%M:%S")
|
|
362
|
+
local displayText = "[" .. timestamp .. "] " .. text
|
|
363
|
+
|
|
364
|
+
local logLabel = Instance.new("TextLabel")
|
|
365
|
+
logLabel.Text = displayText
|
|
366
|
+
logLabel.Font = Enum.Font.Code
|
|
367
|
+
logLabel.TextSize = 11
|
|
368
|
+
logLabel.TextColor3 = color or Color3.fromRGB(180, 180, 190)
|
|
369
|
+
logLabel.BackgroundTransparency = 1
|
|
370
|
+
logLabel.TextWrapped = true
|
|
371
|
+
logLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
372
|
+
logLabel.RichText = false
|
|
373
|
+
logLabel.AutomaticSize = Enum.AutomaticSize.Y
|
|
374
|
+
logLabel.Size = UDim2.new(1, -6, 0, 0) -- Width fill, height auto
|
|
375
|
+
|
|
376
|
+
local logPadding = Instance.new("UIPadding")
|
|
377
|
+
logPadding.PaddingLeft = UDim.new(0, 4)
|
|
378
|
+
logPadding.PaddingTop = UDim.new(0, 1)
|
|
379
|
+
logPadding.PaddingBottom = UDim.new(0, 1)
|
|
380
|
+
logPadding.Parent = logLabel
|
|
381
|
+
|
|
382
|
+
logLabel.LayoutOrder = self.logsCount
|
|
383
|
+
logLabel.Parent = self.scrollingFrame
|
|
384
|
+
self.logsCount = self.logsCount + 1
|
|
385
|
+
|
|
386
|
+
-- Limit max logs to prevent memory bloat
|
|
387
|
+
if self.logsCount > 150 then
|
|
388
|
+
local first = self.scrollingFrame:FindFirstChildWhichIsA("TextLabel")
|
|
389
|
+
if first then first:Destroy() end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
-- Update canvas size after a frame so AutomaticSize has resolved
|
|
393
|
+
task.defer(function()
|
|
394
|
+
self.scrollingFrame.CanvasSize = UDim2.new(0, 0, 0, self.listLayout.AbsoluteContentSize.Y + 4)
|
|
395
|
+
self.scrollingFrame.CanvasPosition = Vector2.new(0, math.max(0, self.listLayout.AbsoluteContentSize.Y - self.scrollingFrame.AbsoluteSize.Y))
|
|
396
|
+
end)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
-- ═══════════════════════════════════════════
|
|
400
|
+
-- 3 State Setter (Disconnected, Waiting, Connected)
|
|
401
|
+
-- ═══════════════════════════════════════════
|
|
402
|
+
function UIManager:setStatus(stateStr)
|
|
403
|
+
if stateStr == "disconnected" then
|
|
404
|
+
self.connectBtn.Text = " Connect"
|
|
405
|
+
self._updateBtnBaseColor(Color3.fromRGB(40, 160, 80))
|
|
406
|
+
self.statusLabel.Text = "● Disconnected"
|
|
407
|
+
self.statusLabel.TextColor3 = Color3.fromRGB(255, 100, 100)
|
|
408
|
+
self.termStroke.Color = Color3.fromRGB(40, 40, 48)
|
|
409
|
+
|
|
410
|
+
elseif stateStr == "waiting" then
|
|
411
|
+
self.connectBtn.Text = " Disconnect"
|
|
412
|
+
self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
|
|
413
|
+
self.statusLabel.Text = "● Waiting for AI..."
|
|
414
|
+
self.statusLabel.TextColor3 = Color3.fromRGB(255, 200, 80)
|
|
415
|
+
self.termStroke.Color = Color3.fromRGB(130, 110, 50)
|
|
416
|
+
|
|
417
|
+
elseif stateStr == "connected" then
|
|
418
|
+
self.connectBtn.Text = " Disconnect"
|
|
419
|
+
self._updateBtnBaseColor(Color3.fromRGB(180, 50, 50))
|
|
420
|
+
self.statusLabel.Text = "● Connected"
|
|
421
|
+
self.statusLabel.TextColor3 = Color3.fromRGB(100, 230, 120)
|
|
422
|
+
self.termStroke.Color = Color3.fromRGB(50, 120, 60)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
-- Mengubah visual custom checkbox
|
|
427
|
+
function UIManager:setAutoConnectState(isAuto)
|
|
428
|
+
self.checkboxInner.Visible = isAuto
|
|
429
|
+
if isAuto then
|
|
430
|
+
self.checkboxStroke.Color = Color3.fromRGB(80, 170, 255)
|
|
431
|
+
else
|
|
432
|
+
self.checkboxStroke.Color = Color3.fromRGB(80, 80, 95)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
-- Sakelar visibilitas panel GUI
|
|
437
|
+
function UIManager:toggleVisibility()
|
|
438
|
+
if self.widget then
|
|
439
|
+
self.widget.Enabled = not self.widget.Enabled
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
return UIManager
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
return function(targetInstance, data, ctx, targetPath)
|
|
2
|
+
if targetInstance:IsA("LuaSourceContainer") then
|
|
3
|
+
local newCode = data or ""
|
|
4
|
+
|
|
5
|
+
-- Catatan: Validasi AST menggunakan loadstring() sengaja DIHAPUS.
|
|
6
|
+
-- Karena secara default Roblox Studio mematikan LoadStringEnabled, menggunakannya
|
|
7
|
+
-- hanya akan membuat AI gagal terus menerus untuk menulis script.
|
|
8
|
+
-- Error syntax akan ditangkap secara native oleh Roblox Output.
|
|
9
|
+
|
|
10
|
+
ctx.scriptHistory[targetPath] = targetInstance.Source
|
|
11
|
+
targetInstance.Source = newCode
|
|
12
|
+
return { status = "success", result = "Script '" .. targetInstance.Name .. "' berhasil diperbarui." }
|
|
13
|
+
else
|
|
14
|
+
return { status = "error", error = "Target BUKAN Script (ClassName: " .. targetInstance.ClassName .. ")." }
|
|
15
|
+
end
|
|
16
|
+
end
|