From 52989e292663e6b338096d9cfc71f198524683c7 Mon Sep 17 00:00:00 2001 From: Joshua Augustinus Date: Wed, 17 Apr 2024 00:30:36 +1000 Subject: [PATCH] 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