@benev/archimedes 0.1.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +19 -0
- package/package.json +44 -0
- package/s/_archive/GLOSSARY.md +31 -0
- package/s/_archive/README.md +209 -0
- package/s/_archive/core/authority.ts +62 -0
- package/s/_archive/core/contact/codecs.i.ts +3 -0
- package/s/_archive/core/contact/codecs.ts +14 -0
- package/s/_archive/core/contact/contact.test.ts +32 -0
- package/s/_archive/core/contact/contact.ts +63 -0
- package/s/_archive/core/contact/types.ts +24 -0
- package/s/_archive/core/contact/wiring.i.ts +3 -0
- package/s/_archive/core/contact/wiring.ts +212 -0
- package/s/_archive/core/fiber.ts +94 -0
- package/s/_archive/core/liaison.ts +73 -0
- package/s/_archive/core/parcels/inbox.ts +83 -0
- package/s/_archive/core/parcels/parceller.ts +20 -0
- package/s/_archive/core/parcels/parcels.test.ts +136 -0
- package/s/_archive/core/parcels/types.ts +5 -0
- package/s/_archive/core/parcels/utils/nanny.ts +23 -0
- package/s/_archive/core/simulator.ts +11 -0
- package/s/_archive/core/speculator.ts +63 -0
- package/s/_archive/core/types.ts +32 -0
- package/s/_archive/core/utils/handle-telegrams.ts +34 -0
- package/s/_archive/core/utils/is-input-dispatch.ts +10 -0
- package/s/_archive/core/utils/make-telegram-for-inputs.ts +12 -0
- package/s/_archive/core/utils/on-channel-message.ts +11 -0
- package/s/_archive/eureka/eureka.ts +20 -0
- package/s/_archive/eureka/index.ts +0 -0
- package/s/_archive/eureka/integration/context.ts +9 -0
- package/s/_archive/eureka/integration/simulator.ts +75 -0
- package/s/_archive/eureka/integration/types.ts +12 -0
- package/s/_archive/eureka/integration/utils/inputs.ts +24 -0
- package/s/_archive/eureka/integration/utils/relevance.ts +10 -0
- package/s/_archive/eureka/parts/entity.ts +37 -0
- package/s/_archive/eureka/parts/system.ts +38 -0
- package/s/_archive/eureka/parts/types.ts +21 -0
- package/s/_archive/eureka/parts/world.ts +140 -0
- package/s/_archive/eureka/testing/eureka.test.ts +27 -0
- package/s/_archive/eureka/testing/integration.test.ts +40 -0
- package/s/_archive/eureka/testing/situations/health.ts +43 -0
- package/s/_archive/eureka/testing/situations/integration.ts +45 -0
- package/s/_archive/index.ts +6 -0
- package/s/_archive/session/client.ts +56 -0
- package/s/_archive/session/host.ts +71 -0
- package/s/_archive/session/meta/meta-client.ts +5 -0
- package/s/_archive/session/meta/meta-host.ts +18 -0
- package/s/_archive/session/meta/types.ts +15 -0
- package/s/_archive/session/parts/client-on.ts +8 -0
- package/s/_archive/session/parts/fiber-rpc.ts +28 -0
- package/s/_archive/session/parts/host-on.ts +9 -0
- package/s/_archive/session/parts/hub.ts +40 -0
- package/s/_archive/session/parts/netfibers.ts +14 -0
- package/s/_archive/session/parts/seat.ts +35 -0
- package/s/_archive/session/parts/spoke.ts +10 -0
- package/s/_archive/sugar/easy-host.ts +68 -0
- package/s/_archive/tests.test.ts +17 -0
- package/s/_archive/tools/averager.ts +29 -0
- package/s/_archive/tools/bucket.ts +17 -0
- package/s/_archive/tools/chronicle.ts +32 -0
- package/s/_archive/tools/disposers.ts +5 -0
- package/s/_archive/tools/id-counter.ts +13 -0
- package/s/_archive/tools/loop.ts +12 -0
- package/s/_archive/tools/pub.ts +22 -0
- package/s/_archive/tools/ticker.ts +22 -0
- package/s/_archive/tools/u8.ts +7 -0
- package/s/_archive/transports/sparrow/fibers.ts +19 -0
- package/s/_archive/transports/sparrow/start-sparrow-host.ts +26 -0
- package/s/_archive/transports/sparrow/utils/netfibers-from-cable.ts +10 -0
- package/s/ecs/index.ts +7 -0
- package/s/ecs/parts/changers.ts +16 -0
- package/s/ecs/parts/make-id.ts +7 -0
- package/s/ecs/parts/world.ts +68 -0
- package/s/ecs/test/setup-example-world.ts +42 -0
- package/s/ecs/test/setup-lifecycle-counts.ts +17 -0
- package/s/ecs/test.ts +99 -0
- package/s/ecs/types.ts +19 -0
- package/s/ecs/utils/is-match.ts +7 -0
- package/s/ecs/utils/optimizer.ts +38 -0
- package/s/index.ts +3 -0
- package/s/net/test.ts +9 -0
- package/s/net/types.ts +1 -0
- package/s/sim/test.ts +9 -0
- package/s/sim/types.ts +1 -0
- package/s/test.ts +8 -0
- package/x/ecs/index.d.ts +4 -0
- package/x/ecs/index.js +5 -0
- package/x/ecs/index.js.map +1 -0
- package/x/ecs/parts/changers.d.ts +4 -0
- package/x/ecs/parts/changers.js +11 -0
- package/x/ecs/parts/changers.js.map +1 -0
- package/x/ecs/parts/make-id.d.ts +1 -0
- package/x/ecs/parts/make-id.js +5 -0
- package/x/ecs/parts/make-id.js.map +1 -0
- package/x/ecs/parts/world.d.ts +11 -0
- package/x/ecs/parts/world.js +59 -0
- package/x/ecs/parts/world.js.map +1 -0
- package/x/ecs/test/setup-example-world.d.ts +10 -0
- package/x/ecs/test/setup-example-world.js +31 -0
- package/x/ecs/test/setup-example-world.js.map +1 -0
- package/x/ecs/test/setup-lifecycle-counts.d.ts +6 -0
- package/x/ecs/test/setup-lifecycle-counts.js +15 -0
- package/x/ecs/test/setup-lifecycle-counts.js.map +1 -0
- package/x/ecs/test.d.ts +13 -0
- package/x/ecs/test.js +85 -0
- package/x/ecs/test.js.map +1 -0
- package/x/ecs/types.d.ts +12 -0
- package/x/ecs/types.js +2 -0
- package/x/ecs/types.js.map +1 -0
- package/x/ecs/utils/is-match.d.ts +2 -0
- package/x/ecs/utils/is-match.js +4 -0
- package/x/ecs/utils/is-match.js.map +1 -0
- package/x/ecs/utils/optimizer.d.ts +9 -0
- package/x/ecs/utils/optimizer.js +37 -0
- package/x/ecs/utils/optimizer.js.map +1 -0
- package/x/index.d.ts +1 -0
- package/x/index.js +2 -0
- package/x/index.js.map +1 -0
- package/x/net/test.d.ts +4 -0
- package/x/net/test.js +7 -0
- package/x/net/test.js.map +1 -0
- package/x/net/types.d.ts +1 -0
- package/x/net/types.js +2 -0
- package/x/net/types.js.map +1 -0
- package/x/sim/test.d.ts +4 -0
- package/x/sim/test.js +7 -0
- package/x/sim/test.js.map +1 -0
- package/x/sim/types.d.ts +1 -0
- package/x/sim/types.js +2 -0
- package/x/sim/types.js.map +1 -0
- package/x/test.d.ts +1 -0
- package/x/test.js +6 -0
- package/x/test.js.map +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
import {Liaison} from "./liaison.js"
|
|
3
|
+
import {loop} from "../tools/loop.js"
|
|
4
|
+
import {Simulator} from "./simulator.js"
|
|
5
|
+
import {Chronicle} from "../tools/chronicle.js"
|
|
6
|
+
import {AuthorId, Schema, Telegram} from "./types.js"
|
|
7
|
+
import {makeTelegramForInputs} from "./utils/make-telegram-for-inputs.js"
|
|
8
|
+
|
|
9
|
+
export class Speculator<xSchema extends Schema> {
|
|
10
|
+
#currentTick = 0
|
|
11
|
+
#chronicle = new Chronicle<xSchema["input"]>()
|
|
12
|
+
|
|
13
|
+
maxIncomingTicks = 5
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
public authorId: AuthorId,
|
|
17
|
+
public liaison: Liaison<Telegram<xSchema>>,
|
|
18
|
+
public pastSimulator: Simulator<xSchema>,
|
|
19
|
+
public futureSimulator: Simulator<xSchema>,
|
|
20
|
+
public hz: number,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
get ticksAhead() {
|
|
24
|
+
const rtt = this.liaison.pingponger.averageRtt
|
|
25
|
+
return Math.round(rtt / (1000 / this.hz))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
tick() {
|
|
29
|
+
const telegrams = this.liaison.recv().slice(-this.maxIncomingTicks)
|
|
30
|
+
const lastTelegram = telegrams.at(-1)
|
|
31
|
+
|
|
32
|
+
for (const telegram of telegrams)
|
|
33
|
+
this.pastSimulator.simulate(telegram)
|
|
34
|
+
|
|
35
|
+
if (lastTelegram) {
|
|
36
|
+
const [latestTick] = lastTelegram
|
|
37
|
+
this.#currentTick = latestTick
|
|
38
|
+
}
|
|
39
|
+
else this.#currentTick += 1
|
|
40
|
+
|
|
41
|
+
// roll-forward
|
|
42
|
+
this.futureSimulator.state = structuredClone(this.pastSimulator.state)
|
|
43
|
+
|
|
44
|
+
for (const t of loop(this.ticksAhead)) {
|
|
45
|
+
for (const inputs of this.#chronicle.at(t)) {
|
|
46
|
+
this.futureSimulator.simulate(
|
|
47
|
+
makeTelegramForInputs(t, this.authorId, inputs)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
sendInputs(inputs: xSchema["input"][]) {
|
|
54
|
+
const futureTick = this.#currentTick + this.ticksAhead
|
|
55
|
+
|
|
56
|
+
// immediately send down the wire
|
|
57
|
+
this.liaison.send(makeTelegramForInputs(futureTick, this.authorId, inputs))
|
|
58
|
+
|
|
59
|
+
// schedule local inputs into the future
|
|
60
|
+
this.#chronicle.add(inputs, futureTick)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
import {Simulator} from "./simulator.js"
|
|
3
|
+
|
|
4
|
+
export type TickNumber = number
|
|
5
|
+
export type AuthorId = number
|
|
6
|
+
export type Authored<Content> = [AuthorId, Content]
|
|
7
|
+
|
|
8
|
+
export type Schema = {
|
|
9
|
+
state: any
|
|
10
|
+
delta: any
|
|
11
|
+
input: any
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type StateDispatch<xSchema extends Schema> = ["state", xSchema["state"]]
|
|
15
|
+
export type DeltaDispatch<xSchema extends Schema> = ["delta", xSchema["delta"]]
|
|
16
|
+
export type InputDispatch<xSchema extends Schema> = ["input", Authored<xSchema["input"]>]
|
|
17
|
+
|
|
18
|
+
export type Dispatch<xSchema extends Schema> = (
|
|
19
|
+
| StateDispatch<xSchema>
|
|
20
|
+
| DeltaDispatch<xSchema>
|
|
21
|
+
| InputDispatch<xSchema>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export type Telegram<xSchema extends Schema> = [TickNumber, Dispatch<xSchema>[]]
|
|
25
|
+
export type InputTelegram<xSchema extends Schema> = [TickNumber, InputDispatch<xSchema["input"]>]
|
|
26
|
+
|
|
27
|
+
export type InferSimulatorSchema<S extends Simulator<any>> = (
|
|
28
|
+
S extends Simulator<infer xSchema>
|
|
29
|
+
? xSchema
|
|
30
|
+
: never
|
|
31
|
+
)
|
|
32
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
import {AuthorId, Schema, Telegram} from "../types.js"
|
|
3
|
+
|
|
4
|
+
export function handleTelegram<xSchema extends Schema>(
|
|
5
|
+
telegram: Telegram<xSchema>,
|
|
6
|
+
callbacks: DispatchCallbacks<xSchema>,
|
|
7
|
+
) {
|
|
8
|
+
|
|
9
|
+
const [authorId, dispatches] = telegram
|
|
10
|
+
|
|
11
|
+
for (const [key, content] of dispatches) {
|
|
12
|
+
switch (key) {
|
|
13
|
+
|
|
14
|
+
case "state":
|
|
15
|
+
callbacks.state(content, authorId)
|
|
16
|
+
break
|
|
17
|
+
|
|
18
|
+
case "delta":
|
|
19
|
+
callbacks.delta(content, authorId)
|
|
20
|
+
break
|
|
21
|
+
|
|
22
|
+
case "input":
|
|
23
|
+
callbacks.input(content, authorId)
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type DispatchCallbacks<xSchema extends Schema> = {
|
|
30
|
+
state: (state: xSchema["state"], authorId: AuthorId) => void
|
|
31
|
+
delta: (delta: xSchema["delta"], authorId: AuthorId) => void
|
|
32
|
+
input: (input: xSchema["input"], authorId: AuthorId) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
import {Schema, Telegram} from "../types.js"
|
|
3
|
+
|
|
4
|
+
export function makeTelegramForInputs<xSchema extends Schema>(
|
|
5
|
+
futureTick: number,
|
|
6
|
+
authorId: number,
|
|
7
|
+
inputs: xSchema["input"][],
|
|
8
|
+
): Telegram<xSchema> {
|
|
9
|
+
|
|
10
|
+
return [futureTick, [["input", [authorId, inputs]]]]
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
export function onChannelMessage(
|
|
3
|
+
channel: RTCDataChannel,
|
|
4
|
+
onmessage: (message: any) => void,
|
|
5
|
+
) {
|
|
6
|
+
|
|
7
|
+
const listener = (event: MessageEvent) => onmessage(event.data)
|
|
8
|
+
channel.addEventListener("message", listener)
|
|
9
|
+
return () => channel.removeEventListener("message", listener)
|
|
10
|
+
}
|
|
11
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
import {World} from "./parts/world.js"
|
|
3
|
+
import {System} from "./parts/system.js"
|
|
4
|
+
import {Components, SystemFn} from "./parts/types.js"
|
|
5
|
+
|
|
6
|
+
export const setupEureka = <Context, C extends Components>() => ({
|
|
7
|
+
system: (label: string) => ({
|
|
8
|
+
select: <K extends keyof C>(...keys: K[]) => ({
|
|
9
|
+
fn: (fn: SystemFn<Context, C, K, undefined>) => new System(label, keys, [], fn),
|
|
10
|
+
andMaybe: <K2 extends keyof C>(...keysOptional: K2[]) => ({
|
|
11
|
+
fn: (fn: SystemFn<Context, C, K, K2>) => new System(label, keys, keysOptional, fn),
|
|
12
|
+
}),
|
|
13
|
+
}),
|
|
14
|
+
}),
|
|
15
|
+
|
|
16
|
+
world: (context: Context, systems: System[]) => (
|
|
17
|
+
new World<Context, C>(context, systems)
|
|
18
|
+
),
|
|
19
|
+
})
|
|
20
|
+
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
import {World} from "../parts/world.js"
|
|
3
|
+
import {EurekaSchema} from "./types.js"
|
|
4
|
+
import {EurekaContext} from "./context.js"
|
|
5
|
+
import {Components, EntityId} from "../parts/types.js"
|
|
6
|
+
import {Simulator} from "../../core/simulator.js"
|
|
7
|
+
import {AuthorId, Dispatch, Telegram} from "../../core/types.js"
|
|
8
|
+
|
|
9
|
+
export class EurekaSimulator
|
|
10
|
+
<xContext extends EurekaContext, C extends Components>
|
|
11
|
+
extends Simulator<EurekaSchema<C>> {
|
|
12
|
+
|
|
13
|
+
#deltas: EurekaSchema<C>["delta"] = []
|
|
14
|
+
#authorityId = 0
|
|
15
|
+
|
|
16
|
+
constructor(public world: World<xContext, C>) {
|
|
17
|
+
super([...world.data()])
|
|
18
|
+
world.on((id, entity) => void this.#deltas.push([id, entity?.components ?? null]))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
simulate(telegram: Telegram<EurekaSchema<C>>): EurekaSchema<C>["delta"] {
|
|
22
|
+
this.#deltas = []
|
|
23
|
+
|
|
24
|
+
Simulator.handleTelegram(telegram, {
|
|
25
|
+
input: (inputs) => {
|
|
26
|
+
this.world.context.inputs.add(inputs)
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
state: (state, authorId) => {
|
|
30
|
+
if (authorId === this.#authorityId) {
|
|
31
|
+
this.world.clear()
|
|
32
|
+
this.world.overwrite(state)
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
delta: (deltas, authorId) => {
|
|
37
|
+
if (authorId === this.#authorityId)
|
|
38
|
+
return undefined
|
|
39
|
+
for (const [id, components] of deltas)
|
|
40
|
+
this.world.write(id, components)
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
this.world.execute()
|
|
45
|
+
return this.#deltas
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
tailor(audienceAuthorId: AuthorId, telegram: Telegram<EurekaSchema<C>>): Telegram<EurekaSchema<C>> {
|
|
49
|
+
const check = (eid: EntityId) => this.world.context.relevance.check(audienceAuthorId, eid)
|
|
50
|
+
const [telegramAuthorId, dispatches] = telegram
|
|
51
|
+
const relevantDispatches: Dispatch<EurekaSchema<C>>[] = []
|
|
52
|
+
for (const [kind, x] of dispatches) {
|
|
53
|
+
switch (kind) {
|
|
54
|
+
|
|
55
|
+
case "state": {
|
|
56
|
+
relevantDispatches.push([kind, x.filter(([id]) => check(id))])
|
|
57
|
+
} break
|
|
58
|
+
|
|
59
|
+
case "delta": {
|
|
60
|
+
relevantDispatches.push([kind, x.filter(([id]) => check(id))])
|
|
61
|
+
} break
|
|
62
|
+
|
|
63
|
+
case "input": {
|
|
64
|
+
const [authorId, inputEntries] = x
|
|
65
|
+
relevantDispatches.push([
|
|
66
|
+
kind,
|
|
67
|
+
[authorId, inputEntries.filter(([id]) => check(id))],
|
|
68
|
+
])
|
|
69
|
+
} break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [telegramAuthorId, relevantDispatches]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
import {Components, EntityId, EntityData} from "../parts/types.js"
|
|
3
|
+
|
|
4
|
+
export type InputEntry = [EntityId, unknown[]]
|
|
5
|
+
export type Delta<C extends Components> = EntityData<C>
|
|
6
|
+
|
|
7
|
+
export type EurekaSchema<C extends Components> = {
|
|
8
|
+
state: EntityData<C>[]
|
|
9
|
+
delta: Delta<C>[]
|
|
10
|
+
input: InputEntry[]
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
import {InputEntry} from "../types.js"
|
|
4
|
+
import {EntityId} from "../../parts/types.js"
|
|
5
|
+
|
|
6
|
+
export class Inputs {
|
|
7
|
+
#inbox = new MapG<EntityId, unknown[]>()
|
|
8
|
+
|
|
9
|
+
add(entries: InputEntry[]) {
|
|
10
|
+
for (const [id, newInputs] of entries) {
|
|
11
|
+
const inputs = this.#inbox.guarantee(id, () => [])
|
|
12
|
+
inputs.push(...newInputs)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
read<Input = unknown>(id: EntityId) {
|
|
17
|
+
return (this.#inbox.get(id) ?? []) as Input[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clear() {
|
|
21
|
+
this.#inbox.clear()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
import {Components, EntityId, EntityData} from "./types.js"
|
|
3
|
+
|
|
4
|
+
export class Entity<C extends Components = any> {
|
|
5
|
+
static setComponents(entity: Entity, components: Components) {
|
|
6
|
+
entity.#components = new Proxy(components, {
|
|
7
|
+
set: (target: any, key: string, value: any) => {
|
|
8
|
+
if (value === undefined) delete target[key]
|
|
9
|
+
else target[key] = value
|
|
10
|
+
entity.onChange()
|
|
11
|
+
return true
|
|
12
|
+
},
|
|
13
|
+
deleteProperty: (target: any, key: string) => {
|
|
14
|
+
delete target[key]
|
|
15
|
+
entity.onChange()
|
|
16
|
+
return true
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#components!: C
|
|
22
|
+
get components() { return this.#components }
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
public readonly id: EntityId,
|
|
26
|
+
private onChange: () => void,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
write() {
|
|
30
|
+
this.onChange()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
data() {
|
|
34
|
+
return [this.id, this.components] as EntityData<C>
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
import {MapG} from "@e280/stz"
|
|
3
|
+
import {Entity} from "./entity.js"
|
|
4
|
+
import {World} from "./world.js"
|
|
5
|
+
import {SystemFn, UnknownComponents} from "./types.js"
|
|
6
|
+
|
|
7
|
+
export class System {
|
|
8
|
+
#cacheMap = new MapG<number, Entity>()
|
|
9
|
+
#cacheArray: Entity[] = []
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
public label: string,
|
|
13
|
+
public keys: any[],
|
|
14
|
+
public keysOptional: any[],
|
|
15
|
+
public fn: SystemFn<any, any, any, any>,
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
execute(world: World<any, any>) {
|
|
19
|
+
this.fn(this.#cacheArray, world)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
cache(id: number, entity: Entity | null) {
|
|
23
|
+
if (entity) {
|
|
24
|
+
if (this.#matching(entity)) this.#cacheMap.set(id, entity)
|
|
25
|
+
else this.#cacheMap.delete(id)
|
|
26
|
+
this.#cacheArray = [...this.#cacheMap.values()]
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.#cacheMap.delete(id)
|
|
30
|
+
this.#cacheArray = [...this.#cacheMap.values()]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#matching(entity: UnknownComponents) {
|
|
35
|
+
return this.keys.every(requiredKey => requiredKey in entity.components)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import {World} from "./world.js"
|
|
3
|
+
import {Entity} from "./entity.js"
|
|
4
|
+
|
|
5
|
+
export type Components = Record<string, any>
|
|
6
|
+
export type EntityId = number
|
|
7
|
+
|
|
8
|
+
export type EntityData<C extends Components = any> = [EntityId, UnknownComponents<C> | null]
|
|
9
|
+
export type UnknownComponents<C extends Components = any> = Partial<C>
|
|
10
|
+
export type SpecificComponents<C extends Components, K extends keyof C> = {[X in K]: C[X]}
|
|
11
|
+
|
|
12
|
+
export type FancySelect<C extends Components, K extends keyof C, K2 extends (keyof C) | undefined> = (
|
|
13
|
+
K2 extends keyof C
|
|
14
|
+
? Entity<SpecificComponents<C, K> & Partial<SpecificComponents<C, K2>>>
|
|
15
|
+
: Entity<SpecificComponents<C, K>>
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export type SystemFn<Context, C extends Components, K extends keyof C, K2 extends (keyof C) | undefined> = (
|
|
19
|
+
(entities: FancySelect<C, K, K2>[], world: World<Context, C>) => void
|
|
20
|
+
)
|
|
21
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
|
|
2
|
+
import {deep, MapG, sub} from "@e280/stz"
|
|
3
|
+
import {System} from "./system.js"
|
|
4
|
+
import {Entity} from "./entity.js"
|
|
5
|
+
import {IdCounter} from "../../tools/id-counter.js"
|
|
6
|
+
import {Components, EntityId, EntityData, UnknownComponents} from "./types.js"
|
|
7
|
+
|
|
8
|
+
export class World<Context, C extends Components> {
|
|
9
|
+
counter = new IdCounter()
|
|
10
|
+
on = sub<[EntityId, Entity<UnknownComponents<C>> | null]>()
|
|
11
|
+
|
|
12
|
+
#entities = new MapG<EntityId, Entity<Partial<C>>>()
|
|
13
|
+
#changed = new Set<EntityId>()
|
|
14
|
+
|
|
15
|
+
constructor(public context: Context, private systems: System[]) {}
|
|
16
|
+
|
|
17
|
+
/** iterate all entities */
|
|
18
|
+
all() {
|
|
19
|
+
return this.#entities.values()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** iterate all entity ids */
|
|
23
|
+
ids() {
|
|
24
|
+
return this.#entities.keys()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** get an entity by id (return null if not found) */
|
|
28
|
+
get<C2 extends Partial<C2>>(id: number) {
|
|
29
|
+
return (this.#entities.get(id) ?? null) as Entity<C2> | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** get an entity by id (throw an error if not found) */
|
|
33
|
+
require<C2 extends Partial<C2>>(id: number) {
|
|
34
|
+
return this.#entities.require(id) as unknown as C2
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** create, update, or delete an entity */
|
|
38
|
+
write<C2 extends Partial<C>>(id: number, components: C2 | null) {
|
|
39
|
+
if (components) {
|
|
40
|
+
const onChange = () => this.#changed.add(id)
|
|
41
|
+
|
|
42
|
+
// get or create entity
|
|
43
|
+
const entity = this.#entities.guarantee(
|
|
44
|
+
id,
|
|
45
|
+
() => new Entity(id, onChange),
|
|
46
|
+
) as Entity<C2>
|
|
47
|
+
|
|
48
|
+
Entity.setComponents(entity, components)
|
|
49
|
+
|
|
50
|
+
// initialize the systems cache for this entity
|
|
51
|
+
for (const system of this.systems)
|
|
52
|
+
system.cache(id, entity)
|
|
53
|
+
|
|
54
|
+
// recognize that a change happened
|
|
55
|
+
this.on.pub(id, entity)
|
|
56
|
+
return entity
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// delete the entity
|
|
60
|
+
this.#entities.delete(id)
|
|
61
|
+
|
|
62
|
+
// update the cache
|
|
63
|
+
for (const system of this.systems)
|
|
64
|
+
system.cache(id, null)
|
|
65
|
+
|
|
66
|
+
// recognize that a change happened
|
|
67
|
+
this.on.pub(id, null)
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** create a new entity */
|
|
73
|
+
create<C2 extends Partial<C>>(components: C2) {
|
|
74
|
+
const id = this.counter.next()
|
|
75
|
+
return this.write(id, components)!
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** update or create an entity */
|
|
79
|
+
update<C2 extends Partial<C>>(id: number, components: C2) {
|
|
80
|
+
return this.write(id, components) as Entity<C2>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** delete an entity, dispatching relevant pubsubs */
|
|
84
|
+
delete(id: number) {
|
|
85
|
+
return this.write(id, null)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** iterate all entity data */
|
|
89
|
+
*data() {
|
|
90
|
+
for (const entity of this.all())
|
|
91
|
+
yield entity.data()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** delete everything */
|
|
95
|
+
clear() {
|
|
96
|
+
for (const id of [...this.#entities.keys()])
|
|
97
|
+
this.delete(id)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** forget everything and start from a new state */
|
|
101
|
+
overwrite(data: EntityData<C>[]) {
|
|
102
|
+
for (const [id, components] of data)
|
|
103
|
+
this.write(id, components)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** execute all systems (and maintain caching stuff) */
|
|
107
|
+
execute() {
|
|
108
|
+
// execute all systems
|
|
109
|
+
for (const system of this.systems) {
|
|
110
|
+
system.execute(this)
|
|
111
|
+
|
|
112
|
+
// update all system caches for changes
|
|
113
|
+
for (const id of this.#changed) {
|
|
114
|
+
const entity = this.get(id)
|
|
115
|
+
for (const system of this.systems)
|
|
116
|
+
system.cache(id, entity)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// dispatch all pent up changes
|
|
121
|
+
for (const id of this.#changed)
|
|
122
|
+
this.on.pub(id, this.get(id))
|
|
123
|
+
|
|
124
|
+
// clear changes
|
|
125
|
+
this.#changed.clear()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** create a new downstream clone world that stays synchronized with this one */
|
|
129
|
+
replicate(
|
|
130
|
+
context: Context,
|
|
131
|
+
systems: System[],
|
|
132
|
+
) {
|
|
133
|
+
const downstream = new World<Context, C>(context, systems)
|
|
134
|
+
const detach = this.on((id, entity) => {
|
|
135
|
+
downstream.write(id, deep.clone(entity?.components ?? null))
|
|
136
|
+
})
|
|
137
|
+
return [downstream, detach]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
import {Science, test, expect} from "@e280/science"
|
|
3
|
+
import {setupHealthSituation} from "./situations/health.js"
|
|
4
|
+
|
|
5
|
+
export default Science.suite({
|
|
6
|
+
"systems can execute on the right entities": test(async() => {
|
|
7
|
+
const {world} = setupHealthSituation()
|
|
8
|
+
const warrior = world.create({health: 100, bleeding: 1})
|
|
9
|
+
const wizard = world.create({health: 100, mana: 0, manaRegen: 1})
|
|
10
|
+
expect(warrior.components.health).is(100)
|
|
11
|
+
expect(wizard.components.health).is(100)
|
|
12
|
+
world.execute()
|
|
13
|
+
expect(warrior.components.health).is(99)
|
|
14
|
+
expect(wizard.components.health).is(100)
|
|
15
|
+
}),
|
|
16
|
+
|
|
17
|
+
"warrior can bleed out, gets deleted": test(async() => {
|
|
18
|
+
const {world} = setupHealthSituation()
|
|
19
|
+
const warrior = world.create({health: 2, bleeding: 1})
|
|
20
|
+
expect(world.get(warrior.id)).ok()
|
|
21
|
+
world.execute()
|
|
22
|
+
expect(world.get(warrior.id)).ok()
|
|
23
|
+
world.execute()
|
|
24
|
+
expect(world.get(warrior.id)).not.ok()
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import {loop} from "@e280/stz"
|
|
3
|
+
import {Science, test, expect} from "@e280/science"
|
|
4
|
+
|
|
5
|
+
import {EasyHost} from "../../sugar/easy-host.js"
|
|
6
|
+
import {setupEurekaDemo} from "./situations/integration.js"
|
|
7
|
+
import {EurekaSimulator} from "../integration/simulator.js"
|
|
8
|
+
|
|
9
|
+
export default Science.suite({
|
|
10
|
+
|
|
11
|
+
"we host a game, we join it": test(async() => {
|
|
12
|
+
const host = new EasyHost({
|
|
13
|
+
hz: 10,
|
|
14
|
+
makeSimulator: () => {
|
|
15
|
+
const {world} = setupEurekaDemo()
|
|
16
|
+
return new EurekaSimulator(world)
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
const client = await host.localClient()
|
|
20
|
+
|
|
21
|
+
const {world} = host.session.simulator
|
|
22
|
+
const warrior = world.create({health: 100, bleeding: 1})
|
|
23
|
+
|
|
24
|
+
for (const _ of loop(50)) {
|
|
25
|
+
host.session.authority.tick()
|
|
26
|
+
client.speculator.tick()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(warrior.components.health).is(50)
|
|
30
|
+
|
|
31
|
+
// TODO wut?
|
|
32
|
+
// // FAILS HERE, component not found
|
|
33
|
+
// const clientWarrior = client.pastSimulator.world
|
|
34
|
+
// .require<typeof warrior.components>(warrior.id)
|
|
35
|
+
//
|
|
36
|
+
// expect(clientWarrior).ok()
|
|
37
|
+
// expect(clientWarrior.health).is(50)
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
import {setupEureka} from "../../eureka.js"
|
|
3
|
+
|
|
4
|
+
class MyContext {}
|
|
5
|
+
|
|
6
|
+
type MyComponents = {
|
|
7
|
+
health: number
|
|
8
|
+
bleeding: number
|
|
9
|
+
mana: number
|
|
10
|
+
manaRegen: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function setupHealthSituation() {
|
|
14
|
+
const eureka = setupEureka<MyContext, MyComponents>()
|
|
15
|
+
|
|
16
|
+
const world = eureka.world(new MyContext(), [
|
|
17
|
+
eureka.system("health")
|
|
18
|
+
.select("health").andMaybe("bleeding")
|
|
19
|
+
.fn((entities, world) => {
|
|
20
|
+
for (const {id, components} of entities) {
|
|
21
|
+
|
|
22
|
+
// process bleeding
|
|
23
|
+
if (components.bleeding)
|
|
24
|
+
components.health -= components.bleeding
|
|
25
|
+
|
|
26
|
+
// process death
|
|
27
|
+
if (components.health <= 0)
|
|
28
|
+
world.delete(id)
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
eureka.system("mana")
|
|
33
|
+
.select("mana").andMaybe("manaRegen")
|
|
34
|
+
.fn((entities, _world) => {
|
|
35
|
+
for (const {components} of entities)
|
|
36
|
+
if (components.manaRegen)
|
|
37
|
+
components.mana += components.manaRegen
|
|
38
|
+
}),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
return {world}
|
|
42
|
+
}
|
|
43
|
+
|