export type Jsonified = { [K in keyof T]: JsonEncoded; }; export type JsonEncoded = T extends Function ? never : T extends Date ? string : T extends (infer U)[] ? JsonEncoded[] : T extends Record ? Jsonified : T ; export type Store> = Record; export interface Clone { clone(): T; } export function copyDense, S extends Store>(storage?: S): T[] { if(Array.isArray(storage)) { return storage.map(x => x.clone()); } else if(storage) { const result: T[] = []; for(const key in storage) { result[key] = storage[key].clone(); } return result; } else { return []; } } export function copySparse, S extends Store>(storage?: S): Store { if(storage) { const result: Store = {}; for(const key in storage) { result[key] = storage[key].clone(); } return result; } else { return {}; } } export type Id = [number, number]; export const enum Liveness { DEAD = 0, ALIVE = 1, INACTIVE = 2 } export abstract class Component implements Clone> { public generation: number; public constructor(from: Partial>) { this.generation = from.generation ?? -1; } abstract clone(): Component & T; } export class EntityState extends Component { public alive: Liveness; public constructor(from: Partial) { super(from); this.alive = from.alive ?? Liveness.ALIVE; } clone(): EntityState { return new EntityState(this); } } export type StateForSchema = { [K in keyof T]: Record>; } & { entity: EntityState[]; } & Clone>; // Ergonomic Lookup typings type StoreKeysOf = { [K in keyof DATA]: DATA[K] extends Record> ? K : never; }[keyof DATA]; type StoreTypeOf = K extends keyof DATA ? DATA[K] extends Record> ? T : never : never; /** * 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 = { [K in StoreKeysOf]?: StoreTypeOf; }; 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] = new EntityState({ generation, alive: state }); for (const key in assign) { const store = data[key as keyof DATA] as Store; const component = (assign[key as keyof Assigner] as Component).clone(); component.generation = generation; store[freeId] = component; } 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: StateForSchema, [id, generation]: Id, state = Liveness.DEAD) { if (data.entity[id] && data.entity[id].generation == generation) { data.entity[id].alive = state; } } /** * 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 */ type MaybeStoreTypes = { [I in keyof Q]: StoreTypeOf | null; }; export function Lookup, Q extends StoreKeysOf[]>(data: DATA, [id, generation]: Id, ...components: Q): 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 store = data[storeName as unknown as keyof DATA] as Store; const component = store[id]; if (component && component.generation == generation) { return component; } else { return null; } }) as MaybeStoreTypes; } else { return components.map(() => null) as MaybeStoreTypes; } } /** * Query a Data collection for all Alive entities possessing the named set of Components. * "id" can be used as a pseudo-component to get the Ids for a match * @returns an array of tuples containing the matching entity Components */ type ResultTypes = { [I in keyof Q]: Q[I] extends "id" ? Id : StoreTypeOf; }; export function Join, Q extends (StoreKeysOf | "id")[]>(data: DATA, ...components: Q): ResultTypes[] { const entities = data.entity; const stores = components.map(name => { if (name == "id") { return "id"; } else { return data[name as unknown as keyof DATA] as Store; }; }); const results: ResultTypes[] = []; const firstStore = stores.filter(store => store !== "id")[0] as Store; 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, Q extends (StoreKeysOf | "id")[]>(id: number, entities: EntityState[], stores: (Record> | "id")[], results: ResultTypes[]) { const fullId: Id = [id, -1]; const result: (Component<{}> | Id)[] = []; let generation = -1; for (const store of stores) { if (store === "id") { result.push(fullId); continue; } 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, where the generation is known) const entity = entities[id]; if (entity.alive != Liveness.ALIVE || generation != entity.generation) return; // backpatch generation now that it's known fullId[1] = generation; results.push(result as ResultTypes); }