skillprint-js-sdk 1.1.0-beta.7 โ 1.1.0-beta.8
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/README.md +49 -16
- package/dist/index.cjs +154 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +118 -23
- package/dist/index.d.ts +118 -23
- package/dist/index.global.js +150 -39
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +150 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ A professional JavaScript & TypeScript SDK for integrating Skillprint's real-tim
|
|
|
14
14
|
- ๐ธ **Automated Screenshot Capture**: Asynchronous, throttled gameplay screenshot captures for Canvas and WebGL contexts without dropping game frame rates.
|
|
15
15
|
- ๐ **Real-Time Dynamic Parameters**: Receive, convert, clamp, and apply AI-driven parameter modifications seamlessly during active sessions.
|
|
16
16
|
- ๐ฎ **Built-In Engine Adapters**: Ready-to-use adapter wrappers for Phaser.js, Three.js, PixiJS, and Generic Canvas.
|
|
17
|
-
- ๐ **Discrete Telemetry Events**:
|
|
17
|
+
- ๐ **Discrete Telemetry Events**: `logEvent()` for universal and game-specific gameplay events, sent with the screenshots and timestamped from session start. See [Discrete Telemetry Events](#discrete-telemetry-events).
|
|
18
18
|
- ๐ **WebGL & URL Extraction**: Automatic query parameter extraction for web deployments (`mood`, `playerId`).
|
|
19
19
|
- ๐งช **100% Test Coverage Ready**: Fully tested with Vitest and jsdom.
|
|
20
20
|
|
|
@@ -191,35 +191,68 @@ manager.startGameSessionWithOverrides(
|
|
|
191
191
|
|
|
192
192
|
## Discrete Telemetry Events
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Screenshots are the SDK's main telemetry and cover every game automatically. `logEvent()` adds discrete events: what the player did and when. Events and screenshots are both stamped with the **milliseconds since the session started**, so Skillprint can line each event up with the frames around it.
|
|
195
195
|
|
|
196
196
|
```typescript
|
|
197
197
|
import { GameEvent } from 'skillprint-js-sdk';
|
|
198
198
|
|
|
199
|
-
manager.logEvent(GameEvent.LEVEL_START);
|
|
199
|
+
manager.logEvent(GameEvent.LEVEL_START, { level: 3 });
|
|
200
|
+
manager.logEvent('ROTATE_CLOCKWISE');
|
|
200
201
|
manager.logEvent(GameEvent.LEVEL_COMPLETE, { level: 3, score: 1200 });
|
|
201
202
|
```
|
|
202
203
|
|
|
203
|
-
|
|
204
|
+
Events are queued and sent with the next screenshot upload, or on their own when there is no screenshot to send. `stopGameSession()` sends the rest with the closing upload, so they arrive before the session closes. `logEvent` never throws, and with no active session it only logs a warning.
|
|
204
205
|
|
|
205
|
-
###
|
|
206
|
+
### Universal and game-specific events
|
|
206
207
|
|
|
207
|
-
**
|
|
208
|
+
**Universal events** (`GameEvent`) mean the same thing in every game. Send the ones that apply to yours:
|
|
208
209
|
|
|
209
|
-
| Event |
|
|
210
|
-
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
210
|
+
| Event | When | Scoring signal |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `GAME_START`, `GAME_END` | The game begins or ends | |
|
|
213
|
+
| `GAME_PAUSE`, `GAME_RESUME` | Play is paused or resumed | |
|
|
214
|
+
| `LEVEL_START`, `LEVEL_COMPLETE` | A level begins or is completed | Positive |
|
|
215
|
+
| `LEVEL_QUIT`, `LEVEL_FAILED`, `LEVEL_RESTART` | A level is abandoned, lost or restarted | Negative |
|
|
216
|
+
| `MATCH`, `UNMATCH` | A match is made or undone, in games built on matching | |
|
|
217
|
+
| `HINT` | The player asks for a hint | Negative |
|
|
218
|
+
| `GENERIC_POSITIVE`, `GENERIC_NEGATIVE` | Anything else clearly good or bad for the player | Positive / Negative |
|
|
214
219
|
|
|
215
|
-
**
|
|
220
|
+
**Game-specific events** are any other name, for actions only your game has. For Hextris, for example:
|
|
216
221
|
|
|
217
222
|
```typescript
|
|
218
|
-
manager.logEvent('
|
|
219
|
-
manager.logEvent('
|
|
223
|
+
manager.logEvent('ROTATE_CLOCKWISE');
|
|
224
|
+
manager.logEvent('ROTATE_ANTICLOCKWISE');
|
|
220
225
|
```
|
|
221
226
|
|
|
222
|
-
|
|
227
|
+
They don't need registering; send what your game already tracks, in `UPPER_SNAKE_CASE`. `isUniversalEvent(name)` tells the two kinds apart.
|
|
228
|
+
|
|
229
|
+
Each event can carry data (`{ level: 3, score: 1200 }`). `event` and `timestamp` are set by the SDK and can't be overridden.
|
|
230
|
+
|
|
231
|
+
### What is sent
|
|
232
|
+
|
|
233
|
+
Each screenshot upload can carry, beside the images:
|
|
234
|
+
- `offset_ms<n>`: when the n-th screenshot was captured, in ms from session start
|
|
235
|
+
- `events`: a JSON array of the events since the last upload (at most 1000), e.g. `[{"event": "ROTATE_CLOCKWISE", "timestamp": 1250}]`, where `timestamp` is ms from session start
|
|
236
|
+
|
|
237
|
+
### Pages that upload their own frames
|
|
238
|
+
|
|
239
|
+
A page that captures its own screenshots and calls `SkillprintAPIClient` directly (see [Host mode](#skillprintapiclient)) keeps a `SessionTimeline` itself:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import { SessionTimeline, SkillprintAPIClient } from 'skillprint-js-sdk';
|
|
243
|
+
|
|
244
|
+
const timeline = new SessionTimeline();
|
|
245
|
+
await client.startSession(sessionId, 'focus', null, 'hextris');
|
|
246
|
+
timeline.start();
|
|
247
|
+
|
|
248
|
+
const offset = timeline.offsetMs(); // when a frame is captured
|
|
249
|
+
timeline.record('ROTATE_CLOCKWISE'); // when the player acts
|
|
250
|
+
|
|
251
|
+
await client.postScreenshots(sessionId, [frame], false, {
|
|
252
|
+
offsetsMs: [offset],
|
|
253
|
+
events: timeline.takeEvents()
|
|
254
|
+
});
|
|
255
|
+
```
|
|
223
256
|
|
|
224
257
|
## API Reference
|
|
225
258
|
|
|
@@ -249,7 +282,7 @@ Custom event names don't need to be registered in advance โ send whatever your
|
|
|
249
282
|
The HTTP client `SkillprintManager` uses. Use it directly when your page already captures its own frames, or when you need session scores.
|
|
250
283
|
- `constructor(baseUrl: string, partnerApiKey?: string, logger?: SDKLogger)`
|
|
251
284
|
- `startSession(sessionId, targetMood, customPlayerId?, gameName?, gameParameters?: ParameterInfo[], options?: { deviceContext? })`
|
|
252
|
-
- `postScreenshots(sessionId, screenshots: Blob[], isLastChunk?, options?: { gameStates? })`: `gameStates[n]` (for example `{ score: 1200 }`) is sent with the n-th screenshot, and the latest `score` becomes the session's score. `inputCount` is how many player inputs the batch covers; a batch with 0 is left out of skill scores. `isLastChunk: true` scores the batch and closes the session.
|
|
285
|
+
- `postScreenshots(sessionId, screenshots: Blob[], isLastChunk?, options?: { gameStates?, inputCount?, offsetsMs?, events? })`: `offsetsMs[n]` is when the n-th screenshot was captured and `events` are the events since the last upload, both in ms from session start (see [Discrete Telemetry Events](#discrete-telemetry-events)). `gameStates[n]` (for example `{ score: 1200 }`) is sent with the n-th screenshot, and the latest `score` becomes the session's score. `inputCount` is how many player inputs the batch covers; a batch with 0 is left out of skill scores. `isLastChunk: true` scores the batch and closes the session.
|
|
253
286
|
- `getSession(sessionId): Promise<SessionResult>`: the session's `state` (`OPEN`, then `CLOSED` once scored), `skillScores`, `moodScores`, `score`, `telemetry` (adjustments and logged events) and `parameterUpdates`
|
|
254
287
|
- `pollParameterResults(sessionId)`: only the parameter updates
|
|
255
288
|
- `getUserProfile()`: the player's skill profile; needs a player token
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
GameEvent: () => GameEvent,
|
|
25
25
|
GenericCanvasSkillprintAdapter: () => GenericCanvasSkillprintAdapter,
|
|
26
26
|
LogLevel: () => LogLevel,
|
|
27
|
+
MAX_EVENTS_PER_UPLOAD: () => MAX_EVENTS_PER_UPLOAD,
|
|
27
28
|
Mood: () => Mood,
|
|
28
29
|
ParameterDefinition: () => ParameterDefinition,
|
|
29
30
|
ParameterInfo: () => ParameterInfo,
|
|
@@ -33,6 +34,7 @@ __export(index_exports, {
|
|
|
33
34
|
PixiSkillprintAdapter: () => PixiSkillprintAdapter,
|
|
34
35
|
PollResultsResponse: () => PollResultsResponse,
|
|
35
36
|
ScreenshotUtility: () => ScreenshotUtility,
|
|
37
|
+
SessionTimeline: () => SessionTimeline,
|
|
36
38
|
SkillprintAPIClient: () => SkillprintAPIClient,
|
|
37
39
|
SkillprintApiError: () => SkillprintApiError,
|
|
38
40
|
SkillprintConfig: () => SkillprintConfig,
|
|
@@ -41,7 +43,8 @@ __export(index_exports, {
|
|
|
41
43
|
StartSessionRequest: () => StartSessionRequest,
|
|
42
44
|
TelemetryEventRequest: () => TelemetryEventRequest,
|
|
43
45
|
ThreeSkillprintAdapter: () => ThreeSkillprintAdapter,
|
|
44
|
-
WebGLUrlParameterExtractor: () => WebGLUrlParameterExtractor
|
|
46
|
+
WebGLUrlParameterExtractor: () => WebGLUrlParameterExtractor,
|
|
47
|
+
isUniversalEvent: () => isUniversalEvent
|
|
45
48
|
});
|
|
46
49
|
module.exports = __toCommonJS(index_exports);
|
|
47
50
|
|
|
@@ -76,16 +79,25 @@ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
|
|
76
79
|
return LogLevel2;
|
|
77
80
|
})(LogLevel || {});
|
|
78
81
|
var GameEvent = /* @__PURE__ */ ((GameEvent4) => {
|
|
82
|
+
GameEvent4["GAME_START"] = "GAME_START";
|
|
83
|
+
GameEvent4["GAME_END"] = "GAME_END";
|
|
84
|
+
GameEvent4["GAME_PAUSE"] = "GAME_PAUSE";
|
|
85
|
+
GameEvent4["GAME_RESUME"] = "GAME_RESUME";
|
|
79
86
|
GameEvent4["LEVEL_START"] = "LEVEL_START";
|
|
80
87
|
GameEvent4["LEVEL_COMPLETE"] = "LEVEL_COMPLETE";
|
|
81
88
|
GameEvent4["LEVEL_QUIT"] = "LEVEL_QUIT";
|
|
82
89
|
GameEvent4["LEVEL_FAILED"] = "LEVEL_FAILED";
|
|
83
90
|
GameEvent4["LEVEL_RESTART"] = "LEVEL_RESTART";
|
|
91
|
+
GameEvent4["MATCH"] = "MATCH";
|
|
92
|
+
GameEvent4["UNMATCH"] = "UNMATCH";
|
|
84
93
|
GameEvent4["HINT"] = "HINT";
|
|
85
94
|
GameEvent4["GENERIC_POSITIVE"] = "GENERIC_POSITIVE";
|
|
86
95
|
GameEvent4["GENERIC_NEGATIVE"] = "GENERIC_NEGATIVE";
|
|
87
96
|
return GameEvent4;
|
|
88
97
|
})(GameEvent || {});
|
|
98
|
+
function isUniversalEvent(name) {
|
|
99
|
+
return Object.values(GameEvent).includes(name);
|
|
100
|
+
}
|
|
89
101
|
|
|
90
102
|
// src/config.ts
|
|
91
103
|
var SkillprintConfig = class {
|
|
@@ -473,8 +485,9 @@ var SkillprintAPIClient = class {
|
|
|
473
485
|
const endpoint = this.UPLOAD_SCREENSHOTS_ENDPOINT.replace("{sessionId}", sessionId);
|
|
474
486
|
const url = `${this.baseUrl}${endpoint}`;
|
|
475
487
|
this.logger?.(`Posting ${screenshots.length} screenshots (isLastChunk: ${isLastChunk}): POST ${url}`, "info" /* INFO */);
|
|
476
|
-
|
|
477
|
-
|
|
488
|
+
const events = options.events || [];
|
|
489
|
+
if (screenshots.length === 0 && !isLastChunk && events.length === 0) {
|
|
490
|
+
const errorMsg = "No screenshots or events provided, and 'is_last_chunk' is false. Nothing to upload.";
|
|
478
491
|
this.logger?.(errorMsg, "warning" /* WARNING */);
|
|
479
492
|
throw new Error(errorMsg);
|
|
480
493
|
}
|
|
@@ -498,6 +511,14 @@ var SkillprintAPIClient = class {
|
|
|
498
511
|
if (typeof options.inputCount === "number") {
|
|
499
512
|
formData.append("input_count", String(options.inputCount));
|
|
500
513
|
}
|
|
514
|
+
(options.offsetsMs || []).forEach((offset, i) => {
|
|
515
|
+
if (typeof offset === "number" && offset >= 0) {
|
|
516
|
+
formData.append(`offset_ms${i}`, String(Math.round(offset)));
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
if (events.length > 0) {
|
|
520
|
+
formData.append("events", JSON.stringify(events));
|
|
521
|
+
}
|
|
501
522
|
try {
|
|
502
523
|
const response = await fetch(url, {
|
|
503
524
|
method: "POST",
|
|
@@ -819,6 +840,71 @@ var ScreenshotUtility = class {
|
|
|
819
840
|
}
|
|
820
841
|
};
|
|
821
842
|
|
|
843
|
+
// src/session-timeline.ts
|
|
844
|
+
var MAX_EVENTS_PER_UPLOAD = 1e3;
|
|
845
|
+
var defaultNow = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
846
|
+
var SessionTimeline = class {
|
|
847
|
+
/**
|
|
848
|
+
* @param now Clock in milliseconds; defaults to `performance.now()`
|
|
849
|
+
* @param maxQueued Events kept while waiting for an upload; later ones are dropped
|
|
850
|
+
*/
|
|
851
|
+
constructor(now = defaultNow, maxQueued = 5e3) {
|
|
852
|
+
this.startedAt = null;
|
|
853
|
+
this.queue = [];
|
|
854
|
+
this.now = now;
|
|
855
|
+
this.maxQueued = maxQueued;
|
|
856
|
+
}
|
|
857
|
+
/** Starts the clock at 0 and empties the queue. */
|
|
858
|
+
start() {
|
|
859
|
+
this.startedAt = this.now();
|
|
860
|
+
this.queue = [];
|
|
861
|
+
}
|
|
862
|
+
/** Stops the clock and empties the queue. */
|
|
863
|
+
reset() {
|
|
864
|
+
this.startedAt = null;
|
|
865
|
+
this.queue = [];
|
|
866
|
+
}
|
|
867
|
+
get isStarted() {
|
|
868
|
+
return this.startedAt !== null;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Whole milliseconds from `start()` to `at` (default: now); 0 before
|
|
872
|
+
* the start, or for a time earlier than it.
|
|
873
|
+
*/
|
|
874
|
+
offsetMs(at) {
|
|
875
|
+
if (this.startedAt === null) return 0;
|
|
876
|
+
const when = typeof at === "number" && Number.isFinite(at) ? at : this.now();
|
|
877
|
+
return Math.max(0, Math.round(when - this.startedAt));
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Stamps an event with the current offset and queues it.
|
|
881
|
+
* Returns the queued event, or null when the queue is full.
|
|
882
|
+
*
|
|
883
|
+
* `event` is a `GameEvent` for the universal vocabulary, or the game's
|
|
884
|
+
* own name for a game-specific event (e.g. 'ROTATE_CLOCKWISE'). `data`
|
|
885
|
+
* is merged in; it can't override `event` or `timestamp`. `at` is when
|
|
886
|
+
* it happened, on this timeline's clock (default: now).
|
|
887
|
+
*/
|
|
888
|
+
record(event, data = {}, at) {
|
|
889
|
+
if (this.queue.length >= this.maxQueued) return null;
|
|
890
|
+
const entry = { ...data, event, timestamp: this.offsetMs(at) };
|
|
891
|
+
this.queue.push(entry);
|
|
892
|
+
return entry;
|
|
893
|
+
}
|
|
894
|
+
get pendingEvents() {
|
|
895
|
+
return this.queue.length;
|
|
896
|
+
}
|
|
897
|
+
/** Removes and returns the oldest queued events, at most `max` of them. */
|
|
898
|
+
takeEvents(max = MAX_EVENTS_PER_UPLOAD) {
|
|
899
|
+
return this.queue.splice(0, max);
|
|
900
|
+
}
|
|
901
|
+
/** Puts events back at the front of the queue, e.g. after a failed upload. */
|
|
902
|
+
requeue(events) {
|
|
903
|
+
const room = Math.max(0, this.maxQueued - this.queue.length);
|
|
904
|
+
this.queue.unshift(...events.slice(0, room));
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
|
|
822
908
|
// src/url-parameter-extractor.ts
|
|
823
909
|
var WebGLUrlParameterExtractor = class {
|
|
824
910
|
/**
|
|
@@ -941,6 +1027,8 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
941
1027
|
this.apiClient = null;
|
|
942
1028
|
this.screenshotUtility = null;
|
|
943
1029
|
this.screenshotQueue = [];
|
|
1030
|
+
/** The session's clock and its queue of events waiting for an upload. */
|
|
1031
|
+
this.timeline = new SessionTimeline();
|
|
944
1032
|
this.registeredParameters = /* @__PURE__ */ new Map();
|
|
945
1033
|
// Timers
|
|
946
1034
|
this.screenshotCaptureTimer = null;
|
|
@@ -960,6 +1048,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
960
1048
|
this.apiClient = null;
|
|
961
1049
|
this.screenshotUtility = null;
|
|
962
1050
|
this.screenshotQueue = [];
|
|
1051
|
+
this.timeline = new SessionTimeline();
|
|
963
1052
|
this.registeredParameters = /* @__PURE__ */ new Map();
|
|
964
1053
|
this.initializeSDK();
|
|
965
1054
|
_SkillprintManager.instance = this;
|
|
@@ -1061,6 +1150,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1061
1150
|
}
|
|
1062
1151
|
this.currentSessionId = this.generateSessionId();
|
|
1063
1152
|
this.isSessionActive = true;
|
|
1153
|
+
this.timeline.start();
|
|
1064
1154
|
this.log(`Starting game session for ${this.config.targetEnvironment} environment.`, "info" /* INFO */);
|
|
1065
1155
|
const parameterInfos = Array.from(this.registeredParameters.values()).map(
|
|
1066
1156
|
(p) => new ParameterInfo(
|
|
@@ -1089,6 +1179,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1089
1179
|
this.log(`Failed to start Skillprint session: ${message}`, "error" /* ERROR */);
|
|
1090
1180
|
this.isSessionActive = false;
|
|
1091
1181
|
this.currentSessionId = null;
|
|
1182
|
+
this.timeline.reset();
|
|
1092
1183
|
}
|
|
1093
1184
|
}
|
|
1094
1185
|
/**
|
|
@@ -1109,6 +1200,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1109
1200
|
}
|
|
1110
1201
|
const sessionId = this.currentSessionId;
|
|
1111
1202
|
const remaining = this.screenshotQueue;
|
|
1203
|
+
const events = this.timeline.takeEvents(Number.MAX_SAFE_INTEGER);
|
|
1112
1204
|
const inFlightPost = this.inFlightPost;
|
|
1113
1205
|
const apiClient = this.apiClient;
|
|
1114
1206
|
this.log(`Stopping Skillprint session: ${sessionId}`);
|
|
@@ -1122,6 +1214,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1122
1214
|
this.screenshotQueue = [];
|
|
1123
1215
|
this.currentSessionId = null;
|
|
1124
1216
|
this.inFlightPost = null;
|
|
1217
|
+
this.timeline.reset();
|
|
1125
1218
|
if (!sessionId || !apiClient) {
|
|
1126
1219
|
this.log("Skillprint session stopped.");
|
|
1127
1220
|
return;
|
|
@@ -1129,41 +1222,44 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1129
1222
|
if (inFlightPost) await inFlightPost;
|
|
1130
1223
|
const batchSize = this.postBatchSize();
|
|
1131
1224
|
try {
|
|
1225
|
+
while (events.length > MAX_EVENTS_PER_UPLOAD) {
|
|
1226
|
+
await apiClient.postScreenshots(sessionId, [], false, { events: events.splice(0, MAX_EVENTS_PER_UPLOAD) });
|
|
1227
|
+
}
|
|
1132
1228
|
if (remaining.length === 0) {
|
|
1133
|
-
await apiClient.postScreenshots(sessionId, [], true);
|
|
1229
|
+
await apiClient.postScreenshots(sessionId, [], true, { events });
|
|
1134
1230
|
} else {
|
|
1135
1231
|
for (let i = 0; i < remaining.length; i += batchSize) {
|
|
1136
1232
|
const batch = remaining.slice(i, i + batchSize);
|
|
1137
1233
|
const isLastChunk = i + batchSize >= remaining.length;
|
|
1138
|
-
await apiClient.postScreenshots(sessionId, batch, isLastChunk
|
|
1234
|
+
await apiClient.postScreenshots(sessionId, batch.map((s) => s.blob), isLastChunk, {
|
|
1235
|
+
offsetsMs: batch.map((s) => s.offsetMs),
|
|
1236
|
+
events: isLastChunk ? events : []
|
|
1237
|
+
});
|
|
1139
1238
|
}
|
|
1140
1239
|
}
|
|
1141
|
-
this.log(`Skillprint session stopped. Flushed ${remaining.length} queued screenshots.`);
|
|
1240
|
+
this.log(`Skillprint session stopped. Flushed ${remaining.length} queued screenshots and ${events.length} events.`);
|
|
1142
1241
|
} catch (error) {
|
|
1143
1242
|
const message = error instanceof Error ? error.message : String(error);
|
|
1144
1243
|
this.log(`Failed to flush screenshots on session stop: ${message}`, "error" /* ERROR */);
|
|
1145
1244
|
}
|
|
1146
1245
|
}
|
|
1147
1246
|
/**
|
|
1148
|
-
* Logs a discrete gameplay event for the active session
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1247
|
+
* Logs a discrete gameplay event for the active session: a universal
|
|
1248
|
+
* `GameEvent`, or the game's own name for a game-specific event (e.g.
|
|
1249
|
+
* 'ROTATE_CLOCKWISE'). It is stamped with the milliseconds since the
|
|
1250
|
+
* session started and sent with the next screenshot upload (or on its own
|
|
1251
|
+
* when there is no screenshot to send), and the rest go with the closing
|
|
1252
|
+
* upload on stopGameSession().
|
|
1253
|
+
*
|
|
1254
|
+
* Never throws; with no active session it is ignored with a warning.
|
|
1152
1255
|
*/
|
|
1153
1256
|
async logEvent(event, data = {}) {
|
|
1154
1257
|
if (!this.isSessionActive || !this.currentSessionId || !this.apiClient) {
|
|
1155
1258
|
this.log("logEvent called with no active session. Ignoring.", "warning" /* WARNING */);
|
|
1156
1259
|
return;
|
|
1157
1260
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
this.currentSessionId,
|
|
1161
|
-
this.config.gameName,
|
|
1162
|
-
new TelemetryEventRequest(event, data)
|
|
1163
|
-
);
|
|
1164
|
-
} catch (error) {
|
|
1165
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1166
|
-
this.log(`Failed to log event '${event}': ${message}`, "warning" /* WARNING */);
|
|
1261
|
+
if (!this.timeline.record(event, data)) {
|
|
1262
|
+
this.log(`Event queue full. Dropping '${event}'.`, "warning" /* WARNING */);
|
|
1167
1263
|
}
|
|
1168
1264
|
}
|
|
1169
1265
|
startScreenshotCaptureLoop() {
|
|
@@ -1175,11 +1271,12 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1175
1271
|
return;
|
|
1176
1272
|
}
|
|
1177
1273
|
const sessionId = this.currentSessionId;
|
|
1274
|
+
const offsetMs = this.timeline.offsetMs();
|
|
1178
1275
|
const screenshot = await this.screenshotUtility.captureScreenshot(canvas);
|
|
1179
1276
|
if (!this.isSessionActive || this.currentSessionId !== sessionId) return;
|
|
1180
1277
|
if (screenshot) {
|
|
1181
1278
|
if (this.screenshotQueue.length < 50) {
|
|
1182
|
-
this.screenshotQueue.push(screenshot);
|
|
1279
|
+
this.screenshotQueue.push({ blob: screenshot, offsetMs });
|
|
1183
1280
|
this.log(`Screenshot captured. Queue size: ${this.screenshotQueue.length}`);
|
|
1184
1281
|
} else {
|
|
1185
1282
|
this.log("Screenshot queue full. Discarding new screenshot.", "warning" /* WARNING */);
|
|
@@ -1189,27 +1286,41 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1189
1286
|
}
|
|
1190
1287
|
startScreenshotPostLoop() {
|
|
1191
1288
|
this.screenshotPostTimer = setInterval(async () => {
|
|
1192
|
-
if (!this.isSessionActive ||
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1289
|
+
if (!this.isSessionActive || !this.apiClient || !this.currentSessionId) return;
|
|
1290
|
+
if (this.screenshotQueue.length === 0 && this.timeline.pendingEvents === 0) return;
|
|
1291
|
+
const batch = this.screenshotQueue.splice(0, Math.min(this.screenshotQueue.length, this.postBatchSize()));
|
|
1292
|
+
const events = this.timeline.takeEvents();
|
|
1293
|
+
const apiClient = this.apiClient;
|
|
1294
|
+
const sessionId = this.currentSessionId;
|
|
1295
|
+
this.log(`Posting ${batch.length} screenshots and ${events.length} events...`);
|
|
1296
|
+
const post = (async () => {
|
|
1297
|
+
try {
|
|
1298
|
+
const result = await apiClient.postScreenshots(sessionId, batch.map((s) => s.blob), false, {
|
|
1299
|
+
offsetsMs: batch.map((s) => s.offsetMs),
|
|
1300
|
+
events
|
|
1301
|
+
});
|
|
1302
|
+
this.log(`Successfully posted ${batch.length} screenshots and ${events.length} events. Response: ${result.data}`);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1305
|
+
this.log(`Failed to post screenshots: ${message}`, "error" /* ERROR */);
|
|
1306
|
+
this.requeueEvents(events, error, sessionId);
|
|
1307
|
+
}
|
|
1308
|
+
})();
|
|
1309
|
+
this.inFlightPost = post;
|
|
1310
|
+
await post;
|
|
1311
|
+
if (this.inFlightPost === post) this.inFlightPost = null;
|
|
1211
1312
|
}, this.config.screenshotPostIntervalSeconds * 1e3);
|
|
1212
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Keeps events from an upload that may succeed if tried again (a network
|
|
1316
|
+
* error or a 5xx), so the next upload carries them. A 4xx would fail the
|
|
1317
|
+
* same way again, so those are dropped.
|
|
1318
|
+
*/
|
|
1319
|
+
requeueEvents(events, error, sessionId) {
|
|
1320
|
+
if (events.length === 0 || !this.isSessionActive || this.currentSessionId !== sessionId) return;
|
|
1321
|
+
if (error instanceof SkillprintApiError && error.status < 500) return;
|
|
1322
|
+
this.timeline.requeue(events);
|
|
1323
|
+
}
|
|
1213
1324
|
postBatchSize() {
|
|
1214
1325
|
return Math.max(
|
|
1215
1326
|
1,
|
|
@@ -1456,6 +1567,7 @@ var GenericCanvasSkillprintAdapter = class _GenericCanvasSkillprintAdapter {
|
|
|
1456
1567
|
GameEvent,
|
|
1457
1568
|
GenericCanvasSkillprintAdapter,
|
|
1458
1569
|
LogLevel,
|
|
1570
|
+
MAX_EVENTS_PER_UPLOAD,
|
|
1459
1571
|
Mood,
|
|
1460
1572
|
ParameterDefinition,
|
|
1461
1573
|
ParameterInfo,
|
|
@@ -1465,6 +1577,7 @@ var GenericCanvasSkillprintAdapter = class _GenericCanvasSkillprintAdapter {
|
|
|
1465
1577
|
PixiSkillprintAdapter,
|
|
1466
1578
|
PollResultsResponse,
|
|
1467
1579
|
ScreenshotUtility,
|
|
1580
|
+
SessionTimeline,
|
|
1468
1581
|
SkillprintAPIClient,
|
|
1469
1582
|
SkillprintApiError,
|
|
1470
1583
|
SkillprintConfig,
|
|
@@ -1473,6 +1586,7 @@ var GenericCanvasSkillprintAdapter = class _GenericCanvasSkillprintAdapter {
|
|
|
1473
1586
|
StartSessionRequest,
|
|
1474
1587
|
TelemetryEventRequest,
|
|
1475
1588
|
ThreeSkillprintAdapter,
|
|
1476
|
-
WebGLUrlParameterExtractor
|
|
1589
|
+
WebGLUrlParameterExtractor,
|
|
1590
|
+
isUniversalEvent
|
|
1477
1591
|
});
|
|
1478
1592
|
//# sourceMappingURL=index.cjs.map
|