roport 1.4.0 → 2.0.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/dist/index.js +933 -0
- package/package.json +25 -32
- package/README.md +0 -52
- package/assets/RoportSyncPlugin.rbxmx +0 -1864
- package/bin/roport.js +0 -131
- package/src/builder.js +0 -563
- package/src/context.js +0 -76
- package/src/ignore.js +0 -120
- package/src/server.js +0 -393
- package/templates/default/README.md +0 -28
- package/templates/default/default.project.json +0 -23
- package/templates/default/src/client/init.client.lua +0 -1
- package/templates/default/src/server/init.server.lua +0 -1
- package/templates/default/src/shared/Hello.lua +0 -7
|
@@ -1,1864 +0,0 @@
|
|
|
1
|
-
<roblox version="4">
|
|
2
|
-
<Item class="Folder" referent="RBX0">
|
|
3
|
-
<Properties>
|
|
4
|
-
<string name="Name">RoportSyncPlugin</string>
|
|
5
|
-
</Properties>
|
|
6
|
-
<Item class="Script" referent="RBX1">
|
|
7
|
-
<Properties>
|
|
8
|
-
<string name="Name">RojoSyncPlugin</string>
|
|
9
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
10
|
-
local HttpService = game:GetService("HttpService")
|
|
11
|
-
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
12
|
-
local ScriptContext = game:GetService("ScriptContext")
|
|
13
|
-
local LogService = game:GetService("LogService")
|
|
14
|
-
|
|
15
|
-
-- Prevent running as a normal script
|
|
16
|
-
if not plugin then return end
|
|
17
|
-
|
|
18
|
-
print("Roport Plugin v1.3.2 Loaded (Patched)")
|
|
19
|
-
|
|
20
|
-
local Modules = script.Modules
|
|
21
|
-
local Constants = require(Modules.Constants)
|
|
22
|
-
local State = require(Modules.State)
|
|
23
|
-
local UI = require(Modules.UI)
|
|
24
|
-
local Sync = require(Modules.Sync)
|
|
25
|
-
local Paths = require(Modules.Paths)
|
|
26
|
-
local Serialization = require(Modules.Serialization)
|
|
27
|
-
|
|
28
|
-
-- Load Settings
|
|
29
|
-
State.load(plugin)
|
|
30
|
-
|
|
31
|
-
local function saveSettings()
|
|
32
|
-
State.save(plugin)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
-- Init UI
|
|
36
|
-
local toggleButton = UI.init(plugin)
|
|
37
|
-
local statusDot, statusText = UI.createStatusHeader()
|
|
38
|
-
|
|
39
|
-
-- Inject Plugin into Sync
|
|
40
|
-
Sync.plugin = plugin
|
|
41
|
-
|
|
42
|
-
-- Connect Signals
|
|
43
|
-
Sync.OnStatusChanged:Connect(function(message, type)
|
|
44
|
-
UI.showToast(message, type)
|
|
45
|
-
end)
|
|
46
|
-
|
|
47
|
-
-- Error Reporting
|
|
48
|
-
ScriptContext.Error:Connect(function(message, stackTrace, script)
|
|
49
|
-
if State.isConnected then
|
|
50
|
-
Sync.logError(message, stackTrace)
|
|
51
|
-
end
|
|
52
|
-
end)
|
|
53
|
-
|
|
54
|
-
LogService.MessageOut:Connect(function(message, messageType)
|
|
55
|
-
if State.isConnected and (messageType == Enum.MessageType.MessageError or messageType == Enum.MessageType.MessageWarning) then
|
|
56
|
-
-- Avoid double logging if ScriptContext caught it, but useful for other errors
|
|
57
|
-
-- Sync.logError(message, nil)
|
|
58
|
-
end
|
|
59
|
-
end)
|
|
60
|
-
|
|
61
|
-
-- Buttons
|
|
62
|
-
local syncBtn = UI.createButton("Sync All (Push)", "", Sync.syncAll)
|
|
63
|
-
local pullBtn = UI.createButton("Pull Changes", "", Sync.pullChanges)
|
|
64
|
-
|
|
65
|
-
local autoPullBtn
|
|
66
|
-
autoPullBtn = UI.createButton("Auto-Pull: OFF", "", function()
|
|
67
|
-
if not State.isConnected then UI.showToast("Connect first!", "error") return end
|
|
68
|
-
State.isAutoPull = not State.isAutoPull
|
|
69
|
-
|
|
70
|
-
local lbl = autoPullBtn:FindFirstChild("TextLabel")
|
|
71
|
-
|
|
72
|
-
if State.isAutoPull then
|
|
73
|
-
UI.showToast("Auto-Pull Enabled", "success")
|
|
74
|
-
if lbl then lbl.Text = "Auto-Pull: ON" end
|
|
75
|
-
autoPullBtn.BackgroundColor3 = Constants.COLORS.ACCENT
|
|
76
|
-
else
|
|
77
|
-
UI.showToast("Auto-Pull Disabled", "info")
|
|
78
|
-
if lbl then lbl.Text = "Auto-Pull: OFF" end
|
|
79
|
-
autoPullBtn.BackgroundColor3 = Constants.COLORS.BUTTON
|
|
80
|
-
end
|
|
81
|
-
end)
|
|
82
|
-
|
|
83
|
-
local autoBtn
|
|
84
|
-
autoBtn = UI.createButton("Auto-Sync: OFF", "", function()
|
|
85
|
-
if not State.isConnected then UI.showToast("Connect first!", "error") return end
|
|
86
|
-
State.isAutoSync = not State.isAutoSync
|
|
87
|
-
|
|
88
|
-
local lbl = autoBtn:FindFirstChild("TextLabel")
|
|
89
|
-
|
|
90
|
-
if State.isAutoSync then
|
|
91
|
-
UI.showToast("Auto-Sync Enabled", "success")
|
|
92
|
-
if lbl then lbl.Text = "Auto-Sync: ON" end
|
|
93
|
-
autoBtn.BackgroundColor3 = Constants.COLORS.ACCENT
|
|
94
|
-
else
|
|
95
|
-
UI.showToast("Auto-Sync Disabled", "info")
|
|
96
|
-
if lbl then lbl.Text = "Auto-Sync: OFF" end
|
|
97
|
-
autoBtn.BackgroundColor3 = Constants.COLORS.BUTTON
|
|
98
|
-
end
|
|
99
|
-
end)
|
|
100
|
-
|
|
101
|
-
-- Settings
|
|
102
|
-
local settingsFrame = UI.createSettingsUI(State, saveSettings)
|
|
103
|
-
local openSettingsBtn = UI.createButton("Settings", "", function()
|
|
104
|
-
if UI.ContentFrame then UI.ContentFrame.Visible = false end
|
|
105
|
-
settingsFrame.Visible = true
|
|
106
|
-
end)
|
|
107
|
-
openSettingsBtn.LayoutOrder = 100
|
|
108
|
-
|
|
109
|
-
-- Auto Sync Logic
|
|
110
|
-
local dirtyScripts = {}
|
|
111
|
-
local lastSyncTime = 0
|
|
112
|
-
|
|
113
|
-
ScriptEditorService.TextDocumentDidChange:Connect(function(document, changes)
|
|
114
|
-
if not State.isAutoSync or not State.isConnected then return end
|
|
115
|
-
|
|
116
|
-
local script = document:GetScript()
|
|
117
|
-
if script then
|
|
118
|
-
dirtyScripts[script] = true
|
|
119
|
-
end
|
|
120
|
-
end)
|
|
121
|
-
|
|
122
|
-
task.spawn(function()
|
|
123
|
-
while true do
|
|
124
|
-
task.wait(0.5)
|
|
125
|
-
if State.isAutoSync and State.isConnected then
|
|
126
|
-
if os.time() - lastSyncTime >= State.settings.syncInterval then
|
|
127
|
-
local batch = {}
|
|
128
|
-
local count = 0
|
|
129
|
-
|
|
130
|
-
for script, _ in pairs(dirtyScripts) do
|
|
131
|
-
if script.Parent then -- Ensure script still exists
|
|
132
|
-
local path = Paths.getPath(script)
|
|
133
|
-
if path then
|
|
134
|
-
local ext, content = Serialization.getInfo(script)
|
|
135
|
-
if ext and content then
|
|
136
|
-
local fullPath = path..ext
|
|
137
|
-
table.insert(batch, {filePath = fullPath, content = content})
|
|
138
|
-
count = count + 1
|
|
139
|
-
Sync.registerObject(script, fullPath) -- Ensure tracking
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
if count > 0 then
|
|
146
|
-
-- Clear dirty list immediately to avoid double sync
|
|
147
|
-
dirtyScripts = {}
|
|
148
|
-
|
|
149
|
-
local success, err = pcall(function()
|
|
150
|
-
HttpService:PostAsync(Sync.getUrl("batch"), HttpService:JSONEncode({files=batch}))
|
|
151
|
-
end)
|
|
152
|
-
|
|
153
|
-
if success then
|
|
154
|
-
lastSyncTime = os.time()
|
|
155
|
-
print("Roport: Auto-synced " .. count .. " scripts")
|
|
156
|
-
else
|
|
157
|
-
warn("Roport: Auto-sync failed: " .. tostring(err))
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end)
|
|
164
|
-
|
|
165
|
-
-- Connection Logic
|
|
166
|
-
local hasPrompted = false
|
|
167
|
-
|
|
168
|
-
local function checkConnection()
|
|
169
|
-
local success, _ = pcall(function() return HttpService:GetAsync(Sync.getUrl("ping")) end)
|
|
170
|
-
|
|
171
|
-
if success then
|
|
172
|
-
if not State.isConnected then
|
|
173
|
-
-- Server just appeared
|
|
174
|
-
if not hasPrompted then
|
|
175
|
-
print("Roport: Server detected, prompting user...")
|
|
176
|
-
hasPrompted = true
|
|
177
|
-
UI.showModal("Server Detected", "VS Code sync server is running. Connect now?", function()
|
|
178
|
-
-- Fetch Config First
|
|
179
|
-
local configSuccess, configRes = pcall(function() return HttpService:GetAsync(Sync.getUrl("config")) end)
|
|
180
|
-
if configSuccess then
|
|
181
|
-
local data = HttpService:JSONDecode(configRes)
|
|
182
|
-
if data.mounts then
|
|
183
|
-
State.mounts = data.mounts
|
|
184
|
-
print("Roport: Loaded project config with " .. #State.mounts .. " nodes.")
|
|
185
|
-
Sync.applyProjectConfig()
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
State.isConnected = true
|
|
190
|
-
statusDot.BackgroundColor3 = Constants.COLORS.SUCCESS
|
|
191
|
-
statusText.Text = "Connected"
|
|
192
|
-
UI.showToast("Connected to VS Code", "success")
|
|
193
|
-
|
|
194
|
-
-- Ask for initial sync
|
|
195
|
-
task.wait(0.5)
|
|
196
|
-
UI.showModal("Initial Sync", "Do you want to sync the workspace to VS Code now?", function()
|
|
197
|
-
Sync.syncAll()
|
|
198
|
-
end)
|
|
199
|
-
end)
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
else
|
|
203
|
-
State.isConnected = false
|
|
204
|
-
statusDot.BackgroundColor3 = Constants.COLORS.ERROR
|
|
205
|
-
statusText.Text = "Disconnected (Server Offline)"
|
|
206
|
-
hasPrompted = false -- Reset so we prompt again if it comes back
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
task.spawn(function()
|
|
211
|
-
while true do
|
|
212
|
-
task.wait(1)
|
|
213
|
-
if State.isConnected and State.isAutoPull then
|
|
214
|
-
Sync.pullChanges()
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
end)
|
|
218
|
-
|
|
219
|
-
task.spawn(function()
|
|
220
|
-
while true do
|
|
221
|
-
checkConnection()
|
|
222
|
-
task.wait(3)
|
|
223
|
-
end
|
|
224
|
-
end)
|
|
225
|
-
|
|
226
|
-
local function runDiagnostics()
|
|
227
|
-
print("------------------------------------------------")
|
|
228
|
-
print("[Roport] Running Diagnostics...")
|
|
229
|
-
|
|
230
|
-
-- Check 1: Server Connection
|
|
231
|
-
if game:GetService("RunService"):IsServer() then
|
|
232
|
-
local start = os.clock()
|
|
233
|
-
local success, response = pcall(function() return HttpService:GetAsync(Sync.getUrl("ping")) end)
|
|
234
|
-
local duration = os.clock() - start
|
|
235
|
-
|
|
236
|
-
if success and response == "pong" then
|
|
237
|
-
print("[Roport] ✅ Server Connection: OK (" .. string.format("%.2f", duration*1000) .. "ms)")
|
|
238
|
-
print("[Roport] URL: " .. Sync.getUrl(""))
|
|
239
|
-
else
|
|
240
|
-
print("[Roport] ❌ Server Connection: FAILED")
|
|
241
|
-
print("[Roport] Error: " .. tostring(response))
|
|
242
|
-
print("[Roport] Ensure 'roport serve' is running in your VS Code terminal.")
|
|
243
|
-
end
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
-- Check 2: Project Structure (Client-side check)
|
|
247
|
-
local mappings = {
|
|
248
|
-
{Service = "ReplicatedStorage", Name = "Shared"},
|
|
249
|
-
{Service = "ServerScriptService", Name = "Server"},
|
|
250
|
-
{Service = "StarterPlayer.StarterPlayerScripts", Name = "Client"}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
for _, map in ipairs(mappings) do
|
|
254
|
-
local serviceName = map.Service
|
|
255
|
-
local folderName = map.Name
|
|
256
|
-
|
|
257
|
-
-- Handle nested services
|
|
258
|
-
local parts = string.split(serviceName, ".")
|
|
259
|
-
local current = game
|
|
260
|
-
local foundService = true
|
|
261
|
-
for _, part in ipairs(parts) do
|
|
262
|
-
if current:FindFirstChild(part) then
|
|
263
|
-
current = current[part]
|
|
264
|
-
else
|
|
265
|
-
local s, srv = pcall(function() return game:GetService(part) end)
|
|
266
|
-
if s and srv then
|
|
267
|
-
current = srv
|
|
268
|
-
else
|
|
269
|
-
foundService = false
|
|
270
|
-
break
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
if foundService then
|
|
276
|
-
if current:FindFirstChild(folderName) then
|
|
277
|
-
print("[Roport] ✅ Folder Mapping: " .. serviceName .. "." .. folderName .. " FOUND")
|
|
278
|
-
else
|
|
279
|
-
print("[Roport] ⚠️ Folder Mapping: " .. serviceName .. "." .. folderName .. " MISSING (Will be created on sync)")
|
|
280
|
-
end
|
|
281
|
-
else
|
|
282
|
-
print("[Roport] ❌ Service Missing: " .. serviceName)
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
print("------------------------------------------------")
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
-- Run diagnostics on load
|
|
290
|
-
task.delay(1, runDiagnostics)
|
|
291
|
-
]]></ProtectedString>
|
|
292
|
-
</Properties>
|
|
293
|
-
<Item class="Folder" referent="RBX2">
|
|
294
|
-
<Properties>
|
|
295
|
-
<string name="Name">Modules</string>
|
|
296
|
-
</Properties>
|
|
297
|
-
<Item class="ModuleScript" referent="RBX3">
|
|
298
|
-
<Properties>
|
|
299
|
-
<string name="Name">Constants</string>
|
|
300
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
301
|
-
local Constants = {}
|
|
302
|
-
|
|
303
|
-
Constants.TOOLBAR_NAME = "Rojo Tools"
|
|
304
|
-
Constants.BUTTON_NAME = "Open Sync Panel"
|
|
305
|
-
|
|
306
|
-
Constants.COLORS = {
|
|
307
|
-
BG = Color3.fromRGB(37, 37, 37),
|
|
308
|
-
HEADER = Color3.fromRGB(45, 45, 45),
|
|
309
|
-
BUTTON = Color3.fromRGB(55, 55, 55),
|
|
310
|
-
BUTTON_HOVER = Color3.fromRGB(65, 65, 65),
|
|
311
|
-
ACCENT = Color3.fromRGB(0, 122, 204), -- VS Code Blue
|
|
312
|
-
TEXT = Color3.fromRGB(245, 245, 245),
|
|
313
|
-
SUBTEXT = Color3.fromRGB(180, 180, 180),
|
|
314
|
-
SUCCESS = Color3.fromRGB(90, 200, 90),
|
|
315
|
-
ERROR = Color3.fromRGB(220, 80, 80),
|
|
316
|
-
WARNING = Color3.fromRGB(220, 200, 80)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
Constants.PROPERTY_MAP = {
|
|
320
|
-
Instance = {"Name", "Archivable", "ClassName"},
|
|
321
|
-
BasePart = {"Size", "Position", "Color", "Transparency", "Anchored", "CanCollide", "Material", "Reflectance", "CastShadow", "Locked", "Shape"},
|
|
322
|
-
GuiObject = {"Size", "Position", "AnchorPoint", "BackgroundColor3", "BackgroundTransparency", "BorderColor3", "BorderSizePixel", "Visible", "ZIndex", "LayoutOrder", "ClipsDescendants", "Rotation"},
|
|
323
|
-
TextLabel = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "RichText"},
|
|
324
|
-
TextButton = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "RichText"},
|
|
325
|
-
TextBox = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "PlaceholderText", "ClearTextOnFocus", "MultiLine"},
|
|
326
|
-
ImageLabel = {"Image", "ImageColor3", "ImageTransparency", "ScaleType", "SliceCenter", "TileSize"},
|
|
327
|
-
ImageButton = {"Image", "ImageColor3", "ImageTransparency", "ScaleType", "SliceCenter", "TileSize"},
|
|
328
|
-
ValueBase = {"Value"},
|
|
329
|
-
Sound = {"SoundId", "Volume", "PlaybackSpeed", "Looped", "Playing", "TimePosition"},
|
|
330
|
-
Animation = {"AnimationId"},
|
|
331
|
-
Decal = {"Texture", "Color3", "Transparency", "Face"},
|
|
332
|
-
Texture = {"Texture", "Color3", "Transparency", "Face", "StudsPerTileU", "StudsPerTileV", "OffsetStudsU", "OffsetStudsV"},
|
|
333
|
-
Light = {"Color", "Brightness", "Enabled", "Shadows", "Range"},
|
|
334
|
-
SpotLight = {"Angle", "Face"},
|
|
335
|
-
SurfaceLight = {"Angle", "Face"},
|
|
336
|
-
DataModelMesh = {"Scale", "Offset", "VertexColor"},
|
|
337
|
-
SpecialMesh = {"MeshId", "TextureId", "MeshType"},
|
|
338
|
-
Humanoid = {"Health", "MaxHealth", "WalkSpeed", "JumpPower", "DisplayName", "RigType", "HipHeight"},
|
|
339
|
-
Tool = {"RequiresHandle", "CanBeDropped", "ToolTip", "Enabled", "Grip"},
|
|
340
|
-
Accessory = {"AttachmentPoint"},
|
|
341
|
-
Clothing = {"Color3"},
|
|
342
|
-
Shirt = {"ShirtTemplate"},
|
|
343
|
-
Pants = {"PantsTemplate"},
|
|
344
|
-
ParticleEmitter = {"Color", "LightEmission", "LightInfluence", "Size", "Texture", "Transparency", "ZOffset", "EmissionDirection", "Enabled", "Lifetime", "Rate", "Rotation", "RotSpeed", "Speed", "SpreadAngle"},
|
|
345
|
-
Trail = {"Color", "Enabled", "FaceCamera", "Lifetime", "LightEmission", "LightInfluence", "MaxLength", "MinLength", "Texture", "TextureLength", "Transparency", "WidthScale"},
|
|
346
|
-
Beam = {"Color", "Enabled", "FaceCamera", "LightEmission", "LightInfluence", "Texture", "TextureLength", "TextureMode", "Transparency", "Width0", "Width1", "ZOffset"},
|
|
347
|
-
Fire = {"Color", "Enabled", "Heat", "Size", "SecondaryColor"},
|
|
348
|
-
Smoke = {"Color", "Enabled", "Opacity", "RiseVelocity", "Size"},
|
|
349
|
-
Sparkles = {"SparkleColor", "Enabled"},
|
|
350
|
-
PostEffect = {"Enabled"},
|
|
351
|
-
BlurEffect = {"Size"},
|
|
352
|
-
BloomEffect = {"Intensity", "Size", "Threshold"},
|
|
353
|
-
ColorCorrectionEffect = {"Brightness", "Contrast", "Saturation", "TintColor"},
|
|
354
|
-
SunRaysEffect = {"Intensity", "Spread"},
|
|
355
|
-
Atmosphere = {"Density", "Offset", "Haze", "Color", "Decay", "Glare"},
|
|
356
|
-
Sky = {"SkyboxBk", "SkyboxDn", "SkyboxFt", "SkyboxLf", "SkyboxRt", "SkyboxUp", "SunTextureId", "MoonTextureId", "StarCount"},
|
|
357
|
-
UICorner = {"CornerRadius"},
|
|
358
|
-
UIStroke = {"Color", "Thickness", "Transparency", "ApplyStrokeMode", "LineJoinMode"},
|
|
359
|
-
UIGradient = {"Color", "Offset", "Rotation", "Transparency"},
|
|
360
|
-
UIPadding = {"PaddingBottom", "PaddingLeft", "PaddingRight", "PaddingTop"},
|
|
361
|
-
UIScale = {"Scale"},
|
|
362
|
-
UIListLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "ItemLineAlignment"},
|
|
363
|
-
UIGridLayout = {"CellPadding", "CellSize", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "StartCorner"},
|
|
364
|
-
UITableLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder"},
|
|
365
|
-
UIPageLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "Animated", "Circular", "EasingDirection", "EasingStyle", "GamepadInputEnabled", "ScrollWheelInputEnabled", "TouchInputEnabled", "TweenTime"},
|
|
366
|
-
|
|
367
|
-
-- Physics & Constraints
|
|
368
|
-
BodyAngularVelocity = {"AngularVelocity", "MaxTorque", "P"},
|
|
369
|
-
BodyForce = {"Force"},
|
|
370
|
-
BodyGyro = {"CFrame", "D", "MaxTorque", "P"},
|
|
371
|
-
BodyPosition = {"D", "MaxForce", "P", "Position"},
|
|
372
|
-
BodyThrust = {"Force", "Location"},
|
|
373
|
-
BodyVelocity = {"MaxForce", "P", "Velocity"},
|
|
374
|
-
RocketPropulsion = {"CartoonFactor", "MaxSpeed", "MaxThrust", "Target", "TargetOffset", "TargetRadius", "ThrustP", "TurnD", "TurnP"},
|
|
375
|
-
AlignOrientation = {"AlignType", "Mode", "PrimaryAxisOnly", "ReactionTorqueEnabled", "RigidityEnabled", "MaxAngularVelocity", "MaxTorque", "Responsiveness"},
|
|
376
|
-
AlignPosition = {"ApplyAtCenterOfMass", "MaxForce", "MaxVelocity", "Mode", "Position", "ReactionForceEnabled", "Responsiveness", "RigidityEnabled"},
|
|
377
|
-
BallSocketConstraint = {"LimitsEnabled", "Restitution", "TwistLimitsEnabled", "UpperAngle", "Radius", "TwistLowerAngle", "TwistUpperAngle"},
|
|
378
|
-
HingeConstraint = {"ActuatorType", "AngularSpeed", "AngularVelocity", "LimitsEnabled", "LowerAngle", "MotorMaxAcceleration", "MotorMaxTorque", "Restitution", "ServoMaxTorque", "TargetAngle", "UpperAngle"},
|
|
379
|
-
LinearVelocity = {"MaxForce", "PlaneVelocity", "PrimaryTangentAxis", "RelativeTo", "SecondaryTangentAxis", "VectorVelocity", "VelocityConstraintMode", "LineDirection", "LineVelocity"},
|
|
380
|
-
LineForce = {"ApplyAtCenterOfMass", "InverseSquareLaw", "Magnitude", "MaxForce", "ReactionForceEnabled"},
|
|
381
|
-
RodConstraint = {"Length", "LimitAngle0", "LimitAngle1", "LimitsEnabled", "Thickness"},
|
|
382
|
-
RopeConstraint = {"Length", "Restitution", "Thickness", "WinchEnabled", "WinchForce", "WinchSpeed", "WinchTarget"},
|
|
383
|
-
SpringConstraint = {"Coils", "Damping", "FreeLength", "LimitsEnabled", "MaxForce", "Radius", "Stiffness", "Thickness"},
|
|
384
|
-
Torque = {"RelativeTo", "Torque"},
|
|
385
|
-
VectorForce = {"ApplyAtCenterOfMass", "Force", "RelativeTo"},
|
|
386
|
-
|
|
387
|
-
-- GUI & UI
|
|
388
|
-
ScreenGui = {"DisplayOrder", "IgnoreGuiInset", "ResetOnSpawn", "ZIndexBehavior", "Enabled"},
|
|
389
|
-
BillboardGui = {"Active", "Adornee", "AlwaysOnTop", "Brightness", "ClipsDescendants", "DistanceLowerLimit", "DistanceUpperLimit", "ExtentsOffset", "ExtentsOffsetWorldSpace", "LightInfluence", "MaxDistance", "PlayerToHideFrom", "ResetOnSpawn", "Size", "SizeOffset", "StudsOffset", "StudsOffsetWorldSpace", "ZIndexBehavior"},
|
|
390
|
-
SurfaceGui = {"Active", "Adornee", "AlwaysOnTop", "Brightness", "CanvasSize", "ClipsDescendants", "Face", "LightInfluence", "PixelsPerStud", "ResetOnSpawn", "SizingMode", "ToolPunchThroughDistance", "ZIndexBehavior", "ZOffset"},
|
|
391
|
-
Frame = {"Style", "Active", "AnchorPoint", "BackgroundColor3", "BackgroundTransparency", "BorderColor3", "BorderSizePixel", "ClipsDescendants", "Draggable", "LayoutOrder", "Position", "Rotation", "Selectable", "Size", "SizeConstraint", "Visible", "ZIndex"},
|
|
392
|
-
ScrollingFrame = {"CanvasSize", "CanvasPosition", "ScrollBarThickness", "ScrollBarImageColor3", "ScrollBarImageTransparency", "ScrollingDirection", "ElasticBehavior", "VerticalScrollBarPosition", "HorizontalScrollBarInset", "AutomaticCanvasSize"},
|
|
393
|
-
TextBox = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "PlaceholderText", "ClearTextOnFocus", "MultiLine", "TextEditable", "CursorPosition", "SelectionStart"},
|
|
394
|
-
TextLabel = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "RichText", "TextStrokeColor3", "TextStrokeTransparency", "MaxVisibleGraphemes"},
|
|
395
|
-
TextButton = {"Text", "TextColor3", "TextSize", "Font", "TextTransparency", "TextXAlignment", "TextYAlignment", "TextScaled", "TextWrapped", "RichText", "TextStrokeColor3", "TextStrokeTransparency", "AutoButtonColor", "Modal", "Selected"},
|
|
396
|
-
ImageLabel = {"Image", "ImageColor3", "ImageTransparency", "ScaleType", "SliceCenter", "TileSize", "ImageRectOffset", "ImageRectSize", "ResampleMode"},
|
|
397
|
-
ImageButton = {"Image", "ImageColor3", "ImageTransparency", "ScaleType", "SliceCenter", "TileSize", "ImageRectOffset", "ImageRectSize", "ResampleMode", "AutoButtonColor", "Modal", "HoverImage", "PressedImage"},
|
|
398
|
-
VideoFrame = {"Video", "TimePosition", "Playing", "Looped", "Volume"},
|
|
399
|
-
ViewportFrame = {"Ambient", "LightColor", "LightDirection", "CurrentCamera", "ImageColor3", "ImageTransparency"},
|
|
400
|
-
|
|
401
|
-
-- Layouts & Constraints
|
|
402
|
-
UIAspectRatioConstraint = {"AspectRatio", "AspectType", "DominantAxis"},
|
|
403
|
-
UISizeConstraint = {"MaxSize", "MinSize"},
|
|
404
|
-
UITextSizeConstraint = {"MaxTextSize", "MinTextSize"},
|
|
405
|
-
UICorner = {"CornerRadius"},
|
|
406
|
-
UIStroke = {"Color", "Thickness", "Transparency", "ApplyStrokeMode", "LineJoinMode"},
|
|
407
|
-
UIGradient = {"Color", "Offset", "Rotation", "Transparency"},
|
|
408
|
-
UIPadding = {"PaddingBottom", "PaddingLeft", "PaddingRight", "PaddingTop"},
|
|
409
|
-
UIScale = {"Scale"},
|
|
410
|
-
UIListLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "ItemLineAlignment"},
|
|
411
|
-
UIGridLayout = {"CellPadding", "CellSize", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "StartCorner"},
|
|
412
|
-
UITableLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder"},
|
|
413
|
-
UIPageLayout = {"Padding", "FillDirection", "HorizontalAlignment", "VerticalAlignment", "SortOrder", "Animated", "Circular", "EasingDirection", "EasingStyle", "GamepadInputEnabled", "ScrollWheelInputEnabled", "TouchInputEnabled", "TweenTime"},
|
|
414
|
-
|
|
415
|
-
-- 3D Objects
|
|
416
|
-
Model = {"PrimaryPart", "WorldPivot", "LevelOfDetail"},
|
|
417
|
-
Part = {"Shape", "Material", "Reflectance", "Transparency", "Color", "Size", "Position", "Anchored", "CanCollide", "CanTouch", "CanQuery", "CastShadow", "CollisionGroup", "Locked", "Massless", "RootPriority"},
|
|
418
|
-
MeshPart = {"MeshId", "TextureID", "CollisionFidelity", "RenderFidelity", "DoubleSided", "HasJoints"},
|
|
419
|
-
TrussPart = {"Style"},
|
|
420
|
-
WedgePart = {},
|
|
421
|
-
CornerWedgePart = {},
|
|
422
|
-
SpawnLocation = {"AllowTeamChangeOnTouch", "Duration", "Neutral", "TeamColor", "Enabled"},
|
|
423
|
-
Seat = {"Disabled", "Occupant"},
|
|
424
|
-
VehicleSeat = {"Disabled", "Occupant", "HeadsUpDisplay", "MaxSpeed", "Steer", "SteerFloat", "Throttle", "ThrottleFloat", "Torque", "TurnSpeed"},
|
|
425
|
-
|
|
426
|
-
-- Effects
|
|
427
|
-
ParticleEmitter = {"Color", "LightEmission", "LightInfluence", "Size", "Texture", "Transparency", "ZOffset", "EmissionDirection", "Enabled", "Lifetime", "Rate", "Rotation", "RotSpeed", "Speed", "SpreadAngle", "Acceleration", "Drag", "LockedToPart", "Shape", "ShapeInOut", "ShapeStyle", "Squash", "TimeScale", "VelocityInheritance"},
|
|
428
|
-
Trail = {"Color", "Enabled", "FaceCamera", "Lifetime", "LightEmission", "LightInfluence", "MaxLength", "MinLength", "Texture", "TextureLength", "Transparency", "WidthScale", "Attachment0", "Attachment1"},
|
|
429
|
-
Beam = {"Color", "Enabled", "FaceCamera", "LightEmission", "LightInfluence", "Texture", "TextureLength", "TextureMode", "Transparency", "Width0", "Width1", "ZOffset", "Attachment0", "Attachment1", "CurveSize0", "CurveSize1", "Segments"},
|
|
430
|
-
Fire = {"Color", "Enabled", "Heat", "Size", "SecondaryColor"},
|
|
431
|
-
Smoke = {"Color", "Enabled", "Opacity", "RiseVelocity", "Size"},
|
|
432
|
-
Sparkles = {"SparkleColor", "Enabled"},
|
|
433
|
-
Explosion = {"BlastPressure", "BlastRadius", "DestroyJointRadiusPercent", "ExplosionType", "Position", "Visible"},
|
|
434
|
-
|
|
435
|
-
-- Lighting Effects
|
|
436
|
-
PostEffect = {"Enabled"},
|
|
437
|
-
BlurEffect = {"Size"},
|
|
438
|
-
BloomEffect = {"Intensity", "Size", "Threshold"},
|
|
439
|
-
ColorCorrectionEffect = {"Brightness", "Contrast", "Saturation", "TintColor"},
|
|
440
|
-
SunRaysEffect = {"Intensity", "Spread"},
|
|
441
|
-
Atmosphere = {"Density", "Offset", "Haze", "Color", "Decay", "Glare"},
|
|
442
|
-
Sky = {"SkyboxBk", "SkyboxDn", "SkyboxFt", "SkyboxLf", "SkyboxRt", "SkyboxUp", "SunTextureId", "MoonTextureId", "StarCount", "CelestialBodiesShown"},
|
|
443
|
-
DepthOfFieldEffect = {"FarIntensity", "FocusDistance", "InFocusRadius", "NearIntensity"},
|
|
444
|
-
|
|
445
|
-
-- Animation & Humanoid
|
|
446
|
-
Animation = {"AnimationId"},
|
|
447
|
-
Animator = {},
|
|
448
|
-
AnimationController = {},
|
|
449
|
-
Humanoid = {"Health", "MaxHealth", "WalkSpeed", "JumpPower", "DisplayName", "RigType", "HipHeight", "AutomaticScalingEnabled", "BreakJointsOnDeath", "DisplayDistanceType", "NameOcclusion", "PlatformStand", "Sit", "UseJumpPower"},
|
|
450
|
-
HumanoidDescription = {"Head", "Torso", "LeftArm", "RightArm", "LeftLeg", "RightLeg", "GraphicTShirt", "Pants", "Shirt", "Face", "HeadColor", "TorsoColor", "LeftArmColor", "RightArmColor", "LeftLegColor", "RightLegColor"},
|
|
451
|
-
Bone = {"Transform", "CFrame"},
|
|
452
|
-
|
|
453
|
-
-- Values
|
|
454
|
-
StringValue = {"Value"},
|
|
455
|
-
IntValue = {"Value"},
|
|
456
|
-
NumberValue = {"Value"},
|
|
457
|
-
BoolValue = {"Value"},
|
|
458
|
-
ObjectValue = {"Value"},
|
|
459
|
-
Vector3Value = {"Value"},
|
|
460
|
-
CFrameValue = {"Value"},
|
|
461
|
-
Color3Value = {"Value"},
|
|
462
|
-
BrickColorValue = {"Value"},
|
|
463
|
-
RayValue = {"Value"},
|
|
464
|
-
|
|
465
|
-
-- Misc
|
|
466
|
-
Sound = {"SoundId", "Volume", "PlaybackSpeed", "Looped", "Playing", "TimePosition", "RollOffMaxDistance", "RollOffMinDistance", "RollOffMode"},
|
|
467
|
-
SoundGroup = {"Volume"},
|
|
468
|
-
Tool = {"RequiresHandle", "CanBeDropped", "ToolTip", "Enabled", "Grip", "ManualActivationOnly"},
|
|
469
|
-
Accessory = {"AttachmentPoint"},
|
|
470
|
-
ClickDetector = {"CursorIcon", "MaxActivationDistance"},
|
|
471
|
-
ProximityPrompt = {"ActionText", "ObjectText", "KeyboardKeyCode", "HoldDuration", "MaxActivationDistance", "RequiresLineOfSight", "Exclusivity", "Style", "ClickablePrompt", "GamepadKeyCode", "UIOffset"},
|
|
472
|
-
Highlight = {"Adornee", "FillColor", "FillTransparency", "OutlineColor", "OutlineTransparency", "DepthMode"},
|
|
473
|
-
WrapTarget = {"Stiffness", "DebugMode"},
|
|
474
|
-
WrapLayer = {"Order", "Enabled", "Puffiness", "ShrinkFactor"},
|
|
475
|
-
Configuration = {},
|
|
476
|
-
Folder = {},
|
|
477
|
-
Script = {"Disabled", "RunContext"},
|
|
478
|
-
LocalScript = {"Disabled"},
|
|
479
|
-
ModuleScript = {},
|
|
480
|
-
BindableEvent = {},
|
|
481
|
-
BindableFunction = {},
|
|
482
|
-
RemoteEvent = {},
|
|
483
|
-
RemoteFunction = {},
|
|
484
|
-
|
|
485
|
-
-- Joints
|
|
486
|
-
Weld = {"C0", "C1", "Part0", "Part1"},
|
|
487
|
-
ManualWeld = {"C0", "C1", "Part0", "Part1"},
|
|
488
|
-
Snap = {"C0", "C1", "Part0", "Part1"},
|
|
489
|
-
WeldConstraint = {"Part0", "Part1", "Enabled"},
|
|
490
|
-
Motor = {"C0", "C1", "Part0", "Part1", "MaxVelocity", "DesiredAngle", "CurrentAngle"},
|
|
491
|
-
Motor6D = {"C0", "C1", "Part0", "Part1", "MaxVelocity", "DesiredAngle", "CurrentAngle"},
|
|
492
|
-
Attachment = {"CFrame", "Visible", "Axis", "SecondaryAxis"},
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return Constants
|
|
496
|
-
]]></ProtectedString>
|
|
497
|
-
</Properties>
|
|
498
|
-
</Item>
|
|
499
|
-
<Item class="ModuleScript" referent="RBX4">
|
|
500
|
-
<Properties>
|
|
501
|
-
<string name="Name">Paths</string>
|
|
502
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
503
|
-
local State = require(script.Parent.State)
|
|
504
|
-
local HttpService = game:GetService("HttpService")
|
|
505
|
-
local Paths = {}
|
|
506
|
-
|
|
507
|
-
function Paths.sanitize(name)
|
|
508
|
-
return name:gsub("[\\/:*?\"<>|]", "_")
|
|
509
|
-
end
|
|
510
|
-
|
|
511
|
-
local function resolveRobloxService(robloxPathArray)
|
|
512
|
-
local current = game
|
|
513
|
-
for _, name in ipairs(robloxPathArray) do
|
|
514
|
-
if not current then return nil end
|
|
515
|
-
local child = current:FindFirstChild(name)
|
|
516
|
-
if not child then
|
|
517
|
-
local success, service = pcall(function() return game:GetService(name) end)
|
|
518
|
-
if success and service then
|
|
519
|
-
child = service
|
|
520
|
-
end
|
|
521
|
-
end
|
|
522
|
-
current = child
|
|
523
|
-
end
|
|
524
|
-
return current
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
function Paths.getPath(inst)
|
|
528
|
-
if #State.mounts == 0 then return nil end
|
|
529
|
-
|
|
530
|
-
for _, mount in ipairs(State.mounts) do
|
|
531
|
-
local root = resolveRobloxService(mount.robloxPath)
|
|
532
|
-
if root and (inst == root or inst:IsDescendantOf(root)) then
|
|
533
|
-
local relative = {}
|
|
534
|
-
local current = inst
|
|
535
|
-
while current ~= root do
|
|
536
|
-
table.insert(relative, 1, (Paths.sanitize(current.Name)))
|
|
537
|
-
current = current.Parent
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
local fullPath = mount.filePath
|
|
541
|
-
if #relative > 0 then
|
|
542
|
-
fullPath = fullPath .. "/" .. table.concat(relative, "/")
|
|
543
|
-
end
|
|
544
|
-
return fullPath
|
|
545
|
-
end
|
|
546
|
-
end
|
|
547
|
-
return nil
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
function Paths.findInstanceByPath(path)
|
|
551
|
-
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$")
|
|
552
|
-
local cleanPath = path
|
|
553
|
-
|
|
554
|
-
if isInit then
|
|
555
|
-
cleanPath = path:gsub("/init%.server%.luau$", ""):gsub("/init%.client%.luau$", ""):gsub("/init%.luau$", ""):gsub("/init%.server%.lua$", ""):gsub("/init%.client%.lua$", ""):gsub("/init%.lua$", "")
|
|
556
|
-
else
|
|
557
|
-
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("%.model%.json$", ""):gsub("%.json$", ""):gsub("%.rbxmx$", ""):gsub("%.rbxm$", ""):gsub("%.csv$", "")
|
|
558
|
-
end
|
|
559
|
-
|
|
560
|
-
for _, mount in ipairs(State.mounts) do
|
|
561
|
-
local mountPath = mount.filePath
|
|
562
|
-
if not mountPath then continue end -- Skip virtual nodes
|
|
563
|
-
-- Check if path starts with mountPath
|
|
564
|
-
if cleanPath == mountPath or cleanPath:sub(1, #mountPath + 1) == (mountPath .. "/") then
|
|
565
|
-
local relativeStr = cleanPath:sub(#mountPath + 2)
|
|
566
|
-
local parts = (relativeStr == "") and {} or relativeStr:split("/")
|
|
567
|
-
|
|
568
|
-
local current = resolveRobloxService(mount.robloxPath)
|
|
569
|
-
if not current then return nil end
|
|
570
|
-
|
|
571
|
-
for _, name in ipairs(parts) do
|
|
572
|
-
current = current:FindFirstChild(name)
|
|
573
|
-
if not current then return nil end
|
|
574
|
-
end
|
|
575
|
-
return current
|
|
576
|
-
end
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
return nil
|
|
580
|
-
end
|
|
581
|
-
|
|
582
|
-
function Paths.createInstanceByPath(path, content)
|
|
583
|
-
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$")
|
|
584
|
-
local cleanPath = path
|
|
585
|
-
|
|
586
|
-
if isInit then
|
|
587
|
-
cleanPath = path:gsub("/init%.server%.luau$", ""):gsub("/init%.client%.luau$", ""):gsub("/init%.luau$", ""):gsub("/init%.server%.lua$", ""):gsub("/init%.client%.lua$", ""):gsub("/init%.lua$", "")
|
|
588
|
-
else
|
|
589
|
-
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$", "")
|
|
590
|
-
end
|
|
591
|
-
|
|
592
|
-
for _, mount in ipairs(State.mounts) do
|
|
593
|
-
local mountPath = mount.filePath
|
|
594
|
-
if not mountPath then continue end
|
|
595
|
-
if cleanPath == mountPath or cleanPath:sub(1, #mountPath + 1) == (mountPath .. "/") then
|
|
596
|
-
local relativeStr = cleanPath:sub(#mountPath + 2)
|
|
597
|
-
local parts = (relativeStr == "") and {} or relativeStr:split("/")
|
|
598
|
-
|
|
599
|
-
local current = resolveRobloxService(mount.robloxPath)
|
|
600
|
-
if not current then return nil end
|
|
601
|
-
|
|
602
|
-
for i, name in ipairs(parts) do
|
|
603
|
-
local nextInst = current:FindFirstChild(name)
|
|
604
|
-
local isLast = (i == #parts)
|
|
605
|
-
|
|
606
|
-
if isLast then
|
|
607
|
-
-- Determine desired class
|
|
608
|
-
local desiredClass = "Folder"
|
|
609
|
-
if isInit then
|
|
610
|
-
if path:match("%.server%.luau$") or path:match("%.server%.lua$") then desiredClass = "Script"
|
|
611
|
-
elseif path:match("%.client%.luau$") or path:match("%.client%.lua$") then desiredClass = "LocalScript"
|
|
612
|
-
elseif path:match("%.luau$") or path:match("%.lua$") then desiredClass = "ModuleScript"
|
|
613
|
-
end
|
|
614
|
-
else
|
|
615
|
-
if path:match("%.server%.luau$") or path:match("%.server%.lua$") then desiredClass = "Script"
|
|
616
|
-
elseif path:match("%.client%.luau$") or path:match("%.client%.lua$") then desiredClass = "LocalScript"
|
|
617
|
-
elseif path:match("%.luau$") or path:match("%.lua$") then desiredClass = "ModuleScript"
|
|
618
|
-
elseif path:match("%.txt$") then desiredClass = "StringValue"
|
|
619
|
-
elseif path:match("%.csv$") then desiredClass = "LocalizationTable"
|
|
620
|
-
elseif path:match("%.rbxmx$") or path:match("%.rbxm$") then desiredClass = "Folder" -- Placeholder, will be replaced by applyUpdate
|
|
621
|
-
elseif path:match("%.json$") and not path:match("%.meta%.json$") and not path:match("%.model%.json$") then desiredClass = "ModuleScript"
|
|
622
|
-
elseif path:match("%.model%.json$") or path:match("%.meta%.json$") then
|
|
623
|
-
local success, data = pcall(function() return HttpService:JSONDecode(content) end)
|
|
624
|
-
if success then desiredClass = data.className or data.ClassName or data.type or "Folder" end
|
|
625
|
-
end
|
|
626
|
-
end
|
|
627
|
-
|
|
628
|
-
if nextInst then
|
|
629
|
-
if nextInst.ClassName ~= desiredClass then
|
|
630
|
-
-- Convert
|
|
631
|
-
local newInst = Instance.new(desiredClass)
|
|
632
|
-
newInst.Name = name
|
|
633
|
-
newInst.Parent = current
|
|
634
|
-
for _, child in ipairs(nextInst:GetChildren()) do
|
|
635
|
-
child.Parent = newInst
|
|
636
|
-
end
|
|
637
|
-
nextInst:Destroy()
|
|
638
|
-
nextInst = newInst
|
|
639
|
-
end
|
|
640
|
-
else
|
|
641
|
-
local success, newInst = pcall(function() return Instance.new(desiredClass) end)
|
|
642
|
-
if success and newInst then
|
|
643
|
-
newInst.Name = name
|
|
644
|
-
newInst.Parent = current
|
|
645
|
-
nextInst = newInst
|
|
646
|
-
end
|
|
647
|
-
end
|
|
648
|
-
else
|
|
649
|
-
-- Intermediate node
|
|
650
|
-
if not nextInst then
|
|
651
|
-
local success, newInst = pcall(function() return Instance.new("Folder") end)
|
|
652
|
-
if success and newInst then
|
|
653
|
-
newInst.Name = name
|
|
654
|
-
newInst.Parent = current
|
|
655
|
-
nextInst = newInst
|
|
656
|
-
end
|
|
657
|
-
end
|
|
658
|
-
end
|
|
659
|
-
current = nextInst
|
|
660
|
-
end
|
|
661
|
-
return current
|
|
662
|
-
end
|
|
663
|
-
end
|
|
664
|
-
return nil
|
|
665
|
-
end
|
|
666
|
-
|
|
667
|
-
return Paths
|
|
668
|
-
]]></ProtectedString>
|
|
669
|
-
</Properties>
|
|
670
|
-
</Item>
|
|
671
|
-
<Item class="ModuleScript" referent="RBX5">
|
|
672
|
-
<Properties>
|
|
673
|
-
<string name="Name">Serialization</string>
|
|
674
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
675
|
-
local HttpService = game:GetService("HttpService")
|
|
676
|
-
local Constants = require(script.Parent.Constants)
|
|
677
|
-
|
|
678
|
-
local Serialization = {}
|
|
679
|
-
|
|
680
|
-
function Serialization.serializeValue(val)
|
|
681
|
-
local t = typeof(val)
|
|
682
|
-
if t == "Vector3" then return {val.X, val.Y, val.Z}
|
|
683
|
-
elseif t == "Vector2" then return {val.X, val.Y}
|
|
684
|
-
elseif t == "Color3" then return {val.R, val.G, val.B}
|
|
685
|
-
elseif t == "CFrame" then return {val:GetComponents()}
|
|
686
|
-
elseif t == "UDim2" then return {val.X.Scale, val.X.Offset, val.Y.Scale, val.Y.Offset}
|
|
687
|
-
elseif t == "UDim" then return {val.Scale, val.Offset}
|
|
688
|
-
elseif t == "Rect" then return {val.Min.X, val.Min.Y, val.Max.X, val.Max.Y}
|
|
689
|
-
elseif t == "NumberRange" then return {val.Min, val.Max}
|
|
690
|
-
elseif t == "EnumItem" then return val.Name
|
|
691
|
-
elseif t == "Instance" then return val.Name -- Reference by name (weak)
|
|
692
|
-
elseif t == "ColorSequence" then
|
|
693
|
-
local kps = {}
|
|
694
|
-
for _, kp in ipairs(val.Keypoints) do table.insert(kps, {kp.Time, {kp.Value.R, kp.Value.G, kp.Value.B}}) end
|
|
695
|
-
return {type="ColorSequence", keypoints=kps}
|
|
696
|
-
elseif t == "NumberSequence" then
|
|
697
|
-
local kps = {}
|
|
698
|
-
for _, kp in ipairs(val.Keypoints) do table.insert(kps, {kp.Time, kp.Value, kp.Envelope}) end
|
|
699
|
-
return {type="NumberSequence", keypoints=kps}
|
|
700
|
-
elseif t == "BrickColor" then
|
|
701
|
-
return val.Name
|
|
702
|
-
else return val end
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
function Serialization.deserializeValue(val, targetType, context)
|
|
706
|
-
if targetType == "Color3" and type(val) == "table" then
|
|
707
|
-
return Color3.new(val[1], val[2], val[3])
|
|
708
|
-
elseif targetType == "Vector3" and type(val) == "table" then
|
|
709
|
-
return Vector3.new(val[1], val[2], val[3])
|
|
710
|
-
elseif targetType == "Vector2" and type(val) == "table" then
|
|
711
|
-
return Vector2.new(val[1], val[2])
|
|
712
|
-
elseif targetType == "UDim2" and type(val) == "table" then
|
|
713
|
-
return UDim2.new(val[1], val[2], val[3], val[4])
|
|
714
|
-
elseif targetType == "UDim" and type(val) == "table" then
|
|
715
|
-
return UDim.new(val[1], val[2])
|
|
716
|
-
elseif targetType == "Rect" and type(val) == "table" then
|
|
717
|
-
return Rect.new(val[1], val[2], val[3], val[4])
|
|
718
|
-
elseif targetType == "NumberRange" and type(val) == "table" then
|
|
719
|
-
return NumberRange.new(val[1], val[2])
|
|
720
|
-
elseif targetType == "CFrame" and type(val) == "table" then
|
|
721
|
-
return CFrame.new(unpack(val))
|
|
722
|
-
elseif targetType == "BrickColor" then
|
|
723
|
-
if type(val) == "string" then return BrickColor.new(val)
|
|
724
|
-
elseif type(val) == "number" then return BrickColor.new(val) end
|
|
725
|
-
elseif targetType == "EnumItem" and type(val) == "string" then
|
|
726
|
-
if context and typeof(context) == "Enum" then
|
|
727
|
-
local success, item = pcall(function() return context[val] end)
|
|
728
|
-
if success then return item end
|
|
729
|
-
end
|
|
730
|
-
return val
|
|
731
|
-
else
|
|
732
|
-
return val
|
|
733
|
-
end
|
|
734
|
-
end
|
|
735
|
-
|
|
736
|
-
function Serialization.getProperties(inst)
|
|
737
|
-
local props = {
|
|
738
|
-
className = inst.ClassName,
|
|
739
|
-
name = inst.Name,
|
|
740
|
-
properties = {},
|
|
741
|
-
attributes = {},
|
|
742
|
-
tags = game:GetService("CollectionService"):GetTags(inst)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
-- Serialize Attributes
|
|
746
|
-
for k, v in pairs(inst:GetAttributes()) do
|
|
747
|
-
props.attributes[k] = Serialization.serializeValue(v)
|
|
748
|
-
end
|
|
749
|
-
|
|
750
|
-
-- Table-driven property serialization
|
|
751
|
-
for className, propList in pairs(Constants.PROPERTY_MAP) do
|
|
752
|
-
if inst:IsA(className) then
|
|
753
|
-
for _, propName in ipairs(propList) do
|
|
754
|
-
pcall(function()
|
|
755
|
-
props.properties[propName] = Serialization.serializeValue(inst[propName])
|
|
756
|
-
end)
|
|
757
|
-
end
|
|
758
|
-
end
|
|
759
|
-
end
|
|
760
|
-
|
|
761
|
-
return props
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
function Serialization.getExtension(inst)
|
|
765
|
-
local isInit = (inst:IsA("Script") or inst:IsA("LocalScript") or inst:IsA("ModuleScript")) and #inst:GetChildren() > 0
|
|
766
|
-
|
|
767
|
-
if inst:IsA("Script") then return isInit and "/init.server.luau" or ".server.luau"
|
|
768
|
-
elseif inst:IsA("LocalScript") then return isInit and "/init.client.luau" or ".client.luau"
|
|
769
|
-
elseif inst:IsA("ModuleScript") then return isInit and "/init.luau" or ".luau"
|
|
770
|
-
elseif inst:IsA("StringValue") then return ".txt"
|
|
771
|
-
else return "/init.meta.json" end
|
|
772
|
-
end
|
|
773
|
-
|
|
774
|
-
function Serialization.getContent(inst)
|
|
775
|
-
if inst:IsA("Script") or inst:IsA("LocalScript") or inst:IsA("ModuleScript") then
|
|
776
|
-
return inst.Source
|
|
777
|
-
elseif inst:IsA("StringValue") then
|
|
778
|
-
return inst.Value
|
|
779
|
-
else
|
|
780
|
-
return HttpService:JSONEncode(Serialization.getProperties(inst))
|
|
781
|
-
end
|
|
782
|
-
end
|
|
783
|
-
|
|
784
|
-
function Serialization.getInfo(inst)
|
|
785
|
-
return Serialization.getExtension(inst), Serialization.getContent(inst)
|
|
786
|
-
end
|
|
787
|
-
|
|
788
|
-
return Serialization
|
|
789
|
-
]]></ProtectedString>
|
|
790
|
-
</Properties>
|
|
791
|
-
</Item>
|
|
792
|
-
<Item class="ModuleScript" referent="RBX6">
|
|
793
|
-
<Properties>
|
|
794
|
-
<string name="Name">Signal</string>
|
|
795
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
796
|
-
local Signal = {}
|
|
797
|
-
Signal.__index = Signal
|
|
798
|
-
|
|
799
|
-
export type Connection = {
|
|
800
|
-
Disconnect: (self: Connection) -> ()
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
export type Signal<T...> = {
|
|
804
|
-
Connect: (self: Signal<T...>, callback: (T...) -> ()) -> Connection,
|
|
805
|
-
Fire: (self: Signal<T...>, T...) -> ()
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function Signal.new()
|
|
809
|
-
local self = setmetatable({}, Signal)
|
|
810
|
-
self._listeners = {}
|
|
811
|
-
return self
|
|
812
|
-
end
|
|
813
|
-
|
|
814
|
-
function Signal:Connect(callback)
|
|
815
|
-
local listener = {
|
|
816
|
-
callback = callback,
|
|
817
|
-
isConnected = true
|
|
818
|
-
}
|
|
819
|
-
table.insert(self._listeners, listener)
|
|
820
|
-
|
|
821
|
-
return {
|
|
822
|
-
Disconnect = function()
|
|
823
|
-
listener.isConnected = false
|
|
824
|
-
end
|
|
825
|
-
}
|
|
826
|
-
end
|
|
827
|
-
|
|
828
|
-
function Signal:Fire(...)
|
|
829
|
-
local newListeners = {}
|
|
830
|
-
for _, listener in ipairs(self._listeners) do
|
|
831
|
-
if listener.isConnected then
|
|
832
|
-
task.spawn(listener.callback, ...)
|
|
833
|
-
table.insert(newListeners, listener)
|
|
834
|
-
end
|
|
835
|
-
end
|
|
836
|
-
self._listeners = newListeners
|
|
837
|
-
end
|
|
838
|
-
|
|
839
|
-
return Signal
|
|
840
|
-
]]></ProtectedString>
|
|
841
|
-
</Properties>
|
|
842
|
-
</Item>
|
|
843
|
-
<Item class="ModuleScript" referent="RBX7">
|
|
844
|
-
<Properties>
|
|
845
|
-
<string name="Name">State</string>
|
|
846
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
847
|
-
local State = {}
|
|
848
|
-
|
|
849
|
-
export type Settings = {
|
|
850
|
-
port: number,
|
|
851
|
-
syncInterval: number
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
State.isConnected = false
|
|
855
|
-
State.isAutoSync = false
|
|
856
|
-
State.isAutoPull = false
|
|
857
|
-
State.trackedInstances = {}
|
|
858
|
-
State.pathCache = {}
|
|
859
|
-
State.connections = {}
|
|
860
|
-
State.mounts = {} -- Stores project config from server
|
|
861
|
-
State.settings = {
|
|
862
|
-
port = 3456,
|
|
863
|
-
syncInterval = 2
|
|
864
|
-
} :: Settings
|
|
865
|
-
|
|
866
|
-
function State.load(plugin: Plugin)
|
|
867
|
-
if not State.settings then
|
|
868
|
-
State.settings = {
|
|
869
|
-
port = 3456,
|
|
870
|
-
syncInterval = 2
|
|
871
|
-
}
|
|
872
|
-
end
|
|
873
|
-
local port = plugin:GetSetting("Roport_Port")
|
|
874
|
-
local interval = plugin:GetSetting("Roport_Interval")
|
|
875
|
-
if port then State.settings.port = port end
|
|
876
|
-
if interval then State.settings.syncInterval = interval end
|
|
877
|
-
end
|
|
878
|
-
|
|
879
|
-
function State.save(plugin: Plugin)
|
|
880
|
-
plugin:SetSetting("Roport_Port", State.settings.port)
|
|
881
|
-
plugin:SetSetting("Roport_Interval", State.settings.syncInterval)
|
|
882
|
-
end
|
|
883
|
-
|
|
884
|
-
return State
|
|
885
|
-
]]></ProtectedString>
|
|
886
|
-
</Properties>
|
|
887
|
-
</Item>
|
|
888
|
-
<Item class="ModuleScript" referent="RBX8">
|
|
889
|
-
<Properties>
|
|
890
|
-
<string name="Name">Sync</string>
|
|
891
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
892
|
-
local HttpService = game:GetService("HttpService")
|
|
893
|
-
local State = require(script.Parent.State)
|
|
894
|
-
local Constants = require(script.Parent.Constants)
|
|
895
|
-
local Paths = require(script.Parent.Paths)
|
|
896
|
-
local Serialization = require(script.Parent.Serialization)
|
|
897
|
-
local Signal = require(script.Parent.Signal)
|
|
898
|
-
|
|
899
|
-
local Sync = {}
|
|
900
|
-
Sync.OnStatusChanged = Signal.new()
|
|
901
|
-
Sync.OnSyncCompleted = Signal.new()
|
|
902
|
-
Sync.plugin = nil -- Injected by init.server.lua
|
|
903
|
-
|
|
904
|
-
function Sync.getUrl(endpoint)
|
|
905
|
-
local port = 3456
|
|
906
|
-
if State and State.settings and State.settings.port then
|
|
907
|
-
port = State.settings.port
|
|
908
|
-
end
|
|
909
|
-
return "http://127.0.0.1:" .. port .. "/" .. endpoint
|
|
910
|
-
end
|
|
911
|
-
|
|
912
|
-
local function resolveRobloxService(robloxPathArray)
|
|
913
|
-
local current = game
|
|
914
|
-
for _, name in ipairs(robloxPathArray) do
|
|
915
|
-
if not current then return nil end
|
|
916
|
-
local child = current:FindFirstChild(name)
|
|
917
|
-
if not child then
|
|
918
|
-
local success, service = pcall(function() return game:GetService(name) end)
|
|
919
|
-
if success and service then
|
|
920
|
-
child = service
|
|
921
|
-
end
|
|
922
|
-
end
|
|
923
|
-
current = child
|
|
924
|
-
end
|
|
925
|
-
return current
|
|
926
|
-
end
|
|
927
|
-
|
|
928
|
-
-- local SERVER_URL_PING = Sync.getUrl("ping")
|
|
929
|
-
-- local SERVER_URL_WRITE = Sync.getUrl("write")
|
|
930
|
-
-- local SERVER_URL_BATCH = Sync.getUrl("batch")
|
|
931
|
-
-- local SERVER_URL_DELETE = Sync.getUrl("delete")
|
|
932
|
-
-- local SERVER_URL_POLL = Sync.getUrl("poll")
|
|
933
|
-
|
|
934
|
-
function Sync.registerObject(inst, fullPath)
|
|
935
|
-
if State.pathCache[inst] == fullPath then return end
|
|
936
|
-
State.pathCache[inst] = fullPath
|
|
937
|
-
|
|
938
|
-
-- Clear old connections
|
|
939
|
-
if State.connections[inst] then
|
|
940
|
-
for _, c in pairs(State.connections[inst]) do c:Disconnect() end
|
|
941
|
-
end
|
|
942
|
-
State.connections[inst] = {}
|
|
943
|
-
|
|
944
|
-
-- 1. Name Changed (Rename)
|
|
945
|
-
local nameCon = inst:GetPropertyChangedSignal("Name"):Connect(function()
|
|
946
|
-
if not State.isAutoSync then return end
|
|
947
|
-
local oldPath = State.pathCache[inst]
|
|
948
|
-
local newBasePath = Paths.getPath(inst)
|
|
949
|
-
if not newBasePath then return end -- Moved to invalid location
|
|
950
|
-
|
|
951
|
-
local newPath = newBasePath .. Serialization.getExtension(inst)
|
|
952
|
-
|
|
953
|
-
if oldPath and newPath and oldPath ~= newPath then
|
|
954
|
-
-- Use Move endpoint to preserve children
|
|
955
|
-
pcall(function()
|
|
956
|
-
HttpService:PostAsync(Sync.getUrl("move"), HttpService:JSONEncode({
|
|
957
|
-
oldPath = oldPath,
|
|
958
|
-
newPath = newPath
|
|
959
|
-
}))
|
|
960
|
-
end)
|
|
961
|
-
-- Update cache
|
|
962
|
-
State.pathCache[inst] = newPath
|
|
963
|
-
-- We also need to update cache for all descendants because their paths changed too!
|
|
964
|
-
-- But Paths.getPath calculates dynamically, so we just need to update the cache map.
|
|
965
|
-
for _, desc in ipairs(inst:GetDescendants()) do
|
|
966
|
-
local dPath = Paths.getPath(desc)
|
|
967
|
-
if dPath then
|
|
968
|
-
local dExt = Serialization.getExtension(desc)
|
|
969
|
-
State.pathCache[desc] = dPath .. dExt
|
|
970
|
-
end
|
|
971
|
-
end
|
|
972
|
-
end
|
|
973
|
-
end)
|
|
974
|
-
-- Use direct assignment to avoid table.insert issues
|
|
975
|
-
local cons = State.connections[inst]
|
|
976
|
-
cons[#cons+1] = nameCon
|
|
977
|
-
|
|
978
|
-
-- 2. Ancestry Changed (Move or Delete)
|
|
979
|
-
local ancestryCon = inst.AncestryChanged:Connect(function(_, parent)
|
|
980
|
-
if not State.isAutoSync then return end
|
|
981
|
-
|
|
982
|
-
if parent == nil then
|
|
983
|
-
-- Deleted
|
|
984
|
-
local oldPath = State.pathCache[inst]
|
|
985
|
-
if oldPath then
|
|
986
|
-
pcall(function()
|
|
987
|
-
HttpService:PostAsync(Sync.getUrl("delete"), HttpService:JSONEncode({files={oldPath}}))
|
|
988
|
-
end)
|
|
989
|
-
State.pathCache[inst] = nil
|
|
990
|
-
if State.connections[inst] then
|
|
991
|
-
for _, c in pairs(State.connections[inst]) do c:Disconnect() end
|
|
992
|
-
State.connections[inst] = nil
|
|
993
|
-
end
|
|
994
|
-
end
|
|
995
|
-
else
|
|
996
|
-
-- Moved
|
|
997
|
-
local oldPath = State.pathCache[inst]
|
|
998
|
-
local newBasePath = Paths.getPath(inst)
|
|
999
|
-
if not newBasePath then return end
|
|
1000
|
-
|
|
1001
|
-
local newPath = newBasePath .. Serialization.getExtension(inst)
|
|
1002
|
-
|
|
1003
|
-
if oldPath and newPath and oldPath ~= newPath then
|
|
1004
|
-
-- Use Move endpoint
|
|
1005
|
-
pcall(function()
|
|
1006
|
-
HttpService:PostAsync(Sync.getUrl("move"), HttpService:JSONEncode({
|
|
1007
|
-
oldPath = oldPath,
|
|
1008
|
-
newPath = newPath
|
|
1009
|
-
}))
|
|
1010
|
-
end)
|
|
1011
|
-
State.pathCache[inst] = newPath
|
|
1012
|
-
-- Update descendants cache
|
|
1013
|
-
for _, desc in ipairs(inst:GetDescendants()) do
|
|
1014
|
-
local dPath = Paths.getPath(desc)
|
|
1015
|
-
if dPath then
|
|
1016
|
-
local dExt = Serialization.getExtension(desc)
|
|
1017
|
-
State.pathCache[desc] = dPath .. dExt
|
|
1018
|
-
end
|
|
1019
|
-
end
|
|
1020
|
-
end
|
|
1021
|
-
end
|
|
1022
|
-
end)
|
|
1023
|
-
cons[#cons+1] = ancestryCon
|
|
1024
|
-
|
|
1025
|
-
-- 3. Property Changed (Auto-Push)
|
|
1026
|
-
for className, propList in pairs(Constants.PROPERTY_MAP) do
|
|
1027
|
-
if inst:IsA(className) then
|
|
1028
|
-
for _, propName in ipairs(propList) do
|
|
1029
|
-
if propName == "Name" then continue end -- Handled above
|
|
1030
|
-
|
|
1031
|
-
local success, con = pcall(function()
|
|
1032
|
-
return inst:GetPropertyChangedSignal(propName):Connect(function()
|
|
1033
|
-
if not State.isAutoSync then return end
|
|
1034
|
-
-- Debounce could be added here, but for now direct sync ensures responsiveness
|
|
1035
|
-
Sync.syncInstance(inst)
|
|
1036
|
-
end)
|
|
1037
|
-
end)
|
|
1038
|
-
|
|
1039
|
-
if success and con then
|
|
1040
|
-
cons[#cons+1] = con
|
|
1041
|
-
end
|
|
1042
|
-
end
|
|
1043
|
-
end
|
|
1044
|
-
end
|
|
1045
|
-
end
|
|
1046
|
-
|
|
1047
|
-
function Sync.syncInstance(inst)
|
|
1048
|
-
local path = Paths.getPath(inst)
|
|
1049
|
-
if not path then return end
|
|
1050
|
-
|
|
1051
|
-
local ext = Serialization.getExtension(inst)
|
|
1052
|
-
local content = Serialization.getContent(inst)
|
|
1053
|
-
local fullPath = path..ext
|
|
1054
|
-
|
|
1055
|
-
-- Register for tracking
|
|
1056
|
-
Sync.registerObject(inst, fullPath)
|
|
1057
|
-
|
|
1058
|
-
pcall(function()
|
|
1059
|
-
HttpService:PostAsync(Sync.getUrl("batch"), HttpService:JSONEncode({
|
|
1060
|
-
files = {{filePath = fullPath, content = content}}
|
|
1061
|
-
}))
|
|
1062
|
-
end)
|
|
1063
|
-
end
|
|
1064
|
-
|
|
1065
|
-
function Sync.applyProjectConfig()
|
|
1066
|
-
if not State.mounts then return end
|
|
1067
|
-
print("Roport: Applying project configuration...")
|
|
1068
|
-
|
|
1069
|
-
for _, node in ipairs(State.mounts) do
|
|
1070
|
-
local inst = resolveRobloxService(node.robloxPath)
|
|
1071
|
-
|
|
1072
|
-
-- 1. Create Instance if missing (Virtual Nodes)
|
|
1073
|
-
if not inst and node.className then
|
|
1074
|
-
-- Find parent
|
|
1075
|
-
local parentPath = {unpack(node.robloxPath)}
|
|
1076
|
-
local name = table.remove(parentPath, #parentPath)
|
|
1077
|
-
local parent = resolveRobloxService(parentPath)
|
|
1078
|
-
|
|
1079
|
-
if parent then
|
|
1080
|
-
local success, newInst = pcall(function() return Instance.new(node.className) end)
|
|
1081
|
-
if success and newInst then
|
|
1082
|
-
newInst.Name = name
|
|
1083
|
-
newInst.Parent = parent
|
|
1084
|
-
inst = newInst
|
|
1085
|
-
print("Roport: Created virtual instance " .. name .. " (" .. node.className .. ")")
|
|
1086
|
-
end
|
|
1087
|
-
end
|
|
1088
|
-
end
|
|
1089
|
-
|
|
1090
|
-
if inst then
|
|
1091
|
-
-- 2. Apply Properties
|
|
1092
|
-
if node.properties then
|
|
1093
|
-
for prop, val in pairs(node.properties) do
|
|
1094
|
-
pcall(function()
|
|
1095
|
-
local currentVal = inst[prop]
|
|
1096
|
-
local targetType = typeof(currentVal)
|
|
1097
|
-
local context = nil
|
|
1098
|
-
if targetType == "EnumItem" then
|
|
1099
|
-
context = currentVal.EnumType
|
|
1100
|
-
end
|
|
1101
|
-
inst[prop] = Serialization.deserializeValue(val, targetType, context)
|
|
1102
|
-
end)
|
|
1103
|
-
end
|
|
1104
|
-
end
|
|
1105
|
-
|
|
1106
|
-
-- 3. Handle $ignoreUnknownInstances (Stub for now)
|
|
1107
|
-
if node.ignoreUnknownInstances then
|
|
1108
|
-
-- TODO: Implement deletion logic for unmanaged instances
|
|
1109
|
-
end
|
|
1110
|
-
end
|
|
1111
|
-
end
|
|
1112
|
-
end
|
|
1113
|
-
|
|
1114
|
-
function Sync.syncAll()
|
|
1115
|
-
if not State.isConnected then
|
|
1116
|
-
Sync.OnStatusChanged:Fire("Not connected!", "error")
|
|
1117
|
-
return
|
|
1118
|
-
end
|
|
1119
|
-
Sync.OnStatusChanged:Fire("Starting Batch Sync...", "info")
|
|
1120
|
-
|
|
1121
|
-
local batch = {}
|
|
1122
|
-
local count = 0
|
|
1123
|
-
|
|
1124
|
-
-- Iterate through all mount points
|
|
1125
|
-
for _, mount in ipairs(State.mounts) do
|
|
1126
|
-
local root = resolveRobloxService(mount.robloxPath)
|
|
1127
|
-
if root then
|
|
1128
|
-
-- Sync descendants
|
|
1129
|
-
for _, d in ipairs(root:GetDescendants()) do
|
|
1130
|
-
-- Skip Terrain and Camera
|
|
1131
|
-
if d:IsA("Terrain") or d:IsA("Camera") then continue end
|
|
1132
|
-
if d == script or d == Sync.plugin then continue end
|
|
1133
|
-
|
|
1134
|
-
local path = Paths.getPath(d)
|
|
1135
|
-
if path then
|
|
1136
|
-
local ext, content = Serialization.getInfo(d)
|
|
1137
|
-
if ext and content then
|
|
1138
|
-
local fullPath = path..ext
|
|
1139
|
-
table.insert(batch, {filePath = fullPath, content = content})
|
|
1140
|
-
Sync.registerObject(d, fullPath)
|
|
1141
|
-
count = count + 1
|
|
1142
|
-
end
|
|
1143
|
-
end
|
|
1144
|
-
end
|
|
1145
|
-
end
|
|
1146
|
-
end
|
|
1147
|
-
|
|
1148
|
-
print("Roport: Found " .. count .. " items to sync.")
|
|
1149
|
-
if count > 0 then
|
|
1150
|
-
print("Roport: First item path: " .. batch[1].filePath)
|
|
1151
|
-
end
|
|
1152
|
-
|
|
1153
|
-
-- Send
|
|
1154
|
-
task.spawn(function()
|
|
1155
|
-
local chunkSize = 50
|
|
1156
|
-
for i = 1, #batch, chunkSize do
|
|
1157
|
-
local chunk = {}
|
|
1158
|
-
for j = i, math.min(i + chunkSize - 1, #batch) do table.insert(chunk, batch[j]) end
|
|
1159
|
-
local success, err = pcall(function()
|
|
1160
|
-
HttpService:PostAsync(Sync.getUrl("batch"), HttpService:JSONEncode({files=chunk}))
|
|
1161
|
-
end)
|
|
1162
|
-
if not success then
|
|
1163
|
-
warn("Roport: Batch sync failed: " .. tostring(err))
|
|
1164
|
-
end
|
|
1165
|
-
task.wait(0.1)
|
|
1166
|
-
end
|
|
1167
|
-
Sync.OnStatusChanged:Fire("Sync Complete!", "success")
|
|
1168
|
-
Sync.OnSyncCompleted:Fire(count)
|
|
1169
|
-
end)
|
|
1170
|
-
end
|
|
1171
|
-
|
|
1172
|
-
function Sync.logError(message, stack)
|
|
1173
|
-
pcall(function()
|
|
1174
|
-
HttpService:PostAsync(Sync.getUrl("log"), HttpService:JSONEncode({
|
|
1175
|
-
message = message,
|
|
1176
|
-
stack = stack,
|
|
1177
|
-
timestamp = os.time()
|
|
1178
|
-
}))
|
|
1179
|
-
end)
|
|
1180
|
-
end
|
|
1181
|
-
|
|
1182
|
-
function Sync.checkTemplates(inst, filePath)
|
|
1183
|
-
if not inst then return end
|
|
1184
|
-
|
|
1185
|
-
-- 1. Prefab Entity Template
|
|
1186
|
-
if filePath:match("src/prefabs/[^/]+$") then -- Direct child of prefabs (the prefab folder itself)
|
|
1187
|
-
if not inst:FindFirstChild("entity") then
|
|
1188
|
-
local script = Instance.new("Script")
|
|
1189
|
-
script.Name = "entity"
|
|
1190
|
-
script.Source = [[--!strict
|
|
1191
|
-
local BaseEntity = require(game.ReplicatedStorage.entities.BaseEntity)
|
|
1192
|
-
-- local Enemy = require(game.ReplicatedStorage.entities.Enemy)
|
|
1193
|
-
|
|
1194
|
-
-- To use a specific entity type, require it and use .new(script.Parent)
|
|
1195
|
-
local entity = BaseEntity.new(script.Parent)
|
|
1196
|
-
entity:Init()
|
|
1197
|
-
|
|
1198
|
-
while true do
|
|
1199
|
-
entity:Update(task.wait())
|
|
1200
|
-
end
|
|
1201
|
-
]]
|
|
1202
|
-
script.Parent = inst
|
|
1203
|
-
print("Roport: Auto-inserted entity template for " .. inst.Name)
|
|
1204
|
-
Sync.syncInstance(script) -- Sync back to file system
|
|
1205
|
-
end
|
|
1206
|
-
end
|
|
1207
|
-
|
|
1208
|
-
-- 2. UI Controller Template
|
|
1209
|
-
if inst:IsA("ScreenGui") and filePath:match("src/interface/") then
|
|
1210
|
-
if not inst:FindFirstChild("ui_controller") then
|
|
1211
|
-
local script = Instance.new("LocalScript")
|
|
1212
|
-
script.Name = "ui_controller"
|
|
1213
|
-
script.Source = [[--!strict
|
|
1214
|
-
local UIUtils = require(game.StarterPlayer.StarterPlayerScripts.ui.UIUtils)
|
|
1215
|
-
local gui = script.Parent
|
|
1216
|
-
|
|
1217
|
-
-- Example: Scale all frames
|
|
1218
|
-
for _, child in ipairs(gui:GetChildren()) do
|
|
1219
|
-
if child:IsA("Frame") then
|
|
1220
|
-
UIUtils.scaleToScreen(child, 0.5)
|
|
1221
|
-
end
|
|
1222
|
-
end
|
|
1223
|
-
]]
|
|
1224
|
-
script.Parent = inst
|
|
1225
|
-
print("Roport: Auto-inserted UI controller template for " .. inst.Name)
|
|
1226
|
-
Sync.syncInstance(script)
|
|
1227
|
-
end
|
|
1228
|
-
end
|
|
1229
|
-
end
|
|
1230
|
-
|
|
1231
|
-
function Sync.applyUpdate(filePath, content, absolutePath, isDirectory)
|
|
1232
|
-
local inst = Paths.findInstanceByPath(filePath)
|
|
1233
|
-
|
|
1234
|
-
if isDirectory then
|
|
1235
|
-
if not inst then
|
|
1236
|
-
-- Create Folder
|
|
1237
|
-
inst = Paths.createInstanceByPath(filePath, nil)
|
|
1238
|
-
if inst then
|
|
1239
|
-
print("Roport: Created Folder " .. inst.Name)
|
|
1240
|
-
Sync.checkTemplates(inst, filePath)
|
|
1241
|
-
end
|
|
1242
|
-
else
|
|
1243
|
-
Sync.checkTemplates(inst, filePath)
|
|
1244
|
-
end
|
|
1245
|
-
return
|
|
1246
|
-
end
|
|
1247
|
-
|
|
1248
|
-
if not inst then
|
|
1249
|
-
inst = Paths.createInstanceByPath(filePath, content)
|
|
1250
|
-
if not inst then return end
|
|
1251
|
-
end
|
|
1252
|
-
|
|
1253
|
-
elseif filePath:match("%.txt$") and inst:IsA("StringValue") then
|
|
1254
|
-
inst.Value = content
|
|
1255
|
-
elseif filePath:match("%.model%.json$") then
|
|
1256
|
-
local success, data = pcall(function() return HttpService:JSONDecode(content) end)
|
|
1257
|
-
if success and data then
|
|
1258
|
-
local className = data.className or data.ClassName or data.type or "Folder"
|
|
1259
|
-
if inst.ClassName ~= className then
|
|
1260
|
-
-- Prevent destroying root services
|
|
1261
|
-
if inst.Parent == nil or inst.Parent == game then
|
|
1262
|
-
warn("Roport: Cannot replace root service " .. inst.Name)
|
|
1263
|
-
return
|
|
1264
|
-
end
|
|
1265
|
-
|
|
1266
|
-
-- Replace instance
|
|
1267
|
-
local newInst = Instance.new(className)
|
|
1268
|
-
newInst.Name = inst.Name
|
|
1269
|
-
newInst.Parent = inst.Parent
|
|
1270
|
-
-- Move children
|
|
1271
|
-
for _, child in ipairs(inst:GetChildren()) do
|
|
1272
|
-
child.Parent = newInst
|
|
1273
|
-
end
|
|
1274
|
-
inst:Destroy()
|
|
1275
|
-
inst = newInst
|
|
1276
|
-
print("Roport: Converted " .. inst.Name .. " to " .. className)
|
|
1277
|
-
end
|
|
1278
|
-
|
|
1279
|
-
-- Apply properties
|
|
1280
|
-
if data.properties then
|
|
1281
|
-
for prop, val in pairs(data.properties) do
|
|
1282
|
-
pcall(function()
|
|
1283
|
-
local currentVal = inst[prop]
|
|
1284
|
-
local targetType = typeof(currentVal)
|
|
1285
|
-
local context = nil
|
|
1286
|
-
if targetType == "EnumItem" then
|
|
1287
|
-
context = currentVal.EnumType
|
|
1288
|
-
end
|
|
1289
|
-
inst[prop] = Serialization.deserializeValue(val, targetType, context)
|
|
1290
|
-
end)
|
|
1291
|
-
end
|
|
1292
|
-
end
|
|
1293
|
-
|
|
1294
|
-
-- Apply Attributes
|
|
1295
|
-
if data.attributes then
|
|
1296
|
-
for attr, val in pairs(data.attributes) do
|
|
1297
|
-
pcall(function()
|
|
1298
|
-
inst:SetAttribute(attr, val)
|
|
1299
|
-
end)
|
|
1300
|
-
end
|
|
1301
|
-
end
|
|
1302
|
-
end
|
|
1303
|
-
elseif filePath:match("%.csv$") and inst:IsA("LocalizationTable") then
|
|
1304
|
-
-- CSV parsing is complex, for now we just set Source if it was a script, but LocalizationTable doesn't have Source.
|
|
1305
|
-
-- We would need a CSV parser here to populate the table.
|
|
1306
|
-
-- For now, let's assume the user handles it or we add a CSV parser later.
|
|
1307
|
-
print("Roport: CSV sync not fully implemented yet.")
|
|
1308
|
-
elseif (filePath:match("%.rbxmx$") or filePath:match("%.rbxm$")) and absolutePath then
|
|
1309
|
-
-- Handle Model Update via InsertService
|
|
1310
|
-
local success, model = pcall(function()
|
|
1311
|
-
return game:GetService("InsertService"):LoadLocalAsset(absolutePath)
|
|
1312
|
-
end)
|
|
1313
|
-
|
|
1314
|
-
if success and model then
|
|
1315
|
-
local children = model:GetChildren()
|
|
1316
|
-
if #children > 0 then
|
|
1317
|
-
local newObj = children[1]
|
|
1318
|
-
newObj.Name = inst.Name
|
|
1319
|
-
newObj.Parent = inst.Parent
|
|
1320
|
-
inst:Destroy()
|
|
1321
|
-
inst = newObj
|
|
1322
|
-
print("Roport: Imported " .. newObj.Name .. " from " .. filePath)
|
|
1323
|
-
end
|
|
1324
|
-
else
|
|
1325
|
-
warn("Roport: Failed to load RBXMX: " .. tostring(model))
|
|
1326
|
-
end
|
|
1327
|
-
elseif filePath:match("%.json$") then
|
|
1328
|
-
local success, data = pcall(function() return HttpService:JSONDecode(content) end)
|
|
1329
|
-
if not success then return end
|
|
1330
|
-
|
|
1331
|
-
if inst:IsA("ModuleScript") then
|
|
1332
|
-
-- It's a Data Module
|
|
1333
|
-
-- We wrap the JSON in a Lua table return
|
|
1334
|
-
inst.Source = "return game:GetService('HttpService'):JSONDecode([[\n" .. content .. "\n]])"
|
|
1335
|
-
return
|
|
1336
|
-
end
|
|
1337
|
-
|
|
1338
|
-
-- Update properties
|
|
1339
|
-
if data.properties then
|
|
1340
|
-
for prop, val in pairs(data.properties) do
|
|
1341
|
-
pcall(function()
|
|
1342
|
-
local currentVal = inst[prop]
|
|
1343
|
-
local targetType = typeof(currentVal)
|
|
1344
|
-
local context = nil
|
|
1345
|
-
if targetType == "EnumItem" then
|
|
1346
|
-
context = currentVal.EnumType
|
|
1347
|
-
end
|
|
1348
|
-
inst[prop] = Serialization.deserializeValue(val, targetType, context)
|
|
1349
|
-
end)
|
|
1350
|
-
end
|
|
1351
|
-
end
|
|
1352
|
-
|
|
1353
|
-
-- Update Attributes
|
|
1354
|
-
if data.attributes then
|
|
1355
|
-
for attr, val in pairs(data.attributes) do
|
|
1356
|
-
pcall(function()
|
|
1357
|
-
-- We don't know the target type easily for attributes, so we might need to infer or just pass raw if it's simple
|
|
1358
|
-
-- For now, let's assume simple types or that deserializeValue handles it if we can guess
|
|
1359
|
-
inst:SetAttribute(attr, val)
|
|
1360
|
-
end)
|
|
1361
|
-
end
|
|
1362
|
-
end
|
|
1363
|
-
elseif filePath:match("%.luau$") or filePath:match("%.lua$") then
|
|
1364
|
-
if inst:IsA("LuaSourceContainer") then
|
|
1365
|
-
inst.Source = content
|
|
1366
|
-
end
|
|
1367
|
-
end
|
|
1368
|
-
|
|
1369
|
-
Sync.checkTemplates(inst, filePath)
|
|
1370
|
-
print("Roport: Updated " .. inst.Name .. " from VS Code")
|
|
1371
|
-
end
|
|
1372
|
-
|
|
1373
|
-
function Sync.pullChanges()
|
|
1374
|
-
if not State.isConnected then
|
|
1375
|
-
Sync.OnStatusChanged:Fire("Connect first!", "error")
|
|
1376
|
-
return
|
|
1377
|
-
end
|
|
1378
|
-
Sync.OnStatusChanged:Fire("Checking for updates...", "info")
|
|
1379
|
-
|
|
1380
|
-
local success, res = pcall(function()
|
|
1381
|
-
return HttpService:GetAsync(Sync.getUrl("poll") .. "?t=" .. os.time())
|
|
1382
|
-
end)
|
|
1383
|
-
|
|
1384
|
-
if success then
|
|
1385
|
-
local data = HttpService:JSONDecode(res)
|
|
1386
|
-
|
|
1387
|
-
-- Handle File Changes
|
|
1388
|
-
if (data.changes and #data.changes > 0) or (data.deletions and #data.deletions > 0) then
|
|
1389
|
-
if data.changes then
|
|
1390
|
-
print("Roport: Pulling " .. #data.changes .. " files...")
|
|
1391
|
-
for i, change in ipairs(data.changes) do
|
|
1392
|
-
Sync.applyUpdate(change.filePath, change.content, change.absolutePath, change.isDirectory)
|
|
1393
|
-
if i % 50 == 0 then task.wait() end -- Yield every 50 items to prevent freeze
|
|
1394
|
-
end
|
|
1395
|
-
end
|
|
1396
|
-
|
|
1397
|
-
if data.deletions then
|
|
1398
|
-
print("Roport: Deleting " .. #data.deletions .. " files...")
|
|
1399
|
-
for i, filePath in ipairs(data.deletions) do
|
|
1400
|
-
local inst = Paths.findInstanceByPath(filePath)
|
|
1401
|
-
if inst then
|
|
1402
|
-
inst:Destroy()
|
|
1403
|
-
print("Roport: Deleted " .. filePath)
|
|
1404
|
-
end
|
|
1405
|
-
if i % 50 == 0 then task.wait() end -- Yield every 50 items
|
|
1406
|
-
end
|
|
1407
|
-
end
|
|
1408
|
-
|
|
1409
|
-
Sync.OnStatusChanged:Fire("Synced changes", "success")
|
|
1410
|
-
end
|
|
1411
|
-
|
|
1412
|
-
-- Handle Commands
|
|
1413
|
-
if data.commands and #data.commands > 0 then
|
|
1414
|
-
for _, cmd in ipairs(data.commands) do
|
|
1415
|
-
print("Roport: Executing command: " .. cmd.type)
|
|
1416
|
-
if cmd.type == "STOP" then
|
|
1417
|
-
game:Shutdown()
|
|
1418
|
-
elseif cmd.type == "PRINT_SOURCE" then
|
|
1419
|
-
-- Example command
|
|
1420
|
-
print("Source requested for: " .. tostring(cmd.path))
|
|
1421
|
-
elseif cmd.type == "PLAYTEST_START" then
|
|
1422
|
-
if plugin then plugin:StartPlayMode() end
|
|
1423
|
-
elseif cmd.type == "PUBLISH_TO_ROBLOX" then
|
|
1424
|
-
print("Roport: Attempting to publish to Roblox...")
|
|
1425
|
-
local success, err = pcall(function() game:GetService("AssetService"):SavePlaceAsync() end)
|
|
1426
|
-
if success then
|
|
1427
|
-
print("Roport: Published successfully!")
|
|
1428
|
-
else
|
|
1429
|
-
warn("Roport: Publish failed: " .. tostring(err))
|
|
1430
|
-
end
|
|
1431
|
-
elseif cmd.type == "EXECUTE" then
|
|
1432
|
-
-- Remote Execution
|
|
1433
|
-
local code = cmd.code
|
|
1434
|
-
local id = cmd.id
|
|
1435
|
-
|
|
1436
|
-
task.spawn(function()
|
|
1437
|
-
local func, loadErr = loadstring(code)
|
|
1438
|
-
if not func then
|
|
1439
|
-
pcall(function()
|
|
1440
|
-
HttpService:PostAsync(Sync.getUrl("execute-result"), HttpService:JSONEncode({
|
|
1441
|
-
id = id,
|
|
1442
|
-
success = false,
|
|
1443
|
-
error = "LoadError: " .. tostring(loadErr)
|
|
1444
|
-
}))
|
|
1445
|
-
end)
|
|
1446
|
-
return
|
|
1447
|
-
end
|
|
1448
|
-
|
|
1449
|
-
local success, result = pcall(func)
|
|
1450
|
-
|
|
1451
|
-
-- Serialize result if possible
|
|
1452
|
-
local resultStr = tostring(result)
|
|
1453
|
-
if type(result) == "table" then
|
|
1454
|
-
pcall(function() resultStr = HttpService:JSONEncode(result) end)
|
|
1455
|
-
end
|
|
1456
|
-
|
|
1457
|
-
pcall(function()
|
|
1458
|
-
HttpService:PostAsync(Sync.getUrl("execute-result"), HttpService:JSONEncode({
|
|
1459
|
-
id = id,
|
|
1460
|
-
success = success,
|
|
1461
|
-
result = resultStr,
|
|
1462
|
-
error = not success and tostring(result) or nil
|
|
1463
|
-
}))
|
|
1464
|
-
end)
|
|
1465
|
-
end)
|
|
1466
|
-
end
|
|
1467
|
-
end
|
|
1468
|
-
end
|
|
1469
|
-
|
|
1470
|
-
if (not data.changes or #data.changes == 0) and (not data.deletions or #data.deletions == 0) and (not data.commands or #data.commands == 0) then
|
|
1471
|
-
Sync.OnStatusChanged:Fire("No changes found", "info")
|
|
1472
|
-
end
|
|
1473
|
-
else
|
|
1474
|
-
warn("Roport: Pull failed: " .. tostring(res))
|
|
1475
|
-
Sync.OnStatusChanged:Fire("Pull Failed", "error")
|
|
1476
|
-
end
|
|
1477
|
-
end
|
|
1478
|
-
|
|
1479
|
-
return Sync
|
|
1480
|
-
]]></ProtectedString>
|
|
1481
|
-
</Properties>
|
|
1482
|
-
</Item>
|
|
1483
|
-
<Item class="ModuleScript" referent="RBX9">
|
|
1484
|
-
<Properties>
|
|
1485
|
-
<string name="Name">UI</string>
|
|
1486
|
-
<ProtectedString name="Source"><![CDATA[--!strict
|
|
1487
|
-
local TweenService = game:GetService("TweenService")
|
|
1488
|
-
local Constants = require(script.Parent.Constants)
|
|
1489
|
-
|
|
1490
|
-
local UI = {}
|
|
1491
|
-
UI.Widget = nil
|
|
1492
|
-
UI.ToastContainer = nil
|
|
1493
|
-
UI.ModalContainer = nil
|
|
1494
|
-
UI.ContentFrame = nil
|
|
1495
|
-
UI.SettingsFrame = nil
|
|
1496
|
-
|
|
1497
|
-
local function createCorner(parent, radius)
|
|
1498
|
-
local corner = Instance.new("UICorner")
|
|
1499
|
-
corner.CornerRadius = UDim.new(0, radius or 6)
|
|
1500
|
-
corner.Parent = parent
|
|
1501
|
-
return corner
|
|
1502
|
-
end
|
|
1503
|
-
|
|
1504
|
-
function UI.init(plugin)
|
|
1505
|
-
local toolbar = plugin:CreateToolbar(Constants.TOOLBAR_NAME)
|
|
1506
|
-
local toggleButton
|
|
1507
|
-
local success, err = pcall(function()
|
|
1508
|
-
toggleButton = toolbar:CreateButton(Constants.BUTTON_NAME, "Open the Rojo Sync Panel", "rbxassetid://122082770857390")
|
|
1509
|
-
end)
|
|
1510
|
-
|
|
1511
|
-
if not success then
|
|
1512
|
-
warn("Roport: Failed to create toolbar button (it might already exist): " .. tostring(err))
|
|
1513
|
-
return
|
|
1514
|
-
end
|
|
1515
|
-
|
|
1516
|
-
local widgetInfo = DockWidgetPluginGuiInfo.new(
|
|
1517
|
-
Enum.InitialDockState.Right,
|
|
1518
|
-
false, false, 300, 250, 250, 200
|
|
1519
|
-
)
|
|
1520
|
-
|
|
1521
|
-
local widget = plugin:CreateDockWidgetPluginGui("RojoSyncPanel", widgetInfo)
|
|
1522
|
-
widget.Title = "Rojo Sync"
|
|
1523
|
-
widget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
|
|
1524
|
-
UI.Widget = widget
|
|
1525
|
-
|
|
1526
|
-
-- Main Frame
|
|
1527
|
-
local mainFrame = Instance.new("Frame")
|
|
1528
|
-
mainFrame.Size = UDim2.new(1, 0, 1, 0)
|
|
1529
|
-
mainFrame.BackgroundColor3 = Constants.COLORS.BG
|
|
1530
|
-
mainFrame.BorderSizePixel = 0
|
|
1531
|
-
mainFrame.Parent = widget
|
|
1532
|
-
|
|
1533
|
-
-- Toast Container (ZIndex 10)
|
|
1534
|
-
local toastContainer = Instance.new("Frame")
|
|
1535
|
-
toastContainer.Size = UDim2.new(1, -20, 1, -20)
|
|
1536
|
-
toastContainer.Position = UDim2.new(0, 10, 0, 10)
|
|
1537
|
-
toastContainer.BackgroundTransparency = 1
|
|
1538
|
-
toastContainer.ZIndex = 10
|
|
1539
|
-
toastContainer.Parent = mainFrame
|
|
1540
|
-
UI.ToastContainer = toastContainer
|
|
1541
|
-
|
|
1542
|
-
local toastLayout = Instance.new("UIListLayout")
|
|
1543
|
-
toastLayout.VerticalAlignment = Enum.VerticalAlignment.Bottom
|
|
1544
|
-
toastLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
|
|
1545
|
-
toastLayout.Padding = UDim.new(0, 5)
|
|
1546
|
-
toastLayout.Parent = toastContainer
|
|
1547
|
-
|
|
1548
|
-
-- Modal Container (ZIndex 20)
|
|
1549
|
-
local modalContainer = Instance.new("Frame")
|
|
1550
|
-
modalContainer.Size = UDim2.new(1, 0, 1, 0)
|
|
1551
|
-
modalContainer.BackgroundColor3 = Color3.new(0, 0, 0)
|
|
1552
|
-
modalContainer.BackgroundTransparency = 1 -- Starts invisible
|
|
1553
|
-
modalContainer.Visible = false
|
|
1554
|
-
modalContainer.ZIndex = 20
|
|
1555
|
-
modalContainer.Parent = mainFrame
|
|
1556
|
-
UI.ModalContainer = modalContainer
|
|
1557
|
-
|
|
1558
|
-
-- Content Layout
|
|
1559
|
-
local contentFrame = Instance.new("Frame")
|
|
1560
|
-
contentFrame.Size = UDim2.new(1, 0, 1, 0)
|
|
1561
|
-
contentFrame.BackgroundTransparency = 1
|
|
1562
|
-
contentFrame.Parent = mainFrame
|
|
1563
|
-
UI.ContentFrame = contentFrame
|
|
1564
|
-
|
|
1565
|
-
local listLayout = Instance.new("UIListLayout")
|
|
1566
|
-
listLayout.Padding = UDim.new(0, 10)
|
|
1567
|
-
listLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
|
|
1568
|
-
listLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
1569
|
-
listLayout.Parent = contentFrame
|
|
1570
|
-
|
|
1571
|
-
local padding = Instance.new("UIPadding")
|
|
1572
|
-
padding.PaddingTop = UDim.new(0, 15)
|
|
1573
|
-
padding.PaddingLeft = UDim.new(0, 15)
|
|
1574
|
-
padding.PaddingRight = UDim.new(0, 15)
|
|
1575
|
-
padding.Parent = contentFrame
|
|
1576
|
-
|
|
1577
|
-
toggleButton.Click:Connect(function() widget.Enabled = not widget.Enabled end)
|
|
1578
|
-
|
|
1579
|
-
return toggleButton
|
|
1580
|
-
end
|
|
1581
|
-
|
|
1582
|
-
function UI.showToast(message, type)
|
|
1583
|
-
if not UI.ToastContainer then return end
|
|
1584
|
-
|
|
1585
|
-
local toast = Instance.new("Frame")
|
|
1586
|
-
toast.Size = UDim2.new(1, 0, 0, 0) -- Animate height
|
|
1587
|
-
toast.BackgroundTransparency = 1
|
|
1588
|
-
toast.ClipsDescendants = true
|
|
1589
|
-
toast.Parent = UI.ToastContainer
|
|
1590
|
-
|
|
1591
|
-
local inner = Instance.new("Frame")
|
|
1592
|
-
inner.Size = UDim2.new(1, 0, 0, 35)
|
|
1593
|
-
inner.BackgroundColor3 = Constants.COLORS.HEADER
|
|
1594
|
-
inner.BorderSizePixel = 0
|
|
1595
|
-
createCorner(inner, 6)
|
|
1596
|
-
inner.Parent = toast
|
|
1597
|
-
|
|
1598
|
-
local bar = Instance.new("Frame")
|
|
1599
|
-
bar.Size = UDim2.new(0, 4, 1, 0)
|
|
1600
|
-
bar.BackgroundColor3 = type == "error" and Constants.COLORS.ERROR or (type == "success" and Constants.COLORS.SUCCESS or Constants.COLORS.ACCENT)
|
|
1601
|
-
createCorner(bar, 6)
|
|
1602
|
-
bar.Parent = inner
|
|
1603
|
-
|
|
1604
|
-
local label = Instance.new("TextLabel")
|
|
1605
|
-
label.Size = UDim2.new(1, -15, 1, 0)
|
|
1606
|
-
label.Position = UDim2.new(0, 10, 0, 0)
|
|
1607
|
-
label.BackgroundTransparency = 1
|
|
1608
|
-
label.Text = message
|
|
1609
|
-
label.TextColor3 = Constants.COLORS.TEXT
|
|
1610
|
-
label.Font = Enum.Font.SourceSans
|
|
1611
|
-
label.TextSize = 14
|
|
1612
|
-
label.TextXAlignment = Enum.TextXAlignment.Left
|
|
1613
|
-
label.Parent = inner
|
|
1614
|
-
|
|
1615
|
-
-- Animation
|
|
1616
|
-
local tweenInfo = TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
|
|
1617
|
-
TweenService:Create(toast, tweenInfo, {Size = UDim2.new(1, 0, 0, 40)}):Play()
|
|
1618
|
-
|
|
1619
|
-
task.delay(3, function()
|
|
1620
|
-
local out = TweenService:Create(toast, tweenInfo, {Size = UDim2.new(1, 0, 0, 0)})
|
|
1621
|
-
out:Play()
|
|
1622
|
-
out.Completed:Connect(function() toast:Destroy() end)
|
|
1623
|
-
end)
|
|
1624
|
-
end
|
|
1625
|
-
|
|
1626
|
-
function UI.showModal(title, message, onConfirm)
|
|
1627
|
-
if not UI.ModalContainer then return end
|
|
1628
|
-
|
|
1629
|
-
local modalContainer = UI.ModalContainer
|
|
1630
|
-
modalContainer.Visible = true
|
|
1631
|
-
TweenService:Create(modalContainer, TweenInfo.new(0.3), {BackgroundTransparency = 0.5}):Play()
|
|
1632
|
-
|
|
1633
|
-
-- Clear previous
|
|
1634
|
-
for _, c in ipairs(modalContainer:GetChildren()) do c:Destroy() end
|
|
1635
|
-
|
|
1636
|
-
local box = Instance.new("Frame")
|
|
1637
|
-
box.Size = UDim2.new(0.8, 0, 0, 140)
|
|
1638
|
-
box.Position = UDim2.new(0.5, 0, 0.5, 0)
|
|
1639
|
-
box.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
1640
|
-
box.BackgroundColor3 = Constants.COLORS.HEADER
|
|
1641
|
-
createCorner(box, 8)
|
|
1642
|
-
box.Parent = modalContainer
|
|
1643
|
-
|
|
1644
|
-
-- Scale up animation
|
|
1645
|
-
local uiScale = Instance.new("UIScale")
|
|
1646
|
-
uiScale.Scale = 0.8
|
|
1647
|
-
uiScale.Parent = box
|
|
1648
|
-
|
|
1649
|
-
TweenService:Create(uiScale, TweenInfo.new(0.3, Enum.EasingStyle.Back), {Scale = 1}):Play()
|
|
1650
|
-
|
|
1651
|
-
local titleLbl = Instance.new("TextLabel")
|
|
1652
|
-
titleLbl.Size = UDim2.new(1, 0, 0, 40)
|
|
1653
|
-
titleLbl.BackgroundTransparency = 1
|
|
1654
|
-
titleLbl.Text = title
|
|
1655
|
-
titleLbl.Font = Enum.Font.SourceSansBold
|
|
1656
|
-
titleLbl.TextSize = 18
|
|
1657
|
-
titleLbl.TextColor3 = Constants.COLORS.TEXT
|
|
1658
|
-
titleLbl.Parent = box
|
|
1659
|
-
|
|
1660
|
-
local msgLbl = Instance.new("TextLabel")
|
|
1661
|
-
msgLbl.Size = UDim2.new(1, -20, 0, 50)
|
|
1662
|
-
msgLbl.Position = UDim2.new(0, 10, 0, 40)
|
|
1663
|
-
msgLbl.BackgroundTransparency = 1
|
|
1664
|
-
msgLbl.Text = message
|
|
1665
|
-
msgLbl.Font = Enum.Font.SourceSans
|
|
1666
|
-
msgLbl.TextSize = 14
|
|
1667
|
-
msgLbl.TextColor3 = Constants.COLORS.SUBTEXT
|
|
1668
|
-
msgLbl.TextWrapped = true
|
|
1669
|
-
msgLbl.Parent = box
|
|
1670
|
-
|
|
1671
|
-
local btnFrame = Instance.new("Frame")
|
|
1672
|
-
btnFrame.Size = UDim2.new(1, -20, 0, 35)
|
|
1673
|
-
btnFrame.Position = UDim2.new(0, 10, 1, -45)
|
|
1674
|
-
btnFrame.BackgroundTransparency = 1
|
|
1675
|
-
btnFrame.Parent = box
|
|
1676
|
-
|
|
1677
|
-
local layout = Instance.new("UIListLayout")
|
|
1678
|
-
layout.FillDirection = Enum.FillDirection.Horizontal
|
|
1679
|
-
layout.Padding = UDim.new(0, 10)
|
|
1680
|
-
layout.HorizontalAlignment = Enum.HorizontalAlignment.Center
|
|
1681
|
-
layout.Parent = btnFrame
|
|
1682
|
-
|
|
1683
|
-
local function makeBtn(text, color, cb)
|
|
1684
|
-
local btn = Instance.new("TextButton")
|
|
1685
|
-
btn.Size = UDim2.new(0.45, 0, 1, 0)
|
|
1686
|
-
btn.BackgroundColor3 = color
|
|
1687
|
-
btn.Text = text
|
|
1688
|
-
btn.TextColor3 = Constants.COLORS.TEXT
|
|
1689
|
-
btn.Font = Enum.Font.SourceSansBold
|
|
1690
|
-
btn.TextSize = 14
|
|
1691
|
-
createCorner(btn, 4)
|
|
1692
|
-
btn.Parent = btnFrame
|
|
1693
|
-
btn.MouseButton1Click:Connect(cb)
|
|
1694
|
-
end
|
|
1695
|
-
|
|
1696
|
-
makeBtn("Connect", Constants.COLORS.ACCENT, function()
|
|
1697
|
-
TweenService:Create(modalContainer, TweenInfo.new(0.2), {BackgroundTransparency = 1}):Play()
|
|
1698
|
-
modalContainer.Visible = false
|
|
1699
|
-
onConfirm()
|
|
1700
|
-
end)
|
|
1701
|
-
|
|
1702
|
-
makeBtn("Cancel", Constants.COLORS.BUTTON, function()
|
|
1703
|
-
TweenService:Create(modalContainer, TweenInfo.new(0.2), {BackgroundTransparency = 1}):Play()
|
|
1704
|
-
modalContainer.Visible = false
|
|
1705
|
-
end)
|
|
1706
|
-
end
|
|
1707
|
-
|
|
1708
|
-
function UI.createButton(text, icon, callback)
|
|
1709
|
-
if not UI.ContentFrame then return end
|
|
1710
|
-
|
|
1711
|
-
local btn = Instance.new("TextButton")
|
|
1712
|
-
btn.Size = UDim2.new(1, 0, 0, 40)
|
|
1713
|
-
btn.BackgroundColor3 = Constants.COLORS.BUTTON
|
|
1714
|
-
btn.Text = ""
|
|
1715
|
-
createCorner(btn, 6)
|
|
1716
|
-
btn.Parent = UI.ContentFrame
|
|
1717
|
-
|
|
1718
|
-
local lbl = Instance.new("TextLabel")
|
|
1719
|
-
lbl.Size = UDim2.new(1, 0, 1, 0)
|
|
1720
|
-
lbl.BackgroundTransparency = 1
|
|
1721
|
-
lbl.Text = text
|
|
1722
|
-
lbl.Font = Enum.Font.SourceSansBold
|
|
1723
|
-
lbl.TextSize = 16
|
|
1724
|
-
lbl.TextColor3 = Constants.COLORS.TEXT
|
|
1725
|
-
lbl.Parent = btn
|
|
1726
|
-
|
|
1727
|
-
btn.MouseButton1Click:Connect(callback)
|
|
1728
|
-
return btn
|
|
1729
|
-
end
|
|
1730
|
-
|
|
1731
|
-
function UI.createStatusHeader()
|
|
1732
|
-
if not UI.ContentFrame then return end
|
|
1733
|
-
|
|
1734
|
-
local statusFrame = Instance.new("Frame")
|
|
1735
|
-
statusFrame.Size = UDim2.new(1, 0, 0, 30)
|
|
1736
|
-
statusFrame.BackgroundTransparency = 1
|
|
1737
|
-
statusFrame.LayoutOrder = 0
|
|
1738
|
-
statusFrame.Parent = UI.ContentFrame
|
|
1739
|
-
|
|
1740
|
-
local statusDot = Instance.new("Frame")
|
|
1741
|
-
statusDot.Size = UDim2.new(0, 10, 0, 10)
|
|
1742
|
-
statusDot.Position = UDim2.new(0, 0, 0.5, -5)
|
|
1743
|
-
statusDot.BackgroundColor3 = Constants.COLORS.ERROR
|
|
1744
|
-
createCorner(statusDot, 10)
|
|
1745
|
-
statusDot.Parent = statusFrame
|
|
1746
|
-
|
|
1747
|
-
local statusText = Instance.new("TextLabel")
|
|
1748
|
-
statusText.Size = UDim2.new(1, -20, 1, 0)
|
|
1749
|
-
statusText.Position = UDim2.new(0, 20, 0, 0)
|
|
1750
|
-
statusText.BackgroundTransparency = 1
|
|
1751
|
-
statusText.Text = "Disconnected"
|
|
1752
|
-
statusText.Font = Enum.Font.SourceSans
|
|
1753
|
-
statusText.TextSize = 14
|
|
1754
|
-
statusText.TextColor3 = Constants.COLORS.SUBTEXT
|
|
1755
|
-
statusText.TextXAlignment = Enum.TextXAlignment.Left
|
|
1756
|
-
statusText.Parent = statusFrame
|
|
1757
|
-
|
|
1758
|
-
return statusDot, statusText
|
|
1759
|
-
end
|
|
1760
|
-
|
|
1761
|
-
function UI.createSettingsUI(state, saveSettingsCallback)
|
|
1762
|
-
if not UI.Widget then return end
|
|
1763
|
-
|
|
1764
|
-
local settingsFrame = Instance.new("Frame")
|
|
1765
|
-
settingsFrame.Size = UDim2.new(1, 0, 1, 0)
|
|
1766
|
-
settingsFrame.BackgroundColor3 = Constants.COLORS.BG
|
|
1767
|
-
settingsFrame.Visible = false
|
|
1768
|
-
settingsFrame.ZIndex = 15
|
|
1769
|
-
settingsFrame.Parent = UI.Widget:FindFirstChild("Frame") -- MainFrame
|
|
1770
|
-
|
|
1771
|
-
local settingsLayout = Instance.new("UIListLayout")
|
|
1772
|
-
settingsLayout.Padding = UDim.new(0, 10)
|
|
1773
|
-
settingsLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
|
|
1774
|
-
settingsLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
1775
|
-
settingsLayout.Parent = settingsFrame
|
|
1776
|
-
|
|
1777
|
-
local settingsPadding = Instance.new("UIPadding")
|
|
1778
|
-
settingsPadding.PaddingTop = UDim.new(0, 15)
|
|
1779
|
-
settingsPadding.PaddingLeft = UDim.new(0, 15)
|
|
1780
|
-
settingsPadding.PaddingRight = UDim.new(0, 15)
|
|
1781
|
-
settingsPadding.Parent = settingsFrame
|
|
1782
|
-
|
|
1783
|
-
local function createSettingInput(label, default, callback)
|
|
1784
|
-
local frame = Instance.new("Frame")
|
|
1785
|
-
frame.Size = UDim2.new(1, 0, 0, 50)
|
|
1786
|
-
frame.BackgroundTransparency = 1
|
|
1787
|
-
frame.Parent = settingsFrame
|
|
1788
|
-
|
|
1789
|
-
local lbl = Instance.new("TextLabel")
|
|
1790
|
-
lbl.Size = UDim2.new(1, 0, 0, 20)
|
|
1791
|
-
lbl.BackgroundTransparency = 1
|
|
1792
|
-
lbl.Text = label
|
|
1793
|
-
lbl.TextColor3 = Constants.COLORS.SUBTEXT
|
|
1794
|
-
lbl.Font = Enum.Font.SourceSans
|
|
1795
|
-
lbl.TextSize = 14
|
|
1796
|
-
lbl.TextXAlignment = Enum.TextXAlignment.Left
|
|
1797
|
-
lbl.Parent = frame
|
|
1798
|
-
|
|
1799
|
-
local box = Instance.new("TextBox")
|
|
1800
|
-
box.Size = UDim2.new(1, 0, 0, 30)
|
|
1801
|
-
box.Position = UDim2.new(0, 0, 0, 20)
|
|
1802
|
-
box.BackgroundColor3 = Constants.COLORS.BUTTON
|
|
1803
|
-
box.Text = tostring(default)
|
|
1804
|
-
box.TextColor3 = Constants.COLORS.TEXT
|
|
1805
|
-
box.Font = Enum.Font.SourceSans
|
|
1806
|
-
box.TextSize = 14
|
|
1807
|
-
createCorner(box, 4)
|
|
1808
|
-
box.Parent = frame
|
|
1809
|
-
|
|
1810
|
-
box.FocusLost:Connect(function()
|
|
1811
|
-
callback(box.Text)
|
|
1812
|
-
end)
|
|
1813
|
-
end
|
|
1814
|
-
|
|
1815
|
-
createSettingInput("Server Port", state.settings and state.settings.port or 3456, function(val)
|
|
1816
|
-
local n = tonumber(val)
|
|
1817
|
-
if n then
|
|
1818
|
-
if not state.settings then state.settings = {port=3456, syncInterval=2} end
|
|
1819
|
-
state.settings.port = n
|
|
1820
|
-
saveSettingsCallback()
|
|
1821
|
-
UI.showToast("Port saved: " .. n, "success")
|
|
1822
|
-
end
|
|
1823
|
-
end)
|
|
1824
|
-
|
|
1825
|
-
createSettingInput("Sync Interval (Seconds)", state.settings and state.settings.syncInterval or 2, function(val)
|
|
1826
|
-
local n = tonumber(val)
|
|
1827
|
-
if n then
|
|
1828
|
-
if not state.settings then state.settings = {port=3456, syncInterval=2} end
|
|
1829
|
-
state.settings.syncInterval = n
|
|
1830
|
-
saveSettingsCallback()
|
|
1831
|
-
UI.showToast("Interval saved: " .. n, "success")
|
|
1832
|
-
end
|
|
1833
|
-
end)
|
|
1834
|
-
|
|
1835
|
-
local closeSettingsBtn = Instance.new("TextButton")
|
|
1836
|
-
closeSettingsBtn.Size = UDim2.new(1, 0, 0, 35)
|
|
1837
|
-
closeSettingsBtn.BackgroundColor3 = Constants.COLORS.ACCENT
|
|
1838
|
-
closeSettingsBtn.Text = "Back to Sync"
|
|
1839
|
-
closeSettingsBtn.TextColor3 = Constants.COLORS.TEXT
|
|
1840
|
-
closeSettingsBtn.Font = Enum.Font.SourceSansBold
|
|
1841
|
-
closeSettingsBtn.TextSize = 14
|
|
1842
|
-
createCorner(closeSettingsBtn, 6)
|
|
1843
|
-
closeSettingsBtn.Parent = settingsFrame
|
|
1844
|
-
closeSettingsBtn.MouseButton1Click:Connect(function()
|
|
1845
|
-
settingsFrame.Visible = false
|
|
1846
|
-
if UI.ContentFrame then UI.ContentFrame.Visible = true end
|
|
1847
|
-
end)
|
|
1848
|
-
|
|
1849
|
-
return settingsFrame
|
|
1850
|
-
end
|
|
1851
|
-
|
|
1852
|
-
return UI
|
|
1853
|
-
]]></ProtectedString>
|
|
1854
|
-
</Properties>
|
|
1855
|
-
</Item>
|
|
1856
|
-
</Item>
|
|
1857
|
-
<Item class="Folder" referent="RBX10">
|
|
1858
|
-
<Properties>
|
|
1859
|
-
<string name="Name">workspace</string>
|
|
1860
|
-
</Properties>
|
|
1861
|
-
</Item>
|
|
1862
|
-
</Item>
|
|
1863
|
-
</Item>
|
|
1864
|
-
</roblox>
|