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
831 changes: 818 additions & 13 deletions generated/schema.graphql

Large diffs are not rendered by default.

1,012 changes: 986 additions & 26 deletions generated/schema.ts

Large diffs are not rendered by default.

52,412 changes: 26,757 additions & 25,655 deletions generated/types.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions hasura/enums/match-party-sources.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
insert into e_match_party_sources ("value", "description") values
('lobby', '5stack matchmaking lobby'),
('valve', 'Valve matchmaking reservation')
on conflict(value) do update set "description" = EXCLUDED."description"
41 changes: 41 additions & 0 deletions hasura/functions/match/assign_lobby_parties.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- Queue parties for 5stack matches, derived from the lobby.
CREATE OR REPLACE FUNCTION public.assign_lobby_parties(_match_id uuid) RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
-- A lobby two players happen to share says nothing about how they queued
-- for someone else's server.
IF NOT EXISTS (
SELECT 1
FROM public.matches
WHERE id = _match_id
AND source = '5stack'
) THEN
RETURN;
END IF;

UPDATE public.match_lineup_players mlp
SET party_id = lp.lobby_id,
party_source = 'lobby'
FROM public.match_lineups ml,
public.lobby_players lp
WHERE ml.id = mlp.match_lineup_id
AND ml.match_id = _match_id
AND lp.steam_id = mlp.steam_id
AND lp.status = 'Accepted'
AND mlp.party_id IS NULL
-- A lobby is only a party when someone else from it is in this match.
AND EXISTS (
SELECT 1
FROM public.match_lineup_players other
JOIN public.match_lineups other_ml
ON other_ml.id = other.match_lineup_id
JOIN public.lobby_players other_lp
ON other_lp.steam_id = other.steam_id
AND other_lp.lobby_id = lp.lobby_id
AND other_lp.status = 'Accepted'
WHERE other_ml.match_id = _match_id
AND other.steam_id IS DISTINCT FROM mlp.steam_id
);
END;
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
table:
name: e_match_party_sources
schema: public
is_enum: true
array_relationships:
- name: match_lineup_players
using:
foreign_key_constraint_on:
column: party_source
table:
name: match_lineup_players
schema: public
select_permissions:
- role: guest
permission:
columns:
- description
- value
filter: {}
comment: ""
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ select_permissions:
- checked_in
- discord_id
- placeholder_name
- party_source
- id
- match_lineup_id
- party_id
filter: {}
comment: ""
update_permissions:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
table:
name: v_player_queue_partners
schema: public
object_relationships:
- name: player
using:
manual_configuration:
column_mapping:
steam_id: steam_id
insertion_order: null
remote_table:
name: players
schema: public
- name: partner
using:
manual_configuration:
column_mapping:
partner_steam_id: steam_id
insertion_order: null
remote_table:
name: players
schema: public
select_permissions:
- role: guest
permission:
columns:
- steam_id
- partner_steam_id
- matches_together
- wins_together
- first_played_at
- last_played_at
filter: {}
allow_aggregations: true
comment: ""
2 changes: 2 additions & 0 deletions hasura/metadata/databases/default/tables/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
- "!include public_e_match_clip_visibility.yaml"
- "!include public_e_match_map_status.yaml"
- "!include public_e_match_mode.yaml"
- "!include public_e_match_party_sources.yaml"
- "!include public_e_match_status.yaml"
- "!include public_e_match_types.yaml"
- "!include public_e_notification_types.yaml"
Expand Down Expand Up @@ -177,6 +178,7 @@
- "!include public_v_player_match_performance.yaml"
- "!include public_v_player_match_rating.yaml"
- "!include public_v_player_multi_kills.yaml"
- "!include public_v_player_queue_partners.yaml"
- "!include public_v_player_weapon_damage.yaml"
- "!include public_v_player_weapon_kills.yaml"
- "!include public_v_pool_maps.yaml"
Expand Down
31 changes: 31 additions & 0 deletions hasura/migrations/default/1874000000100_match_parties/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
ALTER TABLE public.pending_match_imports
DROP COLUMN IF EXISTS parties;

DROP VIEW IF EXISTS public.v_player_queue_partners;

DROP TRIGGER IF EXISTS tai_match_lineup_players_parties ON public.match_lineup_players;
DROP FUNCTION IF EXISTS public.tai_match_lineup_players_parties();
DROP FUNCTION IF EXISTS public.assign_lobby_parties(uuid);

DROP INDEX IF EXISTS public.idx_match_lineup_players_party;

ALTER TABLE public.match_lineup_players
DROP COLUMN IF EXISTS party_source,
DROP COLUMN IF EXISTS party_id;

DROP TABLE IF EXISTS public.e_match_party_sources;

-- HasuraService.apply skips a boot-phase object whose digest is unchanged, so
-- the digests must go too or a forward deploy leaves the view dropped.
DO $$
BEGIN
IF to_regclass('migration_hashes.hashes') IS NOT NULL THEN
DELETE FROM migration_hashes.hashes
WHERE name IN (
'hasura/enums/match-party-sources',
'hasura/functions/match/assign_lobby_parties',
'hasura/triggers/match_lineup_players',
'hasura/views/v_player_queue_partners'
);
END IF;
END $$;
27 changes: 27 additions & 0 deletions hasura/migrations/default/1874000000100_match_parties/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS public.e_match_party_sources (
value text NOT NULL PRIMARY KEY,
description text NOT NULL
);

INSERT INTO public.e_match_party_sources (value, description) VALUES
('lobby', '5stack matchmaking lobby'),
('valve', 'Valve matchmaking reservation')
ON CONFLICT (value) DO NOTHING;

-- Solo queuers stay NULL. No FK to lobbies: tad_lobby_players deletes the
-- lobby row once the last member leaves, which would blank out the history of
-- every match it ever played.
ALTER TABLE public.match_lineup_players
ADD COLUMN IF NOT EXISTS party_id uuid,
ADD COLUMN IF NOT EXISTS party_source text
REFERENCES public.e_match_party_sources(value)
ON UPDATE cascade ON DELETE set null;

CREATE INDEX IF NOT EXISTS idx_match_lineup_players_party
ON public.match_lineup_players (party_id)
WHERE party_id IS NOT NULL;

-- The reservation is only available at share-code resolve time, long before
-- the match row exists.
ALTER TABLE public.pending_match_imports
ADD COLUMN IF NOT EXISTS parties jsonb;
28 changes: 28 additions & 0 deletions hasura/triggers/match_lineup_players.sql
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,31 @@ $$;

DROP TRIGGER IF EXISTS tad_match_lineup_players ON public.match_lineup_players;
CREATE TRIGGER tad_match_lineup_players AFTER DELETE ON public.match_lineup_players FOR EACH ROW EXECUTE FUNCTION public.tad_match_lineup_players();

CREATE OR REPLACE FUNCTION public.tai_match_lineup_players_parties() RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_match_id uuid;
BEGIN
FOR v_match_id IN
SELECT DISTINCT ml.match_id
FROM new_rows nr
JOIN public.match_lineups ml ON ml.id = nr.match_lineup_id
WHERE ml.match_id IS NOT NULL
LOOP
PERFORM public.assign_lobby_parties(v_match_id);
END LOOP;

RETURN NULL;
END;
$$;

-- Statement level, not per row: assign_lobby_parties asks whether anyone else
-- from the lobby is in this match, which is never true on the first row of a
-- bulk insert.
DROP TRIGGER IF EXISTS tai_match_lineup_players_parties ON public.match_lineup_players;
CREATE TRIGGER tai_match_lineup_players_parties
AFTER INSERT ON public.match_lineup_players
REFERENCING NEW TABLE AS new_rows
FOR EACH STATEMENT EXECUTE FUNCTION public.tai_match_lineup_players_parties();
31 changes: 31 additions & 0 deletions hasura/views/v_player_queue_partners.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-- Ordered pairs (both directions) so a profile filters on steam_id alone.
--
-- Joined on party_id AND same match, never party_id alone: a 5stack party_id is
-- the lobby id and survives across matches, so party_id by itself would fuse
-- every match that lobby ever played into one pair. Not narrowed to a single
-- lineup either — a lobby that fills the whole match is split across both.
CREATE OR REPLACE VIEW public.v_player_queue_partners AS
SELECT a.steam_id,
b.steam_id AS partner_steam_id,
count(DISTINCT m.id)::int AS matches_together,
count(DISTINCT m.id) FILTER (
WHERE m.winning_lineup_id = a.match_lineup_id
)::int AS wins_together,
min(m.effective_at) AS first_played_at,
max(m.effective_at) AS last_played_at
FROM public.match_lineup_players a
JOIN public.match_lineups mla
ON mla.id = a.match_lineup_id
JOIN public.matches m
ON m.id = mla.match_id
JOIN public.match_lineups mlb
ON mlb.match_id = m.id
JOIN public.match_lineup_players b
ON b.match_lineup_id = mlb.id
AND b.party_id = a.party_id
AND b.steam_id <> a.steam_id
WHERE a.party_id IS NOT NULL
AND a.steam_id IS NOT NULL
AND b.steam_id IS NOT NULL
AND m.status IN ('Finished', 'Tie', 'Forfeit', 'Surrendered')
GROUP BY a.steam_id, b.steam_id;
1 change: 1 addition & 0 deletions src/demos/demo-parser.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type ParsedKitDrop = {
export type ParsedPlayer = {
steam_id: string;
name: string;
starting_side?: string;
rank?: number;
rank_type?: number;
previous_rank?: number;
Expand Down
13 changes: 5 additions & 8 deletions src/faceit/faceit-match-import.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,11 @@ export class FaceitMatchImportService {
throw new Error("demo parse failed");
}

return this.matchImport.importExternalDemo(
parsed,
"faceit",
matchId,
resourceUrl,
startedAt,
matchId,
);
return this.matchImport.importExternalDemo(parsed, "faceit", matchId, {
demoUrl: resourceUrl,
matchStartTime: startedAt,
externalId: matchId,
});
}

public async pollForPlayer(steamId: string): Promise<number> {
Expand Down
24 changes: 19 additions & 5 deletions src/steam-match-history/jobs/ParseImportedDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PostgresService } from "../../postgres/postgres.service";
import { DemoParserService } from "../../demos/demo-parser.service";
import { SteamMatchHistoryQueues } from "../enums/SteamMatchHistoryQueues";
import { MatchImportService } from "../match-import.service";
import { MatchParty } from "../types/MatchParty";

export type ParseImportedDemoPayload = {
valve_match_id: string;
Expand All @@ -15,6 +16,7 @@ export type ParseImportedDemoPayload = {
share_code?: string;
demo_url?: string | null;
match_start_time?: string | null;
parties?: MatchParty[] | null;
};

@UseQueue("SteamMatchHistory", SteamMatchHistoryQueues.ParseImportedDemo, {
Expand Down Expand Up @@ -55,9 +57,10 @@ export class ParseImportedDemo extends WorkerHost {
share_code: string;
demo_url: string | null;
match_start_time: string | null;
parties: MatchParty[] | null;
}>
>(
`SELECT share_code, demo_url, match_start_time
`SELECT share_code, demo_url, match_start_time, parties
FROM public.pending_match_imports
WHERE valve_match_id = $1::numeric`,
[valve_match_id],
Expand All @@ -73,6 +76,7 @@ export class ParseImportedDemo extends WorkerHost {
row?.match_start_time ?? job.data.match_start_time ?? null;

const demoUrl = row?.demo_url ?? job.data.demo_url ?? null;
const parties = row?.parties ?? job.data.parties ?? null;

if (!demoUrl) {
this.logger.warn(
Expand All @@ -87,7 +91,13 @@ export class ParseImportedDemo extends WorkerHost {
[valve_match_id],
);

await this.runImport(valve_match_id, shareCode, demoUrl, matchStartTime);
await this.runImport(
valve_match_id,
shareCode,
demoUrl,
matchStartTime,
parties,
);
} catch (err) {
const lastAttempt =
(job.attemptsMade ?? 0) >= (job.opts.attempts ?? 1) - 1;
Expand All @@ -106,6 +116,7 @@ export class ParseImportedDemo extends WorkerHost {
shareCode: string | null,
demoUrl: string,
matchStartTime: string | null,
parties: MatchParty[] | null,
): Promise<void> {
const parsed = await this.demoParser.parseFromUrl(demoUrl);
if (!parsed) {
Expand All @@ -116,9 +127,12 @@ export class ParseImportedDemo extends WorkerHost {
parsed,
"valve",
shareCode ?? valveMatchId,
demoUrl,
matchStartTime,
valveMatchId,
{
demoUrl,
matchStartTime,
externalId: valveMatchId,
parties,
},
);
if (!result.matchId) {
throw new Error(result.skipped ?? "import failed");
Expand Down
9 changes: 5 additions & 4 deletions src/steam-match-history/jobs/ProcessUploadedDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ export class ProcessUploadedDemo extends WorkerHost {
parsed,
"valve",
dedupeKey,
undefined,
null,
dedupeKey,
key,
{
matchStartTime: null,
externalId: dedupeKey,
sourceObjectKey: key,
},
);

this.logger.log(
Expand Down
Loading
Loading