mirror of
				https://github.com/Ukendio/jecs.git
				synced 2025-11-03 18:39:19 +00:00 
			
		
		
		
	Change Observers to support cleanups and :with/without
	
		
			
	
		
	
	
		
	
		
			Some checks are pending
		
		
	
	
	
				
					
				
			
		
			Some checks are pending
		
		
	
	
This commit is contained in:
		
							parent
							
								
									3dacb2af80
								
							
						
					
					
						commit
						456713c2d5
					
				
					 2 changed files with 142 additions and 88 deletions
				
			
		
							
								
								
									
										192
									
								
								addons/ob.luau
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								addons/ob.luau
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -10,22 +10,16 @@ type Entity<T> = jecs.Entity<T>
 | 
			
		|||
 | 
			
		||||
export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
 | 
			
		||||
 | 
			
		||||
export type Observer<T...> = typeof(setmetatable(
 | 
			
		||||
	{} :: {
 | 
			
		||||
		iter: Iter<T...>,
 | 
			
		||||
		entities: { Entity<nil> },
 | 
			
		||||
		disconnect: (Observer<T...>) -> ()
 | 
			
		||||
	},
 | 
			
		||||
	{} :: {
 | 
			
		||||
		__iter: Iter<T...>,
 | 
			
		||||
	}
 | 
			
		||||
))
 | 
			
		||||
export type Observer<T...> = {
 | 
			
		||||
	disconnect: (Observer<T...>) -> (),
 | 
			
		||||
	added: ((jecs.Entity) -> ()) -> (),
 | 
			
		||||
	removed: ((jecs.Entity) -> ()) -> ()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
local function observers_new<T...>(
 | 
			
		||||
	query: Query<T...>,
 | 
			
		||||
	callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
 | 
			
		||||
	callback: ((Entity<nil>) -> ())
 | 
			
		||||
): Observer<T...>
 | 
			
		||||
 | 
			
		||||
	query:cached()
 | 
			
		||||
 | 
			
		||||
	local world = (query :: Query<T...> & { world: World }).world
 | 
			
		||||
| 
						 | 
				
			
			@ -47,8 +41,6 @@ local function observers_new<T...>(
 | 
			
		|||
	end
 | 
			
		||||
 | 
			
		||||
	local entity_index = world.entity_index :: any
 | 
			
		||||
	local i = 0
 | 
			
		||||
	local entities = {}
 | 
			
		||||
 | 
			
		||||
	local function emplaced<T, a>(
 | 
			
		||||
		entity: jecs.Entity<T>,
 | 
			
		||||
| 
						 | 
				
			
			@ -60,70 +52,74 @@ local function observers_new<T...>(
 | 
			
		|||
		local archetype = r.archetype
 | 
			
		||||
 | 
			
		||||
		if archetypes[archetype.id] then
 | 
			
		||||
			i += 1
 | 
			
		||||
			entities[i] = entity
 | 
			
		||||
			if callback ~= nil then
 | 
			
		||||
				callback(entity, id, value)
 | 
			
		||||
			end
 | 
			
		||||
			callback(entity)
 | 
			
		||||
		end
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local cleanup = {}
 | 
			
		||||
 | 
			
		||||
	for _, term in terms do
 | 
			
		||||
		if jecs.IS_PAIR(term) then
 | 
			
		||||
			term = jecs.ECS_PAIR_FIRST(term)
 | 
			
		||||
		end
 | 
			
		||||
		world:added(term, emplaced)
 | 
			
		||||
		world:changed(term, emplaced)
 | 
			
		||||
		local onadded = world:added(term, emplaced)
 | 
			
		||||
		local onchanged = world:changed(term, emplaced)
 | 
			
		||||
		table.insert(cleanup, onadded)
 | 
			
		||||
		table.insert(cleanup, onchanged)
 | 
			
		||||
 	end
 | 
			
		||||
 | 
			
		||||
  	local function disconnect()
 | 
			
		||||
   		table.remove(observers_on_create, table.find(
 | 
			
		||||
     		observers_on_create,
 | 
			
		||||
       		observer_on_create
 | 
			
		||||
     	))
 | 
			
		||||
	local without = query.filter_without
 | 
			
		||||
   	if without then
 | 
			
		||||
		for _, term in without do
 | 
			
		||||
			if jecs.IS_PAIR(term) then
 | 
			
		||||
				term = jecs.ECS_PAIR_FIRST(term)
 | 
			
		||||
			end
 | 
			
		||||
			local onremoved = world:removed(term, function(entity, id)
 | 
			
		||||
				local r = jecs.record(world, entity)
 | 
			
		||||
				local archetype = r.archetype
 | 
			
		||||
				if archetype then
 | 
			
		||||
					local dst = jecs.archetype_traverse_remove(world, id, archetype)
 | 
			
		||||
					if archetypes[dst.id] then
 | 
			
		||||
						callback(entity)
 | 
			
		||||
					end
 | 
			
		||||
				end
 | 
			
		||||
			end)
 | 
			
		||||
			table.insert(cleanup, onremoved)
 | 
			
		||||
		end
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
     	table.remove(observers_on_delete, table.find(
 | 
			
		||||
      		observers_on_delete,
 | 
			
		||||
        	observer_on_delete
 | 
			
		||||
 	local function disconnect()
 | 
			
		||||
  		table.remove(observers_on_create, table.find(
 | 
			
		||||
	  		observers_on_create,
 | 
			
		||||
	    	observer_on_create
 | 
			
		||||
	   	))
 | 
			
		||||
 | 
			
		||||
	   	table.remove(observers_on_delete, table.find(
 | 
			
		||||
	      	observers_on_delete,
 | 
			
		||||
	       	observer_on_delete
 | 
			
		||||
       	))
 | 
			
		||||
 | 
			
		||||
		table.clear(archetypes)
 | 
			
		||||
 | 
			
		||||
		for _, disconnect in cleanup do
 | 
			
		||||
			disconnect()
 | 
			
		||||
		end
 | 
			
		||||
   	end
 | 
			
		||||
 | 
			
		||||
    local function iter()
 | 
			
		||||
  		local row = i
 | 
			
		||||
  		return function()
 | 
			
		||||
 			if row == 0 then
 | 
			
		||||
     			i = 0
 | 
			
		||||
      			table.clear(entities)
 | 
			
		||||
      		end
 | 
			
		||||
           	local entity = entities[row]
 | 
			
		||||
           	row -= 1
 | 
			
		||||
           	return entity
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
  	local observer = {
 | 
			
		||||
   		disconnect = disconnect,
 | 
			
		||||
     	entities = entities,
 | 
			
		||||
     	__iter = iter,
 | 
			
		||||
      	iter = iter
 | 
			
		||||
	local observer = {
 | 
			
		||||
  		disconnect = disconnect,
 | 
			
		||||
   	}
 | 
			
		||||
 | 
			
		||||
    setmetatable(observer, observer)
 | 
			
		||||
 | 
			
		||||
    return (observer :: any) :: Observer<T...>
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function monitors_new<T...>(
 | 
			
		||||
	query: Query<T...>,
 | 
			
		||||
	callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
 | 
			
		||||
): Observer<T...>
 | 
			
		||||
 | 
			
		||||
local function monitors_new<T...>(query: Query<T...>): Observer<T...>
 | 
			
		||||
	query:cached()
 | 
			
		||||
 | 
			
		||||
	local world = (query :: Query<T...> & { world: World }).world
 | 
			
		||||
 | 
			
		||||
	local archetypes = {}
 | 
			
		||||
	local terms = query.ids
 | 
			
		||||
	local terms = query.filter_with :: { jecs.Id<any> }
 | 
			
		||||
	local first = terms[1]
 | 
			
		||||
 | 
			
		||||
	local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
 | 
			
		||||
| 
						 | 
				
			
			@ -137,47 +133,77 @@ local function monitors_new<T...>(
 | 
			
		|||
		archetypes[archetype.id] = nil
 | 
			
		||||
	end
 | 
			
		||||
	local entity_index = world.entity_index :: any
 | 
			
		||||
	local i = 0
 | 
			
		||||
	local entities = {}
 | 
			
		||||
 | 
			
		||||
	local callback_added: ((jecs.Entity) -> ())?
 | 
			
		||||
	local callback_removed: ((jecs.Entity) -> ())?
 | 
			
		||||
 | 
			
		||||
	local function emplaced<T, a>(
 | 
			
		||||
		entity: jecs.Entity<T>,
 | 
			
		||||
		id: jecs.Id<a>,
 | 
			
		||||
		value: a?
 | 
			
		||||
	)
 | 
			
		||||
		if callback_added == nil then
 | 
			
		||||
			return
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		local r = jecs.entity_index_try_get_fast(
 | 
			
		||||
			entity_index, entity :: any) :: jecs.Record
 | 
			
		||||
 | 
			
		||||
		local archetype = r.archetype
 | 
			
		||||
 | 
			
		||||
		if archetypes[archetype.id] then
 | 
			
		||||
			i += 1
 | 
			
		||||
			entities[i] = entity
 | 
			
		||||
			if callback ~= nil then
 | 
			
		||||
				callback(entity, jecs.OnAdd)
 | 
			
		||||
			end
 | 
			
		||||
			callback_added(entity)
 | 
			
		||||
		end
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local function removed(entity: jecs.Entity, component: jecs.Id)
 | 
			
		||||
		if callback_removed == nil then
 | 
			
		||||
			return
 | 
			
		||||
		end
 | 
			
		||||
		local r = jecs.record(world, entity)
 | 
			
		||||
		if not archetypes[r.archetype.id] then
 | 
			
		||||
			return
 | 
			
		||||
		end
 | 
			
		||||
		local EcsOnRemove = jecs.OnRemove :: jecs.Id
 | 
			
		||||
		if callback ~= nil then
 | 
			
		||||
			callback(entity, EcsOnRemove)
 | 
			
		||||
		end
 | 
			
		||||
		callback_removed(entity)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local cleanup = {}
 | 
			
		||||
 | 
			
		||||
	for _, term in terms do
 | 
			
		||||
		if jecs.IS_PAIR(term) then
 | 
			
		||||
			term = jecs.ECS_PAIR_FIRST(term)
 | 
			
		||||
		end
 | 
			
		||||
		world:added(term, emplaced)
 | 
			
		||||
		world:removed(term, removed)
 | 
			
		||||
		local onadded = world:added(term, emplaced)
 | 
			
		||||
		local onremoved = world:removed(term, removed)
 | 
			
		||||
		table.insert(cleanup, onadded)
 | 
			
		||||
		table.insert(cleanup, onremoved)
 | 
			
		||||
 	end
 | 
			
		||||
 | 
			
		||||
  	local without = query.filter_without
 | 
			
		||||
   	if without then
 | 
			
		||||
	   	for _, term in without do
 | 
			
		||||
			if jecs.IS_PAIR(term) then
 | 
			
		||||
				term = jecs.ECS_PAIR_FIRST(term)
 | 
			
		||||
			end
 | 
			
		||||
			local onadded = world:added(term, removed)
 | 
			
		||||
			local onremoved = world:removed(term, function(entity, id)
 | 
			
		||||
				if callback_added == nil then
 | 
			
		||||
					return
 | 
			
		||||
				end
 | 
			
		||||
				local r = jecs.record(world, entity)
 | 
			
		||||
				local archetype = r.archetype
 | 
			
		||||
				if archetype then
 | 
			
		||||
					local dst = jecs.archetype_traverse_remove(world, id, archetype)
 | 
			
		||||
					if archetypes[dst.id] then
 | 
			
		||||
						callback_added(entity)
 | 
			
		||||
					end
 | 
			
		||||
				end
 | 
			
		||||
			end)
 | 
			
		||||
			table.insert(cleanup, onadded)
 | 
			
		||||
			table.insert(cleanup, onremoved)
 | 
			
		||||
    	end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
 	local function disconnect()
 | 
			
		||||
  		table.remove(observers_on_create, table.find(
 | 
			
		||||
	  		observers_on_create,
 | 
			
		||||
| 
						 | 
				
			
			@ -188,30 +214,28 @@ local function monitors_new<T...>(
 | 
			
		|||
	      	observers_on_delete,
 | 
			
		||||
	       	observer_on_delete
 | 
			
		||||
       	))
 | 
			
		||||
 | 
			
		||||
		table.clear(archetypes)
 | 
			
		||||
 | 
			
		||||
		for _, disconnect in cleanup do
 | 
			
		||||
			disconnect()
 | 
			
		||||
		end
 | 
			
		||||
   	end
 | 
			
		||||
 | 
			
		||||
    local function iter()
 | 
			
		||||
  		local row = i
 | 
			
		||||
  		return function()
 | 
			
		||||
 			if row == 0 then
 | 
			
		||||
     			i = 0
 | 
			
		||||
     			table.clear(entities)
 | 
			
		||||
      		end
 | 
			
		||||
           	local entity = entities[row]
 | 
			
		||||
           	row -= 1
 | 
			
		||||
           	return entity
 | 
			
		||||
        end
 | 
			
		||||
    local function monitor_added(callback)
 | 
			
		||||
		callback_added = callback
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    local function monitor_removed(callback)
 | 
			
		||||
    	callback_removed = callback
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
	local observer = {
 | 
			
		||||
  		disconnect = disconnect,
 | 
			
		||||
	   	entities = entities,
 | 
			
		||||
	   	__iter = iter,
 | 
			
		||||
       	iter = iter
 | 
			
		||||
    	added = monitor_added,
 | 
			
		||||
     	removed = monitor_removed
 | 
			
		||||
   	}
 | 
			
		||||
 | 
			
		||||
    setmetatable(observer, observer)
 | 
			
		||||
 | 
			
		||||
    return (observer :: any) :: Observer<T...>
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,15 +5,42 @@ local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
 | 
			
		|||
local FOCUS = test.FOCUS
 | 
			
		||||
local ob = require("@addons/ob")
 | 
			
		||||
 | 
			
		||||
TEST("addons/observers", function()
 | 
			
		||||
 | 
			
		||||
TEST("addons/ob", function()
 | 
			
		||||
	local world = jecs.world()
 | 
			
		||||
	do CASE "Should support query:without()"
 | 
			
		||||
		local A = world:component()
 | 
			
		||||
		local B = world:component()
 | 
			
		||||
 | 
			
		||||
		local c = 1
 | 
			
		||||
		local monitor = ob.monitor(world:query(A):without(B))
 | 
			
		||||
		monitor.added(function()
 | 
			
		||||
			c += 1
 | 
			
		||||
		end)
 | 
			
		||||
		monitor.removed(function()
 | 
			
		||||
			c += 1
 | 
			
		||||
		end)
 | 
			
		||||
 | 
			
		||||
		local child = world:entity()
 | 
			
		||||
		world:add(child, B)
 | 
			
		||||
		CHECK(c==1)
 | 
			
		||||
		world:add(child, A)
 | 
			
		||||
		CHECK(c==1)
 | 
			
		||||
		world:remove(child, B)
 | 
			
		||||
		CHECK(c==2)
 | 
			
		||||
		world:remove(child, A)
 | 
			
		||||
		CHECK(c==3)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do CASE "monitors should accept pairs"
 | 
			
		||||
		local A = world:component()
 | 
			
		||||
		local B = world:component()
 | 
			
		||||
 | 
			
		||||
		local c = 1
 | 
			
		||||
		ob.monitor(world:query(jecs.pair(A, B)), function (_, event)
 | 
			
		||||
		local monitor = ob.monitor(world:query(jecs.pair(A, B)))
 | 
			
		||||
		monitor.added(function()
 | 
			
		||||
			c += 1
 | 
			
		||||
		end)
 | 
			
		||||
		monitor.removed(function()
 | 
			
		||||
			c += 1
 | 
			
		||||
		end)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +51,7 @@ TEST("addons/observers", function()
 | 
			
		|||
		world:remove(child, jecs.pair(A, B))
 | 
			
		||||
		CHECK(c == 3)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do CASE "Ensure ordering between signals and observers"
 | 
			
		||||
		local A = world:component()
 | 
			
		||||
		local B = world:component()
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +106,9 @@ TEST("addons/observers", function()
 | 
			
		|||
			count += 1
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		ob.monitor(world:query(A), counter)
 | 
			
		||||
		local monitor = ob.monitor(world:query(A))
 | 
			
		||||
		monitor.added(counter)
 | 
			
		||||
		monitor.removed(counter)
 | 
			
		||||
 | 
			
		||||
		local e = world:entity()
 | 
			
		||||
		world:set(e, A, false)
 | 
			
		||||
		Loading…
	
		Reference in a new issue