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 Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Observer<T...> = typeof(setmetatable(
 | 
					export type Observer<T...> = {
 | 
				
			||||||
	{} :: {
 | 
						disconnect: (Observer<T...>) -> (),
 | 
				
			||||||
		iter: Iter<T...>,
 | 
						added: ((jecs.Entity) -> ()) -> (),
 | 
				
			||||||
		entities: { Entity<nil> },
 | 
						removed: ((jecs.Entity) -> ()) -> ()
 | 
				
			||||||
		disconnect: (Observer<T...>) -> ()
 | 
					}
 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	{} :: {
 | 
					 | 
				
			||||||
		__iter: Iter<T...>,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
local function observers_new<T...>(
 | 
					local function observers_new<T...>(
 | 
				
			||||||
	query: Query<T...>,
 | 
						query: Query<T...>,
 | 
				
			||||||
	callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
 | 
						callback: ((Entity<nil>) -> ())
 | 
				
			||||||
): Observer<T...>
 | 
					): Observer<T...>
 | 
				
			||||||
 | 
					 | 
				
			||||||
	query:cached()
 | 
						query:cached()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local world = (query :: Query<T...> & { world: World }).world
 | 
						local world = (query :: Query<T...> & { world: World }).world
 | 
				
			||||||
| 
						 | 
					@ -47,8 +41,6 @@ local function observers_new<T...>(
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local entity_index = world.entity_index :: any
 | 
						local entity_index = world.entity_index :: any
 | 
				
			||||||
	local i = 0
 | 
					 | 
				
			||||||
	local entities = {}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local function emplaced<T, a>(
 | 
						local function emplaced<T, a>(
 | 
				
			||||||
		entity: jecs.Entity<T>,
 | 
							entity: jecs.Entity<T>,
 | 
				
			||||||
| 
						 | 
					@ -60,70 +52,74 @@ local function observers_new<T...>(
 | 
				
			||||||
		local archetype = r.archetype
 | 
							local archetype = r.archetype
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if archetypes[archetype.id] then
 | 
							if archetypes[archetype.id] then
 | 
				
			||||||
			i += 1
 | 
								callback(entity)
 | 
				
			||||||
			entities[i] = entity
 | 
					 | 
				
			||||||
			if callback ~= nil then
 | 
					 | 
				
			||||||
				callback(entity, id, value)
 | 
					 | 
				
			||||||
			end
 | 
					 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						local cleanup = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, term in terms do
 | 
						for _, term in terms do
 | 
				
			||||||
		if jecs.IS_PAIR(term) then
 | 
							if jecs.IS_PAIR(term) then
 | 
				
			||||||
			term = jecs.ECS_PAIR_FIRST(term)
 | 
								term = jecs.ECS_PAIR_FIRST(term)
 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
		world:added(term, emplaced)
 | 
							local onadded = world:added(term, emplaced)
 | 
				
			||||||
		world:changed(term, emplaced)
 | 
							local onchanged = world:changed(term, emplaced)
 | 
				
			||||||
 | 
							table.insert(cleanup, onadded)
 | 
				
			||||||
 | 
							table.insert(cleanup, onchanged)
 | 
				
			||||||
 	end
 | 
					 	end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  	local function disconnect()
 | 
						local without = query.filter_without
 | 
				
			||||||
   		table.remove(observers_on_create, table.find(
 | 
					   	if without then
 | 
				
			||||||
     		observers_on_create,
 | 
							for _, term in without do
 | 
				
			||||||
       		observer_on_create
 | 
								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(
 | 
					 	local function disconnect()
 | 
				
			||||||
      		observers_on_delete,
 | 
					  		table.remove(observers_on_create, table.find(
 | 
				
			||||||
        	observer_on_delete
 | 
						  		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
 | 
					   	end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    local function iter()
 | 
						local observer = {
 | 
				
			||||||
  		local row = i
 | 
					  		disconnect = disconnect,
 | 
				
			||||||
  		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
 | 
					 | 
				
			||||||
   	}
 | 
					   	}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setmetatable(observer, observer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (observer :: any) :: Observer<T...>
 | 
					    return (observer :: any) :: Observer<T...>
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
local function monitors_new<T...>(
 | 
					local function monitors_new<T...>(query: Query<T...>): Observer<T...>
 | 
				
			||||||
	query: Query<T...>,
 | 
					 | 
				
			||||||
	callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
 | 
					 | 
				
			||||||
): Observer<T...>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	query:cached()
 | 
						query:cached()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local world = (query :: Query<T...> & { world: World }).world
 | 
						local world = (query :: Query<T...> & { world: World }).world
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local archetypes = {}
 | 
						local archetypes = {}
 | 
				
			||||||
	local terms = query.ids
 | 
						local terms = query.filter_with :: { jecs.Id<any> }
 | 
				
			||||||
	local first = terms[1]
 | 
						local first = terms[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
 | 
						local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
 | 
				
			||||||
| 
						 | 
					@ -137,47 +133,77 @@ local function monitors_new<T...>(
 | 
				
			||||||
		archetypes[archetype.id] = nil
 | 
							archetypes[archetype.id] = nil
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
	local entity_index = world.entity_index :: any
 | 
						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>(
 | 
						local function emplaced<T, a>(
 | 
				
			||||||
		entity: jecs.Entity<T>,
 | 
							entity: jecs.Entity<T>,
 | 
				
			||||||
		id: jecs.Id<a>,
 | 
							id: jecs.Id<a>,
 | 
				
			||||||
		value: a?
 | 
							value: a?
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
							if callback_added == nil then
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		local r = jecs.entity_index_try_get_fast(
 | 
							local r = jecs.entity_index_try_get_fast(
 | 
				
			||||||
			entity_index, entity :: any) :: jecs.Record
 | 
								entity_index, entity :: any) :: jecs.Record
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		local archetype = r.archetype
 | 
							local archetype = r.archetype
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if archetypes[archetype.id] then
 | 
							if archetypes[archetype.id] then
 | 
				
			||||||
			i += 1
 | 
								callback_added(entity)
 | 
				
			||||||
			entities[i] = entity
 | 
					 | 
				
			||||||
			if callback ~= nil then
 | 
					 | 
				
			||||||
				callback(entity, jecs.OnAdd)
 | 
					 | 
				
			||||||
			end
 | 
					 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local function removed(entity: jecs.Entity, component: jecs.Id)
 | 
						local function removed(entity: jecs.Entity, component: jecs.Id)
 | 
				
			||||||
 | 
							if callback_removed == nil then
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							end
 | 
				
			||||||
		local r = jecs.record(world, entity)
 | 
							local r = jecs.record(world, entity)
 | 
				
			||||||
		if not archetypes[r.archetype.id] then
 | 
							if not archetypes[r.archetype.id] then
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
		local EcsOnRemove = jecs.OnRemove :: jecs.Id
 | 
							callback_removed(entity)
 | 
				
			||||||
		if callback ~= nil then
 | 
					 | 
				
			||||||
			callback(entity, EcsOnRemove)
 | 
					 | 
				
			||||||
		end
 | 
					 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						local cleanup = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, term in terms do
 | 
						for _, term in terms do
 | 
				
			||||||
		if jecs.IS_PAIR(term) then
 | 
							if jecs.IS_PAIR(term) then
 | 
				
			||||||
			term = jecs.ECS_PAIR_FIRST(term)
 | 
								term = jecs.ECS_PAIR_FIRST(term)
 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
		world:added(term, emplaced)
 | 
							local onadded = world:added(term, emplaced)
 | 
				
			||||||
		world:removed(term, removed)
 | 
							local onremoved = world:removed(term, removed)
 | 
				
			||||||
 | 
							table.insert(cleanup, onadded)
 | 
				
			||||||
 | 
							table.insert(cleanup, onremoved)
 | 
				
			||||||
 	end
 | 
					 	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()
 | 
					 	local function disconnect()
 | 
				
			||||||
  		table.remove(observers_on_create, table.find(
 | 
					  		table.remove(observers_on_create, table.find(
 | 
				
			||||||
	  		observers_on_create,
 | 
						  		observers_on_create,
 | 
				
			||||||
| 
						 | 
					@ -188,30 +214,28 @@ local function monitors_new<T...>(
 | 
				
			||||||
	      	observers_on_delete,
 | 
						      	observers_on_delete,
 | 
				
			||||||
	       	observer_on_delete
 | 
						       	observer_on_delete
 | 
				
			||||||
       	))
 | 
					       	))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							table.clear(archetypes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, disconnect in cleanup do
 | 
				
			||||||
 | 
								disconnect()
 | 
				
			||||||
 | 
							end
 | 
				
			||||||
   	end
 | 
					   	end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    local function iter()
 | 
					    local function monitor_added(callback)
 | 
				
			||||||
  		local row = i
 | 
							callback_added = callback
 | 
				
			||||||
  		return function()
 | 
					    end
 | 
				
			||||||
 			if row == 0 then
 | 
					
 | 
				
			||||||
     			i = 0
 | 
					    local function monitor_removed(callback)
 | 
				
			||||||
     			table.clear(entities)
 | 
					    	callback_removed = callback
 | 
				
			||||||
      		end
 | 
					 | 
				
			||||||
           	local entity = entities[row]
 | 
					 | 
				
			||||||
           	row -= 1
 | 
					 | 
				
			||||||
           	return entity
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	local observer = {
 | 
						local observer = {
 | 
				
			||||||
  		disconnect = disconnect,
 | 
					  		disconnect = disconnect,
 | 
				
			||||||
	   	entities = entities,
 | 
					    	added = monitor_added,
 | 
				
			||||||
	   	__iter = iter,
 | 
					     	removed = monitor_removed
 | 
				
			||||||
       	iter = iter
 | 
					 | 
				
			||||||
   	}
 | 
					   	}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setmetatable(observer, observer)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (observer :: any) :: Observer<T...>
 | 
					    return (observer :: any) :: Observer<T...>
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,15 +5,42 @@ local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
 | 
				
			||||||
local FOCUS = test.FOCUS
 | 
					local FOCUS = test.FOCUS
 | 
				
			||||||
local ob = require("@addons/ob")
 | 
					local ob = require("@addons/ob")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
TEST("addons/observers", function()
 | 
					TEST("addons/ob", function()
 | 
				
			||||||
 | 
					 | 
				
			||||||
	local world = jecs.world()
 | 
						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"
 | 
						do CASE "monitors should accept pairs"
 | 
				
			||||||
		local A = world:component()
 | 
							local A = world:component()
 | 
				
			||||||
		local B = world:component()
 | 
							local B = world:component()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		local c = 1
 | 
							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
 | 
								c += 1
 | 
				
			||||||
		end)
 | 
							end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +51,7 @@ TEST("addons/observers", function()
 | 
				
			||||||
		world:remove(child, jecs.pair(A, B))
 | 
							world:remove(child, jecs.pair(A, B))
 | 
				
			||||||
		CHECK(c == 3)
 | 
							CHECK(c == 3)
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	do CASE "Ensure ordering between signals and observers"
 | 
						do CASE "Ensure ordering between signals and observers"
 | 
				
			||||||
		local A = world:component()
 | 
							local A = world:component()
 | 
				
			||||||
		local B = world:component()
 | 
							local B = world:component()
 | 
				
			||||||
| 
						 | 
					@ -78,7 +106,9 @@ TEST("addons/observers", function()
 | 
				
			||||||
			count += 1
 | 
								count += 1
 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ob.monitor(world:query(A), counter)
 | 
							local monitor = ob.monitor(world:query(A))
 | 
				
			||||||
 | 
							monitor.added(counter)
 | 
				
			||||||
 | 
							monitor.removed(counter)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		local e = world:entity()
 | 
							local e = world:entity()
 | 
				
			||||||
		world:set(e, A, false)
 | 
							world:set(e, A, false)
 | 
				
			||||||
		Loading…
	
		Reference in a new issue