blanchon commited on
Commit
1632a07
·
1 Parent(s): 5a67aa1

Trust ticks.parquet `t` and `event_seconds`

Browse files

Upstream fixed the time bases: ticks `t` is monotonic video time and
`event_seconds` is already on the POV video timeline. So:

- world.ts uses `r.t` directly; drop the tick-derived recompute and
the TICK_RATE constant.
- map-results.ts drops the ×2 multiplier for `event_seconds`.
- Recipes drop `event_seconds * 2.0` and the pov_rounds join that was
only there for path lookups we don't need.

src/lib/api/world.ts CHANGED
@@ -37,7 +37,7 @@ function chunkBaseUrl(chunk: PreviewChunk): string {
37
  }
38
 
39
  type TickRow = {
40
- tick: number;
41
  x: number;
42
  y: number;
43
  z: number;
@@ -48,9 +48,6 @@ type TickRow = {
48
  is_alive: boolean;
49
  };
50
 
51
- // CS2 demo tickrate.
52
- const TICK_RATE = 64;
53
-
54
  async function loadChunkFrames(
55
  chunk: PreviewChunk,
56
  player: number,
@@ -58,35 +55,24 @@ async function loadChunkFrames(
58
  ): Promise<WorldFrame[]> {
59
  const base = chunkBaseUrl(chunk);
60
  const rows = await fetchParquetRows<TickRow>(`${base}/ticks.parquet`, opts);
61
- if (!rows.length) return [];
62
 
63
  const steamid = `slot-${player}`;
64
  const name = `Player ${player}`;
65
- // The ticks.parquet `t` column can be non-monotonic (it resets across
66
- // tactical-pause segments), which breaks the binary search in snapshotAt.
67
- // `tick` is the absolute demo tick counter and stays monotonic across the
68
- // whole round, so derive round-relative time from it.
69
- const tick0 = rows[0].tick;
70
-
71
- const frames: WorldFrame[] = new Array(rows.length);
72
- for (let i = 0; i < rows.length; i++) {
73
- const r = rows[i];
74
- frames[i] = {
75
- t: (r.tick - tick0) / TICK_RATE,
76
- player,
77
- steamid,
78
- name,
79
- team_num: r.team_num,
80
- is_alive: r.is_alive,
81
- health: r.health,
82
- armor_value: r.armor_value,
83
- X: r.x,
84
- Y: r.y,
85
- Z: r.z,
86
- yaw: r.yaw
87
- };
88
- }
89
- return frames;
90
  }
91
 
92
  export async function loadRoundWorld(
 
37
  }
38
 
39
  type TickRow = {
40
+ t: number;
41
  x: number;
42
  y: number;
43
  z: number;
 
48
  is_alive: boolean;
49
  };
50
 
 
 
 
51
  async function loadChunkFrames(
52
  chunk: PreviewChunk,
53
  player: number,
 
55
  ): Promise<WorldFrame[]> {
56
  const base = chunkBaseUrl(chunk);
57
  const rows = await fetchParquetRows<TickRow>(`${base}/ticks.parquet`, opts);
 
58
 
59
  const steamid = `slot-${player}`;
60
  const name = `Player ${player}`;
61
+
62
+ return rows.map((r) => ({
63
+ t: r.t,
64
+ player,
65
+ steamid,
66
+ name,
67
+ team_num: r.team_num,
68
+ is_alive: r.is_alive,
69
+ health: r.health,
70
+ armor_value: r.armor_value,
71
+ X: r.x,
72
+ Y: r.y,
73
+ Z: r.z,
74
+ yaw: r.yaw
75
+ }));
 
 
 
 
 
 
 
 
 
 
76
  }
77
 
78
  export async function loadRoundWorld(
src/lib/components/advanced-filter/map-results.ts CHANGED
@@ -15,20 +15,18 @@ const PLAYER_KEYS = [
15
  'player'
16
  ] as const;
17
 
18
- // Timestamp candidates, ordered by preference. `multiplier` translates from
19
- // the source's time base into POV video seconds the events tables store
20
- // `event_seconds` on a half-rate clock per the dataset README, so they need
21
- // ×2 to seek correctly. `lead` rolls the seek back so kills/duels open with
22
- // a couple seconds of context before the actual moment. `start_t` is a
23
- // user-controlled escape hatch with no lead.
24
  const LEAD_S = 2.5;
25
  const TIME_SOURCES = [
26
- { key: 'event_video_seconds', multiplier: 1, lead: LEAD_S },
27
- { key: 'first_kill_video_seconds', multiplier: 1, lead: LEAD_S },
28
- { key: 'last_kill_video_seconds', multiplier: 1, lead: LEAD_S },
29
- { key: 'start_t', multiplier: 1, lead: 0 },
30
- { key: 'event_seconds', multiplier: 2, lead: LEAD_S },
31
- { key: 't', multiplier: 1, lead: 0 }
32
  ] as const;
33
 
34
  function pickPlayer(row: Record<string, unknown>): number | null {
@@ -43,7 +41,7 @@ function pickStartT(row: Record<string, unknown>): number | undefined {
43
  for (const src of TIME_SOURCES) {
44
  const v = row[src.key];
45
  if (typeof v !== 'number' || !Number.isFinite(v)) continue;
46
- return Math.max(0, v * src.multiplier - src.lead);
47
  }
48
  return undefined;
49
  }
 
15
  'player'
16
  ] as const;
17
 
18
+ // Timestamp candidates, ordered by preference. Every column listed here is
19
+ // already in POV-video seconds. `lead` rolls the seek back so kills/duels
20
+ // open with a couple seconds of context before the actual moment; `t` and
21
+ // the explicit `start_t` are interpreted as exact seek points (no lead).
 
 
22
  const LEAD_S = 2.5;
23
  const TIME_SOURCES = [
24
+ { key: 'event_video_seconds', lead: LEAD_S },
25
+ { key: 'event_seconds', lead: LEAD_S },
26
+ { key: 'first_kill_seconds', lead: LEAD_S },
27
+ { key: 'last_kill_seconds', lead: LEAD_S },
28
+ { key: 'start_t', lead: 0 },
29
+ { key: 't', lead: 0 }
30
  ] as const;
31
 
32
  function pickPlayer(row: Record<string, unknown>): number | null {
 
41
  for (const src of TIME_SOURCES) {
42
  const v = row[src.key];
43
  if (typeof v !== 'number' || !Number.isFinite(v)) continue;
44
+ return Math.max(0, v - src.lead);
45
  }
46
  return undefined;
47
  }
src/lib/components/advanced-filter/recipes.ts CHANGED
@@ -1,6 +1,8 @@
1
  // Curated SQL recipes adapted from the OpenCS2 dataset card. Each query must
2
  // SELECT match_id / map_name / round / a *player_slot column and a timestamp
3
  // column the result mapper understands (see ./map-results.ts).
 
 
4
 
5
  export type Recipe = {
6
  id: string;
@@ -9,7 +11,6 @@ export type Recipe = {
9
  sql: string;
10
  };
11
 
12
- const POV = `'hf://datasets/blanchon/opencs2_dataset/index/pov_rounds.parquet'`;
13
  const KILLS = `'hf://datasets/blanchon/opencs2_dataset/events/kills.parquet'`;
14
  const DUELS = `'hf://datasets/blanchon/opencs2_dataset/events/duels.parquet'`;
15
 
@@ -19,23 +20,18 @@ export const RECIPES: Recipe[] = [
19
  label: 'AWP 1v1 duel',
20
  description: 'Winner POV for AWP kills when the duel started as a 1v1.',
21
  sql: `SELECT
22
- d.match_id,
23
- d.map_name,
24
- d.round,
25
- d.winner_player_slot AS player_slot,
26
- d.event_seconds * 2.0 AS event_video_seconds,
27
- d.weapon,
28
- d.distance,
29
- d.headshot
30
- FROM ${DUELS} d
31
- JOIN ${POV} p
32
- ON d.match_id = p.match_id
33
- AND d.map_name = p.map_name
34
- AND d.round = p.round
35
- AND d.winner_player_slot = p.player_slot
36
- WHERE d.weapon = 'awp'
37
- AND d.is_1v1_before
38
- ORDER BY d.event_seconds
39
  LIMIT 50`
40
  },
41
  {
@@ -43,21 +39,16 @@ LIMIT 50`
43
  label: 'Kill through smoke',
44
  description: 'Attacker POV when the kill went through a smoke grenade.',
45
  sql: `SELECT
46
- k.match_id,
47
- k.map_name,
48
- k.round,
49
- k.attacker_player_slot AS player_slot,
50
- k.event_seconds * 2.0 AS event_video_seconds,
51
- k.weapon,
52
- k.distance,
53
- k.headshot
54
- FROM ${KILLS} k
55
- JOIN ${POV} p
56
- ON k.match_id = p.match_id
57
- AND k.map_name = p.map_name
58
- AND k.round = p.round
59
- AND k.attacker_player_slot = p.player_slot
60
- WHERE k.through_smoke
61
  LIMIT 50`
62
  },
63
  {
@@ -65,23 +56,18 @@ LIMIT 50`
65
  label: 'Noscope / wallbang',
66
  description: 'Attacker POV for noscope, wallbang or penetration kills.',
67
  sql: `SELECT
68
- k.match_id,
69
- k.map_name,
70
- k.round,
71
- k.attacker_player_slot AS player_slot,
72
- k.event_seconds * 2.0 AS event_video_seconds,
73
- k.weapon,
74
- k.noscope,
75
- k.wallbang,
76
- k.penetrated
77
- FROM ${KILLS} k
78
- JOIN ${POV} p
79
- ON k.match_id = p.match_id
80
- AND k.map_name = p.map_name
81
- AND k.round = p.round
82
- AND k.attacker_player_slot = p.player_slot
83
- WHERE (k.noscope OR k.wallbang OR k.penetrated > 0)
84
- ORDER BY k.noscope DESC, k.wallbang DESC, k.penetrated DESC
85
  LIMIT 50`
86
  },
87
  {
@@ -89,42 +75,36 @@ LIMIT 50`
89
  label: 'Knife kill',
90
  description: 'Attacker POV for actual knife kills (not just rounds spent holding a knife).',
91
  sql: `SELECT
92
- k.match_id,
93
- k.map_name,
94
- k.round,
95
- k.attacker_player_slot AS player_slot,
96
- k.event_seconds * 2.0 AS event_video_seconds,
97
- k.weapon
98
- FROM ${KILLS} k
99
- JOIN ${POV} p
100
- ON k.match_id = p.match_id
101
- AND k.map_name = p.map_name
102
- AND k.round = p.round
103
- AND k.attacker_player_slot = p.player_slot
104
- WHERE lower(k.weapon_class) = 'knife'
105
- OR lower(k.weapon) LIKE '%knife%'
106
- OR lower(k.weapon) LIKE '%bayonet%'
107
- OR lower(k.weapon) LIKE '%karambit%'
108
  LIMIT 50`
109
  },
110
  {
111
  id: 'five_kills_under_10s',
112
  label: '5 kills in < 10s',
113
  description: 'Player POV when the same player got ≥5 kills within a 10-second window.',
114
- sql: `WITH streaks AS (
115
- SELECT match_id, map_name, round, attacker_player_slot AS player_slot,
116
- COUNT(*) AS n_kills,
117
- MIN(event_seconds) * 2.0 AS first_kill_video_seconds,
118
- MAX(event_seconds) - MIN(event_seconds) AS window_s
119
- FROM ${KILLS}
120
- GROUP BY match_id, map_name, round, attacker_player_slot
121
- HAVING COUNT(*) >= 5 AND MAX(event_seconds) - MIN(event_seconds) < 10.0
122
- )
123
- SELECT s.match_id, s.map_name, s.round, s.player_slot,
124
- s.first_kill_video_seconds AS event_video_seconds,
125
- s.n_kills, s.window_s
126
- FROM streaks s
127
- ORDER BY s.window_s
128
  LIMIT 50`
129
  },
130
  {
@@ -132,21 +112,16 @@ LIMIT 50`
132
  label: 'Long-distance kill',
133
  description: 'Attacker POV for the longest-recorded kills by in-engine distance.',
134
  sql: `SELECT
135
- k.match_id,
136
- k.map_name,
137
- k.round,
138
- k.attacker_player_slot AS player_slot,
139
- k.event_seconds * 2.0 AS event_video_seconds,
140
- k.weapon,
141
- k.distance
142
- FROM ${KILLS} k
143
- JOIN ${POV} p
144
- ON k.match_id = p.match_id
145
- AND k.map_name = p.map_name
146
- AND k.round = p.round
147
- AND k.attacker_player_slot = p.player_slot
148
- WHERE k.distance IS NOT NULL
149
- ORDER BY k.distance DESC
150
  LIMIT 50`
151
  }
152
  ];
 
1
  // Curated SQL recipes adapted from the OpenCS2 dataset card. Each query must
2
  // SELECT match_id / map_name / round / a *player_slot column and a timestamp
3
  // column the result mapper understands (see ./map-results.ts).
4
+ //
5
+ // event_seconds is in POV video seconds — no rescaling needed.
6
 
7
  export type Recipe = {
8
  id: string;
 
11
  sql: string;
12
  };
13
 
 
14
  const KILLS = `'hf://datasets/blanchon/opencs2_dataset/events/kills.parquet'`;
15
  const DUELS = `'hf://datasets/blanchon/opencs2_dataset/events/duels.parquet'`;
16
 
 
20
  label: 'AWP 1v1 duel',
21
  description: 'Winner POV for AWP kills when the duel started as a 1v1.',
22
  sql: `SELECT
23
+ match_id,
24
+ map_name,
25
+ round,
26
+ winner_player_slot AS player_slot,
27
+ event_seconds,
28
+ weapon,
29
+ distance,
30
+ headshot
31
+ FROM ${DUELS}
32
+ WHERE weapon = 'awp'
33
+ AND is_1v1_before
34
+ ORDER BY event_seconds
 
 
 
 
 
35
  LIMIT 50`
36
  },
37
  {
 
39
  label: 'Kill through smoke',
40
  description: 'Attacker POV when the kill went through a smoke grenade.',
41
  sql: `SELECT
42
+ match_id,
43
+ map_name,
44
+ round,
45
+ attacker_player_slot AS player_slot,
46
+ event_seconds,
47
+ weapon,
48
+ distance,
49
+ headshot
50
+ FROM ${KILLS}
51
+ WHERE through_smoke
 
 
 
 
 
52
  LIMIT 50`
53
  },
54
  {
 
56
  label: 'Noscope / wallbang',
57
  description: 'Attacker POV for noscope, wallbang or penetration kills.',
58
  sql: `SELECT
59
+ match_id,
60
+ map_name,
61
+ round,
62
+ attacker_player_slot AS player_slot,
63
+ event_seconds,
64
+ weapon,
65
+ noscope,
66
+ wallbang,
67
+ penetrated
68
+ FROM ${KILLS}
69
+ WHERE noscope OR wallbang OR penetrated > 0
70
+ ORDER BY noscope DESC, wallbang DESC, penetrated DESC
 
 
 
 
 
71
  LIMIT 50`
72
  },
73
  {
 
75
  label: 'Knife kill',
76
  description: 'Attacker POV for actual knife kills (not just rounds spent holding a knife).',
77
  sql: `SELECT
78
+ match_id,
79
+ map_name,
80
+ round,
81
+ attacker_player_slot AS player_slot,
82
+ event_seconds,
83
+ weapon
84
+ FROM ${KILLS}
85
+ WHERE lower(weapon_class) = 'knife'
86
+ OR lower(weapon) LIKE '%knife%'
87
+ OR lower(weapon) LIKE '%bayonet%'
88
+ OR lower(weapon) LIKE '%karambit%'
 
 
 
 
 
89
  LIMIT 50`
90
  },
91
  {
92
  id: 'five_kills_under_10s',
93
  label: '5 kills in < 10s',
94
  description: 'Player POV when the same player got ≥5 kills within a 10-second window.',
95
+ sql: `SELECT
96
+ match_id,
97
+ map_name,
98
+ round,
99
+ attacker_player_slot AS player_slot,
100
+ MIN(event_seconds) AS first_kill_seconds,
101
+ MAX(event_seconds) AS last_kill_seconds,
102
+ COUNT(*) AS n_kills
103
+ FROM ${KILLS}
104
+ GROUP BY match_id, map_name, round, attacker_player_slot
105
+ HAVING COUNT(*) >= 5
106
+ AND MAX(event_seconds) - MIN(event_seconds) < 10.0
107
+ ORDER BY MAX(event_seconds) - MIN(event_seconds)
 
108
  LIMIT 50`
109
  },
110
  {
 
112
  label: 'Long-distance kill',
113
  description: 'Attacker POV for the longest-recorded kills by in-engine distance.',
114
  sql: `SELECT
115
+ match_id,
116
+ map_name,
117
+ round,
118
+ attacker_player_slot AS player_slot,
119
+ event_seconds,
120
+ weapon,
121
+ distance
122
+ FROM ${KILLS}
123
+ WHERE distance IS NOT NULL
124
+ ORDER BY distance DESC
 
 
 
 
 
125
  LIMIT 50`
126
  }
127
  ];