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
|
||||
|
||||
# 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.
|
||||
|
|
202
jecs.luau
202
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)
|
||||
local r = entity_index_try_get_fast(entity_index, child) :: Record
|
||||
entity_move(entity_index, child, r, to)
|
||||
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 i = n, 1, -1 do
|
||||
local child = children[i]
|
||||
on_remove(child)
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, child in children do
|
||||
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