-------------------------------------------------------------------------------- -- jecs.luau -- v0.9.0 -------------------------------------------------------------------------------- local ID_SIZE = 4 local MAX_ENTITIES = 0x0000_FFFF -------------------------------------------------------------------------------- -- types -------------------------------------------------------------------------------- export type entity = number type Array = { [number]: T } type Map = { [T]: U } type Listener = (id: entity, value: T) -> () type CType = unknown export type Signal = { connect: (self: Signal, listener: (T...) -> ()) -> Connection, } export type Connection = { disconnect: (self: Connection) -> (), reconnect: (self: Connection) -> () } type Pool = { -- sparse map_max: number, -- largest key that can fit map: buffer, -- maps keys to internal indexes -- dense capacity: number, -- allocated amount size: number, -- entities in pool entities: buffer, -- all entities (0-indexed) values: Array, -- all values (1-indexed) on_add: Array> | false, on_change: Array> | false, on_remove: Array> | false, on_clear: Array<() -> ()> | false, after_clear: Array<() -> ()> | false, group: GroupData | false, reserve: (self: Pool, size: number) -> () } type EntityList = { data: buffer, capacity: number, free: number, } type GroupData = { size: number, -- entities in group added: boolean, -- flag to detect iter invalidation connections: Array, -- listeners to add and remove from group [number]: Pool -- all pools in group } export type Handle = typeof(setmetatable( {} :: { world: Registry, entity: entity }, {} :: HandleMT )) type HandleMT = { __index: HandleMT, destroy: (self: Handle) -> (), has_none: (self: Handle) -> boolean, add: (self: Handle, T...) -> (), set: (self: Handle, ctype: T, value: T) -> Handle, insert: (self: Handle, ctype: Array, value: T) -> Handle, patch: (self: Handle, ctype: T, fn: ((T) -> T)?) -> T, has: (self: Handle, T...) -> boolean, get: (self: Handle, T...) -> T..., try_get: (self: Handle, T) -> T?, remove: (self: Handle, T...) -> (), } export type View = typeof(setmetatable({} :: { withou: (self: View, U...) -> View, patch: (self: View, fn: (T...) -> T...) -> (), iter: (self: View) -> () -> (entity, T...) }, {} :: { __len: (self: View) -> number , __iter: (self: View) -> () -> (entity, T...) })) export type Observer = typeof(setmetatable({} :: { withou: (self: Observer, U...) -> Observer, disconnect: (self: Observer) -> Observer, reconnect: (self: Observer) -> Observer, clear: (self: Observer) -> Observer, iter: (self: Observer) -> () -> (entity, T...) }, {} :: { __len: (self: Observer) -> number, __iter: (self: Observer) -> () -> (entity, T...) })) export type Group = typeof(setmetatable({} :: { iter: (self: Group) -> () -> (entity, T...) }, {} :: { __len: (self: Group) -> number, __iter: (self: Group) -> () -> (entity, T...) })) export type Registry = { create: ((self: Registry, id: entity) -> entity) & ((self: Registry) -> entity), release: (self: Registry, id: entity) -> (), destroy: (self: Registry, id: entity) -> (), contains: (self: Registry, id: entity) -> boolean, has_none: (self: Registry, id: entity) -> boolean, add: (self: Registry, id: entity, T...) -> (), set: (self: Registry, id: entity, ctype: T, value: T) -> (), insert: (self: Registry, id: entity, ctype: Array, value: T) -> (), patch: (self: Registry, id: entity, ctype: T, fn: ((T) -> T)?) -> T, has: (self: Registry, id: entity, T...) -> boolean, get: (self: Registry, id: entity, T...) -> T..., try_get: (self: Registry, id: entity, T) -> T?, remove: (self: Registry, id: entity, T...) -> (), find: (self: Registry, ctype: T, value: T) -> entity?, copy: (self: Registry, a: T, b: T) -> (), query: (self: Registry, T...) -> View, track: (self: Registry, T...) -> Observer, group: (self: Registry, T...) -> Group, clear: (self: Registry, T...) -> (), storage: ((self: Registry, ctype: T) -> Pool) & ((self: Registry) -> () -> (unknown, Pool)), on_add: (self: Registry, ctype: T) -> Signal, on_change: (self: Registry, ctype: T) -> Signal, on_remove: (self: Registry, ctype: T) -> Signal, on_clear: (self: Registry, ctype: T) -> Signal<>, after_clear: (self: Registry, ctype: T) -> Signal<>, handle: ((self: Registry, id: entity) -> Handle) & ((self: Registry) -> Handle), context: (self: Registry) -> Handle } export type Queue = typeof(setmetatable({} :: { add: (self: Queue, T...) -> (), clear: (self: Queue) -> (), iter: (self: Queue) -> () -> (T...) }, {} :: { __len: (self: Queue) -> number, __iter: (self: Queue) -> () -> (T...) })) local NIL = nil :: any -- error but stack trace always starts at first callsite outside of this file local function throw(msg: string) local s = 1 repeat s += 1 until debug.info(s, "s") ~= debug.info(1, "s") error(msg, s) end local ASSERT = function(v: T, msg: string): T if v then return v end return throw(msg) end :: typeof(assert) -------------------------------------------------------------------------------- -- entity id -------------------------------------------------------------------------------- local RESERVED_BITS = 0 local ID_SIZE_BITS = ID_SIZE * 8 local ID_MAX = 2^(ID_SIZE_BITS - RESERVED_BITS) - 1 local ID_MASK_RES = 0xFFFF_FFFF - ID_MAX local ID_MASK_KEY = MAX_ENTITIES local ID_MASK_VER = ID_MAX - ID_MASK_KEY local ID_MASK_VER_RES = ID_MASK_VER + ID_MASK_RES local ID_LSHIFT = ID_MASK_KEY + 1 local ID_RSHIFT = 1/ID_LSHIFT local ID_NULL_KEY = ID_MASK_KEY local ID_CTX_KEY = 0 local ID_NULL_VER = 0 local ID_MIN_VER = 1 * ID_LSHIFT local ID_MAX_VALID_KEY = ID_MASK_KEY - 1 assert(ID_SIZE <= 4 and ID_SIZE >= 1) assert(MAX_ENTITIES <= ID_MAX/2 and MAX_ENTITIES > 0) assert(bit32.band(MAX_ENTITIES + 1, MAX_ENTITIES) == 0) local function ID_CREATE(key: number, ver: number): number return ver + key end local function ID_KEY(id: number): number return bit32.band(id, ID_MASK_KEY) end local function ID_VER(id: number): number return bit32.band(id, ID_MASK_VER) end local function ID_REPLACE(id: number, new_key: number): number return ID_VER(id) + new_key end local function ID_VERSION_NOT_EQUAL(id1: number, id2: number) return ID_VER(id1) ~= ID_VER(id2) end local function ID_ASSERT_VERSION_EQUAL(id1: number, id2: number) if ID_VERSION_NOT_EQUAL(id1, id2) then throw("invalid entity") end end local ID_NULL = ID_CREATE(ID_NULL_KEY, ID_NULL_VER) local ID_CTX = ID_CREATE(ID_CTX_KEY, ID_MIN_VER) -------------------------------------------------------------------------------- -- component type -------------------------------------------------------------------------------- -- a ctor set to `true` indicates component is a tag local ctype_ctors: Map unknown)> = {} local ctype_names: Map = {} local ctype_n = 0 local function ctype_create(ctor: boolean | () -> unknown): CType ctype_n += 1 ctype_ctors[ctype_n] = ctor return ctype_n end local function ctype_is_tag(ctype: CType): boolean return ctype_ctors[ctype] == true end local function ctype_set_name(ctype: CType, name: unknown) ctype_names[ctype] = tostring(name) end --[[NO INLINE]] local ctype_debug: (ctype: CType?, idx: number?) -> string = (function() return function(ctype: CType?, idx: number?) local name = ctype_names[ctype] return if name then `component "{name}"` elseif idx then `component (arg #{idx})` else "component (unknown)" end end)() local function CTYPE_VALID(v: unknown): boolean return type(v) == "number" and math.floor(v) == v and v > 0 and v <= ctype_n end local function ASSERT_CTYPE_VALID(ctype: unknown, idx: number?) if not CTYPE_VALID(ctype) then throw(`invalid component {idx and `arg #{idx}` or ""}`) end end local ctype_entity = ctype_create(false) ctype_set_name(ctype_entity, "entity") -------------------------------------------------------------------------------- -- pool -------------------------------------------------------------------------------- local function next_pow_of_2(x: number) assert(x > 0 and x <= 2^32) x -= 1 x = bit32.bor(x, bit32.rshift(x, 1)) x = bit32.bor(x, bit32.rshift(x, 2)) x = bit32.bor(x, bit32.rshift(x, 4)) x = bit32.bor(x, bit32.rshift(x, 8)) x = bit32.bor(x, bit32.rshift(x, 16)) return x + 1 end local function fire(listeners: Array>, id: entity, value: T) for i = #listeners, 1, -1 do listeners[i](id, value) end end local function BUFFER_CREATE(size: number): buffer return buffer.create(size * ID_SIZE) end local function BUFFER_GET(b: buffer, i: number): number return buffer.readu32(b, i * ID_SIZE) end local function BUFFER_SET(b: buffer, i: number, id: entity) buffer.writeu32(b, i * ID_SIZE, id) end local function BUFFER_FILL(b: buffer, first: number, last: number, v: number) assert(buffer.len(b) >= last * ID_SIZE) for i = first * ID_SIZE, last * ID_SIZE, ID_SIZE do buffer.writeu32(b, i, v) end end local function BUFFER_RESIZE(b: buffer, size: number): buffer local b_new = buffer.create(size * ID_SIZE) local n_old = buffer.len(b) buffer.copy(b_new, 0, b) for i = n_old, size * ID_SIZE - 1, ID_SIZE do buffer.writeu32(b_new, i, ID_NULL) end return b_new end -- local function BUFFER_CLONE(b: buffer): buffer -- local b_new = buffer.create(buffer.len(b)) -- buffer.copy(b_new, 0, b) -- return b_new -- end -- wrappers to treat arrays as 0-indexed local function ARRAY_SET(t: Array, i: number, v: T) t[i + 1] = v end local function ARRAY_GET(t: Array, i: number): T return t[i + 1] end local function ARRAY_SWAP(t: Array, a: number, b: number) a += 1 b += 1 t[a], t[b] = t[b], t[a] end local function ARRAY_SWAP_REMOVE(t: Array, remove: number, swap: number) swap += 1 t[remove + 1] = t[swap] t[swap] = nil end local function POOL_HAS(pool: Pool, key: number): boolean return pool.map_max >= key and BUFFER_GET(pool.map, key) ~= ID_NULL end local function POOL_NOT_HAS(pool: Pool, key: number): boolean return pool.map_max < key or BUFFER_GET(pool.map, key) == ID_NULL end local function POOL_FIND(pool: Pool, key: number): number if key > pool.map_max then return ID_NULL end return BUFFER_GET(pool.map, key) end local function pool_resize_entities(self: Pool, size: number) if self.capacity >= size then return end local new_capacity = math.ceil(size * 1.5) self.entities = BUFFER_RESIZE(self.entities, new_capacity) self.capacity = new_capacity end local function pool_resize_map(self: Pool, size: number) if self.map_max + 1 >= size then return end local new_map_capacity = math.ceil(size * 1.5) self.map = BUFFER_RESIZE(self.map, new_map_capacity) self.map_max = new_map_capacity - 1 end local function POOL_RESIZE_MAP_IF_NEEDED(self: Pool, key: number) if self.map_max < key then pool_resize_map(self, key + 1) end end local function POOL_SWAP( self: Pool, idx: number, idx_swap: number ) local map = self.map local entities = self.entities local values = self.values local id = BUFFER_GET(entities, idx) local id_swap = BUFFER_GET(entities, idx_swap) BUFFER_SET(entities, idx_swap, id) BUFFER_SET(entities, idx, id_swap) BUFFER_SET(map, ID_KEY(id), ID_REPLACE(id, idx_swap)) BUFFER_SET(map, ID_KEY(id_swap), ID_REPLACE(id_swap, idx)) ARRAY_SWAP(values, idx_swap, idx) end local function GROUP_ADD(group: GroupData, key: number) group.added = true -- used to test for invalidation local n = group.size group.size = n + 1 for _, pool in ipairs(group) do POOL_SWAP(pool, ID_KEY(BUFFER_GET(pool.map, key)), n) end end local function GROUP_CAN_ADD(group: GroupData, key: number): boolean for _, pool in ipairs(group) do if POOL_NOT_HAS(pool, key) then return false end end return true end local function GROUP_REMOVE(group: GroupData, idx: number) local n = group.size - 1 group.size = n for _, pool in ipairs(group) do POOL_SWAP(pool, idx, n) end end local function POOL_TRY_GROUP(pool: Pool, id: number) local group = pool.group :: GroupData local key = ID_KEY(id) if GROUP_CAN_ADD(group, key) then GROUP_ADD(group, key) end end local function POOL_TRY_UNGROUP(pool: Pool, id: entity) local group = pool.group :: GroupData local idx = ID_KEY(BUFFER_GET(pool.map, ID_KEY(id))) if idx < group.size then GROUP_REMOVE(group, idx) end end local function POOL_RESIZE_ENTITIES_IF_NEEDED(self: Pool, size: number) if size >= self.capacity then pool_resize_entities(self, size + 1) end end -- separated to encourage compiler to inline local function _POOL_ADD(self, i, id, v) BUFFER_SET(self.map, ID_KEY(id), ID_REPLACE(id, i)) BUFFER_SET(self.entities, i, id) ARRAY_SET(self.values, i, v) end local function POOL_ADD(self: Pool, id: entity, v: T) local n = self.size POOL_RESIZE_ENTITIES_IF_NEEDED(self, n) self.size = n + 1 _POOL_ADD(self, n, id, v) if self.on_add then fire(self.on_add, id, v) end end local function _POOL_ADD_ID(self, i, id) BUFFER_SET(self.map, ID_KEY(id), ID_REPLACE(id, i)) BUFFER_SET(self.entities, i, id) end local function POOL_ADD_ID(self: Pool, id: entity) local n = self.size POOL_RESIZE_ENTITIES_IF_NEEDED(self, n) self.size = n + 1 _POOL_ADD_ID(self, n, id) if self.on_add then fire(self.on_add, id, NIL) end end local function POOL_REMOVE(self: Pool, key, id) local map = self.map if self.on_remove then fire(self.on_remove, id) end local idx_ver = BUFFER_GET(map, key) local idx = ID_KEY(idx_ver) if ID_VERSION_NOT_EQUAL(idx_ver, id) then return end local n = self.size - 1 self.size = n local entities = self.entities local values = self.values local id_last = BUFFER_GET(entities, n) BUFFER_SET(map, ID_KEY(id_last), ID_REPLACE(id_last, idx)) BUFFER_SET(map, key, ID_NULL) BUFFER_SET(entities, idx, id_last) --BUFFER_SET(entities, n, ID_NULL) ARRAY_SWAP_REMOVE(values, idx, n) end local function POOL_TRY_REMOVE(self: Pool, id: entity) local key = ID_KEY(id) if POOL_HAS(self, key) then POOL_REMOVE(self, key, id) end end local function POOL_COPY(self: Pool, into: Pool) if into.on_add or into.on_change or into.on_remove or into.on_clear then throw("cannot paste into component with signals") end POOL_RESIZE_MAP_IF_NEEDED(into, self.map_max) POOL_RESIZE_ENTITIES_IF_NEEDED(into, self.capacity) into.size = self.size buffer.copy(into.map, 0, self.map, 0) buffer.copy(into.entities, 0, self.entities, 0) table.move(self.values, 1, math.max(self.size, into.size), 1, into.values) end local function pool_clear(self: Pool) local entities = self.entities local on_clear = self.on_clear :: Array<() -> ()> if on_clear then for _, listener in on_clear do listener() end end local on_remove = self.on_remove :: Array> if on_remove then for i = 0, self.size - 1 do fire(on_remove, BUFFER_GET(entities, i)) end end local after_clear = self.after_clear :: Array<() -> ()> if after_clear then for _, listener in after_clear do listener() end end self.size = 0 BUFFER_FILL(self.map, 0, self.map_max, ID_NULL) table.clear(self.values) end local function pool_create(size: number?): Pool local n = size or 1 local map = BUFFER_CREATE(n) BUFFER_FILL(map, 0, n - 1, ID_NULL) return { map_max = n - 1, capacity = n, size = 0, map = map, entities = BUFFER_CREATE(n), values = table.create(n), on_add = false, on_change = false, on_remove = false, on_clear = false, after_clear = false, group = false, reserve = function(self: Pool, size: number) pool_resize_map(self, size) pool_resize_entities(self, size) -- todo: why does this cause innacurate buffer alloc readings? do -- force array reallocation local values = self.values local n = next_pow_of_2(size) for i = self.size + 1, n do values[i] = true :: any end for i = n, self.size + 1, -1 do values[i] = nil end end end } end -------------------------------------------------------------------------------- -- entity_list -------------------------------------------------------------------------------- local function entity_list_create(): EntityList return { data = buffer.create(0), capacity = 0, free = ID_NULL_KEY } end local function entity_list_get_prev_key(self: EntityList, key: number): number local prev = self.free local i = 0; repeat i += 1 local next = ID_KEY(BUFFER_GET(self.data, prev)) if next == key then break end prev = next until i == ID_MAX_VALID_KEY assert(i < ID_MAX_VALID_KEY, "key not found") -- todo: is this a valid case? return prev end local function entity_list_resize(self: EntityList, new_capacity: number, partition_start: number, partition_stop: number) ASSERT(new_capacity > self.capacity, "new capacity must be greater than current capacity") local new_max = new_capacity - 1 ASSERT(new_max <= ID_MAX_VALID_KEY) local old_capacity = self.capacity local old_max = old_capacity - 1 local old_data = self.data local new_data = BUFFER_RESIZE(old_data, new_capacity) if old_max < partition_stop and new_max >= partition_start then local start = old_max < partition_start and partition_start or old_max + 1 -- todo: verify local stop = new_max > partition_stop and partition_stop or new_max if self.free == ID_NULL_KEY then self.free = start else local tail = entity_list_get_prev_key(self, ID_NULL_KEY) BUFFER_SET(new_data, tail, ID_CREATE(start, ID_VER(BUFFER_GET(new_data, tail)))) end for i = start, stop - 1 do BUFFER_SET(new_data, i, ID_CREATE(i + 1, ID_MIN_VER)) end BUFFER_SET(new_data, stop, ID_CREATE(ID_NULL_KEY, ID_MIN_VER)) end self.data = new_data self.capacity = new_capacity end local function entity_list_remove(self: EntityList, id: entity, partition_start: number, partition_stop: number) local key = ID_KEY(id) local ver = ID_VER(id) if key >= partition_start and key <= partition_stop then BUFFER_SET(self.data, key, ID_CREATE(self.free, ver < ID_MASK_VER and ver + 1 * ID_LSHIFT or ID_MIN_VER)) self.free = key else BUFFER_SET(self.data, key, ID_NULL) end end local function entity_list_clear(self: EntityList, partition_start: number, partition_stop: number) for i = self.capacity - 1, 0, -1 do local key_ver = BUFFER_GET(self.data, i) if ID_KEY(key_ver) == i then -- is in use entity_list_remove(self, key_ver, partition_start, partition_stop) end end end local function ENTITY_LIST_ID_IN_USE(self: EntityList, id: entity): boolean local key = ID_KEY(id) return key < self.capacity and BUFFER_GET(self.data, ID_KEY(id)) == id end -------------------------------------------------------------------------------- -- query -------------------------------------------------------------------------------- local query_create, observer_create, group_create do type IRegistry = { storage: (self: IRegistry, ctype: T) -> Pool, on_add: (self: IRegistry, ctype: T) -> Signal, on_change: (self: IRegistry, ctype: T) -> Signal, on_remove: (self: IRegistry, ctype: T) -> Signal, on_clear: (self: IRegistry, ctype: T) -> Signal<>, after_clear: (self: IRegistry, ctype: T) -> Signal<>, } local function HAS_ANY(pools: Array>, key: number): boolean for _, pool in next, pools do if POOL_HAS(pool, key) then return true end end return false end local function get_smallest(pools: Array>): (Pool, number) local pool: Pool? local pos = 0 for i, p in next, pools do if pool == nil or p.size < pool.size then pool = p pos = i end end return assert(pool, "no pools given"), pos end local get_smallest_first_tuple = {} local function get_smallest_first(pools: Array>): (number, ...Pool) table.clear(get_smallest_first_tuple) local s, s_pos = get_smallest(pools) for i, p in next, pools do if p ~= s then table.insert(get_smallest_first_tuple, p) end end return s_pos, s, unpack(get_smallest_first_tuple) end local function get_pools(world: IRegistry, ctypes: Array): Array> local pools = table.create(#ctypes) for i, ctype in next, ctypes do pools[i] = world:storage(ctype) end return pools end local function disconnect_all(connections: Array) for _, connection in next, connections do connection:disconnect() end end local function clear_invalidation_flag(pool: Pool) if pool.group then pool.group.added = false end end local function ASSERT_INVALIDATION(pool: Pool): ...any if pool.group and pool.group.added then throw("group reordered during iteration") end end local function query_iter_1(a: Pool): () -> (entity, A) local n = a.size local entities = a.entities local values = a.values clear_invalidation_flag(a) return function() local i = n - 1; n = i if i < 0 then ASSERT_INVALIDATION(a) return NIL, NIL end return BUFFER_GET(entities, i), ARRAY_GET(values, i) end end local function query_iter_1_excl_1(a: Pool, b: Pool): () -> (entity, A) local n = a.size local entities = a.entities local values = a.values clear_invalidation_flag(a) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) if POOL_FIND(b, ID_KEY(id)) ~= ID_NULL then continue end n = i return id, ARRAY_GET(values, i) end ASSERT_INVALIDATION(a) return NIL, NIL end end local function query_iter_2(a: Pool, b: Pool): () -> (entity, A, B) local na, nb = a.size, b.size if na <= nb then local n = na local entities = a.entities local values = a.values clear_invalidation_flag(a) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local idx_ver = POOL_FIND(b, ID_KEY(id)) if idx_ver == ID_NULL then continue end n = i return id, ARRAY_GET(values, i), ARRAY_GET(b.values, ID_KEY(idx_ver)) end ASSERT_INVALIDATION(a) return NIL, NIL, NIL end else local n = nb local entities = b.entities local values = b.values clear_invalidation_flag(b) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local idx_ver = POOL_FIND(a, ID_KEY(id)) if idx_ver == ID_NULL then continue end n = i return id, ARRAY_GET(a.values, ID_KEY(idx_ver)), ARRAY_GET(values, i) end ASSERT_INVALIDATION(b) return NIL, NIL, NIL end end end local function query_iter_2_excl_1(a: Pool, b: Pool, c: Pool): () -> (entity, A, B) local na, nb = a.size, b.size if na <= nb then local n = na local entities = a.entities local values = a.values clear_invalidation_flag(a) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local key = ID_KEY(id) if POOL_FIND(c, key) ~= ID_NULL then continue end local idx_ver = POOL_FIND(b, key) if idx_ver == ID_NULL then continue end n = i return id, ARRAY_GET(values, i), ARRAY_GET(b.values, ID_KEY(idx_ver)) end ASSERT_INVALIDATION(a) return NIL, NIL, NIL end else local n = nb local entities = b.entities local values = b.values clear_invalidation_flag(b) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local key = ID_KEY(id) if POOL_FIND(c, key) ~= ID_NULL then continue end local idx_ver = POOL_FIND(a, key) if idx_ver == ID_NULL then continue end n = i return id, ARRAY_GET(a.values, ID_KEY(idx_ver)), ARRAY_GET(values, i) end ASSERT_INVALIDATION(b) return NIL, NIL, NIL end end end local function query_iter_3(includes: Array>): () -> (entity, ...any) local s_pos, s, a, b = get_smallest_first(includes) local n = s.size local entities = s.entities local values = s.values clear_invalidation_flag(s) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local key = ID_KEY(id) local a_idx_ver = POOL_FIND(a, key) if a_idx_ver == ID_NULL then continue end local va = ARRAY_GET(a.values, ID_KEY(a_idx_ver)) local b_idx_ver = POOL_FIND(b, key) if b_idx_ver == ID_NULL then continue end local vb = ARRAY_GET(b.values, ID_KEY(b_idx_ver)) local sv = ARRAY_GET(values, i) n = i if s_pos == 1 then return id, sv, va, vb elseif s_pos == 2 then return id, va, sv, vb else return id, va, vb, sv end end ASSERT_INVALIDATION(s) return NIL end end local function query_iter_4(includes: Array>): () -> (entity, ...any) local s_pos, s, a, b, c = get_smallest_first(includes) local n = s.size local entities = s.entities local values = s.values clear_invalidation_flag(s) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local key = ID_KEY(id) local a_idx_ver = POOL_FIND(a, key) if a_idx_ver == ID_NULL then continue end local va = ARRAY_GET(a.values, ID_KEY(a_idx_ver)) local b_idx_ver = POOL_FIND(b, key) if b_idx_ver == ID_NULL then continue end local vb = ARRAY_GET(b.values, ID_KEY(b_idx_ver)) local c_idx_ver = POOL_FIND(c, key) if c_idx_ver == ID_NULL then continue end local vc = ARRAY_GET(c.values, ID_KEY(c_idx_ver)) local sv = ARRAY_GET(values, i) n = i if s_pos == 1 then return id, sv, va, vb, vc elseif s_pos == 2 then return id, va, sv, vb, vc elseif s_pos == 3 then return id, va, vb, sv, vc else return id, va, vb, vc, sv end end ASSERT_INVALIDATION(s) return NIL end end local function query_iter_general( includes: Array>, withouts: Array>?, lead: Pool? ): () -> (entity, ...any) local source = lead or get_smallest(includes) local source_pos: number? for i, pool in next, includes do if pool == source then source_pos = i break end end assert(source_pos) local n = source.size local entities = source.entities local values = source.values local last_pos = #includes local tuple = table.create(last_pos) clear_invalidation_flag(source) return function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local key = ID_KEY(id) if withouts and HAS_ANY(withouts, key) then continue end local has_all = true for pos = 1, source_pos - 1 do local pool = includes[pos] local idx_ver = POOL_FIND(pool, key) if idx_ver == ID_NULL then has_all = false; break end tuple[pos] = ARRAY_GET(pool.values, ID_KEY(idx_ver)) end if has_all == false then continue end for pos = source_pos + 1, last_pos do local pool = includes[pos] local idx_ver = POOL_FIND(pool, key) if idx_ver == ID_NULL then has_all = false; break end tuple[pos] = ARRAY_GET(pool.values, ID_KEY(idx_ver)) end if has_all == false then continue end tuple[source_pos] = ARRAY_GET(values, i) n = i return id, unpack(tuple) end ASSERT_INVALIDATION(source) return NIL end end local function query_patch_1(a: Pool, fn: (A) -> A) local entities = a.entities local values = a.values local on_change = a.on_change :: Array> if on_change then for i, v in values do local v_new = fn(v) fire(on_change, BUFFER_GET(entities, i - 1), v_new) values[i] = v_new end else for i, v in values do values[i] = fn(v) end end end local function query_patch_2(a: Pool, b: Pool, fn: (A, B) -> (A, B)) local na, nb = a.size, b.size local a_on_change = a.on_change :: Array> local b_on_change = b.on_change :: Array> if na <= nb then local n = na local entities = a.entities local values = a.values for i = 0, n - 1 do local id = BUFFER_GET(entities, i) local idx_ver = POOL_FIND(b, ID_KEY(id)) if idx_ver == ID_NULL then continue end local idx = ID_KEY(idx_ver) local va_new, vb_new = fn(ARRAY_GET(values, i), ARRAY_GET(b.values, idx)) if a_on_change then fire(a_on_change, id, va_new) end if b_on_change then fire(b_on_change, id, vb_new) end ARRAY_SET(values, i, va_new) ARRAY_SET(b.values, idx, vb_new) end else local n = nb local entities = b.entities local values = b.values for i = 0, n - 1 do local id = BUFFER_GET(entities, i) local idx_ver = POOL_FIND(a, ID_KEY(id)) if idx_ver == ID_NULL then continue end local idx = ID_KEY(idx_ver) local va_new, vb_new = fn(ARRAY_GET(a.values, idx), ARRAY_GET(values, i)) if a_on_change then fire(a_on_change, id, va_new) end if b_on_change then fire(b_on_change, id, vb_new) end ARRAY_SET(a.values, idx, va_new) ARRAY_SET(values, i, vb_new) end end end local function query_patch_general( includes: Array>, withouts: Array>?, fn: (...unknown) -> ...unknown ) ASSERT(#includes <= 4, "cannot patch a query with more than 4 component types") local source = get_smallest(includes) local source_pos: number? for i, pool in next, includes do if pool == source then source_pos = i break end end assert(source_pos) local n = source.size local entities = source.entities local values = source.values local last_pos = #includes local tuple = table.create(last_pos) local idxs = table.create(last_pos) for i = 0, n - 1 do local id = BUFFER_GET(entities, i) local key = ID_KEY(id) if withouts and HAS_ANY(withouts, key) then continue end local has_all = true for pos = 1, source_pos - 1 do local pool = includes[pos] local idx_ver = POOL_FIND(pool, key) if idx_ver == ID_NULL then has_all = false; break end local idx = ID_KEY(idx_ver) tuple[pos] = ARRAY_GET(pool.values, idx) idxs[pos] = idx end if has_all == false then continue end for pos = source_pos + 1, last_pos do local pool = includes[pos] local idx_ver = POOL_FIND(pool, key) if idx_ver == ID_NULL then has_all = false; break end local idx = ID_KEY(idx_ver) tuple[pos] = ARRAY_GET(pool.values, idx) idxs[pos] = idx end if has_all == false then continue end tuple[source_pos] = ARRAY_GET(values, i) idxs[source_pos] = i tuple[1], tuple[2], tuple[3], tuple[4] = fn(tuple[1], tuple[2], tuple[3], tuple[4]) for pos, pool in includes do local on_change = pool.on_change :: Array> if on_change then fire(on_change, id, tuple[pos]) end ARRAY_SET(pool.values, idxs[pos], tuple[pos]) end end return NIL end type _View = typeof(setmetatable({} :: { world: IRegistry, includes: Array, withouts: Array?, withou: (_View, U...) -> _View, patch: (_View, fn: (T...) -> T...) -> (), iter: (_View) -> () -> (entity, T...) }, {} :: { __len: (_View) -> number, __iter: (_View) -> () -> (entity, T...) })) function query_create(reg: IRegistry, ...: T...): View local includes = {} for i = 1, select("#", ...) do local ctype = select(i, ...) ASSERT_CTYPE_VALID(ctype, i) table.insert(includes, ctype) end local function iter(self: _View): () -> (entity, T...) local includes = get_pools(self.world, self.includes) local withouts = self.withouts and get_pools(self.world, self.withouts) return if #includes == 1 and not withouts then query_iter_1(includes[1]) elseif #includes == 1 and withouts and #withouts == 1 then query_iter_1_excl_1(includes[1], withouts[1]) elseif #includes == 2 and not withouts then query_iter_2(includes[1], includes[2]) elseif #includes == 2 and withouts and #withouts == 1 then query_iter_2_excl_1(includes[1], includes[2], withouts[1]) elseif #includes == 3 and not withouts then query_iter_3(includes) elseif #includes == 4 and not withouts then query_iter_4(includes) else query_iter_general(includes, withouts) end local self = { world = reg, includes = includes, withouts = nil, withou = function(self: _View, ...: E...): _View local includes = self.includes local withouts = self.withouts or (function() local t = {} self.withouts = t return t end)() for i = 1, select("#", ...) do local ctype = select(i, ...) ASSERT_CTYPE_VALID(ctype, i) ASSERT(not table.find(includes, ctype), `cannot withou {ctype_debug(ctype, i)}, component is a part of the query`) if table.find(withouts, ctype) then continue end table.insert(withouts, ctype) end return self end, patch = function(self: _View, fn: (T...) -> T...) local includes = get_pools(self.world, self.includes) local withouts = self.withouts and get_pools(self.world, self.withouts) if #includes == 1 and not withouts then query_patch_1(includes[1], fn :: any) elseif #includes == 2 and not withouts then query_patch_2(includes[1], includes[2], fn :: any) else query_patch_general(includes, withouts, fn :: any) end end, iter = iter, __len = function(self: _View): number return get_smallest(get_pools(self.world, self.includes)).size end, __iter = iter } setmetatable(self, self) return self :: _View end type _Observer = typeof(setmetatable({} :: { world: IRegistry, pool: Pool, includes: Array, withouts: Array?, connections: Array?, withou: (_Observer, U...) -> _Observer, disconnect: (_Observer) -> _Observer, reconnect: (_Observer) -> _Observer, clear: (_Observer) -> _Observer, iter: (_Observer) -> () -> (entity, T...) }, {} :: { __len: (_Observer) -> number, __iter: (_Observer) -> () -> (entity, T...) })) function observer_create(reg: IRegistry, ...: T...): Observer local includes = {} for i = 1, select("#", ...) do local ctype = select(i, ...) ASSERT_CTYPE_VALID(ctype, i) table.insert(includes, ctype) end local function iter(self: _Observer): () -> (entity, T...) local pool = self.pool local reg = self.world local includes = get_pools(reg, self.includes) local withouts = self.withouts and get_pools(reg, self.withouts) local n = pool.size local entities = pool.entities local reg_pool = includes[1] local reg_map = reg_pool.map local reg_values = reg_pool.values local tuple = table.create(#includes) :: Array return if #includes == 1 and not withouts then function() local i = n - 1 n = i if i < 0 then self:clear() return NIL end local id = BUFFER_GET(entities, i) return id, ARRAY_GET( reg_values, ID_KEY(BUFFER_GET(reg_map, ID_KEY(id))) ) :: any end else function() for i = n - 1, 0, -1 do local id = BUFFER_GET(entities, i) local key = ID_KEY(id) if withouts and HAS_ANY(withouts, key) then continue end local has_all = true for pos, pool in next, includes do local idx_ver = POOL_FIND(pool, key) -- todo: guarantee if idx_ver == ID_NULL then has_all = false; break end tuple[pos] = ARRAY_GET(pool.values, ID_KEY(idx_ver)) end if has_all == false then continue end n = i return id, unpack(tuple) end self:clear() return nil :: any, nil :: any end end local self = { world = reg, pool = pool_create(), -- treat all initial ids as changed includes = includes, withouts = nil, connections = nil, withou = function(self: _Observer, ...: E...): _Observer local withouts = self.withouts or (function() local t = {} self.withouts = t return t end)() for i = 1, select("#", ...) do local ctype = select(i, ...) ASSERT_CTYPE_VALID(ctype, i) ASSERT(not table.find(self.includes, ctype), `cannot withou {ctype_debug(ctype, i)}, component is being tracked`) if table.find(withouts, ctype) then continue end table.insert(withouts, ctype) end return self end, disconnect = function(self: _Observer): _Observer ASSERT(self.pool.size == 0, "attempt to disconnect a non-empty observer") if not self.connections then return self end disconnect_all(self.connections) self.connections = nil return self end, reconnect = function(self: _Observer): _Observer if self.connections then return self end local reg = self.world local pool = self.pool local includes = self.includes local all_connections = {} local remove_connections = {} for i, ctype in includes do local function added_or_changed_listener(id) local key = ID_KEY(id) POOL_RESIZE_MAP_IF_NEEDED(pool, key) local idx_ver = BUFFER_GET(pool.map, key) if idx_ver == ID_NULL then local n = pool.size POOL_RESIZE_ENTITIES_IF_NEEDED(pool, n) pool.size = n + 1 BUFFER_SET(pool.map, key, ID_REPLACE(id, n)) BUFFER_SET(pool.entities, n, id) end end local function removed_listener(id: number) local map = pool.map local entities = pool.entities local key = ID_KEY(id) local idx_ver = POOL_FIND(pool, key) if idx_ver ~= ID_NULL then local n = pool.size - 1 pool.size = n local idx = ID_KEY(idx_ver) local id_last = BUFFER_GET(entities, n) BUFFER_SET(map, ID_KEY(id_last), ID_REPLACE(id_last, idx)) BUFFER_SET(map, key, ID_NULL) BUFFER_SET(entities, idx, id_last) --BUFFER_SET(entities, n, ID_NULL) end end table.insert(all_connections, reg:on_add(ctype):connect(added_or_changed_listener)) table.insert(all_connections, reg:on_change(ctype):connect(added_or_changed_listener)) local remove_connection = reg:on_remove(ctype):connect(removed_listener) table.insert(all_connections, remove_connection) table.insert(remove_connections, remove_connection) table.insert(all_connections, reg:on_clear(ctype):connect(function() for _, cn in remove_connections do cn:disconnect() end end)) table.insert(all_connections, reg:after_clear(ctype):connect(function() pool_clear(self.pool) for _, cn in remove_connections do cn:reconnect() end end)) end self.connections = all_connections return self end, clear = function(self: _Observer): _Observer pool_clear(self.pool) return self end, iter = function(self) return iter(self) end, __len = function(self: _Observer): number return self.pool.size end, __iter = function(self) return iter(self) end } setmetatable(self, self) return self:reconnect() :: _Observer end type _Group = typeof(setmetatable({} :: { data: GroupData, pools: Array>, iter: (self: _Group) -> () -> (entity, T...) }, {} :: { __len: (self: _Group) -> number, __iter: (self: _Group) -> () -> (entity, T...) })) function group_create(reg: Registry, data: GroupData, ...: T...): Group local pools = {} for i = 1, select("#", ...) do local ctype = select(i, ...) local pool = reg:storage(ctype) assert(table.find(data, pool), "component type is not in group") pools[i] = pool end local function iter(self: _Group): () -> (entity, T...) local pools = self.pools local n = self.data.size local entities = pools[1].entities local values: Array> = table.create(#pools) for i, pool in next, pools do values[i] = pool.values end if #pools == 1 then local a = unpack(values) return function() local ia = n if ia == 0 then return NIL end local ib = ia - 1 n = ib return BUFFER_GET(entities, ib), a[ia] end elseif #pools == 2 then local a, b = unpack(values) return function() local ia = n if ia == 0 then return NIL end local ib = ia - 1 n = ib return BUFFER_GET(entities, ib), a[ia], b[ia] end elseif #pools == 3 then local a, b, c = unpack(values) return function() local ia = n if ia == 0 then return NIL end local ib = ia - 1 n = ib return BUFFER_GET(entities, ib), a[ia], b[ia], c[ia] end elseif #pools == 4 then local a, b, c, d = unpack(values) return function() local ia = n if ia == 0 then return NIL end local ib = ia - 1 n = ib return BUFFER_GET(entities, ib), a[ia], b[ia], c[ia], d[ia] end else local tuple = table.create(#values) :: Array return function() local ia = n if ia == 0 then return NIL end local ib = ia - 1 n = ib for pos, v in next, values do tuple[pos] = v[ia] end return BUFFER_GET(entities, ib), unpack(tuple) end end end local self = { data = data, pools = pools, iter = iter, __len = function(self: _Group): number return self.data.size end, __iter = iter } setmetatable(self, self) return self :: _Group end end -------------------------------------------------------------------------------- -- signal -------------------------------------------------------------------------------- local signal_create: () -> (Signal, Array<(T...) -> ()>) do type _Signal = { listeners: Array<(T...) -> ()>, connect: (_Signal, (T...) -> ()) -> _Connection, on_empty: () -> ()?, on_not_empty: () -> ()? } type _Connection = { signal: _Signal, listener: (T...) -> (), connected: boolean, disconnect: (_Connection) -> (), reconnect: (_Connection) -> () } local Connection = {} function Connection.new(signal: _Signal, fn: (T...) -> ()): _Connection return { signal = signal, listener = fn, connected = true, disconnect = function(self: _Connection) if self.connected then local listeners = self.signal.listeners local n = #listeners local last = listeners[n] local idx = table.find(listeners, fn) assert(idx, "cannot find listener") listeners[idx] = last listeners[n] = nil self.connected = false if n == 1 and self.signal.on_empty then self.signal.on_empty() end end end, reconnect = function(self: _Connection) if not self.connected then local new = self.signal:connect(self.listener) self.idx = new.idx self.connected = true end end } end local Signal = {} function Signal.new(): _Signal local self: _Signal = { listeners = {}, on_empty = nil, on_not_empty = nil, connect = function(self: _Signal, listener: (T...) -> ()): _Connection local n = #self.listeners if n == 0 and self.on_not_empty then self.on_not_empty() end self.listeners[n + 1] = listener return Connection.new(self, listener) end } return self end function signal_create(): Signal local signal = Signal.new() return signal :: any -- todo end end -------------------------------------------------------------------------------- -- handle -------------------------------------------------------------------------------- local Handle = (function(): HandleMT local Handle = {} Handle.__index = Handle function Handle.destroy(self: Handle) self.world:destroy(self.entity) end function Handle.has_none(self: Handle): boolean return self.world:has_none(self.entity) end function Handle.add(self: Handle, ...: T...) self.world:add(self.entity, ...) end function Handle.set(self: Handle, ctype: T, value: T): Handle self.world:set(self.entity, ctype, value) return self end function Handle.insert(self: Handle, ctype: Array, value: T): Handle self.world:insert(self.entity, ctype, value) return self end function Handle.patch(self: Handle, ctype: T, fn: ((T) -> T)?): T return self.world:patch(self.entity, ctype, fn) end function Handle.has(self: Handle, ...: T...): boolean return self.world:has(self.entity, ...) end function Handle.get(self: Handle, ...: T...): T... return self.world:get(self.entity, ...) end function Handle.try_get(self: Handle, ctype: T): T? return self.world:try_get(self.entity, ctype) end function Handle.remove(self: Handle, ...: T...) self.world:remove(self.entity, ...) end return Handle end)() -------------------------------------------------------------------------------- -- world -------------------------------------------------------------------------------- local function world_create(range_start: number?, range_stop: number?): Registry local MAX_CTYPE = ctype_n if range_start then assert(range_stop) ASSERT(range_start > 0, "start of partition must be at least 1") ASSERT(range_stop >= range_start, "end of partition must be greater then start") ASSERT(range_stop <= ID_MAX_VALID_KEY, `end of partition cannot be greater than {ID_MAX_VALID_KEY}`) end local partition_start = range_start and range_start or 1 local partition_stop = range_stop and range_stop or ID_MAX_VALID_KEY local world = {} local entity_list: EntityList = entity_list_create() local entity_pool = pool_create() local signals = { on_add = {} :: Map>, on_change = {} :: Map>, on_remove = {} :: Map>, on_clear = {} :: Map>, after_clear = {} :: Map> } local handle_cache = {} :: Map setmetatable(handle_cache :: any, { __mode = "v" }) local ctype_pools: Map> = table.create(MAX_CTYPE) setmetatable(ctype_pools :: any, { __index = function(self, ctype: CType): Pool ASSERT_CTYPE_VALID(ctype) ASSERT(ctype ~= ctype_entity, "attempt to use entity type") ASSERT((ctype :: number) <= MAX_CTYPE, `cannot use {ctype_debug(ctype)}, component must be created before world creation`) local pool = pool_create(1) self[ctype] = pool return pool end }) local function STORAGE(ctype: T): Pool return ctype_pools[ctype] :: Pool end local function group_init(...: any): GroupData local group = { size = 0, added = false, connections = {} } for i = 1, select("#", ...) do local ctype = select(i, ...) local pool = STORAGE(ctype) group[i] = pool pool.group = group table.insert(group.connections, world:on_add(ctype):connect(function(id) POOL_TRY_GROUP(pool, id) end)) table.insert(group.connections, world:on_remove(ctype):connect(function(id) POOL_TRY_UNGROUP(pool, id) end)) world:on_clear(ctype):connect(function() for _, cn in group.connections do cn:disconnect() end end) world:after_clear(ctype):connect(function() group.size = 0 for _, cn in group.connections do cn:reconnect() end end) end local pool = STORAGE((...)) for i = 0, pool.size - 1 do POOL_TRY_GROUP(pool, BUFFER_GET(pool.entities, i)) end return group end local function ASSERT_VALID_ENTITY(id: entity) ASSERT(ENTITY_LIST_ID_IN_USE(entity_list, id), "invalid entity") end function world.create(self: Registry, desired_id: entity?): entity if not desired_id then if entity_list.free == ID_NULL_KEY then local old_capacity = entity_list.capacity local old_max = old_capacity - 1 ASSERT(old_max < partition_stop, "cannot create entity; world is at max entities") local new_capacity = math.ceil((old_capacity + 1) * 1.5) local new_max = new_capacity - 1 if new_max > partition_stop then new_capacity = partition_stop + 1 elseif new_max < partition_start then new_capacity = partition_start + 1 end entity_list_resize(entity_list, new_capacity, partition_start, partition_stop) end assert(entity_list.free ~= ID_NULL_KEY) local new_key = entity_list.free local next_key_cur_ver = BUFFER_GET(entity_list.data, new_key) local new_id = ID_CREATE(new_key, ID_VER(next_key_cur_ver)) BUFFER_SET(entity_list.data, new_key, new_id) entity_list.free = ID_KEY(next_key_cur_ver) POOL_RESIZE_MAP_IF_NEEDED(entity_pool, new_key) POOL_ADD_ID(entity_pool, new_id) return new_id else local desired_key = ID_KEY(desired_id) local desired_ver = ID_VER(desired_id) ASSERT( desired_id < ID_MAX and desired_key ~= ID_NULL_KEY and desired_key <= ID_MAX_VALID_KEY and desired_ver ~= ID_NULL_VER and desired_ver <= ID_MASK_VER, "malformed id" ) if desired_key > entity_list.capacity - 1 then local new_capacity = (desired_id + 1) * 1.5 if new_capacity - 1 > ID_MAX_VALID_KEY then new_capacity = ID_MAX_VALID_KEY + 1 end entity_list_resize(entity_list, new_capacity, partition_start, partition_stop) end ASSERT(BUFFER_GET(entity_list.data, desired_key) ~= desired_id, "unable to create entity; key is already in use") if entity_list.free == desired_key then entity_list.free = ID_KEY(BUFFER_GET(entity_list.data, desired_key)) elseif BUFFER_GET(entity_list.data, desired_key) ~= ID_NULL then -- is somewhere along linked list local prev = entity_list_get_prev_key(entity_list, desired_key) BUFFER_SET(entity_list.data, prev, ID_CREATE( -- a -> b -> c, remove b, link a -> c ID_KEY(BUFFER_GET(entity_list.data, desired_key)), ID_VER(BUFFER_GET(entity_list.data, prev)) )) end BUFFER_SET(entity_list.data, desired_key, desired_id) POOL_RESIZE_MAP_IF_NEEDED(entity_pool, desired_key) POOL_ADD_ID(entity_pool, desired_id) return desired_id end end function world.release(self: Registry, id: entity) ASSERT_VALID_ENTITY(id) if entity_pool.on_remove then fire(entity_pool.on_remove, id) end entity_list_remove(entity_list, id, partition_start, partition_stop) POOL_REMOVE(entity_pool, ID_KEY(id), id) end function world.destroy(self: Registry, id: entity) ASSERT_VALID_ENTITY(id) local key = ID_KEY(id) for _, pool in ctype_pools do if POOL_HAS(pool, key) then POOL_REMOVE(pool, key, id) end end if entity_pool.on_remove then fire(entity_pool.on_remove, id) end entity_list_remove(entity_list, id, partition_start, partition_stop) POOL_REMOVE(entity_pool, ID_KEY(id), id) end function world.contains(self: Registry, id: entity): boolean return ENTITY_LIST_ID_IN_USE(entity_list, id) end function world.has_none(self: Registry, id: entity): boolean ASSERT_VALID_ENTITY(id) local key = ID_KEY(id) for _, pool in next, ctype_pools do if POOL_HAS(pool, key) then return false end end return true end function world.add(self: Registry, id: entity, ...: T...) ASSERT_VALID_ENTITY(id) local key = ID_KEY(id) for i = 1, select("#", ...) do local ctype = select(i, ...) local pool = STORAGE(ctype) POOL_RESIZE_MAP_IF_NEEDED(pool, key) if BUFFER_GET(pool.map, key) ~= ID_NULL then continue end local ctor = ctype_ctors[ctype] if ctor == true then POOL_ADD_ID(pool, id) elseif ctor == false then throw(`no constructor defined for {ctype_debug(ctype, i)}`) else local value = (ctor :: () -> unknown)() if value == nil then throw(`{ctype_debug(ctype, i)} constructor did not return a value`) end POOL_ADD(pool, id, value) end end end function world.set(self: Registry, id: entity, ctype: T, value: T) local pool = STORAGE(ctype) local key = ID_KEY(id) POOL_RESIZE_MAP_IF_NEEDED(pool, key) local idx_ver = BUFFER_GET(pool.map, key) if value ~= nil then -- valued component if idx_ver ~= ID_NULL then -- already added, change value ID_ASSERT_VERSION_EQUAL(idx_ver, id) if pool.on_change then fire(pool.on_change, id, value) end ARRAY_SET(pool.values, ID_KEY(idx_ver), value) else -- not added, add value ASSERT_VALID_ENTITY(id) POOL_ADD(pool, id, value) end else -- tag component ASSERT_VALID_ENTITY(id) ASSERT(ctype_is_tag(ctype), "cannot set component value to nil") if idx_ver ~= ID_NULL then return end POOL_ADD_ID(pool, id) end end function world.insert(self: Registry, id: entity, ctype: Array, value: T) local pool = STORAGE(ctype) local key = ID_KEY(id) POOL_RESIZE_MAP_IF_NEEDED(pool, key) local idx_ver = BUFFER_GET(pool.map, key) if idx_ver ~= ID_NULL then ID_ASSERT_VERSION_EQUAL(idx_ver, id) local idx = ID_KEY(idx_ver) local t = ARRAY_GET(pool.values, idx) table.insert(t, value) if pool.on_change then fire(pool.on_change, id, t) end else ASSERT_VALID_ENTITY(id) local t = {value} POOL_ADD(pool, id, t) end end function world.patch(self: Registry, id: entity, ctype: T, fn: ((T) -> T)?): T local pool = STORAGE(ctype) local key = ID_KEY(id) POOL_RESIZE_MAP_IF_NEEDED(pool, key) local idx_ver = BUFFER_GET(pool.map, key) if idx_ver ~= ID_NULL then ID_ASSERT_VERSION_EQUAL(idx_ver, id) local idx = ID_KEY(idx_ver) if fn then local value = fn(ARRAY_GET(pool.values, idx)) ASSERT(value ~= nil, "function cannot return nil") if pool.on_change then fire(pool.on_change, id, value) end ARRAY_SET(pool.values, idx, value) return value else local value = ARRAY_GET(pool.values, idx) if pool.on_change then fire(pool.on_change, id, value) end return value end else ASSERT_VALID_ENTITY(id) local ctor = ctype_ctors[ctype] if ctor == false or ctor == true then throw(`entity does not have component and no constructor for {ctype_debug(ctype)}`) end local value = (ctor :: () -> T)() if fn then value = fn(value) end ASSERT(value ~= nil, "function cannot return nil") POOL_ADD(pool, id, value) return value end end world.has = (function(self: Registry, id: entity, a, b, c, d, e): boolean local key = ID_KEY(id) local idx_ver = POOL_FIND(STORAGE(a), key) return idx_ver ~= ID_NULL and ID_VER(idx_ver) == ID_VER(id) and (b == nil or POOL_FIND(STORAGE(b), key) ~= ID_NULL) and (c == nil or POOL_FIND(STORAGE(c), key) ~= ID_NULL) and (d == nil or POOL_FIND(STORAGE(d), key) ~= ID_NULL) and (e == nil or throw("args exceeded") :: never) end :: any) :: (self: Registry, id: entity, T...) -> boolean local function UNSAFE_GET(ctype: CType, key: number): unknown local pool = STORAGE(ctype) local idx_ver = POOL_FIND(pool, key) if idx_ver == ID_NULL then throw("entity does not have " .. ctype_debug(ctype)) end return ARRAY_GET(pool.values, ID_KEY(idx_ver)) end world.get = (function(self: Registry, id: entity, a, b, c, d, e): ...unknown local pool = STORAGE(a) local key = ID_KEY(id) local idx_ver = POOL_FIND(pool, key) if idx_ver == ID_NULL then throw(`entity does not have {ctype_debug(a, 1)}`) end ID_ASSERT_VERSION_EQUAL(idx_ver, id) local va = ARRAY_GET(pool.values, ID_KEY(idx_ver)) if b == nil then return va elseif c == nil then return va, UNSAFE_GET(b, key) elseif d == nil then return va, UNSAFE_GET(b, key), UNSAFE_GET(c, key) elseif e == nil then return va, UNSAFE_GET(b, key), UNSAFE_GET(c, key), UNSAFE_GET(d, key) else throw("args exceeded") end end :: any) :: (self: Registry, id: entity, T...) -> T... function world.try_get(self: Registry, id: entity, ctype: T): T? local pool = STORAGE(ctype) local idx_ver = POOL_FIND(pool, ID_KEY(id)) if idx_ver == ID_NULL or ID_VERSION_NOT_EQUAL(idx_ver, id) then return nil end return ARRAY_GET(pool.values, ID_KEY(idx_ver)) end function world.remove(self: Registry, id: entity, ...: T...) for i = 1, select("#", ...) do local pool = STORAGE(select(i, ...)) POOL_TRY_REMOVE(pool, id) end end function world.clear(self: Registry, ...: T...) local argn = select("#", ...) if argn > 0 then for i = 1, argn do pool_clear(STORAGE(select(i, ...))) end else for _, pool in next, ctype_pools do pool_clear(pool) end pool_clear(entity_pool) entity_list_clear(entity_list, partition_start, partition_stop) end end function world.find(self: Registry, ctype: T, value: T): entity? local pool = STORAGE(ctype) if value == nil then if pool.size == 0 then return nil end return BUFFER_GET(pool.entities, 0) else local arr_idx = table.find(pool.values, value) return arr_idx and BUFFER_GET(pool.entities, arr_idx - 1) end end function world.copy(self: Registry, copy: T, paste: T) local pool_a = STORAGE(copy) local pool_b = STORAGE(paste) POOL_COPY(pool_a, pool_b) end function world.query(self: Registry, ...: T...): View ASSERT(select("#", ...) > 0, "query must contain at least 1 component") return query_create(world, ...) end function world.track(self: Registry, ...: T...): Observer ASSERT(select("#", ...) > 0, "observer must contain at least 1 component") return observer_create(world, ...) end world.group = nil :: any -- todo: why? function world.group(self: Registry, ...: T...): Group local argn = select("#", ...) ASSERT(argn > 1, "group must contain at least 2 components") local group = STORAGE((select(1, ...))).group :: GroupData for i = 1, argn do local ctype = select(i, ...) ASSERT_CTYPE_VALID(ctype, i) ASSERT(STORAGE(ctype).group == group, `cannot create group; {ctype_debug(ctype, i)} is not owned by the same group as previous args`) end return group_create(world, group or group_init(...), ...) end local function STORAGE_OR_ENTITY_POOL(ctype: T): Pool return ctype == ctype_entity and entity_pool :: Pool or STORAGE(ctype) end function world.storage(self: Registry, ctype: T?): any if ctype == "list" then return entity_list elseif ctype then return STORAGE_OR_ENTITY_POOL(ctype) else return coroutine.wrap(function() for ctype, pool in next, ctype_pools do coroutine.yield(ctype, pool) end end) end end function world.on_add(self: Registry, ctype: T): Signal return (signals.on_add[ctype] or (function() local signal = signal_create() :: any signals.on_add[ctype] = signal function signal.on_empty() STORAGE_OR_ENTITY_POOL(ctype).on_add = false end function signal.on_not_empty() STORAGE_OR_ENTITY_POOL(ctype).on_add = signal.listeners end return signal end)()) :: Signal end function world.on_change(self: Registry, ctype: T): Signal return (signals.on_change[ctype] or (function() local signal = signal_create() :: any signals.on_change[ctype] = signal function signal.on_empty() STORAGE_OR_ENTITY_POOL(ctype).on_change = false end function signal.on_not_empty() STORAGE_OR_ENTITY_POOL(ctype).on_change = signal.listeners end return signal end)()) :: Signal end function world.on_remove(self: Registry, ctype: T): Signal return signals.on_remove[ctype] or (function() local signal = signal_create() :: any signals.on_remove[ctype] = signal function signal.on_empty() STORAGE_OR_ENTITY_POOL(ctype).on_remove = false end function signal.on_not_empty() STORAGE_OR_ENTITY_POOL(ctype).on_remove = signal.listeners end return signal end)() :: Signal end function world.on_clear(self: Registry, ctype: T): Signal<> return signals.on_clear[ctype] or (function() local signal = signal_create() :: any signals.on_clear[ctype] = signal function signal.on_empty() STORAGE_OR_ENTITY_POOL(ctype).on_clear = false end function signal.on_not_empty() STORAGE_OR_ENTITY_POOL(ctype).on_clear = signal.listeners end return signal end)() :: Signal<> end function world.after_clear(self: Registry, ctype: T): Signal<> return signals.after_clear[ctype] or (function() local signal = signal_create() :: any signals.after_clear[ctype] = signal function signal.on_empty() STORAGE_OR_ENTITY_POOL(ctype).after_clear = false end function signal.on_not_empty() STORAGE_OR_ENTITY_POOL(ctype).after_clear = signal.listeners end return signal end)() :: Signal<> end function world.handle(self: Registry, id: entity?): Handle id = id or world:create() local handle = handle_cache[id] if not handle then handle = table.freeze(setmetatable({ world = world, entity = id :: entity }, Handle)) handle_cache[id] = handle end return assert(handle) end function world.context(self: Registry): Handle if not ENTITY_LIST_ID_IN_USE(entity_list, ID_CTX) then world:create(ID_CTX) end return world:handle(ID_CTX) end return table.freeze(world) end -------------------------------------------------------------------------------- -- queue -------------------------------------------------------------------------------- local Queue = {} do Queue.__index = Queue type _Queue = Queue<...any> & { size: number, columns: Array> } function Queue.new(): Queue local self: _Queue = setmetatable({ size = 0, columns = {} }, Queue) :: any setmetatable(self.columns, { __index = function(columns: Array>, idx: number) columns[idx] = {} return columns[idx] end }) return self :: Queue end function Queue.add(self: _Queue, ...: unknown) -- iteration will stop if first value is `nil` ASSERT((...) ~= nil, "first argument cannot be nil") local columns = self.columns local n = self.size + 1 self.size = n for i = 1, select("#", ...) do columns[i][n] = select(i, ...) end end function Queue.clear(self: _Queue) self.size = 0 for _, column in next, self.columns do table.clear(column) end end local function iter(self: _Queue) local columns = self.columns local n = self.size local i = 0 if #columns <= 1 then local column = columns[1] return function() i += 1 local value = column[i] if i == n then self:clear() end return value end else local tuple = table.create(#columns) return function() i += 1 for ci, column in next, columns do tuple[ci] = column[i] end if i == n then self:clear() end return unpack(tuple) end end end Queue.iter = function(self) return iter(self) end Queue.__iter = function(self) return iter(self) end function Queue.__len(self: _Queue) return self.size end end type ISignal = { connect: (self: any, listener: (T...) -> ()) -> () } | { Connect: (self: any, listener: (T...) -> ()) -> () } | (listener: (T...) -> ()) -> () local queue_create = function(signal: ISignal?): Queue local queue = Queue.new() if signal then local function listener(...: T...) queue:add(...) end if type(signal) == "function" then signal(listener) else local connector = (signal :: any).connect or (signal :: any).Connect ASSERT(connector, "signal has no member `connect()`") connector(signal, listener) end end return queue end :: ( () -> Queue ) & ( (signal: ISignal) -> Queue ) -------------------------------------------------------------------------------- -- buffer util -------------------------------------------------------------------------------- local function buffer_to_array(buf: buffer, size: number, arr: Array?): Array ASSERT(size * ID_SIZE <= buffer.len(buf), "buffer is smaller than given size") arr = arr or table.create(size); assert(arr) for i = 0, size - 1 do arr[i + 1] = BUFFER_GET(buf, i) end return arr end local function array_to_buffer(arr: Array, size: number, buf: buffer?): buffer buf = buf or buffer.create(size * ID_SIZE); assert(buf) ASSERT(size * ID_SIZE <= buffer.len(buf), "size is larger than buffer size") for i = 1, size do BUFFER_SET(buf, (i - 1), arr[i]) end return buf end local function buffer_to_buffer(buf_src: buffer, size: number, buf_tar: buffer?): buffer buf_tar = buf_tar or buffer.create(size * ID_SIZE); assert(buf_tar) ASSERT(size * ID_SIZE <= buffer.len(buf_tar), "size is larger than buffer size") buffer.copy(buf_tar, 0, buf_src, 0, size * ID_SIZE) return buf_tar end -------------------------------------------------------------------------------- -- export -------------------------------------------------------------------------------- local ecr = { world = world_create, component = function(constructor: () -> ()?) ASSERT(constructor == nil or type(constructor) == "function", "constructor must be a function") return ctype_create(constructor or false) end :: (() -> unknown) & ((constructor: () -> T) -> T), tag = function() return ctype_create(true) end :: () -> nil, is_tag = function(ctype: T): boolean ASSERT_CTYPE_VALID(ctype) return ctype_is_tag(ctype) end, name = function(v: T): T | string? if type(v) == "table" then for name, ctype in next, v do ASSERT(CTYPE_VALID(ctype), `{name} refers to an invalid component`) ctype_set_name(ctype, name) end return v else ASSERT_CTYPE_VALID(v) return ctype_names[v] end end :: ((names: T & {}) -> T) & ((ctype: T) -> string?), queue = queue_create, array_to_buffer = array_to_buffer, buffer_to_array = buffer_to_array, buffer_to_buffer = buffer_to_buffer, is_pair = function() return false end, entity = ctype_entity, null = ID_NULL, context = ID_CTX, id_size = ID_SIZE, inspect = function(id: entity): (number, number) local key, ver = ID_KEY(id), ID_VER(id) return key, ver * ID_RSHIFT end, _test = { ver_shift = ID_LSHIFT, max_ver = ID_MASK_VER * ID_RSHIFT, max_creatable = ID_MASK_KEY - 1, create_id = function(key: number, ver: number) return ID_CREATE(key, ver * ID_LSHIFT) end, set_key_version = function(reg: Registry, key: number, ver: number) local list = reg:storage("list") :: any local key_ver = BUFFER_GET(list.data, key) if ENTITY_LIST_ID_IN_USE(list, key_ver) then ASSERT(false, "attempt to set version of in-use entity") end BUFFER_SET(list.data, key, ID_CREATE(ID_KEY(key_ver), ver * ID_LSHIFT)) end, get_key_version = function(reg: Registry, key: number): number local list = reg:storage("list") :: any ASSERT(list.capacity > key, "not contained") return ID_VER(BUFFER_GET(list.data, key)) * ID_RSHIFT end } } return table.freeze(ecr)