base2020/src/ecs/Data.ts

225 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>);
}