dominus-cli 2.2.1 → 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.
- package/CHANGELOG.md +58 -0
- package/README.md +32 -3
- package/dist/bridge-daemon.js.map +1 -1
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +658 -20
- package/dist/mcp.js.map +1 -1
- package/package.json +5 -5
- package/plugin/Dominus.rbxmx +1064 -12
- package/plugin/src/Building.lua +427 -0
- package/plugin/src/CommandRouter.lua +41 -0
- package/plugin/src/Metadata.lua +411 -0
- package/plugin/src/Mutation.lua +45 -0
- package/plugin/src/ValueCodec.lua +116 -0
package/plugin/Dominus.rbxmx
CHANGED
|
@@ -256,10 +256,445 @@ 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)
|
|
697
|
+
local Metadata = require(script.Parent.Metadata)
|
|
263
698
|
local Mutation = require(script.Parent.Mutation)
|
|
264
699
|
local Properties = require(script.Parent.Properties)
|
|
265
700
|
local Reflection = require(script.Parent.Reflection)
|
|
@@ -297,6 +732,45 @@ local handlers = {
|
|
|
297
732
|
["studio:v2:apply"] = function(payload)
|
|
298
733
|
return Mutation.apply(payload)
|
|
299
734
|
end,
|
|
735
|
+
["studio:v2:create_parts"] = function(payload)
|
|
736
|
+
return Building.createParts(payload)
|
|
737
|
+
end,
|
|
738
|
+
["studio:v2:clone_instances"] = function(payload)
|
|
739
|
+
return Building.cloneInstances(payload)
|
|
740
|
+
end,
|
|
741
|
+
["studio:v2:group_instances"] = function(payload)
|
|
742
|
+
return Building.groupInstances(payload)
|
|
743
|
+
end,
|
|
744
|
+
["studio:v2:ungroup_instances"] = function(payload)
|
|
745
|
+
return Building.ungroupInstances(payload)
|
|
746
|
+
end,
|
|
747
|
+
["studio:v2:transform_instances"] = function(payload)
|
|
748
|
+
return Building.transformInstances(payload)
|
|
749
|
+
end,
|
|
750
|
+
["studio:v2:create_welds"] = function(payload)
|
|
751
|
+
return Building.createWelds(payload)
|
|
752
|
+
end,
|
|
753
|
+
["studio:v2:query_parts"] = function(payload)
|
|
754
|
+
return Building.queryParts(payload)
|
|
755
|
+
end,
|
|
756
|
+
["studio:v2:set_selection"] = function(payload)
|
|
757
|
+
return Building.setSelection(payload)
|
|
758
|
+
end,
|
|
759
|
+
["studio:v2:get_metadata"] = function(payload)
|
|
760
|
+
return Metadata.get(payload)
|
|
761
|
+
end,
|
|
762
|
+
["studio:v2:set_attributes"] = function(payload)
|
|
763
|
+
return Metadata.setAttributes(payload)
|
|
764
|
+
end,
|
|
765
|
+
["studio:v2:update_tags"] = function(payload)
|
|
766
|
+
return Metadata.updateTags(payload)
|
|
767
|
+
end,
|
|
768
|
+
["studio:v2:list_callable_methods"] = function(payload)
|
|
769
|
+
return Metadata.listCallableMethods(payload)
|
|
770
|
+
end,
|
|
771
|
+
["studio:v2:invoke_methods"] = function(payload)
|
|
772
|
+
return Metadata.invoke(payload)
|
|
773
|
+
end,
|
|
300
774
|
["studio:v2:delete"] = function(payload)
|
|
301
775
|
return Mutation.delete(payload)
|
|
302
776
|
end,
|
|
@@ -355,7 +829,7 @@ return CommandRouter
|
|
|
355
829
|
]]></string>
|
|
356
830
|
</Properties>
|
|
357
831
|
</Item>
|
|
358
|
-
<Item class="ModuleScript" referent="
|
|
832
|
+
<Item class="ModuleScript" referent="4">
|
|
359
833
|
<Properties>
|
|
360
834
|
<string name="Name">Explorer</string>
|
|
361
835
|
<string name="Source"><![CDATA[--[[
|
|
@@ -891,7 +1365,7 @@ return Explorer
|
|
|
891
1365
|
]]></string>
|
|
892
1366
|
</Properties>
|
|
893
1367
|
</Item>
|
|
894
|
-
<Item class="ModuleScript" referent="
|
|
1368
|
+
<Item class="ModuleScript" referent="5">
|
|
895
1369
|
<Properties>
|
|
896
1370
|
<string name="Name">InstanceRegistry</string>
|
|
897
1371
|
<string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
|
|
@@ -1019,12 +1493,430 @@ return InstanceRegistry
|
|
|
1019
1493
|
]]></string>
|
|
1020
1494
|
</Properties>
|
|
1021
1495
|
</Item>
|
|
1022
|
-
<Item class="ModuleScript" referent="
|
|
1496
|
+
<Item class="ModuleScript" referent="6">
|
|
1497
|
+
<Properties>
|
|
1498
|
+
<string name="Name">Metadata</string>
|
|
1499
|
+
<string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
1500
|
+
|
|
1501
|
+
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
1502
|
+
local ValueCodec = require(script.Parent.ValueCodec)
|
|
1503
|
+
|
|
1504
|
+
local Metadata = {}
|
|
1505
|
+
|
|
1506
|
+
local MAX_TARGETS = 100
|
|
1507
|
+
local MAX_ITEMS_PER_TARGET = 100
|
|
1508
|
+
local MAX_METHOD_CALLS = 50
|
|
1509
|
+
|
|
1510
|
+
local ATTRIBUTE_TYPES = {
|
|
1511
|
+
string = true,
|
|
1512
|
+
boolean = true,
|
|
1513
|
+
number = true,
|
|
1514
|
+
UDim = true,
|
|
1515
|
+
UDim2 = true,
|
|
1516
|
+
BrickColor = true,
|
|
1517
|
+
Color3 = true,
|
|
1518
|
+
Vector2 = true,
|
|
1519
|
+
Vector3 = true,
|
|
1520
|
+
CFrame = true,
|
|
1521
|
+
NumberSequence = true,
|
|
1522
|
+
ColorSequence = true,
|
|
1523
|
+
NumberRange = true,
|
|
1524
|
+
Rect = true,
|
|
1525
|
+
Font = true,
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
local METHOD_POLICY = {
|
|
1529
|
+
GetAttribute = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
1530
|
+
GetAttributes = { kind = "read", className = "Instance", params = {} },
|
|
1531
|
+
GetTags = { kind = "read", className = "Instance", params = {} },
|
|
1532
|
+
HasTag = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
1533
|
+
GetFullName = { kind = "read", className = "Instance", params = {} },
|
|
1534
|
+
IsA = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
1535
|
+
IsAncestorOf = { kind = "read", className = "Instance", params = { { type = "Instance" } } },
|
|
1536
|
+
IsDescendantOf = { kind = "read", className = "Instance", params = { { type = "Instance" } } },
|
|
1537
|
+
FindFirstChild = {
|
|
1538
|
+
kind = "read",
|
|
1539
|
+
className = "Instance",
|
|
1540
|
+
params = { { type = "string" }, { type = "boolean", optional = true } },
|
|
1541
|
+
},
|
|
1542
|
+
FindFirstChildOfClass = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
1543
|
+
FindFirstChildWhichIsA = {
|
|
1544
|
+
kind = "read",
|
|
1545
|
+
className = "Instance",
|
|
1546
|
+
params = { { type = "string" }, { type = "boolean", optional = true } },
|
|
1547
|
+
},
|
|
1548
|
+
FindFirstAncestor = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
1549
|
+
FindFirstAncestorOfClass = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
1550
|
+
FindFirstAncestorWhichIsA = { kind = "read", className = "Instance", params = { { type = "string" } } },
|
|
1551
|
+
GetChildren = { kind = "read", className = "Instance", params = {} },
|
|
1552
|
+
GetDescendants = { kind = "read", className = "Instance", params = {} },
|
|
1553
|
+
GetPivot = { kind = "read", className = "PVInstance", params = {} },
|
|
1554
|
+
GetScale = { kind = "read", className = "Model", params = {} },
|
|
1555
|
+
GetBoundingBox = { kind = "read", className = "Model", params = {} },
|
|
1556
|
+
AddTag = { kind = "write", className = "Instance", params = { { type = "string", tag = true } } },
|
|
1557
|
+
RemoveTag = { kind = "write", className = "Instance", params = { { type = "string", tag = true } } },
|
|
1558
|
+
SetAttribute = {
|
|
1559
|
+
kind = "write",
|
|
1560
|
+
className = "Instance",
|
|
1561
|
+
params = { { type = "string", attributeName = true }, { type = "Attribute", allowNil = true } },
|
|
1562
|
+
},
|
|
1563
|
+
PivotTo = { kind = "write", className = "PVInstance", params = { { type = "CFrame" } } },
|
|
1564
|
+
ScaleTo = { kind = "write", className = "Model", params = { { type = "number" } } },
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
local function resolve(ref, label)
|
|
1568
|
+
local instance, err = InstanceRegistry.resolve(ref)
|
|
1569
|
+
if not instance then
|
|
1570
|
+
error(label .. ": " .. tostring(err))
|
|
1571
|
+
end
|
|
1572
|
+
return instance
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
local function validateName(value, label)
|
|
1576
|
+
assert(type(value) == "string", label .. " must be a string")
|
|
1577
|
+
assert(#value > 0 and #value <= 100, label .. " must contain 1 to 100 characters")
|
|
1578
|
+
assert(not value:find("[%c]"), label .. " cannot contain control characters")
|
|
1579
|
+
return value
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
local function validateTag(value)
|
|
1583
|
+
return validateName(value, "Tag")
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
local function validateAttributeName(value)
|
|
1587
|
+
return validateName(value, "Attribute name")
|
|
1588
|
+
end
|
|
1589
|
+
|
|
1590
|
+
local function prepareAttributeChanges(set, remove, label)
|
|
1591
|
+
set = set or {}
|
|
1592
|
+
remove = remove or {}
|
|
1593
|
+
assert(type(set) == "table" and type(remove) == "table", "set and remove must be arrays")
|
|
1594
|
+
assert(#set + #remove > 0, label .. " has no attribute updates")
|
|
1595
|
+
assert(#set + #remove <= MAX_ITEMS_PER_TARGET, "Too many attribute updates in " .. label)
|
|
1596
|
+
|
|
1597
|
+
local changes = {}
|
|
1598
|
+
local seen = {}
|
|
1599
|
+
for itemIndex, item in set do
|
|
1600
|
+
assert(type(item) == "table" and type(item.value) == "table", "Set item " .. itemIndex .. " is invalid")
|
|
1601
|
+
local name = validateAttributeName(item.name)
|
|
1602
|
+
assert(not seen[name], "Attribute " .. name .. " appears more than once")
|
|
1603
|
+
local typeName = item.value.type
|
|
1604
|
+
assert(ATTRIBUTE_TYPES[typeName], "Unsupported attribute type: " .. tostring(typeName))
|
|
1605
|
+
seen[name] = true
|
|
1606
|
+
table.insert(changes, { name = name, value = ValueCodec.decodeTyped(typeName, item.value.value) })
|
|
1607
|
+
end
|
|
1608
|
+
for _, nameValue in remove do
|
|
1609
|
+
local name = validateAttributeName(nameValue)
|
|
1610
|
+
assert(not seen[name], "Attribute " .. name .. " cannot be set and removed together")
|
|
1611
|
+
seen[name] = true
|
|
1612
|
+
table.insert(changes, { name = name, remove = true })
|
|
1613
|
+
end
|
|
1614
|
+
return changes
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
local function applyPreparedAttributeChanges(target, changes)
|
|
1618
|
+
for _, change in changes do
|
|
1619
|
+
if change.remove then
|
|
1620
|
+
target:SetAttribute(change.name, nil)
|
|
1621
|
+
else
|
|
1622
|
+
target:SetAttribute(change.name, change.value)
|
|
1623
|
+
end
|
|
1624
|
+
end
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
local function prepareTagChanges(add, remove, label)
|
|
1628
|
+
add = add or {}
|
|
1629
|
+
remove = remove or {}
|
|
1630
|
+
assert(type(add) == "table" and type(remove) == "table", "add and remove must be arrays")
|
|
1631
|
+
assert(#add + #remove > 0, label .. " has no tag updates")
|
|
1632
|
+
assert(#add + #remove <= MAX_ITEMS_PER_TARGET, "Too many tag updates in " .. label)
|
|
1633
|
+
|
|
1634
|
+
local seen = {}
|
|
1635
|
+
for _, tagValue in add do
|
|
1636
|
+
local tag = validateTag(tagValue)
|
|
1637
|
+
assert(not seen[tag], "Tag " .. tag .. " appears more than once")
|
|
1638
|
+
seen[tag] = "add"
|
|
1639
|
+
end
|
|
1640
|
+
for _, tagValue in remove do
|
|
1641
|
+
local tag = validateTag(tagValue)
|
|
1642
|
+
assert(not seen[tag], "Tag " .. tag .. " cannot be added and removed together")
|
|
1643
|
+
seen[tag] = "remove"
|
|
1644
|
+
end
|
|
1645
|
+
return add, remove
|
|
1646
|
+
end
|
|
1647
|
+
|
|
1648
|
+
local function applyPreparedTagChanges(target, add, remove)
|
|
1649
|
+
for _, tag in add do
|
|
1650
|
+
target:AddTag(tag)
|
|
1651
|
+
end
|
|
1652
|
+
for _, tag in remove do
|
|
1653
|
+
target:RemoveTag(tag)
|
|
1654
|
+
end
|
|
1655
|
+
end
|
|
1656
|
+
|
|
1657
|
+
local function sortedTags(instance)
|
|
1658
|
+
local tags = instance:GetTags()
|
|
1659
|
+
table.sort(tags)
|
|
1660
|
+
return tags
|
|
1661
|
+
end
|
|
1662
|
+
|
|
1663
|
+
local function encodedAttributes(instance)
|
|
1664
|
+
local attributes = instance:GetAttributes()
|
|
1665
|
+
local names = {}
|
|
1666
|
+
for name in attributes do
|
|
1667
|
+
table.insert(names, name)
|
|
1668
|
+
end
|
|
1669
|
+
table.sort(names)
|
|
1670
|
+
|
|
1671
|
+
local encoded = {}
|
|
1672
|
+
for _, name in names do
|
|
1673
|
+
table.insert(encoded, {
|
|
1674
|
+
name = name,
|
|
1675
|
+
value = ValueCodec.encodeTyped(attributes[name]),
|
|
1676
|
+
})
|
|
1677
|
+
end
|
|
1678
|
+
return encoded
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
local function metadataFor(instance)
|
|
1682
|
+
return {
|
|
1683
|
+
ref = InstanceRegistry.toRef(instance),
|
|
1684
|
+
name = instance.Name,
|
|
1685
|
+
className = instance.ClassName,
|
|
1686
|
+
tags = sortedTags(instance),
|
|
1687
|
+
attributes = encodedAttributes(instance),
|
|
1688
|
+
}
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
local function beginRecording(name)
|
|
1692
|
+
local recording = ChangeHistoryService:TryBeginRecording(name)
|
|
1693
|
+
if not recording then
|
|
1694
|
+
error("Another plugin recording is already active")
|
|
1695
|
+
end
|
|
1696
|
+
return recording
|
|
1697
|
+
end
|
|
1698
|
+
|
|
1699
|
+
function Metadata.get(spec)
|
|
1700
|
+
assert(type(spec.targets) == "table" and #spec.targets > 0, "targets must contain at least one instance reference")
|
|
1701
|
+
assert(#spec.targets <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " instances can be inspected at once")
|
|
1702
|
+
local results = {}
|
|
1703
|
+
for index, targetRef in spec.targets do
|
|
1704
|
+
table.insert(results, metadataFor(resolve(targetRef, "Target " .. index)))
|
|
1705
|
+
end
|
|
1706
|
+
return { success = true, results = results }
|
|
1707
|
+
end
|
|
1708
|
+
|
|
1709
|
+
function Metadata.setAttributes(spec)
|
|
1710
|
+
assert(type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one attribute update")
|
|
1711
|
+
assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " attribute operations are allowed")
|
|
1712
|
+
|
|
1713
|
+
local prepared = {}
|
|
1714
|
+
for index, operation in spec.operations do
|
|
1715
|
+
assert(type(operation) == "table", "Operation " .. index .. " must be an object")
|
|
1716
|
+
table.insert(prepared, {
|
|
1717
|
+
target = resolve(operation.target, "Operation " .. index .. " target"),
|
|
1718
|
+
changes = prepareAttributeChanges(operation.set, operation.remove, "Operation " .. index),
|
|
1719
|
+
})
|
|
1720
|
+
end
|
|
1721
|
+
|
|
1722
|
+
local recording = beginRecording("Dominus 2: Update attributes")
|
|
1723
|
+
local ok, err = pcall(function()
|
|
1724
|
+
for _, operation in prepared do
|
|
1725
|
+
applyPreparedAttributeChanges(operation.target, operation.changes)
|
|
1726
|
+
end
|
|
1727
|
+
end)
|
|
1728
|
+
if not ok then
|
|
1729
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
1730
|
+
return { success = false, error = tostring(err), rolledBack = true }
|
|
1731
|
+
end
|
|
1732
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
1733
|
+
|
|
1734
|
+
local results = {}
|
|
1735
|
+
for _, operation in prepared do
|
|
1736
|
+
table.insert(results, metadataFor(operation.target))
|
|
1737
|
+
end
|
|
1738
|
+
return { success = true, operationCount = #prepared, results = results }
|
|
1739
|
+
end
|
|
1740
|
+
|
|
1741
|
+
function Metadata.updateTags(spec)
|
|
1742
|
+
assert(type(spec.operations) == "table" and #spec.operations > 0, "operations must contain at least one tag update")
|
|
1743
|
+
assert(#spec.operations <= MAX_TARGETS, "At most " .. MAX_TARGETS .. " tag operations are allowed")
|
|
1744
|
+
|
|
1745
|
+
local prepared = {}
|
|
1746
|
+
for index, operation in spec.operations do
|
|
1747
|
+
assert(type(operation) == "table", "Operation " .. index .. " must be an object")
|
|
1748
|
+
local add, remove = prepareTagChanges(operation.add, operation.remove, "Operation " .. index)
|
|
1749
|
+
table.insert(prepared, {
|
|
1750
|
+
target = resolve(operation.target, "Operation " .. index .. " target"),
|
|
1751
|
+
add = add,
|
|
1752
|
+
remove = remove,
|
|
1753
|
+
})
|
|
1754
|
+
end
|
|
1755
|
+
|
|
1756
|
+
local recording = beginRecording("Dominus 2: Update tags")
|
|
1757
|
+
local ok, err = pcall(function()
|
|
1758
|
+
for _, operation in prepared do
|
|
1759
|
+
applyPreparedTagChanges(operation.target, operation.add, operation.remove)
|
|
1760
|
+
end
|
|
1761
|
+
end)
|
|
1762
|
+
if not ok then
|
|
1763
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
1764
|
+
return { success = false, error = tostring(err), rolledBack = true }
|
|
1765
|
+
end
|
|
1766
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
1767
|
+
|
|
1768
|
+
local results = {}
|
|
1769
|
+
for _, operation in prepared do
|
|
1770
|
+
table.insert(results, metadataFor(operation.target))
|
|
1771
|
+
end
|
|
1772
|
+
return { success = true, operationCount = #prepared, results = results }
|
|
1773
|
+
end
|
|
1774
|
+
|
|
1775
|
+
function Metadata.applyAttributeChangesWithinRecording(target, set, remove)
|
|
1776
|
+
local changes = prepareAttributeChanges(set, remove, "Mutation operation")
|
|
1777
|
+
applyPreparedAttributeChanges(target, changes)
|
|
1778
|
+
return encodedAttributes(target)
|
|
1779
|
+
end
|
|
1780
|
+
|
|
1781
|
+
function Metadata.applyTagChangesWithinRecording(target, add, remove)
|
|
1782
|
+
local preparedAdd, preparedRemove = prepareTagChanges(add, remove, "Mutation operation")
|
|
1783
|
+
applyPreparedTagChanges(target, preparedAdd, preparedRemove)
|
|
1784
|
+
return sortedTags(target)
|
|
1785
|
+
end
|
|
1786
|
+
|
|
1787
|
+
local function publicPolicy(methodName, policy)
|
|
1788
|
+
local params = {}
|
|
1789
|
+
for _, param in policy.params do
|
|
1790
|
+
table.insert(params, {
|
|
1791
|
+
type = param.type,
|
|
1792
|
+
optional = param.optional or false,
|
|
1793
|
+
})
|
|
1794
|
+
end
|
|
1795
|
+
return {
|
|
1796
|
+
name = methodName,
|
|
1797
|
+
kind = policy.kind,
|
|
1798
|
+
className = policy.className,
|
|
1799
|
+
parameters = params,
|
|
1800
|
+
}
|
|
1801
|
+
end
|
|
1802
|
+
|
|
1803
|
+
function Metadata.listCallableMethods(spec)
|
|
1804
|
+
local target = resolve(spec.target, "Target")
|
|
1805
|
+
local methods = {}
|
|
1806
|
+
for methodName, policy in METHOD_POLICY do
|
|
1807
|
+
if target:IsA(policy.className) then
|
|
1808
|
+
table.insert(methods, publicPolicy(methodName, policy))
|
|
1809
|
+
end
|
|
1810
|
+
end
|
|
1811
|
+
table.sort(methods, function(left, right)
|
|
1812
|
+
return left.name < right.name
|
|
1813
|
+
end)
|
|
1814
|
+
return {
|
|
1815
|
+
success = true,
|
|
1816
|
+
target = InstanceRegistry.toRef(target),
|
|
1817
|
+
methods = methods,
|
|
1818
|
+
blockedByDefault = true,
|
|
1819
|
+
}
|
|
1820
|
+
end
|
|
1821
|
+
|
|
1822
|
+
local function decodeArgument(argument, parameter, label)
|
|
1823
|
+
assert(type(argument) == "table" and type(argument.type) == "string", label .. " must be a typed value")
|
|
1824
|
+
if parameter.type == "Attribute" then
|
|
1825
|
+
if argument.type == "nil" and parameter.allowNil then
|
|
1826
|
+
return nil
|
|
1827
|
+
end
|
|
1828
|
+
assert(ATTRIBUTE_TYPES[argument.type], label .. " must be a supported Roblox attribute type")
|
|
1829
|
+
return ValueCodec.decodeTyped(argument.type, argument.value)
|
|
1830
|
+
end
|
|
1831
|
+
assert(argument.type == parameter.type, label .. " must have type " .. parameter.type)
|
|
1832
|
+
local decoded = ValueCodec.decodeTyped(argument.type, argument.value)
|
|
1833
|
+
if parameter.tag then
|
|
1834
|
+
validateTag(decoded)
|
|
1835
|
+
elseif parameter.attributeName then
|
|
1836
|
+
validateAttributeName(decoded)
|
|
1837
|
+
end
|
|
1838
|
+
return decoded
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1841
|
+
function Metadata.invoke(spec)
|
|
1842
|
+
assert(type(spec.calls) == "table" and #spec.calls > 0, "calls must contain at least one method call")
|
|
1843
|
+
assert(#spec.calls <= MAX_METHOD_CALLS, "At most " .. MAX_METHOD_CALLS .. " method calls are allowed")
|
|
1844
|
+
|
|
1845
|
+
local prepared = {}
|
|
1846
|
+
local hasWrites = false
|
|
1847
|
+
for index, call in spec.calls do
|
|
1848
|
+
assert(type(call) == "table", "Call " .. index .. " must be an object")
|
|
1849
|
+
local methodName = call.method
|
|
1850
|
+
local policy = METHOD_POLICY[methodName]
|
|
1851
|
+
assert(policy, "Method is not allowed by Dominus: " .. tostring(methodName))
|
|
1852
|
+
local target = resolve(call.target, "Call " .. index .. " target")
|
|
1853
|
+
assert(target:IsA(policy.className), methodName .. " requires " .. policy.className .. ", got " .. target.ClassName)
|
|
1854
|
+
local arguments = call.arguments or {}
|
|
1855
|
+
assert(type(arguments) == "table", "Call " .. index .. " arguments must be an array")
|
|
1856
|
+
local minimum = 0
|
|
1857
|
+
for _, parameter in policy.params do
|
|
1858
|
+
if not parameter.optional then
|
|
1859
|
+
minimum += 1
|
|
1860
|
+
end
|
|
1861
|
+
end
|
|
1862
|
+
assert(#arguments >= minimum and #arguments <= #policy.params, methodName .. " received the wrong number of arguments")
|
|
1863
|
+
|
|
1864
|
+
local decoded = {}
|
|
1865
|
+
for argumentIndex, argument in arguments do
|
|
1866
|
+
decoded[argumentIndex] = decodeArgument(argument, policy.params[argumentIndex], "Argument " .. argumentIndex)
|
|
1867
|
+
end
|
|
1868
|
+
table.insert(prepared, {
|
|
1869
|
+
target = target,
|
|
1870
|
+
method = methodName,
|
|
1871
|
+
policy = policy,
|
|
1872
|
+
arguments = decoded,
|
|
1873
|
+
argumentCount = #arguments,
|
|
1874
|
+
})
|
|
1875
|
+
hasWrites = hasWrites or policy.kind == "write"
|
|
1876
|
+
end
|
|
1877
|
+
|
|
1878
|
+
local recording = hasWrites and beginRecording("Dominus 2: Invoke safe instance methods") or nil
|
|
1879
|
+
local results = {}
|
|
1880
|
+
local ok, err = pcall(function()
|
|
1881
|
+
for _, call in prepared do
|
|
1882
|
+
local method = call.target[call.method]
|
|
1883
|
+
assert(type(method) == "function", call.method .. " is unavailable on " .. call.target.ClassName)
|
|
1884
|
+
local returned = table.pack(method(call.target, table.unpack(call.arguments, 1, call.argumentCount)))
|
|
1885
|
+
local values = {}
|
|
1886
|
+
for resultIndex = 1, returned.n do
|
|
1887
|
+
table.insert(values, ValueCodec.encodeTyped(returned[resultIndex]))
|
|
1888
|
+
end
|
|
1889
|
+
table.insert(results, {
|
|
1890
|
+
target = InstanceRegistry.toRef(call.target),
|
|
1891
|
+
method = call.method,
|
|
1892
|
+
kind = call.policy.kind,
|
|
1893
|
+
results = values,
|
|
1894
|
+
})
|
|
1895
|
+
end
|
|
1896
|
+
end)
|
|
1897
|
+
if not ok then
|
|
1898
|
+
if recording then
|
|
1899
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
1900
|
+
end
|
|
1901
|
+
return { success = false, error = tostring(err), rolledBack = recording ~= nil }
|
|
1902
|
+
end
|
|
1903
|
+
if recording then
|
|
1904
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
1905
|
+
end
|
|
1906
|
+
return { success = true, callCount = #results, mutationRecorded = recording ~= nil, results = results }
|
|
1907
|
+
end
|
|
1908
|
+
|
|
1909
|
+
return Metadata
|
|
1910
|
+
]]></string>
|
|
1911
|
+
</Properties>
|
|
1912
|
+
</Item>
|
|
1913
|
+
<Item class="ModuleScript" referent="7">
|
|
1023
1914
|
<Properties>
|
|
1024
1915
|
<string name="Name">Mutation</string>
|
|
1025
1916
|
<string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
1026
1917
|
|
|
1027
1918
|
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
1919
|
+
local Metadata = require(script.Parent.Metadata)
|
|
1028
1920
|
local ValueCodec = require(script.Parent.ValueCodec)
|
|
1029
1921
|
|
|
1030
1922
|
local Mutation = {}
|
|
@@ -1068,6 +1960,7 @@ function Mutation.apply(spec)
|
|
|
1068
1960
|
end
|
|
1069
1961
|
|
|
1070
1962
|
local results = {}
|
|
1963
|
+
local created = {}
|
|
1071
1964
|
local ok, err = pcall(function()
|
|
1072
1965
|
for index, operation in operations do
|
|
1073
1966
|
assert(type(operation) == "table", "Operation " .. index .. " must be an object")
|
|
@@ -1078,17 +1971,53 @@ function Mutation.apply(spec)
|
|
|
1078
1971
|
target = InstanceRegistry.toRef(target),
|
|
1079
1972
|
properties = setProperties(target, operation.properties),
|
|
1080
1973
|
})
|
|
1974
|
+
elseif operation.op == "setAttributes" then
|
|
1975
|
+
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
1976
|
+
table.insert(results, {
|
|
1977
|
+
op = operation.op,
|
|
1978
|
+
target = InstanceRegistry.toRef(target),
|
|
1979
|
+
attributes = Metadata.applyAttributeChangesWithinRecording(target, operation.set, operation.remove),
|
|
1980
|
+
})
|
|
1981
|
+
elseif operation.op == "updateTags" then
|
|
1982
|
+
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
1983
|
+
table.insert(results, {
|
|
1984
|
+
op = operation.op,
|
|
1985
|
+
target = InstanceRegistry.toRef(target),
|
|
1986
|
+
tags = Metadata.applyTagChangesWithinRecording(target, operation.add, operation.remove),
|
|
1987
|
+
})
|
|
1081
1988
|
elseif operation.op == "create" then
|
|
1082
1989
|
local parent = resolve(operation.parent, "Operation " .. index .. " parent")
|
|
1083
1990
|
assert(type(operation.className) == "string", "create.className must be a string")
|
|
1084
1991
|
assert(type(operation.name) == "string", "create.name must be a string")
|
|
1085
1992
|
local instance = Instance.new(operation.className)
|
|
1993
|
+
table.insert(created, instance)
|
|
1086
1994
|
instance.Name = operation.name
|
|
1087
1995
|
if operation.properties then
|
|
1088
1996
|
setProperties(instance, operation.properties)
|
|
1089
1997
|
end
|
|
1090
1998
|
instance.Parent = parent
|
|
1091
1999
|
table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(instance) })
|
|
2000
|
+
elseif operation.op == "clone" then
|
|
2001
|
+
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
2002
|
+
assert(target ~= game and target.Parent ~= game, "Services cannot be cloned")
|
|
2003
|
+
local parent = operation.parent and resolve(operation.parent, "Operation " .. index .. " parent") or target.Parent
|
|
2004
|
+
assert(parent ~= nil, "Clone target has no parent")
|
|
2005
|
+
local clone = target:Clone()
|
|
2006
|
+
assert(clone ~= nil, "Clone target is not Archivable")
|
|
2007
|
+
table.insert(created, clone)
|
|
2008
|
+
if operation.name then
|
|
2009
|
+
clone.Name = operation.name
|
|
2010
|
+
end
|
|
2011
|
+
if operation.properties then
|
|
2012
|
+
setProperties(clone, operation.properties)
|
|
2013
|
+
end
|
|
2014
|
+
if operation.offset then
|
|
2015
|
+
assert(clone:IsA("PVInstance"), "Only Models and BaseParts can be spatially offset")
|
|
2016
|
+
local offset = ValueCodec.decodeForCurrent(Vector3.zero, operation.offset)
|
|
2017
|
+
clone:PivotTo(clone:GetPivot() + offset)
|
|
2018
|
+
end
|
|
2019
|
+
clone.Parent = parent
|
|
2020
|
+
table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(clone) })
|
|
1092
2021
|
elseif operation.op == "move" then
|
|
1093
2022
|
local target = resolve(operation.target, "Operation " .. index .. " target")
|
|
1094
2023
|
local parent = resolve(operation.parent, "Operation " .. index .. " parent")
|
|
@@ -1103,6 +2032,13 @@ function Mutation.apply(spec)
|
|
|
1103
2032
|
|
|
1104
2033
|
if not ok then
|
|
1105
2034
|
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
2035
|
+
for _, instance in created do
|
|
2036
|
+
pcall(function()
|
|
2037
|
+
if instance.Parent == nil then
|
|
2038
|
+
instance:Destroy()
|
|
2039
|
+
end
|
|
2040
|
+
end)
|
|
2041
|
+
end
|
|
1106
2042
|
return { success = false, error = tostring(err), rolledBack = true }
|
|
1107
2043
|
end
|
|
1108
2044
|
|
|
@@ -1160,7 +2096,7 @@ return Mutation
|
|
|
1160
2096
|
]]></string>
|
|
1161
2097
|
</Properties>
|
|
1162
2098
|
</Item>
|
|
1163
|
-
<Item class="ModuleScript" referent="
|
|
2099
|
+
<Item class="ModuleScript" referent="8">
|
|
1164
2100
|
<Properties>
|
|
1165
2101
|
<string name="Name">Properties</string>
|
|
1166
2102
|
<string name="Source"><![CDATA[--[[
|
|
@@ -2025,7 +2961,7 @@ return Properties
|
|
|
2025
2961
|
]]></string>
|
|
2026
2962
|
</Properties>
|
|
2027
2963
|
</Item>
|
|
2028
|
-
<Item class="ModuleScript" referent="
|
|
2964
|
+
<Item class="ModuleScript" referent="9">
|
|
2029
2965
|
<Properties>
|
|
2030
2966
|
<string name="Name">Protocol</string>
|
|
2031
2967
|
<string name="Source"><![CDATA[--[[
|
|
@@ -2061,7 +2997,7 @@ return Protocol
|
|
|
2061
2997
|
]]></string>
|
|
2062
2998
|
</Properties>
|
|
2063
2999
|
</Item>
|
|
2064
|
-
<Item class="ModuleScript" referent="
|
|
3000
|
+
<Item class="ModuleScript" referent="10">
|
|
2065
3001
|
<Properties>
|
|
2066
3002
|
<string name="Name">Reflection</string>
|
|
2067
3003
|
<string name="Source"><![CDATA[local ReflectionService = game:GetService("ReflectionService")
|
|
@@ -2236,7 +3172,7 @@ return Reflection
|
|
|
2236
3172
|
]]></string>
|
|
2237
3173
|
</Properties>
|
|
2238
3174
|
</Item>
|
|
2239
|
-
<Item class="ModuleScript" referent="
|
|
3175
|
+
<Item class="ModuleScript" referent="11">
|
|
2240
3176
|
<Properties>
|
|
2241
3177
|
<string name="Name">TestRunner</string>
|
|
2242
3178
|
<string name="Source"><![CDATA[local StudioTestService = game:GetService("StudioTestService")
|
|
@@ -2311,7 +3247,7 @@ return TestRunner
|
|
|
2311
3247
|
]]></string>
|
|
2312
3248
|
</Properties>
|
|
2313
3249
|
</Item>
|
|
2314
|
-
<Item class="ModuleScript" referent="
|
|
3250
|
+
<Item class="ModuleScript" referent="12">
|
|
2315
3251
|
<Properties>
|
|
2316
3252
|
<string name="Name">UIBuilder</string>
|
|
2317
3253
|
<string name="Source"><![CDATA[--[[
|
|
@@ -3174,7 +4110,7 @@ return UIBuilder
|
|
|
3174
4110
|
]]></string>
|
|
3175
4111
|
</Properties>
|
|
3176
4112
|
</Item>
|
|
3177
|
-
<Item class="ModuleScript" referent="
|
|
4113
|
+
<Item class="ModuleScript" referent="13">
|
|
3178
4114
|
<Properties>
|
|
3179
4115
|
<string name="Name">ValueCodec</string>
|
|
3180
4116
|
<string name="Source"><![CDATA[local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
@@ -3387,6 +4323,122 @@ function ValueCodec.encode(value)
|
|
|
3387
4323
|
return tostring(value)
|
|
3388
4324
|
end
|
|
3389
4325
|
|
|
4326
|
+
local TYPED_DEFAULTS = {
|
|
4327
|
+
UDim = function()
|
|
4328
|
+
return UDim.new()
|
|
4329
|
+
end,
|
|
4330
|
+
UDim2 = function()
|
|
4331
|
+
return UDim2.new()
|
|
4332
|
+
end,
|
|
4333
|
+
Vector2 = function()
|
|
4334
|
+
return Vector2.zero
|
|
4335
|
+
end,
|
|
4336
|
+
Vector3 = function()
|
|
4337
|
+
return Vector3.zero
|
|
4338
|
+
end,
|
|
4339
|
+
Color3 = function()
|
|
4340
|
+
return Color3.new()
|
|
4341
|
+
end,
|
|
4342
|
+
BrickColor = function()
|
|
4343
|
+
return BrickColor.new("Medium stone grey")
|
|
4344
|
+
end,
|
|
4345
|
+
Rect = function()
|
|
4346
|
+
return Rect.new()
|
|
4347
|
+
end,
|
|
4348
|
+
NumberRange = function()
|
|
4349
|
+
return NumberRange.new(0)
|
|
4350
|
+
end,
|
|
4351
|
+
CFrame = function()
|
|
4352
|
+
return CFrame.new()
|
|
4353
|
+
end,
|
|
4354
|
+
NumberSequence = function()
|
|
4355
|
+
return NumberSequence.new(0)
|
|
4356
|
+
end,
|
|
4357
|
+
ColorSequence = function()
|
|
4358
|
+
return ColorSequence.new(Color3.new())
|
|
4359
|
+
end,
|
|
4360
|
+
Font = function()
|
|
4361
|
+
return Font.new("rbxasset://fonts/families/SourceSansPro.json")
|
|
4362
|
+
end,
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
function ValueCodec.decodeTyped(typeName, value)
|
|
4366
|
+
assert(type(typeName) == "string", "Typed value requires a type")
|
|
4367
|
+
if typeName == "nil" then
|
|
4368
|
+
return nil
|
|
4369
|
+
end
|
|
4370
|
+
if typeName == "string" or typeName == "number" or typeName == "boolean" then
|
|
4371
|
+
return ValueCodec.decodeForCurrent(typeName == "string" and "" or typeName == "number" and 0 or false, value)
|
|
4372
|
+
end
|
|
4373
|
+
if typeName == "Instance" then
|
|
4374
|
+
local resolved, err = InstanceRegistry.resolve(value)
|
|
4375
|
+
if not resolved then
|
|
4376
|
+
error(err)
|
|
4377
|
+
end
|
|
4378
|
+
return resolved
|
|
4379
|
+
end
|
|
4380
|
+
local createDefault = TYPED_DEFAULTS[typeName]
|
|
4381
|
+
if not createDefault then
|
|
4382
|
+
error("Unsupported typed value: " .. typeName)
|
|
4383
|
+
end
|
|
4384
|
+
return ValueCodec.decodeForCurrent(createDefault(), value)
|
|
4385
|
+
end
|
|
4386
|
+
|
|
4387
|
+
local function encodeSerializable(value, depth, state, seen)
|
|
4388
|
+
if depth > 5 then
|
|
4389
|
+
return "<maximum depth reached>"
|
|
4390
|
+
end
|
|
4391
|
+
if type(value) ~= "table" then
|
|
4392
|
+
return ValueCodec.encode(value)
|
|
4393
|
+
end
|
|
4394
|
+
if seen[value] then
|
|
4395
|
+
return "<cycle>"
|
|
4396
|
+
end
|
|
4397
|
+
seen[value] = true
|
|
4398
|
+
|
|
4399
|
+
local count = 0
|
|
4400
|
+
local maxIndex = 0
|
|
4401
|
+
local isArray = true
|
|
4402
|
+
for key in value do
|
|
4403
|
+
count += 1
|
|
4404
|
+
state.count += 1
|
|
4405
|
+
if state.count > state.limit then
|
|
4406
|
+
seen[value] = nil
|
|
4407
|
+
return "<result truncated>"
|
|
4408
|
+
end
|
|
4409
|
+
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
|
|
4410
|
+
isArray = false
|
|
4411
|
+
else
|
|
4412
|
+
maxIndex = math.max(maxIndex, key)
|
|
4413
|
+
end
|
|
4414
|
+
end
|
|
4415
|
+
|
|
4416
|
+
local result = {}
|
|
4417
|
+
if isArray and maxIndex == count then
|
|
4418
|
+
for index = 1, maxIndex do
|
|
4419
|
+
result[index] = encodeSerializable(value[index], depth + 1, state, seen)
|
|
4420
|
+
end
|
|
4421
|
+
else
|
|
4422
|
+
for key, child in value do
|
|
4423
|
+
result[tostring(key)] = encodeSerializable(child, depth + 1, state, seen)
|
|
4424
|
+
end
|
|
4425
|
+
end
|
|
4426
|
+
seen[value] = nil
|
|
4427
|
+
return result
|
|
4428
|
+
end
|
|
4429
|
+
|
|
4430
|
+
function ValueCodec.encodeSerializable(value, limit)
|
|
4431
|
+
return encodeSerializable(value, 0, { count = 0, limit = limit or 500 }, {})
|
|
4432
|
+
end
|
|
4433
|
+
|
|
4434
|
+
function ValueCodec.encodeTyped(value, limit)
|
|
4435
|
+
local encoded = { type = typeof(value) }
|
|
4436
|
+
if value ~= nil then
|
|
4437
|
+
encoded.value = ValueCodec.encodeSerializable(value, limit)
|
|
4438
|
+
end
|
|
4439
|
+
return encoded
|
|
4440
|
+
end
|
|
4441
|
+
|
|
3390
4442
|
function ValueCodec.setProperty(instance, propertyName, value)
|
|
3391
4443
|
if propertyName == "Parent" or propertyName == "ClassName" or propertyName == "Source" then
|
|
3392
4444
|
error("Property cannot be changed through setProperties: " .. propertyName)
|
|
@@ -3411,7 +4463,7 @@ return ValueCodec
|
|
|
3411
4463
|
]]></string>
|
|
3412
4464
|
</Properties>
|
|
3413
4465
|
</Item>
|
|
3414
|
-
<Item class="ModuleScript" referent="
|
|
4466
|
+
<Item class="ModuleScript" referent="14">
|
|
3415
4467
|
<Properties>
|
|
3416
4468
|
<string name="Name">Watcher</string>
|
|
3417
4469
|
<string name="Source"><![CDATA[--[[
|
|
@@ -3502,7 +4554,7 @@ return Watcher
|
|
|
3502
4554
|
]]></string>
|
|
3503
4555
|
</Properties>
|
|
3504
4556
|
</Item>
|
|
3505
|
-
<Item class="ModuleScript" referent="
|
|
4557
|
+
<Item class="ModuleScript" referent="15">
|
|
3506
4558
|
<Properties>
|
|
3507
4559
|
<string name="Name">WsClient</string>
|
|
3508
4560
|
<string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
|