diff --git a/lib/init.luau b/lib/init.luau index 4525a60..9993fb5 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -1,74 +1,111 @@ -local RunService = game:GetService 'RunService' +local RunService = game:GetService "RunService" local is_client = RunService:IsClient() -local codec = require '@self/utils/codec' -local event = require '@self/event' -local func = require '@self/func' -local remote_index = require '@self/remote_index' -local state = require '@self/state' +local codec = require "@self/utils/codec" +local event = require "@self/event" +local func = require "@self/func" +local remote_index = require "@self/remote_index" +local state = require "@self/state" + +export type state_type = state.state_type +export type event_type = event.event_type +export type func_type = func.func_type --- defs type remote_desc = { - id: number, + id: number?, rbxclass: string?, codec: remote_index.codec_impl?, wrap: (Instance, any) -> any, single: boolean?, } --- helpers +export type defnet = { + codec: typeof(codec), + + remotes: (T, Instance?) -> T, + + step_sync: (Player?) -> (), + state: (T?) -> state.state_type, + event: typeof(event.define), + func: typeof(func.define), +} + local function sorted_keys(tab: { [string]: any }): { string } local keys = {} + for k in tab do table.insert(keys, k) end + table.sort(keys) + return keys end -local function deterministic_iter(tabl: { string: V }): () -> (string, V) + +local function deterministic_iter(tabl: { [string]: any }) local keys = sorted_keys(tabl) local count = 0 + return function() count += 1 + local key = keys[count] - return key, key and tabl[key] + + if key == nil then + return nil + end + + return key, tabl[key] end end -local function ensure_instance(parent: Instance, class: string, name: string) - if is_client then return parent:WaitForChild(name) end + +local function ensure_instance(parent: Instance, class: string, name: string): Instance + if is_client then + return parent:WaitForChild(name) + end + local instance = Instance.new(class) instance.Name = name instance.Parent = parent + return instance end --- functions local function define_remotes(remotes_def: T, root: Instance?): T - assert(type(remotes_def) == 'table', 'refine type') - local parent: Instance = root or ensure_instance(script, 'Folder', 'rbx') + assert(type(remotes_def) == "table", "refine type") + + local remotes_table = remotes_def :: any + local parent: Instance = root or ensure_instance(script, "Folder", "rbx") + + for name, desc in deterministic_iter(remotes_table) do + if type(desc.rbxclass) ~= "string" then + local folder = ensure_instance(parent, "Folder", name) - for name, desc in deterministic_iter<>(remotes_def) do - if type(desc.rbxclass) ~= 'string' then - local folder = ensure_instance(parent, 'Folder', name) - remotes_def[name] = define_remotes(desc, folder) + remotes_table[name] = define_remotes(desc, folder) continue end + desc.id = remote_index.reserve_id(name) + local rbx = ensure_instance(parent, desc.rbxclass, name) - if desc.single then remote_index.ensure_single(parent) end - if desc.codec then remote_index.set_codec(rbx, desc.codec) end + if desc.single then + remote_index.ensure_single(parent) + end + + if desc.codec then + remote_index.set_codec(rbx, desc.codec) + end - remotes_def[name] = desc.wrap(rbx, desc) + remotes_table[name] = desc.wrap(rbx, desc) end - return table.freeze(remotes_def) :: any + return table.freeze(remotes_table) :: T end --- module -return table.freeze { +local defnet: defnet = table.freeze { codec = codec, remotes = define_remotes, @@ -77,3 +114,5 @@ return table.freeze { event = event.define, func = func.define, } + +return defnet \ No newline at end of file diff --git a/lib/state.luau b/lib/state.luau index 3a7c859..7420937 100644 --- a/lib/state.luau +++ b/lib/state.luau @@ -1,91 +1,162 @@ -local Players = game:GetService 'Players' -local RunService = game:GetService 'RunService' +local Players = game:GetService "Players" +local RunService = game:GetService "RunService" -local codec = require './utils/codec' -local diff = require './utils/diff' -local diff_codec = require './utils/diff_codec' -local remote_index = require './remote_index' -local sync = require './utils/sync' +local codec = require "./utils/codec" +local diff = require "./utils/diff" +local diff_codec = require "./utils/diff_codec" +local remote_index = require "./remote_index" +local sync = require "./utils/sync" + +local is_client = RunService:IsClient() + +local server_pools: { [Player]: codec.pool } = {} +local registry: { sync.syncer } = {} + +export type client_state = { + value: T, + get: () -> T, + changed: ((T, T?) -> ()) -> RBXScriptConnection, +} --- defs -export type remote_desc = { rbxclass: 'RemoteEvent', default: T?, id: number, single: true } export type state_type = { - changed: (() -> ()) -> (), - codec: codec.codec_impl?, + client: client_state, + + changed: (() -> ()) -> RBXScriptConnection, step: (Player?) -> (), + server: { [Player]: T }, - client: T, + + codec: codec.codec_impl?, } --- vars -local is_client = RunService:IsClient() -local server_pools: { [Player]: codec.pool } = {} -local registry: { sync.syncer } = {} +export type remote_desc = { + id: number, + rbxclass: "RemoteEvent", + codec: codec.codec_impl?, + wrap: (RemoteEvent, remote_desc) -> state_type, + single: true, + default: T?, +} --- definition -local function wrap(event: BindableEvent, desc: remote_desc): state_type - local function listen_changed(callback) - return event.Event:Connect(callback) +local function wrap(remote: RemoteEvent, desc: remote_desc): state_type + local changed_event = Instance.new("BindableEvent") + + local function changed(callback: () -> ()): RBXScriptConnection + return changed_event.Event:Connect(callback) end if is_client then - local self = { changed = listen_changed, client = desc.default or {} } - remote_index.on(desc.id, function(buf: buffer, unknowns: { any }) - sync.apply(self.client, buf, remote_index.client_pool, unknowns) - event:Fire() + local value = if desc.default ~= nil then diff.snapshot(desc.default) else {} :: any + + local client = { + value = value, + + get = function(): T + return value + end, + + changed = function(callback: (T, T?) -> ()): RBXScriptConnection + return changed_event.Event:Connect(callback) + end, + } + + remote_index.on(desc.id, function(buf: buffer, unknowns: { any }?) + local patch = sync.apply(value, buf, remote_index.client_pool, unknowns) + + changed_event:Fire(patch, value) end) - return self :: any + + return table.freeze({ + client = table.freeze(client), + + changed = changed, + step = function(_player: Player?) end, + + server = {} :: { [Player]: T }, + + codec = desc.codec, + }) :: any end local function send(player: Player, packet) remote_index.fire(desc.id, player, packet.buf, packet.unknowns) - event:Fire() + changed_event:Fire() end + local syncer = sync.create { default = diff.snapshot(desc.default), pools = server_pools, send = send, } + local function step(player: Player?) sync.step(syncer, player) end table.insert(registry, syncer) - return table.freeze { changed = listen_changed, step = step, server = syncer.targets } :: any + + return table.freeze { + client = table.freeze({ + value = if desc.default ~= nil then diff.snapshot(desc.default) else {} :: any, + + get = function(): T + return if desc.default ~= nil then diff.snapshot(desc.default) else {} :: any + end, + + changed = function(callback: (T, T?) -> ()): RBXScriptConnection + return changed_event.Event:Connect(callback) + end, + }), + + changed = changed, + step = step, + + server = syncer.targets, + + codec = desc.codec, + } :: any end + local function define(default: T?): state_type - return { rbxclass = 'BindableEvent', wrap = wrap, codec = diff_codec.impl, default = default, single = true } :: any + return { + rbxclass = "RemoteEvent", + wrap = wrap, + codec = diff_codec.impl, + default = default, + single = true, + } :: any end --- functions local function step_sync(player: Player?) - for _, s in registry do - sync.step(s, player) + for _, syncer in registry do + sync.step(syncer, player) end end --- @server init if not is_client then Players.PlayerAdded:Connect(function(player: Player) server_pools[player] = codec.create_pool() end) + for _, player in Players:GetPlayers() do server_pools[player] = codec.create_pool() end + Players.PlayerRemoving:Connect(function(player: Player) server_pools[player] = nil - for _, s in registry do - sync.remove(s, player) + + for _, syncer in registry do + sync.remove(syncer, player) end end) + RunService.Heartbeat:Connect(function() step_sync() end) end --- module return table.freeze { step_sync = step_sync, define = define, wrap = wrap, -} +} \ No newline at end of file diff --git a/lib/utils/sync.luau b/lib/utils/sync.luau index 2586c09..c095ea2 100644 --- a/lib/utils/sync.luau +++ b/lib/utils/sync.luau @@ -1,23 +1,25 @@ -local codec = require './codec' -local diff_algo = require './diff' -local diff_codec = require './diff_codec' - --- defs -export type packet = { buf: buffer, unknowns: { any }? } -export type syncer = { - send: (target: Receiver, packet) -> (), - targets: { [Receiver]: any }, - snapshots: { [Receiver]: any }, - pools: { [Receiver]: codec.pool }, +local codec = require "./codec" +local diff_algo = require "./diff" +local diff_codec = require "./diff_codec" + +export type packet = { + buf: buffer, + unknowns: { any }?, +} + +export type syncer = { + send: (Player, packet) -> (), + targets: { [Player]: any }, + snapshots: { [Player]: any }, + pools: { [Player]: codec.pool }, default: any, } --- functions -local function create(params: { +local function create(params: { default: any, - send: (target: Receiver, packet) -> (), - pools: { [Receiver]: codec.pool }, -}): syncer + send: (Player, packet) -> (), + pools: { [Player]: codec.pool }, + }): syncer return { default = params.default, send = params.send, @@ -26,56 +28,106 @@ local function create(params: { snapshots = {}, } end -local function remove(syncer: syncer, target: Receiver) - syncer.targets[target] = nil - syncer.snapshots[target] = nil + +local function remove(syncer: syncer, player: Player) + syncer.targets[player] = nil + syncer.snapshots[player] = nil end -local function step(syncer: syncer, target: Receiver?) - local snapshots: { [any]: any } = {} - local pools = syncer.pools +local function step(syncer: syncer, player: Player?) + local snapshots_cache: { [any]: any } = {} + + local function ensure_snapshot(current: any): any + local snapshot = snapshots_cache[current] + + if snapshot then + return snapshot + end - local function ensure_snapshot(curr: T): T - local snapshot = snapshots[curr] - if snapshot then return snapshot end + snapshot = diff_algo.snapshot(current) + snapshots_cache[current] = snapshot - snapshot = diff_algo.snapshot(curr) - snapshots[curr] = snapshot return snapshot end - local function send_one(target: Receiver) - local curr = syncer.targets[target] - local prev = syncer.snapshots[target] or syncer.default + local function send_one(target_player: Player) + local current = syncer.targets[target_player] - local diff = diff_algo.from(prev, curr) - if diff_algo.is_empty(diff) then return end + if current == nil then + return + end + + local previous = syncer.snapshots[target_player] or syncer.default + local diff_node = diff_algo.from(previous, current) + + if diff_algo.is_empty(diff_node) then + return + end - local buf, unknowns = diff_codec.impl.encode(diff, pools[target]) - syncer.send(target, { buf = buf, unknowns = unknowns }) + local buf, unknowns = diff_codec.impl.encode(diff_node, syncer.pools[target_player]) - syncer.snapshots[target] = ensure_snapshot(curr) + syncer.send(target_player, { + buf = buf, + unknowns = unknowns, + }) + + syncer.snapshots[target_player] = ensure_snapshot(current) end - if target then - send_one(target) + if player then + send_one(player) else - for receiver in syncer.targets do - send_one(receiver) + for target_player in syncer.targets do + send_one(target_player) end end - -- remove snapshots of removed targets - for receiver in syncer.snapshots do - if syncer.targets[receiver] == nil then syncer.snapshots[receiver] = nil end + for target_player in syncer.snapshots do + if syncer.targets[target_player] == nil then + syncer.snapshots[target_player] = nil + end end diff_algo.reset_cache() end -local function apply(state: T, buf: buffer, pool: codec.pool?, unknowns: { any }?) - diff_algo.apply(state, diff_codec.impl.decode(buf, pool, unknowns)) +local function diff_to_patch(node) + local patch = { + changed = {}, + removed = {}, + children = {}, + } + + if not node then + return patch + end + + for key, value in node.changes do + patch.changed[key] = value + end + + for _, key in node.removes do + table.insert(patch.removed, key) + end + + for key, child in node.children do + patch.children[key] = diff_to_patch(child) + end + + return patch +end + +local function apply(state: any, buf: buffer, pool: codec.pool?, unknowns: { any }?) + local diff_node = diff_codec.impl.decode(buf, pool, unknowns) + + diff_algo.apply(state, diff_node) + + return diff_to_patch(diff_node) end --- module -return table.freeze { create = create, remove = remove, step = step, apply = apply } +return table.freeze { + create = create, + remove = remove, + step = step, + apply = apply, +} \ No newline at end of file