cs2-inventory-resolver 0.4.20 → 0.4.21

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.
@@ -6,16 +6,17 @@ import type { GcItemInput, ResolvedItemData } from './types.js';
6
6
  * 1. Skins (by `def_index` + `paint_index`)
7
7
  * 2. Music kits (by attribute 166)
8
8
  * 3. Highlights / souvenir charms (by attribute 314)
9
- * 4. Keychains (by attribute 299)
10
- * 5. Graffiti (by `stickers[0].sticker_id` + attribute 233)
11
- * 6. Keys (by `def_index`)
12
- * 7. Crates / cases (by `def_index`)
13
- * 8. Collectibles (by `def_index`)
14
- * 9. Agents (by `def_index`)
15
- * 10. Tools (by `def_index`)
16
- * 11. Patches (by `stickers[0].sticker_id`)
17
- * 12. Stickers (by `stickers[0].sticker_id`)
18
- * 13. Sticker slabs — TODO: add sticker_slabs resolution
9
+ * 4. Sticker slabs (by attribute 321)
10
+ * 5. Keychains (by attribute 299)
11
+ * 6. Graffiti (by `stickers[0].sticker_id` + attribute 233)
12
+ * 7. Keys (by `def_index`)
13
+ * 8. Crates / cases (by `def_index`)
14
+ * 9. Collectibles (by `def_index`)
15
+ * 10. Agents (by `def_index`)
16
+ * 11. Tools (by `def_index`)
17
+ * 12. Sticker slabs (by `stickers[0].sticker_id`, only for `def_index` 1355)
18
+ * 13. Patches (by `stickers[0].sticker_id`)
19
+ * 14. Stickers (by `stickers[0].sticker_id`)
19
20
  *
20
21
  * @param gcItem - The raw GC item to resolve.
21
22
  * @returns The resolved item data, or `null` if the item could not be identified.
package/dist/resolver.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { getData } from './data-loader.js';
2
2
  /** The `trade_protected_escrow_date` attribute def_index. */
3
3
  const TRADE_PROTECTED_ESCROW_DATE = 312;
4
+ /** The item def_index shared by every sticker slab. */
5
+ const STICKER_SLAB_DEF_INDEX = 1355;
4
6
  /**
5
7
  * Derive the trade state of an item instance from attribute 312
6
8
  * (`trade_protected_escrow_date`).
@@ -92,16 +94,17 @@ function makeResult(entry, gcItem, entity) {
92
94
  * 1. Skins (by `def_index` + `paint_index`)
93
95
  * 2. Music kits (by attribute 166)
94
96
  * 3. Highlights / souvenir charms (by attribute 314)
95
- * 4. Keychains (by attribute 299)
96
- * 5. Graffiti (by `stickers[0].sticker_id` + attribute 233)
97
- * 6. Keys (by `def_index`)
98
- * 7. Crates / cases (by `def_index`)
99
- * 8. Collectibles (by `def_index`)
100
- * 9. Agents (by `def_index`)
101
- * 10. Tools (by `def_index`)
102
- * 11. Patches (by `stickers[0].sticker_id`)
103
- * 12. Stickers (by `stickers[0].sticker_id`)
104
- * 13. Sticker slabs — TODO: add sticker_slabs resolution
97
+ * 4. Sticker slabs (by attribute 321)
98
+ * 5. Keychains (by attribute 299)
99
+ * 6. Graffiti (by `stickers[0].sticker_id` + attribute 233)
100
+ * 7. Keys (by `def_index`)
101
+ * 8. Crates / cases (by `def_index`)
102
+ * 9. Collectibles (by `def_index`)
103
+ * 10. Agents (by `def_index`)
104
+ * 11. Tools (by `def_index`)
105
+ * 12. Sticker slabs (by `stickers[0].sticker_id`, only for `def_index` 1355)
106
+ * 13. Patches (by `stickers[0].sticker_id`)
107
+ * 14. Stickers (by `stickers[0].sticker_id`)
105
108
  *
106
109
  * @param gcItem - The raw GC item to resolve.
107
110
  * @returns The resolved item data, or `null` if the item could not be identified.
@@ -119,6 +122,7 @@ export function resolveItem(gcItem) {
119
122
  const graffitiTint = getAttributeUint32(gcItem, 233);
120
123
  const highlightIndex = getAttributeUint32(gcItem, 314);
121
124
  const keychainIndex = getAttributeUint32(gcItem, 299);
125
+ const stickerSlabIndex = getAttributeUint32(gcItem, 321);
122
126
  // 1. Skins: def_index + paint_index
123
127
  if (gcItem.paint_index && gcItem.paint_index > 0) {
124
128
  const weapon = data.skins[defIdx];
@@ -140,13 +144,23 @@ export function resolveItem(gcItem) {
140
144
  if (highlight)
141
145
  return makeResult(highlight, gcItem, 'highlight');
142
146
  }
143
- // 4. Keychains (charms): keychain_index (attribute 299)
147
+ // 4. Sticker slabs: sticker_slab_index (attribute 321)
148
+ //
149
+ // Checked before keychains: slabs also carry attribute 299 (a constant slab
150
+ // marker, not a keychain id), so leaving keychains first would let a slab
151
+ // resolve as the wrong charm.
152
+ if (stickerSlabIndex && stickerSlabIndex > 0) {
153
+ const slab = data.sticker_slabs[String(stickerSlabIndex)];
154
+ if (slab)
155
+ return makeResult(slab, gcItem, 'sticker_slab');
156
+ }
157
+ // 5. Keychains (charms): keychain_index (attribute 299)
144
158
  if (keychainIndex && keychainIndex > 0) {
145
159
  const keychain = data.keychains[String(keychainIndex)];
146
160
  if (keychain)
147
161
  return makeResult(keychain, gcItem, 'keychain');
148
162
  }
149
- // 5. Graffiti: stickers[0].sticker_id + graffiti_tint (attribute 233)
163
+ // 6. Graffiti: stickers[0].sticker_id + graffiti_tint (attribute 233)
150
164
  if (graffitiTint !== undefined && gcItem.stickers?.length) {
151
165
  const stickerId = gcItem.stickers[0].sticker_id;
152
166
  if (stickerId) {
@@ -159,38 +173,44 @@ export function resolveItem(gcItem) {
159
173
  return makeResult(mono, gcItem, 'graffiti');
160
174
  }
161
175
  }
162
- // 6. Keys
176
+ // 7. Keys
163
177
  const key = data.keys[defIdx];
164
178
  if (key)
165
179
  return makeResult(key, gcItem, 'key');
166
- // 7. Crates / cases
180
+ // 8. Crates / cases
167
181
  const crate = data.crates[defIdx];
168
182
  if (crate)
169
183
  return makeResult(crate, gcItem, 'crate');
170
- // 8. Collectibles (coins, pins, etc.)
184
+ // 9. Collectibles (coins, pins, etc.)
171
185
  const collectible = data.collectibles[defIdx];
172
186
  if (collectible)
173
187
  return makeResult(collectible, gcItem, 'collectible');
174
- // 9. Agents (characters)
188
+ // 10. Agents (characters)
175
189
  const agent = data.agents[defIdx];
176
190
  if (agent)
177
191
  return makeResult(agent, gcItem, 'agent');
178
- // 10. Tools (Name Tag, Storage Unit, etc.)
192
+ // 11. Tools (Name Tag, Storage Unit, etc.)
179
193
  const tool = data.tools[defIdx];
180
194
  if (tool)
181
195
  return makeResult(tool, gcItem, 'tool');
182
- // 11. Patches: use stickers[0].sticker_id (checked before stickers)
183
196
  if (gcItem.stickers?.length) {
184
197
  const stickerId = gcItem.stickers[0].sticker_id;
185
198
  if (stickerId) {
199
+ // 12. Sticker slabs: slabs share their id with the applied sticker of the
200
+ // same design, so only def_index 1355 may resolve to a slab.
201
+ if (gcItem.def_index === STICKER_SLAB_DEF_INDEX) {
202
+ const slab = data.sticker_slabs[String(stickerId)];
203
+ if (slab)
204
+ return makeResult(slab, gcItem, 'sticker_slab');
205
+ }
206
+ // 13. Patches (checked before stickers)
186
207
  const patch = data.patches[String(stickerId)];
187
208
  if (patch)
188
209
  return makeResult(patch, gcItem, 'patch');
189
- // 12. Stickers
210
+ // 14. Stickers
190
211
  const sticker = data.stickers[String(stickerId)];
191
212
  if (sticker)
192
213
  return makeResult(sticker, gcItem, 'sticker');
193
- // TODO: add sticker_slabs resolution (separate entity from stickers)
194
214
  }
195
215
  }
196
216
  return null;
@@ -230,14 +230,39 @@ describe('resolveItem', () => {
230
230
  expect(result.name).toBe(data.stickers[String(stickerId)].name);
231
231
  expect(typeof result.marketable).toBe('boolean');
232
232
  });
233
- // 14. Non-skin name is unchanged
233
+ // 14. Sticker slab
234
+ it('resolves a sticker slab by attribute 321', () => {
235
+ const slabId = firstKey(data.sticker_slabs);
236
+ const result = resolveItem({ def_index: 1355, attribute: [makeAttr(321, slabId)] });
237
+ expect(result).not.toBeNull();
238
+ expect(result.entity).toBe('sticker_slab');
239
+ expect(result.name).toBe(data.sticker_slabs[String(slabId)].name);
240
+ expect(typeof result.marketable).toBe('boolean');
241
+ });
242
+ // 14b. Sticker slab — real GC item (Sticker Slab | rain | Rio 2022)
243
+ it('resolves a real sticker slab GC item', () => {
244
+ const result = resolveItem({
245
+ def_index: 1355,
246
+ quality: 8,
247
+ attribute: [
248
+ makeAttr(299, 37),
249
+ makeAttr(321, 6110),
250
+ makeAttr(75, 1786777200),
251
+ ],
252
+ });
253
+ expect(result).not.toBeNull();
254
+ expect(result.entity).toBe('sticker_slab');
255
+ expect(result.name).toBe('Sticker Slab | rain | Rio 2022');
256
+ expect(result.image).toBe(data.sticker_slabs['6110'].image);
257
+ });
258
+ // 15. Non-skin name is unchanged
234
259
  it('non-skin entities return name unchanged', () => {
235
260
  const crateId = firstKey(data.crates);
236
261
  const result = resolveItem({ def_index: crateId });
237
262
  expect(result).not.toBeNull();
238
263
  expect(result.name).toBe(data.crates[String(crateId)].name);
239
264
  });
240
- // 15. Unknown item → null
265
+ // 16. Unknown item → null
241
266
  it('returns null for an unknown def_index', () => {
242
267
  expect(resolveItem({ def_index: 999999 })).toBeNull();
243
268
  });
@@ -261,6 +286,21 @@ describe('resolveItem — priority', () => {
261
286
  const result = resolveItem({ def_index: keyId });
262
287
  expect(result.entity).toBe('key');
263
288
  });
289
+ it('sticker_id 6110 is a slab at def_index 1355 and a sticker at 1209', () => {
290
+ const slab = resolveItem({ def_index: 1355, stickers: [{ sticker_id: 6110 }] });
291
+ expect(slab.entity).toBe('sticker_slab');
292
+ expect(slab.name).toBe('Sticker Slab | rain | Rio 2022');
293
+ const sticker = resolveItem({ def_index: 1209, stickers: [{ sticker_id: 6110 }] });
294
+ expect(sticker.entity).toBe('sticker');
295
+ expect(sticker.name).toBe('Sticker | rain | Rio 2022');
296
+ });
297
+ it('sticker slab attribute 321 takes priority over keychain attribute 299', () => {
298
+ const result = resolveItem({
299
+ def_index: 1355,
300
+ attribute: [makeAttr(299, firstKey(data.keychains)), makeAttr(321, 6110)],
301
+ });
302
+ expect(result.entity).toBe('sticker_slab');
303
+ });
264
304
  it('patch takes priority over sticker when sticker_id matches both', () => {
265
305
  const patchKeys = Object.keys(data.patches);
266
306
  const overlapping = patchKeys.find(k => data.stickers[k]);
@@ -280,6 +320,40 @@ describe('resolveItem — edge cases', () => {
280
320
  expect(result).not.toBeNull();
281
321
  expect(result.entity).toBe('crate');
282
322
  });
323
+ it('unknown sticker slab id falls through to null', () => {
324
+ const result = resolveItem({ def_index: 1355, attribute: [makeAttr(321, 999999)] });
325
+ expect(result).toBeNull();
326
+ });
327
+ it('sticker slab id of 0 is treated as absent', () => {
328
+ const result = resolveItem({ def_index: 1355, attribute: [makeAttr(321, 0)] });
329
+ expect(result).toBeNull();
330
+ });
331
+ it('def_index 1355 with an unknown sticker_id resolves to null', () => {
332
+ const result = resolveItem({ def_index: 1355, stickers: [{ sticker_id: 999999 }] });
333
+ expect(result).toBeNull();
334
+ });
335
+ it('sticker_id of 0 is treated as absent', () => {
336
+ const result = resolveItem({ def_index: 1355, stickers: [{ sticker_id: 0 }] });
337
+ expect(result).toBeNull();
338
+ });
339
+ it('resolves trade status for a sticker slab', () => {
340
+ const result = resolveItem({
341
+ def_index: 1355,
342
+ attribute: [makeAttr(321, 6110), makeAttr(312, 0)],
343
+ });
344
+ expect(result.entity).toBe('sticker_slab');
345
+ expect(result.status).toBe('market_listed');
346
+ });
347
+ it('StatTrak takes priority over Souvenir when both attributes are present', () => {
348
+ const result = resolveItem({
349
+ def_index: 7,
350
+ paint_index: 282,
351
+ paint_wear: 0.3,
352
+ attribute: [makeAttr(80, 1), makeAttr(140, 1)],
353
+ });
354
+ expect(result.name).toMatch(/^StatTrak™ /);
355
+ expect(result.name).not.toContain('Souvenir');
356
+ });
283
357
  it('empty stickers array does not crash', () => {
284
358
  const crateId = firstKey(data.crates);
285
359
  const result = resolveItem({ def_index: crateId, stickers: [] });
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Possible CS2 item entity types returned by {@link resolveItem}. */
2
- export type ItemEntity = 'skin' | 'music_kit' | 'highlight' | 'keychain' | 'graffiti' | 'crate' | 'key' | 'collectible' | 'agent' | 'tool' | 'patch' | 'sticker';
2
+ export type ItemEntity = 'skin' | 'music_kit' | 'highlight' | 'keychain' | 'graffiti' | 'crate' | 'key' | 'collectible' | 'agent' | 'tool' | 'patch' | 'sticker' | 'sticker_slab';
3
3
  /**
4
4
  * Current trade state of a specific item instance, derived from attribute 312
5
5
  * (`trade_protected_escrow_date`).
@@ -63,7 +63,8 @@ export interface GcItemInput {
63
63
  * Raw attribute array from the GC message.
64
64
  *
65
65
  * Used to extract `music_index` (def_index 166), `graffiti_tint` (def_index 233),
66
- * `keychain_index` (def_index 299), `highlight_index` (def_index 314), and the
66
+ * `keychain_index` (def_index 299), `highlight_index` (def_index 314),
67
+ * `sticker_slab_index` (def_index 321), and the
67
68
  * `trade_protected_escrow_date` (def_index 312) via {@link getAttributeUint32}.
68
69
  */
69
70
  attribute?: {
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "cs2-inventory-resolver",
3
- "version": "0.4.20",
4
- "description": "Resolve CS2 Game Coordinator protobuf items into human-readable names, images, and categories",
5
- "keywords": [
6
- "cs2",
7
- "csgo",
8
- "counter-strike",
9
- "steam",
10
- "inventory",
11
- "skins",
12
- "game-coordinator",
13
- "protobuf",
14
- "resolver",
15
- "valve"
16
- ],
17
- "type": "module",
18
- "main": "./dist/index.js",
19
- "types": "./dist/index.d.ts",
20
- "exports": {
21
- ".": {
22
- "types": "./dist/index.d.ts",
23
- "default": "./dist/index.js"
24
- }
25
- },
26
- "files": [
27
- "dist/",
28
- "data/"
29
- ],
30
- "scripts": {
31
- "build": "tsc",
32
- "test": "vitest run"
33
- },
34
- "devDependencies": {
35
- "@types/node": "^22.0.0",
36
- "typescript": "^5.9.3",
37
- "vitest": "^3.0.0"
38
- }
39
- }
1
+ {
2
+ "name": "cs2-inventory-resolver",
3
+ "version": "0.4.21",
4
+ "description": "Resolve CS2 Game Coordinator protobuf items into human-readable names, images, and categories",
5
+ "keywords": [
6
+ "cs2",
7
+ "csgo",
8
+ "counter-strike",
9
+ "steam",
10
+ "inventory",
11
+ "skins",
12
+ "game-coordinator",
13
+ "protobuf",
14
+ "resolver",
15
+ "valve"
16
+ ],
17
+ "type": "module",
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist/",
28
+ "data/"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "test": "vitest run"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.0.0",
36
+ "typescript": "^5.9.3",
37
+ "vitest": "^3.0.0"
38
+ }
39
+ }