Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
461 changes: 461 additions & 0 deletions generated/schema.graphql

Large diffs are not rendered by default.

441 changes: 441 additions & 0 deletions generated/schema.ts

Large diffs are not rendered by default.

95,590 changes: 48,082 additions & 47,508 deletions generated/types.ts

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions hasura/metadata/actions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,15 @@ type Mutation {
remineUtilityMeta: UtilityRemineOutput
}

type Mutation {
syncMapCallouts: MapCalloutSyncOutput
}

type MapCalloutSyncOutput {
maps: Int!
callouts: Int!
}

type UtilityRemineOutput {
demos: Int!
throws: Int!
Expand Down
8 changes: 8 additions & 0 deletions hasura/metadata/actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,14 @@ actions:
permissions:
- role: administrator
comment: Re-mine one batch of demos after a miner change
- name: syncMapCallouts
definition:
kind: synchronous
handler: '{{HASURA_GRAPHQL_ACTIONS_HOOK}}'
forward_client_headers: true
permissions:
- role: administrator
comment: Pull the published map callouts for every enabled map
- name: backfillUtilityLaunchSeeds
definition:
kind: synchronous
Expand Down
14 changes: 14 additions & 0 deletions hasura/metadata/databases/default/tables/public_map_callouts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
table:
name: map_callouts
schema: public
select_permissions:
- role: guest
permission:
columns:
- map_name
- name
- boxes
- source
- updated_at
filter: {}
comment: ""
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,44 @@ event_triggers:
num_retries: 6
timeout_sec: 60
webhook: '{{HASURA_GRAPHQL_EVENT_HOOK}}'
# The search index deliberately ignores the geometry columns; a practice
# server standing somebody on a throw cannot. Separate trigger rather than
# more columns on that one, so re-indexing and re-serving stay independent.
- name: utility_lineups_practice_events
definition:
delete:
columns: '*'
enable_manual: true
insert:
columns: '*'
update:
columns:
- map_name
- utility_type
- side
- technique
- name
- description
- visibility
- archived_at
- team_id
- aim_tolerance
- origin_x
- origin_y
- origin_z
- eye_z
- view_yaw
- view_pitch
- land_x
- land_y
- land_z
- jump_throw_bind
- throw_strength
retry_conf:
interval_sec: 10
num_retries: 6
timeout_sec: 60
webhook: '{{HASURA_GRAPHQL_EVENT_HOOK}}'
- name: utility_lineups_events
definition:
delete:
Expand Down
1 change: 1 addition & 0 deletions hasura/metadata/databases/default/tables/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
- "!include public_league_teams.yaml"
- "!include public_lobbies.yaml"
- "!include public_lobby_players.yaml"
- "!include public_map_callouts.yaml"
- "!include public_map_pools.yaml"
- "!include public_maps.yaml"
- "!include public_match_clips.yaml"
Expand Down
3 changes: 3 additions & 0 deletions hasura/migrations/default/1880000011000_map_callouts/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP INDEX IF EXISTS public.map_callouts_source_idx;

DROP TABLE IF EXISTS public.map_callouts;
26 changes: 26 additions & 0 deletions hasura/migrations/default/1880000011000_map_callouts/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- The named areas of a map -- "Window", "Catwalk", "A Site" -- as CS2 itself
-- defines them in its `env_cs_place` entities. Extracted from the compiled maps
-- by web/scripts/extract-map-callouts.mjs and published beside the collision
-- meshes; a practice plugin reports them for maps the extract does not cover.
--
-- One row per NAME, not per volume: a callout is legitimately several disjoint
-- boxes (Banana is two), and every reader wants "which place is this point in",
-- never "which box".
CREATE TABLE IF NOT EXISTS public.map_callouts (
map_name text NOT NULL,
name text NOT NULL,
-- [{ "min": [x, y, z], "max": [x, y, z] }, ...] in raw CS2 source units,
-- the same space as demo player positions and the collision meshes.
boxes jsonb NOT NULL,
source text NOT NULL DEFAULT 'cdn',
updated_at timestamp with time zone NOT NULL DEFAULT now(),
PRIMARY KEY (map_name, name),
CONSTRAINT map_callouts_source_check CHECK (source IN ('cdn', 'plugin')),
CONSTRAINT map_callouts_boxes_check CHECK (jsonb_typeof(boxes) = 'array')
);

-- map_name is stored NORMALISED -- lowercased, any `workshop/<id>/` prefix and
-- `_night` suffix stripped -- so a night variant shares the base map's geometry
-- and its callouts, and every reader looks up the one spelling.
CREATE INDEX IF NOT EXISTS map_callouts_source_idx
ON public.map_callouts (map_name, source);
8 changes: 8 additions & 0 deletions src/matches/match-assistant/match-assistant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,14 @@ export class MatchAssistantService {
connected: false,
offline_at: null,
reserved_by_match_id: matchId,
// The row outlives the pod, and the relay account id in it
// belonged to the previous tenant. get_match_server_info
// prefers steam_relay over host:port whenever it is set, so
// leaving it behind handed the next player a connect string
// pointing at somebody else's relay -- which connects and
// then drops. The pod that is about to boot reports its own
// once it is up.
steam_relay: null,
},
},
__typename: true,
Expand Down
133 changes: 92 additions & 41 deletions src/telemetry/telemetry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,52 @@ export class TelemetryService {
"branding",
]);

// Feature key -> the collect count that measures it. Static, rather than
// built inside buildFeatures, so the fleet page can also list a feature no
// panel has reported yet (see getFeatureAdoption) instead of dropping it
// until the fleet has updated.
//
// demo_playback and live_streaming deliberately have no count. The only rows
// that record either -- match_demo_sessions, match_streams -- are deleted
// when the session ends, so counting them reports how many people happen to
// be watching during the collect, not how much the panel has ever used it. A
// panel that has served ten thousand playbacks would report zero. Both are in
// DetectedFeatures, so their adoption comes from `enabled` (a GPU node
// carrying the workload) and the null count reads as "not measured" rather
// than as "nobody uses it".
private static readonly FeatureUsage: Record<string, string> = {
tournaments: "tournaments",
leagues: "league_seasons",
seasons: "seasons",
events: "events",
news: "news_articles",
highlights: "match_clips",
clip_renders: "clip_render_jobs",
system_alerts: "system_alerts",
awards: "award_recipients",
scrims: "scrim_requests",
matchmaking: "lobbies",
custom_pages: "custom_pages",
external_matches: "matches_external",
faceit_import: "matches_faceit",
draft_games: "draft_games",
demos: "demos",
sanctions: "sanctions",
api_keys: "api_keys",
gamedata_validations: "gamedata_validations",
push_notifications: "push_subscriptions",
utility_library: "utility_lineups",
utility_practice: "utility_sessions",
// Lineups an operator bulk-loaded rather than anybody throwing them, which
// is the only thing the switch controls.
utility_import: "utility_imported",
game_server_nodes: "nodes_total",
version_pinning: "nodes_pinned",
discord_bot: "players_discord",
steam_presence: "steam_presence_friends",
player_name_registration: "players_name_registered",
};

// A match this panel actually hosted. Imported demos are stamped with a
// started_at (from demo metadata, or the import time when that is missing),
// so started_at alone would count every FACEIT import as a match we ran.
Expand Down Expand Up @@ -856,7 +902,7 @@ export class TelemetryService {
ORDER BY total DESC, f.key ASC`,
);

return rows.map((row) => {
const adoption = rows.map((row) => {
const counts = TelemetryService.toIntegers(row, [
"enabled",
"flagged",
Expand All @@ -872,6 +918,46 @@ export class TelemetryService {
...counts,
};
});

// A feature added after the builds currently in the fleet has no row here
// at all, and leaving it out reads as a feature that does not exist rather
// than as one nothing has reported yet. Emit it with nothing reported --
// `reporting: 0` is what the page reads to say so -- so a switch shows up
// on the fleet page the moment this build knows about it.
const reported = new Set(adoption.map((feature) => feature.key));

for (const key of TelemetryService.knownFeatures()) {
if (reported.has(key)) {
continue;
}

adoption.push({
key,
kind: TelemetryService.featureKind(key, 0),
enabled: 0,
flagged: 0,
reporting: 0,
counted: 0,
installsUsing: 0,
total: 0,
});
}

// Same order the query asked for, applied again now that the unreported
// ones have been folded in.
return adoption.sort(
(a, b) => b.total - a.total || a.key.localeCompare(b.key),
);
}

// Every feature this build can report, whether or not a panel in the fleet
// has sent it yet.
private static knownFeatures() {
return new Set([
...Object.keys(TelemetryService.FeatureFlags),
...Object.keys(TelemetryService.FeatureUsage),
...TelemetryService.DetectedFeatures,
]);
}

// Detected first. The `supports_*` ones are stored as settings and so are in
Expand Down Expand Up @@ -1290,46 +1376,11 @@ export class TelemetryService {
settings: Map<string, string>,
counts: TelemetryCounts,
): Record<string, TelemetryFeature> {
const usage: Record<string, number> = {
tournaments: counts.tournaments,
leagues: counts.league_seasons,
seasons: counts.seasons,
events: counts.events,
news: counts.news_articles,
highlights: counts.match_clips,
clip_renders: counts.clip_render_jobs,
system_alerts: counts.system_alerts,
awards: counts.award_recipients,
scrims: counts.scrim_requests,
matchmaking: counts.lobbies,
custom_pages: counts.custom_pages,
external_matches: counts.matches_external,
faceit_import: counts.matches_faceit,
draft_games: counts.draft_games,
demos: counts.demos,
sanctions: counts.sanctions,
api_keys: counts.api_keys,
gamedata_validations: counts.gamedata_validations,
push_notifications: counts.push_subscriptions,
utility_library: counts.utility_lineups,
utility_practice: counts.utility_sessions,
// Lineups an operator bulk-loaded rather than anybody throwing them, which
// is the only thing the switch controls.
utility_import: counts.utility_imported,
// demo_playback and live_streaming deliberately have no count. The only
// rows that record either -- match_demo_sessions, match_streams -- are
// deleted when the session ends, so counting them reports how many people
// happen to be watching during the collect, not how much the panel has
// ever used it. A panel that has served ten thousand playbacks would
// report zero. Both are in DetectedFeatures, so their adoption comes from
// `enabled` (a GPU node carrying the workload) and the null count reads as
// "not measured" rather than as "nobody uses it".
game_server_nodes: counts.nodes_total,
version_pinning: counts.nodes_pinned,
discord_bot: counts.players_discord,
steam_presence: counts.steam_presence_friends,
player_name_registration: counts.players_name_registered,
};
const usage: Record<string, number> = {};

for (const [key, count] of Object.entries(TelemetryService.FeatureUsage)) {
usage[key] = counts[count];
}

// Read back rather than switched. The GPU workloads are toggled per node,
// so on means at least one node is carrying that work; branding is on once
Expand Down
1 change: 1 addition & 0 deletions src/utility/enums/UtilityJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
export const UtilityJobs = {
ReapIdleUtilityPracticeSessions: `ReapIdleUtilityPracticeSessions`,
MineUtilityMeta: `MineUtilityMeta`,
SyncMapCallouts: `SyncMapCallouts`,
RunUtilityDriftScan: `RunUtilityDriftScan`,
BatchUtilityRenderJob: `BatchUtilityRenderJob`,
} as const;
26 changes: 26 additions & 0 deletions src/utility/jobs/SyncMapCallouts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Logger } from "@nestjs/common";
import { WorkerHost } from "@nestjs/bullmq";
import { UseQueue } from "../../utilities/QueueProcessors";
import { UtilityQueues } from "../enums/UtilityQueues";
import { UtilityCalloutsService } from "../utility-callouts.service";

@UseQueue("Utility", UtilityQueues.UtilityMeta, { concurrency: 1 })
export class SyncMapCallouts extends WorkerHost {
constructor(
private readonly logger: Logger,
private readonly callouts: UtilityCalloutsService,
) {
super();
}

async process(): Promise<void> {
try {
await this.callouts.syncAll();
} catch (error) {
this.logger.error(
`SyncMapCallouts failed: ${(error as Error)?.message}`,
(error as Error)?.stack,
);
}
}
}
14 changes: 14 additions & 0 deletions src/utility/utility-analysis.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
UtilityAnalysisService,
UtilitySightlinePairInput,
} from "./utility-analysis.service";
import { UtilityCalloutsService } from "./utility-callouts.service";
import { UtilityDriftService } from "./utility-drift.service";

@Controller("utility-analysis")
Expand All @@ -19,6 +20,7 @@ export class UtilityAnalysisController {
private readonly analysis: UtilityAnalysisService,
private readonly drift: UtilityDriftService,
private readonly meta: UtilityMetaService,
private readonly callouts: UtilityCalloutsService,
@InjectQueue(UtilityQueues.UtilityDrift) private readonly driftQueue: Queue,
) {}

Expand All @@ -41,6 +43,18 @@ export class UtilityAnalysisController {
};
}

// The daily job is the normal path; this exists for the run right after a new
// callouts tag is published, when waiting until 4am means every throw named
// in between is named from the old map.
@HasuraAction()
public async syncMapCallouts(data: { user: User }) {
if (!data.user || !isRoleAbove(data.user.role, "administrator")) {
throw new Error("only an administrator can sync map callouts");
}

return await this.callouts.syncAll();
}

@HasuraAction()
public async checkUtilitySightlines(data: {
user: User;
Expand Down
Loading
Loading