Compare commits

...

7 commits

Author SHA1 Message Date
Clown
7aab8290ca
Merge 96bed9bd7e into dab260f733 2025-06-06 22:28:50 -03:00
Ukendio
dab260f733 Cast numbers to Entity
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled
2025-06-07 02:36:34 +02:00
Ukendio
56b52286b8 Rework observers addon interface 2025-06-07 02:11:38 +02:00
dai
35b5f04a7c
Hook typing improvements (#236)
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
* Fix hook callback typings

* Update docs

* Add specialization

* Simplify overloads

* Remove generic
2025-06-06 15:14:23 +02:00
dai
6ce796e7fd
Fix missing parens (#235) 2025-06-06 15:13:54 +02:00
Ukendio
8490dfd294 Prevent partial matching of destroyed archetype
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-06-06 00:38:58 +02:00
YetAnotherClown
96bed9bd7e Empty Commit 2025-02-25 11:28:04 -05:00
8 changed files with 166 additions and 866 deletions

View file

@ -1,26 +1,22 @@
local jecs = require("@jecs")
type Observer = {
callback: (jecs.Entity) -> (),
query: jecs.Query<...any>,
}
type Monitor = {
callback: (jecs.Entity, jecs.Entity) -> (),
query: jecs.Query<any>
}
export type PatchedWorld = jecs.World & {
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (),
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
observer: (PatchedWorld, Observer) -> (),
monitor: (PatchedWorld, Monitor) -> (),
observer: (
PatchedWorld,
any,
(jecs.Entity) -> ()
) -> (),
monitor: (
PatchedWorld,
any,
(jecs.Entity, jecs.Id) -> ()
) -> ()
}
local function observers_new(world, description)
local query = description.query
local callback = description.callback
local function observers_new(world, query, callback)
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
@ -94,9 +90,7 @@ local function join(world, component)
end
end
local function monitors_new(world, description)
local query = description.query
local callback = description.callback
local function monitors_new(world, query, callback)
local terms = query.filter_with :: { jecs.Id }
if not terms then
local ids = query.ids
@ -131,7 +125,8 @@ local function monitors_new(world, description)
local archetype = r.archetype
if jecs.query_match(query, archetype) then
callback(entity, jecs.OnRemove)
local EcsOnRemove = jecs.OnRemove :: jecs.Id
callback(entity, EcsOnRemove)
end
end
@ -162,7 +157,7 @@ local function observers_add(world: jecs.World): PatchedWorld
listeners = {}
signals.added[component] = listeners
local function on_add(entity: number, id: number, value: any)
local function on_add(entity, id, value)
for _, listener in listeners :: any do
listener(entity, id, value)
end
@ -170,13 +165,14 @@ local function observers_add(world: jecs.World): PatchedWorld
local existing_hook = world:get(component, jecs.OnAdd)
if existing_hook then
table.insert(listeners, existing_hook)
local idr = world.component_index[component]
if idr then
idr.hooks.on_add = on_add
end
end
world:set(component, jecs.OnAdd, on_add)
local idr = world.component_index[component]
if idr then
idr.hooks.on_add = on_add
else
world:set(component, jecs.OnAdd, on_add)
end
end
table.insert(listeners, fn)
return function()
@ -196,7 +192,7 @@ local function observers_add(world: jecs.World): PatchedWorld
if not listeners then
listeners = {}
signals.emplaced[component] = listeners
local function on_change(entity: number, id: number, value: any)
local function on_change(entity, id, value: any)
for _, listener in listeners :: any do
listener(entity, id, value)
end
@ -204,12 +200,13 @@ local function observers_add(world: jecs.World): PatchedWorld
local existing_hook = world:get(component, jecs.OnChange)
if existing_hook then
table.insert(listeners, existing_hook)
local idr = world.component_index[component]
if idr then
idr.hooks.on_change = on_change
end
end
world:set(component, jecs.OnChange, on_change)
local idr = world.component_index[component]
if idr then
idr.hooks.on_change = on_change
else
world:set(component, jecs.OnChange, on_change)
end
end
table.insert(listeners, fn)
return function()
@ -229,23 +226,26 @@ local function observers_add(world: jecs.World): PatchedWorld
if not listeners then
listeners = {}
signals.removed[component] = listeners
local function on_remove(entity: number, id: number, value: any)
local function on_remove(entity, id)
for _, listener in listeners :: any do
listener(entity, id, value)
listener(entity, id)
end
end
local existing_hook = world:get(component, jecs.OnRemove)
if existing_hook then
table.insert(listeners, existing_hook)
local idr = world.component_index[component]
if idr then
idr.hooks.on_remove = on_remove
end
end
world:set(component, jecs.OnRemove, on_remove)
local idr = world.component_index[component]
if idr then
idr.hooks.on_remove = on_remove
else
world:set(component, jecs.OnRemove, on_remove)
end
end
table.insert(listeners, fn)
return function()
local n = #listeners
local i = table.find(listeners, fn)

View file

@ -1,18 +0,0 @@
option ClientOutput = "../net/client.luau"
option ServerOutput = "../net/server.luau"
event UpdateTransform {
From: Server,
Type: Unreliable,
Call: SingleSync,
Poll: true,
Data: (f64, CFrame)
}
event SpawnMob {
From: Server,
Type: Reliable,
Call: SingleSync,
Poll: true,
Data: (f64, CFrame, u8)
}

View file

@ -1,336 +0,0 @@
--!strict
--!native
--!optimize 2
--!nolint LocalShadow
--#selene: allow(shadowing)
-- File generated by Blink v0.14.1 (https://github.com/1Axen/Blink)
-- This file is not meant to be edited
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
if not RunService:IsClient() then
error("Client network module can only be required from the client.")
end
local Reliable: RemoteEvent = ReplicatedStorage:WaitForChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent
local Unreliable: UnreliableRemoteEvent =
ReplicatedStorage:WaitForChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
local Invocations = 0
local SendSize = 64
local SendOffset = 0
local SendCursor = 0
local SendBuffer = buffer.create(64)
local SendInstances = {}
local RecieveCursor = 0
local RecieveBuffer = buffer.create(64)
local RecieveInstances = {}
local RecieveInstanceCursor = 0
type Entry = {
value: any,
next: Entry?,
}
type Queue = {
head: Entry?,
tail: Entry?,
}
type BufferSave = {
Size: number,
Cursor: number,
Buffer: buffer,
Instances: { Instance },
}
local function Read(Bytes: number)
local Offset = RecieveCursor
RecieveCursor += Bytes
return Offset
end
local function Save(): BufferSave
return {
Size = SendSize,
Cursor = SendCursor,
Buffer = SendBuffer,
Instances = SendInstances,
}
end
local function Load(Save: BufferSave?)
if Save then
SendSize = Save.Size
SendCursor = Save.Cursor
SendOffset = Save.Cursor
SendBuffer = Save.Buffer
SendInstances = Save.Instances
return
end
SendSize = 64
SendCursor = 0
SendOffset = 0
SendBuffer = buffer.create(64)
SendInstances = {}
end
local function Invoke()
if Invocations == 255 then
Invocations = 0
end
local Invocation = Invocations
Invocations += 1
return Invocation
end
local function Allocate(Bytes: number)
local InUse = (SendCursor + Bytes)
if InUse > SendSize then
--> Avoid resizing the buffer for every write
while InUse > SendSize do
SendSize *= 1.5
end
local Buffer = buffer.create(SendSize)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
SendBuffer = Buffer
end
SendOffset = SendCursor
SendCursor += Bytes
return SendOffset
end
local function CreateQueue(): Queue
return {
head = nil,
tail = nil,
}
end
local function Pop(queue: Queue): any
local head = queue.head
if head == nil then
return
end
queue.head = head.next
return head.value
end
local function Push(queue: Queue, value: any)
local entry: Entry = {
value = value,
next = nil,
}
if queue.tail ~= nil then
queue.tail.next = entry
end
queue.tail = entry
if queue.head == nil then
queue.head = entry
end
end
local Types = {}
local Calls = table.create(256)
local Events: any = {
Reliable = table.create(256),
Unreliable = table.create(256),
}
local Queue: any = {
Reliable = table.create(256),
Unreliable = table.create(256),
}
Queue.Unreliable[0] = CreateQueue()
Queue.Reliable[0] = CreateQueue()
function Types.ReadEVENT_UpdateTransform(): (number, CFrame)
-- Read BLOCK: 32 bytes
local BLOCK_START = Read(32)
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
local Position = Vector3.new(X, Y, Z)
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
return Value1, Value2
end
function Types.WriteEVENT_UpdateTransform(Value1: number, Value2: CFrame): ()
-- Allocate BLOCK: 33 bytes
local BLOCK_START = Allocate(33)
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
local Vector = Value2.Position
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
local rX, rY, rZ = Value2:ToOrientation()
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
end
function Types.ReadEVENT_SpawnMob(): (number, CFrame, number)
-- Read BLOCK: 33 bytes
local BLOCK_START = Read(33)
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
local Position = Vector3.new(X, Y, Z)
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
local Value3 = buffer.readu8(RecieveBuffer, BLOCK_START + 32)
return Value1, Value2, Value3
end
function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: number): ()
-- Allocate BLOCK: 34 bytes
local BLOCK_START = Allocate(34)
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
local Vector = Value2.Position
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
local rX, rY, rZ = Value2:ToOrientation()
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3)
end
local function StepReplication()
if SendCursor <= 0 then
return
end
local Buffer = buffer.create(SendCursor)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
Reliable:FireServer(Buffer, SendInstances)
SendSize = 64
SendCursor = 0
SendOffset = 0
SendBuffer = buffer.create(64)
table.clear(SendInstances)
end
local Elapsed = 0
RunService.Heartbeat:Connect(function(DeltaTime: number)
Elapsed += DeltaTime
if Elapsed >= (1 / 61) then
Elapsed -= (1 / 61)
StepReplication()
end
end)
Reliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance })
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while RecieveCursor < Size do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
if Index == 0 then
Push(Queue.Reliable[0], table.pack(Types.ReadEVENT_SpawnMob()))
end
end
end)
Unreliable.OnClientEvent:Connect(function(Buffer: buffer, Instances: { Instance })
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while RecieveCursor < Size do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
if Index == 0 then
Push(Queue.Unreliable[0], table.pack(Types.ReadEVENT_UpdateTransform()))
end
end
end)
return {
StepReplication = StepReplication,
UpdateTransform = {
Iter = function(): () -> (number, number, CFrame)
local index = 0
local queue = Queue.Unreliable[0]
return function(): (number, number, CFrame)
index += 1
local arguments = Pop(queue)
if arguments ~= nil then
return index, unpack(arguments, 1, arguments.n)
end
return
end
end,
Next = function(): () -> (number, number, CFrame)
local index = 0
local queue = Queue.Unreliable[0]
return function(): (number, number, CFrame)
index += 1
local arguments = Pop(queue)
if arguments ~= nil then
return index, unpack(arguments, 1, arguments.n)
end
return
end
end,
},
SpawnMob = {
Iter = function(): () -> (number, number, CFrame, number)
local index = 0
local queue = Queue.Reliable[0]
return function(): (number, number, CFrame, number)
index += 1
local arguments = Pop(queue)
if arguments ~= nil then
return index, unpack(arguments, 1, arguments.n)
end
return
end
end,
Next = function(): () -> (number, number, CFrame, number)
local index = 0
local queue = Queue.Reliable[0]
return function(): (number, number, CFrame, number)
index += 1
local arguments = Pop(queue)
if arguments ~= nil then
return index, unpack(arguments, 1, arguments.n)
end
return
end
end,
},
}

View file

@ -1,372 +0,0 @@
--!strict
--!native
--!optimize 2
--!nolint LocalShadow
--#selene: allow(shadowing)
-- File generated by Blink v0.14.1 (https://github.com/1Axen/Blink)
-- This file is not meant to be edited
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
if not RunService:IsServer() then
error("Server network module can only be required from the server.")
end
local Reliable: RemoteEvent = ReplicatedStorage:FindFirstChild("BLINK_RELIABLE_REMOTE") :: RemoteEvent
if not Reliable then
local RemoteEvent = Instance.new("RemoteEvent")
RemoteEvent.Name = "BLINK_RELIABLE_REMOTE"
RemoteEvent.Parent = ReplicatedStorage
Reliable = RemoteEvent
end
local Unreliable: UnreliableRemoteEvent =
ReplicatedStorage:FindFirstChild("BLINK_UNRELIABLE_REMOTE") :: UnreliableRemoteEvent
if not Unreliable then
local UnreliableRemoteEvent = Instance.new("UnreliableRemoteEvent")
UnreliableRemoteEvent.Name = "BLINK_UNRELIABLE_REMOTE"
UnreliableRemoteEvent.Parent = ReplicatedStorage
Unreliable = UnreliableRemoteEvent
end
local Invocations = 0
local SendSize = 64
local SendOffset = 0
local SendCursor = 0
local SendBuffer = buffer.create(64)
local SendInstances = {}
local RecieveCursor = 0
local RecieveBuffer = buffer.create(64)
local RecieveInstances = {}
local RecieveInstanceCursor = 0
type Entry = {
value: any,
next: Entry?,
}
type Queue = {
head: Entry?,
tail: Entry?,
}
type BufferSave = {
Size: number,
Cursor: number,
Buffer: buffer,
Instances: { Instance },
}
local function Read(Bytes: number)
local Offset = RecieveCursor
RecieveCursor += Bytes
return Offset
end
local function Save(): BufferSave
return {
Size = SendSize,
Cursor = SendCursor,
Buffer = SendBuffer,
Instances = SendInstances,
}
end
local function Load(Save: BufferSave?)
if Save then
SendSize = Save.Size
SendCursor = Save.Cursor
SendOffset = Save.Cursor
SendBuffer = Save.Buffer
SendInstances = Save.Instances
return
end
SendSize = 64
SendCursor = 0
SendOffset = 0
SendBuffer = buffer.create(64)
SendInstances = {}
end
local function Invoke()
if Invocations == 255 then
Invocations = 0
end
local Invocation = Invocations
Invocations += 1
return Invocation
end
local function Allocate(Bytes: number)
local InUse = (SendCursor + Bytes)
if InUse > SendSize then
--> Avoid resizing the buffer for every write
while InUse > SendSize do
SendSize *= 1.5
end
local Buffer = buffer.create(SendSize)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
SendBuffer = Buffer
end
SendOffset = SendCursor
SendCursor += Bytes
return SendOffset
end
local function CreateQueue(): Queue
return {
head = nil,
tail = nil,
}
end
local function Pop(queue: Queue): any
local head = queue.head
if head == nil then
return
end
queue.head = head.next
return head.value
end
local function Push(queue: Queue, value: any)
local entry: Entry = {
value = value,
next = nil,
}
if queue.tail ~= nil then
queue.tail.next = entry
end
queue.tail = entry
if queue.head == nil then
queue.head = entry
end
end
local Types = {}
local Calls = table.create(256)
local Events: any = {
Reliable = table.create(256),
Unreliable = table.create(256),
}
local Queue: any = {
Reliable = table.create(256),
Unreliable = table.create(256),
}
function Types.ReadEVENT_UpdateTransform(): (number, CFrame)
-- Read BLOCK: 32 bytes
local BLOCK_START = Read(32)
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
local Position = Vector3.new(X, Y, Z)
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
return Value1, Value2
end
function Types.WriteEVENT_UpdateTransform(Value1: number, Value2: CFrame): ()
-- Allocate BLOCK: 33 bytes
local BLOCK_START = Allocate(33)
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
local Vector = Value2.Position
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
local rX, rY, rZ = Value2:ToOrientation()
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
end
function Types.ReadEVENT_SpawnMob(): (number, CFrame, number)
-- Read BLOCK: 33 bytes
local BLOCK_START = Read(33)
local Value1 = buffer.readf64(RecieveBuffer, BLOCK_START + 0)
local X = buffer.readf32(RecieveBuffer, BLOCK_START + 8)
local Y = buffer.readf32(RecieveBuffer, BLOCK_START + 12)
local Z = buffer.readf32(RecieveBuffer, BLOCK_START + 16)
local Position = Vector3.new(X, Y, Z)
local rX = buffer.readf32(RecieveBuffer, BLOCK_START + 20)
local rY = buffer.readf32(RecieveBuffer, BLOCK_START + 24)
local rZ = buffer.readf32(RecieveBuffer, BLOCK_START + 28)
local Value2 = CFrame.new(Position) * CFrame.fromOrientation(rX, rY, rZ)
local Value3 = buffer.readu8(RecieveBuffer, BLOCK_START + 32)
return Value1, Value2, Value3
end
function Types.WriteEVENT_SpawnMob(Value1: number, Value2: CFrame, Value3: number): ()
-- Allocate BLOCK: 34 bytes
local BLOCK_START = Allocate(34)
buffer.writeu8(SendBuffer, BLOCK_START + 0, 0)
buffer.writef64(SendBuffer, BLOCK_START + 1, Value1)
local Vector = Value2.Position
buffer.writef32(SendBuffer, BLOCK_START + 9, Vector.X)
buffer.writef32(SendBuffer, BLOCK_START + 13, Vector.Y)
buffer.writef32(SendBuffer, BLOCK_START + 17, Vector.Z)
local rX, rY, rZ = Value2:ToOrientation()
buffer.writef32(SendBuffer, BLOCK_START + 21, rX)
buffer.writef32(SendBuffer, BLOCK_START + 25, rY)
buffer.writef32(SendBuffer, BLOCK_START + 29, rZ)
buffer.writeu8(SendBuffer, BLOCK_START + 33, Value3)
end
local PlayersMap: { [Player]: BufferSave } = {}
Players.PlayerRemoving:Connect(function(Player)
PlayersMap[Player] = nil
end)
local function StepReplication()
for Player, Send in PlayersMap do
if Send.Cursor <= 0 then
continue
end
local Buffer = buffer.create(Send.Cursor)
buffer.copy(Buffer, 0, Send.Buffer, 0, Send.Cursor)
Reliable:FireClient(Player, Buffer, Send.Instances)
Send.Size = 64
Send.Cursor = 0
Send.Buffer = buffer.create(64)
table.clear(Send.Instances)
end
end
RunService.Heartbeat:Connect(StepReplication)
Reliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance })
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while RecieveCursor < Size do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
end
end)
Unreliable.OnServerEvent:Connect(function(Player: Player, Buffer: buffer, Instances: { Instance })
RecieveCursor = 0
RecieveBuffer = Buffer
RecieveInstances = Instances
RecieveInstanceCursor = 0
local Size = buffer.len(RecieveBuffer)
while RecieveCursor < Size do
-- Read BLOCK: 1 bytes
local BLOCK_START = Read(1)
local Index = buffer.readu8(RecieveBuffer, BLOCK_START + 0)
end
end)
return {
StepReplication = StepReplication,
UpdateTransform = {
Fire = function(Player: Player, Value1: number, Value2: CFrame): ()
Load()
Types.WriteEVENT_UpdateTransform(Value1, Value2)
local Buffer = buffer.create(SendCursor)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
Unreliable:FireClient(Player, Buffer, SendInstances)
end,
FireAll = function(Value1: number, Value2: CFrame): ()
Load()
Types.WriteEVENT_UpdateTransform(Value1, Value2)
local Buffer = buffer.create(SendCursor)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
Unreliable:FireAllClients(Buffer, SendInstances)
end,
FireList = function(List: { Player }, Value1: number, Value2: CFrame): ()
Load()
Types.WriteEVENT_UpdateTransform(Value1, Value2)
local Buffer = buffer.create(SendCursor)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
for _, Player in List do
Unreliable:FireClient(Player, Buffer, SendInstances)
end
end,
FireExcept = function(Except: Player, Value1: number, Value2: CFrame): ()
Load()
Types.WriteEVENT_UpdateTransform(Value1, Value2)
local Buffer = buffer.create(SendCursor)
buffer.copy(Buffer, 0, SendBuffer, 0, SendCursor)
for _, Player in Players:GetPlayers() do
if Player == Except then
continue
end
Unreliable:FireClient(Player, Buffer, SendInstances)
end
end,
},
SpawnMob = {
Fire = function(Player: Player, Value1: number, Value2: CFrame, Value3: number): ()
Load(PlayersMap[Player])
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
PlayersMap[Player] = Save()
end,
FireAll = function(Value1: number, Value2: CFrame, Value3: number): ()
Load()
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
for _, Player in Players:GetPlayers() do
Load(PlayersMap[Player])
local Position = Allocate(Size)
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
PlayersMap[Player] = Save()
end
end,
FireList = function(List: { Player }, Value1: number, Value2: CFrame, Value3: number): ()
Load()
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
for _, Player in List do
Load(PlayersMap[Player])
local Position = Allocate(Size)
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
PlayersMap[Player] = Save()
end
end,
FireExcept = function(Except: Player, Value1: number, Value2: CFrame, Value3: number): ()
Load()
Types.WriteEVENT_SpawnMob(Value1, Value2, Value3)
local Buffer, Size, Instances = SendBuffer, SendCursor, SendInstances
for _, Player in Players:GetPlayers() do
if Player == Except then
continue
end
Load(PlayersMap[Player])
local Position = Allocate(Size)
buffer.copy(SendBuffer, Position, Buffer, 0, Size)
table.move(Instances, 1, #Instances, #SendInstances + 1, SendInstances)
PlayersMap[Player] = Save()
end
end,
},
}

View file

@ -132,26 +132,26 @@ Component data generally need to adhere to a specific interface, and sometimes r
::: code-group
```luau [luau]
local Transform = world:component()
world:set(Transform, OnAdd, function(entity)
-- A transform component has been added to an entity
world:set(Transform, OnAdd, function(entity, id, data)
-- A transform component `id` has been added with `data` to `entity`
end)
world:set(Transform, OnRemove, function(entity)
-- A transform component has been removed from the entity
world:set(Transform, OnRemove, function(entity, id)
-- A transform component `id` has been removed from `entity`
end)
world:set(Transform, OnChange, function(entity, value)
-- A transform component has been changed to value on the entity
world:set(Transform, OnChange, function(entity, id, data)
-- A transform component `id` has been changed to `data` on `entity`
end)
```
```typescript [typescript]
const Transform = world.component();
world.set(Transform, OnAdd, (entity) => {
// A transform component has been added to an entity
world.set(Transform, OnAdd, (entity, id, data) => {
// A transform component `id` has been added with `data` to `entity`
});
world.set(Transform, OnRemove, (entity) => {
// A transform component has been removed from the entity
world.set(Transform, OnRemove, (entity, id) => {
// A transform component `id` has been removed from `entity`
});
world.set(Transform, OnChange, (entity, value) => {
// A transform component has been changed to value on the entity
world.set(Transform, OnChange, (entity, id, data) => {
// A transform component `id` has been changed to `data` on `entity`
});
```
:::
@ -540,10 +540,10 @@ Test if entity has a relationship wildcard
:::code-group
```luau [luau]
world:has(bob, pair(Eats, jecs.Wildcard)
world:has(bob, pair(Eats, jecs.Wildcard))
```
```typescript [typescript]
world.has(bob, pair(Eats, jecs.Wildcard)
world.has(bob, pair(Eats, jecs.Wildcard))
```
:::
@ -578,7 +578,7 @@ for id in world:query(pair(Eats, Apples)) do
end
```
```typescript [typescript]
for (const [id] of world.query(pair(Eats, Apples)) {
for (const [id] of world.query(pair(Eats, Apples))) {
// ...
}
```
@ -593,7 +593,7 @@ for id in world:query(pair(Eats, jecs.Wildcard)) do
end
```
```typescript [typescript]
for (const [id] of world.query(pair(Eats, jecs.Wildcard)) {
for (const [id] of world.query(pair(Eats, jecs.Wildcard))) {
const food = world.target(id, Eats) // Apples, ...
}
```
@ -608,7 +608,7 @@ for child in world:query(pair(jecs.ChildOf, parent)) do
end
```
```typescript [typescript]
for (const [child] of world.query(pair(jecs.ChildOf, parent)) {
for (const [child] of world.query(pair(jecs.ChildOf, parent))) {
// ...
}
```

22
jecs.d.ts vendored
View file

@ -151,6 +151,15 @@ export class World {
*/
add<C>(entity: Entity, component: undefined extends InferComponent<C> ? C : Id<undefined>): void;
/**
* Installs a hook on the given component.
* @param component The target component.
* @param hook The hook to install.
* @param value The hook callback.
*/
set<T>(component: Entity<T>, hook: StatefulHook, value: (e: Entity<T>, id: Id<T>, data: T) => void): void;
set<T>(component: Entity<T>, hook: StatelessHook, value: (e: Entity<T>, id: Id<T>) => void): void;
/**
* Assigns a value to a component on the given entity.
* @param entity The target entity.
@ -280,9 +289,16 @@ export function pair_first<P, O>(world: World, p: Pair<P, O>): Entity<P>;
*/
export function pair_second<P, O>(world: World, p: Pair<P, O>): Entity<O>;
export declare const OnAdd: Entity<(e: Entity) => void>;
export declare const OnRemove: Entity<(e: Entity) => void>;
export declare const OnChange: Entity<(e: Entity, value: unknown) => void>;
type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & {
readonly __nominal_StatefulHook: unique symbol,
}
type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & {
readonly __nominal_StatelessHook: unique symbol,
}
export declare const OnAdd: StatefulHook;
export declare const OnRemove: StatelessHook;
export declare const OnChange: StatefulHook;
export declare const ChildOf: Tag;
export declare const Wildcard: Entity;
export declare const w: Entity;

View file

@ -1,4 +1,3 @@
--!optimize 2
--!native
--!strict
@ -95,30 +94,31 @@ type ecs_world_t = {
observable: Map<i53, Map<i53, { ecs_observer_t }>>,
}
local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
-- stylua: ignore start
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnChange = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6
local EcsOnDelete = HI_COMPONENT_ID + 7
local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11
local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14
local ECS_ID_DELETE = 0b01
local ECS_ID_IS_TAG = 0b10
local ECS_ID_MASK = 0b00
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_PAIR_OFFSET = 2^48
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_PAIR_OFFSET = 2^48
local ECS_ID_DELETE = 0b01
local ECS_ID_IS_TAG = 0b10
local ECS_ID_MASK = 0b00
local HI_COMPONENT_ID = 256
local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2
local EcsOnChange = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6
local EcsOnDelete = HI_COMPONENT_ID + 7
local EcsOnDeleteTarget = HI_COMPONENT_ID + 8
local EcsDelete = HI_COMPONENT_ID + 9
local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11
local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14
local NULL_ARRAY = table.freeze({}) :: Column
local NULL = newproxy(false)
@ -1934,6 +1934,9 @@ local function query_cached(query: ecs_query_data_t)
local function on_delete_callback(archetype)
local i = table.find(archetypes, archetype) :: number
if i == nil then
return
end
local n = #archetypes
archetypes[i] = archetypes[n]
archetypes[n] = nil
@ -2652,19 +2655,19 @@ return {
meta = (ECS_META :: any) :: <T>(id: Entity, id: Id<T>, value: T) -> Entity<T>,
is_tag = (ecs_is_tag :: any) :: <T>(World, Id<T>) -> boolean,
OnAdd = EcsOnAdd :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
OnRemove = EcsOnRemove :: Entity<(entity: Entity, id: Id) -> ()>,
OnChange = EcsOnChange :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
ChildOf = EcsChildOf :: Entity,
Component = EcsComponent :: Entity,
Wildcard = EcsWildcard :: Entity,
w = EcsWildcard :: Entity,
OnDelete = EcsOnDelete :: Entity,
OnDeleteTarget = EcsOnDeleteTarget :: Entity,
Delete = EcsDelete :: Entity,
Remove = EcsRemove :: Entity,
Name = EcsName :: Entity<string>,
Rest = EcsRest :: Entity,
OnAdd = (EcsOnAdd :: any) :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
OnRemove = (EcsOnRemove :: any) :: Entity<(entity: Entity, id: Id) -> ()>,
OnChange = (EcsOnChange :: any) :: Entity<<T>(entity: Entity, id: Id<T>, data: T) -> ()>,
ChildOf = (EcsChildOf :: any) :: Entity,
Component = (EcsComponent :: any) :: Entity,
Wildcard = (EcsWildcard :: any) :: Entity,
w = (EcsWildcard :: any) :: Entity,
OnDelete = (EcsOnDelete :: any) :: Entity,
OnDeleteTarget = (EcsOnDeleteTarget :: any) :: Entity,
Delete = (EcsDelete :: any) :: Entity,
Remove = (EcsRemove :: any) :: Entity,
Name = (EcsName :: any) :: Entity<string>,
Rest = (EcsRest :: any) :: Entity,
pair = (ECS_PAIR :: any) :: <P, O>(first: Id<P>, second: Id<O>) -> Pair<P, O>,

View file

@ -4,114 +4,121 @@ local test = testkit.test()
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local observers_add = require("@addons/observers")
TEST("addons/observers", function()
local world = observers_add(jecs.world())
do CASE "Should work even if set after the component has been used"
local A = world:component()
world:set(world:entity(), A, 2)
local ran = true
world:added(A, function()
ran = false
end)
local entity = world:entity()
world:set(entity, A, 3)
CHECK(ran)
end
do CASE "Should not override hook"
local A = world:component()
local count = 0
local count = 1
local function counter()
count += 1
count += 2
end
world:set(A, jecs.OnAdd, counter)
world:set(world:entity(), A, true)
CHECK(count == 1)
world:added(A, counter)
world:set(world:entity(), A, true)
world:set(world:entity(), A, false)
CHECK(count == 3)
world:set(world:entity(), A, false)
CHECK(count == 5)
end
do CASE "Ensure ordering between signals and observers"
local A = world:component()
local B = world:component()
local count = 0
local count = 1
local function counter()
count += 1
count += 2
end
world:observer({
callback = counter,
query = world:query(A, B),
})
world:observer(world:query(A, B), counter)
world:added(A, counter)
world:added(A, counter)
local e = world:entity()
world:add(e, A)
CHECK(count == 2)
CHECK(count == 3)
world:add(e, B)
CHECK(count == 3)
CHECK(count == 4)
end
do CASE "Rematch entities in observers"
local A = world:component()
local count = 0
local count = 1
local function counter()
count += 1
count += 2
end
world:observer({
query = world:query(A),
callback = counter
})
world:observer(world:query(A), counter)
local e = world:entity()
world:set(e, A, true)
CHECK(count == 1)
world:remove(e, A)
CHECK(count == 1)
world:set(e, A, true)
world:set(e, A, false)
CHECK(count == 2)
world:set(e, A, true)
world:remove(e, A)
CHECK(count == 2)
world:set(e, A, false)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "Don't report changed components in monitor"
local A = world:component()
local count = 0
local count = 1
local function counter()
count += 1
count += 2
end
world:monitor({
query = world:query(A),
callback = counter
})
world:monitor(world:query(A), counter)
local e = world:entity()
world:set(e, A, true)
CHECK(count == 1)
world:remove(e, A)
world:set(e, A, false)
CHECK(count == 2)
world:set(e, A, true)
CHECK(count == 3)
world:set(e, A, true)
world:remove(e, A)
CHECK(count == 3)
world:set(e, A, false)
CHECK(count == 4)
world:set(e, A, false)
CHECK(count == 4)
end
do CASE "Call on pairs"
do CASE "Call off pairs"
local A = world:component()
local callcount = 0
local callcount = 1
world:added(A, function(entity)
callcount += 1
callcount += 2
end)
world:added(A, function(entity)
callcount += 1
callcount += 2
end)
local e = world:entity()
local e1 = world:entity()
local e2 = world:entity()
world:add(e1, jecs.pair(A, e))
world:add(e, jecs.pair(A, e1))
CHECK(callcount == 4)
world:add(e2, jecs.pair(A, e))
world:add(e, jecs.pair(A, e2))
CHECK(callcount == 5)
end
end)