robloxstudio-mcp 2.4.0 → 2.5.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.
@@ -52,6 +52,9 @@ local InstanceHandlers = TS.import(script, script.Parent, "handlers", "InstanceH
52
52
  local ScriptHandlers = TS.import(script, script.Parent, "handlers", "ScriptHandlers")
53
53
  local MetadataHandlers = TS.import(script, script.Parent, "handlers", "MetadataHandlers")
54
54
  local TestHandlers = TS.import(script, script.Parent, "handlers", "TestHandlers")
55
+ local BuildHandlers = TS.import(script, script.Parent, "handlers", "BuildHandlers")
56
+ local AssetHandlers = TS.import(script, script.Parent, "handlers", "AssetHandlers")
57
+ local CaptureHandlers = TS.import(script, script.Parent, "handlers", "CaptureHandlers")
55
58
  local routeMap = {
56
59
  ["/api/file-tree"] = QueryHandlers.getFileTree,
57
60
  ["/api/search-files"] = QueryHandlers.searchFiles,
@@ -63,6 +66,7 @@ local routeMap = {
63
66
  ["/api/search-by-property"] = QueryHandlers.searchByProperty,
64
67
  ["/api/class-info"] = QueryHandlers.getClassInfo,
65
68
  ["/api/project-structure"] = QueryHandlers.getProjectStructure,
69
+ ["/api/grep-scripts"] = QueryHandlers.grepScripts,
66
70
  ["/api/set-property"] = PropertyHandlers.setProperty,
67
71
  ["/api/mass-set-property"] = PropertyHandlers.massSetProperty,
68
72
  ["/api/mass-get-property"] = PropertyHandlers.massGetProperty,
@@ -89,9 +93,18 @@ local routeMap = {
89
93
  ["/api/get-tagged"] = MetadataHandlers.getTagged,
90
94
  ["/api/get-selection"] = MetadataHandlers.getSelection,
91
95
  ["/api/execute-luau"] = MetadataHandlers.executeLuau,
96
+ ["/api/undo"] = MetadataHandlers.undo,
97
+ ["/api/redo"] = MetadataHandlers.redo,
92
98
  ["/api/start-playtest"] = TestHandlers.startPlaytest,
93
99
  ["/api/stop-playtest"] = TestHandlers.stopPlaytest,
94
100
  ["/api/get-playtest-output"] = TestHandlers.getPlaytestOutput,
101
+ ["/api/export-build"] = BuildHandlers.exportBuild,
102
+ ["/api/import-build"] = BuildHandlers.importBuild,
103
+ ["/api/import-scene"] = BuildHandlers.importScene,
104
+ ["/api/search-materials"] = BuildHandlers.searchMaterials,
105
+ ["/api/insert-asset"] = AssetHandlers.insertAsset,
106
+ ["/api/preview-asset"] = AssetHandlers.previewAsset,
107
+ ["/api/capture-screenshot"] = CaptureHandlers.captureScreenshot,
95
108
  }
96
109
  local function processRequest(request)
97
110
  local endpoint = request.endpoint
@@ -465,15 +478,1092 @@ return {
465
478
  </Properties>
466
479
  <Item class="ModuleScript" referent="4">
467
480
  <Properties>
468
- <string name="Name">InstanceHandlers</string>
481
+ <string name="Name">AssetHandlers</string>
469
482
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
470
483
  local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
471
484
  local Utils = TS.import(script, script.Parent.Parent, "Utils")
485
+ local Recording = TS.import(script, script.Parent.Parent, "Recording")
486
+ local AssetService = game:GetService("AssetService")
472
487
  local ChangeHistoryService = game:GetService("ChangeHistoryService")
488
+ local Selection = game:GetService("Selection")
489
+ local _binding = Utils
490
+ local getInstancePath = _binding.getInstancePath
491
+ local getInstanceByPath = _binding.getInstanceByPath
492
+ local _binding_1 = Recording
493
+ local beginRecording = _binding_1.beginRecording
494
+ local finishRecording = _binding_1.finishRecording
495
+ local function insertAsset(requestData)
496
+ local assetId = requestData.assetId
497
+ local _condition = (requestData.parentPath)
498
+ if _condition == nil then
499
+ _condition = "game.Workspace"
500
+ end
501
+ local parentPath = _condition
502
+ local position = requestData.position
503
+ if not (assetId ~= 0 and assetId == assetId and assetId) then
504
+ return {
505
+ error = "assetId is required",
506
+ }
507
+ end
508
+ local parentInstance = getInstanceByPath(parentPath)
509
+ if not parentInstance then
510
+ return {
511
+ error = `Parent instance not found: {parentPath}`,
512
+ }
513
+ end
514
+ local recordingId = beginRecording(`Insert asset {assetId}`)
515
+ local wrapperModel
516
+ local insertSuccess, insertResult = pcall(function()
517
+ local loadedWrapper = AssetService:LoadAssetAsync(assetId)
518
+ wrapperModel = loadedWrapper
519
+ local insertedInstances = {}
520
+ local children = loadedWrapper:GetChildren()
521
+ for _, child in children do
522
+ child.Parent = parentInstance
523
+ table.insert(insertedInstances, child)
524
+ end
525
+ if position then
526
+ local _condition_1 = position.x
527
+ if _condition_1 == nil then
528
+ _condition_1 = 0
529
+ end
530
+ local _condition_2 = position.y
531
+ if _condition_2 == nil then
532
+ _condition_2 = 0
533
+ end
534
+ local _condition_3 = position.z
535
+ if _condition_3 == nil then
536
+ _condition_3 = 0
537
+ end
538
+ local pos = Vector3.new(_condition_1, _condition_2, _condition_3)
539
+ for _, inst in insertedInstances do
540
+ if inst:IsA("BasePart") then
541
+ inst.Position = pos
542
+ elseif inst:IsA("Model") then
543
+ if inst.PrimaryPart then
544
+ inst:PivotTo(CFrame.new(pos))
545
+ else
546
+ local firstPart = inst:FindFirstChildWhichIsA("BasePart", true)
547
+ if firstPart then
548
+ inst:PivotTo(CFrame.new(pos))
549
+ end
550
+ end
551
+ end
552
+ end
553
+ end
554
+ pcall(function()
555
+ Selection:Set(insertedInstances)
556
+ end)
557
+ -- ▼ ReadonlyArray.map ▼
558
+ local _newValue = table.create(#insertedInstances)
559
+ local _callback = function(inst)
560
+ return {
561
+ name = inst.Name,
562
+ className = inst.ClassName,
563
+ path = getInstancePath(inst),
564
+ }
565
+ end
566
+ for _k, _v in insertedInstances do
567
+ _newValue[_k] = _callback(_v, _k - 1, insertedInstances)
568
+ end
569
+ -- ▲ ReadonlyArray.map ▲
570
+ local resultInstances = _newValue
571
+ return {
572
+ success = true,
573
+ assetId = assetId,
574
+ parentPath = parentPath,
575
+ insertedCount = #insertedInstances,
576
+ instances = resultInstances,
577
+ }
578
+ end)
579
+ if wrapperModel then
580
+ pcall(function()
581
+ wrapperModel:Destroy()
582
+ end)
583
+ end
584
+ finishRecording(recordingId, insertSuccess)
585
+ if not insertSuccess then
586
+ return {
587
+ error = `Failed to insert asset {assetId}: {tostring(insertResult)}`,
588
+ }
589
+ end
590
+ return insertResult
591
+ end
592
+ local function previewAsset(requestData)
593
+ local assetId = requestData.assetId
594
+ local _condition = (requestData.includeProperties)
595
+ if _condition == nil then
596
+ _condition = true
597
+ end
598
+ local includeProperties = _condition
599
+ local _condition_1 = (requestData.maxDepth)
600
+ if _condition_1 == nil then
601
+ _condition_1 = 10
602
+ end
603
+ local maxDepth = _condition_1
604
+ if not (assetId ~= 0 and assetId == assetId and assetId) then
605
+ return {
606
+ error = "assetId is required",
607
+ }
608
+ end
609
+ local loadSuccess, wrapperModel = pcall(function()
610
+ return AssetService:LoadAssetAsync(assetId)
611
+ end)
612
+ if not loadSuccess or not wrapperModel then
613
+ return {
614
+ error = `Failed to load asset {assetId}: {tostring(wrapperModel)}`,
615
+ }
616
+ end
617
+ -- Stats tracking
618
+ local totalInstances = 0
619
+ local classCounts = {}
620
+ local hasScripts = false
621
+ local hasAnimations = false
622
+ local hasSounds = false
623
+ local hasParticles = false
624
+ local function buildHierarchy(instance, depth)
625
+ totalInstances += 1
626
+ local className = instance.ClassName
627
+ local _condition_2 = classCounts[className]
628
+ if _condition_2 == nil then
629
+ _condition_2 = 0
630
+ end
631
+ classCounts[className] = _condition_2 + 1
632
+ if instance:IsA("LuaSourceContainer") then
633
+ hasScripts = true
634
+ end
635
+ if className == "Animation" or className == "AnimationController" or className == "Animator" then
636
+ hasAnimations = true
637
+ end
638
+ if instance:IsA("Sound") then
639
+ hasSounds = true
640
+ end
641
+ if className == "ParticleEmitter" or className == "Fire" or className == "Smoke" or className == "Sparkles" then
642
+ hasParticles = true
643
+ end
644
+ local node = {
645
+ name = instance.Name,
646
+ className = className,
647
+ }
648
+ if includeProperties then
649
+ local props = {}
650
+ if instance:IsA("BasePart") then
651
+ props.size = {
652
+ x = instance.Size.X,
653
+ y = instance.Size.Y,
654
+ z = instance.Size.Z,
655
+ }
656
+ props.position = {
657
+ x = instance.Position.X,
658
+ y = instance.Position.Y,
659
+ z = instance.Position.Z,
660
+ }
661
+ props.material = tostring(instance.Material)
662
+ props.color = `{instance.Color.R}, {instance.Color.G}, {instance.Color.B}`
663
+ props.transparency = instance.Transparency
664
+ props.anchored = instance.Anchored
665
+ end
666
+ if instance:IsA("MeshPart") then
667
+ local meshPart = instance
668
+ props.meshId = meshPart.MeshId
669
+ props.textureId = meshPart.TextureID
670
+ end
671
+ if instance:IsA("Model") then
672
+ local model = instance
673
+ if model.PrimaryPart then
674
+ props.primaryPart = model.PrimaryPart.Name
675
+ end
676
+ end
677
+ if instance:IsA("LuaSourceContainer") then
678
+ local ok, src = pcall(function()
679
+ return instance.Source
680
+ end)
681
+ local _value = ok and src
682
+ if _value ~= "" and _value then
683
+ local preview = string.sub(src, 1, 200)
684
+ props.sourcePreview = preview
685
+ props.sourceLength = #src
686
+ end
687
+ end
688
+ if className == "Decal" or className == "Texture" then
689
+ local ok, texId = pcall(function()
690
+ return instance.Texture
691
+ end)
692
+ if ok then
693
+ props.texture = texId
694
+ end
695
+ end
696
+ if instance:IsA("Sound") then
697
+ props.soundId = instance.SoundId
698
+ end
699
+ -- Only include props if there are any
700
+ local hasProps = false
701
+ for _element, _element_1 in pairs(props) do
702
+ local _ = { _element, _element_1 }
703
+ hasProps = true
704
+ break
705
+ end
706
+ if hasProps then
707
+ node.properties = props
708
+ end
709
+ end
710
+ if depth < maxDepth then
711
+ local childNodes = {}
712
+ for _, child in instance:GetChildren() do
713
+ local _arg0 = buildHierarchy(child, depth + 1)
714
+ table.insert(childNodes, _arg0)
715
+ end
716
+ if #childNodes > 0 then
717
+ node.children = childNodes
718
+ end
719
+ else
720
+ local childCount = #instance:GetChildren()
721
+ if childCount > 0 then
722
+ node.childCount = childCount
723
+ node.truncated = true
724
+ end
725
+ end
726
+ return node
727
+ end
728
+ local previewSuccess, previewResult = pcall(function()
729
+ local hierarchyRoots = {}
730
+ for _, child in wrapperModel:GetChildren() do
731
+ local _arg0 = buildHierarchy(child, 0)
732
+ table.insert(hierarchyRoots, _arg0)
733
+ end
734
+ return {
735
+ success = true,
736
+ assetId = assetId,
737
+ hierarchy = hierarchyRoots,
738
+ summary = {
739
+ totalInstances = totalInstances,
740
+ classCounts = classCounts,
741
+ hasScripts = hasScripts,
742
+ hasAnimations = hasAnimations,
743
+ hasSounds = hasSounds,
744
+ hasParticles = hasParticles,
745
+ },
746
+ }
747
+ end)
748
+ pcall(function()
749
+ wrapperModel:Destroy()
750
+ end)
751
+ if not previewSuccess then
752
+ return {
753
+ error = `Failed to preview asset {assetId}: {tostring(previewResult)}`,
754
+ }
755
+ end
756
+ return previewResult
757
+ end
758
+ return {
759
+ insertAsset = insertAsset,
760
+ previewAsset = previewAsset,
761
+ }
762
+ ]]></string>
763
+ </Properties>
764
+ </Item>
765
+ <Item class="ModuleScript" referent="5">
766
+ <Properties>
767
+ <string name="Name">BuildHandlers</string>
768
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
769
+ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
770
+ local Utils = TS.import(script, script.Parent.Parent, "Utils")
771
+ local Recording = TS.import(script, script.Parent.Parent, "Recording")
772
+ local MaterialService = game:GetService("MaterialService")
773
+ local _binding = Utils
774
+ local getInstancePath = _binding.getInstancePath
775
+ local getInstanceByPath = _binding.getInstanceByPath
776
+ local _binding_1 = Recording
777
+ local beginRecording = _binding_1.beginRecording
778
+ local finishRecording = _binding_1.finishRecording
779
+ local MATERIAL_BY_NAME = {}
780
+ for _, enumItem in Enum.Material:GetEnumItems() do
781
+ local _name = enumItem.Name
782
+ MATERIAL_BY_NAME[_name] = enumItem
783
+ end
784
+ -- Shape class mapping
785
+ local SHAPE_CLASSES = {
786
+ Block = "Part",
787
+ Wedge = "WedgePart",
788
+ Cylinder = "Part",
789
+ Ball = "Part",
790
+ CornerWedge = "CornerWedgePart",
791
+ }
792
+ local PALETTE_KEYS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
793
+ local function roundTo(n, decimals)
794
+ local mult = 10 ^ decimals
795
+ return math.round(n * mult) / mult
796
+ end
797
+ local function encodePaletteKey(index)
798
+ local base = #PALETTE_KEYS
799
+ local value = math.floor(index) + 1
800
+ local encoded = ""
801
+ while value > 0 do
802
+ value -= 1
803
+ local digit = value % base
804
+ local _arg0 = digit + 1
805
+ local _arg1 = digit + 1
806
+ encoded = string.sub(PALETTE_KEYS, _arg0, _arg1) .. encoded
807
+ value = math.floor(value / base)
808
+ end
809
+ return encoded
810
+ end
811
+ local function getVariantName(part)
812
+ local variantName = part.MaterialVariant
813
+ if variantName == "" then
814
+ local ok, variantAttr = pcall(function()
815
+ return part:GetAttribute("MaterialVariant")
816
+ end)
817
+ if ok and type(variantAttr) == "string" then
818
+ variantName = variantAttr
819
+ end
820
+ end
821
+ return variantName
822
+ end
823
+ local function exportBuild(requestData)
824
+ local instancePath = requestData.instancePath
825
+ local outputId = requestData.outputId
826
+ local _condition = (requestData.style)
827
+ if _condition == nil then
828
+ _condition = "misc"
829
+ end
830
+ local style = _condition
831
+ if not (instancePath ~= "" and instancePath) then
832
+ return {
833
+ error = "Instance path is required",
834
+ }
835
+ end
836
+ local instance = getInstanceByPath(instancePath)
837
+ if not instance then
838
+ return {
839
+ error = `Instance not found: {instancePath}`,
840
+ }
841
+ end
842
+ if not instance:IsA("Model") and not instance:IsA("Folder") then
843
+ return {
844
+ error = "Instance must be a Model or Folder",
845
+ }
846
+ end
847
+ local success, result = pcall(function()
848
+ local descendants = instance:GetDescendants()
849
+ local baseParts = {}
850
+ for _, desc in descendants do
851
+ if desc:IsA("BasePart") then
852
+ table.insert(baseParts, desc)
853
+ end
854
+ end
855
+ if #baseParts == 0 then
856
+ return {
857
+ error = "No BaseParts found in instance",
858
+ }
859
+ end
860
+ -- Compute bounding box center
861
+ local minX = math.huge
862
+ local minY = math.huge
863
+ local minZ = math.huge
864
+ local maxX = -math.huge
865
+ local maxY = -math.huge
866
+ local maxZ = -math.huge
867
+ for _, part in baseParts do
868
+ local pos = part.Position
869
+ local sz = part.Size
870
+ local halfX = sz.X / 2
871
+ local halfY = sz.Y / 2
872
+ local halfZ = sz.Z / 2
873
+ minX = math.min(minX, pos.X - halfX)
874
+ minY = math.min(minY, pos.Y - halfY)
875
+ minZ = math.min(minZ, pos.Z - halfZ)
876
+ maxX = math.max(maxX, pos.X + halfX)
877
+ maxY = math.max(maxY, pos.Y + halfY)
878
+ maxZ = math.max(maxZ, pos.Z + halfZ)
879
+ end
880
+ local centerX = (minX + maxX) / 2
881
+ local centerY = minY
882
+ local centerZ = (minZ + maxZ) / 2
883
+ local boundsX = roundTo(maxX - minX, 1)
884
+ local boundsY = roundTo(maxY - minY, 1)
885
+ local boundsZ = roundTo(maxZ - minZ, 1)
886
+ -- Build palette from unique (BrickColor, Material, MaterialVariant?) combos
887
+ local paletteMap = {}
888
+ local palette = {}
889
+ local keyIndex = 0
890
+ for _, part in baseParts do
891
+ local colorName = part.BrickColor.Name
892
+ local materialName = part.Material.Name
893
+ local variantName = getVariantName(part)
894
+ local combo = if variantName ~= "" then `{colorName}|{materialName}|{variantName}` else `{colorName}|{materialName}`
895
+ if not (paletteMap[combo] ~= nil) then
896
+ local key = encodePaletteKey(keyIndex)
897
+ keyIndex += 1
898
+ paletteMap[combo] = key
899
+ if variantName ~= "" then
900
+ palette[key] = { colorName, materialName, variantName }
901
+ else
902
+ palette[key] = { colorName, materialName }
903
+ end
904
+ end
905
+ end
906
+ -- Build compact part arrays
907
+ local parts = {}
908
+ for _, part in baseParts do
909
+ local pos = part.Position
910
+ local orient = part.Orientation
911
+ local sz = part.Size
912
+ local colorName = part.BrickColor.Name
913
+ local materialName = part.Material.Name
914
+ local variantName = getVariantName(part)
915
+ local combo = if variantName ~= "" then `{colorName}|{materialName}|{variantName}` else `{colorName}|{materialName}`
916
+ local _condition_1 = paletteMap[combo]
917
+ if _condition_1 == nil then
918
+ _condition_1 = "a"
919
+ end
920
+ local paletteKey = _condition_1
921
+ -- Relative position to center
922
+ local relX = roundTo(pos.X - centerX, 1)
923
+ local relY = roundTo(pos.Y - centerY, 1)
924
+ local relZ = roundTo(pos.Z - centerZ, 1)
925
+ local sizeX = roundTo(sz.X, 1)
926
+ local sizeY = roundTo(sz.Y, 1)
927
+ local sizeZ = roundTo(sz.Z, 1)
928
+ local rotX = roundTo(orient.X, 1)
929
+ local rotY = roundTo(orient.Y, 1)
930
+ local rotZ = roundTo(orient.Z, 1)
931
+ -- Determine shape
932
+ local shape = "Block"
933
+ if part:IsA("WedgePart") then
934
+ shape = "Wedge"
935
+ elseif part:IsA("CornerWedgePart") then
936
+ shape = "CornerWedge"
937
+ elseif part:IsA("Part") then
938
+ local p = part
939
+ if p.Shape == Enum.PartType.Cylinder then
940
+ shape = "Cylinder"
941
+ elseif p.Shape == Enum.PartType.Ball then
942
+ shape = "Ball"
943
+ end
944
+ end
945
+ -- Build part array with optional trailing fields
946
+ local hasTransparency = part.Transparency > 0
947
+ local hasShape = shape ~= "Block"
948
+ local partArr
949
+ if hasTransparency then
950
+ partArr = { relX, relY, relZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, if hasShape then shape else "Block", roundTo(part.Transparency, 2) }
951
+ elseif hasShape then
952
+ partArr = { relX, relY, relZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey, shape }
953
+ else
954
+ partArr = { relX, relY, relZ, sizeX, sizeY, sizeZ, rotX, rotY, rotZ, paletteKey }
955
+ end
956
+ local _partArr = partArr
957
+ table.insert(parts, _partArr)
958
+ end
959
+ local _condition_1 = outputId
960
+ if _condition_1 == nil then
961
+ _condition_1 = `{style}/{(string.gsub(string.lower(instance.Name), " ", "_"))}`
962
+ end
963
+ local buildId = _condition_1
964
+ return {
965
+ success = true,
966
+ buildData = {
967
+ id = buildId,
968
+ style = style,
969
+ bounds = { boundsX, boundsY, boundsZ },
970
+ palette = palette,
971
+ parts = parts,
972
+ },
973
+ }
974
+ end)
975
+ if success and result then
976
+ return result
977
+ else
978
+ return {
979
+ error = `Failed to export build: {result}`,
980
+ }
981
+ end
982
+ end
983
+ local function importBuild(requestData)
984
+ local buildData = requestData.buildData
985
+ local targetPath = requestData.targetPath
986
+ local positionOffset = (requestData.position) or { 0, 0, 0 }
987
+ if not buildData or not (targetPath ~= "" and targetPath) then
988
+ return {
989
+ error = "buildData and targetPath are required",
990
+ }
991
+ end
992
+ local parentInstance = getInstanceByPath(targetPath)
993
+ if not parentInstance then
994
+ return {
995
+ error = `Target not found: {targetPath}`,
996
+ }
997
+ end
998
+ local recordingId = beginRecording("Import build")
999
+ local success, result = pcall(function()
1000
+ local palette = buildData.palette
1001
+ local parts = buildData.parts
1002
+ local _condition = (buildData.id)
1003
+ if _condition == nil then
1004
+ _condition = "imported_build"
1005
+ end
1006
+ local buildId = _condition
1007
+ -- Create model container
1008
+ local model = Instance.new("Model")
1009
+ local _condition_1 = (string.match(buildId, "[^/]+$"))
1010
+ if _condition_1 == nil then
1011
+ _condition_1 = buildId
1012
+ end
1013
+ model.Name = _condition_1
1014
+ local partCount = 0
1015
+ for _, partArr in parts do
1016
+ local _exp = (partArr[1])
1017
+ local _condition_2 = positionOffset[1]
1018
+ if _condition_2 == nil then
1019
+ _condition_2 = 0
1020
+ end
1021
+ local posX = _exp + _condition_2
1022
+ local _exp_1 = (partArr[2])
1023
+ local _condition_3 = positionOffset[2]
1024
+ if _condition_3 == nil then
1025
+ _condition_3 = 0
1026
+ end
1027
+ local posY = _exp_1 + _condition_3
1028
+ local _exp_2 = (partArr[3])
1029
+ local _condition_4 = positionOffset[3]
1030
+ if _condition_4 == nil then
1031
+ _condition_4 = 0
1032
+ end
1033
+ local posZ = _exp_2 + _condition_4
1034
+ local sizeX = partArr[4]
1035
+ local sizeY = partArr[5]
1036
+ local sizeZ = partArr[6]
1037
+ local rotX = partArr[7]
1038
+ local rotY = partArr[8]
1039
+ local rotZ = partArr[9]
1040
+ local paletteKey = partArr[10]
1041
+ local _condition_5 = (partArr[11])
1042
+ if _condition_5 == nil then
1043
+ _condition_5 = "Block"
1044
+ end
1045
+ local shape = _condition_5
1046
+ local _condition_6 = (partArr[12])
1047
+ if _condition_6 == nil then
1048
+ _condition_6 = 0
1049
+ end
1050
+ local transparency = _condition_6
1051
+ -- Determine class from shape
1052
+ local _condition_7 = SHAPE_CLASSES[shape]
1053
+ if _condition_7 == nil then
1054
+ _condition_7 = "Part"
1055
+ end
1056
+ local className = _condition_7
1057
+ local part = Instance.new(className)
1058
+ -- Set shape for Part instances with non-Block shapes
1059
+ if className == "Part" and shape ~= "Block" then
1060
+ if shape == "Cylinder" then
1061
+ part.Shape = Enum.PartType.Cylinder
1062
+ elseif shape == "Ball" then
1063
+ part.Shape = Enum.PartType.Ball
1064
+ end
1065
+ end
1066
+ part.Size = Vector3.new(sizeX, sizeY, sizeZ)
1067
+ part.Position = Vector3.new(posX, posY, posZ)
1068
+ part.Orientation = Vector3.new(rotX, rotY, rotZ)
1069
+ part.Anchored = true
1070
+ if transparency > 0 then
1071
+ part.Transparency = transparency
1072
+ end
1073
+ -- Apply palette
1074
+ local paletteEntry = palette[paletteKey]
1075
+ if paletteEntry then
1076
+ local _binding_2 = paletteEntry
1077
+ local colorName = _binding_2[1]
1078
+ local materialName = _binding_2[2]
1079
+ local variantName = _binding_2[3]
1080
+ pcall(function()
1081
+ part.BrickColor = BrickColor.new(colorName)
1082
+ end)
1083
+ pcall(function()
1084
+ local mat = MATERIAL_BY_NAME[materialName]
1085
+ if mat ~= nil then
1086
+ part.Material = mat
1087
+ end
1088
+ end)
1089
+ -- Apply MaterialVariant if specified
1090
+ if variantName ~= nil and variantName ~= "" then
1091
+ pcall(function()
1092
+ part.MaterialVariant = variantName
1093
+ end)
1094
+ end
1095
+ end
1096
+ part.Parent = model
1097
+ partCount += 1
1098
+ end
1099
+ model.Parent = parentInstance
1100
+ return {
1101
+ success = true,
1102
+ partCount = partCount,
1103
+ modelPath = getInstancePath(model),
1104
+ }
1105
+ end)
1106
+ if success and result then
1107
+ finishRecording(recordingId, true)
1108
+ return result
1109
+ else
1110
+ finishRecording(recordingId, false)
1111
+ return {
1112
+ error = `Failed to import build: {result}`,
1113
+ }
1114
+ end
1115
+ end
1116
+ local function importScene(requestData)
1117
+ local expandedBuilds = requestData.expandedBuilds
1118
+ local _condition = (requestData.targetPath)
1119
+ if _condition == nil then
1120
+ _condition = "game.Workspace"
1121
+ end
1122
+ local targetPath = _condition
1123
+ if not expandedBuilds or not (type(expandedBuilds) == "table") or #expandedBuilds == 0 then
1124
+ return {
1125
+ error = "expandedBuilds array is required",
1126
+ }
1127
+ end
1128
+ local parentInstance = getInstanceByPath(targetPath)
1129
+ if not parentInstance then
1130
+ return {
1131
+ error = `Target not found: {targetPath}`,
1132
+ }
1133
+ end
1134
+ local recordingId = beginRecording("Import scene")
1135
+ local success, result = pcall(function()
1136
+ local modelCount = 0
1137
+ local totalParts = 0
1138
+ local models = {}
1139
+ for _, entry in expandedBuilds do
1140
+ local buildData = entry.buildData
1141
+ local position = (entry.position) or { 0, 0, 0 }
1142
+ local rotation = (entry.rotation) or { 0, 0, 0 }
1143
+ local _condition_1 = (entry.name)
1144
+ if _condition_1 == nil then
1145
+ _condition_1 = "SceneModel"
1146
+ end
1147
+ local name = _condition_1
1148
+ local palette = buildData.palette
1149
+ local parts = buildData.parts
1150
+ local model = Instance.new("Model")
1151
+ model.Name = name
1152
+ local _condition_2 = rotation[1]
1153
+ if _condition_2 == nil then
1154
+ _condition_2 = 0
1155
+ end
1156
+ local _exp = math.rad(_condition_2)
1157
+ local _condition_3 = rotation[2]
1158
+ if _condition_3 == nil then
1159
+ _condition_3 = 0
1160
+ end
1161
+ local _exp_1 = math.rad(_condition_3)
1162
+ local _condition_4 = rotation[3]
1163
+ if _condition_4 == nil then
1164
+ _condition_4 = 0
1165
+ end
1166
+ local rotCF = CFrame.Angles(_exp, _exp_1, math.rad(_condition_4))
1167
+ local _condition_5 = position[1]
1168
+ if _condition_5 == nil then
1169
+ _condition_5 = 0
1170
+ end
1171
+ local _condition_6 = position[2]
1172
+ if _condition_6 == nil then
1173
+ _condition_6 = 0
1174
+ end
1175
+ local _condition_7 = position[3]
1176
+ if _condition_7 == nil then
1177
+ _condition_7 = 0
1178
+ end
1179
+ local originCF = CFrame.new(_condition_5, _condition_6, _condition_7) * rotCF
1180
+ local partCount = 0
1181
+ for _1, partArr in parts do
1182
+ local localX = partArr[1]
1183
+ local localY = partArr[2]
1184
+ local localZ = partArr[3]
1185
+ local sizeX = partArr[4]
1186
+ local sizeY = partArr[5]
1187
+ local sizeZ = partArr[6]
1188
+ local rotX = partArr[7]
1189
+ local rotY = partArr[8]
1190
+ local rotZ = partArr[9]
1191
+ local paletteKey = partArr[10]
1192
+ local _condition_8 = (partArr[11])
1193
+ if _condition_8 == nil then
1194
+ _condition_8 = "Block"
1195
+ end
1196
+ local shape = _condition_8
1197
+ local _condition_9 = (partArr[12])
1198
+ if _condition_9 == nil then
1199
+ _condition_9 = 0
1200
+ end
1201
+ local transparency = _condition_9
1202
+ local _condition_10 = SHAPE_CLASSES[shape]
1203
+ if _condition_10 == nil then
1204
+ _condition_10 = "Part"
1205
+ end
1206
+ local className = _condition_10
1207
+ local part = Instance.new(className)
1208
+ if className == "Part" and shape ~= "Block" then
1209
+ if shape == "Cylinder" then
1210
+ part.Shape = Enum.PartType.Cylinder
1211
+ elseif shape == "Ball" then
1212
+ part.Shape = Enum.PartType.Ball
1213
+ end
1214
+ end
1215
+ part.Size = Vector3.new(sizeX, sizeY, sizeZ)
1216
+ -- Apply local rotation then world transform
1217
+ local localRotCF = CFrame.Angles(math.rad(rotX), math.rad(rotY), math.rad(rotZ))
1218
+ local localPosCF = CFrame.new(localX, localY, localZ) * localRotCF
1219
+ local worldCF = originCF * localPosCF
1220
+ part.CFrame = worldCF
1221
+ part.Anchored = true
1222
+ if transparency > 0 then
1223
+ part.Transparency = transparency
1224
+ end
1225
+ local paletteEntry = palette[paletteKey]
1226
+ if paletteEntry then
1227
+ local _binding_2 = paletteEntry
1228
+ local colorName = _binding_2[1]
1229
+ local materialName = _binding_2[2]
1230
+ local variantName = _binding_2[3]
1231
+ pcall(function()
1232
+ part.BrickColor = BrickColor.new(colorName)
1233
+ end)
1234
+ pcall(function()
1235
+ local mat = MATERIAL_BY_NAME[materialName]
1236
+ if mat ~= nil then
1237
+ part.Material = mat
1238
+ end
1239
+ end)
1240
+ if variantName ~= nil and variantName ~= "" then
1241
+ pcall(function()
1242
+ part.MaterialVariant = variantName
1243
+ end)
1244
+ end
1245
+ end
1246
+ part.Parent = model
1247
+ partCount += 1
1248
+ end
1249
+ model.Parent = parentInstance
1250
+ modelCount += 1
1251
+ totalParts += partCount
1252
+ local _arg0 = {
1253
+ name = name,
1254
+ partCount = partCount,
1255
+ modelPath = getInstancePath(model),
1256
+ }
1257
+ table.insert(models, _arg0)
1258
+ end
1259
+ return {
1260
+ success = true,
1261
+ modelCount = modelCount,
1262
+ totalParts = totalParts,
1263
+ models = models,
1264
+ }
1265
+ end)
1266
+ if success and result then
1267
+ finishRecording(recordingId, true)
1268
+ return result
1269
+ else
1270
+ finishRecording(recordingId, false)
1271
+ return {
1272
+ error = `Failed to import scene: {result}`,
1273
+ }
1274
+ end
1275
+ end
1276
+ local function searchMaterials(requestData)
1277
+ local _condition = (requestData.query)
1278
+ if _condition == nil then
1279
+ _condition = ""
1280
+ end
1281
+ local query = string.lower(_condition)
1282
+ local _condition_1 = (requestData.maxResults)
1283
+ if _condition_1 == nil then
1284
+ _condition_1 = 50
1285
+ end
1286
+ local maxResults = _condition_1
1287
+ local success, result = pcall(function()
1288
+ local children = MaterialService:GetChildren()
1289
+ local materials = {}
1290
+ for _, child in children do
1291
+ if not child:IsA("MaterialVariant") then
1292
+ continue
1293
+ end
1294
+ local nameMatch = query == "" or (string.find(string.lower(child.Name), query)) ~= nil
1295
+ if not nameMatch then
1296
+ continue
1297
+ end
1298
+ local _arg0 = {
1299
+ name = child.Name,
1300
+ baseMaterial = child.BaseMaterial.Name,
1301
+ }
1302
+ table.insert(materials, _arg0)
1303
+ if #materials >= maxResults then
1304
+ break
1305
+ end
1306
+ end
1307
+ return {
1308
+ success = true,
1309
+ materials = materials,
1310
+ total = #materials,
1311
+ }
1312
+ end)
1313
+ if success and result then
1314
+ return result
1315
+ else
1316
+ return {
1317
+ error = `Failed to search materials: {result}`,
1318
+ }
1319
+ end
1320
+ end
1321
+ return {
1322
+ exportBuild = exportBuild,
1323
+ importBuild = importBuild,
1324
+ importScene = importScene,
1325
+ searchMaterials = searchMaterials,
1326
+ }
1327
+ ]]></string>
1328
+ </Properties>
1329
+ </Item>
1330
+ <Item class="ModuleScript" referent="6">
1331
+ <Properties>
1332
+ <string name="Name">CaptureHandlers</string>
1333
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
1334
+ local CaptureService = game:GetService("CaptureService")
1335
+ local AssetService = game:GetService("AssetService")
1336
+ local MAX_TILE_SIZE = 1024
1337
+ local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
1338
+ local function encodeBase64(buf)
1339
+ local len = buffer.len(buf)
1340
+ local parts = {}
1341
+ local i = 0
1342
+ while i + 2 < len do
1343
+ local b0 = buffer.readu8(buf, i)
1344
+ local b1 = buffer.readu8(buf, i + 1)
1345
+ local b2 = buffer.readu8(buf, i + 2)
1346
+ local triplet = bit32.lshift(b0, 16) + bit32.lshift(b1, 8) + b2
1347
+ local _arg0 = string.sub(BASE64_CHARS, bit32.rshift(triplet, 18) + 1, bit32.rshift(triplet, 18) + 1) .. string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 12), 63) + 1, bit32.band(bit32.rshift(triplet, 12), 63) + 1) .. string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 6), 63) + 1, bit32.band(bit32.rshift(triplet, 6), 63) + 1) .. string.sub(BASE64_CHARS, bit32.band(triplet, 63) + 1, bit32.band(triplet, 63) + 1)
1348
+ table.insert(parts, _arg0)
1349
+ i += 3
1350
+ end
1351
+ local remaining = len - i
1352
+ if remaining == 2 then
1353
+ local b0 = buffer.readu8(buf, i)
1354
+ local b1 = buffer.readu8(buf, i + 1)
1355
+ local triplet = bit32.lshift(b0, 16) + bit32.lshift(b1, 8)
1356
+ local _arg0 = string.sub(BASE64_CHARS, bit32.rshift(triplet, 18) + 1, bit32.rshift(triplet, 18) + 1) .. string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 12), 63) + 1, bit32.band(bit32.rshift(triplet, 12), 63) + 1) .. string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 6), 63) + 1, bit32.band(bit32.rshift(triplet, 6), 63) + 1) .. "="
1357
+ table.insert(parts, _arg0)
1358
+ elseif remaining == 1 then
1359
+ local b0 = buffer.readu8(buf, i)
1360
+ local triplet = bit32.lshift(b0, 16)
1361
+ local _arg0 = string.sub(BASE64_CHARS, bit32.rshift(triplet, 18) + 1, bit32.rshift(triplet, 18) + 1) .. string.sub(BASE64_CHARS, bit32.band(bit32.rshift(triplet, 12), 63) + 1, bit32.band(bit32.rshift(triplet, 12), 63) + 1) .. "=="
1362
+ table.insert(parts, _arg0)
1363
+ end
1364
+ return table.concat(parts, "")
1365
+ end
1366
+ local function readPixelsTiled(img, w, h)
1367
+ local BYTES_PER_PIXEL = 4
1368
+ local fullBuf = buffer.create(w * h * BYTES_PER_PIXEL)
1369
+ local fullRowBytes = w * BYTES_PER_PIXEL
1370
+ do
1371
+ local ty = 0
1372
+ local _shouldIncrement = false
1373
+ while true do
1374
+ if _shouldIncrement then
1375
+ ty += MAX_TILE_SIZE
1376
+ else
1377
+ _shouldIncrement = true
1378
+ end
1379
+ if not (ty < h) then
1380
+ break
1381
+ end
1382
+ local tileH = math.min(MAX_TILE_SIZE, h - ty)
1383
+ do
1384
+ local tx = 0
1385
+ local _shouldIncrement_1 = false
1386
+ while true do
1387
+ if _shouldIncrement_1 then
1388
+ tx += MAX_TILE_SIZE
1389
+ else
1390
+ _shouldIncrement_1 = true
1391
+ end
1392
+ if not (tx < w) then
1393
+ break
1394
+ end
1395
+ local tileW = math.min(MAX_TILE_SIZE, w - tx)
1396
+ local tileBuf = img:ReadPixelsBuffer(Vector2.new(tx, ty), Vector2.new(tileW, tileH))
1397
+ local tileRowBytes = tileW * BYTES_PER_PIXEL
1398
+ do
1399
+ local row = 0
1400
+ local _shouldIncrement_2 = false
1401
+ while true do
1402
+ if _shouldIncrement_2 then
1403
+ row += 1
1404
+ else
1405
+ _shouldIncrement_2 = true
1406
+ end
1407
+ if not (row < tileH) then
1408
+ break
1409
+ end
1410
+ buffer.copy(fullBuf, (ty + row) * fullRowBytes + tx * BYTES_PER_PIXEL, tileBuf, row * tileRowBytes, tileRowBytes)
1411
+ end
1412
+ end
1413
+ end
1414
+ end
1415
+ end
1416
+ end
1417
+ return fullBuf
1418
+ end
1419
+ local function captureScreenshot()
1420
+ local contentId
1421
+ CaptureService:CaptureScreenshot(function(id)
1422
+ contentId = id
1423
+ end)
1424
+ local startTime = tick()
1425
+ while contentId == nil do
1426
+ if tick() - startTime > 10 then
1427
+ return {
1428
+ error = "Screenshot capture timed out. Ensure the Studio viewport is visible and you are in Edit mode (not Play mode). Known Roblox bug: capture may fail if viewport renders a solid color.",
1429
+ }
1430
+ end
1431
+ task.wait(0.1)
1432
+ end
1433
+ local editableOk, editableResult = pcall(function()
1434
+ return AssetService:CreateEditableImageAsync(Content.fromUri(contentId))
1435
+ end)
1436
+ if not editableOk then
1437
+ return {
1438
+ error = `Failed to create EditableImage from screenshot. Enable EditableImage API: Game Settings > Security > 'Allow Mesh / Image APIs'. ({tostring(editableResult)})`,
1439
+ }
1440
+ end
1441
+ local editableImage = editableResult
1442
+ local imgSize = editableImage.Size
1443
+ local w = math.floor(imgSize.X)
1444
+ local h = math.floor(imgSize.Y)
1445
+ local readOk, pixelBuffer = pcall(function()
1446
+ return readPixelsTiled(editableImage, w, h)
1447
+ end)
1448
+ editableImage:Destroy()
1449
+ if not readOk then
1450
+ return {
1451
+ error = `Failed to read pixel data: {tostring(pixelBuffer)}`,
1452
+ }
1453
+ end
1454
+ local base64Data = encodeBase64(pixelBuffer)
1455
+ return {
1456
+ success = true,
1457
+ width = w,
1458
+ height = h,
1459
+ data = base64Data,
1460
+ }
1461
+ end
1462
+ return {
1463
+ captureScreenshot = captureScreenshot,
1464
+ }
1465
+ ]]></string>
1466
+ </Properties>
1467
+ </Item>
1468
+ <Item class="ModuleScript" referent="7">
1469
+ <Properties>
1470
+ <string name="Name">InstanceHandlers</string>
1471
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
1472
+ local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
1473
+ local Utils = TS.import(script, script.Parent.Parent, "Utils")
1474
+ local Recording = TS.import(script, script.Parent.Parent, "Recording")
473
1475
  local _binding = Utils
474
1476
  local getInstancePath = _binding.getInstancePath
475
1477
  local getInstanceByPath = _binding.getInstanceByPath
476
1478
  local convertPropertyValue = _binding.convertPropertyValue
1479
+ local _binding_1 = Recording
1480
+ local beginRecording = _binding_1.beginRecording
1481
+ local finishRecording = _binding_1.finishRecording
1482
+ local function processObjectEntries(objects, createFn)
1483
+ local results = {}
1484
+ local successCount = 0
1485
+ local failureCount = 0
1486
+ local loopSuccess, loopError = pcall(function()
1487
+ for _, entry in objects do
1488
+ if not (type(entry) == "table") then
1489
+ failureCount += 1
1490
+ table.insert(results, {
1491
+ success = false,
1492
+ error = "Each object entry must be a table",
1493
+ })
1494
+ continue
1495
+ end
1496
+ local objData = entry
1497
+ local className = objData.className
1498
+ local parentPath = objData.parent
1499
+ if not (className ~= "" and className) or not (parentPath ~= "" and parentPath) then
1500
+ failureCount += 1
1501
+ table.insert(results, {
1502
+ success = false,
1503
+ error = "Class name and parent are required",
1504
+ })
1505
+ continue
1506
+ end
1507
+ local entrySuccess, entryResult = pcall(function()
1508
+ return createFn(objData)
1509
+ end)
1510
+ if not entrySuccess then
1511
+ failureCount += 1
1512
+ local _arg0 = {
1513
+ success = false,
1514
+ className = className,
1515
+ parent = parentPath,
1516
+ error = tostring(entryResult),
1517
+ }
1518
+ table.insert(results, _arg0)
1519
+ continue
1520
+ end
1521
+ if entryResult.instance ~= nil then
1522
+ successCount += 1
1523
+ local _arg0 = {
1524
+ success = true,
1525
+ className = entryResult.className,
1526
+ parent = entryResult.parentPath,
1527
+ instancePath = getInstancePath(entryResult.instance),
1528
+ name = entryResult.instance.Name,
1529
+ }
1530
+ table.insert(results, _arg0)
1531
+ else
1532
+ failureCount += 1
1533
+ local _object = {
1534
+ success = false,
1535
+ }
1536
+ local _left = "className"
1537
+ local _condition = entryResult.className
1538
+ if _condition == nil then
1539
+ _condition = className
1540
+ end
1541
+ _object[_left] = _condition
1542
+ local _left_1 = "parent"
1543
+ local _condition_1 = entryResult.parentPath
1544
+ if _condition_1 == nil then
1545
+ _condition_1 = parentPath
1546
+ end
1547
+ _object[_left_1] = _condition_1
1548
+ _object.error = entryResult.error
1549
+ table.insert(results, _object)
1550
+ end
1551
+ end
1552
+ end)
1553
+ if not loopSuccess then
1554
+ failureCount += 1
1555
+ local _arg0 = {
1556
+ success = false,
1557
+ error = `Unexpected mass create failure: {tostring(loopError)}`,
1558
+ }
1559
+ table.insert(results, _arg0)
1560
+ end
1561
+ return {
1562
+ results = results,
1563
+ successCount = successCount,
1564
+ failureCount = failureCount,
1565
+ }
1566
+ end
477
1567
  local function createObject(requestData)
478
1568
  local className = requestData.className
479
1569
  local parentPath = requestData.parent
@@ -490,6 +1580,7 @@ local function createObject(requestData)
490
1580
  error = `Parent instance not found: {parentPath}`,
491
1581
  }
492
1582
  end
1583
+ local recordingId = beginRecording(`Create {className}`)
493
1584
  local success, newInstance = pcall(function()
494
1585
  local instance = Instance.new(className)
495
1586
  if name ~= "" and name then
@@ -501,10 +1592,10 @@ local function createObject(requestData)
501
1592
  end)
502
1593
  end
503
1594
  instance.Parent = parentInstance
504
- ChangeHistoryService:SetWaypoint(`Create {className}`)
505
1595
  return instance
506
1596
  end)
507
1597
  if success and newInstance then
1598
+ finishRecording(recordingId, true)
508
1599
  return {
509
1600
  success = true,
510
1601
  className = className,
@@ -514,6 +1605,7 @@ local function createObject(requestData)
514
1605
  message = "Object created successfully",
515
1606
  }
516
1607
  else
1608
+ finishRecording(recordingId, false)
517
1609
  return {
518
1610
  error = `Failed to create object: {newInstance}`,
519
1611
  className = className,
@@ -539,20 +1631,20 @@ local function deleteObject(requestData)
539
1631
  error = "Cannot delete the game instance",
540
1632
  }
541
1633
  end
1634
+ local recordingId = beginRecording(`Delete {instance.ClassName} ({instance.Name})`)
542
1635
  local success, result = pcall(function()
543
- local name = instance.Name
544
- local className = instance.ClassName
545
1636
  instance:Destroy()
546
- ChangeHistoryService:SetWaypoint(`Delete {className} ({name})`)
547
1637
  return true
548
1638
  end)
549
1639
  if success then
1640
+ finishRecording(recordingId, true)
550
1641
  return {
551
1642
  success = true,
552
1643
  instancePath = instancePath,
553
1644
  message = "Object deleted successfully",
554
1645
  }
555
1646
  else
1647
+ finishRecording(recordingId, false)
556
1648
  return {
557
1649
  error = `Failed to delete object: {result}`,
558
1650
  instancePath = instancePath,
@@ -566,69 +1658,44 @@ local function massCreateObjects(requestData)
566
1658
  error = "Objects array is required",
567
1659
  }
568
1660
  end
569
- local results = {}
570
- local successCount = 0
571
- local failureCount = 0
572
- for _, objData in objects do
1661
+ local recordingId = beginRecording("Mass create objects")
1662
+ local _binding_2 = processObjectEntries(objects, function(objData)
573
1663
  local className = objData.className
574
1664
  local parentPath = objData.parent
575
1665
  local name = objData.name
576
- local _condition = className
577
- if _condition ~= "" and _condition then
578
- _condition = parentPath
579
- end
580
- if _condition ~= "" and _condition then
581
- local parentInstance = getInstanceByPath(parentPath)
582
- if parentInstance then
583
- local success, newInstance = pcall(function()
584
- local instance = Instance.new(className)
585
- if name ~= "" and name then
586
- instance.Name = name
587
- end
588
- instance.Parent = parentInstance
589
- return instance
590
- end)
591
- if success and newInstance then
592
- successCount += 1
593
- local _arg0 = {
594
- success = true,
595
- className = className,
596
- parent = parentPath,
597
- instancePath = getInstancePath(newInstance),
598
- name = newInstance.Name,
599
- }
600
- table.insert(results, _arg0)
601
- else
602
- failureCount += 1
603
- local _arg0 = {
604
- success = false,
605
- className = className,
606
- parent = parentPath,
607
- error = tostring(newInstance),
608
- }
609
- table.insert(results, _arg0)
610
- end
611
- else
612
- failureCount += 1
613
- local _arg0 = {
614
- success = false,
615
- className = className,
616
- parent = parentPath,
617
- error = "Parent instance not found",
618
- }
619
- table.insert(results, _arg0)
1666
+ local parentInstance = getInstanceByPath(parentPath)
1667
+ if not parentInstance then
1668
+ return {
1669
+ error = "Parent instance not found",
1670
+ className = className,
1671
+ parentPath = parentPath,
1672
+ }
1673
+ end
1674
+ local success, newInstance = pcall(function()
1675
+ local instance = Instance.new(className)
1676
+ if name ~= "" and name then
1677
+ instance.Name = name
620
1678
  end
621
- else
622
- failureCount += 1
623
- table.insert(results, {
624
- success = false,
625
- error = "Class name and parent are required",
626
- })
1679
+ instance.Parent = parentInstance
1680
+ return instance
1681
+ end)
1682
+ if not success or not newInstance then
1683
+ return {
1684
+ error = tostring(newInstance),
1685
+ className = className,
1686
+ parentPath = parentPath,
1687
+ }
627
1688
  end
628
- end
629
- if successCount > 0 then
630
- ChangeHistoryService:SetWaypoint("Mass create objects")
631
- end
1689
+ return {
1690
+ instance = newInstance,
1691
+ className = className,
1692
+ parentPath = parentPath,
1693
+ }
1694
+ end)
1695
+ local results = _binding_2.results
1696
+ local successCount = _binding_2.successCount
1697
+ local failureCount = _binding_2.failureCount
1698
+ finishRecording(recordingId, successCount > 0)
632
1699
  return {
633
1700
  results = results,
634
1701
  summary = {
@@ -645,78 +1712,55 @@ local function massCreateObjectsWithProperties(requestData)
645
1712
  error = "Objects array is required",
646
1713
  }
647
1714
  end
648
- local results = {}
649
- local successCount = 0
650
- local failureCount = 0
651
- for _, objData in objects do
1715
+ local recordingId = beginRecording("Mass create objects with properties")
1716
+ local _binding_2 = processObjectEntries(objects, function(objData)
652
1717
  local className = objData.className
653
1718
  local parentPath = objData.parent
654
1719
  local name = objData.name
655
- local properties = (objData.properties) or {}
656
- local _condition = className
657
- if _condition ~= "" and _condition then
658
- _condition = parentPath
659
- end
660
- if _condition ~= "" and _condition then
661
- local parentInstance = getInstanceByPath(parentPath)
662
- if parentInstance then
663
- local success, newInstance = pcall(function()
664
- local instance = Instance.new(className)
665
- if name ~= "" and name then
666
- instance.Name = name
667
- end
668
- instance.Parent = parentInstance
669
- for propName, propValue in pairs(properties) do
670
- pcall(function()
671
- local converted = convertPropertyValue(instance, propName, propValue)
672
- if converted ~= nil then
673
- instance[propName] = converted
674
- end
675
- end)
1720
+ local propertiesRaw = objData.properties
1721
+ local properties = if type(propertiesRaw) == "table" then propertiesRaw else ({})
1722
+ local parentInstance = getInstanceByPath(parentPath)
1723
+ if not parentInstance then
1724
+ return {
1725
+ error = "Parent instance not found",
1726
+ className = className,
1727
+ parentPath = parentPath,
1728
+ }
1729
+ end
1730
+ local success, newInstance = pcall(function()
1731
+ local instance = Instance.new(className)
1732
+ if name ~= "" and name then
1733
+ instance.Name = name
1734
+ end
1735
+ instance.Parent = parentInstance
1736
+ for propName, propValue in pairs(properties) do
1737
+ pcall(function()
1738
+ local propNameStr = tostring(propName)
1739
+ local converted = convertPropertyValue(instance, propNameStr, propValue)
1740
+ if converted ~= nil then
1741
+ instance[propNameStr] = converted
676
1742
  end
677
- return instance
678
1743
  end)
679
- if success and newInstance then
680
- successCount += 1
681
- local _arg0 = {
682
- success = true,
683
- className = className,
684
- parent = parentPath,
685
- instancePath = getInstancePath(newInstance),
686
- name = newInstance.Name,
687
- }
688
- table.insert(results, _arg0)
689
- else
690
- failureCount += 1
691
- local _arg0 = {
692
- success = false,
693
- className = className,
694
- parent = parentPath,
695
- error = tostring(newInstance),
696
- }
697
- table.insert(results, _arg0)
698
- end
699
- else
700
- failureCount += 1
701
- local _arg0 = {
702
- success = false,
703
- className = className,
704
- parent = parentPath,
705
- error = "Parent instance not found",
706
- }
707
- table.insert(results, _arg0)
708
1744
  end
709
- else
710
- failureCount += 1
711
- table.insert(results, {
712
- success = false,
713
- error = "Class name and parent are required",
714
- })
1745
+ return instance
1746
+ end)
1747
+ if not success or not newInstance then
1748
+ return {
1749
+ error = tostring(newInstance),
1750
+ className = className,
1751
+ parentPath = parentPath,
1752
+ }
715
1753
  end
716
- end
717
- if successCount > 0 then
718
- ChangeHistoryService:SetWaypoint("Mass create objects with properties")
719
- end
1754
+ return {
1755
+ instance = newInstance,
1756
+ className = className,
1757
+ parentPath = parentPath,
1758
+ }
1759
+ end)
1760
+ local results = _binding_2.results
1761
+ local successCount = _binding_2.successCount
1762
+ local failureCount = _binding_2.failureCount
1763
+ finishRecording(recordingId, successCount > 0)
720
1764
  return {
721
1765
  results = results,
722
1766
  summary = {
@@ -726,7 +1770,10 @@ local function massCreateObjectsWithProperties(requestData)
726
1770
  },
727
1771
  }
728
1772
  end
729
- local function smartDuplicate(requestData)
1773
+ local function performSmartDuplicate(requestData, useRecording)
1774
+ if useRecording == nil then
1775
+ useRecording = true
1776
+ end
730
1777
  local instancePath = requestData.instancePath
731
1778
  local count = requestData.count
732
1779
  local options = (requestData.options) or {}
@@ -741,6 +1788,7 @@ local function smartDuplicate(requestData)
741
1788
  error = `Instance not found: {instancePath}`,
742
1789
  }
743
1790
  end
1791
+ local recordingId = if useRecording then beginRecording(`Smart duplicate {instance.Name}`) else nil
744
1792
  local results = {}
745
1793
  local successCount = 0
746
1794
  local failureCount = 0
@@ -885,9 +1933,7 @@ local function smartDuplicate(requestData)
885
1933
  _i = i
886
1934
  end
887
1935
  end
888
- if successCount > 0 then
889
- ChangeHistoryService:SetWaypoint(`Smart duplicate {instance.Name} ({successCount} copies)`)
890
- end
1936
+ finishRecording(recordingId, successCount > 0)
891
1937
  return {
892
1938
  results = results,
893
1939
  summary = {
@@ -898,6 +1944,9 @@ local function smartDuplicate(requestData)
898
1944
  sourceInstance = instancePath,
899
1945
  }
900
1946
  end
1947
+ local function smartDuplicate(requestData)
1948
+ return performSmartDuplicate(requestData, true)
1949
+ end
901
1950
  local function massDuplicate(requestData)
902
1951
  local duplications = requestData.duplications
903
1952
  if not duplications or not (type(duplications) == "table") or #duplications == 0 then
@@ -908,17 +1957,16 @@ local function massDuplicate(requestData)
908
1957
  local allResults = {}
909
1958
  local totalSuccess = 0
910
1959
  local totalFailures = 0
1960
+ local recordingId = beginRecording("Mass duplicate operations")
911
1961
  for _, duplication in duplications do
912
- local result = smartDuplicate(duplication)
1962
+ local result = performSmartDuplicate(duplication, false)
913
1963
  table.insert(allResults, result)
914
1964
  if result.summary then
915
1965
  totalSuccess += result.summary.succeeded
916
1966
  totalFailures += result.summary.failed
917
1967
  end
918
1968
  end
919
- if totalSuccess > 0 then
920
- ChangeHistoryService:SetWaypoint(`Mass duplicate operations ({totalSuccess} objects)`)
921
- end
1969
+ finishRecording(recordingId, totalSuccess > 0)
922
1970
  return {
923
1971
  results = allResults,
924
1972
  summary = {
@@ -939,18 +1987,22 @@ return {
939
1987
  ]]></string>
940
1988
  </Properties>
941
1989
  </Item>
942
- <Item class="ModuleScript" referent="5">
1990
+ <Item class="ModuleScript" referent="8">
943
1991
  <Properties>
944
1992
  <string name="Name">MetadataHandlers</string>
945
1993
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
946
1994
  local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
947
1995
  local CollectionService = TS.import(script, script.Parent.Parent.Parent, "node_modules", "@rbxts", "services").CollectionService
948
1996
  local Utils = TS.import(script, script.Parent.Parent, "Utils")
1997
+ local Recording = TS.import(script, script.Parent.Parent, "Recording")
949
1998
  local ChangeHistoryService = game:GetService("ChangeHistoryService")
950
1999
  local Selection = game:GetService("Selection")
951
2000
  local _binding = Utils
952
2001
  local getInstancePath = _binding.getInstancePath
953
2002
  local getInstanceByPath = _binding.getInstanceByPath
2003
+ local _binding_1 = Recording
2004
+ local beginRecording = _binding_1.beginRecording
2005
+ local finishRecording = _binding_1.finishRecording
954
2006
  local function serializeValue(value)
955
2007
  local _value = value
956
2008
  local vType = typeof(_value)
@@ -1133,10 +2185,10 @@ local function setAttribute(requestData)
1133
2185
  error = `Instance not found: {instancePath}`,
1134
2186
  }
1135
2187
  end
2188
+ local recordingId = beginRecording(`Set attribute {attributeName} on {instance.Name}`)
1136
2189
  local success, result = pcall(function()
1137
2190
  local value = deserializeValue(attributeValue, valueType)
1138
2191
  instance:SetAttribute(attributeName, value)
1139
- ChangeHistoryService:SetWaypoint(`Set attribute {attributeName} on {instance.Name}`)
1140
2192
  return {
1141
2193
  success = true,
1142
2194
  instancePath = instancePath,
@@ -1146,8 +2198,10 @@ local function setAttribute(requestData)
1146
2198
  }
1147
2199
  end)
1148
2200
  if success then
2201
+ finishRecording(recordingId, true)
1149
2202
  return result
1150
2203
  end
2204
+ finishRecording(recordingId, false)
1151
2205
  return {
1152
2206
  error = `Failed to set attribute: {result}`,
1153
2207
  }
@@ -1203,10 +2257,10 @@ local function deleteAttribute(requestData)
1203
2257
  error = `Instance not found: {instancePath}`,
1204
2258
  }
1205
2259
  end
2260
+ local recordingId = beginRecording(`Delete attribute {attributeName} from {instance.Name}`)
1206
2261
  local success, result = pcall(function()
1207
2262
  local existed = instance:GetAttribute(attributeName) ~= nil
1208
2263
  instance:SetAttribute(attributeName, nil)
1209
- ChangeHistoryService:SetWaypoint(`Delete attribute {attributeName} from {instance.Name}`)
1210
2264
  return {
1211
2265
  success = true,
1212
2266
  instancePath = instancePath,
@@ -1216,8 +2270,10 @@ local function deleteAttribute(requestData)
1216
2270
  }
1217
2271
  end)
1218
2272
  if success then
2273
+ finishRecording(recordingId, true)
1219
2274
  return result
1220
2275
  end
2276
+ finishRecording(recordingId, false)
1221
2277
  return {
1222
2278
  error = `Failed to delete attribute: {result}`,
1223
2279
  }
@@ -1264,10 +2320,10 @@ local function addTag(requestData)
1264
2320
  error = `Instance not found: {instancePath}`,
1265
2321
  }
1266
2322
  end
2323
+ local recordingId = beginRecording(`Add tag {tagName} to {instance.Name}`)
1267
2324
  local success, result = pcall(function()
1268
2325
  local alreadyHad = CollectionService:HasTag(instance, tagName)
1269
2326
  CollectionService:AddTag(instance, tagName)
1270
- ChangeHistoryService:SetWaypoint(`Add tag {tagName} to {instance.Name}`)
1271
2327
  return {
1272
2328
  success = true,
1273
2329
  instancePath = instancePath,
@@ -1277,8 +2333,10 @@ local function addTag(requestData)
1277
2333
  }
1278
2334
  end)
1279
2335
  if success then
2336
+ finishRecording(recordingId, true)
1280
2337
  return result
1281
2338
  end
2339
+ finishRecording(recordingId, false)
1282
2340
  return {
1283
2341
  error = `Failed to add tag: {result}`,
1284
2342
  }
@@ -1297,10 +2355,10 @@ local function removeTag(requestData)
1297
2355
  error = `Instance not found: {instancePath}`,
1298
2356
  }
1299
2357
  end
2358
+ local recordingId = beginRecording(`Remove tag {tagName} from {instance.Name}`)
1300
2359
  local success, result = pcall(function()
1301
2360
  local hadTag = CollectionService:HasTag(instance, tagName)
1302
2361
  CollectionService:RemoveTag(instance, tagName)
1303
- ChangeHistoryService:SetWaypoint(`Remove tag {tagName} from {instance.Name}`)
1304
2362
  return {
1305
2363
  success = true,
1306
2364
  instancePath = instancePath,
@@ -1310,8 +2368,10 @@ local function removeTag(requestData)
1310
2368
  }
1311
2369
  end)
1312
2370
  if success then
2371
+ finishRecording(recordingId, true)
1313
2372
  return result
1314
2373
  end
2374
+ finishRecording(recordingId, false)
1315
2375
  return {
1316
2376
  error = `Failed to remove tag: {result}`,
1317
2377
  }
@@ -1442,6 +2502,36 @@ local function executeLuau(requestData)
1442
2502
  }
1443
2503
  end
1444
2504
  end
2505
+ local function undo(_requestData)
2506
+ local success, result = pcall(function()
2507
+ ChangeHistoryService:Undo()
2508
+ return {
2509
+ success = true,
2510
+ message = "Undo executed successfully",
2511
+ }
2512
+ end)
2513
+ if success then
2514
+ return result
2515
+ end
2516
+ return {
2517
+ error = `Failed to undo: {result}`,
2518
+ }
2519
+ end
2520
+ local function redo(_requestData)
2521
+ local success, result = pcall(function()
2522
+ ChangeHistoryService:Redo()
2523
+ return {
2524
+ success = true,
2525
+ message = "Redo executed successfully",
2526
+ }
2527
+ end)
2528
+ if success then
2529
+ return result
2530
+ end
2531
+ return {
2532
+ error = `Failed to redo: {result}`,
2533
+ }
2534
+ end
1445
2535
  return {
1446
2536
  getAttribute = getAttribute,
1447
2537
  setAttribute = setAttribute,
@@ -1453,21 +2543,26 @@ return {
1453
2543
  getTagged = getTagged,
1454
2544
  getSelection = getSelection,
1455
2545
  executeLuau = executeLuau,
2546
+ undo = undo,
2547
+ redo = redo,
1456
2548
  }
1457
2549
  ]]></string>
1458
2550
  </Properties>
1459
2551
  </Item>
1460
- <Item class="ModuleScript" referent="6">
2552
+ <Item class="ModuleScript" referent="9">
1461
2553
  <Properties>
1462
2554
  <string name="Name">PropertyHandlers</string>
1463
2555
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
1464
2556
  local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
1465
2557
  local Utils = TS.import(script, script.Parent.Parent, "Utils")
1466
- local ChangeHistoryService = game:GetService("ChangeHistoryService")
2558
+ local Recording = TS.import(script, script.Parent.Parent, "Recording")
1467
2559
  local _binding = Utils
1468
2560
  local getInstanceByPath = _binding.getInstanceByPath
1469
2561
  local convertPropertyValue = _binding.convertPropertyValue
1470
2562
  local evaluateFormula = _binding.evaluateFormula
2563
+ local _binding_1 = Recording
2564
+ local beginRecording = _binding_1.beginRecording
2565
+ local finishRecording = _binding_1.finishRecording
1471
2566
  local function setProperty(requestData)
1472
2567
  local instancePath = requestData.instancePath
1473
2568
  local propertyName = requestData.propertyName
@@ -1483,6 +2578,7 @@ local function setProperty(requestData)
1483
2578
  error = `Instance not found: {instancePath}`,
1484
2579
  }
1485
2580
  end
2581
+ local recordingId = beginRecording(`Set {propertyName} property`)
1486
2582
  local inst = instance
1487
2583
  local success, result = pcall(function()
1488
2584
  if propertyName == "Parent" or propertyName == "PrimaryPart" then
@@ -1508,10 +2604,10 @@ local function setProperty(requestData)
1508
2604
  inst[propertyName] = propertyValue
1509
2605
  end
1510
2606
  end
1511
- ChangeHistoryService:SetWaypoint(`Set {propertyName} property`)
1512
2607
  return true
1513
2608
  end)
1514
2609
  if success then
2610
+ finishRecording(recordingId, true)
1515
2611
  return {
1516
2612
  success = true,
1517
2613
  instancePath = instancePath,
@@ -1520,6 +2616,7 @@ local function setProperty(requestData)
1520
2616
  message = "Property set successfully",
1521
2617
  }
1522
2618
  else
2619
+ finishRecording(recordingId, false)
1523
2620
  return {
1524
2621
  error = `Failed to set property: {result}`,
1525
2622
  instancePath = instancePath,
@@ -1539,6 +2636,7 @@ local function massSetProperty(requestData)
1539
2636
  local results = {}
1540
2637
  local successCount = 0
1541
2638
  local failureCount = 0
2639
+ local recordingId = beginRecording(`Mass set {propertyName} property`)
1542
2640
  for _, path in paths do
1543
2641
  local instance = getInstanceByPath(path)
1544
2642
  if instance then
@@ -1573,9 +2671,7 @@ local function massSetProperty(requestData)
1573
2671
  table.insert(results, _arg0)
1574
2672
  end
1575
2673
  end
1576
- if successCount > 0 then
1577
- ChangeHistoryService:SetWaypoint(`Mass set {propertyName} property`)
1578
- end
2674
+ finishRecording(recordingId, successCount > 0)
1579
2675
  return {
1580
2676
  results = results,
1581
2677
  summary = {
@@ -1643,6 +2739,7 @@ local function setCalculatedProperty(requestData)
1643
2739
  local results = {}
1644
2740
  local successCount = 0
1645
2741
  local failureCount = 0
2742
+ local recordingId = beginRecording(`Set calculated {propertyName} property`)
1646
2743
  for i = 0, #paths - 1 do
1647
2744
  local path = paths[i + 1]
1648
2745
  local instance = getInstanceByPath(path)
@@ -1695,9 +2792,7 @@ local function setCalculatedProperty(requestData)
1695
2792
  table.insert(results, _arg0)
1696
2793
  end
1697
2794
  end
1698
- if successCount > 0 then
1699
- ChangeHistoryService:SetWaypoint(`Set calculated {propertyName} property`)
1700
- end
2795
+ finishRecording(recordingId, successCount > 0)
1701
2796
  return {
1702
2797
  results = results,
1703
2798
  summary = {
@@ -1722,6 +2817,7 @@ local function setRelativeProperty(requestData)
1722
2817
  local results = {}
1723
2818
  local successCount = 0
1724
2819
  local failureCount = 0
2820
+ local recordingId = beginRecording(`Set relative {propertyName} property`)
1725
2821
  local function applyOp(current, op, val)
1726
2822
  if op == "add" then
1727
2823
  return current + val
@@ -1834,9 +2930,7 @@ local function setRelativeProperty(requestData)
1834
2930
  table.insert(results, _arg0)
1835
2931
  end
1836
2932
  end
1837
- if successCount > 0 then
1838
- ChangeHistoryService:SetWaypoint(`Set relative {propertyName} property`)
1839
- end
2933
+ finishRecording(recordingId, successCount > 0)
1840
2934
  return {
1841
2935
  results = results,
1842
2936
  summary = {
@@ -1858,7 +2952,7 @@ return {
1858
2952
  ]]></string>
1859
2953
  </Properties>
1860
2954
  </Item>
1861
- <Item class="ModuleScript" referent="7">
2955
+ <Item class="ModuleScript" referent="10">
1862
2956
  <Properties>
1863
2957
  <string name="Name">QueryHandlers</string>
1864
2958
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -1897,6 +2991,9 @@ local function getFileTree(requestData)
1897
2991
  if instance:IsA("LuaSourceContainer") then
1898
2992
  node.hasSource = true
1899
2993
  node.scriptType = instance.ClassName
2994
+ if instance:IsA("BaseScript") then
2995
+ node.enabled = instance.Enabled
2996
+ end
1900
2997
  end
1901
2998
  for _, child in instance:GetChildren() do
1902
2999
  local _children = node.children
@@ -1939,13 +3036,16 @@ local function searchFiles(requestData)
1939
3036
  match = (string.find(_exp, _arg0)) ~= nil
1940
3037
  end
1941
3038
  if match then
1942
- local _arg0 = {
3039
+ local entry = {
1943
3040
  name = instance.Name,
1944
3041
  className = instance.ClassName,
1945
3042
  path = getInstancePath(instance),
1946
3043
  hasSource = instance:IsA("LuaSourceContainer"),
1947
3044
  }
1948
- table.insert(results, _arg0)
3045
+ if instance:IsA("BaseScript") then
3046
+ entry.enabled = instance.Enabled
3047
+ end
3048
+ table.insert(results, entry)
1949
3049
  end
1950
3050
  for _, child in instance:GetChildren() do
1951
3051
  searchRecursive(child)
@@ -2072,6 +3172,11 @@ local function searchObjects(requestData)
2072
3172
  end
2073
3173
  local function getInstanceProperties(requestData)
2074
3174
  local instancePath = requestData.instancePath
3175
+ local _condition = (requestData.excludeSource)
3176
+ if _condition == nil then
3177
+ _condition = false
3178
+ end
3179
+ local excludeSource = _condition
2075
3180
  if not (instancePath ~= "" and instancePath) then
2076
3181
  return {
2077
3182
  error = "Instance path is required",
@@ -2127,7 +3232,13 @@ local function getInstanceProperties(requestData)
2127
3232
  end
2128
3233
  end
2129
3234
  if instance:IsA("LuaSourceContainer") then
2130
- properties.Source = readScriptSource(instance)
3235
+ if not excludeSource then
3236
+ properties.Source = readScriptSource(instance)
3237
+ else
3238
+ local src = readScriptSource(instance)
3239
+ properties.SourceLength = #src
3240
+ properties.LineCount = #(Utils.splitLines(src))
3241
+ end
2131
3242
  if instance:IsA("BaseScript") then
2132
3243
  properties.Enabled = tostring(instance.Enabled)
2133
3244
  end
@@ -2195,14 +3306,17 @@ local function getInstanceChildren(requestData)
2195
3306
  end
2196
3307
  local children = {}
2197
3308
  for _, child in instance:GetChildren() do
2198
- local _arg0 = {
3309
+ local entry = {
2199
3310
  name = child.Name,
2200
3311
  className = child.ClassName,
2201
3312
  path = getInstancePath(child),
2202
3313
  hasChildren = #child:GetChildren() > 0,
2203
3314
  hasSource = child:IsA("LuaSourceContainer"),
2204
3315
  }
2205
- table.insert(children, _arg0)
3316
+ if child:IsA("BaseScript") then
3317
+ entry.enabled = child.Enabled
3318
+ end
3319
+ table.insert(children, entry)
2206
3320
  end
2207
3321
  return {
2208
3322
  instancePath = instancePath,
@@ -2494,6 +3608,192 @@ local function getProjectStructure(requestData)
2494
3608
  result.timestamp = tick()
2495
3609
  return result
2496
3610
  end
3611
+ local function grepScripts(requestData)
3612
+ local pattern = requestData.pattern
3613
+ if not (pattern ~= "" and pattern) then
3614
+ return {
3615
+ error = "pattern is required",
3616
+ }
3617
+ end
3618
+ local _condition = (requestData.caseSensitive)
3619
+ if _condition == nil then
3620
+ _condition = false
3621
+ end
3622
+ local caseSensitive = _condition
3623
+ local _condition_1 = (requestData.contextLines)
3624
+ if _condition_1 == nil then
3625
+ _condition_1 = 0
3626
+ end
3627
+ local contextLines = _condition_1
3628
+ local _condition_2 = (requestData.maxResults)
3629
+ if _condition_2 == nil then
3630
+ _condition_2 = 100
3631
+ end
3632
+ local maxResults = _condition_2
3633
+ local _condition_3 = (requestData.maxResultsPerScript)
3634
+ if _condition_3 == nil then
3635
+ _condition_3 = 0
3636
+ end
3637
+ local maxResultsPerScript = _condition_3
3638
+ local _condition_4 = (requestData.usePattern)
3639
+ if _condition_4 == nil then
3640
+ _condition_4 = false
3641
+ end
3642
+ local usePattern = _condition_4
3643
+ local _condition_5 = (requestData.filesOnly)
3644
+ if _condition_5 == nil then
3645
+ _condition_5 = false
3646
+ end
3647
+ local filesOnly = _condition_5
3648
+ local _condition_6 = (requestData.path)
3649
+ if _condition_6 == nil then
3650
+ _condition_6 = ""
3651
+ end
3652
+ local searchPath = _condition_6
3653
+ local classFilter = requestData.classFilter
3654
+ local startInstance = if searchPath ~= "" then getInstanceByPath(searchPath) else game
3655
+ if not startInstance then
3656
+ return {
3657
+ error = `Path not found: {searchPath}`,
3658
+ }
3659
+ end
3660
+ -- Prepare pattern for matching
3661
+ local searchPattern = if caseSensitive then pattern else string.lower(pattern)
3662
+ local results = {}
3663
+ local totalMatches = 0
3664
+ local scriptsSearched = 0
3665
+ local hitLimit = false
3666
+ local function searchInstance(instance)
3667
+ if hitLimit then
3668
+ return nil
3669
+ end
3670
+ if instance:IsA("LuaSourceContainer") then
3671
+ -- Apply class filter
3672
+ if classFilter ~= "" and classFilter then
3673
+ local _exp = string.lower(instance.ClassName)
3674
+ local _arg0 = string.lower(classFilter)
3675
+ local _value = (string.find(_exp, _arg0))
3676
+ if not (_value ~= 0 and _value == _value and _value) then
3677
+ return nil
3678
+ end
3679
+ end
3680
+ scriptsSearched += 1
3681
+ local source = readScriptSource(instance)
3682
+ local lines = Utils.splitLines(source)
3683
+ local scriptMatches = {}
3684
+ local scriptMatchCount = 0
3685
+ for i = 0, #lines - 1 do
3686
+ if hitLimit then
3687
+ break
3688
+ end
3689
+ if maxResultsPerScript > 0 and scriptMatchCount >= maxResultsPerScript then
3690
+ break
3691
+ end
3692
+ local line = lines[i + 1]
3693
+ local searchLine = if caseSensitive then line else string.lower(line)
3694
+ local matchStart
3695
+ local matchEnd
3696
+ if usePattern then
3697
+ matchStart, matchEnd = string.find(searchLine, searchPattern)
3698
+ else
3699
+ matchStart, matchEnd = string.find(searchLine, searchPattern, 1, true)
3700
+ end
3701
+ if matchStart ~= nil then
3702
+ scriptMatchCount += 1
3703
+ totalMatches += 1
3704
+ if totalMatches > maxResults then
3705
+ hitLimit = true
3706
+ break
3707
+ end
3708
+ if not filesOnly then
3709
+ -- Gather context lines
3710
+ local before = {}
3711
+ local after = {}
3712
+ if contextLines > 0 then
3713
+ local beforeStart = math.max(0, i - contextLines)
3714
+ do
3715
+ local j = beforeStart
3716
+ local _shouldIncrement = false
3717
+ while true do
3718
+ if _shouldIncrement then
3719
+ j += 1
3720
+ else
3721
+ _shouldIncrement = true
3722
+ end
3723
+ if not (j < i) then
3724
+ break
3725
+ end
3726
+ local _arg0 = lines[j + 1]
3727
+ table.insert(before, _arg0)
3728
+ end
3729
+ end
3730
+ local afterEnd = math.min(#lines - 1, i + contextLines)
3731
+ do
3732
+ local j = i + 1
3733
+ local _shouldIncrement = false
3734
+ while true do
3735
+ if _shouldIncrement then
3736
+ j += 1
3737
+ else
3738
+ _shouldIncrement = true
3739
+ end
3740
+ if not (j <= afterEnd) then
3741
+ break
3742
+ end
3743
+ local _arg0 = lines[j + 1]
3744
+ table.insert(after, _arg0)
3745
+ end
3746
+ end
3747
+ end
3748
+ local _arg0 = {
3749
+ line = i + 1,
3750
+ column = matchStart,
3751
+ text = line,
3752
+ before = before,
3753
+ after = after,
3754
+ }
3755
+ table.insert(scriptMatches, _arg0)
3756
+ end
3757
+ end
3758
+ end
3759
+ if scriptMatchCount > 0 then
3760
+ local scriptResult = {
3761
+ instancePath = getInstancePath(instance),
3762
+ name = instance.Name,
3763
+ className = instance.ClassName,
3764
+ matches = scriptMatches,
3765
+ }
3766
+ if instance:IsA("BaseScript") then
3767
+ scriptResult.enabled = instance.Enabled
3768
+ end
3769
+ table.insert(results, scriptResult)
3770
+ end
3771
+ end
3772
+ for _, child in instance:GetChildren() do
3773
+ if hitLimit then
3774
+ return nil
3775
+ end
3776
+ searchInstance(child)
3777
+ end
3778
+ end
3779
+ searchInstance(startInstance)
3780
+ return {
3781
+ results = results,
3782
+ pattern = pattern,
3783
+ totalMatches = if hitLimit then `>{maxResults}` else totalMatches,
3784
+ scriptsSearched = scriptsSearched,
3785
+ scriptsMatched = #results,
3786
+ truncated = hitLimit,
3787
+ options = {
3788
+ caseSensitive = caseSensitive,
3789
+ contextLines = contextLines,
3790
+ usePattern = usePattern,
3791
+ filesOnly = filesOnly,
3792
+ maxResults = maxResults,
3793
+ maxResultsPerScript = maxResultsPerScript,
3794
+ },
3795
+ }
3796
+ end
2497
3797
  return {
2498
3798
  getFileTree = getFileTree,
2499
3799
  searchFiles = searchFiles,
@@ -2505,17 +3805,18 @@ return {
2505
3805
  searchByProperty = searchByProperty,
2506
3806
  getClassInfo = getClassInfo,
2507
3807
  getProjectStructure = getProjectStructure,
3808
+ grepScripts = grepScripts,
2508
3809
  }
2509
3810
  ]]></string>
2510
3811
  </Properties>
2511
3812
  </Item>
2512
- <Item class="ModuleScript" referent="8">
3813
+ <Item class="ModuleScript" referent="11">
2513
3814
  <Properties>
2514
3815
  <string name="Name">ScriptHandlers</string>
2515
3816
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
2516
3817
  local TS = require(script.Parent.Parent.Parent.include.RuntimeLib)
2517
3818
  local Utils = TS.import(script, script.Parent.Parent, "Utils")
2518
- local ChangeHistoryService = game:GetService("ChangeHistoryService")
3819
+ local Recording = TS.import(script, script.Parent.Parent, "Recording")
2519
3820
  local ScriptEditorService = game:GetService("ScriptEditorService")
2520
3821
  local _binding = Utils
2521
3822
  local getInstancePath = _binding.getInstancePath
@@ -2523,11 +3824,15 @@ local getInstanceByPath = _binding.getInstanceByPath
2523
3824
  local readScriptSource = _binding.readScriptSource
2524
3825
  local splitLines = _binding.splitLines
2525
3826
  local joinLines = _binding.joinLines
3827
+ local _binding_1 = Recording
3828
+ local beginRecording = _binding_1.beginRecording
3829
+ local finishRecording = _binding_1.finishRecording
2526
3830
  local function normalizeEscapes(s)
2527
3831
  local result = s
2528
3832
  result = (string.gsub(result, "\\n", "\n"))
2529
3833
  result = (string.gsub(result, "\\t", "\t"))
2530
3834
  result = (string.gsub(result, "\\r", "\r"))
3835
+ result = (string.gsub(result, '\\"', '"'))
2531
3836
  result = (string.gsub(result, "\\\\", "\\"))
2532
3837
  return result
2533
3838
  end
@@ -2679,12 +3984,12 @@ local function setScriptSource(requestData)
2679
3984
  }
2680
3985
  end
2681
3986
  local sourceToSet = normalizeEscapes(newSource)
3987
+ local recordingId = beginRecording(`Set script source: {instance.Name}`)
2682
3988
  local updateSuccess, updateResult = pcall(function()
2683
3989
  local oldSourceLength = #readScriptSource(instance)
2684
3990
  ScriptEditorService:UpdateSourceAsync(instance, function()
2685
3991
  return sourceToSet
2686
3992
  end)
2687
- ChangeHistoryService:SetWaypoint(`Set script source: {instance.Name}`)
2688
3993
  return {
2689
3994
  success = true,
2690
3995
  instancePath = instancePath,
@@ -2695,12 +4000,12 @@ local function setScriptSource(requestData)
2695
4000
  }
2696
4001
  end)
2697
4002
  if updateSuccess then
4003
+ finishRecording(recordingId, true)
2698
4004
  return updateResult
2699
4005
  end
2700
4006
  local directSuccess, directResult = pcall(function()
2701
4007
  local oldSource = instance.Source
2702
4008
  instance.Source = sourceToSet
2703
- ChangeHistoryService:SetWaypoint(`Set script source: {instance.Name}`)
2704
4009
  return {
2705
4010
  success = true,
2706
4011
  instancePath = instancePath,
@@ -2711,6 +4016,7 @@ local function setScriptSource(requestData)
2711
4016
  }
2712
4017
  end)
2713
4018
  if directSuccess then
4019
+ finishRecording(recordingId, true)
2714
4020
  return directResult
2715
4021
  end
2716
4022
  local replaceSuccess, replaceResult = pcall(function()
@@ -2727,7 +4033,6 @@ local function setScriptSource(requestData)
2727
4033
  end
2728
4034
  newScript.Parent = parent
2729
4035
  instance:Destroy()
2730
- ChangeHistoryService:SetWaypoint(`Replace script: {name}`)
2731
4036
  return {
2732
4037
  success = true,
2733
4038
  instancePath = getInstancePath(newScript),
@@ -2736,8 +4041,10 @@ local function setScriptSource(requestData)
2736
4041
  }
2737
4042
  end)
2738
4043
  if replaceSuccess then
4044
+ finishRecording(recordingId, true)
2739
4045
  return replaceResult
2740
4046
  end
4047
+ finishRecording(recordingId, false)
2741
4048
  return {
2742
4049
  error = `Failed to set script source. UpdateSourceAsync failed: {updateResult}. Direct assignment failed: {directResult}. Replace method failed: {replaceResult}`,
2743
4050
  }
@@ -2764,6 +4071,7 @@ local function editScriptLines(requestData)
2764
4071
  error = `Instance is not a script-like object: {instance.ClassName}`,
2765
4072
  }
2766
4073
  end
4074
+ local recordingId = beginRecording(`Edit script lines {startLine}-{endLine}: {instance.Name}`)
2767
4075
  local success, result = pcall(function()
2768
4076
  local lines, hadTrailingNewline = splitLines(readScriptSource(instance))
2769
4077
  local totalLines = #lines
@@ -2814,7 +4122,6 @@ local function editScriptLines(requestData)
2814
4122
  ScriptEditorService:UpdateSourceAsync(instance, function()
2815
4123
  return newSource
2816
4124
  end)
2817
- ChangeHistoryService:SetWaypoint(`Edit script lines {startLine}-{endLine}: {instance.Name}`)
2818
4125
  return {
2819
4126
  success = true,
2820
4127
  instancePath = instancePath,
@@ -2829,8 +4136,10 @@ local function editScriptLines(requestData)
2829
4136
  }
2830
4137
  end)
2831
4138
  if success then
4139
+ finishRecording(recordingId, true)
2832
4140
  return result
2833
4141
  end
4142
+ finishRecording(recordingId, false)
2834
4143
  return {
2835
4144
  error = `Failed to edit script lines: {result}`,
2836
4145
  }
@@ -2860,6 +4169,7 @@ local function insertScriptLines(requestData)
2860
4169
  error = `Instance is not a script-like object: {instance.ClassName}`,
2861
4170
  }
2862
4171
  end
4172
+ local recordingId = beginRecording(`Insert script lines after line {afterLine}: {instance.Name}`)
2863
4173
  local success, result = pcall(function()
2864
4174
  local lines, hadTrailingNewline = splitLines(readScriptSource(instance))
2865
4175
  local totalLines = #lines
@@ -2907,7 +4217,6 @@ local function insertScriptLines(requestData)
2907
4217
  ScriptEditorService:UpdateSourceAsync(instance, function()
2908
4218
  return newSource
2909
4219
  end)
2910
- ChangeHistoryService:SetWaypoint(`Insert script lines after line {afterLine}: {instance.Name}`)
2911
4220
  return {
2912
4221
  success = true,
2913
4222
  instancePath = instancePath,
@@ -2918,8 +4227,10 @@ local function insertScriptLines(requestData)
2918
4227
  }
2919
4228
  end)
2920
4229
  if success then
4230
+ finishRecording(recordingId, true)
2921
4231
  return result
2922
4232
  end
4233
+ finishRecording(recordingId, false)
2923
4234
  return {
2924
4235
  error = `Failed to insert script lines: {result}`,
2925
4236
  }
@@ -2944,6 +4255,7 @@ local function deleteScriptLines(requestData)
2944
4255
  error = `Instance is not a script-like object: {instance.ClassName}`,
2945
4256
  }
2946
4257
  end
4258
+ local recordingId = beginRecording(`Delete script lines {startLine}-{endLine}: {instance.Name}`)
2947
4259
  local success, result = pcall(function()
2948
4260
  local lines, hadTrailingNewline = splitLines(readScriptSource(instance))
2949
4261
  local totalLines = #lines
@@ -2990,7 +4302,6 @@ local function deleteScriptLines(requestData)
2990
4302
  ScriptEditorService:UpdateSourceAsync(instance, function()
2991
4303
  return newSource
2992
4304
  end)
2993
- ChangeHistoryService:SetWaypoint(`Delete script lines {startLine}-{endLine}: {instance.Name}`)
2994
4305
  return {
2995
4306
  success = true,
2996
4307
  instancePath = instancePath,
@@ -3004,8 +4315,10 @@ local function deleteScriptLines(requestData)
3004
4315
  }
3005
4316
  end)
3006
4317
  if success then
4318
+ finishRecording(recordingId, true)
3007
4319
  return result
3008
4320
  end
4321
+ finishRecording(recordingId, false)
3009
4322
  return {
3010
4323
  error = `Failed to delete script lines: {result}`,
3011
4324
  }
@@ -3020,7 +4333,7 @@ return {
3020
4333
  ]]></string>
3021
4334
  </Properties>
3022
4335
  </Item>
3023
- <Item class="ModuleScript" referent="9">
4336
+ <Item class="ModuleScript" referent="12">
3024
4337
  <Properties>
3025
4338
  <string name="Name">TestHandlers</string>
3026
4339
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3169,11 +4482,41 @@ return {
3169
4482
  </Properties>
3170
4483
  </Item>
3171
4484
  </Item>
3172
- <Item class="ModuleScript" referent="10">
4485
+ <Item class="ModuleScript" referent="13">
4486
+ <Properties>
4487
+ <string name="Name">Recording</string>
4488
+ <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
4489
+ local ChangeHistoryService = game:GetService("ChangeHistoryService")
4490
+ local function beginRecording(actionName)
4491
+ local success, result = pcall(function()
4492
+ return ChangeHistoryService:TryBeginRecording(`MCP: {actionName}`)
4493
+ end)
4494
+ if success then
4495
+ return result
4496
+ end
4497
+ return nil
4498
+ end
4499
+ local function finishRecording(recordingId, shouldCommit)
4500
+ if recordingId == nil then
4501
+ return nil
4502
+ end
4503
+ local operation = if shouldCommit then Enum.FinishRecordingOperation.Commit else Enum.FinishRecordingOperation.Cancel
4504
+ pcall(function()
4505
+ ChangeHistoryService:FinishRecording(recordingId, operation)
4506
+ end)
4507
+ end
4508
+ return {
4509
+ beginRecording = beginRecording,
4510
+ finishRecording = finishRecording,
4511
+ }
4512
+ ]]></string>
4513
+ </Properties>
4514
+ </Item>
4515
+ <Item class="ModuleScript" referent="14">
3173
4516
  <Properties>
3174
4517
  <string name="Name">State</string>
3175
4518
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
3176
- local CURRENT_VERSION = "2.4.0"
4519
+ local CURRENT_VERSION = "2.5.0"
3177
4520
  local MAX_CONNECTIONS = 5
3178
4521
  local BASE_PORT = 58741
3179
4522
  local activeTabIndex = 0
@@ -3260,7 +4603,7 @@ return {
3260
4603
  ]]></string>
3261
4604
  </Properties>
3262
4605
  </Item>
3263
- <Item class="ModuleScript" referent="11">
4606
+ <Item class="ModuleScript" referent="15">
3264
4607
  <Properties>
3265
4608
  <string name="Name">UI</string>
3266
4609
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3888,7 +5231,7 @@ return {
3888
5231
  ]]></string>
3889
5232
  </Properties>
3890
5233
  </Item>
3891
- <Item class="ModuleScript" referent="12">
5234
+ <Item class="ModuleScript" referent="16">
3892
5235
  <Properties>
3893
5236
  <string name="Name">Utils</string>
3894
5237
  <string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
@@ -3927,14 +5270,10 @@ local function getInstanceByPath(path)
3927
5270
  end
3928
5271
  local current = game
3929
5272
  for _, part in parts do
3930
- local _result = current
3931
- if _result ~= nil then
3932
- _result = _result:FindFirstChild(part)
3933
- end
3934
- current = _result
3935
5273
  if not current then
3936
5274
  return nil
3937
5275
  end
5276
+ current = current:FindFirstChild(part)
3938
5277
  end
3939
5278
  return current
3940
5279
  end
@@ -4422,11 +5761,11 @@ return {
4422
5761
  </Properties>
4423
5762
  </Item>
4424
5763
  </Item>
4425
- <Item class="Folder" referent="16">
5764
+ <Item class="Folder" referent="20">
4426
5765
  <Properties>
4427
5766
  <string name="Name">include</string>
4428
5767
  </Properties>
4429
- <Item class="ModuleScript" referent="13">
5768
+ <Item class="ModuleScript" referent="17">
4430
5769
  <Properties>
4431
5770
  <string name="Name">Promise</string>
4432
5771
  <string name="Source"><![CDATA[--[[
@@ -6500,7 +7839,7 @@ return Promise
6500
7839
  ]]></string>
6501
7840
  </Properties>
6502
7841
  </Item>
6503
- <Item class="ModuleScript" referent="14">
7842
+ <Item class="ModuleScript" referent="18">
6504
7843
  <Properties>
6505
7844
  <string name="Name">RuntimeLib</string>
6506
7845
  <string name="Source"><![CDATA[local Promise = require(script.Parent.Promise)
@@ -6767,15 +8106,15 @@ return TS
6767
8106
  </Properties>
6768
8107
  </Item>
6769
8108
  </Item>
6770
- <Item class="Folder" referent="17">
8109
+ <Item class="Folder" referent="21">
6771
8110
  <Properties>
6772
8111
  <string name="Name">node_modules</string>
6773
8112
  </Properties>
6774
- <Item class="Folder" referent="18">
8113
+ <Item class="Folder" referent="22">
6775
8114
  <Properties>
6776
8115
  <string name="Name">@rbxts</string>
6777
8116
  </Properties>
6778
- <Item class="ModuleScript" referent="15">
8117
+ <Item class="ModuleScript" referent="19">
6779
8118
  <Properties>
6780
8119
  <string name="Name">services</string>
6781
8120
  <string name="Source"><![CDATA[return setmetatable({}, {