cs2df 3.0.2__tar.gz → 3.0.3__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.2 → cs2df-3.0.3}/.gitignore +1 -0
- {cs2df-3.0.2 → cs2df-3.0.3}/PKG-INFO +1 -1
- {cs2df-3.0.2 → cs2df-3.0.3}/pyproject.toml +1 -1
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/__init__.py +1 -1
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/events.py +11 -8
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/parse.py +28 -11
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/rounds.py +14 -3
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/streams.py +5 -2
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/validate.py +18 -4
- cs2df-3.0.3/tests/test_replay_boundaries.py +74 -0
- cs2df-3.0.3/tests/test_round_windows.py +50 -0
- cs2df-3.0.3/tests/test_version_consistency.py +23 -0
- {cs2df-3.0.2 → cs2df-3.0.3}/uv.lock +1 -1
- {cs2df-3.0.2 → cs2df-3.0.3}/README.md +0 -0
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/cli.py +0 -0
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/enums.py +0 -0
- {cs2df-3.0.2 → cs2df-3.0.3}/src/cs2df/package.py +0 -0
- {cs2df-3.0.2 → cs2df-3.0.3}/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.3
|
|
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
|
|
@@ -121,7 +121,8 @@ def _active_event_round_number(round_model: _RoundModel, row: dict) -> int | Non
|
|
|
121
121
|
return None
|
|
122
122
|
tick = int(row.get("tick") or 0)
|
|
123
123
|
window = round_model.window_for_round(n)
|
|
124
|
-
|
|
124
|
+
event_end = round_model.event_end_tick(n)
|
|
125
|
+
if window is None or event_end is None or tick < window.freeze_end_tick or tick > event_end:
|
|
125
126
|
return None
|
|
126
127
|
return n
|
|
127
128
|
|
|
@@ -372,7 +373,8 @@ def build_bombs(raw: dict, players: PlayerDirectory, round_model: _RoundModel) -
|
|
|
372
373
|
continue
|
|
373
374
|
tick = int(r.get("tick") or 0)
|
|
374
375
|
window = round_model.window_for_round(n)
|
|
375
|
-
|
|
376
|
+
event_end = round_model.event_end_tick(n)
|
|
377
|
+
if window is None or event_end is None or tick < window.freeze_end_tick or tick > event_end:
|
|
376
378
|
continue
|
|
377
379
|
actor_sid = _sid(r.get("user_steamid") or r.get("steamid") or r.get("userid"))
|
|
378
380
|
site = bomb_site_from_place(r.get("user_last_place_name"))
|
|
@@ -431,7 +433,8 @@ def build_grenades(raw: dict, players: PlayerDirectory, round_model: _RoundModel
|
|
|
431
433
|
continue
|
|
432
434
|
tick = int(r.get("tick") or 0)
|
|
433
435
|
window = round_model.window_for_round(n)
|
|
434
|
-
|
|
436
|
+
event_end = round_model.event_end_tick(n)
|
|
437
|
+
if window is None or event_end is None or tick < window.freeze_end_tick or tick > event_end:
|
|
435
438
|
continue
|
|
436
439
|
gtype = str(r.get("_grenade_type") or "")
|
|
437
440
|
if gtype not in _GRENADE_TYPE_ENUM:
|
|
@@ -460,7 +463,7 @@ def build_grenades(raw: dict, players: PlayerDirectory, round_model: _RoundModel
|
|
|
460
463
|
if not _is_valid_side(round_model.side_map.get((n, players.team(thrower_sid)), "unknown")):
|
|
461
464
|
continue
|
|
462
465
|
if destroy_tick is not None and (
|
|
463
|
-
destroy_tick < tick or
|
|
466
|
+
destroy_tick < tick or event_end is None or destroy_tick > event_end
|
|
464
467
|
):
|
|
465
468
|
destroy_tick = None
|
|
466
469
|
|
|
@@ -489,13 +492,13 @@ def build_grenades(raw: dict, players: PlayerDirectory, round_model: _RoundModel
|
|
|
489
492
|
for g in out:
|
|
490
493
|
if g["grenade"] != "molotov" or g["destroyTick"] is not None:
|
|
491
494
|
continue
|
|
492
|
-
|
|
495
|
+
event_end = round_model.event_end_tick(g["roundNumber"])
|
|
493
496
|
best = None
|
|
494
497
|
best_d2 = None
|
|
495
498
|
for e in expires:
|
|
496
499
|
if e["used"] or e["rn"] != g["roundNumber"] or e["tick"] < g["effectTick"]:
|
|
497
500
|
continue
|
|
498
|
-
if
|
|
501
|
+
if event_end is not None and e["tick"] > event_end:
|
|
499
502
|
continue
|
|
500
503
|
ep, gp = e["pos"], g["effectPosition"]
|
|
501
504
|
d2 = (ep["x"] - gp["x"]) ** 2 + (ep["y"] - gp["y"]) ** 2
|
|
@@ -521,11 +524,11 @@ def build_grenades(raw: dict, players: PlayerDirectory, round_model: _RoundModel
|
|
|
521
524
|
eid = g.pop("_entityId", None)
|
|
522
525
|
if g["grenade"] != "smoke" or eid is None:
|
|
523
526
|
continue
|
|
524
|
-
|
|
527
|
+
event_end = round_model.event_end_tick(g["roundNumber"])
|
|
525
528
|
for e in smoke_index.get((g["roundNumber"], eid), []):
|
|
526
529
|
if e["tick"] < g["effectTick"]:
|
|
527
530
|
continue
|
|
528
|
-
if
|
|
531
|
+
if event_end is not None and e["tick"] > event_end:
|
|
529
532
|
continue
|
|
530
533
|
g["destroyTick"] = e["tick"]
|
|
531
534
|
break
|
|
@@ -351,7 +351,7 @@ def parse_demo(dem_path: str, *, sample_rate: int = 8, research: bool = False,
|
|
|
351
351
|
stage_started = time.perf_counter()
|
|
352
352
|
round_ends = g_round["round_end"]
|
|
353
353
|
step = max(1, tickrate // max(1, sample_rate))
|
|
354
|
-
replay_ticks = _build_sample_ticks(round_ends, round_freeze_ends, step)
|
|
354
|
+
replay_ticks = _build_sample_ticks(round_ends, round_freeze_ends, g_round["round_start"], step)
|
|
355
355
|
replay_df = _safe_ticks_df(p, _REPLAY_PROPS, replay_ticks) if replay_ticks else None
|
|
356
356
|
_record("parse.replayGrid", stage_started)
|
|
357
357
|
|
|
@@ -368,7 +368,8 @@ def parse_demo(dem_path: str, *, sample_rate: int = 8, research: bool = False,
|
|
|
368
368
|
_p("duel windows (full tick)", 0.78)
|
|
369
369
|
anchor_ticks = [int(r.get("tick") or 0) for r in deaths + hurts]
|
|
370
370
|
duel_windows = _merge_windows(anchor_ticks, round_ends, round_freeze_ends,
|
|
371
|
-
|
|
371
|
+
g_round["round_start"], tickrate,
|
|
372
|
+
window_before_ms, window_after_ms)
|
|
372
373
|
duel_ticks: list[int] = []
|
|
373
374
|
for start, end in duel_windows:
|
|
374
375
|
duel_ticks.extend(range(start, end + 1))
|
|
@@ -444,34 +445,50 @@ def _freeze_by_round(round_freeze_ends: list[dict]) -> dict[int, int]:
|
|
|
444
445
|
return out
|
|
445
446
|
|
|
446
447
|
|
|
448
|
+
def _round_starts_by_round(round_starts: list[dict]) -> dict[int, int]:
|
|
449
|
+
out: dict[int, int] = {}
|
|
450
|
+
for r in round_starts:
|
|
451
|
+
rn = int(r.get("total_rounds_played") or 0) + 1
|
|
452
|
+
t = int(r.get("tick") or 0)
|
|
453
|
+
if rn > 0 and t > 0 and rn not in out:
|
|
454
|
+
out[rn] = t
|
|
455
|
+
return out
|
|
456
|
+
|
|
457
|
+
|
|
447
458
|
def _round_spans(round_ends: list[dict],
|
|
448
|
-
round_freeze_ends: list[dict]
|
|
449
|
-
|
|
459
|
+
round_freeze_ends: list[dict],
|
|
460
|
+
round_starts: list[dict] | None = None) -> list[tuple[int, int]]:
|
|
461
|
+
"""[(freeze_end_tick, event_end_tick)] for rounds with a valid window."""
|
|
450
462
|
freeze = _freeze_by_round(round_freeze_ends)
|
|
463
|
+
starts = _round_starts_by_round(round_starts or [])
|
|
451
464
|
spans: list[tuple[int, int]] = []
|
|
452
465
|
for r in round_ends:
|
|
453
466
|
rn = int(r.get("total_rounds_played") or 0)
|
|
454
467
|
end_t = int(r.get("tick") or 0)
|
|
468
|
+
next_start_t = starts.get(rn + 1)
|
|
469
|
+
event_end_t = next_start_t - 1 if next_start_t is not None else end_t
|
|
455
470
|
start_t = freeze.get(rn, 0)
|
|
456
|
-
if start_t > 0 and
|
|
457
|
-
spans.append((start_t,
|
|
471
|
+
if start_t > 0 and event_end_t > start_t:
|
|
472
|
+
spans.append((start_t, event_end_t))
|
|
458
473
|
return spans
|
|
459
474
|
|
|
460
475
|
|
|
461
476
|
def _build_sample_ticks(round_ends: list[dict], round_freeze_ends: list[dict],
|
|
477
|
+
round_starts: list[dict],
|
|
462
478
|
step: int) -> list[int]:
|
|
463
|
-
"""Sorted unique sample ticks
|
|
479
|
+
"""Sorted unique sample ticks through the post-round tail."""
|
|
464
480
|
ticks: list[int] = []
|
|
465
|
-
for start_t, end_t in _round_spans(round_ends, round_freeze_ends):
|
|
466
|
-
ticks.extend(range(start_t, end_t, step))
|
|
481
|
+
for start_t, end_t in _round_spans(round_ends, round_freeze_ends, round_starts):
|
|
482
|
+
ticks.extend(range(start_t, end_t + 1, step))
|
|
467
483
|
return sorted(set(ticks))
|
|
468
484
|
|
|
469
485
|
|
|
470
486
|
def _merge_windows(anchor_ticks: list[int], round_ends: list[dict],
|
|
471
|
-
round_freeze_ends: list[dict],
|
|
487
|
+
round_freeze_ends: list[dict], round_starts: list[dict],
|
|
488
|
+
tickrate: int,
|
|
472
489
|
before_ms: int, after_ms: int) -> list[tuple[int, int]]:
|
|
473
490
|
"""Merged [start, end] full-tick combat windows, clamped to round spans."""
|
|
474
|
-
spans = _round_spans(round_ends, round_freeze_ends)
|
|
491
|
+
spans = _round_spans(round_ends, round_freeze_ends, round_starts)
|
|
475
492
|
if not spans or not anchor_ticks:
|
|
476
493
|
return []
|
|
477
494
|
before = (before_ms * tickrate) // 1000
|
|
@@ -57,6 +57,7 @@ class _RoundModel:
|
|
|
57
57
|
# Indexes built in __post_init__; events are resolved per-row across every
|
|
58
58
|
# builder, so these turn O(rounds) scans into O(1)/O(log rounds) lookups.
|
|
59
59
|
_by_round: dict[int, _RoundWindow] = field(init=False, repr=False, default_factory=dict)
|
|
60
|
+
_event_end_by_round: dict[int, int] = field(init=False, repr=False, default_factory=dict)
|
|
60
61
|
_sorted_starts: list[int] = field(init=False, repr=False, default_factory=list)
|
|
61
62
|
_sorted_windows: list[_RoundWindow] = field(init=False, repr=False, default_factory=list)
|
|
62
63
|
|
|
@@ -65,6 +66,12 @@ class _RoundModel:
|
|
|
65
66
|
ordered = sorted(self.windows, key=lambda w: w.start_tick)
|
|
66
67
|
self._sorted_windows = ordered
|
|
67
68
|
self._sorted_starts = [w.start_tick for w in ordered]
|
|
69
|
+
self._event_end_by_round = {}
|
|
70
|
+
for i, window in enumerate(ordered):
|
|
71
|
+
next_start = ordered[i + 1].start_tick if i + 1 < len(ordered) else None
|
|
72
|
+
self._event_end_by_round[window.round_number] = (
|
|
73
|
+
next_start - 1 if next_start is not None else window.end_tick
|
|
74
|
+
)
|
|
68
75
|
|
|
69
76
|
def window_for_round(self, round_number: int) -> _RoundWindow | None:
|
|
70
77
|
return self._by_round.get(round_number)
|
|
@@ -72,15 +79,19 @@ class _RoundModel:
|
|
|
72
79
|
def has_round(self, round_number: int) -> bool:
|
|
73
80
|
return round_number in self._by_round
|
|
74
81
|
|
|
82
|
+
def event_end_tick(self, round_number: int) -> int | None:
|
|
83
|
+
return self._event_end_by_round.get(round_number)
|
|
84
|
+
|
|
75
85
|
def round_for_tick(self, tick: int) -> int | None:
|
|
76
86
|
# Windows are sorted by start_tick and non-overlapping: the candidate is
|
|
77
|
-
# the last window whose start_tick <= tick; confirm tick is
|
|
78
|
-
#
|
|
87
|
+
# the last window whose start_tick <= tick; confirm tick is before the
|
|
88
|
+
# next round start (or <= end_tick for the final round).
|
|
79
89
|
i = bisect.bisect_right(self._sorted_starts, tick) - 1
|
|
80
90
|
if i < 0:
|
|
81
91
|
return None
|
|
82
92
|
window = self._sorted_windows[i]
|
|
83
|
-
|
|
93
|
+
event_end = self.event_end_tick(window.round_number)
|
|
94
|
+
return window.round_number if event_end is not None and window.start_tick <= tick <= event_end else None
|
|
84
95
|
|
|
85
96
|
def round_for_event(self, row: dict) -> int | None:
|
|
86
97
|
tick = int(row.get("tick") or 0)
|
|
@@ -178,8 +178,11 @@ def build_replay(raw: dict, players: PlayerDirectory, round_model: _RoundModel,
|
|
|
178
178
|
})
|
|
179
179
|
|
|
180
180
|
rounds_out: list[dict] = []
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
windows = sorted(round_model.windows, key=lambda w: w.round_number)
|
|
182
|
+
for i, w in enumerate(windows):
|
|
183
|
+
next_start_tick = windows[i + 1].start_tick if i + 1 < len(windows) else None
|
|
184
|
+
stop_tick = next_start_tick if next_start_tick is not None else w.end_tick + 1
|
|
185
|
+
grid = np.arange(w.freeze_end_tick, stop_tick, step, dtype="int64")
|
|
183
186
|
if len(grid) == 0:
|
|
184
187
|
continue
|
|
185
188
|
sl = _slice_by_tick(df, tick_values, int(grid[0]), int(grid[-1]))
|
|
@@ -339,7 +339,8 @@ def _check_shots_stream(shots: dict, n_players: int, round_set: set,
|
|
|
339
339
|
break
|
|
340
340
|
ticks = decode_delta(t.get("tick", []))
|
|
341
341
|
rd = rounds_by_number.get(t.get("roundNumber"))
|
|
342
|
-
|
|
342
|
+
event_end = _round_event_end(rounds_by_number, t.get("roundNumber"))
|
|
343
|
+
if rd and ticks and event_end is not None and not all(rd.get("freezeEndTick", 0) <= tk <= event_end for tk in ticks):
|
|
343
344
|
err(f"{label}: decoded ticks fall outside the round window")
|
|
344
345
|
|
|
345
346
|
|
|
@@ -370,7 +371,8 @@ def _check_replay_stream(replay: dict, n_players: int, round_set: set,
|
|
|
370
371
|
step = rd_obj.get("tickStep", 1)
|
|
371
372
|
if fc and rd:
|
|
372
373
|
last_tick = start + (fc - 1) * step
|
|
373
|
-
|
|
374
|
+
event_end = _round_event_end(rounds_by_number, rn)
|
|
375
|
+
if event_end is not None and (start < rd.get("freezeEndTick", 0) or last_tick > event_end):
|
|
374
376
|
err(f"{label}: frame grid [{start}, {last_tick}] outside round window")
|
|
375
377
|
for pi, track in enumerate(rd_obj.get("players", [])):
|
|
376
378
|
tlabel = f"{label} players[{pi}]"
|
|
@@ -414,7 +416,8 @@ def _check_duels_stream(duels: dict, n_players: int, round_set: set,
|
|
|
414
416
|
step = w.get("tickStep", 1)
|
|
415
417
|
if fc and rd:
|
|
416
418
|
last_tick = start + (fc - 1) * step
|
|
417
|
-
|
|
419
|
+
event_end = _round_event_end(rounds_by_number, rn)
|
|
420
|
+
if event_end is not None and (start < rd.get("freezeEndTick", 0) or last_tick > event_end):
|
|
418
421
|
err(f"{label}: frame grid [{start}, {last_tick}] outside round window")
|
|
419
422
|
anchors = w.get("anchors", [])
|
|
420
423
|
if not anchors:
|
|
@@ -499,7 +502,7 @@ def _check_tick_windows(name: str, rows: list, rounds_by_number: dict, err,
|
|
|
499
502
|
if not isinstance(tick, int):
|
|
500
503
|
continue
|
|
501
504
|
start = round_row.get("freezeEndTick")
|
|
502
|
-
end =
|
|
505
|
+
end = _round_event_end(rounds_by_number, row.get("roundNumber"))
|
|
503
506
|
if not isinstance(start, int) or not isinstance(end, int):
|
|
504
507
|
continue
|
|
505
508
|
if tick < start or tick > end:
|
|
@@ -510,6 +513,17 @@ def _check_tick_windows(name: str, rows: list, rounds_by_number: dict, err,
|
|
|
510
513
|
err(f"{name}.json: {len(bad)} row(s) have ticks outside their round window; sample: {sample}")
|
|
511
514
|
|
|
512
515
|
|
|
516
|
+
def _round_event_end(rounds_by_number: dict, round_number) -> int | None:
|
|
517
|
+
round_row = rounds_by_number.get(round_number)
|
|
518
|
+
if not isinstance(round_row, dict):
|
|
519
|
+
return None
|
|
520
|
+
next_round = rounds_by_number.get(round_number + 1) if isinstance(round_number, int) else None
|
|
521
|
+
if isinstance(next_round, dict) and isinstance(next_round.get("startTick"), int):
|
|
522
|
+
return next_round["startTick"] - 1
|
|
523
|
+
end_tick = round_row.get("endTick")
|
|
524
|
+
return end_tick if isinstance(end_tick, int) else None
|
|
525
|
+
|
|
526
|
+
|
|
513
527
|
def _check_bomb_lifecycle(bombs: list, err):
|
|
514
528
|
by_round: dict[object, list[dict]] = {}
|
|
515
529
|
for b in bombs:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from cs2df.events import PlayerDirectory
|
|
6
|
+
from cs2df.parse import _build_sample_ticks
|
|
7
|
+
from cs2df.rounds import _RoundModel, _RoundWindow
|
|
8
|
+
from cs2df.streams import build_replay
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _replay_row(tick: int) -> dict:
|
|
12
|
+
return {
|
|
13
|
+
"steamid": "76561198000000001",
|
|
14
|
+
"tick": tick,
|
|
15
|
+
"active_weapon_name": "ak47",
|
|
16
|
+
"last_place_name": "Middle",
|
|
17
|
+
"inventory": [],
|
|
18
|
+
"X": tick,
|
|
19
|
+
"Y": tick + 1,
|
|
20
|
+
"Z": tick + 2,
|
|
21
|
+
"yaw": 90,
|
|
22
|
+
"pitch": 0,
|
|
23
|
+
"balance": 800,
|
|
24
|
+
"current_equip_value": 2700,
|
|
25
|
+
"health": 100,
|
|
26
|
+
"armor": 100,
|
|
27
|
+
"flash_duration": 0,
|
|
28
|
+
"has_defuser": False,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_replay_round_extends_to_sample_before_next_round_start():
|
|
33
|
+
players = PlayerDirectory([
|
|
34
|
+
{"steamId64": "76561198000000001", "name": "p1", "teamKey": "teamA"},
|
|
35
|
+
])
|
|
36
|
+
round_model = _RoundModel(
|
|
37
|
+
windows=[
|
|
38
|
+
_RoundWindow(round_number=1, start_tick=50, freeze_end_tick=100, end_tick=200),
|
|
39
|
+
_RoundWindow(round_number=2, start_tick=300, freeze_end_tick=360, end_tick=460),
|
|
40
|
+
],
|
|
41
|
+
side_map={},
|
|
42
|
+
)
|
|
43
|
+
raw = {
|
|
44
|
+
"replay_df": pd.DataFrame([_replay_row(tick) for tick in range(100, 360, 16)]),
|
|
45
|
+
"grenade_trajectories": [],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
replay = build_replay(raw, players, round_model, tickrate=128, sample_rate=8)
|
|
49
|
+
|
|
50
|
+
assert replay is not None
|
|
51
|
+
first_round = replay["rounds"][0]
|
|
52
|
+
last_tick = first_round["startTick"] + (first_round["frameCount"] - 1) * first_round["tickStep"]
|
|
53
|
+
assert last_tick == 292
|
|
54
|
+
assert last_tick < 300
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_parse_replay_ticks_include_post_round_tail_before_next_start():
|
|
58
|
+
round_ends = [
|
|
59
|
+
{"total_rounds_played": 1, "tick": 200},
|
|
60
|
+
{"total_rounds_played": 2, "tick": 460},
|
|
61
|
+
]
|
|
62
|
+
round_freeze_ends = [
|
|
63
|
+
{"total_rounds_played": 0, "tick": 100},
|
|
64
|
+
{"total_rounds_played": 1, "tick": 360},
|
|
65
|
+
]
|
|
66
|
+
round_starts = [
|
|
67
|
+
{"total_rounds_played": 0, "tick": 50},
|
|
68
|
+
{"total_rounds_played": 1, "tick": 300},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
ticks = _build_sample_ticks(round_ends, round_freeze_ends, round_starts, step=16)
|
|
72
|
+
|
|
73
|
+
assert 292 in ticks
|
|
74
|
+
assert 300 not in ticks
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from cs2df.events import PlayerDirectory, build_kills
|
|
4
|
+
from cs2df.rounds import _RoundModel, _RoundWindow
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_round_for_tick_assigns_post_round_tail_to_previous_round():
|
|
8
|
+
round_model = _RoundModel(
|
|
9
|
+
windows=[
|
|
10
|
+
_RoundWindow(round_number=1, start_tick=50, freeze_end_tick=100, end_tick=200),
|
|
11
|
+
_RoundWindow(round_number=2, start_tick=300, freeze_end_tick=360, end_tick=460),
|
|
12
|
+
],
|
|
13
|
+
side_map={},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
assert round_model.round_for_tick(200) == 1
|
|
17
|
+
assert round_model.round_for_tick(250) == 1
|
|
18
|
+
assert round_model.round_for_tick(299) == 1
|
|
19
|
+
assert round_model.round_for_tick(300) == 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_kills_after_round_end_before_next_start_are_kept_in_previous_round():
|
|
23
|
+
players = PlayerDirectory([
|
|
24
|
+
{"steamId64": "76561198000000001", "name": "attacker", "teamKey": "teamA"},
|
|
25
|
+
{"steamId64": "76561198000000002", "name": "victim", "teamKey": "teamB"},
|
|
26
|
+
])
|
|
27
|
+
round_model = _RoundModel(
|
|
28
|
+
windows=[
|
|
29
|
+
_RoundWindow(round_number=1, start_tick=50, freeze_end_tick=100, end_tick=200),
|
|
30
|
+
_RoundWindow(round_number=2, start_tick=300, freeze_end_tick=360, end_tick=460),
|
|
31
|
+
],
|
|
32
|
+
side_map={(1, "teamA"): "t", (1, "teamB"): "ct"},
|
|
33
|
+
)
|
|
34
|
+
raw = {
|
|
35
|
+
"deaths": [{
|
|
36
|
+
"tick": 250,
|
|
37
|
+
"user_steamid": "76561198000000002",
|
|
38
|
+
"attacker_steamid": "76561198000000001",
|
|
39
|
+
"weapon": "ak47",
|
|
40
|
+
"user_X": 1,
|
|
41
|
+
"user_Y": 2,
|
|
42
|
+
"user_Z": 3,
|
|
43
|
+
}],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
kills = build_kills(raw, players, round_model)
|
|
47
|
+
|
|
48
|
+
assert len(kills) == 1
|
|
49
|
+
assert kills[0]["roundNumber"] == 1
|
|
50
|
+
assert kills[0]["tick"] == 250
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_check_versions_script_accepts_current_release_versions():
|
|
12
|
+
script = ROOT / "tools" / "check_versions.py"
|
|
13
|
+
|
|
14
|
+
result = subprocess.run(
|
|
15
|
+
[sys.executable, str(script)],
|
|
16
|
+
cwd=ROOT,
|
|
17
|
+
text=True,
|
|
18
|
+
stdout=subprocess.PIPE,
|
|
19
|
+
stderr=subprocess.PIPE,
|
|
20
|
+
check=False,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
assert result.returncode == 0, result.stdout + result.stderr
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|