@powly/sdk 0.4.0 → 0.6.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 +22 -0
- package/README.md +17 -2
- package/dist/powly.d.ts +38 -5
- package/dist/powly.js +67 -7
- package/dist/powly.mjs +67 -7
- package/package.json +10 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fiioonnn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ invites, multiplayer rooms, Twitch chat).
|
|
|
10
10
|
npm install @powly/sdk
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Starting a new game? `npm create powly
|
|
13
|
+
Starting a new game? `npm create powly@latest my-game` sets up a React + Vite project with
|
|
14
14
|
the SDK.
|
|
15
15
|
|
|
16
16
|
## Use
|
|
@@ -31,7 +31,7 @@ await powly.leaderboards.submit("high-score", 1200);
|
|
|
31
31
|
Without a bundler, load the classic script instead; it defines `window.Powly`:
|
|
32
32
|
|
|
33
33
|
```html
|
|
34
|
-
<script src="https://cdn.jsdelivr.net/npm/@powly/sdk@0.
|
|
34
|
+
<script src="https://cdn.jsdelivr.net/npm/@powly/sdk@0.6.0/dist/powly.js"></script>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## Testing
|
|
@@ -54,3 +54,18 @@ await Powly.init({ powlyOrigin: "https://games.example.com" });
|
|
|
54
54
|
The game runs in the player's browser, so nothing here is a security boundary. Saves and
|
|
55
55
|
leaderboard scores are written by your backend unless the studio allows browser writes for them;
|
|
56
56
|
your backend verifies session tokens or exchanges them for an access token.
|
|
57
|
+
|
|
58
|
+
## This repository
|
|
59
|
+
|
|
60
|
+
The SDK is developed in Powly's main repository and mirrored here with each release; this is
|
|
61
|
+
`@powly/sdk` 0.6.0. Issues are welcome; changes land through the main repository.
|
|
62
|
+
|
|
63
|
+
To build it yourself (Node.js 24 or newer):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run build
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`dist/powly.mjs`, `dist/powly.js` and `dist/powly.d.ts` are what npm ships. `examples/` has
|
|
71
|
+
Starfall, a small example game, and the SDK playground (every SDK function behind a button).
|
package/dist/powly.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
/* Powly SDK 0.
|
|
1
|
+
/* Powly SDK 0.6.0: types for powly.js and powly.mjs */
|
|
2
2
|
/** This SDK's version; Powly's Studio shows it next to the downloads. */
|
|
3
|
-
export declare const SDK_VERSION = "0.
|
|
3
|
+
export declare const SDK_VERSION = "0.6.0";
|
|
4
4
|
|
|
5
5
|
export declare const PROTOCOL = "powly-sdk";
|
|
6
6
|
export declare const PROTOCOL_VERSION = 1;
|
|
7
|
+
/** The corners Powly's options button (its mascot, which opens the player's options) can be in. */
|
|
8
|
+
export declare const OPTIONS_BUTTON_CORNERS: readonly ["top-left", "top-right", "bottom-left", "bottom-right"];
|
|
9
|
+
export type OptionsButtonCorner = (typeof OPTIONS_BUTTON_CORNERS)[number];
|
|
10
|
+
export declare const isOptionsButtonCorner: (value: unknown) => value is OptionsButtonCorner;
|
|
7
11
|
export type InitMessage = {
|
|
8
12
|
protocol: typeof PROTOCOL;
|
|
9
13
|
version: number;
|
|
@@ -14,6 +18,11 @@ export type InitMessage = {
|
|
|
14
18
|
* SDKs leave it out.
|
|
15
19
|
*/
|
|
16
20
|
pac?: 1;
|
|
21
|
+
/**
|
|
22
|
+
* Where the game wants Powly's options button to start (`Powly.init({ optionsButton })`); the
|
|
23
|
+
* player's own choice wins. Left out: Powly's default (top left).
|
|
24
|
+
*/
|
|
25
|
+
optionsButton?: OptionsButtonCorner;
|
|
17
26
|
};
|
|
18
27
|
export type PlayerProfile = {
|
|
19
28
|
/** This player's ID in this game: stable, and different in every game. */
|
|
@@ -45,7 +54,7 @@ export type FailedMessage = {
|
|
|
45
54
|
type: "failed";
|
|
46
55
|
error: string;
|
|
47
56
|
};
|
|
48
|
-
export type HostMethod = "getSessionToken" | "purchases.open" | "fullscreen.toggle" | "invites.open";
|
|
57
|
+
export type HostMethod = "getSessionToken" | "purchases.open" | "fullscreen.toggle" | "invites.open" | "quit";
|
|
49
58
|
export type RequestMessage = {
|
|
50
59
|
type: "request";
|
|
51
60
|
id: number;
|
|
@@ -73,7 +82,11 @@ export type ResponseMessage = {
|
|
|
73
82
|
ok: false;
|
|
74
83
|
error: string;
|
|
75
84
|
};
|
|
76
|
-
|
|
85
|
+
/**
|
|
86
|
+
* "shown": Powly's loading screen is gone and the game is on screen (once per launch). "pause" /
|
|
87
|
+
* "resume": Powly UI covers the game or the tab is hidden, and back. "ended": the launch ended.
|
|
88
|
+
*/
|
|
89
|
+
export type HostEventName = "shown" | "pause" | "resume" | "ended";
|
|
77
90
|
export type EventMessage = {
|
|
78
91
|
type: "event";
|
|
79
92
|
event: HostEventName;
|
|
@@ -113,6 +126,8 @@ export type PacSample = {
|
|
|
113
126
|
keyIntervalSd: number | null;
|
|
114
127
|
clickIntervals: number;
|
|
115
128
|
clickIntervalSd: number | null;
|
|
129
|
+
/** Every trusted interaction: key presses, clicks, taps, pointer movement, wheel, gamepads. */
|
|
130
|
+
activity: number;
|
|
116
131
|
};
|
|
117
132
|
};
|
|
118
133
|
export declare const isPacSample: (data: unknown) => data is PacSample;
|
|
@@ -515,6 +530,12 @@ export type InitOptions = {
|
|
|
515
530
|
powlyOrigin?: string | string[];
|
|
516
531
|
/** How long to wait for Powly to answer (ms). Default 10 s. */
|
|
517
532
|
timeoutMs?: number;
|
|
533
|
+
/**
|
|
534
|
+
* Which corner Powly's options button (its mascot, which opens the player's options) starts in:
|
|
535
|
+
* "top-left" (the default), "top-right", "bottom-left" or "bottom-right". Pick the one your own
|
|
536
|
+
* UI leaves free. Players can still move it, and their choice wins.
|
|
537
|
+
*/
|
|
538
|
+
optionsButton?: OptionsButtonCorner;
|
|
518
539
|
};
|
|
519
540
|
export type LeaderboardEntry = {
|
|
520
541
|
rank: number;
|
|
@@ -647,8 +668,20 @@ export type PowlySession = {
|
|
|
647
668
|
fullscreen: {
|
|
648
669
|
toggle(): Promise<void>;
|
|
649
670
|
};
|
|
650
|
-
/**
|
|
671
|
+
/**
|
|
672
|
+
* Closes the game, like the exit button in Powly's options bar: the player goes back to the
|
|
673
|
+
* game's store page and the launch ends ("ended" fires). Save the player's progress first.
|
|
674
|
+
*/
|
|
675
|
+
quit(): Promise<void>;
|
|
676
|
+
/**
|
|
677
|
+
* Powly's loading screen is gone and the game is on screen ("shown": start the intro or music
|
|
678
|
+
* here), Powly UI opened over the game ("pause"), closed ("resume"), or the launch ended
|
|
679
|
+
* ("ended"). "shown" comes once per launch; listening for it later calls the listener right
|
|
680
|
+
* away.
|
|
681
|
+
*/
|
|
651
682
|
on(event: HostEventName, listener: () => void): () => void;
|
|
683
|
+
/** Whether Powly's loading screen is gone and the game is on screen ("shown" came). */
|
|
684
|
+
readonly shown: boolean;
|
|
652
685
|
};
|
|
653
686
|
/** Starts the SDK. Rejects when the game wasn't launched from Powly. */
|
|
654
687
|
export declare function init(options?: InitOptions): Promise<PowlySession>;
|
package/dist/powly.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/* Powly SDK 0.
|
|
1
|
+
/* Powly SDK 0.6.0: defines window.Powly */
|
|
2
2
|
(function () {
|
|
3
3
|
"use strict";
|
|
4
4
|
/** This SDK's version; Powly's Studio shows it next to the downloads. */
|
|
5
|
-
const SDK_VERSION = "0.
|
|
5
|
+
const SDK_VERSION = "0.6.0";
|
|
6
6
|
|
|
7
7
|
// The messages between a game (this SDK, inside Powly's iframe) and the Powly page that framed it.
|
|
8
8
|
// Shared with the host side (apps/web/components/player/sdk-host.ts).
|
|
@@ -14,6 +14,14 @@ const SDK_VERSION = "0.4.0";
|
|
|
14
14
|
// 3. From then on the game also reports to Powly Anti-Cheat over the port (`PacSample`, src/pac.ts).
|
|
15
15
|
const PROTOCOL = "powly-sdk";
|
|
16
16
|
const PROTOCOL_VERSION = 1;
|
|
17
|
+
/** The corners Powly's options button (its mascot, which opens the player's options) can be in. */
|
|
18
|
+
const OPTIONS_BUTTON_CORNERS = [
|
|
19
|
+
"top-left",
|
|
20
|
+
"top-right",
|
|
21
|
+
"bottom-left",
|
|
22
|
+
"bottom-right",
|
|
23
|
+
];
|
|
24
|
+
const isOptionsButtonCorner = (value) => OPTIONS_BUTTON_CORNERS.includes(value);
|
|
17
25
|
const isInitMessage = (data) => typeof data === "object" &&
|
|
18
26
|
data !== null &&
|
|
19
27
|
data.protocol === PROTOCOL &&
|
|
@@ -23,7 +31,7 @@ const isRequestMessage = (data) => typeof data === "object" &&
|
|
|
23
31
|
data !== null &&
|
|
24
32
|
data.type === "request" &&
|
|
25
33
|
Number.isInteger(data.id) &&
|
|
26
|
-
["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open"].includes(data.method);
|
|
34
|
+
["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open", "quit"].includes(data.method);
|
|
27
35
|
|
|
28
36
|
// The Powly origins this build of the SDK trusts by default: a game only handshakes with a parent
|
|
29
37
|
// page on one of them (unless it passes `powlyOrigin` to `Powly.init()` itself). Set at build
|
|
@@ -49,8 +57,9 @@ class PowlyError extends Error {
|
|
|
49
57
|
// the server's to catch speed hacks), browser functions that were replaced (speed hack scripts and
|
|
50
58
|
// extensions patch timers), traces of automated browsers, and how input arrives (scripted events,
|
|
51
59
|
// clicks without pointer movement as AI agents make them, machine-regular timing as macros and
|
|
52
|
-
// auto-clickers make them)
|
|
53
|
-
//
|
|
60
|
+
// auto-clickers make them), and whether the player plays at all (any trusted input, gamepads
|
|
61
|
+
// included: Powly closes games nobody plays). Nothing is sent anywhere else, and nothing here
|
|
62
|
+
// reads what the player types: only event counts and timings.
|
|
54
63
|
//
|
|
55
64
|
// In real players' runs (not developer mode) the SDK also guards the game's code against being
|
|
56
65
|
// copied (`guard`): the browser's context menu and its view-source, save-page and developer-tools
|
|
@@ -62,6 +71,8 @@ class PowlyError extends Error {
|
|
|
62
71
|
// `tamperedFunctions` are shared with the Powly page (apps/web/components/player/pac-sensor.ts).
|
|
63
72
|
/** How often the SDK reports (ms). */
|
|
64
73
|
const PAC_SAMPLE_MS = 5000;
|
|
74
|
+
/** A gamepad axis past this counts as input (sticks rest a little off centre). */
|
|
75
|
+
const GAMEPAD_DEADZONE = 0.3;
|
|
65
76
|
const isPacSample = (data) => typeof data === "object" &&
|
|
66
77
|
data !== null &&
|
|
67
78
|
data.type === "pac" &&
|
|
@@ -210,6 +221,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
210
221
|
synthetic: 0,
|
|
211
222
|
clicks: 0,
|
|
212
223
|
teleportClicks: 0,
|
|
224
|
+
activity: 0,
|
|
213
225
|
};
|
|
214
226
|
const keyTimes = [];
|
|
215
227
|
const clickTimes = [];
|
|
@@ -227,6 +239,9 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
227
239
|
input.synthetic += 1;
|
|
228
240
|
return;
|
|
229
241
|
}
|
|
242
|
+
input.activity += 1;
|
|
243
|
+
if (event.type === "wheel")
|
|
244
|
+
return;
|
|
230
245
|
if (event.type === "pointermove" || event.type === "pointerover") {
|
|
231
246
|
const move = event;
|
|
232
247
|
pointer = { x: move.clientX, y: move.clientY };
|
|
@@ -252,7 +267,26 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
252
267
|
}
|
|
253
268
|
pointer = { x: down.clientX, y: down.clientY };
|
|
254
269
|
};
|
|
255
|
-
const types = ["keydown", "pointerdown", "pointermove", "pointerover"];
|
|
270
|
+
const types = ["keydown", "pointerdown", "pointermove", "pointerover", "wheel"];
|
|
271
|
+
// Gamepads send no events: a pressed button or a pushed stick, looked at a few times a second.
|
|
272
|
+
const watchGamepads = () => {
|
|
273
|
+
let pads;
|
|
274
|
+
try {
|
|
275
|
+
pads = [...(win.navigator.getGamepads?.() ?? [])];
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
for (const pad of pads) {
|
|
281
|
+
if (!pad?.connected)
|
|
282
|
+
continue;
|
|
283
|
+
if (pad.buttons.some((button) => button.pressed) ||
|
|
284
|
+
pad.axes.some((axis) => Math.abs(axis) > GAMEPAD_DEADZONE)) {
|
|
285
|
+
input.activity += 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
256
290
|
for (const type of types) {
|
|
257
291
|
win.addEventListener(type, onInput, { capture: true, passive: true });
|
|
258
292
|
}
|
|
@@ -320,6 +354,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
320
354
|
input.synthetic = 0;
|
|
321
355
|
input.clicks = 0;
|
|
322
356
|
input.teleportClicks = 0;
|
|
357
|
+
input.activity = 0;
|
|
323
358
|
dateJump = 0;
|
|
324
359
|
devtools = false;
|
|
325
360
|
};
|
|
@@ -328,11 +363,13 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
328
363
|
const clearInterval = win.clearInterval.bind(win);
|
|
329
364
|
const dateTimer = setInterval(watchDate, 1000);
|
|
330
365
|
const reportTimer = setInterval(report, PAC_SAMPLE_MS);
|
|
366
|
+
const gamepadTimer = setInterval(watchGamepads, 250);
|
|
331
367
|
const trapTimer = trap ? setInterval(springTrap, 1500) : undefined;
|
|
332
368
|
report();
|
|
333
369
|
return () => {
|
|
334
370
|
clearInterval(dateTimer);
|
|
335
371
|
clearInterval(reportTimer);
|
|
372
|
+
clearInterval(gamepadTimer);
|
|
336
373
|
if (trapTimer !== undefined)
|
|
337
374
|
clearInterval(trapTimer);
|
|
338
375
|
for (const type of types)
|
|
@@ -1174,6 +1211,9 @@ function init(options = {}) {
|
|
|
1174
1211
|
type: "init",
|
|
1175
1212
|
sdkVersion: SDK_VERSION,
|
|
1176
1213
|
pac: 1,
|
|
1214
|
+
...(isOptionsButtonCorner(options.optionsButton) && {
|
|
1215
|
+
optionsButton: options.optionsButton,
|
|
1216
|
+
}),
|
|
1177
1217
|
};
|
|
1178
1218
|
channel.port1.onmessage = (event) => {
|
|
1179
1219
|
const data = event.data;
|
|
@@ -1207,6 +1247,8 @@ function createSession(port, ready) {
|
|
|
1207
1247
|
let nextId = 1;
|
|
1208
1248
|
const pending = new Map();
|
|
1209
1249
|
const listeners = new Map();
|
|
1250
|
+
// "shown" comes once; games that listen for it later still hear it.
|
|
1251
|
+
let shown = false;
|
|
1210
1252
|
port.onmessage = (event) => {
|
|
1211
1253
|
const data = event.data;
|
|
1212
1254
|
if (data?.type === "response") {
|
|
@@ -1220,7 +1262,13 @@ function createSession(port, ready) {
|
|
|
1220
1262
|
waiting.reject(new PowlyError(data.error));
|
|
1221
1263
|
}
|
|
1222
1264
|
else if (data?.type === "event") {
|
|
1223
|
-
|
|
1265
|
+
const name = data.event;
|
|
1266
|
+
if (name === "shown") {
|
|
1267
|
+
if (shown)
|
|
1268
|
+
return;
|
|
1269
|
+
shown = true;
|
|
1270
|
+
}
|
|
1271
|
+
for (const listener of listeners.get(name) ?? [])
|
|
1224
1272
|
listener();
|
|
1225
1273
|
}
|
|
1226
1274
|
};
|
|
@@ -1362,12 +1410,24 @@ function createSession(port, ready) {
|
|
|
1362
1410
|
await call("fullscreen.toggle");
|
|
1363
1411
|
},
|
|
1364
1412
|
},
|
|
1413
|
+
quit: async () => {
|
|
1414
|
+
await call("quit");
|
|
1415
|
+
},
|
|
1365
1416
|
on(event, listener) {
|
|
1366
1417
|
const set = listeners.get(event) ?? new Set();
|
|
1367
1418
|
set.add(listener);
|
|
1368
1419
|
listeners.set(event, set);
|
|
1420
|
+
if (event === "shown" && shown) {
|
|
1421
|
+
queueMicrotask(() => {
|
|
1422
|
+
if (set.has(listener))
|
|
1423
|
+
listener();
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1369
1426
|
return () => set.delete(listener);
|
|
1370
1427
|
},
|
|
1428
|
+
get shown() {
|
|
1429
|
+
return shown;
|
|
1430
|
+
},
|
|
1371
1431
|
};
|
|
1372
1432
|
}
|
|
1373
1433
|
const Powly = { init, SDK_VERSION, PowlyError };
|
package/dist/powly.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/* Powly SDK 0.
|
|
1
|
+
/* Powly SDK 0.6.0: ES module */
|
|
2
2
|
/** This SDK's version; Powly's Studio shows it next to the downloads. */
|
|
3
|
-
const SDK_VERSION = "0.
|
|
3
|
+
const SDK_VERSION = "0.6.0";
|
|
4
4
|
|
|
5
5
|
// The messages between a game (this SDK, inside Powly's iframe) and the Powly page that framed it.
|
|
6
6
|
// Shared with the host side (apps/web/components/player/sdk-host.ts).
|
|
@@ -12,6 +12,14 @@ const SDK_VERSION = "0.4.0";
|
|
|
12
12
|
// 3. From then on the game also reports to Powly Anti-Cheat over the port (`PacSample`, src/pac.ts).
|
|
13
13
|
const PROTOCOL = "powly-sdk";
|
|
14
14
|
const PROTOCOL_VERSION = 1;
|
|
15
|
+
/** The corners Powly's options button (its mascot, which opens the player's options) can be in. */
|
|
16
|
+
const OPTIONS_BUTTON_CORNERS = [
|
|
17
|
+
"top-left",
|
|
18
|
+
"top-right",
|
|
19
|
+
"bottom-left",
|
|
20
|
+
"bottom-right",
|
|
21
|
+
];
|
|
22
|
+
const isOptionsButtonCorner = (value) => OPTIONS_BUTTON_CORNERS.includes(value);
|
|
15
23
|
const isInitMessage = (data) => typeof data === "object" &&
|
|
16
24
|
data !== null &&
|
|
17
25
|
data.protocol === PROTOCOL &&
|
|
@@ -21,7 +29,7 @@ const isRequestMessage = (data) => typeof data === "object" &&
|
|
|
21
29
|
data !== null &&
|
|
22
30
|
data.type === "request" &&
|
|
23
31
|
Number.isInteger(data.id) &&
|
|
24
|
-
["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open"].includes(data.method);
|
|
32
|
+
["getSessionToken", "purchases.open", "fullscreen.toggle", "invites.open", "quit"].includes(data.method);
|
|
25
33
|
|
|
26
34
|
// The Powly origins this build of the SDK trusts by default: a game only handshakes with a parent
|
|
27
35
|
// page on one of them (unless it passes `powlyOrigin` to `Powly.init()` itself). Set at build
|
|
@@ -47,8 +55,9 @@ class PowlyError extends Error {
|
|
|
47
55
|
// the server's to catch speed hacks), browser functions that were replaced (speed hack scripts and
|
|
48
56
|
// extensions patch timers), traces of automated browsers, and how input arrives (scripted events,
|
|
49
57
|
// clicks without pointer movement as AI agents make them, machine-regular timing as macros and
|
|
50
|
-
// auto-clickers make them)
|
|
51
|
-
//
|
|
58
|
+
// auto-clickers make them), and whether the player plays at all (any trusted input, gamepads
|
|
59
|
+
// included: Powly closes games nobody plays). Nothing is sent anywhere else, and nothing here
|
|
60
|
+
// reads what the player types: only event counts and timings.
|
|
52
61
|
//
|
|
53
62
|
// In real players' runs (not developer mode) the SDK also guards the game's code against being
|
|
54
63
|
// copied (`guard`): the browser's context menu and its view-source, save-page and developer-tools
|
|
@@ -60,6 +69,8 @@ class PowlyError extends Error {
|
|
|
60
69
|
// `tamperedFunctions` are shared with the Powly page (apps/web/components/player/pac-sensor.ts).
|
|
61
70
|
/** How often the SDK reports (ms). */
|
|
62
71
|
const PAC_SAMPLE_MS = 5000;
|
|
72
|
+
/** A gamepad axis past this counts as input (sticks rest a little off centre). */
|
|
73
|
+
const GAMEPAD_DEADZONE = 0.3;
|
|
63
74
|
const isPacSample = (data) => typeof data === "object" &&
|
|
64
75
|
data !== null &&
|
|
65
76
|
data.type === "pac" &&
|
|
@@ -208,6 +219,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
208
219
|
synthetic: 0,
|
|
209
220
|
clicks: 0,
|
|
210
221
|
teleportClicks: 0,
|
|
222
|
+
activity: 0,
|
|
211
223
|
};
|
|
212
224
|
const keyTimes = [];
|
|
213
225
|
const clickTimes = [];
|
|
@@ -225,6 +237,9 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
225
237
|
input.synthetic += 1;
|
|
226
238
|
return;
|
|
227
239
|
}
|
|
240
|
+
input.activity += 1;
|
|
241
|
+
if (event.type === "wheel")
|
|
242
|
+
return;
|
|
228
243
|
if (event.type === "pointermove" || event.type === "pointerover") {
|
|
229
244
|
const move = event;
|
|
230
245
|
pointer = { x: move.clientX, y: move.clientY };
|
|
@@ -250,7 +265,26 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
250
265
|
}
|
|
251
266
|
pointer = { x: down.clientX, y: down.clientY };
|
|
252
267
|
};
|
|
253
|
-
const types = ["keydown", "pointerdown", "pointermove", "pointerover"];
|
|
268
|
+
const types = ["keydown", "pointerdown", "pointermove", "pointerover", "wheel"];
|
|
269
|
+
// Gamepads send no events: a pressed button or a pushed stick, looked at a few times a second.
|
|
270
|
+
const watchGamepads = () => {
|
|
271
|
+
let pads;
|
|
272
|
+
try {
|
|
273
|
+
pads = [...(win.navigator.getGamepads?.() ?? [])];
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
for (const pad of pads) {
|
|
279
|
+
if (!pad?.connected)
|
|
280
|
+
continue;
|
|
281
|
+
if (pad.buttons.some((button) => button.pressed) ||
|
|
282
|
+
pad.axes.some((axis) => Math.abs(axis) > GAMEPAD_DEADZONE)) {
|
|
283
|
+
input.activity += 1;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
};
|
|
254
288
|
for (const type of types) {
|
|
255
289
|
win.addEventListener(type, onInput, { capture: true, passive: true });
|
|
256
290
|
}
|
|
@@ -318,6 +352,7 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
318
352
|
input.synthetic = 0;
|
|
319
353
|
input.clicks = 0;
|
|
320
354
|
input.teleportClicks = 0;
|
|
355
|
+
input.activity = 0;
|
|
321
356
|
dateJump = 0;
|
|
322
357
|
devtools = false;
|
|
323
358
|
};
|
|
@@ -326,11 +361,13 @@ function startPacSensor(port, { guard = false } = {}, win = window) {
|
|
|
326
361
|
const clearInterval = win.clearInterval.bind(win);
|
|
327
362
|
const dateTimer = setInterval(watchDate, 1000);
|
|
328
363
|
const reportTimer = setInterval(report, PAC_SAMPLE_MS);
|
|
364
|
+
const gamepadTimer = setInterval(watchGamepads, 250);
|
|
329
365
|
const trapTimer = trap ? setInterval(springTrap, 1500) : undefined;
|
|
330
366
|
report();
|
|
331
367
|
return () => {
|
|
332
368
|
clearInterval(dateTimer);
|
|
333
369
|
clearInterval(reportTimer);
|
|
370
|
+
clearInterval(gamepadTimer);
|
|
334
371
|
if (trapTimer !== undefined)
|
|
335
372
|
clearInterval(trapTimer);
|
|
336
373
|
for (const type of types)
|
|
@@ -1172,6 +1209,9 @@ function init(options = {}) {
|
|
|
1172
1209
|
type: "init",
|
|
1173
1210
|
sdkVersion: SDK_VERSION,
|
|
1174
1211
|
pac: 1,
|
|
1212
|
+
...(isOptionsButtonCorner(options.optionsButton) && {
|
|
1213
|
+
optionsButton: options.optionsButton,
|
|
1214
|
+
}),
|
|
1175
1215
|
};
|
|
1176
1216
|
channel.port1.onmessage = (event) => {
|
|
1177
1217
|
const data = event.data;
|
|
@@ -1205,6 +1245,8 @@ function createSession(port, ready) {
|
|
|
1205
1245
|
let nextId = 1;
|
|
1206
1246
|
const pending = new Map();
|
|
1207
1247
|
const listeners = new Map();
|
|
1248
|
+
// "shown" comes once; games that listen for it later still hear it.
|
|
1249
|
+
let shown = false;
|
|
1208
1250
|
port.onmessage = (event) => {
|
|
1209
1251
|
const data = event.data;
|
|
1210
1252
|
if (data?.type === "response") {
|
|
@@ -1218,7 +1260,13 @@ function createSession(port, ready) {
|
|
|
1218
1260
|
waiting.reject(new PowlyError(data.error));
|
|
1219
1261
|
}
|
|
1220
1262
|
else if (data?.type === "event") {
|
|
1221
|
-
|
|
1263
|
+
const name = data.event;
|
|
1264
|
+
if (name === "shown") {
|
|
1265
|
+
if (shown)
|
|
1266
|
+
return;
|
|
1267
|
+
shown = true;
|
|
1268
|
+
}
|
|
1269
|
+
for (const listener of listeners.get(name) ?? [])
|
|
1222
1270
|
listener();
|
|
1223
1271
|
}
|
|
1224
1272
|
};
|
|
@@ -1360,12 +1408,24 @@ function createSession(port, ready) {
|
|
|
1360
1408
|
await call("fullscreen.toggle");
|
|
1361
1409
|
},
|
|
1362
1410
|
},
|
|
1411
|
+
quit: async () => {
|
|
1412
|
+
await call("quit");
|
|
1413
|
+
},
|
|
1363
1414
|
on(event, listener) {
|
|
1364
1415
|
const set = listeners.get(event) ?? new Set();
|
|
1365
1416
|
set.add(listener);
|
|
1366
1417
|
listeners.set(event, set);
|
|
1418
|
+
if (event === "shown" && shown) {
|
|
1419
|
+
queueMicrotask(() => {
|
|
1420
|
+
if (set.has(listener))
|
|
1421
|
+
listener();
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1367
1424
|
return () => set.delete(listener);
|
|
1368
1425
|
},
|
|
1426
|
+
get shown() {
|
|
1427
|
+
return shown;
|
|
1428
|
+
},
|
|
1369
1429
|
};
|
|
1370
1430
|
}
|
|
1371
1431
|
const Powly = { init, SDK_VERSION, PowlyError };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@powly/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Powly game SDK: launch handshake, session tokens and platform services.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
"browser-games"
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://powly.gg",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/fiioonnn/powly-sdk.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/fiioonnn/powly-sdk/issues"
|
|
21
|
+
},
|
|
15
22
|
"exports": {
|
|
16
23
|
".": {
|
|
17
24
|
"types": "./dist/powly.d.ts",
|
|
@@ -36,6 +43,7 @@
|
|
|
36
43
|
"scripts": {
|
|
37
44
|
"build": "tsc -p tsconfig.build.json && node scripts/bundle.ts",
|
|
38
45
|
"typecheck": "tsc --noEmit",
|
|
39
|
-
"example:zip": "node scripts/example-zip.ts"
|
|
46
|
+
"example:zip": "node scripts/example-zip.ts",
|
|
47
|
+
"mirror": "node scripts/mirror.ts"
|
|
40
48
|
}
|
|
41
49
|
}
|