dominus-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/index.js +4700 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.js +907 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +78 -0
- package/plugin/default.project.json +6 -0
- package/plugin/src/Executor.lua +116 -0
- package/plugin/src/Explorer.lua +251 -0
- package/plugin/src/Properties.lua +424 -0
- package/plugin/src/Protocol.lua +30 -0
- package/plugin/src/Reflection.lua +137 -0
- package/plugin/src/TestRunner.lua +141 -0
- package/plugin/src/UIBuilder.lua +247 -0
- package/plugin/src/Watcher.lua +85 -0
- package/plugin/src/WsClient.lua +81 -0
- package/plugin/src/init.server.lua +315 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
TestRunner: interface with StudioTestService for automated testing
|
|
3
|
+
https://create.roblox.com/docs/reference/engine/classes/StudioTestService
|
|
4
|
+
|
|
5
|
+
StudioTestService allows plugins to:
|
|
6
|
+
- ExecuteRunModeAsync(args) → start Run mode, yield until EndTest
|
|
7
|
+
- ExecutePlayModeAsync(args) → start Play/Solo mode, yield until EndTest
|
|
8
|
+
- GetTestArgs() → retrieve args from inside the running session
|
|
9
|
+
- EndTest(value) → end the session and return a value
|
|
10
|
+
]]
|
|
11
|
+
|
|
12
|
+
local TestRunner = {}
|
|
13
|
+
|
|
14
|
+
local StudioTestService = game:GetService("StudioTestService")
|
|
15
|
+
|
|
16
|
+
function TestRunner.executeRunMode(args)
|
|
17
|
+
local startTime = os.clock()
|
|
18
|
+
local ok, result = pcall(function()
|
|
19
|
+
return StudioTestService:ExecuteRunModeAsync(args)
|
|
20
|
+
end)
|
|
21
|
+
local duration = os.clock() - startTime
|
|
22
|
+
|
|
23
|
+
if not ok then
|
|
24
|
+
return {
|
|
25
|
+
success = false,
|
|
26
|
+
error = tostring(result),
|
|
27
|
+
duration = duration,
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
success = true,
|
|
33
|
+
result = result,
|
|
34
|
+
duration = duration,
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
function TestRunner.executePlayMode(args)
|
|
39
|
+
local startTime = os.clock()
|
|
40
|
+
local ok, result = pcall(function()
|
|
41
|
+
return StudioTestService:ExecutePlayModeAsync(args)
|
|
42
|
+
end)
|
|
43
|
+
local duration = os.clock() - startTime
|
|
44
|
+
|
|
45
|
+
if not ok then
|
|
46
|
+
return {
|
|
47
|
+
success = false,
|
|
48
|
+
error = tostring(result),
|
|
49
|
+
duration = duration,
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
success = true,
|
|
55
|
+
result = result,
|
|
56
|
+
duration = duration,
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
function TestRunner.endTest(value)
|
|
61
|
+
local ok, err = pcall(function()
|
|
62
|
+
StudioTestService:EndTest(value)
|
|
63
|
+
end)
|
|
64
|
+
|
|
65
|
+
if not ok then
|
|
66
|
+
return { success = false, error = tostring(err) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return { success = true }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
function TestRunner.run(testName)
|
|
73
|
+
local results = {}
|
|
74
|
+
|
|
75
|
+
local ok, err = pcall(function()
|
|
76
|
+
local TestService = game:GetService("TestService")
|
|
77
|
+
|
|
78
|
+
if testName then
|
|
79
|
+
local testScript = nil
|
|
80
|
+
for _, desc in game:GetDescendants() do
|
|
81
|
+
if desc:IsA("ModuleScript") and desc.Name == testName then
|
|
82
|
+
testScript = desc
|
|
83
|
+
break
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if not testScript then
|
|
88
|
+
table.insert(results, {
|
|
89
|
+
name = testName,
|
|
90
|
+
passed = false,
|
|
91
|
+
duration = 0,
|
|
92
|
+
error = "Test not found: " .. testName,
|
|
93
|
+
})
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
local startTime = os.clock()
|
|
98
|
+
local testOk, testErr = pcall(function()
|
|
99
|
+
require(testScript)
|
|
100
|
+
end)
|
|
101
|
+
local duration = os.clock() - startTime
|
|
102
|
+
|
|
103
|
+
table.insert(results, {
|
|
104
|
+
name = testName,
|
|
105
|
+
passed = testOk,
|
|
106
|
+
duration = duration,
|
|
107
|
+
error = not testOk and tostring(testErr) or nil,
|
|
108
|
+
})
|
|
109
|
+
else
|
|
110
|
+
for _, desc in game:GetDescendants() do
|
|
111
|
+
if desc:IsA("ModuleScript") then
|
|
112
|
+
local name = desc.Name
|
|
113
|
+
if string.match(name, "%.test$") or string.match(name, "%.spec$")
|
|
114
|
+
or string.match(name, "Test$") or string.match(name, "Spec$") then
|
|
115
|
+
|
|
116
|
+
local startTime = os.clock()
|
|
117
|
+
local testOk, testErr = pcall(function()
|
|
118
|
+
require(desc)
|
|
119
|
+
end)
|
|
120
|
+
local duration = os.clock() - startTime
|
|
121
|
+
|
|
122
|
+
table.insert(results, {
|
|
123
|
+
name = name,
|
|
124
|
+
passed = testOk,
|
|
125
|
+
duration = duration,
|
|
126
|
+
error = not testOk and tostring(testErr) or nil,
|
|
127
|
+
})
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end)
|
|
133
|
+
|
|
134
|
+
if not ok then
|
|
135
|
+
return { results = {{ name = "TestRunner", passed = false, duration = 0, error = tostring(err) }} }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
return { results = results }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
return TestRunner
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
UIBuilder: create entire UI trees from a declarative JSON spec in one call.
|
|
3
|
+
Handles all type coercion natively in Luau — no round trips needed.
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local Explorer = require(script.Parent.Explorer)
|
|
7
|
+
|
|
8
|
+
local UIBuilder = {}
|
|
9
|
+
|
|
10
|
+
local function resolveColor(value)
|
|
11
|
+
if type(value) == "string" then
|
|
12
|
+
if value:sub(1, 1) == "#" then
|
|
13
|
+
return Color3.fromHex(value)
|
|
14
|
+
end
|
|
15
|
+
local r, g, b = value:match("rgb%s*%((%d+)%D+(%d+)%D+(%d+)%)")
|
|
16
|
+
if r then return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b)) end
|
|
17
|
+
local ok, bc = pcall(BrickColor.new, value)
|
|
18
|
+
if ok then return bc.Color end
|
|
19
|
+
end
|
|
20
|
+
if type(value) == "table" then
|
|
21
|
+
if value[1] then
|
|
22
|
+
if value[1] > 1 or value[2] > 1 or value[3] > 1 then
|
|
23
|
+
return Color3.fromRGB(value[1], value[2], value[3])
|
|
24
|
+
end
|
|
25
|
+
return Color3.new(value[1], value[2], value[3])
|
|
26
|
+
end
|
|
27
|
+
if value.R or value.r then
|
|
28
|
+
return Color3.new(value.R or value.r or 0, value.G or value.g or 0, value.B or value.b or 0)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
return Color3.new(1, 1, 1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
local function resolveUDim2(value)
|
|
35
|
+
if type(value) == "table" then
|
|
36
|
+
if value.XScale or value.xScale then
|
|
37
|
+
return UDim2.new(
|
|
38
|
+
value.XScale or value.xScale or 0, value.XOffset or value.xOffset or 0,
|
|
39
|
+
value.YScale or value.yScale or 0, value.YOffset or value.yOffset or 0
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
if value.X and type(value.X) == "table" then
|
|
43
|
+
return UDim2.new(
|
|
44
|
+
value.X.Scale or value.X[1] or 0, value.X.Offset or value.X[2] or 0,
|
|
45
|
+
value.Y and (value.Y.Scale or value.Y[1]) or 0, value.Y and (value.Y.Offset or value.Y[2]) or 0
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
if value[1] ~= nil then
|
|
49
|
+
return UDim2.new(value[1] or 0, value[2] or 0, value[3] or 0, value[4] or 0)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
if type(value) == "string" then
|
|
53
|
+
local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
|
|
54
|
+
if a then return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d)) end
|
|
55
|
+
end
|
|
56
|
+
return UDim2.new(0, 0, 0, 0)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
local function resolveUDim(value)
|
|
60
|
+
if type(value) == "table" then
|
|
61
|
+
return UDim.new(value.Scale or value[1] or 0, value.Offset or value[2] or 0)
|
|
62
|
+
end
|
|
63
|
+
if type(value) == "number" then
|
|
64
|
+
return UDim.new(0, value)
|
|
65
|
+
end
|
|
66
|
+
return UDim.new(0, 0)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
local function resolveVector2(value)
|
|
70
|
+
if type(value) == "table" then
|
|
71
|
+
return Vector2.new(value.X or value.x or value[1] or 0, value.Y or value.y or value[2] or 0)
|
|
72
|
+
end
|
|
73
|
+
return Vector2.new(0, 0)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
local function resolveEnum(instance, key, value)
|
|
77
|
+
if type(value) ~= "string" then return value end
|
|
78
|
+
local curOk, curVal = pcall(function() return instance[key] end)
|
|
79
|
+
if curOk and typeof(curVal) == "EnumItem" then
|
|
80
|
+
local enumType = tostring(curVal.EnumType)
|
|
81
|
+
local ok, enumVal = pcall(function() return Enum[enumType][value] end)
|
|
82
|
+
if ok then return enumVal end
|
|
83
|
+
end
|
|
84
|
+
return value
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
local function resolveFont(value)
|
|
88
|
+
if type(value) == "string" then
|
|
89
|
+
local ok, font = pcall(function() return Enum.Font[value] end)
|
|
90
|
+
if ok then return font end
|
|
91
|
+
end
|
|
92
|
+
return value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
local UDIM2_PROPS = {
|
|
96
|
+
Size = true, Position = true, AnchorPoint_UDim2 = true,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
local COLOR_PROPS = {
|
|
100
|
+
BackgroundColor3 = true, BorderColor3 = true, TextColor3 = true,
|
|
101
|
+
ImageColor3 = true, PlaceholderColor3 = true, TextStrokeColor3 = true,
|
|
102
|
+
ScrollBarImageColor3 = true, Color = true,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
local VECTOR2_PROPS = {
|
|
106
|
+
AnchorPoint = true, CanvasSize_v2 = true,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
local function applyProperty(instance, key, value)
|
|
110
|
+
-- Special handling by property name
|
|
111
|
+
if key == "Size" or key == "Position" then
|
|
112
|
+
if instance:IsA("GuiObject") or instance:IsA("ScreenGui") then
|
|
113
|
+
instance[key] = resolveUDim2(value)
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if key == "AnchorPoint" then
|
|
119
|
+
if instance:IsA("GuiObject") then
|
|
120
|
+
instance[key] = resolveVector2(value)
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
if key == "CanvasSize" and instance:IsA("ScrollingFrame") then
|
|
126
|
+
instance[key] = resolveUDim2(value)
|
|
127
|
+
return
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
if key == "CornerRadius" then
|
|
131
|
+
instance[key] = resolveUDim(value)
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if COLOR_PROPS[key] then
|
|
136
|
+
instance[key] = resolveColor(value)
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if key == "Font" and instance:IsA("TextLabel") or instance:IsA("TextButton") or instance:IsA("TextBox") then
|
|
141
|
+
instance[key] = resolveFont(value)
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
if key == "FontFace" then
|
|
146
|
+
if type(value) == "table" then
|
|
147
|
+
local family = value.Family or "rbxasset://fonts/families/SourceSansPro.json"
|
|
148
|
+
local weight = Enum.FontWeight.Regular
|
|
149
|
+
local style = Enum.FontStyle.Normal
|
|
150
|
+
pcall(function() weight = Enum.FontWeight[value.Weight or "Regular"] end)
|
|
151
|
+
pcall(function() style = Enum.FontStyle[value.Style or "Normal"] end)
|
|
152
|
+
instance.FontFace = Font.new(family, weight, style)
|
|
153
|
+
end
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
-- Enum properties (string values)
|
|
158
|
+
if type(value) == "string" then
|
|
159
|
+
local resolved = resolveEnum(instance, key, value)
|
|
160
|
+
local ok, err = pcall(function() instance[key] = resolved end)
|
|
161
|
+
if not ok then
|
|
162
|
+
-- Maybe it's a direct value
|
|
163
|
+
pcall(function() instance[key] = value end)
|
|
164
|
+
end
|
|
165
|
+
return
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
-- Direct assignment for numbers, booleans, etc.
|
|
169
|
+
pcall(function() instance[key] = value end)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
local function buildNode(spec, parent)
|
|
173
|
+
local className = spec.ClassName or spec.className or "Frame"
|
|
174
|
+
local name = spec.Name or spec.name or className
|
|
175
|
+
|
|
176
|
+
local instance = Instance.new(className)
|
|
177
|
+
instance.Name = name
|
|
178
|
+
|
|
179
|
+
-- Apply all properties
|
|
180
|
+
for key, value in spec do
|
|
181
|
+
if key ~= "ClassName" and key ~= "className" and key ~= "Name" and key ~= "name" and key ~= "Children" and key ~= "children" then
|
|
182
|
+
applyProperty(instance, key, value)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
-- Set parent after properties to avoid unnecessary layout computations
|
|
187
|
+
instance.Parent = parent
|
|
188
|
+
|
|
189
|
+
-- Build children recursively
|
|
190
|
+
local children = spec.Children or spec.children
|
|
191
|
+
if children then
|
|
192
|
+
for _, childSpec in children do
|
|
193
|
+
buildNode(childSpec, instance)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
return instance
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
function UIBuilder.build(spec)
|
|
201
|
+
local parentPath = spec.parent or "StarterGui"
|
|
202
|
+
local parent = Explorer.resolveInstance(parentPath)
|
|
203
|
+
if not parent then
|
|
204
|
+
-- Try as a service
|
|
205
|
+
local ok
|
|
206
|
+
ok, parent = pcall(function() return game:GetService(parentPath) end)
|
|
207
|
+
if not ok then
|
|
208
|
+
return { success = false, error = "Parent not found: " .. parentPath }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
-- Clean up existing instance with same name
|
|
213
|
+
local existingName = spec.tree and (spec.tree.Name or spec.tree.name) or nil
|
|
214
|
+
if existingName then
|
|
215
|
+
local existing = parent:FindFirstChild(existingName)
|
|
216
|
+
if existing then
|
|
217
|
+
existing:Destroy()
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
local tree = spec.tree
|
|
222
|
+
if not tree then
|
|
223
|
+
return { success = false, error = "No 'tree' provided in spec" }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
local ok, result = pcall(function()
|
|
227
|
+
return buildNode(tree, parent)
|
|
228
|
+
end)
|
|
229
|
+
|
|
230
|
+
if not ok then
|
|
231
|
+
return { success = false, error = "Build failed: " .. tostring(result) }
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
local path = Explorer.getPath(result)
|
|
235
|
+
local count = 0
|
|
236
|
+
local function countDescendants(inst)
|
|
237
|
+
count = count + 1
|
|
238
|
+
for _, child in inst:GetChildren() do
|
|
239
|
+
countDescendants(child)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
countDescendants(result)
|
|
243
|
+
|
|
244
|
+
return { success = true, path = path, instanceCount = count }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
return UIBuilder
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
Watcher: monitors LogService output and forwards to CLI
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local LogService = game:GetService("LogService")
|
|
6
|
+
|
|
7
|
+
local Watcher = {}
|
|
8
|
+
|
|
9
|
+
local outputBuffer = {}
|
|
10
|
+
local MAX_BUFFER = 500
|
|
11
|
+
local connection = nil
|
|
12
|
+
local outputCallback = nil
|
|
13
|
+
|
|
14
|
+
local function classifyLevel(messageType)
|
|
15
|
+
if messageType == Enum.MessageType.MessageOutput then
|
|
16
|
+
return "info"
|
|
17
|
+
elseif messageType == Enum.MessageType.MessageWarning then
|
|
18
|
+
return "warn"
|
|
19
|
+
elseif messageType == Enum.MessageType.MessageError then
|
|
20
|
+
return "error"
|
|
21
|
+
end
|
|
22
|
+
return "info"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
function Watcher.start(callback)
|
|
26
|
+
outputCallback = callback
|
|
27
|
+
|
|
28
|
+
if connection then
|
|
29
|
+
connection:Disconnect()
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
connection = LogService.MessageOut:Connect(function(message, messageType)
|
|
33
|
+
-- Skip Dominus's own messages
|
|
34
|
+
if string.find(message, "[Dominus]", 1, true) then
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
local entry = {
|
|
39
|
+
message = message,
|
|
40
|
+
level = classifyLevel(messageType),
|
|
41
|
+
timestamp = os.clock(),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
-- Buffer for get_output requests
|
|
45
|
+
table.insert(outputBuffer, entry)
|
|
46
|
+
if #outputBuffer > MAX_BUFFER then
|
|
47
|
+
table.remove(outputBuffer, 1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
-- Forward to CLI
|
|
51
|
+
if outputCallback then
|
|
52
|
+
outputCallback(entry)
|
|
53
|
+
end
|
|
54
|
+
end)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
function Watcher.stop()
|
|
58
|
+
if connection then
|
|
59
|
+
connection:Disconnect()
|
|
60
|
+
connection = nil
|
|
61
|
+
end
|
|
62
|
+
outputCallback = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
function Watcher.getRecentOutput(limit, levelFilter)
|
|
66
|
+
limit = limit or 50
|
|
67
|
+
local result = {}
|
|
68
|
+
|
|
69
|
+
for i = math.max(1, #outputBuffer - limit + 1), #outputBuffer do
|
|
70
|
+
local entry = outputBuffer[i]
|
|
71
|
+
if entry then
|
|
72
|
+
if not levelFilter or entry.level == levelFilter then
|
|
73
|
+
table.insert(result, entry)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
return { entries = result }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
function Watcher.clearBuffer()
|
|
82
|
+
outputBuffer = {}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return Watcher
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
WebSocket client wrapper using HttpService:CreateWebStreamClient
|
|
3
|
+
Uses the new WebSocket API available in Studio plugins (Oct 2025+)
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local HttpService = game:GetService("HttpService")
|
|
7
|
+
|
|
8
|
+
local WsClient = {}
|
|
9
|
+
WsClient.__index = WsClient
|
|
10
|
+
|
|
11
|
+
local currentConnection = nil
|
|
12
|
+
local messageCallback = nil
|
|
13
|
+
local closeCallback = nil
|
|
14
|
+
local errorCallback = nil
|
|
15
|
+
|
|
16
|
+
function WsClient.connect(url, callbacks)
|
|
17
|
+
local ok, wsOrErr = pcall(function()
|
|
18
|
+
return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, {
|
|
19
|
+
Url = url,
|
|
20
|
+
})
|
|
21
|
+
end)
|
|
22
|
+
|
|
23
|
+
if not ok then
|
|
24
|
+
warn("[Dominus WS] Failed to create WebSocket:", wsOrErr)
|
|
25
|
+
if callbacks.onError then
|
|
26
|
+
callbacks.onError(tostring(wsOrErr))
|
|
27
|
+
end
|
|
28
|
+
return false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
local wsClient = wsOrErr
|
|
32
|
+
currentConnection = wsClient
|
|
33
|
+
|
|
34
|
+
-- Handle incoming messages
|
|
35
|
+
wsClient.MessageReceived:Connect(function(message)
|
|
36
|
+
if callbacks.onMessage then
|
|
37
|
+
callbacks.onMessage(message)
|
|
38
|
+
end
|
|
39
|
+
end)
|
|
40
|
+
|
|
41
|
+
-- Handle disconnection
|
|
42
|
+
wsClient.Closed:Connect(function()
|
|
43
|
+
currentConnection = nil
|
|
44
|
+
if callbacks.onClose then
|
|
45
|
+
callbacks.onClose()
|
|
46
|
+
end
|
|
47
|
+
end)
|
|
48
|
+
|
|
49
|
+
-- Notify connected
|
|
50
|
+
if callbacks.onOpen then
|
|
51
|
+
callbacks.onOpen()
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
function WsClient.send(message)
|
|
58
|
+
if currentConnection then
|
|
59
|
+
local ok, err = pcall(function()
|
|
60
|
+
currentConnection:Send(message)
|
|
61
|
+
end)
|
|
62
|
+
if not ok then
|
|
63
|
+
warn("[Dominus WS] Send error:", err)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
function WsClient.disconnect()
|
|
69
|
+
if currentConnection then
|
|
70
|
+
pcall(function()
|
|
71
|
+
currentConnection:Close()
|
|
72
|
+
end)
|
|
73
|
+
currentConnection = nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
function WsClient.isConnected()
|
|
78
|
+
return currentConnection ~= nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return WsClient
|