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.
- package/README.md +86 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/SteamAPICore.d.ts +221 -8
- package/dist/internal/SteamAPICore.d.ts.map +1 -1
- package/dist/internal/SteamAPICore.js +234 -14
- package/dist/internal/SteamAPICore.js.map +1 -1
- package/dist/internal/SteamAchievementManager.d.ts +602 -31
- package/dist/internal/SteamAchievementManager.d.ts.map +1 -1
- package/dist/internal/SteamAchievementManager.js +601 -32
- package/dist/internal/SteamAchievementManager.js.map +1 -1
- package/dist/internal/SteamCallbackPoller.d.ts +68 -0
- package/dist/internal/SteamCallbackPoller.d.ts.map +1 -0
- package/dist/internal/SteamCallbackPoller.js +134 -0
- package/dist/internal/SteamCallbackPoller.js.map +1 -0
- package/dist/internal/SteamLeaderboardManager.d.ts +338 -0
- package/dist/internal/SteamLeaderboardManager.d.ts.map +1 -0
- package/dist/internal/SteamLeaderboardManager.js +734 -0
- package/dist/internal/SteamLeaderboardManager.js.map +1 -0
- package/dist/internal/SteamLibraryLoader.d.ts +15 -0
- package/dist/internal/SteamLibraryLoader.d.ts.map +1 -1
- package/dist/internal/SteamLibraryLoader.js +42 -5
- package/dist/internal/SteamLibraryLoader.js.map +1 -1
- package/dist/internal/SteamStatsManager.d.ts +357 -50
- package/dist/internal/SteamStatsManager.d.ts.map +1 -1
- package/dist/internal/SteamStatsManager.js +444 -106
- package/dist/internal/SteamStatsManager.js.map +1 -1
- package/dist/steam.d.ts +169 -9
- package/dist/steam.d.ts.map +1 -1
- package/dist/steam.js +178 -0
- package/dist/steam.js.map +1 -1
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +34 -0
- package/dist/types.js.map +1 -1
- 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
|