mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
138 lines
3.5 KiB
Lua
138 lines
3.5 KiB
Lua
local Players = game:GetService("Players")
|
|
|
|
local subscribe = require(script.Parent.Parent.subscribe)
|
|
local types = require(script.Parent.Parent.types)
|
|
type Atom<T> = types.Atom<T>
|
|
type SyncPayload = types.SyncPayload
|
|
local setInterval = require(script.Parent.Parent.utils.setInterval)
|
|
local patch = require(script.Parent.patch)
|
|
|
|
type ServerOptions = {
|
|
--[=[
|
|
The atoms to synchronize with the client.
|
|
]=]
|
|
atoms: { [string]: Atom<any> },
|
|
--[=[
|
|
The interval at which to send patches to the client, in seconds.
|
|
Defaults to `0` (patches are sent up to once per frame). Set to a
|
|
negative value to disable automatic syncing.
|
|
]=]
|
|
interval: number?,
|
|
}
|
|
|
|
type ServerSyncer = {
|
|
--[=[
|
|
Sets up a subscription to each atom that schedules a patch to be sent to
|
|
the client whenever the state changes. When a change occurs, the `callback`
|
|
is called with the player and the payload to send.
|
|
|
|
Note that the `payload` object should not be mutated. If you need to
|
|
modify the payload, apply the changes to a copy of the object.
|
|
|
|
@param callback The function to call when the state changes.
|
|
@return A cleanup function that unsubscribes all listeners.
|
|
]=]
|
|
connect: (self: ServerSyncer, callback: (player: Player, payload: SyncPayload) -> ()) -> () -> (),
|
|
--[=[
|
|
Hydrates the client's state with the server's state. This should be
|
|
called when a player joins the game and requires the server's state.
|
|
|
|
@param player The player to hydrate.
|
|
]=]
|
|
hydrate: (self: ServerSyncer, player: Player) -> (),
|
|
--[=[
|
|
@deprecated For internal use only.
|
|
]=]
|
|
_sendPatch: (self: ServerSyncer, player: Player) -> (),
|
|
}
|
|
|
|
--[=[
|
|
Creates a `ServerSyncer` object that sends patches to the client and
|
|
hydrates the client's state.
|
|
|
|
@param options The atoms to synchronize with the client.
|
|
@return A `ServerSyncer` object.
|
|
]=]
|
|
local function server(options: ServerOptions): ServerSyncer
|
|
local atoms = options.atoms
|
|
local interval = options.interval or 0
|
|
|
|
local self = {} :: ServerSyncer
|
|
local sync: (player: Player, payload: SyncPayload) -> ()
|
|
local state: { [string]: any } = {}
|
|
local stateSnapshot = {}
|
|
local changed = false
|
|
|
|
-- Start the interval to send state patches over the network.
|
|
function self:connect(callback)
|
|
local cleanups = {}
|
|
sync = callback
|
|
|
|
-- Populate the initial state and snapshot for each atom.
|
|
-- Subscribe to each atom and update the state when it changes.
|
|
for key, atom in atoms do
|
|
cleanups[key] = subscribe(atom, function(current)
|
|
state[key] = current
|
|
changed = true
|
|
end)
|
|
|
|
state[key] = atom()
|
|
stateSnapshot[key] = state[key]
|
|
end
|
|
|
|
local disconnect = setInterval(function()
|
|
if not changed then
|
|
return
|
|
end
|
|
|
|
local diffs = patch.diff(stateSnapshot, state)
|
|
stateSnapshot = table.clone(state)
|
|
changed = false
|
|
|
|
for _, player in Players:GetPlayers() do
|
|
callback(player, {
|
|
type = "patch",
|
|
data = diffs,
|
|
})
|
|
end
|
|
end, interval)
|
|
|
|
return function()
|
|
disconnect()
|
|
for _, cleanup in cleanups do
|
|
cleanup()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Send the initial state to a player when they join the server.
|
|
function self:hydrate(player)
|
|
assert(sync, "connect() must be called before hydrate()")
|
|
|
|
sync(player, {
|
|
type = "init",
|
|
data = state,
|
|
})
|
|
end
|
|
|
|
function self:_sendPatch(player: Player)
|
|
assert(sync, "connect() must be called before _sendPatch()")
|
|
|
|
if not changed then
|
|
return
|
|
end
|
|
|
|
local diffs = patch.diff(stateSnapshot, state)
|
|
stateSnapshot = table.clone(state)
|
|
changed = false
|
|
|
|
sync(player, {
|
|
type = "patch",
|
|
data = diffs,
|
|
})
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
return server
|