roport 1.0.1 → 1.2.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.
Files changed (39) hide show
  1. package/README.md +16 -0
  2. package/bin/roport.js +3 -2
  3. package/package.json +2 -2
  4. package/src/server.js +171 -10
  5. package/templates/default/DOCUMENTATION.md +309 -0
  6. package/templates/default/README.md +35 -0
  7. package/templates/default/RoportSyncPlugin.rbxmx +1313 -0
  8. package/templates/default/default.project.json +6 -0
  9. package/templates/default/src/Roport/Modules/Sync.lua +10 -6
  10. package/templates/default/src/Roport/init.server.lua +66 -0
  11. package/templates/default/src/lighting/Atmosphere/init.meta.json +1 -1
  12. package/templates/default/src/lighting/Bloom/init.meta.json +1 -1
  13. package/templates/default/src/lighting/DepthOfField/init.meta.json +1 -1
  14. package/templates/default/src/lighting/Sky/init.meta.json +1 -1
  15. package/templates/default/src/lighting/SunRays/init.meta.json +1 -1
  16. package/templates/default/src/server/network/CombatHandler.server.luau +2 -1
  17. package/templates/default/src/server/network/ShopHandler.server.luau +6 -5
  18. package/templates/default/src/server/world/WorldLoader.server.luau +7 -9
  19. package/templates/default/src/shared/Prefabs/ExampleTree/Part/init.meta.json +10 -0
  20. package/templates/default/src/shared/Prefabs/ExampleTree/init.meta.json +6 -0
  21. package/templates/default/src/shared/Prefabs/init.meta.json +6 -0
  22. package/templates/default/src/shared/movement/Pathfind.luau +42 -0
  23. package/templates/default/src/shared/network/Network.luau +1 -1
  24. package/templates/default/src/workspace/Accessory Storage/init.meta.json +1 -1
  25. package/templates/default/src/workspace/Baseplate/Texture/init.meta.json +1 -1
  26. package/templates/default/src/workspace/Baseplate/init.meta.json +1 -1
  27. package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/Smooth Block Model/Snap/init.meta.json +1 -1
  28. package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/Smooth Block Model/Weld/init.meta.json +1 -1
  29. package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/Smooth Block Model/init.meta.json +1 -1
  30. package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/init.meta.json +1 -1
  31. package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/init.meta.json +1 -1
  32. package/templates/default/src/workspace/Map/Decorated old roblox house/Model/init.meta.json +1 -1
  33. package/templates/default/src/workspace/Map/Decorated old roblox house/init.meta.json +1 -1
  34. package/templates/default/src/workspace/Map/init.meta.json +1 -1
  35. package/templates/default/src/workspace/Part2/Decal/init.meta.json +1 -1
  36. package/templates/default/src/workspace/Part2/init.meta.json +1 -1
  37. package/templates/default/src/workspace/SpawnLocation/Decal/init.meta.json +1 -1
  38. package/templates/default/src/workspace/SpawnLocation/init.meta.json +1 -1
  39. package/templates/default/AI_INSTRUCTIONS.md +0 -72
@@ -0,0 +1,1313 @@
1
+ <roblox version="4">
2
+ <Item class="Folder" referent="0">
3
+ <Properties>
4
+ <string name="Name">RoportExportPlugin</string>
5
+ </Properties>
6
+ <Item class="Script" referent="1">
7
+ <Properties>
8
+ <string name="Name">RoportSyncPlugin</string>
9
+ <token name="RunContext">0</token>
10
+ <string name="Source"><![CDATA[local HttpService = game:GetService("HttpService")
11
+ local RunService = game:GetService("RunService")
12
+ local TweenService = game:GetService("TweenService")
13
+ local ScriptEditorService = game:GetService("ScriptEditorService")
14
+
15
+ print("Roport Plugin v1.3.0 Loaded (Project Config Support)")
16
+
17
+ -- Prevent running as a normal script
18
+ if not plugin then return end
19
+
20
+ local TOOLBAR_NAME = "Roport Tools"
21
+ local BUTTON_NAME = "Open Sync Panel"
22
+
23
+ -- State
24
+ local state = {
25
+ isConnected = false,
26
+ isAutoSync = false,
27
+ isAutoPull = false,
28
+ trackedInstances = {},
29
+ pathCache = {}, -- [Instance] = "full/path/to/file.ext"
30
+ connections = {}, -- [Instance] = {Connection, ...}
31
+ settings = {
32
+ port = 3456,
33
+ syncInterval = 2
34
+ },
35
+ mounts = {} -- Stores project config from server
36
+ }
37
+
38
+ -- Load Settings
39
+ local function loadSettings()
40
+ if not state.settings then
41
+ state.settings = { port = 3456, syncInterval = 2 }
42
+ end
43
+ local port = plugin:GetSetting("Roport_Port")
44
+ local interval = plugin:GetSetting("Roport_Interval")
45
+ if port then state.settings.port = port end
46
+ if interval then state.settings.syncInterval = interval end
47
+ end
48
+ loadSettings()
49
+
50
+ local function saveSettings()
51
+ plugin:SetSetting("Roport_Port", state.settings.port)
52
+ plugin:SetSetting("Roport_Interval", state.settings.syncInterval)
53
+ end
54
+
55
+ local function getUrl(endpoint)
56
+ if not state.settings then
57
+ state.settings = { port = 3456, syncInterval = 2 }
58
+ end
59
+ return "http://127.0.0.1:" .. state.settings.port .. "/" .. endpoint
60
+ end
61
+
62
+ local SERVER_URL_PING = getUrl("ping")
63
+ local SERVER_URL_CONFIG = getUrl("config")
64
+ local SERVER_URL_WRITE = getUrl("write")
65
+ local SERVER_URL_BATCH = getUrl("batch")
66
+ local SERVER_URL_DELETE = getUrl("delete")
67
+ local SERVER_URL_POLL = getUrl("poll")
68
+ local SERVER_URL_COMMAND = getUrl("command")
69
+ local SERVER_URL_LOG = getUrl("log")
70
+ local SERVER_URL_EXEC_RESULT = getUrl("execute-result")
71
+
72
+ -- Colors & Styling
73
+ local COLORS = {
74
+ BG = Color3.fromRGB(37, 37, 37),
75
+ HEADER = Color3.fromRGB(45, 45, 45),
76
+ BUTTON = Color3.fromRGB(55, 55, 55),
77
+ BUTTON_HOVER = Color3.fromRGB(65, 65, 65),
78
+ ACCENT = Color3.fromRGB(0, 122, 204), -- VS Code Blue
79
+ TEXT = Color3.fromRGB(245, 245, 245),
80
+ SUBTEXT = Color3.fromRGB(180, 180, 180),
81
+ SUCCESS = Color3.fromRGB(90, 200, 90),
82
+ ERROR = Color3.fromRGB(220, 80, 80),
83
+ WARNING = Color3.fromRGB(220, 200, 80)
84
+ }
85
+
86
+ -- UI Elements
87
+ local toolbar = plugin:CreateToolbar(TOOLBAR_NAME)
88
+ local toggleButton
89
+ local success, err = pcall(function()
90
+ toggleButton = toolbar:CreateButton(BUTTON_NAME, "Open the Roport Sync Panel", "rbxassetid://122082770857390")
91
+ end)
92
+
93
+ if not success then
94
+ warn("Roport: Failed to create toolbar button (it might already exist): " .. tostring(err))
95
+ return
96
+ end
97
+
98
+
99
+ local widgetInfo = DockWidgetPluginGuiInfo.new(
100
+ Enum.InitialDockState.Right,
101
+ false, false, 300, 250, 250, 200
102
+ )
103
+
104
+ local widget = plugin:CreateDockWidgetPluginGui("RoportSyncPanel", widgetInfo)
105
+ widget.Title = "Roport Sync"
106
+ widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
107
+
108
+ -- Main Frame
109
+ local mainFrame = Instance.new("Frame")
110
+ mainFrame.Size = UDim2.new(1, 0, 1, 0)
111
+ mainFrame.BackgroundColor3 = COLORS.BG
112
+ mainFrame.BorderSizePixel = 0
113
+ mainFrame.Parent = widget
114
+
115
+ -- Toast Container (ZIndex 10)
116
+ local toastContainer = Instance.new("Frame")
117
+ toastContainer.Size = UDim2.new(1, -20, 1, -20)
118
+ toastContainer.Position = UDim2.new(0, 10, 0, 10)
119
+ toastContainer.BackgroundTransparency = 1
120
+ toastContainer.ZIndex = 10
121
+ toastContainer.Parent = mainFrame
122
+
123
+ local toastLayout = Instance.new("UIListLayout")
124
+ toastLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom
125
+ toastLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
126
+ toastLayout.Padding = UDim.new(0, 5)
127
+ toastLayout.Parent = toastContainer
128
+
129
+ -- Modal Container (ZIndex 20)
130
+ local modalContainer = Instance.new("Frame")
131
+ modalContainer.Size = UDim2.new(1, 0, 1, 0)
132
+ modalContainer.BackgroundColor3 = Color3.new(0, 0, 0)
133
+ modalContainer.BackgroundTransparency = 1 -- Starts invisible
134
+ modalContainer.Visible = false
135
+ modalContainer.ZIndex = 20
136
+ modalContainer.Parent = mainFrame
137
+
138
+ -- Content Layout
139
+ local contentFrame = Instance.new("Frame")
140
+ contentFrame.Size = UDim2.new(1, 0, 1, 0)
141
+ contentFrame.BackgroundTransparency = 1
142
+ contentFrame.Parent = mainFrame
143
+
144
+ local listLayout = Instance.new("UIListLayout")
145
+ listLayout.Padding = UDim.new(0, 10)
146
+ listLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
147
+ listLayout.SortOrder = Enum.SortOrder.LayoutOrder
148
+ listLayout.Parent = contentFrame
149
+
150
+ local padding = Instance.new("UIPadding")
151
+ padding.PaddingTop = UDim.new(0, 15)
152
+ padding.PaddingLeft = UDim.new(0, 15)
153
+ padding.PaddingRight = UDim.new(0, 15)
154
+ padding.Parent = contentFrame
155
+
156
+ -- UI Helpers
157
+ local function createCorner(parent, radius)
158
+ local corner = Instance.new("UICorner")
159
+ corner.CornerRadius = UDim.new(0, radius or 6)
160
+ corner.Parent = parent
161
+ return corner
162
+ end
163
+
164
+ local function showToast(message, type)
165
+ local toast = Instance.new("Frame")
166
+ toast.Size = UDim2.new(1, 0, 0, 0) -- Animate height
167
+ toast.BackgroundTransparency = 1
168
+ toast.ClipsDescendants = true
169
+ toast.Parent = toastContainer
170
+
171
+ local inner = Instance.new("Frame")
172
+ inner.Size = UDim2.new(1, 0, 0, 35)
173
+ inner.BackgroundColor3 = COLORS.HEADER
174
+ inner.BorderSizePixel = 0
175
+ createCorner(inner, 6)
176
+ inner.Parent = toast
177
+
178
+ local bar = Instance.new("Frame")
179
+ bar.Size = UDim2.new(0, 4, 1, 0)
180
+ bar.BackgroundColor3 = type == "error" and COLORS.ERROR or (type == "success" and COLORS.SUCCESS or COLORS.ACCENT)
181
+ createCorner(bar, 6)
182
+ bar.Parent = inner
183
+
184
+ local label = Instance.new("TextLabel")
185
+ label.Size = UDim2.new(1, -15, 1, 0)
186
+ label.Position = UDim2.new(0, 10, 0, 0)
187
+ label.BackgroundTransparency = 1
188
+ label.Text = message
189
+ label.TextColor3 = COLORS.TEXT
190
+ label.Font = Enum.Font.SourceSans
191
+ label.TextSize = 14
192
+ label.TextXAlignment = Enum.TextXAlignment.Left
193
+ label.Parent = inner
194
+
195
+ -- Animation
196
+ local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
197
+ TweenService:Create(toast, tweenInfo, {Size = UDim2.new(1, 0, 0, 40)}):Play()
198
+
199
+ task.delay(3, function()
200
+ local out = TweenService:Create(toast, tweenInfo, {Size = UDim2.new(1, 0, 0, 0)})
201
+ out:Play()
202
+ out.Completed:Connect(function() toast:Destroy() end)
203
+ end)
204
+ end
205
+
206
+ local function showModal(title, message, onConfirm)
207
+ modalContainer.Visible = true
208
+ TweenService:Create(modalContainer, TweenInfo.new(0.3), {BackgroundTransparency = 0.5}):Play()
209
+
210
+ -- Clear previous
211
+ for _, c in ipairs(modalContainer:GetChildren()) do c:Destroy() end
212
+
213
+ local box = Instance.new("Frame")
214
+ box.Size = UDim2.new(0.8, 0, 0, 140)
215
+ box.Position = UDim2.new(0.5, 0, 0.5, 0)
216
+ box.AnchorPoint = Vector2.new(0.5, 0.5)
217
+ box.BackgroundColor3 = COLORS.HEADER
218
+ createCorner(box, 8)
219
+ box.Parent = modalContainer
220
+
221
+ -- Scale up animation
222
+ local uiScale = Instance.new("UIScale")
223
+ uiScale.Scale = 0.8
224
+ uiScale.Parent = box
225
+
226
+ TweenService:Create(uiScale, TweenInfo.new(0.3, Enum.EasingStyle.Back), {Scale = 1}):Play()
227
+
228
+ local titleLbl = Instance.new("TextLabel")
229
+ titleLbl.Size = UDim2.new(1, 0, 0, 40)
230
+ titleLbl.BackgroundTransparency = 1
231
+ titleLbl.Text = title
232
+ titleLbl.Font = Enum.Font.SourceSansBold
233
+ titleLbl.TextSize = 18
234
+ titleLbl.TextColor3 = COLORS.TEXT
235
+ titleLbl.Parent = box
236
+
237
+ local msgLbl = Instance.new("TextLabel")
238
+ msgLbl.Size = UDim2.new(1, -20, 0, 50)
239
+ msgLbl.Position = UDim2.new(0, 10, 0, 40)
240
+ msgLbl.BackgroundTransparency = 1
241
+ msgLbl.Text = message
242
+ msgLbl.Font = Enum.Font.SourceSans
243
+ msgLbl.TextSize = 14
244
+ msgLbl.TextColor3 = COLORS.SUBTEXT
245
+ msgLbl.TextWrapped = true
246
+ msgLbl.Parent = box
247
+
248
+ local btnFrame = Instance.new("Frame")
249
+ btnFrame.Size = UDim2.new(1, -20, 0, 35)
250
+ btnFrame.Position = UDim2.new(0, 10, 1, -45)
251
+ btnFrame.BackgroundTransparency = 1
252
+ btnFrame.Parent = box
253
+
254
+ local layout = Instance.new("UIListLayout")
255
+ layout.FillDirection = Enum.FillDirection.Horizontal
256
+ layout.Padding = UDim.new(0, 10)
257
+ layout.HorizontalAlignment = Enum.HorizontalAlignment.Center
258
+ layout.Parent = btnFrame
259
+
260
+ local function makeBtn(text, color, cb)
261
+ local btn = Instance.new("TextButton")
262
+ btn.Size = UDim2.new(0.45, 0, 1, 0)
263
+ btn.BackgroundColor3 = color
264
+ btn.Text = text
265
+ btn.TextColor3 = COLORS.TEXT
266
+ btn.Font = Enum.Font.SourceSansBold
267
+ btn.TextSize = 14
268
+ createCorner(btn, 4)
269
+ btn.Parent = btnFrame
270
+ btn.MouseButton1Click:Connect(cb)
271
+ end
272
+
273
+ makeBtn("Connect", COLORS.ACCENT, function()
274
+ TweenService:Create(modalContainer, TweenInfo.new(0.2), {BackgroundTransparency = 1}):Play()
275
+ modalContainer.Visible = false
276
+ onConfirm()
277
+ end)
278
+
279
+ makeBtn("Cancel", COLORS.BUTTON, function()
280
+ TweenService:Create(modalContainer, TweenInfo.new(0.2), {BackgroundTransparency = 1}):Play()
281
+ modalContainer.Visible = false
282
+ end)
283
+ end
284
+
285
+ -- Components
286
+ local function createButton(text, icon, callback)
287
+ local btn = Instance.new("TextButton")
288
+ btn.Size = UDim2.new(1, 0, 0, 40)
289
+ btn.BackgroundColor3 = COLORS.BUTTON
290
+ btn.Text = ""
291
+ createCorner(btn, 6)
292
+ btn.Parent = contentFrame
293
+
294
+ local lbl = Instance.new("TextLabel")
295
+ lbl.Size = UDim2.new(1, 0, 1, 0)
296
+ lbl.BackgroundTransparency = 1
297
+ lbl.Text = text
298
+ lbl.Font = Enum.Font.SourceSansBold
299
+ lbl.TextSize = 16
300
+ lbl.TextColor3 = COLORS.TEXT
301
+ lbl.Parent = btn
302
+
303
+ btn.MouseButton1Click:Connect(callback)
304
+ return btn
305
+ end
306
+
307
+ -- Status Header
308
+ local statusFrame = Instance.new("Frame")
309
+ statusFrame.Size = UDim2.new(1, 0, 0, 30)
310
+ statusFrame.BackgroundTransparency = 1
311
+ statusFrame.LayoutOrder = 0
312
+ statusFrame.Parent = contentFrame
313
+
314
+ local statusDot = Instance.new("Frame")
315
+ statusDot.Size = UDim2.new(0, 10, 0, 10)
316
+ statusDot.Position = UDim2.new(0, 0, 0.5, -5)
317
+ statusDot.BackgroundColor3 = COLORS.ERROR
318
+ createCorner(statusDot, 10)
319
+ statusDot.Parent = statusFrame
320
+
321
+ local statusText = Instance.new("TextLabel")
322
+ statusText.Size = UDim2.new(1, -20, 1, 0)
323
+ statusText.Position = UDim2.new(0, 20, 0, 0)
324
+ statusText.BackgroundTransparency = 1
325
+ statusText.Text = "Disconnected"
326
+ statusText.Font = Enum.Font.SourceSans
327
+ statusText.TextSize = 14
328
+ statusText.TextColor3 = COLORS.SUBTEXT
329
+ statusText.TextXAlignment = Enum.TextXAlignment.Left
330
+ statusText.Parent = statusFrame
331
+
332
+ -- Helper Functions (Global)
333
+ local function sanitize(name)
334
+ return name:gsub("[\\/:*?\"<>|]", "_")
335
+ end
336
+
337
+ local function resolveRobloxService(robloxPathArray)
338
+ local current = game
339
+ for _, name in ipairs(robloxPathArray) do
340
+ if not current then return nil end
341
+ -- Try to find child, if not found, check if it's a service
342
+ local child = current:FindFirstChild(name)
343
+ if not child then
344
+ local success, service = pcall(function() return game:GetService(name) end)
345
+ if success and service then
346
+ child = service
347
+ end
348
+ end
349
+ current = child
350
+ end
351
+ return current
352
+ end
353
+
354
+ local function getPath(inst)
355
+ if #state.mounts == 0 then return nil end
356
+
357
+ for _, mount in ipairs(state.mounts) do
358
+ local root = resolveRobloxService(mount.robloxPath)
359
+ if root and (inst == root or inst:IsDescendantOf(root)) then
360
+ -- Calculate relative path
361
+ local relative = {}
362
+ local current = inst
363
+ while current ~= root do
364
+ table.insert(relative, 1, sanitize(current.Name))
365
+ current = current.Parent
366
+ end
367
+
368
+ local fullPath = mount.filePath
369
+ if #relative > 0 then
370
+ fullPath = fullPath .. "/" .. table.concat(relative, "/")
371
+ end
372
+ return fullPath
373
+ end
374
+ end
375
+ return nil
376
+ end
377
+
378
+ local function serializeValue(val)
379
+ local t = typeof(val)
380
+ if t == "Vector3" then return {val.X, val.Y, val.Z}
381
+ elseif t == "Vector2" then return {val.X, val.Y}
382
+ elseif t == "Color3" then return {val.R, val.G, val.B}
383
+ elseif t == "CFrame" then return {val:GetComponents()}
384
+ elseif t == "UDim2" then return {val.X.Scale, val.X.Offset, val.Y.Scale, val.Y.Offset}
385
+ elseif t == "UDim" then return {val.Scale, val.Offset}
386
+ elseif t == "Rect" then return {val.Min.X, val.Min.Y, val.Max.X, val.Max.Y}
387
+ elseif t == "NumberRange" then return {val.Min, val.Max}
388
+ elseif t == "EnumItem" then return val.Name
389
+ elseif t == "Instance" then return val.Name -- Reference by name (weak)
390
+ elseif t == "ColorSequence" then
391
+ local kps = {}
392
+ for _, kp in ipairs(val.Keypoints) do table.insert(kps, {kp.Time, {kp.Value.R, kp.Value.G, kp.Value.B}}) end
393
+ return {type="ColorSequence", keypoints=kps}
394
+ elseif t == "NumberSequence" then
395
+ local kps = {}
396
+ for _, kp in ipairs(val.Keypoints) do table.insert(kps, {kp.Time, kp.Value, kp.Envelope}) end
397
+ return {type="NumberSequence", keypoints=kps}
398
+ else return val end
399
+ end
400
+
401
+ local PROPERTY_MAP = {
402
+ Instance = {"Name", "Archivable", "ClassName"},
403
+ BasePart = {"Size", "Position", "Color", "Transparency", "Anchored", "CanCollide", "Material", "Reflectance", "CastShadow", "Locked", "Shape"},
404
+ GuiObject = {"Size", "Position", "AnchorPoint", "BackgroundColor3", "BackgroundTransparency", "BorderColor3", "BorderSizePixel", "Visible", "ZIndex", "LayoutOrder", "ClipsDescendants", "Rotation"},
405
+ TextLabel = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "RichText"},
406
+ TextButton = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "RichText"},
407
+ TextBox = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "PlaceholderText", "ClearTextOnFocus", "MultiLine"},
408
+ ImageLabel = {"Image", "ImageColor3", "ImageTransparency", "ScaleType", "SliceCenter", "TileSize"},
409
+ ImageButton = {"Image", "ImageColor3", "ImageTransparency", "ScaleType", "SliceCenter", "TileSize"},
410
+ ValueBase = {"Value"},
411
+ StringValue = {"Value"},
412
+ Sound = {"SoundId", "Volume", "PlaybackSpeed", "Looped", "Playing", "TimePosition"},
413
+ Animation = {"AnimationId"},
414
+ Decal = {"Texture", "Color3", "Transparency", "Face"},
415
+ Texture = {"Texture", "Color3", "Transparency", "Face", "StudsPerTileU", "StudsPerTileV", "OffsetStudsU", "OffsetStudsV"},
416
+ Light = {"Color", "Brightness", "Enabled", "Shadows", "Range"},
417
+ SpotLight = {"Angle", "Face"},
418
+ SurfaceLight = {"Angle", "Face"},
419
+ DataModelMesh = {"Scale", "Offset", "VertexColor"},
420
+ SpecialMesh = {"MeshId", "TextureId", "MeshType"},
421
+ Humanoid = {"Health", "MaxHealth", "WalkSpeed", "JumpPower", "DisplayName", "RigType", "HipHeight"},
422
+ Tool = {"RequiresHandle", "CanBeDropped", "ToolTip", "Enabled", "Grip"},
423
+ Accessory = {"AttachmentPoint"},
424
+ Clothing = {"Color3"},
425
+ Shirt = {"ShirtTemplate"},
426
+ Pants = {"PantsTemplate"},
427
+ ParticleEmitter = {"Color", "LightEmission", "LightInfluence", "Size", "Texture", "Transparency", "ZOffset", "EmissionDirection", "Enabled", "Lifetime", "Rate", "Rotation", "RotSpeed", "Speed", "SpreadAngle"},
428
+ Trail = {"Color", "Enabled", "FaceCamera", "Lifetime", "LightEmission", "LightInfluence", "MaxLength", "MinLength", "Texture", "TextureLength", "Transparency", "WidthScale"},
429
+ Beam = {"Color", "Enabled", "FaceCamera", "LightEmission", "LightInfluence", "Texture", "TextureLength", "TextureMode", "Transparency", "Width0", "Width1", "ZOffset"},
430
+ Fire = {"Color", "Enabled", "Heat", "Size", "SecondaryColor"},
431
+ Smoke = {"Color", "Enabled", "Opacity", "RiseVelocity", "Size"},
432
+ Sparkles = {"SparkleColor", "Enabled"},
433
+ PostEffect = {"Enabled"},
434
+ BlurEffect = {"Size"},
435
+ BloomEffect = {"Intensity", "Size", "Threshold"},
436
+ ColorCorrectionEffect = {"Brightness", "Contrast", "Saturation", "TintColor"},
437
+ SunRaysEffect = {"Intensity", "Spread"},
438
+ Atmosphere = {"Density", "Offset", "Haze", "Color", "Decay", "Glare"},
439
+ Sky = {"SkyboxBk", "SkyboxDn", "SkyboxFt", "SkyboxLf", "SkyboxRt", "SkyboxUp", "SunTextureId", "MoonTextureId", "StarCount"},
440
+ UICorner = {"CornerRadius"},
441
+ UIStroke = {"Color", "Thickness", "Transparency", "ApplyStrokeMode", "LineJoinMode"},
442
+ UIGradient = {"Color", "Offset", "Rotation", "Transparency"},
443
+ UIPadding = {"PaddingBottom", "PaddingLeft", "PaddingRight", "PaddingTop"},
444
+ UIScale = {"Scale"},
445
+ UIListLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "ItemLineAlignment"},
446
+ UIGridLayout = {"CellPadding", "CellSize", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "StartCorner"},
447
+ UITableLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder"},
448
+ UIPageLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "Animated", "Circular", "EasingDirection", "EasingStyle", "GamepadInputEnabled", "ScrollWheelInputEnabled", "TouchInputEnabled", "TweenTime"},
449
+ Model = {"PrimaryPart", "WorldPivot"},
450
+ ScreenGui = {"ResetOnSpawn", "ZIndexBehavior", "DisplayOrder", "IgnoreGuiInset"},
451
+ BillboardGui = {"Adornee", "Active", "AlwaysOnTop", "ClipsDescendants", "DistanceLowerLimit", "DistanceUpperLimit", "ExtentsOffset", "ExtentsOffsetWorldSpace", "LightInfluence", "MaxDistance", "PlayerToHideFrom", "ResetOnSpawn", "Size", "SizeOffset", "StudsOffset", "StudsOffsetWorldSpace"},
452
+ SurfaceGui = {"Adornee", "Active", "AlwaysOnTop", "CanvasSize", "ClipsDescendants", "Face", "LightInfluence", "PixelsPerStud", "ResetOnSpawn", "SizingMode", "ToolPunchThroughDistance", "ZOffset"},
453
+ ScrollingFrame = {"CanvasPosition", "CanvasSize", "ElasticBehavior", "HorizontalScrollBarInset", "ScrollBarImageColor3", "ScrollBarImageTransparency", "ScrollBarThickness", "ScrollingDirection", "ScrollingEnabled", "VerticalScrollBarInset", "VerticalScrollBarPosition"},
454
+ UIAspectRatioConstraint = {"AspectRatio", "AspectType", "DominantAxis"},
455
+ UISizeConstraint = {"MaxSize", "MinSize"},
456
+ RemoteEvent = {},
457
+ RemoteFunction = {},
458
+ BindableEvent = {},
459
+ BindableFunction = {},
460
+ }
461
+
462
+ local function getProperties(inst)
463
+ local props = {
464
+ className = inst.ClassName,
465
+ name = inst.Name,
466
+ properties = {},
467
+ attributes = {},
468
+ tags = game:GetService("CollectionService"):GetTags(inst)
469
+ }
470
+
471
+ -- Serialize Attributes
472
+ for k, v in pairs(inst:GetAttributes()) do
473
+ props.attributes[k] = serializeValue(v)
474
+ end
475
+
476
+ -- Table-driven property serialization
477
+ for className, propList in pairs(PROPERTY_MAP) do
478
+ if inst:IsA(className) then
479
+ for _, propName in ipairs(propList) do
480
+ pcall(function()
481
+ props.properties[propName] = serializeValue(inst[propName])
482
+ end)
483
+ end
484
+ end
485
+ end
486
+
487
+ return props
488
+ end
489
+
490
+ local function getExtension(inst)
491
+ local isInit = (inst:IsA("Script") or inst:IsA("LocalScript") or inst:IsA("ModuleScript")) and #inst:GetChildren() > 0
492
+
493
+ if inst:IsA("Script") then return isInit and "/init.server.luau" or ".server.luau"
494
+ elseif inst:IsA("LocalScript") then return isInit and "/init.client.luau" or ".client.luau"
495
+ elseif inst:IsA("ModuleScript") then return isInit and "/init.luau" or ".luau"
496
+ elseif inst:IsA("StringValue") then return ".txt"
497
+ else return "/init.meta.json" end
498
+ end
499
+
500
+ local function getContent(inst)
501
+ if inst:IsA("Script") or inst:IsA("LocalScript") or inst:IsA("ModuleScript") then
502
+ return inst.Source
503
+ elseif inst:IsA("StringValue") then
504
+ return inst.Value
505
+ else
506
+ return HttpService:JSONEncode(getProperties(inst))
507
+ end
508
+ end
509
+
510
+ local function getInfo(inst)
511
+ return getExtension(inst), getContent(inst)
512
+ end
513
+
514
+ local function syncInstance(inst)
515
+ local path = getPath(inst)
516
+ if not path then return end
517
+
518
+ local ext = getExtension(inst)
519
+ local content = getContent(inst)
520
+ local fullPath = path..ext
521
+
522
+ -- Register for tracking
523
+ registerObject(inst, fullPath)
524
+
525
+ pcall(function()
526
+ HttpService:PostAsync(SERVER_URL_BATCH, HttpService:JSONEncode({
527
+ files = {{filePath = fullPath, content = content}}
528
+ }))
529
+ end)
530
+ end
531
+
532
+ function registerObject(inst, fullPath)
533
+ if state.pathCache[inst] == fullPath then return end
534
+ state.pathCache[inst] = fullPath
535
+
536
+ -- Clear old connections
537
+ if state.connections[inst] then
538
+ for _, c in pairs(state.connections[inst]) do c:Disconnect() end
539
+ end
540
+ state.connections[inst] = {}
541
+
542
+ -- 1. Name Changed (Rename)
543
+ local nameCon = inst:GetPropertyChangedSignal("Name"):Connect(function()
544
+ if not state.isAutoSync then return end
545
+ local oldPath = state.pathCache[inst]
546
+ local newBasePath = getPath(inst)
547
+ if not newBasePath then return end -- Moved to invalid location
548
+
549
+ local newPath = newBasePath .. getExtension(inst)
550
+
551
+ if oldPath and newPath and oldPath ~= newPath then
552
+ -- Delete old
553
+ pcall(function()
554
+ HttpService:PostAsync(SERVER_URL_DELETE, HttpService:JSONEncode({files={oldPath}}))
555
+ end)
556
+ -- Sync new
557
+ syncInstance(inst)
558
+ end
559
+ end)
560
+ -- Use direct assignment to avoid table.insert issues
561
+ local cons = state.connections[inst]
562
+ cons[#cons+1] = nameCon
563
+
564
+ -- 2. Ancestry Changed (Move or Delete)
565
+ local ancestryCon = inst.AncestryChanged:Connect(function(_, parent)
566
+ if not state.isAutoSync then return end
567
+
568
+ if parent == nil then
569
+ -- Deleted
570
+ local oldPath = state.pathCache[inst]
571
+ if oldPath then
572
+ pcall(function()
573
+ HttpService:PostAsync(SERVER_URL_DELETE, HttpService:JSONEncode({files={oldPath}}))
574
+ end)
575
+ state.pathCache[inst] = nil
576
+ if state.connections[inst] then
577
+ for _, c in pairs(state.connections[inst]) do c:Disconnect() end
578
+ state.connections[inst] = nil
579
+ end
580
+ end
581
+ else
582
+ -- Moved
583
+ local oldPath = state.pathCache[inst]
584
+ local newBasePath = getPath(inst)
585
+ if not newBasePath then return end
586
+
587
+ local newPath = newBasePath .. getExtension(inst)
588
+
589
+ if oldPath and newPath and oldPath ~= newPath then
590
+ pcall(function()
591
+ HttpService:PostAsync(SERVER_URL_DELETE, HttpService:JSONEncode({files={oldPath}}))
592
+ end)
593
+ syncInstance(inst)
594
+ end
595
+ end
596
+ end)
597
+ cons[#cons+1] = ancestryCon
598
+
599
+ -- 3. Property Changed (Auto-Push)
600
+ for className, propList in pairs(PROPERTY_MAP) do
601
+ if inst:IsA(className) then
602
+ for _, propName in ipairs(propList) do
603
+ if propName == "Name" then continue end -- Handled above
604
+
605
+ local success, con = pcall(function()
606
+ return inst:GetPropertyChangedSignal(propName):Connect(function()
607
+ if not state.isAutoSync then return end
608
+ -- Debounce could be added here, but for now direct sync ensures responsiveness
609
+ syncInstance(inst)
610
+ end)
611
+ end)
612
+
613
+ if success and con then
614
+ cons[#cons+1] = con
615
+ end
616
+ end
617
+ end
618
+ end
619
+ end
620
+
621
+ local function syncAll()
622
+ if not state.isConnected then showToast("Not connected!", "error") return end
623
+ showToast("Starting Batch Sync...", "info")
624
+
625
+ local batch = {}
626
+ local count = 0
627
+
628
+ -- Iterate through all mount points
629
+ for _, mount in ipairs(state.mounts) do
630
+ local root = resolveRobloxService(mount.robloxPath)
631
+ if root then
632
+ -- Sync descendants
633
+ for _, d in ipairs(root:GetDescendants()) do
634
+ -- Skip Terrain and Camera
635
+ if d:IsA("Terrain") or d:IsA("Camera") then continue end
636
+ if d == script or d == plugin then continue end
637
+
638
+ local path = getPath(d)
639
+ if path then
640
+ local ext, content = getInfo(d)
641
+ if ext and content then
642
+ local fullPath = path..ext
643
+ table.insert(batch, {filePath = fullPath, content = content})
644
+ registerObject(d, fullPath)
645
+ count = count + 1
646
+ end
647
+ end
648
+ end
649
+ end
650
+ end
651
+
652
+ print("Roport: Found " .. count .. " items to sync.")
653
+
654
+ -- Send
655
+ task.spawn(function()
656
+ local chunkSize = 50
657
+ for i = 1, #batch, chunkSize do
658
+ local chunk = {}
659
+ for j = i, math.min(i + chunkSize - 1, #batch) do table.insert(chunk, batch[j]) end
660
+ local success, err = pcall(function()
661
+ HttpService:PostAsync(SERVER_URL_BATCH, HttpService:JSONEncode({files=chunk}))
662
+ end)
663
+ if not success then
664
+ warn("Roport: Batch sync failed: " .. tostring(err))
665
+ end
666
+ task.wait(0.1)
667
+ end
668
+ showToast("Sync Complete!", "success")
669
+ end)
670
+ end
671
+
672
+ -- Buttons
673
+ local syncBtn = createButton("Sync All (Push)", "", syncAll)
674
+
675
+ local function pullChanges()
676
+ if not state.isConnected then showToast("Connect first!", "error") return end
677
+ showToast("Checking for updates...", "info")
678
+
679
+ local success, res = pcall(function()
680
+ return HttpService:GetAsync(SERVER_URL_POLL .. "?t=" .. os.time())
681
+ end)
682
+
683
+ if success then
684
+ local data = HttpService:JSONDecode(res)
685
+ if data.changes and #data.changes > 0 then
686
+ print("Roport: Pulling " .. #data.changes .. " files...")
687
+ for _, change in ipairs(data.changes) do
688
+ applyUpdate(change.filePath, change.content)
689
+ end
690
+ showToast("Updated " .. #data.changes .. " files", "success")
691
+ else
692
+ showToast("No changes found", "info")
693
+ end
694
+ else
695
+ warn("Roport: Pull failed: " .. tostring(res))
696
+ showToast("Pull Failed", "error")
697
+ end
698
+ end
699
+
700
+ local pullBtn = createButton("Pull Changes", "", pullChanges)
701
+
702
+ local autoPullBtn
703
+ autoPullBtn = createButton("Auto-Pull: OFF", "", function()
704
+ if not state.isConnected then showToast("Connect first!", "error") return end
705
+ state.isAutoPull = not state.isAutoPull
706
+
707
+ local lbl = autoPullBtn:FindFirstChild("TextLabel")
708
+
709
+ if state.isAutoPull then
710
+ showToast("Auto-Pull Enabled", "success")
711
+ if lbl then lbl.Text = "Auto-Pull: ON" end
712
+ autoPullBtn.BackgroundColor3 = COLORS.ACCENT
713
+ else
714
+ showToast("Auto-Pull Disabled", "info")
715
+ if lbl then lbl.Text = "Auto-Pull: OFF" end
716
+ autoPullBtn.BackgroundColor3 = COLORS.BUTTON
717
+ end
718
+ end)
719
+
720
+ local autoBtn -- Forward declaration
721
+
722
+ autoBtn = createButton("Auto-Sync: OFF", "", function()
723
+ if not state.isConnected then showToast("Connect first!", "error") return end
724
+ state.isAutoSync = not state.isAutoSync
725
+
726
+ local lbl = autoBtn:FindFirstChild("TextLabel")
727
+
728
+ if state.isAutoSync then
729
+ showToast("Auto-Sync Enabled", "success")
730
+ if lbl then lbl.Text = "Auto-Sync: ON" end
731
+ autoBtn.BackgroundColor3 = COLORS.ACCENT
732
+ else
733
+ showToast("Auto-Sync Disabled", "info")
734
+ if lbl then lbl.Text = "Auto-Sync: OFF" end
735
+ autoBtn.BackgroundColor3 = COLORS.BUTTON
736
+ end
737
+ end)
738
+
739
+ -- Settings UI
740
+ local settingsFrame = Instance.new("Frame")
741
+ settingsFrame.Size = UDim2.new(1, 0, 1, 0)
742
+ settingsFrame.BackgroundColor3 = COLORS.BG
743
+ settingsFrame.Visible = false
744
+ settingsFrame.ZIndex = 15
745
+ settingsFrame.Parent = mainFrame
746
+
747
+ local settingsLayout = Instance.new("UIListLayout")
748
+ settingsLayout.Padding = UDim.new(0, 10)
749
+ settingsLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
750
+ settingsLayout.SortOrder = Enum.SortOrder.LayoutOrder
751
+ settingsLayout.Parent = settingsFrame
752
+
753
+ local settingsPadding = Instance.new("UIPadding")
754
+ settingsPadding.PaddingTop = UDim.new(0, 15)
755
+ settingsPadding.PaddingLeft = UDim.new(0, 15)
756
+ settingsPadding.PaddingRight = UDim.new(0, 15)
757
+ settingsPadding.Parent = settingsFrame
758
+
759
+ local function createSettingInput(label, default, callback)
760
+ local frame = Instance.new("Frame")
761
+ frame.Size = UDim2.new(1, 0, 0, 50)
762
+ frame.BackgroundTransparency = 1
763
+ frame.Parent = settingsFrame
764
+
765
+ local lbl = Instance.new("TextLabel")
766
+ lbl.Size = UDim2.new(1, 0, 0, 20)
767
+ lbl.BackgroundTransparency = 1
768
+ lbl.Text = label
769
+ lbl.TextColor3 = COLORS.SUBTEXT
770
+ lbl.Font = Enum.Font.SourceSans
771
+ lbl.TextSize = 14
772
+ lbl.TextXAlignment = Enum.TextXAlignment.Left
773
+ lbl.Parent = frame
774
+
775
+ local box = Instance.new("TextBox")
776
+ box.Size = UDim2.new(1, 0, 0, 30)
777
+ box.Position = UDim2.new(0, 0, 0, 20)
778
+ box.BackgroundColor3 = COLORS.BUTTON
779
+ box.Text = tostring(default)
780
+ box.TextColor3 = COLORS.TEXT
781
+ box.Font = Enum.Font.SourceSans
782
+ box.TextSize = 14
783
+ createCorner(box, 4)
784
+ box.Parent = frame
785
+
786
+ box.FocusLost:Connect(function()
787
+ callback(box.Text)
788
+ end)
789
+ end
790
+
791
+ createSettingInput("Server Port", state.settings.port, function(val)
792
+ local n = tonumber(val)
793
+ if n then
794
+ state.settings.port = n
795
+ saveSettings()
796
+ showToast("Port saved: " .. n, "success")
797
+ end
798
+ end)
799
+
800
+ createSettingInput("Sync Interval (Seconds)", state.settings.syncInterval, function(val)
801
+ local n = tonumber(val)
802
+ if n then
803
+ state.settings.syncInterval = n
804
+ saveSettings()
805
+ showToast("Interval saved: " .. n, "success")
806
+ end
807
+ end)
808
+
809
+ local closeSettingsBtn = Instance.new("TextButton")
810
+ closeSettingsBtn.Size = UDim2.new(1, 0, 0, 35)
811
+ closeSettingsBtn.BackgroundColor3 = COLORS.ACCENT
812
+ closeSettingsBtn.Text = "Back to Sync"
813
+ closeSettingsBtn.TextColor3 = COLORS.TEXT
814
+ closeSettingsBtn.Font = Enum.Font.SourceSansBold
815
+ closeSettingsBtn.TextSize = 14
816
+ createCorner(closeSettingsBtn, 6)
817
+ closeSettingsBtn.Parent = settingsFrame
818
+ closeSettingsBtn.MouseButton1Click:Connect(function()
819
+ settingsFrame.Visible = false
820
+ contentFrame.Visible = true
821
+ end)
822
+
823
+ local openSettingsBtn = createButton("Settings", "", function()
824
+ contentFrame.Visible = false
825
+ settingsFrame.Visible = true
826
+ end)
827
+ openSettingsBtn.LayoutOrder = 100 -- Push to bottom
828
+
829
+ -- Auto Sync Logic
830
+ local dirtyScripts = {}
831
+ local lastSyncTime = 0
832
+
833
+ ScriptEditorService.TextDocumentDidChange:Connect(function(document, changes)
834
+ if not state.isAutoSync or not state.isConnected then return end
835
+
836
+ local script = document:GetScript()
837
+ if script then
838
+ dirtyScripts[script] = true
839
+ end
840
+ end)
841
+
842
+ task.spawn(function()
843
+ while true do
844
+ task.wait(0.5)
845
+ if state.isAutoSync and state.isConnected then
846
+ if os.time() - lastSyncTime >= state.settings.syncInterval then
847
+ local batch = {}
848
+ local count = 0
849
+
850
+ for script, _ in pairs(dirtyScripts) do
851
+ if script.Parent then -- Ensure script still exists
852
+ local path = getPath(script)
853
+ if path then
854
+ local ext, content = getInfo(script)
855
+ if ext and content then
856
+ local fullPath = path..ext
857
+ table.insert(batch, {filePath = fullPath, content = content})
858
+ count = count + 1
859
+ registerObject(script, fullPath) -- Ensure tracking
860
+ end
861
+ end
862
+ end
863
+ end
864
+
865
+ if count > 0 then
866
+ -- Clear dirty list immediately to avoid double sync
867
+ dirtyScripts = {}
868
+
869
+ local success, err = pcall(function()
870
+ HttpService:PostAsync(getUrl("batch"), HttpService:JSONEncode({files=batch}))
871
+ end)
872
+
873
+ if success then
874
+ lastSyncTime = os.time()
875
+ print("Roport: Auto-synced " .. count .. " scripts")
876
+ else
877
+ warn("Roport: Auto-sync failed: " .. tostring(err))
878
+ end
879
+ end
880
+ end
881
+ end
882
+ end
883
+ end)
884
+
885
+ -- Connection Logic
886
+ local hasPrompted = false
887
+
888
+ local function checkConnection()
889
+ local success, _ = pcall(function() return HttpService:GetAsync(getUrl("ping")) end)
890
+
891
+ if success then
892
+ if not state.isConnected then
893
+ -- Server just appeared
894
+ if not hasPrompted then
895
+ print("Roport: Server detected, prompting user...")
896
+ hasPrompted = true
897
+ showModal("Server Detected", "VS Code sync server is running. Connect now?", function()
898
+ -- Fetch Config First
899
+ local configSuccess, configRes = pcall(function() return HttpService:GetAsync(SERVER_URL_CONFIG) end)
900
+ if configSuccess then
901
+ local data = HttpService:JSONDecode(configRes)
902
+ if data.mounts then
903
+ state.mounts = data.mounts
904
+ print("Roport: Loaded project config with " .. #state.mounts .. " mount points.")
905
+ end
906
+ end
907
+
908
+ state.isConnected = true
909
+ statusDot.BackgroundColor3 = COLORS.SUCCESS
910
+ statusText.Text = "Connected"
911
+ showToast("Connected to VS Code", "success")
912
+
913
+ -- Ask for initial sync
914
+ task.wait(0.5)
915
+ showModal("Initial Sync", "Do you want to sync the workspace to VS Code now?", function()
916
+ syncAll()
917
+ end)
918
+ end)
919
+ end
920
+ end
921
+ else
922
+ state.isConnected = false
923
+ statusDot.BackgroundColor3 = COLORS.ERROR
924
+ statusText.Text = "Disconnected (Server Offline)"
925
+ hasPrompted = false -- Reset so we prompt again if it comes back
926
+ end
927
+ end
928
+
929
+ -- Two-Way Sync Logic
930
+ local function findInstanceByPath(path)
931
+ local isInit = path:match("/init%.luau$") or path:match("/init%.server%.luau$") or path:match("/init%.client%.luau$") or path:match("/init%.lua$") or path:match("/init%.server%.lua$") or path:match("/init%.client%.lua$")
932
+ local cleanPath = path
933
+
934
+ if isInit then
935
+ cleanPath = path:gsub("/init%.server%.luau$", ""):gsub("/init%.client%.luau$", ""):gsub("/init%.luau$", ""):gsub("/init%.server%.lua$", ""):gsub("/init%.client%.lua$", ""):gsub("/init%.lua$", "")
936
+ else
937
+ cleanPath = path:gsub("%.server%.luau$", ""):gsub("%.client%.luau$", ""):gsub("%.luau$", ""):gsub("%.server%.lua$", ""):gsub("%.client%.lua$", ""):gsub("%.lua$", ""):gsub("/init%.meta%.json$", ""):gsub("%.txt$", ""):gsub("%.json$", "")
938
+ end
939
+
940
+ for _, mount in ipairs(state.mounts) do
941
+ local mountPath = mount.filePath
942
+ -- Check if path starts with mountPath
943
+ if cleanPath == mountPath or cleanPath:sub(1, #mountPath + 1) == (mountPath .. "/") then
944
+ local relativeStr = cleanPath:sub(#mountPath + 2)
945
+ local parts = (relativeStr == "") and {} or relativeStr:split("/")
946
+
947
+ local current = resolveRobloxService(mount.robloxPath)
948
+ if not current then return nil end
949
+
950
+ for _, name in ipairs(parts) do
951
+ current = current:FindFirstChild(name)
952
+ if not current then return nil end
953
+ end
954
+ return current
955
+ end
956
+ end
957
+
958
+ return nil
959
+ end
960
+
961
+ -- XML Parser for .rbxmx support
962
+ local function parseRbxmx(content)
963
+ local roots = {}
964
+ local stack = {}
965
+ local current = nil
966
+
967
+ -- Helper to decode entities
968
+ local function decode(s)
969
+ return s:gsub("&lt;", "<"):gsub("&gt;", ">"):gsub("&quot;", '"'):gsub("&apos;", "'"):gsub("&amp;", "&")
970
+ end
971
+
972
+ -- 1. Extract Items
973
+ -- We iterate through the string finding <Item ...> and </Item>
974
+ -- This is a simplified parser that assumes well-formed Roblox XML
975
+
976
+ local pos = 1
977
+ while true do
978
+ local s, e, tag, attrs = content:find("<Item%s+([^>]+)>", pos)
979
+ local sEnd, eEnd = content:find("</Item>", pos)
980
+
981
+ if not s and not sEnd then break end
982
+
983
+ if s and (not sEnd or s < sEnd) then
984
+ -- Found start tag
985
+ local class = attrs:match('class="([^"]+)"')
986
+ local inst = Instance.new(class or "Folder")
987
+
988
+ -- Parse Properties immediately following
989
+ local propStart, propEnd = content:find("<Properties>", e)
990
+ if propStart then
991
+ local propContentEnd = content:find("</Properties>", propEnd)
992
+ if propContentEnd then
993
+ local propBlock = content:sub(propEnd + 1, propContentEnd - 1)
994
+
995
+ -- Parse properties
996
+ for pType, pName, pVal in propBlock:gmatch("<(%w+)%s+name=\"([^\"]+)\">([^<]*)</%1>") do
997
+ pcall(function()
998
+ if pType == "string" then inst[pName] = decode(pVal)
999
+ elseif pType == "bool" then inst[pName] = (pVal == "true")
1000
+ elseif pType == "float" or pType == "double" or pType == "int" or pType == "int64" then inst[pName] = tonumber(pVal)
1001
+ elseif pType == "Color3" or pType == "Color3uint8" then
1002
+ -- Complex types usually have nested tags in newer format, or text in older
1003
+ -- This simple parser handles basic types. Complex types need more logic.
1004
+ end
1005
+ end)
1006
+ end
1007
+
1008
+ -- Handle nested tags for complex properties (Vector3, Color3, etc)
1009
+ -- <Vector3 name="Position"><X>0</X><Y>10</Y><Z>0</Z></Vector3>
1010
+ for pType, pName, pInner in propBlock:gmatch("<(%w+)%s+name=\"([^\"]+)\">([%s%S]-)</%1>") do
1011
+ pcall(function()
1012
+ if pType == "Vector3" then
1013
+ local x = tonumber(pInner:match("<X>(.-)</X>"))
1014
+ local y = tonumber(pInner:match("<Y>(.-)</Y>"))
1015
+ local z = tonumber(pInner:match("<Z>(.-)</Z>"))
1016
+ inst[pName] = Vector3.new(x, y, z)
1017
+ elseif pType == "Color3" then
1018
+ local r = tonumber(pInner:match("<R>(.-)</R>"))
1019
+ local g = tonumber(pInner:match("<G>(.-)</G>"))
1020
+ local b = tonumber(pInner:match("<B>(.-)</B>"))
1021
+ inst[pName] = Color3.new(r, g, b)
1022
+ elseif pType == "UDim2" then
1023
+ local xs = tonumber(pInner:match("<XS>(.-)</XS>"))
1024
+ local xo = tonumber(pInner:match("<XO>(.-)</XO>"))
1025
+ local ys = tonumber(pInner:match("<YS>(.-)</YS>"))
1026
+ local yo = tonumber(pInner:match("<YO>(.-)</YO>"))
1027
+ inst[pName] = UDim2.new(xs, xo, ys, yo)
1028
+ end
1029
+ end)
1030
+ end
1031
+ end
1032
+ end
1033
+
1034
+ if current then
1035
+ inst.Parent = current
1036
+ else
1037
+ table.insert(roots, inst)
1038
+ end
1039
+
1040
+ table.insert(stack, current)
1041
+ current = inst
1042
+ pos = e + 1
1043
+ else
1044
+ -- Found end tag
1045
+ current = table.remove(stack)
1046
+ pos = eEnd + 1
1047
+ end
1048
+ end
1049
+
1050
+ return roots
1051
+ end
1052
+
1053
+ local function createInstanceByPath(path, content)
1054
+ local isInit = path:match("/init%.luau$") or path:match("/init%.server%.luau$") or path:match("/init%.client%.luau$") or path:match("/init%.lua$") or path:match("/init%.server%.lua$") or path:match("/init%.client%.lua$")
1055
+ local cleanPath = path
1056
+
1057
+ if isInit then
1058
+ cleanPath = path:gsub("/init%.server%.luau$", ""):gsub("/init%.client%.luau$", ""):gsub("/init%.luau$", ""):gsub("/init%.server%.lua$", ""):gsub("/init%.client%.lua$", ""):gsub("/init%.lua$", "")
1059
+ else
1060
+ cleanPath = path:gsub("%.server%.luau$", ""):gsub("%.client%.luau$", ""):gsub("%.luau$", ""):gsub("%.server%.lua$", ""):gsub("%.client%.lua$", ""):gsub("%.lua$", ""):gsub("/init%.meta%.json$", ""):gsub("%.txt$", ""):gsub("%.json$", "")
1061
+ end
1062
+
1063
+ for _, mount in ipairs(state.mounts) do
1064
+ local mountPath = mount.filePath
1065
+ if cleanPath == mountPath or cleanPath:sub(1, #mountPath + 1) == (mountPath .. "/") then
1066
+ local relativeStr = cleanPath:sub(#mountPath + 2)
1067
+ local parts = (relativeStr == "") and {} or relativeStr:split("/")
1068
+
1069
+ local current = resolveRobloxService(mount.robloxPath)
1070
+ if not current then return nil end
1071
+
1072
+ for i, name in ipairs(parts) do
1073
+ local nextInst = current:FindFirstChild(name)
1074
+ local isLast = (i == #parts)
1075
+
1076
+ if isLast then
1077
+ -- Determine desired class
1078
+ local desiredClass = "Folder"
1079
+ if isInit then
1080
+ if path:match("%.server%.luau$") or path:match("%.server%.lua$") then desiredClass = "Script"
1081
+ elseif path:match("%.client%.luau$") or path:match("%.client%.lua$") then desiredClass = "LocalScript"
1082
+ elseif path:match("%.luau$") or path:match("%.lua$") then desiredClass = "ModuleScript"
1083
+ end
1084
+ else
1085
+ if path:match("%.server%.luau$") or path:match("%.server%.lua$") then desiredClass = "Script"
1086
+ elseif path:match("%.client%.luau$") or path:match("%.client%.lua$") then desiredClass = "LocalScript"
1087
+ elseif path:match("%.luau$") or path:match("%.lua$") then desiredClass = "ModuleScript"
1088
+ elseif path:match("%.txt$") then desiredClass = "StringValue"
1089
+ elseif path:match("%.rbxmx$") then desiredClass = "Folder" -- Placeholder, will be replaced by applyUpdate
1090
+ elseif path:match("%.json$") and not path:match("%.meta%.json$") and not path:match("%.model%.json$") then desiredClass = "ModuleScript"
1091
+ elseif path:match("%.model%.json$") or path:match("%.meta%.json$") then
1092
+ local success, data = pcall(function() return HttpService:JSONDecode(content) end)
1093
+ if success and data.className then desiredClass = data.className end
1094
+ end
1095
+ end
1096
+
1097
+ if nextInst then
1098
+ if nextInst.ClassName ~= desiredClass then
1099
+ -- Convert
1100
+ local newInst = Instance.new(desiredClass)
1101
+ newInst.Name = name
1102
+ newInst.Parent = current
1103
+ for _, child in ipairs(nextInst:GetChildren()) do
1104
+ child.Parent = newInst
1105
+ end
1106
+ nextInst:Destroy()
1107
+ nextInst = newInst
1108
+ end
1109
+ else
1110
+ local success, newInst = pcall(function() return Instance.new(desiredClass) end)
1111
+ if success and newInst then
1112
+ newInst.Name = name
1113
+ newInst.Parent = current
1114
+ nextInst = newInst
1115
+ end
1116
+ end
1117
+ else
1118
+ -- Intermediate node
1119
+ if not nextInst then
1120
+ local success, newInst = pcall(function() return Instance.new("Folder") end)
1121
+ if success and newInst then
1122
+ newInst.Name = name
1123
+ newInst.Parent = current
1124
+ nextInst = newInst
1125
+ end
1126
+ end
1127
+ end
1128
+ current = nextInst
1129
+ end
1130
+ return current
1131
+ end
1132
+ end
1133
+ return nil
1134
+ end
1135
+
1136
+ local function applyUpdate(filePath, content)
1137
+ local inst = findInstanceByPath(filePath)
1138
+ if not inst then
1139
+ inst = createInstanceByPath(filePath, content)
1140
+ if not inst then return end
1141
+ end
1142
+
1143
+ if filePath:match("%.txt$") and inst:IsA("StringValue") then
1144
+ inst.Value = content
1145
+ elseif filePath:match("%.json$") and not filePath:match("%.meta%.json$") and not filePath:match("%.model%.json$") and inst:IsA("ModuleScript") then
1146
+ inst.Source = "return " .. content
1147
+ elseif filePath:match("%.rbxmx$") then
1148
+ -- Handle Model Update
1149
+ local roots = parseRbxmx(content)
1150
+ if #roots > 0 then
1151
+ local newInst = roots[1]
1152
+ newInst.Name = inst.Name -- Keep name from path
1153
+ newInst.Parent = inst.Parent
1154
+
1155
+ -- Move children that are NOT part of the model content but exist in the tree?
1156
+ -- Actually, if we replace the model, we replace its children too usually.
1157
+ -- But if the user has scripts inside the model in the file system, they should be preserved?
1158
+ -- Rojo usually treats the model file as the source of truth for that instance and its children.
1159
+
1160
+ inst:Destroy()
1161
+ inst = newInst
1162
+ end
1163
+ elseif filePath:match("%.model%.json$") or filePath:match("%.meta%.json$") then
1164
+ local success, data = pcall(function() return HttpService:JSONDecode(content) end)
1165
+ if not success then return end
1166
+
1167
+ -- Update properties
1168
+ if data.properties then
1169
+ for prop, val in pairs(data.properties) do
1170
+ pcall(function()
1171
+ local currentVal = inst[prop]
1172
+ local targetType = typeof(currentVal)
1173
+
1174
+ if type(val) == "table" and targetType ~= "table" then
1175
+ -- Deserialize complex types
1176
+ if targetType == "Color3" then
1177
+ inst[prop] = Color3.new(val[1], val[2], val[3])
1178
+ elseif targetType == "Vector3" then
1179
+ inst[prop] = Vector3.new(val[1], val[2], val[3])
1180
+ elseif targetType == "UDim2" then
1181
+ inst[prop] = UDim2.new(val[1], val[2], val[3], val[4])
1182
+ elseif targetType == "UDim" then
1183
+ inst[prop] = UDim.new(val[1], val[2])
1184
+ elseif targetType == "Rect" then
1185
+ inst[prop] = Rect.new(val[1], val[2], val[3], val[4])
1186
+ elseif targetType == "NumberRange" then
1187
+ inst[prop] = NumberRange.new(val[1], val[2])
1188
+ elseif targetType == "EnumItem" then
1189
+ -- Try to find enum
1190
+ -- This is tricky without knowing the Enum type.
1191
+ -- We might need to store Enum type in JSON or infer it.
1192
+ -- For now, assume string match if possible or skip.
1193
+ end
1194
+ else
1195
+ inst[prop] = val
1196
+ end
1197
+ end)
1198
+ end
1199
+ end
1200
+ elseif filePath:match("%.luau$") or filePath:match("%.lua$") then
1201
+ if inst:IsA("LuaSourceContainer") then
1202
+ inst.Source = content
1203
+ end
1204
+ end
1205
+ print("Roport: Updated " .. inst.Name .. " from VS Code")
1206
+ end
1207
+
1208
+ local function handleCommand(cmd)
1209
+ if cmd.type == "PLAYTEST_START" then
1210
+ print("Roport: Playtest requested (Manual action required for now)")
1211
+ elseif cmd.type == "PUBLISH_TO_ROBLOX" then
1212
+ game:GetService("AssetService"):SavePlaceAsync()
1213
+ print("Roport: Saved to Roblox")
1214
+ elseif cmd.type == "EXECUTE" then
1215
+ -- Remote Execution
1216
+ local code = cmd.code
1217
+ local id = cmd.id
1218
+
1219
+ task.spawn(function()
1220
+ local func, loadErr = loadstring(code)
1221
+ if not func then
1222
+ pcall(function()
1223
+ HttpService:PostAsync(SERVER_URL_EXEC_RESULT, HttpService:JSONEncode({
1224
+ id = id,
1225
+ success = false,
1226
+ error = "LoadError: " .. tostring(loadErr)
1227
+ }))
1228
+ end)
1229
+ return
1230
+ end
1231
+
1232
+ local success, result = pcall(func)
1233
+
1234
+ -- Serialize result if possible
1235
+ local resultStr = tostring(result)
1236
+ if type(result) == "table" then
1237
+ pcall(function() resultStr = HttpService:JSONEncode(result) end)
1238
+ end
1239
+
1240
+ pcall(function()
1241
+ HttpService:PostAsync(SERVER_URL_EXEC_RESULT, HttpService:JSONEncode({
1242
+ id = id,
1243
+ success = success,
1244
+ result = resultStr,
1245
+ error = not success and tostring(result) or nil
1246
+ }))
1247
+ end)
1248
+ end)
1249
+ end
1250
+ end
1251
+
1252
+ -- Log Streaming
1253
+ local function setupLogStreaming()
1254
+ game:GetService("LogService").MessageOut:Connect(function(message, type)
1255
+ if not state.isConnected then return end
1256
+
1257
+ -- Filter out our own logs to prevent loops if we log network errors
1258
+ if message:find("Roport:") then return end
1259
+
1260
+ pcall(function()
1261
+ HttpService:PostAsync(SERVER_URL_LOG, HttpService:JSONEncode({
1262
+ message = message,
1263
+ type = type.Name, -- Enum name (MessageOutput, Warning, Error)
1264
+ timestamp = os.time()
1265
+ }))
1266
+ end)
1267
+ end)
1268
+ end
1269
+ setupLogStreaming()
1270
+
1271
+ task.spawn(function()
1272
+ while true do
1273
+ task.wait(1)
1274
+ if state.isConnected and state.isAutoPull then
1275
+ local success, res = pcall(function()
1276
+ return HttpService:GetAsync(SERVER_URL_POLL .. "?t=" .. os.time())
1277
+ end)
1278
+
1279
+ if success then
1280
+ local data = HttpService:JSONDecode(res)
1281
+
1282
+ if data.commands then
1283
+ for _, cmd in ipairs(data.commands) do
1284
+ handleCommand(cmd)
1285
+ end
1286
+ end
1287
+
1288
+ if data.changes and #data.changes > 0 then
1289
+ print("Roport: Received " .. #data.changes .. " updates from VS Code")
1290
+ for _, change in ipairs(data.changes) do
1291
+ applyUpdate(change.filePath, change.content)
1292
+ end
1293
+ end
1294
+ else
1295
+ warn("Roport: Poll failed: " .. tostring(res))
1296
+ end
1297
+ end
1298
+ end
1299
+ end)
1300
+
1301
+ task.spawn(function()
1302
+ while true do
1303
+ checkConnection()
1304
+ task.wait(3)
1305
+ end
1306
+ end)
1307
+
1308
+ toggleButton.Click:Connect(function() widget.Enabled = not widget.Enabled end)
1309
+ ]]></string>
1310
+ </Properties>
1311
+ </Item>
1312
+ </Item>
1313
+ </roblox>