skillprint-js-sdk 1.1.0-beta.7 โ 1.1.0-beta.9
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 +202 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +141 -23
- package/dist/index.d.ts +141 -23
- package/dist/index.global.js +198 -39
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +198 -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 each screenshot upload carries the ones logged up to its last screenshot. Later events wait for the upload with the screenshots after them, and `stopGameSession()` sends the rest with the closing upload. The SDK decides which events go with which upload; the server keeps each upload's events with its screenshots and does no time arithmetic. If no screenshots come for a long while, events only go on their own once more than one upload's worth (1000) has piled up. `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 logged up to the upload's last screenshot (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.eventsForUpload(offset) // null for the closing upload
|
|
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 (options.events !== void 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,106 @@ 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.recordedAny = false;
|
|
855
|
+
this.now = now;
|
|
856
|
+
this.maxQueued = maxQueued;
|
|
857
|
+
}
|
|
858
|
+
/** Starts the clock at 0 and empties the queue. */
|
|
859
|
+
start() {
|
|
860
|
+
this.startedAt = this.now();
|
|
861
|
+
this.queue = [];
|
|
862
|
+
this.recordedAny = false;
|
|
863
|
+
}
|
|
864
|
+
/** Stops the clock and empties the queue. */
|
|
865
|
+
reset() {
|
|
866
|
+
this.startedAt = null;
|
|
867
|
+
this.queue = [];
|
|
868
|
+
this.recordedAny = false;
|
|
869
|
+
}
|
|
870
|
+
get isStarted() {
|
|
871
|
+
return this.startedAt !== null;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Whole milliseconds from `start()` to `at` (default: now); 0 before
|
|
875
|
+
* the start, or for a time earlier than it.
|
|
876
|
+
*/
|
|
877
|
+
offsetMs(at) {
|
|
878
|
+
if (this.startedAt === null) return 0;
|
|
879
|
+
const when = typeof at === "number" && Number.isFinite(at) ? at : this.now();
|
|
880
|
+
return Math.max(0, Math.round(when - this.startedAt));
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Stamps an event with the current offset and queues it.
|
|
884
|
+
* Returns the queued event, or null when the queue is full.
|
|
885
|
+
*
|
|
886
|
+
* `event` is a `GameEvent` for the universal vocabulary, or the game's
|
|
887
|
+
* own name for a game-specific event (e.g. 'ROTATE_CLOCKWISE'). `data`
|
|
888
|
+
* is merged in; it can't override `event` or `timestamp`. `at` is when
|
|
889
|
+
* it happened, on this timeline's clock (default: now).
|
|
890
|
+
*/
|
|
891
|
+
record(event, data = {}, at) {
|
|
892
|
+
if (this.queue.length >= this.maxQueued) return null;
|
|
893
|
+
const entry = { ...data, event, timestamp: this.offsetMs(at) };
|
|
894
|
+
this.queue.push(entry);
|
|
895
|
+
this.recordedAny = true;
|
|
896
|
+
return entry;
|
|
897
|
+
}
|
|
898
|
+
get pendingEvents() {
|
|
899
|
+
return this.queue.length;
|
|
900
|
+
}
|
|
901
|
+
/** True once this session has recorded any event. */
|
|
902
|
+
get hasRecorded() {
|
|
903
|
+
return this.recordedAny;
|
|
904
|
+
}
|
|
905
|
+
/** Removes and returns the earliest queued events, at most `max`, in session-time order. */
|
|
906
|
+
takeEvents(max = MAX_EVENTS_PER_UPLOAD) {
|
|
907
|
+
this.sortQueue();
|
|
908
|
+
return this.queue.splice(0, max);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Removes and returns the queued events logged up to `offsetMs`
|
|
912
|
+
* (inclusive), at most `max`, in session-time order. Later ones stay
|
|
913
|
+
* queued for the upload that carries the screenshots after them.
|
|
914
|
+
*/
|
|
915
|
+
takeEventsThrough(offsetMs, max = MAX_EVENTS_PER_UPLOAD) {
|
|
916
|
+
this.sortQueue();
|
|
917
|
+
let n = 0;
|
|
918
|
+
while (n < this.queue.length && n < max && this.queue[n].timestamp <= offsetMs) n++;
|
|
919
|
+
return this.queue.splice(0, n);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* The `events` for an upload whose last screenshot was taken at
|
|
923
|
+
* `lastFrameOffsetMs`: the events logged up to then, or every queued
|
|
924
|
+
* event for the closing upload (pass null). An empty list once the
|
|
925
|
+
* session has recorded any event, so the server can tell a quiet stretch
|
|
926
|
+
* from a game that doesn't log events; undefined before that.
|
|
927
|
+
*/
|
|
928
|
+
eventsForUpload(lastFrameOffsetMs) {
|
|
929
|
+
if (!this.recordedAny) return void 0;
|
|
930
|
+
return lastFrameOffsetMs === null ? this.takeEvents() : this.takeEventsThrough(lastFrameOffsetMs);
|
|
931
|
+
}
|
|
932
|
+
// Events stamped with `at` can arrive out of order; uploads send them in order.
|
|
933
|
+
sortQueue() {
|
|
934
|
+
this.queue.sort((a, b) => a.timestamp - b.timestamp);
|
|
935
|
+
}
|
|
936
|
+
/** Puts events back at the front of the queue, e.g. after a failed upload. */
|
|
937
|
+
requeue(events) {
|
|
938
|
+
const room = Math.max(0, this.maxQueued - this.queue.length);
|
|
939
|
+
this.queue.unshift(...events.slice(0, room));
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
822
943
|
// src/url-parameter-extractor.ts
|
|
823
944
|
var WebGLUrlParameterExtractor = class {
|
|
824
945
|
/**
|
|
@@ -941,6 +1062,8 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
941
1062
|
this.apiClient = null;
|
|
942
1063
|
this.screenshotUtility = null;
|
|
943
1064
|
this.screenshotQueue = [];
|
|
1065
|
+
/** The session's clock and its queue of events waiting for an upload. */
|
|
1066
|
+
this.timeline = new SessionTimeline();
|
|
944
1067
|
this.registeredParameters = /* @__PURE__ */ new Map();
|
|
945
1068
|
// Timers
|
|
946
1069
|
this.screenshotCaptureTimer = null;
|
|
@@ -960,6 +1083,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
960
1083
|
this.apiClient = null;
|
|
961
1084
|
this.screenshotUtility = null;
|
|
962
1085
|
this.screenshotQueue = [];
|
|
1086
|
+
this.timeline = new SessionTimeline();
|
|
963
1087
|
this.registeredParameters = /* @__PURE__ */ new Map();
|
|
964
1088
|
this.initializeSDK();
|
|
965
1089
|
_SkillprintManager.instance = this;
|
|
@@ -1061,6 +1185,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1061
1185
|
}
|
|
1062
1186
|
this.currentSessionId = this.generateSessionId();
|
|
1063
1187
|
this.isSessionActive = true;
|
|
1188
|
+
this.timeline.start();
|
|
1064
1189
|
this.log(`Starting game session for ${this.config.targetEnvironment} environment.`, "info" /* INFO */);
|
|
1065
1190
|
const parameterInfos = Array.from(this.registeredParameters.values()).map(
|
|
1066
1191
|
(p) => new ParameterInfo(
|
|
@@ -1089,6 +1214,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1089
1214
|
this.log(`Failed to start Skillprint session: ${message}`, "error" /* ERROR */);
|
|
1090
1215
|
this.isSessionActive = false;
|
|
1091
1216
|
this.currentSessionId = null;
|
|
1217
|
+
this.timeline.reset();
|
|
1092
1218
|
}
|
|
1093
1219
|
}
|
|
1094
1220
|
/**
|
|
@@ -1109,6 +1235,8 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1109
1235
|
}
|
|
1110
1236
|
const sessionId = this.currentSessionId;
|
|
1111
1237
|
const remaining = this.screenshotQueue;
|
|
1238
|
+
const recorded = this.timeline.hasRecorded;
|
|
1239
|
+
const events = this.timeline.takeEvents(Number.MAX_SAFE_INTEGER);
|
|
1112
1240
|
const inFlightPost = this.inFlightPost;
|
|
1113
1241
|
const apiClient = this.apiClient;
|
|
1114
1242
|
this.log(`Stopping Skillprint session: ${sessionId}`);
|
|
@@ -1122,6 +1250,7 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1122
1250
|
this.screenshotQueue = [];
|
|
1123
1251
|
this.currentSessionId = null;
|
|
1124
1252
|
this.inFlightPost = null;
|
|
1253
|
+
this.timeline.reset();
|
|
1125
1254
|
if (!sessionId || !apiClient) {
|
|
1126
1255
|
this.log("Skillprint session stopped.");
|
|
1127
1256
|
return;
|
|
@@ -1129,41 +1258,56 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1129
1258
|
if (inFlightPost) await inFlightPost;
|
|
1130
1259
|
const batchSize = this.postBatchSize();
|
|
1131
1260
|
try {
|
|
1261
|
+
const eventsUpTo = (lastFrameOffsetMs) => {
|
|
1262
|
+
if (!recorded) return void 0;
|
|
1263
|
+
const n = lastFrameOffsetMs === null ? events.length : events.findIndex((e) => e.timestamp > lastFrameOffsetMs);
|
|
1264
|
+
return events.splice(0, Math.min(n === -1 ? events.length : n, MAX_EVENTS_PER_UPLOAD));
|
|
1265
|
+
};
|
|
1132
1266
|
if (remaining.length === 0) {
|
|
1133
|
-
|
|
1267
|
+
while (events.length > MAX_EVENTS_PER_UPLOAD) {
|
|
1268
|
+
await apiClient.postScreenshots(sessionId, [], false, { events: events.splice(0, MAX_EVENTS_PER_UPLOAD) });
|
|
1269
|
+
}
|
|
1270
|
+
await apiClient.postScreenshots(sessionId, [], true, { events: eventsUpTo(null) });
|
|
1134
1271
|
} else {
|
|
1135
1272
|
for (let i = 0; i < remaining.length; i += batchSize) {
|
|
1136
1273
|
const batch = remaining.slice(i, i + batchSize);
|
|
1137
1274
|
const isLastChunk = i + batchSize >= remaining.length;
|
|
1138
|
-
|
|
1275
|
+
if (isLastChunk) {
|
|
1276
|
+
while (events.length > MAX_EVENTS_PER_UPLOAD) {
|
|
1277
|
+
await apiClient.postScreenshots(sessionId, [], false, { events: events.splice(0, MAX_EVENTS_PER_UPLOAD) });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
const lastOffset = Math.max(...batch.map((s) => s.offsetMs));
|
|
1281
|
+
await apiClient.postScreenshots(sessionId, batch.map((s) => s.blob), isLastChunk, {
|
|
1282
|
+
offsetsMs: batch.map((s) => s.offsetMs),
|
|
1283
|
+
events: eventsUpTo(isLastChunk ? null : lastOffset)
|
|
1284
|
+
});
|
|
1139
1285
|
}
|
|
1140
1286
|
}
|
|
1141
|
-
|
|
1287
|
+
const flushed = remaining.length;
|
|
1288
|
+
this.log(`Skillprint session stopped. Flushed ${flushed} queued screenshots.`);
|
|
1142
1289
|
} catch (error) {
|
|
1143
1290
|
const message = error instanceof Error ? error.message : String(error);
|
|
1144
1291
|
this.log(`Failed to flush screenshots on session stop: ${message}`, "error" /* ERROR */);
|
|
1145
1292
|
}
|
|
1146
1293
|
}
|
|
1147
1294
|
/**
|
|
1148
|
-
* Logs a discrete gameplay event for the active session
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1295
|
+
* Logs a discrete gameplay event for the active session: a universal
|
|
1296
|
+
* `GameEvent`, or the game's own name for a game-specific event (e.g.
|
|
1297
|
+
* 'ROTATE_CLOCKWISE'). It is stamped with the milliseconds since the
|
|
1298
|
+
* session started and sent with the next screenshot upload (or on its own
|
|
1299
|
+
* when there is no screenshot to send), and the rest go with the closing
|
|
1300
|
+
* upload on stopGameSession().
|
|
1301
|
+
*
|
|
1302
|
+
* Never throws; with no active session it is ignored with a warning.
|
|
1152
1303
|
*/
|
|
1153
1304
|
async logEvent(event, data = {}) {
|
|
1154
1305
|
if (!this.isSessionActive || !this.currentSessionId || !this.apiClient) {
|
|
1155
1306
|
this.log("logEvent called with no active session. Ignoring.", "warning" /* WARNING */);
|
|
1156
1307
|
return;
|
|
1157
1308
|
}
|
|
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 */);
|
|
1309
|
+
if (!this.timeline.record(event, data)) {
|
|
1310
|
+
this.log(`Event queue full. Dropping '${event}'.`, "warning" /* WARNING */);
|
|
1167
1311
|
}
|
|
1168
1312
|
}
|
|
1169
1313
|
startScreenshotCaptureLoop() {
|
|
@@ -1175,11 +1319,12 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1175
1319
|
return;
|
|
1176
1320
|
}
|
|
1177
1321
|
const sessionId = this.currentSessionId;
|
|
1322
|
+
const offsetMs = this.timeline.offsetMs();
|
|
1178
1323
|
const screenshot = await this.screenshotUtility.captureScreenshot(canvas);
|
|
1179
1324
|
if (!this.isSessionActive || this.currentSessionId !== sessionId) return;
|
|
1180
1325
|
if (screenshot) {
|
|
1181
1326
|
if (this.screenshotQueue.length < 50) {
|
|
1182
|
-
this.screenshotQueue.push(screenshot);
|
|
1327
|
+
this.screenshotQueue.push({ blob: screenshot, offsetMs });
|
|
1183
1328
|
this.log(`Screenshot captured. Queue size: ${this.screenshotQueue.length}`);
|
|
1184
1329
|
} else {
|
|
1185
1330
|
this.log("Screenshot queue full. Discarding new screenshot.", "warning" /* WARNING */);
|
|
@@ -1189,27 +1334,41 @@ var _SkillprintManager = class _SkillprintManager {
|
|
|
1189
1334
|
}
|
|
1190
1335
|
startScreenshotPostLoop() {
|
|
1191
1336
|
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
|
-
|
|
1337
|
+
if (!this.isSessionActive || !this.apiClient || !this.currentSessionId) return;
|
|
1338
|
+
if (this.screenshotQueue.length === 0 && this.timeline.pendingEvents <= MAX_EVENTS_PER_UPLOAD) return;
|
|
1339
|
+
const batch = this.screenshotQueue.splice(0, Math.min(this.screenshotQueue.length, this.postBatchSize()));
|
|
1340
|
+
const events = batch.length > 0 ? this.timeline.eventsForUpload(Math.max(...batch.map((s) => s.offsetMs))) : this.timeline.takeEvents();
|
|
1341
|
+
const apiClient = this.apiClient;
|
|
1342
|
+
const sessionId = this.currentSessionId;
|
|
1343
|
+
this.log(`Posting ${batch.length} screenshots and ${events?.length ?? 0} events...`);
|
|
1344
|
+
const post = (async () => {
|
|
1345
|
+
try {
|
|
1346
|
+
const result = await apiClient.postScreenshots(sessionId, batch.map((s) => s.blob), false, {
|
|
1347
|
+
offsetsMs: batch.map((s) => s.offsetMs),
|
|
1348
|
+
events
|
|
1349
|
+
});
|
|
1350
|
+
this.log(`Successfully posted ${batch.length} screenshots and ${events?.length ?? 0} events. Response: ${result.data}`);
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1353
|
+
this.log(`Failed to post screenshots: ${message}`, "error" /* ERROR */);
|
|
1354
|
+
this.requeueEvents(events ?? [], error, sessionId);
|
|
1355
|
+
}
|
|
1356
|
+
})();
|
|
1357
|
+
this.inFlightPost = post;
|
|
1358
|
+
await post;
|
|
1359
|
+
if (this.inFlightPost === post) this.inFlightPost = null;
|
|
1211
1360
|
}, this.config.screenshotPostIntervalSeconds * 1e3);
|
|
1212
1361
|
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Keeps events from an upload that may succeed if tried again (a network
|
|
1364
|
+
* error or a 5xx), so the next upload carries them. A 4xx would fail the
|
|
1365
|
+
* same way again, so those are dropped.
|
|
1366
|
+
*/
|
|
1367
|
+
requeueEvents(events, error, sessionId) {
|
|
1368
|
+
if (events.length === 0 || !this.isSessionActive || this.currentSessionId !== sessionId) return;
|
|
1369
|
+
if (error instanceof SkillprintApiError && error.status < 500) return;
|
|
1370
|
+
this.timeline.requeue(events);
|
|
1371
|
+
}
|
|
1213
1372
|
postBatchSize() {
|
|
1214
1373
|
return Math.max(
|
|
1215
1374
|
1,
|
|
@@ -1456,6 +1615,7 @@ var GenericCanvasSkillprintAdapter = class _GenericCanvasSkillprintAdapter {
|
|
|
1456
1615
|
GameEvent,
|
|
1457
1616
|
GenericCanvasSkillprintAdapter,
|
|
1458
1617
|
LogLevel,
|
|
1618
|
+
MAX_EVENTS_PER_UPLOAD,
|
|
1459
1619
|
Mood,
|
|
1460
1620
|
ParameterDefinition,
|
|
1461
1621
|
ParameterInfo,
|
|
@@ -1465,6 +1625,7 @@ var GenericCanvasSkillprintAdapter = class _GenericCanvasSkillprintAdapter {
|
|
|
1465
1625
|
PixiSkillprintAdapter,
|
|
1466
1626
|
PollResultsResponse,
|
|
1467
1627
|
ScreenshotUtility,
|
|
1628
|
+
SessionTimeline,
|
|
1468
1629
|
SkillprintAPIClient,
|
|
1469
1630
|
SkillprintApiError,
|
|
1470
1631
|
SkillprintConfig,
|
|
@@ -1473,6 +1634,7 @@ var GenericCanvasSkillprintAdapter = class _GenericCanvasSkillprintAdapter {
|
|
|
1473
1634
|
StartSessionRequest,
|
|
1474
1635
|
TelemetryEventRequest,
|
|
1475
1636
|
ThreeSkillprintAdapter,
|
|
1476
|
-
WebGLUrlParameterExtractor
|
|
1637
|
+
WebGLUrlParameterExtractor,
|
|
1638
|
+
isUniversalEvent
|
|
1477
1639
|
});
|
|
1478
1640
|
//# sourceMappingURL=index.cjs.map
|