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?)
| Field | Type |
|---|---|
| Signature | (seconds: number?) → number |
| Returns | seconds slept (defaults to 0 for a single-tick yield) |
| Async | yes |
task.wait(0.5) -- yield half a second
task.wait() -- single-tick yield (0 seconds)
task.spawn(fn, ...)
| Field | Type |
|---|---|
| Signature | (fn: (...any) → (), ...any) → number |
| Returns | thread handle id, usable with task.cancel |
| Async | no — schedules onto a fresh tokio task |
local id = task.spawn(function(x, y)
print(x + y)
end, 2, 3)
task.delay(seconds, fn, ...)
| Field | Type |
|---|---|
| Signature | (seconds: number, fn: (...any) → (), ...any) → number |
| Returns | thread 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)
| Field | Type |
|---|---|
| Signature | (id: number) → boolean |
| Returns | true 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.
| Field | Type |
|---|---|
| Signature | (source: string, chunkname: string?) → (function?, string?) |
| Returns | (function, nil) on success, (nil, errmsg) on parse error — same tuple shape as standard Lua |
| Async | no |
-- 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.
| Function | Returns |
|---|---|
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
| Function | Returns |
|---|---|
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?
| Field | Type |
|---|---|
| Signature | (s: string) → number? |
| Returns | unix seconds on success, nil on parse failure |
| Accepts | RFC-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:
- Stateless functions — share a process-wide non-deterministic default.
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)
| Field | Type |
|---|---|
| Signature | (url: string) → (WebSocket?, string?) |
| Returns | (ws, nil) on success, (nil, err) on failure |
| Accepts | ws:// 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
| Method | Args | Effect |
|---|---|---|
ws:send(text) | string | send a Text frame |
ws:sendBinary(bytes) | string | send 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 below | subscribe to an inbound event |
Events
| Event | Callback 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
connectin their own loop. tungstenitealready 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
| Method | Returns |
|---|---|
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()