224 lines
7.1 KiB
TypeScript
224 lines
7.1 KiB
TypeScript
|
|
export type Jsonified<T> = {
|
|
[K in keyof T]: JsonEncoded<T[K]>;
|
|
};
|
|
export type JsonEncoded<T> =
|
|
T extends Function ? never
|
|
: T extends Date ? string
|
|
: T extends (infer U)[] ? JsonEncoded<U>[]
|
|
: T extends Record<string, any> ? Jsonified<T>
|
|
: T
|
|
;
|
|
|
|
export type Store<T extends Component<T>> = Record<number, T>;
|
|
export interface Clone<T> {
|
|
clone(): T;
|
|
}
|
|
|
|
export function copyDense<T extends Component<T>, S extends Store<T>>(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<T extends Component<T>, S extends Store<T>>(storage?: S): Store<T> {
|
|
if(storage) {
|
|
const result: Store<T> = {};
|
|
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<T> implements Clone<Component<T>> {
|
|
public generation: number;
|
|
public constructor(from: Partial<Component<T>>) {
|
|
this.generation = from.generation ?? -1;
|
|
}
|
|
abstract clone(): Component<T> & T;
|
|
}
|
|
export class EntityState extends Component<EntityState> {
|
|
public alive: Liveness;
|
|
public constructor(from: Partial<EntityState>) {
|
|
super(from);
|
|
this.alive = from.alive ?? Liveness.ALIVE;
|
|
}
|
|
|
|
clone(): EntityState {
|
|
return new EntityState(this);
|
|
}
|
|
}
|
|
|
|
export type StateForSchema<T> = {
|
|
[K in keyof T]: Record<number, Component<T[K]>>;
|
|
} & {
|
|
entity: EntityState[];
|
|
} & Clone<StateForSchema<T>>;
|
|
|
|
// Ergonomic Lookup typings
|
|
type StoreKeysOf<DATA> = {
|
|
[K in keyof DATA]: DATA[K] extends Record<number, Component<infer T>> ? K : never;
|
|
}[keyof DATA];
|
|
type StoreTypeOf<DATA, K> = K extends keyof DATA ? DATA[K] extends Record<number, Component<infer T>> ? 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<DATA> = {
|
|
[K in StoreKeysOf<DATA>]?: StoreTypeOf<DATA, K>;
|
|
};
|
|
export function Create<DATA extends StateForSchema<unknown>>(data: DATA, assign: Assigner<DATA>, 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<any>;
|
|
const component = (assign[key as keyof Assigner<DATA>] as Component<unknown>).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<unknown>, [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<DATA, Q> = {
|
|
[I in keyof Q]: StoreTypeOf<DATA, Q[I]> | null;
|
|
};
|
|
export function Lookup<DATA extends StateForSchema<unknown>, Q extends StoreKeysOf<DATA>[]>(data: DATA, [id, generation]: Id, ...components: Q): MaybeStoreTypes<DATA, Q> {
|
|
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<any>;
|
|
const component = store[id];
|
|
if (component && component.generation == generation) {
|
|
return component;
|
|
} else {
|
|
return null;
|
|
}
|
|
}) as MaybeStoreTypes<DATA, Q>;
|
|
} else {
|
|
return components.map(() => null) as MaybeStoreTypes<DATA, Q>;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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<DATA, Q> = {
|
|
[I in keyof Q]: Q[I] extends "id" ? Id : StoreTypeOf<DATA, Q[I]>;
|
|
};
|
|
export function Join<DATA extends StateForSchema<unknown>, Q extends (StoreKeysOf<DATA> | "id")[]>(data: DATA, ...components: Q): ResultTypes<DATA, Q>[] {
|
|
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<any>;
|
|
};
|
|
});
|
|
|
|
const results: ResultTypes<DATA, Q>[] = [];
|
|
const firstStore = stores.filter(store => store !== "id")[0] as Store<any>;
|
|
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<DATA extends StateForSchema<unknown>, Q extends (StoreKeysOf<DATA> | "id")[]>(id: number, entities: EntityState[], stores: (Record<number, Component<{}>> | "id")[], results: ResultTypes<DATA, Q>[]) {
|
|
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<DATA, Q>);
|
|
}
|