cs2df 3.0.0__tar.gz → 3.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cs2df-3.0.0 → cs2df-3.0.2}/PKG-INFO +1 -1
- {cs2df-3.0.0 → cs2df-3.0.2}/pyproject.toml +1 -1
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/enums.py +17 -8
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/events.py +2 -1
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/parse.py +74 -3
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/streams.py +21 -1
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/validate.py +8 -0
- {cs2df-3.0.0 → cs2df-3.0.2}/uv.lock +1 -1
- {cs2df-3.0.0 → cs2df-3.0.2}/.gitignore +0 -0
- {cs2df-3.0.0 → cs2df-3.0.2}/README.md +0 -0
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/__init__.py +0 -0
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/cli.py +0 -0
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/package.py +0 -0
- {cs2df-3.0.0 → cs2df-3.0.2}/src/cs2df/rounds.py +0 -0
- {cs2df-3.0.0 → cs2df-3.0.2}/tests/test_cli_batch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cs2df
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: Reference exporter & validator CLI for cs2-demo-format v3 (CS2 demo → ZIP data package)
|
|
5
5
|
Project-URL: Homepage, https://github.com/Starfie1d1272/cs2-demo-format
|
|
6
6
|
Project-URL: Repository, https://github.com/Starfie1d1272/cs2-demo-format
|
|
@@ -124,6 +124,14 @@ _GRENADE_ITEMS = {
|
|
|
124
124
|
"Smoke Grenade", "Flashbang", "High Explosive Grenade",
|
|
125
125
|
"Incendiary Grenade", "Molotov", "Decoy Grenade",
|
|
126
126
|
}
|
|
127
|
+
_GRENADE_ITEM_TO_TYPE = {
|
|
128
|
+
"Smoke Grenade": "smoke",
|
|
129
|
+
"Flashbang": "flashbang",
|
|
130
|
+
"High Explosive Grenade": "hegrenade",
|
|
131
|
+
"Incendiary Grenade": "incendiary",
|
|
132
|
+
"Molotov": "molotov",
|
|
133
|
+
"Decoy Grenade": "decoy",
|
|
134
|
+
}
|
|
127
135
|
_PISTOL_ITEMS = {
|
|
128
136
|
"Glock-18", "USP-S", "P2000", "Dual Berettas", "P250", "Five-SeveN",
|
|
129
137
|
"Tec-9", "CZ75-Auto", "Desert Eagle", "R8 Revolver",
|
|
@@ -141,27 +149,28 @@ _PRIMARY_ITEMS = {
|
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
|
|
144
|
-
def classify_inventory(items: Any) -> tuple[str | None, str | None, int]:
|
|
145
|
-
"""Return (primaryWeapon, secondaryWeapon, grenadeCount) from an inventory list.
|
|
152
|
+
def classify_inventory(items: Any) -> tuple[str | None, str | None, int, list[str]]:
|
|
153
|
+
"""Return (primaryWeapon, secondaryWeapon, grenadeCount, grenades) from an inventory list.
|
|
146
154
|
|
|
147
155
|
primary/secondary are the first matching gun in each slot; grenadeCount counts
|
|
148
|
-
grenade items (max 4 per CS2 rules, not clamped here)
|
|
149
|
-
Zeus) and unknown
|
|
156
|
+
grenade items (max 4 per CS2 rules, not clamped here), while grenades preserves
|
|
157
|
+
the normalized held utility types. Non-weapons (knife, C4, Zeus) and unknown
|
|
158
|
+
names are ignored.
|
|
150
159
|
"""
|
|
151
160
|
if not isinstance(items, (list, tuple)):
|
|
152
|
-
return None, None, 0
|
|
161
|
+
return None, None, 0, []
|
|
153
162
|
primary: str | None = None
|
|
154
163
|
secondary: str | None = None
|
|
155
|
-
|
|
164
|
+
grenade_types: list[str] = []
|
|
156
165
|
for it in items:
|
|
157
166
|
name = str(it or "").strip()
|
|
158
167
|
if name in _GRENADE_ITEMS:
|
|
159
|
-
|
|
168
|
+
grenade_types.append(_GRENADE_ITEM_TO_TYPE[name])
|
|
160
169
|
elif primary is None and name in _PRIMARY_ITEMS:
|
|
161
170
|
primary = name
|
|
162
171
|
elif secondary is None and name in _PISTOL_ITEMS:
|
|
163
172
|
secondary = name
|
|
164
|
-
return primary, secondary,
|
|
173
|
+
return primary, secondary, len(grenade_types), grenade_types
|
|
165
174
|
|
|
166
175
|
|
|
167
176
|
# ── active weapon normalization ───────────────────────────────────────────────
|
|
@@ -653,7 +653,7 @@ def build_economies(raw: dict, players: PlayerDirectory, round_model: _RoundMode
|
|
|
653
653
|
equip = int(_safe_float(r.get("current_equip_value"), 0))
|
|
654
654
|
start_money = int(_safe_float(r.get("start_balance"), 0))
|
|
655
655
|
eco_type = _economy_type(spent, start_money, equip, n)
|
|
656
|
-
primary, secondary, grenade_count = classify_inventory(r.get("inventory"))
|
|
656
|
+
primary, secondary, grenade_count, grenades = classify_inventory(r.get("inventory"))
|
|
657
657
|
|
|
658
658
|
out.append({
|
|
659
659
|
"roundNumber": n,
|
|
@@ -668,6 +668,7 @@ def build_economies(raw: dict, players: PlayerDirectory, round_model: _RoundMode
|
|
|
668
668
|
"primaryWeapon": primary,
|
|
669
669
|
"secondaryWeapon": secondary,
|
|
670
670
|
"grenadeCount": grenade_count,
|
|
671
|
+
"grenades": grenades,
|
|
671
672
|
})
|
|
672
673
|
team_round_types.setdefault((n, key), []).append(eco_type)
|
|
673
674
|
|
|
@@ -48,13 +48,14 @@ _GRENADE_EVENTS = [
|
|
|
48
48
|
# (raw userid is an entity slot, not a Steam64).
|
|
49
49
|
_GRENADE_PLAYER_FIELDS = ["steamid", "X", "Y", "Z"]
|
|
50
50
|
|
|
51
|
-
# Per-frame props for the replay grid.
|
|
52
|
-
#
|
|
51
|
+
# Per-frame props for the replay grid. `inventory` is sampled here so replay
|
|
52
|
+
# consumers can render current held utility; full-tick paths still avoid it.
|
|
53
|
+
# `balance` is the cash account (replay `money` column);
|
|
53
54
|
# `current_equip_value` is the equipment value (`equipValue` column).
|
|
54
55
|
_REPLAY_PROPS = [
|
|
55
56
|
"steamid", "team_num", "X", "Y", "Z", "yaw", "pitch",
|
|
56
57
|
"health", "armor", "active_weapon_name", "flash_duration",
|
|
57
|
-
"balance", "current_equip_value", "has_defuser", "last_place_name",
|
|
58
|
+
"balance", "current_equip_value", "has_defuser", "last_place_name", "inventory",
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
# Lean per-frame props for full-tick duel windows.
|
|
@@ -129,6 +130,68 @@ def _safe_ticks_df(parser: DemoParser, props: list[str], ticks: list[int]) -> "p
|
|
|
129
130
|
return None
|
|
130
131
|
|
|
131
132
|
|
|
133
|
+
def _rebuild_blinds(parser: DemoParser, detonates: list[dict]) -> list[dict]:
|
|
134
|
+
"""Reconstruct per-victim blind rows from flashbang_detonate + flash_duration.
|
|
135
|
+
|
|
136
|
+
Newer demoparser2 builds no longer emit the player_blind game event; the only
|
|
137
|
+
flash signal left is flashbang_detonate (thrower + position) plus the
|
|
138
|
+
per-player flash_duration tick property. A player whose flash_duration jumps
|
|
139
|
+
across a detonate tick was blinded by that flash; the post-detonate value is
|
|
140
|
+
the (remaining ≈ full) blind duration. When several flashes pop on the same
|
|
141
|
+
tick, attribute by closest detonate position.
|
|
142
|
+
"""
|
|
143
|
+
det_by_tick: dict[int, list[dict]] = {}
|
|
144
|
+
for d in detonates:
|
|
145
|
+
tick = int(d.get("tick") or 0)
|
|
146
|
+
if tick > 0:
|
|
147
|
+
det_by_tick.setdefault(tick, []).append(d)
|
|
148
|
+
if not det_by_tick:
|
|
149
|
+
return []
|
|
150
|
+
det_ticks = sorted(det_by_tick)
|
|
151
|
+
sample_ticks = sorted({t - 1 for t in det_ticks} | {t + 2 for t in det_ticks})
|
|
152
|
+
df = _safe_ticks_df(parser, ["flash_duration", "X", "Y"], sample_ticks)
|
|
153
|
+
if df is None or df.empty or "flash_duration" not in df.columns:
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
state: dict[tuple[int, str], tuple[float, float | None, float | None]] = {}
|
|
157
|
+
has_xy = "X" in df.columns and "Y" in df.columns
|
|
158
|
+
for row in df.itertuples(index=False):
|
|
159
|
+
d = row._asdict()
|
|
160
|
+
sid = d.get("steamid")
|
|
161
|
+
if sid is None:
|
|
162
|
+
continue
|
|
163
|
+
fd = float(d.get("flash_duration") or 0.0)
|
|
164
|
+
x = float(d["X"]) if has_xy and d.get("X") is not None else None
|
|
165
|
+
y = float(d["Y"]) if has_xy and d.get("Y") is not None else None
|
|
166
|
+
state[(int(d["tick"]), str(int(sid)))] = (fd, x, y)
|
|
167
|
+
|
|
168
|
+
out: list[dict] = []
|
|
169
|
+
sids = {sid for (_, sid) in state}
|
|
170
|
+
for tick in det_ticks:
|
|
171
|
+
dets = det_by_tick[tick]
|
|
172
|
+
for sid in sids:
|
|
173
|
+
before = state.get((tick - 1, sid), (0.0, None, None))[0]
|
|
174
|
+
after_state = state.get((tick + 2, sid))
|
|
175
|
+
if after_state is None:
|
|
176
|
+
continue
|
|
177
|
+
after, px, py = after_state
|
|
178
|
+
if after <= before + 0.1:
|
|
179
|
+
continue
|
|
180
|
+
det = dets[0]
|
|
181
|
+
if len(dets) > 1 and px is not None and py is not None:
|
|
182
|
+
det = min(
|
|
183
|
+
dets,
|
|
184
|
+
key=lambda d: (float(d.get("x") or 0.0) - px) ** 2 + (float(d.get("y") or 0.0) - py) ** 2,
|
|
185
|
+
)
|
|
186
|
+
out.append({
|
|
187
|
+
"tick": tick,
|
|
188
|
+
"attacker_steamid": det.get("user_steamid"),
|
|
189
|
+
"user_steamid": sid,
|
|
190
|
+
"blind_duration": after,
|
|
191
|
+
})
|
|
192
|
+
return out
|
|
193
|
+
|
|
194
|
+
|
|
132
195
|
def parse_demo(dem_path: str, *, sample_rate: int = 8, research: bool = False,
|
|
133
196
|
window_before_ms: int = 2000, window_after_ms: int = 1000,
|
|
134
197
|
progress: ProgressFn | None = None) -> dict[str, Any]:
|
|
@@ -171,6 +234,14 @@ def parse_demo(dem_path: str, *, sample_rate: int = 8, research: bool = False,
|
|
|
171
234
|
)
|
|
172
235
|
_record("parse.roundEvents", stage_started)
|
|
173
236
|
|
|
237
|
+
# demoparser2 新版已移除 player_blind 事件:用 flashbang_detonate + flash_duration
|
|
238
|
+
# tick 采样重建逐人致盲数据(旧版仍发 player_blind 时维持原路径)。
|
|
239
|
+
if not g_round["player_blind"]:
|
|
240
|
+
stage_started = time.perf_counter()
|
|
241
|
+
detonates = _safe_event(p, "flashbang_detonate")
|
|
242
|
+
g_round["player_blind"] = _rebuild_blinds(p, detonates)
|
|
243
|
+
_record("parse.blindRebuild", stage_started)
|
|
244
|
+
|
|
174
245
|
_p("kills", 0.12)
|
|
175
246
|
stage_started = time.perf_counter()
|
|
176
247
|
deaths = _safe_event(p, "player_death",
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
-
from .enums import normalize_weapon_name
|
|
16
|
+
from .enums import classify_inventory, normalize_weapon_name
|
|
17
17
|
from .events import PlayerDirectory, _sid
|
|
18
18
|
from .rounds import _RoundModel
|
|
19
19
|
|
|
@@ -153,6 +153,10 @@ def build_replay(raw: dict, players: PlayerDirectory, round_model: _RoundModel,
|
|
|
153
153
|
if str(v).strip()
|
|
154
154
|
}
|
|
155
155
|
df["_plidx"] = df["last_place_name"].map(place_map).fillna(-1).astype("int64")
|
|
156
|
+
if "inventory" in df.columns:
|
|
157
|
+
df["_grenades"] = df["inventory"].map(lambda items: classify_inventory(items)[3])
|
|
158
|
+
else:
|
|
159
|
+
df["_grenades"] = [[] for _ in range(len(df))]
|
|
156
160
|
|
|
157
161
|
bomb_ticks, bomb_carrier = build_bomb_carrier_timeline(raw, players)
|
|
158
162
|
tick_values = df["tick"].to_numpy()
|
|
@@ -249,6 +253,7 @@ def _player_track(g, grid, pidx: int, bomb_ticks, bomb_carrier) -> dict | None:
|
|
|
249
253
|
widx = np.where(present, widx, -1)
|
|
250
254
|
plidx = aligned["_plidx"].fillna(-1).astype("int64").to_numpy()
|
|
251
255
|
plidx = np.where(present, plidx, -1)
|
|
256
|
+
grenades = _align_grenades(aligned, present)
|
|
252
257
|
|
|
253
258
|
alive = (hp > 0).astype("int64")
|
|
254
259
|
has_kit = _num(aligned["has_defuser"], 0.0).to_numpy().astype(bool)
|
|
@@ -270,9 +275,24 @@ def _player_track(g, grid, pidx: int, bomb_ticks, bomb_carrier) -> dict | None:
|
|
|
270
275
|
"place": plidx.tolist(),
|
|
271
276
|
"flash": flash.tolist(),
|
|
272
277
|
"flags": flags.tolist(),
|
|
278
|
+
"grenades": grenades,
|
|
273
279
|
}
|
|
274
280
|
|
|
275
281
|
|
|
282
|
+
def _align_grenades(aligned, present) -> list[list[str]]:
|
|
283
|
+
"""Forward-fill sampled inventory lists and clear them when no player row exists."""
|
|
284
|
+
if "_grenades" not in aligned.columns:
|
|
285
|
+
return [[] for _ in range(len(aligned))]
|
|
286
|
+
filled = aligned["_grenades"].ffill().bfill()
|
|
287
|
+
out: list[list[str]] = []
|
|
288
|
+
for has_row, value in zip(present, filled, strict=False):
|
|
289
|
+
if not has_row or not isinstance(value, list):
|
|
290
|
+
out.append([])
|
|
291
|
+
else:
|
|
292
|
+
out.append([str(item) for item in value])
|
|
293
|
+
return out
|
|
294
|
+
|
|
295
|
+
|
|
276
296
|
# ── duels.json ────────────────────────────────────────────────────────────────
|
|
277
297
|
|
|
278
298
|
def build_duels(raw: dict, players: PlayerDirectory, round_model: _RoundModel,
|
|
@@ -24,6 +24,7 @@ EPS = 0.02
|
|
|
24
24
|
# stream track columns that must all share length == frameCount
|
|
25
25
|
_REPLAY_COLS = ("x", "y", "z", "yaw", "pitch", "hp", "armor", "money",
|
|
26
26
|
"equipValue", "weapon", "place", "flash", "flags")
|
|
27
|
+
_REPLAY_OPTIONAL_FRAME_COLS = ("grenades",)
|
|
27
28
|
_DUEL_COLS = ("x", "y", "z", "yaw", "pitch", "hp", "flash")
|
|
28
29
|
_SHOT_COLS = ("tick", "weapon", "x", "y", "z", "vx", "vy", "vz", "yaw", "pitch")
|
|
29
30
|
|
|
@@ -374,6 +375,7 @@ def _check_replay_stream(replay: dict, n_players: int, round_set: set,
|
|
|
374
375
|
for pi, track in enumerate(rd_obj.get("players", [])):
|
|
375
376
|
tlabel = f"{label} players[{pi}]"
|
|
376
377
|
_check_track_frames(tlabel, track, _REPLAY_COLS, fc, n_players, err)
|
|
378
|
+
_check_optional_track_frames(tlabel, track, _REPLAY_OPTIONAL_FRAME_COLS, fc, err)
|
|
377
379
|
for w in track.get("weapon", []):
|
|
378
380
|
if w != -1 and not (0 <= w < len(wd)):
|
|
379
381
|
err(f"{tlabel}: weapon index {w} out of weaponDict range")
|
|
@@ -390,6 +392,12 @@ def _check_replay_stream(replay: dict, n_players: int, round_set: set,
|
|
|
390
392
|
err(f"{label} projectiles[{qi}]: throwerIndex out of range")
|
|
391
393
|
|
|
392
394
|
|
|
395
|
+
def _check_optional_track_frames(label: str, track: dict, cols: tuple, frame_count: int, err) -> None:
|
|
396
|
+
bad = {c: len(track.get(c, [])) for c in cols if c in track and len(track.get(c, [])) != frame_count}
|
|
397
|
+
if bad:
|
|
398
|
+
err(f"{label}: optional column lengths != frameCount {frame_count}: {bad}")
|
|
399
|
+
|
|
400
|
+
|
|
393
401
|
def _check_duels_stream(duels: dict, n_players: int, round_set: set,
|
|
394
402
|
rounds_by_number: dict, err):
|
|
395
403
|
for wi, w in enumerate(duels.get("windows", [])):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|