diff --git a/lib/teiserver/battle.ex b/lib/teiserver/battle.ex index 2c3f3769a..2445d4a38 100644 --- a/lib/teiserver/battle.ex +++ b/lib/teiserver/battle.ex @@ -644,7 +644,7 @@ defmodule Teiserver.Battle do @spec update_lobby(T.lobby(), nil | atom, any) :: T.lobby() defdelegate update_lobby(lobby, data, reason), to: LobbyLib - @spec rename_lobby(T.lobby_id(), String.t(), T.userid()) :: :ok | nil + @spec rename_lobby(T.lobby_id(), String.t(), T.userid() | nil) :: :ok | nil defdelegate rename_lobby(lobby_id, new_base_name, renamer_id), to: LobbyLib # Requests diff --git a/lib/teiserver/coordinator/consul_commands.ex b/lib/teiserver/coordinator/consul_commands.ex index 228751a7d..53e07dc77 100644 --- a/lib/teiserver/coordinator/consul_commands.ex +++ b/lib/teiserver/coordinator/consul_commands.ex @@ -4,7 +4,7 @@ defmodule Teiserver.Coordinator.ConsulCommands do alias Teiserver.Config alias Teiserver.Coordinator.{ConsulServer, RikerssMemes} alias Teiserver.{Account, Battle, Lobby, Coordinator, CacheUser, Client, Telemetry} - alias Teiserver.Lobby.{ChatLib, LobbyLib} + alias Teiserver.Lobby.{ChatLib, LobbyLib, LobbyRestrictions} alias Teiserver.Chat.WordLib alias Teiserver.Data.Types, as: T import Teiserver.Helper.NumberHelper, only: [int_parse: 1, round: 2] @@ -102,41 +102,8 @@ defmodule Teiserver.Coordinator.ConsulCommands do ["Parties:" | party_list] end - min_rate_play = state.minimum_rating_to_play - max_rate_play = state.maximum_rating_to_play - - play_level_bounds = - cond do - min_rate_play > 0 and max_rate_play < 1000 -> - "Play rating boundaries set to min: #{min_rate_play}, max: #{max_rate_play}" - - min_rate_play > 0 -> - "Play rating boundaries set to min: #{min_rate_play}" - - max_rate_play < 1000 -> - "Play rating boundaries set to max: #{max_rate_play}" - - true -> - nil - end - - min_rank_play = state.minimum_rank_to_play - max_rank_play = state.maximum_rank_to_play - - play_rank_bounds = - cond do - min_rank_play > 0 and max_rank_play < 1000 -> - "Play rank boundaries set to min: #{min_rank_play}, max: #{max_rank_play}" - - min_rank_play > 0 -> - "Play rank boundaries set to min: #{min_rank_play}" - - max_rank_play < 1000 -> - "Play rank boundaries set to max: #{max_rank_play}" - - true -> - nil - end + play_level_bounds = LobbyRestrictions.get_rating_bounds_for_title(state) + play_rank_bounds = LobbyRestrictions.get_rank_bounds_for_title(state) welcome_message = if state.welcome_message do @@ -161,7 +128,9 @@ defmodule Teiserver.Coordinator.ConsulCommands do status_msg = [ - "#{@splitter} Lobby status #{@splitter}", + @splitter, + "Lobby status", + @splitter, "Status for battle ##{state.lobby_id}", "Locks: #{locks}", "Gatekeeper: #{state.gatekeeper}", @@ -754,7 +723,122 @@ defmodule Teiserver.Coordinator.ConsulCommands do def handle_command(%{command: "resetratinglevels", remaining: ""} = cmd, state) do ConsulServer.say_command(cmd, state) LobbyLib.cast_lobby(state.lobby_id, :refresh_name) - %{state | minimum_rating_to_play: 0, maximum_rating_to_play: 1000} + + %{ + state + | minimum_rating_to_play: 0, + maximum_rating_to_play: LobbyRestrictions.rating_upper_bound() + } + end + + def handle_command(%{command: "resetchevlevels", remaining: ""} = cmd, state) do + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + %{state | minimum_rank_to_play: 0, maximum_rank_to_play: LobbyRestrictions.rank_upper_bound()} + end + + # Reset min chev level for a lobby by using empty argument + def handle_command(%{command: "minchevlevel", remaining: ""} = cmd, state) do + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + %{state | minimum_rank_to_play: 0} + end + + # Set min chev level for a lobby + def handle_command( + %{command: "minchevlevel", remaining: remaining, senderid: senderid} = cmd, + state + ) do + result = LobbyRestrictions.allowed_to_set_restrictions(state) + + case result do + :ok -> + # Allowed to set restrictions + case Integer.parse(remaining |> String.trim()) do + :error -> + Lobby.sayprivateex( + state.coordinator_id, + senderid, + [ + "Unable to turn '#{remaining}' into an integer" + ], + state.lobby_id + ) + + state + + {chev_level, _} -> + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + level = chev_level - 1 + + Map.merge(state, %{ + minimum_rank_to_play: level, + maximum_rank_to_play: LobbyRestrictions.rank_upper_bound() + }) + end + + # Not Allowed to set restrictions + {:error, error_msg} -> + Lobby.sayex( + state.coordinator_id, + error_msg, + state.lobby_id + ) + + state + end + end + + # Reset max chev level for a lobby by using empty argument + def handle_command(%{command: "maxchevlevel", remaining: ""} = cmd, state) do + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + %{state | maximum_rank_to_play: LobbyRestrictions.rank_upper_bound()} + end + + # Setting max chev level we reset min chev level + def handle_command( + %{command: "maxchevlevel", remaining: remaining, senderid: senderid} = cmd, + state + ) do + result = LobbyRestrictions.allowed_to_set_restrictions(state) + + case result do + :ok -> + case Integer.parse(remaining |> String.trim()) do + :error -> + Lobby.sayprivateex( + state.coordinator_id, + senderid, + [ + "Unable to turn '#{remaining}' into an integer" + ], + state.lobby_id + ) + + state + + {chev_level, _} -> + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + level = chev_level - 1 + + Map.merge(state, %{ + maximum_rank_to_play: level, + minimum_rank_to_play: 0 + }) + end + + {:error, error_msg} -> + Lobby.sayex( + state.coordinator_id, + error_msg, + state.lobby_id + ) + + error_msg + end end def handle_command(%{command: "minratinglevel", remaining: ""} = cmd, state) do @@ -767,81 +851,89 @@ defmodule Teiserver.Coordinator.ConsulCommands do %{command: "minratinglevel", remaining: remaining, senderid: senderid} = cmd, state ) do - if allowed_to_set_rating_limit?(state) do - case Integer.parse(remaining |> String.trim()) do - :error -> - Lobby.sayprivateex( - state.coordinator_id, - senderid, - [ - "Unable to turn '#{remaining}' into an integer" - ], - state.lobby_id - ) - - state + result = LobbyRestrictions.allowed_to_set_restrictions(state) - {level, _} -> - ConsulServer.say_command(cmd, state) - LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + case result do + :ok -> + case Integer.parse(remaining |> String.trim()) do + :error -> + Lobby.sayprivateex( + state.coordinator_id, + senderid, + [ + "Unable to turn '#{remaining}' into an integer" + ], + state.lobby_id + ) - %{ state - | minimum_rating_to_play: level |> max(0) |> min(state.maximum_rating_to_play - 1) - } - end - else - Lobby.sayex( - state.coordinator_id, - "You cannot set a rating limit if all are welcome to the game", - state.lobby_id - ) - state + {level, _} -> + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + + Map.merge(state, %{ + minimum_rating_to_play: level |> max(0) |> min(state.maximum_rating_to_play - 1) + }) + end + + {:error, error_msg} -> + Lobby.sayex( + state.coordinator_id, + error_msg, + state.lobby_id + ) + + state end end def handle_command(%{command: "maxratinglevel", remaining: ""} = cmd, state) do ConsulServer.say_command(cmd, state) LobbyLib.cast_lobby(state.lobby_id, :refresh_name) - %{state | maximum_rating_to_play: 1000} + %{state | maximum_rating_to_play: LobbyRestrictions.rating_upper_bound()} end def handle_command( %{command: "maxratinglevel", remaining: remaining, senderid: senderid} = cmd, state ) do - if allowed_to_set_rating_limit?(state) do - case Integer.parse(remaining |> String.trim()) do - :error -> - Lobby.sayprivateex( - state.coordinator_id, - senderid, - [ - "Unable to turn '#{remaining}' into an integer" - ], - state.lobby_id - ) - - state + result = LobbyRestrictions.allowed_to_set_restrictions(state) - {level, _} -> - ConsulServer.say_command(cmd, state) - LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + case result do + :ok -> + case Integer.parse(remaining |> String.trim()) do + :error -> + Lobby.sayprivateex( + state.coordinator_id, + senderid, + [ + "Unable to turn '#{remaining}' into an integer" + ], + state.lobby_id + ) - %{ state - | maximum_rating_to_play: level |> min(1000) |> max(state.minimum_rating_to_play + 1) - } - end - else - Lobby.sayex( - state.coordinator_id, - "You cannot set a rating limit if all are welcome to the game", - state.lobby_id - ) - state + {level, _} -> + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + + %{ + state + | maximum_rating_to_play: + level |> min(1000) |> max(state.minimum_rating_to_play + 1) + } + end + + {:error, error_msg} -> + Lobby.sayex( + state.coordinator_id, + error_msg, + state.lobby_id + ) + + state end end @@ -877,26 +969,30 @@ defmodule Teiserver.Coordinator.ConsulCommands do state {{min_level_o, _}, {max_level_o, _}} -> - if allowed_to_set_rating_limit?(state) do - min_level = min(min_level_o, max_level_o) - max_level = max(min_level_o, max_level_o) + result = LobbyRestrictions.allowed_to_set_restrictions(state) - ConsulServer.say_command(cmd, state) - LobbyLib.cast_lobby(state.lobby_id, :refresh_name) + case result do + :ok -> + min_level = min(min_level_o, max_level_o) + max_level = max(min_level_o, max_level_o) - %{ - state - | minimum_rating_to_play: max(min_level, 0), - maximum_rating_to_play: min(max_level, 1000) - } - else - Lobby.sayex( - state.coordinator_id, - "You cannot set a rating limit if all are welcome to the game", - state.lobby_id - ) + ConsulServer.say_command(cmd, state) + LobbyLib.cast_lobby(state.lobby_id, :refresh_name) - state + %{ + state + | minimum_rating_to_play: max(min_level, 0), + maximum_rating_to_play: min(max_level, 1000) + } + + {:error, error_msg} -> + Lobby.sayex( + state.coordinator_id, + error_msg, + state.lobby_id + ) + + state end end @@ -976,7 +1072,14 @@ defmodule Teiserver.Coordinator.ConsulCommands do {level, _} -> ConsulServer.say_command(cmd, state) LobbyLib.cast_lobby(state.lobby_id, :refresh_name) - %{state | maximum_rank_to_play: level |> min(1000) |> max(state.minimum_rank_to_play + 1)} + + %{ + state + | maximum_rank_to_play: + level + |> min(LobbyRestrictions.rating_upper_bound()) + |> max(state.minimum_rank_to_play + 1) + } end end @@ -1021,7 +1124,7 @@ defmodule Teiserver.Coordinator.ConsulCommands do %{ state | minimum_rank_to_play: max(min_level, 0), - maximum_rank_to_play: min(max_level, 1000) + maximum_rank_to_play: min(max_level, LobbyRestrictions.rank_upper_bound()) } end @@ -1418,12 +1521,7 @@ defmodule Teiserver.Coordinator.ConsulCommands do lobby = Lobby.get_lobby(state.lobby_id) - not_all_welcome = - cond do - state.maximum_rating_to_play < 1000 -> true - state.minimum_rating_to_play > 0 -> true - true -> false - end + {check_name_result, check_name_msg} = LobbyRestrictions.check_lobby_name(stripped_name, state) starts_with_lobby_policy = new_name @@ -1470,10 +1568,10 @@ defmodule Teiserver.Coordinator.ConsulCommands do state - not_all_welcome && allwelcome_name?(stripped_name) -> + check_name_result != :ok -> Lobby.sayex( state.coordinator_id, - "You cannot declare a lobby to be welcome to all if there is a rating limit", + check_name_msg, state.lobby_id ) @@ -1498,21 +1596,9 @@ defmodule Teiserver.Coordinator.ConsulCommands do ConsulServer.say_command(cmd, state) - downcase_name = new_name |> String.downcase() - - skill_name = - [ - String.contains?(downcase_name, "noob"), - String.contains?(downcase_name, "newbie") - ] - |> Enum.any?() - - if skill_name do - Lobby.sayex( - state.coordinator_id, - "Don't forget you can set skill requirements using the setratinglevels command, use $help setratinglevels for more information.", - state.lobby_id - ) + if check_name_msg != nil do + # Send coordinator message which can be long; appears on right + CacheUser.send_direct_message(state.coordinator_id, senderid, check_name_msg) end state @@ -1522,6 +1608,7 @@ defmodule Teiserver.Coordinator.ConsulCommands do true -> Battle.rename_lobby(state.lobby_id, new_name, nil) + state end end @@ -2007,30 +2094,6 @@ defmodule Teiserver.Coordinator.ConsulCommands do ) end - defp allowed_to_set_rating_limit?(state) do - name = - state.lobby_id - |> Battle.get_lobby() - |> Map.get(:name) - - cond do - allwelcome_name?(name) -> false - true -> true - end - end - - defp allwelcome_name?(name) do - name = - name - |> String.downcase() - |> String.replace(" ", "") - - cond do - String.contains?(name, "allwelcome") -> true - true -> false - end - end - @spec get_lock(String.t()) :: atom | nil defp get_lock(name) do case name |> String.downcase() |> String.trim() do diff --git a/lib/teiserver/coordinator/consul_server.ex b/lib/teiserver/coordinator/consul_server.ex index e731e665c..15850e1f9 100644 --- a/lib/teiserver/coordinator/consul_server.ex +++ b/lib/teiserver/coordinator/consul_server.ex @@ -18,18 +18,15 @@ defmodule Teiserver.Coordinator.ConsulServer do Communication } - alias Teiserver.Lobby.{ChatLib} + alias Teiserver.Lobby.{ChatLib, LobbyRestrictions} import Teiserver.Helper.NumberHelper, only: [int_parse: 1] alias Phoenix.PubSub - alias Teiserver.Battle.{BalanceLib, MatchLib} + alias Teiserver.Battle.BalanceLib alias Teiserver.Data.Types, as: T - alias Teiserver.Coordinator.{ConsulCommands, CoordinatorLib, SpadsParser} - - # Commands that are always forwarded to the coordinator itself, not the consul server - @coordinator_bot ~w(whoami whois check discord help coc ignore mute ignore unmute unignore matchmaking website party modparty unparty) + alias Teiserver.Coordinator.{ConsulCommands, CoordinatorLib, SpadsParser, CoordinatorCommands} @always_allow ~w(status s y n follow joinq leaveq splitlobby afks roll players password? newlobby jazlobby tournament) - @boss_commands ~w(balancemode gatekeeper welcome-message meme reset-approval rename resetratinglevels minratinglevel maxratinglevel setratinglevels) + @boss_commands ~w(balancemode gatekeeper welcome-message meme reset-approval rename minchevlevel maxchevlevel resetchevlevels resetratinglevels minratinglevel maxratinglevel setratinglevels) @vip_boss_commands ~w(shuffle) @host_commands ~w(specunready makeready settag speclock forceplay lobbyban lobbybanmult unban forcespec forceplay lock unlock makebalance) @@ -351,7 +348,7 @@ defmodule Teiserver.Coordinator.ConsulServer do def handle_info(%{command: command} = cmd, state) do cond do - Enum.member?(@coordinator_bot, command) -> + CoordinatorCommands.is_coordinator_command?(command) -> Coordinator.cast_coordinator( {:consul_command, Map.merge(cmd, %{lobby_id: state.lobby_id, host_id: state.host_id})} ) @@ -376,23 +373,33 @@ defmodule Teiserver.Coordinator.ConsulServer do end @doc """ - This method handles state when all players have left the lobby + Update lobby state when everyone leaves """ def handle_info( %{channel: "teiserver_lobby_updates", event: :remove_user, client: _client}, state ) do - new_player_count = get_player_count(state) + # Get count of people in lobby - both players and specs + new_member_count = get_member_count(state) - # Everyone left the lobby - # Restore some settings to default - if new_player_count == 0 do - new_state = %{ - state - | minimum_rating_to_play: 0, - maximum_rating_to_play: 1000, - balance_algorithm: @default_balance_algorithm - } + if new_member_count == 0 do + # Remove filters from the lobby + # reset stuff to default + new_state = + Map.merge(state, %{ + minimum_rating_to_play: 0, + maximum_rating_to_play: LobbyRestrictions.rating_upper_bound(), + minimum_rank_to_play: 0, + maximum_rank_to_play: LobbyRestrictions.rank_upper_bound(), + balance_algorithm: @default_balance_algorithm, + welcome_message: nil + }) + + # Remove filters from lobby name + lobby = Lobby.get_lobby(state.lobby_id) + old_name = lobby.name + new_name = String.split(old_name, "|", trim: true) |> Enum.at(0) + Battle.rename_lobby(state.lobby_id, new_name, nil) {:noreply, new_state} else @@ -401,41 +408,7 @@ defmodule Teiserver.Coordinator.ConsulServer do end def handle_info(%{channel: "teiserver_lobby_updates", event: :add_user, client: client}, state) do - min_rate_play = state.minimum_rating_to_play - max_rate_play = state.maximum_rating_to_play - - play_level_bounds = - cond do - min_rate_play > 0 and max_rate_play < 1000 -> - "Play rating boundaries set to min: #{min_rate_play}, max: #{max_rate_play}" - - min_rate_play > 0 -> - "Play rating boundaries set to min: #{min_rate_play}" - - max_rate_play < 1000 -> - "Play rating boundaries set to max: #{max_rate_play}" - - true -> - nil - end - - min_rank_play = state.minimum_rank_to_play - max_rank_play = state.maximum_rank_to_play - - play_rank_bounds = - cond do - min_rank_play > 0 and max_rank_play < 1000 -> - "Play rank boundaries set to min: #{min_rank_play}, max: #{max_rank_play}" - - min_rank_play > 0 -> - "Play rank boundaries set to min: #{min_rank_play}" - - max_rank_play < 1000 -> - "Play rank boundaries set to max: #{max_rank_play}" - - true -> - nil - end + restrictions = LobbyRestrictions.get_lobby_restrictions_welcome_text(state) welcome_message = if state.welcome_message do @@ -445,8 +418,7 @@ defmodule Teiserver.Coordinator.ConsulServer do msg = [ welcome_message, - play_level_bounds, - play_rank_bounds + restrictions ] |> List.flatten() |> Enum.filter(fn s -> s != nil end) @@ -835,12 +807,8 @@ defmodule Teiserver.Coordinator.ConsulServer do @spec user_allowed_to_play?(T.user(), T.client(), map()) :: boolean() defp user_allowed_to_play?(user, client, state) do player_list = list_players(state) - rating_type = MatchLib.game_type(state.host_teamsize, state.host_teamcount) + userid = user.id - {player_rating, player_uncertainty} = - BalanceLib.get_user_rating_value_uncertainty_pair(user.id, rating_type) - - player_rating = max(player_rating, 1) avoid_status = Account.check_avoid_status(user.id, player_list) boss_avoid_status = @@ -850,25 +818,21 @@ defmodule Teiserver.Coordinator.ConsulServer do end) |> Enum.any?() - cond do - state.minimum_rating_to_play != nil and player_rating < state.minimum_rating_to_play -> - false - - state.maximum_rating_to_play != nil and player_rating > state.maximum_rating_to_play -> - false - - state.minimum_uncertainty_to_play != nil and - player_uncertainty < state.minimum_uncertainty_to_play -> - false + rating_check_result = LobbyRestrictions.check_rating_to_play(userid, state) - state.maximum_uncertainty_to_play != nil and - player_uncertainty > state.maximum_uncertainty_to_play -> - false + rank_check_result = LobbyRestrictions.check_rank_to_play(user, state) - state.minimum_rank_to_play != nil and user.rank < state.minimum_rank_to_play -> + cond do + rating_check_result != :ok -> + # Send message + {_, msg} = rating_check_result + CacheUser.send_direct_message(get_coordinator_userid(), userid, msg) false - state.maximum_rank_to_play != nil and user.rank > state.maximum_rank_to_play -> + rank_check_result != :ok -> + # Send message + {_, msg} = rank_check_result + CacheUser.send_direct_message(get_coordinator_userid(), userid, msg) false not Enum.empty?(client.queues) -> @@ -1273,6 +1237,20 @@ defmodule Teiserver.Coordinator.ConsulServer do @spec list_players(map()) :: [T.client()] def list_players(%{lobby_id: lobby_id}) do + list_members(%{lobby_id: lobby_id}) + |> Enum.filter(fn client -> client.player end) + end + + @spec get_player_count(map()) :: non_neg_integer + def get_player_count(%{lobby_id: lobby_id}) do + list_players(%{lobby_id: lobby_id}) + |> Enum.count() + end + + @doc """ + Lists members which includes players and non players but excludes the SPADS bot. + """ + def list_members(%{lobby_id: lobby_id}) do member_list = Battle.get_lobby_member_list(lobby_id) case member_list do @@ -1283,13 +1261,13 @@ defmodule Teiserver.Coordinator.ConsulServer do member_list |> Enum.map(fn userid -> Client.get_client_by_id(userid) end) |> Enum.filter(fn client -> client != nil end) - |> Enum.filter(fn client -> client.player == true and client.lobby_id == lobby_id end) + |> Enum.filter(fn client -> client.lobby_id == lobby_id end) end end - @spec get_player_count(map()) :: non_neg_integer - def get_player_count(%{lobby_id: lobby_id}) do - list_players(%{lobby_id: lobby_id}) + # Get count of members which includes players and non players but excludes the SPADS bot. + defp get_member_count(%{lobby_id: lobby_id}) do + list_members(%{lobby_id: lobby_id}) |> Enum.count() end @@ -1358,6 +1336,10 @@ defmodule Teiserver.Coordinator.ConsulServer do |> String.trim() end + defp get_coordinator_userid do + Coordinator.get_coordinator_userid() + end + @spec empty_state(T.lobby_id()) :: map() def empty_state(lobby_id) do # it's possible the lobby is nil before we even get to start this up (tests in particular) diff --git a/lib/teiserver/coordinator/coordinator_commands.ex b/lib/teiserver/coordinator/coordinator_commands.ex index 8be306b6a..6f7bea777 100644 --- a/lib/teiserver/coordinator/coordinator_commands.ex +++ b/lib/teiserver/coordinator/coordinator_commands.ex @@ -8,8 +8,16 @@ defmodule Teiserver.Coordinator.CoordinatorCommands do @splitter "---------------------------" @always_allow ~w(help whoami whois discord coc ignore mute ignore unmute unignore matchmaking website party) + # These commands are handled by coordinator commands, but are not on the always allow list + @mod_allow ~w(check modparty unparty) @forward_to_consul ~w(s status players follow joinq leaveq splitlobby y yes n no explain) + def is_coordinator_command?(command) do + # The list of allowed commands are now defined in this file + # They used to be defined in consul_server.ex under @coordinator_bot variable + Enum.member?(@always_allow, command) || Enum.member?(@mod_allow, command) + end + @spec allow_command?(Map.t(), Map.t()) :: boolean() defp allow_command?(%{senderid: senderid} = cmd, state) do client = Client.get_client_by_id(senderid) diff --git a/lib/teiserver/coordinator/coordinator_lib.ex b/lib/teiserver/coordinator/coordinator_lib.ex index ca6c8deca..82782b081 100644 --- a/lib/teiserver/coordinator/coordinator_lib.ex +++ b/lib/teiserver/coordinator/coordinator_lib.ex @@ -63,9 +63,6 @@ or are following someone that voted yes are also moved to that lobby.", :everybo :everybody}, {"rename", ["new name"], "Renames the lobby to the name given. Requires boss privileges.", :everybody}, - {"resetratinglevels", [], - "Resets the rating level limits to not exist. Player limiting commands are designed to be used with $rename, please be careful not to abuse them. Requires boss privileges.", - :everybody}, {"minratinglevel", ["min-level"], "Sets the minimum level for players, you must be at least this rating to be a player. Requires boss privileges.", :everybody}, @@ -75,7 +72,17 @@ or are following someone that voted yes are also moved to that lobby.", :everybo {"setratinglevels", ["min-level", "max-level"], "Sets the minimum and maximum rating levels for players. Requires boss privileges.", :everybody}, - + {"resetratinglevels", [], + "Resets the rating level limits to not exist. Requires boss privileges.", :everybody}, + {"minchevlevel", ["min-level"], + "Sets the minimum chevron level for players. Requires boss privileges. Leave number blank to reset it.", + :everybody}, + {"maxchevlevel", ["max-level"], + "Sets the maximum chevron level for players. Requires boss privileges. Leave number blank to reset it.", + :everybody}, + {"resetchevlevels", [], + "Resets the chevron level restrictions to not exist. Requires boss privileges.", + :everybody}, # {"resetranklevels", [], "Resets the rank level limits to not exist. Player limiting commands are designed to be used with $rename, please be careful not to abuse them. Requires boss privileges.", :everybody}, # {"minranklevel", ["min-level"], "Sets the minimum rank level for players, you must be at least this rank to be a player. Requires boss privileges.", :everybody}, # {"maxranklevel", ["max-level"], "Sets the maximum rank level for players, you must be at below this rank to be a player. Requires boss privileges.", :everybody}, diff --git a/lib/teiserver/data/cache_user.ex b/lib/teiserver/data/cache_user.ex index c3bc15a5b..0ffcef2b5 100644 --- a/lib/teiserver/data/cache_user.ex +++ b/lib/teiserver/data/cache_user.ex @@ -566,6 +566,7 @@ defmodule Teiserver.CacheUser do def send_direct_message(from_id, to_id, "!joinas" <> s), do: send_direct_message(from_id, to_id, "!cv joinas" <> s) + @spec send_direct_message(T.userid(), T.userid(), list) :: :ok def send_direct_message(sender_id, to_id, message_parts) when is_list(message_parts) do sender = get_user_by_id(sender_id) msg_str = Enum.join(message_parts, "\n") @@ -1257,6 +1258,12 @@ defmodule Teiserver.CacheUser do def is_moderator?(%{roles: roles}), do: Enum.member?(roles, "Moderator") def is_moderator?(_), do: false + @spec is_contributor?(T.userid() | T.user()) :: boolean() + def is_contributor?(nil), do: true + def is_contributor?(userid) when is_integer(userid), do: is_contributor?(get_user_by_id(userid)) + def is_contributor?(%{roles: roles}), do: Enum.member?(roles, "Contributor") + def is_contributor?(_), do: false + @spec is_verified?(T.userid() | T.user()) :: boolean() def is_verified?(nil), do: true def is_verified?(userid) when is_integer(userid), do: is_verified?(get_user_by_id(userid)) diff --git a/lib/teiserver/lobby/libs/lobby_restrictions.ex b/lib/teiserver/lobby/libs/lobby_restrictions.ex new file mode 100644 index 000000000..73098e387 --- /dev/null +++ b/lib/teiserver/lobby/libs/lobby_restrictions.ex @@ -0,0 +1,281 @@ +defmodule Teiserver.Lobby.LobbyRestrictions do + @moduledoc """ + Helper methods for lobby policies + """ + alias Teiserver.{CacheUser, Config} + require Logger + alias Teiserver.Battle.{BalanceLib, MatchLib} + alias Teiserver.Battle + + @rank_upper_bound 1000 + @rating_upper_bound 1000 + @splitter "---------------------------" + @spec rank_upper_bound() :: number + def rank_upper_bound, do: @rank_upper_bound + def rating_upper_bound, do: @rating_upper_bound + + def get_lobby_restrictions_welcome_text(state) do + play_level_bounds = get_rating_bounds_for_title(state) + play_rank_bounds = get_rank_bounds_for_title(state) + + cond do + play_level_bounds == nil && play_rank_bounds == nil -> + [] + + true -> + ["This lobby has the following play restrictions:", play_level_bounds, play_rank_bounds] + |> Enum.filter(fn x -> x != nil end) + end + end + + def get_rank_bounds_for_title(consul_state) when consul_state == nil do + nil + end + + def get_rank_bounds_for_title(consul_state) do + max_rank_to_play = Map.get(consul_state, :maximum_rank_to_play, @rank_upper_bound) + min_rank_to_play = Map.get(consul_state, :minimum_rank_to_play, 0) + + # Chevlevel stuff here + cond do + # Default chev levels + max_rank_to_play >= @rank_upper_bound && + min_rank_to_play <= 0 -> + nil + + # Just a max rating + max_rank_to_play < @rank_upper_bound && + min_rank_to_play <= 0 -> + "Max chev: #{max_rank_to_play + 1}" + + # Just a min rating + max_rank_to_play >= @rank_upper_bound && + min_rank_to_play > 0 -> + "Min chev: #{min_rank_to_play + 1}" + + # Chev range + # It shouldn't go here + max_rank_to_play < @rank_upper_bound || + min_rank_to_play > 0 -> + "Chev between: #{min_rank_to_play} - #{max_rank_to_play}" + + true -> + nil + end + end + + def get_rating_bounds_for_title(consul_state) when consul_state == nil do + nil + end + + def get_rating_bounds_for_title(consul_state) do + max_rating_to_play = Map.get(consul_state, :maximum_rating_to_play, @rating_upper_bound) + min_rating_to_play = Map.get(consul_state, :minimum_rating_to_play, 0) + + cond do + # Default ratings + max_rating_to_play >= @rating_upper_bound && + min_rating_to_play <= 0 -> + nil + + # Just a max rating + max_rating_to_play < @rating_upper_bound && + min_rating_to_play <= 0 -> + "Max rating: #{max_rating_to_play}" + + # Just a min rating + max_rating_to_play >= @rating_upper_bound && + min_rating_to_play > 0 -> + "Min rating: #{min_rating_to_play}" + + # Rating range + max_rating_to_play < @rating_upper_bound || + min_rating_to_play > 0 -> + "Rating between: #{min_rating_to_play} - #{max_rating_to_play}" + + true -> + nil + end + end + + def get_failed_rank_check_text(player_rank, consul_state) do + bounds = get_rank_bounds_for_title(consul_state) + + [ + @splitter, + "You don't meet the chevron requirements for this lobby (#{bounds}). Your chevron level is #{player_rank + 1}. Learn more about chevrons here:", + "https://www.beyondallreason.info/guide/rating-and-lobby-balance#rank-icons" + ] + end + + def get_failed_rating_check_text(player_rating, consul_state, rating_type) do + bounds = get_rating_bounds_for_title(consul_state) + player_rating_text = player_rating |> Decimal.from_float() |> Decimal.round(2) + + [ + @splitter, + "You don't meet the rating requirements for this lobby (#{bounds}). Your #{rating_type} match rating is #{player_rating_text}. Learn more about rating here:", + "https://www.beyondallreason.info/guide/rating-and-lobby-balance#openskill" + ] + end + + defp allow_bypass_rank_check?(user) do + method = Config.get_site_config_cache("profile.Rank method") + # When using Role method for ranks, + # contributors auto pass since their ranks are not defined on playtime. To be fixed seperately. + method == "Role" && CacheUser.is_contributor?(user) + end + + @spec check_rank_to_play(any(), any()) :: :ok | {:error, iodata()} + def check_rank_to_play(user, consul_state) do + state = consul_state + + if allow_bypass_rank_check?(user) do + :ok + else + cond do + state.minimum_rank_to_play != nil and user.rank < state.minimum_rank_to_play -> + # Send message + msg = get_failed_rank_check_text(user.rank, state) + {:error, msg} + + state.maximum_rank_to_play != nil and user.rank > state.maximum_rank_to_play -> + # Send message + msg = get_failed_rank_check_text(user.rank, state) + {:error, msg} + + true -> + :ok + end + end + end + + @doc """ + Determining the rating type is slightly different for lobby restrictions compared to rating a match. + When rating a match we want to use the number of players and team count in the match. + But with the lobby, we want to use the target team size/count defined by the dropdowns in the lobby. + So if there are two players in the lobby, but the team size dropdown is 8, we want to use the "Team" rating. + """ + @spec check_rating_to_play(any(), any()) :: :ok | {:error, iodata()} + def check_rating_to_play(user_id, consul_state) do + state = consul_state + team_size = state.host_teamsize + team_count = state.host_teamcount + + rating_type = MatchLib.game_type(team_size, team_count) + + {player_rating, _player_uncertainty} = + BalanceLib.get_user_rating_value_uncertainty_pair(user_id, rating_type) + + cond do + state.minimum_rating_to_play != nil and player_rating < state.minimum_rating_to_play -> + msg = get_failed_rating_check_text(player_rating, state, rating_type) + {:error, msg} + + state.maximum_rating_to_play != nil and player_rating > state.maximum_rating_to_play -> + msg = get_failed_rating_check_text(player_rating, state, rating_type) + {:error, msg} + + true -> + # All good + :ok + end + end + + @doc """ + You cannot have all welcome lobby name if there are restrictions + """ + @spec check_lobby_name(String.t(), any()) :: + {:error, String.t()} | {:ok, String.t()} | {:ok, nil} + def check_lobby_name(name, consul_state) do + cond do + has_restrictions?(consul_state) and allwelcome_name?(name) -> + {:error, + "* You cannot declare a lobby to be all welcome if there are player restrictions"} + + is_noob_title?(name) -> + {:ok, get_noob_looby_tips()} + + true -> + {:ok, nil} + end + end + + defp get_noob_looby_tips() do + [ + @splitter, + "Noob lobby tips", + @splitter, + "To restrict this lobby to players who are new, use command:", + "$maxchevlevel ", + "To ensure new players are distributed evenly across teams, use command:", + "$balancemode split_one_chevs" + ] + end + + # Check if lobby has restrictions for playing + defp has_restrictions?(consul_state) do + state = consul_state + + cond do + state.maximum_rating_to_play < @rating_upper_bound -> true + state.minimum_rating_to_play > 0 -> true + state.minimum_rank_to_play > 0 -> true + state.maximum_rank_to_play < @rank_upper_bound -> true + true -> false + end + end + + # Teifion added code to prevent setting restrictions to All Welcome lobbies + @spec allowed_to_set_restrictions(map()) :: :ok | {:error, String.t()} + def allowed_to_set_restrictions(state) do + name = + state.lobby_id + |> Battle.get_lobby() + |> Map.get(:name) + + cond do + allwelcome_name?(name) -> + {:error, "You cannot set a rating limit if all are welcome to the game"} + + true -> + :ok + end + end + + defp allwelcome_name?(name) do + name = + name + |> String.downcase() + |> String.replace(" ", "") + + cond do + String.contains?(name, "allwelcome") -> true + true -> false + end + end + + @doc """ + Checks if the lobby title indicates a noob lobby + """ + @spec is_noob_title?(String.t()) :: boolean() + def is_noob_title?(title) do + title = + title + |> String.downcase() + + anti_noob_regex = ~r/no (noob|newb|nub)/ + noob_regex = ~r/\b(noob|newb|nub(s|\b))/ + + noob_matches = + Regex.scan(noob_regex, title) + |> Enum.count() + + anti_noob_matches = + Regex.scan(anti_noob_regex, title) + |> Enum.count() + + # Returns true if both critera met + noob_matches > 0 && anti_noob_matches == 0 + end +end diff --git a/lib/teiserver/lobby/servers/lobby_server.ex b/lib/teiserver/lobby/servers/lobby_server.ex index 06b7c4979..4c3c71a11 100644 --- a/lib/teiserver/lobby/servers/lobby_server.ex +++ b/lib/teiserver/lobby/servers/lobby_server.ex @@ -3,7 +3,7 @@ defmodule Teiserver.Battle.LobbyServer do use GenServer require Logger alias Teiserver.{Account, Battle, Config, Telemetry, Coordinator, Communication} - alias Teiserver.Lobby.CommandLib + alias Teiserver.Lobby.{CommandLib, LobbyRestrictions} alias Phoenix.PubSub @player_list_cache_age_max 200 @@ -544,35 +544,9 @@ defmodule Teiserver.Battle.LobbyServer do parts = [ "", - + LobbyRestrictions.get_rank_bounds_for_title(consul_state), # Rating stuff here - cond do - consul_state == nil -> - nil - - # Default ratings - consul_state.maximum_rating_to_play >= 1000 && - consul_state.minimum_rating_to_play <= 0 -> - nil - - # Just a max rating - consul_state.maximum_rating_to_play < 1000 && - consul_state.minimum_rating_to_play <= 0 -> - "Max rating: #{consul_state.maximum_rating_to_play}" - - # Just a min rating - consul_state.maximum_rating_to_play >= 1000 && - consul_state.minimum_rating_to_play > 0 -> - "Min rating: #{consul_state.minimum_rating_to_play}" - - # Rating range - consul_state.maximum_rating_to_play < 1000 || - consul_state.minimum_rating_to_play > 0 -> - "Rating between: #{consul_state.minimum_rating_to_play} - #{consul_state.maximum_rating_to_play}" - - true -> - nil - end + LobbyRestrictions.get_rating_bounds_for_title(consul_state) ] |> Enum.reject(&(&1 == nil)) |> Enum.join(" | ") diff --git a/test/teiserver/lobby/libs/lobby_restrictions_test.exs b/test/teiserver/lobby/libs/lobby_restrictions_test.exs new file mode 100644 index 000000000..ec6c10f85 --- /dev/null +++ b/test/teiserver/lobby/libs/lobby_restrictions_test.exs @@ -0,0 +1,53 @@ +defmodule Teiserver.Lobby.Libs.LobbyRestrictionsTest do + @moduledoc false + use ExUnit.Case + alias Teiserver.Lobby.LobbyRestrictions + + test "check for noob title" do + assert LobbyRestrictions.is_noob_title?("Noobs 1v1") + + refute LobbyRestrictions.is_noob_title?("No Noobs 1v1") + + refute LobbyRestrictions.is_noob_title?("All Welcome 1v1") + + assert LobbyRestrictions.is_noob_title?("Newbies 1v1") + + assert LobbyRestrictions.is_noob_title?("Nubs 1v1") + end + + test "get title based on consul state rank filters" do + result = LobbyRestrictions.get_rank_bounds_for_title(nil) + assert result == nil + + result = LobbyRestrictions.get_rank_bounds_for_title(%{}) + assert result == nil + + result = LobbyRestrictions.get_rank_bounds_for_title(%{maximum_rank_to_play: 4}) + assert result == "Max chev: 5" + + result = LobbyRestrictions.get_rank_bounds_for_title(%{minimum_rank_to_play: 4}) + assert result == "Min chev: 5" + end + + test "get title based on consul state rating filters" do + result = LobbyRestrictions.get_rating_bounds_for_title(nil) + assert result == nil + + result = LobbyRestrictions.get_rating_bounds_for_title(%{}) + assert result == nil + + result = LobbyRestrictions.get_rating_bounds_for_title(%{maximum_rating_to_play: 4}) + assert result == "Max rating: 4" + + result = LobbyRestrictions.get_rating_bounds_for_title(%{minimum_rating_to_play: 4}) + assert result == "Min rating: 4" + + result = + LobbyRestrictions.get_rating_bounds_for_title(%{ + minimum_rating_to_play: 4, + maximum_rating_to_play: 20 + }) + + assert result == "Rating between: 4 - 20" + end +end