From 6b33c8e9cc58e77dca7c154a68fad555cac56386 Mon Sep 17 00:00:00 2001 From: ANAND Date: Wed, 27 Nov 2019 11:30:39 +0530 Subject: [PATCH] ctf_stats: Add support for retrieving stats by rank (#454) --- mods/ctf/ctf_stats/chat.lua | 155 +++++++++++++++------------- mods/ctf/ctf_stats/gui.lua | 122 ++++++++++------------ mods/ctf/ctf_stats/init.lua | 58 +++++++++++ mods/other/random_messages/init.lua | 1 + 4 files changed, 197 insertions(+), 139 deletions(-) diff --git a/mods/ctf/ctf_stats/chat.lua b/mods/ctf/ctf_stats/chat.lua index e2b18f3..e174a56 100644 --- a/mods/ctf/ctf_stats/chat.lua +++ b/mods/ctf/ctf_stats/chat.lua @@ -2,47 +2,53 @@ -- Helpers -- ------------- -local function return_as_chat_result(to, name) - local players = {} - for pname, pstat in pairs(ctf_stats.players) do - pstat.name = pname - pstat.color = nil - table.insert(players, pstat) +local function return_as_chat_result(to, target) + local players = ctf_stats.get_ordered_players() + + local name, place, stat + if type(target) == "number" then + place = target + stat = players[target] + name = stat.name + elseif type(target) == "string" then + -- If target is a string, search through the player stats for a match + name = target + for i = 1, #players do + local pstat = players[i] + if pstat.name == name then + stat = pstat + place = i + break + end + end + + -- If stat does not exist yet, set place to size of players + 1 + if place < 1 then + place = #players + 1 + end + else + error("Invalid type passed to return_as_chat_result!", 2) end - table.sort(players, function(one, two) - return one.score > two.score - end) + -- Build return string + local result = (to == name and "You are in " or name .. " is in ") .. + place .. " place.\n" - local place = -1 - local me = nil - for i = 1, #players do - local pstat = players[i] - if pstat.name == name then - me = pstat - place = i - break + if stat then + local kd = stat.kills + if stat.deaths > 1 then + kd = kd / stat.deaths end - end - if place < 1 then - place = #players + 1 - end - local you_are_in = (to == name) and "You are in " or name .. " is in " - local result = you_are_in .. place .. " place.\n" - if me then - local kd = me.kills - if me.deaths > 1 then - kd = kd / me.deaths - end - result = result .. "Kills: " .. me.kills .. - " | Deaths: " .. me.deaths .. + result = result .. + "Kills: " .. stat.kills .. + " | Deaths: " .. stat.deaths .. " | K/D: " .. math.floor(kd * 10) / 10 .. - "\nBounty kills: " .. me.bounty_kills .. - " | Captures: " .. me.captures .. - " | Attempts: " .. me.attempts .. - "\nScore: " .. math.floor(me.score) + "\nBounty kills: " .. stat.bounty_kills .. + " | Captures: " .. stat.captures .. + " | Attempts: " .. stat.attempts .. + "\nScore: " .. math.floor(stat.score) end - return true, result + return result end ------------------- @@ -62,56 +68,57 @@ minetest.register_chatcommand("summary", { }) minetest.register_chatcommand("r", { - params = "[]", - description = "Display rankings of yourself or another player as a chat result.", + params = "[ | ]", + description = "Display rankings of yourself, or another player or rank, as a chat result.", func = function(name, param) - local target - if param ~= "" then - param = param:trim() - if ctf_stats.players[param] then - target = param - minetest.log("action", name .. " ran /r " .. param) - else - return false, "Can't find player '" .. param .. "'" - end - else - target = name - minetest.log("action", name .. " ran /r") + local target, error = ctf_stats.get_target(name, param) + if not target then + return false, error end - return return_as_chat_result(name, target) + + minetest.log("action", name .. " runs /r " .. param) + return true, return_as_chat_result(name, target) + end +}) + +minetest.register_chatcommand("rn", { + params = "", + description = "Display rankings of player at the specified rank.", + func = function(name, param) + if not param or param == "" then + return false, "Empty arguments not allowed! Specify a rank." + end + + param = tonumber(param) + if not param then + return false, "Argument isn't a valid number!" + elseif param <= 0 or param > #ctf_stats.get_ordered_players() or + param ~= math.floor(param) then + return false, "Invalid number or number out of bounds!" + -- TODO: This is the worst way to do it. FIX IT. + end + + minetest.log("action", name .. " runs /rn " .. param) + return true, return_as_chat_result(name, param) end }) minetest.register_chatcommand("rankings", { - params = "[]", - description = "Display rankings of yourself or another player.", + params = "[ | ]", + description = "Display rankings of yourself, or another player or rank.", func = function(name, param) - local target - if param ~= "" then - param = param:trim() - if ctf_stats.players[param] then - target = param - minetest.log("action", name .. " ran /rankings " .. param) - else - return false, "Can't find player '" .. param .. "'" - end - else - target = name - minetest.log("action", name .. " ran /rankings") + local target, error = ctf_stats.get_target(name, param) + if not target then + return false, error end + minetest.log("action", name .. " runs /rankings " .. param) if not minetest.get_player_by_name(name) then - return return_as_chat_result(name, target) + return true, return_as_chat_result(name, target) else - local players = {} - for pname, pstat in pairs(ctf_stats.players) do - pstat.name = pname - pstat.color = nil - table.insert(players, pstat) - end - - local fs = ctf_stats.get_formspec("Player Rankings", players, 0, target) - minetest.show_formspec(name, "ctf_stats:rankings", fs) + minetest.show_formspec(name, "ctf_stats:rankings", ctf_stats.get_formspec( + "Player Rankings", ctf_stats.get_ordered_players(), 0, target)) + return true end end }) diff --git a/mods/ctf/ctf_stats/gui.lua b/mods/ctf/ctf_stats/gui.lua index db5ea1f..8e3b7bd 100644 --- a/mods/ctf/ctf_stats/gui.lua +++ b/mods/ctf/ctf_stats/gui.lua @@ -1,3 +1,6 @@ +-- Number of entries to display in the player rankings table +ctf_stats.rankings_display_count = 50 + -- Formspec element that governs table columns and their attributes local tablecolumns = { "tablecolumns[color;", @@ -20,8 +23,8 @@ local function render_team_stats(red, blue, stat, round) blue_stat = math.floor(blue_stat * 10) / 10 end return red_stat + blue_stat .. " (" .. - minetest.colorize(red.color, tostring(red_stat)) .. " - " .. - minetest.colorize(blue.color, tostring(blue_stat)) .. ")" + minetest.colorize(red.color, tostring(red_stat)) .. " - " .. + minetest.colorize(blue.color, tostring(blue_stat)) .. ")" end function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player, time) @@ -90,7 +93,7 @@ function ctf_stats.get_formspec_match_summary(stats, winner_team, winner_player, return ret end -function ctf_stats.get_formspec(title, players, header, hlt_name) +function ctf_stats.get_formspec(title, players, header, target) table.sort(players, function(one, two) return one.score > two.score end) @@ -105,14 +108,25 @@ function ctf_stats.get_formspec(title, players, header, hlt_name) ret = ret .. "table[0.5,0;13.25,6.1;scores;" ret = ret .. "#ffffff,,Player,Kills,Deaths,K/D,Bounty Kills,Captures,Attempts,Score" - local player_in_top_50 = false + local hstat, hplace + if type(target) == "number" then + hstat = players[target] + hplace = target + elseif type(target) == "string" then + for i, stat in pairs(players) do + if stat.name == target then + hplace = i + hstat = stat + break + end + end + end - for i = 1, math.min(#players, 50) do + for i = 1, math.min(#players, ctf_stats.rankings_display_count) do local pstat = players[i] local color - if hlt_name and pstat.name == hlt_name then + if hplace == i then color = "#ffff00" - player_in_top_50 = true else color = pstat.color or "#ffffff" end @@ -121,58 +135,46 @@ function ctf_stats.get_formspec(title, players, header, hlt_name) kd = kd / pstat.deaths end ret = ret .. - "," .. string.gsub(color, "0x", "#") .. - "," .. i .. - "," .. pstat.name .. - "," .. pstat.kills .. - "," .. pstat.deaths .. - "," .. math.floor(kd * 10) / 10 .. - "," .. pstat.bounty_kills .. - "," .. pstat.captures .. - "," .. pstat.attempts .. - "," .. math.floor(pstat.score * 10) / 10 + "," .. string.gsub(color, "0x", "#") .. + "," .. i .. + "," .. pstat.name .. + "," .. pstat.kills .. + "," .. pstat.deaths .. + "," .. math.floor(kd * 10) / 10 .. + "," .. pstat.bounty_kills .. + "," .. pstat.captures .. + "," .. pstat.attempts .. + "," .. math.floor(pstat.score * 10) / 10 end ret = ret .. ";-1]" - -- If hlt_name not in top 50, add a separate table + -- If target not in top 50, add a separate table -- This would result in the player's score displayed at the bottom -- of the list but yet be visible without having to scroll - if hlt_name and not player_in_top_50 then - local hlt_player, hlt_rank, hlt_kd - - for i = 1, #players do - if players[i].name == hlt_name then - hlt_player = players[i] - hlt_rank = i - break - end + if hplace and hplace > ctf_stats.rankings_display_count then + local h_kd = hstat.kills + if hstat.deaths > 1 then + h_kd = h_kd / hstat.deaths end - if hlt_player then - hlt_kd = hlt_player.kills - if hlt_player.deaths > 1 then - hlt_kd = hlt_kd / hlt_player.deaths - end - - ret = ret .. tablecolumns - ret = ret .. "tableoptions[highlight=#00000000]" - ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;" - ret = ret .. "#ffff00" .. - "," .. hlt_rank .. - "," .. hlt_player.name .. - "," .. hlt_player.kills .. - "," .. hlt_player.deaths .. - "," .. math.floor(hlt_kd * 10) / 10 .. - "," .. hlt_player.bounty_kills .. - "," .. hlt_player.captures .. - "," .. hlt_player.attempts .. - "," .. math.floor(hlt_player.score * 10) / 10 .. ";-1]" - end - -- else - -- ret = ret .. "box[0.5,6.1;13.25,0.4;#101010]" - -- Adds a box where the extra table should be, in order to make it - -- appear as an extension of the main table, but the color can't be - -- matched, and looks slightly brighter or slightly darker than the table + ret = ret .. tablecolumns + ret = ret .. "tableoptions[highlight=#00000000]" + ret = ret .. "table[0.5,6.1;13.25,0.4;hlt_score;" + ret = ret .. "#ffff00" .. + "," .. hplace .. + "," .. hstat.name .. + "," .. hstat.kills .. + "," .. hstat.deaths .. + "," .. math.floor(h_kd * 10) / 10 .. + "," .. hstat.bounty_kills .. + "," .. hstat.captures .. + "," .. hstat.attempts .. + "," .. math.floor(hstat.score * 10) / 10 .. ";-1]" + --[[ else + ret = ret .. "box[0.5,6.1;13.25,0.4;#101010]" + Adds a box where the extra table should be, in order to make it + appear as an extension of the main table, but the color can't be + matched, and looks slightly brighter or slightly darker than the table]] end ret = ret .. "button_exit[10,6.5;3,1;close;Close]" @@ -180,11 +182,8 @@ function ctf_stats.get_formspec(title, players, header, hlt_name) return ret end -function ctf_stats.get_html(title, players) - table.sort(players, function(one, two) - return one.score > two.score - end) - +function ctf_stats.get_html(title) + local players = ctf_stats.get_ordered_players() local ret = "

" .. title .. "

" ret = ret .. "" .. "" .. @@ -220,13 +219,6 @@ function ctf_stats.get_html(title, players) end function ctf_stats.html_to_file(filepath) - local players = {} - for name, pstat in pairs(ctf_stats.players) do - pstat.name = name - pstat.color = nil - table.insert(players, pstat) - end - local html = ctf_stats.get_html("Player Rankings", players) local f = io.open(filepath, "w") f:write("\n") f:write("\n") @@ -234,7 +226,7 @@ function ctf_stats.html_to_file(filepath) f:write("Player Rankings\n") f:write("\n") f:write("\n") - f:write(html) + f:write(ctf_stats.get_html("Player Rankings")) f:write("\n") f:close() end diff --git a/mods/ctf/ctf_stats/init.lua b/mods/ctf/ctf_stats/init.lua index ba4bcdf..e4d71b1 100644 --- a/mods/ctf/ctf_stats/init.lua +++ b/mods/ctf/ctf_stats/init.lua @@ -137,6 +137,64 @@ function ctf_stats.player(name) return player_stats, match_player_stats end +function ctf_stats.get_ordered_players() + local players = {} + + -- Copy player stats into new empty table + for pname, pstat in pairs(ctf_stats.players) do + pstat.name = pname + pstat.color = nil + table.insert(players, pstat) + end + + -- Sort table in the order of descending scores + table.sort(players, function(one, two) + return one.score > two.score + end) + + return players +end + +function ctf_stats.get_target(name, param) + param = param:trim() + + -- If param is not empty, check if it's a number or a string + if param ~= "" then + -- Order of the following checks are as given below: + -- + -- * `param` is returned as a string if player's stats exists + -- * If no matching stats exist, `param` is checked if it's a number + -- * If `param` isn't a number, it is assumed to be invalid, and nil is returned + -- * If `param` is a number, `param` is checked if out of bounds + -- * If `param` is not out of bounds, `param` is returned as a number, else nil + -- + -- This order of checks is important because, in the case of `param` matching + -- both a number and a player name, it would be considered as a player name. + + -- Check if param matches a player name + if ctf_stats.players[param] then + return param + else + -- Check if param is a number + local rank = tonumber(param) + if rank then + -- Check if param is within range + -- TODO: Fix this hack by maintaining two tables - an ordered list, and a hashmap + if rank <= 0 or rank > #ctf_stats.get_ordered_players() or + rank ~= math.floor(rank) then + return nil, "Invalid number or number out of bounds!" + else + return rank + end + else + return nil, "Invalid player name specified!" + end + end + else + return name + end +end + ctf.register_on_join_team(function(name, tname) ctf_stats.current[tname][name] = ctf_stats.current[tname][name] or { kills = 0, diff --git a/mods/other/random_messages/init.lua b/mods/other/random_messages/init.lua index 28ee7fc..38c1141 100644 --- a/mods/other/random_messages/init.lua +++ b/mods/other/random_messages/init.lua @@ -69,6 +69,7 @@ function random_messages.read_messages() "Help your team claim victory by storing extra weapons in the team chest, and never taking more than you need.", "Excessive spawn-killing is a direct violation of the rules - appropriate punishments will be given.", "Use /r to check your score and rank, and /rankings to see the league tables.", + "Use /r or /rn to check the rankings of the player in the given rank.", "Use bandages on team-mates to heal them by 3-4 HP if their health is below 15 HP.", "Use /m to add a team marker at pointed location, that's visible only to team-mates.", "Use /summary to check scores of the current match and the previous match.",