mirror of
				https://github.com/Ukendio/jecs.git
				synced 2025-11-04 02:49:18 +00:00 
			
		
		
		
	Cleanup codebase
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	
This commit is contained in:
		
							parent
							
								
									a39fc8d0a2
								
							
						
					
					
						commit
						cc7daa6a06
					
				
					 9 changed files with 923 additions and 507 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -64,5 +64,4 @@ drafts/
 | 
			
		|||
.vitepress/dist
 | 
			
		||||
 | 
			
		||||
# Luau tools
 | 
			
		||||
/tools
 | 
			
		||||
profile.*
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -165,7 +165,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to
 | 
			
		|||
 | 
			
		||||
-   Separates ranges for components and entity IDs.
 | 
			
		||||
 | 
			
		||||
    -   IDs created with `world:component()` will promote array lookups rather than map lookups in the `componentIndex` which is a significant boost
 | 
			
		||||
    -   IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` which is a significant boost
 | 
			
		||||
 | 
			
		||||
-   No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals
 | 
			
		||||
    -   This was an issue with the iterator being invalidated when you move an entity to a different archetype.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										196
									
								
								jecs.luau
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								jecs.luau
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -275,7 +275,7 @@ local function query_match(query, archetype: Archetype)
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
local function find_observers(world: World, event, component): { Observer }?
 | 
			
		||||
	local cache = world.observerable[event]
 | 
			
		||||
	local cache = world.observable[event]
 | 
			
		||||
	if not cache then
 | 
			
		||||
		return nil
 | 
			
		||||
	end
 | 
			
		||||
| 
						 | 
				
			
			@ -471,7 +471,7 @@ local function world_target(world: World, entity: i53, relation: i24, index: num
 | 
			
		|||
		return nil
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)]
 | 
			
		||||
	local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)]
 | 
			
		||||
	if not idr then
 | 
			
		||||
		return nil
 | 
			
		||||
	end
 | 
			
		||||
| 
						 | 
				
			
			@ -502,8 +502,8 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
local function id_record_ensure(world: World, id: number): IdRecord
 | 
			
		||||
	local componentIndex = world.componentIndex
 | 
			
		||||
	local idr: IdRecord = componentIndex[id]
 | 
			
		||||
	local component_index = world.component_index
 | 
			
		||||
	local idr: IdRecord = component_index[id]
 | 
			
		||||
 | 
			
		||||
	if not idr then
 | 
			
		||||
		local flags = ECS_ID_MASK
 | 
			
		||||
| 
						 | 
				
			
			@ -550,7 +550,7 @@ local function id_record_ensure(world: World, id: number): IdRecord
 | 
			
		|||
			},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		componentIndex[id] = idr
 | 
			
		||||
		component_index[id] = idr
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	return idr
 | 
			
		||||
| 
						 | 
				
			
			@ -575,8 +575,8 @@ local function archetype_append_to_records(
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype
 | 
			
		||||
	local archetype_id = (world.nextArchetypeId :: number) + 1
 | 
			
		||||
	world.nextArchetypeId = archetype_id
 | 
			
		||||
	local archetype_id = (world.max_archetype_id :: number) + 1
 | 
			
		||||
	world.max_archetype_id = archetype_id
 | 
			
		||||
 | 
			
		||||
	local length = #id_types
 | 
			
		||||
	local columns = (table.create(length) :: any) :: { Column }
 | 
			
		||||
| 
						 | 
				
			
			@ -632,7 +632,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?)
 | 
			
		|||
		end
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	world.archetypeIndex[ty] = archetype
 | 
			
		||||
	world.archetype_index[ty] = archetype
 | 
			
		||||
	world.archetypes[archetype_id] = archetype
 | 
			
		||||
 | 
			
		||||
	return archetype
 | 
			
		||||
| 
						 | 
				
			
			@ -652,7 +652,7 @@ local function archetype_ensure(world: World, id_types): Archetype
 | 
			
		|||
	end
 | 
			
		||||
 | 
			
		||||
	local ty = hash(id_types)
 | 
			
		||||
	local archetype = world.archetypeIndex[ty]
 | 
			
		||||
	local archetype = world.archetype_index[ty]
 | 
			
		||||
	if archetype then
 | 
			
		||||
		return archetype
 | 
			
		||||
	end
 | 
			
		||||
| 
						 | 
				
			
			@ -814,7 +814,7 @@ local function world_add(world: World, entity: i53, id: i53): ()
 | 
			
		|||
		end
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local idr = world.componentIndex[id]
 | 
			
		||||
	local idr = world.component_index[id]
 | 
			
		||||
	local on_add = idr.hooks.on_add
 | 
			
		||||
 | 
			
		||||
	if on_add then
 | 
			
		||||
| 
						 | 
				
			
			@ -831,15 +831,10 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
 | 
			
		|||
 | 
			
		||||
	local from: Archetype = record.archetype
 | 
			
		||||
	local to: Archetype = archetype_traverse_add(world, id, from)
 | 
			
		||||
	local idr = world.componentIndex[id]
 | 
			
		||||
	local flags = idr.flags
 | 
			
		||||
	local is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
 | 
			
		||||
	local idr = world.component_index[id]
 | 
			
		||||
	local idr_hooks = idr.hooks
 | 
			
		||||
 | 
			
		||||
	if from == to then
 | 
			
		||||
		if is_tag then
 | 
			
		||||
			return
 | 
			
		||||
		end
 | 
			
		||||
		-- If the archetypes are the same it can avoid moving the entity
 | 
			
		||||
		-- and just set the data directly.
 | 
			
		||||
		local tr = to.records[id]
 | 
			
		||||
| 
						 | 
				
			
			@ -868,10 +863,6 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
 | 
			
		|||
		on_add(entity)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	if is_tag then
 | 
			
		||||
		return
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local tr = to.records[id]
 | 
			
		||||
	local column = to.columns[tr.column]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -884,15 +875,15 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
local function world_component(world: World): i53
 | 
			
		||||
	local componentId = (world.nextComponentId :: number) + 1
 | 
			
		||||
	if componentId > HI_COMPONENT_ID then
 | 
			
		||||
	local id = (world.max_component_id :: number) + 1
 | 
			
		||||
	if id > 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
 | 
			
		||||
	world.max_component_id = id
 | 
			
		||||
 | 
			
		||||
	return componentId
 | 
			
		||||
	return id
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function world_remove(world: World, entity: i53, id: i53)
 | 
			
		||||
| 
						 | 
				
			
			@ -909,7 +900,7 @@ local function world_remove(world: World, entity: i53, id: i53)
 | 
			
		|||
	local to = archetype_traverse_remove(world, id, from)
 | 
			
		||||
 | 
			
		||||
	if from and not (from == to) then
 | 
			
		||||
		local idr = world.componentIndex[id]
 | 
			
		||||
		local idr = world.component_index[id]
 | 
			
		||||
		local on_remove = idr.hooks.on_remove
 | 
			
		||||
		if on_remove then
 | 
			
		||||
			on_remove(entity)
 | 
			
		||||
| 
						 | 
				
			
			@ -937,28 +928,27 @@ local function archetype_fast_delete(columns: { Column }, column_count: number,
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?)
 | 
			
		||||
	local entityIndex = world.entity_index
 | 
			
		||||
	local entity_index = world.entity_index
 | 
			
		||||
	local component_index = world.component_index
 | 
			
		||||
	local columns = archetype.columns
 | 
			
		||||
	local id_types = archetype.types
 | 
			
		||||
	local entities = archetype.entities
 | 
			
		||||
	local column_count = #entities
 | 
			
		||||
	local last = #entities
 | 
			
		||||
	local move = entities[last]
 | 
			
		||||
	local delete = entities[row]
 | 
			
		||||
	entities[row] = move
 | 
			
		||||
	entities[last] = nil :: any
 | 
			
		||||
	-- We assume first that the entity is the last in the archetype
 | 
			
		||||
	local delete = move
 | 
			
		||||
 | 
			
		||||
	if row ~= last then
 | 
			
		||||
		-- TODO: should be "entity_index_sparse_get(entityIndex, move)"
 | 
			
		||||
		local record_to_move = entity_index_try_get_any(entityIndex, move)
 | 
			
		||||
		local record_to_move = entity_index_try_get_any(entity_index, move)
 | 
			
		||||
		if record_to_move then
 | 
			
		||||
			record_to_move.row = row
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		entities[row] = move
 | 
			
		||||
		delete = entities[row]
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	-- TODO: if last == 0 then deactivate table
 | 
			
		||||
 | 
			
		||||
	local component_index = world.componentIndex
 | 
			
		||||
	for _, id in id_types do
 | 
			
		||||
		local idr = component_index[id]
 | 
			
		||||
		local on_remove = idr.hooks.on_remove
 | 
			
		||||
| 
						 | 
				
			
			@ -967,6 +957,8 @@ local function archetype_delete(world: World, archetype: Archetype, row: number,
 | 
			
		|||
		end
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	entities[last] = nil :: any
 | 
			
		||||
 | 
			
		||||
	if row == last then
 | 
			
		||||
		archetype_fast_delete_last(columns, column_count, id_types, delete)
 | 
			
		||||
	else
 | 
			
		||||
| 
						 | 
				
			
			@ -1048,11 +1040,11 @@ local function archetype_destroy(world: World, archetype: Archetype)
 | 
			
		|||
		return
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local component_index = world.componentIndex
 | 
			
		||||
	local component_index = world.component_index
 | 
			
		||||
	archetype_clear_edges(archetype)
 | 
			
		||||
	local archetype_id = archetype.id
 | 
			
		||||
	world.archetypes[archetype_id] = nil :: any
 | 
			
		||||
	world.archetypeIndex[archetype.type] = nil :: any
 | 
			
		||||
	world.archetype_index[archetype.type] = nil :: any
 | 
			
		||||
	local records = archetype.records
 | 
			
		||||
 | 
			
		||||
	for id in records do
 | 
			
		||||
| 
						 | 
				
			
			@ -1096,7 +1088,7 @@ local function world_cleanup(world: World)
 | 
			
		|||
	end
 | 
			
		||||
 | 
			
		||||
	world.archetypes = new_archetypes
 | 
			
		||||
	world.archetypeIndex = new_archetype_map
 | 
			
		||||
	world.archetype_index = new_archetype_map
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local world_delete: (world: World, entity: i53, destruct: boolean?) -> ()
 | 
			
		||||
| 
						 | 
				
			
			@ -1118,7 +1110,7 @@ do
 | 
			
		|||
		end
 | 
			
		||||
 | 
			
		||||
		local delete = entity
 | 
			
		||||
		local component_index = world.componentIndex
 | 
			
		||||
		local component_index = world.component_index
 | 
			
		||||
		local archetypes: Archetypes = world.archetypes
 | 
			
		||||
		local tgt = ECS_PAIR(EcsWildcard, delete)
 | 
			
		||||
		local idr_t = component_index[tgt]
 | 
			
		||||
| 
						 | 
				
			
			@ -1135,6 +1127,8 @@ do
 | 
			
		|||
					for i = n, 1, -1 do
 | 
			
		||||
						world_delete(world, entities[i])
 | 
			
		||||
					end
 | 
			
		||||
 | 
			
		||||
					archetype_destroy(world, idr_archetype)
 | 
			
		||||
				end
 | 
			
		||||
			else
 | 
			
		||||
				for archetype_id in idr.cache do
 | 
			
		||||
| 
						 | 
				
			
			@ -1144,10 +1138,7 @@ do
 | 
			
		|||
					for i = n, 1, -1 do
 | 
			
		||||
						world_remove(world, entities[i], delete)
 | 
			
		||||
					end
 | 
			
		||||
				end
 | 
			
		||||
 | 
			
		||||
				for archetype_id in idr.cache do
 | 
			
		||||
					local idr_archetype = archetypes[archetype_id]
 | 
			
		||||
					archetype_destroy(world, idr_archetype)
 | 
			
		||||
				end
 | 
			
		||||
			end
 | 
			
		||||
| 
						 | 
				
			
			@ -1164,6 +1155,8 @@ do
 | 
			
		|||
					table.insert(children, child)
 | 
			
		||||
				end
 | 
			
		||||
 | 
			
		||||
				local n = #children
 | 
			
		||||
 | 
			
		||||
				for _, id in idr_t_types do
 | 
			
		||||
					if not ECS_IS_PAIR(id) then
 | 
			
		||||
						continue
 | 
			
		||||
| 
						 | 
				
			
			@ -1174,22 +1167,30 @@ do
 | 
			
		|||
						local flags = id_record.flags
 | 
			
		||||
						local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE)
 | 
			
		||||
						if flags_delete_mask ~= 0 then
 | 
			
		||||
							for _, child in children do
 | 
			
		||||
								-- Cascade deletions of it has Delete as component trait
 | 
			
		||||
								world_delete(world, child, destruct)
 | 
			
		||||
							for i = n, 1, -1 do
 | 
			
		||||
								world_delete(world, children[i])
 | 
			
		||||
							end
 | 
			
		||||
							break
 | 
			
		||||
						else
 | 
			
		||||
							local on_remove = id_record.hooks.on_remove
 | 
			
		||||
							local to = archetype_traverse_remove(world, id, idr_t_archetype)
 | 
			
		||||
							if on_remove then
 | 
			
		||||
								for _, child in children do
 | 
			
		||||
									on_remove(child)
 | 
			
		||||
								if to then
 | 
			
		||||
									for i = n, 1, -1 do
 | 
			
		||||
										local child = children[i]
 | 
			
		||||
										on_remove(children[i])
 | 
			
		||||
										local r = entity_index_try_get_fast(entity_index, child) :: Record
 | 
			
		||||
										entity_move(entity_index, child, r, to)
 | 
			
		||||
									end
 | 
			
		||||
								else
 | 
			
		||||
								for _, child in children do
 | 
			
		||||
									for i = n, 1, -1 do
 | 
			
		||||
										local child = children[i]
 | 
			
		||||
										on_remove(child)
 | 
			
		||||
									end
 | 
			
		||||
								end
 | 
			
		||||
							elseif to then
 | 
			
		||||
								for i = n, 1, -1 do
 | 
			
		||||
									local child = children[i]
 | 
			
		||||
									local r = entity_index_try_get_fast(entity_index, child) :: Record
 | 
			
		||||
									entity_move(entity_index, child, r, to)
 | 
			
		||||
								end
 | 
			
		||||
| 
						 | 
				
			
			@ -1608,14 +1609,14 @@ local function query_cached(query: QueryInner)
 | 
			
		|||
	local records: { ArchetypeRecord }
 | 
			
		||||
	local archetypes = query.compatible_archetypes
 | 
			
		||||
 | 
			
		||||
	local world = query.world :: World
 | 
			
		||||
	local world = query.world :: { observable: Observable }
 | 
			
		||||
	-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
 | 
			
		||||
	-- because the event will be emitted for all components of that Archetype.
 | 
			
		||||
	local observerable = world.observerable
 | 
			
		||||
	local on_create_action = observerable[EcsOnArchetypeCreate]
 | 
			
		||||
	local observable = world.observable :: Observable
 | 
			
		||||
	local on_create_action = observable[EcsOnArchetypeCreate]
 | 
			
		||||
	if not on_create_action then
 | 
			
		||||
		on_create_action = {}
 | 
			
		||||
		observerable[EcsOnArchetypeCreate] = on_create_action
 | 
			
		||||
		observable[EcsOnArchetypeCreate] = on_create_action
 | 
			
		||||
	end
 | 
			
		||||
	local query_cache_on_create = on_create_action[A]
 | 
			
		||||
	if not query_cache_on_create then
 | 
			
		||||
| 
						 | 
				
			
			@ -1623,10 +1624,10 @@ local function query_cached(query: QueryInner)
 | 
			
		|||
		on_create_action[A] = query_cache_on_create
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	local on_delete_action = observerable[EcsOnArchetypeDelete]
 | 
			
		||||
	local on_delete_action = observable[EcsOnArchetypeDelete]
 | 
			
		||||
	if not on_delete_action then
 | 
			
		||||
		on_delete_action = {}
 | 
			
		||||
		observerable[EcsOnArchetypeDelete] = on_delete_action
 | 
			
		||||
		observable[EcsOnArchetypeDelete] = on_delete_action
 | 
			
		||||
	end
 | 
			
		||||
	local query_cache_on_delete = on_delete_action[A]
 | 
			
		||||
	if not query_cache_on_delete then
 | 
			
		||||
| 
						 | 
				
			
			@ -1920,7 +1921,7 @@ local function world_query(world: World, ...)
 | 
			
		|||
	local archetypes = world.archetypes
 | 
			
		||||
 | 
			
		||||
	local idr: IdRecord?
 | 
			
		||||
	local componentIndex = world.componentIndex
 | 
			
		||||
	local component_index = world.component_index
 | 
			
		||||
 | 
			
		||||
	local q = setmetatable({
 | 
			
		||||
		ids = ids,
 | 
			
		||||
| 
						 | 
				
			
			@ -1929,7 +1930,7 @@ local function world_query(world: World, ...)
 | 
			
		|||
	}, Query)
 | 
			
		||||
 | 
			
		||||
	for _, id in ids do
 | 
			
		||||
		local map = componentIndex[id]
 | 
			
		||||
		local map = component_index[id]
 | 
			
		||||
		if not map then
 | 
			
		||||
			return q
 | 
			
		||||
		end
 | 
			
		||||
| 
						 | 
				
			
			@ -1972,7 +1973,7 @@ local function world_query(world: World, ...)
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
local function world_each(world: World, id): () -> ()
 | 
			
		||||
	local idr = world.componentIndex[id]
 | 
			
		||||
	local idr = world.component_index[id]
 | 
			
		||||
	if not idr then
 | 
			
		||||
		return NOOP
 | 
			
		||||
	end
 | 
			
		||||
| 
						 | 
				
			
			@ -2146,15 +2147,16 @@ function World.new()
 | 
			
		|||
		max_id = 0,
 | 
			
		||||
	}
 | 
			
		||||
	local self = setmetatable({
 | 
			
		||||
		archetypeIndex = {} :: { [string]: Archetype },
 | 
			
		||||
		archetype_index = {} :: { [string]: Archetype },
 | 
			
		||||
		archetypes = {} :: Archetypes,
 | 
			
		||||
		componentIndex = {} :: ComponentIndex,
 | 
			
		||||
		component_index = {} :: ComponentIndex,
 | 
			
		||||
		entity_index = entity_index,
 | 
			
		||||
		nextArchetypeId = 0 :: number,
 | 
			
		||||
		nextComponentId = 0 :: number,
 | 
			
		||||
		nextEntityId = 0 :: number,
 | 
			
		||||
		ROOT_ARCHETYPE = (nil :: any) :: Archetype,
 | 
			
		||||
		observerable = {},
 | 
			
		||||
 | 
			
		||||
		max_archetype_id = 0,
 | 
			
		||||
		max_component_id = 0,
 | 
			
		||||
 | 
			
		||||
		observable = {} :: Observable,
 | 
			
		||||
	}, World) :: any
 | 
			
		||||
 | 
			
		||||
	self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
 | 
			
		||||
| 
						 | 
				
			
			@ -2194,6 +2196,8 @@ function World.new()
 | 
			
		|||
	return self
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
export type Entity<T = nil> = number & { __T: T }
 | 
			
		||||
 | 
			
		||||
export type Id<T = nil> =
 | 
			
		||||
    | Entity<T>
 | 
			
		||||
    | Pair<Entity<T>, Entity>
 | 
			
		||||
| 
						 | 
				
			
			@ -2205,29 +2209,11 @@ export type Pair<P, O> = number & {
 | 
			
		|||
    __O: O,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
-- type function ecs_id_t(entity)
 | 
			
		||||
-- 	local ty = entity:components()[2]
 | 
			
		||||
-- 	local __T = ty:readproperty(types.singleton("__T"))
 | 
			
		||||
-- 	if not __T then
 | 
			
		||||
-- 		return ty:readproperty(types.singleton("__jecs_pair_value"))
 | 
			
		||||
-- 	end
 | 
			
		||||
-- 	return __T
 | 
			
		||||
-- end
 | 
			
		||||
 | 
			
		||||
-- type function ecs_pair_t(first, second)
 | 
			
		||||
-- 	if ecs_id_t(first):is("nil") then
 | 
			
		||||
-- 		return second
 | 
			
		||||
-- 	else
 | 
			
		||||
-- 		return first
 | 
			
		||||
-- 	end
 | 
			
		||||
-- end
 | 
			
		||||
 | 
			
		||||
type Item<T...> = (self: Query<T...>) -> (Entity, T...)
 | 
			
		||||
 | 
			
		||||
export type Entity<T = nil> = number & { __T: T }
 | 
			
		||||
 | 
			
		||||
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type Query<T...> = typeof(setmetatable({}, {
 | 
			
		||||
	__iter = (nil :: any) :: Iter<T...>,
 | 
			
		||||
})) & {
 | 
			
		||||
| 
						 | 
				
			
			@ -2238,30 +2224,31 @@ export type Query<T...> = typeof(setmetatable({}, {
 | 
			
		|||
	cached: (self: Query<T...>) -> Query<T...>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Observer = {
 | 
			
		||||
export type Observer = {
 | 
			
		||||
 	callback: (archetype: Archetype) -> (),
 | 
			
		||||
   	query: QueryInner,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Observable = {
 | 
			
		||||
	[i53]: {
 | 
			
		||||
		[i53]: {
 | 
			
		||||
			{ Observer }
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type World = {
 | 
			
		||||
	archetypeIndex: { [string]: Archetype },
 | 
			
		||||
	archetype_index: { [string]: Archetype },
 | 
			
		||||
	archetypes: Archetypes,
 | 
			
		||||
	componentIndex: ComponentIndex,
 | 
			
		||||
	component_index: ComponentIndex,
 | 
			
		||||
	entity_index: EntityIndex,
 | 
			
		||||
	ROOT_ARCHETYPE: Archetype,
 | 
			
		||||
 | 
			
		||||
	nextComponentId: number,
 | 
			
		||||
	nextEntityId: number,
 | 
			
		||||
	nextArchetypeId: number,
 | 
			
		||||
	max_component_id: number,
 | 
			
		||||
	max_archetype_id: number,
 | 
			
		||||
 | 
			
		||||
	observable: any,
 | 
			
		||||
 | 
			
		||||
	observerable: {
 | 
			
		||||
		[i53]: {
 | 
			
		||||
			[i53]: {
 | 
			
		||||
				{ query: QueryInner, callback: (Archetype) -> () }
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
} & {
 | 
			
		||||
	--- Creates a new entity
 | 
			
		||||
	entity: (self: World) -> Entity,
 | 
			
		||||
	--- Creates a new entity located in the first 256 ids.
 | 
			
		||||
| 
						 | 
				
			
			@ -2312,6 +2299,22 @@ export type World = {
 | 
			
		|||
		& (<A, B, C, D, E, F, G>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>, Id<F>, Id<G>) -> Query<A, B, C, D, E, F, G>)
 | 
			
		||||
		& (<A, B, C, D, E, F, G, H>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>, Id<F>, Id<G>, Id<H>, ...Id<any>) -> Query<A, B, C, D, E, F, G, H>)
 | 
			
		||||
}
 | 
			
		||||
-- type function ecs_id_t(entity)
 | 
			
		||||
-- 	local ty = entity:components()[2]
 | 
			
		||||
-- 	local __T = ty:readproperty(types.singleton("__T"))
 | 
			
		||||
-- 	if not __T then
 | 
			
		||||
-- 		return ty:readproperty(types.singleton("__jecs_pair_value"))
 | 
			
		||||
-- 	end
 | 
			
		||||
-- 	return __T
 | 
			
		||||
-- end
 | 
			
		||||
 | 
			
		||||
-- type function ecs_pair_t(first, second)
 | 
			
		||||
-- 	if ecs_id_t(first):is("nil") then
 | 
			
		||||
-- 		return second
 | 
			
		||||
-- 	else
 | 
			
		||||
-- 		return first
 | 
			
		||||
-- 	end
 | 
			
		||||
-- end
 | 
			
		||||
 | 
			
		||||
return {
 | 
			
		||||
	World = World :: { new: () -> World },
 | 
			
		||||
| 
						 | 
				
			
			@ -2370,4 +2373,7 @@ return {
 | 
			
		|||
	query_with = query_with,
 | 
			
		||||
	query_without = query_without,
 | 
			
		||||
	query_archetypes = query_archetypes,
 | 
			
		||||
	query_match = query_match,
 | 
			
		||||
 | 
			
		||||
	find_observers = find_observers,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										183
									
								
								test/gen.luau
									
									
									
									
									
								
							
							
						
						
									
										183
									
								
								test/gen.luau
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,183 +0,0 @@
 | 
			
		|||
type i53 = number
 | 
			
		||||
type i24 = number
 | 
			
		||||
 | 
			
		||||
type Ty = { i53 }
 | 
			
		||||
type ArchetypeId = number
 | 
			
		||||
 | 
			
		||||
type Column = { any }
 | 
			
		||||
 | 
			
		||||
type Map<K, V> = { [K]: V }
 | 
			
		||||
 | 
			
		||||
type GraphEdge = {
 | 
			
		||||
	from: Archetype,
 | 
			
		||||
	to: Archetype?,
 | 
			
		||||
	prev: GraphEdge?,
 | 
			
		||||
	next: GraphEdge?,
 | 
			
		||||
	id: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GraphEdges = Map<i53, GraphEdge>
 | 
			
		||||
 | 
			
		||||
type GraphNode = {
 | 
			
		||||
	add: GraphEdges,
 | 
			
		||||
	remove: GraphEdges,
 | 
			
		||||
	refs: GraphEdge,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ArchetypeRecord = {
 | 
			
		||||
	count: number,
 | 
			
		||||
	column: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Archetype = {
 | 
			
		||||
	id: number,
 | 
			
		||||
	node: GraphNode,
 | 
			
		||||
	types: Ty,
 | 
			
		||||
	type: string,
 | 
			
		||||
	entities: { number },
 | 
			
		||||
	columns: { Column },
 | 
			
		||||
	records: { ArchetypeRecord },
 | 
			
		||||
}
 | 
			
		||||
type Record = {
 | 
			
		||||
	archetype: Archetype,
 | 
			
		||||
	row: number,
 | 
			
		||||
	dense: i24,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EntityIndex = {
 | 
			
		||||
	dense_array: Map<i24, i53>,
 | 
			
		||||
	sparse_array: Map<i53, Record>,
 | 
			
		||||
	sparse_count: number,
 | 
			
		||||
	alive_count: number,
 | 
			
		||||
	max_id: number,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
local ECS_PAIR_FLAG = 0x8
 | 
			
		||||
local ECS_ID_FLAGS_MASK = 0x10
 | 
			
		||||
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
 | 
			
		||||
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
 | 
			
		||||
 | 
			
		||||
-- HIGH 24 bits LOW 24 bits
 | 
			
		||||
local function ECS_GENERATION(e: i53): i24
 | 
			
		||||
	return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function ECS_COMBINE(source: number, target: number): i53
 | 
			
		||||
	return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function ECS_GENERATION_INC(e: i53)
 | 
			
		||||
	if e > ECS_ENTITY_MASK then
 | 
			
		||||
		local flags = e // ECS_ID_FLAGS_MASK
 | 
			
		||||
		local id = flags // ECS_ENTITY_MASK
 | 
			
		||||
		local generation = flags % ECS_GENERATION_MASK
 | 
			
		||||
		print(generation)
 | 
			
		||||
		return ECS_COMBINE(id, generation + 1)
 | 
			
		||||
	end
 | 
			
		||||
	return ECS_COMBINE(e, 1)
 | 
			
		||||
end
 | 
			
		||||
local function ECS_ENTITY_T_LO(e: i53): i24
 | 
			
		||||
	return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record?
 | 
			
		||||
	local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
 | 
			
		||||
 | 
			
		||||
	if not r or r.dense == 0 then
 | 
			
		||||
		return nil
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	return r
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record?
 | 
			
		||||
	local r = entity_index_try_get_any(entity_index, entity)
 | 
			
		||||
	if r then
 | 
			
		||||
		local r_dense = r.dense
 | 
			
		||||
		if r_dense > entity_index.alive_count then
 | 
			
		||||
			return nil
 | 
			
		||||
		end
 | 
			
		||||
		if entity_index.dense_array[r_dense] ~= entity then
 | 
			
		||||
			return nil
 | 
			
		||||
		end
 | 
			
		||||
	end
 | 
			
		||||
	return r
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function entity_index_get_alive(entity_index: EntityIndex, entity: number): number
 | 
			
		||||
	local r = entity_index_try_get_any(entity_index, entity)
 | 
			
		||||
	if r then
 | 
			
		||||
		return entity_index.dense_array[r.dense]
 | 
			
		||||
	end
 | 
			
		||||
	return 0
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function entity_index_remove(entity_index: EntityIndex, entity: number)
 | 
			
		||||
	local r = entity_index_try_get(entity_index, entity)
 | 
			
		||||
	if not r then
 | 
			
		||||
		return
 | 
			
		||||
	end
 | 
			
		||||
	local dense_array = entity_index.dense_array
 | 
			
		||||
	local index_of_deleted_entity = r.dense
 | 
			
		||||
	local last_entity_alive_at_index = entity_index.alive_count
 | 
			
		||||
	entity_index.alive_count -= 1
 | 
			
		||||
 | 
			
		||||
	local last_alive_entity = dense_array[last_entity_alive_at_index]
 | 
			
		||||
	local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: Record
 | 
			
		||||
	r_swap.dense = index_of_deleted_entity
 | 
			
		||||
	r.archetype = nil :: any
 | 
			
		||||
	r.row = nil :: any
 | 
			
		||||
	r.dense = last_entity_alive_at_index
 | 
			
		||||
 | 
			
		||||
	dense_array[index_of_deleted_entity] = last_alive_entity
 | 
			
		||||
	dense_array[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function entity_index_new_id(entity_index: EntityIndex): i53
 | 
			
		||||
	local dense_array = entity_index.dense_array
 | 
			
		||||
	if entity_index.alive_count ~= #dense_array then
 | 
			
		||||
		entity_index.alive_count += 1
 | 
			
		||||
		local id = dense_array[entity_index.alive_count]
 | 
			
		||||
		return id
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	entity_index.max_id += 1
 | 
			
		||||
	local id = entity_index.max_id
 | 
			
		||||
	entity_index.alive_count += 1
 | 
			
		||||
 | 
			
		||||
	dense_array[entity_index.alive_count] = id
 | 
			
		||||
	entity_index.sparse_array[id] = {
 | 
			
		||||
		dense = entity_index.alive_count,
 | 
			
		||||
	} :: Record
 | 
			
		||||
 | 
			
		||||
	return id
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function entity_index_is_alive(entity_index: EntityIndex, entity: number)
 | 
			
		||||
	return entity_index_try_get(entity_index, entity) ~= nil
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local eidx = {
 | 
			
		||||
	alive_count = 0,
 | 
			
		||||
	max_id = 0,
 | 
			
		||||
	sparse_array = {} :: { Record },
 | 
			
		||||
	sparse_count = 0,
 | 
			
		||||
	dense_array = {} :: { i53 },
 | 
			
		||||
}
 | 
			
		||||
local e1v0 = entity_index_new_id(eidx, "e1v0")
 | 
			
		||||
local e2v0 = entity_index_new_id(eidx, "e2v0")
 | 
			
		||||
local e3v0 = entity_index_new_id(eidx, "e3v0")
 | 
			
		||||
local e4v0 = entity_index_new_id(eidx, "e4v0")
 | 
			
		||||
local e5v0 = entity_index_new_id(eidx, "e5v0")
 | 
			
		||||
 | 
			
		||||
local e6v0 = entity_index_new_id(eidx)
 | 
			
		||||
entity_index_remove(eidx, e6v0)
 | 
			
		||||
local e6v1 = entity_index_new_id(eidx)
 | 
			
		||||
entity_index_remove(eidx, e6v1)
 | 
			
		||||
local e6v2 = entity_index_new_id(eidx)
 | 
			
		||||
print(ECS_ENTITY_T_LO(e6v2), ECS_GENERATION(e6v2))
 | 
			
		||||
 | 
			
		||||
print("-----")
 | 
			
		||||
local e2 = ECS_GENERATION_INC(ECS_GENERATION_INC(269))
 | 
			
		||||
print("-----")
 | 
			
		||||
print(ECS_ENTITY_T_LO(e2), ECS_GENERATION(e2))
 | 
			
		||||
| 
						 | 
				
			
			@ -123,6 +123,7 @@ local function remove(entity)
 | 
			
		|||
	r.dense = last_entity_alive_at_index
 | 
			
		||||
	dense[index_of_deleted_entity] = last_alive_entity
 | 
			
		||||
	dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
 | 
			
		||||
	print("*dellocated", pe(id))
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function alive(e)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -180,6 +180,24 @@ local function CASE(name: string)
 | 
			
		|||
	table.insert(test.cases, case)
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function CHECK_EXPECT_ERR(fn, ...)
 | 
			
		||||
	assert(test, "no active test")
 | 
			
		||||
	local case = test.case
 | 
			
		||||
	if not case then
 | 
			
		||||
		CASE("")
 | 
			
		||||
		case = test.case
 | 
			
		||||
	end
 | 
			
		||||
	assert(case, "no active case")
 | 
			
		||||
	if case.result ~= FAIL then
 | 
			
		||||
		local ok, err = pcall(fn, ...)
 | 
			
		||||
		case.result = if ok then FAIL else PASS
 | 
			
		||||
		if skip then
 | 
			
		||||
			case.result = SKIPPED
 | 
			
		||||
		end
 | 
			
		||||
		case.line = debug.info(stack and stack + 1 or 2, "l")
 | 
			
		||||
	end
 | 
			
		||||
end
 | 
			
		||||
 | 
			
		||||
local function CHECK<T>(value: T, stack: number?): T?
 | 
			
		||||
	assert(test, "no active test")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -509,7 +527,15 @@ end
 | 
			
		|||
 | 
			
		||||
return {
 | 
			
		||||
	test = function()
 | 
			
		||||
		return TEST, CASE, CHECK, FINISH, SKIP, FOCUS
 | 
			
		||||
		return {
 | 
			
		||||
			TEST = TEST,
 | 
			
		||||
			CASE = CASE,
 | 
			
		||||
			CHECK = CHECK,
 | 
			
		||||
			FINISH = FINISH,
 | 
			
		||||
			SKIP = SKIP,
 | 
			
		||||
			FOCUS = FOCUS,
 | 
			
		||||
			CHECK_EXPECT_ERR = CHECK_EXPECT_ERR
 | 
			
		||||
		}
 | 
			
		||||
	end,
 | 
			
		||||
 | 
			
		||||
	benchmark = function()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										331
									
								
								test/tests.luau
									
									
									
									
									
								
							
							
						
						
									
										331
									
								
								test/tests.luau
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -15,7 +15,11 @@ local entity_index_is_alive = jecs.entity_index_is_alive
 | 
			
		|||
local ChildOf = jecs.ChildOf
 | 
			
		||||
local world_new = jecs.World.new
 | 
			
		||||
 | 
			
		||||
local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test()
 | 
			
		||||
local it = testkit.test()
 | 
			
		||||
local TEST, CASE = it.TEST, it.CASE
 | 
			
		||||
local CHECK, FINISH = it.CHECK, it.FINISH
 | 
			
		||||
local SKIP, FOCUS = it.SKIP, it.FOCUS
 | 
			
		||||
local CHECK_EXPECT_ERR = it.CHECK_EXPECT_ERR
 | 
			
		||||
 | 
			
		||||
local N = 2 ^ 8
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -68,19 +72,6 @@ local function name(world, e)
 | 
			
		|||
end
 | 
			
		||||
 | 
			
		||||
TEST("archetype", function()
 | 
			
		||||
	local archetype_append_to_records = jecs.archetype_append_to_records
 | 
			
		||||
	local id_record_ensure = jecs.id_record_ensure
 | 
			
		||||
	local archetype_create = jecs.archetype_create
 | 
			
		||||
	local archetype_ensure = jecs.archetype_ensure
 | 
			
		||||
	local find_insert = jecs.find_insert
 | 
			
		||||
	local find_archetype_with = jecs.find_archetype_with
 | 
			
		||||
	local find_archetype_without = jecs.find_archetype_without
 | 
			
		||||
	local archetype_init_edge = jecs.archetype_init_edge
 | 
			
		||||
	local archetype_ensure_edge = jecs.archetype_ensure_edge
 | 
			
		||||
	local init_edge_for_add = jecs.init_edge_for_add
 | 
			
		||||
	local init_edge_for_remove = jecs.init_edge_for_remove
 | 
			
		||||
	local create_edge_for_add = jecs.create_edge_for_add
 | 
			
		||||
	local create_edge_for_remove = jecs.create_edge_for_remove
 | 
			
		||||
	local archetype_traverse_add = jecs.archetype_traverse_add
 | 
			
		||||
	local archetype_traverse_remove = jecs.archetype_traverse_remove
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +81,7 @@ TEST("archetype", function()
 | 
			
		|||
	local c2 = world:component()
 | 
			
		||||
	local c3 = world:component()
 | 
			
		||||
 | 
			
		||||
	local a1 = archetype_traverse_add(world, c1, nil)
 | 
			
		||||
	local a1 = archetype_traverse_add(world, c1, nil :: any)
 | 
			
		||||
	local a2 = archetype_traverse_remove(world, c1, a1)
 | 
			
		||||
	CHECK(root.add[c1].to == a1)
 | 
			
		||||
	CHECK(root == a2)
 | 
			
		||||
| 
						 | 
				
			
			@ -116,11 +107,11 @@ TEST("world:cleanup()", function()
 | 
			
		|||
	world:set(e3, B, true)
 | 
			
		||||
	world:set(e3, C, true)
 | 
			
		||||
 | 
			
		||||
	local archetypeIndex = world.archetypeIndex
 | 
			
		||||
	local archetype_index = world.archetype_index
 | 
			
		||||
 | 
			
		||||
	CHECK(#archetypeIndex["1"].entities == 1)
 | 
			
		||||
	CHECK(#archetypeIndex["1_2"].entities == 1)
 | 
			
		||||
	CHECK(#archetypeIndex["1_2_3"].entities == 1)
 | 
			
		||||
	CHECK(#archetype_index["1"].entities == 1)
 | 
			
		||||
	CHECK(#archetype_index["1_2"].entities == 1)
 | 
			
		||||
	CHECK(#archetype_index["1_2_3"].entities == 1)
 | 
			
		||||
 | 
			
		||||
	world:delete(e1)
 | 
			
		||||
	world:delete(e2)
 | 
			
		||||
| 
						 | 
				
			
			@ -128,25 +119,25 @@ TEST("world:cleanup()", function()
 | 
			
		|||
 | 
			
		||||
	world:cleanup()
 | 
			
		||||
 | 
			
		||||
	archetypeIndex = world.archetypeIndex
 | 
			
		||||
	archetype_index = world.archetype_index
 | 
			
		||||
 | 
			
		||||
	CHECK((archetypeIndex["1"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK((archetype_index["1"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
 | 
			
		||||
 | 
			
		||||
	local e4 = world:entity()
 | 
			
		||||
	world:set(e4, A, true)
 | 
			
		||||
	CHECK(#archetypeIndex["1"].entities == 1)
 | 
			
		||||
	CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK(#archetype_index["1"].entities == 1)
 | 
			
		||||
	CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	world:set(e4, B, true)
 | 
			
		||||
	CHECK(#archetypeIndex["1"].entities == 0)
 | 
			
		||||
	CHECK(#archetypeIndex["1_2"].entities == 1)
 | 
			
		||||
	CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	CHECK(#archetype_index["1"].entities == 0)
 | 
			
		||||
	CHECK(#archetype_index["1_2"].entities == 1)
 | 
			
		||||
	CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
 | 
			
		||||
	world:set(e4, C, true)
 | 
			
		||||
	CHECK(#archetypeIndex["1"].entities == 0)
 | 
			
		||||
	CHECK(#archetypeIndex["1_2"].entities == 0)
 | 
			
		||||
	CHECK(#archetypeIndex["1_2_3"].entities == 1)
 | 
			
		||||
	CHECK(#archetype_index["1"].entities == 0)
 | 
			
		||||
	CHECK(#archetype_index["1_2"].entities == 0)
 | 
			
		||||
	CHECK(#archetype_index["1_2_3"].entities == 1)
 | 
			
		||||
end)
 | 
			
		||||
 | 
			
		||||
TEST("world:entity()", function()
 | 
			
		||||
| 
						 | 
				
			
			@ -163,14 +154,14 @@ TEST("world:entity()", function()
 | 
			
		|||
	do
 | 
			
		||||
		CASE("generations")
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
		local e = world:entity()
 | 
			
		||||
		local e = world:entity() :: number
 | 
			
		||||
		CHECK(ECS_ID(e) == 1 + jecs.Rest :: number)
 | 
			
		||||
		CHECK(ECS_GENERATION(e) == 0) -- 0
 | 
			
		||||
		e = ECS_GENERATION_INC(e)
 | 
			
		||||
		CHECK(ECS_GENERATION(e) == 1) -- 1
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do CASE("pairs")
 | 
			
		||||
	do CASE "pairs"
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
		local _e = world:entity()
 | 
			
		||||
		local e2 = world:entity()
 | 
			
		||||
| 
						 | 
				
			
			@ -182,8 +173,8 @@ TEST("world:entity()", function()
 | 
			
		|||
		local p = pair(e2, e3)
 | 
			
		||||
		CHECK(IS_PAIR(p) == true)
 | 
			
		||||
 | 
			
		||||
		CHECK(ecs_pair_first(world, p) == e2)
 | 
			
		||||
		CHECK(ecs_pair_second(world, p) == e3)
 | 
			
		||||
		CHECK(ecs_pair_first(world, p) == e2 :: number)
 | 
			
		||||
		CHECK(ecs_pair_second(world, p) == e3 :: number)
 | 
			
		||||
 | 
			
		||||
		world:delete(e2)
 | 
			
		||||
		local e2v2 = world:entity()
 | 
			
		||||
| 
						 | 
				
			
			@ -199,7 +190,7 @@ TEST("world:entity()", function()
 | 
			
		|||
		local e1 = world:entity()
 | 
			
		||||
		world:delete(e1)
 | 
			
		||||
		local e2 = world:entity()
 | 
			
		||||
		CHECK(ECS_ID(e2) == e)
 | 
			
		||||
		CHECK(ECS_ID(e2) == e :: number)
 | 
			
		||||
		CHECK(ECS_GENERATION(e2) == 2)
 | 
			
		||||
		CHECK(world:contains(e2))
 | 
			
		||||
		CHECK(not world:contains(e1))
 | 
			
		||||
| 
						 | 
				
			
			@ -224,8 +215,7 @@ TEST("world:entity()", function()
 | 
			
		|||
end)
 | 
			
		||||
 | 
			
		||||
TEST("world:set()", function()
 | 
			
		||||
	do
 | 
			
		||||
		CASE("archetype move")
 | 
			
		||||
	do CASE "archetype move"
 | 
			
		||||
		do
 | 
			
		||||
			local world = jecs.World.new()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -255,26 +245,11 @@ TEST("world:set()", function()
 | 
			
		|||
			-- Should have tuple of fields to the next archetype and set the component data
 | 
			
		||||
			CHECK(d.tuple(e, 1, 2))
 | 
			
		||||
			-- Should have moved the data from the old archetype
 | 
			
		||||
			CHECK(world.archetypeIndex[oldArchetype].columns[_1][oldRow] == nil)
 | 
			
		||||
			CHECK(world.archetype_index[oldArchetype].columns[_1][oldRow] == nil)
 | 
			
		||||
		end
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do
 | 
			
		||||
		CASE("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("pairs")
 | 
			
		||||
	do CASE "pairs"
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
 | 
			
		||||
		local C1 = world:component()
 | 
			
		||||
| 
						 | 
				
			
			@ -287,7 +262,10 @@ TEST("world:set()", function()
 | 
			
		|||
		world:set(e, pair(C1, C2), true)
 | 
			
		||||
		world:set(e, pair(C1, T1), true)
 | 
			
		||||
		world:set(e, pair(T1, C1), true)
 | 
			
		||||
		world:set(e, pair(T1, T2), true)
 | 
			
		||||
 | 
			
		||||
		CHECK_EXPECT_ERR(function()
 | 
			
		||||
			world:set(e, pair(T1, T2), true :: any)
 | 
			
		||||
		end)
 | 
			
		||||
 | 
			
		||||
		CHECK(world:get(e, pair(C1, C2)))
 | 
			
		||||
		CHECK(world:get(e, pair(C1, T1)))
 | 
			
		||||
| 
						 | 
				
			
			@ -296,7 +274,9 @@ TEST("world:set()", function()
 | 
			
		|||
 | 
			
		||||
		local e2 = world:entity()
 | 
			
		||||
 | 
			
		||||
		world:set(e2, pair(jecs.ChildOf, e), true)
 | 
			
		||||
		CHECK_EXPECT_ERR(function()
 | 
			
		||||
			world:set(e2, pair(jecs.ChildOf, e), true :: any)
 | 
			
		||||
		end)
 | 
			
		||||
		CHECK(not world:get(e2, pair(jecs.ChildOf, e)))
 | 
			
		||||
	end
 | 
			
		||||
end)
 | 
			
		||||
| 
						 | 
				
			
			@ -385,21 +365,21 @@ TEST("world:query()", function()
 | 
			
		|||
			i=2
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(i == 2)
 | 
			
		||||
		for _, e in q do
 | 
			
		||||
		for _, e in q :: any do
 | 
			
		||||
			i=3
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(i == 3)
 | 
			
		||||
		for _, e in q do
 | 
			
		||||
		for _, e in q :: any do
 | 
			
		||||
			i=4
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(i == 4)
 | 
			
		||||
 | 
			
		||||
		CHECK(#q:archetypes() == 1)
 | 
			
		||||
		CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")]))
 | 
			
		||||
		CHECK(not table.find(q:archetypes(), world.archetype_index[table.concat({Foo, Bar, Baz}, "_")]))
 | 
			
		||||
		world:delete(Foo)
 | 
			
		||||
		CHECK(#q:archetypes() == 0)
 | 
			
		||||
	end
 | 
			
		||||
	do CASE("multiple iter")
 | 
			
		||||
	do CASE "multiple iter"
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
		local A = world:component()
 | 
			
		||||
		local B = world:component()
 | 
			
		||||
| 
						 | 
				
			
			@ -416,18 +396,21 @@ TEST("world:query()", function()
 | 
			
		|||
		end
 | 
			
		||||
		CHECK(counter == 2)
 | 
			
		||||
	end
 | 
			
		||||
	do
 | 
			
		||||
		CASE("tag")
 | 
			
		||||
	do CASE "tag"
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
		local A = world:entity()
 | 
			
		||||
		local e = world:entity()
 | 
			
		||||
		world:set(e, A, "test")
 | 
			
		||||
		for id, a in world:query(A) do
 | 
			
		||||
		CHECK_EXPECT_ERR(function()
 | 
			
		||||
			world:set(e, A, "test" :: any)
 | 
			
		||||
		end)
 | 
			
		||||
		local count = 0
 | 
			
		||||
		for id, a in world:query(A) :: any do
 | 
			
		||||
			count += 1
 | 
			
		||||
			CHECK(a == nil)
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(count == 1)
 | 
			
		||||
	end
 | 
			
		||||
	do
 | 
			
		||||
		CASE("pairs")
 | 
			
		||||
	do CASE "pairs"
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
 | 
			
		||||
		local C1 = world:component()
 | 
			
		||||
| 
						 | 
				
			
			@ -440,9 +423,11 @@ TEST("world:query()", function()
 | 
			
		|||
		world:set(e, pair(C1, C2), true)
 | 
			
		||||
		world:set(e, pair(C1, T1), true)
 | 
			
		||||
		world:set(e, pair(T1, C1), true)
 | 
			
		||||
		world:set(e, pair(T1, T2), true)
 | 
			
		||||
		CHECK_EXPECT_ERR(function()
 | 
			
		||||
			world:set(e, pair(T1, T2), true :: any)
 | 
			
		||||
		end)
 | 
			
		||||
 | 
			
		||||
		for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) do
 | 
			
		||||
		for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) :: any do
 | 
			
		||||
			CHECK(a == true)
 | 
			
		||||
			CHECK(b == true)
 | 
			
		||||
			CHECK(c == true)
 | 
			
		||||
| 
						 | 
				
			
			@ -467,7 +452,7 @@ TEST("world:query()", function()
 | 
			
		|||
				entities[i] = id
 | 
			
		||||
			end
 | 
			
		||||
 | 
			
		||||
			for id in world:query(A) do
 | 
			
		||||
			for id in world:query(A) :: any do
 | 
			
		||||
				table.remove(entities, CHECK(table.find(entities, id)))
 | 
			
		||||
			end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -491,10 +476,10 @@ TEST("world:query()", function()
 | 
			
		|||
 | 
			
		||||
			local i = 0
 | 
			
		||||
			local j = 0
 | 
			
		||||
			for _ in q do
 | 
			
		||||
			for _ in q :: any do
 | 
			
		||||
				i += 1
 | 
			
		||||
			end
 | 
			
		||||
			for _ in q do
 | 
			
		||||
			for _ in q :: any do
 | 
			
		||||
				j += 1
 | 
			
		||||
			end
 | 
			
		||||
			CHECK(i == 2)
 | 
			
		||||
| 
						 | 
				
			
			@ -518,7 +503,7 @@ TEST("world:query()", function()
 | 
			
		|||
		world:set(e2, B, 457)
 | 
			
		||||
 | 
			
		||||
		local counter = 0
 | 
			
		||||
		for _ in world:query(B, C) do
 | 
			
		||||
		for _ in world:query(B, C) :: any do
 | 
			
		||||
			counter += 1
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(counter == 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -538,7 +523,7 @@ TEST("world:query()", function()
 | 
			
		|||
			world:set(e, id, 13 ^ i)
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) do
 | 
			
		||||
		for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) :: any do
 | 
			
		||||
			CHECK(a == 13 ^ 1)
 | 
			
		||||
			CHECK(b == 13 ^ 2)
 | 
			
		||||
			CHECK(c == 13 ^ 3)
 | 
			
		||||
| 
						 | 
				
			
			@ -567,11 +552,11 @@ TEST("world:query()", function()
 | 
			
		|||
 | 
			
		||||
		local it = world:query(A):iter()
 | 
			
		||||
 | 
			
		||||
		local e, data = it()
 | 
			
		||||
		local e: number, data = it()
 | 
			
		||||
		while e do
 | 
			
		||||
			if e == eA then
 | 
			
		||||
			if e == eA :: number then
 | 
			
		||||
				CHECK(data)
 | 
			
		||||
			elseif e == eAB then
 | 
			
		||||
			elseif e == eAB :: number then
 | 
			
		||||
				CHECK(data)
 | 
			
		||||
			else
 | 
			
		||||
				CHECK(false)
 | 
			
		||||
| 
						 | 
				
			
			@ -582,8 +567,7 @@ TEST("world:query()", function()
 | 
			
		|||
		CHECK(true)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do
 | 
			
		||||
		CASE("should query all matching entities when irrelevant component is removed")
 | 
			
		||||
	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()
 | 
			
		||||
| 
						 | 
				
			
			@ -604,7 +588,7 @@ TEST("world:query()", function()
 | 
			
		|||
		end
 | 
			
		||||
 | 
			
		||||
		local added = 0
 | 
			
		||||
		for id in world:query(A) do
 | 
			
		||||
		for id in world:query(A) :: any do
 | 
			
		||||
			added += 1
 | 
			
		||||
			table.remove(entities, CHECK(table.find(entities, id)))
 | 
			
		||||
		end
 | 
			
		||||
| 
						 | 
				
			
			@ -630,7 +614,7 @@ TEST("world:query()", function()
 | 
			
		|||
			end
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		for id in world:query(A):without(B) do
 | 
			
		||||
		for id in world:query(A):without(B) :: any do
 | 
			
		||||
			table.remove(entities, CHECK(table.find(entities, id)))
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -645,7 +629,7 @@ TEST("world:query()", function()
 | 
			
		|||
		local bob = world:entity()
 | 
			
		||||
 | 
			
		||||
		world:set(bob, pair(Eats, Apples), true)
 | 
			
		||||
		for e, bool in world:query(pair(Eats, Apples)) do
 | 
			
		||||
		for e, bool in world:query(pair(Eats, Apples)) :: any do
 | 
			
		||||
			CHECK(e == bob)
 | 
			
		||||
			CHECK(bool)
 | 
			
		||||
		end
 | 
			
		||||
| 
						 | 
				
			
			@ -661,11 +645,11 @@ TEST("world:query()", function()
 | 
			
		|||
		world:set(bob, pair(Eats, Apples), "bob eats apples")
 | 
			
		||||
 | 
			
		||||
		local w = jecs.Wildcard
 | 
			
		||||
		for e, data in world:query(pair(Eats, w)) do
 | 
			
		||||
		for e, data in world:query(pair(Eats, w)) :: any do
 | 
			
		||||
			CHECK(e == bob)
 | 
			
		||||
			CHECK(data == "bob eats apples")
 | 
			
		||||
		end
 | 
			
		||||
		for e, data in world:query(pair(w, Apples)) do
 | 
			
		||||
		for e, data in world:query(pair(w, Apples)) :: any do
 | 
			
		||||
			CHECK(e == bob)
 | 
			
		||||
			CHECK(data == "bob eats apples")
 | 
			
		||||
		end
 | 
			
		||||
| 
						 | 
				
			
			@ -685,7 +669,7 @@ TEST("world:query()", function()
 | 
			
		|||
 | 
			
		||||
		local w = jecs.Wildcard
 | 
			
		||||
		local count = 0
 | 
			
		||||
		for e, data in world:query(pair(Eats, w)) do
 | 
			
		||||
		for e, data in world:query(pair(Eats, w)) :: any do
 | 
			
		||||
			count += 1
 | 
			
		||||
			if e == bob then
 | 
			
		||||
				CHECK(data == "bob eats apples")
 | 
			
		||||
| 
						 | 
				
			
			@ -697,32 +681,30 @@ TEST("world:query()", function()
 | 
			
		|||
		CHECK(count == 2)
 | 
			
		||||
		count = 0
 | 
			
		||||
 | 
			
		||||
		for e, data in world:query(pair(w, Apples)) do
 | 
			
		||||
		for e, data in world:query(pair(w, Apples)) :: any do
 | 
			
		||||
			count += 1
 | 
			
		||||
			CHECK(data == "bob eats apples")
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(count == 1)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do
 | 
			
		||||
		CASE("should only relate alive entities")
 | 
			
		||||
		SKIP()
 | 
			
		||||
	do CASE "should only relate alive entities"
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
		local Eats = world:entity()
 | 
			
		||||
		local Apples = world:entity()
 | 
			
		||||
		local Oranges = world:entity()
 | 
			
		||||
		local Apples = world:component()
 | 
			
		||||
		local Oranges = world:component()
 | 
			
		||||
		local bob = world:entity()
 | 
			
		||||
		local alice = world:entity()
 | 
			
		||||
 | 
			
		||||
		world:set(bob, Apples, "apples")
 | 
			
		||||
		world:set(bob, pair(Eats, Apples), "bob eats apples")
 | 
			
		||||
		world:set(alice, pair(Eats, Oranges), "alice eats oranges")
 | 
			
		||||
		world:set(alice, pair(Eats, Oranges) :: Entity<string>, "alice eats oranges")
 | 
			
		||||
 | 
			
		||||
		world:delete(Apples)
 | 
			
		||||
		local Wildcard = jecs.Wildcard
 | 
			
		||||
 | 
			
		||||
		local count = 0
 | 
			
		||||
		for _, data in world:query(pair(Wildcard, Apples)) do
 | 
			
		||||
		for _, data in world:query(pair(Wildcard, Apples)) :: any do
 | 
			
		||||
			count += 1
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -730,6 +712,7 @@ TEST("world:query()", function()
 | 
			
		|||
 | 
			
		||||
		CHECK(count == 0)
 | 
			
		||||
		CHECK(world:get(bob, pair(Eats, Apples)) == nil)
 | 
			
		||||
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do
 | 
			
		||||
| 
						 | 
				
			
			@ -759,10 +742,10 @@ TEST("world:query()", function()
 | 
			
		|||
		world:set(bob, Name, "bob")
 | 
			
		||||
		world:add(sara, pair(ChildOf, alice))
 | 
			
		||||
		world:set(sara, Name, "sara")
 | 
			
		||||
		CHECK(world:parent(bob) == alice) -- O(1)
 | 
			
		||||
		CHECK(world:parent(bob) :: number == alice :: number) -- O(1)
 | 
			
		||||
 | 
			
		||||
		local count = 0
 | 
			
		||||
		for _, name in world:query(Name, pair(ChildOf, alice)) do
 | 
			
		||||
		for _, name in world:query(Name, pair(ChildOf, alice)) :: any do
 | 
			
		||||
			count += 1
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(count == 2)
 | 
			
		||||
| 
						 | 
				
			
			@ -781,7 +764,7 @@ TEST("world:query()", function()
 | 
			
		|||
		world:add(e2, B)
 | 
			
		||||
 | 
			
		||||
		local count = 0
 | 
			
		||||
		for id in world:query(A) do
 | 
			
		||||
		for id in world:query(A) :: any do
 | 
			
		||||
			world:clear(id)
 | 
			
		||||
			count += 1
 | 
			
		||||
		end
 | 
			
		||||
| 
						 | 
				
			
			@ -804,7 +787,7 @@ TEST("world:query()", function()
 | 
			
		|||
			world:add(e2, B)
 | 
			
		||||
 | 
			
		||||
			local count = 0
 | 
			
		||||
			for id in world:query(A) do
 | 
			
		||||
			for id in world:query(A) :: any do
 | 
			
		||||
				world:add(id, B)
 | 
			
		||||
 | 
			
		||||
				count += 1
 | 
			
		||||
| 
						 | 
				
			
			@ -825,7 +808,7 @@ TEST("world:query()", function()
 | 
			
		|||
			world:add(e2, A)
 | 
			
		||||
			world:add(e2, B)
 | 
			
		||||
 | 
			
		||||
			for id in world:query(A) do
 | 
			
		||||
			for id in world:query(A) :: any do
 | 
			
		||||
				local e = world:entity()
 | 
			
		||||
				world:add(e, A)
 | 
			
		||||
				world:add(e, B)
 | 
			
		||||
| 
						 | 
				
			
			@ -847,7 +830,7 @@ TEST("world:query()", function()
 | 
			
		|||
		world:add(helloBob, Bob)
 | 
			
		||||
 | 
			
		||||
		local withoutCount = 0
 | 
			
		||||
		for _ in world:query(pair(Hello, Bob)):without(Bob) do
 | 
			
		||||
		for _ in world:query(pair(Hello, Bob)):without(Bob) :: any do
 | 
			
		||||
			withoutCount += 1
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -862,7 +845,7 @@ TEST("world:query()", function()
 | 
			
		|||
			local _1, _2, _3 = world:component(), world:component(), world:component()
 | 
			
		||||
 | 
			
		||||
			local counter = 0
 | 
			
		||||
			for e, a, b in world:query(_1, _2):without(_3) do
 | 
			
		||||
			for e, a, b in world:query(_1, _2):without(_3) :: any do
 | 
			
		||||
				counter += 1
 | 
			
		||||
			end
 | 
			
		||||
			CHECK(counter == 0)
 | 
			
		||||
| 
						 | 
				
			
			@ -876,9 +859,9 @@ TEST("world:each", function()
 | 
			
		|||
	local B = world:component()
 | 
			
		||||
	local C = world:component()
 | 
			
		||||
 | 
			
		||||
	local e3 = world:entity()
 | 
			
		||||
	local e1 = world:entity()
 | 
			
		||||
	local e2 = world:entity()
 | 
			
		||||
	local e3 = world:entity()
 | 
			
		||||
 | 
			
		||||
	world:set(e1, A, true)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -889,8 +872,8 @@ TEST("world:each", function()
 | 
			
		|||
	world:set(e3, B, true)
 | 
			
		||||
	world:set(e3, C, true)
 | 
			
		||||
 | 
			
		||||
	for entity in world:each(A) do
 | 
			
		||||
		if entity == e1 or entity == e2 or entity == e3 then
 | 
			
		||||
	for entity: number in world:each(A) do
 | 
			
		||||
		if entity == e1 :: number or entity == e2 :: number or entity == e3 :: number then
 | 
			
		||||
			CHECK(true)
 | 
			
		||||
			continue
 | 
			
		||||
		end
 | 
			
		||||
| 
						 | 
				
			
			@ -906,16 +889,16 @@ TEST("world:children", function()
 | 
			
		|||
	local e1 = world:entity()
 | 
			
		||||
	world:set(e1, C, true)
 | 
			
		||||
 | 
			
		||||
	local e2 = world:entity()
 | 
			
		||||
	local e2 = world:entity() :: number
 | 
			
		||||
 | 
			
		||||
	world:add(e2, T)
 | 
			
		||||
	world:add(e2, pair(ChildOf, e1))
 | 
			
		||||
 | 
			
		||||
	local e3 = world:entity()
 | 
			
		||||
	local e3 = world:entity() :: number
 | 
			
		||||
	world:add(e3, pair(ChildOf, e1))
 | 
			
		||||
 | 
			
		||||
	local count = 0
 | 
			
		||||
	for entity in world:children(e1) do
 | 
			
		||||
	for entity: number in world:children(e1) do
 | 
			
		||||
		count += 1
 | 
			
		||||
		if entity == e2 or entity == e3 then
 | 
			
		||||
			CHECK(true)
 | 
			
		||||
| 
						 | 
				
			
			@ -966,7 +949,7 @@ TEST("world:clear()", function()
 | 
			
		|||
		world:add(e, A)
 | 
			
		||||
		world:add(e1, A)
 | 
			
		||||
 | 
			
		||||
		local archetype = world.archetypeIndex["1"]
 | 
			
		||||
		local archetype = world.archetype_index["1"]
 | 
			
		||||
		local archetype_entities = archetype.entities
 | 
			
		||||
 | 
			
		||||
		local _e = e :: number
 | 
			
		||||
| 
						 | 
				
			
			@ -1050,7 +1033,9 @@ TEST("world:component()", function()
 | 
			
		|||
		local e = world:entity()
 | 
			
		||||
		world:set(e, A, "test")
 | 
			
		||||
		world:add(e, B)
 | 
			
		||||
		world:set(e, C, 11)
 | 
			
		||||
		CHECK_EXPECT_ERR(function()
 | 
			
		||||
			world:set(e, C, 11 :: any)
 | 
			
		||||
		end)
 | 
			
		||||
 | 
			
		||||
		CHECK(world:has(e, A))
 | 
			
		||||
		CHECK(world:get(e, A) == "test")
 | 
			
		||||
| 
						 | 
				
			
			@ -1113,10 +1098,14 @@ TEST("world:delete", function()
 | 
			
		|||
 | 
			
		||||
		local id = world:entity()
 | 
			
		||||
		world:set(id, Poison, 5)
 | 
			
		||||
		world:set(id, Health, 50)
 | 
			
		||||
		CHECK_EXPECT_ERR(function()
 | 
			
		||||
			world:set(id, Health, 50 :: any)
 | 
			
		||||
		end)
 | 
			
		||||
		local id1 = world:entity()
 | 
			
		||||
		world:set(id1, Poison, 500)
 | 
			
		||||
		world:set(id1, Health, 50)
 | 
			
		||||
		CHECK_EXPECT_ERR(function()
 | 
			
		||||
			world:set(id1, Health, 50 :: any)
 | 
			
		||||
		end)
 | 
			
		||||
 | 
			
		||||
		CHECK(world:has(id, Poison, Health))
 | 
			
		||||
		CHECK(world:has(id1, Poison, Health))
 | 
			
		||||
| 
						 | 
				
			
			@ -1355,7 +1344,7 @@ TEST("Hooks", function()
 | 
			
		|||
		do
 | 
			
		||||
			-- basic
 | 
			
		||||
			local world = jecs.World.new()
 | 
			
		||||
			local A = world:component()
 | 
			
		||||
			local A = world:component() :: Entity<boolean>
 | 
			
		||||
			local e1 = world:entity()
 | 
			
		||||
			world:add(e1, A)
 | 
			
		||||
			world:set(A, jecs.OnRemove, function(entity)
 | 
			
		||||
| 
						 | 
				
			
			@ -1387,9 +1376,9 @@ TEST("Hooks", function()
 | 
			
		|||
end)
 | 
			
		||||
 | 
			
		||||
TEST("change tracking", function()
 | 
			
		||||
	CASE "#1" do
 | 
			
		||||
	do CASE "#1"
 | 
			
		||||
		local world = world_new()
 | 
			
		||||
		local Foo = world:component()
 | 
			
		||||
		local Foo = world:component() :: Entity<number>
 | 
			
		||||
		local Previous = jecs.Rest
 | 
			
		||||
 | 
			
		||||
		local q1 = world
 | 
			
		||||
| 
						 | 
				
			
			@ -1403,14 +1392,14 @@ TEST("change tracking", function()
 | 
			
		|||
		world:set(e2, Foo, 2)
 | 
			
		||||
 | 
			
		||||
		local i = 0
 | 
			
		||||
		for e, new in q1 do
 | 
			
		||||
		for e, new in q1 :: any do
 | 
			
		||||
			i += 1
 | 
			
		||||
			world:set(e, pair(Previous, Foo), new)
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		CHECK(i == 2)
 | 
			
		||||
		local j = 0
 | 
			
		||||
		for e, new in q1 do
 | 
			
		||||
		for e, new in q1 :: any do
 | 
			
		||||
			j += 1
 | 
			
		||||
			world:set(e, pair(Previous, Foo), new)
 | 
			
		||||
		end
 | 
			
		||||
| 
						 | 
				
			
			@ -1418,9 +1407,9 @@ TEST("change tracking", function()
 | 
			
		|||
		CHECK(j == 0)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	CASE "#2" do
 | 
			
		||||
	do CASE "#2"
 | 
			
		||||
		local world = world_new()
 | 
			
		||||
		local component = world:component()
 | 
			
		||||
		local component = world:component() :: Entity<number>
 | 
			
		||||
		local tag = world:entity()
 | 
			
		||||
		local previous = jecs.Rest
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1431,14 +1420,14 @@ TEST("change tracking", function()
 | 
			
		|||
		world:set(testEntity, component, 10)
 | 
			
		||||
 | 
			
		||||
		local i = 0
 | 
			
		||||
		for entity, number in q1 do
 | 
			
		||||
		for entity, number in q1 :: any do
 | 
			
		||||
			i += 1
 | 
			
		||||
			world:add(testEntity, tag)
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		CHECK(i == 1)
 | 
			
		||||
 | 
			
		||||
		for e, n in q1 do
 | 
			
		||||
		for e, n in q1 :: any do
 | 
			
		||||
			world:set(e, pair(previous, component), n)
 | 
			
		||||
		end
 | 
			
		||||
	end
 | 
			
		||||
| 
						 | 
				
			
			@ -1477,107 +1466,7 @@ TEST("repro", function()
 | 
			
		|||
		updateCooldowns(1.5)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do CASE "#2"
 | 
			
		||||
		local world = jecs.World.new()
 | 
			
		||||
 | 
			
		||||
		export type Iterator<T> = () -> (Entity, T?, T?)
 | 
			
		||||
		export type Destructor = () -> ()
 | 
			
		||||
 | 
			
		||||
		-- Helpers
 | 
			
		||||
 | 
			
		||||
		type ValuesMap<T> = { [Entity]: T? }
 | 
			
		||||
		type ChangeSet = { [Entity]: true? }
 | 
			
		||||
		type ChangeSets = { [ChangeSet]: true? }
 | 
			
		||||
		type ChangeSetsCache = {
 | 
			
		||||
			Added: ChangeSets,
 | 
			
		||||
			Changed: ChangeSets,
 | 
			
		||||
			Removed: ChangeSets,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		local cachedChangeSets = {}
 | 
			
		||||
		local function getChangeSets(component): ChangeSetsCache
 | 
			
		||||
			if cachedChangeSets[component] == nil then
 | 
			
		||||
				local changeSetsAdded: ChangeSets = {}
 | 
			
		||||
				local changeSetsChanged: ChangeSets = {}
 | 
			
		||||
				local changeSetsRemoved: ChangeSets = {}
 | 
			
		||||
				world:set(component, jecs.OnAdd, function(id)
 | 
			
		||||
					for set in changeSetsAdded do
 | 
			
		||||
						set[id] = true
 | 
			
		||||
					end
 | 
			
		||||
				end)
 | 
			
		||||
				world:set(component, jecs.OnSet, function(id)
 | 
			
		||||
					for set in changeSetsChanged do
 | 
			
		||||
						set[id] = true
 | 
			
		||||
					end
 | 
			
		||||
				end)
 | 
			
		||||
				world:set(component, jecs.OnRemove, function(id)
 | 
			
		||||
					for set in changeSetsRemoved do
 | 
			
		||||
						set[id] = true
 | 
			
		||||
					end
 | 
			
		||||
				end)
 | 
			
		||||
				cachedChangeSets[component] = {
 | 
			
		||||
					Added = changeSetsAdded,
 | 
			
		||||
					Changed = changeSetsChanged,
 | 
			
		||||
					Removed = changeSetsRemoved,
 | 
			
		||||
				}
 | 
			
		||||
			end
 | 
			
		||||
			return cachedChangeSets[component]
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		local function ChangeTracker<T>(component: jecs.Id): (Iterator<T>, Destructor)
 | 
			
		||||
			local values: ValuesMap<T> = {}
 | 
			
		||||
			local changeSet: ChangeSet = {}
 | 
			
		||||
 | 
			
		||||
			for id in world:query(component) do
 | 
			
		||||
				changeSet[id] = true
 | 
			
		||||
			end
 | 
			
		||||
 | 
			
		||||
			local changeSets = getChangeSets(component)
 | 
			
		||||
			changeSets.Added[changeSet] = true
 | 
			
		||||
			changeSets.Changed[changeSet] = true
 | 
			
		||||
			changeSets.Removed[changeSet] = true
 | 
			
		||||
 | 
			
		||||
			local id: jecs.Id? = nil
 | 
			
		||||
			local iter: Iterator<T> = function()
 | 
			
		||||
				id = next(changeSet)
 | 
			
		||||
				if id then
 | 
			
		||||
					changeSet[id] = nil
 | 
			
		||||
					local old: T? = values[id]
 | 
			
		||||
					local new: T? = world:get(id, component)
 | 
			
		||||
					if old ~= nil and new == nil then
 | 
			
		||||
						-- Old value but no new value = removed
 | 
			
		||||
						values[id] = nil
 | 
			
		||||
					else
 | 
			
		||||
						-- Old+new value or just new value = new becomes old
 | 
			
		||||
						values[id] = new
 | 
			
		||||
					end
 | 
			
		||||
					return id, old, new
 | 
			
		||||
				end
 | 
			
		||||
				return nil :: any, nil, nil
 | 
			
		||||
			end
 | 
			
		||||
 | 
			
		||||
			local destroy: Destructor = function()
 | 
			
		||||
				changeSets.Added[changeSet] = nil
 | 
			
		||||
				changeSets.Changed[changeSet] = nil
 | 
			
		||||
				changeSets.Removed[changeSet] = nil
 | 
			
		||||
			end
 | 
			
		||||
 | 
			
		||||
			return iter, destroy
 | 
			
		||||
		end
 | 
			
		||||
 | 
			
		||||
		local Transform = world:component()
 | 
			
		||||
		local iter, destroy = ChangeTracker(Transform)
 | 
			
		||||
 | 
			
		||||
		local e1 = world:entity()
 | 
			
		||||
		world:set(e1, Transform, { 1, 1 })
 | 
			
		||||
		local counter = 0
 | 
			
		||||
		for _ in iter do
 | 
			
		||||
			counter += 1
 | 
			
		||||
		end
 | 
			
		||||
		CHECK(counter == 1)
 | 
			
		||||
	end
 | 
			
		||||
 | 
			
		||||
	do CASE "#3" -- ISSUE #171
 | 
			
		||||
	do CASE "#2" -- ISSUE #171
 | 
			
		||||
		local world = world_new()
 | 
			
		||||
		local component1 = world:component()
 | 
			
		||||
		local tag1 = world:entity()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										177
									
								
								tools/perfgraph.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								tools/perfgraph.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,177 @@
 | 
			
		|||
#!/usr/bin/python3
 | 
			
		||||
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
 | 
			
		||||
 | 
			
		||||
# Given a profile dump, this tool generates a flame graph based on the stacks listed in the profile
 | 
			
		||||
# The result of analysis is a .svg file which can be viewed in a browser
 | 
			
		||||
 | 
			
		||||
import svg
 | 
			
		||||
import argparse
 | 
			
		||||
import json
 | 
			
		||||
 | 
			
		||||
argumentParser = argparse.ArgumentParser(description='Generate flamegraph SVG from Luau sampling profiler dumps')
 | 
			
		||||
argumentParser.add_argument('source_file', type=open)
 | 
			
		||||
argumentParser.add_argument('--json', dest='useJson',action='store_const',const=1,default=0,help='Parse source_file as JSON')
 | 
			
		||||
 | 
			
		||||
class Node(svg.Node):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        svg.Node.__init__(self)
 | 
			
		||||
        self.function = ""
 | 
			
		||||
        self.source = ""
 | 
			
		||||
        self.line = 0
 | 
			
		||||
        self.ticks = 0
 | 
			
		||||
 | 
			
		||||
    def text(self):
 | 
			
		||||
        return self.function
 | 
			
		||||
 | 
			
		||||
    def title(self):
 | 
			
		||||
        if self.line > 0:
 | 
			
		||||
            return "{}\n{}:{}".format(self.function, self.source, self.line)
 | 
			
		||||
        else:
 | 
			
		||||
            return self.function
 | 
			
		||||
 | 
			
		||||
    def details(self, root):
 | 
			
		||||
        return "Function: {} [{}:{}] ({:,} usec, {:.1%}); self: {:,} usec".format(self.function, self.source, self.line, self.width, self.width / root.width, self.ticks)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def nodeFromCallstackListFile(source_file):
 | 
			
		||||
    dump = source_file.readlines()
 | 
			
		||||
    root = Node()
 | 
			
		||||
 | 
			
		||||
    for l in dump:
 | 
			
		||||
        ticks, stack = l.strip().split(" ", 1)
 | 
			
		||||
        node = root
 | 
			
		||||
 | 
			
		||||
        for f in reversed(stack.split(";")):
 | 
			
		||||
            source, function, line = f.split(",")
 | 
			
		||||
 | 
			
		||||
            child = node.child(f)
 | 
			
		||||
            child.function = function
 | 
			
		||||
            child.source = source
 | 
			
		||||
            child.line = int(line) if len(line) > 0 else 0
 | 
			
		||||
 | 
			
		||||
            node = child
 | 
			
		||||
 | 
			
		||||
        node.ticks += int(ticks)
 | 
			
		||||
 | 
			
		||||
    return root
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getDuration(nodes, nid):
 | 
			
		||||
    node = nodes[nid - 1]
 | 
			
		||||
    total = node['TotalDuration']
 | 
			
		||||
 | 
			
		||||
    if 'NodeIds' in node:
 | 
			
		||||
        for cid in node['NodeIds']:
 | 
			
		||||
            total -= nodes[cid - 1]['TotalDuration']
 | 
			
		||||
 | 
			
		||||
    return total
 | 
			
		||||
 | 
			
		||||
def getFunctionKey(fn):
 | 
			
		||||
    source = fn['Source'] if 'Source' in fn else ''
 | 
			
		||||
    name = fn['Name'] if 'Name' in fn else ''
 | 
			
		||||
    line = str(fn['Line']) if 'Line' in fn else '-1'
 | 
			
		||||
 | 
			
		||||
    return source + "," + name + "," + line
 | 
			
		||||
 | 
			
		||||
def recursivelyBuildNodeTree(nodes, functions, parent, fid, nid):
 | 
			
		||||
    ninfo = nodes[nid - 1]
 | 
			
		||||
    finfo = functions[fid - 1]
 | 
			
		||||
 | 
			
		||||
    child = parent.child(getFunctionKey(finfo))
 | 
			
		||||
    child.source = finfo['Source'] if 'Source' in finfo else ''
 | 
			
		||||
    child.function = finfo['Name'] if 'Name' in finfo else ''
 | 
			
		||||
    child.line = int(finfo['Line']) if 'Line' in finfo and finfo['Line'] > 0 else 0
 | 
			
		||||
 | 
			
		||||
    child.ticks = getDuration(nodes, nid)
 | 
			
		||||
 | 
			
		||||
    if 'FunctionIds' in ninfo:
 | 
			
		||||
        assert(len(ninfo['FunctionIds']) == len(ninfo['NodeIds']))
 | 
			
		||||
 | 
			
		||||
        for i in range(0, len(ninfo['FunctionIds'])):
 | 
			
		||||
            recursivelyBuildNodeTree(nodes, functions, child, ninfo['FunctionIds'][i], ninfo['NodeIds'][i])
 | 
			
		||||
 | 
			
		||||
    return
 | 
			
		||||
 | 
			
		||||
def nodeFromJSONV2(dump):
 | 
			
		||||
    assert(dump['Version'] == 2)
 | 
			
		||||
 | 
			
		||||
    nodes = dump['Nodes']
 | 
			
		||||
    functions = dump['Functions']
 | 
			
		||||
    categories = dump['Categories']
 | 
			
		||||
 | 
			
		||||
    root = Node()
 | 
			
		||||
 | 
			
		||||
    for category in categories:
 | 
			
		||||
        nid = category['NodeId']
 | 
			
		||||
        node = nodes[nid - 1]
 | 
			
		||||
        name = category['Name']
 | 
			
		||||
 | 
			
		||||
        child = root.child(name)
 | 
			
		||||
        child.function = name
 | 
			
		||||
        child.ticks = getDuration(nodes, nid)
 | 
			
		||||
 | 
			
		||||
        if 'FunctionIds' in node:
 | 
			
		||||
            assert(len(node['FunctionIds']) == len(node['NodeIds']))
 | 
			
		||||
 | 
			
		||||
            for i in range(0, len(node['FunctionIds'])):
 | 
			
		||||
                recursivelyBuildNodeTree(nodes, functions, child, node['FunctionIds'][i], node['NodeIds'][i])
 | 
			
		||||
 | 
			
		||||
    return root
 | 
			
		||||
 | 
			
		||||
def getDurationV1(obj):
 | 
			
		||||
    total = obj['TotalDuration']
 | 
			
		||||
 | 
			
		||||
    if 'Children' in obj:
 | 
			
		||||
        for key, obj in obj['Children'].items():
 | 
			
		||||
            total -= obj['TotalDuration']
 | 
			
		||||
 | 
			
		||||
    return total
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def nodeFromJSONObject(node, key, obj):
 | 
			
		||||
    source, function, line = key.split(",")
 | 
			
		||||
 | 
			
		||||
    node.function = function
 | 
			
		||||
    node.source = source
 | 
			
		||||
    node.line = int(line) if len(line) > 0 else 0
 | 
			
		||||
 | 
			
		||||
    node.ticks = getDurationV1(obj)
 | 
			
		||||
 | 
			
		||||
    if 'Children' in obj:
 | 
			
		||||
        for key, obj in obj['Children'].items():
 | 
			
		||||
            nodeFromJSONObject(node.child(key), key, obj)
 | 
			
		||||
 | 
			
		||||
    return node
 | 
			
		||||
 | 
			
		||||
def nodeFromJSONV1(dump):
 | 
			
		||||
    assert(dump['Version'] == 1)
 | 
			
		||||
    root = Node()
 | 
			
		||||
 | 
			
		||||
    if 'Children' in dump:
 | 
			
		||||
        for key, obj in dump['Children'].items():
 | 
			
		||||
            nodeFromJSONObject(root.child(key), key, obj)
 | 
			
		||||
 | 
			
		||||
    return root
 | 
			
		||||
 | 
			
		||||
def nodeFromJSONFile(source_file):
 | 
			
		||||
    dump = json.load(source_file)
 | 
			
		||||
 | 
			
		||||
    if dump['Version'] == 2:
 | 
			
		||||
        return nodeFromJSONV2(dump)
 | 
			
		||||
    elif dump['Version'] == 1:
 | 
			
		||||
        return nodeFromJSONV1(dump)
 | 
			
		||||
 | 
			
		||||
    return Node()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
arguments = argumentParser.parse_args()
 | 
			
		||||
 | 
			
		||||
if arguments.useJson:
 | 
			
		||||
    root = nodeFromJSONFile(arguments.source_file)
 | 
			
		||||
else:
 | 
			
		||||
    root = nodeFromCallstackListFile(arguments.source_file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
svg.layout(root, lambda n: n.ticks)
 | 
			
		||||
svg.display(root, "Flame Graph", "hot", flip = True)
 | 
			
		||||
							
								
								
									
										501
									
								
								tools/svg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										501
									
								
								tools/svg.py
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,501 @@
 | 
			
		|||
# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
 | 
			
		||||
 | 
			
		||||
class Node:
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.name = ""
 | 
			
		||||
        self.children = {}
 | 
			
		||||
        # computed
 | 
			
		||||
        self.depth = 0
 | 
			
		||||
        self.width = 0
 | 
			
		||||
        self.offset = 0
 | 
			
		||||
 | 
			
		||||
    def child(self, name):
 | 
			
		||||
        node = self.children.get(name)
 | 
			
		||||
        if not node:
 | 
			
		||||
            node = self.__class__()
 | 
			
		||||
            node.name = name
 | 
			
		||||
            self.children[name] = node
 | 
			
		||||
        return node
 | 
			
		||||
 | 
			
		||||
    def subtree(self):
 | 
			
		||||
        result = [self]
 | 
			
		||||
        offset = 0
 | 
			
		||||
 | 
			
		||||
        while offset < len(result):
 | 
			
		||||
            p = result[offset]
 | 
			
		||||
            offset += 1
 | 
			
		||||
            for c in p.children.values():
 | 
			
		||||
                result.append(c)
 | 
			
		||||
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
def escape(s):
 | 
			
		||||
    return s.replace("&", "&").replace("<", "<").replace(">", ">")
 | 
			
		||||
 | 
			
		||||
def layout(root, widthcb):
 | 
			
		||||
    for n in reversed(root.subtree()):
 | 
			
		||||
        # propagate width to the parent
 | 
			
		||||
        n.width = widthcb(n)
 | 
			
		||||
        for c in n.children.values():
 | 
			
		||||
            n.width += c.width
 | 
			
		||||
 | 
			
		||||
        # compute offset from parent for every child in width order (layout order)
 | 
			
		||||
        offset = 0
 | 
			
		||||
        for c in sorted(n.children.values(), key = lambda x: x.width, reverse = True):
 | 
			
		||||
            c.offset = offset
 | 
			
		||||
            offset += c.width
 | 
			
		||||
 | 
			
		||||
    for n in root.subtree():
 | 
			
		||||
        for c in n.children.values():
 | 
			
		||||
            c.depth = n.depth + 1
 | 
			
		||||
            c.offset += n.offset
 | 
			
		||||
 | 
			
		||||
# svg template (stolen from framegraph.pl)
 | 
			
		||||
template = r"""<?xml version="1.0" standalone="no"?>
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 | 
			
		||||
<svg version="1.1" width="1200" height="$height" onload="init(evt)" viewBox="0 0 1200 $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
			
		||||
<!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. -->
 | 
			
		||||
<defs>
 | 
			
		||||
    <linearGradient id="background" y1="0" y2="1" x1="0" x2="0" >
 | 
			
		||||
        <stop stop-color="$gradient-start" offset="5%" />
 | 
			
		||||
        <stop stop-color="$gradient-end" offset="95%" />
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
</defs>
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
    text { font-family:Verdana; font-size:12px; fill:rgb(0,0,0); }
 | 
			
		||||
    #search, #ignorecase { opacity:0.1; cursor:pointer; }
 | 
			
		||||
    #search:hover, #search.show, #ignorecase:hover, #ignorecase.show { opacity:1; }
 | 
			
		||||
    #subtitle { text-anchor:middle; font-color:rgb(160,160,160); }
 | 
			
		||||
    #title { text-anchor:middle; font-size:17px}
 | 
			
		||||
    #unzoom { cursor:pointer; }
 | 
			
		||||
    #frames > *:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
 | 
			
		||||
    .hide { display:none; }
 | 
			
		||||
    .parent { opacity:0.5; }
 | 
			
		||||
</style>
 | 
			
		||||
<script type="text/ecmascript">
 | 
			
		||||
<![CDATA[
 | 
			
		||||
    "use strict";
 | 
			
		||||
    var details, searchbtn, unzoombtn, matchedtxt, svg, searching, currentSearchTerm, ignorecase, ignorecaseBtn;
 | 
			
		||||
    function init(evt) {
 | 
			
		||||
        details = document.getElementById("details").firstChild;
 | 
			
		||||
        searchbtn = document.getElementById("search");
 | 
			
		||||
        ignorecaseBtn = document.getElementById("ignorecase");
 | 
			
		||||
        unzoombtn = document.getElementById("unzoom");
 | 
			
		||||
        matchedtxt = document.getElementById("matched");
 | 
			
		||||
        svg = document.getElementsByTagName("svg")[0];
 | 
			
		||||
        searching = 0;
 | 
			
		||||
        currentSearchTerm = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.addEventListener("click", function(e) {
 | 
			
		||||
        var target = find_group(e.target);
 | 
			
		||||
        if (target) {
 | 
			
		||||
            if (target.nodeName == "a") {
 | 
			
		||||
                if (e.ctrlKey === false) return;
 | 
			
		||||
                e.preventDefault();
 | 
			
		||||
            }
 | 
			
		||||
            if (target.classList.contains("parent")) unzoom();
 | 
			
		||||
            zoom(target);
 | 
			
		||||
        }
 | 
			
		||||
        else if (e.target.id == "unzoom") unzoom();
 | 
			
		||||
        else if (e.target.id == "search") search_prompt();
 | 
			
		||||
        else if (e.target.id == "ignorecase") toggle_ignorecase();
 | 
			
		||||
    }, false)
 | 
			
		||||
 | 
			
		||||
    // mouse-over for info
 | 
			
		||||
    // show
 | 
			
		||||
    window.addEventListener("mouseover", function(e) {
 | 
			
		||||
        var target = find_group(e.target);
 | 
			
		||||
        if (target) details.nodeValue = g_to_text(target);
 | 
			
		||||
    }, false)
 | 
			
		||||
 | 
			
		||||
    // clear
 | 
			
		||||
    window.addEventListener("mouseout", function(e) {
 | 
			
		||||
        var target = find_group(e.target);
 | 
			
		||||
        if (target) details.nodeValue = ' ';
 | 
			
		||||
    }, false)
 | 
			
		||||
 | 
			
		||||
    // ctrl-F for search
 | 
			
		||||
    window.addEventListener("keydown",function (e) {
 | 
			
		||||
        if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            search_prompt();
 | 
			
		||||
        }
 | 
			
		||||
    }, false)
 | 
			
		||||
 | 
			
		||||
    // ctrl-I to toggle case-sensitive search
 | 
			
		||||
    window.addEventListener("keydown",function (e) {
 | 
			
		||||
        if (e.ctrlKey && e.keyCode === 73) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            toggle_ignorecase();
 | 
			
		||||
        }
 | 
			
		||||
    }, false)
 | 
			
		||||
 | 
			
		||||
    // functions
 | 
			
		||||
    function find_child(node, selector) {
 | 
			
		||||
        var children = node.querySelectorAll(selector);
 | 
			
		||||
        if (children.length) return children[0];
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    function find_group(node) {
 | 
			
		||||
        var parent = node.parentElement;
 | 
			
		||||
        if (!parent) return;
 | 
			
		||||
        if (parent.id == "frames") return node;
 | 
			
		||||
        return find_group(parent);
 | 
			
		||||
    }
 | 
			
		||||
    function orig_save(e, attr, val) {
 | 
			
		||||
        if (e.attributes["_orig_" + attr] != undefined) return;
 | 
			
		||||
        if (e.attributes[attr] == undefined) return;
 | 
			
		||||
        if (val == undefined) val = e.attributes[attr].value;
 | 
			
		||||
        e.setAttribute("_orig_" + attr, val);
 | 
			
		||||
    }
 | 
			
		||||
    function orig_load(e, attr) {
 | 
			
		||||
        if (e.attributes["_orig_"+attr] == undefined) return;
 | 
			
		||||
        e.attributes[attr].value = e.attributes["_orig_" + attr].value;
 | 
			
		||||
        e.removeAttribute("_orig_"+attr);
 | 
			
		||||
    }
 | 
			
		||||
    function g_to_text(e) {
 | 
			
		||||
        var text = find_child(e, "details").firstChild.nodeValue;
 | 
			
		||||
        return (text)
 | 
			
		||||
    }
 | 
			
		||||
    function g_to_func(e) {
 | 
			
		||||
        var child = find_child(e, "rawtext");
 | 
			
		||||
        return child ? child.textContent : null;
 | 
			
		||||
    }
 | 
			
		||||
    function update_text(e) {
 | 
			
		||||
        var r = find_child(e, "rect");
 | 
			
		||||
        var t = find_child(e, "text");
 | 
			
		||||
        var w = parseFloat(r.attributes.width.value) -3;
 | 
			
		||||
        var txt = find_child(e, "rawtext").textContent.replace(/\([^(]*\)$/,"");
 | 
			
		||||
        t.attributes.x.value = parseFloat(r.attributes.x.value) + 3;
 | 
			
		||||
 | 
			
		||||
        // Smaller than this size won't fit anything
 | 
			
		||||
        if (w < 2 * 12 * 0.59) {
 | 
			
		||||
            t.textContent = "";
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        t.textContent = txt;
 | 
			
		||||
        // Fit in full text width
 | 
			
		||||
        if (/^ *$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        for (var x = txt.length - 2; x > 0; x--) {
 | 
			
		||||
            if (t.getSubStringLength(0, x + 2) <= w) {
 | 
			
		||||
                t.textContent = txt.substring(0, x) + "..";
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        t.textContent = "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // zoom
 | 
			
		||||
    function zoom_reset(e) {
 | 
			
		||||
        if (e.attributes != undefined) {
 | 
			
		||||
            orig_load(e, "x");
 | 
			
		||||
            orig_load(e, "width");
 | 
			
		||||
        }
 | 
			
		||||
        if (e.childNodes == undefined) return;
 | 
			
		||||
        for (var i = 0, c = e.childNodes; i < c.length; i++) {
 | 
			
		||||
            zoom_reset(c[i]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    function zoom_child(e, x, ratio) {
 | 
			
		||||
        if (e.attributes != undefined) {
 | 
			
		||||
            if (e.attributes.x != undefined) {
 | 
			
		||||
                orig_save(e, "x");
 | 
			
		||||
                e.attributes.x.value = (parseFloat(e.attributes.x.value) - x - 10) * ratio + 10;
 | 
			
		||||
                if (e.tagName == "text")
 | 
			
		||||
                    e.attributes.x.value = find_child(e.parentNode, "rect[x]").attributes.x.value + 3;
 | 
			
		||||
            }
 | 
			
		||||
            if (e.attributes.width != undefined) {
 | 
			
		||||
                orig_save(e, "width");
 | 
			
		||||
                e.attributes.width.value = parseFloat(e.attributes.width.value) * ratio;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (e.childNodes == undefined) return;
 | 
			
		||||
        for (var i = 0, c = e.childNodes; i < c.length; i++) {
 | 
			
		||||
            zoom_child(c[i], x - 10, ratio);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    function zoom_parent(e) {
 | 
			
		||||
        if (e.attributes) {
 | 
			
		||||
            if (e.attributes.x != undefined) {
 | 
			
		||||
                orig_save(e, "x");
 | 
			
		||||
                e.attributes.x.value = 10;
 | 
			
		||||
            }
 | 
			
		||||
            if (e.attributes.width != undefined) {
 | 
			
		||||
                orig_save(e, "width");
 | 
			
		||||
                e.attributes.width.value = parseInt(svg.width.baseVal.value) - (10 * 2);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (e.childNodes == undefined) return;
 | 
			
		||||
        for (var i = 0, c = e.childNodes; i < c.length; i++) {
 | 
			
		||||
            zoom_parent(c[i]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    function zoom(node) {
 | 
			
		||||
        var attr = find_child(node, "rect").attributes;
 | 
			
		||||
        var width = parseFloat(attr.width.value);
 | 
			
		||||
        var xmin = parseFloat(attr.x.value);
 | 
			
		||||
        var xmax = parseFloat(xmin + width);
 | 
			
		||||
        var ymin = parseFloat(attr.y.value);
 | 
			
		||||
        var ratio = (svg.width.baseVal.value - 2 * 10) / width;
 | 
			
		||||
 | 
			
		||||
        // XXX: Workaround for JavaScript float issues (fix me)
 | 
			
		||||
        var fudge = 0.0001;
 | 
			
		||||
 | 
			
		||||
        unzoombtn.classList.remove("hide");
 | 
			
		||||
 | 
			
		||||
        var el = document.getElementById("frames").children;
 | 
			
		||||
        for (var i = 0; i < el.length; i++) {
 | 
			
		||||
            var e = el[i];
 | 
			
		||||
            var a = find_child(e, "rect").attributes;
 | 
			
		||||
            var ex = parseFloat(a.x.value);
 | 
			
		||||
            var ew = parseFloat(a.width.value);
 | 
			
		||||
            var upstack;
 | 
			
		||||
            // Is it an ancestor
 | 
			
		||||
            if ($flip == 1) {
 | 
			
		||||
                upstack = parseFloat(a.y.value) > ymin;
 | 
			
		||||
            } else {
 | 
			
		||||
                upstack = parseFloat(a.y.value) < ymin;
 | 
			
		||||
            }
 | 
			
		||||
            if (upstack) {
 | 
			
		||||
                // Direct ancestor
 | 
			
		||||
                if (ex <= xmin && (ex+ew+fudge) >= xmax) {
 | 
			
		||||
                    e.classList.add("parent");
 | 
			
		||||
                    zoom_parent(e);
 | 
			
		||||
                    update_text(e);
 | 
			
		||||
                }
 | 
			
		||||
                // not in current path
 | 
			
		||||
                else
 | 
			
		||||
                    e.classList.add("hide");
 | 
			
		||||
            }
 | 
			
		||||
            // Children maybe
 | 
			
		||||
            else {
 | 
			
		||||
                // no common path
 | 
			
		||||
                if (ex < xmin || ex + fudge >= xmax) {
 | 
			
		||||
                    e.classList.add("hide");
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    zoom_child(e, xmin, ratio);
 | 
			
		||||
                    update_text(e);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        search();
 | 
			
		||||
    }
 | 
			
		||||
    function unzoom() {
 | 
			
		||||
        unzoombtn.classList.add("hide");
 | 
			
		||||
        var el = document.getElementById("frames").children;
 | 
			
		||||
        for(var i = 0; i < el.length; i++) {
 | 
			
		||||
            el[i].classList.remove("parent");
 | 
			
		||||
            el[i].classList.remove("hide");
 | 
			
		||||
            zoom_reset(el[i]);
 | 
			
		||||
            update_text(el[i]);
 | 
			
		||||
        }
 | 
			
		||||
        search();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // search
 | 
			
		||||
    function toggle_ignorecase() {
 | 
			
		||||
        ignorecase = !ignorecase;
 | 
			
		||||
        if (ignorecase) {
 | 
			
		||||
            ignorecaseBtn.classList.add("show");
 | 
			
		||||
        } else {
 | 
			
		||||
            ignorecaseBtn.classList.remove("show");
 | 
			
		||||
        }
 | 
			
		||||
        reset_search();
 | 
			
		||||
        search();
 | 
			
		||||
    }
 | 
			
		||||
    function reset_search() {
 | 
			
		||||
        var el = document.querySelectorAll("#frames rect");
 | 
			
		||||
        for (var i = 0; i < el.length; i++) {
 | 
			
		||||
            orig_load(el[i], "fill")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    function search_prompt() {
 | 
			
		||||
        if (!searching) {
 | 
			
		||||
            var term = prompt("Enter a search term (regexp " +
 | 
			
		||||
                "allowed, eg: ^ext4_)"
 | 
			
		||||
                + (ignorecase ? ", ignoring case" : "")
 | 
			
		||||
                + "\nPress Ctrl-i to toggle case sensitivity", "");
 | 
			
		||||
            if (term != null) {
 | 
			
		||||
                currentSearchTerm = term;
 | 
			
		||||
                search();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            reset_search();
 | 
			
		||||
            searching = 0;
 | 
			
		||||
            currentSearchTerm = null;
 | 
			
		||||
            searchbtn.classList.remove("show");
 | 
			
		||||
            searchbtn.firstChild.nodeValue = "Search"
 | 
			
		||||
            matchedtxt.classList.add("hide");
 | 
			
		||||
            matchedtxt.firstChild.nodeValue = ""
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    function search(term) {
 | 
			
		||||
        if (currentSearchTerm === null) return;
 | 
			
		||||
        var term = currentSearchTerm;
 | 
			
		||||
 | 
			
		||||
        var re = new RegExp(term, ignorecase ? 'i' : '');
 | 
			
		||||
        var el = document.getElementById("frames").children;
 | 
			
		||||
        var matches = new Object();
 | 
			
		||||
        var maxwidth = 0;
 | 
			
		||||
        for (var i = 0; i < el.length; i++) {
 | 
			
		||||
            var e = el[i];
 | 
			
		||||
            var func = g_to_func(e);
 | 
			
		||||
            var rect = find_child(e, "rect");
 | 
			
		||||
            if (func == null || rect == null)
 | 
			
		||||
                continue;
 | 
			
		||||
 | 
			
		||||
            // Save max width. Only works as we have a root frame
 | 
			
		||||
            var w = parseFloat(rect.attributes.width.value);
 | 
			
		||||
            if (w > maxwidth)
 | 
			
		||||
                maxwidth = w;
 | 
			
		||||
 | 
			
		||||
            if (func.match(re)) {
 | 
			
		||||
                // highlight
 | 
			
		||||
                var x = parseFloat(rect.attributes.x.value);
 | 
			
		||||
                orig_save(rect, "fill");
 | 
			
		||||
                rect.attributes.fill.value = "rgb(230,0,230)";
 | 
			
		||||
 | 
			
		||||
                // remember matches
 | 
			
		||||
                if (matches[x] == undefined) {
 | 
			
		||||
                    matches[x] = w;
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (w > matches[x]) {
 | 
			
		||||
                        // overwrite with parent
 | 
			
		||||
                        matches[x] = w;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                searching = 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!searching)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        searchbtn.classList.add("show");
 | 
			
		||||
        searchbtn.firstChild.nodeValue = "Reset Search";
 | 
			
		||||
 | 
			
		||||
        // calculate percent matched, excluding vertical overlap
 | 
			
		||||
        var count = 0;
 | 
			
		||||
        var lastx = -1;
 | 
			
		||||
        var lastw = 0;
 | 
			
		||||
        var keys = Array();
 | 
			
		||||
        for (k in matches) {
 | 
			
		||||
            if (matches.hasOwnProperty(k))
 | 
			
		||||
                keys.push(k);
 | 
			
		||||
        }
 | 
			
		||||
        // sort the matched frames by their x location
 | 
			
		||||
        // ascending, then width descending
 | 
			
		||||
        keys.sort(function(a, b){
 | 
			
		||||
            return a - b;
 | 
			
		||||
        });
 | 
			
		||||
        // Step through frames saving only the biggest bottom-up frames
 | 
			
		||||
        // thanks to the sort order. This relies on the tree property
 | 
			
		||||
        // where children are always smaller than their parents.
 | 
			
		||||
        var fudge = 0.0001; // JavaScript floating point
 | 
			
		||||
        for (var k in keys) {
 | 
			
		||||
            var x = parseFloat(keys[k]);
 | 
			
		||||
            var w = matches[keys[k]];
 | 
			
		||||
            if (x >= lastx + lastw - fudge) {
 | 
			
		||||
                count += w;
 | 
			
		||||
                lastx = x;
 | 
			
		||||
                lastw = w;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // display matched percent
 | 
			
		||||
        matchedtxt.classList.remove("hide");
 | 
			
		||||
        var pct = 100 * count / maxwidth;
 | 
			
		||||
        if (pct != 100) pct = pct.toFixed(1)
 | 
			
		||||
        matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
 | 
			
		||||
    }
 | 
			
		||||
]]>
 | 
			
		||||
</script>
 | 
			
		||||
<rect x="0.0" y="0" width="1200.0" height="$height.0" fill="url(#background)"  />
 | 
			
		||||
<text id="title" x="600.00" y="24" >$title</text>
 | 
			
		||||
<text id="unzoom" x="10.00" y="24" class="hide">Reset Zoom</text>
 | 
			
		||||
<text id="search" x="1090.00" y="24" >Search</text>
 | 
			
		||||
<text id="ignorecase" x="1174.00" y="24" >ic</text>
 | 
			
		||||
<text id="matched" x="1090.00" y="$status" > </text>
 | 
			
		||||
<text id="details" x="10.00" y="$status" > </text>
 | 
			
		||||
<g id="frames">
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
def namehash(s):
 | 
			
		||||
    # FNV-1a
 | 
			
		||||
    hval = 0x811c9dc5
 | 
			
		||||
    for ch in s:
 | 
			
		||||
        hval = hval ^ ord(ch)
 | 
			
		||||
        hval = hval * 0x01000193
 | 
			
		||||
        hval = hval % (2 ** 32)
 | 
			
		||||
    return (hval % 31337) / 31337.0
 | 
			
		||||
 | 
			
		||||
def display(root, title, colors, flip = False):
 | 
			
		||||
    if colors == "cold":
 | 
			
		||||
        gradient_start = "#eef2ee"
 | 
			
		||||
        gradient_end = "#e0ffe0"
 | 
			
		||||
    else:
 | 
			
		||||
        gradient_start = "#eeeeee"
 | 
			
		||||
        gradient_end = "#eeeeb0"
 | 
			
		||||
 | 
			
		||||
    maxdepth = 0
 | 
			
		||||
    for n in root.subtree():
 | 
			
		||||
        maxdepth = max(maxdepth, n.depth)
 | 
			
		||||
 | 
			
		||||
    svgheight = maxdepth * 16 + 3 * 16 + 2 * 16
 | 
			
		||||
 | 
			
		||||
    print(template
 | 
			
		||||
        .replace("$title", title)
 | 
			
		||||
        .replace("$gradient-start", gradient_start)
 | 
			
		||||
        .replace("$gradient-end", gradient_end)
 | 
			
		||||
        .replace("$height", str(svgheight))
 | 
			
		||||
        .replace("$status", str((svgheight - 16 + 3 if flip else 3 * 16 - 3)))
 | 
			
		||||
        .replace("$flip", str(int(flip)))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    framewidth = 1200 - 20
 | 
			
		||||
 | 
			
		||||
    def pixels(x):
 | 
			
		||||
        return float(x) / root.width * framewidth if root.width > 0 else 0
 | 
			
		||||
 | 
			
		||||
    for n in root.subtree():
 | 
			
		||||
        if pixels(n.width) < 0.1:
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        x = 10 + pixels(n.offset)
 | 
			
		||||
        y = (maxdepth - 1 - n.depth if flip else n.depth) * 16 + 3 * 16
 | 
			
		||||
        width = pixels(n.width)
 | 
			
		||||
        height = 15
 | 
			
		||||
 | 
			
		||||
        if colors == "cold":
 | 
			
		||||
            fillr = 0
 | 
			
		||||
            fillg = int(190 + 50 * namehash(n.name))
 | 
			
		||||
            fillb = int(210 * namehash(n.name[::-1]))
 | 
			
		||||
        else:
 | 
			
		||||
            fillr = int(205 + 50 * namehash(n.name))
 | 
			
		||||
            fillg = int(230 * namehash(n.name[::-1]))
 | 
			
		||||
            fillb = int(55 * namehash(n.name[::-2]))
 | 
			
		||||
 | 
			
		||||
        fill = "rgb({},{},{})".format(fillr, fillg, fillb)
 | 
			
		||||
        chars = width / (12 * 0.59)
 | 
			
		||||
 | 
			
		||||
        text = n.text()
 | 
			
		||||
 | 
			
		||||
        if chars >= 3:
 | 
			
		||||
            if chars < len(text):
 | 
			
		||||
                text = text[:int(chars-2)] + ".."
 | 
			
		||||
        else:
 | 
			
		||||
            text = ""
 | 
			
		||||
 | 
			
		||||
        print("<g>")
 | 
			
		||||
        print("<title>{}</title>".format(escape(n.title())))
 | 
			
		||||
        print("<details>{}</details>".format(escape(n.details(root))))
 | 
			
		||||
        print("<rect x='{}' y='{}' width='{}' height='{}' fill='{}' rx='2' ry='2' />".format(x, y, width, height, fill))
 | 
			
		||||
        print("<text x='{}' y='{}'>{}</text>".format(x + 3, y + 10.5, escape(text)))
 | 
			
		||||
        print("<rawtext>{}</rawtext>".format(escape(n.text())))
 | 
			
		||||
        print("</g>")
 | 
			
		||||
 | 
			
		||||
    print("</g>\n</svg>\n")
 | 
			
		||||
		Loading…
	
		Reference in a new issue