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.
- package/README.md +16 -0
- package/bin/roport.js +3 -2
- package/package.json +2 -2
- package/src/server.js +171 -10
- package/templates/default/DOCUMENTATION.md +309 -0
- package/templates/default/README.md +35 -0
- package/templates/default/RoportSyncPlugin.rbxmx +1313 -0
- package/templates/default/default.project.json +6 -0
- package/templates/default/src/Roport/Modules/Sync.lua +10 -6
- package/templates/default/src/Roport/init.server.lua +66 -0
- package/templates/default/src/lighting/Atmosphere/init.meta.json +1 -1
- package/templates/default/src/lighting/Bloom/init.meta.json +1 -1
- package/templates/default/src/lighting/DepthOfField/init.meta.json +1 -1
- package/templates/default/src/lighting/Sky/init.meta.json +1 -1
- package/templates/default/src/lighting/SunRays/init.meta.json +1 -1
- package/templates/default/src/server/network/CombatHandler.server.luau +2 -1
- package/templates/default/src/server/network/ShopHandler.server.luau +6 -5
- package/templates/default/src/server/world/WorldLoader.server.luau +7 -9
- package/templates/default/src/shared/Prefabs/ExampleTree/Part/init.meta.json +10 -0
- package/templates/default/src/shared/Prefabs/ExampleTree/init.meta.json +6 -0
- package/templates/default/src/shared/Prefabs/init.meta.json +6 -0
- package/templates/default/src/shared/movement/Pathfind.luau +42 -0
- package/templates/default/src/shared/network/Network.luau +1 -1
- package/templates/default/src/workspace/Accessory Storage/init.meta.json +1 -1
- package/templates/default/src/workspace/Baseplate/Texture/init.meta.json +1 -1
- package/templates/default/src/workspace/Baseplate/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/Smooth Block Model/Snap/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/Smooth Block Model/Weld/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/Smooth Block Model/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/Model/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/Decorated old roblox house/Model/FirstFloor/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/Decorated old roblox house/Model/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/Decorated old roblox house/init.meta.json +1 -1
- package/templates/default/src/workspace/Map/init.meta.json +1 -1
- package/templates/default/src/workspace/Part2/Decal/init.meta.json +1 -1
- package/templates/default/src/workspace/Part2/init.meta.json +1 -1
- package/templates/default/src/workspace/SpawnLocation/Decal/init.meta.json +1 -1
- package/templates/default/src/workspace/SpawnLocation/init.meta.json +1 -1
- 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("<", "<"):gsub(">", ">"):gsub(""", '"'):gsub("'", "'"):gsub("&", "&")
|
|
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>
|