Created
December 20, 2024 23:02
-
-
Save clinuxrulz/d51ae124195a89962af091216bf98db3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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