Warp/src/Client/init.luau

266 lines
6.9 KiB
Text
Raw Normal View History

2026-02-10 18:11:25 +00:00
--!optimize 2
--!strict
--@EternityDev
local Client = {}
local RunService = game:GetService("RunService")
local Thread = require("./Util/Thread")
local Buffer = require("./Util/Buffer")
local Replication = require("./Replication")
2026-02-10 18:11:25 +00:00
local Event: RemoteEvent = script.Parent:WaitForChild("Event")
2026-02-11 08:00:23 +00:00
local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent")
2026-02-10 18:11:25 +00:00
local deltaT: number, cycle: number = 0, 1 / 61
local writer: Buffer.Writer = Buffer.createWriter()
type Connection = {
Connected: boolean,
Disconnect: (self: Connection) -> (),
2026-02-10 18:11:25 +00:00
}
type Event = {
i: number,
c: (Player, ...any?) -> ...any?,
2026-02-10 18:11:25 +00:00
}
local queueEvent: { { any } } = {}
2026-02-11 08:00:23 +00:00
local queueUnreliableEvent: { { any } } = {}
2026-04-08 17:35:48 +00:00
local eventListeners: { [number]: { Event } } = {}
local eventSchemas: { [number]: Buffer.SchemaType } = {}
2026-02-10 18:11:25 +00:00
local pendingInvokes: { [string]: thread } = {}
local invokeId = 0
2026-02-23 05:02:18 +00:00
--@optional
-- Yields the current thread until the client has successfully initialized and synchronized with the server's replication data (identifier). Its optionally, but highly recommended to call this before firing or connecting to any events to ensure the network is fully ready.
Client.awaitReady = Replication.wait_for_ready
2026-02-23 05:02:18 +00:00
--@remoteName string
--@schema Buffer.SchemaType
-- Define a schema for strict data packing on a specific event.
2026-02-11 04:34:45 +00:00
Client.useSchema = function(remoteName: string, schema: Buffer.SchemaType)
local id = Replication.get_id[remoteName]
if not id then
warn(`[Warp]: ".useSchema"::"{remoteName}" does not exist, likely its not registered on the server yet.`)
return
end
eventSchemas[id] = schema
2026-02-11 04:34:45 +00:00
end
2026-02-23 05:02:18 +00:00
--@remoteName string
--@fn function
-- Connect to an event to receive incoming data from the server.
2026-02-11 08:00:23 +00:00
Client.Connect = function(remoteName: string, fn: (Player, ...any?) -> ...any?): Connection
local id = Replication.get_id[remoteName]
if not id then
warn(`[Warp]: ".Connect"::"{remoteName}" does not exist, likely its not registered on the server yet.`)
2026-04-08 17:35:48 +00:00
return {
Connected = false,
Disconnect = function()
return
end,
} :: Connection
end
local detail = {
i = id,
c = fn,
}
2026-04-08 17:35:48 +00:00
if not eventListeners[id] then
eventListeners[id] = {}
end
table.insert(eventListeners[id], detail)
return {
Connected = true,
Disconnect = function(self: Connection)
if not self.Connected then
return
end
self.Connected = false
2026-04-08 17:35:48 +00:00
local bucket = eventListeners[detail.i]
if bucket then
local idx = table.find(bucket, detail)
if idx then
table.remove(bucket, idx)
end
end
end,
} :: Connection
2026-02-10 18:11:25 +00:00
end
2026-02-23 05:02:18 +00:00
--@remoteName string
--@fn function
-- Similar to :Connect but automatically disconnects after the first firing.
2026-02-10 18:11:25 +00:00
Client.Once = function(remoteName: string, fn: (...any?) -> ()): Connection
local connection
connection = Client.Connect(remoteName, function(...: any?)
if connection then
connection:Disconnect()
end
fn(...)
end)
return connection
2026-02-10 18:11:25 +00:00
end
2026-02-23 05:02:18 +00:00
--@remoteName string
-- Wait for an event to be triggered. Yields the current thread.
2026-02-10 18:11:25 +00:00
Client.Wait = function(remoteName: string): (number, ...any?)
local thread, t = coroutine.running(), os.clock()
Client.Once(remoteName, function(...: any?)
task.spawn(thread, os.clock() - t, ...)
end)
return coroutine.yield()
2026-02-10 18:11:25 +00:00
end
2026-02-23 05:02:18 +00:00
--@remoteName string
-- Disconnect all connections for a specific event.
2026-02-10 18:11:25 +00:00
Client.DisconnectAll = function(remoteName: string)
local id = Replication.get_id[remoteName]
if not id then
return
end
2026-04-08 17:35:48 +00:00
eventListeners[id] = nil
2026-02-10 18:11:25 +00:00
end
2026-02-23 05:02:18 +00:00
--@remoteName string
-- Disconnect all connections and remove the event.
Client.Destroy = Client.DisconnectAll
2026-02-10 18:11:25 +00:00
2026-02-23 05:02:18 +00:00
--@remoteName string
--@reliable boolean
-- Fire an event to the server.
2026-02-16 09:50:43 +00:00
Client.Fire = function(remoteName: string, reliable: boolean, ...: any?)
local id = Replication.get_id[remoteName]
if id then
table.insert(reliable and queueEvent or queueUnreliableEvent, {
Replication.get_id[remoteName],
{ ... } :: any,
})
end
2026-02-10 18:11:25 +00:00
end
2026-02-23 05:02:18 +00:00
--@remoteName string
--@timeout number?
-- Invoke the server with timeout support. Yields the current thread. Returns nil if timeout occurs.
2026-02-10 18:11:25 +00:00
Client.Invoke = function(remoteName: string, timeout: number?, ...: any?): ...any?
local id = Replication.get_id[remoteName]
if not id then
return nil
end
invokeId += 1
local reqid, thread = `{invokeId}`, coroutine.running()
2026-02-23 05:02:18 +00:00
pendingInvokes[reqid] = thread
task.delay(timeout or 2, function()
local pending = pendingInvokes[reqid]
if not pending then
return
end
task.spawn(pending, nil)
pendingInvokes[reqid] = nil
end)
table.insert(queueEvent, {
0,
{ id, reqid :: any, { ... } :: any } :: any,
})
return coroutine.yield()
2026-02-10 18:11:25 +00:00
end
if RunService:IsClient() then
local function processIncoming(b: buffer, ref: { Instance }?, handleInvokes: boolean)
if type(b) ~= "buffer" then
return
end
local contents = Buffer.readEvents(b, ref, eventSchemas)
for _, content in contents do
local remote = content[1]
local content = content[2]
if handleInvokes then
if remote == 0 then
local id = content[1]
local results = content[2]
local pending = pendingInvokes[id]
if pending then
task.spawn(pending :: any, table.unpack(results))
pendingInvokes[id] = nil
end
continue
end
if remote == 1 then
local remoteName = content[1]
local id = content[2]
local args = content[3]
2026-04-08 17:35:48 +00:00
local connections = eventListeners[remoteName]
if connections and #connections > 0 then
Thread(function()
local results = { connections[1].c(table.unpack(args)) }
table.insert(queueEvent, {
1,
{ id, results } :: any,
})
end)
end
continue
end
end
2026-04-08 17:35:48 +00:00
local connections = eventListeners[remote]
if connections then
for _, connection in connections do
Thread(connection.c, table.unpack(content))
end
end
end
end
2026-02-23 05:02:18 +00:00
Event.OnClientEvent:Connect(function(b: buffer, ref: { Instance }?)
processIncoming(b, ref, true)
end)
2026-02-23 05:02:18 +00:00
UnreliableEvent.OnClientEvent:Connect(function(b: buffer, ref: { Instance }?)
processIncoming(b, ref, false)
end)
2026-02-23 05:02:18 +00:00
RunService.PostSimulation:Connect(function(d: number)
deltaT += d
if deltaT < cycle then
return
end
deltaT = 0
2026-02-23 05:02:18 +00:00
-- reliable
if #queueEvent > 0 then
Buffer.writeEvents(writer, queueEvent, eventSchemas)
do
local buf, ref = Buffer.buildWithRefs(writer)
Buffer.reset(writer)
if not ref or #ref == 0 then
Event:FireServer(buf)
else
Event:FireServer(buf, ref)
end
end
table.clear(queueEvent)
end
-- unreliable
if #queueUnreliableEvent > 0 then
Buffer.writeEvents(writer, queueUnreliableEvent, eventSchemas)
do
local buf, ref = Buffer.buildWithRefs(writer)
Buffer.reset(writer)
if not ref or #ref == 0 then
UnreliableEvent:FireServer(buf)
else
UnreliableEvent:FireServer(buf, ref)
end
end
table.clear(queueUnreliableEvent)
end
end)
2026-02-10 18:11:25 +00:00
end
2026-02-11 04:34:45 +00:00
--[[
2026-02-23 05:02:18 +00:00
@class Client
@schema
define a schema for your data and use a strict packing
2026-02-11 04:34:45 +00:00
]]
Client.Schema = Buffer.Schema
2026-02-10 18:11:25 +00:00
return Client :: typeof(Client)