|
| 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 |
0 commit comments