@crowdedkingdoms/crowdyjs 8.8.0 → 8.9.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/MIGRATION.md +43 -0
- package/README.md +1 -1
- package/dist/domains/compute.d.ts +12 -1
- package/dist/domains/compute.d.ts.map +1 -1
- package/dist/domains/compute.js +18 -1
- package/dist/generated/graphql.d.ts +60 -0
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/kit/abilities.d.ts +66 -0
- package/dist/kit/abilities.d.ts.map +1 -0
- package/dist/kit/abilities.js +71 -0
- package/dist/kit/blueprints/index.d.ts +2 -0
- package/dist/kit/blueprints/index.d.ts.map +1 -1
- package/dist/kit/blueprints/index.js +2 -0
- package/dist/kit/blueprints/liveops.d.ts +42 -0
- package/dist/kit/blueprints/liveops.d.ts.map +1 -0
- package/dist/kit/blueprints/liveops.js +170 -0
- package/dist/kit/blueprints/moderation.d.ts +32 -0
- package/dist/kit/blueprints/moderation.d.ts.map +1 -0
- package/dist/kit/blueprints/moderation.js +140 -0
- package/dist/kit/index.d.ts +9 -2
- package/dist/kit/index.d.ts.map +1 -1
- package/dist/kit/index.js +9 -2
- package/dist/kit/kit.d.ts +44 -0
- package/dist/kit/kit.d.ts.map +1 -1
- package/dist/kit/kit.js +35 -1
- package/dist/kit/liveops.d.ts +112 -0
- package/dist/kit/liveops.d.ts.map +1 -0
- package/dist/kit/liveops.js +183 -0
- package/dist/kit/loot.d.ts +22 -1
- package/dist/kit/loot.d.ts.map +1 -1
- package/dist/kit/loot.js +41 -1
- package/dist/kit/moderation.d.ts +73 -0
- package/dist/kit/moderation.d.ts.map +1 -0
- package/dist/kit/moderation.js +103 -0
- package/dist/kit/movement.d.ts +78 -0
- package/dist/kit/movement.d.ts.map +1 -0
- package/dist/kit/movement.js +103 -0
- package/dist/kit/npcs.d.ts.map +1 -1
- package/dist/kit/racing.d.ts +85 -0
- package/dist/kit/racing.d.ts.map +1 -0
- package/dist/kit/racing.js +99 -0
- package/dist/kit/social.d.ts.map +1 -1
- package/dist/kit/telemetry.d.ts +66 -0
- package/dist/kit/telemetry.d.ts.map +1 -0
- package/dist/kit/telemetry.js +133 -0
- package/dist/kit/territory.d.ts +97 -0
- package/dist/kit/territory.d.ts.map +1 -0
- package/dist/kit/territory.js +114 -0
- package/dist/kit/wire.d.ts +51 -0
- package/dist/kit/wire.d.ts.map +1 -1
- package/dist/kit/wire.js +60 -0
- package/package.json +1 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { liveopsNames } from './blueprints/liveops.js';
|
|
2
|
+
import { kitContainerProperties, kitInvoke } from './shared.js';
|
|
3
|
+
import { parseEngineEvent, EVENT_ZONE_CHANGE } from './wire.js';
|
|
4
|
+
/**
|
|
5
|
+
* Runtime helpers for the liveops blueprint: event windows (admin CRUD +
|
|
6
|
+
* active reads — engine-backed when the liveops-scheduler is deployed),
|
|
7
|
+
* seasons with battle-pass composition, and the type-98 zone-change parser.
|
|
8
|
+
*
|
|
9
|
+
* Obtained via `client.kit(appId).liveops`.
|
|
10
|
+
*/
|
|
11
|
+
export class LiveopsKit {
|
|
12
|
+
constructor(appId, gameModel, options = {}, engines) {
|
|
13
|
+
this.appId = appId;
|
|
14
|
+
this.gameModel = gameModel;
|
|
15
|
+
this.engines = engines;
|
|
16
|
+
this.names = liveopsNames(options.typePrefix ?? '');
|
|
17
|
+
this.moduleName = options.moduleName ?? 'liveops-scheduler';
|
|
18
|
+
}
|
|
19
|
+
/** Is the liveops-scheduler engine deployed + enabled (cached)? */
|
|
20
|
+
engineAvailable() {
|
|
21
|
+
if (!this.engines)
|
|
22
|
+
return Promise.resolve(false);
|
|
23
|
+
return this.engines.has(this.moduleName);
|
|
24
|
+
}
|
|
25
|
+
/** STUDIO (admin) — create an event window. */
|
|
26
|
+
async defineWindow(input) {
|
|
27
|
+
return this.gameModel.createContainer({
|
|
28
|
+
appId: this.appId,
|
|
29
|
+
typeName: this.names.windowType,
|
|
30
|
+
displayName: input.displayName ?? `Window ${input.windowId}`,
|
|
31
|
+
properties: [
|
|
32
|
+
{ key: 'window_id', valueType: 'string', valueJson: JSON.stringify(input.windowId) },
|
|
33
|
+
...(input.opensAtMs !== undefined
|
|
34
|
+
? [{ key: 'opens_at_ms', valueType: 'int', valueJson: String(input.opensAtMs) }]
|
|
35
|
+
: []),
|
|
36
|
+
...(input.closesAtMs !== undefined
|
|
37
|
+
? [{ key: 'closes_at_ms', valueType: 'int', valueJson: String(input.closesAtMs) }]
|
|
38
|
+
: []),
|
|
39
|
+
{
|
|
40
|
+
key: 'modifiers',
|
|
41
|
+
valueType: 'string',
|
|
42
|
+
valueJson: JSON.stringify(JSON.stringify(input.modifiers ?? {})),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Every window (model read). */
|
|
48
|
+
async windows() {
|
|
49
|
+
const containers = await this.gameModel.containers({
|
|
50
|
+
appId: this.appId,
|
|
51
|
+
typeName: this.names.windowType,
|
|
52
|
+
});
|
|
53
|
+
return Promise.all(containers.map(async (c) => {
|
|
54
|
+
const props = await kitContainerProperties(this.gameModel, String(this.appId), c.containerId);
|
|
55
|
+
let modifiers = {};
|
|
56
|
+
try {
|
|
57
|
+
modifiers = JSON.parse(String(props.modifiers ?? '{}'));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* opaque */
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
containerId: c.containerId,
|
|
64
|
+
windowId: String(props.window_id ?? ''),
|
|
65
|
+
active: props.active === true,
|
|
66
|
+
opensAtMs: Number(props.opens_at_ms ?? 0),
|
|
67
|
+
closesAtMs: Number(props.closes_at_ms ?? 0),
|
|
68
|
+
modifiers,
|
|
69
|
+
};
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The ACTIVE windows. Engine path (scheduler deployed): the module's
|
|
74
|
+
* authoritative view; otherwise filters the model read.
|
|
75
|
+
*/
|
|
76
|
+
async activeWindows() {
|
|
77
|
+
if (this.engines && (await this.engineAvailable())) {
|
|
78
|
+
const result = await this.engines.invoke(this.moduleName, 'active_windows', {});
|
|
79
|
+
if (result.success && Array.isArray(result.body.windows)) {
|
|
80
|
+
const all = await this.windows();
|
|
81
|
+
const activeIds = new Set(result.body.windows.map((w) => String(w.windowId)));
|
|
82
|
+
return all.filter((w) => activeIds.has(w.windowId));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return (await this.windows()).filter((w) => w.active);
|
|
86
|
+
}
|
|
87
|
+
/** STUDIO (admin/automation) — open a window through the model function. */
|
|
88
|
+
async openWindow(windowContainerId, sessionId) {
|
|
89
|
+
return kitInvoke(this.gameModel, {
|
|
90
|
+
appId: String(this.appId),
|
|
91
|
+
functionName: this.names.openWindowFn,
|
|
92
|
+
selfContainerId: windowContainerId,
|
|
93
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** STUDIO (admin/automation) — close a window through the model function. */
|
|
97
|
+
async closeWindow(windowContainerId, sessionId) {
|
|
98
|
+
return kitInvoke(this.gameModel, {
|
|
99
|
+
appId: String(this.appId),
|
|
100
|
+
functionName: this.names.closeWindowFn,
|
|
101
|
+
selfContainerId: windowContainerId,
|
|
102
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** STUDIO (admin) — create a season (+ battle-pass composition). */
|
|
106
|
+
async defineSeason(input) {
|
|
107
|
+
return this.gameModel.createContainer({
|
|
108
|
+
appId: this.appId,
|
|
109
|
+
typeName: this.names.seasonType,
|
|
110
|
+
displayName: input.displayName ?? `Season ${input.seasonId}`,
|
|
111
|
+
properties: [
|
|
112
|
+
{ key: 'season_id', valueType: 'string', valueJson: JSON.stringify(input.seasonId) },
|
|
113
|
+
...(input.startsAtMs !== undefined
|
|
114
|
+
? [{ key: 'starts_at_ms', valueType: 'int', valueJson: String(input.startsAtMs) }]
|
|
115
|
+
: []),
|
|
116
|
+
...(input.endsAtMs !== undefined
|
|
117
|
+
? [{ key: 'ends_at_ms', valueType: 'int', valueJson: String(input.endsAtMs) }]
|
|
118
|
+
: []),
|
|
119
|
+
...(input.passTrack !== undefined
|
|
120
|
+
? [{ key: 'pass_track', valueType: 'string', valueJson: JSON.stringify(input.passTrack) }]
|
|
121
|
+
: []),
|
|
122
|
+
{
|
|
123
|
+
key: 'pass_features',
|
|
124
|
+
valueType: 'string',
|
|
125
|
+
valueJson: JSON.stringify(JSON.stringify(input.passFeatures ?? [])),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Every season (model read). */
|
|
131
|
+
async seasons() {
|
|
132
|
+
const containers = await this.gameModel.containers({
|
|
133
|
+
appId: this.appId,
|
|
134
|
+
typeName: this.names.seasonType,
|
|
135
|
+
});
|
|
136
|
+
return Promise.all(containers.map(async (c) => {
|
|
137
|
+
const props = await kitContainerProperties(this.gameModel, String(this.appId), c.containerId);
|
|
138
|
+
let passFeatures = [];
|
|
139
|
+
try {
|
|
140
|
+
passFeatures = JSON.parse(String(props.pass_features ?? '[]')).map(String);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* opaque */
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
containerId: c.containerId,
|
|
147
|
+
seasonId: String(props.season_id ?? ''),
|
|
148
|
+
active: props.active === true,
|
|
149
|
+
startsAtMs: Number(props.starts_at_ms ?? 0),
|
|
150
|
+
endsAtMs: Number(props.ends_at_ms ?? 0),
|
|
151
|
+
passTrack: String(props.pass_track ?? ''),
|
|
152
|
+
passFeatures,
|
|
153
|
+
};
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
/** The active season, when one exists. */
|
|
157
|
+
async currentSeason() {
|
|
158
|
+
return (await this.seasons()).find((s) => s.active) ?? null;
|
|
159
|
+
}
|
|
160
|
+
/** STUDIO (admin/automation) — activate a season through the model function. */
|
|
161
|
+
async activateSeason(seasonContainerId, sessionId) {
|
|
162
|
+
return kitInvoke(this.gameModel, {
|
|
163
|
+
appId: String(this.appId),
|
|
164
|
+
functionName: this.names.activateSeasonFn,
|
|
165
|
+
selfContainerId: seasonContainerId,
|
|
166
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** Parse a type-98 zone-change server event (BR circles, event areas). */
|
|
170
|
+
parseZoneChange(bytes) {
|
|
171
|
+
const parsed = parseEngineEvent(bytes);
|
|
172
|
+
if (!parsed || parsed.eventType !== EVENT_ZONE_CHANGE)
|
|
173
|
+
return null;
|
|
174
|
+
return {
|
|
175
|
+
kind: String(parsed.body.kind ?? ''),
|
|
176
|
+
phase: parsed.body.phase != null ? Number(parsed.body.phase) : null,
|
|
177
|
+
radiusNow: Number(parsed.body.radiusNow ?? 0),
|
|
178
|
+
centerX: Number(parsed.body.centerX ?? 0),
|
|
179
|
+
centerZ: Number(parsed.body.centerZ ?? 0),
|
|
180
|
+
body: parsed.body,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/kit/loot.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { GameModelAPI } from '../domains/gameModel.js';
|
|
2
|
+
import type { EngineDetector } from './engine.js';
|
|
2
3
|
import type { Scalars } from '../generated/graphql.js';
|
|
3
4
|
import { type KitInvokeResult } from './shared.js';
|
|
4
5
|
/** Options for {@link LootKit}. Must match the deployed loot blueprint. */
|
|
5
6
|
export interface LootKitOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The big-table loot module (pity timers, audit trails). Defaults to
|
|
9
|
+
* `'loot-engine'`; the gacha-shrine example ships this contract.
|
|
10
|
+
*/
|
|
11
|
+
engineModuleName?: string;
|
|
6
12
|
/** The `typePrefix` the loot blueprint was deployed with. */
|
|
7
13
|
typePrefix?: string;
|
|
8
14
|
}
|
|
@@ -29,9 +35,24 @@ export interface KitLootRoll {
|
|
|
29
35
|
export declare class LootKit {
|
|
30
36
|
private readonly appId;
|
|
31
37
|
private readonly gameModel;
|
|
38
|
+
private readonly engines?;
|
|
32
39
|
private readonly names;
|
|
33
40
|
private readonly typePrefix;
|
|
34
|
-
constructor(appId: Scalars['BigInt']['input'], gameModel: GameModelAPI, options?: LootKitOptions);
|
|
41
|
+
constructor(appId: Scalars['BigInt']['input'], gameModel: GameModelAPI, options?: LootKitOptions, engines?: EngineDetector | undefined);
|
|
42
|
+
private readonly engineModuleName;
|
|
43
|
+
/**
|
|
44
|
+
* Is a big-table loot module deployed + enabled (cached per session)?
|
|
45
|
+
* When true, {@link enginePull} routes rolls through server-held RNG with
|
|
46
|
+
* pity timers + audit trails; the blueprint's weighted model rolls stay
|
|
47
|
+
* for small tables (the coexistence policy — "the server picks the path").
|
|
48
|
+
*/
|
|
49
|
+
engineAvailable(): Promise<boolean>;
|
|
50
|
+
/** Pull from the module's table (pity-timer path); count caps at 10. */
|
|
51
|
+
enginePull(count?: number): Promise<Record<string, unknown>>;
|
|
52
|
+
/** Your pity counters + roll totals from the module. */
|
|
53
|
+
enginePity(): Promise<Record<string, unknown>>;
|
|
54
|
+
/** Your rolling audit trail from the module (dispute resolution). */
|
|
55
|
+
engineAudit(): Promise<Record<string, unknown>>;
|
|
35
56
|
/**
|
|
36
57
|
* Create an unrolled `LootRoll` for a table, owned by `ownerUserId` (who
|
|
37
58
|
* will claim it). Event-drop automations pick from the pool of unrolled
|
package/dist/kit/loot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loot.d.ts","sourceRoot":"","sources":["../../src/kit/loot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAErB,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,qBAAa,OAAO;IAKhB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;
|
|
1
|
+
{"version":3,"file":"loot.d.ts","sourceRoot":"","sources":["../../src/kit/loot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEvD,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAErB,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,sCAAsC;AACtC,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,qBAAa,OAAO;IAKhB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAE1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAP3B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAGjB,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EACjC,SAAS,EAAE,YAAY,EACxC,OAAO,GAAE,cAAmB,EACX,OAAO,CAAC,EAAE,cAAc,YAAA;IAO3C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C;;;;;OAKG;IACH,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAKnC,wEAAwE;IAClE,UAAU,CAAC,KAAK,SAAI;IAO1B,wDAAwD;IAClD,UAAU;IAOhB,qEAAqE;IAC/D,WAAW;IAOjB;;;;OAIG;IACG,UAAU,CAAC,KAAK,EAAE;QACtB,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC;QACxC,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB;;;;;;;;;;;IAqBD;;;;;;OAMG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAU9E;;;;OAIG;IACG,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAShF,6BAA6B;IACvB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAYjD;;;OAGG;IACG,KAAK,CACT,WAAW,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EACvC,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAO,GAC1D,OAAO,CAAC,WAAW,EAAE,CAAC;IAwBzB;;;OAGG;IACG,OAAO,CACX,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO;;;;;;;;;;;;;;;;;YAYtC,MAAM;CAoBrB"}
|
package/dist/kit/loot.js
CHANGED
|
@@ -10,11 +10,51 @@ import { kitContainerProperties, kitInvoke, } from './shared.js';
|
|
|
10
10
|
* Obtained via `client.kit(appId).loot`.
|
|
11
11
|
*/
|
|
12
12
|
export class LootKit {
|
|
13
|
-
constructor(appId, gameModel, options = {}) {
|
|
13
|
+
constructor(appId, gameModel, options = {}, engines) {
|
|
14
14
|
this.appId = appId;
|
|
15
15
|
this.gameModel = gameModel;
|
|
16
|
+
this.engines = engines;
|
|
16
17
|
this.typePrefix = options.typePrefix ?? '';
|
|
17
18
|
this.names = lootNames(this.typePrefix);
|
|
19
|
+
this.engineModuleName = options.engineModuleName ?? 'loot-engine';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Is a big-table loot module deployed + enabled (cached per session)?
|
|
23
|
+
* When true, {@link enginePull} routes rolls through server-held RNG with
|
|
24
|
+
* pity timers + audit trails; the blueprint's weighted model rolls stay
|
|
25
|
+
* for small tables (the coexistence policy — "the server picks the path").
|
|
26
|
+
*/
|
|
27
|
+
engineAvailable() {
|
|
28
|
+
if (!this.engines)
|
|
29
|
+
return Promise.resolve(false);
|
|
30
|
+
return this.engines.has(this.engineModuleName);
|
|
31
|
+
}
|
|
32
|
+
/** Pull from the module's table (pity-timer path); count caps at 10. */
|
|
33
|
+
async enginePull(count = 1) {
|
|
34
|
+
if (!this.engines)
|
|
35
|
+
throw new Error('loot engine unavailable: compute domain not wired');
|
|
36
|
+
const result = await this.engines.invoke(this.engineModuleName, 'pull', { count });
|
|
37
|
+
if (!result.success)
|
|
38
|
+
throw new Error(`loot.pull failed: ${result.reason ?? 'unknown'}`);
|
|
39
|
+
return result.body;
|
|
40
|
+
}
|
|
41
|
+
/** Your pity counters + roll totals from the module. */
|
|
42
|
+
async enginePity() {
|
|
43
|
+
if (!this.engines)
|
|
44
|
+
throw new Error('loot engine unavailable: compute domain not wired');
|
|
45
|
+
const result = await this.engines.invoke(this.engineModuleName, 'pity', {});
|
|
46
|
+
if (!result.success)
|
|
47
|
+
throw new Error(`loot.pity failed: ${result.reason ?? 'unknown'}`);
|
|
48
|
+
return result.body;
|
|
49
|
+
}
|
|
50
|
+
/** Your rolling audit trail from the module (dispute resolution). */
|
|
51
|
+
async engineAudit() {
|
|
52
|
+
if (!this.engines)
|
|
53
|
+
throw new Error('loot engine unavailable: compute domain not wired');
|
|
54
|
+
const result = await this.engines.invoke(this.engineModuleName, 'audit', {});
|
|
55
|
+
if (!result.success)
|
|
56
|
+
throw new Error(`loot.audit failed: ${result.reason ?? 'unknown'}`);
|
|
57
|
+
return result.body;
|
|
18
58
|
}
|
|
19
59
|
/**
|
|
20
60
|
* Create an unrolled `LootRoll` for a table, owned by `ownerUserId` (who
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { GameModelAPI } from '../domains/gameModel.js';
|
|
2
|
+
import type { Scalars } from '../generated/graphql.js';
|
|
3
|
+
/** Options for {@link ModerationKit}. */
|
|
4
|
+
export interface ModerationKitOptions {
|
|
5
|
+
/** The `typePrefix` the moderation blueprint was deployed with. */
|
|
6
|
+
typePrefix?: string;
|
|
7
|
+
}
|
|
8
|
+
/** A parsed report row. */
|
|
9
|
+
export interface KitModReport {
|
|
10
|
+
containerId: string;
|
|
11
|
+
reporterUserId: string;
|
|
12
|
+
subjectUserId: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
status: string;
|
|
16
|
+
resolution: string;
|
|
17
|
+
filedAtMs: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Runtime helpers for the moderation blueprint: file reports, read the
|
|
21
|
+
* admin escalation queue, resolve reports, and manage the caller's personal
|
|
22
|
+
* mute list (client-enforced chat filtering). Enforcement stays on platform
|
|
23
|
+
* surfaces — tier revocation and grid permissions.
|
|
24
|
+
*
|
|
25
|
+
* Obtained via `client.kit(appId).moderation`.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ModerationKit {
|
|
28
|
+
private readonly appId;
|
|
29
|
+
private readonly gameModel;
|
|
30
|
+
private readonly names;
|
|
31
|
+
constructor(appId: Scalars['BigInt']['input'], gameModel: GameModelAPI, options?: ModerationKitOptions);
|
|
32
|
+
/** File a report (creates the caller's report row in the queue). */
|
|
33
|
+
report(input: {
|
|
34
|
+
reporterUserId: string;
|
|
35
|
+
subjectUserId: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
detail?: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
__typename?: "GmContainer";
|
|
40
|
+
containerId: string;
|
|
41
|
+
appId: string;
|
|
42
|
+
sessionId: string | null;
|
|
43
|
+
typeName: string;
|
|
44
|
+
displayName: string;
|
|
45
|
+
description: string | null;
|
|
46
|
+
ownerUserId: string | null;
|
|
47
|
+
metadataJson: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** The escalation queue (admins; filter by status, default `'open'`). */
|
|
50
|
+
queue(status?: string): Promise<KitModReport[]>;
|
|
51
|
+
/** ADMIN — resolve a report with a disposition. */
|
|
52
|
+
resolve(reportContainerId: string, status: 'actioned' | 'dismissed', resolution: string): Promise<import("./shared.js").KitInvokeResult<string>>;
|
|
53
|
+
/** Mute a player (adds to YOUR client-enforced mute list). */
|
|
54
|
+
mute(ownerUserId: string, mutedUserId: string): Promise<{
|
|
55
|
+
__typename?: "GmContainer";
|
|
56
|
+
containerId: string;
|
|
57
|
+
appId: string;
|
|
58
|
+
sessionId: string | null;
|
|
59
|
+
typeName: string;
|
|
60
|
+
displayName: string;
|
|
61
|
+
description: string | null;
|
|
62
|
+
ownerUserId: string | null;
|
|
63
|
+
metadataJson: string;
|
|
64
|
+
}>;
|
|
65
|
+
/** Unmute: delete the matching mute row. */
|
|
66
|
+
unmute(ownerUserId: string, mutedUserId: string): Promise<boolean>;
|
|
67
|
+
/** The caller's mute list (feed it to your chat renderer). */
|
|
68
|
+
mutes(ownerUserId: string): Promise<Array<{
|
|
69
|
+
containerId: string;
|
|
70
|
+
mutedUserId: string;
|
|
71
|
+
}>>;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=moderation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation.d.ts","sourceRoot":"","sources":["../../src/kit/moderation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAIvD,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,2BAA2B;AAC3B,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,qBAAa,aAAa;IAItB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAJ5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;gBAGrB,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EACjC,SAAS,EAAE,YAAY,EACxC,OAAO,GAAE,oBAAyB;IAKpC,oEAAoE;IAC9D,MAAM,CAAC,KAAK,EAAE;QAClB,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,MAAM,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB;;;;;;;;;;;IAeD,yEAAyE;IACnE,KAAK,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAyBrD,mDAAmD;IAC7C,OAAO,CAAC,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,WAAW,EAAE,UAAU,EAAE,MAAM;IAS7F,8DAA8D;IACxD,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;;;;;;;;;;;IAYnD,4CAA4C;IACtC,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAQxE,8DAA8D;IACxD,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAmB/F"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { moderationNames } from './blueprints/moderation.js';
|
|
2
|
+
import { kitContainerProperties, kitInvoke } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* Runtime helpers for the moderation blueprint: file reports, read the
|
|
5
|
+
* admin escalation queue, resolve reports, and manage the caller's personal
|
|
6
|
+
* mute list (client-enforced chat filtering). Enforcement stays on platform
|
|
7
|
+
* surfaces — tier revocation and grid permissions.
|
|
8
|
+
*
|
|
9
|
+
* Obtained via `client.kit(appId).moderation`.
|
|
10
|
+
*/
|
|
11
|
+
export class ModerationKit {
|
|
12
|
+
constructor(appId, gameModel, options = {}) {
|
|
13
|
+
this.appId = appId;
|
|
14
|
+
this.gameModel = gameModel;
|
|
15
|
+
this.names = moderationNames(options.typePrefix ?? '');
|
|
16
|
+
}
|
|
17
|
+
/** File a report (creates the caller's report row in the queue). */
|
|
18
|
+
async report(input) {
|
|
19
|
+
return this.gameModel.createContainer({
|
|
20
|
+
appId: this.appId,
|
|
21
|
+
typeName: this.names.reportType,
|
|
22
|
+
displayName: `report ${input.reason} vs ${input.subjectUserId}`,
|
|
23
|
+
properties: [
|
|
24
|
+
{ key: 'reporter_user_id', valueType: 'string', valueJson: JSON.stringify(input.reporterUserId) },
|
|
25
|
+
{ key: 'subject_user_id', valueType: 'string', valueJson: JSON.stringify(input.subjectUserId) },
|
|
26
|
+
{ key: 'reason', valueType: 'string', valueJson: JSON.stringify(input.reason) },
|
|
27
|
+
{ key: 'detail', valueType: 'string', valueJson: JSON.stringify((input.detail ?? '').slice(0, 500)) },
|
|
28
|
+
{ key: 'filed_at_ms', valueType: 'int', valueJson: String(Date.now()) },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** The escalation queue (admins; filter by status, default `'open'`). */
|
|
33
|
+
async queue(status = 'open') {
|
|
34
|
+
const containers = await this.gameModel.containers({
|
|
35
|
+
appId: this.appId,
|
|
36
|
+
typeName: this.names.reportType,
|
|
37
|
+
});
|
|
38
|
+
const rows = await Promise.all(containers.map(async (c) => {
|
|
39
|
+
const props = await kitContainerProperties(this.gameModel, String(this.appId), c.containerId);
|
|
40
|
+
return {
|
|
41
|
+
containerId: c.containerId,
|
|
42
|
+
reporterUserId: String(props.reporter_user_id ?? ''),
|
|
43
|
+
subjectUserId: String(props.subject_user_id ?? ''),
|
|
44
|
+
reason: String(props.reason ?? ''),
|
|
45
|
+
detail: String(props.detail ?? ''),
|
|
46
|
+
status: String(props.status ?? 'open'),
|
|
47
|
+
resolution: String(props.resolution ?? ''),
|
|
48
|
+
filedAtMs: Number(props.filed_at_ms ?? 0),
|
|
49
|
+
};
|
|
50
|
+
}));
|
|
51
|
+
return rows
|
|
52
|
+
.filter((r) => !status || r.status === status)
|
|
53
|
+
.sort((a, b) => a.filedAtMs - b.filedAtMs);
|
|
54
|
+
}
|
|
55
|
+
/** ADMIN — resolve a report with a disposition. */
|
|
56
|
+
async resolve(reportContainerId, status, resolution) {
|
|
57
|
+
return kitInvoke(this.gameModel, {
|
|
58
|
+
appId: String(this.appId),
|
|
59
|
+
functionName: this.names.resolveReportFn,
|
|
60
|
+
selfContainerId: reportContainerId,
|
|
61
|
+
params: { status, resolution },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Mute a player (adds to YOUR client-enforced mute list). */
|
|
65
|
+
async mute(ownerUserId, mutedUserId) {
|
|
66
|
+
return this.gameModel.createContainer({
|
|
67
|
+
appId: this.appId,
|
|
68
|
+
typeName: this.names.muteType,
|
|
69
|
+
displayName: `mute ${mutedUserId}`,
|
|
70
|
+
properties: [
|
|
71
|
+
{ key: 'owner_user_id', valueType: 'string', valueJson: JSON.stringify(ownerUserId) },
|
|
72
|
+
{ key: 'muted_user_id', valueType: 'string', valueJson: JSON.stringify(mutedUserId) },
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Unmute: delete the matching mute row. */
|
|
77
|
+
async unmute(ownerUserId, mutedUserId) {
|
|
78
|
+
const mutes = await this.mutes(ownerUserId);
|
|
79
|
+
const row = mutes.find((m) => m.mutedUserId === mutedUserId);
|
|
80
|
+
if (!row)
|
|
81
|
+
return false;
|
|
82
|
+
await this.gameModel.deleteContainer({ appId: this.appId, containerId: row.containerId });
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/** The caller's mute list (feed it to your chat renderer). */
|
|
86
|
+
async mutes(ownerUserId) {
|
|
87
|
+
const containers = await this.gameModel.containers({
|
|
88
|
+
appId: this.appId,
|
|
89
|
+
typeName: this.names.muteType,
|
|
90
|
+
});
|
|
91
|
+
const rows = await Promise.all(containers.map(async (c) => {
|
|
92
|
+
const props = await kitContainerProperties(this.gameModel, String(this.appId), c.containerId);
|
|
93
|
+
return {
|
|
94
|
+
containerId: c.containerId,
|
|
95
|
+
ownerUserId: String(props.owner_user_id ?? ''),
|
|
96
|
+
mutedUserId: String(props.muted_user_id ?? ''),
|
|
97
|
+
};
|
|
98
|
+
}));
|
|
99
|
+
return rows
|
|
100
|
+
.filter((r) => r.ownerUserId === String(ownerUserId))
|
|
101
|
+
.map(({ containerId, mutedUserId }) => ({ containerId, mutedUserId }));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { GameModelAPI } from '../domains/gameModel.js';
|
|
2
|
+
import type { Scalars } from '../generated/graphql.js';
|
|
3
|
+
import type { EngineDetector } from './engine.js';
|
|
4
|
+
import { type MovementViolationEvent } from './wire.js';
|
|
5
|
+
/** Options for {@link MovementKit}. */
|
|
6
|
+
export interface MovementKitOptions {
|
|
7
|
+
/** The warden module name. Defaults to `'movement-warden'`. */
|
|
8
|
+
moduleName?: string;
|
|
9
|
+
/** The warden-config container type. Defaults to `'WardenConfig'`. */
|
|
10
|
+
configTypeName?: string;
|
|
11
|
+
}
|
|
12
|
+
/** A user's violation book as the warden reports it. */
|
|
13
|
+
export interface KitViolations {
|
|
14
|
+
userId: string;
|
|
15
|
+
speed: number;
|
|
16
|
+
teleport: number;
|
|
17
|
+
bounds: number;
|
|
18
|
+
log: Array<{
|
|
19
|
+
atMs: number;
|
|
20
|
+
kind: string;
|
|
21
|
+
detail: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runtime helpers for the movement-warden (Wave 3, observe/flag posture):
|
|
26
|
+
* read violation books, inspect the live envelope config, and parse type-95
|
|
27
|
+
* violation events. The warden never corrects or kicks — client prediction
|
|
28
|
+
* stays exactly as it is; games decide what flags mean (scoreboard shame,
|
|
29
|
+
* moderation reports, tournament DQs).
|
|
30
|
+
*
|
|
31
|
+
* Client-prediction guidance: keep your movement client-authoritative and
|
|
32
|
+
* SMOOTH — the envelopes are generous (speed tolerance + one-sample jitter
|
|
33
|
+
* forgiveness) so honest clients never flag; teleports (fast travel,
|
|
34
|
+
* respawns) should be paired with game-known context you can correlate
|
|
35
|
+
* against the violation log.
|
|
36
|
+
*
|
|
37
|
+
* Obtained via `client.kit(appId).movement`.
|
|
38
|
+
*/
|
|
39
|
+
export declare class MovementKit {
|
|
40
|
+
private readonly appId;
|
|
41
|
+
private readonly gameModel;
|
|
42
|
+
private readonly engines;
|
|
43
|
+
private readonly moduleName;
|
|
44
|
+
private readonly configTypeName;
|
|
45
|
+
constructor(appId: Scalars['BigInt']['input'], gameModel: GameModelAPI, engines: EngineDetector, options?: MovementKitOptions);
|
|
46
|
+
/** Is the warden deployed + enabled (cached per session)? */
|
|
47
|
+
engineAvailable(): Promise<boolean>;
|
|
48
|
+
/** A user's violation book (your own without an argument). */
|
|
49
|
+
violations(userId?: string): Promise<KitViolations>;
|
|
50
|
+
/** The live envelope configuration (posture is always `'observe'` v1). */
|
|
51
|
+
config(): Promise<Record<string, unknown>>;
|
|
52
|
+
/** Warden totals (watched actors, flagged users, samples). */
|
|
53
|
+
status(): Promise<Record<string, unknown>>;
|
|
54
|
+
/** STUDIO (admin) — create/adjust the WardenConfig container. */
|
|
55
|
+
defineConfig(input: {
|
|
56
|
+
displayName?: string;
|
|
57
|
+
chunk?: [number, number, number];
|
|
58
|
+
radiusXz?: number;
|
|
59
|
+
maxSpeed?: number;
|
|
60
|
+
maxTeleport?: number;
|
|
61
|
+
bounds?: [number, number, number, number];
|
|
62
|
+
tolerancePct?: number;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
__typename?: "GmContainer";
|
|
65
|
+
containerId: string;
|
|
66
|
+
appId: string;
|
|
67
|
+
sessionId: string | null;
|
|
68
|
+
typeName: string;
|
|
69
|
+
displayName: string;
|
|
70
|
+
description: string | null;
|
|
71
|
+
ownerUserId: string | null;
|
|
72
|
+
metadataJson: string;
|
|
73
|
+
}>;
|
|
74
|
+
/** Parse a type-95 movement-violation server event. */
|
|
75
|
+
parseViolation(bytes: Uint8Array): MovementViolationEvent | null;
|
|
76
|
+
private invoke;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=movement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"movement.d.ts","sourceRoot":"","sources":["../../src/kit/movement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAEhF,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wDAAwD;AACxD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC5D;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAGrB,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EACjC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,cAAc,EACxC,OAAO,GAAE,kBAAuB;IAMlC,6DAA6D;IAC7D,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAInC,8DAA8D;IACxD,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAiBzD,0EAA0E;IACpE,MAAM;IAIZ,8DAA8D;IACxD,MAAM;IAIZ,iEAAiE;IAC3D,YAAY,CAAC,KAAK,EAAE;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB;;;;;;;;;;;IAsCD,uDAAuD;IACvD,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,sBAAsB,GAAG,IAAI;YAIlD,MAAM;CAOrB"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { parseMovementViolation } from './wire.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runtime helpers for the movement-warden (Wave 3, observe/flag posture):
|
|
4
|
+
* read violation books, inspect the live envelope config, and parse type-95
|
|
5
|
+
* violation events. The warden never corrects or kicks — client prediction
|
|
6
|
+
* stays exactly as it is; games decide what flags mean (scoreboard shame,
|
|
7
|
+
* moderation reports, tournament DQs).
|
|
8
|
+
*
|
|
9
|
+
* Client-prediction guidance: keep your movement client-authoritative and
|
|
10
|
+
* SMOOTH — the envelopes are generous (speed tolerance + one-sample jitter
|
|
11
|
+
* forgiveness) so honest clients never flag; teleports (fast travel,
|
|
12
|
+
* respawns) should be paired with game-known context you can correlate
|
|
13
|
+
* against the violation log.
|
|
14
|
+
*
|
|
15
|
+
* Obtained via `client.kit(appId).movement`.
|
|
16
|
+
*/
|
|
17
|
+
export class MovementKit {
|
|
18
|
+
constructor(appId, gameModel, engines, options = {}) {
|
|
19
|
+
this.appId = appId;
|
|
20
|
+
this.gameModel = gameModel;
|
|
21
|
+
this.engines = engines;
|
|
22
|
+
this.moduleName = options.moduleName ?? 'movement-warden';
|
|
23
|
+
this.configTypeName = options.configTypeName ?? 'WardenConfig';
|
|
24
|
+
}
|
|
25
|
+
/** Is the warden deployed + enabled (cached per session)? */
|
|
26
|
+
engineAvailable() {
|
|
27
|
+
return this.engines.has(this.moduleName);
|
|
28
|
+
}
|
|
29
|
+
/** A user's violation book (your own without an argument). */
|
|
30
|
+
async violations(userId) {
|
|
31
|
+
const body = await this.invoke('violations', userId ? { userId } : {});
|
|
32
|
+
return {
|
|
33
|
+
userId: String(body.userId ?? ''),
|
|
34
|
+
speed: Number(body.speed ?? 0),
|
|
35
|
+
teleport: Number(body.teleport ?? 0),
|
|
36
|
+
bounds: Number(body.bounds ?? 0),
|
|
37
|
+
log: Array.isArray(body.log)
|
|
38
|
+
? body.log.map((entry) => ({
|
|
39
|
+
atMs: Number(entry.atMs ?? 0),
|
|
40
|
+
kind: String(entry.kind ?? ''),
|
|
41
|
+
detail: String(entry.detail ?? ''),
|
|
42
|
+
}))
|
|
43
|
+
: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The live envelope configuration (posture is always `'observe'` v1). */
|
|
47
|
+
async config() {
|
|
48
|
+
return this.invoke('config', {});
|
|
49
|
+
}
|
|
50
|
+
/** Warden totals (watched actors, flagged users, samples). */
|
|
51
|
+
async status() {
|
|
52
|
+
return this.invoke('status', {});
|
|
53
|
+
}
|
|
54
|
+
/** STUDIO (admin) — create/adjust the WardenConfig container. */
|
|
55
|
+
async defineConfig(input) {
|
|
56
|
+
const properties = [
|
|
57
|
+
...(input.chunk
|
|
58
|
+
? [
|
|
59
|
+
{ key: 'chunk_x', valueType: 'int', valueJson: String(input.chunk[0]) },
|
|
60
|
+
{ key: 'chunk_y', valueType: 'int', valueJson: String(input.chunk[1]) },
|
|
61
|
+
{ key: 'chunk_z', valueType: 'int', valueJson: String(input.chunk[2]) },
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
64
|
+
...(input.radiusXz !== undefined
|
|
65
|
+
? [{ key: 'radius_xz', valueType: 'int', valueJson: String(input.radiusXz) }]
|
|
66
|
+
: []),
|
|
67
|
+
...(input.maxSpeed !== undefined
|
|
68
|
+
? [{ key: 'max_speed', valueType: 'int', valueJson: String(input.maxSpeed) }]
|
|
69
|
+
: []),
|
|
70
|
+
...(input.maxTeleport !== undefined
|
|
71
|
+
? [{ key: 'max_teleport', valueType: 'int', valueJson: String(input.maxTeleport) }]
|
|
72
|
+
: []),
|
|
73
|
+
...(input.bounds
|
|
74
|
+
? [
|
|
75
|
+
{ key: 'min_x', valueType: 'int', valueJson: String(input.bounds[0]) },
|
|
76
|
+
{ key: 'max_x', valueType: 'int', valueJson: String(input.bounds[1]) },
|
|
77
|
+
{ key: 'min_z', valueType: 'int', valueJson: String(input.bounds[2]) },
|
|
78
|
+
{ key: 'max_z', valueType: 'int', valueJson: String(input.bounds[3]) },
|
|
79
|
+
]
|
|
80
|
+
: []),
|
|
81
|
+
...(input.tolerancePct !== undefined
|
|
82
|
+
? [{ key: 'tolerance_pct', valueType: 'int', valueJson: String(input.tolerancePct) }]
|
|
83
|
+
: []),
|
|
84
|
+
];
|
|
85
|
+
return this.gameModel.createContainer({
|
|
86
|
+
appId: this.appId,
|
|
87
|
+
typeName: this.configTypeName,
|
|
88
|
+
displayName: input.displayName ?? 'warden-config',
|
|
89
|
+
properties,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Parse a type-95 movement-violation server event. */
|
|
93
|
+
parseViolation(bytes) {
|
|
94
|
+
return parseMovementViolation(bytes);
|
|
95
|
+
}
|
|
96
|
+
async invoke(exportName, params) {
|
|
97
|
+
const result = await this.engines.invoke(this.moduleName, exportName, params);
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
throw new Error(`movement.${exportName} failed: ${result.reason ?? 'unknown'}`);
|
|
100
|
+
}
|
|
101
|
+
return result.body;
|
|
102
|
+
}
|
|
103
|
+
}
|