@aethervtt/shard-net 0.0.0-stage → 0.4.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 +21 -0
- package/README.md +102 -2
- package/dist/client.d.ts +76 -0
- package/dist/client.js +527 -0
- package/dist/components.d.ts +72 -0
- package/dist/components.js +71 -0
- package/dist/conditions.d.ts +29 -0
- package/dist/conditions.js +128 -0
- package/dist/host.d.ts +38 -0
- package/dist/host.js +55 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +110 -0
- package/dist/interest.d.ts +67 -0
- package/dist/interest.js +277 -0
- package/dist/loopback.d.ts +12 -0
- package/dist/loopback.js +103 -0
- package/dist/plugin.d.ts +101 -0
- package/dist/plugin.js +373 -0
- package/dist/registry.d.ts +53 -0
- package/dist/registry.js +44 -0
- package/dist/server.d.ts +239 -0
- package/dist/server.js +1002 -0
- package/dist/testing.d.ts +60 -0
- package/dist/testing.js +200 -0
- package/dist/transport.d.ts +79 -0
- package/dist/transport.js +22 -0
- package/dist/vec.d.ts +13 -0
- package/dist/vec.js +27 -0
- package/dist/wire.d.ts +60 -0
- package/dist/wire.js +141 -0
- package/package.json +37 -4
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Rng } from "@aethervtt/shard-core";
|
|
2
|
+
function isConditioned(transport) {
|
|
3
|
+
return typeof transport.setConditions === "function";
|
|
4
|
+
}
|
|
5
|
+
const defaultNow = () => globalThis.performance?.now() ?? Date.now();
|
|
6
|
+
class ConditionedConnection {
|
|
7
|
+
inner;
|
|
8
|
+
shared;
|
|
9
|
+
outbound = [];
|
|
10
|
+
inbound = [];
|
|
11
|
+
messageFns = [];
|
|
12
|
+
lastReliableOut = 0;
|
|
13
|
+
lastReliableIn = 0;
|
|
14
|
+
lost = 0;
|
|
15
|
+
constructor(inner, shared) {
|
|
16
|
+
this.inner = inner;
|
|
17
|
+
this.shared = shared;
|
|
18
|
+
inner.onMessage((channel, bytes) => {
|
|
19
|
+
this.schedule(this.inbound, channel, bytes.slice(), "in");
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
get peer() {
|
|
23
|
+
return this.inner.peer;
|
|
24
|
+
}
|
|
25
|
+
send(channel, bytes) {
|
|
26
|
+
this.schedule(this.outbound, channel, bytes.slice(), "out");
|
|
27
|
+
const now = this.shared.now();
|
|
28
|
+
while (this.outbound.length > 0 && this.outbound[0].at <= now) {
|
|
29
|
+
const m = this.outbound.shift();
|
|
30
|
+
this.inner.send(m.channel, m.bytes);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
schedule(queue, channel, bytes, dir) {
|
|
34
|
+
const c = this.shared.conditions;
|
|
35
|
+
const rng = this.shared.rng;
|
|
36
|
+
const now = this.shared.now();
|
|
37
|
+
if (channel === "unreliable" && c.loss > 0 && rng.float() < c.loss) {
|
|
38
|
+
this.lost++;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let at = now + (c.latency + rng.float() * c.jitter) * 1e3;
|
|
42
|
+
if (channel === "reliable" || !c.reorder) {
|
|
43
|
+
const last = dir === "in" ? this.lastReliableIn : this.lastReliableOut;
|
|
44
|
+
if (at < last) at = last;
|
|
45
|
+
if (dir === "in") this.lastReliableIn = at;
|
|
46
|
+
else this.lastReliableOut = at;
|
|
47
|
+
}
|
|
48
|
+
insert(queue, { at, channel, bytes });
|
|
49
|
+
if (channel === "unreliable" && c.duplicate > 0 && rng.float() < c.duplicate) {
|
|
50
|
+
insert(queue, { at: at + rng.float() * c.jitter * 1e3, channel, bytes });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
pump() {
|
|
54
|
+
this.inner.pump?.();
|
|
55
|
+
const now = this.shared.now();
|
|
56
|
+
let n = 0;
|
|
57
|
+
while (n < this.outbound.length && this.outbound[n].at <= now) n++;
|
|
58
|
+
for (const m of this.outbound.splice(0, n)) this.inner.send(m.channel, m.bytes);
|
|
59
|
+
n = 0;
|
|
60
|
+
while (n < this.inbound.length && this.inbound[n].at <= now) n++;
|
|
61
|
+
for (const m of this.inbound.splice(0, n)) {
|
|
62
|
+
for (const fn of this.messageFns) fn(m.channel, m.bytes);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
onMessage(fn) {
|
|
66
|
+
this.messageFns.push(fn);
|
|
67
|
+
return () => {
|
|
68
|
+
const i = this.messageFns.indexOf(fn);
|
|
69
|
+
if (i !== -1) this.messageFns.splice(i, 1);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
onClose(fn) {
|
|
73
|
+
return this.inner.onClose(fn);
|
|
74
|
+
}
|
|
75
|
+
close(reason) {
|
|
76
|
+
this.inner.close(reason);
|
|
77
|
+
}
|
|
78
|
+
stats() {
|
|
79
|
+
const s = this.inner.stats();
|
|
80
|
+
return { ...s, lost: s.lost + this.lost };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function insert(queue, m) {
|
|
84
|
+
let i = queue.length;
|
|
85
|
+
while (i > 0 && queue[i - 1].at > m.at) i--;
|
|
86
|
+
queue.splice(i, 0, m);
|
|
87
|
+
}
|
|
88
|
+
function simulateConditions(transport, options = {}) {
|
|
89
|
+
const shared = {
|
|
90
|
+
conditions: {
|
|
91
|
+
latency: options.latency ?? 0,
|
|
92
|
+
jitter: options.jitter ?? 0,
|
|
93
|
+
loss: options.loss ?? 0,
|
|
94
|
+
duplicate: options.duplicate ?? 0,
|
|
95
|
+
reorder: options.reorder ?? true
|
|
96
|
+
},
|
|
97
|
+
rng: new Rng(options.seed ?? 1),
|
|
98
|
+
now: options.now ?? defaultNow
|
|
99
|
+
};
|
|
100
|
+
const wrapped = {
|
|
101
|
+
name: `${transport.name}+conditions`,
|
|
102
|
+
caps: transport.caps,
|
|
103
|
+
get conditions() {
|
|
104
|
+
return shared.conditions;
|
|
105
|
+
},
|
|
106
|
+
setConditions(conditions) {
|
|
107
|
+
shared.conditions = { ...shared.conditions, ...conditions };
|
|
108
|
+
},
|
|
109
|
+
async connect(address, connectOptions) {
|
|
110
|
+
return new ConditionedConnection(await transport.connect(address, connectOptions), shared);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (transport.listen) {
|
|
114
|
+
const listen = transport.listen.bind(transport);
|
|
115
|
+
wrapped.listen = async (listenOptions) => {
|
|
116
|
+
const inner = await listen(listenOptions);
|
|
117
|
+
return {
|
|
118
|
+
onConnection: (fn) => inner.onConnection((connection) => fn(new ConditionedConnection(connection, shared))),
|
|
119
|
+
close: () => inner.close()
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return wrapped;
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
isConditioned,
|
|
127
|
+
simulateConditions
|
|
128
|
+
};
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { App, type Plugin, type Runner } from '@aethervtt/shard-runtime';
|
|
2
|
+
import { type NetOptions } from './plugin.js';
|
|
3
|
+
import type { NetSessionProvider, NetTransport } from './transport.js';
|
|
4
|
+
export interface HostOptions<U = unknown, L = unknown, S = unknown> {
|
|
5
|
+
/** The server App's plugins (the game's rules), made fresh for it. */
|
|
6
|
+
plugins?: () => Plugin[];
|
|
7
|
+
/** Server net options: replicate, commands, events, accept, players, interest, meta… */
|
|
8
|
+
net: Omit<NetOptions<U, L, S>, 'role' | 'transport' | 'address' | 'session' | 'code'>;
|
|
9
|
+
/** How remote players reach the host: a lobby such as `webRtcLobby` (0091). */
|
|
10
|
+
session: NetSessionProvider;
|
|
11
|
+
fixedHz?: number;
|
|
12
|
+
seed?: number;
|
|
13
|
+
/** Default `intervalRunner()`: the host keeps simulating without a display to pace it. */
|
|
14
|
+
runner?: Runner;
|
|
15
|
+
/** Makes the server App (default `new App`): a project's headless build, say (0092). */
|
|
16
|
+
createApp?: () => App;
|
|
17
|
+
/** Runs after the server App's init, before it runs (an asset scan). */
|
|
18
|
+
afterInit?: (app: App) => Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export interface Host {
|
|
21
|
+
/** The server App, running. */
|
|
22
|
+
readonly app: App;
|
|
23
|
+
/** The room code remote players join with. */
|
|
24
|
+
readonly code: string;
|
|
25
|
+
/** The host player's own connection: spread into the client's `netPlugin`. */
|
|
26
|
+
readonly local: {
|
|
27
|
+
readonly transport: NetTransport;
|
|
28
|
+
readonly address: string;
|
|
29
|
+
};
|
|
30
|
+
/** Saves players (0090), closes the room and every connection, and stops the server. */
|
|
31
|
+
stop(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Hosts a game from this process (0091): runs the server App next to the player's client App,
|
|
35
|
+
* takes remote players through `session`, and gives the host player a loopback connection. The
|
|
36
|
+
* host player is an ordinary client of its own server.
|
|
37
|
+
*/
|
|
38
|
+
export declare function startHost<U = unknown, L = unknown, S = unknown>(options: HostOptions<U, L, S>): Promise<Host>;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { App, intervalRunner } from "@aethervtt/shard-runtime";
|
|
3
|
+
import { loopback } from "./loopback.js";
|
|
4
|
+
import { netPlugin, netSession } from "./plugin.js";
|
|
5
|
+
function mergeListeners(listeners) {
|
|
6
|
+
return {
|
|
7
|
+
onConnection(fn) {
|
|
8
|
+
const offs = listeners.map((l) => l.onConnection(fn));
|
|
9
|
+
return () => {
|
|
10
|
+
for (const off of offs) off();
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
async close() {
|
|
14
|
+
await Promise.all(listeners.map((l) => l.close()));
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function startHost(options) {
|
|
19
|
+
const local = loopback({ maxMessage: options.session.caps?.maxMessage ?? 1 << 20 });
|
|
20
|
+
const address = "host";
|
|
21
|
+
const remote = options.session;
|
|
22
|
+
const session = {
|
|
23
|
+
name: `host+${remote.name}`,
|
|
24
|
+
...remote.caps ? { caps: remote.caps } : {},
|
|
25
|
+
async host(listen) {
|
|
26
|
+
const [hosted, mine] = await Promise.all([remote.host(listen), local.listen({ address })]);
|
|
27
|
+
return { code: hosted.code, listener: mergeListeners([hosted.listener, mine]) };
|
|
28
|
+
},
|
|
29
|
+
join: (code, join) => remote.join(code, join)
|
|
30
|
+
};
|
|
31
|
+
const app = options.createApp?.() ?? new App({ fixedHz: options.fixedHz ?? 60, seed: options.seed ?? 0 });
|
|
32
|
+
for (const p of options.plugins?.() ?? []) app.addPlugin(p);
|
|
33
|
+
app.addPlugin(netPlugin({ ...options.net, role: "server", session }));
|
|
34
|
+
app.setRunner(options.runner ?? intervalRunner());
|
|
35
|
+
await app.init();
|
|
36
|
+
await options.afterInit?.(app);
|
|
37
|
+
const net = netSession(app.world);
|
|
38
|
+
await net.ready;
|
|
39
|
+
if (net.code === void 0) {
|
|
40
|
+
throw new ShardError("net/cannot-listen", `The ${remote.name} session gave no room code`);
|
|
41
|
+
}
|
|
42
|
+
const running = app.run();
|
|
43
|
+
return {
|
|
44
|
+
app,
|
|
45
|
+
code: net.code,
|
|
46
|
+
local: { transport: local, address },
|
|
47
|
+
async stop() {
|
|
48
|
+
await app.dispose();
|
|
49
|
+
await running;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
startHost
|
|
55
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { NetClient, PendingCommand } from './client.js';
|
|
2
|
+
export { Connected, Disconnected, Focus, NetClock, type NetClockData, NetId, NetRole, type NetRoleData, type NetRoleName, type PeerInfo, PeerJoined, PeerLeft, Peers, PlayerSaveFailed, Relevance, Replicated, } from './components.js';
|
|
3
|
+
export { type ConditionedTransport, type ConditionsOptions, isConditioned, type NetConditions, simulateConditions, } from './conditions.js';
|
|
4
|
+
export { type Host, type HostOptions, startHost } from './host.js';
|
|
5
|
+
export { allOf, alwaysInterest, anyOf, type GridInterestOptions, gridInterest, type InterestPeer, type InterestPolicy, ownerInterest, RelevanceSet, teamInterest, } from './interest.js';
|
|
6
|
+
export { type LoopbackOptions, loopback } from './loopback.js';
|
|
7
|
+
export { commands, connected, hasAuthority, type NetOptions, NetSession, type NetSessionData, NetSystems, netPlugin, netSession, onClient, onServer, type SendEventOptions, savePlayer, sendCommand, sendEvent, } from './plugin.js';
|
|
8
|
+
export { defineNetCommand, defineNetEvent, type NetCommandDef, type NetEventData, type NetEventDef, ownerOf, ownerOnly, type ReplicateOptions, type ReplicationEntry, type ReplicationMode, replicate, } from './registry.js';
|
|
9
|
+
export { type AcceptResult, type JoinContext, type LeaveContext, NetServer, type PeerStats, type PlayerHooks, ServerCommand, } from './server.js';
|
|
10
|
+
export { type Channel, type ConnectionStats, type ConnectOptions, directSession, type ListenOptions, type NetConnection, type NetListener, type NetSessionProvider, type NetTransport, type PeerId, type SessionInfo, type TransportCaps, } from './transport.js';
|
|
11
|
+
export { buildManifest, Fragmenter, type Manifest, Msg, PROTOCOL_VERSION } from './wire.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { NetClient, PendingCommand } from "./client.js";
|
|
2
|
+
import {
|
|
3
|
+
Connected,
|
|
4
|
+
Disconnected,
|
|
5
|
+
Focus,
|
|
6
|
+
NetClock,
|
|
7
|
+
NetId,
|
|
8
|
+
NetRole,
|
|
9
|
+
PeerJoined,
|
|
10
|
+
PeerLeft,
|
|
11
|
+
Peers,
|
|
12
|
+
PlayerSaveFailed,
|
|
13
|
+
Relevance,
|
|
14
|
+
Replicated
|
|
15
|
+
} from "./components.js";
|
|
16
|
+
import {
|
|
17
|
+
isConditioned,
|
|
18
|
+
simulateConditions
|
|
19
|
+
} from "./conditions.js";
|
|
20
|
+
import { startHost } from "./host.js";
|
|
21
|
+
import {
|
|
22
|
+
allOf,
|
|
23
|
+
alwaysInterest,
|
|
24
|
+
anyOf,
|
|
25
|
+
gridInterest,
|
|
26
|
+
ownerInterest,
|
|
27
|
+
RelevanceSet,
|
|
28
|
+
teamInterest
|
|
29
|
+
} from "./interest.js";
|
|
30
|
+
import { loopback } from "./loopback.js";
|
|
31
|
+
import {
|
|
32
|
+
commands,
|
|
33
|
+
connected,
|
|
34
|
+
hasAuthority,
|
|
35
|
+
NetSession,
|
|
36
|
+
NetSystems,
|
|
37
|
+
netPlugin,
|
|
38
|
+
netSession,
|
|
39
|
+
onClient,
|
|
40
|
+
onServer,
|
|
41
|
+
savePlayer,
|
|
42
|
+
sendCommand,
|
|
43
|
+
sendEvent
|
|
44
|
+
} from "./plugin.js";
|
|
45
|
+
import {
|
|
46
|
+
defineNetCommand,
|
|
47
|
+
defineNetEvent,
|
|
48
|
+
ownerOf,
|
|
49
|
+
ownerOnly,
|
|
50
|
+
replicate
|
|
51
|
+
} from "./registry.js";
|
|
52
|
+
import {
|
|
53
|
+
NetServer,
|
|
54
|
+
ServerCommand
|
|
55
|
+
} from "./server.js";
|
|
56
|
+
import {
|
|
57
|
+
directSession
|
|
58
|
+
} from "./transport.js";
|
|
59
|
+
import { buildManifest, Fragmenter, Msg, PROTOCOL_VERSION } from "./wire.js";
|
|
60
|
+
export {
|
|
61
|
+
Connected,
|
|
62
|
+
Disconnected,
|
|
63
|
+
Focus,
|
|
64
|
+
Fragmenter,
|
|
65
|
+
Msg,
|
|
66
|
+
NetClient,
|
|
67
|
+
NetClock,
|
|
68
|
+
NetId,
|
|
69
|
+
NetRole,
|
|
70
|
+
NetServer,
|
|
71
|
+
NetSession,
|
|
72
|
+
NetSystems,
|
|
73
|
+
PROTOCOL_VERSION,
|
|
74
|
+
PeerJoined,
|
|
75
|
+
PeerLeft,
|
|
76
|
+
Peers,
|
|
77
|
+
PendingCommand,
|
|
78
|
+
PlayerSaveFailed,
|
|
79
|
+
Relevance,
|
|
80
|
+
RelevanceSet,
|
|
81
|
+
Replicated,
|
|
82
|
+
ServerCommand,
|
|
83
|
+
allOf,
|
|
84
|
+
alwaysInterest,
|
|
85
|
+
anyOf,
|
|
86
|
+
buildManifest,
|
|
87
|
+
commands,
|
|
88
|
+
connected,
|
|
89
|
+
defineNetCommand,
|
|
90
|
+
defineNetEvent,
|
|
91
|
+
directSession,
|
|
92
|
+
gridInterest,
|
|
93
|
+
hasAuthority,
|
|
94
|
+
isConditioned,
|
|
95
|
+
loopback,
|
|
96
|
+
netPlugin,
|
|
97
|
+
netSession,
|
|
98
|
+
onClient,
|
|
99
|
+
onServer,
|
|
100
|
+
ownerInterest,
|
|
101
|
+
ownerOf,
|
|
102
|
+
ownerOnly,
|
|
103
|
+
replicate,
|
|
104
|
+
savePlayer,
|
|
105
|
+
sendCommand,
|
|
106
|
+
sendEvent,
|
|
107
|
+
simulateConditions,
|
|
108
|
+
startHost,
|
|
109
|
+
teamInterest
|
|
110
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ComponentDef, type Entity, type Fields, type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { PeerId } from './transport.js';
|
|
3
|
+
/**
|
|
4
|
+
* A set of entities with a stable iteration order. Membership is a stamp per entity index, so
|
|
5
|
+
* `clear` is O(1) and refilling it every tick allocates nothing once it has grown.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RelevanceSet {
|
|
8
|
+
private readonly list;
|
|
9
|
+
private stamp;
|
|
10
|
+
private marks;
|
|
11
|
+
private ids;
|
|
12
|
+
private grow;
|
|
13
|
+
add(entity: Entity): void;
|
|
14
|
+
has(entity: Entity): boolean;
|
|
15
|
+
get size(): number;
|
|
16
|
+
/** The entity at `i`, in the order they were added. */
|
|
17
|
+
at(i: number): Entity;
|
|
18
|
+
/** A copy, for tools. */
|
|
19
|
+
get entities(): Entity[];
|
|
20
|
+
clear(): void;
|
|
21
|
+
/** Keeps the entities `keep` returns true for. */
|
|
22
|
+
retain(keep: (entity: Entity) => boolean): void;
|
|
23
|
+
}
|
|
24
|
+
/** What a policy knows about the peer it decides for. */
|
|
25
|
+
export interface InterestPeer {
|
|
26
|
+
readonly id: PeerId;
|
|
27
|
+
/** The entities the peer owns and its `Focus` entities, this tick. */
|
|
28
|
+
readonly focus: readonly Entity[];
|
|
29
|
+
/** Whether the peer has the entity now: policies keep known entities a little longer. */
|
|
30
|
+
knows(entity: Entity): boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Decides which replicated entities a peer receives (0087). The server runs it every network
|
|
34
|
+
* tick; anything it leaves out is never encoded for that peer, and despawns on its side.
|
|
35
|
+
*/
|
|
36
|
+
export interface InterestPolicy {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
/** Once per network tick, before any peer: rebuild shared indexes. */
|
|
39
|
+
prepare?(world: World): void;
|
|
40
|
+
update(world: World, peer: InterestPeer, out: RelevanceSet): void;
|
|
41
|
+
}
|
|
42
|
+
/** Every replicated entity (with all of `filter`, if given): turn order, scores, the board. */
|
|
43
|
+
export declare function alwaysInterest(filter?: readonly ComponentDef[]): InterestPolicy;
|
|
44
|
+
/** Entities the peer owns, and their children (through `ChildOf`). */
|
|
45
|
+
export declare function ownerInterest(): InterestPolicy;
|
|
46
|
+
export interface GridInterestOptions {
|
|
47
|
+
/** Cell edge in metres. */
|
|
48
|
+
cellSize: number;
|
|
49
|
+
/** Cells around each focus cell a peer receives (Chebyshev distance). */
|
|
50
|
+
range: number;
|
|
51
|
+
/** Cells on Y too (space, tall worlds). Default false: cells are columns on X and Z. */
|
|
52
|
+
vertical?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Entities within `range` cells of the peer's focus (0087), by world position. A known entity
|
|
56
|
+
* stays relevant one cell further, so one at the edge doesn't flicker in and out.
|
|
57
|
+
*/
|
|
58
|
+
export declare function gridInterest(options: GridInterestOptions): InterestPolicy;
|
|
59
|
+
/**
|
|
60
|
+
* Entities whose `field` of `component` equals that of the peer's first focus entity having it:
|
|
61
|
+
* teammates see each other wherever they are.
|
|
62
|
+
*/
|
|
63
|
+
export declare function teamInterest<F extends Fields>(component: ComponentDef<F>, field: keyof F & string): InterestPolicy;
|
|
64
|
+
/** Everything any of the policies includes. */
|
|
65
|
+
export declare function anyOf(...policies: InterestPolicy[]): InterestPolicy;
|
|
66
|
+
/** What every policy includes. */
|
|
67
|
+
export declare function allOf(...policies: InterestPolicy[]): InterestPolicy;
|
package/dist/interest.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChildOf,
|
|
3
|
+
entityIndex
|
|
4
|
+
} from "@aethervtt/shard-core";
|
|
5
|
+
import { GlobalTransform, Transform } from "@aethervtt/shard-transform";
|
|
6
|
+
import { Replicated } from "./components.js";
|
|
7
|
+
import { NumberList } from "./vec.js";
|
|
8
|
+
class RelevanceSet {
|
|
9
|
+
list = new NumberList();
|
|
10
|
+
stamp = 1;
|
|
11
|
+
marks = new Uint32Array(64);
|
|
12
|
+
ids = new Float64Array(64);
|
|
13
|
+
grow(index) {
|
|
14
|
+
let size = this.marks.length;
|
|
15
|
+
while (size <= index) size *= 2;
|
|
16
|
+
const marks = new Uint32Array(size);
|
|
17
|
+
marks.set(this.marks);
|
|
18
|
+
const ids = new Float64Array(size);
|
|
19
|
+
ids.set(this.ids);
|
|
20
|
+
this.marks = marks;
|
|
21
|
+
this.ids = ids;
|
|
22
|
+
}
|
|
23
|
+
add(entity) {
|
|
24
|
+
const i = entityIndex(entity);
|
|
25
|
+
if (i >= this.marks.length) this.grow(i);
|
|
26
|
+
if (this.marks[i] === this.stamp && this.ids[i] === entity) return;
|
|
27
|
+
this.marks[i] = this.stamp;
|
|
28
|
+
this.ids[i] = entity;
|
|
29
|
+
this.list.push(entity);
|
|
30
|
+
}
|
|
31
|
+
has(entity) {
|
|
32
|
+
const i = entityIndex(entity);
|
|
33
|
+
return i < this.marks.length && this.marks[i] === this.stamp && this.ids[i] === entity;
|
|
34
|
+
}
|
|
35
|
+
get size() {
|
|
36
|
+
return this.list.length;
|
|
37
|
+
}
|
|
38
|
+
/** The entity at `i`, in the order they were added. */
|
|
39
|
+
at(i) {
|
|
40
|
+
return this.list.data[i];
|
|
41
|
+
}
|
|
42
|
+
/** A copy, for tools. */
|
|
43
|
+
get entities() {
|
|
44
|
+
return this.list.toArray();
|
|
45
|
+
}
|
|
46
|
+
clear() {
|
|
47
|
+
this.list.clear();
|
|
48
|
+
if (++this.stamp === 4294967295) {
|
|
49
|
+
this.marks.fill(0);
|
|
50
|
+
this.stamp = 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Keeps the entities `keep` returns true for. */
|
|
54
|
+
retain(keep) {
|
|
55
|
+
const data = this.list.data;
|
|
56
|
+
let n = 0;
|
|
57
|
+
for (let i = 0; i < this.list.length; i++) {
|
|
58
|
+
const e = data[i];
|
|
59
|
+
if (keep(e)) data[n++] = e;
|
|
60
|
+
else this.marks[entityIndex(e)] = 0;
|
|
61
|
+
}
|
|
62
|
+
this.list.length = n;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function cachedQuery(desc) {
|
|
66
|
+
let world;
|
|
67
|
+
let query;
|
|
68
|
+
return (w) => {
|
|
69
|
+
if (w !== world || !query) {
|
|
70
|
+
world = w;
|
|
71
|
+
query = w.query(desc());
|
|
72
|
+
}
|
|
73
|
+
return query;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function alwaysInterest(filter = []) {
|
|
77
|
+
const query = cachedQuery(() => ({ with: [Replicated, ...filter] }));
|
|
78
|
+
return {
|
|
79
|
+
name: "always",
|
|
80
|
+
update(world, _peer, out) {
|
|
81
|
+
const q = query(world);
|
|
82
|
+
for (const table of q.tables) {
|
|
83
|
+
for (let row = 0; row < table.count; row++) out.add(table.entities[row]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function ownerInterest() {
|
|
89
|
+
const query = cachedQuery(() => ({ with: [Replicated] }));
|
|
90
|
+
const children = cachedQuery(() => ({ with: [Replicated, ChildOf] }));
|
|
91
|
+
return {
|
|
92
|
+
name: "owner",
|
|
93
|
+
update(world, peer, out) {
|
|
94
|
+
const q = query(world);
|
|
95
|
+
for (const table of q.tables) {
|
|
96
|
+
const owner = table.column(Replicated, "owner");
|
|
97
|
+
for (let row = 0; row < table.count; row++) {
|
|
98
|
+
if (owner[row] === peer.id) out.add(table.entities[row]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
addChildren(children(world), out);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function addChildren(q, out) {
|
|
106
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
107
|
+
const before = out.size;
|
|
108
|
+
for (const table of q.tables) {
|
|
109
|
+
const parent = table.column(ChildOf, "parent");
|
|
110
|
+
for (let row = 0; row < table.count; row++) {
|
|
111
|
+
if (parent[row] >= 0 && out.has(parent[row])) out.add(table.entities[row]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (out.size === before) return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const BIAS = 65536;
|
|
118
|
+
const SPAN = 131072;
|
|
119
|
+
function gridInterest(options) {
|
|
120
|
+
const { cellSize, range } = options;
|
|
121
|
+
const vertical = options.vertical ?? false;
|
|
122
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
123
|
+
const cellOf = /* @__PURE__ */ new Map();
|
|
124
|
+
let lastTick = 0;
|
|
125
|
+
const p = [0, 0, 0];
|
|
126
|
+
const key = (x, y, z) => ((x + BIAS) * SPAN + (vertical ? y + BIAS : 0)) * SPAN + (z + BIAS);
|
|
127
|
+
function position(table, row, out) {
|
|
128
|
+
if (table.has(ChildOf) && table.has(GlobalTransform)) {
|
|
129
|
+
const m = table.column(GlobalTransform, "matrix");
|
|
130
|
+
out[0] = m[row * 12 + 3];
|
|
131
|
+
out[1] = m[row * 12 + 7];
|
|
132
|
+
out[2] = m[row * 12 + 11];
|
|
133
|
+
} else {
|
|
134
|
+
const t = table.column(Transform, "translation");
|
|
135
|
+
out[0] = t[row * 3];
|
|
136
|
+
out[1] = t[row * 3 + 1];
|
|
137
|
+
out[2] = t[row * 3 + 2];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function cellKey(pos) {
|
|
141
|
+
return key(
|
|
142
|
+
Math.floor(pos[0] / cellSize),
|
|
143
|
+
Math.floor(pos[1] / cellSize),
|
|
144
|
+
Math.floor(pos[2] / cellSize)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
function move(entity, next) {
|
|
148
|
+
const prev = cellOf.get(entity);
|
|
149
|
+
if (prev === next) return;
|
|
150
|
+
if (prev !== void 0) {
|
|
151
|
+
const list2 = buckets.get(prev);
|
|
152
|
+
const i = list2.indexOf(entity);
|
|
153
|
+
if (i !== -1) list2.splice(i, 1);
|
|
154
|
+
}
|
|
155
|
+
cellOf.set(entity, next);
|
|
156
|
+
let list = buckets.get(next);
|
|
157
|
+
if (!list) {
|
|
158
|
+
list = [];
|
|
159
|
+
buckets.set(next, list);
|
|
160
|
+
}
|
|
161
|
+
list.push(entity);
|
|
162
|
+
}
|
|
163
|
+
function focusCell(world, entity) {
|
|
164
|
+
if (!world.isAlive(entity) || !world.has(entity, Transform)) return void 0;
|
|
165
|
+
position(world.entityTable(entity), world.entityRow(entity), p);
|
|
166
|
+
return cellKey(p);
|
|
167
|
+
}
|
|
168
|
+
const query = cachedQuery(() => ({ with: [Replicated, Transform] }));
|
|
169
|
+
return {
|
|
170
|
+
name: "grid",
|
|
171
|
+
prepare(world) {
|
|
172
|
+
const q = query(world);
|
|
173
|
+
for (const table of q.tables) {
|
|
174
|
+
const moved = table.lastStructural > lastTick || table.lastChanged(Transform) > lastTick || table.has(ChildOf) && table.lastChanged(GlobalTransform) > lastTick;
|
|
175
|
+
if (!moved) continue;
|
|
176
|
+
for (let row = 0; row < table.count; row++) {
|
|
177
|
+
const e = table.entities[row];
|
|
178
|
+
position(table, row, p);
|
|
179
|
+
move(e, cellKey(p));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
lastTick = world.tick;
|
|
183
|
+
},
|
|
184
|
+
update(world, peer, out) {
|
|
185
|
+
const reach = range + 1;
|
|
186
|
+
for (const f of peer.focus) {
|
|
187
|
+
const center = focusCell(world, f);
|
|
188
|
+
if (center === void 0) continue;
|
|
189
|
+
const cz = center % SPAN - BIAS;
|
|
190
|
+
const rest = Math.floor(center / SPAN);
|
|
191
|
+
const cy = vertical ? rest % SPAN - BIAS : 0;
|
|
192
|
+
const cx = Math.floor(rest / SPAN) - BIAS;
|
|
193
|
+
const yr = vertical ? reach : 0;
|
|
194
|
+
for (let dx = -reach; dx <= reach; dx++) {
|
|
195
|
+
for (let dy = -yr; dy <= yr; dy++) {
|
|
196
|
+
for (let dz = -reach; dz <= reach; dz++) {
|
|
197
|
+
const list = buckets.get(key(cx + dx, cy + dy, cz + dz));
|
|
198
|
+
if (!list) continue;
|
|
199
|
+
const edge = Math.abs(dx) === reach || Math.abs(dy) === reach || Math.abs(dz) === reach;
|
|
200
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
201
|
+
const e = list[i];
|
|
202
|
+
if (!world.isAlive(e)) {
|
|
203
|
+
list.splice(i, 1);
|
|
204
|
+
cellOf.delete(e);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!edge || peer.knows(e)) out.add(e);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function teamInterest(component, field) {
|
|
217
|
+
const query = cachedQuery(() => ({ with: [Replicated, component] }));
|
|
218
|
+
return {
|
|
219
|
+
name: "team",
|
|
220
|
+
update(world, peer, out) {
|
|
221
|
+
let team;
|
|
222
|
+
for (const f of peer.focus) {
|
|
223
|
+
if (world.isAlive(f) && world.has(f, component)) {
|
|
224
|
+
team = world.entityTable(f).column(component, field)[world.entityRow(f)];
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (team === void 0) return;
|
|
229
|
+
const q = query(world);
|
|
230
|
+
for (const table of q.tables) {
|
|
231
|
+
const column = table.column(component, field);
|
|
232
|
+
for (let row = 0; row < table.count; row++) {
|
|
233
|
+
if (column[row] === team) out.add(table.entities[row]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function anyOf(...policies) {
|
|
240
|
+
return {
|
|
241
|
+
name: `anyOf(${policies.map((p) => p.name).join(", ")})`,
|
|
242
|
+
prepare(world) {
|
|
243
|
+
for (const p of policies) p.prepare?.(world);
|
|
244
|
+
},
|
|
245
|
+
update(world, peer, out) {
|
|
246
|
+
for (const p of policies) p.update(world, peer, out);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function allOf(...policies) {
|
|
251
|
+
const scratch = new RelevanceSet();
|
|
252
|
+
return {
|
|
253
|
+
name: `allOf(${policies.map((p) => p.name).join(", ")})`,
|
|
254
|
+
prepare(world) {
|
|
255
|
+
for (const p of policies) p.prepare?.(world);
|
|
256
|
+
},
|
|
257
|
+
update(world, peer, out) {
|
|
258
|
+
const [first, ...rest] = policies;
|
|
259
|
+
if (!first) return;
|
|
260
|
+
first.update(world, peer, out);
|
|
261
|
+
for (const p of rest) {
|
|
262
|
+
scratch.clear();
|
|
263
|
+
p.update(world, peer, scratch);
|
|
264
|
+
out.retain((e) => scratch.has(e));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export {
|
|
270
|
+
RelevanceSet,
|
|
271
|
+
allOf,
|
|
272
|
+
alwaysInterest,
|
|
273
|
+
anyOf,
|
|
274
|
+
gridInterest,
|
|
275
|
+
ownerInterest,
|
|
276
|
+
teamInterest
|
|
277
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ShardError } from '@aethervtt/shard-core';
|
|
2
|
+
import type { NetTransport } from './transport.js';
|
|
3
|
+
export interface LoopbackOptions {
|
|
4
|
+
/** Defaults to 1 MiB. Tests lower it to exercise fragmenting. */
|
|
5
|
+
maxMessage?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function tooLarge(size: number, max: number): ShardError;
|
|
8
|
+
/**
|
|
9
|
+
* An in-process transport (0086): a server App and client Apps in one process connect by address.
|
|
10
|
+
* Delivery is immediate; wrap it in `simulateConditions` for latency and loss.
|
|
11
|
+
*/
|
|
12
|
+
export declare function loopback(options?: LoopbackOptions): NetTransport;
|