@el4cteo/rbx-studio-mcp 0.6.0 → 0.6.5
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/README.md +4 -4
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/anim.js +159 -0
- package/dist/tools/anim.js.map +1 -0
- package/dist/tools/character.js +95 -5
- package/dist/tools/character.js.map +1 -1
- package/dist/tools/data.js +173 -0
- package/dist/tools/data.js.map +1 -0
- package/dist/tools/device.js +77 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/discover.js +80 -4
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/input.js +35 -9
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/perf.js +74 -7
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +51 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +317 -44
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +622 -562
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +1909 -1843
- package/plugin/src/Emulation.luau +172 -0
- package/plugin/src/Phrase.luau +164 -16
- package/plugin/src/Png.luau +8 -4
- package/plugin/src/Serialize.luau +499 -327
- package/plugin/src/Undo.luau +94 -6
- package/plugin/src/handlers/Anim.luau +897 -0
- package/plugin/src/handlers/Assets.luau +587 -352
- package/plugin/src/handlers/Capture.luau +155 -20
- package/plugin/src/handlers/Character.luau +823 -361
- package/plugin/src/handlers/Data.luau +539 -0
- package/plugin/src/handlers/Device.luau +394 -139
- package/plugin/src/handlers/Discover.luau +685 -363
- package/plugin/src/handlers/Geometry.luau +127 -0
- package/plugin/src/handlers/Perf.luau +227 -0
- package/plugin/src/handlers/Scripts.luau +673 -539
- package/plugin/src/handlers/Session.luau +3 -0
- package/plugin/src/handlers/Viewport.luau +268 -0
- package/plugin/src/handlers/World.luau +89 -15
- package/plugin/src/init.server.luau +879 -832
- package/scripts/build-plugin.mjs +20 -0
- package/scripts/check-plugin.mjs +171 -124
|
@@ -146,6 +146,9 @@ function Session.status(): { [string]: any }
|
|
|
146
146
|
scriptCount = totals.scripts,
|
|
147
147
|
studioVersion = studioVersion(),
|
|
148
148
|
emulatedDevice = Emulation.summary(),
|
|
149
|
+
-- Separate from the device: traffic can be shaped with no device set,
|
|
150
|
+
-- and that is the case most likely to be forgotten about.
|
|
151
|
+
emulatedNetwork = Emulation.networkSummary(),
|
|
149
152
|
}
|
|
150
153
|
end
|
|
151
154
|
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
|
|
16
16
|
local RunService = game:GetService("RunService")
|
|
17
17
|
local Selection = game:GetService("Selection")
|
|
18
|
+
local StarterGui = game:GetService("StarterGui")
|
|
18
19
|
local TextService = game:GetService("TextService")
|
|
19
20
|
local Workspace = game:GetService("Workspace")
|
|
20
21
|
|
|
21
22
|
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
23
|
+
local Emulation = require(script.Parent.Parent.Emulation)
|
|
22
24
|
local Paths = require(script.Parent.Parent.Paths)
|
|
23
25
|
local Serialize = require(script.Parent.Parent.Serialize)
|
|
24
26
|
|
|
@@ -403,8 +405,274 @@ function Viewport.textbounds(params: { [string]: any }): { [string]: any }
|
|
|
403
405
|
return report
|
|
404
406
|
end
|
|
405
407
|
|
|
408
|
+
--[[
|
|
409
|
+
Every interface fault that is invisible in the data model.
|
|
410
|
+
|
|
411
|
+
`textbounds` already answers "does this text fit its label". This is the same
|
|
412
|
+
question asked of the whole screen, and it exists because the data model
|
|
413
|
+
cannot answer it at all: a button positioned off the side of a phone has a
|
|
414
|
+
perfectly correct `Position`, a `Size` that is not zero, and a parent that is
|
|
415
|
+
visible. Nothing about the instance is wrong. It is simply not where anyone
|
|
416
|
+
can reach it, and the only ways to find that out today are to play the game
|
|
417
|
+
or to look at a picture.
|
|
418
|
+
|
|
419
|
+
Studio lays StarterGui out for real in edit mode -- measured:
|
|
420
|
+
`AbsolutePosition` and `AbsoluteSize` carry live values with nothing running,
|
|
421
|
+
and they move when a device is emulated. So the audit runs against whatever
|
|
422
|
+
`device` is currently emulating, which is what makes it worth running twice:
|
|
423
|
+
once on desktop, once on a phone.
|
|
424
|
+
|
|
425
|
+
It reports rather than judges. Every finding names the element and the
|
|
426
|
+
number, so a caller can decide whether a label four pixels off the edge is a
|
|
427
|
+
bug or a deliberate bleed.
|
|
428
|
+
]]
|
|
429
|
+
|
|
430
|
+
--[[
|
|
431
|
+
Text below this many pixels tall is not readable on a phone.
|
|
432
|
+
|
|
433
|
+
Roblox's own UI guidance and every accessibility standard land in the same
|
|
434
|
+
place: somewhere around 12 device-independent pixels is the floor for body
|
|
435
|
+
text. 10 is used here because it is the point past which there is no
|
|
436
|
+
argument -- a label under it is unreadable on any display, not merely small.
|
|
437
|
+
]]
|
|
438
|
+
local MIN_TEXT_PIXELS = 10
|
|
439
|
+
|
|
440
|
+
--[[
|
|
441
|
+
Pairs compared for overlap before the check gives up.
|
|
442
|
+
|
|
443
|
+
Overlap is quadratic in the number of siblings, and a scrolling list of two
|
|
444
|
+
hundred rows is both a legitimate design and forty thousand comparisons of
|
|
445
|
+
rectangles that are supposed to sit next to each other. Past this the check
|
|
446
|
+
reports that it stopped rather than continuing to spend the user's frame on
|
|
447
|
+
an answer nobody asked for.
|
|
448
|
+
]]
|
|
449
|
+
local MAX_OVERLAP_PAIRS = 2000
|
|
450
|
+
|
|
451
|
+
local function rectOf(element: GuiObject): (number, number, number, number)
|
|
452
|
+
local position = element.AbsolutePosition
|
|
453
|
+
local size = element.AbsoluteSize
|
|
454
|
+
return position.X, position.Y, position.X + size.X, position.Y + size.Y
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
local function overlaps(a: GuiObject, b: GuiObject): boolean
|
|
458
|
+
local ax1, ay1, ax2, ay2 = rectOf(a)
|
|
459
|
+
local bx1, by1, bx2, by2 = rectOf(b)
|
|
460
|
+
return ax1 < bx2 and bx1 < ax2 and ay1 < by2 and by1 < ay2
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
--[[
|
|
464
|
+
Whether this element is actually on screen at all.
|
|
465
|
+
|
|
466
|
+
Walks up rather than trusting `Visible`: an element with Visible true inside
|
|
467
|
+
a frame with Visible false is not on screen, and reporting it as a fault
|
|
468
|
+
would bury the real findings under every hidden menu in the place.
|
|
469
|
+
]]
|
|
470
|
+
local function shown(element: Instance): boolean
|
|
471
|
+
local node: Instance? = element
|
|
472
|
+
while node ~= nil and not node:IsA("LayerCollector") do
|
|
473
|
+
if node:IsA("GuiObject") and not (node :: GuiObject).Visible then
|
|
474
|
+
return false
|
|
475
|
+
end
|
|
476
|
+
node = node.Parent
|
|
477
|
+
end
|
|
478
|
+
if node ~= nil and node:IsA("ScreenGui") then
|
|
479
|
+
return (node :: ScreenGui).Enabled
|
|
480
|
+
end
|
|
481
|
+
return node ~= nil
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
function Viewport.ui(params: { [string]: any }): { [string]: any }
|
|
485
|
+
local root: Instance = if typeof(params.path) == "string" and params.path ~= ""
|
|
486
|
+
then Paths.resolve(params.path)
|
|
487
|
+
else StarterGui
|
|
488
|
+
|
|
489
|
+
local camera = Workspace.CurrentCamera
|
|
490
|
+
if camera == nil then
|
|
491
|
+
Dispatch.fail("NO_CAMERA", "This session has no camera, so there is no screen to measure against.")
|
|
492
|
+
end
|
|
493
|
+
local screen = (camera :: Camera).ViewportSize
|
|
494
|
+
local screenWidth, screenHeight = math.floor(screen.X), math.floor(screen.Y)
|
|
495
|
+
|
|
496
|
+
local elements: { GuiObject } = {}
|
|
497
|
+
for _, descendant in root:GetDescendants() do
|
|
498
|
+
if descendant:IsA("GuiObject") then
|
|
499
|
+
table.insert(elements, descendant :: GuiObject)
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
local findings: { { [string]: any } } = {}
|
|
504
|
+
local checked = 0
|
|
505
|
+
local hidden = 0
|
|
506
|
+
|
|
507
|
+
local function report(element: GuiObject, issue: string, detail: string)
|
|
508
|
+
if #findings >= 100 then
|
|
509
|
+
return
|
|
510
|
+
end
|
|
511
|
+
table.insert(findings, {
|
|
512
|
+
path = Paths.of(element),
|
|
513
|
+
name = element.Name,
|
|
514
|
+
className = element.ClassName,
|
|
515
|
+
issue = issue,
|
|
516
|
+
detail = detail,
|
|
517
|
+
})
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
local visible: { GuiObject } = {}
|
|
521
|
+
|
|
522
|
+
for _, element in elements do
|
|
523
|
+
if not shown(element) then
|
|
524
|
+
hidden += 1
|
|
525
|
+
continue
|
|
526
|
+
end
|
|
527
|
+
checked += 1
|
|
528
|
+
table.insert(visible, element)
|
|
529
|
+
|
|
530
|
+
local size = element.AbsoluteSize
|
|
531
|
+
local position = element.AbsolutePosition
|
|
532
|
+
|
|
533
|
+
if size.X <= 0 or size.Y <= 0 then
|
|
534
|
+
report(element, "zero size", string.format("%.0f x %.0f -- nothing is drawn", size.X, size.Y))
|
|
535
|
+
continue
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
--[[
|
|
539
|
+
Off screen, and by how much.
|
|
540
|
+
|
|
541
|
+
Reported as a measurement rather than a yes/no, because the two cases
|
|
542
|
+
it covers are different bugs: an element wholly outside the screen was
|
|
543
|
+
positioned for a display that is not this one, and one clipped at an
|
|
544
|
+
edge is usually a layout that does not fit. The numbers tell them
|
|
545
|
+
apart; a boolean would not.
|
|
546
|
+
]]
|
|
547
|
+
local right = position.X + size.X
|
|
548
|
+
local bottom = position.Y + size.Y
|
|
549
|
+
if right <= 0 or bottom <= 0 or position.X >= screenWidth or position.Y >= screenHeight then
|
|
550
|
+
report(
|
|
551
|
+
element,
|
|
552
|
+
"off screen",
|
|
553
|
+
string.format(
|
|
554
|
+
"at %.0f,%.0f on a %dx%d screen -- entirely outside it",
|
|
555
|
+
position.X,
|
|
556
|
+
position.Y,
|
|
557
|
+
screenWidth,
|
|
558
|
+
screenHeight
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
elseif position.X < 0 or position.Y < 0 or right > screenWidth or bottom > screenHeight then
|
|
562
|
+
report(
|
|
563
|
+
element,
|
|
564
|
+
"clipped",
|
|
565
|
+
string.format(
|
|
566
|
+
"%.0f,%.0f to %.0f,%.0f runs past the %dx%d screen",
|
|
567
|
+
position.X,
|
|
568
|
+
position.Y,
|
|
569
|
+
right,
|
|
570
|
+
bottom,
|
|
571
|
+
screenWidth,
|
|
572
|
+
screenHeight
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
if element:IsA("TextLabel") or element:IsA("TextButton") or element:IsA("TextBox") then
|
|
578
|
+
local text = element :: any
|
|
579
|
+
--[[
|
|
580
|
+
The size the text is actually drawn at, which is not TextSize when
|
|
581
|
+
TextScaled is on: the engine then fits the string to the box and
|
|
582
|
+
`TextBounds` is the only honest report of the result.
|
|
583
|
+
]]
|
|
584
|
+
local drawn = if text.TextScaled then text.TextBounds.Y else text.TextSize
|
|
585
|
+
if drawn > 0 and drawn < MIN_TEXT_PIXELS and text.Text ~= "" then
|
|
586
|
+
report(
|
|
587
|
+
element,
|
|
588
|
+
"text too small",
|
|
589
|
+
string.format("%.0fpx -- under %dpx is unreadable on a phone", drawn, MIN_TEXT_PIXELS)
|
|
590
|
+
)
|
|
591
|
+
end
|
|
592
|
+
if text.TextTruncate == Enum.TextTruncate.None and not text.TextScaled and not text.TextWrapped then
|
|
593
|
+
if text.TextBounds.X > size.X + 1 then
|
|
594
|
+
report(
|
|
595
|
+
element,
|
|
596
|
+
"text overflows",
|
|
597
|
+
string.format("needs %.0fpx, has %.0fpx", text.TextBounds.X, size.X)
|
|
598
|
+
)
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
--[[
|
|
605
|
+
Overlap between siblings only.
|
|
606
|
+
|
|
607
|
+
A child sitting on top of its parent is the normal way UI is built, and
|
|
608
|
+
reporting it would make the check useless. Two buttons in the same frame
|
|
609
|
+
covering each other is the fault worth naming.
|
|
610
|
+
]]
|
|
611
|
+
--[[
|
|
612
|
+
Whether an element is something a user reads or presses.
|
|
613
|
+
|
|
614
|
+
Overlap between decoration is not a fault, it is how UI is built: a drop
|
|
615
|
+
shadow sits under a panel, a bevel sits under a label, a playhead sits
|
|
616
|
+
over a timeline. Measured on a real HUD, the first version of this check
|
|
617
|
+
reported four overlaps and all four were of that kind -- a Frame named
|
|
618
|
+
Shadow behind the three things it was drawn to sit behind.
|
|
619
|
+
|
|
620
|
+
Two pieces of TEXT on top of each other, or two images, is the fault
|
|
621
|
+
worth naming. So a plain coloured Frame is not counted; something
|
|
622
|
+
carrying text or an image is.
|
|
623
|
+
]]
|
|
624
|
+
local function carriesContent(element: GuiObject): boolean
|
|
625
|
+
if element:IsA("TextLabel") or element:IsA("TextButton") or element:IsA("TextBox") then
|
|
626
|
+
return (element :: any).Text ~= ""
|
|
627
|
+
end
|
|
628
|
+
if element:IsA("ImageLabel") or element:IsA("ImageButton") then
|
|
629
|
+
return (element :: any).Image ~= ""
|
|
630
|
+
end
|
|
631
|
+
return false
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
local pairs = 0
|
|
635
|
+
local stopped = false
|
|
636
|
+
for index = 1, #visible do
|
|
637
|
+
for other = index + 1, #visible do
|
|
638
|
+
if pairs >= MAX_OVERLAP_PAIRS then
|
|
639
|
+
stopped = true
|
|
640
|
+
break
|
|
641
|
+
end
|
|
642
|
+
local a, b = visible[index], visible[other]
|
|
643
|
+
if a.Parent == b.Parent then
|
|
644
|
+
pairs += 1
|
|
645
|
+
if
|
|
646
|
+
overlaps(a, b)
|
|
647
|
+
and a.AbsoluteSize.X > 0
|
|
648
|
+
and b.AbsoluteSize.X > 0
|
|
649
|
+
and carriesContent(a)
|
|
650
|
+
and carriesContent(b)
|
|
651
|
+
then
|
|
652
|
+
report(a, "overlaps", string.format("covers or is covered by %s", b.Name))
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
if stopped then
|
|
657
|
+
break
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
screen = string.format("%dx%d", screenWidth, screenHeight),
|
|
663
|
+
device = Emulation.deviceId(),
|
|
664
|
+
root = Paths.of(root),
|
|
665
|
+
checked = checked,
|
|
666
|
+
hidden = hidden,
|
|
667
|
+
findings = findings,
|
|
668
|
+
findingCount = #findings,
|
|
669
|
+
overlapStopped = stopped,
|
|
670
|
+
}
|
|
671
|
+
end
|
|
672
|
+
|
|
406
673
|
function Viewport.register()
|
|
407
674
|
Dispatch.registerAll("viewport", {
|
|
675
|
+
ui = Viewport.ui,
|
|
408
676
|
textbounds = Viewport.textbounds,
|
|
409
677
|
raycast = Viewport.raycast,
|
|
410
678
|
select = Viewport.select,
|
|
@@ -18,15 +18,21 @@
|
|
|
18
18
|
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
19
19
|
|
|
20
20
|
--[[
|
|
21
|
-
Collision groups are read and written through
|
|
21
|
+
Collision groups are read and written through a WorldRoot, not PhysicsService.
|
|
22
22
|
|
|
23
23
|
Roblox moved collision group management onto WorldRoot in September 2026 and
|
|
24
24
|
deprecated the PhysicsService methods in the same announcement. Both still
|
|
25
|
-
work today -- checked against Studio 0.
|
|
26
|
-
a removal rather than a fix.
|
|
27
|
-
|
|
25
|
+
work today -- checked against Studio 0.738 -- so this is a migration ahead of
|
|
26
|
+
a removal rather than a fix.
|
|
27
|
+
|
|
28
|
+
The move is not only a rename. A `WorldModel` is a WorldRoot too, and it now
|
|
29
|
+
keeps its OWN registry: a group named "Doors" inside a viewport's WorldModel
|
|
30
|
+
is a different group from the Workspace's "Doors", with its own collidability
|
|
31
|
+
matrix. Going through `PhysicsService` -- or hard-coding `workspace` here --
|
|
32
|
+
can only ever reach one of them, which would quietly make this tool lie about
|
|
33
|
+
a place that uses both. So the root is chosen per call; see `root`.
|
|
28
34
|
]]
|
|
29
|
-
local
|
|
35
|
+
local Workspace = game:GetService("Workspace")
|
|
30
36
|
|
|
31
37
|
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
32
38
|
local Paths = require(script.Parent.Parent.Paths)
|
|
@@ -34,6 +40,33 @@ local Undo = require(script.Parent.Parent.Undo)
|
|
|
34
40
|
|
|
35
41
|
local World = {}
|
|
36
42
|
|
|
43
|
+
--[[
|
|
44
|
+
Which world the call is about.
|
|
45
|
+
|
|
46
|
+
Defaults to the Workspace, which is what nearly every caller means. Naming a
|
|
47
|
+
`worldModel` path targets that model's private registry instead -- the
|
|
48
|
+
viewport-preview case -- and anything that is not a WorldRoot is refused by
|
|
49
|
+
name rather than left to fail later inside `RegisterCollisionGroup` with a
|
|
50
|
+
message about a method that does not exist.
|
|
51
|
+
]]
|
|
52
|
+
local function root(params: { [string]: any }): Instance
|
|
53
|
+
local path = params.worldModel
|
|
54
|
+
if typeof(path) ~= "string" or path == "" then
|
|
55
|
+
return Workspace
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
local instance = Paths.resolve(path)
|
|
59
|
+
if not instance:IsA("WorldRoot") then
|
|
60
|
+
Dispatch.fail(
|
|
61
|
+
"BAD_PARAMS",
|
|
62
|
+
string.format("%s is a %s, not a WorldModel.", path, instance.ClassName),
|
|
63
|
+
"Collision groups live on a WorldRoot: the Workspace, or a WorldModel "
|
|
64
|
+
.. "inside a ViewportFrame. Omit `worldModel` for the Workspace."
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
return instance
|
|
68
|
+
end
|
|
69
|
+
|
|
37
70
|
--[[
|
|
38
71
|
Steps the undo stack.
|
|
39
72
|
|
|
@@ -101,18 +134,59 @@ end
|
|
|
101
134
|
]]
|
|
102
135
|
function World.collision(params: { [string]: any }): { [string]: any }
|
|
103
136
|
local action = tostring(params.action or "list")
|
|
137
|
+
local world = root(params) :: any
|
|
104
138
|
|
|
105
139
|
if action == "list" then
|
|
106
140
|
local groups: { { [string]: any } } = {}
|
|
107
141
|
local ok, registered = pcall(function()
|
|
108
|
-
return
|
|
142
|
+
return world:GetRegisteredCollisionGroups()
|
|
109
143
|
end)
|
|
110
144
|
if ok and typeof(registered) == "table" then
|
|
145
|
+
--[[
|
|
146
|
+
Named pairs, not the raw mask.
|
|
147
|
+
|
|
148
|
+
`mask` is the engine's bitfield, and it is not readable: a group
|
|
149
|
+
that passes through only itself reports -3, which says nothing to
|
|
150
|
+
anyone without a calculator and the group ordering to hand. The
|
|
151
|
+
whole reason a collision group exists is "these two do not
|
|
152
|
+
collide", so that is what is listed -- by name, both ways round,
|
|
153
|
+
which is the answer the caller came for.
|
|
154
|
+
|
|
155
|
+
The mask is kept beside it, because a caller comparing against
|
|
156
|
+
something they stored earlier still needs the number.
|
|
157
|
+
]]
|
|
111
158
|
for _, group in registered :: { any } do
|
|
112
|
-
|
|
159
|
+
local passesThrough: { string } = {}
|
|
160
|
+
for _, other in registered :: { any } do
|
|
161
|
+
local okPair, collides = pcall(function()
|
|
162
|
+
return world:CollisionGroupsAreCollidable(group.name, other.name)
|
|
163
|
+
end)
|
|
164
|
+
if okPair and collides == false then
|
|
165
|
+
table.insert(passesThrough, other.name)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
table.sort(passesThrough)
|
|
169
|
+
table.insert(groups, {
|
|
170
|
+
name = group.name,
|
|
171
|
+
mask = group.mask,
|
|
172
|
+
passesThrough = if #passesThrough > 0 then table.concat(passesThrough, ", ") else "nothing",
|
|
173
|
+
})
|
|
113
174
|
end
|
|
114
175
|
end
|
|
115
|
-
|
|
176
|
+
--[[
|
|
177
|
+
The ceiling is reported alongside the groups because it is low --
|
|
178
|
+
32 in practice -- and a caller building groups programmatically has
|
|
179
|
+
no other way to learn it before the registration that fails.
|
|
180
|
+
]]
|
|
181
|
+
local limit: number? = nil
|
|
182
|
+
local okLimit, maximum = pcall(function()
|
|
183
|
+
return world:GetMaxCollisionGroups()
|
|
184
|
+
end)
|
|
185
|
+
if okLimit then
|
|
186
|
+
limit = tonumber(maximum)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
return { groups = groups, world = Paths.of(world), count = #groups, max = limit }
|
|
116
190
|
end
|
|
117
191
|
|
|
118
192
|
local name = params.group
|
|
@@ -122,7 +196,7 @@ function World.collision(params: { [string]: any }): { [string]: any }
|
|
|
122
196
|
|
|
123
197
|
if action == "create" then
|
|
124
198
|
local ok, err = pcall(function()
|
|
125
|
-
|
|
199
|
+
world:RegisterCollisionGroup(name)
|
|
126
200
|
end)
|
|
127
201
|
if not ok then
|
|
128
202
|
-- Already existing is the common case and is not a failure worth
|
|
@@ -131,7 +205,7 @@ function World.collision(params: { [string]: any }): { [string]: any }
|
|
|
131
205
|
Dispatch.fail("COLLISION_FAILED", string.format("Could not create %q: %s", name, tostring(err)))
|
|
132
206
|
end
|
|
133
207
|
end
|
|
134
|
-
return { group = name, created = ok, existed = not ok }
|
|
208
|
+
return { group = name, created = ok, existed = not ok, world = Paths.of(world) }
|
|
135
209
|
end
|
|
136
210
|
|
|
137
211
|
if action == "assign" then
|
|
@@ -154,7 +228,7 @@ function World.collision(params: { [string]: any }): { [string]: any }
|
|
|
154
228
|
end
|
|
155
229
|
end
|
|
156
230
|
end)
|
|
157
|
-
return { group = name, assigned = #assigned, parts = assigned, undoable = undoable }
|
|
231
|
+
return { group = name, assigned = #assigned, parts = assigned, undoable = undoable, world = Paths.of(world) }
|
|
158
232
|
end
|
|
159
233
|
|
|
160
234
|
if action == "remove" then
|
|
@@ -168,12 +242,12 @@ function World.collision(params: { [string]: any }): { [string]: any }
|
|
|
168
242
|
has existed the whole time; it was just never wired up.
|
|
169
243
|
]]
|
|
170
244
|
local ok, err = pcall(function()
|
|
171
|
-
|
|
245
|
+
world:UnregisterCollisionGroup(name)
|
|
172
246
|
end)
|
|
173
247
|
if not ok then
|
|
174
248
|
Dispatch.fail("COLLISION_FAILED", string.format("Could not remove %q: %s", name, tostring(err)))
|
|
175
249
|
end
|
|
176
|
-
return { group = name, removed = true }
|
|
250
|
+
return { group = name, removed = true, world = Paths.of(world) }
|
|
177
251
|
end
|
|
178
252
|
|
|
179
253
|
if action == "collidable" then
|
|
@@ -183,12 +257,12 @@ function World.collision(params: { [string]: any }): { [string]: any }
|
|
|
183
257
|
end
|
|
184
258
|
local collidable = params.collidable ~= false
|
|
185
259
|
local ok, err = pcall(function()
|
|
186
|
-
|
|
260
|
+
world:CollisionGroupSetCollidable(name, other, collidable)
|
|
187
261
|
end)
|
|
188
262
|
if not ok then
|
|
189
263
|
Dispatch.fail("COLLISION_FAILED", string.format("Could not set collidability: %s", tostring(err)))
|
|
190
264
|
end
|
|
191
|
-
return { group = name, with = other, collidable = collidable }
|
|
265
|
+
return { group = name, with = other, collidable = collidable, world = Paths.of(world) }
|
|
192
266
|
end
|
|
193
267
|
|
|
194
268
|
Dispatch.fail("BAD_PARAMS", string.format("unknown collision action %q", action))
|