From 9aa5fcbecfca2b5102dbbc3431ee02f335f70137 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:59:34 +0200 Subject: [PATCH] Remove unused --- app/db/json-columns.ts | 1 - app/db/tables.ts | 2 - .../BroadcastRepository.server.test.ts | 62 +--------- .../broadcast/BroadcastRepository.server.ts | 67 ++--------- .../to.$id.broadcast.$channel.server.ts | 44 +------ app/features/broadcast/broadcast-constants.ts | 20 ++-- app/features/broadcast/broadcast-schemas.ts | 44 +------ app/features/broadcast/broadcast-types.ts | 32 ++---- app/features/broadcast/broadcast-utils.ts | 6 + .../components/BroadcastOverlay.module.css | 6 +- .../components/CastersScene.module.css | 7 ++ .../broadcast/components/CastersScene.tsx | 11 +- .../broadcast/components/GameScene.tsx | 15 ++- .../core/BroadcastSnapshot.server.ts | 76 ++++--------- .../broadcast/core/TeamOverrides.test.ts | 107 ------------------ app/features/broadcast/core/TeamOverrides.ts | 73 ------------ .../to.$id.broadcast.$channel.module.css | 6 - .../routes/to.$id.broadcast.$channel.tsx | 82 +++----------- locales/en/broadcast.json | 2 - locales/en/forms.json | 4 +- .../20261028184547-tournament-broadcast.ts | 3 +- 21 files changed, 117 insertions(+), 553 deletions(-) delete mode 100644 app/features/broadcast/core/TeamOverrides.test.ts delete mode 100644 app/features/broadcast/core/TeamOverrides.ts diff --git a/app/db/json-columns.ts b/app/db/json-columns.ts index bad53d0d7..a151ef228 100644 --- a/app/db/json-columns.ts +++ b/app/db/json-columns.ts @@ -25,7 +25,6 @@ export const JSON_COLUMNS: ReadonlySet = new Set([ "Tournament.seedingSnapshot", "Tournament.settings", "TournamentAuditLog.metadata", - "TournamentBroadcast.overrides", "TournamentBroadcast.settings", "TournamentBroadcast.state", "TournamentMatch.opponentOne", diff --git a/app/db/tables.ts b/app/db/tables.ts index bec394ca3..816d494af 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -30,7 +30,6 @@ import type { } from "~/db/tables-json"; import type { ApiTokenType } from "~/features/api/api-types"; import type { - BroadcastOverrides, BroadcastSettings, BroadcastStoredState, } from "~/features/broadcast/broadcast-types"; @@ -1248,7 +1247,6 @@ export interface TournamentBroadcast { /** Theme, not staged */ settings: JSONColumnType; /** Team and player values the producer edited over the tournament's, not staged */ - overrides: JSONColumnType; /** Chat of the tournament's organizers and streamers producing the broadcast, created once the cast account is set */ chatRoomId: number | null; createdAt: Generated; diff --git a/app/features/broadcast/BroadcastRepository.server.test.ts b/app/features/broadcast/BroadcastRepository.server.test.ts index 14a001de1..95a120b05 100644 --- a/app/features/broadcast/BroadcastRepository.server.test.ts +++ b/app/features/broadcast/BroadcastRepository.server.test.ts @@ -74,7 +74,7 @@ describe("BroadcastRepository.updateSettings", () => { }); await BroadcastRepository.updateSettings({ ...key, - patch: { headline: "Finals at 18:00", casters: ["Sendou", "Kalle"] }, + patch: { headline: "Finals at 18:00", casterIds: [1, 2] }, }); const broadcast = await BroadcastRepository.findByChannel(key); @@ -82,7 +82,7 @@ describe("BroadcastRepository.updateSettings", () => { css: "body { color: red; }", stinger: "burst", headline: "Finals at 18:00", - casters: ["Sendou", "Kalle"], + casterIds: [1, 2], }); }); }); @@ -130,64 +130,6 @@ describe("BroadcastRepository.replaceState", () => { }); }); -describe("BroadcastRepository.updateTeamOverrides", () => { - beforeEach(async () => { - await users.create(1); - }); - - test("keeps the overrides of other teams", async () => { - const key = await createBroadcastKey(); - - await BroadcastRepository.updateTeamOverrides({ - ...key, - teamId: 1, - team: { name: "Team One" }, - players: [{ userId: 10, override: { weapons: [0, 10] } }], - }); - await BroadcastRepository.updateTeamOverrides({ - ...key, - teamId: 2, - team: { name: "Team Two" }, - players: [{ userId: 20, override: { name: "Twenty" } }], - }); - - const broadcast = await BroadcastRepository.findByChannel(key); - expect(broadcast?.overrides).toEqual({ - teams: { 1: { name: "Team One" }, 2: { name: "Team Two" } }, - players: { 10: { weapons: [0, 10] }, 20: { name: "Twenty" } }, - }); - }); - - test("removes the entries set to null", async () => { - const key = await createBroadcastKey(); - await BroadcastRepository.updateTeamOverrides({ - ...key, - teamId: 1, - team: { name: "Team One" }, - players: [ - { userId: 10, override: { name: "Ten" } }, - { userId: 11, override: { name: "Eleven" } }, - ], - }); - - await BroadcastRepository.updateTeamOverrides({ - ...key, - teamId: 1, - team: null, - players: [ - { userId: 10, override: null }, - { userId: 11, override: { name: "Eleven" } }, - ], - }); - - const broadcast = await BroadcastRepository.findByChannel(key); - expect(broadcast?.overrides).toEqual({ - teams: {}, - players: { 11: { name: "Eleven" } }, - }); - }); -}); - describe("BroadcastRepository.updateLiveResults", () => { beforeEach(async () => { await users.create(1); diff --git a/app/features/broadcast/BroadcastRepository.server.ts b/app/features/broadcast/BroadcastRepository.server.ts index 91f79e8b8..eafe1504d 100644 --- a/app/features/broadcast/BroadcastRepository.server.ts +++ b/app/features/broadcast/BroadcastRepository.server.ts @@ -10,15 +10,12 @@ import { } from "~/utils/kysely.server"; import { BROADCAST_WEAPONS_PER_PLAYER, - DEFAULT_BROADCAST_OVERRIDES, DEFAULT_BROADCAST_SETTINGS, } from "./broadcast-constants"; import type { BroadcastChannel, - BroadcastPlayerOverride, BroadcastSettings, BroadcastStoredState, - BroadcastTeamOverride, LiveResults, ProducerState, } from "./broadcast-types"; @@ -29,14 +26,13 @@ interface BroadcastKey { channel: Pick; } -/** Producer state, settings, edited values and chat room of the channel's broadcast, `undefined` before anything was written to it. */ +/** Producer state, settings and chat room of the channel's broadcast, `undefined` before anything was written to it. */ export function findByChannel({ tournamentId, channel }: BroadcastKey) { return db .selectFrom("TournamentBroadcast") .select([ "TournamentBroadcast.state", "TournamentBroadcast.settings", - "TournamentBroadcast.overrides", "TournamentBroadcast.chatRoomId", ]) .where("TournamentBroadcast.tournamentId", "=", tournamentId) @@ -76,6 +72,17 @@ export async function findAllByChatRoomIds(chatRoomIds: number[]) { .execute(); } +/** Casters by their user ids, in no particular order. */ +export function findAllCastersByUserIds(userIds: number[]) { + if (userIds.length === 0) return Promise.resolve([]); + + return db + .selectFrom("User") + .select(["User.id as userId", "User.username as name", "User.pronouns"]) + .where("User.id", "in", userIds) + .execute(); +} + // xxx: do we want to show country? /** Members of the teams with their top weapons (favorites first, then the pool's order), owner first then in joining order. */ export function findAllPlayersByTournamentTeamIds(tournamentTeamIds: number[]) { @@ -195,55 +202,6 @@ export async function updateSettings({ .execute(); } -/** - * Sets the edited values of one team and its players key by key, so edits of other teams made meanwhile stay. - * `null` removes the entry, going back to the tournament's values. - */ -export async function updateTeamOverrides({ - teamId, - team, - players, - ...key -}: BroadcastKey & { - teamId: number; - team: BroadcastTeamOverride | null; - players: Array<{ userId: number; override: BroadcastPlayerOverride | null }>; -}) { - await insertIfMissing(key); - - const entries = [ - { path: `$.teams."${teamId}"`, value: team }, - ...players.map((player) => ({ - path: `$.players."${player.userId}"`, - value: player.override, - })), - ]; - const removed = entries.filter((entry) => entry.value === null); - const set = entries.filter((entry) => entry.value !== null); - - let overrides = sql`"TournamentBroadcast"."overrides"`; - if (removed.length > 0) { - overrides = sql`json_remove(${overrides}, ${sql.join( - removed.map((entry) => sql`${entry.path}`), - )})`; - } - if (set.length > 0) { - overrides = sql`json_set(${overrides}, ${sql.join( - set.map( - (entry) => sql`${entry.path}, json(${JSON.stringify(entry.value)})`, - ), - )})`; - } - - await db - .updateTable("TournamentBroadcast") - .set({ overrides }) - .where("TournamentBroadcast.tournamentId", "=", key.tournamentId) - .where("TournamentBroadcast.platform", "=", key.channel.platform) - .where("TournamentBroadcast.account", "=", key.channel.account) - .execute(); -} - /** * Gives the channel's broadcast its chat room, creating the broadcast too when nothing was written to it yet. * Returns the room's id and whether it was created now. @@ -295,7 +253,6 @@ function insertIfMissing( state: JSON.stringify(Producer.initialState()), // xxx: autofill settings (theme) from the same channel's broadcast in the last tournament of this series settings: JSON.stringify(DEFAULT_BROADCAST_SETTINGS), - overrides: JSON.stringify(DEFAULT_BROADCAST_OVERRIDES), }) .onConflict((oc) => oc.columns(["tournamentId", "platform", "account"]).doNothing(), diff --git a/app/features/broadcast/actions/to.$id.broadcast.$channel.server.ts b/app/features/broadcast/actions/to.$id.broadcast.$channel.server.ts index 3af007044..0aab0d924 100644 --- a/app/features/broadcast/actions/to.$id.broadcast.$channel.server.ts +++ b/app/features/broadcast/actions/to.$id.broadcast.$channel.server.ts @@ -10,10 +10,8 @@ import { } from "../broadcast-schemas"; import { requireBroadcastProducer } from "../broadcast-utils.server"; import * as BroadcastHub from "../core/BroadcastHub.server"; -import * as BroadcastSnapshot from "../core/BroadcastSnapshot.server"; import * as CastChannel from "../core/CastChannel"; import * as Producer from "../core/Producer"; -import * as TeamOverrides from "../core/TeamOverrides"; const STALE_TAKE_MESSAGE = "Someone else took in the meantime, check what is live now"; @@ -87,46 +85,6 @@ export const action = defineAction( }); break; } - case "UPDATE_TEAM": { - const defaults = notFoundIfNullish( - await BroadcastSnapshot.findTeamValues({ - tournament, - teamId: body.teamId, - }), - ); - - await BroadcastRepository.updateTeamOverrides({ - ...key, - teamId: body.teamId, - ...TeamOverrides.fromSubmitted({ - defaults, - submitted: { - name: body.name, - players: body.players.map((player) => ({ - userId: player.userId, - name: player.name, - weapons: player.weapons.map((weapon) => weapon.id), - })), - }, - }), - }); - break; - } - case "RESET_TEAM": { - const defaults = notFoundIfNullish( - await BroadcastSnapshot.findTeamValues({ - tournament, - teamId: body.teamId, - }), - ); - - await BroadcastRepository.updateTeamOverrides({ - ...key, - teamId: body.teamId, - ...TeamOverrides.fromSubmitted({ defaults, submitted: defaults }), - }); - break; - } case "UPDATE_THEME": { await BroadcastRepository.updateSettings({ ...key, @@ -137,7 +95,7 @@ export const action = defineAction( case "UPDATE_INFO": { await BroadcastRepository.updateSettings({ ...key, - patch: { headline: body.headline ?? "", casters: body.casters }, + patch: { headline: body.headline ?? "", casterIds: body.casterIds }, }); break; } diff --git a/app/features/broadcast/broadcast-constants.ts b/app/features/broadcast/broadcast-constants.ts index 62fbc3855..0f7c2aae0 100644 --- a/app/features/broadcast/broadcast-constants.ts +++ b/app/features/broadcast/broadcast-constants.ts @@ -160,7 +160,14 @@ export const BROADCAST_SCENE_PARTS = { "stage", "winner", ], - casters: ["header", "tournament-name", "title", "camera", "caster-name"], + casters: [ + "header", + "tournament-name", + "title", + "camera", + "caster-name", + "caster-pronouns", + ], replays: ["corner", "replay-bug", "replay-dot", "replay-label", "game"], hold: [ "tournament-logo", @@ -218,7 +225,7 @@ export const DEFAULT_BROADCAST_SETTINGS = { css: "", stinger: "versus", headline: "", - casters: [], + casterIds: [], } as const satisfies BroadcastSettings; export const BROADCAST_HEADLINE_MAX_LENGTH = 100; @@ -226,17 +233,8 @@ export const BROADCAST_HEADLINE_MAX_LENGTH = 100; // xxx: or 2? export const BROADCAST_MAX_CASTERS = 3; -export const BROADCAST_CASTER_NAME_MAX_LENGTH = 40; - -export const DEFAULT_BROADCAST_OVERRIDES = { teams: {}, players: {} } as const; - export const BROADCAST_WEAPONS_PER_PLAYER = 3; -export const BROADCAST_PLAYER_NAME_MAX_LENGTH = 32; - -/** Upper bound of the edit form's roster, rosters are capped well below by the tournament. */ -export const BROADCAST_MAX_PLAYERS_PER_TEAM = 20; - /** Days the producers' chat lives from the tournament's start, or from when the channel was set if that's later. */ export const BROADCAST_CHAT_ROOM_LIFESPAN_DAYS = 7; diff --git a/app/features/broadcast/broadcast-schemas.ts b/app/features/broadcast/broadcast-schemas.ts index 5235b25d7..8d57b4bed 100644 --- a/app/features/broadcast/broadcast-schemas.ts +++ b/app/features/broadcast/broadcast-schemas.ts @@ -1,29 +1,21 @@ import * as v from "valibot"; -import { TOURNAMENT } from "~/features/tournament/tournament-constants"; import { array, - fieldset, - idConstant, select, stringConstant, textAreaOptional, - textField, textFieldOptional, - weaponPool, + userSearch, } from "~/form/fields"; import { _action, coerceNumber, id } from "~/utils/schema"; import { - BROADCAST_CASTER_NAME_MAX_LENGTH, BROADCAST_CSS_MAX_LENGTH, BROADCAST_HEADLINE_MAX_LENGTH, BROADCAST_INK_COLORS, BROADCAST_MAX_CASTERS, - BROADCAST_MAX_PLAYERS_PER_TEAM, - BROADCAST_PLAYER_NAME_MAX_LENGTH, BROADCAST_SCENES, BROADCAST_STINGERS, BROADCAST_TRANSITIONS, - BROADCAST_WEAPONS_PER_PLAYER, } from "./broadcast-constants"; const nonNegativeInteger = v.pipe(coerceNumber(), v.integer(), v.minValue(0)); @@ -61,39 +53,11 @@ export const broadcastInfoFormSchema = v.object({ bottomText: "bottomTexts.broadcastHeadline", maxLength: BROADCAST_HEADLINE_MAX_LENGTH, }), - casters: array({ + casterIds: array({ label: "labels.broadcastCasters", bottomText: "bottomTexts.broadcastCasters", max: BROADCAST_MAX_CASTERS, - field: textField({ maxLength: BROADCAST_CASTER_NAME_MAX_LENGTH }), - }), -}); - -export const broadcastTeamFormSchema = v.object({ - _action: stringConstant("UPDATE_TEAM"), - teamId: idConstant(), - name: textField({ - label: "labels.teamName", - maxLength: TOURNAMENT.TEAM_NAME_MAX_LENGTH, - }), - players: array({ - label: "labels.broadcastPlayers", - max: BROADCAST_MAX_PLAYERS_PER_TEAM, - addable: false, - field: fieldset({ - fields: v.object({ - userId: idConstant(), - name: textField({ - label: "labels.name", - maxLength: BROADCAST_PLAYER_NAME_MAX_LENGTH, - }), - weapons: weaponPool({ - label: "labels.broadcastWeapons", - maxCount: BROADCAST_WEAPONS_PER_PLAYER, - disableFavorites: true, - }), - }), - }), + field: userSearch({ label: "labels.user" }), }), }); @@ -122,8 +86,6 @@ export const broadcastActionSchema = v.union([ matchId: id, resultCount: nonNegativeInteger, }), - v.object({ _action: _action("RESET_TEAM"), teamId: id }), broadcastThemeFormSchema, broadcastInfoFormSchema, - broadcastTeamFormSchema, ]); diff --git a/app/features/broadcast/broadcast-types.ts b/app/features/broadcast/broadcast-types.ts index f5cf6cc0c..9a3a6070f 100644 --- a/app/features/broadcast/broadcast-types.ts +++ b/app/features/broadcast/broadcast-types.ts @@ -1,3 +1,4 @@ +import type { Pronouns } from "~/db/tables-json"; import type { StreamPlatform } from "~/features/live-streams/live-streams-constants"; import type { MainWeaponId, @@ -67,25 +68,8 @@ export interface BroadcastSettings { stinger: BroadcastStinger; /** Message of the full screen scenes, e.g. "Top 8 starts at 18:00 CEST". */ headline: string; - /** Names under the camera frames of the casters scene, one frame per caster. */ - casters: string[]; -} - -/** Team and player values the producer edited, shown instead of the tournament's in both live and next. */ -export interface BroadcastOverrides { - /** By tournament team id. */ - teams: Record; - /** By user id. */ - players: Record; -} - -export interface BroadcastTeamOverride { - name?: string; -} - -export interface BroadcastPlayerOverride { - name?: string; - weapons?: MainWeaponId[]; + /** Users under the camera frames of the casters scene, one frame per caster. */ + casterIds: number[]; } /** The cast account a broadcast is for. */ @@ -101,6 +85,8 @@ export interface BroadcastChannel { export interface BroadcastState extends BroadcastStoredState { v: 1; settings: BroadcastSettings; + /** Users of `settings.casterIds` in order, ones that no longer exist left out. */ + casters: BroadcastCaster[]; tournament: { name: string; logoUrl: string; @@ -163,8 +149,12 @@ export interface BroadcastTeam { logoUrl: string | null; seed: number | null; players: BroadcastPlayer[]; - /** The producer edited the team's or its players' values. */ - isEdited: boolean; +} + +export interface BroadcastCaster { + userId: number; + name: string; + pronouns: Pronouns | null; } export interface BroadcastPlayer { diff --git a/app/features/broadcast/broadcast-utils.ts b/app/features/broadcast/broadcast-utils.ts index 88a999584..75b4f72f3 100644 --- a/app/features/broadcast/broadcast-utils.ts +++ b/app/features/broadcast/broadcast-utils.ts @@ -1,3 +1,4 @@ +import type { Pronouns } from "~/db/tables-json"; import { BROADCAST_INK_COLORS, type BROADCAST_SCENE_PARTS, @@ -74,6 +75,11 @@ export function inkColorVars( }; } +/** Pronouns as shown next to a name, e.g. "he/him". */ +export function pronounsLabel(pronouns: Pronouns) { + return `${pronouns.subject}/${pronouns.object}`; +} + /** Bracket and round, e.g. "Main bracket ยท Winners round 2". */ export function roundLabel( match: Pick, diff --git a/app/features/broadcast/components/BroadcastOverlay.module.css b/app/features/broadcast/components/BroadcastOverlay.module.css index 6680fbcff..76a704950 100644 --- a/app/features/broadcast/components/BroadcastOverlay.module.css +++ b/app/features/broadcast/components/BroadcastOverlay.module.css @@ -12,7 +12,11 @@ --bc-surface-high: var(--color-bg-higher); --bc-border: var(--color-border); --bc-text: var(--color-text); - --bc-text-muted: color-mix(in oklch, var(--color-text) 45%, var(--color-text-high)); + --bc-text-muted: color-mix( + in oklch, + var(--color-text) 45%, + var(--color-text-high) + ); --bc-accent: var(--color-fill-accent); --bc-accent-text: var(--color-fg-on-accent); --bc-team-a: var(--color-chart-alpha); diff --git a/app/features/broadcast/components/CastersScene.module.css b/app/features/broadcast/components/CastersScene.module.css index a49a28ffe..ad39d91d8 100644 --- a/app/features/broadcast/components/CastersScene.module.css +++ b/app/features/broadcast/components/CastersScene.module.css @@ -68,6 +68,13 @@ white-space: nowrap; } +.pronouns { + margin-inline-start: 14px; + color: var(--bc-text-muted); + font-size: 26px; + font-weight: 600; +} + .matchup { --bc-enter-y: var(--bc-enter-distance); diff --git a/app/features/broadcast/components/CastersScene.tsx b/app/features/broadcast/components/CastersScene.tsx index 6138ed5f3..c4d57cb5e 100644 --- a/app/features/broadcast/components/CastersScene.tsx +++ b/app/features/broadcast/components/CastersScene.tsx @@ -2,7 +2,7 @@ import type * as React from "react"; import { useTranslation } from "react-i18next"; import { BROADCAST_CANVAS } from "../broadcast-constants"; import type { BroadcastState, BroadcastView } from "../broadcast-types"; -import { scenePart } from "../broadcast-utils"; +import { pronounsLabel, scenePart } from "../broadcast-utils"; import styles from "./CastersScene.module.css"; import { Matchup } from "./Matchup"; import { SceneBackdrop } from "./SceneBackdrop"; @@ -41,7 +41,7 @@ export function CastersScene({ view: BroadcastView; }) { const { t } = useTranslation(["broadcast"]); - const casters = state.settings.casters; + const casters = state.casters; const frames = FRAME_LAYOUTS[casters.length] ?? FRAME_LAYOUTS[DEFAULT_FRAME_COUNT]; @@ -75,7 +75,12 @@ export function CastersScene({ > {casters[i] ? (
- {casters[i]} + {casters[i].name} + {casters[i].pronouns ? ( + + {pronounsLabel(casters[i].pronouns)} + + ) : null}
) : null} diff --git a/app/features/broadcast/components/GameScene.tsx b/app/features/broadcast/components/GameScene.tsx index 9f64d9d49..2a7d80de4 100644 --- a/app/features/broadcast/components/GameScene.tsx +++ b/app/features/broadcast/components/GameScene.tsx @@ -7,7 +7,12 @@ import type { BroadcastState, BroadcastView, } from "../broadcast-types"; -import { type MatchSide, matchSides, scenePart } from "../broadcast-utils"; +import { + type MatchSide, + matchSides, + pronounsLabel, + scenePart, +} from "../broadcast-utils"; import styles from "./GameScene.module.css"; import { SetLength } from "./SetLength"; @@ -89,7 +94,13 @@ function TeamRow({ function InfoBar({ state }: { state: BroadcastState }) { const match = state.match; - const casters = state.settings.casters.join(" // "); + const casters = state.casters + .map((caster) => + caster.pronouns + ? `${caster.name} (${pronounsLabel(caster.pronouns)})` + : caster.name, + ) + .join(" // "); return (
diff --git a/app/features/broadcast/core/BroadcastSnapshot.server.ts b/app/features/broadcast/core/BroadcastSnapshot.server.ts index b874d636f..6a5ee62b2 100644 --- a/app/features/broadcast/core/BroadcastSnapshot.server.ts +++ b/app/features/broadcast/core/BroadcastSnapshot.server.ts @@ -6,14 +6,11 @@ import * as TournamentMatchRepository from "~/features/tournament-match/Tourname import { inGameNameWithoutDiscriminator } from "~/utils/strings"; import { avatarUrl } from "~/utils/urls"; import * as BroadcastRepository from "../BroadcastRepository.server"; -import { - DEFAULT_BROADCAST_OVERRIDES, - DEFAULT_BROADCAST_SETTINGS, -} from "../broadcast-constants"; +import { DEFAULT_BROADCAST_SETTINGS } from "../broadcast-constants"; import type { + BroadcastCaster, BroadcastChannel, BroadcastMatch, - BroadcastOverrides, BroadcastPlayer, BroadcastState, BroadcastTeam, @@ -22,7 +19,6 @@ import type { import * as CastChannel from "./CastChannel"; import * as Producer from "./Producer"; import * as SetProgress from "./SetProgress"; -import * as TeamOverrides from "./TeamOverrides"; type PlayerRow = Awaited< ReturnType @@ -50,6 +46,8 @@ export async function build({ tournamentId, channel, }); + // settings added later are missing from broadcasts written before + const settings = { ...DEFAULT_BROADCAST_SETTINGS, ...stored?.settings }; const featuredMatchId = CastChannel.featuredMatchId({ castedMatchesInfo: tournament.ctx.castedMatchesInfo, channel, @@ -60,8 +58,8 @@ export async function build({ snapshot: { v: 1, ...(stored?.state ?? Producer.initialState()), - // settings added later are missing from broadcasts written before - settings: { ...DEFAULT_BROADCAST_SETTINGS, ...stored?.settings }, + settings, + casters: await casters(settings.casterIds), tournament: { name: tournament.ctx.name, logoUrl: tournament.ctx.logoUrl, @@ -70,7 +68,6 @@ export async function build({ ? await featuredMatch({ tournament, matchId: featuredMatchId, - overrides: stored?.overrides ?? DEFAULT_BROADCAST_OVERRIDES, liveResults: stored?.state.liveResults ?? null, }) : null, @@ -78,41 +75,22 @@ export async function build({ }; } -/** The team's editable values as the tournament has them, without the producer's edits. `null` for a team not in the tournament. */ -export async function findTeamValues({ - tournament, - teamId, -}: { - tournament: Tournament; - teamId: number; -}): Promise { - const tournamentTeam = tournament.teamById(teamId); - if (!tournamentTeam) return null; +async function casters(userIds: number[]): Promise { + const users = await BroadcastRepository.findAllCastersByUserIds(userIds); - const players = await BroadcastRepository.findAllPlayersByTournamentTeamIds([ - teamId, - ]); - - return { - name: tournamentTeam.name, - players: rosterPlayers({ players, tournament, tournamentTeam }).map( - (player) => { - const { userId, name, weapons } = toBroadcastPlayer(player); - return { userId, name, weapons }; - }, - ), - }; + return userIds.flatMap((userId) => { + const user = users.find((candidate) => candidate.userId === userId); + return user ? [user] : []; + }); } async function featuredMatch({ tournament, matchId, - overrides, liveResults, }: { tournament: Tournament; matchId: number; - overrides: BroadcastOverrides; liveResults: LiveResults | null; }): Promise { const match = await TournamentMatchRepository.findMatchById(matchId); @@ -153,23 +131,19 @@ async function featuredMatch({ : null; if (!opponent?.id || !tournamentTeam) return null; - return TeamOverrides.apply( - { - id: tournamentTeam.id, - name: tournamentTeam.name, - logoUrl: tournamentTeam.logoUrl, - seed: tournamentTeam.seed, - players: rosterPlayers({ - players: players.filter( - (player) => player.tournamentTeamId === tournamentTeam.id, - ), - tournament, - tournamentTeam, - }).map(toBroadcastPlayer), - isEdited: false, - }, - overrides, - ); + return { + id: tournamentTeam.id, + name: tournamentTeam.name, + logoUrl: tournamentTeam.logoUrl, + seed: tournamentTeam.seed, + players: rosterPlayers({ + players: players.filter( + (player) => player.tournamentTeamId === tournamentTeam.id, + ), + tournament, + tournamentTeam, + }).map(toBroadcastPlayer), + }; }; const names = tournament.matchContextNamesById(matchId); diff --git a/app/features/broadcast/core/TeamOverrides.test.ts b/app/features/broadcast/core/TeamOverrides.test.ts deleted file mode 100644 index 7005b750e..000000000 --- a/app/features/broadcast/core/TeamOverrides.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, test } from "vitest"; -import type { BroadcastTeam } from "../broadcast-types"; -import * as TeamOverrides from "./TeamOverrides"; - -const team: BroadcastTeam = { - id: 1, - name: "Team One", - logoUrl: null, - seed: 1, - isEdited: false, - players: [ - { - userId: 10, - name: "Ten", - avatarUrl: null, - weapons: [0, 10], - }, - { - userId: 11, - name: "Eleven", - avatarUrl: null, - weapons: [20], - }, - ], -}; - -const defaults: TeamOverrides.TeamValues = { - name: team.name, - players: team.players.map(({ userId, name, weapons }) => ({ - userId, - name, - weapons, - })), -}; - -describe("TeamOverrides.apply", () => { - test("shows the edited values over the tournament's", () => { - const applied = TeamOverrides.apply(team, { - teams: { 1: { name: "Renamed" } }, - players: { 11: { weapons: [40] } }, - }); - - expect(applied.name).toBe("Renamed"); - expect(applied.players[0]).toEqual(team.players[0]); - expect(applied.players[1]).toEqual({ ...team.players[1], weapons: [40] }); - expect(applied.isEdited).toBe(true); - }); - - test("ignores overrides of other teams and their players", () => { - const applied = TeamOverrides.apply(team, { - teams: { 2: { name: "Other" } }, - players: { 20: { name: "Other player" } }, - }); - - expect(applied).toEqual(team); - }); -}); - -describe("TeamOverrides.fromSubmitted", () => { - test("stores nothing when the submitted values are the tournament's", () => { - expect( - TeamOverrides.fromSubmitted({ defaults, submitted: defaults }), - ).toEqual({ - team: null, - players: [ - { userId: 10, override: null }, - { userId: 11, override: null }, - ], - }); - }); - - test("stores only the fields that differ", () => { - const overrides = TeamOverrides.fromSubmitted({ - defaults, - submitted: { - name: "Renamed", - players: [ - { userId: 10, name: "Ten", weapons: [10, 0] }, - { userId: 11, name: "Eleventh", weapons: [20] }, - ], - }, - }); - - expect(overrides).toEqual({ - team: { name: "Renamed" }, - players: [ - { userId: 10, override: { weapons: [10, 0] } }, - { userId: 11, override: { name: "Eleventh" } }, - ], - }); - }); - - test("ignores submitted players not on the team", () => { - const overrides = TeamOverrides.fromSubmitted({ - defaults, - submitted: { - ...defaults, - players: [ - ...defaults.players, - { userId: 99, name: "Intruder", weapons: [] }, - ], - }, - }); - - expect(overrides.players.map((player) => player.userId)).toEqual([10, 11]); - }); -}); diff --git a/app/features/broadcast/core/TeamOverrides.ts b/app/features/broadcast/core/TeamOverrides.ts deleted file mode 100644 index 147c6554b..000000000 --- a/app/features/broadcast/core/TeamOverrides.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as R from "remeda"; -import type { - BroadcastOverrides, - BroadcastPlayer, - BroadcastPlayerOverride, - BroadcastTeam, - BroadcastTeamOverride, -} from "../broadcast-types"; - -/** The editable values of a team, as the tournament has them or as the producer submitted them. */ -export interface TeamValues { - name: string; - players: Array>; -} - -/** The team with the producer's edits applied over the tournament's values. */ -export function apply( - team: BroadcastTeam, - overrides: BroadcastOverrides, -): BroadcastTeam { - const teamOverride = overrides.teams[team.id]; - - return { - ...team, - ...teamOverride, - players: team.players.map((player) => ({ - ...player, - ...overrides.players[player.userId], - })), - isEdited: - Boolean(teamOverride) || - team.players.some((player) => overrides.players[player.userId]), - }; -} - -/** - * Overrides to store for the submitted values: only what differs from the tournament's values, `null` where - * nothing does (so later changes to the tournament's values show again). Submitted players not on the team are ignored. - */ -export function fromSubmitted({ - defaults, - submitted, -}: { - defaults: TeamValues; - submitted: TeamValues; -}): { - team: BroadcastTeamOverride | null; - players: Array<{ userId: number; override: BroadcastPlayerOverride | null }>; -} { - return { - team: submitted.name !== defaults.name ? { name: submitted.name } : null, - players: defaults.players.map((player) => { - const submittedPlayer = submitted.players.find( - (candidate) => candidate.userId === player.userId, - ); - const override: BroadcastPlayerOverride = {}; - if (submittedPlayer && submittedPlayer.name !== player.name) { - override.name = submittedPlayer.name; - } - if ( - submittedPlayer && - !R.isDeepEqual(submittedPlayer.weapons, player.weapons) - ) { - override.weapons = submittedPlayer.weapons; - } - - return { - userId: player.userId, - override: R.isEmpty(override) ? null : override, - }; - }), - }; -} diff --git a/app/features/broadcast/routes/to.$id.broadcast.$channel.module.css b/app/features/broadcast/routes/to.$id.broadcast.$channel.module.css index eceab4e06..bb8c944f1 100644 --- a/app/features/broadcast/routes/to.$id.broadcast.$channel.module.css +++ b/app/features/broadcast/routes/to.$id.broadcast.$channel.module.css @@ -118,12 +118,6 @@ text-transform: uppercase; } -.teams { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); - gap: var(--s-8); -} - .viewHeader { font-size: var(--font-md); text-transform: uppercase; diff --git a/app/features/broadcast/routes/to.$id.broadcast.$channel.tsx b/app/features/broadcast/routes/to.$id.broadcast.$channel.tsx index 497b327a7..b26b9faac 100644 --- a/app/features/broadcast/routes/to.$id.broadcast.$channel.tsx +++ b/app/features/broadcast/routes/to.$id.broadcast.$channel.tsx @@ -11,7 +11,6 @@ import { Map as MapIcon, Mic, Play, - RotateCcw, TriangleAlert, Trophy, Users, @@ -43,13 +42,11 @@ import { useBroadcastStream } from "../broadcast-hooks"; import { broadcastActionSchema, broadcastInfoFormSchema, - broadcastTeamFormSchema, broadcastThemeFormSchema, } from "../broadcast-schemas"; import type { BroadcastScene, BroadcastState, - BroadcastTeam, BroadcastView, } from "../broadcast-types"; import { broadcastOverlayPage, broadcastStreamUrl } from "../broadcast-urls"; @@ -175,13 +172,13 @@ export default function BroadcastDashboardPage() { schema={broadcastInfoFormSchema} defaultValues={{ headline: state.settings.headline || null, - casters: state.settings.casters, + casterIds: state.settings.casterIds, }} > {({ FormField }) => ( <> - + )} @@ -367,27 +364,18 @@ function Teams({ state }: { state: BroadcastState }) { return (
-
-

- {t("broadcast:dashboard.teams.description")} -

- } - > - {t("broadcast:dashboard.swapSides")} - -
+ } + formClassName="self-start" + > + {t("broadcast:dashboard.swapSides")} + -
- {state.match.teams.map((team, i) => - team ? :
, - )} -
); @@ -470,50 +458,6 @@ function InkColors({ state }: { state: BroadcastState }) { ); } -function TeamEditor({ team }: { team: BroadcastTeam }) { - const { t } = useTranslation(["broadcast"]); - const defaultValues = { - teamId: team.id, - name: team.name, - players: team.players.map((player) => ({ - userId: player.userId, - name: player.name, - weapons: player.weapons.map((id) => ({ id, isFavorite: false })), - })), - }; - - return ( -
- {team.isEdited ? ( - } - formClassName="self-start" - > - {t("broadcast:dashboard.teams.reset")} - - ) : null} - - {({ FormField }) => ( - <> - - false} /> - - )} - -
- ); -} - function ResultsNotOnAir({ state }: { state: BroadcastState }) { const { t } = useTranslation(["broadcast"]); diff --git a/locales/en/broadcast.json b/locales/en/broadcast.json index 634be4bf3..8e9866872 100644 --- a/locales/en/broadcast.json +++ b/locales/en/broadcast.json @@ -18,8 +18,6 @@ "dashboard.showResults": "Show now", "dashboard.gameplay": "Gameplay footage", "dashboard.teams.header": "Teams", - "dashboard.teams.description": "Edits show on both live and next right away. Values you leave as they are keep following the tournament.", - "dashboard.teams.reset": "Reset to tournament values", "dashboard.health.sources_one": "OBS: {{count}} source connected", "dashboard.health.sources_other": "OBS: {{count}} sources connected", "dashboard.health.disconnected": "Disconnected from sendou.ink, reconnecting...", diff --git a/locales/en/forms.json b/locales/en/forms.json index a85133aae..e42c76966 100644 --- a/locales/en/forms.json +++ b/locales/en/forms.json @@ -530,8 +530,6 @@ "errors.canNotSetIfStartingNow": "Can not be set if starting now", "labels.broadcastCss": "Custom CSS", "bottomTexts.broadcastCss": "Applied after the default styles. Override the variables on [data-broadcast], redefine the bc-* @keyframes or target parts like [data-scene=\"game\"] [data-part=\"team-name\"].", - "labels.broadcastPlayers": "Players", - "labels.broadcastWeapons": "Weapons", "labels.broadcastStinger": "Stinger", "bottomTexts.broadcastStinger": "Full screen animation with the upcoming match's info, covering the scene change on Takes that use the stinger transition.", "options.broadcastStinger.versus": "Versus: the teams clash from both sides", @@ -541,5 +539,5 @@ "labels.broadcastHeadline": "Headline", "bottomTexts.broadcastHeadline": "Shown on the full screen scenes, e.g. Starting soon and Break.", "labels.broadcastCasters": "Casters", - "bottomTexts.broadcastCasters": "Shown as written, e.g. \"Sendou - he/him\". The Game scene lists them next to the round, the Casters scene has a camera frame for each (put your camera sources under the browser source in OBS)." + "bottomTexts.broadcastCasters": "Shown with the pronouns of their profile. The Game scene lists them next to the round, the Casters scene has a camera frame for each (put your camera sources under the browser source in OBS)." } diff --git a/migrations/20261028184547-tournament-broadcast.ts b/migrations/20261028184547-tournament-broadcast.ts index 7a601965e..5b14df8b2 100644 --- a/migrations/20261028184547-tournament-broadcast.ts +++ b/migrations/20261028184547-tournament-broadcast.ts @@ -1,6 +1,6 @@ import { type Kysely, sql } from "kysely"; -/** Broadcast overlays: one row per cast account of a tournament holding its producer state, theme, edited team values and chat room. */ +/** Broadcast overlays: one row per cast account of a tournament holding its producer state, theme and chat room. */ export async function up(db: Kysely): Promise { await db.transaction().execute(async (trx) => { await trx.schema @@ -13,7 +13,6 @@ export async function up(db: Kysely): Promise { .addColumn("account", "text", (col) => col.notNull()) .addColumn("state", "text", (col) => col.notNull()) .addColumn("settings", "text", (col) => col.notNull()) - .addColumn("overrides", "text", (col) => col.notNull()) .addColumn("chatRoomId", "integer", (col) => col.references("ChatRoom.id").onDelete("set null"), )