@adiholick/nvstudio-mcp 2.2.3 → 2.4.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.
Files changed (42) hide show
  1. package/README.md +57 -47
  2. package/agent-plugin/mcp_config.json +8 -0
  3. package/agent-plugin/plugin.json +6 -0
  4. package/agent-plugin/rules/AGENTS.md +118 -0
  5. package/agent-plugin/skills/nvstudio-animation-vfx/SKILL.md +120 -0
  6. package/agent-plugin/skills/nvstudio-assets-terrain-guide/SKILL.md +27 -0
  7. package/agent-plugin/skills/nvstudio-gui-standards/SKILL.md +419 -0
  8. package/agent-plugin/skills/nvstudio-mcp-guide/SKILL.md +83 -0
  9. package/agent-plugin/skills/nvstudio-rig-engine/SKILL.md +170 -0
  10. package/agent-plugin/skills/nvstudio-roblox-sync-guide/SKILL.md +16 -0
  11. package/dist/build-plugin.js +55 -0
  12. package/dist/index.js +841 -3
  13. package/dist/luau-validator.js +99 -34
  14. package/dist/roblox-bridge.js +152 -4
  15. package/dist/roblox-docs.js +264 -0
  16. package/dist/vfx-database.js +507 -0
  17. package/package.json +6 -4
  18. package/public/index.html +1590 -180
  19. package/studio-plugin/Main.server.lua +1 -1
  20. package/studio-plugin/animate_character.lua +36 -0
  21. package/studio-plugin/apply_vfx.lua +59 -0
  22. package/studio-plugin/batch_create_instances.lua +128 -0
  23. package/studio-plugin/batch_set_properties.lua +69 -0
  24. package/studio-plugin/edit_rig.lua +35 -0
  25. package/studio-plugin/insert_asset.lua +85 -12
  26. package/studio-plugin/nv_anim_dispatcher.lua +239 -0
  27. package/studio-plugin/nv_auto_rigger.lua +392 -0
  28. package/studio-plugin/nv_curves.lua +260 -0
  29. package/studio-plugin/nv_joint_math.lua +138 -0
  30. package/studio-plugin/nv_kinematics.lua +248 -0
  31. package/studio-plugin/nv_motion_generator.lua +279 -0
  32. package/studio-plugin/nv_rig_dispatcher.lua +319 -0
  33. package/studio-plugin/nv_vfx_serializer.lua +363 -0
  34. package/studio-plugin/nvstudio_mcp.rbxmx +3824 -60
  35. package/studio-plugin/preview_asset.lua +99 -0
  36. package/studio-plugin/scan_security.lua +81 -0
  37. package/studio-plugin/scan_vfx.lua +85 -0
  38. package/studio-plugin/scrub_timeline.lua +122 -0
  39. package/studio-plugin/security_scanner.lua +346 -0
  40. package/studio-plugin/set_properties.lua +83 -0
  41. package/studio-plugin/solve_limb_ik.lua +63 -0
  42. package/studio-plugin/ui_manager.lua +1 -1
@@ -0,0 +1,346 @@
1
+ --!strict
2
+ -- ============================================================================
3
+ -- Module: security_scanner
4
+ -- Deskripsi: Engine Heuristik Pemindaian Keamanan & Backdoor Detection
5
+ -- Standar: Business-Grade, Zero-Execution Isolation, Comprehensive Threat Rules
6
+ -- ============================================================================
7
+
8
+ local SecurityScanner = {}
9
+
10
+ -- Daftar nama skrip yang sering digunakan oleh virus/backdoor klasik Roblox
11
+ local SUSPICIOUS_SCRIPT_NAMES = {
12
+ "[Vv]accine",
13
+ "[Aa]nti[Ll]ag",
14
+ "[Ff]ix[Ll]ag",
15
+ "[Ii]nfection",
16
+ "[Ff]ire",
17
+ "[Cc]lean",
18
+ "[Vv]irus",
19
+ "4[Dd]",
20
+ "4[Dd]_[Pp]lugin",
21
+ "[Ss]pread",
22
+ "[Ll]ag[Ff]ixer",
23
+ "[Dd]up",
24
+ }
25
+
26
+ -- Aturan Pola Heuristik Kode Berbahaya
27
+ local THREAT_PATTERNS = {
28
+ {
29
+ id = "BACKDOOR_REQUIRE",
30
+ pattern = "require%s*%(%s*%d+%s*%)",
31
+ severity = "HIGH",
32
+ name = "Backdoor Numeric Require",
33
+ description = "Memanggil modul eksternal pihak ketiga via Asset ID numerik (indikasi backdoor/remote loader).",
34
+ },
35
+ {
36
+ id = "LOADSTRING_EXEC",
37
+ pattern = "loadstring%s*%(",
38
+ severity = "HIGH",
39
+ name = "Dynamic Code Execution (loadstring)",
40
+ description = "Mengeksekusi string kode dinamis yang tidak dapat diaudit.",
41
+ },
42
+ {
43
+ id = "GETFENV_SETFENV",
44
+ pattern = "[gs]etfenv%s*%(",
45
+ severity = "HIGH",
46
+ name = "Environment Manipulation (getfenv/setfenv)",
47
+ description = "Memanipulasi environment global Luau untuk mengelabui analisis statis.",
48
+ },
49
+ {
50
+ id = "DISCORD_WEBHOOK",
51
+ pattern = "discord[a-z]*%.com/api/webhooks",
52
+ severity = "HIGH",
53
+ name = "Discord Webhook Exfiltration",
54
+ description = "Mencoba mengirimkan data game atau informasi pemain ke webhook eksternal Discord.",
55
+ },
56
+ {
57
+ id = "IP_LOGGER",
58
+ pattern = "webhook%.site|grabify%.link|iplogger|blasze",
59
+ severity = "HIGH",
60
+ name = "External IP/Data Logger",
61
+ description = "Menghubungi domain pencatat IP atau data capture pihak ketiga.",
62
+ },
63
+ {
64
+ id = "FORCED_PURCHASE",
65
+ pattern = "MarketplaceService%s*:%s*Prompt[A-Za-z]*Purchase",
66
+ severity = "MEDIUM",
67
+ name = "Forced Robux/Product Purchase",
68
+ description = "Memicu dialog pembelian paksa Gamepass atau Developer Product.",
69
+ },
70
+ {
71
+ id = "FORCED_TELEPORT",
72
+ pattern = "TeleportService%s*:%s*Teleport",
73
+ severity = "MEDIUM",
74
+ name = "Forced Game Teleportation",
75
+ description = "Memindahkan pemain secara paksa ke game/place lain.",
76
+ },
77
+ {
78
+ id = "CHAR_TABLE_OBFUSCATION",
79
+ pattern = "string%.char%s*%(%s*%d+%s*,%s*%d+%s*,%s*%d+",
80
+ severity = "HIGH",
81
+ name = "Obfuscated string.char Array",
82
+ description = "Menyusun kode rahasia tersembunyi menggunakan deretan kode karakter ASCII.",
83
+ },
84
+ }
85
+
86
+ -- Helper: Pengecekan nama script mencurigakan
87
+ local function checkSuspiciousName(name: string): boolean
88
+ for _, pattern in ipairs(SUSPICIOUS_SCRIPT_NAMES) do
89
+ if string.find(name, pattern) then
90
+ return true
91
+ end
92
+ end
93
+ return false
94
+ end
95
+
96
+ -- Helper: Deteksi obfuscation byte escape (\108\111\97\100) atau hex (\x6c\x6f)
97
+ local function checkByteObfuscation(source: string): (boolean, string?)
98
+ local byteCount = 0
99
+ for _ in string.gmatch(source, "\\[0-9][0-9][0-9]") do
100
+ byteCount = byteCount + 1
101
+ if byteCount >= 4 then
102
+ return true, "Terdeteksi deretan escape byte ASCII (\\ddd) berulang."
103
+ end
104
+ end
105
+
106
+ local hexCount = 0
107
+ for _ in string.gmatch(source, "\\x[0-9a-fA-F][0-9a-fA-F]") do
108
+ hexCount = hexCount + 1
109
+ if hexCount >= 4 then
110
+ return true, "Terdeteksi deretan escape hex (\\xHH) berulang."
111
+ end
112
+ end
113
+
114
+ return false, nil
115
+ end
116
+
117
+ -- Helper: Deteksi kode yang sengaja disembunyikan dengan spasi/baris kosong berlebih
118
+ local function checkOffscreenHiding(source: string): (boolean, string?)
119
+ -- Cek spasi horizontal masif (> 80 spasi berturut-turut)
120
+ if string.find(source, " " .. string.rep(" ", 80)) then
121
+ return true, "Kode sengaja didorong ke kanan dengan lebih dari 80 spasi agar tersembunyi dari layar Studio."
122
+ end
123
+
124
+ -- Cek baris kosong masif di awal skrip (> 15 baris kosong)
125
+ local leadingNewlines = 0
126
+ for line in string.gmatch(source, "([^\r\n]*)\r?\n") do
127
+ if string.gsub(line, "%s+", "") == "" then
128
+ leadingNewlines = leadingNewlines + 1
129
+ if leadingNewlines >= 15 then
130
+ return true, "Terdapat lebih dari 15 baris kosong di awal skrip untuk menyembunyikan payload di baris bawah."
131
+ end
132
+ else
133
+ break
134
+ end
135
+ end
136
+
137
+ return false, nil
138
+ end
139
+
140
+ -- Analisis Source Code dari sebuah Script
141
+ local function scanScriptSource(scriptInstance: Instance, source: string): { any }
142
+ local threats = {}
143
+ local lines = string.split(source, "\n")
144
+
145
+ -- 1. Scan pola heuristik baris per baris
146
+ for lineIdx, line in ipairs(lines) do
147
+ -- Skip baris komentar murni
148
+ local trimmed = string.match(line, "^%s*(.-)%s*$") or ""
149
+ if not string.sub(trimmed, 1, 2) == "--" then
150
+ for _, rule in ipairs(THREAT_PATTERNS) do
151
+ local s, e = string.find(line, rule.pattern)
152
+ if s then
153
+ local snippet = string.sub(line, math.max(1, s - 30), math.min(#line, e + 30))
154
+ table.insert(threats, {
155
+ severity = rule.severity,
156
+ ruleId = rule.id,
157
+ name = rule.name,
158
+ scriptName = scriptInstance.Name,
159
+ scriptPath = scriptInstance:GetFullName(),
160
+ scriptClass = scriptInstance.ClassName,
161
+ line = lineIdx,
162
+ snippet = string.match(snippet, "^%s*(.-)%s*$") or snippet,
163
+ description = rule.description,
164
+ })
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ -- 2. Cek Obfuscation
171
+ local isObf, obfReason = checkByteObfuscation(source)
172
+ if isObf then
173
+ table.insert(threats, {
174
+ severity = "HIGH",
175
+ ruleId = "BYTE_OBFUSCATION",
176
+ name = "Byte / Hex Obfuscation Payload",
177
+ scriptName = scriptInstance.Name,
178
+ scriptPath = scriptInstance:GetFullName(),
179
+ scriptClass = scriptInstance.ClassName,
180
+ line = 1,
181
+ snippet = string.sub(source, 1, 60) .. "...",
182
+ description = obfReason or "Payload kode disamarkan menggunakan escape bytes.",
183
+ })
184
+ end
185
+
186
+ -- 3. Cek Off-screen Code Hiding
187
+ local isHidden, hideReason = checkOffscreenHiding(source)
188
+ if isHidden then
189
+ table.insert(threats, {
190
+ severity = "HIGH",
191
+ ruleId = "OFFSCREEN_HIDING",
192
+ name = "Off-Screen Hidden Code",
193
+ scriptName = scriptInstance.Name,
194
+ scriptPath = scriptInstance:GetFullName(),
195
+ scriptClass = scriptInstance.ClassName,
196
+ line = 1,
197
+ snippet = "[Whitespace Padding Terdeteksi]",
198
+ description = hideReason or "Kode disembunyikan menggunakan whitespace ekstrem.",
199
+ })
200
+ end
201
+
202
+ return threats
203
+ end
204
+
205
+ -- ============================================================================
206
+ -- PUBLIC API
207
+ -- ============================================================================
208
+
209
+ -- Memindai Instance secara rekursif
210
+ function SecurityScanner.scanInstance(rootInstance: Instance, deepScan: boolean?)
211
+ local shouldDeepScan = (deepScan ~= false)
212
+ local allThreats = {}
213
+ local stats = {
214
+ totalInstances = 1,
215
+ models = 0,
216
+ parts = 0,
217
+ meshParts = 0,
218
+ textures = 0,
219
+ scripts = {
220
+ total = 0,
221
+ server = 0,
222
+ localScript = 0,
223
+ module = 0,
224
+ },
225
+ }
226
+
227
+ local function inspectItem(item: Instance)
228
+ stats.totalInstances = stats.totalInstances + 1
229
+
230
+ if item:IsA("Model") then
231
+ stats.models = stats.models + 1
232
+ elseif item:IsA("MeshPart") then
233
+ stats.meshParts = stats.meshParts + 1
234
+ stats.parts = stats.parts + 1
235
+ elseif item:IsA("BasePart") then
236
+ stats.parts = stats.parts + 1
237
+ elseif item:IsA("Texture") or item:IsA("Decal") or item:IsA("SurfaceAppearance") then
238
+ stats.textures = stats.textures + 1
239
+ end
240
+
241
+ if item:IsA("LuaSourceContainer") then
242
+ stats.scripts.total = stats.scripts.total + 1
243
+ if item:IsA("Script") then
244
+ stats.scripts.server = stats.scripts.server + 1
245
+ elseif item:IsA("LocalScript") then
246
+ stats.scripts.localScript = stats.scripts.localScript + 1
247
+ elseif item:IsA("ModuleScript") then
248
+ stats.scripts.module = stats.scripts.module + 1
249
+ end
250
+
251
+ -- Cek nama mencurigakan
252
+ if checkSuspiciousName(item.Name) then
253
+ table.insert(allThreats, {
254
+ severity = "HIGH",
255
+ ruleId = "SUSPICIOUS_SCRIPT_NAME",
256
+ name = "Known Malicious Script Name Pattern",
257
+ scriptName = item.Name,
258
+ scriptPath = item:GetFullName(),
259
+ scriptClass = item.ClassName,
260
+ line = 0,
261
+ snippet = item.Name,
262
+ description = string.format("Nama skrip '%s' cocok dengan pola nama virus/backdoor klasik.", item.Name),
263
+ })
264
+ end
265
+
266
+ -- Deep scan isi source code
267
+ if shouldDeepScan then
268
+ local success, source = pcall(function()
269
+ return (item :: any).Source
270
+ end)
271
+ if success and typeof(source) == "string" and #source > 0 then
272
+ local scriptThreats = scanScriptSource(item, source)
273
+ for _, t in ipairs(scriptThreats) do
274
+ table.insert(allThreats, t)
275
+ end
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ inspectItem(rootInstance)
282
+ for _, desc in ipairs(rootInstance:GetDescendants()) do
283
+ inspectItem(desc)
284
+ end
285
+
286
+ -- Hitung Level Risiko
287
+ local riskLevel = "SAFE"
288
+ local highCount = 0
289
+ local medCount = 0
290
+
291
+ for _, t in ipairs(allThreats) do
292
+ if t.severity == "HIGH" then
293
+ highCount = highCount + 1
294
+ elseif t.severity == "MEDIUM" then
295
+ medCount = medCount + 1
296
+ end
297
+ end
298
+
299
+ if highCount > 0 then
300
+ riskLevel = "DANGEROUS"
301
+ elseif medCount > 0 or stats.scripts.total > 0 then
302
+ riskLevel = "WARNING"
303
+ end
304
+
305
+ return {
306
+ riskLevel = riskLevel,
307
+ threats = allThreats,
308
+ stats = stats,
309
+ highThreats = highCount,
310
+ mediumThreats = medCount,
311
+ }
312
+ end
313
+
314
+ -- Menghapus seluruh skrip dari sebuah instance (Sanitasi Total)
315
+ function SecurityScanner.stripScripts(rootInstance: Instance): { any }
316
+ local removed = {}
317
+
318
+ for _, desc in ipairs(rootInstance:GetDescendants()) do
319
+ if desc:IsA("LuaSourceContainer") then
320
+ table.insert(removed, {
321
+ name = desc.Name,
322
+ className = desc.ClassName,
323
+ path = desc:GetFullName(),
324
+ })
325
+ pcall(function()
326
+ desc:Destroy()
327
+ end)
328
+ end
329
+ end
330
+
331
+ if rootInstance:IsA("LuaSourceContainer") then
332
+ table.insert(removed, {
333
+ name = rootInstance.Name,
334
+ className = rootInstance.ClassName,
335
+ path = rootInstance:GetFullName(),
336
+ })
337
+ pcall(function()
338
+ rootInstance:Destroy()
339
+ end)
340
+ end
341
+
342
+ return removed
343
+ end
344
+
345
+ return SecurityScanner
346
+
@@ -12,6 +12,10 @@ return function(targetInstance, data, ctx, targetPath)
12
12
  local currentVal = targetInstance[prop]
13
13
  if typeof(currentVal) == "Vector3" and type(val) == "table" and #val == 3 then
14
14
  targetInstance[prop] = Vector3.new(val[1], val[2], val[3])
15
+ elseif typeof(currentVal) == "Vector2" and type(val) == "table" and #val >= 2 then
16
+ targetInstance[prop] = Vector2.new(val[1], val[2])
17
+ elseif typeof(currentVal) == "UDim" and type(val) == "table" and #val >= 2 then
18
+ targetInstance[prop] = UDim.new(val[1], val[2])
15
19
  elseif typeof(currentVal) == "Color3" and type(val) == "table" and #val == 3 then
16
20
  local r, g, b = val[1], val[2], val[3]
17
21
  if r > 1 or g > 1 or b > 1 then
@@ -21,8 +25,87 @@ return function(targetInstance, data, ctx, targetPath)
21
25
  end
22
26
  elseif typeof(currentVal) == "UDim2" and type(val) == "table" and #val == 4 then
23
27
  targetInstance[prop] = UDim2.new(val[1], val[2], val[3], val[4])
28
+ elseif typeof(currentVal) == "NumberRange" and type(val) == "table" then
29
+ targetInstance[prop] = NumberRange.new(val[1], val[2] or val[1])
30
+ elseif typeof(currentVal) == "Rect" and type(val) == "table" and #val >= 4 then
31
+ targetInstance[prop] = Rect.new(val[1], val[2], val[3], val[4])
32
+ elseif typeof(currentVal) == "BrickColor" and type(val) == "string" then
33
+ targetInstance[prop] = BrickColor.new(val)
24
34
  elseif typeof(currentVal) == "CFrame" and type(val) == "table" then
25
35
  targetInstance[prop] = CFrame.new(unpack(val))
36
+ elseif typeof(currentVal) == "ColorSequence" or (type(val) == "table" and val.t == "ColorSequence") then
37
+ local kps = {}
38
+ local rawList = (type(val) == "table" and val.t == "ColorSequence") and val.v or val
39
+ if type(rawList) == "table" then
40
+ for _, kp in ipairs(rawList) do
41
+ local t = kp[1] or 0
42
+ local col = kp[2]
43
+ local c3
44
+ if typeof(col) == "Color3" then
45
+ c3 = col
46
+ elseif type(col) == "table" and col.t == "Color3" then
47
+ c3 = Color3.new(col.v[1], col.v[2], col.v[3])
48
+ elseif type(col) == "table" and #col >= 3 then
49
+ c3 = (col[1] > 1 or col[2] > 1 or col[3] > 1) and Color3.fromRGB(col[1], col[2], col[3]) or Color3.new(col[1], col[2], col[3])
50
+ else
51
+ c3 = Color3.new(1, 1, 1)
52
+ end
53
+ table.insert(kps, ColorSequenceKeypoint.new(math.clamp(t, 0, 1), c3))
54
+ end
55
+ end
56
+ if #kps >= 2 then
57
+ table.sort(kps, function(a, b) return a.Time < b.Time end)
58
+ -- Pastikan keypoint pertama t=0 dan terakhir t=1
59
+ if kps[1].Time ~= 0 then
60
+ kps[1] = ColorSequenceKeypoint.new(0, kps[1].Value)
61
+ end
62
+ if kps[#kps].Time ~= 1 then
63
+ kps[#kps] = ColorSequenceKeypoint.new(1, kps[#kps].Value)
64
+ end
65
+ -- Pastikan waktu meningkat secara monoton murni
66
+ for i = 2, #kps do
67
+ if kps[i].Time <= kps[i - 1].Time then
68
+ local newT = math.min(1.0, kps[i - 1].Time + 0.001)
69
+ kps[i] = ColorSequenceKeypoint.new(newT, kps[i].Value)
70
+ end
71
+ end
72
+ targetInstance[prop] = ColorSequence.new(kps)
73
+ elseif #kps == 1 then
74
+ targetInstance[prop] = ColorSequence.new(kps[1].Value)
75
+ end
76
+ elseif typeof(currentVal) == "NumberSequence" or (type(val) == "table" and val.t == "NumberSequence") then
77
+ local kps = {}
78
+ local rawList = (type(val) == "table" and val.t == "NumberSequence") and val.v or val
79
+ if type(rawList) == "table" then
80
+ for _, kp in ipairs(rawList) do
81
+ if type(kp) == "table" then
82
+ local t = kp[1] or 0
83
+ local v = kp[2] or 0
84
+ local env = kp[3] or 0
85
+ table.insert(kps, NumberSequenceKeypoint.new(math.clamp(t, 0, 1), v, env))
86
+ end
87
+ end
88
+ end
89
+ if #kps >= 2 then
90
+ table.sort(kps, function(a, b) return a.Time < b.Time end)
91
+ -- Pastikan keypoint pertama t=0 dan terakhir t=1
92
+ if kps[1].Time ~= 0 then
93
+ kps[1] = NumberSequenceKeypoint.new(0, kps[1].Value, kps[1].Envelope)
94
+ end
95
+ if kps[#kps].Time ~= 1 then
96
+ kps[#kps] = NumberSequenceKeypoint.new(1, kps[#kps].Value, kps[#kps].Envelope)
97
+ end
98
+ -- Pastikan waktu meningkat secara monoton murni
99
+ for i = 2, #kps do
100
+ if kps[i].Time <= kps[i - 1].Time then
101
+ local newT = math.min(1.0, kps[i - 1].Time + 0.001)
102
+ kps[i] = NumberSequenceKeypoint.new(newT, kps[i].Value, kps[i].Envelope)
103
+ end
104
+ end
105
+ targetInstance[prop] = NumberSequence.new(kps)
106
+ elseif #kps == 1 then
107
+ targetInstance[prop] = NumberSequence.new(kps[1].Value)
108
+ end
26
109
  elseif typeof(currentVal) == "EnumItem" and type(val) == "string" then
27
110
  local enumType = tostring(currentVal.EnumType)
28
111
  local successEnum, enumValue = pcall(function() return Enum[enumType][val] end)
@@ -0,0 +1,63 @@
1
+ --!strict
2
+ -- ══════════════════════════════════════════════════════════════════════
3
+ -- Skill Module: solve_limb_ik
4
+ -- Deskripsi: Semantic Tool untuk AI Agent menghitung orientasi sendi
5
+ -- lengan / kaki menuju koordinat sasaran 3D secara analitis O(1).
6
+ -- ══════════════════════════════════════════════════════════════════════
7
+
8
+ local HttpService = game:GetService("HttpService")
9
+ local kinematicsMod = script.Parent:WaitForChild("nv_kinematics")
10
+ local Kinematics = require(kinematicsMod)
11
+
12
+ return function(targetInstance: Instance?, data: any, ctx: any, targetPath: string?)
13
+ if not targetInstance then
14
+ return { status = "error", error = "Target karakter rig untuk solve_limb_ik tidak ditemukan: " .. tostring(targetPath) }
15
+ end
16
+
17
+ local rigInfo = Kinematics.inspectRig(targetInstance:IsA("Model") and targetInstance or targetInstance:FindFirstAncestorOfClass("Model") :: any)
18
+ if not rigInfo then
19
+ return { status = "error", error = "Target bukan model karakter rig (R6/R15) yang valid: " .. targetInstance:GetFullName() }
20
+ end
21
+
22
+ local payload: { [string]: any } = {}
23
+ pcall(function()
24
+ if type(data) == "table" then payload = data
25
+ else payload = HttpService:JSONDecode(tostring(data)) end
26
+ end)
27
+
28
+ local limb = tostring(payload.limb or "RightArm")
29
+ local goalArr = payload.goal
30
+ if not goalArr or type(goalArr) ~= "table" or #goalArr < 3 then
31
+ return { status = "error", error = "Parameter 'goal' [X, Y, Z] wajib diisi." }
32
+ end
33
+
34
+ local goalPos = Vector3.new(goalArr[1], goalArr[2], goalArr[3])
35
+ local polePos: Vector3? = nil
36
+ if payload.pole and type(payload.pole) == "table" and #payload.pole >= 3 then
37
+ polePos = Vector3.new(payload.pole[1], payload.pole[2], payload.pole[3])
38
+ end
39
+
40
+ local apply = payload.apply_to_rig ~= false
41
+ local solved = Kinematics.solveLimb(rigInfo, limb, goalPos, polePos, apply)
42
+
43
+ if not solved then
44
+ return { status = "error", error = "Gagal menyelesaikan IK untuk anggota tubuh: " .. limb }
45
+ end
46
+
47
+ local resultC0: { [string]: any } = {}
48
+ for mName, cf in pairs(solved) do
49
+ resultC0[mName] = { t = "CFrame", v = { cf:GetComponents() } }
50
+ end
51
+
52
+ return {
53
+ status = "success",
54
+ result = {
55
+ limb = limb,
56
+ rigType = rigInfo.isR15 and "R15" or "R6",
57
+ applied = apply,
58
+ targetGoal = { goalPos.X, goalPos.Y, goalPos.Z },
59
+ solvedMotors = resultC0
60
+ }
61
+ }
62
+ end
63
+
@@ -95,7 +95,7 @@ function UIManager.init(pluginInstance, studioId)
95
95
 
96
96
  -- Version
97
97
  local versionLabel = Instance.new("TextLabel")
98
- versionLabel.Text = "v2.2.0"
98
+ versionLabel.Text = "v2.4.0"
99
99
  versionLabel.Font = Enum.Font.Gotham
100
100
  versionLabel.TextSize = 11
101
101
  versionLabel.TextColor3 = Color3.fromRGB(120, 120, 130)