Skip to content

Instantly share code, notes, and snippets.

@clinuxrulz
Created December 20, 2024 23:02
Show Gist options
  • Save clinuxrulz/d51ae124195a89962af091216bf98db3 to your computer and use it in GitHub Desktop.
Save clinuxrulz/d51ae124195a89962af091216bf98db3 to your computer and use it in GitHub Desktop.
import { SetStoreFunction, Store, createStore, reconcile, unwrap } from "solid-js/store";
import { parseJsonViaPropertiesSchema, writeJsonViaPropertiesSchema } from "../model/PropertiesSchema";
import { Result, err, ok } from "../model/Result";
import { Component, ComponentType, IsComponent, IsComponentType } from "./Component";
import { Registry } from "./Registry";
import { generateUUID } from "three/src/math/MathUtils";
import { parentComponentType } from "./components/ParentComponent";
import { childrenComponentType, ChildrenState } from "./components/ChildrenComponent";
import { Accessor, batch, createMemo, createSignal, onCleanup, untrack } from "solid-js";
type WorldEvent = {
type: "EntityCreated",
entityId: string,
} | {
type: "EntityDestroyed",
entityId: string,
} | {
type: "EntityComponentsSet",
entityId: string,
components: { [componentType: string]: IsComponent, },
} | {
type: "EntityComponentsUnset",
entityId: string,
componentTypes: string[],
};
type WorldMonitor<A> = {
handleEvent: (event: WorldEvent) => void,
result: Accessor<A>,
refCount: number,
};
export class World {
private entities: {
[entityId: string]: {
[componentType: string]: IsComponent,
},
};
private allEntitiesMonitor: WorldMonitor<string[]> | undefined = undefined;
private entityComponentMonitor: Map<string, WorldMonitor<IsComponent | undefined>> = new Map();
private entityComponentSetMonitor: Map<string, WorldMonitor<{
[componentType: string]: IsComponent,
} | undefined>> = new Map();
private entitiesWithComponentTypesMonitors: Map<string, WorldMonitor<string[]>> = new Map();
private monitors: Set<WorldMonitor<unknown>> = new Set();
private entityMonitors: Map<string, Set<WorldMonitor<unknown>>> = new Map();
private addEntityMonitor(entityId: string, monitor: WorldMonitor<unknown>) {
let entityMonitors = this.entityMonitors.get(entityId);
if (entityMonitors == undefined) {
this.entityMonitors.set(entityId, new Set([monitor]));
} else {
entityMonitors.add(monitor);
}
}
private removeEntityMonitor(entityId: string, monitor: WorldMonitor<unknown>) {
let entityMonitors = this.entityMonitors.get(entityId);
if (entityMonitors == undefined) {
return;
}
entityMonitors.delete(monitor);
if (entityMonitors.size === 0) {
this.entityMonitors.delete(entityId);
}
}
private fireEvent(event: WorldEvent) {
untrack(() => {
for (let monitor of this.monitors) {
monitor.handleEvent(event);
}
let entityMonitors = this.entityMonitors.get(event.entityId);
if (entityMonitors != undefined) {
for (let monitor of entityMonitors) {
monitor.handleEvent(event);
}
}
});
}
hookupAllEntities(): Accessor<string[]> {
let hookupCleanup = (monitor: WorldMonitor<string[]>) => {
onCleanup(() => {
monitor.refCount--;
if (monitor.refCount === 0) {
this.monitors.delete(monitor);
this.allEntitiesMonitor = undefined;
}
});
};
{
let monitor = this.allEntitiesMonitor;
if (monitor != undefined) {
monitor.refCount++;
hookupCleanup(monitor);
return monitor.result;
}
}
let initValue = Object.keys(this.entities);
let [ result, setResult, ] = createSignal<string[]>(
initValue,
);
let resultSet = new Set<string>(initValue);
let monitor: WorldMonitor<string[]> = {
handleEvent: (event) => {
switch (event.type) {
case "EntityCreated": {
if (!resultSet.has(event.entityId)) {
resultSet.add(event.entityId);
setResult([...resultSet]);
}
break;
}
case "EntityDestroyed": {
if (resultSet.has(event.entityId)) {
resultSet.delete(event.entityId);
setResult([...resultSet]);
}
break;
}
}
},
result,
refCount: 1,
};
this.allEntitiesMonitor = monitor;
this.monitors.add(monitor);
hookupCleanup(monitor);
return result;
}
hookupEntityComponent<A extends object>(entityId: string, componentType: ComponentType<A>): Accessor<Component<A> | undefined> {
let key = entityId + "," + componentType.typeName;
let hookupCleanup = (monitor: WorldMonitor<IsComponent | undefined>) => {
onCleanup(() => {
monitor.refCount--;
if (monitor.refCount === 0) {
this.removeEntityMonitor(entityId, monitor);
this.entityComponentMonitor.delete(key);
}
});
};
{
let monitor = this.entityComponentMonitor.get(key);
if (monitor != undefined) {
monitor.refCount++;
hookupCleanup(monitor);
return monitor.result as Accessor<Component<A> | undefined>;
}
}
let [ result, setResult, ] = createSignal<IsComponent | undefined>(
this.entities[entityId]?.[componentType.typeName],
);
let monitor: WorldMonitor<IsComponent | undefined> = {
handleEvent: (event) => {
switch (event.type) {
case "EntityComponentsSet": {
if (event.entityId == entityId) {
if (event.components[componentType.typeName] != undefined) {
setResult(event.components[componentType.typeName]);
}
}
break;
}
case "EntityComponentsUnset": {
if (event.entityId == entityId) {
if (event.componentTypes.some((componentTypeName) => componentTypeName == componentType.typeName)) {
setResult(undefined);
}
}
break;
}
}
},
result,
refCount: 1,
};
this.entityComponentMonitor.set(key, monitor);
this.addEntityMonitor(entityId, monitor);
hookupCleanup(monitor);
return result as Accessor<Component<A> | undefined>;
}
hookupEntityComponentSet(entityId: string): Accessor<{
[componentType: string]: IsComponent,
} | undefined> {
let key = entityId;
let hookupCleanup = (monitor: WorldMonitor<{
[componentType: string]: IsComponent,
} | undefined>) => {
onCleanup(() => {
monitor.refCount--;
if (monitor.refCount === 0) {
this.removeEntityMonitor(entityId, monitor);
this.entityComponentSetMonitor.delete(key);
}
});
};
{
let monitor = this.entityComponentSetMonitor.get(key);
if (monitor != undefined) {
monitor.refCount++;
hookupCleanup(monitor);
return monitor.result;
}
}
let [ result, setResult, ] = createSignal<{
[componentType: string]: IsComponent,
} | undefined>(
this.entities[entityId],
);
let monitor: WorldMonitor<{
[componentType: string]: IsComponent,
} | undefined> = {
handleEvent: (event) => {
switch (event.type) {
case "EntityComponentsSet": {
if (event.entityId == entityId) {
setResult(this.entities[entityId] == undefined ? undefined : { ...this.entities[entityId], });
}
break;
}
case "EntityComponentsUnset": {
if (event.entityId == entityId) {
setResult(this.entities[entityId] == undefined ? undefined : { ...this.entities[entityId], });
}
break;
}
}
},
result,
refCount: 1,
};
this.entityComponentSetMonitor.set(key, monitor);
this.addEntityMonitor(entityId, monitor);
hookupCleanup(monitor);
return result;
}
hookupEntitiesWithComponentTypes(componentTypes: IsComponentType[]): Accessor<string[]> {
let key = componentTypes.map((componentType) => componentType.typeName).sort().join(",");
let hookupCleanup = (monitor: WorldMonitor<string[]>) => {
onCleanup(() => {
monitor.refCount--;
if (monitor.refCount === 0) {
this.monitors.delete(monitor);
this.entitiesWithComponentTypesMonitors.delete(key);
}
});
};
{
let monitor = this.entitiesWithComponentTypesMonitors.get(key);
if (monitor != undefined) {
monitor.refCount++;
hookupCleanup(monitor);
return monitor.result;
}
}
let componentTypesSet = new Set(componentTypes.map((c) => c.typeName));
let checkEntityForMatch = (entityId: string) => {
for (let componentType of componentTypes) {
if (this.entities[entityId]?.[componentType.typeName] == undefined) {
return false;
}
}
return true;
};
let initValue = Object.keys(this.entities).filter(checkEntityForMatch);
let [ result, setResult, ] = createSignal<string[]>(
initValue,
);
let resultSet = new Set<string>(initValue);
let monitor: WorldMonitor<string[]> = {
handleEvent: (event) => {
switch (event.type) {
case "EntityComponentsSet": {
if (checkEntityForMatch(event.entityId)) {
if (!resultSet.has(event.entityId)) {
resultSet.add(event.entityId)
setResult([...result(), event.entityId]);
}
}
break;
}
case "EntityComponentsUnset": {
if (event.componentTypes.some((x) => componentTypesSet.has(x))) {
if (resultSet.has(event.entityId)) {
resultSet.delete(event.entityId);
setResult(result().filter((entityId) => entityId != event.entityId));
}
}
break;
}
}
},
result,
refCount: 1,
};
this.entitiesWithComponentTypesMonitors.set(key, monitor);
this.monitors.add(monitor);
hookupCleanup(monitor);
return result;
}
hookupChildEntities(entityId: string): Accessor<string[]> {
let childComponent = this.hookupEntityComponent(entityId, childrenComponentType);
return createMemo(() => {
let childComponent2 = childComponent();
return childComponent2 == undefined ? [] : childComponent2.state.childIds;
});
}
hookupDescendantEntities(entityId: string): Accessor<string[]> {
let children = this.hookupChildEntities(entityId);
let descendants = createMemo(() => {
let children2 = children();
let result: Accessor<string[]>[] = [];
for (let child of children2) {
result.push(this.hookupDescendantEntities(child));
}
return result;
});
return createMemo(() => [
...children(),
...descendants().flatMap((x) => x()),
]);
}
private constructor() {
this.entities = {};
}
static mkEmpty(): World {
return new World();
}
static fromJSON(registry: Registry, obj: any): Result<World> {
let world = new World();
if (!obj.hasOwnProperty("entities")) {
return err("worldFromJson: entities field is missing.");
}
let entities = obj["entities"];
if (typeof entities !== "object") {
return err("worldFromJson: entities is meant to be an object.");
}
for (let entityId in entities) {
let componentTypes = entities[entityId];
if (typeof componentTypes !== "object") {
return err("worldFromJson: entity with id `" + entityId + "` has componentTypes that needs to be an object.");
}
let components: { [componentType: string]: IsComponent } = {};
for (let componentType in componentTypes) {
let component = componentTypes[componentType];
if (typeof component !== "object") {
return err("worldFromJson: entity with id `" + entityId + "` has a component `" + componentType + "` that needs to be an object.");
}
let componentType2 = registry.lookupComponentType(componentType);
if (componentType2 == undefined) {
return err("worldFromJson: entity with id `" + entityId + "` has a componentType of `" + componentType + "` that is not found in registry.");
}
let val = parseJsonViaPropertiesSchema<object>(componentType2.propertiesSchema, component, false);
if (val.type == "Err") {
return err("worldFromJson: entity with id `" + entityId + "`, componentType `" + componentType + "`: " + val.message);
}
components[componentType] = componentType2.create(val.value);
}
world.entities[entityId] = components;
}
return ok(world);
}
toJSON(): any {
let result = {
entities: {} as any,
};
for (let entityId in this.entities) {
let entity = this.entities[entityId];
let components: any = {};
for (let componentType in entity) {
let component = entity[componentType];
let component2 = writeJsonViaPropertiesSchema(component.type.propertiesSchema, component.state);
components[componentType] = component2;
}
result.entities[entityId] = components;
}
return result;
}
deepClone(registry: Registry): World {
let world = World.fromJSON(registry, this.toJSON());
if (world.type == "Err") {
throw new Error("deepClone: " + world.message);
}
return world.value;
}
getEntities(): string[] {
return Object.keys(this.entities);
}
getEntitiesWithComponentTypes(componentTypes: IsComponentType[]): string[] {
let result: string[] = [];
for (let entityId in this.entities) {
if (componentTypes.every((componentType) => this.hasComponent(entityId, componentType))) {
result.push(entityId);
}
}
return result;
}
hasEntity(entityId: string): boolean {
return this.entities.hasOwnProperty(entityId);
}
createEntityWithId(entityId: string, components: IsComponent[]): void {
let components2: { [componentType: string]: IsComponent } = {};
for (let component of components) {
components2[component.type.typeName] = component;
}
this.entities[entityId] = components2;
batch(() => {
this.fireEvent({
type: "EntityCreated",
entityId,
});
if (components.length != 0) {
this.fireEvent({
type: "EntityComponentsSet",
entityId,
components: components2,
});
}
});
}
createEntity(components: IsComponent[]): string {
let entityId = generateUUID();
this.createEntityWithId(entityId, components);
return entityId;
}
destroyEntity(entityId: string): void {
let parentComponent = this.getComponent(entityId, parentComponentType);
if (parentComponent != undefined) {
let childrenComponent = this.getComponent(parentComponent.state.parentId, childrenComponentType);
if (childrenComponent != undefined) {
childrenComponent.setState("childIds", childrenComponent.state.childIds.filter((id) => id != entityId));
}
}
let components = this.entities[entityId];
if (components != undefined) {
this.fireEvent({
type: "EntityComponentsUnset",
entityId,
componentTypes: Object.keys(components),
});
}
delete this.entities[entityId];
this.fireEvent({
type: "EntityDestroyed",
entityId,
});
}
destroyEntityAndDescendants(entityId: string): void {
batch(() => {
let parentComponent = this.getComponent(entityId, parentComponentType);
if (parentComponent != undefined) {
let childrenComponent = this.getComponent(parentComponent.state.parentId, childrenComponentType);
if (childrenComponent != undefined) {
childrenComponent.setState("childIds", childrenComponent.state.childIds.filter((id) => id != entityId));
}
}
let stack = [ entityId, ];
while (true) {
let atEntityId = stack.pop();
if (atEntityId == undefined) {
break;
}
let childrenComponent = this.getComponent(atEntityId, childrenComponentType);
if (childrenComponent != undefined) {
stack.push(...childrenComponent.state.childIds);
}
let components = this.entities[entityId];
if (components != undefined) {
this.fireEvent({
type: "EntityComponentsUnset",
entityId,
componentTypes: Object.keys(components),
});
}
delete this.entities[atEntityId];
this.fireEvent({
type: "EntityDestroyed",
entityId: atEntityId,
});
}
});
}
getDescendantEntities(entityId: string): string[] {
let result: string[] = [];
let stack = [ entityId, ];
while (true) {
let atEntityId = stack.pop();
if (atEntityId == undefined) {
break;
}
let childrenComponent = this.getComponent(atEntityId, childrenComponentType);
if (childrenComponent != undefined) {
stack.push(...childrenComponent.state.childIds);
result.push(...childrenComponent.state.childIds);
}
}
return result;
}
hasComponent(entityId: string, componentType: IsComponentType): boolean {
let components = this.entities[entityId];
if (components == undefined) {
return false;
}
return components[componentType.typeName] != undefined;
}
getComponent<A extends object>(entityId: string, componentType: ComponentType<A>): Component<A> | undefined {
let components = this.entities[entityId];
if (components == undefined) {
return undefined;
}
return components[componentType.typeName] as Component<A> | undefined;
}
getComponents(entityId: string): IsComponent[] {
let components = this.entities[entityId];
if (components == undefined) {
return [];
}
return Object.values(components);
}
getComponentSet(entityId: string): { [componentType: string]: IsComponent } {
return this.entities[entityId] ?? {};
}
getComponentTypes(entityId: string): string[] {
let components = this.entities[entityId];
if (components == undefined) {
return [];
}
return Object.keys(components);
}
setComponents(entityId: string, components: IsComponent[]) {
let components2: { [componentType: string]: IsComponent } | undefined = this.entities[entityId];
if (components2 == undefined) {
components2 = {};
this.entities[entityId] = components2;
}
for (let component of components) {
components2[component.type.typeName] = component;
}
this.fireEvent({
type: "EntityComponentsSet",
entityId,
components: components2,
});
}
unsetComponents(entityId: string, componentTypes: IsComponentType[]) {
let components2: { [componentType: string]: IsComponent } | undefined = this.entities[entityId];
if (components2 == undefined) {
return;
}
for (let componentType of componentTypes) {
delete components2[componentType.typeName];
}
this.fireEvent({
type: "EntityComponentsUnset",
entityId,
componentTypes: componentTypes.map((componentType) => componentType.typeName),
});
}
}
export function entityComponentsGetComponent<A extends object>(entityComponents: { [componentType: string]: IsComponent }, componentType: ComponentType<A>): Component<A> | undefined {
return entityComponents[componentType.typeName] as Component<A> | undefined;
}
export function entityComponentsSetComponentPurely<A extends object>(entityComponents: { [componentType: string]: IsComponent, }, component: Component<A>): { [componentType: string]: IsComponent, } {
let result = { ...entityComponents, };
result[component.type.typeName] = component;
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment