mirror of
				https://github.com/Ukendio/jecs.git
				synced 2025-10-30 16:59:17 +00:00 
			
		
		
		
	Add tests for relations
This commit is contained in:
		
							parent
							
								
									77c7a862ea
								
							
						
					
					
						commit
						2bff9b4551
					
				
					 3 changed files with 205 additions and 306 deletions
				
			
		
							
								
								
									
										325
									
								
								lib/init.lua
									
									
									
									
									
								
							
							
						
						
									
										325
									
								
								lib/init.lua
									
									
									
									
									
								
							|  | @ -199,27 +199,168 @@ function World.new() | |||
| 	return self | ||||
| end | ||||
| 
 | ||||
| local function emit(world, eventDescription) | ||||
| 	local event = eventDescription.event | ||||
| local FLAGS_PAIR = 0x8 | ||||
| 
 | ||||
| 	table.insert(world.hooks[event], { | ||||
| 		archetype = eventDescription.archetype; | ||||
| 		ids = eventDescription.ids; | ||||
| 		offset = eventDescription.offset; | ||||
| 		otherArchetype = eventDescription.otherArchetype; | ||||
| 	}) | ||||
| local function addFlags(flags)  | ||||
|     local typeFlags = 0x0 | ||||
|     if flags.isPair then | ||||
|         typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. | ||||
|     end | ||||
|     if false then | ||||
|         typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true | ||||
|     end | ||||
|     if false then | ||||
|         typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true | ||||
|     end | ||||
|     if false then | ||||
|         typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. | ||||
|     end | ||||
| 
 | ||||
|     return typeFlags | ||||
| end | ||||
| 
 | ||||
| local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) | ||||
| 	if #added > 0 then | ||||
| 		emit(world, { | ||||
| 			archetype = archetype; | ||||
| 			event = ON_ADD; | ||||
| 			ids = added; | ||||
| 			offset = row; | ||||
| 			otherArchetype = otherArchetype; | ||||
| 		}) | ||||
| local ECS_ID_FLAGS_MASK = 0x10 | ||||
| 
 | ||||
| -- ECS_ENTITY_MASK               (0xFFFFFFFFull << 28) | ||||
| local ECS_ENTITY_MASK = bit32.lshift(1, 24) | ||||
| 
 | ||||
| -- ECS_GENERATION_MASK           (0xFFFFull << 24) | ||||
| local ECS_GENERATION_MASK = bit32.lshift(1, 16) | ||||
| 
 | ||||
| local function newId(source: number, target: number)  | ||||
|     local e = source * 2^28 + target * ECS_ID_FLAGS_MASK | ||||
|     return e | ||||
| end | ||||
| 
 | ||||
| local function isPair(e: number)  | ||||
|     return (e % 2^4) // FLAGS_PAIR ~= 0 | ||||
| end | ||||
| 
 | ||||
| function separate(entity: number) | ||||
|     local _typeFlags = entity % 0x10 | ||||
|     entity //= ECS_ID_FLAGS_MASK | ||||
|     return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags | ||||
| end | ||||
| 
 | ||||
| -- HIGH 24 bits LOW 24 bits | ||||
| local function ECS_GENERATION(e: i53) | ||||
|     e //= 0x10 | ||||
|     return e % ECS_GENERATION_MASK | ||||
| end | ||||
| 
 | ||||
| local function ECS_ID(e: i53)  | ||||
|     e //= 0x10 | ||||
|     return e // ECS_ENTITY_MASK | ||||
| end | ||||
| 
 | ||||
| local function ECS_GENERATION_INC(e: i53) | ||||
|     local id, generation, flags = separate(e)     | ||||
| 
 | ||||
|     return newId(id, generation + 1) + flags | ||||
| end | ||||
| 
 | ||||
| -- gets the high ID | ||||
| local function ECS_PAIR_FIRST(entity: i53): i24 | ||||
|     entity //= 0x10 | ||||
|     local first = entity % ECS_ENTITY_MASK | ||||
|     return first | ||||
| end | ||||
| 
 | ||||
| -- gets the low ID | ||||
| local ECS_PAIR_SECOND = ECS_ID | ||||
| 
 | ||||
| local function ECS_PAIR(source: number, target: number) | ||||
|     local id = newId(ECS_PAIR_SECOND(target), ECS_PAIR_SECOND(source)) + addFlags({ isPair = true }) | ||||
|     return id | ||||
| end | ||||
| 
 | ||||
| local function getAlive(entityIndex, id)  | ||||
|     return entityIndex.dense[id] | ||||
| end | ||||
| 
 | ||||
| local function ecs_pair_first(entityIndex, e)  | ||||
|     assert(isPair(e)) | ||||
|     return getAlive(entityIndex, ECS_PAIR_FIRST(e)) | ||||
| end | ||||
| local function ecs_pair_second(entityIndex, e)  | ||||
|     assert(isPair(e)) | ||||
|     return getAlive(entityIndex, ECS_PAIR_SECOND(e)) | ||||
| end | ||||
| 
 | ||||
| function World.component(world: World) | ||||
| 	local componentId = world.nextComponentId + 1 | ||||
| 	if componentId > HI_COMPONENT_ID then | ||||
| 		-- IDs are partitioned into ranges because component IDs are not nominal, | ||||
| 		-- so it needs to error when IDs intersect into the entity range. | ||||
| 		error("Too many components, consider using world:entity() instead to create components.") | ||||
| 	end | ||||
| 	world.nextComponentId = componentId | ||||
| 	return componentId | ||||
| end | ||||
| 
 | ||||
| function World.entity(world: World) | ||||
| 	local nextEntityId = world.nextEntityId + 1 | ||||
| 	world.nextEntityId = nextEntityId | ||||
| 	local index = nextEntityId + REST | ||||
| 	local id = newId(index, 0) | ||||
| 	local entityIndex = world.entityIndex | ||||
| 	entityIndex.sparse[id] = { | ||||
| 		dense = index | ||||
| 	} :: Record | ||||
| 	entityIndex.dense[index] = id | ||||
| 
 | ||||
| 	return id | ||||
| end | ||||
| 
 | ||||
| -- should reuse this logic in World.set instead of swap removing in transition archetype | ||||
| local function destructColumns(columns, count, row)  | ||||
| 	if row == count then  | ||||
| 		for _, column in columns do  | ||||
| 			column[count] = nil | ||||
| 		end | ||||
| 	else | ||||
| 		for _, column in columns do  | ||||
| 			column[row] = column[count] | ||||
| 			column[count] = nil | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
| 
 | ||||
| local function archetypeDelete(entityIndex, record: Record, entityId: i53, destruct: boolean)  | ||||
| 	local sparse, dense = entityIndex.sparse, entityIndex.dense | ||||
| 	local archetype = record.archetype | ||||
| 	local row = record.row | ||||
| 	local entities = archetype.entities | ||||
| 	local last = #entities | ||||
| 
 | ||||
| 	local entityToMove = entities[last] | ||||
| 
 | ||||
| 	if row ~= last then  | ||||
| 		dense[record.dense] = entityToMove | ||||
| 		sparse[entityToMove] = record | ||||
| 	end | ||||
| 
 | ||||
| 	sparse[entityId] = nil | ||||
| 	dense[#dense] = nil | ||||
| 
 | ||||
| 	entities[row], entities[last] = entities[last], nil | ||||
| 
 | ||||
| 	local columns = archetype.columns | ||||
| 
 | ||||
| 	if not destruct then  | ||||
| 		return | ||||
| 	end | ||||
| 
 | ||||
| 	destructColumns(columns, last, row) | ||||
| end | ||||
| 
 | ||||
| function World.delete(world: World, entityId: i53)  | ||||
| 	local entityIndex = world.entityIndex | ||||
| 	local record = entityIndex.sparse[entityId] | ||||
| 	if not record then  | ||||
| 		return | ||||
| 	end | ||||
| 	archetypeDelete(entityIndex, record, entityId, true) | ||||
| end | ||||
| 
 | ||||
| export type World = typeof(World.new()) | ||||
|  | @ -300,22 +441,8 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet | |||
| 	return add | ||||
| end | ||||
| 
 | ||||
| local function ensureRecord(entityIndex: EntityIndex, entityId: i53): Record | ||||
| 	local sparse = entityIndex.sparse | ||||
| 	local dense = entityIndex.dense | ||||
| 	local page = sparse[entityId] | ||||
| 	if not page then | ||||
| 		local i = #dense + 1 | ||||
| 		page = { dense = i } :: Record | ||||
| 		sparse[entityId] = page | ||||
| 		dense[i] = entityId | ||||
| 	end | ||||
| 
 | ||||
| 	return page | ||||
| end | ||||
| 
 | ||||
| function World.set(world: World, entityId: i53, componentId: i53, data: unknown) | ||||
| 	local record = ensureRecord(world.entityIndex, entityId) | ||||
| 	local record = world.entityIndex.sparse[entityId] | ||||
| 	local from = record.archetype | ||||
| 	local to = archetypeTraverseAdd(world, componentId, from) | ||||
| 
 | ||||
|  | @ -335,7 +462,6 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) | |||
| 		if #to.types > 0 then | ||||
| 			-- When there is no previous archetype it should create the archetype | ||||
| 			newEntity(entityId, record, to) | ||||
| 			onNotifyAdd(world, to, from, record.row, {componentId}) | ||||
| 		end | ||||
| 	end | ||||
| 
 | ||||
|  | @ -360,7 +486,7 @@ end | |||
| 
 | ||||
| function World.remove(world: World, entityId: i53, componentId: i53) | ||||
| 	local entityIndex = world.entityIndex | ||||
| 	local record = ensureRecord(entityIndex, entityId) | ||||
| 	local record = entityIndex.sparse[entityId] | ||||
| 	local sourceArchetype = record.archetype | ||||
| 	local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) | ||||
| 
 | ||||
|  | @ -579,129 +705,6 @@ function World.query(world: World, ...: i53): Query | |||
| 	return setmetatable({}, preparedQuery) :: any | ||||
| end | ||||
| 
 | ||||
| function World.component(world: World) | ||||
| 	local componentId = world.nextComponentId + 1 | ||||
| 	if componentId > HI_COMPONENT_ID then | ||||
| 		-- IDs are partitioned into ranges because component IDs are not nominal, | ||||
| 		-- so it needs to error when IDs intersect into the entity range. | ||||
| 		error("Too many components, consider using world:entity() instead to create components.") | ||||
| 	end | ||||
| 	world.nextComponentId = componentId | ||||
| 	return componentId | ||||
| end | ||||
| 
 | ||||
| function World.entity(world: World) | ||||
| 	local nextEntityId = world.nextEntityId + 1 | ||||
| 	world.nextEntityId = nextEntityId | ||||
| 	return nextEntityId + REST | ||||
| end | ||||
| 
 | ||||
| -- should reuse this logic in World.set instead of swap removing in transition archetype | ||||
| local function destructColumns(columns, count, row)  | ||||
| 	if row == count then  | ||||
| 		for _, column in columns do  | ||||
| 			column[count] = nil | ||||
| 		end | ||||
| 	else | ||||
| 		for _, column in columns do  | ||||
| 			column[row] = column[count] | ||||
| 			column[count] = nil | ||||
| 		end | ||||
| 	end | ||||
| end | ||||
| 
 | ||||
| local function archetypeDelete(entityIndex, record: Record, entityId: i53, destruct: boolean)  | ||||
| 	local sparse, dense = entityIndex.sparse, entityIndex.dense | ||||
| 	local archetype = record.archetype | ||||
| 	local row = record.row | ||||
| 	local entities = archetype.entities | ||||
| 	local last = #entities | ||||
| 
 | ||||
| 	local entityToMove = entities[last] | ||||
| 
 | ||||
| 	if row ~= last then  | ||||
| 		dense[record.dense] = entityToMove | ||||
| 		sparse[entityToMove] = record | ||||
| 	end | ||||
| 
 | ||||
| 	sparse[entityId] = nil | ||||
| 	dense[#dense] = nil | ||||
| 
 | ||||
| 	entities[row], entities[last] = entities[last], nil | ||||
| 
 | ||||
| 	local columns = archetype.columns | ||||
| 
 | ||||
| 	if not destruct then  | ||||
| 		return | ||||
| 	end | ||||
| 
 | ||||
| 	destructColumns(columns, last, row) | ||||
| 
 | ||||
| 	 | ||||
| end | ||||
| 
 | ||||
| function World.delete(world: World, entityId: i53)  | ||||
| 	local entityIndex = world.entityIndex | ||||
| 	local record = entityIndex.sparse[entityId] | ||||
| 	if not record then  | ||||
| 		return | ||||
| 	end | ||||
| 	archetypeDelete(entityIndex, record, entityId, true) | ||||
| end | ||||
| 
 | ||||
| function World.observer(world: World, ...) | ||||
| 	local componentIds = {...} | ||||
| 	local idsCount = #componentIds | ||||
| 	local hooks = world.hooks | ||||
| 
 | ||||
| 	return { | ||||
| 		event = function(event) | ||||
| 			local hook = hooks[event] | ||||
| 			hooks[event] = nil | ||||
| 
 | ||||
| 			local last, change | ||||
| 			return function() | ||||
| 				last, change = next(hook, last) | ||||
| 				if not last then | ||||
| 					return | ||||
| 				end | ||||
| 
 | ||||
| 				local matched = false | ||||
| 				local ids = change.ids | ||||
| 
 | ||||
| 				while not matched do | ||||
| 					local skip = false | ||||
| 					for _, id in ids do | ||||
| 						if not table.find(componentIds, id) then | ||||
| 							skip = true | ||||
| 							break | ||||
| 						end | ||||
| 					end | ||||
| 
 | ||||
| 					if skip then | ||||
| 						last, change = next(hook, last) | ||||
| 						ids = change.ids | ||||
| 						continue | ||||
| 					end | ||||
| 
 | ||||
| 					matched = true | ||||
| 				end | ||||
| 
 | ||||
| 				local queryOutput = table.create(idsCount) | ||||
| 				local row = change.offset | ||||
| 				local archetype = change.archetype | ||||
| 				local columns = archetype.columns | ||||
| 				local archetypeRecords = archetype.records | ||||
| 				for index, id in componentIds do | ||||
| 					queryOutput[index] = columns[archetypeRecords[id]][row] | ||||
| 				end | ||||
| 
 | ||||
| 				return archetype.entities[row], unpack(queryOutput, 1, idsCount) | ||||
| 			end | ||||
| 		end; | ||||
| 	} | ||||
| end | ||||
| 
 | ||||
| function World.__iter(world: World): () -> (number?, unknown?) | ||||
| 	local dense = world.entityIndex.dense | ||||
| 	local sparse = world.entityIndex.sparse | ||||
|  | @ -740,4 +743,12 @@ return table.freeze({ | |||
| 	ON_ADD = ON_ADD; | ||||
| 	ON_REMOVE = ON_REMOVE; | ||||
| 	ON_SET = ON_SET; | ||||
| 	ECS_ID = ECS_ID, | ||||
| 	IS_PAIR = isPair, | ||||
| 	ECS_PAIR = ECS_PAIR, | ||||
| 	ECS_GENERATION = ECS_GENERATION, | ||||
| 	ECS_GENERATION_INC = ECS_GENERATION_INC, | ||||
| 	getAlive = getAlive, | ||||
| 	ecs_pair_first = ecs_pair_first, | ||||
| 	ecs_pair_second = ecs_pair_second | ||||
| }) | ||||
|  |  | |||
							
								
								
									
										148
									
								
								tests/test1.lua
									
									
									
									
									
								
							
							
						
						
									
										148
									
								
								tests/test1.lua
									
									
									
									
									
								
							|  | @ -1,148 +0,0 @@ | |||
| local testkit = require("../testkit") | ||||
| local jecs = require("../lib/init") | ||||
| 
 | ||||
| local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() | ||||
| 
 | ||||
| local N = 10 | ||||
| 
 | ||||
| TEST("world:query", function()  | ||||
|     do CASE "should query all matching entities" | ||||
| 
 | ||||
|         local world = jecs.World.new() | ||||
|         local A = world:component() | ||||
|         local B = world:component() | ||||
| 
 | ||||
|         local entities = {} | ||||
|         for i = 1, N do | ||||
|             local id = world:entity() | ||||
| 
 | ||||
| 
 | ||||
|             world:set(id, A, true) | ||||
|             if i > 5 then world:set(id, B, true) end | ||||
|             entities[i] = id | ||||
|         end | ||||
| 
 | ||||
|         for id in world:query(A) do | ||||
|             table.remove(entities, CHECK(table.find(entities, id))) | ||||
|         end | ||||
| 
 | ||||
|         CHECK(#entities == 0) | ||||
| 
 | ||||
|     end | ||||
| 
 | ||||
|     do CASE "should query all matching entities when irrelevant component is removed" | ||||
| 
 | ||||
|         local world = jecs.World.new() | ||||
|         local A = world:component() | ||||
|         local B = world:component() | ||||
| 
 | ||||
|         local entities = {} | ||||
|         for i = 1, N do | ||||
|             local id = world:entity() | ||||
| 
 | ||||
|             world:set(id, A, true) | ||||
|             world:set(id, B, true) | ||||
|             if i > 5 then world:remove(id, B, true) end | ||||
|             entities[i] = id | ||||
|         end | ||||
| 
 | ||||
|         local added = 0 | ||||
|         for id in world:query(A) do | ||||
|             added += 1 | ||||
|             table.remove(entities, CHECK(table.find(entities, id))) | ||||
|         end | ||||
| 
 | ||||
|         CHECK(added == N) | ||||
|     end | ||||
| 
 | ||||
|     do CASE "should query all entities without B" | ||||
| 
 | ||||
|         local world = jecs.World.new() | ||||
|         local A = world:component() | ||||
|         local B = world:component() | ||||
| 
 | ||||
|         local entities = {} | ||||
|         for i = 1, N do | ||||
|             local id = world:entity() | ||||
| 
 | ||||
|             world:set(id, A, true) | ||||
|             if i < 5 then | ||||
|                 entities[i] = id | ||||
|             else | ||||
|                 world:set(id, B, true) | ||||
|             end | ||||
|              | ||||
|         end | ||||
| 
 | ||||
|         for id in world:query(A):without(B) do | ||||
|             table.remove(entities, CHECK(table.find(entities, id))) | ||||
|         end | ||||
| 
 | ||||
|         CHECK(#entities == 0) | ||||
| 
 | ||||
|     end | ||||
| 
 | ||||
|     do CASE "should allow setting components in arbitrary order"  | ||||
|         local world = jecs.World.new() | ||||
| 
 | ||||
|         local Health = world:entity() | ||||
|         local Poison = world:component() | ||||
| 
 | ||||
|         local id = world:entity() | ||||
|         world:set(id, Poison, 5) | ||||
|         world:set(id, Health, 50) | ||||
| 
 | ||||
|         CHECK(world:get(id, Poison) == 5) | ||||
|     end | ||||
| 
 | ||||
|     do CASE "Should allow deleting components"  | ||||
|         local world = jecs.World.new() | ||||
| 
 | ||||
|         local Health = world:entity() | ||||
|         local Poison = world:component() | ||||
| 
 | ||||
|         local id = world:entity() | ||||
|         world:set(id, Poison, 5) | ||||
|         world:set(id, Health, 50) | ||||
|         world:delete(id) | ||||
| 
 | ||||
|         CHECK(world:get(id, Poison) == nil) | ||||
|         CHECK(world:get(id, Health) == nil) | ||||
|     end | ||||
| 
 | ||||
|     do CASE "Should allow iterating the whole world"  | ||||
|         local world = jecs.World.new() | ||||
| 
 | ||||
|         local A, B = world:entity(), world:entity() | ||||
| 
 | ||||
| 			local eA = world:entity() | ||||
| 			world:set(eA, A, true) | ||||
| 			local eB = world:entity() | ||||
| 			world:set(eB, B, true) | ||||
| 			local eAB = world:entity() | ||||
| 			world:set(eAB, A, true) | ||||
| 			world:set(eAB, B, true) | ||||
| 
 | ||||
| 			local count = 0 | ||||
| 			for id, data in world do | ||||
| 				count += 1 | ||||
| 				if id == eA then | ||||
| 					CHECK(data[A] == true) | ||||
|                     CHECK(data[B] == nil) | ||||
| 				elseif id == eB then | ||||
|                     CHECK(data[B] == true) | ||||
|                     CHECK(data[A] == nil) | ||||
| 				elseif id == eAB then | ||||
| 					CHECK(data[A] == true) | ||||
| 					CHECK(data[B] == true) | ||||
| 				else | ||||
| 					error("unknown entity", id) | ||||
| 				end | ||||
| 			end | ||||
| 
 | ||||
| 			CHECK(count == 3) | ||||
|     end | ||||
| 
 | ||||
| end) | ||||
| 
 | ||||
| FINISH() | ||||
|  | @ -1,5 +1,12 @@ | |||
| local testkit = require("../testkit") | ||||
| local jecs = require("../lib/init") | ||||
| local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION | ||||
| local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC | ||||
| local IS_PAIR = jecs.IS_PAIR | ||||
| local ECS_PAIR = jecs.ECS_PAIR | ||||
| local getAlive = jecs.getAlive | ||||
| local ecs_pair_first = jecs.ecs_pair_first | ||||
| local ecs_pair_second = jecs.ecs_pair_second | ||||
| 
 | ||||
| local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() | ||||
| 
 | ||||
|  | @ -127,7 +134,7 @@ TEST("world", function() | |||
|         CHECK(world:get(id, Poison) == 5) | ||||
|     end | ||||
| 
 | ||||
|     do CASE "Should allow deleting components"  | ||||
|     do CASE "should allow deleting components"  | ||||
|         local world = jecs.World.new() | ||||
| 
 | ||||
|         local Health = world:entity() | ||||
|  | @ -149,6 +156,35 @@ TEST("world", function() | |||
| 
 | ||||
|     end | ||||
| 
 | ||||
|     do CASE "should increment generation"  | ||||
|         local world = jecs.World.new() | ||||
|         local e = world:entity() | ||||
|         local REST = 256 + 4 | ||||
|         CHECK(ECS_ID(e) == 1 + REST) | ||||
|         CHECK(ECS_GENERATION(e) == 0) -- 0 | ||||
|         e = ECS_GENERATION_INC(e)  | ||||
|         CHECK(ECS_GENERATION(e) == 1) -- 1 | ||||
|     end | ||||
| 
 | ||||
|     do CASE "relations"  | ||||
|         local world = jecs.World.new() | ||||
|         local _e = world:entity() | ||||
|         local e2 = world:entity() | ||||
|         local e3 = world:entity() | ||||
|         local REST = 256 + 4 | ||||
|         CHECK(ECS_ID(e2) == 2 + REST) | ||||
|         CHECK(ECS_ID(e3) == 3 + REST) | ||||
|         CHECK(ECS_GENERATION(e2) == 0)  | ||||
|         CHECK(ECS_GENERATION(e3) == 0)  | ||||
| 
 | ||||
|         CHECK(IS_PAIR(world:entity()) == false) | ||||
| 
 | ||||
|         local pair = ECS_PAIR(e2, e3) | ||||
|         CHECK(IS_PAIR(pair) == true) | ||||
|         CHECK(ecs_pair_first(world.entityIndex, pair) == e2) | ||||
|         CHECK(ecs_pair_second(world.entityIndex, pair) == e3) | ||||
|     end | ||||
| 
 | ||||
| end) | ||||
| 
 | ||||
| FINISH() | ||||
		Loading…
	
		Reference in a new issue