Skip to content

Commit de2755d

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

File tree

12 files changed

+3141
-0
lines changed

12 files changed

+3141
-0
lines changed

.github/workflows/tests.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
php: ["8.2", "8.3", "8.4"]
16+
17+
name: PHP ${{ matrix.php }}
18+
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- name: Setup PHP
23+
uses: shivammathur/setup-php@v2
24+
with:
25+
php-version: ${{ matrix.php }}
26+
coverage: none
27+
28+
- name: Install dependencies
29+
run: composer install --no-interaction --prefer-dist
30+
31+
- name: Run tests
32+
run: composer test

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
.phpunit.result.cache
3+
.DS_Store
4+
.idea/

README.md

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

composer.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "vladdnepr/cs2-masked-inspect",
3+
"description": "Encode and decode CS2 masked inspect links",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Vladyslav Lyshenko",
9+
"email": "vladdnepr1989@gmail.com",
10+
"homepage": "https://github.com/vladdnepr"
11+
}
12+
],
13+
"keywords": ["cs2", "csgo", "inspect", "protobuf", "steam"],
14+
"require": {
15+
"php": ">=8.2"
16+
},
17+
"require-dev": {
18+
"phpunit/phpunit": "^10.5 || ^11.0"
19+
},
20+
"autoload": {
21+
"psr-4": {
22+
"VladDnepr\\Steam\\": "src/"
23+
}
24+
},
25+
"autoload-dev": {
26+
"psr-4": {
27+
"VladDnepr\\Steam\\Tests\\": "tests/"
28+
}
29+
},
30+
"scripts": {
31+
"test": "vendor/bin/phpunit tests/"
32+
},
33+
"config": {
34+
"sort-packages": true
35+
}
36+
}

0 commit comments

Comments
 (0)