@driftengine/script 3.61.0
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 +202 -0
- package/NOTICE +9 -0
- package/README.md +81 -0
- package/dist/bindings/ai.d.ts +66 -0
- package/dist/bindings/ai.js +173 -0
- package/dist/bindings/animation.d.ts +13 -0
- package/dist/bindings/animation.js +177 -0
- package/dist/bindings/audio.d.ts +56 -0
- package/dist/bindings/audio.js +126 -0
- package/dist/bindings/behavior.d.ts +36 -0
- package/dist/bindings/behavior.js +101 -0
- package/dist/bindings/chemistry.d.ts +72 -0
- package/dist/bindings/chemistry.js +253 -0
- package/dist/bindings/core.d.ts +172 -0
- package/dist/bindings/core.js +814 -0
- package/dist/bindings/editor.d.ts +53 -0
- package/dist/bindings/editor.js +248 -0
- package/dist/bindings/entities.d.ts +73 -0
- package/dist/bindings/entities.js +339 -0
- package/dist/bindings/interface.d.ts +55 -0
- package/dist/bindings/interface.js +200 -0
- package/dist/bindings/navigation.d.ts +36 -0
- package/dist/bindings/navigation.js +255 -0
- package/dist/bindings/network.d.ts +51 -0
- package/dist/bindings/network.js +96 -0
- package/dist/bindings/render.d.ts +71 -0
- package/dist/bindings/render.js +143 -0
- package/dist/bindings/rollback.d.ts +41 -0
- package/dist/bindings/rollback.js +74 -0
- package/dist/bindings/sprites.d.ts +53 -0
- package/dist/bindings/sprites.js +186 -0
- package/dist/bindings/terrain.d.ts +54 -0
- package/dist/bindings/terrain.js +138 -0
- package/dist/bindings/xr.d.ts +56 -0
- package/dist/bindings/xr.js +155 -0
- package/dist/entityHost.d.ts +80 -0
- package/dist/entityHost.js +159 -0
- package/dist/host.d.ts +153 -0
- package/dist/host.js +233 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +18 -0
- package/package.json +68 -0
- package/src/bindings/ai.ts +306 -0
- package/src/bindings/animation.ts +346 -0
- package/src/bindings/audio.ts +185 -0
- package/src/bindings/behavior.ts +181 -0
- package/src/bindings/chemistry.ts +548 -0
- package/src/bindings/core.ts +1604 -0
- package/src/bindings/editor.ts +393 -0
- package/src/bindings/entities.ts +550 -0
- package/src/bindings/interface.ts +300 -0
- package/src/bindings/navigation.ts +367 -0
- package/src/bindings/network.ts +178 -0
- package/src/bindings/render.ts +191 -0
- package/src/bindings/rollback.ts +110 -0
- package/src/bindings/sprites.ts +310 -0
- package/src/bindings/terrain.ts +180 -0
- package/src/bindings/xr.ts +272 -0
- package/src/entityHost.ts +219 -0
- package/src/host.ts +465 -0
- package/src/index.ts +41 -0
package/dist/host.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { BEHAVIOR_CAPABILITIES, BEHAVIOR_MODULE, BEHAVIOR_TYPES, behaviorImplementation, } from './bindings/behavior.js';
|
|
2
|
+
import { NAVIGATION_CAPABILITIES, NAVIGATION_MODULE, NAVIGATION_TYPES, navigationImplementation, } from './bindings/navigation.js';
|
|
3
|
+
import { TERRAIN_CAPABILITIES, TERRAIN_MODULE, TERRAIN_TYPES, terrainImplementation, } from './bindings/terrain.js';
|
|
4
|
+
import { SPRITES_CAPABILITIES, SPRITES_MODULE, SPRITES_TYPES, spritesImplementation, } from './bindings/sprites.js';
|
|
5
|
+
import { INTERFACE_CAPABILITIES, INTERFACE_MODULE, INTERFACE_TYPES, interfaceImplementation, } from './bindings/interface.js';
|
|
6
|
+
import { EDITOR_CAPABILITIES, EDITOR_MODULE, EDITOR_TYPES, editorImplementation, } from './bindings/editor.js';
|
|
7
|
+
import { NETWORK_CAPABILITIES, NETWORK_MODULE, NETWORK_TYPES, networkImplementation, } from './bindings/network.js';
|
|
8
|
+
import { ROLLBACK_CAPABILITIES, ROLLBACK_MODULE, ROLLBACK_TYPES, rollbackImplementation, } from './bindings/rollback.js';
|
|
9
|
+
import { bindHost, createRegistry, defineTarget, } from 'driftscript';
|
|
10
|
+
import { AI_CAPABILITIES, AI_MODULE, AI_TYPES, aiImplementation, } from './bindings/ai.js';
|
|
11
|
+
import { AUDIO_CAPABILITIES, AUDIO_MODULE, AUDIO_TYPES, audioImplementation, } from './bindings/audio.js';
|
|
12
|
+
import { XR_CAPABILITIES, XR_MODULE, XR_TYPES, xrImplementation, } from './bindings/xr.js';
|
|
13
|
+
import { RENDER_CAPABILITIES, RENDER_MODULE, RENDER_TYPES, renderImplementation, } from './bindings/render.js';
|
|
14
|
+
import { ANIMATION_CAPABILITIES, ANIMATION_MODULE, ANIMATION_TYPES, animationImplementation, } from './bindings/animation.js';
|
|
15
|
+
import { CAMERA_CAPABILITIES, CAMERA_MODULE, CORE_TYPES, EVENTS_CAPABILITIES, EVENTS_MODULE, INPUT_CAPABILITIES, INPUT_MODULE, PERSISTENCE_CAPABILITIES, PERSISTENCE_MODULE, PHYSICS_CAPABILITIES, PHYSICS_MODULE, RANDOM_CAPABILITIES, RANDOM_MODULE, SCENE_CAPABILITIES, SCENE_MODULE, TIME_CAPABILITIES, TIME_MODULE, cameraImplementation, eventsImplementation, inputImplementation, persistenceImplementation, physicsImplementation, randomImplementation, sceneImplementation, timeImplementation, } from './bindings/core.js';
|
|
16
|
+
import { ECS_CAPABILITIES, QUERY_CAPABILITIES, ECS_MODULE, ENTITY_TYPES, PREFAB_CAPABILITIES, PREFAB_MODULE, entitiesImplementation, prefabImplementation, } from './bindings/entities.js';
|
|
17
|
+
import { CHEMISTRY_CAPABILITIES, CHEMISTRY_MODULE, CHEMISTRY_TYPES, chemistryImplementation, } from './bindings/chemistry.js';
|
|
18
|
+
import { registerStd, stdImplementations } from 'driftscript/std';
|
|
19
|
+
/**
|
|
20
|
+
* What the engine can provide. Everything wired; nothing that is not.
|
|
21
|
+
*
|
|
22
|
+
* `drift/core` is deliberately absent: §11 gives its provider as `startLoop` and `LoopHooks`, which
|
|
23
|
+
* are what *drives* a script rather than what a script calls. A module with nothing to call is not
|
|
24
|
+
* registered rather than registered empty.
|
|
25
|
+
*/
|
|
26
|
+
export const ENGINE_MODULES = [
|
|
27
|
+
AI_MODULE,
|
|
28
|
+
XR_MODULE,
|
|
29
|
+
ANIMATION_MODULE,
|
|
30
|
+
BEHAVIOR_MODULE,
|
|
31
|
+
AUDIO_MODULE,
|
|
32
|
+
CAMERA_MODULE,
|
|
33
|
+
CHEMISTRY_MODULE,
|
|
34
|
+
ECS_MODULE,
|
|
35
|
+
EDITOR_MODULE,
|
|
36
|
+
EVENTS_MODULE,
|
|
37
|
+
NETWORK_MODULE,
|
|
38
|
+
ROLLBACK_MODULE,
|
|
39
|
+
INPUT_MODULE,
|
|
40
|
+
NAVIGATION_MODULE,
|
|
41
|
+
PERSISTENCE_MODULE,
|
|
42
|
+
PHYSICS_MODULE,
|
|
43
|
+
RANDOM_MODULE,
|
|
44
|
+
RENDER_MODULE,
|
|
45
|
+
SCENE_MODULE,
|
|
46
|
+
SPRITES_MODULE,
|
|
47
|
+
TERRAIN_MODULE,
|
|
48
|
+
TIME_MODULE,
|
|
49
|
+
INTERFACE_MODULE,
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* A registry describing this engine's capabilities.
|
|
53
|
+
*
|
|
54
|
+
* Built rather than exported as a constant, because a registry refuses a duplicate registration —
|
|
55
|
+
* two consumers sharing one module-level instance would be one consumer's startup failing on
|
|
56
|
+
* another's import. Cheap: a few dozen object literals.
|
|
57
|
+
*/
|
|
58
|
+
export function engineRegistry() {
|
|
59
|
+
const registry = createRegistry();
|
|
60
|
+
/* The standard library first, because it belongs to the language rather than to this host and a
|
|
61
|
+
host that forgot it would produce scripts that fail on `math.clamp`. */
|
|
62
|
+
registerStd(registry);
|
|
63
|
+
for (const type of [
|
|
64
|
+
...AI_TYPES,
|
|
65
|
+
...AUDIO_TYPES,
|
|
66
|
+
...ANIMATION_TYPES,
|
|
67
|
+
...CHEMISTRY_TYPES,
|
|
68
|
+
...CORE_TYPES,
|
|
69
|
+
...ENTITY_TYPES,
|
|
70
|
+
...BEHAVIOR_TYPES,
|
|
71
|
+
...NAVIGATION_TYPES,
|
|
72
|
+
...TERRAIN_TYPES,
|
|
73
|
+
...SPRITES_TYPES,
|
|
74
|
+
...INTERFACE_TYPES,
|
|
75
|
+
...EDITOR_TYPES,
|
|
76
|
+
...NETWORK_TYPES,
|
|
77
|
+
...ROLLBACK_TYPES,
|
|
78
|
+
...RENDER_TYPES,
|
|
79
|
+
...XR_TYPES,
|
|
80
|
+
]) {
|
|
81
|
+
registry.addType(type);
|
|
82
|
+
}
|
|
83
|
+
for (const capability of [
|
|
84
|
+
...AI_CAPABILITIES,
|
|
85
|
+
...ANIMATION_CAPABILITIES,
|
|
86
|
+
...AUDIO_CAPABILITIES,
|
|
87
|
+
...BEHAVIOR_CAPABILITIES,
|
|
88
|
+
...CAMERA_CAPABILITIES,
|
|
89
|
+
...CHEMISTRY_CAPABILITIES,
|
|
90
|
+
...ECS_CAPABILITIES,
|
|
91
|
+
...EDITOR_CAPABILITIES,
|
|
92
|
+
...NETWORK_CAPABILITIES,
|
|
93
|
+
...ROLLBACK_CAPABILITIES,
|
|
94
|
+
...QUERY_CAPABILITIES,
|
|
95
|
+
...EVENTS_CAPABILITIES,
|
|
96
|
+
...INPUT_CAPABILITIES,
|
|
97
|
+
...NAVIGATION_CAPABILITIES,
|
|
98
|
+
...PERSISTENCE_CAPABILITIES,
|
|
99
|
+
...PHYSICS_CAPABILITIES,
|
|
100
|
+
...PREFAB_CAPABILITIES,
|
|
101
|
+
...RANDOM_CAPABILITIES,
|
|
102
|
+
...RENDER_CAPABILITIES,
|
|
103
|
+
...XR_CAPABILITIES,
|
|
104
|
+
...SCENE_CAPABILITIES,
|
|
105
|
+
...SPRITES_CAPABILITIES,
|
|
106
|
+
...INTERFACE_CAPABILITIES,
|
|
107
|
+
...TERRAIN_CAPABILITIES,
|
|
108
|
+
...TIME_CAPABILITIES,
|
|
109
|
+
]) {
|
|
110
|
+
registry.add(capability);
|
|
111
|
+
}
|
|
112
|
+
return registry;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* A target providing everything this engine has wired.
|
|
116
|
+
*
|
|
117
|
+
* A consumer that wants less passes its own list. **A consumer that wants more cannot** — the
|
|
118
|
+
* manifest may only name modules something provides, so a target claiming `drift/ecs` is refused
|
|
119
|
+
* at the manifest rather than at every call site.
|
|
120
|
+
*/
|
|
121
|
+
export function engineTarget(name = 'driftengine') {
|
|
122
|
+
return defineTarget(name, ENGINE_MODULES);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The implementation map a generated module's `__bind` receives.
|
|
126
|
+
*
|
|
127
|
+
* Keyed by logical module name, because that is what generated code looks up: `audio` in a `.drs`
|
|
128
|
+
* file is `$host['drift/audio']` in the output.
|
|
129
|
+
*
|
|
130
|
+
* **A service the consumer did not supply is absent rather than stubbed.** A module that binds it
|
|
131
|
+
* and then calls it fails at the call with the namespace's own name in the error, which is what the
|
|
132
|
+
* rule against silent no-ops asks for — a stub that quietly did nothing would make a script that
|
|
133
|
+
* plays no sound indistinguishable from one whose audio is not connected.
|
|
134
|
+
*/
|
|
135
|
+
export function engineImplementations(services) {
|
|
136
|
+
/* The standard library is merged in rather than bound separately, because a module importing both
|
|
137
|
+
`std/math` and `drift/audio` receives one host object. Leaving it to a consumer would produce a
|
|
138
|
+
script that fails on `math.clamp` with the namespace undefined. */
|
|
139
|
+
const map = { ...stdImplementations() };
|
|
140
|
+
if (services.audio !== undefined) {
|
|
141
|
+
map[AUDIO_MODULE] = audioImplementation(services.audio.graph, services.audio.registry);
|
|
142
|
+
}
|
|
143
|
+
if (services.clocks !== undefined)
|
|
144
|
+
map[TIME_MODULE] = timeImplementation(services.clocks);
|
|
145
|
+
if (services.entities !== undefined) {
|
|
146
|
+
map[ECS_MODULE] = entitiesImplementation(services.entities);
|
|
147
|
+
map[PREFAB_MODULE] = prefabImplementation(services.entities);
|
|
148
|
+
}
|
|
149
|
+
/*
|
|
150
|
+
* These four take their handle as an argument rather than from the host, so they need nothing
|
|
151
|
+
* supplied and are always available. A script passes the `Node` or the `Actions` it was given.
|
|
152
|
+
*/
|
|
153
|
+
map[RANDOM_MODULE] = randomImplementation();
|
|
154
|
+
map[SCENE_MODULE] = sceneImplementation();
|
|
155
|
+
map[RENDER_MODULE] = renderImplementation();
|
|
156
|
+
map[PHYSICS_MODULE] = physicsImplementation();
|
|
157
|
+
map[CAMERA_MODULE] = cameraImplementation();
|
|
158
|
+
map[ANIMATION_MODULE] = animationImplementation();
|
|
159
|
+
map[EVENTS_MODULE] = eventsImplementation();
|
|
160
|
+
map[PERSISTENCE_MODULE] = persistenceImplementation();
|
|
161
|
+
/* **Unconditional, unlike the four below it.** Every terrain capability takes the field it is
|
|
162
|
+
asking about, so the module needs nothing from the host: a `Terrain` reaches a script through
|
|
163
|
+
`uses`, the way a `NavGraph` does. There is nothing for an absent service to make fail. */
|
|
164
|
+
map[TERRAIN_MODULE] = terrainImplementation();
|
|
165
|
+
/* Unconditional for the same reason: every capability takes the batch, sheet, map or tree it
|
|
166
|
+
acts on, so neither module needs anything from the host to answer. */
|
|
167
|
+
map[SPRITES_MODULE] = spritesImplementation();
|
|
168
|
+
map[INTERFACE_MODULE] = interfaceImplementation();
|
|
169
|
+
map[INPUT_MODULE] = inputImplementation();
|
|
170
|
+
/* And once more for `drift/editor`: every capability takes the gizmo it is asking about, which
|
|
171
|
+
reaches a script through `uses`. There is nothing for an absent service to make fail. */
|
|
172
|
+
map[EDITOR_MODULE] = editorImplementation();
|
|
173
|
+
/* And the two Track J surfaces: each capability takes the session or the rewind it asks about,
|
|
174
|
+
so neither needs anything from the host beyond the handle a script is given through `uses`. */
|
|
175
|
+
map[NETWORK_MODULE] = networkImplementation();
|
|
176
|
+
map[ROLLBACK_MODULE] = rollbackImplementation();
|
|
177
|
+
/* `drift/ai` needs a registry for the same reason `drift/audio` needs one: an opaque
|
|
178
|
+
handle has to enter a script through a capability, and which agents exist in a
|
|
179
|
+
scene is not something an engine can know. Absent means a script's `ai.agent` calls
|
|
180
|
+
resolve to nothing — the same bargain every other optional service makes. */
|
|
181
|
+
if (services.ai !== undefined)
|
|
182
|
+
map[AI_MODULE] = aiImplementation(services.ai);
|
|
183
|
+
if (services.xr !== undefined)
|
|
184
|
+
map[XR_MODULE] = xrImplementation(services.xr);
|
|
185
|
+
/* And `drift/chemistry` needs a world for the same reason: a `Chemistry` handle has to enter a
|
|
186
|
+
script through a capability, and which parcels exist in a scene is not something an engine can
|
|
187
|
+
know. */
|
|
188
|
+
if (services.chemistry !== undefined) {
|
|
189
|
+
map[CHEMISTRY_MODULE] = chemistryImplementation(services.chemistry);
|
|
190
|
+
}
|
|
191
|
+
/* And `drift/navigation` needs the network, for the reason above stated once more: which places
|
|
192
|
+
a world has and which of them connect is the world's, not the engine's. */
|
|
193
|
+
if (services.navigation !== undefined) {
|
|
194
|
+
map[NAVIGATION_MODULE] = navigationImplementation(services.navigation);
|
|
195
|
+
}
|
|
196
|
+
/* And `drift/behavior` needs to know what a tick is handed, which is the consumer's world. */
|
|
197
|
+
if (services.behavior !== undefined) {
|
|
198
|
+
map[BEHAVIOR_MODULE] = behaviorImplementation(services.behavior);
|
|
199
|
+
}
|
|
200
|
+
return map;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Give a loaded module its host, and refuse in words when the host cannot satisfy it.
|
|
204
|
+
*
|
|
205
|
+
* Checked against `__drift.requires` before anything is called, so a missing service is reported at
|
|
206
|
+
* bind time naming the module — rather than at the first capability call, deep in a frame, as an
|
|
207
|
+
* undefined property.
|
|
208
|
+
*/
|
|
209
|
+
export function bindModule(module, services) {
|
|
210
|
+
const implementations = engineImplementations(services);
|
|
211
|
+
const missing = module.info.requires.filter((required) => required.startsWith('drift/') && implementations[required] === undefined);
|
|
212
|
+
if (missing.length > 0) {
|
|
213
|
+
return {
|
|
214
|
+
bound: false,
|
|
215
|
+
reason: `\`${module.info.module}\` requires ${missing.map((m) => `\`${m}\``).join(', ')}, which ` +
|
|
216
|
+
'this host has not been given. Pass the service to `bindModule` before loading the module.',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/*
|
|
220
|
+
* Through `bindHost` rather than by calling `__bind` directly, because the runtime has to
|
|
221
|
+
* remember which host this module was bound to.
|
|
222
|
+
*
|
|
223
|
+
* `patchModule` re-binds the new version to it. Calling `__bind` here would bind this version and
|
|
224
|
+
* leave the next one with every namespace at `undefined` — which is what happened for as long as
|
|
225
|
+
* `__bind` has existed, invisible because the only module anything ever patched imported no
|
|
226
|
+
* capability at all.
|
|
227
|
+
*
|
|
228
|
+
* A module with no capability imports has no `__bind`; `bindHost` treats that as a no-op, because
|
|
229
|
+
* a consumer should not have to know which of its scripts happen to use one.
|
|
230
|
+
*/
|
|
231
|
+
bindHost(module, implementations);
|
|
232
|
+
return { bound: true };
|
|
233
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
|
|
2
|
+
/**
|
|
3
|
+
* `@driftengine/script` — the `drift/*` bindings, and the only place the engine and the language
|
|
4
|
+
* are coupled.
|
|
5
|
+
*
|
|
6
|
+
* `driftscript` is host-neutral and imports no `@driftengine/*` at all, which three separate
|
|
7
|
+
* mechanisms assert. Everything this engine contributes to the language arrives through this
|
|
8
|
+
* package: registry entries describing capabilities that already exist, and the implementations a
|
|
9
|
+
* host hands to a compiled module.
|
|
10
|
+
*
|
|
11
|
+
* **There is no new engine code here.** A binding is a description plus a lookup. If one needed the
|
|
12
|
+
* engine to grow a function, that function belongs in the package that owns the subsystem, and the
|
|
13
|
+
* binding then describes it like any other.
|
|
14
|
+
*/
|
|
15
|
+
export { AUDIO_CAPABILITIES, AUDIO_MODULE, AUDIO_TYPES, audioImplementation, } from './bindings/audio.ts';
|
|
16
|
+
export type { ChemistryServices } from './bindings/chemistry.ts';
|
|
17
|
+
export { CHEMISTRY_CAPABILITIES, CHEMISTRY_COMPONENT, CHEMISTRY_MODULE, CHEMISTRY_TYPES, chemistryImplementation, } from './bindings/chemistry.ts';
|
|
18
|
+
export type { ComponentRegistry, RegisteredModule } from './entityHost.ts';
|
|
19
|
+
export { assertHostShapes, registerEntityModule } from './entityHost.ts';
|
|
20
|
+
export type { BindResult, HostServices } from './host.ts';
|
|
21
|
+
export { ENGINE_MODULES, bindModule, engineImplementations, engineRegistry, engineTarget, } from './host.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*! DriftEngine | Copyright 2026 Drift Technologies | Apache-2.0 | https://github.com/drftrun/driftengine */
|
|
2
|
+
/**
|
|
3
|
+
* `@driftengine/script` — the `drift/*` bindings, and the only place the engine and the language
|
|
4
|
+
* are coupled.
|
|
5
|
+
*
|
|
6
|
+
* `driftscript` is host-neutral and imports no `@driftengine/*` at all, which three separate
|
|
7
|
+
* mechanisms assert. Everything this engine contributes to the language arrives through this
|
|
8
|
+
* package: registry entries describing capabilities that already exist, and the implementations a
|
|
9
|
+
* host hands to a compiled module.
|
|
10
|
+
*
|
|
11
|
+
* **There is no new engine code here.** A binding is a description plus a lookup. If one needed the
|
|
12
|
+
* engine to grow a function, that function belongs in the package that owns the subsystem, and the
|
|
13
|
+
* binding then describes it like any other.
|
|
14
|
+
*/
|
|
15
|
+
export { AUDIO_CAPABILITIES, AUDIO_MODULE, AUDIO_TYPES, audioImplementation, } from './bindings/audio.js';
|
|
16
|
+
export { CHEMISTRY_CAPABILITIES, CHEMISTRY_COMPONENT, CHEMISTRY_MODULE, CHEMISTRY_TYPES, chemistryImplementation, } from './bindings/chemistry.js';
|
|
17
|
+
export { assertHostShapes, registerEntityModule } from './entityHost.js';
|
|
18
|
+
export { ENGINE_MODULES, bindModule, engineImplementations, engineRegistry, engineTarget, } from './host.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@driftengine/script",
|
|
3
|
+
"version": "3.61.0",
|
|
4
|
+
"description": "The drift/* capability bindings: what this engine provides to DriftScript, and the only place the two are coupled",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"drift-source": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./capabilities.json": "./capabilities.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"!src/**/*.test.ts",
|
|
21
|
+
"!src/**/*.test.mjs",
|
|
22
|
+
"!src/**/__snapshots__",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"NOTICE"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"driftscript": "1.13.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@driftengine/ai": "3.61.0",
|
|
33
|
+
"@driftengine/animation": "3.61.0",
|
|
34
|
+
"@driftengine/audio": "3.61.0",
|
|
35
|
+
"@driftengine/chemistry": "3.61.0",
|
|
36
|
+
"@driftengine/core": "3.61.0",
|
|
37
|
+
"@driftengine/editor": "3.61.0",
|
|
38
|
+
"@driftengine/entities": "3.61.0",
|
|
39
|
+
"@driftengine/network": "3.61.0",
|
|
40
|
+
"@driftengine/terrain": "3.61.0",
|
|
41
|
+
"@driftengine/ui2d": "3.61.0",
|
|
42
|
+
"@driftengine/xr": "3.61.0"
|
|
43
|
+
},
|
|
44
|
+
"author": "Drift Technologies",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/drftrun/driftengine.git",
|
|
48
|
+
"directory": "packages/script"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/drftrun/driftengine#readme",
|
|
51
|
+
"bugs": "https://github.com/drftrun/driftengine/issues",
|
|
52
|
+
"keywords": [
|
|
53
|
+
"driftengine",
|
|
54
|
+
"3d",
|
|
55
|
+
"webgl",
|
|
56
|
+
"webgpu",
|
|
57
|
+
"typescript",
|
|
58
|
+
"scripting",
|
|
59
|
+
"driftscript",
|
|
60
|
+
"bindings"
|
|
61
|
+
],
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=22.12.0"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `drift/ai` — what this engine lets a script ask of an agent.
|
|
3
|
+
*
|
|
4
|
+
* **Deliberately small, and deliberately not a way to call a model.** A script wakes an
|
|
5
|
+
* agent, asks it to consider, and reads what it is doing. It never awaits a provider,
|
|
6
|
+
* because a script that could would be a script that can stall a fixed step for five
|
|
7
|
+
* seconds — and the whole of Track O is the argument that nothing should.
|
|
8
|
+
*
|
|
9
|
+
* Three constraints this surface inherits:
|
|
10
|
+
*
|
|
11
|
+
* - **`ai` is outside the determinism boundary.** A `@deterministic` function may not
|
|
12
|
+
* call any of it. The floor is deterministic and runs inside the simulation; asking a
|
|
13
|
+
* provider is not, and the compiler is making a line that already exists visible.
|
|
14
|
+
* - **Nothing here returns a model's answer.** `wake` and `consider` are requests that
|
|
15
|
+
* an agent think, and they return immediately. What the agent decided arrives as the
|
|
16
|
+
* intent its session hands the consumer on a later tick.
|
|
17
|
+
* - **Reading is cheap and stays inside the boundary.** `intentId` and `degraded` are
|
|
18
|
+
* `@deterministic`-safe: the current intent is state the simulation already owns,
|
|
19
|
+
* because a deterministic floor put it there and a replay recomputes it.
|
|
20
|
+
*/
|
|
21
|
+
import { type CapabilityDefinition, type OpaqueType, defineCapability } from 'driftscript';
|
|
22
|
+
import type { AgentSession, NavigateArgs, NavigateResult, ToolDefinition } from '@driftengine/ai';
|
|
23
|
+
import type { NavPath } from '@driftengine/core';
|
|
24
|
+
|
|
25
|
+
export const AI_MODULE = 'drift/ai';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A live agent session, held by a script and passed back to the engine.
|
|
29
|
+
*
|
|
30
|
+
* Opaque, for the reason `Sound` is: a script that could read its fields would be a
|
|
31
|
+
* script depending on a representation this package cannot then change.
|
|
32
|
+
*/
|
|
33
|
+
export const AI_TYPES: readonly OpaqueType[] = [
|
|
34
|
+
{
|
|
35
|
+
module: AI_MODULE,
|
|
36
|
+
name: 'Agent',
|
|
37
|
+
doc: 'A live agent session. Held and passed back; never read into.',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const define = (
|
|
42
|
+
name: string,
|
|
43
|
+
params: readonly { name: string; type: string }[],
|
|
44
|
+
returns: string,
|
|
45
|
+
effects: CapabilityDefinition['effects'],
|
|
46
|
+
deterministic: boolean,
|
|
47
|
+
doc: string,
|
|
48
|
+
implementation: string,
|
|
49
|
+
): CapabilityDefinition =>
|
|
50
|
+
defineCapability({
|
|
51
|
+
module: AI_MODULE,
|
|
52
|
+
name,
|
|
53
|
+
signature: `fn(${params.map((p) => `${p.name}: ${p.type}`).join(', ')}) -> ${returns}`,
|
|
54
|
+
params: [...params],
|
|
55
|
+
returns,
|
|
56
|
+
effects,
|
|
57
|
+
deterministic,
|
|
58
|
+
doc,
|
|
59
|
+
implementation,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const AI_CAPABILITIES: readonly CapabilityDefinition[] = [
|
|
63
|
+
define(
|
|
64
|
+
'agent',
|
|
65
|
+
[{ name: 'id', type: 'String' }],
|
|
66
|
+
/*
|
|
67
|
+
* `Agent?`, and the `?` is the point, exactly as it is for `audio.sound`.
|
|
68
|
+
*
|
|
69
|
+
* A consumer registers the agents a script may talk to, and a script naming one
|
|
70
|
+
* that was not registered is ordinary rather than exceptional — a scene loaded
|
|
71
|
+
* without its agents, a name that moved. Returning a bare `Agent` would make every
|
|
72
|
+
* script that forgot to check talk to nothing and report success.
|
|
73
|
+
*/
|
|
74
|
+
'Agent?',
|
|
75
|
+
['pure'],
|
|
76
|
+
true,
|
|
77
|
+
'Resolve an agent by the id the consumer registered it under. Absent when there is none.',
|
|
78
|
+
'AgentRegistry.get',
|
|
79
|
+
),
|
|
80
|
+
define(
|
|
81
|
+
'wake',
|
|
82
|
+
[
|
|
83
|
+
{ name: 'agent', type: 'Agent' },
|
|
84
|
+
{ name: 'reason', type: 'String' },
|
|
85
|
+
{ name: 'priority', type: 'i32' },
|
|
86
|
+
],
|
|
87
|
+
'void',
|
|
88
|
+
['ai'],
|
|
89
|
+
false,
|
|
90
|
+
'Tell an agent something happened. Returns immediately; the agent acts at its next tick.',
|
|
91
|
+
'AgentSession.observe',
|
|
92
|
+
),
|
|
93
|
+
define(
|
|
94
|
+
'consider',
|
|
95
|
+
[{ name: 'agent', type: 'Agent' }],
|
|
96
|
+
'void',
|
|
97
|
+
['ai'],
|
|
98
|
+
false,
|
|
99
|
+
'Ask an agent to think, at ordinary priority. Returns immediately.',
|
|
100
|
+
'AgentSession.consider',
|
|
101
|
+
),
|
|
102
|
+
define(
|
|
103
|
+
'intentId',
|
|
104
|
+
[{ name: 'agent', type: 'Agent' }],
|
|
105
|
+
'String',
|
|
106
|
+
['pure'],
|
|
107
|
+
/*
|
|
108
|
+
* Readable from a `@deterministic` function, unlike everything else here.
|
|
109
|
+
*
|
|
110
|
+
* The current intent is state the simulation already owns — the floor put it there
|
|
111
|
+
* and a replay recomputes it — so reading it is reading the simulation rather than
|
|
112
|
+
* reaching outside it. Asking a provider is the part that is nondeterministic, and
|
|
113
|
+
* that is what `wake` and `consider` carry `ai` for.
|
|
114
|
+
*/
|
|
115
|
+
true,
|
|
116
|
+
'What the agent is doing now. Empty before its first tick.',
|
|
117
|
+
'AgentSession.intentId',
|
|
118
|
+
),
|
|
119
|
+
define(
|
|
120
|
+
'degraded',
|
|
121
|
+
[{ name: 'agent', type: 'Agent' }],
|
|
122
|
+
'bool',
|
|
123
|
+
['pure'],
|
|
124
|
+
true,
|
|
125
|
+
'Whether the agent is over budget and running on its policy floor alone.',
|
|
126
|
+
'AgentSession.degraded',
|
|
127
|
+
),
|
|
128
|
+
|
|
129
|
+
/*
|
|
130
|
+
* The two bridges, and every effect below is inherited rather than invented.
|
|
131
|
+
*
|
|
132
|
+
* **None of these duplicates `drift/navigation`.** That module binds twelve capabilities and
|
|
133
|
+
* every one takes a `NavPath` or a `NavGraph`; what a script could not do is aim any of them at
|
|
134
|
+
* an `Agent`, which is a session keyed by an id the consumer registered. `path` is the join, so
|
|
135
|
+
* one capability makes all twelve work on an agent instead of any being bound a second time.
|
|
136
|
+
*/
|
|
137
|
+
define(
|
|
138
|
+
'reachable',
|
|
139
|
+
[
|
|
140
|
+
{ name: 'agent', type: 'Agent' },
|
|
141
|
+
{ name: 'x', type: 'float' },
|
|
142
|
+
{ name: 'y', type: 'float' },
|
|
143
|
+
{ name: 'z', type: 'float' },
|
|
144
|
+
],
|
|
145
|
+
'bool',
|
|
146
|
+
['navigation.read'],
|
|
147
|
+
/*
|
|
148
|
+
* Deterministic, and it is the admission guard as a question. `navigation.route` computes *and
|
|
149
|
+
* writes*, so a script wanting to score a destination before committing to it had to route and
|
|
150
|
+
* then undo — which is a write a `@deterministic` system would have had to make and take back.
|
|
151
|
+
*/
|
|
152
|
+
true,
|
|
153
|
+
'Whether a route exists from where this agent is to a point, without writing one.',
|
|
154
|
+
'navigationBridge.admits',
|
|
155
|
+
),
|
|
156
|
+
define(
|
|
157
|
+
'navigate',
|
|
158
|
+
[
|
|
159
|
+
{ name: 'agent', type: 'Agent' },
|
|
160
|
+
{ name: 'x', type: 'float' },
|
|
161
|
+
{ name: 'y', type: 'float' },
|
|
162
|
+
{ name: 'z', type: 'float' },
|
|
163
|
+
],
|
|
164
|
+
'bool',
|
|
165
|
+
['navigation.write'],
|
|
166
|
+
/*
|
|
167
|
+
* **Deterministic, and safe on every peer, which only looks like it contradicts `deciding`.**
|
|
168
|
+
* The authority rule is about *model*-authored intents: a provider is nondeterministic, slow
|
|
169
|
+
* and metered, so two peers running one diverge. A script's own command is a function of the
|
|
170
|
+
* simulation — DriftScript 1.12.0 admitted `navigation.write` to `DETERMINISTIC_EFFECTS` on
|
|
171
|
+
* exactly that argument, since a route is a function of the graph and two endpoints and
|
|
172
|
+
* `navSearch` breaks ties on the node index — so every peer computes the same route and it
|
|
173
|
+
* replicates itself by being re-run.
|
|
174
|
+
*/
|
|
175
|
+
true,
|
|
176
|
+
'Route this agent to a point. False when no route exists, and then it clears the old one.',
|
|
177
|
+
'navigationBridge.execute',
|
|
178
|
+
),
|
|
179
|
+
define(
|
|
180
|
+
'path',
|
|
181
|
+
[{ name: 'agent', type: 'Agent' }],
|
|
182
|
+
/*
|
|
183
|
+
* `NavPath?`, from `drift/navigation`. Cross-module opaque types are ordinary here:
|
|
184
|
+
* `drift/editor.rebuildTree` takes a `drift/scene.Node` and `drift/ui.draw` takes a
|
|
185
|
+
* `drift/2d.SpriteBatch`.
|
|
186
|
+
*
|
|
187
|
+
* Optional because an agent the consumer never gave a route object to is ordinary rather than
|
|
188
|
+
* exceptional, and a bare `NavPath` would hand every script that forgot to check a handle to
|
|
189
|
+
* nothing.
|
|
190
|
+
*/
|
|
191
|
+
'NavPath?',
|
|
192
|
+
['navigation.read'],
|
|
193
|
+
true,
|
|
194
|
+
'The route object this agent follows, for `drift/navigation` to read or steer along.',
|
|
195
|
+
'AgentNavigation.pathOf',
|
|
196
|
+
),
|
|
197
|
+
define(
|
|
198
|
+
'deciding',
|
|
199
|
+
[{ name: 'agent', type: 'Agent' }],
|
|
200
|
+
'bool',
|
|
201
|
+
/*
|
|
202
|
+
* `network.read`, and inside `DETERMINISTIC_EFFECTS`, on the same narrowing
|
|
203
|
+
* `drift/network.authority` already uses: which participant this is, and whether it is the
|
|
204
|
+
* authority, are facts that cannot vary with packet timing. This is that fact per agent.
|
|
205
|
+
*/
|
|
206
|
+
['network.read'],
|
|
207
|
+
true,
|
|
208
|
+
'Whether this peer decides this agent’s model intents. Elsewhere they arrive already taken.',
|
|
209
|
+
'AuthoritativeAgent.deciding',
|
|
210
|
+
),
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A consumer's agents, by the id a script may name them with.
|
|
215
|
+
*
|
|
216
|
+
* The registry is the consumer's, exactly as `SoundRegistry` is: this package owns the
|
|
217
|
+
* loop and the floor, and which agents exist in a scene is not something an engine can
|
|
218
|
+
* know.
|
|
219
|
+
*/
|
|
220
|
+
export interface AgentRegistry {
|
|
221
|
+
get(id: string): AgentSession | undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* What the navigation bridge needs from the host to answer for an agent.
|
|
226
|
+
*
|
|
227
|
+
* The world and the tools are the consumer's: `navigationBridge` is a factory over their adapter,
|
|
228
|
+
* and this is where the result of that factory meets a script.
|
|
229
|
+
*/
|
|
230
|
+
export interface AgentNavigation<W = unknown> {
|
|
231
|
+
readonly world: W;
|
|
232
|
+
readonly tools: readonly ToolDefinition<NavigateArgs, NavigateResult, W>[];
|
|
233
|
+
pathOf(agentId: string): NavPath | undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Which agents this peer decides for. One method, because that is the whole script-visible fact. */
|
|
237
|
+
export interface AgentAuthority {
|
|
238
|
+
deciding(agentId: string): boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface AiServices<W = unknown> {
|
|
242
|
+
readonly agents: AgentRegistry;
|
|
243
|
+
/** Absent means `reachable`, `navigate` and `path` fail at the call saying what to provide. */
|
|
244
|
+
readonly navigation?: AgentNavigation<W>;
|
|
245
|
+
/** Absent means `deciding` fails at the call. */
|
|
246
|
+
readonly authority?: AgentAuthority;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Missing wiring fails at the call, naming what is absent.
|
|
251
|
+
*
|
|
252
|
+
* **Not a falsy default**, for the reason `HostServices` already gives about `drift/behavior`: a
|
|
253
|
+
* stub makes an agent whose routine never runs look exactly like one standing about with nothing to
|
|
254
|
+
* do. `navigate` returning `false` because nobody wired a bridge is indistinguishable from `false`
|
|
255
|
+
* because there is no route, and the second is a thing a script is meant to handle.
|
|
256
|
+
*/
|
|
257
|
+
function required<T>(value: T | undefined, what: string): T {
|
|
258
|
+
if (value === undefined) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`drift/ai: this host provides no ${what}, so that capability cannot answer. Pass it in ` +
|
|
261
|
+
`\`services.ai\` — see \`navigationBridge\` in @driftengine/ai.`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The implementations the host supplies at link time, keyed by capability name. */
|
|
268
|
+
export function aiImplementation(services: AiServices): Readonly<Record<string, unknown>> {
|
|
269
|
+
const { agents } = services;
|
|
270
|
+
const navigate = (
|
|
271
|
+
agent: AgentSession,
|
|
272
|
+
x: number,
|
|
273
|
+
y: number,
|
|
274
|
+
z: number,
|
|
275
|
+
act: boolean,
|
|
276
|
+
): boolean => {
|
|
277
|
+
const nav = required(services.navigation, 'navigation bridge');
|
|
278
|
+
const tool = nav.tools.find((candidate) => candidate.id === 'navigate@1');
|
|
279
|
+
if (tool === undefined) return false;
|
|
280
|
+
const args = { agentId: agent.agentId, x, y, z };
|
|
281
|
+
/* `admits` is the guard and `execute` is the act, and `reachable` is deliberately the first
|
|
282
|
+
without the second — the whole point of exposing it is not having to route and undo. */
|
|
283
|
+
return act ? tool.execute(args, nav.world).found : tool.admits(args, nav.world);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
agent: (id: string): AgentSession | undefined => agents.get(id),
|
|
288
|
+
wake: (agent: AgentSession, reason: string, priority: number): void => {
|
|
289
|
+
agent.observe({ id: reason, priority, text: reason });
|
|
290
|
+
},
|
|
291
|
+
consider: (agent: AgentSession): void => {
|
|
292
|
+
agent.observe({ id: 'consider', priority: 0, text: 'consider' });
|
|
293
|
+
},
|
|
294
|
+
intentId: (agent: AgentSession): string => agent.current?.id ?? '',
|
|
295
|
+
degraded: (agent: AgentSession): boolean => agent.degraded,
|
|
296
|
+
|
|
297
|
+
reachable: (agent: AgentSession, x: number, y: number, z: number): boolean =>
|
|
298
|
+
navigate(agent, x, y, z, false),
|
|
299
|
+
navigate: (agent: AgentSession, x: number, y: number, z: number): boolean =>
|
|
300
|
+
navigate(agent, x, y, z, true),
|
|
301
|
+
path: (agent: AgentSession): NavPath | undefined =>
|
|
302
|
+
required(services.navigation, 'navigation bridge').pathOf(agent.agentId),
|
|
303
|
+
deciding: (agent: AgentSession): boolean =>
|
|
304
|
+
required(services.authority, 'authority bridge').deciding(agent.agentId),
|
|
305
|
+
};
|
|
306
|
+
}
|