From 8b8b29e9a93f1ba03ae2474d2b1a5b88f0ebe237 Mon Sep 17 00:00:00 2001 From: khtsly Date: Mon, 16 Feb 2026 14:28:47 +0700 Subject: [PATCH] rewrite(phase4): rewrite entire identifier mechanism --- docs/guide/example.md | 4 +-- sourcemap.json | 2 +- src/Client/init.luau | 14 ++++---- src/Replication/init.luau | 74 +++++++++++++++++++++++++++++++++++++++ src/Server/init.luau | 13 ++++--- src/Util/Buffer/init.luau | 60 +++++++++++++++++++++++++------ src/Util/Identifier.luau | 63 ++++++++++++++++++--------------- 7 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 src/Replication/init.luau diff --git a/docs/guide/example.md b/docs/guide/example.md index 2415d55..6c66a91 100644 --- a/docs/guide/example.md +++ b/docs/guide/example.md @@ -37,8 +37,8 @@ end) ```luau [Client] local Players = game:GetService("Players") -local Warp = require(game.ReplicatedStorage.WarpNew).Client() -local Schemas = require(game.ReplicatedStorage.Schemas) +local Warp = require(path.to.warp).Client() +local Schemas = require(path.to.schemas) -- Use schemas for eventName, schema in Schemas do diff --git a/sourcemap.json b/sourcemap.json index 4c14984..88aa896 100644 --- a/sourcemap.json +++ b/sourcemap.json @@ -1 +1 @@ -{"name":"warp","className":"ModuleScript","filePaths":["src/init.luau","default.project.json"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/Client/init.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/Server/init.luau"]},{"name":"Util","className":"Folder","children":[{"name":"Buffer","className":"ModuleScript","filePaths":["src/Util/Buffer/init.luau"]},{"name":"Identifier","className":"ModuleScript","filePaths":["src/Util/Identifier.luau"]},{"name":"Thread","className":"ModuleScript","filePaths":["src/Util/Thread.luau"]}]}]} \ No newline at end of file +{"name":"warp","className":"ModuleScript","filePaths":["src/init.luau","default.project.json"],"children":[{"name":"Client","className":"ModuleScript","filePaths":["src/Client/init.luau"]},{"name":"Server","className":"ModuleScript","filePaths":["src/Server/init.luau"]},{"name":"Util","className":"Folder","children":[{"name":"Buffer","className":"ModuleScript","filePaths":["src/Util/Buffer/init.luau"]},{"name":"Identifier","className":"ModuleScript","filePaths":["src/Util/Identifier.luau"]},{"name":"Thread","className":"ModuleScript","filePaths":["src/Util/Thread.luau"]}]},{"name":"Replication","className":"ModuleScript","filePaths":["src/Replication/init.luau"]}]} \ No newline at end of file diff --git a/src/Client/init.luau b/src/Client/init.luau index ea8d2ec..e994b9b 100644 --- a/src/Client/init.luau +++ b/src/Client/init.luau @@ -6,7 +6,7 @@ local Client = {} local RunService = game:GetService("RunService") local Thread = require("./Util/Thread") local Buffer = require("./Util/Buffer") -local Identifier = require("./Util/Identifier") +local Replication = require("./Replication") local Event: RemoteEvent = script.Parent:WaitForChild("Event") local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent") local deltaT: number, cycle: number = 0, 1 / 61 @@ -21,6 +21,8 @@ type Event = { c: (Player, ...any?) -> ...any?, } +local identifiers: { [string]: number } = {} + local queueEvent: { { any } } = {} local queueUnreliableEvent: { { any } } = {} local eventListeners: { Event } = {} @@ -30,12 +32,12 @@ local pendingInvokes: { [string]: thread } = {} local invokeId = 0 Client.useSchema = function(remoteName: string, schema: Buffer.SchemaType) - eventSchemas[Identifier(remoteName)] = schema + eventSchemas[Replication.get_id(remoteName)] = schema end Client.Connect = function(remoteName: string, fn: (Player, ...any?) -> ...any?): Connection local detail = { - i = Identifier(remoteName), + i = Replication.get_id(remoteName), c = fn, } table.insert(eventListeners, detail) @@ -74,7 +76,7 @@ Client.Wait = function(remoteName: string): (number, ...any?) end Client.DisconnectAll = function(remoteName: string) - local id = Identifier(remoteName) + local id = Replication.get_id(remoteName) for idx = #eventListeners, 1, -1 do if eventListeners[idx].i == id then table.remove(eventListeners, idx) @@ -86,7 +88,7 @@ Client.Destroy = Client.DisconnectAll Client.Fire = function(remoteName: string, reliable: boolean, ...: any?) table.insert(reliable and queueEvent or queueUnreliableEvent, { - Identifier(remoteName), + Replication.get_id(remoteName), { ... } :: any, }) end @@ -106,7 +108,7 @@ Client.Invoke = function(remoteName: string, timeout: number?, ...: any?): ...an end) table.insert(queueEvent, { 0, - { Identifier(remoteName), reqid :: any, { ... } :: any } :: any, + { Replication.get_id(remoteName), reqid :: any, { ... } :: any } :: any, }) return coroutine.yield() end diff --git a/src/Replication/init.luau b/src/Replication/init.luau new file mode 100644 index 0000000..5cf5ebf --- /dev/null +++ b/src/Replication/init.luau @@ -0,0 +1,74 @@ +--!optimize 2 +local Replication = {} + +local RunService = game:GetService("RunService") +local _repl: RemoteEvent = script.Parent:WaitForChild("_repl") + +local Buffer = require("./Util/Buffer") +local Identifier = require("./Util/Identifier") + +local identifiers_schema = Buffer.Schema.string +local writer: Buffer.Writer = Buffer.createWriter() +local warp_identifier_registry = shared.__warp_identifier_registry + +if RunService:IsServer() then + local replication_ready: { Player } = {} + local replication_id: number = Identifier.get("id_replication") or 1 + + if not Identifier.has("id_replication") or not replication_id then + replication_id = Identifier.get("id_replication") or 1 + end + + local function replicateToAll(content: any, id: number?) + if #replication_ready == 0 then return end + local to_repl: any = type(content) == "string" and { content = id } or content + Buffer.writeRepl(writer, to_repl, warp_identifier_registry.counter, identifiers_schema) + do + local buf = Buffer.build(writer) + Buffer.reset(writer) + for _, player: Player in replication_ready do + _repl:FireClient(player, buf, replication_id) + end + end + end + + local function replicateTo(player: Player) + Buffer.writeRepl(writer, warp_identifier_registry.cache, warp_identifier_registry.counter, identifiers_schema) + do + local buf = Buffer.build(writer) + Buffer.reset(writer) + _repl:FireClient(player, buf, replication_id) + end + end + + Identifier.on_added(replicateToAll) + + _repl.OnServerEvent:Connect(function(player: Player) + if table.find(replication_ready, player) then return end + table.insert(replication_ready, player) + replicateTo(player) + end) + + Replication.remove = function(player: Player) + table.remove(replication_ready, table.find(replication_ready, player)) + end + +elseif RunService:IsClient() then + _repl.OnClientEvent:Connect(function(b: buffer, id: number) + if type(b) ~= "buffer" then + return + end + local contents = Buffer.readRepl(b, identifiers_schema) + if #contents == 0 then return end + for _, content in contents do + warp_identifier_registry.cache[content[2]] = content[1] + end + end) + _repl:FireServer() + + Replication.get_id = function(name: string): number + return warp_identifier_registry.cache[name] + end +end + +return Replication :: typeof(Replication) diff --git a/src/Server/init.luau b/src/Server/init.luau index cef4110..da8020e 100644 --- a/src/Server/init.luau +++ b/src/Server/init.luau @@ -8,7 +8,9 @@ local RunService = game:GetService("RunService") local Thread = require("./Util/Thread") local Buffer = require("./Util/Buffer") local Identifier = require("./Util/Identifier") +local Replication = require("./Replication") local Event: RemoteEvent = script.Parent:WaitForChild("Event") +local _repl: RemoteEvent = script.Parent:WaitForChild("_repl") local UnreliableEvent: UnreliableRemoteEvent = script.Parent:WaitForChild("UnreliableEvent") local deltaT: number, cycle: number = 0, 1 / 61 local writer: Buffer.Writer = Buffer.createWriter() @@ -36,12 +38,12 @@ local pendingInvokes: { [string]: thread } = {} local invokeId = 0 Server.useSchema = function(remoteName: string, schema: Buffer.SchemaType) - eventSchemas[Identifier(remoteName)] = schema + eventSchemas[Identifier.get(remoteName)] = schema end Server.Connect = function(remoteName: string, fn: (Player, ...any?) -> ...any?): Connection local detail = { - i = Identifier(remoteName), + i = Identifier.get(remoteName), c = fn, } table.insert(eventListeners, detail) @@ -80,7 +82,7 @@ Server.Wait = function(remoteName: string): (number, ...any?) end Server.DisconnectAll = function(remoteName: string) - local id = Identifier(remoteName) + local id = Identifier.get(remoteName) if not id then return end @@ -99,7 +101,7 @@ Server.Fire = function(remoteName: string, reliable: boolean, player: Player, .. targetQueue[player] = {} :: any end table.insert(targetQueue[player], { - Identifier(remoteName), + Identifier.get(remoteName), { ... } :: any, }) end @@ -135,7 +137,7 @@ Server.Invoke = function(remoteName: string, player: Player, timeout: number?, . end table.insert(queueEvent[player], { 0, - { Identifier(remoteName), reqId :: any, { ... } :: any } :: any, + { Identifier.get(remoteName), reqId :: any, { ... } :: any } :: any, }) return coroutine.yield() end @@ -261,6 +263,7 @@ if RunService:IsServer() then end Players.PlayerAdded:Connect(onAdded) Players.PlayerRemoving:Connect(function(player: Player) + Replication.remove(player) table.remove(players_ready, table.find(players_ready, player)) if queueEvent[player] then table.clear(queueEvent[player]) diff --git a/src/Util/Buffer/init.luau b/src/Util/Buffer/init.luau index e5168f3..36f4cc9 100644 --- a/src/Util/Buffer/init.luau +++ b/src/Util/Buffer/init.luau @@ -633,9 +633,9 @@ local function packColor3(w: Writer, c: Color3) if r > 1 or g > 1 or b > 1 then wByte(w, T_COLOR3_F) - wF32(w, r) - wF32(w, g) - wF32(w, b) + wF16(w, r) + wF16(w, g) + wF16(w, b) else wByte(w, T_COLOR3) wByte(w, math.clamp(math.round(r * 255), 0, 255)) @@ -974,10 +974,10 @@ unpackValue = function(buf: buffer, pos: number, refs: { any }?): (any, number) return Color3.fromRGB(r, g, b), pos + 3 end if t == T_COLOR3_F then - local r = buffer.readf32(buf, pos) - local g = buffer.readf32(buf, pos + 4) - local b = buffer.readf32(buf, pos + 8) - return Color3.new(r, g, b), pos + 12 + local r = readF16(buf, pos) + local g = readF16(buf, pos + 2) + local b = readF16(buf, pos + 4) + return Color3.new(r, g, b), pos + 6 end if t == T_BRICKCOLOR then @@ -1647,7 +1647,7 @@ local function writeEvents(w: Writer, events: { { any } }, schemas: { [number]: for _, event in events do local id = event[1] local args = event[2] - wU16(w, id) + wByte(w, id) local schema = schemas[id] if schema then packStrict(w, schema, args[1]) @@ -1662,8 +1662,8 @@ local function readEvents(buf: buffer, refs: { any }?, schemas: { [number]: Sche count, pos = readVarUInt(buf, pos) local events = table.create(count) for i = 1, count do - local id: number = buffer.readu16(buf, pos) - pos += 2 + local id: number = buffer.readu8(buf, pos) + pos += 1 local args: any local schema = schemas[id] if schema then @@ -1678,13 +1678,51 @@ local function readEvents(buf: buffer, refs: { any }?, schemas: { [number]: Sche return events end +local function writeRepl(w: Writer, content: { [string]: number }, count: number, schema: SchemaType) + wVarUInt(w, count) + for name, id in content do + wByte(w, id) + if schema then + packStrict(w, schema, name) + else + packValue(w, name) + end + end +end + +local function readRepl(buf: buffer, schema: SchemaType): { { any } } + local pos, count = 0, 0 + count, pos = readVarUInt(buf, pos) + local events = table.create(count) + for i = 1, count do + local id: number = buffer.readu8(buf, pos) + pos += 1 + local args: any + if schema then + local val + val, pos = readStrict(buf, pos, schema) + args = val + else + args, pos = unpackValue(buf, pos) + end + events[i] = { id, args } + end + return events +end + local BufferSerdes = {} +BufferSerdes.Schema = Schema + +BufferSerdes.writeRepl = writeRepl +BufferSerdes.readRepl = readRepl + BufferSerdes.writeEvents = writeEvents BufferSerdes.readEvents = readEvents -BufferSerdes.Schema = Schema + BufferSerdes.compilePacker = compilePacker BufferSerdes.compileReader = compileReader + BufferSerdes.packStrict = packStrict BufferSerdes.readStrict = readStrict diff --git a/src/Util/Identifier.luau b/src/Util/Identifier.luau index 2a95798..dccbc1b 100644 --- a/src/Util/Identifier.luau +++ b/src/Util/Identifier.luau @@ -1,42 +1,49 @@ --!optimize 2 ---!native +--!strict --@EternityDev -if not shared.__identifier_cache then - shared.__identifier_cache = { +local BITS: number = 8 +local MAX_VALUE: number = bit32.lshift(1, BITS) - 1 +local hook_fn: (string, number) -> () + +if not shared.__warp_identifier_registry then + shared.__warp_identifier_registry = { cache = {} :: { [string]: number }, - used = {} :: { [number]: string }, - count = 0 :: number, + counter = 0, } end -local registry = shared.__identifier_cache -local cache = registry.cache -local used = registry.used -local count = registry.count +local registry = shared.__warp_identifier_registry +local function fastAssert(condition: boolean, ...: any) + if not condition then + error(...) + end +end -local BITS: number = 16 -local MAX_VALUE: number = bit32.lshift(1, BITS) - 1 -local hash: number = game.PlaceVersion + 5381 +local Identifier = {} -return function(name: string): number - local cached = cache[name] +Identifier.on_added = function(fn: (string, number) -> ()) + fastAssert(hook_fn == nil, ".on_added can only be called once.") + hook_fn = fn +end + +Identifier.has = function(name: string): boolean + return registry.cache[name] ~= nil +end + +Identifier.get = function(name: string): number + local cached = registry.cache[name] if cached then return cached end - local b: number = hash - for i = 1, #name do - b = bit32.bxor(bit32.lshift(b, 5) + b, string.byte(name, i)) + local id = registry.counter + 1 + fastAssert(id <= MAX_VALUE, `Identifier overflow: exceeded {MAX_VALUE + 1} unique names.`) + registry.counter = id + registry.cache[name] = id + if hook_fn then + task.defer(hook_fn, name, id) end - - local reduced: number = (b % (MAX_VALUE - 2)) + 2 - local existing = used[reduced] - if existing and existing ~= name then - error(`[Warp] Hash collision: "{name}" & "{existing}" both map to ID {reduced}. Rename one.`) - end - - used[reduced] = name - cache[name] = reduced - count += 1 - return reduced + return id end + +return Identifier :: typeof(Identifier)