Skip to content

Commit b1115f2

Browse files
committed
feat: CS2 masked inspect URL decoder — pure Python implementation
0 parents  commit b1115f2

File tree

9 files changed

+1147
-0
lines changed

9 files changed

+1147
-0
lines changed

.github/workflows/tests.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
python: ["3.9", "3.10", "3.11", "3.12"]
16+
17+
name: Python ${{ matrix.python }}
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup Python
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: ${{ matrix.python }}
26+
27+
- name: Install dependencies
28+
run: pip install pytest
29+
30+
- name: Run tests
31+
run: pytest tests/ -v

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.venv/
2+
__pycache__/
3+
*.pyc
4+
*.pyo
5+
.pytest_cache/
6+
*.egg-info/
7+
dist/
8+
build/
9+
.DS_Store
10+
.idea/

README.md

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# cs2-masked-inspect (Python)
2+
3+
Pure Python library for encoding and decoding CS2 masked inspect links — no external dependencies.
4+
5+
## Installation
6+
7+
```bash
8+
pip install vladdnepr-cs2-masked-inspect
9+
```
10+
11+
## Usage
12+
13+
### Deserialize a CS2 inspect link
14+
15+
```python
16+
from cs2_inspect import deserialize
17+
18+
# Accepts a full steam:// URL or a raw hex string
19+
item = deserialize(
20+
'steam://run/730//+csgo_econ_action_preview%20E3F3367440334DE2FBE4C345E0CBE0D3...'
21+
)
22+
23+
print(item.defindex) # 7 (AK-47)
24+
print(item.paintindex) # 422
25+
print(item.paintseed) # 922
26+
print(item.paintwear) # ~0.04121
27+
print(item.itemid) # 46876117973
28+
29+
for s in item.stickers:
30+
print(s.sticker_id) # 7436, 5144, 6970, 8069, 5592
31+
```
32+
33+
### Serialize an item to a hex payload
34+
35+
```python
36+
from cs2_inspect import serialize, ItemPreviewData
37+
38+
data = ItemPreviewData(
39+
defindex=60,
40+
paintindex=440,
41+
paintseed=353,
42+
paintwear=0.005411375779658556,
43+
rarity=5,
44+
)
45+
46+
hex_str = serialize(data)
47+
# 00183C20B803280538E9A3C5DD0340E102C246A0D1
48+
49+
url = f"steam://run/730//+csgo_econ_action_preview%20{hex_str}"
50+
```
51+
52+
### Item with stickers and keychains
53+
54+
```python
55+
from cs2_inspect import serialize, ItemPreviewData, Sticker
56+
57+
data = ItemPreviewData(
58+
defindex=7,
59+
paintindex=422,
60+
paintseed=922,
61+
paintwear=0.04121,
62+
rarity=3,
63+
quality=4,
64+
stickers=[
65+
Sticker(slot=0, sticker_id=7436),
66+
Sticker(slot=1, sticker_id=5144, wear=0.1),
67+
],
68+
)
69+
70+
hex_str = serialize(data)
71+
decoded = deserialize(hex_str) # round-trip
72+
```
73+
74+
---
75+
76+
## Validation
77+
78+
Use `is_masked()` and `is_classic()` to detect the link type without decoding it.
79+
80+
```python
81+
from cs2_inspect import is_masked, is_classic
82+
83+
# New masked format (pure hex blob) — can be decoded offline
84+
masked_url = 'steam://run/730//+csgo_econ_action_preview%20E3F3...'
85+
is_masked(masked_url) # True
86+
is_classic(masked_url) # False
87+
88+
# Hybrid format (S/A/D prefix with hex proto after D) — also decodable offline
89+
hybrid_url = 'steam://rungame/730/.../+csgo_econ_action_preview%20S76561199323320483A50075495125D1101C4C4FCD4AB10...'
90+
is_masked(hybrid_url) # True
91+
is_classic(hybrid_url) # False
92+
93+
# Classic format — requires Steam Game Coordinator to fetch item info
94+
classic_url = 'steam://rungame/730/.../+csgo_econ_action_preview%20S76561199842063946A49749521570D2751293026650298712'
95+
is_masked(classic_url) # False
96+
is_classic(classic_url) # True
97+
```
98+
99+
---
100+
101+
## How the format works
102+
103+
Three URL formats are handled:
104+
105+
1. **New masked format** — pure hex blob after `csgo_econ_action_preview`:
106+
```
107+
steam://run/730//+csgo_econ_action_preview%20<hexbytes>
108+
```
109+
110+
2. **Hybrid format** — old-style `S/A/D` prefix, but with a hex proto appended after `D` (instead of a decimal did):
111+
```
112+
steam://rungame/730/.../+csgo_econ_action_preview%20S<steamid>A<assetid>D<hexproto>
113+
```
114+
115+
3. **Classic format** — old-style `S/A/D` with a decimal did; requires Steam GC to resolve item details.
116+
117+
For formats 1 and 2 the library decodes the item offline. For format 3 only URL parsing is possible.
118+
119+
The hex blob (formats 1 and 2) has the following binary layout:
120+
121+
```
122+
[key_byte] [proto_bytes XOR'd with key] [4-byte checksum XOR'd with key]
123+
```
124+
125+
| Section | Size | Description |
126+
|---------|------|-------------|
127+
| `key_byte` | 1 byte | XOR key. `0x00` = no obfuscation (tool links). Other values = native CS2 links. |
128+
| `proto_bytes` | variable | `CEconItemPreviewDataBlock` protobuf, each byte XOR'd with `key_byte`. |
129+
| `checksum` | 4 bytes | Big-endian uint32, XOR'd with `key_byte`. |
130+
131+
### Checksum algorithm
132+
133+
```python
134+
import zlib, struct
135+
136+
buffer = b'\x00' + proto_bytes
137+
crc = zlib.crc32(buffer) & 0xFFFFFFFF
138+
xored = ((crc & 0xFFFF) ^ (len(proto_bytes) * crc)) & 0xFFFFFFFF
139+
checksum = struct.pack('>I', xored) # big-endian uint32
140+
```
141+
142+
### `paintwear` encoding
143+
144+
`paintwear` is stored as a `uint32` varint whose bit pattern is the IEEE 754 representation
145+
of a `float32`. The library handles this transparently — callers always work with Python `float` values.
146+
147+
---
148+
149+
## Proto field reference
150+
151+
### CEconItemPreviewDataBlock
152+
153+
| Field | Number | Type | Description |
154+
|-------|--------|------|-------------|
155+
| `accountid` | 1 | uint32 | Steam account ID (often 0) |
156+
| `itemid` | 2 | uint64 | Item ID in the owner's inventory |
157+
| `defindex` | 3 | uint32 | Item definition index (weapon type) |
158+
| `paintindex` | 4 | uint32 | Skin paint index |
159+
| `rarity` | 5 | uint32 | Item rarity |
160+
| `quality` | 6 | uint32 | Item quality |
161+
| `paintwear` | 7 | uint32* | float32 reinterpreted as uint32 |
162+
| `paintseed` | 8 | uint32 | Pattern seed (0–1000) |
163+
| `killeaterscoretype` | 9 | uint32 | StatTrak counter type |
164+
| `killeatervalue` | 10 | uint32 | StatTrak value |
165+
| `customname` | 11 | string | Name tag |
166+
| `stickers` | 12 | repeated Sticker | Applied stickers |
167+
| `inventory` | 13 | uint32 | Inventory flags |
168+
| `origin` | 14 | uint32 | Origin |
169+
| `questid` | 15 | uint32 | Quest ID |
170+
| `dropreason` | 16 | uint32 | Drop reason |
171+
| `musicindex` | 17 | uint32 | Music kit index |
172+
| `entindex` | 18 | int32 | Entity index |
173+
| `petindex` | 19 | uint32 | Pet index |
174+
| `keychains` | 20 | repeated Sticker | Applied keychains |
175+
176+
### Sticker
177+
178+
| Field | Number | Type | Description |
179+
|-------|--------|------|-------------|
180+
| `slot` | 1 | uint32 | Slot position |
181+
| `sticker_id` | 2 | uint32 | Sticker definition ID |
182+
| `wear` | 3 | float32 | Wear (fixed32) |
183+
| `scale` | 4 | float32 | Scale (fixed32) |
184+
| `rotation` | 5 | float32 | Rotation (fixed32) |
185+
| `tint_id` | 6 | uint32 | Tint |
186+
| `offset_x` | 7 | float32 | X offset (fixed32) |
187+
| `offset_y` | 8 | float32 | Y offset (fixed32) |
188+
| `offset_z` | 9 | float32 | Z offset (fixed32) |
189+
| `pattern` | 10 | uint32 | Pattern (keychains) |
190+
191+
---
192+
193+
## Known test vectors
194+
195+
### Vector 1 — Native CS2 link (XOR key 0xE3)
196+
197+
```
198+
E3F3367440334DE2FBE4C345E0CBE0D3E7DB6943400AE0A379E481ECEBE2F36F
199+
D9DE2BDB515EA6E30D74D981ECEBE3F37BCBDE640D475DA6E35EFCD881ECEBE3
200+
F359D5DE37E9D75DA6436DD3DD81ECEBE3F366DCDE3F8F9BDDA69B43B6DE81EC
201+
EBE3F33BC8DEBB1CA3DFA623F7DDDF8B71E293EBFD43382B
202+
```
203+
204+
| Field | Value |
205+
|-------|-------|
206+
| `itemid` | `46876117973` |
207+
| `defindex` | `7` (AK-47) |
208+
| `paintindex` | `422` |
209+
| `paintseed` | `922` |
210+
| `paintwear` | `≈ 0.04121` |
211+
| `rarity` | `3` |
212+
| `quality` | `4` |
213+
| sticker IDs | `[7436, 5144, 6970, 8069, 5592]` |
214+
215+
### Vector 2 — Tool-generated link (key 0x00)
216+
217+
```python
218+
ItemPreviewData(defindex=60, paintindex=440, paintseed=353,
219+
paintwear=0.005411375779658556, rarity=5)
220+
```
221+
222+
Expected hex:
223+
224+
```
225+
00183C20B803280538E9A3C5DD0340E102C246A0D1
226+
```
227+
228+
---
229+
230+
## Running tests
231+
232+
```bash
233+
pip install pytest
234+
pytest tests/
235+
```
236+
237+
---
238+
239+
## Contributing
240+
241+
Bug reports and pull requests are welcome on [GitHub](https://github.com/vladdnepr/cs2-masked-inspect-python).
242+
243+
1. Fork the repository
244+
2. Create a branch: `git checkout -b my-fix`
245+
3. Make your changes and add tests
246+
4. Ensure all tests pass: `pytest tests/`
247+
5. Open a Pull Request
248+
249+
All PRs require the CI checks to pass before merging.
250+
251+
---
252+
253+
## Author
254+
255+
[Vladyslav Lyshenko](https://github.com/vladdnepr)vladdnepr1989@gmail.com

cs2_inspect/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""CS2 inspect link encoder/decoder.
2+
3+
Usage:
4+
from cs2_inspect import serialize, deserialize, ItemPreviewData, Sticker
5+
6+
# Decode a native CS2 inspect link (XOR key != 0x00)
7+
item = deserialize("E3F3367440334D...")
8+
print(item.defindex, item.paintwear)
9+
10+
# Encode an item to a hex payload
11+
data = ItemPreviewData(defindex=60, paintindex=440, paintseed=353,
12+
paintwear=0.005411375779658556, rarity=5)
13+
hex_str = serialize(data) # "00183C20B803..."
14+
"""
15+
16+
from .inspect_link import deserialize, is_classic, is_masked, serialize
17+
from .models import ItemPreviewData, Sticker
18+
19+
__all__ = ["serialize", "deserialize", "is_masked", "is_classic", "ItemPreviewData", "Sticker"]

0 commit comments

Comments
 (0)