roport 1.4.0 → 2.0.1

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.
@@ -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>