feat: redesign match play and add broadcast tooling
This commit is contained in:
47
scripts/migrations/0001-platform.sql
Normal file
47
scripts/migrations/0001-platform.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- Platform columns and tables added in the 2026 rewrite. Idempotent: safe on
|
||||
-- databases where the old migrate-platform script already ran.
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS osu_id integer;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS country_code text;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS discord_id text;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS banned_at timestamptz;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS timezone text;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS available_hours jsonb;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_osu_id_idx ON "user" (osu_id) WHERE osu_id IS NOT NULL;
|
||||
|
||||
ALTER TABLE player_rating ADD COLUMN IF NOT EXISTS provisional boolean NOT NULL DEFAULT true;
|
||||
UPDATE player_rating SET provisional = false WHERE wins + losses >= 5 AND provisional;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_rating (
|
||||
team_id uuid PRIMARY KEY REFERENCES team(id) ON DELETE CASCADE,
|
||||
elo integer NOT NULL DEFAULT 1000,
|
||||
wins integer NOT NULL DEFAULT 0,
|
||||
losses integer NOT NULL DEFAULT 0,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE mappool ADD COLUMN IF NOT EXISTS origin text;
|
||||
ALTER TABLE mappool ADD COLUMN IF NOT EXISTS rating real;
|
||||
|
||||
ALTER TABLE match ADD COLUMN IF NOT EXISTS game text NOT NULL DEFAULT 'osu';
|
||||
ALTER TABLE match ADD COLUMN IF NOT EXISTS kind text NOT NULL DEFAULT 'custom';
|
||||
ALTER TABLE match ADD COLUMN IF NOT EXISTS procedure jsonb;
|
||||
ALTER TABLE match ADD COLUMN IF NOT EXISTS deadline_at timestamptz;
|
||||
ALTER TABLE match ADD COLUMN IF NOT EXISTS elo_applied boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE match_queue ADD COLUMN IF NOT EXISTS format integer NOT NULL DEFAULT 1;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS match_event (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
match_id uuid NOT NULL REFERENCES match(id) ON DELETE CASCADE,
|
||||
type text NOT NULL,
|
||||
detail text,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_match_event_match ON match_event (match_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS system_state (
|
||||
id text PRIMARY KEY,
|
||||
draining boolean NOT NULL DEFAULT false,
|
||||
reason text,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
37
scripts/migrations/0002-auth-connections.sql
Normal file
37
scripts/migrations/0002-auth-connections.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Additive migration: old releases can still read their original email column.
|
||||
-- Runtime identity is always account(provider_id, account_id), never an email pattern.
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS contact_email text;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS root_admin boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE "user" ADD COLUMN IF NOT EXISTS merged_into_user_id text;
|
||||
ALTER TABLE account ADD COLUMN IF NOT EXISTS profile jsonb;
|
||||
|
||||
UPDATE "user" SET contact_email = lower(email)
|
||||
WHERE contact_email IS NULL AND email !~* '@(osu\.local|discord\.local|identity\.invalid)$';
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS user_contact_email_unique ON "user" (contact_email);
|
||||
|
||||
-- Fail the transaction for conflicting connections; never guess which user owns one.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS account_provider_identity_idx ON account (provider_id, account_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS account_user_provider_idx ON account (user_id, provider_id);
|
||||
|
||||
-- Profile backfill is best effort; OAuth refreshes it on the next successful sign-in.
|
||||
UPDATE account a SET profile = jsonb_build_object(
|
||||
'username', u.name, 'image', u.image, 'email', null, 'emailVerified', false,
|
||||
'countryCode', u.country_code, 'globalRank', r.osu_rank_at_seed
|
||||
)
|
||||
FROM "user" u LEFT JOIN player_rating r ON r.user_id = u.id
|
||||
WHERE a.user_id = u.id AND a.provider_id IN ('osu', 'discord') AND a.profile IS NULL;
|
||||
|
||||
-- Repair solo roster snapshots only where ownership is unambiguous. Match discovery
|
||||
-- and authorization can then use the same persisted participant roster.
|
||||
INSERT INTO team_member (team_id, user_id, role)
|
||||
SELECT t.id, t.owner_id, 'owner' FROM team t
|
||||
JOIN "user" u ON u.id = t.owner_id
|
||||
WHERE t.is_personal AND NOT EXISTS (
|
||||
SELECT 1 FROM team_member tm WHERE tm.team_id = t.id AND tm.user_id = t.owner_id
|
||||
);
|
||||
INSERT INTO match_participant_player (participant_id, user_id)
|
||||
SELECT p.id, t.owner_id FROM match_participant p JOIN team t ON t.id = p.team_id
|
||||
JOIN "user" u ON u.id = t.owner_id
|
||||
WHERE t.is_personal AND NOT EXISTS (
|
||||
SELECT 1 FROM match_participant_player pp WHERE pp.participant_id = p.id
|
||||
);
|
||||
20
scripts/migrations/0003-match-runtime.sql
Normal file
20
scripts/migrations/0003-match-runtime.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Optimistic locking for the match actor, the hosting IRC account for
|
||||
-- rejoining after a restart, and an undo log for rating changes.
|
||||
ALTER TABLE match ADD COLUMN IF NOT EXISTS revision integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE match ADD COLUMN IF NOT EXISTS irc_account text;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS rating_change (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
match_id uuid NOT NULL REFERENCES match(id) ON DELETE CASCADE,
|
||||
subject text NOT NULL,
|
||||
subject_id text NOT NULL,
|
||||
before integer NOT NULL,
|
||||
after integer NOT NULL,
|
||||
won boolean NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_rating_change_match ON rating_change (match_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_rating_change_subject ON rating_change (subject_id);
|
||||
|
||||
-- Matches the previous process left mid-flight are resumed by the runtime.
|
||||
CREATE INDEX IF NOT EXISTS idx_match_game_match_state ON match_game (match_id, state);
|
||||
7
scripts/migrations/0004-tournament-branding.sql
Normal file
7
scripts/migrations/0004-tournament-branding.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Tournament pages carry their own identity: icon, accent, forum post, links, prizes.
|
||||
ALTER TABLE tournament ADD COLUMN IF NOT EXISTS tagline text;
|
||||
ALTER TABLE tournament ADD COLUMN IF NOT EXISTS icon_url text;
|
||||
ALTER TABLE tournament ADD COLUMN IF NOT EXISTS accent_color text;
|
||||
ALTER TABLE tournament ADD COLUMN IF NOT EXISTS forum_post text;
|
||||
ALTER TABLE tournament ADD COLUMN IF NOT EXISTS links jsonb;
|
||||
ALTER TABLE tournament ADD COLUMN IF NOT EXISTS prizes jsonb;
|
||||
6
scripts/migrations/0005-openskill.sql
Normal file
6
scripts/migrations/0005-openskill.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- OpenSkill ratings: elo becomes mu, and sigma holds the uncertainty.
|
||||
-- Existing rows keep sigma NULL; the app estimates it from games played.
|
||||
ALTER TABLE player_rating ADD COLUMN IF NOT EXISTS sigma real;
|
||||
ALTER TABLE team_rating ADD COLUMN IF NOT EXISTS sigma real;
|
||||
ALTER TABLE rating_change ADD COLUMN IF NOT EXISTS sigma_before real;
|
||||
ALTER TABLE rating_change ADD COLUMN IF NOT EXISTS sigma_after real;
|
||||
18
scripts/migrations/0006-predictions.sql
Normal file
18
scripts/migrations/0006-predictions.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Spectator predictions and tournament pick'ems.
|
||||
CREATE TABLE IF NOT EXISTS prediction (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id text NOT NULL,
|
||||
match_id uuid REFERENCES match(id) ON DELETE CASCADE,
|
||||
tournament_match_id uuid REFERENCES tournament_match(id) ON DELETE CASCADE,
|
||||
winner_team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
|
||||
winner_score integer,
|
||||
loser_score integer,
|
||||
points integer,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT prediction_one_subject CHECK ((match_id IS NULL) <> (tournament_match_id IS NULL))
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_prediction_user_match ON prediction (user_id, match_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_prediction_user_tmatch ON prediction (user_id, tournament_match_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_prediction_match ON prediction (match_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_prediction_tmatch ON prediction (tournament_match_id);
|
||||
15
scripts/migrations/0007-jsonb-repair.sql
Normal file
15
scripts/migrations/0007-jsonb-repair.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Drizzle's jsonb with Bun.SQL stored objects as JSON strings ("{\"a\":1}")
|
||||
-- until src/lib/server/db/jsonb.ts. Turn those strings back into objects.
|
||||
DO $$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT table_name, column_name FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND data_type = 'jsonb'
|
||||
LOOP
|
||||
EXECUTE format(
|
||||
'UPDATE %I SET %I = (%I #>> ''{}'')::jsonb WHERE jsonb_typeof(%I) = ''string'' AND left(%I #>> ''{}'', 1) IN (''{'', ''['')',
|
||||
r.table_name, r.column_name, r.column_name, r.column_name, r.column_name
|
||||
);
|
||||
END LOOP;
|
||||
END $$;
|
||||
13
scripts/migrations/0008-broadcast.sql
Normal file
13
scripts/migrations/0008-broadcast.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Stream channels: what an overlay shows and how it is laid out.
|
||||
CREATE TABLE IF NOT EXISTS broadcast_channel (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
mode text NOT NULL DEFAULT 'auto',
|
||||
match_id uuid REFERENCES match(id) ON DELETE SET NULL,
|
||||
tournament_id uuid REFERENCES tournament(id) ON DELETE CASCADE,
|
||||
layout jsonb NOT NULL,
|
||||
agent_token_hash text,
|
||||
created_by text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
4
scripts/migrations/0009-match-lobby-kind.sql
Normal file
4
scripts/migrations/0009-match-lobby-kind.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Matches now remember where their lobby lives. Simulated lobbies are numbered
|
||||
-- from 2,090,000,000 up, far above real osu! match ids.
|
||||
UPDATE match SET config = config || '{"lobby":"simulator"}'::jsonb
|
||||
WHERE osu_lobby_id >= 2000000000 AND NOT (config ? 'lobby');
|
||||
30
scripts/migrations/0010-streaming.sql
Normal file
30
scripts/migrations/0010-streaming.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- Streaming to platforms, recordings and on-site playback.
|
||||
ALTER TABLE broadcast_channel ADD COLUMN IF NOT EXISTS format text NOT NULL DEFAULT 'landscape';
|
||||
ALTER TABLE broadcast_channel ADD COLUMN IF NOT EXISTS quality text NOT NULL DEFAULT '1080p30';
|
||||
ALTER TABLE broadcast_channel ADD COLUMN IF NOT EXISTS live boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE broadcast_channel ADD COLUMN IF NOT EXISTS record boolean NOT NULL DEFAULT true;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS broadcast_destination (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id uuid NOT NULL REFERENCES broadcast_channel(id) ON DELETE CASCADE,
|
||||
platform text NOT NULL,
|
||||
label text NOT NULL,
|
||||
ingest_url text NOT NULL,
|
||||
stream_key text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_broadcast_destination_channel ON broadcast_destination (channel_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS broadcast_session (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
channel_id uuid NOT NULL REFERENCES broadcast_channel(id) ON DELETE CASCADE,
|
||||
started_at timestamptz NOT NULL DEFAULT now(),
|
||||
ended_at timestamptz,
|
||||
seen_at timestamptz NOT NULL DEFAULT now(),
|
||||
storage_prefix text,
|
||||
duration_sec integer NOT NULL DEFAULT 0,
|
||||
chapters jsonb NOT NULL DEFAULT '[]',
|
||||
destinations jsonb NOT NULL DEFAULT '[]'
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_broadcast_session_channel ON broadcast_session (channel_id, started_at);
|
||||
Reference in New Issue
Block a user