first commit

This commit is contained in:
Reid 2023-07-18 22:22:52 -07:00
commit 5302ecc6cc
43 changed files with 2530 additions and 0 deletions

View file

@ -0,0 +1,104 @@
import { AnyEntity, World } from "@rbxts/matter"
import { waitForEvent } from "ReplicatedStorage/remotes"
import * as Components from "./components"
import { State } from "./state"
type ComponentNames = keyof typeof Components
type ComponentConstructors = (typeof Components)[ComponentNames]
const DEBUG_SPAWN = "Spawn %ds%d with %s"
const DEBUG_DESPAWN = "Despawn %ds%d"
const DEBUG_MODIFY = "Modify %ds%d adding %s, removing %s"
let connection: RBXScriptConnection | undefined
/**
* Starts the replication receiver.
*
* @param world - The world to replicate components in
* @param state - The global state for the ECS
*/
export function start(world: World, state: State): void {
if (connection) return
function debugPrint(message: string, args: () => (string | number)[]): void {
if (state.debugEnabled) {
print("ECS Replication>", string.format(message, ...args()))
}
}
const replicationEvent = waitForEvent("EcsReplication")
const serverToClientEntity = new Map<string, AnyEntity>()
connection = replicationEvent.OnClientEvent.Connect(
(entities: Map<string, Map<ComponentNames, { data?: Components.GooplerComponentType }>>) => {
for (const [serverId, componentMap] of entities) {
const clientId = serverToClientEntity.get(serverId)
if (clientId !== undefined && next(componentMap)[0] === undefined) {
world.despawn(clientId)
serverToClientEntity.delete(serverId)
debugPrint(DEBUG_DESPAWN, () => [clientId, serverId])
continue
}
const componentsToInsert: Components.GooplerComponent[] = []
const componentsToRemove: ComponentConstructors[] = []
const insertNames: ComponentNames[] = []
const removeNames: ComponentNames[] = []
for (const [name, container] of componentMap) {
const component = Components[name]
if (container.data) {
componentsToInsert.push(
// The type of component above is an intersection of all possible
// component types since it can't know which specific component is
// associated with the name. Therefore here, we must cast to an
// intersection so that the data can be used.
//
// This is okay because the data must be associated with the name
// it was created with, but the type checker can't verify this for
// us. To solve this the type must somehow be associated with the
// name in the type system. For now, this cast works fine.
component(container.data as UnionToIntersection<Components.GooplerComponentType>)
)
insertNames.push(name)
} else {
componentsToRemove.push(component)
removeNames.push(name)
}
}
if (clientId === undefined) {
const clientId = world.spawn(...componentsToInsert)
serverToClientEntity.set(serverId, clientId)
debugPrint(DEBUG_SPAWN, () => [clientId, serverId, insertNames.join(",")])
} else {
if (componentsToInsert.size() > 0) {
world.insert(clientId, ...componentsToInsert)
}
if (componentsToRemove.size() > 0) {
world.remove(clientId, ...componentsToRemove)
}
debugPrint(DEBUG_MODIFY, () => [
clientId,
serverId,
insertNames.size() > 0 ? insertNames.join(", ") : "nothing",
removeNames.size() > 0 ? removeNames.join(", ") : "nothing"
])
}
}
}
)
}
/**
* Stops receiving replication.
*/
export function stop(): void {
if (!connection) return
connection.Disconnect()
connection = undefined
}