dominus-cli 2.2.0 → 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 +48 -0
- package/README.md +34 -12
- package/dist/bridge-daemon.js +581 -0
- package/dist/bridge-daemon.js.map +1 -0
- package/dist/install-plugin.js +497 -27
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +417 -479
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -5
- package/plugin/Dominus.rbxmx +507 -15
- package/plugin/default.project.json +4 -1
- package/plugin/src/Building.lua +427 -0
- package/plugin/src/CommandRouter.lua +25 -0
- package/plugin/src/Mutation.lua +30 -0
- package/plugin/src/init.server.lua +5 -1
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
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
<Item class="Script" referent="0">
|
|
3
3
|
<Properties>
|
|
4
4
|
<string name="Name">Dominus</string>
|
|
5
|
-
<token name="RunContext">
|
|
6
|
-
<string name="Source"><![CDATA[
|
|
5
|
+
<token name="RunContext">3</token>
|
|
6
|
+
<string name="Source"><![CDATA[if script.RunContext ~= Enum.RunContext.Plugin then
|
|
7
|
+
return
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
local HttpService = game:GetService("HttpService")
|
|
7
11
|
|
|
8
12
|
local EmbeddedBridgeToken = require(script.BridgeConfig)
|
|
9
13
|
local CommandRouter = require(script.CommandRouter)
|
|
@@ -121,7 +125,7 @@ local function handleMessage(rawMessage)
|
|
|
121
125
|
end
|
|
122
126
|
|
|
123
127
|
if message.type == "server:auth_rejected" then
|
|
124
|
-
warn("[Dominus 2] Authentication failed. Run dominus-install-plugin
|
|
128
|
+
warn("[Dominus 2] Authentication failed. Run dominus-install-plugin to refresh the local plugin.")
|
|
125
129
|
WsClient.disconnect()
|
|
126
130
|
return
|
|
127
131
|
end
|
|
@@ -252,9 +256,443 @@ return "__DOMINUS_BRIDGE_TOKEN__"
|
|
|
252
256
|
</Properties>
|
|
253
257
|
</Item>
|
|
254
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">
|
|
255
692
|
<Properties>
|
|
256
693
|
<string name="Name">CommandRouter</string>
|
|
257
|
-
<string name="Source"><![CDATA[local
|
|
694
|
+
<string name="Source"><![CDATA[local Building = require(script.Parent.Building)
|
|
695
|
+
local Explorer = require(script.Parent.Explorer)
|
|
258
696
|
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
259
697
|
local Mutation = require(script.Parent.Mutation)
|
|
260
698
|
local Properties = require(script.Parent.Properties)
|
|
@@ -293,6 +731,30 @@ local handlers = {
|
|
|
293
731
|
["studio:v2:apply"] = function(payload)
|
|
294
732
|
return Mutation.apply(payload)
|
|
295
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,
|
|
296
758
|
["studio:v2:delete"] = function(payload)
|
|
297
759
|
return Mutation.delete(payload)
|
|
298
760
|
end,
|
|
@@ -351,7 +813,7 @@ return CommandRouter
|
|
|
351
813
|
]]></string>
|
|
352
814
|
</Properties>
|
|
353
815
|
</Item>
|
|
354
|
-
<Item class="ModuleScript" referent="
|
|
816
|
+
<Item class="ModuleScript" referent="4">
|
|
355
817
|
<Properties>
|
|
356
818
|
<string name="Name">Explorer</string>
|
|
357
819
|
<string name="Source"><![CDATA[--[[
|
|
@@ -887,7 +1349,7 @@ return Explorer
|
|
|
887
1349
|
]]></string>
|
|
888
1350
|
</Properties>
|
|
889
1351
|
</Item>
|
|
890
|
-
<Item class="ModuleScript" referent="
|
|
1352
|
+
<Item class="ModuleScript" referent="5">
|
|
891
1353
|
<Properties>
|
|
892
1354
|
<string name="Name">InstanceRegistry</string>
|
|
893
1355
|
<string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
|
|
@@ -1015,7 +1477,7 @@ return InstanceRegistry
|
|
|
1015
1477
|
]]></string>
|
|
1016
1478
|
</Properties>
|
|
1017
1479
|
</Item>
|
|
1018
|
-
<Item class="ModuleScript" referent="
|
|
1480
|
+
<Item class="ModuleScript" referent="6">
|
|
1019
1481
|
<Properties>
|
|
1020
1482
|
<string name="Name">Mutation</string>
|
|
1021
1483
|
<string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
@@ -1064,6 +1526,7 @@ function Mutation.apply(spec)
|
|
|
1064
1526
|
end
|
|
1065
1527
|
|
|
1066
1528
|
local results = {}
|
|
1529
|
+
local created = {}
|
|
1067
1530
|
local ok, err = pcall(function()
|
|
1068
1531
|
for index, operation in operations do
|
|
1069
1532
|
assert(type(operation) == "table", "Operation " .. index .. " must be an object")
|
|
@@ -1079,12 +1542,34 @@ function Mutation.apply(spec)
|
|
|
1079
1542
|
assert(type(operation.className) == "string", "create.className must be a string")
|
|
1080
1543
|
assert(type(operation.name) == "string", "create.name must be a string")
|
|
1081
1544
|
local instance = Instance.new(operation.className)
|
|
1545
|
+
table.insert(created, instance)
|
|
1082
1546
|
instance.Name = operation.name
|
|
1083
1547
|
if operation.properties then
|
|
1084
1548
|
setProperties(instance, operation.properties)
|
|
1085
1549
|
end
|
|
1086
1550
|
instance.Parent = parent
|
|
1087
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) })
|
|
1088
1573
|
elseif operation.op == "move" then
|
|
1089
1574
|
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
1090
1575
|
local parent = resolve(operation.parent, "Operation " .. index .. " parent")
|
|
@@ -1099,6 +1584,13 @@ function Mutation.apply(spec)
|
|
|
1099
1584
|
|
|
1100
1585
|
if not ok then
|
|
1101
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
|
|
1102
1594
|
return { success = false, error = tostring(err), rolledBack = true }
|
|
1103
1595
|
end
|
|
1104
1596
|
|
|
@@ -1156,7 +1648,7 @@ return Mutation
|
|
|
1156
1648
|
]]></string>
|
|
1157
1649
|
</Properties>
|
|
1158
1650
|
</Item>
|
|
1159
|
-
<Item class="ModuleScript" referent="
|
|
1651
|
+
<Item class="ModuleScript" referent="7">
|
|
1160
1652
|
<Properties>
|
|
1161
1653
|
<string name="Name">Properties</string>
|
|
1162
1654
|
<string name="Source"><![CDATA[--[[
|
|
@@ -2021,7 +2513,7 @@ return Properties
|
|
|
2021
2513
|
]]></string>
|
|
2022
2514
|
</Properties>
|
|
2023
2515
|
</Item>
|
|
2024
|
-
<Item class="ModuleScript" referent="
|
|
2516
|
+
<Item class="ModuleScript" referent="8">
|
|
2025
2517
|
<Properties>
|
|
2026
2518
|
<string name="Name">Protocol</string>
|
|
2027
2519
|
<string name="Source"><![CDATA[--[[
|
|
@@ -2057,7 +2549,7 @@ return Protocol
|
|
|
2057
2549
|
]]></string>
|
|
2058
2550
|
</Properties>
|
|
2059
2551
|
</Item>
|
|
2060
|
-
<Item class="ModuleScript" referent="
|
|
2552
|
+
<Item class="ModuleScript" referent="9">
|
|
2061
2553
|
<Properties>
|
|
2062
2554
|
<string name="Name">Reflection</string>
|
|
2063
2555
|
<string name="Source"><![CDATA[local ReflectionService = game:GetService("ReflectionService")
|
|
@@ -2232,7 +2724,7 @@ return Reflection
|
|
|
2232
2724
|
]]></string>
|
|
2233
2725
|
</Properties>
|
|
2234
2726
|
</Item>
|
|
2235
|
-
<Item class="ModuleScript" referent="
|
|
2727
|
+
<Item class="ModuleScript" referent="10">
|
|
2236
2728
|
<Properties>
|
|
2237
2729
|
<string name="Name">TestRunner</string>
|
|
2238
2730
|
<string name="Source"><![CDATA[local StudioTestService = game:GetService("StudioTestService")
|
|
@@ -2307,7 +2799,7 @@ return TestRunner
|
|
|
2307
2799
|
]]></string>
|
|
2308
2800
|
</Properties>
|
|
2309
2801
|
</Item>
|
|
2310
|
-
<Item class="ModuleScript" referent="
|
|
2802
|
+
<Item class="ModuleScript" referent="11">
|
|
2311
2803
|
<Properties>
|
|
2312
2804
|
<string name="Name">UIBuilder</string>
|
|
2313
2805
|
<string name="Source"><![CDATA[--[[
|
|
@@ -3170,7 +3662,7 @@ return UIBuilder
|
|
|
3170
3662
|
]]></string>
|
|
3171
3663
|
</Properties>
|
|
3172
3664
|
</Item>
|
|
3173
|
-
<Item class="ModuleScript" referent="
|
|
3665
|
+
<Item class="ModuleScript" referent="12">
|
|
3174
3666
|
<Properties>
|
|
3175
3667
|
<string name="Name">ValueCodec</string>
|
|
3176
3668
|
<string name="Source"><![CDATA[local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
@@ -3407,7 +3899,7 @@ return ValueCodec
|
|
|
3407
3899
|
]]></string>
|
|
3408
3900
|
</Properties>
|
|
3409
3901
|
</Item>
|
|
3410
|
-
<Item class="ModuleScript" referent="
|
|
3902
|
+
<Item class="ModuleScript" referent="13">
|
|
3411
3903
|
<Properties>
|
|
3412
3904
|
<string name="Name">Watcher</string>
|
|
3413
3905
|
<string name="Source"><![CDATA[--[[
|
|
@@ -3498,7 +3990,7 @@ return Watcher
|
|
|
3498
3990
|
]]></string>
|
|
3499
3991
|
</Properties>
|
|
3500
3992
|
</Item>
|
|
3501
|
-
<Item class="ModuleScript" referent="
|
|
3993
|
+
<Item class="ModuleScript" referent="14">
|
|
3502
3994
|
<Properties>
|
|
3503
3995
|
<string name="Name">WsClient</string>
|
|
3504
3996
|
<string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
|