dominus-cli 2.2.1 → 2.3.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/CHANGELOG.md +30 -0
- package/README.md +16 -2
- package/dist/bridge-daemon.js.map +1 -1
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +299 -16
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -5
- package/plugin/Dominus.rbxmx +500 -12
- package/plugin/src/Building.lua +427 -0
- package/plugin/src/CommandRouter.lua +25 -0
- package/plugin/src/Mutation.lua +30 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dominus-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Dominus 2: a secure, MCP-native Roblox Studio engineering agent bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"dominus-mcp": "
|
|
8
|
-
"dominus-install-plugin": "
|
|
7
|
+
"dominus-mcp": "dist/mcp.js",
|
|
8
|
+
"dominus-install-plugin": "dist/install-plugin.js"
|
|
9
9
|
},
|
|
10
|
-
"main": "
|
|
10
|
+
"main": "dist/mcp.js",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "tsx src/mcp/index.ts",
|
|
13
13
|
"dev:mcp": "tsx src/mcp/index.ts",
|
|
@@ -76,6 +76,6 @@
|
|
|
76
76
|
"license": "MIT",
|
|
77
77
|
"repository": {
|
|
78
78
|
"type": "git",
|
|
79
|
-
"url": "https://github.com/eli/dominus"
|
|
79
|
+
"url": "git+https://github.com/eli/dominus.git"
|
|
80
80
|
}
|
|
81
81
|
}
|
package/plugin/Dominus.rbxmx
CHANGED
|
@@ -256,9 +256,443 @@ return "__DOMINUS_BRIDGE_TOKEN__"
|
|
|
256
256
|
</Properties>
|
|
257
257
|
</Item>
|
|
258
258
|
<Item class="ModuleScript" referent="2">
|
|
259
|
+
<Properties>
|
|
260
|
+
<string name="Name">Building</string>
|
|
261
|
+
<string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
262
|
+
local Selection = game:GetService("Selection")
|
|
263
|
+
|
|
264
|
+
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
265
|
+
local ValueCodec = require(script.Parent.ValueCodec)
|
|
266
|
+
|
|
267
|
+
local Building = {}
|
|
268
|
+
|
|
269
|
+
local MAX_ITEMS = 200
|
|
270
|
+
local MAX_PROPERTIES = 100
|
|
271
|
+
|
|
272
|
+
local ALLOWED_PART_CLASSES = {
|
|
273
|
+
Part = true,
|
|
274
|
+
WedgePart = true,
|
|
275
|
+
CornerWedgePart = true,
|
|
276
|
+
TrussPart = true,
|
|
277
|
+
MeshPart = true,
|
|
278
|
+
SpawnLocation = true,
|
|
279
|
+
Seat = true,
|
|
280
|
+
VehicleSeat = true,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
local function resolve(target, label)
|
|
284
|
+
local instance, err = InstanceRegistry.resolve(target)
|
|
285
|
+
if not instance then
|
|
286
|
+
error(label .. ": " .. tostring(err))
|
|
287
|
+
end
|
|
288
|
+
return instance
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
local function cleanup(instances)
|
|
292
|
+
for _, instance in instances do
|
|
293
|
+
pcall(function()
|
|
294
|
+
if instance.Parent == nil then
|
|
295
|
+
instance:Destroy()
|
|
296
|
+
end
|
|
297
|
+
end)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
local function record(name, callback, created)
|
|
302
|
+
local recording = ChangeHistoryService:TryBeginRecording(name)
|
|
303
|
+
if not recording then
|
|
304
|
+
return { success = false, error = "Another plugin recording is already active" }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
local ok, result = pcall(callback)
|
|
308
|
+
if not ok then
|
|
309
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
310
|
+
cleanup(created or {})
|
|
311
|
+
return { success = false, error = tostring(result), rolledBack = true }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
315
|
+
result.success = true
|
|
316
|
+
return result
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
local function finiteNumber(value, label)
|
|
320
|
+
assert(type(value) == "number" and value == value and math.abs(value) < math.huge, label .. " must be finite")
|
|
321
|
+
return value
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
local function vector3(value, label, default)
|
|
325
|
+
if value == nil then
|
|
326
|
+
return default
|
|
327
|
+
end
|
|
328
|
+
assert(type(value) == "table", label .. " must be a Vector3 object")
|
|
329
|
+
return Vector3.new(
|
|
330
|
+
finiteNumber(value.X or value.x or value[1], label .. ".x"),
|
|
331
|
+
finiteNumber(value.Y or value.y or value[2], label .. ".y"),
|
|
332
|
+
finiteNumber(value.Z or value.z or value[3], label .. ".z")
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
local function rotation(value, label)
|
|
337
|
+
local degrees = vector3(value, label, Vector3.zero)
|
|
338
|
+
return CFrame.fromOrientation(math.rad(degrees.X), math.rad(degrees.Y), math.rad(degrees.Z))
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
local function setProperties(instance, properties)
|
|
342
|
+
if properties == nil then
|
|
343
|
+
return {}
|
|
344
|
+
end
|
|
345
|
+
assert(type(properties) == "table", "properties must be an object")
|
|
346
|
+
local count = 0
|
|
347
|
+
local applied = {}
|
|
348
|
+
for propertyName, value in properties do
|
|
349
|
+
count += 1
|
|
350
|
+
if count > MAX_PROPERTIES then
|
|
351
|
+
error("At most " .. MAX_PROPERTIES .. " properties are allowed per item")
|
|
352
|
+
end
|
|
353
|
+
applied[propertyName] = ValueCodec.setProperty(instance, propertyName, value)
|
|
354
|
+
end
|
|
355
|
+
return applied
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
local function validateItemCount(items, label, maximum)
|
|
359
|
+
assert(type(items) == "table" and #items > 0, label .. " must contain at least one item")
|
|
360
|
+
assert(#items <= maximum, label .. " is limited to " .. maximum .. " items")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
local function assertMovable(instance, label)
|
|
364
|
+
assert(instance ~= game and instance.Parent ~= game, label .. " cannot be a DataModel service")
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
function Building.createParts(spec)
|
|
368
|
+
validateItemCount(spec.parts, "parts", MAX_ITEMS)
|
|
369
|
+
local parent = spec.parent and resolve(spec.parent, "parent") or workspace
|
|
370
|
+
local group = nil
|
|
371
|
+
local created = {}
|
|
372
|
+
|
|
373
|
+
return record("Dominus 2: Build parts", function()
|
|
374
|
+
if spec.groupName ~= nil then
|
|
375
|
+
assert(type(spec.groupName) == "string" and #spec.groupName > 0, "groupName must be a non-empty string")
|
|
376
|
+
local groupClass = spec.groupClass or "Model"
|
|
377
|
+
assert(groupClass == "Model" or groupClass == "Folder", "groupClass must be Model or Folder")
|
|
378
|
+
group = Instance.new(groupClass)
|
|
379
|
+
group.Name = spec.groupName
|
|
380
|
+
table.insert(created, group)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
local parts = {}
|
|
384
|
+
for index, item in spec.parts do
|
|
385
|
+
assert(type(item) == "table", "Part " .. index .. " must be an object")
|
|
386
|
+
assert(type(item.name) == "string" and #item.name > 0, "Part " .. index .. ".name must be a non-empty string")
|
|
387
|
+
local className = item.className or "Part"
|
|
388
|
+
if item.shape == "Wedge" then
|
|
389
|
+
className = "WedgePart"
|
|
390
|
+
elseif item.shape == "CornerWedge" then
|
|
391
|
+
className = "CornerWedgePart"
|
|
392
|
+
end
|
|
393
|
+
assert(ALLOWED_PART_CLASSES[className] == true, "Part " .. index .. " uses an unsupported class: " .. tostring(className))
|
|
394
|
+
|
|
395
|
+
local part = Instance.new(className)
|
|
396
|
+
table.insert(created, part)
|
|
397
|
+
part.Name = item.name
|
|
398
|
+
if part:IsA("BasePart") then
|
|
399
|
+
part.Anchored = item.anchored ~= false
|
|
400
|
+
end
|
|
401
|
+
if item.position then
|
|
402
|
+
part.Position = vector3(item.position, "Part " .. index .. ".position")
|
|
403
|
+
end
|
|
404
|
+
if item.orientation then
|
|
405
|
+
part.Orientation = vector3(item.orientation, "Part " .. index .. ".orientation")
|
|
406
|
+
end
|
|
407
|
+
if item.size then
|
|
408
|
+
part.Size = vector3(item.size, "Part " .. index .. ".size")
|
|
409
|
+
end
|
|
410
|
+
if item.color then
|
|
411
|
+
ValueCodec.setProperty(part, "Color", item.color)
|
|
412
|
+
end
|
|
413
|
+
if item.material then
|
|
414
|
+
ValueCodec.setProperty(part, "Material", item.material)
|
|
415
|
+
end
|
|
416
|
+
if item.transparency ~= nil then
|
|
417
|
+
part.Transparency = finiteNumber(item.transparency, "Part " .. index .. ".transparency")
|
|
418
|
+
end
|
|
419
|
+
if item.canCollide ~= nil then
|
|
420
|
+
assert(type(item.canCollide) == "boolean", "Part " .. index .. ".canCollide must be a boolean")
|
|
421
|
+
part.CanCollide = item.canCollide
|
|
422
|
+
end
|
|
423
|
+
if item.shape and className == "Part" then
|
|
424
|
+
ValueCodec.setProperty(part, "Shape", item.shape)
|
|
425
|
+
end
|
|
426
|
+
setProperties(part, item.properties)
|
|
427
|
+
part.Parent = group or parent
|
|
428
|
+
table.insert(parts, part)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
if group then
|
|
432
|
+
group.Parent = parent
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
local refs = {}
|
|
436
|
+
for _, part in parts do
|
|
437
|
+
table.insert(refs, InstanceRegistry.toRef(part))
|
|
438
|
+
end
|
|
439
|
+
return {
|
|
440
|
+
created = refs,
|
|
441
|
+
group = group and InstanceRegistry.toRef(group) or nil,
|
|
442
|
+
count = #refs,
|
|
443
|
+
}
|
|
444
|
+
end, created)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
function Building.cloneInstances(spec)
|
|
448
|
+
validateItemCount(spec.requests, "requests", 100)
|
|
449
|
+
local created = {}
|
|
450
|
+
|
|
451
|
+
return record("Dominus 2: Clone instances", function()
|
|
452
|
+
local clones = {}
|
|
453
|
+
local totalCopies = 0
|
|
454
|
+
for requestIndex, request in spec.requests do
|
|
455
|
+
assert(type(request) == "table", "Clone request " .. requestIndex .. " must be an object")
|
|
456
|
+
local source = resolve(request.target, "Clone request " .. requestIndex .. " target")
|
|
457
|
+
assertMovable(source, "Clone source")
|
|
458
|
+
local parent = request.parent and resolve(request.parent, "Clone request " .. requestIndex .. " parent") or source.Parent
|
|
459
|
+
assert(parent ~= nil, "Clone request " .. requestIndex .. " source has no parent")
|
|
460
|
+
local count = request.count or 1
|
|
461
|
+
assert(type(count) == "number" and count % 1 == 0 and count >= 1 and count <= 50, "count must be an integer from 1 to 50")
|
|
462
|
+
totalCopies += count
|
|
463
|
+
assert(totalCopies <= MAX_ITEMS, "A clone batch is limited to " .. MAX_ITEMS .. " copies")
|
|
464
|
+
local offset = vector3(request.offset, "Clone request " .. requestIndex .. ".offset", Vector3.zero)
|
|
465
|
+
local stepOffset = vector3(request.stepOffset, "Clone request " .. requestIndex .. ".stepOffset", Vector3.zero)
|
|
466
|
+
|
|
467
|
+
for copyIndex = 1, count do
|
|
468
|
+
local clone = source:Clone()
|
|
469
|
+
assert(clone ~= nil, "Clone request " .. requestIndex .. " target is not Archivable")
|
|
470
|
+
table.insert(created, clone)
|
|
471
|
+
if request.name then
|
|
472
|
+
clone.Name = count == 1 and request.name or (request.name .. " " .. copyIndex)
|
|
473
|
+
end
|
|
474
|
+
setProperties(clone, request.properties)
|
|
475
|
+
if clone:IsA("PVInstance") then
|
|
476
|
+
local totalOffset = offset + stepOffset * (copyIndex - 1)
|
|
477
|
+
clone:PivotTo(clone:GetPivot() + totalOffset)
|
|
478
|
+
elseif offset.Magnitude > 0 or stepOffset.Magnitude > 0 then
|
|
479
|
+
error("Clone request " .. requestIndex .. " cannot offset a non-PVInstance")
|
|
480
|
+
end
|
|
481
|
+
clone.Parent = parent
|
|
482
|
+
table.insert(clones, InstanceRegistry.toRef(clone))
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
return { clones = clones, count = #clones }
|
|
486
|
+
end, created)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
local function validateIndependentTargets(targets)
|
|
490
|
+
for leftIndex, left in targets do
|
|
491
|
+
for rightIndex = leftIndex + 1, #targets do
|
|
492
|
+
local right = targets[rightIndex]
|
|
493
|
+
assert(not left:IsAncestorOf(right) and not right:IsAncestorOf(left), "Targets " .. leftIndex .. " and " .. rightIndex .. " overlap")
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
function Building.groupInstances(spec)
|
|
499
|
+
validateItemCount(spec.targets, "targets", 100)
|
|
500
|
+
assert(type(spec.name) == "string" and #spec.name > 0, "name must be a non-empty string")
|
|
501
|
+
local className = spec.className or "Model"
|
|
502
|
+
assert(className == "Model" or className == "Folder", "className must be Model or Folder")
|
|
503
|
+
local targets = {}
|
|
504
|
+
for index, targetRef in spec.targets do
|
|
505
|
+
local target = resolve(targetRef, "Target " .. index)
|
|
506
|
+
assertMovable(target, "Target " .. index)
|
|
507
|
+
table.insert(targets, target)
|
|
508
|
+
end
|
|
509
|
+
validateIndependentTargets(targets)
|
|
510
|
+
local parent = spec.parent and resolve(spec.parent, "parent") or targets[1].Parent
|
|
511
|
+
assert(parent ~= nil, "The first target has no parent")
|
|
512
|
+
local created = {}
|
|
513
|
+
|
|
514
|
+
return record("Dominus 2: Group instances", function()
|
|
515
|
+
local group = Instance.new(className)
|
|
516
|
+
table.insert(created, group)
|
|
517
|
+
group.Name = spec.name
|
|
518
|
+
group.Parent = parent
|
|
519
|
+
for _, target in targets do
|
|
520
|
+
assert(not target:IsAncestorOf(group), "Cannot group an ancestor into its descendant")
|
|
521
|
+
target.Parent = group
|
|
522
|
+
end
|
|
523
|
+
return { group = InstanceRegistry.toRef(group), count = #targets }
|
|
524
|
+
end, created)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
function Building.ungroupInstances(spec)
|
|
528
|
+
assert(spec.confirm == true, "confirm must be true")
|
|
529
|
+
validateItemCount(spec.targets, "targets", 20)
|
|
530
|
+
|
|
531
|
+
return record("Dominus 2: Ungroup instances", function()
|
|
532
|
+
local moved = {}
|
|
533
|
+
local removed = {}
|
|
534
|
+
for index, targetRef in spec.targets do
|
|
535
|
+
local group = resolve(targetRef, "Group " .. index)
|
|
536
|
+
assert(group:IsA("Model") or group:IsA("Folder"), "Group " .. index .. " must be a Model or Folder")
|
|
537
|
+
assertMovable(group, "Group " .. index)
|
|
538
|
+
local parent = spec.parent and resolve(spec.parent, "parent") or group.Parent
|
|
539
|
+
assert(parent ~= nil, "Group " .. index .. " has no parent")
|
|
540
|
+
for _, child in group:GetChildren() do
|
|
541
|
+
child.Parent = parent
|
|
542
|
+
table.insert(moved, InstanceRegistry.toRef(child))
|
|
543
|
+
end
|
|
544
|
+
table.insert(removed, { name = group.Name, className = group.ClassName })
|
|
545
|
+
group:Destroy()
|
|
546
|
+
end
|
|
547
|
+
return { moved = moved, removedGroups = removed, count = #moved }
|
|
548
|
+
end)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
local function absolutePivot(current, operation)
|
|
552
|
+
if operation.cframe then
|
|
553
|
+
return ValueCodec.decodeForCurrent(current, operation.cframe)
|
|
554
|
+
end
|
|
555
|
+
local position = vector3(operation.position, "position", current.Position)
|
|
556
|
+
local x, y, z = current:ToOrientation()
|
|
557
|
+
if operation.orientation then
|
|
558
|
+
local degrees = vector3(operation.orientation, "orientation")
|
|
559
|
+
x, y, z = math.rad(degrees.X), math.rad(degrees.Y), math.rad(degrees.Z)
|
|
560
|
+
end
|
|
561
|
+
return CFrame.new(position) * CFrame.fromOrientation(x, y, z)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
function Building.transformInstances(spec)
|
|
565
|
+
validateItemCount(spec.operations, "operations", 100)
|
|
566
|
+
|
|
567
|
+
return record("Dominus 2: Transform instances", function()
|
|
568
|
+
local results = {}
|
|
569
|
+
for index, operation in spec.operations do
|
|
570
|
+
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
571
|
+
assert(target:IsA("PVInstance"), "Operation " .. index .. " target must be a Model or BasePart")
|
|
572
|
+
local current = target:GetPivot()
|
|
573
|
+
local mode = operation.mode or "absolute"
|
|
574
|
+
local nextPivot
|
|
575
|
+
if mode == "absolute" then
|
|
576
|
+
nextPivot = absolutePivot(current, operation)
|
|
577
|
+
elseif mode == "relative" then
|
|
578
|
+
local offset = vector3(operation.offset, "Operation " .. index .. ".offset", Vector3.zero)
|
|
579
|
+
local rotate = rotation(operation.rotationOffset, "Operation " .. index .. ".rotationOffset")
|
|
580
|
+
if operation.space == "local" then
|
|
581
|
+
nextPivot = current * CFrame.new(offset) * rotate
|
|
582
|
+
else
|
|
583
|
+
nextPivot = CFrame.new(offset) * current * rotate
|
|
584
|
+
end
|
|
585
|
+
else
|
|
586
|
+
error("Operation " .. index .. ".mode must be absolute or relative")
|
|
587
|
+
end
|
|
588
|
+
target:PivotTo(nextPivot)
|
|
589
|
+
table.insert(results, { target = InstanceRegistry.toRef(target), pivot = ValueCodec.encode(target:GetPivot()) })
|
|
590
|
+
end
|
|
591
|
+
return { transformed = results, count = #results }
|
|
592
|
+
end)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
function Building.createWelds(spec)
|
|
596
|
+
validateItemCount(spec.welds, "welds", MAX_ITEMS)
|
|
597
|
+
local created = {}
|
|
598
|
+
|
|
599
|
+
return record("Dominus 2: Create welds", function()
|
|
600
|
+
local welds = {}
|
|
601
|
+
for index, item in spec.welds do
|
|
602
|
+
local part0 = resolve(item.part0, "Weld " .. index .. " part0")
|
|
603
|
+
local part1 = resolve(item.part1, "Weld " .. index .. " part1")
|
|
604
|
+
assert(part0:IsA("BasePart") and part1:IsA("BasePart"), "Weld " .. index .. " endpoints must be BaseParts")
|
|
605
|
+
assert(part0 ~= part1, "Weld " .. index .. " endpoints must be different")
|
|
606
|
+
local parent = item.parent and resolve(item.parent, "Weld " .. index .. " parent") or part0
|
|
607
|
+
local weld = Instance.new("WeldConstraint")
|
|
608
|
+
table.insert(created, weld)
|
|
609
|
+
weld.Name = item.name or (part0.Name .. "_to_" .. part1.Name)
|
|
610
|
+
weld.Part0 = part0
|
|
611
|
+
weld.Part1 = part1
|
|
612
|
+
weld.Parent = parent
|
|
613
|
+
table.insert(welds, InstanceRegistry.toRef(weld))
|
|
614
|
+
end
|
|
615
|
+
return { welds = welds, count = #welds }
|
|
616
|
+
end, created)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
local function overlapParams(spec)
|
|
620
|
+
local params = OverlapParams.new()
|
|
621
|
+
local maxParts = spec.maxParts or 200
|
|
622
|
+
assert(type(maxParts) == "number" and maxParts % 1 == 0 and maxParts >= 1 and maxParts <= 500, "maxParts must be an integer from 1 to 500")
|
|
623
|
+
params.MaxParts = maxParts
|
|
624
|
+
params.RespectCanCollide = spec.respectCanCollide == true
|
|
625
|
+
if spec.filter and #spec.filter > 0 then
|
|
626
|
+
local instances = {}
|
|
627
|
+
for index, targetRef in spec.filter do
|
|
628
|
+
table.insert(instances, resolve(targetRef, "Filter " .. index))
|
|
629
|
+
end
|
|
630
|
+
params.FilterDescendantsInstances = instances
|
|
631
|
+
params.FilterType = spec.filterType == "Include" and Enum.RaycastFilterType.Include or Enum.RaycastFilterType.Exclude
|
|
632
|
+
end
|
|
633
|
+
return params
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
function Building.queryParts(spec)
|
|
637
|
+
local root = spec.root and resolve(spec.root, "root") or workspace
|
|
638
|
+
assert(root:IsA("WorldRoot"), "root must be Workspace or a WorldModel")
|
|
639
|
+
local mode = spec.mode or "box"
|
|
640
|
+
local found
|
|
641
|
+
if mode == "box" then
|
|
642
|
+
local position = vector3(spec.position, "position")
|
|
643
|
+
local size = vector3(spec.size, "size")
|
|
644
|
+
assert(size.X > 0 and size.Y > 0 and size.Z > 0, "size components must be greater than zero")
|
|
645
|
+
local box = CFrame.new(position) * rotation(spec.orientation, "orientation")
|
|
646
|
+
found = root:GetPartBoundsInBox(box, size, overlapParams(spec))
|
|
647
|
+
elseif mode == "radius" then
|
|
648
|
+
local position = vector3(spec.position, "position")
|
|
649
|
+
local radius = finiteNumber(spec.radius, "radius")
|
|
650
|
+
assert(radius > 0, "radius must be greater than zero")
|
|
651
|
+
found = root:GetPartBoundsInRadius(position, radius, overlapParams(spec))
|
|
652
|
+
else
|
|
653
|
+
error("mode must be box or radius")
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
table.sort(found, function(left, right)
|
|
657
|
+
return InstanceRegistry.getDisplayPath(left) < InstanceRegistry.getDisplayPath(right)
|
|
658
|
+
end)
|
|
659
|
+
local parts = {}
|
|
660
|
+
for _, part in found do
|
|
661
|
+
table.insert(parts, {
|
|
662
|
+
name = part.Name,
|
|
663
|
+
className = part.ClassName,
|
|
664
|
+
ref = InstanceRegistry.toRef(part),
|
|
665
|
+
position = ValueCodec.encode(part.Position),
|
|
666
|
+
size = ValueCodec.encode(part.Size),
|
|
667
|
+
})
|
|
668
|
+
end
|
|
669
|
+
return { success = true, parts = parts, count = #parts, mode = mode }
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
function Building.setSelection(spec)
|
|
673
|
+
assert(type(spec.targets) == "table", "targets must be an array")
|
|
674
|
+
assert(#spec.targets <= 200, "At most 200 instances can be selected")
|
|
675
|
+
local targets = {}
|
|
676
|
+
for index, targetRef in spec.targets do
|
|
677
|
+
table.insert(targets, resolve(targetRef, "Target " .. index))
|
|
678
|
+
end
|
|
679
|
+
Selection:Set(targets)
|
|
680
|
+
local selected = {}
|
|
681
|
+
for _, target in targets do
|
|
682
|
+
table.insert(selected, InstanceRegistry.toRef(target))
|
|
683
|
+
end
|
|
684
|
+
return { success = true, selection = selected, count = #selected }
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
return Building
|
|
688
|
+
]]></string>
|
|
689
|
+
</Properties>
|
|
690
|
+
</Item>
|
|
691
|
+
<Item class="ModuleScript" referent="3">
|
|
259
692
|
<Properties>
|
|
260
693
|
<string name="Name">CommandRouter</string>
|
|
261
|
-
<string name="Source"><![CDATA[local
|
|
694
|
+
<string name="Source"><![CDATA[local Building = require(script.Parent.Building)
|
|
695
|
+
local Explorer = require(script.Parent.Explorer)
|
|
262
696
|
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
263
697
|
local Mutation = require(script.Parent.Mutation)
|
|
264
698
|
local Properties = require(script.Parent.Properties)
|
|
@@ -297,6 +731,30 @@ local handlers = {
|
|
|
297
731
|
["studio:v2:apply"] = function(payload)
|
|
298
732
|
return Mutation.apply(payload)
|
|
299
733
|
end,
|
|
734
|
+
["studio:v2:create_parts"] = function(payload)
|
|
735
|
+
return Building.createParts(payload)
|
|
736
|
+
end,
|
|
737
|
+
["studio:v2:clone_instances"] = function(payload)
|
|
738
|
+
return Building.cloneInstances(payload)
|
|
739
|
+
end,
|
|
740
|
+
["studio:v2:group_instances"] = function(payload)
|
|
741
|
+
return Building.groupInstances(payload)
|
|
742
|
+
end,
|
|
743
|
+
["studio:v2:ungroup_instances"] = function(payload)
|
|
744
|
+
return Building.ungroupInstances(payload)
|
|
745
|
+
end,
|
|
746
|
+
["studio:v2:transform_instances"] = function(payload)
|
|
747
|
+
return Building.transformInstances(payload)
|
|
748
|
+
end,
|
|
749
|
+
["studio:v2:create_welds"] = function(payload)
|
|
750
|
+
return Building.createWelds(payload)
|
|
751
|
+
end,
|
|
752
|
+
["studio:v2:query_parts"] = function(payload)
|
|
753
|
+
return Building.queryParts(payload)
|
|
754
|
+
end,
|
|
755
|
+
["studio:v2:set_selection"] = function(payload)
|
|
756
|
+
return Building.setSelection(payload)
|
|
757
|
+
end,
|
|
300
758
|
["studio:v2:delete"] = function(payload)
|
|
301
759
|
return Mutation.delete(payload)
|
|
302
760
|
end,
|
|
@@ -355,7 +813,7 @@ return CommandRouter
|
|
|
355
813
|
]]></string>
|
|
356
814
|
</Properties>
|
|
357
815
|
</Item>
|
|
358
|
-
<Item class="ModuleScript" referent="
|
|
816
|
+
<Item class="ModuleScript" referent="4">
|
|
359
817
|
<Properties>
|
|
360
818
|
<string name="Name">Explorer</string>
|
|
361
819
|
<string name="Source"><![CDATA[--[[
|
|
@@ -891,7 +1349,7 @@ return Explorer
|
|
|
891
1349
|
]]></string>
|
|
892
1350
|
</Properties>
|
|
893
1351
|
</Item>
|
|
894
|
-
<Item class="ModuleScript" referent="
|
|
1352
|
+
<Item class="ModuleScript" referent="5">
|
|
895
1353
|
<Properties>
|
|
896
1354
|
<string name="Name">InstanceRegistry</string>
|
|
897
1355
|
<string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
|
|
@@ -1019,7 +1477,7 @@ return InstanceRegistry
|
|
|
1019
1477
|
]]></string>
|
|
1020
1478
|
</Properties>
|
|
1021
1479
|
</Item>
|
|
1022
|
-
<Item class="ModuleScript" referent="
|
|
1480
|
+
<Item class="ModuleScript" referent="6">
|
|
1023
1481
|
<Properties>
|
|
1024
1482
|
<string name="Name">Mutation</string>
|
|
1025
1483
|
<string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
@@ -1068,6 +1526,7 @@ function Mutation.apply(spec)
|
|
|
1068
1526
|
end
|
|
1069
1527
|
|
|
1070
1528
|
local results = {}
|
|
1529
|
+
local created = {}
|
|
1071
1530
|
local ok, err = pcall(function()
|
|
1072
1531
|
for index, operation in operations do
|
|
1073
1532
|
assert(type(operation) == "table", "Operation " .. index .. " must be an object")
|
|
@@ -1083,12 +1542,34 @@ function Mutation.apply(spec)
|
|
|
1083
1542
|
assert(type(operation.className) == "string", "create.className must be a string")
|
|
1084
1543
|
assert(type(operation.name) == "string", "create.name must be a string")
|
|
1085
1544
|
local instance = Instance.new(operation.className)
|
|
1545
|
+
table.insert(created, instance)
|
|
1086
1546
|
instance.Name = operation.name
|
|
1087
1547
|
if operation.properties then
|
|
1088
1548
|
setProperties(instance, operation.properties)
|
|
1089
1549
|
end
|
|
1090
1550
|
instance.Parent = parent
|
|
1091
1551
|
table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(instance) })
|
|
1552
|
+
elseif operation.op == "clone" then
|
|
1553
|
+
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
1554
|
+
assert(target ~= game and target.Parent ~= game, "Services cannot be cloned")
|
|
1555
|
+
local parent = operation.parent and resolve(operation.parent, "Operation " .. index .. " parent") or target.Parent
|
|
1556
|
+
assert(parent ~= nil, "Clone target has no parent")
|
|
1557
|
+
local clone = target:Clone()
|
|
1558
|
+
assert(clone ~= nil, "Clone target is not Archivable")
|
|
1559
|
+
table.insert(created, clone)
|
|
1560
|
+
if operation.name then
|
|
1561
|
+
clone.Name = operation.name
|
|
1562
|
+
end
|
|
1563
|
+
if operation.properties then
|
|
1564
|
+
setProperties(clone, operation.properties)
|
|
1565
|
+
end
|
|
1566
|
+
if operation.offset then
|
|
1567
|
+
assert(clone:IsA("PVInstance"), "Only Models and BaseParts can be spatially offset")
|
|
1568
|
+
local offset = ValueCodec.decodeForCurrent(Vector3.zero, operation.offset)
|
|
1569
|
+
clone:PivotTo(clone:GetPivot() + offset)
|
|
1570
|
+
end
|
|
1571
|
+
clone.Parent = parent
|
|
1572
|
+
table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(clone) })
|
|
1092
1573
|
elseif operation.op == "move" then
|
|
1093
1574
|
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
1094
1575
|
local parent = resolve(operation.parent, "Operation " .. index .. " parent")
|
|
@@ -1103,6 +1584,13 @@ function Mutation.apply(spec)
|
|
|
1103
1584
|
|
|
1104
1585
|
if not ok then
|
|
1105
1586
|
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
1587
|
+
for _, instance in created do
|
|
1588
|
+
pcall(function()
|
|
1589
|
+
if instance.Parent == nil then
|
|
1590
|
+
instance:Destroy()
|
|
1591
|
+
end
|
|
1592
|
+
end)
|
|
1593
|
+
end
|
|
1106
1594
|
return { success = false, error = tostring(err), rolledBack = true }
|
|
1107
1595
|
end
|
|
1108
1596
|
|
|
@@ -1160,7 +1648,7 @@ return Mutation
|
|
|
1160
1648
|
]]></string>
|
|
1161
1649
|
</Properties>
|
|
1162
1650
|
</Item>
|
|
1163
|
-
<Item class="ModuleScript" referent="
|
|
1651
|
+
<Item class="ModuleScript" referent="7">
|
|
1164
1652
|
<Properties>
|
|
1165
1653
|
<string name="Name">Properties</string>
|
|
1166
1654
|
<string name="Source"><![CDATA[--[[
|
|
@@ -2025,7 +2513,7 @@ return Properties
|
|
|
2025
2513
|
]]></string>
|
|
2026
2514
|
</Properties>
|
|
2027
2515
|
</Item>
|
|
2028
|
-
<Item class="ModuleScript" referent="
|
|
2516
|
+
<Item class="ModuleScript" referent="8">
|
|
2029
2517
|
<Properties>
|
|
2030
2518
|
<string name="Name">Protocol</string>
|
|
2031
2519
|
<string name="Source"><![CDATA[--[[
|
|
@@ -2061,7 +2549,7 @@ return Protocol
|
|
|
2061
2549
|
]]></string>
|
|
2062
2550
|
</Properties>
|
|
2063
2551
|
</Item>
|
|
2064
|
-
<Item class="ModuleScript" referent="
|
|
2552
|
+
<Item class="ModuleScript" referent="9">
|
|
2065
2553
|
<Properties>
|
|
2066
2554
|
<string name="Name">Reflection</string>
|
|
2067
2555
|
<string name="Source"><![CDATA[local ReflectionService = game:GetService("ReflectionService")
|
|
@@ -2236,7 +2724,7 @@ return Reflection
|
|
|
2236
2724
|
]]></string>
|
|
2237
2725
|
</Properties>
|
|
2238
2726
|
</Item>
|
|
2239
|
-
<Item class="ModuleScript" referent="
|
|
2727
|
+
<Item class="ModuleScript" referent="10">
|
|
2240
2728
|
<Properties>
|
|
2241
2729
|
<string name="Name">TestRunner</string>
|
|
2242
2730
|
<string name="Source"><![CDATA[local StudioTestService = game:GetService("StudioTestService")
|
|
@@ -2311,7 +2799,7 @@ return TestRunner
|
|
|
2311
2799
|
]]></string>
|
|
2312
2800
|
</Properties>
|
|
2313
2801
|
</Item>
|
|
2314
|
-
<Item class="ModuleScript" referent="
|
|
2802
|
+
<Item class="ModuleScript" referent="11">
|
|
2315
2803
|
<Properties>
|
|
2316
2804
|
<string name="Name">UIBuilder</string>
|
|
2317
2805
|
<string name="Source"><![CDATA[--[[
|
|
@@ -3174,7 +3662,7 @@ return UIBuilder
|
|
|
3174
3662
|
]]></string>
|
|
3175
3663
|
</Properties>
|
|
3176
3664
|
</Item>
|
|
3177
|
-
<Item class="ModuleScript" referent="
|
|
3665
|
+
<Item class="ModuleScript" referent="12">
|
|
3178
3666
|
<Properties>
|
|
3179
3667
|
<string name="Name">ValueCodec</string>
|
|
3180
3668
|
<string name="Source"><![CDATA[local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
@@ -3411,7 +3899,7 @@ return ValueCodec
|
|
|
3411
3899
|
]]></string>
|
|
3412
3900
|
</Properties>
|
|
3413
3901
|
</Item>
|
|
3414
|
-
<Item class="ModuleScript" referent="
|
|
3902
|
+
<Item class="ModuleScript" referent="13">
|
|
3415
3903
|
<Properties>
|
|
3416
3904
|
<string name="Name">Watcher</string>
|
|
3417
3905
|
<string name="Source"><![CDATA[--[[
|
|
@@ -3502,7 +3990,7 @@ return Watcher
|
|
|
3502
3990
|
]]></string>
|
|
3503
3991
|
</Properties>
|
|
3504
3992
|
</Item>
|
|
3505
|
-
<Item class="ModuleScript" referent="
|
|
3993
|
+
<Item class="ModuleScript" referent="14">
|
|
3506
3994
|
<Properties>
|
|
3507
3995
|
<string name="Name">WsClient</string>
|
|
3508
3996
|
<string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
|