@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
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Chase Moskal
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
# 🏛️ archimedes netlogic engine
|
|
5
|
+
|
|
6
|
+
> [***"do not disturb my circles!"***](https://en.wikipedia.org/wiki/noli_turbare_circulos_meos!)
|
|
7
|
+
> — *archimedes, c. 212 bc*
|
|
8
|
+
|
|
9
|
+
## rollforward netcode for web games
|
|
10
|
+
|
|
11
|
+
🔮 **automatic networking**
|
|
12
|
+
you just code your game naively as though it's singleplayer, archimedes handles the multiplayer.
|
|
13
|
+
|
|
14
|
+
🌎 **whole-world rollforward**
|
|
15
|
+
clientside prediction applies to the whole simulation. all effects of player inputs feel instant.
|
|
16
|
+
|
|
17
|
+
🧩 **ecs architecture**
|
|
18
|
+
program your simulation with entities, components, and systems.
|
|
19
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@benev/archimedes",
|
|
3
|
+
"version": "0.1.0-1",
|
|
4
|
+
"description": "game ecs with auto networking",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "x/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"x",
|
|
10
|
+
"s"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "octo-s 'rm -rf x' 'mkdir x' 'tsc'",
|
|
14
|
+
"dev": "octo 'node --watch x/test.js' 'tsc -w'",
|
|
15
|
+
"test": "node x/test.js",
|
|
16
|
+
"count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@e280/renraku": "^0.5.7",
|
|
20
|
+
"@e280/stz": "^0.2.27",
|
|
21
|
+
"@msgpack/msgpack": "^3.1.3",
|
|
22
|
+
"sparrow-rtc": "^0.2.15"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@e280/octo": "^0.1.0",
|
|
26
|
+
"@e280/science": "^0.1.9",
|
|
27
|
+
"npm-run-all": "^4.1.5",
|
|
28
|
+
"typescript": "^6.0.2"
|
|
29
|
+
},
|
|
30
|
+
"author": "Chase Moskal <chasemoskal@gmail.com>",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"networking",
|
|
33
|
+
"rollforward",
|
|
34
|
+
"web-games"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/benevolent-games/archimedes.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/benevolent-games/archimedes/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/benevolent-games/archimedes#readme"
|
|
44
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
## Archimedes Concepts
|
|
3
|
+
|
|
4
|
+
### Core concepts
|
|
5
|
+
|
|
6
|
+
**Simulator**
|
|
7
|
+
Abstract class that holds a game state and a `simulate` method. Also has a `tailor` method which can extract a subset of the state for a specific author.
|
|
8
|
+
- *Author ID*, clarifies who an input is coming from. Host alwaysg gets authorId `0`, each connected client gets a different number.
|
|
9
|
+
- *Telegram*, stream of information about the changing state of a simulation. Includes dispatches about state, delta, and input.
|
|
10
|
+
- *Schema*, the types that a simulator operates on.
|
|
11
|
+
- `state`, full representation of the game state.
|
|
12
|
+
- `delta`, partial game state, reprents an update/patch to be applied to the state.
|
|
13
|
+
- `input`, data that clients send back to the host, which may influence the simulation.
|
|
14
|
+
|
|
15
|
+
**Liaison**
|
|
16
|
+
Symmetric comms channel, which handles ping/pongs, jitter-smoothing, inbox and outbox. Clients and hosts use a liaison.
|
|
17
|
+
|
|
18
|
+
**Authority**
|
|
19
|
+
Hosting rig. Wires a simulator up with a liaison, providing a tick method that handles all communications.
|
|
20
|
+
|
|
21
|
+
**Speculator**
|
|
22
|
+
Client rig. Bundles up a pastSimulator and a futureSimulator, and coordinates with a liaison to implement roll-forward networking. Clientside should render the state of the futureSimulator, as it represents the clientside prediction.
|
|
23
|
+
|
|
24
|
+
### Session concepts
|
|
25
|
+
|
|
26
|
+
**SessionHost**
|
|
27
|
+
Creates an Authority and coordinates metadata and userland api setup.
|
|
28
|
+
|
|
29
|
+
**SessionClient**
|
|
30
|
+
Creates a Speculator and coordinates metadata and userland api setup.
|
|
31
|
+
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
# 🏛️ Archimedes
|
|
5
|
+
|
|
6
|
+
> [***"Do not disturb my circles!"***](https://en.wikipedia.org/wiki/Noli_turbare_circulos_meos!)
|
|
7
|
+
> — *Archimedes, c. 212 BC*
|
|
8
|
+
|
|
9
|
+
## Tournament-grade rollforward netcode for web games
|
|
10
|
+
|
|
11
|
+
🔮 **Whole-world rollforward:**
|
|
12
|
+
Player inputs feel instant, and mispredictions correct themselves. It's all automatic, so you don't even have to think about lag when you're coding your game's logic.
|
|
13
|
+
|
|
14
|
+
🚚 **Transport-agnostic:**
|
|
15
|
+
Default to free player-hosted games via [Sparrow RTC](https://github.com/benevolent-games/sparrow), but you can swap in webtransport or websockets if you want.
|
|
16
|
+
|
|
17
|
+
⛵ **Logic-agnostic:**
|
|
18
|
+
We don't care if your game has fancy-schmancy ECS architecture, or a humble classical setup, whatever: just subclass `Simulator` and it's smooth sailing.
|
|
19
|
+
|
|
20
|
+
🛟 **Seamless reconnects:**
|
|
21
|
+
Users *will* accidentally close their browser tab mid-game. Archimedes helps them pick up where they left off, no harm, no foul.
|
|
22
|
+
|
|
23
|
+
📦 **Npm package `@benev/archimedes`:**
|
|
24
|
+
Archimedes isn't finished yet, it's under active development.
|
|
25
|
+
|
|
26
|
+
<br/>
|
|
27
|
+
|
|
28
|
+
# 🤯 Eureka
|
|
29
|
+
|
|
30
|
+
> [***"Eureka! I have found it!"***](https://en.wikipedia.org/wiki/Eureka_%28word%29)
|
|
31
|
+
> — *Archimedes, sometime before 212 BC*
|
|
32
|
+
|
|
33
|
+
## An elegant ECS framework
|
|
34
|
+
|
|
35
|
+
*Eureka* is an Entity-Component-System framework. It was designed alongside Archimedes, and it's the easiest way to get into Archimedes without making a custom Simulator integration. Eureka can be used without Archimedes, and Archimedes can be used without Eureka — but when they're together, it's *a vibe.*
|
|
36
|
+
|
|
37
|
+
### Eureka quick setup
|
|
38
|
+
|
|
39
|
+
- Define your context.
|
|
40
|
+
```ts
|
|
41
|
+
// All your systems will have access to this.
|
|
42
|
+
export class MyContext {}
|
|
43
|
+
```
|
|
44
|
+
- Declare all your components.
|
|
45
|
+
```ts
|
|
46
|
+
// Each component can be any serializable data.
|
|
47
|
+
export type MyComponents = {
|
|
48
|
+
health: number
|
|
49
|
+
bleeding: number
|
|
50
|
+
mana: number
|
|
51
|
+
manaRegen: number
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
- Setup the Eureka helper.
|
|
55
|
+
It has your context and component types baked-in.
|
|
56
|
+
```ts
|
|
57
|
+
import {setupEureka} from "@benev/archimedes"
|
|
58
|
+
|
|
59
|
+
export const eureka = setupEureka<MyContext, MyComponents>()
|
|
60
|
+
```
|
|
61
|
+
- Create the world, and declare your systems.
|
|
62
|
+
Each system selects entities by their components, and runs behaviors on them.
|
|
63
|
+
```ts
|
|
64
|
+
// Instance your context.
|
|
65
|
+
const context = new MyContext()
|
|
66
|
+
|
|
67
|
+
// Create the world, providing context and an array of systems.
|
|
68
|
+
const world = eureka.world(context, [
|
|
69
|
+
|
|
70
|
+
// Give the system a friendly name.
|
|
71
|
+
eureka.system("my health system")
|
|
72
|
+
|
|
73
|
+
// We select which components this system operates on.
|
|
74
|
+
.select("health").andMaybe("bleeding")
|
|
75
|
+
|
|
76
|
+
// Behavior logic for this system.
|
|
77
|
+
.fn((entities, world) => {
|
|
78
|
+
for (const {id, components} of entities) {
|
|
79
|
+
|
|
80
|
+
// process bleeding
|
|
81
|
+
if (components.bleeding)
|
|
82
|
+
components.health -= components.bleeding
|
|
83
|
+
|
|
84
|
+
// process death
|
|
85
|
+
if (components.health <= 0)
|
|
86
|
+
world.delete(id)
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
|
|
90
|
+
eureka.system("mana regeneration")
|
|
91
|
+
.select("mana", "manaRegen")
|
|
92
|
+
.fn((entities, _world) => {
|
|
93
|
+
for (const {components} of entities)
|
|
94
|
+
components.mana += components.manaRegen
|
|
95
|
+
}),
|
|
96
|
+
])
|
|
97
|
+
```
|
|
98
|
+
- Systems are executed sequentially, from top to bottom.
|
|
99
|
+
- You can organize your systems into separate files, of course.
|
|
100
|
+
- Please notice that I painstakingly designed all of this for immaculate typescript typings. You are welcome 😌
|
|
101
|
+
|
|
102
|
+
### Running a Eureka world that does stuff
|
|
103
|
+
|
|
104
|
+
Okay, now it's time to put something into the world and watch something happen.
|
|
105
|
+
|
|
106
|
+
- Add an entity.
|
|
107
|
+
```ts
|
|
108
|
+
const warrior = world.create({health: 100, bleeding: 1})
|
|
109
|
+
```
|
|
110
|
+
- Execute world systems (one simulation tick).
|
|
111
|
+
```ts
|
|
112
|
+
world.execute()
|
|
113
|
+
|
|
114
|
+
// we see that the bleeding behavior worked.
|
|
115
|
+
warrior.components.health // 99
|
|
116
|
+
```
|
|
117
|
+
- Alright, that's the basics, go ahead and setup a 60hz tickloop.
|
|
118
|
+
```ts
|
|
119
|
+
setInterval(() => world.execute(), 1000 / 60)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Entity method reference
|
|
123
|
+
|
|
124
|
+
- `entity.id` — the id number for this entity.
|
|
125
|
+
```ts
|
|
126
|
+
entity.id // 123
|
|
127
|
+
```
|
|
128
|
+
- `entity.components` — the components object for this entity.
|
|
129
|
+
```ts
|
|
130
|
+
entity.components.health // 99
|
|
131
|
+
entity.components.bleeding // 1
|
|
132
|
+
```
|
|
133
|
+
- The components object is a proxy, and setting its properties automatically informs the eureka about the change.
|
|
134
|
+
```ts
|
|
135
|
+
entity.components.bleeding = 0 // change auto-detected
|
|
136
|
+
entity.components.health += 25 // change auto-detected
|
|
137
|
+
```
|
|
138
|
+
- You can also delete components, and that'll be detected
|
|
139
|
+
```ts
|
|
140
|
+
delete entity.components.bleeding // auto-detected
|
|
141
|
+
```
|
|
142
|
+
- If you want to change a component's shape in terms of components, you can do this:
|
|
143
|
+
```ts
|
|
144
|
+
entity.components.health // unhappy typescript, entity doesn't have health
|
|
145
|
+
|
|
146
|
+
// updating the entity
|
|
147
|
+
const entityWithHealth = world.update(entity.id, {
|
|
148
|
+
...entity.components,
|
|
149
|
+
health: 100,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
entityWithHealth.components.health // happy typescript, knows it has health
|
|
153
|
+
|
|
154
|
+
// note, this is really a typescript ergonomics thing
|
|
155
|
+
entity === entityWithHealth // true
|
|
156
|
+
```
|
|
157
|
+
- `entity.write()` — manually inform eureka that you've made changes inside the entity's components.
|
|
158
|
+
```ts
|
|
159
|
+
entity.components.myObject.health += 1 // change inside object, not auto-detected
|
|
160
|
+
entity.write() // manually tell eureka about the change
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### World method reference
|
|
164
|
+
|
|
165
|
+
Creating and updating entities.
|
|
166
|
+
- `world.create(components)` — a new entity.
|
|
167
|
+
```ts
|
|
168
|
+
const wizard = world.create({health: 100, mana: 50, manaRegen: 1})
|
|
169
|
+
```
|
|
170
|
+
- `world.update(id, components)` — create or update an entity.
|
|
171
|
+
```ts
|
|
172
|
+
world.write(wizard.id, {health: 100, mana: 75, manaRegen: 1})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Getting entities.
|
|
176
|
+
- `world.get(id)` — get an entity by id, or return undefined if not present.
|
|
177
|
+
```ts
|
|
178
|
+
const entity = world.get(id)
|
|
179
|
+
```
|
|
180
|
+
- `world.require(id)` — get an entity by id, or throw an error if not present.
|
|
181
|
+
```ts
|
|
182
|
+
const entity = world.require(id)
|
|
183
|
+
```
|
|
184
|
+
- `world.all()` — iterate over all entities
|
|
185
|
+
```ts
|
|
186
|
+
for (const entity of world.all)
|
|
187
|
+
console.log(entity.id)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Removing entities.
|
|
191
|
+
- `world.delete(id)` — remove an entity from the world.
|
|
192
|
+
```ts
|
|
193
|
+
world.delete(id)
|
|
194
|
+
```
|
|
195
|
+
- `world.clear()` — remove *everything* from the world.
|
|
196
|
+
```ts
|
|
197
|
+
world.clear()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Full-world data operations.
|
|
201
|
+
- `world.data()` — iterate over all entity data.
|
|
202
|
+
```ts
|
|
203
|
+
const data = [...world.data()]
|
|
204
|
+
```
|
|
205
|
+
- `world.overwrite(data)` — create or update all entities with the provided data.
|
|
206
|
+
```ts
|
|
207
|
+
const entity = world.overwrite(data)
|
|
208
|
+
```
|
|
209
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
import {Liaison} from "./liaison.js"
|
|
3
|
+
import {Simulator} from "./simulator.js"
|
|
4
|
+
import {Ticker} from "../tools/ticker.js"
|
|
5
|
+
import {IdCounter} from "../tools/id-counter.js"
|
|
6
|
+
import {isInputDispatch} from "./utils/is-input-dispatch.js"
|
|
7
|
+
import {DeltaDispatch, InputDispatch, Schema, StateDispatch, Telegram} from "./types.js"
|
|
8
|
+
|
|
9
|
+
export class Authority<xSchema extends Schema> {
|
|
10
|
+
idCounter = new IdCounter()
|
|
11
|
+
liaisons = new Set<Liaison<Telegram<xSchema>>>()
|
|
12
|
+
authorId = this.idCounter.next()
|
|
13
|
+
currentTick = 0
|
|
14
|
+
|
|
15
|
+
constructor(public simulator: Simulator<xSchema>) {}
|
|
16
|
+
|
|
17
|
+
tick() {
|
|
18
|
+
const tick = ++this.currentTick
|
|
19
|
+
const inputDispatches = this.#collectInputDispatches()
|
|
20
|
+
const delta = this.simulator.simulate([tick, inputDispatches])
|
|
21
|
+
const deltaDispatch: DeltaDispatch<xSchema> = ["delta", delta]
|
|
22
|
+
|
|
23
|
+
const broadcast: Telegram<xSchema> = [
|
|
24
|
+
tick,
|
|
25
|
+
[...inputDispatches, deltaDispatch],
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
for (const liaison of this.liaisons) {
|
|
29
|
+
const tailored = this.simulator.tailor(liaison.authorId, broadcast)
|
|
30
|
+
liaison.send(tailored)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
makeTicker(hz: number) {
|
|
35
|
+
return new Ticker(hz, () => this.tick())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getStateTelegram(): Telegram<xSchema> {
|
|
39
|
+
const dispatch: StateDispatch<xSchema> = ["state", this.simulator.state]
|
|
40
|
+
return [this.authorId, [dispatch]]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#collectInputDispatches() {
|
|
44
|
+
const dispatches: InputDispatch<xSchema>[] = []
|
|
45
|
+
|
|
46
|
+
for (const liaison of this.liaisons)
|
|
47
|
+
liaison.recv()
|
|
48
|
+
.map(([_, dispatches]) => ["input", [
|
|
49
|
+
|
|
50
|
+
// overwrite author id to prevent spoofing
|
|
51
|
+
liaison.authorId,
|
|
52
|
+
|
|
53
|
+
// filter for inputs
|
|
54
|
+
dispatches.filter(isInputDispatch),
|
|
55
|
+
|
|
56
|
+
]] as InputDispatch<xSchema>)
|
|
57
|
+
.forEach(t => dispatches.push(t))
|
|
58
|
+
|
|
59
|
+
return dispatches
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
import * as m from "@msgpack/msgpack"
|
|
3
|
+
import {asCodec} from "./types.js"
|
|
4
|
+
|
|
5
|
+
export const json = asCodec({
|
|
6
|
+
encode: <D>(data: D) => new TextEncoder().encode(JSON.stringify(data)),
|
|
7
|
+
decode: <D>(code: Uint8Array) => JSON.parse(new TextDecoder().decode(code)) as D,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const msgpack = asCodec({
|
|
11
|
+
encode: <D>(data: D) => m.encode(data),
|
|
12
|
+
decode: <D>(code: Uint8Array) => m.decode(code) as D,
|
|
13
|
+
})
|
|
14
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
import {coalesce} from "@e280/stz"
|
|
3
|
+
import {Science, test, expect, spy} from "@e280/science"
|
|
4
|
+
|
|
5
|
+
import {Contact} from "./contact.js"
|
|
6
|
+
import {wiring} from "./wiring.i.js"
|
|
7
|
+
|
|
8
|
+
export default Science.suite({
|
|
9
|
+
"binary exchange": test(async() => {
|
|
10
|
+
const alice = new Contact<string, number>()
|
|
11
|
+
const bob = new Contact<Uint8Array>()
|
|
12
|
+
const charlie = new Contact<Uint8Array>()
|
|
13
|
+
const debbie = new Contact<number, string>()
|
|
14
|
+
|
|
15
|
+
const stop = coalesce(
|
|
16
|
+
wiring.relayBinary(alice, bob),
|
|
17
|
+
wiring.relayBinary(debbie, charlie),
|
|
18
|
+
wiring.exchange(bob, charlie),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const debbieRecv = spy((_: number) => {})
|
|
22
|
+
debbie.recv.on(debbieRecv)
|
|
23
|
+
expect(debbieRecv.spy.calls.length).is(0)
|
|
24
|
+
|
|
25
|
+
alice.send(123, true)
|
|
26
|
+
expect(debbieRecv.spy.calls.length).is(1)
|
|
27
|
+
expect(debbieRecv.spy.calls.at(0)!.args[0]).is(123)
|
|
28
|
+
|
|
29
|
+
stop()
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
import {pub} from "@e280/stz"
|
|
3
|
+
import {StdCable} from "sparrow-rtc"
|
|
4
|
+
|
|
5
|
+
import {json} from "./codecs.js"
|
|
6
|
+
import {ContactInput, Codec} from "./types.js"
|
|
7
|
+
import {exchange, mirror, relayCable} from "./wiring.js"
|
|
8
|
+
|
|
9
|
+
export class Contact<I = any, O = I> {
|
|
10
|
+
send = pub<[output: O, reliable: boolean]>()
|
|
11
|
+
recv = pub<[input: I, reliable: boolean]>()
|
|
12
|
+
|
|
13
|
+
/** clear all send and recv listeners */
|
|
14
|
+
dispose() {
|
|
15
|
+
this.send.clear()
|
|
16
|
+
this.recv.clear()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** create a new contact that is an exchange partner (see wiring.exchange) */
|
|
20
|
+
exchange() {
|
|
21
|
+
const bob = new Contact<O, I>()
|
|
22
|
+
const detach = exchange(this, bob)
|
|
23
|
+
return [bob, detach] as [typeof bob, typeof detach]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** create a new contact that is a mirror (see wiring.mirror) */
|
|
27
|
+
mirror() {
|
|
28
|
+
const bob = new Contact<I, O>()
|
|
29
|
+
const detach = mirror(this, bob)
|
|
30
|
+
return [bob, detach] as [typeof bob, typeof detach]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** create a new contact that is a relay (see wiring.relay) */
|
|
34
|
+
relay() {
|
|
35
|
+
const bob = new Contact<I, O>()
|
|
36
|
+
const detach = mirror(this, bob)
|
|
37
|
+
return [bob, detach] as [typeof bob, typeof detach]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** attach an rtc cable to this contact as a relay */
|
|
41
|
+
relayCable(cable: StdCable, codec: Codec = json) {
|
|
42
|
+
return relayCable(this, cable, codec)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** roll multiple sub-contacts into a single mega-contact */
|
|
46
|
+
static multiplex<Sc extends {[key: string]: Contact}>(subcontacts: Sc) {
|
|
47
|
+
type Submessages = {[K in keyof Sc]: [K, ContactInput<Sc[K]>]}
|
|
48
|
+
type Submessage = Submessages[keyof Sc]
|
|
49
|
+
const megacontact = new Contact<Submessage>()
|
|
50
|
+
|
|
51
|
+
for (const [key, subcontact] of Object.entries(subcontacts))
|
|
52
|
+
subcontact.send.on((x, reliable) => megacontact.send([key, x], reliable))
|
|
53
|
+
|
|
54
|
+
megacontact.recv.on(([key, data], reliable) => {
|
|
55
|
+
const subcontact = subcontacts[key as any]
|
|
56
|
+
if (!subcontact) throw new Error(`unknown subcontact "${key as any}"`)
|
|
57
|
+
subcontact.recv(data as any, reliable)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return megacontact
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
import {Contact} from "./contact.js"
|
|
3
|
+
|
|
4
|
+
/** infer a contact's input data type */
|
|
5
|
+
export type ContactInput<C extends Contact> = (
|
|
6
|
+
C extends Contact<infer I>
|
|
7
|
+
? I
|
|
8
|
+
: never
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
/** infer a contact's output data type */
|
|
12
|
+
export type ContactOutput<C extends Contact> = (
|
|
13
|
+
C extends Contact<any, infer O>
|
|
14
|
+
? O
|
|
15
|
+
: never
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export type Codec = {
|
|
19
|
+
encode: <Data>(data: Data) => Uint8Array
|
|
20
|
+
decode: <Data>(code: Uint8Array) => Data
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const asCodec = (t: Codec) => t
|
|
24
|
+
|