export type Id = [number, number]; export const enum Liveness { DEAD = 0, ALIVE = 1, INACTIVE = 2 } export interface HasGeneration { generation: number; } export interface EntityState { alive: Liveness; } export type Store = (T & HasGeneration)[]; export type SparseStore = Record; export class Data { entity: Store = []; [name: string]: Store<{}> | SparseStore<{}>; } /** * Create an entity in the store * @param data store * @param assign map of components to attach * @param state Liveness state, allows creating an inactive entity * @returns the new entity's ID and generation */ type Assigner = { [S in keyof DATA]?: Pick> }; export function Create(data: DATA, assign: Assigner, state = Liveness.ALIVE): Id { const entities = data.entity; // find free ID let freeId = -1; let generation = -1; for(let id = 0; id < entities.length; id++) { if(entities[id].alive == Liveness.DEAD) { freeId = id; generation = entities[id].generation + 1; break; } } if(freeId == -1) { freeId = entities.length; generation = 1; } entities[freeId] = { generation, alive: state }; for(const key in assign) { data[key][freeId] = {...(assign[key] as {}), generation}; } return [freeId, generation]; } /** * "Delete" an entity * @param data store * @param id entity ID * @param generation entity ID generation * @param state can be set to Liveness.INACTIVE to disable an entity without actually killing it, for later resurrection */ export function Remove(data: DATA, [id, generation]: Id, state = Liveness.DEAD) { if(data.entity[id] && data.entity[id].generation == generation) { data.entity[id].alive = state; } } // Ergonomic Lookup typings type ItemType = S extends Record ? T : never; type StoreType = K extends keyof DATA ? ItemType : never; type StoreTypes = { [I in keyof K]: StoreType; }; type MaybeStoreTypes = { [I in keyof K]: StoreType | null; }; /** * Look up components that may or may not exist for an entity * @param data store * @param param1 entity Id * @param components names of components to look for * @returns the cooresponding components, with unfound ones replaced by nulls */ export function Lookup(data: DATA, [id, generation]: Id, ...components: K): MaybeStoreTypes { const entity = data.entity[id]; // inactive entities are fine to lookup, but dead ones are not if(entity && entity.generation == generation && entity.alive != Liveness.DEAD) { return components.map(storeName => { const component = data[storeName][id]; if(component && component.generation == generation) { return component; } else { return null; } }) as MaybeStoreTypes; } else { return components.map(() => null) as MaybeStoreTypes; } } // Ergonomic Join typings export function Join< DATA extends Data, A extends keyof DATA, > ( data: DATA, a: A, ): [ Id, DATA[A][number] ][]; export function Join< DATA extends Data, A extends keyof DATA, B extends keyof DATA, > ( data: DATA, a: A, b: B, ): [ Id, DATA[A][number], DATA[B][number] ][]; export function Join< DATA extends Data, A extends keyof DATA, B extends keyof DATA, C extends keyof DATA, > ( data: DATA, a: A, b: B, c: C, ): [ Id, DATA[A][number], DATA[B][number], DATA[C][number] ][]; export function Join< DATA extends Data, A extends keyof DATA, B extends keyof DATA, C extends keyof DATA, D extends keyof DATA, > ( data: DATA, a: A, b: B, c: C, d: D, ): [ Id, DATA[A][number], DATA[B][number], DATA[C][number], DATA[D][number] ][]; /** * Query a Data collection for all Alive entities possessing the named set of Components. * @returns an array of tuples containing the matching entity [ID, generation]s & associated Components */ export function Join(data: DATA, ...components: K[]): [Id, ...{}[]][] { const entities = data.entity; const stores: (Store<{}>|SparseStore<{}>)[] = components.map(name => data[name]); const results: [Id, ...{}[]][] = []; const firstStore = stores[0]; if(Array.isArray(firstStore)) { for(let id = 0; id < firstStore.length; id++) { JoinLoop(id, entities, stores, results); } } else { for(const id in firstStore) { JoinLoop(Number(id), entities, stores, results); } } return results; } function JoinLoop(id: number, entities: Store, stores: (Store<{}>|SparseStore<{}>)[], results: [Id, ...{}[]][]) { const result: [Id, ...{}[]] = [[id, -1]]; let generation = -1; for (const store of stores) { const component = store[id]; if(component && (component.generation == generation || generation == -1)) { generation = component.generation; result.push(component); } else { return; } } // only accept active entities (do this check here) const entity = entities[id]; if(entity.alive != Liveness.ALIVE || generation != entity.generation) return; // backpatch generation now that it's known result[0][1] = generation; results.push(result); }