@rbxts/zyntex-sdk 6.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Zyntex.luau DELETED
@@ -1,1631 +0,0 @@
1
- --[[
2
- Zyntex Roblox SDK
3
- Version: 6
4
- Last Updated: 2025-09-01
5
- Author: Deluct
6
-
7
- This module is the main client for the Zyntex Advanced Admin/Ops Panel.
8
- It handles communication with the Zyntex API, manages server status, player events,
9
- and listens for administrative actions from the Zyntex dashboard.
10
-
11
- For full documentation, visit: https://docs.zyntex.dev/
12
- ]]
13
-
14
- local Stats = game:GetService("Stats")
15
- local RunService = game:GetService("RunService")
16
- local Players = game:GetService("Players")
17
- local LogService = game:GetService("LogService")
18
- local Chat = game:GetService("Chat")
19
- local TCS = game:GetService("TextChatService")
20
- local MS = game:GetService("MarketplaceService")
21
-
22
- local API = require(script.Parent:FindFirstChild("api"))
23
- local TYPES = require(script.Parent:FindFirstChild("types"))
24
- local Telemetry = require(script.Parent:FindFirstChild("Telemetry"))
25
- local Experiments = require(script.Parent:FindFirstChild("Experiments"))
26
-
27
- local CLIENT_VERSION = 6
28
-
29
-
30
-
31
- --[[
32
- @function now
33
- @return string
34
-
35
- Generates a UTC timestamp in ISO 8601 format. This is used for 'since'
36
- parameters in API calls to ensure all data is synchronized correctly.
37
- ]]
38
- local function now()
39
- local dt = DateTime.now():ToUniversalTime()
40
- return string.format(
41
- "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
42
- dt.Year, dt.Month, dt.Day,
43
- dt.Hour, dt.Minute, dt.Second,
44
- dt.Millisecond
45
- )
46
- end
47
-
48
- --[[
49
- @function toFormat
50
- @param date DateTime
51
- @return string
52
-
53
- Converts a given Roblox DateTime object into a UTC timestamp in ISO 8601 format.
54
- ]]
55
- local function toFormat(date: DateTime)
56
- local date = date:ToUniversalTime()
57
- return string.format(
58
- "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
59
- date.Year, date.Month, date.Day,
60
- date.Hour, date.Minute, date.Second,
61
- date.Millisecond
62
- )
63
- end
64
-
65
- --[[
66
- https://devforum.roblox.com/t/mute-players-with-textchatservice/2519300/4?u=deluc_t
67
- ]]
68
- local function muteUserId(mutedUserId)
69
- -- listen for future TextSources
70
- TCS.DescendantAdded:Connect(function(child)
71
- if child:IsA("TextSource") then
72
- if child.UserId == mutedUserId then
73
- child:Destroy()
74
- end
75
- end
76
- end)
77
-
78
- -- mute any current TextSources
79
- for _, child in TCS:GetDescendants() do
80
- if child:IsA("TextSource") then
81
- if child.UserId == mutedUserId then
82
- child:Destroy()
83
- end
84
- end
85
- end
86
-
87
- game:GetService("ReplicatedStorage"):FindFirstChild("zyntex.events"):FindFirstChild("SystemChat"):FireClient(Players:GetPlayerByUserId(mutedUserId), `You're muted.`)
88
- end
89
-
90
- --[[
91
- _____ _
92
- | __ \| |
93
- | |__) | | __ _ _ _ ___ _ __
94
- | ___/| |/ _` | | | |/ _ \ '__|
95
- | | | | (_| | |_| | __/ |
96
- |_| |_|\__,_|\__, |\___|_|
97
- __/ |
98
- |___/
99
-
100
- This section defines the 'ZyntexPlayer' object, which represents a Player
101
- in the Zyntex dashboard.
102
- ]]
103
-
104
- local ZyntexPlayer = {}
105
- ZyntexPlayer.__index = ZyntexPlayer
106
-
107
- -- Represents a single, recorded instance of an invoked event.
108
- type ZyntexPlayerType = {
109
- id: number; -- The unique ID of this specific player. Equivalent to the Roblox UserId
110
- name: string; -- The player's username as it appears on Roblox.
111
- avatar_url: string; -- The player's headshot avatar url.
112
- avatar_url_cache_expiry: string; -- When the player's avatar URL expires. Not important for Roblox.
113
- reputation: string; -- The player's reputation enum. Can be: 'clean', 'suspect', or 'offender'. https://docs.zyntex.dev/moderation/reputation
114
- raw_reputation: number; -- The player's raw reputation as an integer. https://docs.zyntex.dev/moderation/reputation
115
- }
116
-
117
- export type ZyntexPlayer = typeof(setmetatable({} :: ZyntexPlayerType, ZyntexPlayer))
118
-
119
- function ZyntexPlayer.new(id: number, name: string, avatar_url: string, avatar_url_cache_expiry: string, reputation: string, raw_reputation: number)
120
- local self = {}
121
- self.id = id;
122
- self.name = name
123
- self.avatar_url = avatar_url
124
- self.avatar_url_cache_expiry = avatar_url_cache_expiry
125
- self.reputation = reputation
126
- self.raw_reputation = raw_reputation
127
-
128
- return setmetatable(self, ZyntexPlayer)
129
- end
130
-
131
- local SERVER_ACTIVITY = {} -- stores player joins & leaves to send to Zyntex in the case that the servers go down during the server
132
- local sendingStatus = false
133
-
134
- --[[
135
-
136
- _____ _ _
137
- |_ _| | | (_)
138
- | | _ ____ _____ ___ __ _ | |_ _ ___ _ __ ___
139
- | | | '_ \ \ / / _ \ / __/ _` | __| |/ _ \| '_ \ / __|
140
- _| |_| | | \ V / (_) | (_| (_| | |_| | (_) | | | \__ \
141
- |_____|_| |_|\_/ \___/ \___\__,_|\__|_|\___/|_| |_|___/
142
-
143
-
144
-
145
- This section defines the 'Invocation' object, which represents a single,
146
- successful execution of an Event.
147
- ]]
148
-
149
- export type Data = {{["key"]: string, ["value"]: any}}
150
- export type DataSchema = {{["key"]: string, ["type"]: string}}
151
-
152
- local Invocation = {}
153
- Invocation.__index = Invocation
154
-
155
- -- Represents a single, recorded instance of an invoked event.
156
- type InvocationType = {
157
- id: number; -- The unique ID of this specific invocation.
158
- eventId: number; -- The ID of the Event that was invoked.
159
- data: Data; -- The data payload that was sent with the invocation.
160
- to: string; -- The destination of the invocation (e.g., a specific server or 'global').
161
- sender: string; -- Who or what sent the invocation (e.g., 'dashboard', 'server').
162
- fromServer: string; -- The ID of the server that sent the invocation, if applicable.
163
- invokedAt: string; -- The ISO 8601 timestamp of when the invocation occurred.
164
- toServer: string; -- The ID of the specific server this was sent to, if applicable.
165
- gameId: number; -- The Roblox Place ID where the invocation originated.
166
- invokedBy: number; -- The UserID of the admin who triggered the invocation from the dashboard, if applicable.
167
- }
168
-
169
- export type Invocation = typeof(setmetatable({} :: InvocationType, Invocation))
170
-
171
- --[[
172
- @constructor Invocation.new
173
- @description Constructs a new Invocation object from raw data. This is typically used internally
174
- by the client to wrap data received from the API into a usable object.
175
- @param id number
176
- @param eventId number
177
- @param data Data
178
- @param to string
179
- @param sender string
180
- @param fromServer string
181
- @param invokedAt string
182
- @param toServer string
183
- @param gameId string
184
- @param invokedBy number
185
- @return Invocation
186
- ]]
187
- function Invocation.new(id: number, eventId: number, data: Data, to: string, sender: string, fromServer: string, invokedAt: string, toServer: string, gameId: string, invokedBy: number)
188
- local self = {}
189
- self.id = id;
190
- self.eventId = eventId;
191
- self.data = data;
192
- self.to = to;
193
- self.sender = sender;
194
- self.fromServer = fromServer;
195
- self.invokedAt = invokedAt;
196
- self.toServer = toServer;
197
- self.gameId = gameId;
198
- self.invokedBy = invokedBy;
199
-
200
- return setmetatable(self, Invocation)
201
- end
202
-
203
- --[[
204
-
205
- ______ _
206
- | ____| | |
207
- | |__ _____ _ __ | |_ ___
208
- | __| / / _ \ '_ \| __/ __|
209
- | |____ V / __/ | | | |_\__ \
210
- |______\_/ \___|_| |_|\__|___/
211
-
212
-
213
- This section defines the 'Event' object, which represents a configurable,
214
- remote event that can be invoked from the game server or listened to.
215
- ]]
216
-
217
- local Event = {}
218
- Event.__index = Event
219
-
220
- -- Represents a remote event defined in the Zyntex dashboard.
221
- type EventType = {
222
- id: number; -- The unique ID of the event.
223
- name: string; -- The user-defined name of the event.
224
- description: string; -- The user-defined description of the event.
225
- dataStructure: Data; -- The schema defining the expected data payload.
226
- deleted: boolean; -- Whether the event has been marked as deleted.
227
- _zyntex: Zyntex; -- Internal reference to the main Zyntex client instance.
228
- }
229
-
230
- export type Event = typeof(setmetatable({} :: EventType, Event))
231
-
232
- --[[
233
- @method Event:Invoke
234
- @description Sends an invocation for this Event to the Zyntex backend.
235
- This allows the game server to trigger events that can be logged or listened to by other systems.
236
- The data structure provided must match the schema defined on the Zyntex dashboard.
237
-
238
- @param self Event -- The event object to invoke.
239
- @param data Data -- A table containing the key-value data payload for the event.
240
- @return Invocation -- Returns an Invocation object if the API call is successful.
241
-
242
- @see https://docs.zyntex.dev/ for more details on creating and using events.
243
-
244
- @example
245
- local killEvent = Zyntex:GetEvent("PlayerKilled")
246
- killEvent:Invoke({
247
- killerId = 12345,
248
- victimId = 54321,
249
- weapon = "Sword"
250
- })
251
- ]]
252
- function Event.Invoke(self: Event, data: Data): Invocation
253
- -- Enqueue; response comes back via /push listen_data later.
254
- self._zyntex._session:post(
255
- `/roblox/events/invoke`,
256
- {
257
- ["server_id"] = self._zyntex._session.jobID,
258
- ["event_id"] = self.id;
259
- ["data"] = data
260
- }
261
- )
262
-
263
- -- Return a synthetic "queued" invocation placeholder
264
- return Invocation.new(
265
- -1,
266
- self.id,
267
- data,
268
- "web",
269
- "roblox",
270
- self._zyntex._session.jobID,
271
- now(),
272
- nil,
273
- 0,
274
- 0
275
- )
276
- end
277
-
278
- --[[
279
- @method Event:Connect
280
- @description Establishes a listener for this specific event. The provided callback function
281
- will be executed whenever an invocation for this event is received from the Zyntex backend (e.g., sent from the dashboard).
282
-
283
- @param self Event -- The event object to listen to.
284
- @param listener (({[string]: any}, Invocation) -> nil) -- The callback function to execute.
285
- - The first argument passed to the listener is a simplified data table: `{["key"] = value}`.
286
- - The second argument is the full, raw Invocation object.
287
-
288
- @example
289
- local broadcastEvent = Zyntex:GetEvent("BroadcastMessage")
290
- broadcastEvent:Connect(function(data, invocation)
291
- print(`Received broadcast from user {invocation.invokedBy}: {data.Message}`)
292
- end)
293
- ]]
294
- function Event.Connect(self: Event, listener: ({[string]: any}, Invocation) -> nil)
295
- local function wrapper(inv)
296
- if inv.event_id == self.id then
297
- local simpleData = {}
298
-
299
- for _,v in pairs(inv.data) do
300
- simpleData[v["key"]] = v["value"]
301
- end
302
-
303
- listener(
304
- simpleData,
305
- Invocation.new(
306
- inv.id,
307
- inv.event_id,
308
- inv.data,
309
- inv.to,
310
- inv.sender,
311
- inv.from_server,
312
- inv.invoked_at,
313
- inv.to_server,
314
- inv.game_id,
315
- inv.invoked_by
316
- )
317
- )
318
- end
319
- end
320
- if not self._zyntex._listeners["invocation"] then
321
- self._zyntex._listeners["invocation"] = {}
322
- end
323
- table.insert(
324
- self._zyntex._listeners["invocation"],
325
- wrapper
326
- )
327
- end
328
-
329
- --[[
330
- @constructor Event.new
331
- @description Constructs a new Event object. This is used internally during the initialization
332
- process to populate the list of available events from the Zyntex backend.
333
- ]]
334
- function Event.new(zyntex: Zyntex, id: number, name: string, description: string, dataStructure: DataSchema, deleted: boolean)
335
- local self = {}
336
- self.id = id
337
- self.name = name
338
- self.description = description
339
- self.dataStructure = dataStructure
340
- self.deleted = deleted
341
- self._zyntex = zyntex
342
-
343
- return setmetatable(self, Event)
344
- end
345
-
346
- --[[
347
- ______ _ _
348
- | ____| | | (_)
349
- | |__ _ _ _ __ ___| |_ _ ___ _ __ ___
350
- | __| | | | '_ \ / __| __| |/ _ \| '_ \/ __|
351
- | | | |_| | | | | (__| |_| | (_) | | | \__ \
352
- |_| \__,_|_| |_|\___|\__|_|\___/|_| |_|___/
353
-
354
- This section defines the 'Function' object, which is similar to events, but
355
- can only be invoked from the web-dashboard and it sends back data
356
- ]]
357
-
358
- local Function = {}
359
- Function.__index = Function
360
-
361
- type FunctionParameter = {
362
- name: string; -- The name of the paramater
363
- type: string; -- The type of the parameter's value
364
- }
365
-
366
- -- Represents a remote function defined in the Zyntex dashboard.
367
- type FunctionType = {
368
- id: number; -- The unique ID of the function.
369
- name: string; -- The user-defined name of the event.
370
- description: string; -- The user-defined description of the event.
371
- parameters: {FunctionParameter}; -- The list of parameters the function accepts
372
- _zyntex: Zyntex;
373
-
374
- Connect: (self: Function, listener: ({[string]: any}) -> any) -> nil;
375
-
376
- }
377
-
378
- export type Function = typeof(setmetatable({} :: FunctionType, Function))
379
-
380
- --[[
381
- @constructor Function.new
382
- @description Constructs a new Function object that represents a callable remote function
383
- defined in the Zyntex dashboard. This is typically used internally when the client
384
- initializes and materializes the functions available to your experience.
385
-
386
- @param zyntex Zyntex -- The active Zyntex client, used for networking and configuration.
387
- @param id number -- The unique ID of the remote function.
388
- @param name string -- The user-defined name of the function.
389
- @param description string -- A human-readable description of the function.
390
- @param parameters {FunctionParameter} -- The schema describing the parameters this function accepts.
391
-
392
- @return Function -- The constructed Function instance.
393
- ]]
394
- function Function.new(zyntex: Zyntex, id: number, name: string, description: string, parameters: {FunctionParameter})
395
- local self = {}
396
- self.id = id
397
- self.name = name
398
- self.description = description
399
- self.parameters = parameters
400
- self._zyntex = zyntex
401
-
402
- return setmetatable(self, Function)
403
- end
404
-
405
- --[[
406
- @method Function:Connect
407
- @description Registers a listener that will be invoked whenever a call targeting this
408
- remote function is received from the Zyntex backend. The listener is passed a simple
409
- parameters table and is expected to return a value (typically a table) which will be
410
- sent back to Zyntex as the payload response.
411
-
412
- @param self Function -- The Function instance to listen on.
413
- @param listener (({[string]: any}) -> any) -- Callback executed when this function is invoked.
414
- - Receives: a table of parameter key/value pairs (`metadata.parameters`).
415
- - Returns: any serializable value (commonly a table) that becomes the payload response.
416
-
417
- @return nil -- This method registers the listener and does not return a connection handle.
418
-
419
- @errors Asserts if the payload POST fails; the assertion message is the API's `user_message`.
420
-
421
- @example
422
- -- Assume a remote function "Add" with parameters: { a: number, b: number }
423
- local addFn = Zyntex:GetFunction("Add")
424
- addFn:Connect(function(params)
425
- local a = params.a
426
- local b = params.b
427
- -- Whatever is returned here is sent back to Zyntex as the payload:
428
- return { sum = a + b }
429
- end)
430
- ]]
431
- function Function.Connect(self: Function, listener: ({[string]: any}) -> any)
432
- local function wrapper(data)
433
- if data.metadata.function_id == self.id then
434
- if self._zyntex._config.debug then
435
- print(`[Zyntex]: Function received, calling listener...`)
436
- end
437
-
438
- local payload = listener(
439
- data.metadata.parameters
440
- )
441
-
442
- if self._zyntex._config.debug then
443
- print(`[Zyntex]: Sending payload response...`)
444
- end
445
-
446
- assert(payload ~= nil, 'Function:Connect hook must return a value')
447
-
448
- local callRes = self._zyntex._session:post(
449
- `/roblox/actions/{data.id}/send_payload`,
450
- { ["data"] = payload },
451
- true
452
- )
453
-
454
- if self._zyntex._config.debug then
455
- print(`[Zyntex]: Successfully sent payload response to function`)
456
- end
457
- end
458
- end
459
- if not self._zyntex._listeners["function"] then
460
- self._zyntex._listeners["function"] = {}
461
- end
462
- table.insert(
463
- self._zyntex._listeners["function"],
464
- wrapper
465
- )
466
- end
467
-
468
-
469
- --[[
470
-
471
- ______ _
472
- |___ / | |
473
- / /_ __ _ _ __ | |_ _____ __
474
- / /| | | | '_ \| __/ _ \ \/ /
475
- / /_| |_| | | | | || __/> <
476
- /_____\__, |_| |_|\__\___/_/\_\
477
- __/ |
478
- |___/
479
-
480
- This section defines the main 'Zyntex' client object, which serves as the primary
481
- interface for interacting with the entire Zyntex system.
482
- ]]
483
-
484
- local Zyntex = { VERSION = CLIENT_VERSION }
485
- Zyntex.__index = Zyntex
486
-
487
- -- The main state container for the Zyntex client.
488
- type ZyntexType = {
489
- _session: API.Session; -- Handles authenticated requests to the Zyntex API.
490
- _config: TYPES.Config; -- Stores the user-defined configuration for the client.
491
- _events: {Event}; -- A list of all available Event objects fetched from the backend.
492
- _functions: {Function}; -- A list of all available Function objects fetched from the backend.
493
- _listeners: {[string]: ({[string]: any}) -> nil}; -- A dictionary of listeners for incoming actions (e.g., 'shutdown', 'rce').
494
- _version: number;
495
- _pendingJoins: {[string]: Player}; -- key: batch rid -> Player
496
- }
497
-
498
- export type Zyntex = typeof(setmetatable({} :: ZyntexType, Zyntex))
499
-
500
- local maxPlayers = 0;
501
-
502
- --[[
503
- @function serverStatus
504
- @description Gathers real-time performance metrics about the current server instance.
505
- @return table -- A dictionary containing health status, memory usage, network traffic, and FPS.
506
- ]]
507
- local function serverStatus()
508
- local maxMemory = 6400 + 100 * maxPlayers
509
- local stats = {
510
- ["memory_usage"] = Stats:GetTotalMemoryUsageMb() / maxMemory,
511
- ["data_receive_kbps"] = Stats.DataReceiveKbps or 0,
512
- ["data_send_kbps"] = Stats.DataSendKbps or 0,
513
- ["server_fps"] = math.clamp(1/RunService.Heartbeat:Wait(), 0, 100),
514
- ["activity"] = SERVER_ACTIVITY
515
- }
516
-
517
- if stats.memory_usage > .5 or stats.data_send_kbps > 500 or stats.data_receive_kbps > 500 or stats.server_fps < 30 then
518
- stats["health"] = "unhealthy"
519
- return stats
520
- end
521
-
522
- stats["health"] = "healthy"
523
- return stats
524
- end
525
-
526
- --[[
527
- @method Zyntex:statusUpdate
528
- @description Sends the server's current health and performance metrics to the Zyntex dashboard.
529
- @param self Zyntex
530
- @param status {string: number} -- A table of metrics generated by `serverStatus()`.
531
- ]]
532
- function Zyntex.statusUpdate(self: Zyntex, status: {string: number})
533
- if sendingStatus then return end
534
- sendingStatus = true
535
- if self._config.debug then
536
- print("[Zyntex]: Submitting server status update...")
537
- end
538
-
539
- pcall(function()
540
- self._session:post(
541
- "/roblox/servers/status",
542
- table.freeze({
543
- ["server_id"] = self._session.jobID,
544
- ["memory_usage"] = status["memory_usage"],
545
- ["server_fps"] = status["server_fps"],
546
- ["data_send_kbps"] = status["data_send_kbps"],
547
- ["data_receive_kbps"] = status["data_receive_kbps"],
548
- ["activity"] = status["activity"],
549
- ["health"] = if status["health"] == "unhealthy" then "unhealthy" else "healthy"
550
- })
551
- )
552
- if self._config.debug then
553
- print("[Zyntex]: Sent server status update (queued)")
554
- end
555
- end)
556
-
557
- sendingStatus = false
558
- end
559
-
560
- --[[
561
- @function statusUpdateLoop
562
- @description A background loop that periodically collects and sends server status updates.
563
- It intelligently sends updates only when metrics have changed to reduce unnecessary network traffic.
564
- @param self Zyntex
565
- ]]
566
- local function statusUpdateLoop(self: Zyntex)
567
- local lastStatus = serverStatus()
568
-
569
- while true do
570
- task.wait(15)
571
-
572
- local newStatus = serverStatus()
573
-
574
- self:statusUpdate(newStatus)
575
- end
576
- end
577
-
578
- --[[
579
- @function onPlayerAdd
580
- @description Handles all tasks associated with a player joining the game, including
581
- registering them with the Zyntex backend and setting up client-side scripts.
582
- @param self Zyntex
583
- @param player Player -- The player who just joined.
584
- ]]
585
- local function onPlayerAdd(self: Zyntex, player: Player)
586
- if self._config.debug then
587
- print(`[Zyntex]: Handling player join for {player.Name}`)
588
- end
589
-
590
- local when = now()
591
- local activity = {
592
- player_id = player.UserId,
593
- player_name = player.Name,
594
- joined_at = when
595
- }
596
-
597
- -- Enqueue join; **capture the batch item's rid** returned by api.post
598
- local rid = self._session:post(
599
- "/roblox/players",
600
- {
601
- ["server_id"] = self._session.jobID,
602
- ["id"] = player.UserId,
603
- ["name"] = player.Name,
604
- ["timestamp"] = when
605
- }
606
- )
607
- -- Track pending join by the **batch rid** so we can reconcile on push results
608
- self._pendingJoins[rid] = player
609
-
610
- pcall(function()
611
- -- No immediate response under /push; moderation blocks are surfaced via server on next poll.
612
-
613
- if self._config.debug then
614
- print("[Zyntex]: Player join submitted.")
615
- end
616
-
617
- local zyntexEvents = Instance.new("Folder")
618
- zyntexEvents.Name = "zyntex.events"
619
- zyntexEvents.Parent = script.Parent
620
-
621
- local SystemChat = Instance.new("RemoteEvent")
622
- SystemChat.Name = "SystemChat"
623
- SystemChat.Parent = zyntexEvents
624
-
625
- -- Clone necessary client-side remote events into ReplicatedStorage if they don't exist.
626
- if not game:GetService("ReplicatedStorage"):FindFirstChild("zyntex.events") then
627
- zyntexEvents:Clone().Parent = game:GetService("ReplicatedStorage")
628
- end
629
-
630
- -- Provide the player with the client-side script handler.
631
- script.Parent:FindFirstChild("zyntex.client"):Clone().Parent = player.PlayerGui
632
-
633
- -- Listen for player chats and log them.
634
- pcall(function()
635
- player.Chatted:Connect(function(msg)
636
- self._session:post(
637
- "/roblox/players/chat",
638
- {
639
- ["server_id"] = self._session.jobID,
640
- ["message"] = msg;
641
- ["player_id"] = player.UserId
642
- }
643
- )
644
- end)
645
- end)
646
-
647
- -- Track the maximum concurrent players for server health calculations.
648
- local count = #Players:GetPlayers()
649
- if count > maxPlayers then
650
- maxPlayers = count
651
- end
652
- end)
653
-
654
- -- visit_id will be reconciled on the backend; we don't have it synchronously here.
655
-
656
- table.insert(SERVER_ACTIVITY, activity)
657
- end
658
-
659
- --[[
660
- @function onPlayerRemove
661
- @description Handles a player leaving the game by notifying the Zyntex backend,
662
- which marks the player's session as ended.
663
- @param self Zyntex
664
- @param player Player -- The player who just left.
665
- ]]
666
- local function onPlayerRemove(self: Zyntex, player: Player)
667
- if self._config.debug then
668
- print(`[Zyntex]: Handling player leave for {player.Name}`)
669
- end
670
-
671
- local when = now()
672
-
673
- -- Clean up any pending joins for this player
674
- for rid, pendingPlayer in pairs(self._pendingJoins) do
675
- if pendingPlayer.UserId == player.UserId then
676
- self._pendingJoins[rid] = nil
677
- if self._config.debug then
678
- print(("[Zyntex]: Cleaned up pending join for leaving player %s (RID: %s)"):format(player.Name, rid))
679
- end
680
- break
681
- end
682
- end
683
-
684
- -- Enqueue leave (handled by /roblox/push). Fire-and-forget.
685
- self._session:delete(
686
- "/roblox/players",
687
- {
688
- ["server_id"] = self._session.jobID,
689
- ["id"] = player.UserId,
690
- ["timestamp"] = when
691
- }
692
- )
693
-
694
- if self._config.debug then
695
- print("[Zyntex]: Player leave submitted.")
696
- end
697
-
698
- for _,activity in SERVER_ACTIVITY do
699
- if activity.player_id == player.UserId and activity.left_at == nil then
700
- activity["left_at"] = when
701
- end
702
- end
703
- end
704
-
705
- --[[
706
- @method Zyntex:GetEventByID
707
- @description Fetches a pre-loaded Event object using its unique numerical ID.
708
- @param self Zyntex
709
- @param eventId number -- The ID of the event to retrieve.
710
- @return Event? -- The corresponding Event object, if exists.
711
- ]]
712
- function Zyntex.GetEventByID(self: Zyntex, eventId: number): Event?
713
- for i,event in pairs(self._events) do
714
- if event.id == eventId then
715
- return event
716
- end
717
- end
718
- return nil
719
- end
720
-
721
- --[[
722
- @method Zyntex:GetEvent
723
- @description Fetches a pre-loaded Event object using its unique string name. This is the most common way to get an event.
724
- @param self Zyntex
725
- @param eventName string -- The case-sensitive name of the event.
726
- @return Event? -- The corresponding Event object, if it exists.
727
- ]]
728
- function Zyntex.GetEvent(self: Zyntex, eventName: string): Event?
729
- for i,event in pairs(self._events) do
730
- if event.name == eventName then
731
- return event
732
- end
733
- end
734
- return nil
735
- end
736
-
737
- --[[
738
- @method Zyntex:GetFunctionByID
739
- @description Fetches a pre-loaded Function object using its unique numerical ID.
740
- @param self Zyntex
741
- @param functionId number -- The ID of the function to retrieve.
742
- @return Function? -- The corresponding Function object, if it exists.
743
- ]]
744
- function Zyntex.GetFunctionByID(self: Zyntex, functionId: number): Function?
745
- for i,func in self._functions do
746
- if func.id == functionId then
747
- return func
748
- end
749
- end
750
- return nil
751
- end
752
-
753
- --[[
754
- @method Zyntex:GetFunction
755
- @description Fetches a pre-loaded Function object using its unique string name. This is the most common way to get a function.
756
- @param self Zyntex
757
- @param functionName string -- The case-sensitive name of the function.
758
- @return Function? -- The corresponding Function object, if it exists.
759
- ]]
760
- function Zyntex.GetFunction(self: Zyntex, functionName: string): Function?
761
- for i,func in self._functions do
762
- if func.name == functionName then
763
- return func
764
- end
765
- end
766
- return nil
767
- end
768
-
769
- --[[
770
- @method Zyntex:Moderate
771
- @description Submits a generic moderation action to the Zyntex backend. This is a flexible, low-level function;
772
- it is often easier to use the shorthand methods (:Ban, :Kick, :Mute, :Report).
773
-
774
- @warning If the `test` parameter is `false` or `nil` (and not in Studio), this action WILL permanently affect a player's reputation.
775
-
776
- @param self Zyntex
777
- @param player (Player | number) -- The Player object or the UserId of the target.
778
- @param type string -- The type of moderation (e.g., "ban", "kick", "mute", "report").
779
- @param reason string -- The reason for the moderation action. This will be visible to staff.
780
- @param expiresAt (DateTime?) -- An optional DateTime for when the moderation expires. If nil, it may be permanent depending on the type.
781
- @param test (boolean?) -- If `true`, the moderation is treated as a test and does not affect reputation. Defaults to `true` in Studio.
782
- @return boolean -- Returns `true` on success.
783
- ]]
784
- function Zyntex.Moderate(self: Zyntex, player: Player | number, type: string, reason: string, expiresAt: DateTime?, test: boolean?)
785
- local playerId = if typeof(player) == "Player" then player.UserId else player
786
- local playerObject = Players:GetPlayerByUserId(playerId)
787
- if self._config.debug then
788
- print(`[Zyntex]: Creating moderation {type} for {playerId}...`)
789
- end
790
- if expiresAt then
791
- expiresAt = toFormat(expiresAt)
792
- end
793
-
794
- -- Default to test mode if running in Roblox Studio unless explicitly overridden.
795
- if test == nil then
796
- test = RunService:IsStudio()
797
- end
798
-
799
- if type == "ban" then
800
- playerObject:Kick(reason)
801
- end
802
-
803
- local res = self._session:post(
804
- `/roblox/players/{playerId}/moderate`,
805
- {
806
- ["server_id"] = self._session.jobID,
807
- ["type"] = type,
808
- ["reason"] = reason,
809
- ["expires_at"] = expiresAt,
810
- ["test"] = test
811
- }
812
- )
813
-
814
- if self._config.debug then
815
- print(`[Zyntex]: Moderation created successfully.`)
816
- end
817
-
818
- return true
819
- end
820
-
821
- --[[
822
- @method Zyntex:Report
823
- @description Shorthand method to create a "report" moderation. Reports are used for logging player behavior
824
- and contribute to their reputation score but do not have direct in-game consequences by default.
825
- @param self Zyntex
826
- @param player (Player | number) -- The Player object or the UserId of the target.
827
- @param reason string -- The reason for the report.
828
- @param test (boolean?) -- If true, the report will not affect player reputation. Defaults to `true` in Studio.
829
- @return boolean -- Returns `true` on success.
830
- ]]
831
- function Zyntex.Report(self: Zyntex, player: Player | number, reason: string, test: boolean?)
832
- return self:Moderate(player, "report", reason, nil, test)
833
- end
834
-
835
- --[[
836
- @method Zyntex:Ban
837
- @description Shorthand method to create a "ban" moderation. This logs the ban action.
838
- Handles automatic kick automatically.
839
- @param self Zyntex
840
- @param player (Player | number) -- The Player object or the UserId of the target.
841
- @param reason string -- The reason for the ban.
842
- @param expiresAt (DateTime?) -- Optional expiration for the ban. If nil, the ban is permanent.
843
- @param test (boolean?) -- If true, the ban will not affect player reputation. Defaults to `true` in Studio.
844
- @return boolean -- Returns `true` on success.
845
- ]]
846
- function Zyntex.Ban(self: Zyntex, player: Player | number, reason: string, expiresAt: DateTime?, test: boolean?)
847
- return self:Moderate(player, "ban", reason, expiresAt, test)
848
- end
849
-
850
- --[[
851
- @method Zyntex:Mute
852
- @description Shorthand method to create a "mute" moderation. This logs the mute action.
853
- Mutes the player automatically.
854
- @param self Zyntex
855
- @param player (Player | number) -- The Player object or the UserId of the target.
856
- @param reason string -- The reason for the mute.
857
- @param expiresAt (DateTime?) -- Optional expiration for the mute. If nil, the mute is permanent.
858
- @param test (boolean?) -- If true, the mute will not affect player reputation. Defaults to `true` in Studio.
859
- @return boolean -- Returns `true` on success.
860
- ]]
861
- function Zyntex.Mute(self: Zyntex, player: Player | number, reason: string, expiresAt: DateTime?, test: boolean?)
862
- return self:Moderate(player, "mute", reason, expiresAt, test)
863
- end
864
-
865
- --[[
866
- @method Zyntex:Kick
867
- @description Shorthand method to create a "kick" moderation. This logs the kick action.
868
- Calls Player:Kick() automatically.
869
- @param self Zyntex
870
- @param player (Player | number) -- The Player object or the UserId of the target.
871
- @param reason string -- The reason for the kick.
872
- @param test (boolean?) -- If true, the kick will not affect player reputation. Defaults to `true` in Studio.
873
- @return boolean -- Returns `true` on success.
874
- ]]
875
- function Zyntex.Kick(self: Zyntex, player: Player | number, reason: string, test: boolean?)
876
- return self:Moderate(player, "kick", reason, nil, test)
877
- end
878
-
879
-
880
-
881
- --[[
882
- @method Zyntex:CleanupPendingJoins
883
- @description Manually clean up pending joins that may have been stuck.
884
- This is useful for debugging or in case of network issues.
885
- @param self Zyntex
886
- @return number -- Returns the number of pending joins that were cleaned up.
887
- ]]
888
- -- Note: CleanupPendingJoins removed - joins are cleaned up immediately on result
889
-
890
- --[[
891
- @method Zyntex:GetPendingJoinsCount
892
- @description Get the current count of pending joins.
893
- This is useful for monitoring the state of join operations.
894
- @param self Zyntex
895
- @return number -- Returns the number of pending joins.
896
- ]]
897
- function Zyntex.GetPendingJoinsCount(self: Zyntex)
898
- local count = 0
899
- for _ in pairs(self._pendingJoins) do
900
- count = count + 1
901
- end
902
- return count
903
- end
904
-
905
- --[[
906
- @method Zyntex:poll
907
- @description Initiates the long-polling loop to listen for real-time actions and invocations
908
- from the Zyntex dashboard. This runs in its own thread.
909
- @param self Zyntex
910
- @param since string -- An ISO 8601 timestamp to start listening from.
911
- @return (-> ()) -- Returns the polling function to be spawned in a new thread.
912
- ]]
913
- function Zyntex.poll(self: Zyntex, since: string)
914
- return function()
915
- local nextWaitTime = Random.new():NextInteger(4, 7)
916
- while true do
917
- if self._config.debug then
918
- print("[Zyntex]: Flushing /push buffer (poll)...")
919
- end
920
-
921
- local items = {}
922
- local ok, resp = pcall(function()
923
- return self._session:Flush(since)
924
- end)
925
- if ok and resp and resp.success then
926
- items = (resp.data and resp.data.listen_data) or {}
927
- if #items == 0 then
928
- nextWaitTime = math.clamp(nextWaitTime + 0.1, 2, 10)
929
- else
930
- nextWaitTime = 2
931
- since = now()
932
- end
933
- else
934
- nextWaitTime = nextWaitTime + 10
935
- end
936
-
937
- -- Dispatch any received events
938
- for _, entry in pairs(items) do
939
- local bucket = self._listeners[entry.type]
940
- if bucket then
941
- for _, listener in pairs(bucket) do
942
- listener(entry.data)
943
- end
944
- end
945
- end
946
-
947
- -- React to per-item results: moderate on failed joins (401/403)
948
- if resp and resp.data and resp.data.results then
949
- for _, result in pairs(resp.data.results) do
950
- -- We only care about failed player joins here
951
- if result.method == "POST /roblox/players" and result.rid then
952
- local rid = result.rid
953
- local player = self._pendingJoins[rid]
954
- if player then
955
- if result.success == true then
956
- -- successfully admitted; clear pending
957
- self._pendingJoins[rid] = nil
958
- if self._config.debug then
959
- print(`[Zyntex]: Join confirmed for {player.Name} (RID: {rid})`)
960
- end
961
- else
962
- -- Parse "CODE: reason" -> e.g. "403: racism" / "401: spam"
963
- local codeStr, reason = nil, nil
964
- if typeof(result.error) == "string" then
965
- codeStr, reason = string.match(result.error, "^(%d+)%s*:%s*(.+)$")
966
- end
967
- local code = tonumber(codeStr or "")
968
- reason = reason or "Moderation"
969
-
970
- if code == 403 then
971
- -- BAN: kick immediately + fire ban listeners
972
- local p = Players:GetPlayerByUserId(player.UserId)
973
- if p then p:Kick(reason) end
974
- if self._listeners["ban"] then
975
- for _,cb in self._listeners["ban"] do
976
- cb(player.UserId, reason)
977
- end
978
- end
979
- if self._config.debug then
980
- warn(`[Zyntex]: {player.Name} blocked (BAN) — {reason}`)
981
- end
982
- elseif code == 401 then
983
- -- MUTE: silence chat + fire mute listeners
984
- muteUserId(player.UserId)
985
- if self._listeners["mute"] then
986
- for _,cb in self._listeners["mute"] do
987
- cb(player.UserId, reason)
988
- end
989
- end
990
- if self._config.debug then
991
- warn(`[Zyntex]: {player.Name} auto-muted — {reason}`)
992
- end
993
- else
994
- -- Unknown failure; just warn
995
- warn(`[Zyntex]: Join failed for {player.Name} (RID: {rid}) — {tostring(result.error)}`)
996
- end
997
-
998
- -- In all error cases, clear pending
999
- self._pendingJoins[rid] = nil
1000
- end
1001
- elseif self._config.debug then
1002
- print(`[Zyntex]: Result for unknown RID (possibly already cleaned): {result.rid}`)
1003
- end
1004
- end
1005
- end
1006
- end
1007
-
1008
- task.wait(nextWaitTime)
1009
- end
1010
- end
1011
- end
1012
-
1013
- --[[
1014
- @method Zyntex:logServiceMessage
1015
- @description Internal handler for capturing messages from Roblox's LogService (print, warn, error)
1016
- and forwarding them to the Zyntex logs for the server.
1017
- @param self Zyntex
1018
- @param msg string -- The content of the log message.
1019
- @param type Enum.MessageType -- The type of message (Output, Info, Warning, Error).
1020
- ]]
1021
- function Zyntex.logServiceMessage(self: Zyntex, msg: string, type: Enum.MessageType)
1022
- local convertedType: string?
1023
- if type == Enum.MessageType.MessageOutput then
1024
- convertedType = "info.print"
1025
- end
1026
- if type == Enum.MessageType.MessageError then
1027
- convertedType = "info.error"
1028
- end
1029
- if type == Enum.MessageType.MessageWarning then
1030
- convertedType = "info.warning"
1031
- end
1032
- if convertedType == nil then
1033
- return type
1034
- end
1035
-
1036
- -- Truncate long messages to prevent overly large payloads.
1037
- local msg = string.sub(msg, 1, 100)
1038
-
1039
- pcall(function()
1040
- self._session:post(
1041
- "/roblox/logservice",
1042
- {
1043
- ["server_id"] = self._session.jobID,
1044
- ["message"] = msg,
1045
- ["type"] = convertedType
1046
- }
1047
- )
1048
- end)
1049
- end
1050
-
1051
- --[[
1052
- @method Zyntex:Log
1053
- @description Logs a custom message to the Zyntex servers. This is useful for tracking specific
1054
- game events. The log will appear in the main logs, the server's log tab, and the associated player's log tab if a player is provided.
1055
-
1056
- @param self Zyntex
1057
- @param message string -- The custom message to log.
1058
- @param player (Player? | number?) -- Optional. The Player or UserId to associate with this log entry.
1059
- @return boolean -- Returns `true` on success.
1060
- ]]
1061
- function Zyntex.Log(self: Zyntex, message: string, player: Player? | number?)
1062
- if self._config.debug then
1063
- print(`[Zyntex]: Posting log...`)
1064
- end
1065
- local playerId, playerName;
1066
- if player then
1067
- playerId = if typeof(player) == "Player" then player.UserId else player
1068
- playerName = if typeof(player) == "Player" then player.Name else Players:GetPlayerByUserId(player).Name
1069
- end
1070
-
1071
- local payload = {
1072
- ["message"] = message
1073
- }
1074
-
1075
- if playerId then
1076
- payload["player_id"] = playerId
1077
- payload["player_name"] = playerName
1078
- end
1079
-
1080
- local res = self._session:post(
1081
- `/roblox/log`,
1082
- payload
1083
- )
1084
-
1085
- -- Under /push this is enqueued; no synchronous assert.
1086
-
1087
- if self._config.debug then
1088
- print(`[Zyntex]: Log posted succesfully.`)
1089
- end
1090
-
1091
- return true
1092
- end
1093
-
1094
- --[[
1095
- @method Zyntex:ProcessPurchase
1096
- @description Logs a player's in-game purchase. This should be wired up to `MarketplaceService.ProcessReceipt`
1097
- to track player spending and LTV on the dashboard.
1098
-
1099
- @param self Zyntex
1100
- @param player (Player | number) -- The Player object or UserId who made the purchase.
1101
- @param price number -- The price of the item in Robux.
1102
- @param productName string -- The name of the product purchased. Using the name is preferred over the ID for clarity on the dashboard.
1103
- @return boolean -- Returns `true` on success.
1104
- ]]
1105
- function Zyntex.ProcessPurchase(self: Zyntex, player: Player | number, price: number, productName: string): boolean
1106
- local res = self._session:post(
1107
- `/roblox/players/{if type(player) == "number" then player else player.UserId}/robux-spent`,
1108
- {
1109
- ["server_id"] = self._session.jobID,
1110
- ["robux_spent"] = price;
1111
- ["metadata"] = productName
1112
- }
1113
- )
1114
-
1115
- return true
1116
- end
1117
-
1118
- --[[
1119
- @constructor Zyntex.new
1120
- @description Constructs the main Zyntex client object and validates the API connection.
1121
- It also checks if the client version is up-to-date with the latest version recommended by the backend.
1122
-
1123
- @param gameToken string -- The unique game token obtained from the Zyntex dashboard.
1124
- @return Zyntex -- The newly created Zyntex instance.
1125
- ]]
1126
- function Zyntex.new(gameToken: string): Zyntex
1127
- local self = {}
1128
-
1129
- -- Pass JobId to enable the API push buffer/flush worker
1130
- self._session = API.new(gameToken)
1131
- self._config = {}
1132
- self._events = {}
1133
- self._functions = {}
1134
- self._listeners = {}
1135
- self._pendingJoins = {}
1136
- self._version = CLIENT_VERSION
1137
-
1138
- local current_version = self._session:get("/roblox/latest-client-version") -- direct HTTP (not push)
1139
- assert(current_version and current_version.success, `Zyntex API is currently down.`)
1140
- if (current_version.data ~= CLIENT_VERSION) then
1141
- warn(`[Zyntex]: Your client is outdated, please download the latest version. current: v{CLIENT_VERSION}; latest: v{current_version.data}`)
1142
- end
1143
-
1144
- -- Inside Zyntex.init, right after `self._session` is created
1145
- local function handlePush(data)
1146
- -- reuse the same logic currently inside Zyntex.poll
1147
- if data and data.results then
1148
- for _, result in ipairs(data.results) do
1149
- if result.method == "POST /roblox/players" and result.rid then
1150
- local player = self._pendingJoins[result.rid]
1151
- if not player then continue end
1152
-
1153
- if not result.success and typeof(result.error) == "string" then
1154
- local codeStr, reason = string.match(
1155
- result.error,
1156
- "^(%d+)%s*:%s*(.+)$"
1157
- )
1158
- local code = tonumber(codeStr)
1159
- reason = reason or "Moderation"
1160
-
1161
- if code == 403 then
1162
- local p = Players:GetPlayerByUserId(player.UserId)
1163
- if p then p:Kick(reason) end
1164
- elseif code == 401 then
1165
- muteUserId(player.UserId)
1166
- end
1167
- end
1168
- self._pendingJoins[result.rid] = nil
1169
- end
1170
- end
1171
- end
1172
- end
1173
-
1174
- -- register the universal handler
1175
- self._session:onFlush(handlePush)
1176
-
1177
- return setmetatable(self, Zyntex)
1178
- end
1179
-
1180
- --[[
1181
- @function randomUsername
1182
- @description Generates a random username for simulation purposes.
1183
- ]]
1184
- local function randomUsername()
1185
- return "User_" .. math.random(1000, 9999)
1186
- end
1187
-
1188
- --[[
1189
- @function simulateActivity
1190
- @description When `config.simulate` is true, this function creates fake player join/leave events
1191
- to help test the system in Studio without needing real players.
1192
- ]]
1193
- local function simulateActivity(self)
1194
- local fakePlayers = {}
1195
-
1196
- while true do
1197
- task.wait(math.random(0.1, 10))
1198
-
1199
- local action = math.random(1, 4)
1200
-
1201
- if action == 1 or action == 2 then
1202
- -- Simulate Player Join
1203
- local name = randomUsername()
1204
- local id = 16054156146
1205
- local fakePlayer = {
1206
- Name = name,
1207
- UserId = id
1208
- }
1209
- table.insert(fakePlayers, fakePlayer)
1210
- pcall(onPlayerAdd, self, fakePlayer)
1211
-
1212
- elseif (action == 3 or action == 4) and #fakePlayers > 0 then
1213
- -- Simulate Player Leave
1214
- local index = math.random(1, #fakePlayers)
1215
- local fakePlayer = table.remove(fakePlayers, index)
1216
- pcall(onPlayerRemove, self, fakePlayer)
1217
- end
1218
- end
1219
- end
1220
-
1221
- --[[
1222
- @method Zyntex:GetPlayerInfo
1223
- @description Fetches and returns player information, such as reputation, total robux spent, and total time played.
1224
-
1225
- @param self Zyntex
1226
- @param player Player|number -- Either a Player object or the player's UserId. If UserId, the player does not have to be in the server.
1227
- ]]
1228
- function Zyntex.GetPlayerInfo(self: Zyntex, player: Player | number): {player: ZyntexPlayer, total_robux_spent: number, total_time_played: number}
1229
- local res = self._session:get(`/roblox/players/{if type(player) == "number" then player else player.UserId}`)
1230
-
1231
- assert(res.success, `Failed when attempting to fetch player: {res.user_message}`)
1232
-
1233
- local playerInfo = res.data
1234
- local playerInfoRaw = res.data.player
1235
-
1236
- playerInfo["player"] = ZyntexPlayer.new(
1237
- playerInfoRaw.id,
1238
- playerInfoRaw.name,
1239
- playerInfoRaw.avatar_url,
1240
- playerInfoRaw.avatar_url_cache_expiry,
1241
- playerInfoRaw.reputation,
1242
- playerInfoRaw.raw_reputation
1243
- )
1244
-
1245
- return playerInfo
1246
- end
1247
-
1248
- --[[
1249
- @method Zyntex:OnModeration
1250
- @description Creates a hook to the moderation action.
1251
-
1252
- @param self Zyntex
1253
- @param moderationType string -- Returns a moderationType. Either 'ban', 'mute', or 'kick'.
1254
- @param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the moderation occurs.
1255
- ]]
1256
- function Zyntex.OnModeration(self: Zyntex, moderationType: string, callback: (player: number, reason: string) -> nil)
1257
- table.insert(self._listeners[moderationType], callback)
1258
- end
1259
-
1260
- --[[
1261
- @method Zyntex:OnModeration
1262
- @description Creates a hook to the 'kick' mo eration action.
1263
-
1264
- @param self Zyntex
1265
- @param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the kick occurs.
1266
- ]]
1267
- function Zyntex.OnKick(self: Zyntex, callback: (player: number, reason: string) -> nil)
1268
- return self:OnModeration("kick", callback)
1269
- end
1270
-
1271
- --[[
1272
- @method Zyntex:OnModeration
1273
- @description Creates a hook to the 'ban' moderation action.
1274
-
1275
- @param self Zyntex
1276
- @param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the ban occurs.
1277
- ]]
1278
- function Zyntex.OnBan(self: Zyntex, callback: (player: number, reason: string) -> nil)
1279
- return self:OnModeration("ban", callback)
1280
- end
1281
-
1282
- --[[
1283
- @method Zyntex:OnMute
1284
- @description Creates a hook to the 'mute' moderation action.
1285
-
1286
- @param self Zyntex
1287
- @param callback (playerId: number, reason: string) -> nil -- The callback that is called whenever the mute occurs.
1288
- ]]
1289
- function Zyntex.OnMute(self: Zyntex, callback: (player: number, reason: string) -> nil)
1290
- return self:OnModeration("mute", callback)
1291
- end
1292
-
1293
- --[[
1294
- @method Zyntex:init
1295
- @description The main entry point to start the Zyntex client. This function initializes all core processes:
1296
- it registers the server with the Zyntex backend, starts the status update and polling loops,
1297
- sets up event listeners, and connects player tracking signals.
1298
-
1299
- @param self Zyntex
1300
- @param config TYPES.Config -- A configuration table that controls client behavior (e.g., `debug`, `simulate`).
1301
- ]]
1302
- function Zyntex.init(self: Zyntex, config: TYPES.Config)
1303
- self._config = config
1304
-
1305
- local since = now()
1306
-
1307
- if config.debug then
1308
- print("[Zyntex]: Zyntex.init")
1309
- print("[Zyntex]: Initializing server...")
1310
- end
1311
-
1312
- --// Initial server creation (ENQUEUED for /roblox/push)
1313
- local privateServerId = game.PrivateServerId
1314
- local privateServerParameter = ""
1315
- if privateServerId ~= "" and game.PrivateServerOwnerId ~= 0 then
1316
- privateServerParameter = privateServerId
1317
- end
1318
- self._session:post(
1319
- "/roblox/servers",
1320
- {
1321
- ["server_id"] = self._session.jobID,
1322
- ["version"] = game.PlaceVersion,
1323
- ["isPrivate"] = privateServerParameter
1324
- }
1325
- )
1326
-
1327
- -- FIRST FLUSH: create server + fetch manifest + initial listen data + moderation lists
1328
- local firstPush = self._session:Flush(since)
1329
- if not firstPush or not firstPush.success then
1330
- error(`Error when attempting to initialize server via /push`)
1331
- end
1332
- local manifest = firstPush.data and firstPush.data.manifest
1333
- local listen_data = firstPush.data and firstPush.data.listen_data
1334
-
1335
- -- Materialize functions + events from manifest (from /push)
1336
- if manifest then
1337
- for _,v in pairs(manifest) do
1338
- if v.type == "function" then
1339
- table.insert(self._functions, Function.new(
1340
- self,
1341
- v.id,
1342
- v.name,
1343
- v.description or "",
1344
- v.parameters or {}
1345
- ))
1346
- elseif v.type == "event" then
1347
- table.insert(self._events, Event.new(
1348
- self,
1349
- v.id,
1350
- v.name,
1351
- v.description or "",
1352
- v.data or {},
1353
- false
1354
- ))
1355
- end
1356
- end
1357
- end
1358
-
1359
- --// Capture and send historical logs from before the client initialized.
1360
- -- Don't want to send 300 messages all at once :)
1361
- task.spawn(function()
1362
- local history = LogService:GetLogHistory()
1363
- local delay = 0
1364
- if #history > 5 then
1365
- delay = 1
1366
- end
1367
- -- Avoid sending an excessive number of historical logs.
1368
- if #history > 30 then
1369
- return
1370
- end
1371
- for _,message in pairs(history) do
1372
- self:logServiceMessage(message.message, message.messageType)
1373
- task.wait(delay)
1374
- end
1375
- end)
1376
-
1377
- --// Connect live LogService listener.
1378
- LogService.MessageOut:Connect(function(msg: string, type: Enum.MessageType)
1379
- self:logServiceMessage(msg, type)
1380
- end)
1381
-
1382
- if config.debug then
1383
- print("[Zyntex]: Server initialized")
1384
- end
1385
-
1386
- --// On server shutdown, notify the Zyntex backend.
1387
- game:BindToClose(function()
1388
- if config.debug then
1389
- print("[Zyntex]: Server closing...")
1390
- end
1391
- sendingStatus = true
1392
- self._session:delete("/roblox/servers", { ["server_id"] = self._session.jobID })
1393
- self._session:Flush()
1394
-
1395
- -- Clean up pending joins on shutdown
1396
- local pendingCount = 0
1397
- for rid, _ in pairs(self._pendingJoins) do
1398
- pendingCount = pendingCount + 1
1399
- end
1400
- if pendingCount > 0 and config.debug then
1401
- print(("[Zyntex]: Cleaning up %d pending joins on shutdown"):format(pendingCount))
1402
- end
1403
- self._pendingJoins = {}
1404
- end)
1405
-
1406
- --// Start the background processes.
1407
- task.spawn(statusUpdateLoop, self)
1408
- task.spawn(self:poll(since)) -- poll now uses /roblox/push flush
1409
-
1410
- -- (No direct /manifest fetch: handled via first /push response)
1411
-
1412
- --// Handle server shutdown action from the dashboard.
1413
- self._listeners["shutdown"] = {}
1414
- table.insert(self._listeners["shutdown"], function(action)
1415
- local reason = action.metadata
1416
-
1417
- -- Kick all current and future players with the provided reason.
1418
- for _,player in pairs(Players:GetPlayers()) do
1419
- player:Kick(reason)
1420
- end
1421
-
1422
- Players.PlayerAdded:Connect(function(player)
1423
- player:Kick(reason)
1424
- end)
1425
-
1426
- -- Notify the dashboard that the action has been fulfilled.
1427
- local res = self._session:post(
1428
- `/roblox/actions/{action.id}/fulfill`,
1429
- {},
1430
- true
1431
- )
1432
-
1433
- if not res.success then
1434
- warn(`[Zyntex]: Server shutdown fulfillment failed: {res.user_message}`)
1435
- end
1436
-
1437
- if config.debug then
1438
- print(`[Zyntex]: Server shutdown fulfilled.`)
1439
- end
1440
- end)
1441
-
1442
- --// Handle remote code execution (RCE) action from the dashboard.
1443
- self._listeners["rce"] = {}
1444
- table.insert(self._listeners["rce"], function(action)
1445
- local code = action.metadata
1446
-
1447
- if config.debug then
1448
- print(`[Zyntex]: Fulfilling RCE request...`)
1449
- end
1450
-
1451
- -- Execute the code in a protected call to prevent crashes.
1452
- task.spawn(function()
1453
- local success,data = pcall(function()
1454
- local executable,msg = require(script.Parent:FindFirstChild("Loadstring") :: ModuleScript)(code)
1455
- executable()
1456
- end)
1457
-
1458
- if not success then
1459
- warn(`RCE Failure: {data}`)
1460
- end
1461
- end)
1462
-
1463
- local res = self._session:post(
1464
- `/roblox/actions/{action.id}/fulfill`,
1465
- {},
1466
- true
1467
- )
1468
-
1469
- if config.debug then
1470
- print(`[Zyntex]: RCE fulfilled.`)
1471
- end
1472
- end)
1473
-
1474
- --// Handle system chat message action from the dashboard.
1475
- self._listeners["chat"] = {}
1476
- table.insert(self._listeners["chat"], function(action)
1477
- if config.debug then
1478
- print(`[Zyntex]: Fulfilling chat request...`)
1479
- end
1480
-
1481
- -- Fire a remote event to all clients to display a system message.
1482
- game:GetService("ReplicatedStorage"):FindFirstChild("zyntex.events"):FindFirstChild("SystemChat"):FireAllClients(action.metadata)
1483
-
1484
- local res = self._session:post(
1485
- `/roblox/actions/{action.id}/fulfill`,
1486
- {},
1487
- true
1488
- )
1489
-
1490
- if not res.success then
1491
- warn(`[Zyntex]: Chat fulfillment failed: {res.user_message}`)
1492
- end
1493
-
1494
- if config.debug then
1495
- print(`[Zyntex]: Chat fulfilled.`)
1496
- end
1497
- end)
1498
-
1499
- --// Handle moderation action from the dashboard.
1500
- self._listeners["moderation"] = {}
1501
- self._listeners["ban"] = {}
1502
- self._listeners["mute"] = {}
1503
- self._listeners["kick"] = {}
1504
- self._listeners["report"] = {}
1505
- table.insert(self._listeners["moderation"], function(action)
1506
- if config.debug then
1507
- print(`[Zyntex]: Fulfilling moderation request...`)
1508
- print(`[Zyntex]: {action}`)
1509
- end
1510
-
1511
- local fullfilled = false
1512
-
1513
- local type = action.metadata.type
1514
- local player_id = action.metadata.player_id
1515
- local reason = action.metadata.reason
1516
-
1517
-
1518
- if #self._listeners[type] == 0 then
1519
- if type == "ban" or type == "kick" then
1520
- local player = Players:GetPlayerByUserId(player_id)
1521
- if player then
1522
- player:Kick(reason)
1523
- fullfilled = true
1524
- end
1525
- end
1526
-
1527
- if type == "mute" then
1528
- local player = Players:GetPlayerByUserId(player_id)
1529
- if player then
1530
- muteUserId(player.UserId)
1531
- fullfilled = true
1532
- end
1533
- end
1534
- else
1535
- for _,listener in self._listeners[type] do
1536
- listener(player_id, reason)
1537
- end
1538
- fullfilled = true
1539
- end
1540
-
1541
- if fullfilled then
1542
- local res = self._session:post(
1543
- `/roblox/actions/{action.id}/fulfill`,
1544
- {},
1545
- true
1546
- )
1547
-
1548
- if not res.success then
1549
- warn(`[Zyntex]: Moderation fulfillment failed: {res.user_message}`)
1550
- end
1551
- end
1552
-
1553
- if config.debug then
1554
- print(`[Zyntex]: Moderation request fulfilled.`)
1555
- end
1556
- end)
1557
-
1558
- --// If simulation is enabled in the config, start the activity simulator.
1559
- if config.simulate then
1560
- task.spawn(function()
1561
- simulateActivity(self)
1562
- end)
1563
- end
1564
-
1565
- --// Connect player join/leave listeners.
1566
- Players.PlayerAdded:Connect(function(player: Player)
1567
- return onPlayerAdd(self, player)
1568
- end)
1569
-
1570
- --// Ensure any players already present at initialization are registered.
1571
- for i,player in Players:GetPlayers() do
1572
- onPlayerAdd(self, player)
1573
- end
1574
-
1575
- Players.PlayerRemoving:Connect(function(player: Player)
1576
- return onPlayerRemove(self, player)
1577
- end)
1578
- end
1579
-
1580
- --[[
1581
- @method Zyntex:link
1582
- @description A utility function used during initial setup. When called, it attempts
1583
- to link the provided game token to the user's Roblox account via the Zyntex backend.
1584
- This is typically only run once from the command bar in Studio.
1585
- ]]
1586
- function Zyntex.link(self: Zyntex)
1587
- local response = self._session:post("/links/games/link")
1588
-
1589
- if response.success == false then
1590
- if response.statusCode == 404 then
1591
- error(`Game token "{self._session.gameToken}" not found. Please make sure you pasted the full linking script from the site.`)
1592
- end
1593
- error(`Linking failed: {response.user_message}`)
1594
- end
1595
-
1596
- print(`[Zyntex]: Linking success!`)
1597
- end
1598
-
1599
- --[[
1600
- @method Zyntex:Telemetry
1601
- @description Constructs a new Telemetry object used for prometheus-style metrics.
1602
- @param flushEvery number? -- How often to flush the buffer in seconds. Default is 10,
1603
- which is the minimum to not get ratelimited.
1604
- @param registryName string? -- The name of the registry. Default is "default"
1605
- ]]
1606
- function Zyntex.Telemetry(self: Zyntex, flushEvery: number?, registryName: string?)
1607
- if not flushEvery then
1608
- flushEvery = 10
1609
- end
1610
- assert(flushEvery >= 10, `flushEvery must be no less than 10`)
1611
- return Telemetry.new(
1612
- registryName,
1613
- flushEvery,
1614
- self._session
1615
- )
1616
- end
1617
-
1618
- --[[
1619
- @method Zyntex:Experiments
1620
- @description Constructs a new Experiments object used for A/B testing.
1621
- @param experimentId string -- The ID of the experiment, i.e. "new_shop_ui". Set when creating experiment.
1622
- @return Experiment? -- The Experiment, if exists and is not Archived
1623
- ]]
1624
- function Zyntex.GetExperiment(self: Zyntex, experimentId: string): Experiments.Experiment?
1625
- return Experiments.new(
1626
- self._session,
1627
- experimentId
1628
- )
1629
- end
1630
-
1631
- return Zyntex