Skip to main content

Utility Modules

Roblox-style utility globals exposed alongside the bot/world bindings — task, crypto, datetime, uuid, random, websocket, and regex. Lean, pure-function surfaces (no userdata churn except where state is the point) so you can copy-paste idiomatic Roblox snippets without rewriting them.


task — Roblox-compat scheduler

Maps onto the existing runThread / removeThread / sleep primitives so scripts copied from Roblox sources work without rewrites. Note: task.wait takes seconds (float), sleep takes milliseconds — pick whichever vocabulary you prefer.

task.wait(seconds?)

FieldType
Signature(seconds: number?) → number
Returnsseconds slept (defaults to 0 for a single-tick yield)
Asyncyes
task.wait(0.5) -- yield half a second
task.wait() -- single-tick yield (0 seconds)

task.spawn(fn, ...)

FieldType
Signature(fn: (...any) → (), ...any) → number
Returnsthread handle id, usable with task.cancel
Asyncno — schedules onto a fresh tokio task
local id = task.spawn(function(x, y)
print(x + y)
end, 2, 3)

task.delay(seconds, fn, ...)

FieldType
Signature(seconds: number, fn: (...any) → (), ...any) → number
Returnsthread handle id
task.delay(2, function() print("2 seconds later") end)

task.defer(fn, ...)

Same as spawn but with a single-tick yield first — runs at the next resumption point.

task.cancel(id)

FieldType
Signature(id: number) → boolean
Returnstrue if the handle matched a live task

loadstring / load — dynamic source compilation

Luau strips loadstring from the standard library. Seraph re-adds both loadstring(source) and load(source) (alias) so dynamic-source flows work — fetch a script over HTTP, compile it, run it, all without writing to disk.

FieldType
Signature(source: string, chunkname: string?) → (function?, string?)
Returns(function, nil) on success, (nil, errmsg) on parse error — same tuple shape as standard Lua
Asyncno
-- Run a remote script
local source = http:get("https://docs-seraph.growpai.site/scripts/auto_mine_v2.lua")
local fn, err = loadstring(source, "auto_mine_v2")
if not fn then error(err) end
fn()

-- Wrap user-typed input from a chat command
client:on("p:BGM", function(doc)
local cmd = doc.CmB and doc.CmB.message
if cmd and cmd:sub(1, 5) == "!eval" then
local fn, err = loadstring(cmd:sub(7))
if fn then print(fn()) else print("err:", err) end
end
end)

The bytecode-loader overload (binary chunks via second-arg mode) is intentionally not supported — pre-compiled bytecode would bypass the sandbox.


crypto — hash + encoding

Inputs/outputs are byte strings (Lua strings are binary-safe, not UTF-8). Decoders raise on invalid input — silent typos surface immediately instead of returning nil.

FunctionReturns
crypto.sha256(input)hex digest, 64 chars
crypto.sha512(input)hex digest, 128 chars
crypto.hexEncode(bytes)lower-case hex
crypto.hexDecode(hex)raw bytes
crypto.base64Encode(input)standard base64 (padded with =)
crypto.base64Decode(b64)raw bytes
crypto.base64UrlEncode(input)URL-safe base64 (no padding)
crypto.base64UrlDecode(b64)raw bytes
print(crypto.sha256("hello"))
--> "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

local b = crypto.base64Encode("hello world")
print(b) --> "aGVsbG8gd29ybGQ="
print(crypto.base64Decode(b)) --> "hello world"

No HMAC / signing primitives yet. Add when a script actually needs them.


datetime — wall-clock helpers

Fills the gaps that os.time() / os.date(...) from Lua stdlib don't cover: millisecond precision, ISO-8601 round-tripping, explicit UTC vs local conversions.

Quick reference

FunctionReturns
datetime.unixSeconds()current UTC unix seconds
datetime.unixMillis()current UTC unix millis
datetime.iso()"2026-05-05T08:55:07Z"
datetime.format(fmt?)strftime UTC, default ISO-8601
datetime.formatLocal(fmt?)strftime LOCAL TZ
datetime.formatUnix(ts, fmt?)format a specific unix-second timestamp
datetime.parseIso(s)unix seconds, or nil on failure
datetime.monotonicMillis()wall-clock millis since epoch

datetime.parseIso(s) → number?

FieldType
Signature(s: string) → number?
Returnsunix seconds on success, nil on parse failure
AcceptsRFC-3339 / ISO-8601 with or without trailing Z, plus YYYY-MM-DD HH:MM:SS
local secs = datetime.parseIso("2026-05-05T08:55:07Z")
if secs then
print("delta:", datetime.unixSeconds() - secs, "seconds ago")
end

Round-trip

local original = "2026-05-05T08:55:07Z"
local secs = datetime.parseIso(original)
local back = datetime.formatUnix(secs) -- defaults to ISO-8601
assert(back == original)

uuid — UUID v4

print(uuid.new()) --> "f47ac10b-58cc-4372-a567-0e02b2c3d479"
print(uuid.new("simple")) --> "f47ac10b58cc4372a5670e02b2c3d479"
print(uuid.new("urn")) --> "urn:uuid:f47ac10b-58cc-4372-a567-0e02b2c3d479"

Cryptographically random (UUID v4). Use cases: per-script run id, idempotency keys, ephemeral filenames, correlation ids in HTTP requests.


random — better RNG

Two surfaces:

  1. Stateless functions — share a process-wide non-deterministic default.
  2. random.new(seed) userdata — deterministic stream for reproducible runs.

Stateless

print(random.integer(1, 6)) -- inclusive [1, 6]
print(random.number()) -- [0, 1) float
print(random.number(10)) -- [0, 10)
print(random.number(5, 10)) -- [5, 10)
print(random.choice({"a","b"}))
local salt = random.bytes(16) -- 16-byte random Lua string

local t = {1, 2, 3, 4, 5}
random.shuffle(t) -- in place

Seeded (deterministic)

local rng = random.new(42)
for i = 1, 5 do
print(rng:integer(1, 100)) -- always the same sequence on seed=42
end

RandomState exposes the same method set as the stateless functions: :integer, :number, :bytes, :choice, :shuffle.


websocket — async client

Minimal Roblox-shaped surface. connect returns (ws, err) so scripts can if err then return end without a pcall.

websocket.connect(url) → (ws, err)

FieldType
Signature(url: string) → (WebSocket?, string?)
Returns(ws, nil) on success, (nil, err) on failure
Acceptsws:// and wss://
local ws, err = websocket.connect("wss://echo.websocket.org")
if err then return print("connect failed:", err) end

ws:on("message", function(text) print("recv:", text) end)
ws:on("close", function(code, reason) print("closed:", code, reason) end)

ws:send("hello")
sleep(1000)
ws:close()

Methods on ws

MethodArgsEffect
ws:send(text)stringsend a Text frame
ws:sendBinary(bytes)stringsend a Binary frame (raw bytes)
ws:close()close the connection (idempotent)
ws:isClosed()true after local or peer-side close
ws:on(event, fn)see belowsubscribe to an inbound event

Events

EventCallback signature
"message"(text: string) — Text frame
"binary"(bytes: string) — Binary frame
"close"(code: number, reason: string) — peer closed
"error"(message: string) — read-side error

Notes

  • No automatic reconnect / backoff / heartbeat. Scripts that need it wrap connect in their own loop.
  • tungstenite already handles ping/pong frames internally — your code never sees them.
  • Each connection spawns one tokio task each for read + write. They tear down when close() is called or the peer closes.

regex — PCRE-style patterns

Lua's built-in string.match / string.gmatch use the bare-bones Lua pattern dialect (no | alternation, no character classes beyond %w/%d, no quantifier groups). This module exposes Rust's regex crate so scripts can lean on the engine they already know from every other language.

regex.new(pattern) → (re, err)

Compile once, reuse cheaply. Returns (nil, err) on a syntax error.

local re, err = regex.new([[(\d{4})-(\d{2})-(\d{2})]])
if err then return print(err) end

local m = re:find("today is 2026-05-05 thanks")
-- m = { match = "2026-05-05", start = 10, ["end"] = 19,
-- groups = { "2026", "05", "05" } }

Compiled Regex methods

MethodReturns
re:isMatch(haystack)boolean
re:find(haystack)first match-table, or nil
re:findAll(haystack){RegexMatch} — every non-overlapping match
re:gmatch(haystack)iterator — for m in re:gmatch(s) do ... end
re:replace(haystack, replacement)replaced string (dollar-form $1/$2)
re:split(haystack){string}
re:pattern()original pattern source

Match-table shape

{
match = "2026-05-05", -- the full matched substring
start = 10, -- 1-based byte offset of first char
["end"] = 19, -- 1-based byte offset of last char (inclusive)
groups = { "2026", "05", "05" }, -- 1-indexed capture groups
}

Indices match Lua's string.find convention (1-based, end-inclusive). Unmatched optional groups come back as nil in the array.

One-shot helpers

For quick compares without keeping a compiled handle around:

print(regex.isMatch([[(?i)hello]], "Hello World")) --> true
print(regex.find([[\d+]], "abc 42 def").match) --> "42"
print(regex.replace([[\s+]], " a b c", " ")) --> " a b c"

for _, word in ipairs(regex.split([[\W+]], "hello, world!")) do
print(word)
end

regex.escape(s)

Neutralises every metachar so s matches literally. Use when you splice user input into a pattern:

local needle = "1.2.3+x"
local pat = regex.escape(needle)
print(regex.isMatch(pat, "find 1.2.3+x here")) --> true
print(regex.isMatch(pat, "find 1X2X3+x here")) --> false

Replace with capture groups

replace uses dollar-form group references (regex crate convention), NOT Lua's %1:

local re = regex.new([[(\w+)@(\w+)]])
print(re:replace("alice@example, bob@test", "$2/$1"))
--> "example/alice, test/bob"

When to use regex.new vs the one-shot helpers

  • Hot loops / repeated patterns: regex.new(p) compiles once. Reuse the userdata.
  • One-off scrubs: regex.replace(p, h, r) etc. compile per call — fine for cold paths.

Inline flags

The regex crate supports inline flag groups: (?i) for case-insensitive, (?s) for . matches newline, (?m) for multi-line, (?x) for verbose. Combine with (?im)hello etc.


Combined recipe — signed-payload echo bot

Sign each outbound message with HMAC-style crypto.sha256(secret + payload), attach a UUID correlation id, and forward responses back to the script:

local SECRET = "shhh"

local ws, err = websocket.connect("wss://echo.websocket.org")
if err then return print(err) end

ws:on("message", function(reply)
print(datetime.iso(), "<", reply)
end)

for i = 1, 5 do
local cid = uuid.new("simple")
local body = string.format('{"id":"%s","i":%d,"ts":%d}', cid, i, datetime.unixSeconds())
local sig = crypto.sha256(SECRET .. body)
ws:send(body .. " sig=" .. sig)
task.wait(0.5)
end

task.wait(1)
ws:close()

See also

  • Eventsclient:on(event, callback) for game-side events
  • Examples — practical script patterns
  • HTTP — request/response client (sibling of websocket)
  • JSON — encode/decode JSON tables