Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Blocked) Add rank_icon to ADDUSER command #275

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
2 changes: 1 addition & 1 deletion lib/teiserver/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 55 additions & 12 deletions lib/teiserver/data/cache_user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1264,23 +1264,26 @@ 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
@spec calculate_rank(T.userid(), String.t()) :: non_neg_integer()
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
Expand All @@ -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

Expand All @@ -1321,6 +1319,51 @@ 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

@doc"""
stats_data will be data column from teiserver_acctoun_users_stats
"""
def get_rank_icon(%{roles: roles} = _user, stats_data) do

# Play time Rank
rank =
cond do
# This is only for tournament winners
stats_data["rank_override"] != nil ->
stats_data["rank_override"] |> int_parse

true ->
hours = rank_time(stats_data)
convert_hours_to_rank(hours)
end

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)
Expand Down
15 changes: 12 additions & 3 deletions lib/teiserver/data/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
protocol == :internal -> "Bot"
is_bot? -> "Bot"
true -> CacheUser.get_rank_icon(user, stats)
end

clan_tag =
case Clans.get_clan(user.clan_id) do
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion lib/teiserver/libs/teiserver_configs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,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"]]
Expand Down
4 changes: 3 additions & 1 deletion lib/teiserver/protocols/spring/spring_out.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ 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"
#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}\t#{client.rank_icon}\n"
end

defp do_reply(:friendlist, nil), do: "FRIENDLISTBEGIN\FRIENDLISTEND\n"
Expand Down
196 changes: 196 additions & 0 deletions lib/test_util/test_script.ex
Original file line number Diff line number Diff line change
@@ -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
Loading