goldsync 0.1.11
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/LICENSE.txt +5 -0
- package/README.md +129 -0
- package/assets/GoldSync.rbxmx +2516 -0
- package/bin/goldsync-agent.mjs +54 -0
- package/bin/install-companion.mjs +96 -0
- package/package.json +42 -0
- package/src/broker.mjs +306 -0
- package/src/companion.mjs +220 -0
- package/src/config.mjs +165 -0
- package/src/git-conflicts.mjs +127 -0
- package/src/rojo-session.mjs +165 -0
- package/src/server.mjs +308 -0
- package/src/session-marker.mjs +27 -0
- package/src/store.mjs +337 -0
|
@@ -0,0 +1,2516 @@
|
|
|
1
|
+
<roblox version="4">
|
|
2
|
+
<Item class="Folder" referent="0">
|
|
3
|
+
<Properties>
|
|
4
|
+
<string name="Name">GoldSync</string>
|
|
5
|
+
</Properties>
|
|
6
|
+
<Item class="Script" referent="1">
|
|
7
|
+
<Properties>
|
|
8
|
+
<string name="Name">GoldSync</string>
|
|
9
|
+
<token name="RunContext">3</token>
|
|
10
|
+
<string name="Source"><![CDATA[local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
11
|
+
local HttpService = game:GetService("HttpService")
|
|
12
|
+
local ReflectionService = game:GetService("ReflectionService")
|
|
13
|
+
local SerializationService = game:GetService("SerializationService")
|
|
14
|
+
local ServerStorage = game:GetService("ServerStorage")
|
|
15
|
+
local Selection = game:GetService("Selection")
|
|
16
|
+
local StudioService = game:GetService("StudioService")
|
|
17
|
+
|
|
18
|
+
local ENDPOINT = "http://127.0.0.1:34873"
|
|
19
|
+
local SESSION_MARKER_NAME = "__GoldSyncSession"
|
|
20
|
+
local CLIENT_HEADERS = {
|
|
21
|
+
["X-GoldSync-Client"] = "studio-plugin",
|
|
22
|
+
}
|
|
23
|
+
local SETTINGS_PREFIX = "GoldSync:v2:"
|
|
24
|
+
local UI_SETTINGS_PREFIX = "GoldSync:ui:v1:"
|
|
25
|
+
local VERSION = "0.1.11"
|
|
26
|
+
local REQUEST_INTERVAL_SECONDS = 0.15
|
|
27
|
+
local REQUEST_RETRY_DELAYS = { 1, 2, 4 }
|
|
28
|
+
|
|
29
|
+
local showSynced = plugin:GetSetting(UI_SETTINGS_PREFIX .. "showSynced") ~= false
|
|
30
|
+
local autoExpandConflicts = plugin:GetSetting(UI_SETTINGS_PREFIX .. "autoExpandConflicts") ~= false
|
|
31
|
+
local visualDepth = math.clamp(tonumber(plugin:GetSetting(UI_SETTINGS_PREFIX .. "visualDepth")) or 3, 0, 5)
|
|
32
|
+
|
|
33
|
+
local toolbar = plugin:CreateToolbar("GoldSync")
|
|
34
|
+
local toolbarButton = toolbar:CreateButton(
|
|
35
|
+
"GoldSync",
|
|
36
|
+
"GoldSync v" .. VERSION .. " · Two-way native RBXM asset sync",
|
|
37
|
+
"rbxassetid://127943421516268"
|
|
38
|
+
)
|
|
39
|
+
toolbarButton.ClickableWhenViewportHidden = true
|
|
40
|
+
|
|
41
|
+
local widgetInfo = DockWidgetPluginGuiInfo.new(
|
|
42
|
+
Enum.InitialDockState.Right,
|
|
43
|
+
false,
|
|
44
|
+
false,
|
|
45
|
+
360,
|
|
46
|
+
300,
|
|
47
|
+
280,
|
|
48
|
+
180
|
|
49
|
+
)
|
|
50
|
+
local widget = plugin:CreateDockWidgetPluginGui("GoldSyncWidget", widgetInfo)
|
|
51
|
+
widget.Title = "GoldSync v" .. VERSION
|
|
52
|
+
|
|
53
|
+
local inspectorInfo = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Right, false, false, 620, 500, 420, 280)
|
|
54
|
+
local inspectorWidget = plugin:CreateDockWidgetPluginGui("GoldSyncConflictInspector", inspectorInfo)
|
|
55
|
+
inspectorWidget.Title = "GoldSync Conflict Inspector"
|
|
56
|
+
|
|
57
|
+
local inspectorContent = Instance.new("ScrollingFrame")
|
|
58
|
+
inspectorContent.AutomaticCanvasSize = Enum.AutomaticSize.Y
|
|
59
|
+
inspectorContent.BackgroundColor3 = Color3.fromRGB(30, 30, 34)
|
|
60
|
+
inspectorContent.BorderSizePixel = 0
|
|
61
|
+
inspectorContent.CanvasSize = UDim2.new()
|
|
62
|
+
inspectorContent.ScrollBarThickness = 7
|
|
63
|
+
inspectorContent.Size = UDim2.fromScale(1, 1)
|
|
64
|
+
inspectorContent.Parent = inspectorWidget
|
|
65
|
+
|
|
66
|
+
local inspectorPadding = Instance.new("UIPadding")
|
|
67
|
+
inspectorPadding.PaddingBottom = UDim.new(0, 10)
|
|
68
|
+
inspectorPadding.PaddingLeft = UDim.new(0, 10)
|
|
69
|
+
inspectorPadding.PaddingRight = UDim.new(0, 10)
|
|
70
|
+
inspectorPadding.PaddingTop = UDim.new(0, 10)
|
|
71
|
+
inspectorPadding.Parent = inspectorContent
|
|
72
|
+
|
|
73
|
+
local inspectorLayout = Instance.new("UIListLayout")
|
|
74
|
+
inspectorLayout.Padding = UDim.new(0, 7)
|
|
75
|
+
inspectorLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
76
|
+
inspectorLayout.Parent = inspectorContent
|
|
77
|
+
|
|
78
|
+
local inspectorTitle = Instance.new("TextLabel")
|
|
79
|
+
inspectorTitle.BackgroundTransparency = 1
|
|
80
|
+
inspectorTitle.Font = Enum.Font.BuilderSansBold
|
|
81
|
+
inspectorTitle.LayoutOrder = 1
|
|
82
|
+
inspectorTitle.Size = UDim2.new(1, 0, 0, 28)
|
|
83
|
+
inspectorTitle.Text = "No conflict workspace open"
|
|
84
|
+
inspectorTitle.TextColor3 = Color3.fromRGB(240, 240, 245)
|
|
85
|
+
inspectorTitle.TextSize = 18
|
|
86
|
+
inspectorTitle.TextXAlignment = Enum.TextXAlignment.Left
|
|
87
|
+
inspectorTitle.Parent = inspectorContent
|
|
88
|
+
|
|
89
|
+
local inspectorSummary = Instance.new("TextLabel")
|
|
90
|
+
inspectorSummary.AutomaticSize = Enum.AutomaticSize.Y
|
|
91
|
+
inspectorSummary.BackgroundTransparency = 1
|
|
92
|
+
inspectorSummary.Font = Enum.Font.BuilderSans
|
|
93
|
+
inspectorSummary.LayoutOrder = 2
|
|
94
|
+
inspectorSummary.Size = UDim2.new(1, 0, 0, 18)
|
|
95
|
+
inspectorSummary.Text = "Open Merge Workspace from a conflicted root."
|
|
96
|
+
inspectorSummary.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
97
|
+
inspectorSummary.TextSize = 13
|
|
98
|
+
inspectorSummary.TextWrapped = true
|
|
99
|
+
inspectorSummary.TextXAlignment = Enum.TextXAlignment.Left
|
|
100
|
+
inspectorSummary.Parent = inspectorContent
|
|
101
|
+
|
|
102
|
+
local inspectorEntries = {}
|
|
103
|
+
local inspectorRendered = 0
|
|
104
|
+
local inspectorGeneration = 0
|
|
105
|
+
local inspectorState
|
|
106
|
+
local renderInspector
|
|
107
|
+
|
|
108
|
+
local content = Instance.new("ScrollingFrame")
|
|
109
|
+
content.Name = "Content"
|
|
110
|
+
content.AutomaticCanvasSize = Enum.AutomaticSize.Y
|
|
111
|
+
content.BackgroundColor3 = Color3.fromRGB(30, 30, 34)
|
|
112
|
+
content.BorderSizePixel = 0
|
|
113
|
+
content.CanvasSize = UDim2.new()
|
|
114
|
+
content.ScrollBarThickness = 6
|
|
115
|
+
content.Size = UDim2.fromScale(1, 1)
|
|
116
|
+
content.Parent = widget
|
|
117
|
+
|
|
118
|
+
local padding = Instance.new("UIPadding")
|
|
119
|
+
padding.PaddingBottom = UDim.new(0, 10)
|
|
120
|
+
padding.PaddingLeft = UDim.new(0, 10)
|
|
121
|
+
padding.PaddingRight = UDim.new(0, 10)
|
|
122
|
+
padding.PaddingTop = UDim.new(0, 10)
|
|
123
|
+
padding.Parent = content
|
|
124
|
+
|
|
125
|
+
local layout = Instance.new("UIListLayout")
|
|
126
|
+
layout.Padding = UDim.new(0, 8)
|
|
127
|
+
layout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
128
|
+
layout.Parent = content
|
|
129
|
+
|
|
130
|
+
local header = Instance.new("Frame")
|
|
131
|
+
header.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
132
|
+
header.BorderSizePixel = 0
|
|
133
|
+
header.LayoutOrder = 1
|
|
134
|
+
header.Size = UDim2.new(1, 0, 0, 58)
|
|
135
|
+
header.Parent = content
|
|
136
|
+
Instance.new("UICorner", header).CornerRadius = UDim.new(0, 6)
|
|
137
|
+
|
|
138
|
+
local pluginIcon = Instance.new("ImageLabel")
|
|
139
|
+
pluginIcon.BackgroundTransparency = 1
|
|
140
|
+
pluginIcon.Image = "rbxassetid://127943421516268"
|
|
141
|
+
pluginIcon.Position = UDim2.new(0, 8, 0.5, -20)
|
|
142
|
+
pluginIcon.Size = UDim2.fromOffset(40, 40)
|
|
143
|
+
pluginIcon.Parent = header
|
|
144
|
+
|
|
145
|
+
local heading = Instance.new("TextLabel")
|
|
146
|
+
heading.BackgroundTransparency = 1
|
|
147
|
+
heading.Font = Enum.Font.BuilderSansBold
|
|
148
|
+
heading.Position = UDim2.new(0, 56, 0, 7)
|
|
149
|
+
heading.Size = UDim2.new(1, -64, 0, 22)
|
|
150
|
+
heading.Text = "GoldSync · v" .. VERSION
|
|
151
|
+
heading.TextColor3 = Color3.fromRGB(245, 245, 248)
|
|
152
|
+
heading.TextSize = 18
|
|
153
|
+
heading.TextXAlignment = Enum.TextXAlignment.Left
|
|
154
|
+
heading.Parent = header
|
|
155
|
+
|
|
156
|
+
local connectionDot = Instance.new("TextLabel")
|
|
157
|
+
connectionDot.BackgroundTransparency = 1
|
|
158
|
+
connectionDot.Font = Enum.Font.BuilderSansBold
|
|
159
|
+
connectionDot.Position = UDim2.new(0, 56, 0, 30)
|
|
160
|
+
connectionDot.Size = UDim2.fromOffset(12, 18)
|
|
161
|
+
connectionDot.Text = "●"
|
|
162
|
+
connectionDot.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
163
|
+
connectionDot.TextSize = 9
|
|
164
|
+
connectionDot.Parent = header
|
|
165
|
+
|
|
166
|
+
local connectionStatus = Instance.new("TextLabel")
|
|
167
|
+
connectionStatus.BackgroundTransparency = 1
|
|
168
|
+
connectionStatus.Font = Enum.Font.BuilderSansBold
|
|
169
|
+
connectionStatus.Position = UDim2.new(0, 70, 0, 29)
|
|
170
|
+
connectionStatus.Size = UDim2.new(1, -78, 0, 20)
|
|
171
|
+
connectionStatus.Text = "Waiting for daemon"
|
|
172
|
+
connectionStatus.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
173
|
+
connectionStatus.TextSize = 12
|
|
174
|
+
connectionStatus.TextTruncate = Enum.TextTruncate.AtEnd
|
|
175
|
+
connectionStatus.TextXAlignment = Enum.TextXAlignment.Left
|
|
176
|
+
connectionStatus.Parent = header
|
|
177
|
+
|
|
178
|
+
local controls = Instance.new("Frame")
|
|
179
|
+
controls.BackgroundTransparency = 1
|
|
180
|
+
controls.LayoutOrder = 2
|
|
181
|
+
controls.Size = UDim2.new(1, 0, 0, 28)
|
|
182
|
+
controls.Parent = content
|
|
183
|
+
|
|
184
|
+
local searchBox = Instance.new("TextBox")
|
|
185
|
+
searchBox.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
186
|
+
searchBox.BorderSizePixel = 0
|
|
187
|
+
searchBox.ClearTextOnFocus = false
|
|
188
|
+
searchBox.Font = Enum.Font.BuilderSans
|
|
189
|
+
searchBox.PlaceholderColor3 = Color3.fromRGB(130, 130, 140)
|
|
190
|
+
searchBox.PlaceholderText = "Search files and instances..."
|
|
191
|
+
searchBox.Size = UDim2.new(1, -132, 1, 0)
|
|
192
|
+
searchBox.Text = ""
|
|
193
|
+
searchBox.TextColor3 = Color3.fromRGB(235, 235, 240)
|
|
194
|
+
searchBox.TextSize = 12
|
|
195
|
+
searchBox.TextXAlignment = Enum.TextXAlignment.Left
|
|
196
|
+
searchBox.Parent = controls
|
|
197
|
+
Instance.new("UICorner", searchBox).CornerRadius = UDim.new(0, 4)
|
|
198
|
+
local searchPadding = Instance.new("UIPadding", searchBox)
|
|
199
|
+
searchPadding.PaddingLeft = UDim.new(0, 8)
|
|
200
|
+
searchPadding.PaddingRight = UDim.new(0, 8)
|
|
201
|
+
|
|
202
|
+
local unsyncedButton = Instance.new("TextButton")
|
|
203
|
+
unsyncedButton.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
204
|
+
unsyncedButton.BorderSizePixel = 0
|
|
205
|
+
unsyncedButton.Font = Enum.Font.BuilderSansBold
|
|
206
|
+
unsyncedButton.Position = UDim2.new(1, -126, 0, 0)
|
|
207
|
+
unsyncedButton.Size = UDim2.fromOffset(92, 28)
|
|
208
|
+
unsyncedButton.Text = "Unsynced · 0"
|
|
209
|
+
unsyncedButton.TextColor3 = Color3.fromRGB(190, 190, 198)
|
|
210
|
+
unsyncedButton.TextSize = 11
|
|
211
|
+
unsyncedButton.Parent = controls
|
|
212
|
+
Instance.new("UICorner", unsyncedButton).CornerRadius = UDim.new(0, 4)
|
|
213
|
+
|
|
214
|
+
local settingsButton = Instance.new("TextButton")
|
|
215
|
+
settingsButton.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
216
|
+
settingsButton.BorderSizePixel = 0
|
|
217
|
+
settingsButton.Font = Enum.Font.BuilderSansBold
|
|
218
|
+
settingsButton.Position = UDim2.new(1, -28, 0, 0)
|
|
219
|
+
settingsButton.Size = UDim2.fromOffset(28, 28)
|
|
220
|
+
settingsButton.Text = "⚙"
|
|
221
|
+
settingsButton.TextColor3 = Color3.fromRGB(210, 210, 218)
|
|
222
|
+
settingsButton.TextSize = 15
|
|
223
|
+
settingsButton.Parent = controls
|
|
224
|
+
Instance.new("UICorner", settingsButton).CornerRadius = UDim.new(0, 4)
|
|
225
|
+
|
|
226
|
+
local settingsPanel = Instance.new("Frame")
|
|
227
|
+
settingsPanel.AutomaticSize = Enum.AutomaticSize.Y
|
|
228
|
+
settingsPanel.BackgroundColor3 = Color3.fromRGB(36, 36, 42)
|
|
229
|
+
settingsPanel.BorderSizePixel = 0
|
|
230
|
+
settingsPanel.LayoutOrder = 3
|
|
231
|
+
settingsPanel.Size = UDim2.new(1, 0, 0, 0)
|
|
232
|
+
settingsPanel.Visible = false
|
|
233
|
+
settingsPanel.Parent = content
|
|
234
|
+
Instance.new("UICorner", settingsPanel).CornerRadius = UDim.new(0, 5)
|
|
235
|
+
local settingsPadding = Instance.new("UIPadding", settingsPanel)
|
|
236
|
+
settingsPadding.PaddingBottom = UDim.new(0, 6)
|
|
237
|
+
settingsPadding.PaddingLeft = UDim.new(0, 6)
|
|
238
|
+
settingsPadding.PaddingRight = UDim.new(0, 6)
|
|
239
|
+
settingsPadding.PaddingTop = UDim.new(0, 6)
|
|
240
|
+
local settingsLayout = Instance.new("UIListLayout", settingsPanel)
|
|
241
|
+
settingsLayout.Padding = UDim.new(0, 4)
|
|
242
|
+
|
|
243
|
+
local showSyncedButton = Instance.new("TextButton")
|
|
244
|
+
showSyncedButton.BackgroundColor3 = Color3.fromRGB(48, 48, 56)
|
|
245
|
+
showSyncedButton.BorderSizePixel = 0
|
|
246
|
+
showSyncedButton.Font = Enum.Font.BuilderSans
|
|
247
|
+
showSyncedButton.Size = UDim2.new(1, 0, 0, 26)
|
|
248
|
+
showSyncedButton.TextColor3 = Color3.fromRGB(230, 230, 235)
|
|
249
|
+
showSyncedButton.TextSize = 12
|
|
250
|
+
showSyncedButton.TextXAlignment = Enum.TextXAlignment.Left
|
|
251
|
+
showSyncedButton.Parent = settingsPanel
|
|
252
|
+
Instance.new("UICorner", showSyncedButton).CornerRadius = UDim.new(0, 4)
|
|
253
|
+
local showSyncedPadding = Instance.new("UIPadding", showSyncedButton)
|
|
254
|
+
showSyncedPadding.PaddingLeft = UDim.new(0, 8)
|
|
255
|
+
|
|
256
|
+
local autoExpandButton = showSyncedButton:Clone()
|
|
257
|
+
autoExpandButton.Parent = settingsPanel
|
|
258
|
+
|
|
259
|
+
local depthRow = Instance.new("Frame")
|
|
260
|
+
depthRow.BackgroundColor3 = Color3.fromRGB(48, 48, 56)
|
|
261
|
+
depthRow.BorderSizePixel = 0
|
|
262
|
+
depthRow.Size = UDim2.new(1, 0, 0, 26)
|
|
263
|
+
depthRow.Parent = settingsPanel
|
|
264
|
+
Instance.new("UICorner", depthRow).CornerRadius = UDim.new(0, 4)
|
|
265
|
+
|
|
266
|
+
local depthLabel = Instance.new("TextLabel")
|
|
267
|
+
depthLabel.BackgroundTransparency = 1
|
|
268
|
+
depthLabel.Font = Enum.Font.BuilderSans
|
|
269
|
+
depthLabel.Position = UDim2.new(0, 8, 0, 0)
|
|
270
|
+
depthLabel.Size = UDim2.new(1, -64, 1, 0)
|
|
271
|
+
depthLabel.TextColor3 = Color3.fromRGB(230, 230, 235)
|
|
272
|
+
depthLabel.TextSize = 12
|
|
273
|
+
depthLabel.TextXAlignment = Enum.TextXAlignment.Left
|
|
274
|
+
depthLabel.Parent = depthRow
|
|
275
|
+
|
|
276
|
+
local depthMinus = Instance.new("TextButton")
|
|
277
|
+
depthMinus.BackgroundTransparency = 1
|
|
278
|
+
depthMinus.Font = Enum.Font.BuilderSansBold
|
|
279
|
+
depthMinus.Position = UDim2.new(1, -52, 0, 0)
|
|
280
|
+
depthMinus.Size = UDim2.new(0, 26, 1, 0)
|
|
281
|
+
depthMinus.Text = "−"
|
|
282
|
+
depthMinus.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
283
|
+
depthMinus.TextSize = 16
|
|
284
|
+
depthMinus.Parent = depthRow
|
|
285
|
+
|
|
286
|
+
local depthPlus = depthMinus:Clone()
|
|
287
|
+
depthPlus.Position = UDim2.new(1, -26, 0, 0)
|
|
288
|
+
depthPlus.Text = "+"
|
|
289
|
+
depthPlus.Parent = depthRow
|
|
290
|
+
|
|
291
|
+
local statusBanner = Instance.new("TextLabel")
|
|
292
|
+
statusBanner.AutomaticSize = Enum.AutomaticSize.Y
|
|
293
|
+
statusBanner.BackgroundColor3 = Color3.fromRGB(66, 38, 40)
|
|
294
|
+
statusBanner.BorderSizePixel = 0
|
|
295
|
+
statusBanner.Font = Enum.Font.BuilderSans
|
|
296
|
+
statusBanner.LayoutOrder = 4
|
|
297
|
+
statusBanner.Size = UDim2.new(1, 0, 0, 0)
|
|
298
|
+
statusBanner.Text = ""
|
|
299
|
+
statusBanner.TextColor3 = Color3.fromRGB(255, 205, 205)
|
|
300
|
+
statusBanner.TextSize = 11
|
|
301
|
+
statusBanner.TextWrapped = true
|
|
302
|
+
statusBanner.TextXAlignment = Enum.TextXAlignment.Left
|
|
303
|
+
statusBanner.Visible = false
|
|
304
|
+
statusBanner.Parent = content
|
|
305
|
+
Instance.new("UICorner", statusBanner).CornerRadius = UDim.new(0, 5)
|
|
306
|
+
local statusBannerPadding = Instance.new("UIPadding", statusBanner)
|
|
307
|
+
statusBannerPadding.PaddingBottom = UDim.new(0, 7)
|
|
308
|
+
statusBannerPadding.PaddingLeft = UDim.new(0, 8)
|
|
309
|
+
statusBannerPadding.PaddingRight = UDim.new(0, 8)
|
|
310
|
+
statusBannerPadding.PaddingTop = UDim.new(0, 7)
|
|
311
|
+
|
|
312
|
+
local directionLegend = Instance.new("TextLabel")
|
|
313
|
+
directionLegend.BackgroundTransparency = 1
|
|
314
|
+
directionLegend.Font = Enum.Font.BuilderSans
|
|
315
|
+
directionLegend.LayoutOrder = 5
|
|
316
|
+
directionLegend.Size = UDim2.new(1, 0, 0, 18)
|
|
317
|
+
directionLegend.Text = "→ Studio to file ← File to Studio"
|
|
318
|
+
directionLegend.TextColor3 = Color3.fromRGB(145, 145, 155)
|
|
319
|
+
directionLegend.TextSize = 11
|
|
320
|
+
directionLegend.TextXAlignment = Enum.TextXAlignment.Right
|
|
321
|
+
directionLegend.Parent = content
|
|
322
|
+
|
|
323
|
+
local rootsContainer = Instance.new("Frame")
|
|
324
|
+
rootsContainer.AutomaticSize = Enum.AutomaticSize.Y
|
|
325
|
+
rootsContainer.BackgroundTransparency = 1
|
|
326
|
+
rootsContainer.LayoutOrder = 6
|
|
327
|
+
rootsContainer.Size = UDim2.new(1, 0, 0, 0)
|
|
328
|
+
rootsContainer.Parent = content
|
|
329
|
+
|
|
330
|
+
local rootsLayout = Instance.new("UIListLayout")
|
|
331
|
+
rootsLayout.Padding = UDim.new(0, 8)
|
|
332
|
+
rootsLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
333
|
+
rootsLayout.Parent = rootsContainer
|
|
334
|
+
|
|
335
|
+
local running = true
|
|
336
|
+
local bulkRunning = false
|
|
337
|
+
local requestLocked = false
|
|
338
|
+
local requestWaiters = {}
|
|
339
|
+
local lastRequestAt = 0
|
|
340
|
+
local serverConfig
|
|
341
|
+
local states = {}
|
|
342
|
+
local groups = {}
|
|
343
|
+
local refreshConfig
|
|
344
|
+
local updateGroupSummaries
|
|
345
|
+
local refreshStatusSummary
|
|
346
|
+
local refreshUnsyncedButton
|
|
347
|
+
local upload
|
|
348
|
+
local pull
|
|
349
|
+
local deleteSnapshot
|
|
350
|
+
local openGitConflict
|
|
351
|
+
local resolveGitConflict
|
|
352
|
+
local applyTreeFilter
|
|
353
|
+
local resetInstanceTrees
|
|
354
|
+
|
|
355
|
+
local function collectSplitPaths(root, depth)
|
|
356
|
+
local paths = {}
|
|
357
|
+
local function visit(instance, remainingDepth, relativePath)
|
|
358
|
+
if remainingDepth == 0 then
|
|
359
|
+
table.insert(paths, relativePath)
|
|
360
|
+
return
|
|
361
|
+
end
|
|
362
|
+
for _, child in instance:GetChildren() do
|
|
363
|
+
local childPath = table.clone(relativePath)
|
|
364
|
+
table.insert(childPath, child.Name)
|
|
365
|
+
visit(child, remainingDepth - 1, childPath)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
visit(root, depth, {})
|
|
369
|
+
return paths
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
local function acquireRequestLock()
|
|
373
|
+
if requestLocked then
|
|
374
|
+
local waiter = Instance.new("BindableEvent")
|
|
375
|
+
table.insert(requestWaiters, waiter)
|
|
376
|
+
waiter.Event:Wait()
|
|
377
|
+
waiter:Destroy()
|
|
378
|
+
end
|
|
379
|
+
requestLocked = true
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
local function releaseRequestLock()
|
|
383
|
+
local waiter = table.remove(requestWaiters, 1)
|
|
384
|
+
if waiter then
|
|
385
|
+
waiter:Fire()
|
|
386
|
+
else
|
|
387
|
+
requestLocked = false
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
local function request(method, path, body, headers)
|
|
392
|
+
local requestHeaders = table.clone(CLIENT_HEADERS)
|
|
393
|
+
local sessionMarker = ServerStorage:FindFirstChild(SESSION_MARKER_NAME)
|
|
394
|
+
if sessionMarker and sessionMarker.ClassName == "StringValue" and sessionMarker.Value ~= "" then
|
|
395
|
+
requestHeaders["X-GoldSync-Session"] = sessionMarker.Value
|
|
396
|
+
end
|
|
397
|
+
if headers then
|
|
398
|
+
for key, value in headers do
|
|
399
|
+
requestHeaders[key] = value
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
acquireRequestLock()
|
|
404
|
+
local requestError
|
|
405
|
+
for attempt = 1, #REQUEST_RETRY_DELAYS + 1 do
|
|
406
|
+
local delayUntilRequest = REQUEST_INTERVAL_SECONDS - (os.clock() - lastRequestAt)
|
|
407
|
+
if delayUntilRequest > 0 then
|
|
408
|
+
task.wait(delayUntilRequest)
|
|
409
|
+
end
|
|
410
|
+
local success, response = pcall(function()
|
|
411
|
+
return HttpService:RequestAsync({
|
|
412
|
+
Url = ENDPOINT .. path,
|
|
413
|
+
Method = method,
|
|
414
|
+
Headers = requestHeaders,
|
|
415
|
+
Body = body,
|
|
416
|
+
})
|
|
417
|
+
end)
|
|
418
|
+
lastRequestAt = os.clock()
|
|
419
|
+
if success then
|
|
420
|
+
releaseRequestLock()
|
|
421
|
+
return response, nil
|
|
422
|
+
end
|
|
423
|
+
requestError = tostring(response)
|
|
424
|
+
if not string.find(requestError, "Number of requests exceeded limit", 1, true)
|
|
425
|
+
or attempt > #REQUEST_RETRY_DELAYS
|
|
426
|
+
then
|
|
427
|
+
break
|
|
428
|
+
end
|
|
429
|
+
task.wait(REQUEST_RETRY_DELAYS[attempt])
|
|
430
|
+
end
|
|
431
|
+
releaseRequestLock()
|
|
432
|
+
return nil, requestError
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
local function responseError(response)
|
|
436
|
+
if response.Body ~= "" then
|
|
437
|
+
local success, decoded = pcall(HttpService.JSONDecode, HttpService, response.Body)
|
|
438
|
+
if success and type(decoded) == "table" and type(decoded.error) == "string" then
|
|
439
|
+
return decoded.error
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
return string.format("HTTP %d · %s", response.StatusCode, tostring(response.StatusMessage))
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
local function resolveRoot(studioPath)
|
|
446
|
+
local current = game:GetService(studioPath[1])
|
|
447
|
+
for index = 2, #studioPath do
|
|
448
|
+
current = current:FindFirstChild(studioPath[index])
|
|
449
|
+
if not current then
|
|
450
|
+
return nil
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
return current
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
local function resolveParent(studioPath)
|
|
457
|
+
local current = game:GetService(studioPath[1])
|
|
458
|
+
for index = 2, #studioPath - 1 do
|
|
459
|
+
current = current:FindFirstChild(studioPath[index])
|
|
460
|
+
if not current then
|
|
461
|
+
return nil
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
return current
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
local function applyClassIcon(image, className)
|
|
468
|
+
local success, icon = pcall(function()
|
|
469
|
+
return StudioService:GetClassIcon(className)
|
|
470
|
+
end)
|
|
471
|
+
if success then
|
|
472
|
+
for property, value in icon do
|
|
473
|
+
image[property] = value
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
local function addButtonHover(button)
|
|
479
|
+
button.MouseEnter:Connect(function()
|
|
480
|
+
button.BackgroundColor3 = Color3.fromRGB(58, 58, 68)
|
|
481
|
+
button.BackgroundTransparency = 0
|
|
482
|
+
end)
|
|
483
|
+
button.MouseLeave:Connect(function()
|
|
484
|
+
button.BackgroundTransparency = 1
|
|
485
|
+
end)
|
|
486
|
+
Instance.new("UICorner", button).CornerRadius = UDim.new(0, 3)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
local function findCode(root)
|
|
490
|
+
if root:IsA("BaseScript") then
|
|
491
|
+
return root
|
|
492
|
+
end
|
|
493
|
+
for _, descendant in root:GetDescendants() do
|
|
494
|
+
if descendant:IsA("BaseScript") then
|
|
495
|
+
return descendant
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
return nil
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
local function setStatus(state, text, color)
|
|
502
|
+
local previousStatusKind = state.statusKind
|
|
503
|
+
state.status.Text = text
|
|
504
|
+
state.status.TextColor3 = color or Color3.fromRGB(180, 180, 190)
|
|
505
|
+
state.dot.TextColor3 = color or Color3.fromRGB(180, 180, 190)
|
|
506
|
+
state.searchQuery = nil
|
|
507
|
+
state.statusKind = if string.sub(text, 1, 6) == "Synced"
|
|
508
|
+
then "synced"
|
|
509
|
+
elseif string.find(text, "syncing")
|
|
510
|
+
or string.find(text, "Serializing")
|
|
511
|
+
or string.find(text, "Loading")
|
|
512
|
+
or string.find(text, "Saving")
|
|
513
|
+
or string.find(text, "applying")
|
|
514
|
+
then "working"
|
|
515
|
+
elseif string.find(text, "Rojo") and string.find(text, "offline") then "paused"
|
|
516
|
+
else "issue"
|
|
517
|
+
if updateGroupSummaries then
|
|
518
|
+
if previousStatusKind ~= state.statusKind then
|
|
519
|
+
updateGroupSummaries()
|
|
520
|
+
elseif refreshStatusSummary then
|
|
521
|
+
refreshStatusSummary()
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
local propertyNamesByClass = {}
|
|
527
|
+
local propertySetsByClass = {}
|
|
528
|
+
|
|
529
|
+
local function buildInstanceMap(root)
|
|
530
|
+
local byKey = { [""] = root }
|
|
531
|
+
local keyByInstance = { [root] = "" }
|
|
532
|
+
local displayByKey = { [""] = root.Name }
|
|
533
|
+
local function visit(parent, parentKey, displayPath)
|
|
534
|
+
local occurrences = {}
|
|
535
|
+
for _, child in parent:GetChildren() do
|
|
536
|
+
local identity = child.ClassName .. "\0" .. child.Name
|
|
537
|
+
occurrences[identity] = (occurrences[identity] or 0) + 1
|
|
538
|
+
local key = parentKey .. "\1" .. identity .. "\0" .. occurrences[identity]
|
|
539
|
+
local childDisplayPath = displayPath .. "." .. child.Name
|
|
540
|
+
byKey[key] = child
|
|
541
|
+
keyByInstance[child] = key
|
|
542
|
+
displayByKey[key] = childDisplayPath
|
|
543
|
+
visit(child, key, childDisplayPath)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
visit(root, "", root.Name)
|
|
547
|
+
return byKey, keyByInstance, displayByKey
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
local function comparableValue(value, keyByInstance)
|
|
551
|
+
if typeof(value) == "Instance" then
|
|
552
|
+
return "Instance:" .. (keyByInstance[value] or value:GetFullName())
|
|
553
|
+
end
|
|
554
|
+
return typeof(value) .. ":" .. tostring(value)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
local function displayValue(value)
|
|
558
|
+
if #value <= 240 then
|
|
559
|
+
return value
|
|
560
|
+
end
|
|
561
|
+
return string.sub(value, 1, 237) .. "..."
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
local function getSerializedPropertyNames(className)
|
|
565
|
+
if propertyNamesByClass[className] then
|
|
566
|
+
return propertyNamesByClass[className]
|
|
567
|
+
end
|
|
568
|
+
local names = {}
|
|
569
|
+
local success, properties = pcall(function()
|
|
570
|
+
return ReflectionService:GetPropertiesOfClass(className)
|
|
571
|
+
end)
|
|
572
|
+
if success then
|
|
573
|
+
for _, property in properties do
|
|
574
|
+
if property.Serialized and property.Name ~= "UniqueId" and property.Name ~= "HistoryId" then
|
|
575
|
+
table.insert(names, property.Name)
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
table.sort(names)
|
|
579
|
+
end
|
|
580
|
+
propertyNamesByClass[className] = names
|
|
581
|
+
return names
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
local function getSerializedPropertySet(className)
|
|
585
|
+
if propertySetsByClass[className] then
|
|
586
|
+
return propertySetsByClass[className]
|
|
587
|
+
end
|
|
588
|
+
local properties = {}
|
|
589
|
+
for _, propertyName in getSerializedPropertyNames(className) do
|
|
590
|
+
properties[propertyName] = true
|
|
591
|
+
end
|
|
592
|
+
propertySetsByClass[className] = properties
|
|
593
|
+
return properties
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
local function readProperty(instance, propertyName)
|
|
597
|
+
if not instance then
|
|
598
|
+
return false, nil
|
|
599
|
+
end
|
|
600
|
+
return pcall(function()
|
|
601
|
+
return instance[propertyName]
|
|
602
|
+
end)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
local function addInspectorEntry(entry)
|
|
606
|
+
table.insert(inspectorEntries, entry)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
local function compareConflictWorkspace(state, versions, resultRoot)
|
|
610
|
+
inspectorGeneration += 1
|
|
611
|
+
inspectorState = state
|
|
612
|
+
local generation = inspectorGeneration
|
|
613
|
+
table.clear(inspectorEntries)
|
|
614
|
+
inspectorRendered = 0
|
|
615
|
+
inspectorTitle.Text = table.concat(state.manifest.studioPath, ".")
|
|
616
|
+
inspectorSummary.Text = "Scanning serialized properties..."
|
|
617
|
+
inspectorWidget.Enabled = true
|
|
618
|
+
|
|
619
|
+
local maps = {}
|
|
620
|
+
for _, stage in { "base", "ours", "theirs" } do
|
|
621
|
+
if versions[stage] then
|
|
622
|
+
local byKey, keyByInstance, displayByKey = buildInstanceMap(versions[stage])
|
|
623
|
+
maps[stage] = { byKey = byKey, keyByInstance = keyByInstance, displayByKey = displayByKey }
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
local resultByKey, resultKeyByInstance, resultDisplayByKey = buildInstanceMap(resultRoot)
|
|
627
|
+
maps.result = { byKey = resultByKey, keyByInstance = resultKeyByInstance, displayByKey = resultDisplayByKey }
|
|
628
|
+
|
|
629
|
+
local allKeys = {}
|
|
630
|
+
for _, stage in { "base", "ours", "theirs" } do
|
|
631
|
+
if maps[stage] then
|
|
632
|
+
for key in maps[stage].byKey do
|
|
633
|
+
allKeys[key] = true
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
local sortedKeys = {}
|
|
638
|
+
for key in allKeys do
|
|
639
|
+
table.insert(sortedKeys, key)
|
|
640
|
+
end
|
|
641
|
+
table.sort(sortedKeys)
|
|
642
|
+
|
|
643
|
+
local scanned = 0
|
|
644
|
+
for _, key in sortedKeys do
|
|
645
|
+
if generation ~= inspectorGeneration or not state.conflictWorkspace then
|
|
646
|
+
return
|
|
647
|
+
end
|
|
648
|
+
local base = maps.base and maps.base.byKey[key]
|
|
649
|
+
local ours = maps.ours and maps.ours.byKey[key]
|
|
650
|
+
local theirs = maps.theirs and maps.theirs.byKey[key]
|
|
651
|
+
local result = maps.result.byKey[key]
|
|
652
|
+
local displayPath = (maps.ours and maps.ours.displayByKey[key])
|
|
653
|
+
or (maps.theirs and maps.theirs.displayByKey[key])
|
|
654
|
+
or (maps.base and maps.base.displayByKey[key])
|
|
655
|
+
or resultDisplayByKey[key]
|
|
656
|
+
local basePresence = base and base.ClassName or "<deleted>"
|
|
657
|
+
local oursPresence = ours and ours.ClassName or "<deleted>"
|
|
658
|
+
local theirsPresence = theirs and theirs.ClassName or "<deleted>"
|
|
659
|
+
if basePresence ~= oursPresence or basePresence ~= theirsPresence then
|
|
660
|
+
local status = if oursPresence ~= basePresence and theirsPresence ~= basePresence
|
|
661
|
+
then (if oursPresence == theirsPresence then "same" else "conflict")
|
|
662
|
+
elseif oursPresence ~= basePresence then "ours"
|
|
663
|
+
else "theirs"
|
|
664
|
+
addInspectorEntry({
|
|
665
|
+
path = displayPath,
|
|
666
|
+
property = "(instance)",
|
|
667
|
+
baseText = basePresence,
|
|
668
|
+
oursText = oursPresence,
|
|
669
|
+
theirsText = theirsPresence,
|
|
670
|
+
status = status,
|
|
671
|
+
resultInstance = result,
|
|
672
|
+
})
|
|
673
|
+
elseif base and ours and theirs and base.ClassName == ours.ClassName and base.ClassName == theirs.ClassName then
|
|
674
|
+
for _, propertyName in getSerializedPropertyNames(base.ClassName) do
|
|
675
|
+
local baseReadable, baseValue = readProperty(base, propertyName)
|
|
676
|
+
local oursReadable, oursValue = readProperty(ours, propertyName)
|
|
677
|
+
local theirsReadable, theirsValue = readProperty(theirs, propertyName)
|
|
678
|
+
if baseReadable and oursReadable and theirsReadable then
|
|
679
|
+
local baseText = comparableValue(baseValue, maps.base.keyByInstance)
|
|
680
|
+
local oursText = comparableValue(oursValue, maps.ours.keyByInstance)
|
|
681
|
+
local theirsText = comparableValue(theirsValue, maps.theirs.keyByInstance)
|
|
682
|
+
local oursChanged = oursText ~= baseText
|
|
683
|
+
local theirsChanged = theirsText ~= baseText
|
|
684
|
+
if oursChanged or theirsChanged then
|
|
685
|
+
local status = if oursChanged and theirsChanged
|
|
686
|
+
then (if oursText == theirsText then "same" else "conflict")
|
|
687
|
+
elseif oursChanged then "ours"
|
|
688
|
+
else "theirs"
|
|
689
|
+
addInspectorEntry({
|
|
690
|
+
path = displayPath,
|
|
691
|
+
property = propertyName,
|
|
692
|
+
baseText = baseText,
|
|
693
|
+
oursText = oursText,
|
|
694
|
+
theirsText = theirsText,
|
|
695
|
+
status = status,
|
|
696
|
+
oursValue = oursValue,
|
|
697
|
+
theirsValue = theirsValue,
|
|
698
|
+
oursKeyByInstance = maps.ours.keyByInstance,
|
|
699
|
+
theirsKeyByInstance = maps.theirs.keyByInstance,
|
|
700
|
+
resultByKey = resultByKey,
|
|
701
|
+
resultInstance = result,
|
|
702
|
+
})
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
scanned += 1
|
|
708
|
+
if scanned % 100 == 0 then
|
|
709
|
+
inspectorSummary.Text = string.format("Scanning... %d instances, %d changes", scanned, #inspectorEntries)
|
|
710
|
+
task.wait()
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
local conflicts = 0
|
|
715
|
+
for _, entry in inspectorEntries do
|
|
716
|
+
if entry.status == "conflict" then
|
|
717
|
+
conflicts += 1
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
inspectorSummary.Text = string.format(
|
|
721
|
+
"%d changed properties/instances; %d true conflicts. Blue = yours, yellow = theirs, red = both, green = same.",
|
|
722
|
+
#inspectorEntries,
|
|
723
|
+
conflicts
|
|
724
|
+
)
|
|
725
|
+
renderInspector()
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
renderInspector = function()
|
|
729
|
+
for _, child in inspectorContent:GetChildren() do
|
|
730
|
+
if child ~= inspectorPadding and child ~= inspectorLayout and child ~= inspectorTitle and child ~= inspectorSummary then
|
|
731
|
+
child:Destroy()
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
local count = math.min(#inspectorEntries, inspectorRendered + 100)
|
|
735
|
+
inspectorRendered = count
|
|
736
|
+
for index = 1, count do
|
|
737
|
+
local entry = inspectorEntries[index]
|
|
738
|
+
local colors = {
|
|
739
|
+
ours = Color3.fromRGB(48, 91, 132),
|
|
740
|
+
theirs = Color3.fromRGB(123, 96, 38),
|
|
741
|
+
conflict = Color3.fromRGB(126, 52, 57),
|
|
742
|
+
same = Color3.fromRGB(45, 104, 67),
|
|
743
|
+
}
|
|
744
|
+
local row = Instance.new("Frame")
|
|
745
|
+
row.AutomaticSize = Enum.AutomaticSize.Y
|
|
746
|
+
row.BackgroundColor3 = colors[entry.status]
|
|
747
|
+
row.BorderSizePixel = 0
|
|
748
|
+
row.LayoutOrder = 10 + index
|
|
749
|
+
row.Size = UDim2.new(1, 0, 0, 0)
|
|
750
|
+
row.Parent = inspectorContent
|
|
751
|
+
Instance.new("UICorner", row).CornerRadius = UDim.new(0, 5)
|
|
752
|
+
local rowPadding = Instance.new("UIPadding", row)
|
|
753
|
+
rowPadding.PaddingBottom = UDim.new(0, 7)
|
|
754
|
+
rowPadding.PaddingLeft = UDim.new(0, 8)
|
|
755
|
+
rowPadding.PaddingRight = UDim.new(0, 8)
|
|
756
|
+
rowPadding.PaddingTop = UDim.new(0, 7)
|
|
757
|
+
local rowLayout = Instance.new("UIListLayout", row)
|
|
758
|
+
rowLayout.Padding = UDim.new(0, 4)
|
|
759
|
+
local pathButton = Instance.new("TextButton")
|
|
760
|
+
pathButton.AutomaticSize = Enum.AutomaticSize.Y
|
|
761
|
+
pathButton.BackgroundTransparency = 1
|
|
762
|
+
pathButton.Font = Enum.Font.BuilderSansBold
|
|
763
|
+
pathButton.Size = UDim2.new(1, 0, 0, 18)
|
|
764
|
+
pathButton.Text = entry.path .. " • " .. entry.property
|
|
765
|
+
pathButton.TextColor3 = Color3.fromRGB(250, 250, 252)
|
|
766
|
+
pathButton.TextSize = 13
|
|
767
|
+
pathButton.TextWrapped = true
|
|
768
|
+
pathButton.TextXAlignment = Enum.TextXAlignment.Left
|
|
769
|
+
pathButton.Parent = row
|
|
770
|
+
pathButton.Activated:Connect(function()
|
|
771
|
+
if entry.resultInstance then
|
|
772
|
+
Selection:Set({ entry.resultInstance })
|
|
773
|
+
end
|
|
774
|
+
end)
|
|
775
|
+
local values = Instance.new("TextLabel")
|
|
776
|
+
values.AutomaticSize = Enum.AutomaticSize.Y
|
|
777
|
+
values.BackgroundTransparency = 1
|
|
778
|
+
values.Font = Enum.Font.Code
|
|
779
|
+
values.Size = UDim2.new(1, 0, 0, 18)
|
|
780
|
+
values.Text = "Base: "
|
|
781
|
+
.. displayValue(entry.baseText)
|
|
782
|
+
.. "\nYours: "
|
|
783
|
+
.. displayValue(entry.oursText)
|
|
784
|
+
.. "\nTheirs: "
|
|
785
|
+
.. displayValue(entry.theirsText)
|
|
786
|
+
values.TextColor3 = Color3.fromRGB(232, 232, 238)
|
|
787
|
+
values.TextSize = 11
|
|
788
|
+
values.TextWrapped = true
|
|
789
|
+
values.TextXAlignment = Enum.TextXAlignment.Left
|
|
790
|
+
values.Parent = row
|
|
791
|
+
if entry.property ~= "(instance)" and entry.resultInstance then
|
|
792
|
+
local actions = Instance.new("Frame")
|
|
793
|
+
actions.BackgroundTransparency = 1
|
|
794
|
+
actions.Size = UDim2.new(1, 0, 0, 24)
|
|
795
|
+
actions.Parent = row
|
|
796
|
+
local actionsLayout = Instance.new("UIListLayout", actions)
|
|
797
|
+
actionsLayout.FillDirection = Enum.FillDirection.Horizontal
|
|
798
|
+
actionsLayout.Padding = UDim.new(0, 6)
|
|
799
|
+
for buttonIndex, side in { "ours", "theirs" } do
|
|
800
|
+
local button = Instance.new("TextButton")
|
|
801
|
+
button.BackgroundColor3 = Color3.fromRGB(50, 50, 58)
|
|
802
|
+
button.BorderSizePixel = 0
|
|
803
|
+
button.Font = Enum.Font.BuilderSansBold
|
|
804
|
+
button.Size = UDim2.new(0.5, -3, 1, 0)
|
|
805
|
+
button.Text = if side == "ours" then "Use Yours" else "Use Theirs"
|
|
806
|
+
button.TextColor3 = Color3.fromRGB(245, 245, 248)
|
|
807
|
+
button.TextSize = 11
|
|
808
|
+
button.LayoutOrder = buttonIndex
|
|
809
|
+
button.Parent = actions
|
|
810
|
+
Instance.new("UICorner", button).CornerRadius = UDim.new(0, 4)
|
|
811
|
+
button.Activated:Connect(function()
|
|
812
|
+
local value = entry[side .. "Value"]
|
|
813
|
+
if typeof(value) == "Instance" then
|
|
814
|
+
value = entry.resultByKey[entry[side .. "KeyByInstance"][value]]
|
|
815
|
+
end
|
|
816
|
+
local applied = pcall(function()
|
|
817
|
+
entry.resultInstance[entry.property] = value
|
|
818
|
+
end)
|
|
819
|
+
if applied then
|
|
820
|
+
Selection:Set({ entry.resultInstance })
|
|
821
|
+
end
|
|
822
|
+
end)
|
|
823
|
+
end
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
if inspectorRendered < #inspectorEntries then
|
|
827
|
+
local more = Instance.new("TextButton")
|
|
828
|
+
more.BackgroundColor3 = Color3.fromRGB(70, 70, 80)
|
|
829
|
+
more.BorderSizePixel = 0
|
|
830
|
+
more.Font = Enum.Font.BuilderSansBold
|
|
831
|
+
more.LayoutOrder = 1000000
|
|
832
|
+
more.Size = UDim2.new(1, 0, 0, 32)
|
|
833
|
+
more.Text = string.format("Load 100 more (%d remaining)", #inspectorEntries - inspectorRendered)
|
|
834
|
+
more.TextColor3 = Color3.fromRGB(240, 240, 245)
|
|
835
|
+
more.TextSize = 12
|
|
836
|
+
more.Parent = inspectorContent
|
|
837
|
+
Instance.new("UICorner", more).CornerRadius = UDim.new(0, 5)
|
|
838
|
+
more.Activated:Connect(renderInspector)
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
local function disconnectRoot(state)
|
|
843
|
+
for _, connection in state.rootConnections do
|
|
844
|
+
connection:Disconnect()
|
|
845
|
+
end
|
|
846
|
+
table.clear(state.rootConnections)
|
|
847
|
+
for instance, connections in state.instanceConnections do
|
|
848
|
+
for _, connection in connections do
|
|
849
|
+
connection:Disconnect()
|
|
850
|
+
end
|
|
851
|
+
state.instanceConnections[instance] = nil
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
local function markDirty(state)
|
|
856
|
+
if state.suppress or state.conflict or state.deleting or not state.root then
|
|
857
|
+
return
|
|
858
|
+
end
|
|
859
|
+
state.dirty = true
|
|
860
|
+
state.generation += 1
|
|
861
|
+
local generation = state.generation
|
|
862
|
+
setStatus(state, "Studio changed; syncing...", Color3.fromRGB(244, 194, 92))
|
|
863
|
+
task.delay((serverConfig and serverConfig.debounceMs or 600) / 1000, function()
|
|
864
|
+
if running and state.generation == generation and state.dirty and not state.conflict and not state.deleting then
|
|
865
|
+
upload(state, false)
|
|
866
|
+
end
|
|
867
|
+
end)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
local function watchInstance(state, instance)
|
|
871
|
+
if state.instanceConnections[instance] then
|
|
872
|
+
return
|
|
873
|
+
end
|
|
874
|
+
local connections = {
|
|
875
|
+
instance.AttributeChanged:Connect(function()
|
|
876
|
+
markDirty(state)
|
|
877
|
+
end),
|
|
878
|
+
}
|
|
879
|
+
if instance:IsA("ValueBase") then
|
|
880
|
+
for _, propertyName in getSerializedPropertyNames(instance.ClassName) do
|
|
881
|
+
local connected, connection = pcall(function()
|
|
882
|
+
return instance:GetPropertyChangedSignal(propertyName):Connect(function()
|
|
883
|
+
markDirty(state)
|
|
884
|
+
end)
|
|
885
|
+
end)
|
|
886
|
+
if connected then
|
|
887
|
+
table.insert(connections, connection)
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
else
|
|
891
|
+
local serializedProperties = getSerializedPropertySet(instance.ClassName)
|
|
892
|
+
table.insert(connections, instance.Changed:Connect(function(propertyName)
|
|
893
|
+
if serializedProperties[propertyName] then
|
|
894
|
+
markDirty(state)
|
|
895
|
+
end
|
|
896
|
+
end))
|
|
897
|
+
end
|
|
898
|
+
state.instanceConnections[instance] = connections
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
local function watchRoot(state, root)
|
|
902
|
+
disconnectRoot(state)
|
|
903
|
+
state.root = root
|
|
904
|
+
watchInstance(state, root)
|
|
905
|
+
for _, descendant in root:GetDescendants() do
|
|
906
|
+
watchInstance(state, descendant)
|
|
907
|
+
end
|
|
908
|
+
table.insert(state.rootConnections, root.DescendantAdded:Connect(function(descendant)
|
|
909
|
+
watchInstance(state, descendant)
|
|
910
|
+
markDirty(state)
|
|
911
|
+
end))
|
|
912
|
+
table.insert(state.rootConnections, root.DescendantRemoving:Connect(function(descendant)
|
|
913
|
+
local connections = state.instanceConnections[descendant]
|
|
914
|
+
if connections then
|
|
915
|
+
for _, connection in connections do
|
|
916
|
+
connection:Disconnect()
|
|
917
|
+
end
|
|
918
|
+
state.instanceConnections[descendant] = nil
|
|
919
|
+
end
|
|
920
|
+
markDirty(state)
|
|
921
|
+
end))
|
|
922
|
+
table.insert(state.rootConnections, root.AncestryChanged:Connect(function()
|
|
923
|
+
if not state.suppress and resolveRoot(state.manifest.studioPath) ~= root then
|
|
924
|
+
deleteSnapshot(state)
|
|
925
|
+
end
|
|
926
|
+
end))
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
local function rememberHash(state, hash)
|
|
930
|
+
state.lastHash = hash
|
|
931
|
+
plugin:SetSetting(
|
|
932
|
+
SETTINGS_PREFIX .. serverConfig.projectId .. ":" .. serverConfig.workspaceId .. ":" .. state.manifest.id .. ":hash",
|
|
933
|
+
hash
|
|
934
|
+
)
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
local function forgetHash(state)
|
|
938
|
+
state.lastHash = nil
|
|
939
|
+
plugin:SetSetting(
|
|
940
|
+
SETTINGS_PREFIX .. serverConfig.projectId .. ":" .. serverConfig.workspaceId .. ":" .. state.manifest.id .. ":hash",
|
|
941
|
+
nil
|
|
942
|
+
)
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
deleteSnapshot = function(state)
|
|
946
|
+
if state.uploading or state.pulling or state.deleting then
|
|
947
|
+
return
|
|
948
|
+
end
|
|
949
|
+
state.generation += 1
|
|
950
|
+
state.dirty = false
|
|
951
|
+
if state.manifest.gitConflict or not state.lastHash then
|
|
952
|
+
state.conflict = true
|
|
953
|
+
setStatus(state, "Root removed; resolve the existing file conflict manually.", Color3.fromRGB(245, 106, 106))
|
|
954
|
+
return
|
|
955
|
+
end
|
|
956
|
+
if not serverConfig.rojoConnected then
|
|
957
|
+
state.conflict = true
|
|
958
|
+
setStatus(state, "Root removed while Rojo is offline.", Color3.fromRGB(244, 194, 92))
|
|
959
|
+
return
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
state.deleting = true
|
|
963
|
+
setStatus(state, "Deleting file snapshot...", Color3.fromRGB(115, 182, 255))
|
|
964
|
+
local response, requestError = request("DELETE", "/v1/roots/" .. state.manifest.id .. "/snapshot", nil, {
|
|
965
|
+
["If-Match"] = state.lastHash,
|
|
966
|
+
})
|
|
967
|
+
state.deleting = false
|
|
968
|
+
if not response then
|
|
969
|
+
state.conflict = true
|
|
970
|
+
setStatus(state, "Daemon unavailable: " .. requestError, Color3.fromRGB(245, 106, 106))
|
|
971
|
+
return
|
|
972
|
+
end
|
|
973
|
+
if response.StatusCode == 409 then
|
|
974
|
+
local reason = responseError(response)
|
|
975
|
+
state.conflict = string.find(reason, "snapshot changed", 1, true) ~= nil
|
|
976
|
+
setStatus(
|
|
977
|
+
state,
|
|
978
|
+
if state.conflict then "Conflict: file changed before Studio deleted it." else "Paused — " .. reason,
|
|
979
|
+
if state.conflict then Color3.fromRGB(245, 106, 106) else Color3.fromRGB(244, 194, 92)
|
|
980
|
+
)
|
|
981
|
+
return
|
|
982
|
+
end
|
|
983
|
+
if not response.Success then
|
|
984
|
+
state.conflict = true
|
|
985
|
+
setStatus(state, "Delete failed (HTTP " .. response.StatusCode .. ")", Color3.fromRGB(245, 106, 106))
|
|
986
|
+
return
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
disconnectRoot(state)
|
|
990
|
+
state.root = nil
|
|
991
|
+
state.manifest = HttpService:JSONDecode(response.Body)
|
|
992
|
+
state.conflict = false
|
|
993
|
+
forgetHash(state)
|
|
994
|
+
setStatus(state, "Deleted Studio → file", Color3.fromRGB(102, 214, 139))
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
upload = function(state, overwrite)
|
|
998
|
+
if state.uploading or state.pulling or state.deleting then
|
|
999
|
+
return
|
|
1000
|
+
end
|
|
1001
|
+
if state.manifest.gitConflict then
|
|
1002
|
+
setStatus(state, "Resolve the Git conflict before syncing.", Color3.fromRGB(245, 106, 106))
|
|
1003
|
+
return
|
|
1004
|
+
end
|
|
1005
|
+
if not serverConfig.rojoConnected then
|
|
1006
|
+
setStatus(state, "Paused — Rojo is offline.", Color3.fromRGB(244, 194, 92))
|
|
1007
|
+
return
|
|
1008
|
+
end
|
|
1009
|
+
local root = resolveRoot(state.manifest.studioPath)
|
|
1010
|
+
if not root then
|
|
1011
|
+
state.conflict = true
|
|
1012
|
+
setStatus(state, "Studio root is missing. Pull to restore it.", Color3.fromRGB(245, 106, 106))
|
|
1013
|
+
return
|
|
1014
|
+
end
|
|
1015
|
+
local scriptInstance = findCode(root)
|
|
1016
|
+
if scriptInstance then
|
|
1017
|
+
state.conflict = true
|
|
1018
|
+
setStatus(state, "Blocked: contains code at " .. scriptInstance:GetFullName(), Color3.fromRGB(245, 106, 106))
|
|
1019
|
+
return
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
state.uploading = true
|
|
1023
|
+
setStatus(state, "Serializing Studio root...", Color3.fromRGB(115, 182, 255))
|
|
1024
|
+
local serialized, serializationError
|
|
1025
|
+
local success, result = pcall(function()
|
|
1026
|
+
return SerializationService:SerializeInstancesAsync({ root })
|
|
1027
|
+
end)
|
|
1028
|
+
if success then
|
|
1029
|
+
serialized = result
|
|
1030
|
+
else
|
|
1031
|
+
serializationError = tostring(result)
|
|
1032
|
+
end
|
|
1033
|
+
if not serialized then
|
|
1034
|
+
state.uploading = false
|
|
1035
|
+
setStatus(state, "Serialization failed: " .. serializationError, Color3.fromRGB(245, 106, 106))
|
|
1036
|
+
return
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
local expectedHash = "*"
|
|
1040
|
+
if overwrite then
|
|
1041
|
+
expectedHash = state.manifest.exists and state.manifest.hash or "*"
|
|
1042
|
+
elseif state.lastHash then
|
|
1043
|
+
expectedHash = state.lastHash
|
|
1044
|
+
end
|
|
1045
|
+
local response, requestError = request("PUT", "/v1/roots/" .. state.manifest.id .. "/snapshot", buffer.tostring(serialized), {
|
|
1046
|
+
["Content-Type"] = "application/octet-stream",
|
|
1047
|
+
["If-Match"] = expectedHash,
|
|
1048
|
+
})
|
|
1049
|
+
state.uploading = false
|
|
1050
|
+
if not response then
|
|
1051
|
+
setStatus(state, "Daemon unavailable: " .. requestError, Color3.fromRGB(245, 106, 106))
|
|
1052
|
+
return
|
|
1053
|
+
end
|
|
1054
|
+
if response.StatusCode == 409 then
|
|
1055
|
+
local reason = responseError(response)
|
|
1056
|
+
state.conflict = string.find(reason, "snapshot changed", 1, true) ~= nil
|
|
1057
|
+
setStatus(
|
|
1058
|
+
state,
|
|
1059
|
+
if state.conflict then "Conflict: disk changed too. Push or Pull." else "Paused — " .. reason,
|
|
1060
|
+
if state.conflict then Color3.fromRGB(245, 106, 106) else Color3.fromRGB(244, 194, 92)
|
|
1061
|
+
)
|
|
1062
|
+
return
|
|
1063
|
+
end
|
|
1064
|
+
if not response.Success then
|
|
1065
|
+
setStatus(state, "Upload failed (HTTP " .. response.StatusCode .. ")", Color3.fromRGB(245, 106, 106))
|
|
1066
|
+
return
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
local manifest = HttpService:JSONDecode(response.Body)
|
|
1070
|
+
state.manifest = manifest
|
|
1071
|
+
state.dirty = false
|
|
1072
|
+
state.conflict = false
|
|
1073
|
+
rememberHash(state, manifest.hash)
|
|
1074
|
+
watchRoot(state, root)
|
|
1075
|
+
setStatus(state, "Synced Studio → file", Color3.fromRGB(102, 214, 139))
|
|
1076
|
+
end
|
|
1077
|
+
|
|
1078
|
+
pull = function(state)
|
|
1079
|
+
if state.uploading or state.pulling or state.deleting then
|
|
1080
|
+
return
|
|
1081
|
+
end
|
|
1082
|
+
if state.manifest.gitConflict then
|
|
1083
|
+
setStatus(state, "Resolve the Git conflict before syncing.", Color3.fromRGB(245, 106, 106))
|
|
1084
|
+
return
|
|
1085
|
+
end
|
|
1086
|
+
if not serverConfig.rojoConnected then
|
|
1087
|
+
setStatus(state, "Paused — Rojo is offline.", Color3.fromRGB(244, 194, 92))
|
|
1088
|
+
return
|
|
1089
|
+
end
|
|
1090
|
+
if not state.manifest.exists then
|
|
1091
|
+
setStatus(state, "No file snapshot exists. Push Studio first.", Color3.fromRGB(245, 106, 106))
|
|
1092
|
+
return
|
|
1093
|
+
end
|
|
1094
|
+
local parent = resolveParent(state.manifest.studioPath)
|
|
1095
|
+
if not parent then
|
|
1096
|
+
setStatus(state, "Studio parent path is missing.", Color3.fromRGB(245, 106, 106))
|
|
1097
|
+
return
|
|
1098
|
+
end
|
|
1099
|
+
local currentRoot = resolveRoot(state.manifest.studioPath)
|
|
1100
|
+
local currentScript = currentRoot and findCode(currentRoot)
|
|
1101
|
+
if currentScript then
|
|
1102
|
+
state.conflict = true
|
|
1103
|
+
setStatus(state, "Blocked: Studio root contains code at " .. currentScript:GetFullName(), Color3.fromRGB(245, 106, 106))
|
|
1104
|
+
return
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
state.pulling = true
|
|
1108
|
+
setStatus(state, "Loading file snapshot...", Color3.fromRGB(115, 182, 255))
|
|
1109
|
+
local response, requestError = request("GET", "/v1/roots/" .. state.manifest.id .. "/snapshot")
|
|
1110
|
+
if not response then
|
|
1111
|
+
state.pulling = false
|
|
1112
|
+
setStatus(state, "Daemon unavailable: " .. requestError, Color3.fromRGB(245, 106, 106))
|
|
1113
|
+
return
|
|
1114
|
+
end
|
|
1115
|
+
if not response.Success then
|
|
1116
|
+
state.pulling = false
|
|
1117
|
+
setStatus(state, "Download failed (HTTP " .. response.StatusCode .. ")", Color3.fromRGB(245, 106, 106))
|
|
1118
|
+
return
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
local success, instances = pcall(function()
|
|
1122
|
+
return SerializationService:DeserializeInstancesAsync(buffer.fromstring(response.Body))
|
|
1123
|
+
end)
|
|
1124
|
+
if not success then
|
|
1125
|
+
state.pulling = false
|
|
1126
|
+
setStatus(state, "Invalid RBXM: " .. tostring(instances), Color3.fromRGB(245, 106, 106))
|
|
1127
|
+
return
|
|
1128
|
+
end
|
|
1129
|
+
if #instances ~= 1 then
|
|
1130
|
+
for _, instance in instances do
|
|
1131
|
+
instance:Destroy()
|
|
1132
|
+
end
|
|
1133
|
+
state.pulling = false
|
|
1134
|
+
setStatus(state, "Blocked: snapshot must contain exactly one root.", Color3.fromRGB(245, 106, 106))
|
|
1135
|
+
return
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
local replacement = instances[1]
|
|
1139
|
+
local expectedName = state.manifest.studioPath[#state.manifest.studioPath]
|
|
1140
|
+
local scriptInstance = findCode(replacement)
|
|
1141
|
+
if replacement.Name ~= expectedName or scriptInstance then
|
|
1142
|
+
replacement:Destroy()
|
|
1143
|
+
state.pulling = false
|
|
1144
|
+
if scriptInstance then
|
|
1145
|
+
setStatus(state, "Blocked: file snapshot contains code.", Color3.fromRGB(245, 106, 106))
|
|
1146
|
+
else
|
|
1147
|
+
setStatus(state, "Blocked: root must be named " .. expectedName, Color3.fromRGB(245, 106, 106))
|
|
1148
|
+
end
|
|
1149
|
+
return
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
state.suppress = true
|
|
1153
|
+
disconnectRoot(state)
|
|
1154
|
+
local previous = resolveRoot(state.manifest.studioPath)
|
|
1155
|
+
ChangeHistoryService:SetWaypoint("GoldSync before Pull")
|
|
1156
|
+
local applied, applyError = pcall(function()
|
|
1157
|
+
replacement.Parent = parent
|
|
1158
|
+
end)
|
|
1159
|
+
if not applied then
|
|
1160
|
+
replacement:Destroy()
|
|
1161
|
+
state.suppress = false
|
|
1162
|
+
state.pulling = false
|
|
1163
|
+
if previous then
|
|
1164
|
+
watchRoot(state, previous)
|
|
1165
|
+
end
|
|
1166
|
+
setStatus(state, "Could not apply snapshot: " .. tostring(applyError), Color3.fromRGB(245, 106, 106))
|
|
1167
|
+
return
|
|
1168
|
+
end
|
|
1169
|
+
if previous then
|
|
1170
|
+
previous:Destroy()
|
|
1171
|
+
end
|
|
1172
|
+
ChangeHistoryService:SetWaypoint("GoldSync Pull " .. expectedName)
|
|
1173
|
+
state.root = replacement
|
|
1174
|
+
state.dirty = false
|
|
1175
|
+
state.conflict = false
|
|
1176
|
+
state.pulling = false
|
|
1177
|
+
rememberHash(state, state.manifest.hash)
|
|
1178
|
+
watchRoot(state, replacement)
|
|
1179
|
+
state.suppress = false
|
|
1180
|
+
setStatus(state, "Synced file → Studio", Color3.fromRGB(102, 214, 139))
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1183
|
+
local function deserializeConflictStage(state, stage)
|
|
1184
|
+
if not state.manifest.conflictStages[stage] then
|
|
1185
|
+
return nil
|
|
1186
|
+
end
|
|
1187
|
+
local response, requestError = request("GET", "/v1/roots/" .. state.manifest.id .. "/conflict/" .. stage)
|
|
1188
|
+
if not response then
|
|
1189
|
+
return nil, requestError
|
|
1190
|
+
end
|
|
1191
|
+
if not response.Success then
|
|
1192
|
+
return nil, "HTTP " .. response.StatusCode
|
|
1193
|
+
end
|
|
1194
|
+
local success, instances = pcall(function()
|
|
1195
|
+
return SerializationService:DeserializeInstancesAsync(buffer.fromstring(response.Body))
|
|
1196
|
+
end)
|
|
1197
|
+
if not success then
|
|
1198
|
+
return nil, tostring(instances)
|
|
1199
|
+
end
|
|
1200
|
+
if #instances ~= 1 then
|
|
1201
|
+
for _, instance in instances do
|
|
1202
|
+
instance:Destroy()
|
|
1203
|
+
end
|
|
1204
|
+
return nil, "snapshot must contain exactly one root"
|
|
1205
|
+
end
|
|
1206
|
+
if findCode(instances[1]) then
|
|
1207
|
+
instances[1]:Destroy()
|
|
1208
|
+
return nil, "snapshot contains code"
|
|
1209
|
+
end
|
|
1210
|
+
return instances[1]
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
local function destroyConflictWorkspace(state)
|
|
1214
|
+
if inspectorState == state then
|
|
1215
|
+
inspectorGeneration += 1
|
|
1216
|
+
inspectorState = nil
|
|
1217
|
+
table.clear(inspectorEntries)
|
|
1218
|
+
inspectorRendered = 0
|
|
1219
|
+
inspectorTitle.Text = "No conflict workspace open"
|
|
1220
|
+
inspectorSummary.Text = "Open Merge Workspace from a conflicted root."
|
|
1221
|
+
renderInspector()
|
|
1222
|
+
end
|
|
1223
|
+
if state.conflictWorkspace then
|
|
1224
|
+
local parent = state.conflictWorkspace.Parent
|
|
1225
|
+
state.conflictWorkspace:Destroy()
|
|
1226
|
+
state.conflictWorkspace = nil
|
|
1227
|
+
if parent and #parent:GetChildren() == 0 then
|
|
1228
|
+
parent:Destroy()
|
|
1229
|
+
end
|
|
1230
|
+
end
|
|
1231
|
+
state.mergeButton.Text = "Open Merge Workspace"
|
|
1232
|
+
state.mergeButton.Size = UDim2.fromScale(1, 1)
|
|
1233
|
+
state.diffButton.Visible = false
|
|
1234
|
+
end
|
|
1235
|
+
|
|
1236
|
+
openGitConflict = function(state)
|
|
1237
|
+
if state.uploading or state.pulling or not state.manifest.gitConflict then
|
|
1238
|
+
return
|
|
1239
|
+
end
|
|
1240
|
+
if state.conflictWorkspace and state.conflictWorkspace.Parent then
|
|
1241
|
+
Selection:Set({ state.conflictWorkspace })
|
|
1242
|
+
setStatus(state, "Edit ServerStorage.GoldSync Conflicts." .. state.manifest.id .. ".Result, then click Resolve Result.", Color3.fromRGB(244, 194, 92))
|
|
1243
|
+
return
|
|
1244
|
+
elseif state.conflictWorkspace then
|
|
1245
|
+
state.conflictWorkspace = nil
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
state.pulling = true
|
|
1249
|
+
setStatus(state, "Loading Git conflict versions...", Color3.fromRGB(115, 182, 255))
|
|
1250
|
+
local versions = {}
|
|
1251
|
+
for _, stage in { "base", "ours", "theirs" } do
|
|
1252
|
+
local instance, stageError = deserializeConflictStage(state, stage)
|
|
1253
|
+
if stageError then
|
|
1254
|
+
for _, loaded in versions do
|
|
1255
|
+
loaded:Destroy()
|
|
1256
|
+
end
|
|
1257
|
+
state.pulling = false
|
|
1258
|
+
setStatus(state, "Could not load " .. stage .. ": " .. stageError, Color3.fromRGB(245, 106, 106))
|
|
1259
|
+
return
|
|
1260
|
+
end
|
|
1261
|
+
if instance then
|
|
1262
|
+
versions[stage] = instance
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
local resultStage = if versions.ours then "ours" else "theirs"
|
|
1267
|
+
if not versions[resultStage] then
|
|
1268
|
+
for _, loaded in versions do
|
|
1269
|
+
loaded:Destroy()
|
|
1270
|
+
end
|
|
1271
|
+
state.pulling = false
|
|
1272
|
+
setStatus(state, "Conflict has no usable version.", Color3.fromRGB(245, 106, 106))
|
|
1273
|
+
return
|
|
1274
|
+
end
|
|
1275
|
+
local resultRoot, resultError = deserializeConflictStage(state, resultStage)
|
|
1276
|
+
if not resultRoot then
|
|
1277
|
+
for _, loaded in versions do
|
|
1278
|
+
loaded:Destroy()
|
|
1279
|
+
end
|
|
1280
|
+
state.pulling = false
|
|
1281
|
+
setStatus(state, "Could not create Result: " .. resultError, Color3.fromRGB(245, 106, 106))
|
|
1282
|
+
return
|
|
1283
|
+
end
|
|
1284
|
+
|
|
1285
|
+
local workspaces = ServerStorage:FindFirstChild("GoldSync Conflicts")
|
|
1286
|
+
if not workspaces then
|
|
1287
|
+
workspaces = Instance.new("Folder")
|
|
1288
|
+
workspaces.Name = "GoldSync Conflicts"
|
|
1289
|
+
workspaces.Archivable = false
|
|
1290
|
+
workspaces.Parent = ServerStorage
|
|
1291
|
+
end
|
|
1292
|
+
local existing = workspaces:FindFirstChild(state.manifest.id)
|
|
1293
|
+
if existing then
|
|
1294
|
+
existing:Destroy()
|
|
1295
|
+
end
|
|
1296
|
+
|
|
1297
|
+
local workspace = Instance.new("Folder")
|
|
1298
|
+
workspace.Name = state.manifest.id
|
|
1299
|
+
workspace.Archivable = false
|
|
1300
|
+
workspace:SetAttribute("GoldSyncWorkspaceId", serverConfig.workspaceId)
|
|
1301
|
+
workspace.Parent = workspaces
|
|
1302
|
+
for _, stage in { "base", "ours", "theirs" } do
|
|
1303
|
+
local versionFolder = Instance.new("Folder")
|
|
1304
|
+
versionFolder.Name = if stage == "base" then "Base" elseif stage == "ours" then "Yours" else "Theirs"
|
|
1305
|
+
versionFolder.Parent = workspace
|
|
1306
|
+
if versions[stage] then
|
|
1307
|
+
versions[stage].Parent = versionFolder
|
|
1308
|
+
else
|
|
1309
|
+
versionFolder:SetAttribute("Deleted", true)
|
|
1310
|
+
end
|
|
1311
|
+
end
|
|
1312
|
+
local resultFolder = Instance.new("Folder")
|
|
1313
|
+
resultFolder.Name = "Result"
|
|
1314
|
+
resultFolder.Parent = workspace
|
|
1315
|
+
resultRoot.Parent = resultFolder
|
|
1316
|
+
|
|
1317
|
+
state.conflictWorkspace = workspace
|
|
1318
|
+
state.pulling = false
|
|
1319
|
+
state.mergeButton.Text = "Resolve Result"
|
|
1320
|
+
state.mergeButton.Size = UDim2.new(0.5, -3, 1, 0)
|
|
1321
|
+
state.diffButton.Visible = true
|
|
1322
|
+
Selection:Set({ workspace })
|
|
1323
|
+
setStatus(state, "Edit Result in ServerStorage.GoldSync Conflicts, then click Resolve Result.", Color3.fromRGB(244, 194, 92))
|
|
1324
|
+
task.spawn(compareConflictWorkspace, state, versions, resultRoot)
|
|
1325
|
+
end
|
|
1326
|
+
|
|
1327
|
+
resolveGitConflict = function(state)
|
|
1328
|
+
local workspace = state.conflictWorkspace
|
|
1329
|
+
if not workspace or state.uploading or state.pulling then
|
|
1330
|
+
return
|
|
1331
|
+
end
|
|
1332
|
+
local resultChildren = workspace.Result:GetChildren()
|
|
1333
|
+
if #resultChildren ~= 1 then
|
|
1334
|
+
setStatus(state, "Result must contain exactly one root.", Color3.fromRGB(245, 106, 106))
|
|
1335
|
+
return
|
|
1336
|
+
end
|
|
1337
|
+
local resultRoot = resultChildren[1]
|
|
1338
|
+
local expectedName = state.manifest.studioPath[#state.manifest.studioPath]
|
|
1339
|
+
if resultRoot.Name ~= expectedName or findCode(resultRoot) then
|
|
1340
|
+
setStatus(state, "Result must be a code-free root named " .. expectedName .. ".", Color3.fromRGB(245, 106, 106))
|
|
1341
|
+
return
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
state.uploading = true
|
|
1345
|
+
setStatus(state, "Saving merged result...", Color3.fromRGB(115, 182, 255))
|
|
1346
|
+
local success, serialized = pcall(function()
|
|
1347
|
+
return SerializationService:SerializeInstancesAsync({ resultRoot })
|
|
1348
|
+
end)
|
|
1349
|
+
if not success then
|
|
1350
|
+
state.uploading = false
|
|
1351
|
+
setStatus(state, "Serialization failed: " .. tostring(serialized), Color3.fromRGB(245, 106, 106))
|
|
1352
|
+
return
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
local response, requestError = request(
|
|
1356
|
+
"PUT",
|
|
1357
|
+
"/v1/roots/" .. state.manifest.id .. "/conflict/resolve",
|
|
1358
|
+
buffer.tostring(serialized),
|
|
1359
|
+
{
|
|
1360
|
+
["Content-Type"] = "application/octet-stream",
|
|
1361
|
+
["X-GoldSync-Conflict-Token"] = state.manifest.conflictToken,
|
|
1362
|
+
}
|
|
1363
|
+
)
|
|
1364
|
+
state.uploading = false
|
|
1365
|
+
if not response then
|
|
1366
|
+
setStatus(state, "Daemon unavailable: " .. requestError, Color3.fromRGB(245, 106, 106))
|
|
1367
|
+
return
|
|
1368
|
+
end
|
|
1369
|
+
if not response.Success then
|
|
1370
|
+
setStatus(state, "Resolve failed (HTTP " .. response.StatusCode .. ")", Color3.fromRGB(245, 106, 106))
|
|
1371
|
+
return
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
destroyConflictWorkspace(state)
|
|
1375
|
+
state.conflict = false
|
|
1376
|
+
setStatus(state, "Git conflict resolved; applying result...", Color3.fromRGB(102, 214, 139))
|
|
1377
|
+
refreshConfig()
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
local function setGroupExpanded(group, expanded)
|
|
1381
|
+
group.expanded = expanded
|
|
1382
|
+
group.children.Visible = expanded
|
|
1383
|
+
group.disclosure.Text = if expanded then "▾" else "▸"
|
|
1384
|
+
group.name.Text = group.label .. group.summary
|
|
1385
|
+
end
|
|
1386
|
+
|
|
1387
|
+
local function pathStartsWith(prefix, fullPath)
|
|
1388
|
+
if #prefix > #fullPath then
|
|
1389
|
+
return false
|
|
1390
|
+
end
|
|
1391
|
+
for index, segment in prefix do
|
|
1392
|
+
if fullPath[index] ~= segment then
|
|
1393
|
+
return false
|
|
1394
|
+
end
|
|
1395
|
+
end
|
|
1396
|
+
return true
|
|
1397
|
+
end
|
|
1398
|
+
|
|
1399
|
+
local function createGroup(path, order)
|
|
1400
|
+
local key = table.concat(path, "\0")
|
|
1401
|
+
if groups[key] then
|
|
1402
|
+
return groups[key]
|
|
1403
|
+
end
|
|
1404
|
+
local parentGroup
|
|
1405
|
+
if #path > 1 then
|
|
1406
|
+
local parentPath = table.clone(path)
|
|
1407
|
+
table.remove(parentPath)
|
|
1408
|
+
parentGroup = createGroup(parentPath, order)
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
local section = Instance.new("Frame")
|
|
1412
|
+
section.AutomaticSize = Enum.AutomaticSize.Y
|
|
1413
|
+
section.BackgroundTransparency = 1
|
|
1414
|
+
section.LayoutOrder = order
|
|
1415
|
+
section.Size = UDim2.new(1, 0, 0, 0)
|
|
1416
|
+
section.Parent = if parentGroup then parentGroup.list else rootsContainer
|
|
1417
|
+
if parentGroup then
|
|
1418
|
+
local connector = Instance.new("Frame")
|
|
1419
|
+
connector.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
1420
|
+
connector.BorderSizePixel = 0
|
|
1421
|
+
connector.Position = UDim2.new(0, -8, 0, 11)
|
|
1422
|
+
connector.Size = UDim2.fromOffset(35, 1)
|
|
1423
|
+
connector.Parent = section
|
|
1424
|
+
end
|
|
1425
|
+
|
|
1426
|
+
local sectionLayout = Instance.new("UIListLayout")
|
|
1427
|
+
sectionLayout.Padding = UDim.new(0, 2)
|
|
1428
|
+
sectionLayout.Parent = section
|
|
1429
|
+
|
|
1430
|
+
local header = Instance.new("Frame")
|
|
1431
|
+
header.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
1432
|
+
header.BackgroundTransparency = 1
|
|
1433
|
+
header.BorderSizePixel = 0
|
|
1434
|
+
header.LayoutOrder = 1
|
|
1435
|
+
header.Size = UDim2.new(1, 0, 0, 24)
|
|
1436
|
+
header.Parent = section
|
|
1437
|
+
|
|
1438
|
+
local disclosure = Instance.new("TextButton")
|
|
1439
|
+
disclosure.BackgroundTransparency = 1
|
|
1440
|
+
disclosure.Font = Enum.Font.BuilderSansBold
|
|
1441
|
+
disclosure.Size = UDim2.new(0, 18, 1, 0)
|
|
1442
|
+
disclosure.TextColor3 = Color3.fromRGB(180, 180, 188)
|
|
1443
|
+
disclosure.TextSize = 12
|
|
1444
|
+
disclosure.Parent = header
|
|
1445
|
+
addButtonHover(disclosure)
|
|
1446
|
+
|
|
1447
|
+
local icon = Instance.new("ImageLabel")
|
|
1448
|
+
icon.BackgroundTransparency = 1
|
|
1449
|
+
icon.Position = UDim2.new(0, 19, 0.5, -8)
|
|
1450
|
+
icon.Size = UDim2.fromOffset(16, 16)
|
|
1451
|
+
icon.Parent = header
|
|
1452
|
+
local parentInstance = resolveRoot(path)
|
|
1453
|
+
applyClassIcon(icon, if parentInstance then parentInstance.ClassName else "Folder")
|
|
1454
|
+
|
|
1455
|
+
local name = Instance.new("TextButton")
|
|
1456
|
+
name.BackgroundTransparency = 1
|
|
1457
|
+
name.Font = Enum.Font.BuilderSansBold
|
|
1458
|
+
name.Position = UDim2.new(0, 39, 0, 0)
|
|
1459
|
+
name.Size = UDim2.new(1, -101, 1, 0)
|
|
1460
|
+
name.TextColor3 = Color3.fromRGB(226, 226, 232)
|
|
1461
|
+
name.TextSize = 13
|
|
1462
|
+
name.TextXAlignment = Enum.TextXAlignment.Left
|
|
1463
|
+
name.Parent = header
|
|
1464
|
+
|
|
1465
|
+
local statusDot = Instance.new("TextLabel")
|
|
1466
|
+
statusDot.BackgroundTransparency = 1
|
|
1467
|
+
statusDot.Font = Enum.Font.BuilderSansBold
|
|
1468
|
+
statusDot.Position = UDim2.new(1, -60, 0, 0)
|
|
1469
|
+
statusDot.Size = UDim2.new(0, 12, 1, 0)
|
|
1470
|
+
statusDot.Text = "●"
|
|
1471
|
+
statusDot.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
1472
|
+
statusDot.TextSize = 9
|
|
1473
|
+
statusDot.Parent = header
|
|
1474
|
+
|
|
1475
|
+
local bulkPush = Instance.new("TextButton")
|
|
1476
|
+
bulkPush.BackgroundTransparency = 1
|
|
1477
|
+
bulkPush.Font = Enum.Font.BuilderSansBold
|
|
1478
|
+
bulkPush.Position = UDim2.new(1, -48, 0, 0)
|
|
1479
|
+
bulkPush.Size = UDim2.new(0, 24, 1, 0)
|
|
1480
|
+
bulkPush.Text = "→"
|
|
1481
|
+
bulkPush.TextColor3 = Color3.fromRGB(115, 182, 255)
|
|
1482
|
+
bulkPush.TextSize = 16
|
|
1483
|
+
bulkPush.Visible = false
|
|
1484
|
+
bulkPush.Parent = header
|
|
1485
|
+
addButtonHover(bulkPush)
|
|
1486
|
+
|
|
1487
|
+
local bulkPull = Instance.new("TextButton")
|
|
1488
|
+
bulkPull.BackgroundTransparency = 1
|
|
1489
|
+
bulkPull.Font = Enum.Font.BuilderSansBold
|
|
1490
|
+
bulkPull.Position = UDim2.new(1, -24, 0, 0)
|
|
1491
|
+
bulkPull.Size = UDim2.new(0, 24, 1, 0)
|
|
1492
|
+
bulkPull.Text = "←"
|
|
1493
|
+
bulkPull.TextColor3 = Color3.fromRGB(115, 182, 255)
|
|
1494
|
+
bulkPull.TextSize = 16
|
|
1495
|
+
bulkPull.Visible = false
|
|
1496
|
+
bulkPull.Parent = header
|
|
1497
|
+
addButtonHover(bulkPull)
|
|
1498
|
+
|
|
1499
|
+
header.MouseEnter:Connect(function()
|
|
1500
|
+
header.BackgroundTransparency = 0
|
|
1501
|
+
end)
|
|
1502
|
+
header.MouseLeave:Connect(function()
|
|
1503
|
+
header.BackgroundTransparency = 1
|
|
1504
|
+
end)
|
|
1505
|
+
|
|
1506
|
+
local children = Instance.new("Frame")
|
|
1507
|
+
children.AutomaticSize = Enum.AutomaticSize.Y
|
|
1508
|
+
children.BackgroundTransparency = 1
|
|
1509
|
+
children.LayoutOrder = 2
|
|
1510
|
+
children.Size = UDim2.new(1, 0, 0, 0)
|
|
1511
|
+
children.Visible = false
|
|
1512
|
+
children.Parent = section
|
|
1513
|
+
local guide = Instance.new("Frame")
|
|
1514
|
+
guide.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
1515
|
+
guide.BorderSizePixel = 0
|
|
1516
|
+
guide.Position = UDim2.new(0, 6, 0, 0)
|
|
1517
|
+
guide.Size = UDim2.fromOffset(1, 0)
|
|
1518
|
+
guide.Parent = children
|
|
1519
|
+
|
|
1520
|
+
local childList = Instance.new("Frame")
|
|
1521
|
+
childList.AutomaticSize = Enum.AutomaticSize.Y
|
|
1522
|
+
childList.BackgroundTransparency = 1
|
|
1523
|
+
childList.Position = UDim2.new(0, 14, 0, 0)
|
|
1524
|
+
childList.Size = UDim2.new(1, -14, 0, 0)
|
|
1525
|
+
childList.Parent = children
|
|
1526
|
+
local childrenLayout = Instance.new("UIListLayout", childList)
|
|
1527
|
+
childrenLayout.Padding = UDim.new(0, 2)
|
|
1528
|
+
childrenLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
1529
|
+
childrenLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
|
|
1530
|
+
guide.Size = UDim2.fromOffset(1, childrenLayout.AbsoluteContentSize.Y)
|
|
1531
|
+
end)
|
|
1532
|
+
|
|
1533
|
+
local group = {
|
|
1534
|
+
key = key,
|
|
1535
|
+
label = path[#path],
|
|
1536
|
+
path = table.clone(path),
|
|
1537
|
+
section = section,
|
|
1538
|
+
header = header,
|
|
1539
|
+
disclosure = disclosure,
|
|
1540
|
+
name = name,
|
|
1541
|
+
statusDot = statusDot,
|
|
1542
|
+
bulkPush = bulkPush,
|
|
1543
|
+
bulkPull = bulkPull,
|
|
1544
|
+
children = children,
|
|
1545
|
+
list = childList,
|
|
1546
|
+
expanded = false,
|
|
1547
|
+
hasDirectRoots = false,
|
|
1548
|
+
summary = "",
|
|
1549
|
+
}
|
|
1550
|
+
groups[key] = group
|
|
1551
|
+
setGroupExpanded(group, #path <= 2)
|
|
1552
|
+
disclosure.Activated:Connect(function()
|
|
1553
|
+
setGroupExpanded(group, not group.expanded)
|
|
1554
|
+
end)
|
|
1555
|
+
name.Activated:Connect(function()
|
|
1556
|
+
setGroupExpanded(group, not group.expanded)
|
|
1557
|
+
end)
|
|
1558
|
+
bulkPush.Activated:Connect(function()
|
|
1559
|
+
if bulkRunning then
|
|
1560
|
+
return
|
|
1561
|
+
end
|
|
1562
|
+
bulkRunning = true
|
|
1563
|
+
for _, state in states do
|
|
1564
|
+
if pathStartsWith(group.path, state.manifest.studioPath) and not state.manifest.gitConflict then
|
|
1565
|
+
upload(state, true)
|
|
1566
|
+
end
|
|
1567
|
+
end
|
|
1568
|
+
bulkRunning = false
|
|
1569
|
+
end)
|
|
1570
|
+
bulkPull.Activated:Connect(function()
|
|
1571
|
+
if bulkRunning then
|
|
1572
|
+
return
|
|
1573
|
+
end
|
|
1574
|
+
bulkRunning = true
|
|
1575
|
+
for _, state in states do
|
|
1576
|
+
if pathStartsWith(group.path, state.manifest.studioPath) and not state.manifest.gitConflict then
|
|
1577
|
+
pull(state)
|
|
1578
|
+
end
|
|
1579
|
+
end
|
|
1580
|
+
bulkRunning = false
|
|
1581
|
+
end)
|
|
1582
|
+
return group
|
|
1583
|
+
end
|
|
1584
|
+
|
|
1585
|
+
local function setRootExpanded(state, expanded)
|
|
1586
|
+
state.expanded = expanded
|
|
1587
|
+
state.row.BackgroundTransparency = 1
|
|
1588
|
+
state.file.Visible = expanded
|
|
1589
|
+
state.status.Visible = expanded
|
|
1590
|
+
state.buttons.Visible = expanded
|
|
1591
|
+
state.infoButton.TextColor3 = if expanded then Color3.fromRGB(235, 235, 240) else Color3.fromRGB(150, 150, 160)
|
|
1592
|
+
end
|
|
1593
|
+
|
|
1594
|
+
updateGroupSummaries = function()
|
|
1595
|
+
for _, group in groups do
|
|
1596
|
+
local total = 0
|
|
1597
|
+
local synced = 0
|
|
1598
|
+
local issues = 0
|
|
1599
|
+
local pending = 0
|
|
1600
|
+
local paused = 0
|
|
1601
|
+
for _, state in states do
|
|
1602
|
+
if pathStartsWith(group.path, state.manifest.studioPath) then
|
|
1603
|
+
total += 1
|
|
1604
|
+
if state.statusKind == "synced" then
|
|
1605
|
+
synced += 1
|
|
1606
|
+
elseif state.statusKind == "issue" then
|
|
1607
|
+
issues += 1
|
|
1608
|
+
elseif state.statusKind == "paused" then
|
|
1609
|
+
paused += 1
|
|
1610
|
+
else
|
|
1611
|
+
pending += 1
|
|
1612
|
+
end
|
|
1613
|
+
end
|
|
1614
|
+
end
|
|
1615
|
+
group.summary = string.format(" (%d/%d) synced", synced, total)
|
|
1616
|
+
if issues > 0 then
|
|
1617
|
+
group.summary ..= string.format(" · %d need attention", issues)
|
|
1618
|
+
elseif pending > 0 then
|
|
1619
|
+
group.summary ..= string.format(" · %d pending", pending)
|
|
1620
|
+
elseif paused > 0 then
|
|
1621
|
+
group.summary ..= string.format(" · %d paused", paused)
|
|
1622
|
+
end
|
|
1623
|
+
if total == 0 then
|
|
1624
|
+
group.section.Visible = false
|
|
1625
|
+
else
|
|
1626
|
+
group.section.Visible = true
|
|
1627
|
+
group.bulkPush.Visible = true
|
|
1628
|
+
group.bulkPull.Visible = true
|
|
1629
|
+
group.statusDot.TextColor3 = if issues > 0
|
|
1630
|
+
then Color3.fromRGB(245, 106, 106)
|
|
1631
|
+
elseif synced == total then Color3.fromRGB(102, 214, 139)
|
|
1632
|
+
elseif pending > 0 or paused > 0 then Color3.fromRGB(244, 194, 92)
|
|
1633
|
+
else Color3.fromRGB(180, 180, 190)
|
|
1634
|
+
group.bulkPush.Active = serverConfig.rojoConnected
|
|
1635
|
+
group.bulkPull.Active = serverConfig.rojoConnected
|
|
1636
|
+
group.bulkPush.AutoButtonColor = serverConfig.rojoConnected
|
|
1637
|
+
group.bulkPull.AutoButtonColor = serverConfig.rojoConnected
|
|
1638
|
+
local arrowColor = if serverConfig.rojoConnected
|
|
1639
|
+
then Color3.fromRGB(115, 182, 255)
|
|
1640
|
+
else Color3.fromRGB(95, 95, 105)
|
|
1641
|
+
group.bulkPush.TextColor3 = arrowColor
|
|
1642
|
+
group.bulkPull.TextColor3 = arrowColor
|
|
1643
|
+
setGroupExpanded(group, group.expanded or (issues > 0 and autoExpandConflicts))
|
|
1644
|
+
end
|
|
1645
|
+
end
|
|
1646
|
+
if refreshUnsyncedButton then
|
|
1647
|
+
refreshUnsyncedButton()
|
|
1648
|
+
end
|
|
1649
|
+
if refreshStatusSummary then
|
|
1650
|
+
refreshStatusSummary()
|
|
1651
|
+
end
|
|
1652
|
+
end
|
|
1653
|
+
|
|
1654
|
+
refreshUnsyncedButton = function()
|
|
1655
|
+
local unsynced = 0
|
|
1656
|
+
for _, state in states do
|
|
1657
|
+
if state.statusKind ~= "synced" then
|
|
1658
|
+
unsynced += 1
|
|
1659
|
+
end
|
|
1660
|
+
end
|
|
1661
|
+
unsyncedButton.Text = (if showSynced then "" else "✓ ") .. string.format("Unsynced · %d", unsynced)
|
|
1662
|
+
unsyncedButton.BackgroundColor3 = if showSynced
|
|
1663
|
+
then Color3.fromRGB(40, 40, 46)
|
|
1664
|
+
else Color3.fromRGB(78, 66, 38)
|
|
1665
|
+
unsyncedButton.TextColor3 = if showSynced
|
|
1666
|
+
then Color3.fromRGB(190, 190, 198)
|
|
1667
|
+
else Color3.fromRGB(255, 218, 126)
|
|
1668
|
+
end
|
|
1669
|
+
|
|
1670
|
+
refreshStatusSummary = function()
|
|
1671
|
+
local issues = {}
|
|
1672
|
+
local working = {}
|
|
1673
|
+
for _, state in states do
|
|
1674
|
+
local entry = {
|
|
1675
|
+
path = table.concat(state.manifest.studioPath, "."),
|
|
1676
|
+
status = state.status.Text,
|
|
1677
|
+
}
|
|
1678
|
+
if state.statusKind == "issue" then
|
|
1679
|
+
table.insert(issues, entry)
|
|
1680
|
+
elseif state.statusKind == "working" then
|
|
1681
|
+
table.insert(working, entry)
|
|
1682
|
+
end
|
|
1683
|
+
end
|
|
1684
|
+
local entries = if #issues > 0 then issues else working
|
|
1685
|
+
if #entries == 0 then
|
|
1686
|
+
statusBanner.Visible = false
|
|
1687
|
+
return
|
|
1688
|
+
end
|
|
1689
|
+
table.sort(entries, function(left, right)
|
|
1690
|
+
return left.path < right.path
|
|
1691
|
+
end)
|
|
1692
|
+
local lines = {
|
|
1693
|
+
if #issues > 0
|
|
1694
|
+
then string.format("%d need attention", #issues)
|
|
1695
|
+
else string.format("%d still syncing", #working),
|
|
1696
|
+
}
|
|
1697
|
+
for index = 1, math.min(#entries, 3) do
|
|
1698
|
+
local entry = entries[index]
|
|
1699
|
+
table.insert(lines, entry.path .. " — " .. entry.status)
|
|
1700
|
+
end
|
|
1701
|
+
if #entries > 3 then
|
|
1702
|
+
table.insert(lines, string.format("+%d more", #entries - 3))
|
|
1703
|
+
end
|
|
1704
|
+
statusBanner.BackgroundColor3 = if #issues > 0
|
|
1705
|
+
then Color3.fromRGB(66, 38, 40)
|
|
1706
|
+
else Color3.fromRGB(66, 56, 34)
|
|
1707
|
+
statusBanner.TextColor3 = if #issues > 0
|
|
1708
|
+
then Color3.fromRGB(255, 205, 205)
|
|
1709
|
+
else Color3.fromRGB(255, 224, 145)
|
|
1710
|
+
statusBanner.Text = table.concat(lines, "\n")
|
|
1711
|
+
statusBanner.Visible = true
|
|
1712
|
+
end
|
|
1713
|
+
|
|
1714
|
+
local function createInstanceNode(instance, parent, depth, order)
|
|
1715
|
+
local section = Instance.new("Frame")
|
|
1716
|
+
section.AutomaticSize = Enum.AutomaticSize.Y
|
|
1717
|
+
section.BackgroundTransparency = 1
|
|
1718
|
+
section.LayoutOrder = order
|
|
1719
|
+
section.Size = UDim2.new(1, 0, 0, 0)
|
|
1720
|
+
section.Parent = parent
|
|
1721
|
+
local sectionLayout = Instance.new("UIListLayout", section)
|
|
1722
|
+
sectionLayout.Padding = UDim.new(0, 2)
|
|
1723
|
+
|
|
1724
|
+
local connector = Instance.new("Frame")
|
|
1725
|
+
connector.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
1726
|
+
connector.BorderSizePixel = 0
|
|
1727
|
+
connector.Position = UDim2.new(0, -8, 0, 11)
|
|
1728
|
+
connector.Size = UDim2.fromOffset(35, 1)
|
|
1729
|
+
connector.Parent = section
|
|
1730
|
+
|
|
1731
|
+
local header = Instance.new("Frame")
|
|
1732
|
+
header.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
1733
|
+
header.BackgroundTransparency = 1
|
|
1734
|
+
header.LayoutOrder = 1
|
|
1735
|
+
header.Size = UDim2.new(1, 0, 0, 22)
|
|
1736
|
+
header.Parent = section
|
|
1737
|
+
header.MouseEnter:Connect(function()
|
|
1738
|
+
header.BackgroundTransparency = 0
|
|
1739
|
+
end)
|
|
1740
|
+
header.MouseLeave:Connect(function()
|
|
1741
|
+
header.BackgroundTransparency = 1
|
|
1742
|
+
end)
|
|
1743
|
+
|
|
1744
|
+
local disclosure = Instance.new("TextButton")
|
|
1745
|
+
disclosure.BackgroundTransparency = 1
|
|
1746
|
+
disclosure.Font = Enum.Font.BuilderSansBold
|
|
1747
|
+
disclosure.Size = UDim2.new(0, 18, 1, 0)
|
|
1748
|
+
disclosure.Text = "▸"
|
|
1749
|
+
disclosure.TextColor3 = Color3.fromRGB(170, 170, 180)
|
|
1750
|
+
disclosure.TextSize = 11
|
|
1751
|
+
disclosure.Visible = depth < visualDepth and #instance:GetChildren() > 0
|
|
1752
|
+
disclosure.Parent = header
|
|
1753
|
+
addButtonHover(disclosure)
|
|
1754
|
+
|
|
1755
|
+
local icon = Instance.new("ImageLabel")
|
|
1756
|
+
icon.BackgroundTransparency = 1
|
|
1757
|
+
icon.Position = UDim2.new(0, 19, 0.5, -8)
|
|
1758
|
+
icon.Size = UDim2.fromOffset(16, 16)
|
|
1759
|
+
icon.Parent = header
|
|
1760
|
+
applyClassIcon(icon, instance.ClassName)
|
|
1761
|
+
|
|
1762
|
+
local name = Instance.new("TextButton")
|
|
1763
|
+
name.BackgroundTransparency = 1
|
|
1764
|
+
name.Font = Enum.Font.BuilderSans
|
|
1765
|
+
name.Position = UDim2.new(0, 39, 0, 0)
|
|
1766
|
+
name.Size = UDim2.new(1, -39, 1, 0)
|
|
1767
|
+
name.Text = instance.Name
|
|
1768
|
+
name.TextColor3 = Color3.fromRGB(218, 218, 225)
|
|
1769
|
+
name.TextSize = 12
|
|
1770
|
+
name.TextTruncate = Enum.TextTruncate.AtEnd
|
|
1771
|
+
name.TextXAlignment = Enum.TextXAlignment.Left
|
|
1772
|
+
name.Parent = header
|
|
1773
|
+
name.Activated:Connect(function()
|
|
1774
|
+
if instance.Parent then
|
|
1775
|
+
Selection:Set({ instance })
|
|
1776
|
+
end
|
|
1777
|
+
end)
|
|
1778
|
+
|
|
1779
|
+
local children = Instance.new("Frame")
|
|
1780
|
+
children.AutomaticSize = Enum.AutomaticSize.Y
|
|
1781
|
+
children.BackgroundTransparency = 1
|
|
1782
|
+
children.LayoutOrder = 2
|
|
1783
|
+
children.Size = UDim2.new(1, 0, 0, 0)
|
|
1784
|
+
children.Visible = false
|
|
1785
|
+
children.Parent = section
|
|
1786
|
+
local guide = Instance.new("Frame")
|
|
1787
|
+
guide.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
1788
|
+
guide.BorderSizePixel = 0
|
|
1789
|
+
guide.Position = UDim2.new(0, 6, 0, 0)
|
|
1790
|
+
guide.Size = UDim2.fromOffset(1, 0)
|
|
1791
|
+
guide.Parent = children
|
|
1792
|
+
local childList = Instance.new("Frame")
|
|
1793
|
+
childList.AutomaticSize = Enum.AutomaticSize.Y
|
|
1794
|
+
childList.BackgroundTransparency = 1
|
|
1795
|
+
childList.Position = UDim2.new(0, 14, 0, 0)
|
|
1796
|
+
childList.Size = UDim2.new(1, -14, 0, 0)
|
|
1797
|
+
childList.Parent = children
|
|
1798
|
+
local childLayout = Instance.new("UIListLayout", childList)
|
|
1799
|
+
childLayout.Padding = UDim.new(0, 2)
|
|
1800
|
+
childLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
1801
|
+
childLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
|
|
1802
|
+
guide.Size = UDim2.fromOffset(1, childLayout.AbsoluteContentSize.Y)
|
|
1803
|
+
end)
|
|
1804
|
+
|
|
1805
|
+
local expanded = false
|
|
1806
|
+
disclosure.Activated:Connect(function()
|
|
1807
|
+
expanded = not expanded
|
|
1808
|
+
disclosure.Text = if expanded then "▾" else "▸"
|
|
1809
|
+
children.Visible = expanded
|
|
1810
|
+
for _, child in childList:GetChildren() do
|
|
1811
|
+
if child ~= childLayout then
|
|
1812
|
+
child:Destroy()
|
|
1813
|
+
end
|
|
1814
|
+
end
|
|
1815
|
+
if expanded then
|
|
1816
|
+
local childInstances = instance:GetChildren()
|
|
1817
|
+
table.sort(childInstances, function(left, right)
|
|
1818
|
+
return string.lower(left.Name) < string.lower(right.Name)
|
|
1819
|
+
end)
|
|
1820
|
+
for childOrder, child in childInstances do
|
|
1821
|
+
createInstanceNode(child, childList, depth + 1, childOrder)
|
|
1822
|
+
end
|
|
1823
|
+
end
|
|
1824
|
+
end)
|
|
1825
|
+
end
|
|
1826
|
+
|
|
1827
|
+
local function resetStateContents(state)
|
|
1828
|
+
for _, child in state.contentsList:GetChildren() do
|
|
1829
|
+
if child ~= state.contentsLayout then
|
|
1830
|
+
child:Destroy()
|
|
1831
|
+
end
|
|
1832
|
+
end
|
|
1833
|
+
state.contentsLoaded = false
|
|
1834
|
+
state.contentsExpanded = false
|
|
1835
|
+
state.contents.Visible = false
|
|
1836
|
+
state.contentDisclosure.Text = "▸"
|
|
1837
|
+
local root = resolveRoot(state.manifest.studioPath)
|
|
1838
|
+
state.contentDisclosure.Visible = visualDepth > 0 and root ~= nil and #root:GetChildren() > 0
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1841
|
+
resetInstanceTrees = function()
|
|
1842
|
+
for _, state in states do
|
|
1843
|
+
resetStateContents(state)
|
|
1844
|
+
end
|
|
1845
|
+
end
|
|
1846
|
+
|
|
1847
|
+
local function toggleRootContents(state)
|
|
1848
|
+
if visualDepth == 0 then
|
|
1849
|
+
return
|
|
1850
|
+
end
|
|
1851
|
+
state.contentsExpanded = not state.contentsExpanded
|
|
1852
|
+
state.contents.Visible = state.contentsExpanded
|
|
1853
|
+
state.contentDisclosure.Text = if state.contentsExpanded then "▾" else "▸"
|
|
1854
|
+
for _, child in state.contentsList:GetChildren() do
|
|
1855
|
+
if child ~= state.contentsLayout then
|
|
1856
|
+
child:Destroy()
|
|
1857
|
+
end
|
|
1858
|
+
end
|
|
1859
|
+
state.contentsLoaded = false
|
|
1860
|
+
if not state.contentsExpanded then
|
|
1861
|
+
return
|
|
1862
|
+
end
|
|
1863
|
+
local root = resolveRoot(state.manifest.studioPath)
|
|
1864
|
+
if not root then
|
|
1865
|
+
return
|
|
1866
|
+
end
|
|
1867
|
+
local children = root:GetChildren()
|
|
1868
|
+
table.sort(children, function(left, right)
|
|
1869
|
+
return string.lower(left.Name) < string.lower(right.Name)
|
|
1870
|
+
end)
|
|
1871
|
+
for order, child in children do
|
|
1872
|
+
createInstanceNode(child, state.contentsList, 1, order)
|
|
1873
|
+
end
|
|
1874
|
+
state.contentsLoaded = true
|
|
1875
|
+
end
|
|
1876
|
+
|
|
1877
|
+
local function createRootRow(manifest, order)
|
|
1878
|
+
local parentPath = table.clone(manifest.studioPath)
|
|
1879
|
+
table.remove(parentPath)
|
|
1880
|
+
local group = createGroup(parentPath, order)
|
|
1881
|
+
group.hasDirectRoots = true
|
|
1882
|
+
group.bulkPush.Visible = true
|
|
1883
|
+
group.bulkPull.Visible = true
|
|
1884
|
+
local row = Instance.new("Frame")
|
|
1885
|
+
row.AutomaticSize = Enum.AutomaticSize.Y
|
|
1886
|
+
row.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
1887
|
+
row.BackgroundTransparency = 1
|
|
1888
|
+
row.BorderSizePixel = 0
|
|
1889
|
+
row.LayoutOrder = order
|
|
1890
|
+
row.Size = UDim2.new(1, 0, 0, 0)
|
|
1891
|
+
row.Parent = group.list
|
|
1892
|
+
local connector = Instance.new("Frame")
|
|
1893
|
+
connector.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
1894
|
+
connector.BorderSizePixel = 0
|
|
1895
|
+
connector.Position = UDim2.new(0, -8, 0, 11)
|
|
1896
|
+
connector.Size = UDim2.fromOffset(35, 1)
|
|
1897
|
+
connector.Parent = row
|
|
1898
|
+
|
|
1899
|
+
local corner = Instance.new("UICorner")
|
|
1900
|
+
corner.CornerRadius = UDim.new(0, 6)
|
|
1901
|
+
corner.Parent = row
|
|
1902
|
+
|
|
1903
|
+
local rowPadding = Instance.new("UIPadding")
|
|
1904
|
+
rowPadding.PaddingBottom = UDim.new(0, 5)
|
|
1905
|
+
rowPadding.PaddingLeft = UDim.new(0, 6)
|
|
1906
|
+
rowPadding.PaddingRight = UDim.new(0, 6)
|
|
1907
|
+
rowPadding.PaddingTop = UDim.new(0, 5)
|
|
1908
|
+
rowPadding.Parent = row
|
|
1909
|
+
|
|
1910
|
+
local rowLayout = Instance.new("UIListLayout")
|
|
1911
|
+
rowLayout.Padding = UDim.new(0, 5)
|
|
1912
|
+
rowLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
1913
|
+
rowLayout.Parent = row
|
|
1914
|
+
|
|
1915
|
+
local titleLine = Instance.new("Frame")
|
|
1916
|
+
titleLine.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
1917
|
+
titleLine.BackgroundTransparency = 1
|
|
1918
|
+
titleLine.LayoutOrder = 1
|
|
1919
|
+
titleLine.Size = UDim2.new(1, 0, 0, 22)
|
|
1920
|
+
titleLine.Parent = row
|
|
1921
|
+
|
|
1922
|
+
local contentDisclosure = Instance.new("TextButton")
|
|
1923
|
+
contentDisclosure.BackgroundTransparency = 1
|
|
1924
|
+
contentDisclosure.Font = Enum.Font.BuilderSansBold
|
|
1925
|
+
contentDisclosure.Size = UDim2.new(0, 18, 1, 0)
|
|
1926
|
+
contentDisclosure.Text = "▸"
|
|
1927
|
+
contentDisclosure.TextColor3 = Color3.fromRGB(170, 170, 180)
|
|
1928
|
+
contentDisclosure.TextSize = 11
|
|
1929
|
+
contentDisclosure.Visible = false
|
|
1930
|
+
contentDisclosure.Parent = titleLine
|
|
1931
|
+
addButtonHover(contentDisclosure)
|
|
1932
|
+
|
|
1933
|
+
local icon = Instance.new("ImageLabel")
|
|
1934
|
+
icon.BackgroundTransparency = 1
|
|
1935
|
+
icon.Position = UDim2.new(0, 19, 0.5, -8)
|
|
1936
|
+
icon.Size = UDim2.fromOffset(16, 16)
|
|
1937
|
+
icon.Parent = titleLine
|
|
1938
|
+
local rootInstance = resolveRoot(manifest.studioPath)
|
|
1939
|
+
applyClassIcon(icon, if rootInstance then rootInstance.ClassName else "Model")
|
|
1940
|
+
|
|
1941
|
+
local title = Instance.new("TextButton")
|
|
1942
|
+
title.BackgroundTransparency = 1
|
|
1943
|
+
title.Font = Enum.Font.BuilderSansBold
|
|
1944
|
+
title.Position = UDim2.new(0, 39, 0, 0)
|
|
1945
|
+
title.Size = UDim2.new(1, -109, 1, 0)
|
|
1946
|
+
title.Text = manifest.studioPath[#manifest.studioPath]
|
|
1947
|
+
title.TextColor3 = Color3.fromRGB(235, 235, 240)
|
|
1948
|
+
title.TextSize = 13
|
|
1949
|
+
title.TextXAlignment = Enum.TextXAlignment.Left
|
|
1950
|
+
title.Parent = titleLine
|
|
1951
|
+
|
|
1952
|
+
local dot = Instance.new("TextLabel")
|
|
1953
|
+
dot.BackgroundTransparency = 1
|
|
1954
|
+
dot.Font = Enum.Font.BuilderSansBold
|
|
1955
|
+
dot.Position = UDim2.new(1, -70, 0, 0)
|
|
1956
|
+
dot.Size = UDim2.new(0, 12, 1, 0)
|
|
1957
|
+
dot.Text = "●"
|
|
1958
|
+
dot.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
1959
|
+
dot.TextSize = 9
|
|
1960
|
+
dot.Parent = titleLine
|
|
1961
|
+
|
|
1962
|
+
local infoButton = Instance.new("TextButton")
|
|
1963
|
+
infoButton.BackgroundTransparency = 1
|
|
1964
|
+
infoButton.Font = Enum.Font.BuilderSansBold
|
|
1965
|
+
infoButton.Position = UDim2.new(1, -58, 0, 0)
|
|
1966
|
+
infoButton.Size = UDim2.new(0, 18, 1, 0)
|
|
1967
|
+
infoButton.Text = "•••"
|
|
1968
|
+
infoButton.TextColor3 = Color3.fromRGB(150, 150, 160)
|
|
1969
|
+
infoButton.TextSize = 10
|
|
1970
|
+
infoButton.Parent = titleLine
|
|
1971
|
+
addButtonHover(infoButton)
|
|
1972
|
+
|
|
1973
|
+
local miniPush = Instance.new("TextButton")
|
|
1974
|
+
miniPush.BackgroundTransparency = 1
|
|
1975
|
+
miniPush.Font = Enum.Font.BuilderSansBold
|
|
1976
|
+
miniPush.Position = UDim2.new(1, -40, 0, 0)
|
|
1977
|
+
miniPush.Size = UDim2.new(0, 20, 1, 0)
|
|
1978
|
+
miniPush.Text = "→"
|
|
1979
|
+
miniPush.TextColor3 = Color3.fromRGB(115, 182, 255)
|
|
1980
|
+
miniPush.TextSize = 15
|
|
1981
|
+
miniPush.Parent = titleLine
|
|
1982
|
+
addButtonHover(miniPush)
|
|
1983
|
+
|
|
1984
|
+
local miniPull = Instance.new("TextButton")
|
|
1985
|
+
miniPull.BackgroundTransparency = 1
|
|
1986
|
+
miniPull.Font = Enum.Font.BuilderSansBold
|
|
1987
|
+
miniPull.Position = UDim2.new(1, -20, 0, 0)
|
|
1988
|
+
miniPull.Size = UDim2.new(0, 20, 1, 0)
|
|
1989
|
+
miniPull.Text = "←"
|
|
1990
|
+
miniPull.TextColor3 = Color3.fromRGB(115, 182, 255)
|
|
1991
|
+
miniPull.TextSize = 15
|
|
1992
|
+
miniPull.Parent = titleLine
|
|
1993
|
+
addButtonHover(miniPull)
|
|
1994
|
+
|
|
1995
|
+
titleLine.MouseEnter:Connect(function()
|
|
1996
|
+
titleLine.BackgroundTransparency = 0
|
|
1997
|
+
end)
|
|
1998
|
+
titleLine.MouseLeave:Connect(function()
|
|
1999
|
+
titleLine.BackgroundTransparency = 1
|
|
2000
|
+
end)
|
|
2001
|
+
|
|
2002
|
+
local file = Instance.new("TextLabel")
|
|
2003
|
+
file.BackgroundTransparency = 1
|
|
2004
|
+
file.Font = Enum.Font.Code
|
|
2005
|
+
file.LayoutOrder = 2
|
|
2006
|
+
file.Size = UDim2.new(1, 0, 0, 18)
|
|
2007
|
+
file.Text = manifest.file
|
|
2008
|
+
file.TextColor3 = Color3.fromRGB(145, 145, 155)
|
|
2009
|
+
file.TextSize = 11
|
|
2010
|
+
file.TextTruncate = Enum.TextTruncate.AtEnd
|
|
2011
|
+
file.TextXAlignment = Enum.TextXAlignment.Left
|
|
2012
|
+
file.Visible = false
|
|
2013
|
+
file.Parent = row
|
|
2014
|
+
|
|
2015
|
+
local status = Instance.new("TextLabel")
|
|
2016
|
+
status.AutomaticSize = Enum.AutomaticSize.Y
|
|
2017
|
+
status.BackgroundTransparency = 1
|
|
2018
|
+
status.Font = Enum.Font.BuilderSans
|
|
2019
|
+
status.LayoutOrder = 3
|
|
2020
|
+
status.Size = UDim2.new(1, 0, 0, 18)
|
|
2021
|
+
status.Text = "Checking..."
|
|
2022
|
+
status.TextColor3 = Color3.fromRGB(180, 180, 190)
|
|
2023
|
+
status.TextSize = 12
|
|
2024
|
+
status.TextWrapped = true
|
|
2025
|
+
status.TextXAlignment = Enum.TextXAlignment.Left
|
|
2026
|
+
status.Visible = false
|
|
2027
|
+
status.Parent = row
|
|
2028
|
+
|
|
2029
|
+
local buttons = Instance.new("Frame")
|
|
2030
|
+
buttons.BackgroundTransparency = 1
|
|
2031
|
+
buttons.LayoutOrder = 4
|
|
2032
|
+
buttons.Size = UDim2.new(1, 0, 0, 26)
|
|
2033
|
+
buttons.Visible = false
|
|
2034
|
+
buttons.Parent = row
|
|
2035
|
+
|
|
2036
|
+
local buttonsLayout = Instance.new("UIListLayout")
|
|
2037
|
+
buttonsLayout.FillDirection = Enum.FillDirection.Horizontal
|
|
2038
|
+
buttonsLayout.Padding = UDim.new(0, 6)
|
|
2039
|
+
buttonsLayout.Parent = buttons
|
|
2040
|
+
|
|
2041
|
+
local pushButton = Instance.new("TextButton")
|
|
2042
|
+
pushButton.BackgroundColor3 = Color3.fromRGB(70, 70, 80)
|
|
2043
|
+
pushButton.BorderSizePixel = 0
|
|
2044
|
+
pushButton.Font = Enum.Font.BuilderSansBold
|
|
2045
|
+
pushButton.Size = UDim2.new(0.5, -3, 1, 0)
|
|
2046
|
+
pushButton.Text = "Push Studio"
|
|
2047
|
+
pushButton.TextColor3 = Color3.fromRGB(240, 240, 245)
|
|
2048
|
+
pushButton.TextSize = 12
|
|
2049
|
+
pushButton.Parent = buttons
|
|
2050
|
+
Instance.new("UICorner", pushButton).CornerRadius = UDim.new(0, 4)
|
|
2051
|
+
|
|
2052
|
+
local pullButton = Instance.new("TextButton")
|
|
2053
|
+
pullButton.BackgroundColor3 = Color3.fromRGB(70, 70, 80)
|
|
2054
|
+
pullButton.BorderSizePixel = 0
|
|
2055
|
+
pullButton.Font = Enum.Font.BuilderSansBold
|
|
2056
|
+
pullButton.Size = UDim2.new(0.5, -3, 1, 0)
|
|
2057
|
+
pullButton.Text = "Pull File"
|
|
2058
|
+
pullButton.TextColor3 = Color3.fromRGB(240, 240, 245)
|
|
2059
|
+
pullButton.TextSize = 12
|
|
2060
|
+
pullButton.Parent = buttons
|
|
2061
|
+
Instance.new("UICorner", pullButton).CornerRadius = UDim.new(0, 4)
|
|
2062
|
+
|
|
2063
|
+
local mergeButton = Instance.new("TextButton")
|
|
2064
|
+
mergeButton.BackgroundColor3 = Color3.fromRGB(151, 92, 41)
|
|
2065
|
+
mergeButton.BorderSizePixel = 0
|
|
2066
|
+
mergeButton.Font = Enum.Font.BuilderSansBold
|
|
2067
|
+
mergeButton.Size = UDim2.fromScale(1, 1)
|
|
2068
|
+
mergeButton.Text = "Open Merge Workspace"
|
|
2069
|
+
mergeButton.TextColor3 = Color3.fromRGB(255, 245, 230)
|
|
2070
|
+
mergeButton.TextSize = 12
|
|
2071
|
+
mergeButton.Visible = false
|
|
2072
|
+
mergeButton.Parent = buttons
|
|
2073
|
+
Instance.new("UICorner", mergeButton).CornerRadius = UDim.new(0, 4)
|
|
2074
|
+
|
|
2075
|
+
local diffButton = Instance.new("TextButton")
|
|
2076
|
+
diffButton.BackgroundColor3 = Color3.fromRGB(48, 91, 132)
|
|
2077
|
+
diffButton.BorderSizePixel = 0
|
|
2078
|
+
diffButton.Font = Enum.Font.BuilderSansBold
|
|
2079
|
+
diffButton.Size = UDim2.new(0.5, -3, 1, 0)
|
|
2080
|
+
diffButton.Text = "Property Diff"
|
|
2081
|
+
diffButton.TextColor3 = Color3.fromRGB(240, 247, 255)
|
|
2082
|
+
diffButton.TextSize = 12
|
|
2083
|
+
diffButton.Visible = false
|
|
2084
|
+
diffButton.Parent = buttons
|
|
2085
|
+
Instance.new("UICorner", diffButton).CornerRadius = UDim.new(0, 4)
|
|
2086
|
+
|
|
2087
|
+
local contents = Instance.new("Frame")
|
|
2088
|
+
contents.AutomaticSize = Enum.AutomaticSize.Y
|
|
2089
|
+
contents.BackgroundTransparency = 1
|
|
2090
|
+
contents.LayoutOrder = 5
|
|
2091
|
+
contents.Size = UDim2.new(1, 0, 0, 0)
|
|
2092
|
+
contents.Visible = false
|
|
2093
|
+
contents.Parent = row
|
|
2094
|
+
local contentsGuide = Instance.new("Frame")
|
|
2095
|
+
contentsGuide.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
2096
|
+
contentsGuide.BorderSizePixel = 0
|
|
2097
|
+
contentsGuide.Position = UDim2.new(0, 6, 0, 0)
|
|
2098
|
+
contentsGuide.Size = UDim2.fromOffset(1, 0)
|
|
2099
|
+
contentsGuide.Parent = contents
|
|
2100
|
+
local contentsList = Instance.new("Frame")
|
|
2101
|
+
contentsList.AutomaticSize = Enum.AutomaticSize.Y
|
|
2102
|
+
contentsList.BackgroundTransparency = 1
|
|
2103
|
+
contentsList.Position = UDim2.new(0, 14, 0, 0)
|
|
2104
|
+
contentsList.Size = UDim2.new(1, -14, 0, 0)
|
|
2105
|
+
contentsList.Parent = contents
|
|
2106
|
+
local contentsLayout = Instance.new("UIListLayout", contentsList)
|
|
2107
|
+
contentsLayout.Padding = UDim.new(0, 2)
|
|
2108
|
+
contentsLayout.SortOrder = Enum.SortOrder.LayoutOrder
|
|
2109
|
+
contentsLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function()
|
|
2110
|
+
contentsGuide.Size = UDim2.fromOffset(1, contentsLayout.AbsoluteContentSize.Y)
|
|
2111
|
+
end)
|
|
2112
|
+
|
|
2113
|
+
local savedHash = plugin:GetSetting(
|
|
2114
|
+
SETTINGS_PREFIX .. serverConfig.projectId .. ":" .. serverConfig.workspaceId .. ":" .. manifest.id .. ":hash"
|
|
2115
|
+
) or plugin:GetSetting(SETTINGS_PREFIX .. serverConfig.projectId .. ":" .. manifest.id .. ":hash")
|
|
2116
|
+
local state = {
|
|
2117
|
+
manifest = manifest,
|
|
2118
|
+
group = group,
|
|
2119
|
+
row = row,
|
|
2120
|
+
title = title,
|
|
2121
|
+
icon = icon,
|
|
2122
|
+
iconClass = if rootInstance then rootInstance.ClassName else "Model",
|
|
2123
|
+
contentDisclosure = contentDisclosure,
|
|
2124
|
+
contents = contents,
|
|
2125
|
+
contentsList = contentsList,
|
|
2126
|
+
contentsLayout = contentsLayout,
|
|
2127
|
+
contentsLoaded = false,
|
|
2128
|
+
contentsExpanded = false,
|
|
2129
|
+
searchQuery = nil,
|
|
2130
|
+
searchMatches = true,
|
|
2131
|
+
dot = dot,
|
|
2132
|
+
infoButton = infoButton,
|
|
2133
|
+
miniPush = miniPush,
|
|
2134
|
+
miniPull = miniPull,
|
|
2135
|
+
file = file,
|
|
2136
|
+
status = status,
|
|
2137
|
+
buttons = buttons,
|
|
2138
|
+
pushButton = pushButton,
|
|
2139
|
+
pullButton = pullButton,
|
|
2140
|
+
mergeButton = mergeButton,
|
|
2141
|
+
diffButton = diffButton,
|
|
2142
|
+
root = nil,
|
|
2143
|
+
rootConnections = {},
|
|
2144
|
+
instanceConnections = {},
|
|
2145
|
+
dirty = false,
|
|
2146
|
+
generation = 0,
|
|
2147
|
+
suppress = false,
|
|
2148
|
+
conflict = false,
|
|
2149
|
+
uploading = false,
|
|
2150
|
+
pulling = false,
|
|
2151
|
+
deleting = false,
|
|
2152
|
+
conflictWorkspace = nil,
|
|
2153
|
+
expanded = false,
|
|
2154
|
+
lastHash = if type(savedHash) == "string" then savedHash else nil,
|
|
2155
|
+
}
|
|
2156
|
+
states[manifest.id] = state
|
|
2157
|
+
contentDisclosure.Activated:Connect(function()
|
|
2158
|
+
toggleRootContents(state)
|
|
2159
|
+
end)
|
|
2160
|
+
infoButton.Activated:Connect(function()
|
|
2161
|
+
setRootExpanded(state, not state.expanded)
|
|
2162
|
+
end)
|
|
2163
|
+
title.Activated:Connect(function()
|
|
2164
|
+
local root = resolveRoot(state.manifest.studioPath)
|
|
2165
|
+
if root then
|
|
2166
|
+
Selection:Set({ root })
|
|
2167
|
+
end
|
|
2168
|
+
end)
|
|
2169
|
+
miniPush.Activated:Connect(function()
|
|
2170
|
+
upload(state, true)
|
|
2171
|
+
end)
|
|
2172
|
+
miniPull.Activated:Connect(function()
|
|
2173
|
+
pull(state)
|
|
2174
|
+
end)
|
|
2175
|
+
pushButton.Activated:Connect(function()
|
|
2176
|
+
upload(state, true)
|
|
2177
|
+
end)
|
|
2178
|
+
pullButton.Activated:Connect(function()
|
|
2179
|
+
pull(state)
|
|
2180
|
+
end)
|
|
2181
|
+
mergeButton.Activated:Connect(function()
|
|
2182
|
+
if state.conflictWorkspace then
|
|
2183
|
+
resolveGitConflict(state)
|
|
2184
|
+
else
|
|
2185
|
+
openGitConflict(state)
|
|
2186
|
+
end
|
|
2187
|
+
end)
|
|
2188
|
+
diffButton.Activated:Connect(function()
|
|
2189
|
+
if state.conflictWorkspace then
|
|
2190
|
+
inspectorWidget.Enabled = true
|
|
2191
|
+
end
|
|
2192
|
+
end)
|
|
2193
|
+
return state
|
|
2194
|
+
end
|
|
2195
|
+
|
|
2196
|
+
local function reconcile(state, manifest)
|
|
2197
|
+
state.manifest = manifest
|
|
2198
|
+
if manifest.gitConflict then
|
|
2199
|
+
state.conflict = true
|
|
2200
|
+
state.pushButton.Visible = false
|
|
2201
|
+
state.pullButton.Visible = false
|
|
2202
|
+
state.miniPush.Visible = false
|
|
2203
|
+
state.miniPull.Visible = false
|
|
2204
|
+
state.mergeButton.Visible = true
|
|
2205
|
+
setStatus(state, "Git conflict — resolve in VS Code or open the optional merge workspace.", Color3.fromRGB(245, 106, 106))
|
|
2206
|
+
if autoExpandConflicts then
|
|
2207
|
+
setGroupExpanded(state.group, true)
|
|
2208
|
+
setRootExpanded(state, true)
|
|
2209
|
+
end
|
|
2210
|
+
return
|
|
2211
|
+
end
|
|
2212
|
+
state.pushButton.Visible = true
|
|
2213
|
+
state.pullButton.Visible = true
|
|
2214
|
+
state.miniPush.Visible = true
|
|
2215
|
+
state.miniPull.Visible = true
|
|
2216
|
+
state.mergeButton.Visible = false
|
|
2217
|
+
state.pushButton.Active = serverConfig.rojoConnected
|
|
2218
|
+
state.pullButton.Active = serverConfig.rojoConnected
|
|
2219
|
+
state.miniPush.Active = serverConfig.rojoConnected
|
|
2220
|
+
state.miniPull.Active = serverConfig.rojoConnected
|
|
2221
|
+
state.pushButton.AutoButtonColor = serverConfig.rojoConnected
|
|
2222
|
+
state.pullButton.AutoButtonColor = serverConfig.rojoConnected
|
|
2223
|
+
state.miniPush.AutoButtonColor = serverConfig.rojoConnected
|
|
2224
|
+
state.miniPull.AutoButtonColor = serverConfig.rojoConnected
|
|
2225
|
+
state.miniPush.TextColor3 = if serverConfig.rojoConnected
|
|
2226
|
+
then Color3.fromRGB(115, 182, 255)
|
|
2227
|
+
else Color3.fromRGB(95, 95, 105)
|
|
2228
|
+
state.miniPull.TextColor3 = state.miniPush.TextColor3
|
|
2229
|
+
if state.conflictWorkspace then
|
|
2230
|
+
destroyConflictWorkspace(state)
|
|
2231
|
+
end
|
|
2232
|
+
if not serverConfig.rojoConnected then
|
|
2233
|
+
setStatus(state, "Paused — Rojo offline; conflict viewer remains available.", Color3.fromRGB(244, 194, 92))
|
|
2234
|
+
return
|
|
2235
|
+
end
|
|
2236
|
+
local root = resolveRoot(manifest.studioPath)
|
|
2237
|
+
if state.contentRoot ~= root then
|
|
2238
|
+
state.contentRoot = root
|
|
2239
|
+
state.searchQuery = nil
|
|
2240
|
+
resetStateContents(state)
|
|
2241
|
+
elseif root then
|
|
2242
|
+
state.contentDisclosure.Visible = visualDepth > 0 and #root:GetChildren() > 0
|
|
2243
|
+
end
|
|
2244
|
+
if root and state.iconClass ~= root.ClassName then
|
|
2245
|
+
state.iconClass = root.ClassName
|
|
2246
|
+
applyClassIcon(state.icon, root.ClassName)
|
|
2247
|
+
end
|
|
2248
|
+
if root and state.root ~= root then
|
|
2249
|
+
watchRoot(state, root)
|
|
2250
|
+
end
|
|
2251
|
+
|
|
2252
|
+
if not manifest.exists then
|
|
2253
|
+
if manifest.deleted and not root then
|
|
2254
|
+
state.conflict = false
|
|
2255
|
+
setStatus(state, "Deleted", Color3.fromRGB(102, 214, 139))
|
|
2256
|
+
elseif state.lastHash then
|
|
2257
|
+
state.conflict = true
|
|
2258
|
+
setStatus(state, "File was deleted. Push to recreate it.", Color3.fromRGB(245, 106, 106))
|
|
2259
|
+
elseif root then
|
|
2260
|
+
upload(state, false)
|
|
2261
|
+
else
|
|
2262
|
+
setStatus(state, "Both Studio root and file are missing.", Color3.fromRGB(245, 106, 106))
|
|
2263
|
+
end
|
|
2264
|
+
return
|
|
2265
|
+
end
|
|
2266
|
+
|
|
2267
|
+
if not root then
|
|
2268
|
+
if not state.conflict and not state.deleting then
|
|
2269
|
+
deleteSnapshot(state)
|
|
2270
|
+
end
|
|
2271
|
+
return
|
|
2272
|
+
end
|
|
2273
|
+
if not state.lastHash then
|
|
2274
|
+
state.conflict = true
|
|
2275
|
+
setStatus(state, "First sync needs Push or Pull once.", Color3.fromRGB(244, 194, 92))
|
|
2276
|
+
return
|
|
2277
|
+
end
|
|
2278
|
+
if manifest.hash ~= state.lastHash then
|
|
2279
|
+
if state.dirty or state.uploading then
|
|
2280
|
+
state.conflict = true
|
|
2281
|
+
setStatus(state, "Conflict: Studio and file both changed.", Color3.fromRGB(245, 106, 106))
|
|
2282
|
+
else
|
|
2283
|
+
pull(state)
|
|
2284
|
+
end
|
|
2285
|
+
return
|
|
2286
|
+
end
|
|
2287
|
+
if not state.dirty and not state.uploading and not state.pulling then
|
|
2288
|
+
state.conflict = false
|
|
2289
|
+
setStatus(state, "Synced", Color3.fromRGB(102, 214, 139))
|
|
2290
|
+
end
|
|
2291
|
+
end
|
|
2292
|
+
|
|
2293
|
+
local function refreshSettingsLabels()
|
|
2294
|
+
showSyncedButton.Text = (if showSynced then "✓ " else " ") .. "Show synced files"
|
|
2295
|
+
autoExpandButton.Text = (if autoExpandConflicts then "✓ " else " ") .. "Auto-expand conflicts"
|
|
2296
|
+
depthLabel.Text = string.format("Internal browse depth: %d", visualDepth)
|
|
2297
|
+
refreshUnsyncedButton()
|
|
2298
|
+
end
|
|
2299
|
+
|
|
2300
|
+
applyTreeFilter = function()
|
|
2301
|
+
local query = string.lower(searchBox.Text)
|
|
2302
|
+
for _, state in states do
|
|
2303
|
+
if state.searchQuery ~= query then
|
|
2304
|
+
local searchable = string.lower(
|
|
2305
|
+
table.concat(state.manifest.studioPath, ".") .. " " .. state.manifest.file .. " " .. state.status.Text
|
|
2306
|
+
)
|
|
2307
|
+
local matches = query == "" or string.find(searchable, query, 1, true) ~= nil
|
|
2308
|
+
if not matches then
|
|
2309
|
+
local root = resolveRoot(state.manifest.studioPath)
|
|
2310
|
+
if root then
|
|
2311
|
+
for _, descendant in root:GetDescendants() do
|
|
2312
|
+
if string.find(string.lower(descendant.Name .. " " .. descendant.ClassName), query, 1, true) then
|
|
2313
|
+
matches = true
|
|
2314
|
+
break
|
|
2315
|
+
end
|
|
2316
|
+
end
|
|
2317
|
+
end
|
|
2318
|
+
end
|
|
2319
|
+
state.searchQuery = query
|
|
2320
|
+
state.searchMatches = matches
|
|
2321
|
+
end
|
|
2322
|
+
state.row.Visible = state.searchMatches and (showSynced or state.statusKind ~= "synced")
|
|
2323
|
+
end
|
|
2324
|
+
for _, group in groups do
|
|
2325
|
+
local hasVisibleDescendant = false
|
|
2326
|
+
for _, state in states do
|
|
2327
|
+
if state.row.Visible and pathStartsWith(group.path, state.manifest.studioPath) then
|
|
2328
|
+
hasVisibleDescendant = true
|
|
2329
|
+
break
|
|
2330
|
+
end
|
|
2331
|
+
end
|
|
2332
|
+
group.section.Visible = hasVisibleDescendant
|
|
2333
|
+
if query ~= "" and hasVisibleDescendant then
|
|
2334
|
+
setGroupExpanded(group, true)
|
|
2335
|
+
end
|
|
2336
|
+
end
|
|
2337
|
+
end
|
|
2338
|
+
|
|
2339
|
+
addButtonHover(settingsButton)
|
|
2340
|
+
addButtonHover(showSyncedButton)
|
|
2341
|
+
addButtonHover(autoExpandButton)
|
|
2342
|
+
addButtonHover(depthMinus)
|
|
2343
|
+
addButtonHover(depthPlus)
|
|
2344
|
+
unsyncedButton.MouseEnter:Connect(function()
|
|
2345
|
+
unsyncedButton.BackgroundColor3 = if showSynced
|
|
2346
|
+
then Color3.fromRGB(58, 58, 68)
|
|
2347
|
+
else Color3.fromRGB(94, 78, 42)
|
|
2348
|
+
end)
|
|
2349
|
+
unsyncedButton.MouseLeave:Connect(refreshUnsyncedButton)
|
|
2350
|
+
refreshSettingsLabels()
|
|
2351
|
+
|
|
2352
|
+
settingsButton.Activated:Connect(function()
|
|
2353
|
+
settingsPanel.Visible = not settingsPanel.Visible
|
|
2354
|
+
end)
|
|
2355
|
+
unsyncedButton.Activated:Connect(function()
|
|
2356
|
+
showSynced = not showSynced
|
|
2357
|
+
plugin:SetSetting(UI_SETTINGS_PREFIX .. "showSynced", showSynced)
|
|
2358
|
+
refreshSettingsLabels()
|
|
2359
|
+
applyTreeFilter()
|
|
2360
|
+
end)
|
|
2361
|
+
showSyncedButton.Activated:Connect(function()
|
|
2362
|
+
showSynced = not showSynced
|
|
2363
|
+
plugin:SetSetting(UI_SETTINGS_PREFIX .. "showSynced", showSynced)
|
|
2364
|
+
refreshSettingsLabels()
|
|
2365
|
+
applyTreeFilter()
|
|
2366
|
+
end)
|
|
2367
|
+
autoExpandButton.Activated:Connect(function()
|
|
2368
|
+
autoExpandConflicts = not autoExpandConflicts
|
|
2369
|
+
plugin:SetSetting(UI_SETTINGS_PREFIX .. "autoExpandConflicts", autoExpandConflicts)
|
|
2370
|
+
refreshSettingsLabels()
|
|
2371
|
+
end)
|
|
2372
|
+
depthMinus.Activated:Connect(function()
|
|
2373
|
+
visualDepth = math.max(0, visualDepth - 1)
|
|
2374
|
+
plugin:SetSetting(UI_SETTINGS_PREFIX .. "visualDepth", visualDepth)
|
|
2375
|
+
refreshSettingsLabels()
|
|
2376
|
+
resetInstanceTrees()
|
|
2377
|
+
end)
|
|
2378
|
+
depthPlus.Activated:Connect(function()
|
|
2379
|
+
visualDepth = math.min(5, visualDepth + 1)
|
|
2380
|
+
plugin:SetSetting(UI_SETTINGS_PREFIX .. "visualDepth", visualDepth)
|
|
2381
|
+
refreshSettingsLabels()
|
|
2382
|
+
resetInstanceTrees()
|
|
2383
|
+
end)
|
|
2384
|
+
|
|
2385
|
+
local searchGeneration = 0
|
|
2386
|
+
searchBox:GetPropertyChangedSignal("Text"):Connect(function()
|
|
2387
|
+
searchGeneration += 1
|
|
2388
|
+
local generation = searchGeneration
|
|
2389
|
+
task.delay(0.15, function()
|
|
2390
|
+
if generation == searchGeneration then
|
|
2391
|
+
applyTreeFilter()
|
|
2392
|
+
end
|
|
2393
|
+
end)
|
|
2394
|
+
end)
|
|
2395
|
+
|
|
2396
|
+
refreshConfig = function()
|
|
2397
|
+
local response, requestError = request("GET", "/v1/config")
|
|
2398
|
+
if not response or not response.Success then
|
|
2399
|
+
local reason = requestError or (if response then responseError(response) else nil) or "connect Rojo to start syncing"
|
|
2400
|
+
if serverConfig then
|
|
2401
|
+
serverConfig.rojoConnected = false
|
|
2402
|
+
end
|
|
2403
|
+
connectionStatus.Text = "Offline · " .. reason
|
|
2404
|
+
connectionStatus.TextColor3 = Color3.fromRGB(245, 106, 106)
|
|
2405
|
+
connectionDot.TextColor3 = connectionStatus.TextColor3
|
|
2406
|
+
return false
|
|
2407
|
+
end
|
|
2408
|
+
|
|
2409
|
+
local decoded = HttpService:JSONDecode(response.Body)
|
|
2410
|
+
if serverConfig
|
|
2411
|
+
and (serverConfig.projectId ~= decoded.projectId or serverConfig.workspaceId ~= decoded.workspaceId)
|
|
2412
|
+
then
|
|
2413
|
+
for _, state in states do
|
|
2414
|
+
disconnectRoot(state)
|
|
2415
|
+
destroyConflictWorkspace(state)
|
|
2416
|
+
end
|
|
2417
|
+
table.clear(states)
|
|
2418
|
+
for _, child in rootsContainer:GetChildren() do
|
|
2419
|
+
if child ~= rootsLayout then
|
|
2420
|
+
child:Destroy()
|
|
2421
|
+
end
|
|
2422
|
+
end
|
|
2423
|
+
table.clear(groups)
|
|
2424
|
+
end
|
|
2425
|
+
serverConfig = decoded
|
|
2426
|
+
if decoded.rojoConnected then
|
|
2427
|
+
connectionStatus.Text = "Connected · " .. decoded.name
|
|
2428
|
+
connectionStatus.TextColor3 = Color3.fromRGB(102, 214, 139)
|
|
2429
|
+
else
|
|
2430
|
+
connectionStatus.Text = "Rojo offline · connect Rojo to start syncing"
|
|
2431
|
+
connectionStatus.TextColor3 = Color3.fromRGB(244, 194, 92)
|
|
2432
|
+
end
|
|
2433
|
+
connectionDot.TextColor3 = connectionStatus.TextColor3
|
|
2434
|
+
|
|
2435
|
+
local existingSplitPaths = {}
|
|
2436
|
+
for _, manifest in decoded.roots do
|
|
2437
|
+
if manifest.splitRootId then
|
|
2438
|
+
existingSplitPaths[manifest.splitRootId .. "\0" .. table.concat(manifest.studioPath, "\0")] = true
|
|
2439
|
+
end
|
|
2440
|
+
end
|
|
2441
|
+
for _, splitRoot in decoded.splitRoots do
|
|
2442
|
+
local root = resolveRoot(splitRoot.studioPath)
|
|
2443
|
+
if root then
|
|
2444
|
+
local missingPaths = {}
|
|
2445
|
+
for _, relativePath in collectSplitPaths(root, splitRoot.splitDepth) do
|
|
2446
|
+
local fullPath = table.clone(splitRoot.studioPath)
|
|
2447
|
+
for _, segment in relativePath do
|
|
2448
|
+
table.insert(fullPath, segment)
|
|
2449
|
+
end
|
|
2450
|
+
if not existingSplitPaths[splitRoot.id .. "\0" .. table.concat(fullPath, "\0")] then
|
|
2451
|
+
table.insert(missingPaths, relativePath)
|
|
2452
|
+
end
|
|
2453
|
+
end
|
|
2454
|
+
if #missingPaths > 0 then
|
|
2455
|
+
request("POST", "/v1/splits/" .. splitRoot.id .. "/discover", HttpService:JSONEncode({ paths = missingPaths }), {
|
|
2456
|
+
["Content-Type"] = "application/json",
|
|
2457
|
+
})
|
|
2458
|
+
end
|
|
2459
|
+
end
|
|
2460
|
+
end
|
|
2461
|
+
|
|
2462
|
+
for order, manifest in decoded.roots do
|
|
2463
|
+
local state = states[manifest.id] or createRootRow(manifest, order)
|
|
2464
|
+
reconcile(state, manifest)
|
|
2465
|
+
end
|
|
2466
|
+
local currentIds = {}
|
|
2467
|
+
for _, manifest in decoded.roots do
|
|
2468
|
+
currentIds[manifest.id] = true
|
|
2469
|
+
end
|
|
2470
|
+
local staleIds = {}
|
|
2471
|
+
for id in states do
|
|
2472
|
+
if not currentIds[id] then
|
|
2473
|
+
table.insert(staleIds, id)
|
|
2474
|
+
end
|
|
2475
|
+
end
|
|
2476
|
+
for _, id in staleIds do
|
|
2477
|
+
local state = states[id]
|
|
2478
|
+
disconnectRoot(state)
|
|
2479
|
+
destroyConflictWorkspace(state)
|
|
2480
|
+
state.row:Destroy()
|
|
2481
|
+
states[id] = nil
|
|
2482
|
+
end
|
|
2483
|
+
updateGroupSummaries()
|
|
2484
|
+
applyTreeFilter()
|
|
2485
|
+
return true
|
|
2486
|
+
end
|
|
2487
|
+
|
|
2488
|
+
toolbarButton.Click:Connect(function()
|
|
2489
|
+
widget.Enabled = not widget.Enabled
|
|
2490
|
+
if widget.Enabled then
|
|
2491
|
+
refreshConfig()
|
|
2492
|
+
end
|
|
2493
|
+
end)
|
|
2494
|
+
widget:GetPropertyChangedSignal("Enabled"):Connect(function()
|
|
2495
|
+
toolbarButton:SetActive(widget.Enabled)
|
|
2496
|
+
end)
|
|
2497
|
+
|
|
2498
|
+
plugin.Unloading:Connect(function()
|
|
2499
|
+
running = false
|
|
2500
|
+
for _, state in states do
|
|
2501
|
+
disconnectRoot(state)
|
|
2502
|
+
destroyConflictWorkspace(state)
|
|
2503
|
+
end
|
|
2504
|
+
end)
|
|
2505
|
+
|
|
2506
|
+
task.spawn(function()
|
|
2507
|
+
while running do
|
|
2508
|
+
refreshConfig()
|
|
2509
|
+
task.wait((serverConfig and serverConfig.pollIntervalMs or 750) / 1000)
|
|
2510
|
+
end
|
|
2511
|
+
end)
|
|
2512
|
+
]]></string>
|
|
2513
|
+
</Properties>
|
|
2514
|
+
</Item>
|
|
2515
|
+
</Item>
|
|
2516
|
+
</roblox>
|