|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace VlyDev\Steam; |
| 6 | + |
| 7 | +/** |
| 8 | + * Gen code utilities for CS2 inspect links. |
| 9 | + * |
| 10 | + * Gen codes are space-separated command strings used on community servers: |
| 11 | + * !gen {defindex} {paintindex} {paintseed} {paintwear} |
| 12 | + * !gen {defindex} {paintindex} {paintseed} {paintwear} {s0_id} {s0_wear} ... {s4_id} {s4_wear} [{kc_id} {kc_wear} ...] |
| 13 | + * |
| 14 | + * Stickers are always padded to 5 slot pairs. Keychains follow without padding. |
| 15 | + */ |
| 16 | +final class GenCode |
| 17 | +{ |
| 18 | + public const INSPECT_BASE = 'steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20'; |
| 19 | + |
| 20 | + /** |
| 21 | + * Format a float value, stripping trailing zeros (max 8 decimal places). |
| 22 | + */ |
| 23 | + private static function formatFloat(float $value): string |
| 24 | + { |
| 25 | + $s = rtrim(number_format($value, 8, '.', ''), '0'); |
| 26 | + $s = rtrim($s, '.'); |
| 27 | + return $s === '' ? '0' : $s; |
| 28 | + } |
| 29 | + |
| 30 | + /** |
| 31 | + * Serialize stickers to [id, wear] pairs, optionally padded to N slots. |
| 32 | + * |
| 33 | + * @param Sticker[] $stickers |
| 34 | + * @param int|null $padTo |
| 35 | + * @return string[] |
| 36 | + */ |
| 37 | + private static function serializeStickerPairs(array $stickers, ?int $padTo): array |
| 38 | + { |
| 39 | + $result = []; |
| 40 | + $filtered = array_filter($stickers, fn(Sticker $s) => $s->stickerId !== 0); |
| 41 | + |
| 42 | + if ($padTo !== null) { |
| 43 | + $slotMap = []; |
| 44 | + foreach ($filtered as $s) { |
| 45 | + $slotMap[$s->slot] = $s; |
| 46 | + } |
| 47 | + for ($slot = 0; $slot < $padTo; $slot++) { |
| 48 | + if (isset($slotMap[$slot])) { |
| 49 | + $s = $slotMap[$slot]; |
| 50 | + $result[] = (string) $s->stickerId; |
| 51 | + $result[] = self::formatFloat($s->wear ?? 0.0); |
| 52 | + } else { |
| 53 | + $result[] = '0'; |
| 54 | + $result[] = '0'; |
| 55 | + } |
| 56 | + } |
| 57 | + } else { |
| 58 | + usort($filtered, fn(Sticker $a, Sticker $b) => $a->slot <=> $b->slot); |
| 59 | + foreach ($filtered as $s) { |
| 60 | + $result[] = (string) $s->stickerId; |
| 61 | + $result[] = self::formatFloat($s->wear ?? 0.0); |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + return $result; |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * Convert an ItemPreviewData to a gen code string. |
| 70 | + * |
| 71 | + * @param string $prefix The command prefix, e.g. "!gen" or "!g". |
| 72 | + */ |
| 73 | + public static function toGenCode(ItemPreviewData $item, string $prefix = '!gen'): string |
| 74 | + { |
| 75 | + $wearStr = $item->paintwear !== null ? self::formatFloat($item->paintwear) : '0'; |
| 76 | + $parts = [ |
| 77 | + (string) $item->defindex, |
| 78 | + (string) $item->paintindex, |
| 79 | + (string) $item->paintseed, |
| 80 | + $wearStr, |
| 81 | + ]; |
| 82 | + |
| 83 | + array_push($parts, ...self::serializeStickerPairs($item->stickers, 5)); |
| 84 | + array_push($parts, ...self::serializeStickerPairs($item->keychains, null)); |
| 85 | + |
| 86 | + $payload = implode(' ', $parts); |
| 87 | + return $prefix !== '' ? "{$prefix} {$payload}" : $payload; |
| 88 | + } |
| 89 | + |
| 90 | + /** |
| 91 | + * Generate a full Steam inspect URL from item parameters. |
| 92 | + * |
| 93 | + * @param int $defIndex Weapon definition ID (e.g. 7 = AK-47) |
| 94 | + * @param int $paintIndex Skin/paint ID |
| 95 | + * @param int $paintSeed Pattern index (0-1000) |
| 96 | + * @param float $paintWear Float value (0.0-1.0) |
| 97 | + * @param int $rarity Item rarity tier |
| 98 | + * @param int $quality Item quality (e.g. 9 = StatTrak) |
| 99 | + * @param Sticker[] $stickers |
| 100 | + * @param Sticker[] $keychains |
| 101 | + */ |
| 102 | + public static function generate( |
| 103 | + int $defIndex, |
| 104 | + int $paintIndex, |
| 105 | + int $paintSeed, |
| 106 | + float $paintWear, |
| 107 | + int $rarity = 0, |
| 108 | + int $quality = 0, |
| 109 | + array $stickers = [], |
| 110 | + array $keychains = [], |
| 111 | + ): string { |
| 112 | + $data = new ItemPreviewData( |
| 113 | + defindex: $defIndex, |
| 114 | + paintindex: $paintIndex, |
| 115 | + paintseed: $paintSeed, |
| 116 | + paintwear: $paintWear, |
| 117 | + rarity: $rarity, |
| 118 | + quality: $quality, |
| 119 | + stickers: $stickers, |
| 120 | + keychains: $keychains, |
| 121 | + ); |
| 122 | + $hex = InspectLink::serialize($data); |
| 123 | + return self::INSPECT_BASE . $hex; |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * Generate a gen code string from an existing CS2 inspect link. |
| 128 | + * |
| 129 | + * Deserializes the inspect link and converts the item data to gen code format. |
| 130 | + * |
| 131 | + * @param string $hexOrUrl A hex payload or full steam:// inspect URL. |
| 132 | + * @param string $prefix The command prefix, e.g. "!gen" or "!g". |
| 133 | + */ |
| 134 | + public static function genCodeFromLink(string $hexOrUrl, string $prefix = '!gen'): string |
| 135 | + { |
| 136 | + $item = InspectLink::deserialize($hexOrUrl); |
| 137 | + return self::toGenCode($item, $prefix); |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Parse a gen code string into an ItemPreviewData. |
| 142 | + * |
| 143 | + * Accepts codes like: |
| 144 | + * "!gen 7 474 306 0.22540508" |
| 145 | + * "7 941 2 0.22540508 0 0 0 0 7203 0 0 0 0 0 36 0" |
| 146 | + * |
| 147 | + * @throws \InvalidArgumentException If the code has fewer than 4 tokens. |
| 148 | + */ |
| 149 | + public static function parseGenCode(string $genCode): ItemPreviewData |
| 150 | + { |
| 151 | + $tokens = preg_split('/\s+/', trim($genCode)); |
| 152 | + if ($tokens === false) { |
| 153 | + $tokens = []; |
| 154 | + } |
| 155 | + |
| 156 | + // Skip leading !-prefixed command |
| 157 | + if (!empty($tokens) && str_starts_with($tokens[0], '!')) { |
| 158 | + array_shift($tokens); |
| 159 | + } |
| 160 | + |
| 161 | + if (count($tokens) < 4) { |
| 162 | + throw new \InvalidArgumentException( |
| 163 | + "Gen code must have at least 4 tokens, got: \"{$genCode}\"" |
| 164 | + ); |
| 165 | + } |
| 166 | + |
| 167 | + $defIndex = (int) $tokens[0]; |
| 168 | + $paintIndex = (int) $tokens[1]; |
| 169 | + $paintSeed = (int) $tokens[2]; |
| 170 | + $paintWear = (float) $tokens[3]; |
| 171 | + $rest = array_slice($tokens, 4); |
| 172 | + |
| 173 | + $stickers = []; |
| 174 | + $keychains = []; |
| 175 | + |
| 176 | + if (count($rest) >= 10) { |
| 177 | + $stickerTokens = array_slice($rest, 0, 10); |
| 178 | + for ($slot = 0; $slot < 5; $slot++) { |
| 179 | + $sid = (int) $stickerTokens[$slot * 2]; |
| 180 | + $wear = (float) $stickerTokens[$slot * 2 + 1]; |
| 181 | + if ($sid !== 0) { |
| 182 | + $stickers[] = new Sticker(slot: $slot, stickerId: $sid, wear: $wear); |
| 183 | + } |
| 184 | + } |
| 185 | + $rest = array_slice($rest, 10); |
| 186 | + } |
| 187 | + |
| 188 | + for ($i = 0; $i + 1 < count($rest); $i += 2) { |
| 189 | + $sid = (int) $rest[$i]; |
| 190 | + $wear = (float) $rest[$i + 1]; |
| 191 | + if ($sid !== 0) { |
| 192 | + $keychains[] = new Sticker(slot: (int) ($i / 2), stickerId: $sid, wear: $wear); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + return new ItemPreviewData( |
| 197 | + defindex: $defIndex, |
| 198 | + paintindex: $paintIndex, |
| 199 | + paintseed: $paintSeed, |
| 200 | + paintwear: $paintWear, |
| 201 | + stickers: $stickers, |
| 202 | + keychains: $keychains, |
| 203 | + ); |
| 204 | + } |
| 205 | +} |
0 commit comments