Skip to content

Commit e5839f9

Browse files
committed
feat: add gen codes (generate, toGenCode, parseGenCode, genCodeFromLink)
1 parent 50f9127 commit e5839f9

File tree

2 files changed

+307
-0
lines changed

2 files changed

+307
-0
lines changed

cs2_inspect/gen_codes.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Gen code utilities for CS2 inspect links.
2+
3+
Gen codes are space-separated command strings used on community servers:
4+
!gen {defindex} {paintindex} {paintseed} {paintwear}
5+
!gen {defindex} {paintindex} {paintseed} {paintwear} {s0_id} {s0_wear} ... {s4_id} {s4_wear} [{kc_id} {kc_wear} ...]
6+
7+
Stickers are always represented as 5 slot pairs (padded with 0 0 for empty slots).
8+
Keychains follow stickers without padding.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import Optional
14+
15+
from .models import ItemPreviewData, Sticker
16+
17+
INSPECT_BASE = "steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20"
18+
19+
20+
def _format_float(value: float, precision: int = 8) -> str:
21+
"""Format float, stripping trailing zeros up to 8 decimal places."""
22+
formatted = f"{value:.{precision}f}".rstrip("0").rstrip(".")
23+
return formatted or "0"
24+
25+
26+
def _serialize_sticker_pairs(stickers: list[Sticker], pad_to: Optional[int] = None) -> list[str]:
27+
"""Serialize stickers to pairs of [id, wear], optionally padding to fixed number of slots."""
28+
result: list[str] = []
29+
filtered = [s for s in stickers if s.sticker_id != 0]
30+
31+
if pad_to is not None:
32+
slot_map = {s.slot: s for s in filtered}
33+
for slot in range(pad_to):
34+
s = slot_map.get(slot)
35+
if s:
36+
result.append(str(s.sticker_id))
37+
result.append(_format_float(float(s.wear) if s.wear is not None else 0.0))
38+
else:
39+
result.extend(["0", "0"])
40+
else:
41+
for s in sorted(filtered, key=lambda x: x.slot):
42+
result.append(str(s.sticker_id))
43+
result.append(_format_float(float(s.wear) if s.wear is not None else 0.0))
44+
45+
return result
46+
47+
48+
def to_gen_code(item: ItemPreviewData, prefix: str = "!gen") -> str:
49+
"""Convert an ItemPreviewData to a gen code string.
50+
51+
Args:
52+
item: The item to convert.
53+
prefix: The command prefix, e.g. ``"!gen"`` or ``"!g"``.
54+
55+
Returns:
56+
A space-separated gen code string like ``"!gen 7 474 306 0.22540508"``.
57+
"""
58+
wear_str = _format_float(float(item.paintwear)) if item.paintwear is not None else "0"
59+
parts = [
60+
str(item.defindex),
61+
str(item.paintindex),
62+
str(item.paintseed),
63+
wear_str,
64+
]
65+
66+
has_stickers = any(s.sticker_id != 0 for s in item.stickers)
67+
has_keychains = any(s.sticker_id != 0 for s in item.keychains)
68+
69+
if has_stickers or has_keychains:
70+
parts.extend(_serialize_sticker_pairs(item.stickers, pad_to=5))
71+
parts.extend(_serialize_sticker_pairs(item.keychains))
72+
73+
payload = " ".join(parts)
74+
return f"{prefix} {payload}" if prefix else payload
75+
76+
77+
def generate(
78+
def_index: int,
79+
paint_index: int,
80+
paint_seed: int,
81+
paint_wear: float,
82+
*,
83+
rarity: int = 0,
84+
quality: int = 0,
85+
stickers: Optional[list[Sticker]] = None,
86+
keychains: Optional[list[Sticker]] = None,
87+
) -> str:
88+
"""Generate a full Steam inspect URL from item parameters.
89+
90+
Args:
91+
def_index: Weapon definition ID (e.g. 7 = AK-47).
92+
paint_index: Skin/paint ID.
93+
paint_seed: Pattern index (0-1000).
94+
paint_wear: Float value (0.0-1.0).
95+
rarity: Item rarity tier (0-7).
96+
quality: Item quality (e.g. 9 = StatTrak).
97+
stickers: List of Sticker objects applied to the item.
98+
keychains: List of Sticker objects used as keychains.
99+
100+
Returns:
101+
A full ``steam://rungame/...`` inspect URL.
102+
"""
103+
from .inspect_link import serialize
104+
105+
data = ItemPreviewData(
106+
defindex=def_index,
107+
paintindex=paint_index,
108+
paintseed=paint_seed,
109+
paintwear=paint_wear,
110+
rarity=rarity,
111+
quality=quality,
112+
stickers=stickers or [],
113+
keychains=keychains or [],
114+
)
115+
hex_str = serialize(data)
116+
return f"{INSPECT_BASE}{hex_str}"
117+
118+
119+
def gen_code_from_link(hex_or_url: str, prefix: str = "!gen") -> str:
120+
"""Generate a gen code string from an existing CS2 inspect link.
121+
122+
Deserializes the inspect link and converts the item data to gen code format.
123+
124+
Args:
125+
hex_or_url: A hex payload or full steam:// inspect URL.
126+
prefix: The command prefix, e.g. ``"!gen"`` or ``"!g"``.
127+
128+
Returns:
129+
A gen code string like ``"!gen 7 474 306 0.22540508"``.
130+
"""
131+
from .inspect_link import deserialize
132+
item = deserialize(hex_or_url)
133+
return to_gen_code(item, prefix)
134+
135+
136+
def parse_gen_code(gen_code: str) -> ItemPreviewData:
137+
"""Parse a gen code string into an ItemPreviewData.
138+
139+
Accepts codes like:
140+
``"!gen 7 474 306 0.22540508"``
141+
``"7 941 2 0.22540508 0 0 0 0 7203 0 0 0 0 0 36 0"``
142+
143+
Args:
144+
gen_code: The gen code string to parse.
145+
146+
Returns:
147+
An :class:`ItemPreviewData` with the parsed values.
148+
149+
Raises:
150+
ValueError: If the code has fewer than 4 numeric tokens.
151+
"""
152+
tokens = gen_code.strip().split()
153+
# Skip leading !-prefixed command (e.g. !gen, !g)
154+
if tokens and tokens[0].startswith("!"):
155+
tokens = tokens[1:]
156+
157+
if len(tokens) < 4:
158+
raise ValueError(
159+
f"Gen code must have at least 4 tokens (defindex paintindex paintseed paintwear), got: {gen_code!r}"
160+
)
161+
162+
def_index = int(tokens[0])
163+
paint_index = int(tokens[1])
164+
paint_seed = int(tokens[2])
165+
paint_wear = float(tokens[3])
166+
rest = tokens[4:]
167+
168+
stickers: list[Sticker] = []
169+
keychains: list[Sticker] = []
170+
171+
if len(rest) >= 10:
172+
# 5 sticker pairs
173+
sticker_tokens = rest[:10]
174+
for slot in range(5):
175+
sid = int(sticker_tokens[slot * 2])
176+
wear = float(sticker_tokens[slot * 2 + 1])
177+
if sid != 0:
178+
stickers.append(Sticker(slot=slot, sticker_id=sid, wear=wear))
179+
rest = rest[10:]
180+
181+
for i in range(0, len(rest) - 1, 2):
182+
sid = int(rest[i])
183+
wear = float(rest[i + 1])
184+
if sid != 0:
185+
keychains.append(Sticker(slot=i // 2, sticker_id=sid, wear=wear))
186+
187+
return ItemPreviewData(
188+
defindex=def_index,
189+
paintindex=paint_index,
190+
paintseed=paint_seed,
191+
paintwear=paint_wear,
192+
stickers=stickers,
193+
keychains=keychains,
194+
)

tests/test_gen_codes.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""Tests for gen code functionality."""
2+
3+
from cs2_inspect import generate, parse_gen_code, to_gen_code
4+
from cs2_inspect import ItemPreviewData, Sticker
5+
6+
INSPECT_BASE = "steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20"
7+
8+
9+
class TestToGenCode:
10+
def test_basic_item(self):
11+
item = ItemPreviewData(defindex=7, paintindex=474, paintseed=306,
12+
paintwear=0.22540508210659027)
13+
code = to_gen_code(item)
14+
assert code == "!gen 7 474 306 0.22540508"
15+
16+
def test_custom_prefix(self):
17+
item = ItemPreviewData(defindex=7, paintindex=474, paintseed=306,
18+
paintwear=0.22540508210659027)
19+
code = to_gen_code(item, prefix="!g")
20+
assert code == "!g 7 474 306 0.22540508"
21+
22+
def test_with_sticker_in_slot_2(self):
23+
item = ItemPreviewData(
24+
defindex=7, paintindex=941, paintseed=2,
25+
paintwear=0.22540508210659027,
26+
stickers=[Sticker(slot=2, sticker_id=7203, wear=0.0)],
27+
)
28+
code = to_gen_code(item, prefix="!g")
29+
assert code == "!g 7 941 2 0.22540508 0 0 0 0 7203 0 0 0 0 0"
30+
31+
def test_with_sticker_and_keychain(self):
32+
item = ItemPreviewData(
33+
defindex=7, paintindex=941, paintseed=2,
34+
paintwear=0.22540508210659027,
35+
stickers=[Sticker(slot=2, sticker_id=7203, wear=0.0)],
36+
keychains=[Sticker(slot=0, sticker_id=36, wear=0.0)],
37+
)
38+
code = to_gen_code(item, prefix="!g")
39+
assert code == "!g 7 941 2 0.22540508 0 0 0 0 7203 0 0 0 0 0 36 0"
40+
41+
def test_zero_wear_float_format(self):
42+
item = ItemPreviewData(defindex=7, paintindex=474, paintseed=306,
43+
paintwear=0.0)
44+
code = to_gen_code(item)
45+
assert code == "!gen 7 474 306 0"
46+
47+
48+
class TestParseGenCode:
49+
def test_basic_parse(self):
50+
item = parse_gen_code("!gen 7 474 306 0.22540508")
51+
assert item.defindex == 7
52+
assert item.paintindex == 474
53+
assert item.paintseed == 306
54+
assert abs(item.paintwear - 0.22540508) < 1e-6
55+
56+
def test_parse_without_prefix(self):
57+
item = parse_gen_code("7 474 306 0.22540508")
58+
assert item.defindex == 7
59+
60+
def test_parse_with_sticker(self):
61+
item = parse_gen_code("!gen 7 941 2 0.22540508 0 0 0 0 7203 0 0 0 0 0")
62+
assert len(item.stickers) == 1
63+
assert item.stickers[0].slot == 2
64+
assert item.stickers[0].sticker_id == 7203
65+
66+
def test_parse_with_sticker_and_keychain(self):
67+
item = parse_gen_code("!g 7 941 2 0.22540508 0 0 0 0 7203 0 0 0 0 0 36 0")
68+
assert len(item.stickers) == 1
69+
assert item.stickers[0].sticker_id == 7203
70+
assert len(item.keychains) == 1
71+
assert item.keychains[0].sticker_id == 36
72+
73+
74+
class TestGenCodeFromLink:
75+
def test_from_hex(self):
76+
from cs2_inspect import generate, gen_code_from_link
77+
url = generate(7, 474, 306, 0.22540508)
78+
hex_payload = url.split("csgo_econ_action_preview%20")[1]
79+
code = gen_code_from_link(hex_payload)
80+
assert code.startswith("!gen 7 474 306")
81+
82+
def test_from_full_url(self):
83+
from cs2_inspect import generate, gen_code_from_link
84+
url = generate(7, 474, 306, 0.22540508)
85+
code = gen_code_from_link(url)
86+
assert code.startswith("!gen 7 474 306")
87+
88+
89+
class TestGenerate:
90+
def test_returns_inspect_url(self):
91+
url = generate(7, 474, 306, 0.22540508)
92+
assert url.startswith(INSPECT_BASE)
93+
94+
def test_roundtrip(self):
95+
from cs2_inspect import deserialize
96+
url = generate(7, 474, 306, 0.22540508)
97+
# Extract hex payload and deserialize
98+
hex_payload = url.split(INSPECT_BASE)[1]
99+
item = deserialize(hex_payload)
100+
assert item.defindex == 7
101+
assert item.paintindex == 474
102+
assert item.paintseed == 306
103+
assert item.paintwear is not None
104+
assert abs(item.paintwear - 0.22540508) < 1e-5
105+
106+
def test_with_stickers(self):
107+
from cs2_inspect import deserialize
108+
stickers = [Sticker(slot=0, sticker_id=123, wear=0.1)]
109+
url = generate(7, 474, 306, 0.22540508, stickers=stickers)
110+
hex_payload = url.split(INSPECT_BASE)[1]
111+
item = deserialize(hex_payload)
112+
assert len(item.stickers) == 1
113+
assert item.stickers[0].sticker_id == 123

0 commit comments

Comments
 (0)