From 52989e292663e6b338096d9cfc71f198524683c7 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Wed, 17 Apr 2024 00:30:36 +1000 Subject: [PATCH 1/5] Add rankicon parameter to CLIENTSTATUS command --- config/dev.exs | 2 +- lib/teiserver/account.ex | 2 +- lib/teiserver/account/libs/role_lib.ex | 2 +- lib/teiserver/data/cache_user.ex | 65 ++++-- lib/teiserver/libs/teiserver_configs.ex | 2 +- lib/teiserver/protocols/spring/spring_out.ex | 10 +- lib/test_util/test_script.ex | 196 +++++++++++++++++++ 7 files changed, 262 insertions(+), 17 deletions(-) create mode 100644 lib/test_util/test_script.ex diff --git a/config/dev.exs b/config/dev.exs index d18f112fa..9cce6c979 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -88,7 +88,7 @@ config :teiserver, Oban, crontab: false # Do not include metadata nor timestamps in development logs -config :logger, :console, format: "[$level] $message\n" +config :logger, :console, format: "[$level] $message\n", level: :info config :logger, backends: [ diff --git a/lib/teiserver/account.ex b/lib/teiserver/account.ex index 9d4497c10..0ba017d7f 100644 --- a/lib/teiserver/account.ex +++ b/lib/teiserver/account.ex @@ -137,7 +137,7 @@ defmodule Teiserver.Account do |> Repo.one() end - @spec get_user_stat_data(integer()) :: Map.t() + @spec get_user_stat_data(integer()) :: map() def get_user_stat_data(userid) do Teiserver.cache_get_or_store(:teiserver_user_stat_cache, userid, fn -> case get_user_stat(userid) do diff --git a/lib/teiserver/account/libs/role_lib.ex b/lib/teiserver/account/libs/role_lib.ex index ff8938dab..1e8036a6d 100644 --- a/lib/teiserver/account/libs/role_lib.ex +++ b/lib/teiserver/account/libs/role_lib.ex @@ -263,7 +263,7 @@ defmodule Teiserver.Account.RoleLib do end def allowed_role_management("Moderator") do - global_roles() ++ moderation_roles() ++ property_roles() + global_roles() ++ moderation_roles() ++ property_roles() ++ community_roles() end def allowed_role_management(_) do diff --git a/lib/teiserver/data/cache_user.ex b/lib/teiserver/data/cache_user.ex index 3de66687f..eaee1c837 100644 --- a/lib/teiserver/data/cache_user.ex +++ b/lib/teiserver/data/cache_user.ex @@ -1264,14 +1264,18 @@ defmodule Teiserver.CacheUser do def is_verified?(%{roles: roles}), do: Enum.member?(roles, "Verified") def is_verified?(_), do: false - @spec rank_time(T.userid()) :: non_neg_integer() - def rank_time(userid) do + @spec rank_time(T.userid() | map()) :: non_neg_integer() + def rank_time(userid) when is_integer(userid) do stats = Account.get_user_stat(userid) || %{data: %{}} + rank_time(stats.data) + end + + def rank_time(stats_data) when is_map(stats_data) do ingame_minutes = - (stats.data["player_minutes"] || 0) + (stats.data["spectator_minutes"] || 0) * 0.5 + (stats_data["player_minutes"] || 0) + (stats_data["spectator_minutes"] || 0) * 0.5 - round(ingame_minutes / 60) + floor(ingame_minutes / 60) end # Based on actual ingame time @@ -1279,8 +1283,7 @@ defmodule Teiserver.CacheUser do def calculate_rank(userid, "Playtime") do ingame_hours = rank_time(userid) - [5, 15, 30, 100, 300, 1000, 3000] - |> Enum.count(fn r -> r <= ingame_hours end) + convert_hours_to_rank(ingame_hours) end # Using leaderboard rating @@ -1306,12 +1309,7 @@ defmodule Teiserver.CacheUser do cond do has_any_role?(userid, ~w(Core Contributor)) -> 6 - ingame_hours > 1000 -> 5 - ingame_hours > 250 -> 4 - ingame_hours > 100 -> 3 - ingame_hours > 15 -> 2 - ingame_hours > 5 -> 1 - true -> 0 + true -> convert_hours_to_rank(ingame_hours) end end @@ -1321,6 +1319,49 @@ defmodule Teiserver.CacheUser do calculate_rank(userid, method) end + @doc """ + This should match + https://www.beyondallreason.info/guide/rating-and-lobby-balance#rank-icons + However special ranks are ignored + """ + defp convert_hours_to_rank(hours) do + cond do + hours >= 1000 -> 5 + hours >= 250 -> 4 + hours >= 100 -> 3 + hours >= 15 -> 2 + hours >= 5 -> 1 + true -> 0 + end + end + + def get_rank_icon(userid) do + stats = Account.get_user_stat_data(userid) + # Play time Rank + rank = + cond do + # This is only for tournament winners + stats["rank_override"] != nil -> + stats["rank_override"] |> int_parse + + true -> + hours = rank_time(stats) + convert_hours_to_rank(hours) + end + + %{roles: roles} = Account.get_user_by_id(userid) + chev_level = rank + 1 + + cond do + Enum.member?(roles, "Moderator") -> "#{chev_level}Moderator" + Enum.member?(roles, "Contributor") -> "#{chev_level}Contributor" + Enum.member?(roles, "Streamer") -> "#{chev_level}Streamer" + Enum.member?(roles, "Mentor") -> "#{chev_level}Mentor" + true -> "#{chev_level}Chev" + + end + end + # Used to reset the spring password of the user when the site password is updated def set_new_spring_password(userid, new_password) do user = get_user_by_id(userid) diff --git a/lib/teiserver/libs/teiserver_configs.ex b/lib/teiserver/libs/teiserver_configs.ex index d9e107237..3c70f806d 100644 --- a/lib/teiserver/libs/teiserver_configs.ex +++ b/lib/teiserver/libs/teiserver_configs.ex @@ -572,7 +572,7 @@ defmodule Teiserver.TeiserverConfigs do key: "profile.Rank method", section: "Profiles", type: "select", - default: "Leaderboard rating", + default: "Role", permissions: ["Admin"], description: "The value used to assign rank icons at login", opts: [choices: ["Leaderboard rating", "Rating value", "Playtime", "Role"]] diff --git a/lib/teiserver/protocols/spring/spring_out.ex b/lib/teiserver/protocols/spring/spring_out.ex index 4989f8c11..6e5d14831 100644 --- a/lib/teiserver/protocols/spring/spring_out.ex +++ b/lib/teiserver/protocols/spring/spring_out.ex @@ -328,8 +328,16 @@ defmodule Teiserver.Protocols.SpringOut do defp do_reply(:client_status, nil), do: "" defp do_reply(:client_status, client) do + + rank_icon = cond do + client.lobby_client == "Teiserver Internal Client" -> "Bot" + client.bot -> "Bot" + true -> CacheUser.get_rank_icon(client.userid) + end + + status = Spring.create_client_status(client) - "CLIENTSTATUS #{client.name} #{status}\n" + "CLIENTSTATUS #{client.name} #{status} #{rank_icon}\n" end defp do_reply(:client_battlestatus, nil), do: nil diff --git a/lib/test_util/test_script.ex b/lib/test_util/test_script.ex new file mode 100644 index 000000000..59720a183 --- /dev/null +++ b/lib/test_util/test_script.ex @@ -0,0 +1,196 @@ +defmodule TestScript do + @moduledoc """ + This module is not used anywhere but it can be called while developing to create 4 users for testing: + Alpha, Bravo, Charlie, Delta + password is password + + + Run the server while enabling iex commands: + + iex -S mix phx.server + TestScript.run() + """ + alias Teiserver.Helper.StylingHelper + require Logger + alias Teiserver.{Account, CacheUser} + alias Teiserver.Game.MatchRatingLib + + def run() do + if Application.get_env(:teiserver, Teiserver)[:enable_hailstorm] do + # Start by rebuilding the database + + + users = [ + %{ + name: "1Chev", + player_minutes: 0, + os: 17 + }, + %{ + name: "2Chev", + player_minutes: 5 * 60, + os: 17 + }, + %{ + name: "3Chev", + player_minutes: 15 * 60, + os: 17 + }, + %{ + name: "4Chev", + player_minutes: 100 * 60, + os: 17 + }, + %{ + name: "5Chev", + player_minutes: 250 * 60, + os: 17 + }, + %{ + name: "6Chev", + player_minutes: 1001 * 60, + os: 17 + }, + %{ + name: "7Chev", + player_minutes: 1001 * 60, + os: 17, + rank_override: 6 + }, + %{ + name: "8Chev", + player_minutes: 1001 * 60, + os: 17, + rank_override: 7 + } + ] + + user_names = Enum.map(users, fn x -> x.name end) + make_accounts(user_names) + update_stats(users) + + "Test script finished successfully" + else + Logger.error("Hailstorm mode is not enabled, you cannot run the fakedata task") + end + end + + defp make_accounts(list_of_names) do + root_user = Teiserver.Repo.get_by(Teiserver.Account.User, email: "root@localhost") + + fixed_users = + list_of_names + |> Enum.map(fn x -> make_user(x, root_user) end) + |> Enum.filter(fn x -> x != nil end) + + Ecto.Multi.new() + |> Ecto.Multi.insert_all(:insert_all, Teiserver.Account.User, fixed_users) + |> Teiserver.Repo.transaction() + end + + # root_user is used to copy password and hash + # returns nil if user exists + defp make_user(name, root_user, day \\ 0, minutes \\ 0) do + name = name |> String.replace(" ", "") + + case Teiserver.Account.UserCacheLib.get_user_by_name(name) do + nil -> + %{ + name: name, + email: "#{name}", + password: root_user.password, + permissions: ["admin.dev.developer"], + icon: "fa-solid #{StylingHelper.random_icon()}", + colour: StylingHelper.random_colour(), + trust_score: 10_000, + behaviour_score: 10_000, + roles: ["Verified"], + data: %{ + lobby_client: "FakeData", + bot: false, + password_hash: root_user.data["password_hash"] + }, + inserted_at: Timex.shift(Timex.now(), days: -day, minutes: -minutes) |> time_convert, + updated_at: Timex.shift(Timex.now(), days: -day, minutes: -minutes) |> time_convert + } + + # Handle not nil + _ -> + Logger.info("#{name} already exists") + nil + end + end + + # This allows us to round off microseconds and convert datetime to naive_datetime + defp time_convert(t) do + t + |> Timex.to_unix() + |> Timex.from_unix() + |> Timex.to_naive_datetime() + end + + defp update_stats(users) when is_list(users) do + for user <- users, do: update_stats(user.name, user.player_minutes, user.os, user[:rank_override]) + end + + # Update the database with player minutes + def update_stats(username, player_minutes, os, rank_override \\ nil) do + user = Teiserver.Account.UserCacheLib.get_user_by_name(username) + user_id = user.id + + user_stat = %{ + player_minutes: player_minutes, + total_minutes: player_minutes + } + + user_stat = cond do + rank_override != nil -> Map.put(user_stat, :rank_override, rank_override) + true -> user_stat + end + + Account.update_user_stat(user_id, user_stat) + + # Now recalculate ranks + # This calc would usually be done in do_login + rank = CacheUser.calculate_rank(user_id) + + user = %{ + user + | rank: rank + } + + CacheUser.update_user(user, true) + update_rating(user.id, os) + end + + defp update_rating(user_id, os) do + new_uncertainty = 6 + new_skill = os + new_uncertainty + new_rating_value = os + new_leaderboard_rating = os - 2 * new_uncertainty + rating_type = "Team" + rating_type_id = MatchRatingLib.rating_type_name_lookup()[rating_type] + + case Account.get_rating(user_id, rating_type_id) do + nil -> + Account.create_rating(%{ + user_id: user_id, + rating_type_id: rating_type_id, + rating_value: new_rating_value, + skill: new_skill, + uncertainty: new_uncertainty, + leaderboard_rating: new_leaderboard_rating, + last_updated: Timex.now() + }) + + existing -> + Account.update_rating(existing, %{ + rating_value: new_rating_value, + skill: new_skill, + uncertainty: new_uncertainty, + leaderboard_rating: new_leaderboard_rating, + last_updated: Timex.now() + }) + end + end +end From 0be00ca77267cb6b9bdbc74680567fe88237608b Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Wed, 17 Apr 2024 07:10:05 +1000 Subject: [PATCH 2/5] Add rankicon parameter to ADDUSER command --- lib/teiserver/data/cache_user.ex | 14 ++++++++------ lib/teiserver/data/client.ex | 15 ++++++++++++--- lib/teiserver/protocols/spring/spring_out.ex | 15 +++++---------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/teiserver/data/cache_user.ex b/lib/teiserver/data/cache_user.ex index eaee1c837..15866ddbe 100644 --- a/lib/teiserver/data/cache_user.ex +++ b/lib/teiserver/data/cache_user.ex @@ -1335,21 +1335,23 @@ defmodule Teiserver.CacheUser do end end - def get_rank_icon(userid) do - stats = Account.get_user_stat_data(userid) + @doc""" + stats_data will be data column from teiserver_acctoun_users_stats + """ + def get_rank_icon(%{roles: roles} = _user, stats_data) do + stats_data # Play time Rank rank = cond do # This is only for tournament winners - stats["rank_override"] != nil -> - stats["rank_override"] |> int_parse + stats_data["rank_override"] != nil -> + stats_data["rank_override"] |> int_parse true -> - hours = rank_time(stats) + hours = rank_time(stats_data) convert_hours_to_rank(hours) end - %{roles: roles} = Account.get_user_by_id(userid) chev_level = rank + 1 cond do diff --git a/lib/teiserver/data/client.ex b/lib/teiserver/data/client.ex index 26c834f19..4ef186a9a 100644 --- a/lib/teiserver/data/client.ex +++ b/lib/teiserver/data/client.ex @@ -91,6 +91,14 @@ defmodule Teiserver.Client do @spec login(T.user(), atom(), String.t() | nil) :: T.client() def login(user, protocol, ip \\ nil, token_id \\ nil) do stats = Account.get_user_stat_data(user.id) + is_bot? = CacheUser.is_bot?(user) + lobby_client = stats["lobby_client"] + + rank_icon = cond do + lobby_client == "Teiserver Internal Client" -> "Bot" + is_bot? -> "Bot" + true -> CacheUser.get_rank_icon(user, stats) + end clan_tag = case Clans.get_clan(user.clan_id) do @@ -106,19 +114,20 @@ defmodule Teiserver.Client do tcp_pid: self(), rank: user.rank, moderator: CacheUser.is_moderator?(user), - bot: CacheUser.is_bot?(user), + bot: is_bot?, away: false, in_game: false, ip: ip || stats["last_ip"], country: stats["country"] || "??", - lobby_client: stats["lobby_client"], + lobby_client: lobby_client, shadowbanned: CacheUser.is_shadowbanned?(user), muted: CacheUser.has_mute?(user), awaiting_warn_ack: false, warned: false, token_id: token_id, clan_tag: clan_tag, - protocol: protocol + protocol: protocol, + rank_icon: rank_icon }) ClientLib.start_client_server(client) diff --git a/lib/teiserver/protocols/spring/spring_out.ex b/lib/teiserver/protocols/spring/spring_out.ex index 6e5d14831..197f14b58 100644 --- a/lib/teiserver/protocols/spring/spring_out.ex +++ b/lib/teiserver/protocols/spring/spring_out.ex @@ -147,7 +147,10 @@ defmodule Teiserver.Protocols.SpringOut do defp do_reply(:add_user, %{userid: nil}), do: "" defp do_reply(:add_user, client) do - "ADDUSER #{client.name} #{client.country} #{client.userid} #{client.lobby_client}\n" + IO.inspect(client, label: "client", charlists: :as_lists) + #rank_icon is not in Spring protocol and is an extra argument + #Full definition of client can be found in client.ex in the login function + "ADDUSER #{client.name} #{client.country} #{client.userid} #{client.lobby_client} #{client.rank_icon}\n" end defp do_reply(:friendlist, nil), do: "FRIENDLISTBEGIN\FRIENDLISTEND\n" @@ -328,16 +331,8 @@ defmodule Teiserver.Protocols.SpringOut do defp do_reply(:client_status, nil), do: "" defp do_reply(:client_status, client) do - - rank_icon = cond do - client.lobby_client == "Teiserver Internal Client" -> "Bot" - client.bot -> "Bot" - true -> CacheUser.get_rank_icon(client.userid) - end - - status = Spring.create_client_status(client) - "CLIENTSTATUS #{client.name} #{status} #{rank_icon}\n" + "CLIENTSTATUS #{client.name} #{status}\n" end defp do_reply(:client_battlestatus, nil), do: nil From e973411b8c782274ceb5d8c338af317400b3aa9f Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Wed, 17 Apr 2024 07:30:26 +1000 Subject: [PATCH 3/5] Remove logs --- lib/teiserver/protocols/spring/spring_out.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/teiserver/protocols/spring/spring_out.ex b/lib/teiserver/protocols/spring/spring_out.ex index 197f14b58..f78e63ad7 100644 --- a/lib/teiserver/protocols/spring/spring_out.ex +++ b/lib/teiserver/protocols/spring/spring_out.ex @@ -147,7 +147,6 @@ defmodule Teiserver.Protocols.SpringOut do defp do_reply(:add_user, %{userid: nil}), do: "" defp do_reply(:add_user, client) do - IO.inspect(client, label: "client", charlists: :as_lists) #rank_icon is not in Spring protocol and is an extra argument #Full definition of client can be found in client.ex in the login function "ADDUSER #{client.name} #{client.country} #{client.userid} #{client.lobby_client} #{client.rank_icon}\n" From a744e92d006c540516d9525fcda25c94b0d71535 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Wed, 17 Apr 2024 08:16:06 +1000 Subject: [PATCH 4/5] Minor cleanup --- lib/teiserver/account/libs/role_lib.ex | 5 +++-- lib/teiserver/data/cache_user.ex | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/teiserver/account/libs/role_lib.ex b/lib/teiserver/account/libs/role_lib.ex index 1e8036a6d..95207d39b 100644 --- a/lib/teiserver/account/libs/role_lib.ex +++ b/lib/teiserver/account/libs/role_lib.ex @@ -259,11 +259,12 @@ defmodule Teiserver.Account.RoleLib do end def allowed_role_management("Admin") do - staff_roles() ++ privileged_roles() ++ allowed_role_management("Moderator") + # Remove this code later; it has been fixed in another PR + staff_roles() ++ privileged_roles() ++ allowed_role_management("Moderator") ++ community_roles() end def allowed_role_management("Moderator") do - global_roles() ++ moderation_roles() ++ property_roles() ++ community_roles() + global_roles() ++ moderation_roles() ++ property_roles() end def allowed_role_management(_) do diff --git a/lib/teiserver/data/cache_user.ex b/lib/teiserver/data/cache_user.ex index 15866ddbe..ad0e0b553 100644 --- a/lib/teiserver/data/cache_user.ex +++ b/lib/teiserver/data/cache_user.ex @@ -1339,7 +1339,7 @@ defmodule Teiserver.CacheUser do stats_data will be data column from teiserver_acctoun_users_stats """ def get_rank_icon(%{roles: roles} = _user, stats_data) do - stats_data + # Play time Rank rank = cond do From 1cee1d2590380074a5f37fd886171b4bef7a5f43 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Thu, 18 Apr 2024 07:45:27 +1000 Subject: [PATCH 5/5] Add tab --- lib/teiserver/data/client.ex | 2 +- lib/teiserver/protocols/spring/spring_out.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/teiserver/data/client.ex b/lib/teiserver/data/client.ex index 4ef186a9a..59acae9ff 100644 --- a/lib/teiserver/data/client.ex +++ b/lib/teiserver/data/client.ex @@ -95,7 +95,7 @@ defmodule Teiserver.Client do lobby_client = stats["lobby_client"] rank_icon = cond do - lobby_client == "Teiserver Internal Client" -> "Bot" + protocol == :internal -> "Bot" is_bot? -> "Bot" true -> CacheUser.get_rank_icon(user, stats) end diff --git a/lib/teiserver/protocols/spring/spring_out.ex b/lib/teiserver/protocols/spring/spring_out.ex index f78e63ad7..d9d97517b 100644 --- a/lib/teiserver/protocols/spring/spring_out.ex +++ b/lib/teiserver/protocols/spring/spring_out.ex @@ -149,7 +149,7 @@ defmodule Teiserver.Protocols.SpringOut do defp do_reply(:add_user, client) do #rank_icon is not in Spring protocol and is an extra argument #Full definition of client can be found in client.ex in the login function - "ADDUSER #{client.name} #{client.country} #{client.userid} #{client.lobby_client} #{client.rank_icon}\n" + "ADDUSER #{client.name} #{client.country} #{client.userid} #{client.lobby_client}\t#{client.rank_icon}\n" end defp do_reply(:friendlist, nil), do: "FRIENDLISTBEGIN\FRIENDLISTEND\n"