Spaces:
Running
Running
Trust ticks.parquet `t` and `event_seconds`
Browse filesUpstream 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 |
-
|
| 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 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 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.
|
| 19 |
-
//
|
| 20 |
-
//
|
| 21 |
-
//
|
| 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',
|
| 27 |
-
{ key: '
|
| 28 |
-
{ key: '
|
| 29 |
-
{ key: '
|
| 30 |
-
{ key: '
|
| 31 |
-
{ key: 't',
|
| 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
|
| 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 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
FROM ${DUELS}
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 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 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
FROM ${KILLS}
|
| 55 |
-
|
| 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 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
FROM ${KILLS}
|
| 78 |
-
|
| 79 |
-
|
| 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 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
FROM ${KILLS}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 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: `
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 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 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
FROM ${KILLS}
|
| 143 |
-
|
| 144 |
-
|
| 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 |
];
|