From b59139f5fdb49cd0efb3152ab649e8e7da0959dd Mon Sep 17 00:00:00 2001 From: Kristians Laukis <7798036+LCrew@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:08:50 +0300 Subject: [PATCH 1/3] feat(draft): let a custom game mode define its own team size A match's player count came from match_options.type alone, hard-coded in four places that had to be kept in sync. A custom game mode could change the plugins and cvars a match runs under but not its shape, so a 3v3 retakes mode still demanded a full 5v5 lobby. game_modes gains players_per_team and allow_short_handed_start. A new resolver, get_match_options_min_players(mo), coalesces the size a match actually launched at, then the mode's, then the type's -- and every gate that gated on the type now reads it instead: match_min/max_players_per_lineup, check_match_has_min_players, is_match_lineup_ready, the match_lineup_players removal trigger, and draft_games capacity. A mode may also permit starting a draft lobby before both sides are full. The new startDraftGame action narrows the lobby to the players who turned up *before* flipping it to Filled -- doing it the other way round would leave a draft stuck in CreatingMatch that beginDraft then refuses to act on, with every player in it locked out of joining anything else. Uneven sides are allowed; Captains still requires an even pool, because get_draft_game_pattern builds its pick order off capacity / 2. match_options.min_players_per_lineup records the size a short-handed match launched at, so editing the mode later cannot invalidate a match already running, and the game server is told the same number. All three columns are nullable, so with no custom mode configured every one of those resolves to exactly what the match type gave before. The feature is inert until an admin sets players_per_team, and clearing it back to NULL is a kill switch that needs no deploy. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01G8LmkD3i1HyGjbcxAZ5Crz --- generated/schema.graphql | 45 ++++ generated/schema.ts | 82 ++++--- generated/types.ts | 117 ++++++++++ .../match/check_match_has_min_players.sql | 12 +- .../match/get_match_options_min_players.sql | 35 +++ .../match/lineups/is_match_lineup_ready.sql | 10 +- .../match/match_players_per_lineup.sql | 8 +- hasura/metadata/actions.graphql | 7 + hasura/metadata/actions.yaml | 8 + .../default/tables/public_game_modes.yaml | 8 + .../default/tables/public_match_options.yaml | 1 + .../down.sql | 5 + .../up.sql | 28 +++ hasura/triggers/draft_games.sql | 42 +++- hasura/triggers/match_lineup_players.sql | 8 +- src/draft-games/draft-game.service.ts | 210 +++++++++++++++++- src/draft-games/draft-games.controller.ts | 14 ++ src/draft-games/draft-match.service.ts | 92 +++++++- src/draft-games/draft.service.ts | 19 +- src/game-plugins/game-modes.service.ts | 9 +- src/game-plugins/resolveExpectedPlayers.ts | 32 +++ .../events/MatchUpdatedLineupsEvent.ts | 11 +- .../match-assistant.service.ts | 5 + src/matches/matches.controller.ts | 9 + 24 files changed, 752 insertions(+), 65 deletions(-) create mode 100644 hasura/functions/match/get_match_options_min_players.sql create mode 100644 hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql create mode 100644 hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql create mode 100644 src/game-plugins/resolveExpectedPlayers.ts diff --git a/generated/schema.graphql b/generated/schema.graphql index 1b7c659c0..ad62470fd 100644 --- a/generated/schema.graphql +++ b/generated/schema.graphql @@ -22950,6 +22950,8 @@ type game_modes { archived_at: timestamptz cfg: String competitive_safe: Boolean! + players_per_team: Int + allow_short_handed_start: Boolean! created_at: timestamptz! description: String enabled: Boolean! @@ -23074,6 +23076,8 @@ input game_modes_bool_exp { archived_at: timestamptz_comparison_exp cfg: String_comparison_exp competitive_safe: Boolean_comparison_exp + players_per_team: Int_comparison_exp + allow_short_handed_start: Boolean_comparison_exp created_at: timestamptz_comparison_exp description: String_comparison_exp enabled: Boolean_comparison_exp @@ -23113,6 +23117,8 @@ input game_modes_insert_input { archived_at: timestamptz cfg: String competitive_safe: Boolean + players_per_team: Int + allow_short_handed_start: Boolean created_at: timestamptz description: String enabled: Boolean @@ -23189,6 +23195,8 @@ input game_modes_order_by { archived_at: order_by cfg: order_by competitive_safe: order_by + players_per_team: order_by + allow_short_handed_start: order_by created_at: order_by description: order_by enabled: order_by @@ -23221,6 +23229,8 @@ enum game_modes_select_column { """column name""" competitive_safe + players_per_team + allow_short_handed_start """column name""" created_at @@ -23257,6 +23267,8 @@ input game_modes_set_input { archived_at: timestamptz cfg: String competitive_safe: Boolean + players_per_team: Int + allow_short_handed_start: Boolean created_at: timestamptz description: String enabled: Boolean @@ -23284,6 +23296,8 @@ input game_modes_stream_cursor_value_input { archived_at: timestamptz cfg: String competitive_safe: Boolean + players_per_team: Int + allow_short_handed_start: Boolean created_at: timestamptz description: String enabled: Boolean @@ -23307,6 +23321,8 @@ enum game_modes_update_column { """column name""" competitive_safe + players_per_team + allow_short_handed_start """column name""" created_at @@ -40162,6 +40178,7 @@ type match_options { ): matches_aggregate! mr: Int! number_of_substitutes: Int! + min_players_per_lineup: Int overtime: Boolean! prefer_dedicated_server: Boolean! ready_setting: e_ready_settings_enum! @@ -40270,6 +40287,7 @@ type match_options_avg_fields { live_match_timeout: Float mr: Float number_of_substitutes: Float + min_players_per_lineup: Float round_restart_delay: Float tv_delay: Float veto_pick_timeout: Float @@ -40284,6 +40302,7 @@ input match_options_avg_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by @@ -40320,6 +40339,7 @@ input match_options_bool_exp { matches_aggregate: matches_aggregate_bool_exp mr: Int_comparison_exp number_of_substitutes: Int_comparison_exp + min_players_per_lineup: Int_comparison_exp overtime: Boolean_comparison_exp prefer_dedicated_server: Boolean_comparison_exp ready_setting: e_ready_settings_enum_comparison_exp @@ -40355,6 +40375,7 @@ input match_options_inc_input { live_match_timeout: Int mr: Int number_of_substitutes: Int + min_players_per_lineup: Int round_restart_delay: Int tv_delay: Int veto_pick_timeout: Int @@ -40386,6 +40407,7 @@ input match_options_insert_input { matches: matches_arr_rel_insert_input mr: Int number_of_substitutes: Int + min_players_per_lineup: Int overtime: Boolean prefer_dedicated_server: Boolean ready_setting: e_ready_settings_enum @@ -40413,6 +40435,7 @@ type match_options_max_fields { map_pool_id: uuid mr: Int number_of_substitutes: Int + min_players_per_lineup: Int regions: [String!] round_restart_delay: Int tv_delay: Int @@ -40432,6 +40455,7 @@ input match_options_max_order_by { map_pool_id: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by regions: order_by round_restart_delay: order_by tv_delay: order_by @@ -40449,6 +40473,7 @@ type match_options_min_fields { map_pool_id: uuid mr: Int number_of_substitutes: Int + min_players_per_lineup: Int regions: [String!] round_restart_delay: Int tv_delay: Int @@ -40468,6 +40493,7 @@ input match_options_min_order_by { map_pool_id: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by regions: order_by round_restart_delay: order_by tv_delay: order_by @@ -40529,6 +40555,7 @@ input match_options_order_by { matches_aggregate: matches_aggregate_order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by overtime: order_by prefer_dedicated_server: order_by ready_setting: order_by @@ -40610,6 +40637,7 @@ enum match_options_select_column { """column name""" number_of_substitutes + min_players_per_lineup """column name""" overtime @@ -40744,6 +40772,7 @@ input match_options_set_input { match_mode: e_match_mode_enum mr: Int number_of_substitutes: Int + min_players_per_lineup: Int overtime: Boolean prefer_dedicated_server: Boolean ready_setting: e_ready_settings_enum @@ -40764,6 +40793,7 @@ type match_options_stddev_fields { live_match_timeout: Float mr: Float number_of_substitutes: Float + min_players_per_lineup: Float round_restart_delay: Float tv_delay: Float veto_pick_timeout: Float @@ -40778,6 +40808,7 @@ input match_options_stddev_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by @@ -40790,6 +40821,7 @@ type match_options_stddev_pop_fields { live_match_timeout: Float mr: Float number_of_substitutes: Float + min_players_per_lineup: Float round_restart_delay: Float tv_delay: Float veto_pick_timeout: Float @@ -40804,6 +40836,7 @@ input match_options_stddev_pop_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by @@ -40816,6 +40849,7 @@ type match_options_stddev_samp_fields { live_match_timeout: Float mr: Float number_of_substitutes: Float + min_players_per_lineup: Float round_restart_delay: Float tv_delay: Float veto_pick_timeout: Float @@ -40830,6 +40864,7 @@ input match_options_stddev_samp_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by @@ -40867,6 +40902,7 @@ input match_options_stream_cursor_value_input { match_mode: e_match_mode_enum mr: Int number_of_substitutes: Int + min_players_per_lineup: Int overtime: Boolean prefer_dedicated_server: Boolean ready_setting: e_ready_settings_enum @@ -40887,6 +40923,7 @@ type match_options_sum_fields { live_match_timeout: Int mr: Int number_of_substitutes: Int + min_players_per_lineup: Int round_restart_delay: Int tv_delay: Int veto_pick_timeout: Int @@ -40901,6 +40938,7 @@ input match_options_sum_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by @@ -40966,6 +41004,7 @@ enum match_options_update_column { """column name""" number_of_substitutes + min_players_per_lineup """column name""" overtime @@ -41019,6 +41058,7 @@ type match_options_var_pop_fields { live_match_timeout: Float mr: Float number_of_substitutes: Float + min_players_per_lineup: Float round_restart_delay: Float tv_delay: Float veto_pick_timeout: Float @@ -41033,6 +41073,7 @@ input match_options_var_pop_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by @@ -41045,6 +41086,7 @@ type match_options_var_samp_fields { live_match_timeout: Float mr: Float number_of_substitutes: Float + min_players_per_lineup: Float round_restart_delay: Float tv_delay: Float veto_pick_timeout: Float @@ -41059,6 +41101,7 @@ input match_options_var_samp_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by @@ -41071,6 +41114,7 @@ type match_options_variance_fields { live_match_timeout: Float mr: Float number_of_substitutes: Float + min_players_per_lineup: Float round_restart_delay: Float tv_delay: Float veto_pick_timeout: Float @@ -41085,6 +41129,7 @@ input match_options_variance_order_by { live_match_timeout: order_by mr: order_by number_of_substitutes: order_by + min_players_per_lineup: order_by round_restart_delay: order_by tv_delay: order_by veto_pick_timeout: order_by diff --git a/generated/schema.ts b/generated/schema.ts index cf1294428..9dc132888 100644 --- a/generated/schema.ts +++ b/generated/schema.ts @@ -8751,6 +8751,8 @@ export interface game_modes { archived_at: (Scalars['timestamptz'] | null) cfg: (Scalars['String'] | null) competitive_safe: Scalars['Boolean'] + players_per_team: (Scalars['Int'] | null) + allow_short_handed_start: Scalars['Boolean'] created_at: Scalars['timestamptz'] description: (Scalars['String'] | null) enabled: Scalars['Boolean'] @@ -8840,11 +8842,11 @@ export interface game_modes_mutation_response { /** select columns of table "game_modes" */ -export type game_modes_select_column = 'archived_at' | 'cfg' | 'competitive_safe' | 'created_at' | 'description' | 'enabled' | 'extra_game_params' | 'icon' | 'id' | 'name' | 'slug' | 'updated_at' +export type game_modes_select_column = 'archived_at' | 'cfg' | 'competitive_safe' | 'players_per_team' | 'allow_short_handed_start' | 'created_at' | 'description' | 'enabled' | 'extra_game_params' | 'icon' | 'id' | 'name' | 'slug' | 'updated_at' /** update columns of table "game_modes" */ -export type game_modes_update_column = 'archived_at' | 'cfg' | 'competitive_safe' | 'created_at' | 'description' | 'enabled' | 'extra_game_params' | 'icon' | 'id' | 'name' | 'slug' | 'updated_at' +export type game_modes_update_column = 'archived_at' | 'cfg' | 'competitive_safe' | 'players_per_team' | 'allow_short_handed_start' | 'created_at' | 'description' | 'enabled' | 'extra_game_params' | 'icon' | 'id' | 'name' | 'slug' | 'updated_at' /** columns and relationships of "game_plugin_installs" */ @@ -13958,6 +13960,7 @@ export interface match_options { matches_aggregate: matches_aggregate mr: Scalars['Int'] number_of_substitutes: Scalars['Int'] + min_players_per_lineup: (Scalars['Int'] | null) overtime: Scalars['Boolean'] prefer_dedicated_server: Scalars['Boolean'] ready_setting: e_ready_settings_enum @@ -14011,6 +14014,7 @@ export interface match_options_avg_fields { live_match_timeout: (Scalars['Float'] | null) mr: (Scalars['Float'] | null) number_of_substitutes: (Scalars['Float'] | null) + min_players_per_lineup: (Scalars['Float'] | null) round_restart_delay: (Scalars['Float'] | null) tv_delay: (Scalars['Float'] | null) veto_pick_timeout: (Scalars['Float'] | null) @@ -14033,6 +14037,7 @@ export interface match_options_max_fields { map_pool_id: (Scalars['uuid'] | null) mr: (Scalars['Int'] | null) number_of_substitutes: (Scalars['Int'] | null) + min_players_per_lineup: (Scalars['Int'] | null) regions: (Scalars['String'][] | null) round_restart_delay: (Scalars['Int'] | null) tv_delay: (Scalars['Int'] | null) @@ -14052,6 +14057,7 @@ export interface match_options_min_fields { map_pool_id: (Scalars['uuid'] | null) mr: (Scalars['Int'] | null) number_of_substitutes: (Scalars['Int'] | null) + min_players_per_lineup: (Scalars['Int'] | null) regions: (Scalars['String'][] | null) round_restart_delay: (Scalars['Int'] | null) tv_delay: (Scalars['Int'] | null) @@ -14071,7 +14077,7 @@ export interface match_options_mutation_response { /** select columns of table "match_options" */ -export type match_options_select_column = 'auto_cancel_duration' | 'auto_cancellation' | 'best_of' | 'camera_allow_teammates' | 'camera_required' | 'check_in_setting' | 'coaches' | 'default_models' | 'game_mode_id' | 'halftime_pausematch' | 'id' | 'invite_code' | 'knife_round' | 'live_match_timeout' | 'map_pool_id' | 'map_veto' | 'match_mode' | 'mr' | 'number_of_substitutes' | 'overtime' | 'prefer_dedicated_server' | 'ready_setting' | 'region_veto' | 'regions' | 'round_restart_delay' | 'tech_timeout_setting' | 'timeout_setting' | 'tv_delay' | 'type' | 'veto_pick_timeout' +export type match_options_select_column = 'auto_cancel_duration' | 'auto_cancellation' | 'best_of' | 'camera_allow_teammates' | 'camera_required' | 'check_in_setting' | 'coaches' | 'default_models' | 'game_mode_id' | 'halftime_pausematch' | 'id' | 'invite_code' | 'knife_round' | 'live_match_timeout' | 'map_pool_id' | 'map_veto' | 'match_mode' | 'mr' | 'number_of_substitutes' | 'min_players_per_lineup' | 'overtime' | 'prefer_dedicated_server' | 'ready_setting' | 'region_veto' | 'regions' | 'round_restart_delay' | 'tech_timeout_setting' | 'timeout_setting' | 'tv_delay' | 'type' | 'veto_pick_timeout' /** select "match_options_aggregate_bool_exp_bool_and_arguments_columns" columns of table "match_options" */ @@ -14089,6 +14095,7 @@ export interface match_options_stddev_fields { live_match_timeout: (Scalars['Float'] | null) mr: (Scalars['Float'] | null) number_of_substitutes: (Scalars['Float'] | null) + min_players_per_lineup: (Scalars['Float'] | null) round_restart_delay: (Scalars['Float'] | null) tv_delay: (Scalars['Float'] | null) veto_pick_timeout: (Scalars['Float'] | null) @@ -14103,6 +14110,7 @@ export interface match_options_stddev_pop_fields { live_match_timeout: (Scalars['Float'] | null) mr: (Scalars['Float'] | null) number_of_substitutes: (Scalars['Float'] | null) + min_players_per_lineup: (Scalars['Float'] | null) round_restart_delay: (Scalars['Float'] | null) tv_delay: (Scalars['Float'] | null) veto_pick_timeout: (Scalars['Float'] | null) @@ -14117,6 +14125,7 @@ export interface match_options_stddev_samp_fields { live_match_timeout: (Scalars['Float'] | null) mr: (Scalars['Float'] | null) number_of_substitutes: (Scalars['Float'] | null) + min_players_per_lineup: (Scalars['Float'] | null) round_restart_delay: (Scalars['Float'] | null) tv_delay: (Scalars['Float'] | null) veto_pick_timeout: (Scalars['Float'] | null) @@ -14131,6 +14140,7 @@ export interface match_options_sum_fields { live_match_timeout: (Scalars['Int'] | null) mr: (Scalars['Int'] | null) number_of_substitutes: (Scalars['Int'] | null) + min_players_per_lineup: (Scalars['Int'] | null) round_restart_delay: (Scalars['Int'] | null) tv_delay: (Scalars['Int'] | null) veto_pick_timeout: (Scalars['Int'] | null) @@ -14139,7 +14149,7 @@ export interface match_options_sum_fields { /** update columns of table "match_options" */ -export type match_options_update_column = 'auto_cancel_duration' | 'auto_cancellation' | 'best_of' | 'camera_allow_teammates' | 'camera_required' | 'check_in_setting' | 'coaches' | 'default_models' | 'game_mode_id' | 'halftime_pausematch' | 'id' | 'invite_code' | 'knife_round' | 'live_match_timeout' | 'map_pool_id' | 'map_veto' | 'match_mode' | 'mr' | 'number_of_substitutes' | 'overtime' | 'prefer_dedicated_server' | 'ready_setting' | 'region_veto' | 'regions' | 'round_restart_delay' | 'tech_timeout_setting' | 'timeout_setting' | 'tv_delay' | 'type' | 'veto_pick_timeout' +export type match_options_update_column = 'auto_cancel_duration' | 'auto_cancellation' | 'best_of' | 'camera_allow_teammates' | 'camera_required' | 'check_in_setting' | 'coaches' | 'default_models' | 'game_mode_id' | 'halftime_pausematch' | 'id' | 'invite_code' | 'knife_round' | 'live_match_timeout' | 'map_pool_id' | 'map_veto' | 'match_mode' | 'mr' | 'number_of_substitutes' | 'min_players_per_lineup' | 'overtime' | 'prefer_dedicated_server' | 'ready_setting' | 'region_veto' | 'regions' | 'round_restart_delay' | 'tech_timeout_setting' | 'timeout_setting' | 'tv_delay' | 'type' | 'veto_pick_timeout' /** aggregate var_pop on columns */ @@ -14149,6 +14159,7 @@ export interface match_options_var_pop_fields { live_match_timeout: (Scalars['Float'] | null) mr: (Scalars['Float'] | null) number_of_substitutes: (Scalars['Float'] | null) + min_players_per_lineup: (Scalars['Float'] | null) round_restart_delay: (Scalars['Float'] | null) tv_delay: (Scalars['Float'] | null) veto_pick_timeout: (Scalars['Float'] | null) @@ -14163,6 +14174,7 @@ export interface match_options_var_samp_fields { live_match_timeout: (Scalars['Float'] | null) mr: (Scalars['Float'] | null) number_of_substitutes: (Scalars['Float'] | null) + min_players_per_lineup: (Scalars['Float'] | null) round_restart_delay: (Scalars['Float'] | null) tv_delay: (Scalars['Float'] | null) veto_pick_timeout: (Scalars['Float'] | null) @@ -14177,6 +14189,7 @@ export interface match_options_variance_fields { live_match_timeout: (Scalars['Float'] | null) mr: (Scalars['Float'] | null) number_of_substitutes: (Scalars['Float'] | null) + min_players_per_lineup: (Scalars['Float'] | null) round_restart_delay: (Scalars['Float'] | null) tv_delay: (Scalars['Float'] | null) veto_pick_timeout: (Scalars['Float'] | null) @@ -59461,6 +59474,8 @@ export interface game_modesGenqlSelection{ archived_at?: boolean | number cfg?: boolean | number competitive_safe?: boolean | number + players_per_team?: boolean | number + allow_short_handed_start?: boolean | number created_at?: boolean | number description?: boolean | number enabled?: boolean | number @@ -59551,11 +59566,11 @@ export interface game_modes_aggregate_fieldsGenqlSelection{ /** Boolean expression to filter rows from the table "game_modes". All fields are combined with a logical 'AND'. */ -export interface game_modes_bool_exp {_and?: (game_modes_bool_exp[] | null),_not?: (game_modes_bool_exp | null),_or?: (game_modes_bool_exp[] | null),archived_at?: (timestamptz_comparison_exp | null),cfg?: (String_comparison_exp | null),competitive_safe?: (Boolean_comparison_exp | null),created_at?: (timestamptz_comparison_exp | null),description?: (String_comparison_exp | null),enabled?: (Boolean_comparison_exp | null),extra_game_params?: (String_comparison_exp | null),icon?: (String_comparison_exp | null),id?: (uuid_comparison_exp | null),match_options?: (match_options_bool_exp | null),match_options_aggregate?: (match_options_aggregate_bool_exp | null),name?: (String_comparison_exp | null),plugins?: (game_mode_plugins_bool_exp | null),plugins_aggregate?: (game_mode_plugins_aggregate_bool_exp | null),runtime_conflicts?: (jsonb_comparison_exp | null),slug?: (String_comparison_exp | null),supported_runtimes?: (jsonb_comparison_exp | null),updated_at?: (timestamptz_comparison_exp | null)} +export interface game_modes_bool_exp {_and?: (game_modes_bool_exp[] | null),_not?: (game_modes_bool_exp | null),_or?: (game_modes_bool_exp[] | null),archived_at?: (timestamptz_comparison_exp | null),cfg?: (String_comparison_exp | null),competitive_safe?: (Boolean_comparison_exp | null),players_per_team?: (Int_comparison_exp | null),allow_short_handed_start?: (Boolean_comparison_exp | null),created_at?: (timestamptz_comparison_exp | null),description?: (String_comparison_exp | null),enabled?: (Boolean_comparison_exp | null),extra_game_params?: (String_comparison_exp | null),icon?: (String_comparison_exp | null),id?: (uuid_comparison_exp | null),match_options?: (match_options_bool_exp | null),match_options_aggregate?: (match_options_aggregate_bool_exp | null),name?: (String_comparison_exp | null),plugins?: (game_mode_plugins_bool_exp | null),plugins_aggregate?: (game_mode_plugins_aggregate_bool_exp | null),runtime_conflicts?: (jsonb_comparison_exp | null),slug?: (String_comparison_exp | null),supported_runtimes?: (jsonb_comparison_exp | null),updated_at?: (timestamptz_comparison_exp | null)} /** input type for inserting data into table "game_modes" */ -export interface game_modes_insert_input {archived_at?: (Scalars['timestamptz'] | null),cfg?: (Scalars['String'] | null),competitive_safe?: (Scalars['Boolean'] | null),created_at?: (Scalars['timestamptz'] | null),description?: (Scalars['String'] | null),enabled?: (Scalars['Boolean'] | null),extra_game_params?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),id?: (Scalars['uuid'] | null),match_options?: (match_options_arr_rel_insert_input | null),name?: (Scalars['String'] | null),plugins?: (game_mode_plugins_arr_rel_insert_input | null),slug?: (Scalars['String'] | null),updated_at?: (Scalars['timestamptz'] | null)} +export interface game_modes_insert_input {archived_at?: (Scalars['timestamptz'] | null),cfg?: (Scalars['String'] | null),competitive_safe?: (Scalars['Boolean'] | null),players_per_team?: (Scalars['Int'] | null),allow_short_handed_start?: (Scalars['Boolean'] | null),created_at?: (Scalars['timestamptz'] | null),description?: (Scalars['String'] | null),enabled?: (Scalars['Boolean'] | null),extra_game_params?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),id?: (Scalars['uuid'] | null),match_options?: (match_options_arr_rel_insert_input | null),name?: (Scalars['String'] | null),plugins?: (game_mode_plugins_arr_rel_insert_input | null),slug?: (Scalars['String'] | null),updated_at?: (Scalars['timestamptz'] | null)} /** aggregate max on columns */ @@ -59614,7 +59629,7 @@ export interface game_modes_on_conflict {constraint: game_modes_constraint,updat /** Ordering options when selecting data from "game_modes". */ -export interface game_modes_order_by {archived_at?: (order_by | null),cfg?: (order_by | null),competitive_safe?: (order_by | null),created_at?: (order_by | null),description?: (order_by | null),enabled?: (order_by | null),extra_game_params?: (order_by | null),icon?: (order_by | null),id?: (order_by | null),match_options_aggregate?: (match_options_aggregate_order_by | null),name?: (order_by | null),plugins_aggregate?: (game_mode_plugins_aggregate_order_by | null),runtime_conflicts?: (order_by | null),slug?: (order_by | null),supported_runtimes?: (order_by | null),updated_at?: (order_by | null)} +export interface game_modes_order_by {archived_at?: (order_by | null),cfg?: (order_by | null),competitive_safe?: (order_by | null),players_per_team?: (order_by | null),allow_short_handed_start?: (order_by | null),created_at?: (order_by | null),description?: (order_by | null),enabled?: (order_by | null),extra_game_params?: (order_by | null),icon?: (order_by | null),id?: (order_by | null),match_options_aggregate?: (match_options_aggregate_order_by | null),name?: (order_by | null),plugins_aggregate?: (game_mode_plugins_aggregate_order_by | null),runtime_conflicts?: (order_by | null),slug?: (order_by | null),supported_runtimes?: (order_by | null),updated_at?: (order_by | null)} /** primary key columns input for table: game_modes */ @@ -59622,7 +59637,7 @@ export interface game_modes_pk_columns_input {id: Scalars['uuid']} /** input type for updating data in table "game_modes" */ -export interface game_modes_set_input {archived_at?: (Scalars['timestamptz'] | null),cfg?: (Scalars['String'] | null),competitive_safe?: (Scalars['Boolean'] | null),created_at?: (Scalars['timestamptz'] | null),description?: (Scalars['String'] | null),enabled?: (Scalars['Boolean'] | null),extra_game_params?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),id?: (Scalars['uuid'] | null),name?: (Scalars['String'] | null),slug?: (Scalars['String'] | null),updated_at?: (Scalars['timestamptz'] | null)} +export interface game_modes_set_input {archived_at?: (Scalars['timestamptz'] | null),cfg?: (Scalars['String'] | null),competitive_safe?: (Scalars['Boolean'] | null),players_per_team?: (Scalars['Int'] | null),allow_short_handed_start?: (Scalars['Boolean'] | null),created_at?: (Scalars['timestamptz'] | null),description?: (Scalars['String'] | null),enabled?: (Scalars['Boolean'] | null),extra_game_params?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),id?: (Scalars['uuid'] | null),name?: (Scalars['String'] | null),slug?: (Scalars['String'] | null),updated_at?: (Scalars['timestamptz'] | null)} /** Streaming cursor of the table "game_modes" */ @@ -59634,7 +59649,7 @@ ordering?: (cursor_ordering | null)} /** Initial value of the column from where the streaming should start */ -export interface game_modes_stream_cursor_value_input {archived_at?: (Scalars['timestamptz'] | null),cfg?: (Scalars['String'] | null),competitive_safe?: (Scalars['Boolean'] | null),created_at?: (Scalars['timestamptz'] | null),description?: (Scalars['String'] | null),enabled?: (Scalars['Boolean'] | null),extra_game_params?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),id?: (Scalars['uuid'] | null),name?: (Scalars['String'] | null),slug?: (Scalars['String'] | null),updated_at?: (Scalars['timestamptz'] | null)} +export interface game_modes_stream_cursor_value_input {archived_at?: (Scalars['timestamptz'] | null),cfg?: (Scalars['String'] | null),competitive_safe?: (Scalars['Boolean'] | null),players_per_team?: (Scalars['Int'] | null),allow_short_handed_start?: (Scalars['Boolean'] | null),created_at?: (Scalars['timestamptz'] | null),description?: (Scalars['String'] | null),enabled?: (Scalars['Boolean'] | null),extra_game_params?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),id?: (Scalars['uuid'] | null),name?: (Scalars['String'] | null),slug?: (Scalars['String'] | null),updated_at?: (Scalars['timestamptz'] | null)} export interface game_modes_updates { /** sets the columns of the filtered rows to the given values */ @@ -68348,6 +68363,7 @@ export interface match_optionsGenqlSelection{ where?: (matches_bool_exp | null)} }) mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number overtime?: boolean | number prefer_dedicated_server?: boolean | number ready_setting?: boolean | number @@ -68422,6 +68438,7 @@ export interface match_options_avg_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68431,19 +68448,19 @@ export interface match_options_avg_fieldsGenqlSelection{ /** order by avg() on columns of table "match_options" */ -export interface match_options_avg_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_avg_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** Boolean expression to filter rows from the table "match_options". All fields are combined with a logical 'AND'. */ -export interface match_options_bool_exp {_and?: (match_options_bool_exp[] | null),_not?: (match_options_bool_exp | null),_or?: (match_options_bool_exp[] | null),auto_cancel_duration?: (Int_comparison_exp | null),auto_cancellation?: (Boolean_comparison_exp | null),best_of?: (Int_comparison_exp | null),camera_allow_teammates?: (Boolean_comparison_exp | null),camera_required?: (Boolean_comparison_exp | null),check_in_setting?: (e_check_in_settings_enum_comparison_exp | null),coaches?: (Boolean_comparison_exp | null),default_models?: (Boolean_comparison_exp | null),game_mode?: (game_modes_bool_exp | null),game_mode_id?: (uuid_comparison_exp | null),halftime_pausematch?: (Boolean_comparison_exp | null),has_active_matches?: (Boolean_comparison_exp | null),id?: (uuid_comparison_exp | null),invite_code?: (String_comparison_exp | null),knife_round?: (Boolean_comparison_exp | null),live_match_timeout?: (Int_comparison_exp | null),map_pool?: (map_pools_bool_exp | null),map_pool_id?: (uuid_comparison_exp | null),map_veto?: (Boolean_comparison_exp | null),match_mode?: (e_match_mode_enum_comparison_exp | null),matches?: (matches_bool_exp | null),matches_aggregate?: (matches_aggregate_bool_exp | null),mr?: (Int_comparison_exp | null),number_of_substitutes?: (Int_comparison_exp | null),overtime?: (Boolean_comparison_exp | null),prefer_dedicated_server?: (Boolean_comparison_exp | null),ready_setting?: (e_ready_settings_enum_comparison_exp | null),region_veto?: (Boolean_comparison_exp | null),regions?: (String_array_comparison_exp | null),round_restart_delay?: (Int_comparison_exp | null),tech_timeout_setting?: (e_timeout_settings_enum_comparison_exp | null),timeout_setting?: (e_timeout_settings_enum_comparison_exp | null),tournament?: (tournaments_bool_exp | null),tournament_bracket?: (tournament_brackets_bool_exp | null),tournament_stage?: (tournament_stages_bool_exp | null),tv_delay?: (Int_comparison_exp | null),type?: (e_match_types_enum_comparison_exp | null),veto_pick_timeout?: (Int_comparison_exp | null)} +export interface match_options_bool_exp {_and?: (match_options_bool_exp[] | null),_not?: (match_options_bool_exp | null),_or?: (match_options_bool_exp[] | null),auto_cancel_duration?: (Int_comparison_exp | null),auto_cancellation?: (Boolean_comparison_exp | null),best_of?: (Int_comparison_exp | null),camera_allow_teammates?: (Boolean_comparison_exp | null),camera_required?: (Boolean_comparison_exp | null),check_in_setting?: (e_check_in_settings_enum_comparison_exp | null),coaches?: (Boolean_comparison_exp | null),default_models?: (Boolean_comparison_exp | null),game_mode?: (game_modes_bool_exp | null),game_mode_id?: (uuid_comparison_exp | null),halftime_pausematch?: (Boolean_comparison_exp | null),has_active_matches?: (Boolean_comparison_exp | null),id?: (uuid_comparison_exp | null),invite_code?: (String_comparison_exp | null),knife_round?: (Boolean_comparison_exp | null),live_match_timeout?: (Int_comparison_exp | null),map_pool?: (map_pools_bool_exp | null),map_pool_id?: (uuid_comparison_exp | null),map_veto?: (Boolean_comparison_exp | null),match_mode?: (e_match_mode_enum_comparison_exp | null),matches?: (matches_bool_exp | null),matches_aggregate?: (matches_aggregate_bool_exp | null),mr?: (Int_comparison_exp | null),number_of_substitutes?: (Int_comparison_exp | null),min_players_per_lineup?: (Int_comparison_exp | null),overtime?: (Boolean_comparison_exp | null),prefer_dedicated_server?: (Boolean_comparison_exp | null),ready_setting?: (e_ready_settings_enum_comparison_exp | null),region_veto?: (Boolean_comparison_exp | null),regions?: (String_array_comparison_exp | null),round_restart_delay?: (Int_comparison_exp | null),tech_timeout_setting?: (e_timeout_settings_enum_comparison_exp | null),timeout_setting?: (e_timeout_settings_enum_comparison_exp | null),tournament?: (tournaments_bool_exp | null),tournament_bracket?: (tournament_brackets_bool_exp | null),tournament_stage?: (tournament_stages_bool_exp | null),tv_delay?: (Int_comparison_exp | null),type?: (e_match_types_enum_comparison_exp | null),veto_pick_timeout?: (Int_comparison_exp | null)} /** input type for incrementing numeric columns in table "match_options" */ -export interface match_options_inc_input {auto_cancel_duration?: (Scalars['Int'] | null),best_of?: (Scalars['Int'] | null),live_match_timeout?: (Scalars['Int'] | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),round_restart_delay?: (Scalars['Int'] | null),tv_delay?: (Scalars['Int'] | null),veto_pick_timeout?: (Scalars['Int'] | null)} +export interface match_options_inc_input {auto_cancel_duration?: (Scalars['Int'] | null),best_of?: (Scalars['Int'] | null),live_match_timeout?: (Scalars['Int'] | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),min_players_per_lineup?: (Scalars['Int'] | null),round_restart_delay?: (Scalars['Int'] | null),tv_delay?: (Scalars['Int'] | null),veto_pick_timeout?: (Scalars['Int'] | null)} /** input type for inserting data into table "match_options" */ -export interface match_options_insert_input {auto_cancel_duration?: (Scalars['Int'] | null),auto_cancellation?: (Scalars['Boolean'] | null),best_of?: (Scalars['Int'] | null),camera_allow_teammates?: (Scalars['Boolean'] | null),camera_required?: (Scalars['Boolean'] | null),check_in_setting?: (e_check_in_settings_enum | null),coaches?: (Scalars['Boolean'] | null),default_models?: (Scalars['Boolean'] | null),game_mode?: (game_modes_obj_rel_insert_input | null),game_mode_id?: (Scalars['uuid'] | null),halftime_pausematch?: (Scalars['Boolean'] | null),id?: (Scalars['uuid'] | null),invite_code?: (Scalars['String'] | null),knife_round?: (Scalars['Boolean'] | null),live_match_timeout?: (Scalars['Int'] | null),map_pool?: (map_pools_obj_rel_insert_input | null),map_pool_id?: (Scalars['uuid'] | null),map_veto?: (Scalars['Boolean'] | null),match_mode?: (e_match_mode_enum | null),matches?: (matches_arr_rel_insert_input | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),overtime?: (Scalars['Boolean'] | null),prefer_dedicated_server?: (Scalars['Boolean'] | null),ready_setting?: (e_ready_settings_enum | null),region_veto?: (Scalars['Boolean'] | null),regions?: (Scalars['String'][] | null),round_restart_delay?: (Scalars['Int'] | null),tech_timeout_setting?: (e_timeout_settings_enum | null),timeout_setting?: (e_timeout_settings_enum | null),tournament?: (tournaments_obj_rel_insert_input | null),tournament_bracket?: (tournament_brackets_obj_rel_insert_input | null),tournament_stage?: (tournament_stages_obj_rel_insert_input | null),tv_delay?: (Scalars['Int'] | null),type?: (e_match_types_enum | null),veto_pick_timeout?: (Scalars['Int'] | null)} +export interface match_options_insert_input {auto_cancel_duration?: (Scalars['Int'] | null),auto_cancellation?: (Scalars['Boolean'] | null),best_of?: (Scalars['Int'] | null),camera_allow_teammates?: (Scalars['Boolean'] | null),camera_required?: (Scalars['Boolean'] | null),check_in_setting?: (e_check_in_settings_enum | null),coaches?: (Scalars['Boolean'] | null),default_models?: (Scalars['Boolean'] | null),game_mode?: (game_modes_obj_rel_insert_input | null),game_mode_id?: (Scalars['uuid'] | null),halftime_pausematch?: (Scalars['Boolean'] | null),id?: (Scalars['uuid'] | null),invite_code?: (Scalars['String'] | null),knife_round?: (Scalars['Boolean'] | null),live_match_timeout?: (Scalars['Int'] | null),map_pool?: (map_pools_obj_rel_insert_input | null),map_pool_id?: (Scalars['uuid'] | null),map_veto?: (Scalars['Boolean'] | null),match_mode?: (e_match_mode_enum | null),matches?: (matches_arr_rel_insert_input | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),min_players_per_lineup?: (Scalars['Int'] | null),overtime?: (Scalars['Boolean'] | null),prefer_dedicated_server?: (Scalars['Boolean'] | null),ready_setting?: (e_ready_settings_enum | null),region_veto?: (Scalars['Boolean'] | null),regions?: (Scalars['String'][] | null),round_restart_delay?: (Scalars['Int'] | null),tech_timeout_setting?: (e_timeout_settings_enum | null),timeout_setting?: (e_timeout_settings_enum | null),tournament?: (tournaments_obj_rel_insert_input | null),tournament_bracket?: (tournament_brackets_obj_rel_insert_input | null),tournament_stage?: (tournament_stages_obj_rel_insert_input | null),tv_delay?: (Scalars['Int'] | null),type?: (e_match_types_enum | null),veto_pick_timeout?: (Scalars['Int'] | null)} /** aggregate max on columns */ @@ -68457,6 +68474,7 @@ export interface match_options_max_fieldsGenqlSelection{ map_pool_id?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number regions?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number @@ -68467,7 +68485,7 @@ export interface match_options_max_fieldsGenqlSelection{ /** order by max() on columns of table "match_options" */ -export interface match_options_max_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),game_mode_id?: (order_by | null),id?: (order_by | null),invite_code?: (order_by | null),live_match_timeout?: (order_by | null),map_pool_id?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),regions?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_max_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),game_mode_id?: (order_by | null),id?: (order_by | null),invite_code?: (order_by | null),live_match_timeout?: (order_by | null),map_pool_id?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),regions?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** aggregate min on columns */ @@ -68481,6 +68499,7 @@ export interface match_options_min_fieldsGenqlSelection{ map_pool_id?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number regions?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number @@ -68491,7 +68510,7 @@ export interface match_options_min_fieldsGenqlSelection{ /** order by min() on columns of table "match_options" */ -export interface match_options_min_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),game_mode_id?: (order_by | null),id?: (order_by | null),invite_code?: (order_by | null),live_match_timeout?: (order_by | null),map_pool_id?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),regions?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_min_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),game_mode_id?: (order_by | null),id?: (order_by | null),invite_code?: (order_by | null),live_match_timeout?: (order_by | null),map_pool_id?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),regions?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** response of any mutation on the table "match_options" */ @@ -68516,7 +68535,7 @@ export interface match_options_on_conflict {constraint: match_options_constraint /** Ordering options when selecting data from "match_options". */ -export interface match_options_order_by {auto_cancel_duration?: (order_by | null),auto_cancellation?: (order_by | null),best_of?: (order_by | null),camera_allow_teammates?: (order_by | null),camera_required?: (order_by | null),check_in_setting?: (order_by | null),coaches?: (order_by | null),default_models?: (order_by | null),game_mode?: (game_modes_order_by | null),game_mode_id?: (order_by | null),halftime_pausematch?: (order_by | null),has_active_matches?: (order_by | null),id?: (order_by | null),invite_code?: (order_by | null),knife_round?: (order_by | null),live_match_timeout?: (order_by | null),map_pool?: (map_pools_order_by | null),map_pool_id?: (order_by | null),map_veto?: (order_by | null),match_mode?: (order_by | null),matches_aggregate?: (matches_aggregate_order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),overtime?: (order_by | null),prefer_dedicated_server?: (order_by | null),ready_setting?: (order_by | null),region_veto?: (order_by | null),regions?: (order_by | null),round_restart_delay?: (order_by | null),tech_timeout_setting?: (order_by | null),timeout_setting?: (order_by | null),tournament?: (tournaments_order_by | null),tournament_bracket?: (tournament_brackets_order_by | null),tournament_stage?: (tournament_stages_order_by | null),tv_delay?: (order_by | null),type?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_order_by {auto_cancel_duration?: (order_by | null),auto_cancellation?: (order_by | null),best_of?: (order_by | null),camera_allow_teammates?: (order_by | null),camera_required?: (order_by | null),check_in_setting?: (order_by | null),coaches?: (order_by | null),default_models?: (order_by | null),game_mode?: (game_modes_order_by | null),game_mode_id?: (order_by | null),halftime_pausematch?: (order_by | null),has_active_matches?: (order_by | null),id?: (order_by | null),invite_code?: (order_by | null),knife_round?: (order_by | null),live_match_timeout?: (order_by | null),map_pool?: (map_pools_order_by | null),map_pool_id?: (order_by | null),map_veto?: (order_by | null),match_mode?: (order_by | null),matches_aggregate?: (matches_aggregate_order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),overtime?: (order_by | null),prefer_dedicated_server?: (order_by | null),ready_setting?: (order_by | null),region_veto?: (order_by | null),regions?: (order_by | null),round_restart_delay?: (order_by | null),tech_timeout_setting?: (order_by | null),timeout_setting?: (order_by | null),tournament?: (tournaments_order_by | null),tournament_bracket?: (tournament_brackets_order_by | null),tournament_stage?: (tournament_stages_order_by | null),tv_delay?: (order_by | null),type?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** primary key columns input for table: match_options */ @@ -68524,7 +68543,7 @@ export interface match_options_pk_columns_input {id: Scalars['uuid']} /** input type for updating data in table "match_options" */ -export interface match_options_set_input {auto_cancel_duration?: (Scalars['Int'] | null),auto_cancellation?: (Scalars['Boolean'] | null),best_of?: (Scalars['Int'] | null),camera_allow_teammates?: (Scalars['Boolean'] | null),camera_required?: (Scalars['Boolean'] | null),check_in_setting?: (e_check_in_settings_enum | null),coaches?: (Scalars['Boolean'] | null),default_models?: (Scalars['Boolean'] | null),game_mode_id?: (Scalars['uuid'] | null),halftime_pausematch?: (Scalars['Boolean'] | null),id?: (Scalars['uuid'] | null),invite_code?: (Scalars['String'] | null),knife_round?: (Scalars['Boolean'] | null),live_match_timeout?: (Scalars['Int'] | null),map_pool_id?: (Scalars['uuid'] | null),map_veto?: (Scalars['Boolean'] | null),match_mode?: (e_match_mode_enum | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),overtime?: (Scalars['Boolean'] | null),prefer_dedicated_server?: (Scalars['Boolean'] | null),ready_setting?: (e_ready_settings_enum | null),region_veto?: (Scalars['Boolean'] | null),regions?: (Scalars['String'][] | null),round_restart_delay?: (Scalars['Int'] | null),tech_timeout_setting?: (e_timeout_settings_enum | null),timeout_setting?: (e_timeout_settings_enum | null),tv_delay?: (Scalars['Int'] | null),type?: (e_match_types_enum | null),veto_pick_timeout?: (Scalars['Int'] | null)} +export interface match_options_set_input {auto_cancel_duration?: (Scalars['Int'] | null),auto_cancellation?: (Scalars['Boolean'] | null),best_of?: (Scalars['Int'] | null),camera_allow_teammates?: (Scalars['Boolean'] | null),camera_required?: (Scalars['Boolean'] | null),check_in_setting?: (e_check_in_settings_enum | null),coaches?: (Scalars['Boolean'] | null),default_models?: (Scalars['Boolean'] | null),game_mode_id?: (Scalars['uuid'] | null),halftime_pausematch?: (Scalars['Boolean'] | null),id?: (Scalars['uuid'] | null),invite_code?: (Scalars['String'] | null),knife_round?: (Scalars['Boolean'] | null),live_match_timeout?: (Scalars['Int'] | null),map_pool_id?: (Scalars['uuid'] | null),map_veto?: (Scalars['Boolean'] | null),match_mode?: (e_match_mode_enum | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),min_players_per_lineup?: (Scalars['Int'] | null),overtime?: (Scalars['Boolean'] | null),prefer_dedicated_server?: (Scalars['Boolean'] | null),ready_setting?: (e_ready_settings_enum | null),region_veto?: (Scalars['Boolean'] | null),regions?: (Scalars['String'][] | null),round_restart_delay?: (Scalars['Int'] | null),tech_timeout_setting?: (e_timeout_settings_enum | null),timeout_setting?: (e_timeout_settings_enum | null),tv_delay?: (Scalars['Int'] | null),type?: (e_match_types_enum | null),veto_pick_timeout?: (Scalars['Int'] | null)} /** aggregate stddev on columns */ @@ -68534,6 +68553,7 @@ export interface match_options_stddev_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68543,7 +68563,7 @@ export interface match_options_stddev_fieldsGenqlSelection{ /** order by stddev() on columns of table "match_options" */ -export interface match_options_stddev_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_stddev_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** aggregate stddev_pop on columns */ @@ -68553,6 +68573,7 @@ export interface match_options_stddev_pop_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68562,7 +68583,7 @@ export interface match_options_stddev_pop_fieldsGenqlSelection{ /** order by stddev_pop() on columns of table "match_options" */ -export interface match_options_stddev_pop_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_stddev_pop_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** aggregate stddev_samp on columns */ @@ -68572,6 +68593,7 @@ export interface match_options_stddev_samp_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68581,7 +68603,7 @@ export interface match_options_stddev_samp_fieldsGenqlSelection{ /** order by stddev_samp() on columns of table "match_options" */ -export interface match_options_stddev_samp_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_stddev_samp_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** Streaming cursor of the table "match_options" */ @@ -68593,7 +68615,7 @@ ordering?: (cursor_ordering | null)} /** Initial value of the column from where the streaming should start */ -export interface match_options_stream_cursor_value_input {auto_cancel_duration?: (Scalars['Int'] | null),auto_cancellation?: (Scalars['Boolean'] | null),best_of?: (Scalars['Int'] | null),camera_allow_teammates?: (Scalars['Boolean'] | null),camera_required?: (Scalars['Boolean'] | null),check_in_setting?: (e_check_in_settings_enum | null),coaches?: (Scalars['Boolean'] | null),default_models?: (Scalars['Boolean'] | null),game_mode_id?: (Scalars['uuid'] | null),halftime_pausematch?: (Scalars['Boolean'] | null),id?: (Scalars['uuid'] | null),invite_code?: (Scalars['String'] | null),knife_round?: (Scalars['Boolean'] | null),live_match_timeout?: (Scalars['Int'] | null),map_pool_id?: (Scalars['uuid'] | null),map_veto?: (Scalars['Boolean'] | null),match_mode?: (e_match_mode_enum | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),overtime?: (Scalars['Boolean'] | null),prefer_dedicated_server?: (Scalars['Boolean'] | null),ready_setting?: (e_ready_settings_enum | null),region_veto?: (Scalars['Boolean'] | null),regions?: (Scalars['String'][] | null),round_restart_delay?: (Scalars['Int'] | null),tech_timeout_setting?: (e_timeout_settings_enum | null),timeout_setting?: (e_timeout_settings_enum | null),tv_delay?: (Scalars['Int'] | null),type?: (e_match_types_enum | null),veto_pick_timeout?: (Scalars['Int'] | null)} +export interface match_options_stream_cursor_value_input {auto_cancel_duration?: (Scalars['Int'] | null),auto_cancellation?: (Scalars['Boolean'] | null),best_of?: (Scalars['Int'] | null),camera_allow_teammates?: (Scalars['Boolean'] | null),camera_required?: (Scalars['Boolean'] | null),check_in_setting?: (e_check_in_settings_enum | null),coaches?: (Scalars['Boolean'] | null),default_models?: (Scalars['Boolean'] | null),game_mode_id?: (Scalars['uuid'] | null),halftime_pausematch?: (Scalars['Boolean'] | null),id?: (Scalars['uuid'] | null),invite_code?: (Scalars['String'] | null),knife_round?: (Scalars['Boolean'] | null),live_match_timeout?: (Scalars['Int'] | null),map_pool_id?: (Scalars['uuid'] | null),map_veto?: (Scalars['Boolean'] | null),match_mode?: (e_match_mode_enum | null),mr?: (Scalars['Int'] | null),number_of_substitutes?: (Scalars['Int'] | null),min_players_per_lineup?: (Scalars['Int'] | null),overtime?: (Scalars['Boolean'] | null),prefer_dedicated_server?: (Scalars['Boolean'] | null),ready_setting?: (e_ready_settings_enum | null),region_veto?: (Scalars['Boolean'] | null),regions?: (Scalars['String'][] | null),round_restart_delay?: (Scalars['Int'] | null),tech_timeout_setting?: (e_timeout_settings_enum | null),timeout_setting?: (e_timeout_settings_enum | null),tv_delay?: (Scalars['Int'] | null),type?: (e_match_types_enum | null),veto_pick_timeout?: (Scalars['Int'] | null)} /** aggregate sum on columns */ @@ -68603,6 +68625,7 @@ export interface match_options_sum_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68612,7 +68635,7 @@ export interface match_options_sum_fieldsGenqlSelection{ /** order by sum() on columns of table "match_options" */ -export interface match_options_sum_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_sum_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} export interface match_options_updates { /** increments the numeric columns with given value of the filtered values */ @@ -68630,6 +68653,7 @@ export interface match_options_var_pop_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68639,7 +68663,7 @@ export interface match_options_var_pop_fieldsGenqlSelection{ /** order by var_pop() on columns of table "match_options" */ -export interface match_options_var_pop_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_var_pop_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** aggregate var_samp on columns */ @@ -68649,6 +68673,7 @@ export interface match_options_var_samp_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68658,7 +68683,7 @@ export interface match_options_var_samp_fieldsGenqlSelection{ /** order by var_samp() on columns of table "match_options" */ -export interface match_options_var_samp_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_var_samp_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** aggregate variance on columns */ @@ -68668,6 +68693,7 @@ export interface match_options_variance_fieldsGenqlSelection{ live_match_timeout?: boolean | number mr?: boolean | number number_of_substitutes?: boolean | number + min_players_per_lineup?: boolean | number round_restart_delay?: boolean | number tv_delay?: boolean | number veto_pick_timeout?: boolean | number @@ -68677,7 +68703,7 @@ export interface match_options_variance_fieldsGenqlSelection{ /** order by variance() on columns of table "match_options" */ -export interface match_options_variance_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} +export interface match_options_variance_order_by {auto_cancel_duration?: (order_by | null),best_of?: (order_by | null),live_match_timeout?: (order_by | null),mr?: (order_by | null),number_of_substitutes?: (order_by | null),min_players_per_lineup?: (order_by | null),round_restart_delay?: (order_by | null),tv_delay?: (order_by | null),veto_pick_timeout?: (order_by | null)} /** columns and relationships of "match_region_veto_picks" */ @@ -151602,6 +151628,8 @@ export const enumGameModesSelectColumn = { archived_at: 'archived_at' as const, cfg: 'cfg' as const, competitive_safe: 'competitive_safe' as const, + players_per_team: 'players_per_team' as const, + allow_short_handed_start: 'allow_short_handed_start' as const, created_at: 'created_at' as const, description: 'description' as const, enabled: 'enabled' as const, @@ -151617,6 +151645,8 @@ export const enumGameModesUpdateColumn = { archived_at: 'archived_at' as const, cfg: 'cfg' as const, competitive_safe: 'competitive_safe' as const, + players_per_team: 'players_per_team' as const, + allow_short_handed_start: 'allow_short_handed_start' as const, created_at: 'created_at' as const, description: 'description' as const, enabled: 'enabled' as const, @@ -152754,6 +152784,7 @@ export const enumMatchOptionsSelectColumn = { match_mode: 'match_mode' as const, mr: 'mr' as const, number_of_substitutes: 'number_of_substitutes' as const, + min_players_per_lineup: 'min_players_per_lineup' as const, overtime: 'overtime' as const, prefer_dedicated_server: 'prefer_dedicated_server' as const, ready_setting: 'ready_setting' as const, @@ -152815,6 +152846,7 @@ export const enumMatchOptionsUpdateColumn = { match_mode: 'match_mode' as const, mr: 'mr' as const, number_of_substitutes: 'number_of_substitutes' as const, + min_players_per_lineup: 'min_players_per_lineup' as const, overtime: 'overtime' as const, prefer_dedicated_server: 'prefer_dedicated_server' as const, ready_setting: 'ready_setting' as const, diff --git a/generated/types.ts b/generated/types.ts index 013b86380..f3859b26f 100644 --- a/generated/types.ts +++ b/generated/types.ts @@ -32929,6 +32929,12 @@ export default { "competitive_safe": [ 6 ], + "allow_short_handed_start": [ + 6 + ], + "players_per_team": [ + 41 + ], "created_at": [ 5243 ], @@ -33117,6 +33123,12 @@ export default { "competitive_safe": [ 7 ], + "allow_short_handed_start": [ + 7 + ], + "players_per_team": [ + 42 + ], "created_at": [ 5244 ], @@ -33177,6 +33189,12 @@ export default { "competitive_safe": [ 6 ], + "allow_short_handed_start": [ + 6 + ], + "players_per_team": [ + 41 + ], "created_at": [ 5243 ], @@ -33330,6 +33348,12 @@ export default { "competitive_safe": [ 3648 ], + "allow_short_handed_start": [ + 3648 + ], + "players_per_team": [ + 3648 + ], "created_at": [ 3648 ], @@ -33392,6 +33416,12 @@ export default { "competitive_safe": [ 6 ], + "allow_short_handed_start": [ + 6 + ], + "players_per_team": [ + 41 + ], "created_at": [ 5243 ], @@ -33444,6 +33474,12 @@ export default { "competitive_safe": [ 6 ], + "allow_short_handed_start": [ + 6 + ], + "players_per_team": [ + 41 + ], "created_at": [ 5243 ], @@ -59332,6 +59368,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "overtime": [ 6 ], @@ -59566,6 +59605,9 @@ export default { "number_of_substitutes": [ 32 ], + "min_players_per_lineup": [ + 32 + ], "round_restart_delay": [ 32 ], @@ -59595,6 +59637,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], @@ -59690,6 +59735,9 @@ export default { "number_of_substitutes": [ 42 ], + "min_players_per_lineup": [ + 42 + ], "overtime": [ 7 ], @@ -59753,6 +59801,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "round_restart_delay": [ 41 ], @@ -59833,6 +59884,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "overtime": [ 6 ], @@ -59907,6 +59961,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "regions": [ 85 ], @@ -59951,6 +60008,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "regions": [ 3648 ], @@ -59995,6 +60055,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "regions": [ 85 ], @@ -60039,6 +60102,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "regions": [ 3648 ], @@ -60161,6 +60227,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "overtime": [ 3648 ], @@ -60276,6 +60345,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "overtime": [ 6 ], @@ -60329,6 +60401,9 @@ export default { "number_of_substitutes": [ 32 ], + "min_players_per_lineup": [ + 32 + ], "round_restart_delay": [ 32 ], @@ -60358,6 +60433,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], @@ -60387,6 +60465,9 @@ export default { "number_of_substitutes": [ 32 ], + "min_players_per_lineup": [ + 32 + ], "round_restart_delay": [ 32 ], @@ -60416,6 +60497,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], @@ -60445,6 +60529,9 @@ export default { "number_of_substitutes": [ 32 ], + "min_players_per_lineup": [ + 32 + ], "round_restart_delay": [ 32 ], @@ -60474,6 +60561,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], @@ -60556,6 +60646,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "overtime": [ 6 ], @@ -60609,6 +60702,9 @@ export default { "number_of_substitutes": [ 41 ], + "min_players_per_lineup": [ + 41 + ], "round_restart_delay": [ 41 ], @@ -60638,6 +60734,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], @@ -60682,6 +60781,9 @@ export default { "number_of_substitutes": [ 32 ], + "min_players_per_lineup": [ + 32 + ], "round_restart_delay": [ 32 ], @@ -60711,6 +60813,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], @@ -60740,6 +60845,9 @@ export default { "number_of_substitutes": [ 32 ], + "min_players_per_lineup": [ + 32 + ], "round_restart_delay": [ 32 ], @@ -60769,6 +60877,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], @@ -60798,6 +60909,9 @@ export default { "number_of_substitutes": [ 32 ], + "min_players_per_lineup": [ + 32 + ], "round_restart_delay": [ 32 ], @@ -60827,6 +60941,9 @@ export default { "number_of_substitutes": [ 3648 ], + "min_players_per_lineup": [ + 3648 + ], "round_restart_delay": [ 3648 ], diff --git a/hasura/functions/match/check_match_has_min_players.sql b/hasura/functions/match/check_match_has_min_players.sql index 5b484c08a..be41259d9 100644 --- a/hasura/functions/match/check_match_has_min_players.sql +++ b/hasura/functions/match/check_match_has_min_players.sql @@ -5,11 +5,11 @@ DECLARE min_players INTEGER; lineup_1_count INTEGER; lineup_2_count INTEGER; - match_type VARCHAR(255); + options public.match_options; BEGIN - SELECT type INTO match_type - FROM match_options - WHERE id = match.match_options_id; + SELECT mo.* INTO options + FROM match_options mo + WHERE mo.id = match.match_options_id; WITH lineup_counts AS ( SELECT get_lineup_counts(match) AS counts @@ -22,7 +22,7 @@ BEGIN lineup_2_count FROM lineup_counts; - min_players := get_match_type_min_players(match_type); + min_players := get_match_options_min_players(options); IF lineup_1_count < min_players OR lineup_2_count < min_players THEN RETURN false; @@ -30,4 +30,4 @@ BEGIN RETURN true; END; -$$; \ No newline at end of file +$$; diff --git a/hasura/functions/match/get_match_options_min_players.sql b/hasura/functions/match/get_match_options_min_players.sql new file mode 100644 index 000000000..53a213257 --- /dev/null +++ b/hasura/functions/match/get_match_options_min_players.sql @@ -0,0 +1,35 @@ +-- The per-lineup minimum for one match, in precedence order: +-- 1. what the match actually launched at (set when a draft force-started short-handed) +-- 2. the custom game mode's own team size +-- 3. the match type's fixed 5 / 2 / 1 +-- Everything that gates on "enough players" reads this rather than the type, so +-- a custom mode can size a match without touching the closed e_match_types enum. +-- +-- plpgsql, not sql: this file sorts before get_match_type_min_players.sql, and a +-- LANGUAGE sql body is resolved at creation time -- which would fail the very +-- first boot against an empty database. +CREATE OR REPLACE FUNCTION public.get_match_options_min_players(mo public.match_options) +RETURNS integer +LANGUAGE plpgsql +STABLE +AS $$ +DECLARE + mode_players integer; +BEGIN + IF mo.min_players_per_lineup IS NOT NULL THEN + RETURN mo.min_players_per_lineup; + END IF; + + IF mo.game_mode_id IS NOT NULL THEN + SELECT gm.players_per_team INTO mode_players + FROM public.game_modes gm + WHERE gm.id = mo.game_mode_id; + + IF mode_players IS NOT NULL THEN + RETURN mode_players; + END IF; + END IF; + + RETURN public.get_match_type_min_players(mo.type); +END; +$$; diff --git a/hasura/functions/match/lineups/is_match_lineup_ready.sql b/hasura/functions/match/lineups/is_match_lineup_ready.sql index 0820180fa..04bfa9984 100644 --- a/hasura/functions/match/lineups/is_match_lineup_ready.sql +++ b/hasura/functions/match/lineups/is_match_lineup_ready.sql @@ -3,17 +3,19 @@ RETURNS boolean LANGUAGE plpgsql STABLE AS $$ DECLARE - match_type text; + options public.match_options; total_checked_in int; _check_in_setting text; BEGIN - SELECT mo.type, mo.check_in_setting - INTO match_type, _check_in_setting + SELECT mo.* + INTO options FROM matches m INNER JOIN match_options mo ON mo.id = m.match_options_id WHERE m.id = match_lineup.match_id LIMIT 1; + _check_in_setting := options.check_in_setting; + IF _check_in_setting = 'Captains' THEN SELECT count(*) INTO total_checked_in @@ -29,6 +31,6 @@ BEGIN FROM match_lineup_players mlp WHERE mlp.match_lineup_id = match_lineup.id AND mlp.checked_in = true; - RETURN total_checked_in >= get_match_type_min_players(match_type); + RETURN total_checked_in >= get_match_options_min_players(options); END; $$; diff --git a/hasura/functions/match/match_players_per_lineup.sql b/hasura/functions/match/match_players_per_lineup.sql index 0aa36adf3..2c00229ed 100644 --- a/hasura/functions/match/match_players_per_lineup.sql +++ b/hasura/functions/match/match_players_per_lineup.sql @@ -3,7 +3,7 @@ RETURNS integer LANGUAGE sql STABLE AS $$ - SELECT get_match_type_min_players(mo.type) + COALESCE(mo.number_of_substitutes, 0) + SELECT get_match_options_min_players(mo) + COALESCE(mo.number_of_substitutes, 0) FROM match_options mo WHERE mo.id = match.match_options_id; $$; @@ -13,7 +13,7 @@ RETURNS integer LANGUAGE sql STABLE AS $$ - SELECT get_match_type_min_players(mo.type) + SELECT get_match_options_min_players(mo) FROM match_options mo WHERE mo.id = match.match_options_id; $$; @@ -23,7 +23,7 @@ RETURNS integer LANGUAGE sql STABLE AS $$ - SELECT get_match_type_min_players(mo.type) + COALESCE(mo.number_of_substitutes, 0) + SELECT get_match_options_min_players(mo) + COALESCE(mo.number_of_substitutes, 0) FROM match_options mo WHERE mo.id = tournament.match_options_id; $$; @@ -33,7 +33,7 @@ RETURNS integer LANGUAGE sql STABLE AS $$ - SELECT get_match_type_min_players(mo.type) + SELECT get_match_options_min_players(mo) FROM match_options mo WHERE mo.id = tournament.match_options_id; $$; diff --git a/hasura/metadata/actions.graphql b/hasura/metadata/actions.graphql index c489d7cf3..eeeed742d 100644 --- a/hasura/metadata/actions.graphql +++ b/hasura/metadata/actions.graphql @@ -1687,6 +1687,13 @@ type Mutation { ): SuccessOutput } +type Mutation { + startDraftGame( + draftGameId: uuid! + force: Boolean + ): SuccessOutput +} + type Mutation { joinDraftGame( draftGameId: uuid! diff --git a/hasura/metadata/actions.yaml b/hasura/metadata/actions.yaml index a82594ea4..6a75ae528 100644 --- a/hasura/metadata/actions.yaml +++ b/hasura/metadata/actions.yaml @@ -65,6 +65,14 @@ actions: permissions: - role: user comment: updateDraftGame + - name: startDraftGame + definition: + kind: synchronous + handler: '{{HASURA_GRAPHQL_ACTIONS_HOOK}}' + forward_client_headers: true + permissions: + - role: user + comment: startDraftGame - name: joinDraftGame definition: kind: synchronous diff --git a/hasura/metadata/databases/default/tables/public_game_modes.yaml b/hasura/metadata/databases/default/tables/public_game_modes.yaml index d7b2e171a..0861ec018 100644 --- a/hasura/metadata/databases/default/tables/public_game_modes.yaml +++ b/hasura/metadata/databases/default/tables/public_game_modes.yaml @@ -41,6 +41,8 @@ select_permissions: - enabled - archived_at - competitive_safe + - players_per_team + - allow_short_handed_start - cfg - extra_game_params - created_at @@ -62,6 +64,8 @@ select_permissions: - enabled - archived_at - competitive_safe + - players_per_team + - allow_short_handed_start - cfg - extra_game_params - created_at @@ -86,6 +90,8 @@ insert_permissions: - enabled - archived_at - competitive_safe + - players_per_team + - allow_short_handed_start - cfg - extra_game_params computed_fields: @@ -103,6 +109,8 @@ update_permissions: - enabled - archived_at - competitive_safe + - players_per_team + - allow_short_handed_start - cfg - extra_game_params computed_fields: diff --git a/hasura/metadata/databases/default/tables/public_match_options.yaml b/hasura/metadata/databases/default/tables/public_match_options.yaml index a424839a6..7119e1fe2 100644 --- a/hasura/metadata/databases/default/tables/public_match_options.yaml +++ b/hasura/metadata/databases/default/tables/public_match_options.yaml @@ -122,6 +122,7 @@ select_permissions: - match_mode - mr - number_of_substitutes + - min_players_per_lineup - overtime - prefer_dedicated_server - ready_setting diff --git a/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql b/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql new file mode 100644 index 000000000..238375981 --- /dev/null +++ b/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "public"."match_options" DROP COLUMN IF EXISTS "min_players_per_lineup"; + +ALTER TABLE "public"."game_modes" DROP CONSTRAINT IF EXISTS "game_modes_players_per_team_check"; +ALTER TABLE "public"."game_modes" DROP COLUMN IF EXISTS "allow_short_handed_start"; +ALTER TABLE "public"."game_modes" DROP COLUMN IF EXISTS "players_per_team"; diff --git a/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql b/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql new file mode 100644 index 000000000..f0c2cacc8 --- /dev/null +++ b/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql @@ -0,0 +1,28 @@ +-- A mode's team size. NULL means "inherit the match type's count", which is what +-- every mode created before this migration does, so nothing changes for them. +-- Capped at 5 because a CS2 team has five slots and match_lineup_players is +-- sized off the same number. +ALTER TABLE "public"."game_modes" + ADD COLUMN IF NOT EXISTS "players_per_team" integer; + +-- Whether a draft lobby running this mode may start before both sides are full. +-- Defaults off so an existing mode can never start short-handed by accident. +ALTER TABLE "public"."game_modes" + ADD COLUMN IF NOT EXISTS "allow_short_handed_start" boolean NOT NULL DEFAULT false; + +DO $$ +BEGIN + ALTER TABLE "public"."game_modes" + ADD CONSTRAINT "game_modes_players_per_team_check" + CHECK ("players_per_team" IS NULL OR "players_per_team" BETWEEN 1 AND 5); +EXCEPTION + WHEN duplicate_object THEN NULL; +END; +$$; + +-- The size a match actually launched at, written only when a lobby force-starts +-- short-handed. Snapshotted rather than resolved live so editing the mode later +-- cannot retroactively invalidate a match that is already running. NULL falls +-- back to the mode, then to the match type. +ALTER TABLE "public"."match_options" + ADD COLUMN IF NOT EXISTS "min_players_per_lineup" integer; diff --git a/hasura/triggers/draft_games.sql b/hasura/triggers/draft_games.sql index 4163b0f5e..a434836da 100644 --- a/hasura/triggers/draft_games.sql +++ b/hasura/triggers/draft_games.sql @@ -1,16 +1,26 @@ CREATE OR REPLACE FUNCTION public.tbi_draft_games() RETURNS TRIGGER LANGUAGE plpgsql AS $$ +DECLARE + _mode_capacity integer; BEGIN IF NOT has_available_server_region() THEN RAISE EXCEPTION 'No game server regions are currently available' USING ERRCODE = '22000'; END IF; - NEW.capacity := CASE NEW.type + -- A custom mode can size its own teams; everything else falls back to the + -- match type. match_options is inserted before the draft row, so the mode is + -- already reachable here. + SELECT gm.players_per_team * 2 INTO _mode_capacity + FROM public.match_options mo + INNER JOIN public.game_modes gm ON gm.id = mo.game_mode_id + WHERE mo.id = NEW.match_options_id; + + NEW.capacity := COALESCE(_mode_capacity, CASE NEW.type WHEN 'Duel' THEN 2 WHEN 'Wingman' THEN 4 ELSE 10 - END; + END); IF NEW.mode = 'Teams' AND NEW.team_1_id IS NOT NULL AND NEW.team_2_id IS NOT NULL THEN NEW.access := 'Private'; @@ -33,6 +43,7 @@ DECLARE accepted_count integer; team1 integer; team2 integer; + short_handed boolean; BEGIN IF NEW.expires_at IS DISTINCT FROM OLD.expires_at AND NEW.expires_at IS NOT NULL THEN NEW.expires_at := LEAST(NEW.expires_at, NEW.created_at + interval '120 minutes'); @@ -54,16 +65,39 @@ BEGIN FROM public.draft_game_players WHERE draft_game_id = NEW.id; + -- A custom mode may permit starting before both sides are full. The + -- lobby still has to hold a real match: at least one player a side, and + -- an even pool for Captains, whose pick pattern is built on capacity / 2 + -- and produces a malformed order for an odd number of players. + SELECT COALESCE(gm.allow_short_handed_start, false) INTO short_handed + FROM public.match_options mo + INNER JOIN public.game_modes gm ON gm.id = mo.game_mode_id + WHERE mo.id = NEW.match_options_id; + + short_handed := COALESCE(short_handed, false); + IF NEW.mode = 'Teams' THEN IF NEW.team_1_id IS NULL THEN RAISE EXCEPTION 'Select at least one team first' USING ERRCODE = '22000'; END IF; ELSIF NEW.mode = 'Host' THEN - IF team1 <> NEW.capacity / 2 OR team2 <> NEW.capacity / 2 THEN + IF short_handed THEN + IF team1 < 1 OR team2 < 1 THEN + RAISE EXCEPTION 'Assign at least one player to each team first' USING ERRCODE = '22000'; + END IF; + ELSIF team1 <> NEW.capacity / 2 OR team2 <> NEW.capacity / 2 THEN RAISE EXCEPTION 'Assign all players into balanced teams first' USING ERRCODE = '22000'; END IF; ELSE - IF accepted_count <> NEW.capacity THEN + IF short_handed THEN + IF accepted_count < 2 THEN + RAISE EXCEPTION 'At least two players must accept before starting' USING ERRCODE = '22000'; + END IF; + + IF NEW.mode = 'Captains' AND accepted_count % 2 <> 0 THEN + RAISE EXCEPTION 'A captains draft needs an even number of players' USING ERRCODE = '22000'; + END IF; + ELSIF accepted_count <> NEW.capacity THEN RAISE EXCEPTION 'The lobby must be full to start' USING ERRCODE = '22000'; END IF; END IF; diff --git a/hasura/triggers/match_lineup_players.sql b/hasura/triggers/match_lineup_players.sql index 2194bd5a1..c6b691fad 100644 --- a/hasura/triggers/match_lineup_players.sql +++ b/hasura/triggers/match_lineup_players.sql @@ -42,11 +42,13 @@ LANGUAGE plpgsql AS $$ DECLARE status text; - match_type text; lineup_count INT; _max_players_per_lineup INT; BEGIN - SELECT mo.type, m.status INTO match_type, status + -- Resolved here rather than carrying the match_options row: plpgsql refuses + -- a record variable in a multiple-item INTO list. + SELECT get_match_options_min_players(mo), m.status + INTO _max_players_per_lineup, status FROM matches m INNER JOIN match_lineups ml ON ml.match_id = m.id INNER JOIN match_options mo ON mo.id = m.match_options_id @@ -64,8 +66,6 @@ BEGIN WHERE match_lineup_id = OLD.match_lineup_id; IF ((status != 'PickingPlayers' AND status != 'Canceled') AND (current_setting('hasura.user', true)::jsonb ->> 'x-hasura-role')::text != 'admin') THEN - SELECT get_match_type_min_players(match_type) INTO _max_players_per_lineup; - IF (lineup_count - 1) >= _max_players_per_lineup THEN RETURN OLD; END IF; diff --git a/src/draft-games/draft-game.service.ts b/src/draft-games/draft-game.service.ts index 15b8db876..4619a59fe 100644 --- a/src/draft-games/draft-game.service.ts +++ b/src/draft-games/draft-game.service.ts @@ -15,7 +15,11 @@ import { import { HasuraService } from "src/hasura/hasura.service"; import { NotificationsService } from "../notifications/notifications.service"; import { CacheService } from "src/cache/cache.service"; -import { ExpectedPlayers } from "src/discord-bot/enums/ExpectedPlayers"; +import { + GameModeSizing, + allowsShortHandedStart as modeAllowsShortHandedStart, + resolveCapacity, +} from "src/game-plugins/resolveExpectedPlayers"; import { isRoleAbove } from "src/utilities/isRoleAbove"; import { DraftGame } from "./types/DraftGame"; import { DraftGameError } from "./types/DraftGameError"; @@ -132,7 +136,13 @@ export class DraftGameService { } } - const capacity = ExpectedPlayers[settings.type]; + // A custom mode can size its own teams. The DB trigger derives capacity + // the same way off match_options; this copy is what seeds the lobby with + // the host's party, so the two have to agree. + const gameMode = await this.getGameModeSizing( + this.requestedGameModeId(user, settings.options), + ); + const capacity = resolveCapacity(settings.type, gameMode); const elo = hostJoins ? await this.getPlayerElo(user.steam_id, settings.type) : null; @@ -1049,6 +1059,51 @@ export class DraftGameService { return insert_map_pools_one.id; } + // game_mode_id is gated behind match_organizer in matchOptionScalars, so a + // lower role submitting one must not be allowed to resize the lobby with it. + private requestedGameModeId( + user: User, + options?: Record, + ): string | null { + if (!options || !isRoleAbove(user.role, "match_organizer")) { + return null; + } + + return (options as Record).game_mode_id || null; + } + + // What the mode will be after this settings update: the submitted one when + // the user may set it, otherwise whatever match_options already holds. + private async nextGameModeId( + user: User, + settings: Partial, + draftGame: DraftGame, + ): Promise { + if (settings.options && isRoleAbove(user.role, "match_organizer")) { + return this.requestedGameModeId(user, settings.options); + } + + return this.currentGameModeId(draftGame); + } + + private async getGameModeSizing( + gameModeId?: string | null, + ): Promise { + if (!gameModeId) { + return null; + } + + const { game_modes_by_pk } = await this.hasura.query({ + game_modes_by_pk: { + __args: { id: gameModeId }, + players_per_team: true, + allow_short_handed_start: true, + }, + }); + + return game_modes_by_pk ?? null; + } + private matchOptionScalars( user: User, options: Record, @@ -1177,6 +1232,134 @@ export class DraftGameService { } } + // Flipping the row to Filled is what kicks off beginDraft, and the DB trigger + // refuses that unless the lobby is ready. A short-handed start has to narrow + // the lobby to the players who actually turned up *before* that trigger and + // the draft pick pattern read capacity, which a raw client mutation cannot do. + public async startDraftGame(user: User, draftGameId: string, force: boolean) { + return this.draftLock(draftGameId, async () => { + const draftGame = await this.getDraftGame(draftGameId); + + if (!draftGame) { + throw new DraftGameError("Draft game not found"); + } + + if (!this.isOrganizerOrHost(user, draftGame)) { + throw new DraftGameError( + "Only the host or an organizer can start the match", + ); + } + + if (draftGame.status !== "Open") { + throw new DraftGameError("This draft has already started"); + } + + const accepted = this.acceptedPlayers(draftGame); + const isFull = + draftGame.mode === "Teams" || accepted.length >= draftGame.capacity; + + // Narrowing happens before the status flip, never after: the trigger lets + // a short-handed mode through on the mode alone, so flipping first and + // narrowing second would leave a lobby stuck in Filled that beginDraft + // then refuses to act on -- and every player in it locked out of joining + // anything else. + if (!isFull) { + if (!force) { + throw new DraftGameError("The lobby must be full to start"); + } + + await this.applyShortHandedStart(draftGame, accepted.length); + } + + await this.hasura.mutation({ + update_draft_games_by_pk: { + __args: { + pk_columns: { id: draftGameId }, + _set: { status: "Filled" }, + }, + __typename: true, + }, + }); + }); + } + + // Pug and Captains split whoever accepted across two sides, so the lobby's + // capacity has to come down to that pool or autoSplit and the pick pattern + // still plan around empty seats. Host and Teams place players by hand and are + // left alone -- their sides are already whatever the host made them. + private async applyShortHandedStart( + draftGame: DraftGame, + acceptedCount: number, + ) { + const gameMode = await this.getGameModeSizing( + await this.currentGameModeId(draftGame), + ); + + if (!modeAllowsShortHandedStart(gameMode)) { + throw new DraftGameError("The lobby must be full to start"); + } + + if (draftGame.mode === "Teams" || draftGame.mode === "Host") { + return; + } + + if (acceptedCount < 2) { + throw new DraftGameError( + "At least two players must accept before starting", + ); + } + + // get_draft_game_pattern builds the pick order off capacity / 2, which an + // odd pool turns into a short pattern that strands the last player. + if (draftGame.mode === "Captains" && acceptedCount % 2 !== 0) { + throw new DraftGameError( + "A captains draft needs an even number of players", + ); + } + + if (acceptedCount >= draftGame.capacity) { + return; + } + + await this.hasura.mutation({ + update_draft_games_by_pk: { + __args: { + pk_columns: { id: draftGame.id }, + _set: { capacity: acceptedCount }, + }, + __typename: true, + }, + }); + + draftGame.capacity = acceptedCount; + } + + // Whether this lobby's custom mode lets it start before both sides are full. + public async allowsShortHandedStart(draftGame: DraftGame): Promise { + const gameMode = await this.getGameModeSizing( + await this.currentGameModeId(draftGame), + ); + + return modeAllowsShortHandedStart(gameMode); + } + + private async currentGameModeId( + draftGame: DraftGame, + ): Promise { + if (!draftGame.match_options_id) { + return null; + } + + const { match_options_by_pk } = await this.hasura.query({ + match_options_by_pk: { + __args: { id: draftGame.match_options_id }, + game_mode_id: true, + }, + }); + + return match_options_by_pk?.game_mode_id || null; + } + public async updateDraftSettings( user: User, draftGameId: string, @@ -1211,17 +1394,28 @@ export class DraftGameService { ]); } - let capacity = draftGame.capacity; - if (settings.type && settings.type !== draftGame.type) { - if (!DraftGameService.DRAFTABLE_TYPES.includes(settings.type)) { - throw new DraftGameError("Invalid draft game type"); - } - capacity = ExpectedPlayers[settings.type]; + if ( + settings.type && + settings.type !== draftGame.type && + !DraftGameService.DRAFTABLE_TYPES.includes(settings.type) + ) { + throw new DraftGameError("Invalid draft game type"); } + // Swapping the custom mode resizes the lobby just as swapping the type + // does, so both have to be resolved together -- a 3v3 mode traded for + // Competitive has to grow back to 10 even though the type never moved. + const nextType = settings.type || draftGame.type; + const nextGameMode = await this.getGameModeSizing( + await this.nextGameModeId(user, settings, draftGame), + ); + const capacity = resolveCapacity(nextType, nextGameMode); + const _set: Record = {}; if (settings.type) { _set.type = settings.type; + } + if (capacity !== draftGame.capacity) { _set.capacity = capacity; } if (settings.regions) { diff --git a/src/draft-games/draft-games.controller.ts b/src/draft-games/draft-games.controller.ts index 3f728504e..2140b2f8b 100644 --- a/src/draft-games/draft-games.controller.ts +++ b/src/draft-games/draft-games.controller.ts @@ -72,6 +72,20 @@ export class DraftGamesController { return { success: true }; } + @HasuraAction() + public async startDraftGame(data: { + user: User; + draftGameId: string; + force?: boolean; + }) { + await this.draftGameService.startDraftGame( + data.user, + data.draftGameId, + data.force === true, + ); + return { success: true }; + } + @HasuraAction() public async joinDraftGame(data: { user: User; diff --git a/src/draft-games/draft-match.service.ts b/src/draft-games/draft-match.service.ts index 2ba147a45..794b35c7b 100644 --- a/src/draft-games/draft-match.service.ts +++ b/src/draft-games/draft-match.service.ts @@ -1,7 +1,7 @@ import { Logger } from "@nestjs/common"; import { forwardRef, Inject, Injectable } from "@nestjs/common"; import { e_map_pool_types_enum } from "generated"; -import { ExpectedPlayers } from "src/discord-bot/enums/ExpectedPlayers"; +import { resolvePlayersPerTeam } from "src/game-plugins/resolveExpectedPlayers"; import { HasuraService } from "src/hasura/hasura.service"; import { CacheService } from "src/cache/cache.service"; import { MatchAssistantService } from "src/matches/match-assistant/match-assistant.service"; @@ -93,6 +93,8 @@ export class DraftMatchService { await this.ensureLineups(draftGame, match); + await this.stampLaunchSize(draftGame, match); + // The room's conversation continues in the match chat: carry the history // over and tear down the draft lobby so non-participants drop out. await this.chat.migrateLobbyMessages( @@ -125,6 +127,81 @@ export class DraftMatchService { await this.matchAssistant.updateMatchStatus(match.id, "WaitingForCheckIn"); } + // A lobby that started short-handed produces a match smaller than its mode + // declares, and every gate downstream -- check_match_has_min_players, + // is_match_lineup_ready, the lineup removal trigger -- would refuse it. Pin + // the size it actually launched at so those read the real shape. + // + // The smaller side wins because the gates apply one number to both lineups; an + // uneven 3v2 has to clear at 2 or it can never start. + private async stampLaunchSize( + draftGame: DraftGame, + match: { id: string; lineup_1_id?: string | null; lineup_2_id?: string | null }, + ) { + if (!draftGame.match_options_id) { + return; + } + + const { match_options_by_pk } = await this.hasura.query({ + match_options_by_pk: { + __args: { id: draftGame.match_options_id }, + game_mode: { + players_per_team: true, + allow_short_handed_start: true, + }, + }, + }); + + const gameMode = match_options_by_pk?.game_mode; + + if (!gameMode?.allow_short_handed_start) { + return; + } + + const declared = resolvePlayersPerTeam(draftGame.type, gameMode); + + const counts = await this.lineupCounts(match); + const smallest = Math.min(...counts); + + if (smallest >= declared) { + return; + } + + await this.hasura.mutation({ + update_match_options_by_pk: { + __args: { + pk_columns: { id: draftGame.match_options_id }, + _set: { min_players_per_lineup: Math.max(1, smallest) }, + }, + __typename: true, + }, + }); + } + + private async lineupCounts(match: { + lineup_1_id?: string | null; + lineup_2_id?: string | null; + }): Promise> { + const counts: Array = []; + + for (const lineupId of [match.lineup_1_id, match.lineup_2_id]) { + if (!lineupId) { + continue; + } + + const { match_lineup_players } = await this.hasura.query({ + match_lineup_players: { + __args: { where: { match_lineup_id: { _eq: lineupId } } }, + steam_id: true, + }, + }); + + counts.push(match_lineup_players.length); + } + + return counts.length > 0 ? counts : [0]; + } + private async ensureLineups( draftGame: DraftGame, match: { lineup_1_id?: string | null; lineup_2_id?: string | null }, @@ -333,19 +410,26 @@ export class DraftMatchService { } private async maxPlayersPerLineup(draftGame: DraftGame): Promise { - const starters = Math.floor(ExpectedPlayers[draftGame.type] / 2); - if (!draftGame.match_options_id) { - return starters; + return resolvePlayersPerTeam(draftGame.type); } const { match_options_by_pk } = await this.hasura.query({ match_options_by_pk: { __args: { id: draftGame.match_options_id }, number_of_substitutes: true, + game_mode: { + players_per_team: true, + allow_short_handed_start: true, + }, }, }); + const starters = resolvePlayersPerTeam( + draftGame.type, + match_options_by_pk?.game_mode, + ); + return starters + (match_options_by_pk?.number_of_substitutes ?? 0); } diff --git a/src/draft-games/draft.service.ts b/src/draft-games/draft.service.ts index d09b23840..fefc5ca04 100644 --- a/src/draft-games/draft.service.ts +++ b/src/draft-games/draft.service.ts @@ -45,6 +45,8 @@ export class DraftService { const perTeam = draftGame.capacity / 2; const accepted = this.draftGameService.acceptedPlayers(draftGame); + const shortHanded = + await this.draftGameService.allowsShortHandedStart(draftGame); if (draftGame.mode === "Teams") { if (!draftGame.team_1_id) { @@ -54,10 +56,13 @@ export class DraftService { return; } + // Only a short-handed lobby relaxes these. It narrows capacity to the + // players who turned up, so an equality could strand a room the trigger + // already cleared; every other lobby keeps the exact == it always had. if (draftGame.mode === "Host") { const team1 = accepted.filter((player) => player.lineup === 1).length; const team2 = accepted.filter((player) => player.lineup === 2).length; - if (team1 !== perTeam || team2 !== perTeam) { + if (shortHanded ? team1 < 1 || team2 < 1 : team1 !== perTeam || team2 !== perTeam) { return; } await this.draftMatchService.finalize(draftGameId); @@ -65,7 +70,11 @@ export class DraftService { } if (draftGame.mode === "Pug") { - if (accepted.length !== draftGame.capacity) { + if ( + shortHanded + ? accepted.length < draftGame.capacity + : accepted.length !== draftGame.capacity + ) { return; } await this.autoSplit(draftGame); @@ -74,7 +83,11 @@ export class DraftService { } if (draftGame.mode === "Captains") { - if (accepted.length !== draftGame.capacity) { + if ( + shortHanded + ? accepted.length < draftGame.capacity + : accepted.length !== draftGame.capacity + ) { return; } await this.runDraftStart(draftGameId); diff --git a/src/game-plugins/game-modes.service.ts b/src/game-plugins/game-modes.service.ts index eceab9590..be381a601 100644 --- a/src/game-plugins/game-modes.service.ts +++ b/src/game-plugins/game-modes.service.ts @@ -9,6 +9,9 @@ export type ResolvedGameMode = { name: string; cfg: string | null; extraGameParams: string | null; + // How many players a side this mode is built for; null means it inherits the + // match type's count. + playersPerTeam: number | null; enabledPlugins: string; pluginConfigs: string | null; missingRequired: Array; @@ -41,6 +44,7 @@ type ModeRow = { name: string; cfg: string | null; extra_game_params: string | null; + players_per_team: number | null; }; type ModePluginRow = { @@ -197,6 +201,8 @@ export class GameModesService { name: "", cfg: null, extraGameParams: null, + // No mode means no size override; the match keeps its type's count. + playersPerTeam: null, enabledPlugins: enabled.join(","), pluginConfigs: null, missingRequired: [], @@ -257,7 +263,7 @@ export class GameModesService { scope?: PluginScope, ): Promise { const [mode] = await this.postgres.query>( - `SELECT id, slug, name, cfg, extra_game_params + `SELECT id, slug, name, cfg, extra_game_params, players_per_team FROM game_modes WHERE id = $1 AND enabled = true AND archived_at IS NULL`, [gameModeId], @@ -328,6 +334,7 @@ export class GameModesService { name: mode.name, cfg: mode.cfg, extraGameParams: mode.extra_game_params, + playersPerTeam: mode.players_per_team ?? null, enabledPlugins: enabled.join(","), pluginConfigs: Object.keys(configs).length > 0 diff --git a/src/game-plugins/resolveExpectedPlayers.ts b/src/game-plugins/resolveExpectedPlayers.ts new file mode 100644 index 000000000..4ccc66591 --- /dev/null +++ b/src/game-plugins/resolveExpectedPlayers.ts @@ -0,0 +1,32 @@ +import { e_match_types_enum } from "generated"; +import { ExpectedPlayers } from "src/discord-bot/enums/ExpectedPlayers"; + +// A custom game mode may declare its own team size. Everything else -- every +// ranked type, and any mode that leaves players_per_team null -- keeps the fixed +// count the match type has always carried. +export interface GameModeSizing { + players_per_team?: number | null; + allow_short_handed_start?: boolean | null; +} + +export function resolvePlayersPerTeam( + type: e_match_types_enum, + gameMode?: GameModeSizing | null, +): number { + if (gameMode?.players_per_team) { + return gameMode.players_per_team; + } + + return ExpectedPlayers[type] / 2; +} + +export function resolveCapacity( + type: e_match_types_enum, + gameMode?: GameModeSizing | null, +): number { + return resolvePlayersPerTeam(type, gameMode) * 2; +} + +export function allowsShortHandedStart(gameMode?: GameModeSizing | null) { + return gameMode?.allow_short_handed_start === true; +} diff --git a/src/matches/events/MatchUpdatedLineupsEvent.ts b/src/matches/events/MatchUpdatedLineupsEvent.ts index d93e1c10c..0ab66cfa4 100644 --- a/src/matches/events/MatchUpdatedLineupsEvent.ts +++ b/src/matches/events/MatchUpdatedLineupsEvent.ts @@ -1,4 +1,4 @@ -import { ExpectedPlayers } from "src/discord-bot/enums/ExpectedPlayers"; +import { resolvePlayersPerTeam } from "src/game-plugins/resolveExpectedPlayers"; import MatchEventProcessor from "./abstracts/MatchEventProcessor"; export default class MatchUpdatedLineupsEvent extends MatchEventProcessor<{ @@ -73,7 +73,14 @@ export default class MatchUpdatedLineupsEvent extends MatchEventProcessor<{ } } - if (players.length < ExpectedPlayers[match.options.type]) { + // A short-handed custom-mode match legitimately reports fewer players than + // the type expects; measuring against the type would throw the whole roster + // away and leave the lineups stale. + const expected = + match.options.min_players_per_lineup ?? + resolvePlayersPerTeam(match.options.type, match.options.game_mode); + + if (players.length < expected * 2) { return; } diff --git a/src/matches/match-assistant/match-assistant.service.ts b/src/matches/match-assistant/match-assistant.service.ts index eb00feb43..4de594e8b 100644 --- a/src/matches/match-assistant/match-assistant.service.ts +++ b/src/matches/match-assistant/match-assistant.service.ts @@ -246,6 +246,11 @@ export class MatchAssistantService { map_veto_picking_lineup_id: true, options: { type: true, + min_players_per_lineup: true, + game_mode: { + players_per_team: true, + allow_short_handed_start: true, + }, }, lineup_1_id: true, lineup_2_id: true, diff --git a/src/matches/matches.controller.ts b/src/matches/matches.controller.ts index 85fe3f18c..028525297 100644 --- a/src/matches/matches.controller.ts +++ b/src/matches/matches.controller.ts @@ -230,6 +230,7 @@ export class MatchesController { halftime_pausematch: true, camera_required: true, game_mode_id: true, + min_players_per_lineup: true, }, match_maps: { id: true, @@ -460,6 +461,14 @@ export class MatchesController { match.options.cfg_execs.push("mode"); } + // Tells the server this match is not the size its type implies, so the ready + // check, the short-handed auto-pause and the per-team join cap all measure + // against the roster it was actually given. The snapshot wins: a lobby that + // started short-handed is smaller than even its mode declares. Null on a + // normal match, where the plugin keeps its own 5/2/1 table. + match.options.min_players_per_lineup = + match.options.min_players_per_lineup ?? gameMode?.playersPerTeam ?? null; + // withAlwaysLoad answers with a nameless mode when the only thing to load // is an always-load plugin; that is not a mode the match is playing under. match.options.game_mode = gameMode?.id From 6a7f9c07f9c73dc0391e2be349bab6dcd5eafdf0 Mon Sep 17 00:00:00 2001 From: Kristians Laukis <7798036+LCrew@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:51:39 +0300 Subject: [PATCH 2/3] fix(draft): send the real total when a short-handed start is uneven min_players_per_lineup records the SMALLER side, because the gates apply one number to both lineups -- a 1v2 has to record 1 or the short side never clears check_match_has_min_players. The game server was doubling it, so a 1v2 asked for 2 players and could go live while the third was still connecting. One number cannot express an uneven start, so record the total too. expected_players counts starters off draft_game_players rather than the match lineups: buildTeams also seats waitlisted backups in the substitute slots, and a backup is not someone warmup should wait for. Only written when a lobby actually starts short-handed. An even start leaves it null and the server keeps doubling the per-lineup figure. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01G8LmkD3i1HyGjbcxAZ5Crz --- .../default/tables/public_match_options.yaml | 1 + .../down.sql | 1 + .../up.sql | 8 +++ src/draft-games/draft-match.service.ts | 52 +++++++++---------- src/matches/matches.controller.ts | 6 +++ 5 files changed, 42 insertions(+), 26 deletions(-) diff --git a/hasura/metadata/databases/default/tables/public_match_options.yaml b/hasura/metadata/databases/default/tables/public_match_options.yaml index 7119e1fe2..407fa0bc6 100644 --- a/hasura/metadata/databases/default/tables/public_match_options.yaml +++ b/hasura/metadata/databases/default/tables/public_match_options.yaml @@ -123,6 +123,7 @@ select_permissions: - mr - number_of_substitutes - min_players_per_lineup + - expected_players - overtime - prefer_dedicated_server - ready_setting diff --git a/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql b/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql index 238375981..f37dddc1d 100644 --- a/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql +++ b/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql @@ -1,3 +1,4 @@ +ALTER TABLE "public"."match_options" DROP COLUMN IF EXISTS "expected_players"; ALTER TABLE "public"."match_options" DROP COLUMN IF EXISTS "min_players_per_lineup"; ALTER TABLE "public"."game_modes" DROP CONSTRAINT IF EXISTS "game_modes_players_per_team_check"; diff --git a/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql b/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql index f0c2cacc8..4b6024535 100644 --- a/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql +++ b/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql @@ -26,3 +26,11 @@ $$; -- back to the mode, then to the match type. ALTER TABLE "public"."match_options" ADD COLUMN IF NOT EXISTS "min_players_per_lineup" integer; + +-- The total the match actually launched with, across both lineups. Stamped +-- beside min_players_per_lineup rather than derived from it, because a single +-- per-lineup number cannot express an uneven start: a 1v2 has to record 1 above +-- (the gates apply that to *both* sides, so the smaller one has to clear) while +-- the server still has to wait for 3 people. Counts starters only. +ALTER TABLE "public"."match_options" + ADD COLUMN IF NOT EXISTS "expected_players" integer; diff --git a/src/draft-games/draft-match.service.ts b/src/draft-games/draft-match.service.ts index 794b35c7b..5ae773a9c 100644 --- a/src/draft-games/draft-match.service.ts +++ b/src/draft-games/draft-match.service.ts @@ -132,8 +132,17 @@ export class DraftMatchService { // is_match_lineup_ready, the lineup removal trigger -- would refuse it. Pin // the size it actually launched at so those read the real shape. // - // The smaller side wins because the gates apply one number to both lineups; an - // uneven 3v2 has to clear at 2 or it can never start. + // Two numbers, because one cannot express an uneven start: + // + // min_players_per_lineup -- the SMALLER side. The gates apply it to both + // lineups, so a 1v2 has to record 1 or the short side never clears. + // expected_players -- the TOTAL. What the game server waits for. A 1v2 + // records 3; deriving it as min x 2 would give 2 and take the match live + // with someone still connecting. + // + // Both count starters only, read off the draft rather than the match lineups: + // buildTeams also seats waitlisted backups in the substitute slots, and a + // backup is not someone warmup should wait for. private async stampLaunchSize( draftGame: DraftGame, match: { id: string; lineup_1_id?: string | null; lineup_2_id?: string | null }, @@ -160,8 +169,8 @@ export class DraftMatchService { const declared = resolvePlayersPerTeam(draftGame.type, gameMode); - const counts = await this.lineupCounts(match); - const smallest = Math.min(...counts); + const [side1, side2] = this.starterCounts(draftGame); + const smallest = Math.min(side1, side2); if (smallest >= declared) { return; @@ -171,35 +180,26 @@ export class DraftMatchService { update_match_options_by_pk: { __args: { pk_columns: { id: draftGame.match_options_id }, - _set: { min_players_per_lineup: Math.max(1, smallest) }, + _set: { + min_players_per_lineup: Math.max(1, smallest), + expected_players: Math.max(2, side1 + side2), + }, }, __typename: true, }, }); } - private async lineupCounts(match: { - lineup_1_id?: string | null; - lineup_2_id?: string | null; - }): Promise> { - const counts: Array = []; - - for (const lineupId of [match.lineup_1_id, match.lineup_2_id]) { - if (!lineupId) { - continue; - } - - const { match_lineup_players } = await this.hasura.query({ - match_lineup_players: { - __args: { where: { match_lineup_id: { _eq: lineupId } } }, - steam_id: true, - }, - }); - - counts.push(match_lineup_players.length); - } + private starterCounts(draftGame: DraftGame): [number, number] { + const starters = (lineup: number) => + draftGame.players.filter( + (player) => + player.lineup === lineup && + player.status !== "Waitlist" && + player.status !== "Requested", + ).length; - return counts.length > 0 ? counts : [0]; + return [starters(1), starters(2)]; } private async ensureLineups( diff --git a/src/matches/matches.controller.ts b/src/matches/matches.controller.ts index 028525297..6f9c35ed6 100644 --- a/src/matches/matches.controller.ts +++ b/src/matches/matches.controller.ts @@ -231,6 +231,7 @@ export class MatchesController { camera_required: true, game_mode_id: true, min_players_per_lineup: true, + expected_players: true, }, match_maps: { id: true, @@ -469,6 +470,11 @@ export class MatchesController { match.options.min_players_per_lineup = match.options.min_players_per_lineup ?? gameMode?.playersPerTeam ?? null; + // The total to wait for, which min_players_per_lineup cannot express once + // the sides are uneven. Only a short-handed start records one; otherwise the + // server doubles the per-lineup figure as before. + match.options.expected_players = match.options.expected_players ?? null; + // withAlwaysLoad answers with a nameless mode when the only thing to load // is an always-load plugin; that is not a mode the match is playing under. match.options.game_mode = gameMode?.id From cd86def49d5740c59f7a15e25addea868819b38e Mon Sep 17 00:00:00 2001 From: Kristians Laukis <7798036+LCrew@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:15:45 +0300 Subject: [PATCH 3/3] fix(migrations): give expected_players its own version applyMigrations tracks versions, not content: `if (!applied.has(version))`. Appending the column to 1886000000000 was therefore a silent no-op on any database that had already run it -- while the metadata in the same build still granted permission on it. Hasura then failed to track match_options, dropped the objects that depended on it, and `matches_bool_exp` lost its `options` relationship: the draft room stopped loading. Its own version applies everywhere -- in order on a fresh install, and on its own for a database already carrying 1886000000000. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01G8LmkD3i1HyGjbcxAZ5Crz --- .../1886000000000_game_mode_player_counts/down.sql | 1 - .../1886000000000_game_mode_player_counts/up.sql | 8 -------- .../down.sql | 1 + .../up.sql | 13 +++++++++++++ 4 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 hasura/migrations/default/1886000000100_match_options_expected_players/down.sql create mode 100644 hasura/migrations/default/1886000000100_match_options_expected_players/up.sql diff --git a/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql b/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql index f37dddc1d..238375981 100644 --- a/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql +++ b/hasura/migrations/default/1886000000000_game_mode_player_counts/down.sql @@ -1,4 +1,3 @@ -ALTER TABLE "public"."match_options" DROP COLUMN IF EXISTS "expected_players"; ALTER TABLE "public"."match_options" DROP COLUMN IF EXISTS "min_players_per_lineup"; ALTER TABLE "public"."game_modes" DROP CONSTRAINT IF EXISTS "game_modes_players_per_team_check"; diff --git a/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql b/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql index 4b6024535..f0c2cacc8 100644 --- a/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql +++ b/hasura/migrations/default/1886000000000_game_mode_player_counts/up.sql @@ -26,11 +26,3 @@ $$; -- back to the mode, then to the match type. ALTER TABLE "public"."match_options" ADD COLUMN IF NOT EXISTS "min_players_per_lineup" integer; - --- The total the match actually launched with, across both lineups. Stamped --- beside min_players_per_lineup rather than derived from it, because a single --- per-lineup number cannot express an uneven start: a 1v2 has to record 1 above --- (the gates apply that to *both* sides, so the smaller one has to clear) while --- the server still has to wait for 3 people. Counts starters only. -ALTER TABLE "public"."match_options" - ADD COLUMN IF NOT EXISTS "expected_players" integer; diff --git a/hasura/migrations/default/1886000000100_match_options_expected_players/down.sql b/hasura/migrations/default/1886000000100_match_options_expected_players/down.sql new file mode 100644 index 000000000..253f57fe3 --- /dev/null +++ b/hasura/migrations/default/1886000000100_match_options_expected_players/down.sql @@ -0,0 +1 @@ +ALTER TABLE "public"."match_options" DROP COLUMN IF EXISTS "expected_players"; diff --git a/hasura/migrations/default/1886000000100_match_options_expected_players/up.sql b/hasura/migrations/default/1886000000100_match_options_expected_players/up.sql new file mode 100644 index 000000000..ee5a76c6c --- /dev/null +++ b/hasura/migrations/default/1886000000100_match_options_expected_players/up.sql @@ -0,0 +1,13 @@ +-- The total a match actually launched with, across both lineups. +-- +-- Its own migration rather than an edit to 1886000000000, because migrations are +-- tracked by version and never re-read: appending to one that has already been +-- applied is a silent no-op on every database that ran it. +-- +-- Separate from min_players_per_lineup because a single per-lineup number cannot +-- express an uneven start. A 1v2 records 1 there -- the gates apply that to +-- *both* sides, so the smaller one has to clear -- while the game server still +-- has to wait for 3 people. Counts starters only; NULL on every match that did +-- not start short-handed. +ALTER TABLE "public"."match_options" + ADD COLUMN IF NOT EXISTS "expected_players" integer;