steamworks-ffi-node 0.3.0 → 0.4.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.
Files changed (38) hide show
  1. package/README.md +86 -10
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/SteamAPICore.d.ts +221 -8
  7. package/dist/internal/SteamAPICore.d.ts.map +1 -1
  8. package/dist/internal/SteamAPICore.js +234 -14
  9. package/dist/internal/SteamAPICore.js.map +1 -1
  10. package/dist/internal/SteamAchievementManager.d.ts +602 -31
  11. package/dist/internal/SteamAchievementManager.d.ts.map +1 -1
  12. package/dist/internal/SteamAchievementManager.js +601 -32
  13. package/dist/internal/SteamAchievementManager.js.map +1 -1
  14. package/dist/internal/SteamCallbackPoller.d.ts +68 -0
  15. package/dist/internal/SteamCallbackPoller.d.ts.map +1 -0
  16. package/dist/internal/SteamCallbackPoller.js +134 -0
  17. package/dist/internal/SteamCallbackPoller.js.map +1 -0
  18. package/dist/internal/SteamLeaderboardManager.d.ts +338 -0
  19. package/dist/internal/SteamLeaderboardManager.d.ts.map +1 -0
  20. package/dist/internal/SteamLeaderboardManager.js +734 -0
  21. package/dist/internal/SteamLeaderboardManager.js.map +1 -0
  22. package/dist/internal/SteamLibraryLoader.d.ts +15 -0
  23. package/dist/internal/SteamLibraryLoader.d.ts.map +1 -1
  24. package/dist/internal/SteamLibraryLoader.js +42 -5
  25. package/dist/internal/SteamLibraryLoader.js.map +1 -1
  26. package/dist/internal/SteamStatsManager.d.ts +357 -50
  27. package/dist/internal/SteamStatsManager.d.ts.map +1 -1
  28. package/dist/internal/SteamStatsManager.js +444 -106
  29. package/dist/internal/SteamStatsManager.js.map +1 -1
  30. package/dist/steam.d.ts +169 -9
  31. package/dist/steam.d.ts.map +1 -1
  32. package/dist/steam.js +178 -0
  33. package/dist/steam.js.map +1 -1
  34. package/dist/types.d.ts +91 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/types.js +34 -0
  37. package/dist/types.js.map +1 -1
  38. package/package.json +4 -3
@@ -0,0 +1,734 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SteamLeaderboardManager = void 0;
37
+ const koffi = __importStar(require("koffi"));
38
+ const SteamCallbackPoller_1 = require("./SteamCallbackPoller");
39
+ /**
40
+ * LeaderboardFindResult_t - Result of FindOrCreateLeaderboard/FindLeaderboard
41
+ * Callback ID: k_iSteamUserStatsCallbacks + 4 = 1104
42
+ */
43
+ const LeaderboardFindResult_t = koffi.struct('LeaderboardFindResult_t', {
44
+ m_hSteamLeaderboard: 'uint64', // Leaderboard handle (0 if not found)
45
+ m_bLeaderboardFound: 'uint8' // 1 if found, 0 otherwise
46
+ });
47
+ /**
48
+ * LeaderboardScoreUploaded_t - Result of UploadLeaderboardScore
49
+ * Callback ID: k_iSteamUserStatsCallbacks + 6 = 1106
50
+ */
51
+ const LeaderboardScoreUploaded_t = koffi.struct('LeaderboardScoreUploaded_t', {
52
+ m_bSuccess: 'uint8', // 1 if successful
53
+ m_hSteamLeaderboard: 'uint64', // Leaderboard handle
54
+ m_nScore: 'int32', // Score that was uploaded
55
+ m_bScoreChanged: 'uint8', // 1 if score changed
56
+ m_nGlobalRankNew: 'int', // New global rank
57
+ m_nGlobalRankPrevious: 'int' // Previous global rank (0 if no existing entry)
58
+ });
59
+ /**
60
+ * LeaderboardScoresDownloaded_t - Result of DownloadLeaderboardEntries
61
+ * Callback ID: k_iSteamUserStatsCallbacks + 5 = 1105
62
+ */
63
+ const LeaderboardScoresDownloaded_t = koffi.struct('LeaderboardScoresDownloaded_t', {
64
+ m_hSteamLeaderboard: 'uint64', // Leaderboard handle
65
+ m_hSteamLeaderboardEntries: 'uint64', // Handle for GetDownloadedLeaderboardEntry
66
+ m_cEntryCount: 'int' // Number of entries downloaded
67
+ });
68
+ /**
69
+ * LeaderboardUGCSet_t - Result of AttachLeaderboardUGC
70
+ * Callback ID: k_iSteamUserStatsCallbacks + 11 = 1111
71
+ */
72
+ const LeaderboardUGCSet_t = koffi.struct('LeaderboardUGCSet_t', {
73
+ m_eResult: 'int', // EResult value
74
+ m_hSteamLeaderboard: 'uint64' // Leaderboard handle
75
+ });
76
+ /**
77
+ * LeaderboardEntry_t - Individual leaderboard entry data
78
+ * Used with GetDownloadedLeaderboardEntry
79
+ */
80
+ const LeaderboardEntry_t = koffi.struct('LeaderboardEntry_t', {
81
+ m_steamIDUser: 'uint64', // Steam ID of the user
82
+ m_nGlobalRank: 'int32', // Global rank [1..N]
83
+ m_nScore: 'int32', // Score value
84
+ m_cDetails: 'int32', // Number of details available
85
+ m_hUGC: 'uint64' // UGC handle attached to entry
86
+ });
87
+ // Callback IDs (k_iSteamUserStatsCallbacks = 1100)
88
+ const k_iCallback_LeaderboardFindResult = 1104;
89
+ const k_iCallback_LeaderboardScoresDownloaded = 1105;
90
+ const k_iCallback_LeaderboardScoreUploaded = 1106;
91
+ const k_iCallback_LeaderboardUGCSet = 1111;
92
+ /**
93
+ * SteamLeaderboardManager
94
+ *
95
+ * Manages all Steam leaderboard operations including:
96
+ * - Finding and creating leaderboards
97
+ * - Uploading scores with optional details
98
+ * - Downloading leaderboard entries (global, friends, around user)
99
+ * - Retrieving leaderboard metadata
100
+ *
101
+ * Leaderboards are persistent scoreboards stored on Steam servers that allow
102
+ * players to compete globally or with friends. Each entry can include a score
103
+ * and up to 64 int32 detail values for additional game-specific data.
104
+ *
105
+ * ✅ IMPLEMENTATION NOTE:
106
+ * This implementation uses ISteamUtils polling to retrieve callback results
107
+ * synchronously after async operations complete. This provides full access to
108
+ * Steam callback data without requiring a C++ addon:
109
+ *
110
+ * 1. Initiates the async operation (returns SteamAPICall_t handle)
111
+ * 2. Polls ISteamUtils::IsAPICallCompleted() to check completion
112
+ * 3. Calls ISteamUtils::GetAPICallResult() to retrieve result struct
113
+ * 4. Returns complete callback data (handles, ranks, scores, etc.)
114
+ *
115
+ * All async operations now return actual results:
116
+ * - findOrCreateLeaderboard/findLeaderboard: Returns LeaderboardInfo with handle
117
+ * - uploadScore: Returns upload result with ranks and success status
118
+ * - downloadLeaderboardEntries: Returns array of LeaderboardEntry objects
119
+ * - attachLeaderboardUGC: Returns true/false based on actual result
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * const leaderboardManager = new SteamLeaderboardManager(libraryLoader, apiCore);
124
+ *
125
+ * // Find or create a leaderboard
126
+ * const leaderboard = await leaderboardManager.findOrCreateLeaderboard(
127
+ * 'HighScores',
128
+ * LeaderboardSortMethod.Descending,
129
+ * LeaderboardDisplayType.Numeric
130
+ * );
131
+ *
132
+ * // Upload a score and get rank information
133
+ * const result = await leaderboardManager.uploadScore(
134
+ * leaderboard.handle,
135
+ * 1000,
136
+ * LeaderboardUploadScoreMethod.KeepBest
137
+ * );
138
+ * console.log(`New rank: ${result.globalRankNew}`);
139
+ *
140
+ * // Download top 10 entries
141
+ * const entries = await leaderboardManager.downloadLeaderboardEntries(
142
+ * leaderboard.handle,
143
+ * LeaderboardDataRequest.Global,
144
+ * 1,
145
+ * 10
146
+ * );
147
+ * entries.forEach(entry => {
148
+ * console.log(`${entry.globalRank}. Score: ${entry.score}`);
149
+ * });
150
+ * ```
151
+ */
152
+ class SteamLeaderboardManager {
153
+ /**
154
+ * Creates a new SteamLeaderboardManager instance
155
+ *
156
+ * @param libraryLoader - The Steam library loader for FFI calls
157
+ * @param apiCore - The Steam API core for lifecycle management
158
+ */
159
+ constructor(libraryLoader, apiCore) {
160
+ this.libraryLoader = libraryLoader;
161
+ this.apiCore = apiCore;
162
+ this.callbackPoller = new SteamCallbackPoller_1.SteamCallbackPoller(libraryLoader, apiCore);
163
+ }
164
+ // ========================================
165
+ // Leaderboard Discovery
166
+ // ========================================
167
+ /**
168
+ * Find or create a leaderboard
169
+ *
170
+ * Searches for a leaderboard by name, and creates it if it doesn't exist.
171
+ * This is an asynchronous operation that communicates with Steam servers.
172
+ *
173
+ * @param name - Name of the leaderboard (max 128 UTF-8 bytes)
174
+ * @param sortMethod - How entries should be sorted
175
+ * @param displayType - How scores should be displayed
176
+ * @returns Promise resolving to leaderboard info, or null on error
177
+ *
178
+ * @example
179
+ * ```typescript
180
+ * // Create a high score leaderboard
181
+ * const leaderboard = await leaderboardManager.findOrCreateLeaderboard(
182
+ * 'HighScores',
183
+ * LeaderboardSortMethod.Descending, // Highest is best
184
+ * LeaderboardDisplayType.Numeric
185
+ * );
186
+ *
187
+ * // Create a speedrun leaderboard
188
+ * const speedrun = await leaderboardManager.findOrCreateLeaderboard(
189
+ * 'FastestTime',
190
+ * LeaderboardSortMethod.Ascending, // Lowest is best
191
+ * LeaderboardDisplayType.TimeMilliseconds
192
+ * );
193
+ * ```
194
+ *
195
+ * @remarks
196
+ * - Leaderboard names must be unique per game
197
+ * - Maximum name length is 128 UTF-8 bytes
198
+ * - Waits up to 5 seconds for Steam server response
199
+ *
200
+ * Steamworks SDK Functions:
201
+ * - `SteamAPI_ISteamUserStats_FindOrCreateLeaderboard()` - Find/create leaderboard
202
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardName()` - Get leaderboard name
203
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardEntryCount()` - Get entry count
204
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardSortMethod()` - Get sort method
205
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardDisplayType()` - Get display type
206
+ */
207
+ async findOrCreateLeaderboard(name, sortMethod, displayType) {
208
+ if (!this.apiCore.isInitialized()) {
209
+ console.warn('[Steamworks] Steam API not initialized');
210
+ return null;
211
+ }
212
+ const userStatsInterface = this.apiCore.getUserStatsInterface();
213
+ if (!userStatsInterface) {
214
+ console.warn('[Steamworks] UserStats interface not available');
215
+ return null;
216
+ }
217
+ try {
218
+ console.log(`[Steamworks] Finding or creating leaderboard: ${name}`);
219
+ const callHandle = this.libraryLoader.SteamAPI_ISteamUserStats_FindOrCreateLeaderboard(userStatsInterface, name, sortMethod, displayType);
220
+ if (callHandle === BigInt(0)) {
221
+ console.error(`[Steamworks] Failed to request leaderboard: ${name}`);
222
+ return null;
223
+ }
224
+ const result = await this.callbackPoller.poll(callHandle, LeaderboardFindResult_t, k_iCallback_LeaderboardFindResult);
225
+ if (!result) {
226
+ console.error(`[Steamworks] Failed to get leaderboard result for: ${name}`);
227
+ return null;
228
+ }
229
+ if (!result.m_bLeaderboardFound) {
230
+ console.warn(`[Steamworks] Leaderboard not found/created: ${name}`);
231
+ return null;
232
+ }
233
+ // Successfully got the leaderboard handle! Get full info
234
+ console.log(`[Steamworks] Leaderboard found/created: ${name} (handle: ${result.m_hSteamLeaderboard})`);
235
+ return this.getLeaderboardInfo(result.m_hSteamLeaderboard);
236
+ }
237
+ catch (error) {
238
+ console.error(`[Steamworks] Error finding/creating leaderboard "${name}":`, error.message);
239
+ return null;
240
+ }
241
+ }
242
+ /**
243
+ * Find an existing leaderboard
244
+ *
245
+ * Searches for a leaderboard by name. Unlike findOrCreateLeaderboard(),
246
+ * this will not create the leaderboard if it doesn't exist.
247
+ *
248
+ * @param name - Name of the leaderboard to find
249
+ * @returns Promise resolving to leaderboard info, or null if not found
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * const leaderboard = await leaderboardManager.findLeaderboard('HighScores');
254
+ * if (leaderboard) {
255
+ * console.log(`[Steamworks] Found leaderboard with ${leaderboard.entryCount} entries`);
256
+ * } else {
257
+ * console.log('[Steamworks] Leaderboard does not exist');
258
+ * }
259
+ * ```
260
+ *
261
+ * @remarks
262
+ * - Returns null if leaderboard doesn't exist
263
+ * - Waits up to 5 seconds for Steam server response
264
+ *
265
+ * Steamworks SDK Functions:
266
+ * - `SteamAPI_ISteamUserStats_FindLeaderboard()` - Find existing leaderboard
267
+ */
268
+ async findLeaderboard(name) {
269
+ if (!this.apiCore.isInitialized()) {
270
+ console.warn('[Steamworks] Steam API not initialized');
271
+ return null;
272
+ }
273
+ const userStatsInterface = this.apiCore.getUserStatsInterface();
274
+ if (!userStatsInterface) {
275
+ console.warn('[Steamworks] UserStats interface not available');
276
+ return null;
277
+ }
278
+ try {
279
+ console.log(`[Steamworks] Finding leaderboard: ${name}`);
280
+ const callHandle = this.libraryLoader.SteamAPI_ISteamUserStats_FindLeaderboard(userStatsInterface, name);
281
+ if (callHandle === BigInt(0)) {
282
+ console.error(`[Steamworks] Failed to request leaderboard: ${name}`);
283
+ return null;
284
+ }
285
+ const result = await this.callbackPoller.poll(callHandle, LeaderboardFindResult_t, k_iCallback_LeaderboardFindResult);
286
+ if (!result) {
287
+ console.error(`[Steamworks] Failed to get leaderboard result for: ${name}`);
288
+ return null;
289
+ }
290
+ if (!result.m_bLeaderboardFound) {
291
+ console.log(`[Steamworks] Leaderboard does not exist: ${name}`);
292
+ return null;
293
+ }
294
+ // Successfully got the leaderboard handle! Get full info
295
+ console.log(`[Steamworks] Leaderboard found: ${name} (handle: ${result.m_hSteamLeaderboard})`);
296
+ return this.getLeaderboardInfo(result.m_hSteamLeaderboard);
297
+ }
298
+ catch (error) {
299
+ console.error(`[Steamworks] Error finding leaderboard "${name}":`, error.message);
300
+ return null;
301
+ }
302
+ }
303
+ /**
304
+ * Get information about a leaderboard
305
+ *
306
+ * Retrieves metadata for a leaderboard using its handle.
307
+ *
308
+ * @param handle - Leaderboard handle
309
+ * @returns Leaderboard information, or null on error
310
+ *
311
+ * @remarks
312
+ * - Synchronous operation, no Steam server communication needed
313
+ * - Handle must be from a previous find/create operation
314
+ *
315
+ * Steamworks SDK Functions:
316
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardName()` - Get leaderboard name
317
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardEntryCount()` - Get entry count
318
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardSortMethod()` - Get sort method
319
+ * - `SteamAPI_ISteamUserStats_GetLeaderboardDisplayType()` - Get display type
320
+ */
321
+ getLeaderboardInfo(handle) {
322
+ if (!this.apiCore.isInitialized()) {
323
+ return null;
324
+ }
325
+ const userStatsInterface = this.apiCore.getUserStatsInterface();
326
+ if (!userStatsInterface) {
327
+ return null;
328
+ }
329
+ try {
330
+ const name = this.libraryLoader.SteamAPI_ISteamUserStats_GetLeaderboardName(userStatsInterface, handle);
331
+ const entryCount = this.libraryLoader.SteamAPI_ISteamUserStats_GetLeaderboardEntryCount(userStatsInterface, handle);
332
+ const sortMethod = this.libraryLoader.SteamAPI_ISteamUserStats_GetLeaderboardSortMethod(userStatsInterface, handle);
333
+ const displayType = this.libraryLoader.SteamAPI_ISteamUserStats_GetLeaderboardDisplayType(userStatsInterface, handle);
334
+ return {
335
+ handle,
336
+ name,
337
+ entryCount,
338
+ sortMethod,
339
+ displayType
340
+ };
341
+ }
342
+ catch (error) {
343
+ console.error(`[Steamworks] Error getting leaderboard info:`, error.message);
344
+ return null;
345
+ }
346
+ }
347
+ // ========================================
348
+ // Score Upload
349
+ // ========================================
350
+ /**
351
+ * Upload a score to a leaderboard
352
+ *
353
+ * Submits a score for the current user to the specified leaderboard.
354
+ * Can optionally include up to 64 int32 detail values.
355
+ *
356
+ * @param leaderboardHandle - Handle to the leaderboard
357
+ * @param score - Score value to upload
358
+ * @param uploadMethod - How to handle the score (keep best or force update)
359
+ * @param details - Optional array of detail values (max 64)
360
+ * @returns Promise resolving to upload result, or null on error
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * // Upload simple score (keep best)
365
+ * await leaderboardManager.uploadScore(
366
+ * leaderboard.handle,
367
+ * 1000,
368
+ * LeaderboardUploadScoreMethod.KeepBest
369
+ * );
370
+ *
371
+ * // Upload score with details (e.g., level, time, difficulty)
372
+ * await leaderboardManager.uploadScore(
373
+ * leaderboard.handle,
374
+ * 5000,
375
+ * LeaderboardUploadScoreMethod.KeepBest,
376
+ * [10, 300, 2] // level 10, 300 seconds, difficulty 2
377
+ * );
378
+ *
379
+ * // Force update score (even if worse)
380
+ * await leaderboardManager.uploadScore(
381
+ * leaderboard.handle,
382
+ * 750,
383
+ * LeaderboardUploadScoreMethod.ForceUpdate
384
+ * );
385
+ * ```
386
+ *
387
+ * @remarks
388
+ * - KeepBest: Only updates if new score is better
389
+ * - ForceUpdate: Always updates to new score
390
+ * - Details array is limited to 64 int32 values
391
+ * - Waits up to 3 seconds for Steam server response
392
+ *
393
+ * Steamworks SDK Functions:
394
+ * - `SteamAPI_ISteamUserStats_UploadLeaderboardScore()` - Upload score to leaderboard
395
+ */
396
+ async uploadScore(leaderboardHandle, score, uploadMethod, details) {
397
+ if (!this.apiCore.isInitialized()) {
398
+ console.warn('[Steamworks] Steam API not initialized');
399
+ return null;
400
+ }
401
+ const userStatsInterface = this.apiCore.getUserStatsInterface();
402
+ if (!userStatsInterface) {
403
+ console.warn('[Steamworks] UserStats interface not available');
404
+ return null;
405
+ }
406
+ try {
407
+ const detailsArray = details || [];
408
+ const detailsCount = Math.min(detailsArray.length, 64);
409
+ console.log(`[Steamworks] Uploading score: ${score} (details: ${detailsCount})`);
410
+ const detailsPtr = detailsCount > 0 ? koffi.alloc('int32', detailsCount) : null;
411
+ if (detailsPtr && detailsCount > 0) {
412
+ for (let i = 0; i < detailsCount; i++) {
413
+ koffi.encode(detailsPtr, 'int32', detailsArray[i], i);
414
+ }
415
+ }
416
+ const callHandle = this.libraryLoader.SteamAPI_ISteamUserStats_UploadLeaderboardScore(userStatsInterface, leaderboardHandle, uploadMethod, score, detailsPtr, detailsCount);
417
+ if (callHandle === BigInt(0)) {
418
+ console.error(`[Steamworks] Failed to upload score`);
419
+ return null;
420
+ }
421
+ // Poll for the result using ISteamUtils
422
+ const result = await this.callbackPoller.poll(callHandle, LeaderboardScoreUploaded_t, k_iCallback_LeaderboardScoreUploaded);
423
+ if (!result) {
424
+ console.error(`[Steamworks] Failed to get upload result`);
425
+ return null;
426
+ }
427
+ if (!result.m_bSuccess) {
428
+ console.warn(`[Steamworks] Score upload was not successful`);
429
+ return null;
430
+ }
431
+ // Successfully uploaded! Return the result
432
+ const uploadResult = {
433
+ success: true,
434
+ leaderboardHandle: result.m_hSteamLeaderboard,
435
+ score: result.m_nScore,
436
+ scoreChanged: result.m_bScoreChanged === 1,
437
+ globalRankNew: result.m_nGlobalRankNew,
438
+ globalRankPrevious: result.m_nGlobalRankPrevious
439
+ };
440
+ console.log(`[Steamworks] Score uploaded: ${result.m_nScore} | Rank: ${result.m_nGlobalRankPrevious} → ${result.m_nGlobalRankNew} | Changed: ${result.m_bScoreChanged === 1}`);
441
+ return uploadResult;
442
+ }
443
+ catch (error) {
444
+ console.error(`[Steamworks] Error uploading score:`, error.message);
445
+ return null;
446
+ }
447
+ }
448
+ // ========================================
449
+ // Entry Download
450
+ // ========================================
451
+ /**
452
+ * Download leaderboard entries
453
+ *
454
+ * Retrieves a range of entries from a leaderboard. Can fetch global top scores,
455
+ * entries around the current user, or friend entries.
456
+ *
457
+ * @param leaderboardHandle - Handle to the leaderboard
458
+ * @param dataRequest - Type of data to request
459
+ * @param rangeStart - Start of range (1-based for global, offset for around user)
460
+ * @param rangeEnd - End of range (1-based for global, offset for around user)
461
+ * @returns Promise resolving to array of entries, or empty array on error
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * // Get top 10 global entries
466
+ * const top10 = await leaderboardManager.downloadLeaderboardEntries(
467
+ * leaderboard.handle,
468
+ * LeaderboardDataRequest.Global,
469
+ * 1, // Start at rank 1
470
+ * 10 // End at rank 10
471
+ * );
472
+ *
473
+ * // Get entries around current user (3 above, 3 below)
474
+ * const aroundMe = await leaderboardManager.downloadLeaderboardEntries(
475
+ * leaderboard.handle,
476
+ * LeaderboardDataRequest.GlobalAroundUser,
477
+ * -3, // 3 entries above
478
+ * 3 // 3 entries below
479
+ * );
480
+ *
481
+ * // Get friend entries
482
+ * const friends = await leaderboardManager.downloadLeaderboardEntries(
483
+ * leaderboard.handle,
484
+ * LeaderboardDataRequest.Friends,
485
+ * 0, // Ignored for friends
486
+ * 0 // Ignored for friends
487
+ * );
488
+ * ```
489
+ *
490
+ * @remarks
491
+ * - Global: rangeStart and rangeEnd are 1-based ranks [1, N]
492
+ * - GlobalAroundUser: rangeStart is negative offset, rangeEnd is positive offset
493
+ * - Friends: range parameters are ignored
494
+ * - Returns up to requested number of entries, or fewer if not available
495
+ * - Waits up to 3 seconds for Steam server response
496
+ *
497
+ * Steamworks SDK Functions:
498
+ * - `SteamAPI_ISteamUserStats_DownloadLeaderboardEntries()` - Download leaderboard entries
499
+ * - `SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry()` - Get individual entry data
500
+ */
501
+ async downloadLeaderboardEntries(leaderboardHandle, dataRequest, rangeStart, rangeEnd) {
502
+ if (!this.apiCore.isInitialized()) {
503
+ console.warn('[Steamworks] Steam API not initialized');
504
+ return [];
505
+ }
506
+ const userStatsInterface = this.apiCore.getUserStatsInterface();
507
+ if (!userStatsInterface) {
508
+ console.warn('[Steamworks] UserStats interface not available');
509
+ return [];
510
+ }
511
+ try {
512
+ console.log(`[Steamworks] Downloading entries (${rangeStart} to ${rangeEnd})`);
513
+ const callHandle = this.libraryLoader.SteamAPI_ISteamUserStats_DownloadLeaderboardEntries(userStatsInterface, leaderboardHandle, dataRequest, rangeStart, rangeEnd);
514
+ if (callHandle === BigInt(0)) {
515
+ console.error(`[Steamworks] Failed to download entries`);
516
+ return [];
517
+ }
518
+ const result = await this.callbackPoller.poll(callHandle, LeaderboardScoresDownloaded_t, k_iCallback_LeaderboardScoresDownloaded);
519
+ if (!result) {
520
+ console.error(`[Steamworks] Failed to get download result`);
521
+ return [];
522
+ }
523
+ if (result.m_cEntryCount === 0) {
524
+ console.log(`[Steamworks] No entries downloaded`);
525
+ return [];
526
+ }
527
+ // Now retrieve each individual entry using GetDownloadedLeaderboardEntry
528
+ const entries = [];
529
+ const entriesHandle = result.m_hSteamLeaderboardEntries;
530
+ for (let i = 0; i < result.m_cEntryCount; i++) {
531
+ const entryData = koffi.alloc(LeaderboardEntry_t, 1);
532
+ const detailsArray = koffi.alloc('int32', 64); // Max 64 details
533
+ const success = this.libraryLoader.SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry(userStatsInterface, entriesHandle, i, entryData, detailsArray, 64);
534
+ if (success) {
535
+ const entry = koffi.decode(entryData, LeaderboardEntry_t);
536
+ const details = [];
537
+ // Read details if any
538
+ for (let j = 0; j < entry.m_cDetails && j < 64; j++) {
539
+ details.push(koffi.decode(detailsArray, 'int32', j));
540
+ }
541
+ entries.push({
542
+ steamId: entry.m_steamIDUser.toString(),
543
+ globalRank: entry.m_nGlobalRank,
544
+ score: entry.m_nScore,
545
+ details,
546
+ ugcHandle: entry.m_hUGC
547
+ });
548
+ }
549
+ }
550
+ console.log(`[Steamworks] Downloaded ${entries.length} entries`);
551
+ return entries;
552
+ }
553
+ catch (error) {
554
+ console.error(`[Steamworks] Error downloading entries:`, error.message);
555
+ return [];
556
+ }
557
+ }
558
+ /**
559
+ * Download leaderboard entries for specific users
560
+ *
561
+ * Retrieves leaderboard entries for an arbitrary set of Steam users.
562
+ * Useful for comparing scores with specific players.
563
+ *
564
+ * @param leaderboardHandle - Handle to the leaderboard
565
+ * @param steamIds - Array of Steam IDs to retrieve (max 100)
566
+ * @returns Promise resolving to array of entries, or empty array on error
567
+ *
568
+ * @example
569
+ * ```typescript
570
+ * // Compare scores with specific players
571
+ * const playerIds = ['76561198012345678', '76561198087654321'];
572
+ * const entries = await leaderboardManager.downloadLeaderboardEntriesForUsers(
573
+ * leaderboard.handle,
574
+ * playerIds
575
+ * );
576
+ *
577
+ * entries.forEach(entry => {
578
+ * console.log(`[Steamworks] ${entry.steamId}: ${entry.score} (rank ${entry.globalRank})`);
579
+ * });
580
+ * ```
581
+ *
582
+ * @remarks
583
+ * - Maximum 100 users per request
584
+ * - Only returns entries for users who have scores
585
+ * - Only one outstanding request at a time
586
+ * - Waits up to 3 seconds for Steam server response
587
+ *
588
+ * Steamworks SDK Functions:
589
+ * - `SteamAPI_ISteamUserStats_DownloadLeaderboardEntriesForUsers()` - Download entries for users
590
+ * - `SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry()` - Get individual entry data
591
+ */
592
+ async downloadLeaderboardEntriesForUsers(leaderboardHandle, steamIds) {
593
+ if (!this.apiCore.isInitialized()) {
594
+ console.warn('[Steamworks] Steam API not initialized');
595
+ return [];
596
+ }
597
+ const userStatsInterface = this.apiCore.getUserStatsInterface();
598
+ if (!userStatsInterface) {
599
+ console.warn('[Steamworks] UserStats interface not available');
600
+ return [];
601
+ }
602
+ try {
603
+ const userCount = Math.min(steamIds.length, 100);
604
+ console.log(`[Steamworks] Downloading entries for ${userCount} users`);
605
+ // Convert Steam IDs to BigInt array
606
+ const steamIdArray = koffi.alloc('uint64', userCount);
607
+ for (let i = 0; i < userCount; i++) {
608
+ koffi.encode(steamIdArray, 'uint64', BigInt(steamIds[i]), i);
609
+ }
610
+ const callHandle = this.libraryLoader.SteamAPI_ISteamUserStats_DownloadLeaderboardEntriesForUsers(userStatsInterface, leaderboardHandle, steamIdArray, userCount);
611
+ if (callHandle === BigInt(0)) {
612
+ console.error(`[Steamworks] Failed to download user entries`);
613
+ return [];
614
+ }
615
+ const result = await this.callbackPoller.poll(callHandle, LeaderboardScoresDownloaded_t, k_iCallback_LeaderboardScoresDownloaded);
616
+ if (!result) {
617
+ console.error(`[Steamworks] Failed to get download result`);
618
+ return [];
619
+ }
620
+ if (result.m_cEntryCount === 0) {
621
+ console.log(`[Steamworks] No entries downloaded for specified users`);
622
+ return [];
623
+ }
624
+ // Now retrieve each individual entry using GetDownloadedLeaderboardEntry
625
+ const entries = [];
626
+ const entriesHandle = result.m_hSteamLeaderboardEntries;
627
+ for (let i = 0; i < result.m_cEntryCount; i++) {
628
+ const entryData = koffi.alloc(LeaderboardEntry_t, 1);
629
+ const detailsArray = koffi.alloc('int32', 64); // Max 64 details
630
+ const success = this.libraryLoader.SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry(userStatsInterface, entriesHandle, i, entryData, detailsArray, 64);
631
+ if (success) {
632
+ const entry = koffi.decode(entryData, LeaderboardEntry_t);
633
+ const details = [];
634
+ // Read details if any
635
+ for (let j = 0; j < entry.m_cDetails && j < 64; j++) {
636
+ details.push(koffi.decode(detailsArray, 'int32', j));
637
+ }
638
+ entries.push({
639
+ steamId: entry.m_steamIDUser.toString(),
640
+ globalRank: entry.m_nGlobalRank,
641
+ score: entry.m_nScore,
642
+ details,
643
+ ugcHandle: entry.m_hUGC
644
+ });
645
+ }
646
+ }
647
+ console.log(`[Steamworks] Downloaded ${entries.length} user entries`);
648
+ return entries;
649
+ }
650
+ catch (error) {
651
+ console.error(`[Steamworks] Error downloading user entries:`, error.message);
652
+ return [];
653
+ }
654
+ }
655
+ // ========================================
656
+ // UGC Attachment
657
+ // ========================================
658
+ /**
659
+ * Attach user-generated content to a leaderboard entry
660
+ *
661
+ * Associates a piece of UGC (like a replay file, screenshot, or level)
662
+ * with the current user's leaderboard entry. The UGC must first be shared
663
+ * using ISteamRemoteStorage::FileShare().
664
+ *
665
+ * @param leaderboardHandle - Handle to the leaderboard
666
+ * @param ugcHandle - Handle to the shared UGC content
667
+ * @returns Promise resolving to true if successful, false otherwise
668
+ *
669
+ * @example
670
+ * ```typescript
671
+ * // First, share a file to get UGC handle
672
+ * // const ugcHandle = await steamRemoteStorage.fileShare('replay.dat');
673
+ *
674
+ * // Then attach it to leaderboard entry
675
+ * const ugcHandle = BigInt('123456789'); // From FileShare
676
+ * const success = await leaderboardManager.attachLeaderboardUGC(
677
+ * leaderboard.handle,
678
+ * ugcHandle
679
+ * );
680
+ *
681
+ * if (success) {
682
+ * console.log('[Steamworks] UGC attached to leaderboard entry');
683
+ * }
684
+ * ```
685
+ *
686
+ * @remarks
687
+ * - UGC must be created with ISteamRemoteStorage::FileShare() first
688
+ * - Only one UGC item can be attached per leaderboard entry
689
+ * - Attaching new UGC replaces any previously attached UGC
690
+ * - Common use cases: replays, screenshots, custom levels
691
+ * - Waits up to 3 seconds for Steam server response
692
+ *
693
+ * Steamworks SDK Functions:
694
+ * - `SteamAPI_ISteamUserStats_AttachLeaderboardUGC()` - Attach UGC to leaderboard entry
695
+ */
696
+ async attachLeaderboardUGC(leaderboardHandle, ugcHandle) {
697
+ if (!this.apiCore.isInitialized()) {
698
+ console.warn('[Steamworks] Steam API not initialized');
699
+ return false;
700
+ }
701
+ const userStatsInterface = this.apiCore.getUserStatsInterface();
702
+ if (!userStatsInterface) {
703
+ console.warn('[Steamworks] UserStats interface not available');
704
+ return false;
705
+ }
706
+ try {
707
+ console.log(`[Steamworks] Attaching UGC to leaderboard entry`);
708
+ const callHandle = this.libraryLoader.SteamAPI_ISteamUserStats_AttachLeaderboardUGC(userStatsInterface, leaderboardHandle, ugcHandle);
709
+ if (callHandle === BigInt(0)) {
710
+ console.error(`[Steamworks] Failed to attach UGC`);
711
+ return false;
712
+ }
713
+ // Poll for the result using ISteamUtils
714
+ const result = await this.callbackPoller.poll(callHandle, LeaderboardUGCSet_t, k_iCallback_LeaderboardUGCSet);
715
+ if (!result) {
716
+ console.error(`[Steamworks] Failed to get UGC attachment result`);
717
+ return false;
718
+ }
719
+ // EResult k_EResultOK = 1
720
+ if (result.m_eResult !== 1) {
721
+ console.warn(`[Steamworks] UGC attachment failed. Result code: ${result.m_eResult}`);
722
+ return false;
723
+ }
724
+ console.log(`[Steamworks] UGC attached successfully`);
725
+ return true;
726
+ }
727
+ catch (error) {
728
+ console.error(`[Steamworks] Error attaching UGC:`, error.message);
729
+ return false;
730
+ }
731
+ }
732
+ }
733
+ exports.SteamLeaderboardManager = SteamLeaderboardManager;
734
+ //# sourceMappingURL=SteamLeaderboardManager.js.map