mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +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
|
.vitepress/dist
|
||||||
|
|
||||||
# Luau tools
|
# Luau tools
|
||||||
/tools
|
|
||||||
profile.*
|
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.
|
- 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
|
- 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.
|
- This was an issue with the iterator being invalidated when you move an entity to a different archetype.
|
||||||
|
|
202
jecs.luau
202
jecs.luau
|
@ -275,7 +275,7 @@ local function query_match(query, archetype: Archetype)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function find_observers(world: World, event, component): { Observer }?
|
local function find_observers(world: World, event, component): { Observer }?
|
||||||
local cache = world.observerable[event]
|
local cache = world.observable[event]
|
||||||
if not cache then
|
if not cache then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
@ -471,7 +471,7 @@ local function world_target(world: World, entity: i53, relation: i24, index: num
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)]
|
local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)]
|
||||||
if not idr then
|
if not idr then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
@ -502,8 +502,8 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean
|
||||||
end
|
end
|
||||||
|
|
||||||
local function id_record_ensure(world: World, id: number): IdRecord
|
local function id_record_ensure(world: World, id: number): IdRecord
|
||||||
local componentIndex = world.componentIndex
|
local component_index = world.component_index
|
||||||
local idr: IdRecord = componentIndex[id]
|
local idr: IdRecord = component_index[id]
|
||||||
|
|
||||||
if not idr then
|
if not idr then
|
||||||
local flags = ECS_ID_MASK
|
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
|
end
|
||||||
|
|
||||||
return idr
|
return idr
|
||||||
|
@ -575,8 +575,8 @@ local function archetype_append_to_records(
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype
|
local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype
|
||||||
local archetype_id = (world.nextArchetypeId :: number) + 1
|
local archetype_id = (world.max_archetype_id :: number) + 1
|
||||||
world.nextArchetypeId = archetype_id
|
world.max_archetype_id = archetype_id
|
||||||
|
|
||||||
local length = #id_types
|
local length = #id_types
|
||||||
local columns = (table.create(length) :: any) :: { Column }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
world.archetypeIndex[ty] = archetype
|
world.archetype_index[ty] = archetype
|
||||||
world.archetypes[archetype_id] = archetype
|
world.archetypes[archetype_id] = archetype
|
||||||
|
|
||||||
return archetype
|
return archetype
|
||||||
|
@ -652,7 +652,7 @@ local function archetype_ensure(world: World, id_types): Archetype
|
||||||
end
|
end
|
||||||
|
|
||||||
local ty = hash(id_types)
|
local ty = hash(id_types)
|
||||||
local archetype = world.archetypeIndex[ty]
|
local archetype = world.archetype_index[ty]
|
||||||
if archetype then
|
if archetype then
|
||||||
return archetype
|
return archetype
|
||||||
end
|
end
|
||||||
|
@ -814,7 +814,7 @@ local function world_add(world: World, entity: i53, id: i53): ()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local idr = world.componentIndex[id]
|
local idr = world.component_index[id]
|
||||||
local on_add = idr.hooks.on_add
|
local on_add = idr.hooks.on_add
|
||||||
|
|
||||||
if on_add then
|
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 from: Archetype = record.archetype
|
||||||
local to: Archetype = archetype_traverse_add(world, id, from)
|
local to: Archetype = archetype_traverse_add(world, id, from)
|
||||||
local idr = world.componentIndex[id]
|
local idr = world.component_index[id]
|
||||||
local flags = idr.flags
|
|
||||||
local is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
|
|
||||||
local idr_hooks = idr.hooks
|
local idr_hooks = idr.hooks
|
||||||
|
|
||||||
if from == to then
|
if from == to then
|
||||||
if is_tag then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
-- If the archetypes are the same it can avoid moving the entity
|
-- If the archetypes are the same it can avoid moving the entity
|
||||||
-- and just set the data directly.
|
-- and just set the data directly.
|
||||||
local tr = to.records[id]
|
local tr = to.records[id]
|
||||||
|
@ -868,10 +863,6 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
|
||||||
on_add(entity)
|
on_add(entity)
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_tag then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local tr = to.records[id]
|
local tr = to.records[id]
|
||||||
local column = to.columns[tr.column]
|
local column = to.columns[tr.column]
|
||||||
|
|
||||||
|
@ -884,15 +875,15 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function world_component(world: World): i53
|
local function world_component(world: World): i53
|
||||||
local componentId = (world.nextComponentId :: number) + 1
|
local id = (world.max_component_id :: number) + 1
|
||||||
if componentId > HI_COMPONENT_ID then
|
if id > HI_COMPONENT_ID then
|
||||||
-- IDs are partitioned into ranges because component IDs are not nominal,
|
-- IDs are partitioned into ranges because component IDs are not nominal,
|
||||||
-- so it needs to error when IDs intersect into the entity range.
|
-- so it needs to error when IDs intersect into the entity range.
|
||||||
error("Too many components, consider using world:entity() instead to create components.")
|
error("Too many components, consider using world:entity() instead to create components.")
|
||||||
end
|
end
|
||||||
world.nextComponentId = componentId
|
world.max_component_id = id
|
||||||
|
|
||||||
return componentId
|
return id
|
||||||
end
|
end
|
||||||
|
|
||||||
local function world_remove(world: World, entity: i53, id: i53)
|
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)
|
local to = archetype_traverse_remove(world, id, from)
|
||||||
|
|
||||||
if from and not (from == to) then
|
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
|
local on_remove = idr.hooks.on_remove
|
||||||
if on_remove then
|
if on_remove then
|
||||||
on_remove(entity)
|
on_remove(entity)
|
||||||
|
@ -937,28 +928,27 @@ local function archetype_fast_delete(columns: { Column }, column_count: number,
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?)
|
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 columns = archetype.columns
|
||||||
local id_types = archetype.types
|
local id_types = archetype.types
|
||||||
local entities = archetype.entities
|
local entities = archetype.entities
|
||||||
local column_count = #entities
|
local column_count = #entities
|
||||||
local last = #entities
|
local last = #entities
|
||||||
local move = entities[last]
|
local move = entities[last]
|
||||||
local delete = entities[row]
|
-- We assume first that the entity is the last in the archetype
|
||||||
entities[row] = move
|
local delete = move
|
||||||
entities[last] = nil :: any
|
|
||||||
|
|
||||||
if row ~= last then
|
if row ~= last then
|
||||||
-- TODO: should be "entity_index_sparse_get(entityIndex, move)"
|
local record_to_move = entity_index_try_get_any(entity_index, move)
|
||||||
local record_to_move = entity_index_try_get_any(entityIndex, move)
|
|
||||||
if record_to_move then
|
if record_to_move then
|
||||||
record_to_move.row = row
|
record_to_move.row = row
|
||||||
end
|
end
|
||||||
|
|
||||||
|
entities[row] = move
|
||||||
|
delete = entities[row]
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: if last == 0 then deactivate table
|
|
||||||
|
|
||||||
local component_index = world.componentIndex
|
|
||||||
for _, id in id_types do
|
for _, id in id_types do
|
||||||
local idr = component_index[id]
|
local idr = component_index[id]
|
||||||
local on_remove = idr.hooks.on_remove
|
local on_remove = idr.hooks.on_remove
|
||||||
|
@ -967,6 +957,8 @@ local function archetype_delete(world: World, archetype: Archetype, row: number,
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
entities[last] = nil :: any
|
||||||
|
|
||||||
if row == last then
|
if row == last then
|
||||||
archetype_fast_delete_last(columns, column_count, id_types, delete)
|
archetype_fast_delete_last(columns, column_count, id_types, delete)
|
||||||
else
|
else
|
||||||
|
@ -1048,11 +1040,11 @@ local function archetype_destroy(world: World, archetype: Archetype)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local component_index = world.componentIndex
|
local component_index = world.component_index
|
||||||
archetype_clear_edges(archetype)
|
archetype_clear_edges(archetype)
|
||||||
local archetype_id = archetype.id
|
local archetype_id = archetype.id
|
||||||
world.archetypes[archetype_id] = nil :: any
|
world.archetypes[archetype_id] = nil :: any
|
||||||
world.archetypeIndex[archetype.type] = nil :: any
|
world.archetype_index[archetype.type] = nil :: any
|
||||||
local records = archetype.records
|
local records = archetype.records
|
||||||
|
|
||||||
for id in records do
|
for id in records do
|
||||||
|
@ -1096,7 +1088,7 @@ local function world_cleanup(world: World)
|
||||||
end
|
end
|
||||||
|
|
||||||
world.archetypes = new_archetypes
|
world.archetypes = new_archetypes
|
||||||
world.archetypeIndex = new_archetype_map
|
world.archetype_index = new_archetype_map
|
||||||
end
|
end
|
||||||
|
|
||||||
local world_delete: (world: World, entity: i53, destruct: boolean?) -> ()
|
local world_delete: (world: World, entity: i53, destruct: boolean?) -> ()
|
||||||
|
@ -1118,7 +1110,7 @@ do
|
||||||
end
|
end
|
||||||
|
|
||||||
local delete = entity
|
local delete = entity
|
||||||
local component_index = world.componentIndex
|
local component_index = world.component_index
|
||||||
local archetypes: Archetypes = world.archetypes
|
local archetypes: Archetypes = world.archetypes
|
||||||
local tgt = ECS_PAIR(EcsWildcard, delete)
|
local tgt = ECS_PAIR(EcsWildcard, delete)
|
||||||
local idr_t = component_index[tgt]
|
local idr_t = component_index[tgt]
|
||||||
|
@ -1135,6 +1127,8 @@ do
|
||||||
for i = n, 1, -1 do
|
for i = n, 1, -1 do
|
||||||
world_delete(world, entities[i])
|
world_delete(world, entities[i])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
archetype_destroy(world, idr_archetype)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
for archetype_id in idr.cache do
|
for archetype_id in idr.cache do
|
||||||
|
@ -1144,10 +1138,7 @@ do
|
||||||
for i = n, 1, -1 do
|
for i = n, 1, -1 do
|
||||||
world_remove(world, entities[i], delete)
|
world_remove(world, entities[i], delete)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
for archetype_id in idr.cache do
|
|
||||||
local idr_archetype = archetypes[archetype_id]
|
|
||||||
archetype_destroy(world, idr_archetype)
|
archetype_destroy(world, idr_archetype)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1164,6 +1155,8 @@ do
|
||||||
table.insert(children, child)
|
table.insert(children, child)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local n = #children
|
||||||
|
|
||||||
for _, id in idr_t_types do
|
for _, id in idr_t_types do
|
||||||
if not ECS_IS_PAIR(id) then
|
if not ECS_IS_PAIR(id) then
|
||||||
continue
|
continue
|
||||||
|
@ -1174,22 +1167,30 @@ do
|
||||||
local flags = id_record.flags
|
local flags = id_record.flags
|
||||||
local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE)
|
local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE)
|
||||||
if flags_delete_mask ~= 0 then
|
if flags_delete_mask ~= 0 then
|
||||||
for _, child in children do
|
for i = n, 1, -1 do
|
||||||
-- Cascade deletions of it has Delete as component trait
|
world_delete(world, children[i])
|
||||||
world_delete(world, child, destruct)
|
|
||||||
end
|
end
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
local on_remove = id_record.hooks.on_remove
|
local on_remove = id_record.hooks.on_remove
|
||||||
local to = archetype_traverse_remove(world, id, idr_t_archetype)
|
local to = archetype_traverse_remove(world, id, idr_t_archetype)
|
||||||
if on_remove then
|
if on_remove then
|
||||||
for _, child in children do
|
if to then
|
||||||
on_remove(child)
|
for i = n, 1, -1 do
|
||||||
local r = entity_index_try_get_fast(entity_index, child) :: Record
|
local child = children[i]
|
||||||
entity_move(entity_index, child, r, to)
|
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 i = n, 1, -1 do
|
||||||
|
local child = children[i]
|
||||||
|
on_remove(child)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
elseif to then
|
||||||
for _, child in children do
|
for i = n, 1, -1 do
|
||||||
|
local child = children[i]
|
||||||
local r = entity_index_try_get_fast(entity_index, child) :: Record
|
local r = entity_index_try_get_fast(entity_index, child) :: Record
|
||||||
entity_move(entity_index, child, r, to)
|
entity_move(entity_index, child, r, to)
|
||||||
end
|
end
|
||||||
|
@ -1608,14 +1609,14 @@ local function query_cached(query: QueryInner)
|
||||||
local records: { ArchetypeRecord }
|
local records: { ArchetypeRecord }
|
||||||
local archetypes = query.compatible_archetypes
|
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
|
-- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively
|
||||||
-- because the event will be emitted for all components of that Archetype.
|
-- because the event will be emitted for all components of that Archetype.
|
||||||
local observerable = world.observerable
|
local observable = world.observable :: Observable
|
||||||
local on_create_action = observerable[EcsOnArchetypeCreate]
|
local on_create_action = observable[EcsOnArchetypeCreate]
|
||||||
if not on_create_action then
|
if not on_create_action then
|
||||||
on_create_action = {}
|
on_create_action = {}
|
||||||
observerable[EcsOnArchetypeCreate] = on_create_action
|
observable[EcsOnArchetypeCreate] = on_create_action
|
||||||
end
|
end
|
||||||
local query_cache_on_create = on_create_action[A]
|
local query_cache_on_create = on_create_action[A]
|
||||||
if not query_cache_on_create then
|
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
|
on_create_action[A] = query_cache_on_create
|
||||||
end
|
end
|
||||||
|
|
||||||
local on_delete_action = observerable[EcsOnArchetypeDelete]
|
local on_delete_action = observable[EcsOnArchetypeDelete]
|
||||||
if not on_delete_action then
|
if not on_delete_action then
|
||||||
on_delete_action = {}
|
on_delete_action = {}
|
||||||
observerable[EcsOnArchetypeDelete] = on_delete_action
|
observable[EcsOnArchetypeDelete] = on_delete_action
|
||||||
end
|
end
|
||||||
local query_cache_on_delete = on_delete_action[A]
|
local query_cache_on_delete = on_delete_action[A]
|
||||||
if not query_cache_on_delete then
|
if not query_cache_on_delete then
|
||||||
|
@ -1920,7 +1921,7 @@ local function world_query(world: World, ...)
|
||||||
local archetypes = world.archetypes
|
local archetypes = world.archetypes
|
||||||
|
|
||||||
local idr: IdRecord?
|
local idr: IdRecord?
|
||||||
local componentIndex = world.componentIndex
|
local component_index = world.component_index
|
||||||
|
|
||||||
local q = setmetatable({
|
local q = setmetatable({
|
||||||
ids = ids,
|
ids = ids,
|
||||||
|
@ -1929,7 +1930,7 @@ local function world_query(world: World, ...)
|
||||||
}, Query)
|
}, Query)
|
||||||
|
|
||||||
for _, id in ids do
|
for _, id in ids do
|
||||||
local map = componentIndex[id]
|
local map = component_index[id]
|
||||||
if not map then
|
if not map then
|
||||||
return q
|
return q
|
||||||
end
|
end
|
||||||
|
@ -1972,7 +1973,7 @@ local function world_query(world: World, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function world_each(world: World, id): () -> ()
|
local function world_each(world: World, id): () -> ()
|
||||||
local idr = world.componentIndex[id]
|
local idr = world.component_index[id]
|
||||||
if not idr then
|
if not idr then
|
||||||
return NOOP
|
return NOOP
|
||||||
end
|
end
|
||||||
|
@ -2146,15 +2147,16 @@ function World.new()
|
||||||
max_id = 0,
|
max_id = 0,
|
||||||
}
|
}
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
archetypeIndex = {} :: { [string]: Archetype },
|
archetype_index = {} :: { [string]: Archetype },
|
||||||
archetypes = {} :: Archetypes,
|
archetypes = {} :: Archetypes,
|
||||||
componentIndex = {} :: ComponentIndex,
|
component_index = {} :: ComponentIndex,
|
||||||
entity_index = entity_index,
|
entity_index = entity_index,
|
||||||
nextArchetypeId = 0 :: number,
|
|
||||||
nextComponentId = 0 :: number,
|
|
||||||
nextEntityId = 0 :: number,
|
|
||||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||||
observerable = {},
|
|
||||||
|
max_archetype_id = 0,
|
||||||
|
max_component_id = 0,
|
||||||
|
|
||||||
|
observable = {} :: Observable,
|
||||||
}, World) :: any
|
}, World) :: any
|
||||||
|
|
||||||
self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
|
self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
|
||||||
|
@ -2194,6 +2196,8 @@ function World.new()
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
export type Entity<T = nil> = number & { __T: T }
|
||||||
|
|
||||||
export type Id<T = nil> =
|
export type Id<T = nil> =
|
||||||
| Entity<T>
|
| Entity<T>
|
||||||
| Pair<Entity<T>, Entity>
|
| Pair<Entity<T>, Entity>
|
||||||
|
@ -2205,29 +2209,11 @@ export type Pair<P, O> = number & {
|
||||||
__O: O,
|
__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...)
|
type Item<T...> = (self: Query<T...>) -> (Entity, T...)
|
||||||
|
|
||||||
export type Entity<T = nil> = number & { __T: T }
|
|
||||||
|
|
||||||
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
|
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
|
||||||
|
|
||||||
|
|
||||||
export type Query<T...> = typeof(setmetatable({}, {
|
export type Query<T...> = typeof(setmetatable({}, {
|
||||||
__iter = (nil :: any) :: Iter<T...>,
|
__iter = (nil :: any) :: Iter<T...>,
|
||||||
})) & {
|
})) & {
|
||||||
|
@ -2238,30 +2224,31 @@ export type Query<T...> = typeof(setmetatable({}, {
|
||||||
cached: (self: Query<T...>) -> Query<T...>,
|
cached: (self: Query<T...>) -> Query<T...>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Observer = {
|
export type Observer = {
|
||||||
callback: (archetype: Archetype) -> (),
|
callback: (archetype: Archetype) -> (),
|
||||||
query: QueryInner,
|
query: QueryInner,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Observable = {
|
||||||
|
[i53]: {
|
||||||
|
[i53]: {
|
||||||
|
{ Observer }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type World = {
|
export type World = {
|
||||||
archetypeIndex: { [string]: Archetype },
|
archetype_index: { [string]: Archetype },
|
||||||
archetypes: Archetypes,
|
archetypes: Archetypes,
|
||||||
componentIndex: ComponentIndex,
|
component_index: ComponentIndex,
|
||||||
entity_index: EntityIndex,
|
entity_index: EntityIndex,
|
||||||
ROOT_ARCHETYPE: Archetype,
|
ROOT_ARCHETYPE: Archetype,
|
||||||
|
|
||||||
nextComponentId: number,
|
max_component_id: number,
|
||||||
nextEntityId: number,
|
max_archetype_id: number,
|
||||||
nextArchetypeId: number,
|
|
||||||
|
observable: any,
|
||||||
|
|
||||||
observerable: {
|
|
||||||
[i53]: {
|
|
||||||
[i53]: {
|
|
||||||
{ query: QueryInner, callback: (Archetype) -> () }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} & {
|
|
||||||
--- Creates a new entity
|
--- Creates a new entity
|
||||||
entity: (self: World) -> Entity,
|
entity: (self: World) -> Entity,
|
||||||
--- Creates a new entity located in the first 256 ids.
|
--- 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>(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>)
|
& (<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 {
|
return {
|
||||||
World = World :: { new: () -> World },
|
World = World :: { new: () -> World },
|
||||||
|
@ -2370,4 +2373,7 @@ return {
|
||||||
query_with = query_with,
|
query_with = query_with,
|
||||||
query_without = query_without,
|
query_without = query_without,
|
||||||
query_archetypes = query_archetypes,
|
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
|
r.dense = last_entity_alive_at_index
|
||||||
dense[index_of_deleted_entity] = last_alive_entity
|
dense[index_of_deleted_entity] = last_alive_entity
|
||||||
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
|
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
|
||||||
|
print("*dellocated", pe(id))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function alive(e)
|
local function alive(e)
|
||||||
|
|
|
@ -180,6 +180,24 @@ local function CASE(name: string)
|
||||||
table.insert(test.cases, case)
|
table.insert(test.cases, case)
|
||||||
end
|
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?
|
local function CHECK<T>(value: T, stack: number?): T?
|
||||||
assert(test, "no active test")
|
assert(test, "no active test")
|
||||||
|
|
||||||
|
@ -509,7 +527,15 @@ end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
test = function()
|
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,
|
end,
|
||||||
|
|
||||||
benchmark = function()
|
benchmark = function()
|
||||||
|
|
335
test/tests.luau
335
test/tests.luau
|
@ -15,7 +15,11 @@ local entity_index_is_alive = jecs.entity_index_is_alive
|
||||||
local ChildOf = jecs.ChildOf
|
local ChildOf = jecs.ChildOf
|
||||||
local world_new = jecs.World.new
|
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
|
local N = 2 ^ 8
|
||||||
|
|
||||||
|
@ -68,19 +72,6 @@ local function name(world, e)
|
||||||
end
|
end
|
||||||
|
|
||||||
TEST("archetype", function()
|
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_add = jecs.archetype_traverse_add
|
||||||
local archetype_traverse_remove = jecs.archetype_traverse_remove
|
local archetype_traverse_remove = jecs.archetype_traverse_remove
|
||||||
|
|
||||||
|
@ -90,7 +81,7 @@ TEST("archetype", function()
|
||||||
local c2 = world:component()
|
local c2 = world:component()
|
||||||
local c3 = 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)
|
local a2 = archetype_traverse_remove(world, c1, a1)
|
||||||
CHECK(root.add[c1].to == a1)
|
CHECK(root.add[c1].to == a1)
|
||||||
CHECK(root == a2)
|
CHECK(root == a2)
|
||||||
|
@ -116,11 +107,11 @@ TEST("world:cleanup()", function()
|
||||||
world:set(e3, B, true)
|
world:set(e3, B, true)
|
||||||
world:set(e3, C, true)
|
world:set(e3, C, true)
|
||||||
|
|
||||||
local archetypeIndex = world.archetypeIndex
|
local archetype_index = world.archetype_index
|
||||||
|
|
||||||
CHECK(#archetypeIndex["1"].entities == 1)
|
CHECK(#archetype_index["1"].entities == 1)
|
||||||
CHECK(#archetypeIndex["1_2"].entities == 1)
|
CHECK(#archetype_index["1_2"].entities == 1)
|
||||||
CHECK(#archetypeIndex["1_2_3"].entities == 1)
|
CHECK(#archetype_index["1_2_3"].entities == 1)
|
||||||
|
|
||||||
world:delete(e1)
|
world:delete(e1)
|
||||||
world:delete(e2)
|
world:delete(e2)
|
||||||
|
@ -128,25 +119,25 @@ TEST("world:cleanup()", function()
|
||||||
|
|
||||||
world:cleanup()
|
world:cleanup()
|
||||||
|
|
||||||
archetypeIndex = world.archetypeIndex
|
archetype_index = world.archetype_index
|
||||||
|
|
||||||
CHECK((archetypeIndex["1"] :: jecs.Archetype?) == nil)
|
CHECK((archetype_index["1"] :: jecs.Archetype?) == nil)
|
||||||
CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil)
|
CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
|
||||||
CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
|
CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
|
||||||
|
|
||||||
local e4 = world:entity()
|
local e4 = world:entity()
|
||||||
world:set(e4, A, true)
|
world:set(e4, A, true)
|
||||||
CHECK(#archetypeIndex["1"].entities == 1)
|
CHECK(#archetype_index["1"].entities == 1)
|
||||||
CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil)
|
CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil)
|
||||||
CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
|
CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
|
||||||
world:set(e4, B, true)
|
world:set(e4, B, true)
|
||||||
CHECK(#archetypeIndex["1"].entities == 0)
|
CHECK(#archetype_index["1"].entities == 0)
|
||||||
CHECK(#archetypeIndex["1_2"].entities == 1)
|
CHECK(#archetype_index["1_2"].entities == 1)
|
||||||
CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil)
|
CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil)
|
||||||
world:set(e4, C, true)
|
world:set(e4, C, true)
|
||||||
CHECK(#archetypeIndex["1"].entities == 0)
|
CHECK(#archetype_index["1"].entities == 0)
|
||||||
CHECK(#archetypeIndex["1_2"].entities == 0)
|
CHECK(#archetype_index["1_2"].entities == 0)
|
||||||
CHECK(#archetypeIndex["1_2_3"].entities == 1)
|
CHECK(#archetype_index["1_2_3"].entities == 1)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("world:entity()", function()
|
TEST("world:entity()", function()
|
||||||
|
@ -163,14 +154,14 @@ TEST("world:entity()", function()
|
||||||
do
|
do
|
||||||
CASE("generations")
|
CASE("generations")
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local e = world:entity()
|
local e = world:entity() :: number
|
||||||
CHECK(ECS_ID(e) == 1 + jecs.Rest :: number)
|
CHECK(ECS_ID(e) == 1 + jecs.Rest :: number)
|
||||||
CHECK(ECS_GENERATION(e) == 0) -- 0
|
CHECK(ECS_GENERATION(e) == 0) -- 0
|
||||||
e = ECS_GENERATION_INC(e)
|
e = ECS_GENERATION_INC(e)
|
||||||
CHECK(ECS_GENERATION(e) == 1) -- 1
|
CHECK(ECS_GENERATION(e) == 1) -- 1
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE("pairs")
|
do CASE "pairs"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local _e = world:entity()
|
local _e = world:entity()
|
||||||
local e2 = world:entity()
|
local e2 = world:entity()
|
||||||
|
@ -182,8 +173,8 @@ TEST("world:entity()", function()
|
||||||
local p = pair(e2, e3)
|
local p = pair(e2, e3)
|
||||||
CHECK(IS_PAIR(p) == true)
|
CHECK(IS_PAIR(p) == true)
|
||||||
|
|
||||||
CHECK(ecs_pair_first(world, p) == e2)
|
CHECK(ecs_pair_first(world, p) == e2 :: number)
|
||||||
CHECK(ecs_pair_second(world, p) == e3)
|
CHECK(ecs_pair_second(world, p) == e3 :: number)
|
||||||
|
|
||||||
world:delete(e2)
|
world:delete(e2)
|
||||||
local e2v2 = world:entity()
|
local e2v2 = world:entity()
|
||||||
|
@ -199,7 +190,7 @@ TEST("world:entity()", function()
|
||||||
local e1 = world:entity()
|
local e1 = world:entity()
|
||||||
world:delete(e1)
|
world:delete(e1)
|
||||||
local e2 = world:entity()
|
local e2 = world:entity()
|
||||||
CHECK(ECS_ID(e2) == e)
|
CHECK(ECS_ID(e2) == e :: number)
|
||||||
CHECK(ECS_GENERATION(e2) == 2)
|
CHECK(ECS_GENERATION(e2) == 2)
|
||||||
CHECK(world:contains(e2))
|
CHECK(world:contains(e2))
|
||||||
CHECK(not world:contains(e1))
|
CHECK(not world:contains(e1))
|
||||||
|
@ -224,8 +215,7 @@ TEST("world:entity()", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("world:set()", function()
|
TEST("world:set()", function()
|
||||||
do
|
do CASE "archetype move"
|
||||||
CASE("archetype move")
|
|
||||||
do
|
do
|
||||||
local world = jecs.World.new()
|
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
|
-- Should have tuple of fields to the next archetype and set the component data
|
||||||
CHECK(d.tuple(e, 1, 2))
|
CHECK(d.tuple(e, 1, 2))
|
||||||
-- Should have moved the data from the old archetype
|
-- 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
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do CASE "pairs"
|
||||||
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")
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local C1 = world:component()
|
local C1 = world:component()
|
||||||
|
@ -287,7 +262,10 @@ TEST("world:set()", function()
|
||||||
world:set(e, pair(C1, C2), true)
|
world:set(e, pair(C1, C2), true)
|
||||||
world:set(e, pair(C1, T1), true)
|
world:set(e, pair(C1, T1), true)
|
||||||
world:set(e, pair(T1, C1), 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, C2)))
|
||||||
CHECK(world:get(e, pair(C1, T1)))
|
CHECK(world:get(e, pair(C1, T1)))
|
||||||
|
@ -296,7 +274,9 @@ TEST("world:set()", function()
|
||||||
|
|
||||||
local e2 = world:entity()
|
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)))
|
CHECK(not world:get(e2, pair(jecs.ChildOf, e)))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
@ -385,21 +365,21 @@ TEST("world:query()", function()
|
||||||
i=2
|
i=2
|
||||||
end
|
end
|
||||||
CHECK(i == 2)
|
CHECK(i == 2)
|
||||||
for _, e in q do
|
for _, e in q :: any do
|
||||||
i=3
|
i=3
|
||||||
end
|
end
|
||||||
CHECK(i == 3)
|
CHECK(i == 3)
|
||||||
for _, e in q do
|
for _, e in q :: any do
|
||||||
i=4
|
i=4
|
||||||
end
|
end
|
||||||
CHECK(i == 4)
|
CHECK(i == 4)
|
||||||
|
|
||||||
CHECK(#q:archetypes() == 1)
|
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)
|
world:delete(Foo)
|
||||||
CHECK(#q:archetypes() == 0)
|
CHECK(#q:archetypes() == 0)
|
||||||
end
|
end
|
||||||
do CASE("multiple iter")
|
do CASE "multiple iter"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
|
@ -416,18 +396,21 @@ TEST("world:query()", function()
|
||||||
end
|
end
|
||||||
CHECK(counter == 2)
|
CHECK(counter == 2)
|
||||||
end
|
end
|
||||||
do
|
do CASE "tag"
|
||||||
CASE("tag")
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:entity()
|
local A = world:entity()
|
||||||
local e = world:entity()
|
local e = world:entity()
|
||||||
world:set(e, A, "test")
|
CHECK_EXPECT_ERR(function()
|
||||||
for id, a in world:query(A) do
|
world:set(e, A, "test" :: any)
|
||||||
|
end)
|
||||||
|
local count = 0
|
||||||
|
for id, a in world:query(A) :: any do
|
||||||
|
count += 1
|
||||||
CHECK(a == nil)
|
CHECK(a == nil)
|
||||||
end
|
end
|
||||||
|
CHECK(count == 1)
|
||||||
end
|
end
|
||||||
do
|
do CASE "pairs"
|
||||||
CASE("pairs")
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local C1 = world:component()
|
local C1 = world:component()
|
||||||
|
@ -440,9 +423,11 @@ TEST("world:query()", function()
|
||||||
world:set(e, pair(C1, C2), true)
|
world:set(e, pair(C1, C2), true)
|
||||||
world:set(e, pair(C1, T1), true)
|
world:set(e, pair(C1, T1), true)
|
||||||
world:set(e, pair(T1, C1), 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(a == true)
|
||||||
CHECK(b == true)
|
CHECK(b == true)
|
||||||
CHECK(c == true)
|
CHECK(c == true)
|
||||||
|
@ -467,7 +452,7 @@ TEST("world:query()", function()
|
||||||
entities[i] = id
|
entities[i] = id
|
||||||
end
|
end
|
||||||
|
|
||||||
for id in world:query(A) do
|
for id in world:query(A) :: any do
|
||||||
table.remove(entities, CHECK(table.find(entities, id)))
|
table.remove(entities, CHECK(table.find(entities, id)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -491,10 +476,10 @@ TEST("world:query()", function()
|
||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
local j = 0
|
local j = 0
|
||||||
for _ in q do
|
for _ in q :: any do
|
||||||
i += 1
|
i += 1
|
||||||
end
|
end
|
||||||
for _ in q do
|
for _ in q :: any do
|
||||||
j += 1
|
j += 1
|
||||||
end
|
end
|
||||||
CHECK(i == 2)
|
CHECK(i == 2)
|
||||||
|
@ -518,7 +503,7 @@ TEST("world:query()", function()
|
||||||
world:set(e2, B, 457)
|
world:set(e2, B, 457)
|
||||||
|
|
||||||
local counter = 0
|
local counter = 0
|
||||||
for _ in world:query(B, C) do
|
for _ in world:query(B, C) :: any do
|
||||||
counter += 1
|
counter += 1
|
||||||
end
|
end
|
||||||
CHECK(counter == 0)
|
CHECK(counter == 0)
|
||||||
|
@ -538,7 +523,7 @@ TEST("world:query()", function()
|
||||||
world:set(e, id, 13 ^ i)
|
world:set(e, id, 13 ^ i)
|
||||||
end
|
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(a == 13 ^ 1)
|
||||||
CHECK(b == 13 ^ 2)
|
CHECK(b == 13 ^ 2)
|
||||||
CHECK(c == 13 ^ 3)
|
CHECK(c == 13 ^ 3)
|
||||||
|
@ -567,11 +552,11 @@ TEST("world:query()", function()
|
||||||
|
|
||||||
local it = world:query(A):iter()
|
local it = world:query(A):iter()
|
||||||
|
|
||||||
local e, data = it()
|
local e: number, data = it()
|
||||||
while e do
|
while e do
|
||||||
if e == eA then
|
if e == eA :: number then
|
||||||
CHECK(data)
|
CHECK(data)
|
||||||
elseif e == eAB then
|
elseif e == eAB :: number then
|
||||||
CHECK(data)
|
CHECK(data)
|
||||||
else
|
else
|
||||||
CHECK(false)
|
CHECK(false)
|
||||||
|
@ -582,8 +567,7 @@ TEST("world:query()", function()
|
||||||
CHECK(true)
|
CHECK(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do CASE "should query all matching entities when irrelevant component is removed"
|
||||||
CASE("should query all matching entities when irrelevant component is removed")
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
|
@ -604,7 +588,7 @@ TEST("world:query()", function()
|
||||||
end
|
end
|
||||||
|
|
||||||
local added = 0
|
local added = 0
|
||||||
for id in world:query(A) do
|
for id in world:query(A) :: any do
|
||||||
added += 1
|
added += 1
|
||||||
table.remove(entities, CHECK(table.find(entities, id)))
|
table.remove(entities, CHECK(table.find(entities, id)))
|
||||||
end
|
end
|
||||||
|
@ -630,7 +614,7 @@ TEST("world:query()", function()
|
||||||
end
|
end
|
||||||
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)))
|
table.remove(entities, CHECK(table.find(entities, id)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -645,7 +629,7 @@ TEST("world:query()", function()
|
||||||
local bob = world:entity()
|
local bob = world:entity()
|
||||||
|
|
||||||
world:set(bob, pair(Eats, Apples), true)
|
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(e == bob)
|
||||||
CHECK(bool)
|
CHECK(bool)
|
||||||
end
|
end
|
||||||
|
@ -661,11 +645,11 @@ TEST("world:query()", function()
|
||||||
world:set(bob, pair(Eats, Apples), "bob eats apples")
|
world:set(bob, pair(Eats, Apples), "bob eats apples")
|
||||||
|
|
||||||
local w = jecs.Wildcard
|
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(e == bob)
|
||||||
CHECK(data == "bob eats apples")
|
CHECK(data == "bob eats apples")
|
||||||
end
|
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(e == bob)
|
||||||
CHECK(data == "bob eats apples")
|
CHECK(data == "bob eats apples")
|
||||||
end
|
end
|
||||||
|
@ -685,7 +669,7 @@ TEST("world:query()", function()
|
||||||
|
|
||||||
local w = jecs.Wildcard
|
local w = jecs.Wildcard
|
||||||
local count = 0
|
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
|
count += 1
|
||||||
if e == bob then
|
if e == bob then
|
||||||
CHECK(data == "bob eats apples")
|
CHECK(data == "bob eats apples")
|
||||||
|
@ -697,32 +681,30 @@ TEST("world:query()", function()
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
count = 0
|
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
|
count += 1
|
||||||
CHECK(data == "bob eats apples")
|
CHECK(data == "bob eats apples")
|
||||||
end
|
end
|
||||||
CHECK(count == 1)
|
CHECK(count == 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do CASE "should only relate alive entities"
|
||||||
CASE("should only relate alive entities")
|
|
||||||
SKIP()
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local Eats = world:entity()
|
local Eats = world:entity()
|
||||||
local Apples = world:entity()
|
local Apples = world:component()
|
||||||
local Oranges = world:entity()
|
local Oranges = world:component()
|
||||||
local bob = world:entity()
|
local bob = world:entity()
|
||||||
local alice = world:entity()
|
local alice = world:entity()
|
||||||
|
|
||||||
world:set(bob, Apples, "apples")
|
world:set(bob, Apples, "apples")
|
||||||
world:set(bob, pair(Eats, Apples), "bob eats 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)
|
world:delete(Apples)
|
||||||
local Wildcard = jecs.Wildcard
|
local Wildcard = jecs.Wildcard
|
||||||
|
|
||||||
local count = 0
|
local count = 0
|
||||||
for _, data in world:query(pair(Wildcard, Apples)) do
|
for _, data in world:query(pair(Wildcard, Apples)) :: any do
|
||||||
count += 1
|
count += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -730,6 +712,7 @@ TEST("world:query()", function()
|
||||||
|
|
||||||
CHECK(count == 0)
|
CHECK(count == 0)
|
||||||
CHECK(world:get(bob, pair(Eats, Apples)) == nil)
|
CHECK(world:get(bob, pair(Eats, Apples)) == nil)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
|
@ -759,10 +742,10 @@ TEST("world:query()", function()
|
||||||
world:set(bob, Name, "bob")
|
world:set(bob, Name, "bob")
|
||||||
world:add(sara, pair(ChildOf, alice))
|
world:add(sara, pair(ChildOf, alice))
|
||||||
world:set(sara, Name, "sara")
|
world:set(sara, Name, "sara")
|
||||||
CHECK(world:parent(bob) == alice) -- O(1)
|
CHECK(world:parent(bob) :: number == alice :: number) -- O(1)
|
||||||
|
|
||||||
local count = 0
|
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
|
count += 1
|
||||||
end
|
end
|
||||||
CHECK(count == 2)
|
CHECK(count == 2)
|
||||||
|
@ -781,7 +764,7 @@ TEST("world:query()", function()
|
||||||
world:add(e2, B)
|
world:add(e2, B)
|
||||||
|
|
||||||
local count = 0
|
local count = 0
|
||||||
for id in world:query(A) do
|
for id in world:query(A) :: any do
|
||||||
world:clear(id)
|
world:clear(id)
|
||||||
count += 1
|
count += 1
|
||||||
end
|
end
|
||||||
|
@ -804,7 +787,7 @@ TEST("world:query()", function()
|
||||||
world:add(e2, B)
|
world:add(e2, B)
|
||||||
|
|
||||||
local count = 0
|
local count = 0
|
||||||
for id in world:query(A) do
|
for id in world:query(A) :: any do
|
||||||
world:add(id, B)
|
world:add(id, B)
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
@ -825,7 +808,7 @@ TEST("world:query()", function()
|
||||||
world:add(e2, A)
|
world:add(e2, A)
|
||||||
world:add(e2, B)
|
world:add(e2, B)
|
||||||
|
|
||||||
for id in world:query(A) do
|
for id in world:query(A) :: any do
|
||||||
local e = world:entity()
|
local e = world:entity()
|
||||||
world:add(e, A)
|
world:add(e, A)
|
||||||
world:add(e, B)
|
world:add(e, B)
|
||||||
|
@ -847,7 +830,7 @@ TEST("world:query()", function()
|
||||||
world:add(helloBob, Bob)
|
world:add(helloBob, Bob)
|
||||||
|
|
||||||
local withoutCount = 0
|
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
|
withoutCount += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -862,7 +845,7 @@ TEST("world:query()", function()
|
||||||
local _1, _2, _3 = world:component(), world:component(), world:component()
|
local _1, _2, _3 = world:component(), world:component(), world:component()
|
||||||
|
|
||||||
local counter = 0
|
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
|
counter += 1
|
||||||
end
|
end
|
||||||
CHECK(counter == 0)
|
CHECK(counter == 0)
|
||||||
|
@ -876,9 +859,9 @@ TEST("world:each", function()
|
||||||
local B = world:component()
|
local B = world:component()
|
||||||
local C = world:component()
|
local C = world:component()
|
||||||
|
|
||||||
|
local e3 = world:entity()
|
||||||
local e1 = world:entity()
|
local e1 = world:entity()
|
||||||
local e2 = world:entity()
|
local e2 = world:entity()
|
||||||
local e3 = world:entity()
|
|
||||||
|
|
||||||
world:set(e1, A, true)
|
world:set(e1, A, true)
|
||||||
|
|
||||||
|
@ -889,8 +872,8 @@ TEST("world:each", function()
|
||||||
world:set(e3, B, true)
|
world:set(e3, B, true)
|
||||||
world:set(e3, C, true)
|
world:set(e3, C, true)
|
||||||
|
|
||||||
for entity in world:each(A) do
|
for entity: number in world:each(A) do
|
||||||
if entity == e1 or entity == e2 or entity == e3 then
|
if entity == e1 :: number or entity == e2 :: number or entity == e3 :: number then
|
||||||
CHECK(true)
|
CHECK(true)
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
@ -906,16 +889,16 @@ TEST("world:children", function()
|
||||||
local e1 = world:entity()
|
local e1 = world:entity()
|
||||||
world:set(e1, C, true)
|
world:set(e1, C, true)
|
||||||
|
|
||||||
local e2 = world:entity()
|
local e2 = world:entity() :: number
|
||||||
|
|
||||||
world:add(e2, T)
|
world:add(e2, T)
|
||||||
world:add(e2, pair(ChildOf, e1))
|
world:add(e2, pair(ChildOf, e1))
|
||||||
|
|
||||||
local e3 = world:entity()
|
local e3 = world:entity() :: number
|
||||||
world:add(e3, pair(ChildOf, e1))
|
world:add(e3, pair(ChildOf, e1))
|
||||||
|
|
||||||
local count = 0
|
local count = 0
|
||||||
for entity in world:children(e1) do
|
for entity: number in world:children(e1) do
|
||||||
count += 1
|
count += 1
|
||||||
if entity == e2 or entity == e3 then
|
if entity == e2 or entity == e3 then
|
||||||
CHECK(true)
|
CHECK(true)
|
||||||
|
@ -966,7 +949,7 @@ TEST("world:clear()", function()
|
||||||
world:add(e, A)
|
world:add(e, A)
|
||||||
world:add(e1, A)
|
world:add(e1, A)
|
||||||
|
|
||||||
local archetype = world.archetypeIndex["1"]
|
local archetype = world.archetype_index["1"]
|
||||||
local archetype_entities = archetype.entities
|
local archetype_entities = archetype.entities
|
||||||
|
|
||||||
local _e = e :: number
|
local _e = e :: number
|
||||||
|
@ -1050,7 +1033,9 @@ TEST("world:component()", function()
|
||||||
local e = world:entity()
|
local e = world:entity()
|
||||||
world:set(e, A, "test")
|
world:set(e, A, "test")
|
||||||
world:add(e, B)
|
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:has(e, A))
|
||||||
CHECK(world:get(e, A) == "test")
|
CHECK(world:get(e, A) == "test")
|
||||||
|
@ -1113,10 +1098,14 @@ TEST("world:delete", function()
|
||||||
|
|
||||||
local id = world:entity()
|
local id = world:entity()
|
||||||
world:set(id, Poison, 5)
|
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()
|
local id1 = world:entity()
|
||||||
world:set(id1, Poison, 500)
|
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(id, Poison, Health))
|
||||||
CHECK(world:has(id1, Poison, Health))
|
CHECK(world:has(id1, Poison, Health))
|
||||||
|
@ -1355,7 +1344,7 @@ TEST("Hooks", function()
|
||||||
do
|
do
|
||||||
-- basic
|
-- basic
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
local A = world:component()
|
local A = world:component() :: Entity<boolean>
|
||||||
local e1 = world:entity()
|
local e1 = world:entity()
|
||||||
world:add(e1, A)
|
world:add(e1, A)
|
||||||
world:set(A, jecs.OnRemove, function(entity)
|
world:set(A, jecs.OnRemove, function(entity)
|
||||||
|
@ -1387,9 +1376,9 @@ TEST("Hooks", function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("change tracking", function()
|
TEST("change tracking", function()
|
||||||
CASE "#1" do
|
do CASE "#1"
|
||||||
local world = world_new()
|
local world = world_new()
|
||||||
local Foo = world:component()
|
local Foo = world:component() :: Entity<number>
|
||||||
local Previous = jecs.Rest
|
local Previous = jecs.Rest
|
||||||
|
|
||||||
local q1 = world
|
local q1 = world
|
||||||
|
@ -1403,14 +1392,14 @@ TEST("change tracking", function()
|
||||||
world:set(e2, Foo, 2)
|
world:set(e2, Foo, 2)
|
||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
for e, new in q1 do
|
for e, new in q1 :: any do
|
||||||
i += 1
|
i += 1
|
||||||
world:set(e, pair(Previous, Foo), new)
|
world:set(e, pair(Previous, Foo), new)
|
||||||
end
|
end
|
||||||
|
|
||||||
CHECK(i == 2)
|
CHECK(i == 2)
|
||||||
local j = 0
|
local j = 0
|
||||||
for e, new in q1 do
|
for e, new in q1 :: any do
|
||||||
j += 1
|
j += 1
|
||||||
world:set(e, pair(Previous, Foo), new)
|
world:set(e, pair(Previous, Foo), new)
|
||||||
end
|
end
|
||||||
|
@ -1418,9 +1407,9 @@ TEST("change tracking", function()
|
||||||
CHECK(j == 0)
|
CHECK(j == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
CASE "#2" do
|
do CASE "#2"
|
||||||
local world = world_new()
|
local world = world_new()
|
||||||
local component = world:component()
|
local component = world:component() :: Entity<number>
|
||||||
local tag = world:entity()
|
local tag = world:entity()
|
||||||
local previous = jecs.Rest
|
local previous = jecs.Rest
|
||||||
|
|
||||||
|
@ -1431,14 +1420,14 @@ TEST("change tracking", function()
|
||||||
world:set(testEntity, component, 10)
|
world:set(testEntity, component, 10)
|
||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
for entity, number in q1 do
|
for entity, number in q1 :: any do
|
||||||
i += 1
|
i += 1
|
||||||
world:add(testEntity, tag)
|
world:add(testEntity, tag)
|
||||||
end
|
end
|
||||||
|
|
||||||
CHECK(i == 1)
|
CHECK(i == 1)
|
||||||
|
|
||||||
for e, n in q1 do
|
for e, n in q1 :: any do
|
||||||
world:set(e, pair(previous, component), n)
|
world:set(e, pair(previous, component), n)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1477,107 +1466,7 @@ TEST("repro", function()
|
||||||
updateCooldowns(1.5)
|
updateCooldowns(1.5)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "#2"
|
do CASE "#2" -- ISSUE #171
|
||||||
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
|
|
||||||
local world = world_new()
|
local world = world_new()
|
||||||
local component1 = world:component()
|
local component1 = world:component()
|
||||||
local tag1 = world:entity()
|
local tag1 = world:entity()
|
||||||
|
@ -1663,7 +1552,7 @@ end)
|
||||||
TEST("world:delete() invokes OnRemove hook", function()
|
TEST("world:delete() invokes OnRemove hook", function()
|
||||||
do CASE "#1"
|
do CASE "#1"
|
||||||
local world = world_new()
|
local world = world_new()
|
||||||
|
|
||||||
local A = world:entity()
|
local A = world:entity()
|
||||||
local entity = world:entity()
|
local entity = world:entity()
|
||||||
|
|
||||||
|
@ -1696,7 +1585,7 @@ TEST("world:delete() invokes OnRemove hook", function()
|
||||||
|
|
||||||
world:add(entity, A)
|
world:add(entity, A)
|
||||||
world:add(entity, pair(Relation, B))
|
world:add(entity, pair(Relation, B))
|
||||||
|
|
||||||
world:delete(B)
|
world:delete(B)
|
||||||
|
|
||||||
CHECK(called)
|
CHECK(called)
|
||||||
|
|
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