ctf_stats = {} local _needs_save = false local storage = minetest.get_mod_storage() local data_to_persist = { "matches", "players" } ctf_stats.prev_match_summary = storage:get_string("prev_match_summary") function ctf_stats.load_legacy() local file = io.open(minetest.get_worldpath() .. "/ctf_stats.txt", "r") if not file then return false end local table = minetest.deserialize(file:read("*all")) file:close() if type(table) ~= "table" then return false end ctf.log("ctf_stats", "Migrating stats...") ctf_stats.matches = table.matches ctf_stats.players = table.players for name, player_stats in pairs(ctf_stats.players) do if not player_stats.score or player_stats.score < 0 then player_stats.score = 0 end if player_stats.score > 300 then player_stats.score = (player_stats.score - 300) / 30 + 300 end if player_stats.score > 800 then player_stats.score = 800 end player_stats.wins = player_stats.wins or {} if player_stats.blue_wins then player_stats.wins.blue = player_stats.blue_wins player_stats.blue_wins = nil end if player_stats.red_wins then player_stats.wins.red = player_stats.red_wins player_stats.red_wins = nil end player_stats.wins.blue = player_stats.wins.blue or 0 player_stats.wins.red = player_stats.wins.red or 0 end ctf_stats.matches.wins = ctf_stats.matches.wins or { red = ctf_stats.matches.red_wins or 0, blue = ctf_stats.matches.blue_wins or 0, } _needs_save = true os.remove(minetest.get_worldpath() .. "/ctf_stats.txt") return true end -- Load persistant data from mod storage (or legacy file) -- and initialize empty tables where required function ctf_stats.load() if not ctf_stats.load_legacy() then for _, key in pairs(data_to_persist) do ctf_stats[key] = minetest.parse_json(storage:get_string(key)) end _needs_save = true end -- Make sure all tables are present ctf_stats.players = ctf_stats.players or {} ctf_stats.matches = ctf_stats.matches or { wins = { blue = 0, red = 0, }, skipped = 0, } ctf_stats.current = ctf_stats.current or { red = {}, blue = {} } -- Strip players which have no score for name, player_stats in pairs(ctf_stats.players) do if not player_stats.score or player_stats.score <= 0 then ctf_stats.players[name] = nil _needs_save = true else player_stats.bounty_kills = player_stats.bounty_kills or 0 end end end -- Save persistant data to mod storage function ctf_stats.save() for _, key in pairs(data_to_persist) do storage:set_string(key, minetest.write_json(ctf_stats[key])) end end -- Separate recursion to check if save required and then call ctf_stats.save -- This allows ctf_stats.save to be called directly when an immediate save is required local function check_if_save_needed() if _needs_save then ctf_stats.save() _needs_save = false end minetest.after(13, check_if_save_needed) end minetest.after(13, check_if_save_needed) -- API function to allow other mods to request a save -- TODO: This should be done automatically once a proper API is in place function ctf_stats.request_save() _needs_save = true end function ctf_stats.player_or_nil(name) return ctf_stats.players[name], ctf_stats.current.red[name] or ctf_stats.current.blue[name] end -- Returns a tuple: `player_stats`, `match_player_stats` function ctf_stats.player(name) local player_stats = ctf_stats.players[name] if not player_stats then player_stats = { name = name, wins = { red = 0, blue = 0, }, kills = 0, deaths = 0, captures = 0, attempts = 0, score = 0, bounty_kills = 0, } ctf_stats.players[name] = player_stats end local match_player_stats = ctf_stats.current.red[name] or ctf_stats.current.blue[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 function ctf_stats.is_pro(name) local stats = ctf_stats.player(name) local kd = stats.kills / (stats.deaths == 0 and 1 or stats.deaths) return stats.score >= 10000 and kd >= 1.5 end ctf.register_on_join_team(function(name, tname) ctf_stats.current[tname][name] = ctf_stats.current[tname][name] or { kills = 0, kills_since_death = 0, deaths = 0, attempts = 0, captures = 0, score = 0, bounty_kills = 0, } end) ctf_stats.winner_team = "-" ctf_stats.winner_player = "-" table.insert(ctf_flag.registered_on_capture, 1, function(name, flag) local main, match = ctf_stats.player(name) if main and match then main.captures = main.captures + 1 main.score = main.score + 50 match.captures = match.captures + 1 match.score = match.score + 50 _needs_save = true end ctf_stats.winner_player = name hud_score.new(name, { name = "ctf_stats:flag_capture", color = "0xFF00FF", value = 50 }) end) ctf_match.register_on_winner(function(winner) ctf_stats.matches.wins[winner] = ctf_stats.matches.wins[winner] + 1 ctf_stats.winner_team = winner -- Show match summary local fs = ctf_stats.get_formspec_match_summary(ctf_stats.current, ctf_stats.winner_team, ctf_stats.winner_player, ctf_match.get_match_duration()) for _, player in pairs(minetest.get_connected_players()) do minetest.show_formspec(player:get_player_name(), "ctf_stats:eom", fs) end -- Set prev_match_summary and write to mod_storage ctf_stats.prev_match_summary = fs storage:set_string("prev_match_summary", fs) -- Flush data to mod_storage at the end of each match ctf_stats.save() end) ctf_match.register_on_skip_map(function() ctf_stats.matches.skipped = ctf_stats.matches.skipped + 1 -- Show match summary local fs = ctf_stats.get_formspec_match_summary(ctf_stats.current, ctf_stats.winner_team, ctf_stats.winner_player, ctf_match.get_match_duration()) for _, player in pairs(minetest.get_connected_players()) do minetest.show_formspec(player:get_player_name(), "ctf_stats:eom", fs) end -- Set prev_match_summary and write to mod_storage ctf_stats.prev_match_summary = fs storage:set_string("prev_match_summary", fs) ctf_stats.save() end) ctf_match.register_on_new_match(function() ctf_stats.current = { red = {}, blue = {} } ctf_stats.winner_team = "-" ctf_stats.winner_player = "-" _needs_save = true end) -- ctf_map can't be added as a dependency, as that'd result -- in cyclic dependencies between ctf_map and ctf_stats minetest.after(0, function() ctf_map.register_on_map_loaded(function(map) ctf_stats.current.map = map.name end) end) ctf_flag.register_on_pick_up(function(name, flag) local main, match = ctf_stats.player(name) if main and match then main.attempts = main.attempts + 1 main.score = main.score + 20 match.attempts = match.attempts + 1 match.score = match.score + 20 _needs_save = true end hud_score.new(name, { name = "ctf_stats:flag_pick_up", color = "0xAA00AA", value = 20 }) end) ctf_flag.register_on_precapture(function(name, flag) local tplayer = ctf.player(name) local main, _ = ctf_stats.player(name) if main then main.wins[tplayer.team] = main.wins[tplayer.team] + 1 _needs_save = true end return true end) local good_weapons = { "default:sword_steel", "default:sword_bronze", "default:sword_mese", "default:sword_diamond", "default:pick_mese", "default:pick_diamond", "default:axe_mese", "default:axe_diamond", "default:shovel_mese", "default:shovel_diamond", "shooter:grenade", "shooter:shotgun", "shooter:rifle", "shooter:machine_gun", "sniper_rifles:rifle_762", "sniper_rifles:rifle_magnum", } local function invHasGoodWeapons(inv) for _, weapon in pairs(good_weapons) do if inv:contains_item("main", weapon) then return true end end return false end local function calculateKillReward(victim, killer) local vmain, victim_match = ctf_stats.player(victim) -- +5 for every kill they've made since last death in this match. local reward = victim_match.kills_since_death * 5 ctf.log("ctf_stats", "Player " .. victim .. " has made " .. reward .. " score worth of kills since last death") -- 30 * K/D ratio, with variable based on player's score local kdreward = 30 * vmain.kills / (vmain.deaths + 1) local max = vmain.score / 5 if kdreward > max then kdreward = max end if kdreward > 100 then kdreward = 100 end reward = reward + kdreward -- Limited to 5 <= X <= 250 if reward > 250 then reward = 250 elseif reward < 5 then reward = 5 end -- Half if no good weapons local inv = minetest.get_inventory({ type = "player", name = victim }) if not invHasGoodWeapons(inv) then ctf.log("ctf_stats", "Player " .. victim .. " has no good weapons") reward = reward * 0.5 else ctf.log("ctf_stats", "Player " .. victim .. " has good weapons") end return reward end ctf.register_on_killedplayer(function(victim, killer) -- Suicide is not encouraged here at CTF if victim == killer then return end local main, match = ctf_stats.player(killer) if main and match then local reward = calculateKillReward(victim, killer) main.kills = main.kills + 1 main.score = main.score + reward match.kills = match.kills + 1 match.score = match.score + reward match.kills_since_death = match.kills_since_death + 1 _needs_save = true reward = math.floor(reward * 100) / 100 hud_score.new(killer, { name = "ctf_stats:kill_score", color = "0x00FF00", value = reward }) end end) minetest.register_on_dieplayer(function(player) local main, match = ctf_stats.player(player:get_player_name()) if main and match then main.deaths = main.deaths + 1 match.deaths = match.deaths + 1 match.kills_since_death = 0 _needs_save = true end end) ctf_stats.load() dofile(minetest.get_modpath("ctf_stats") .. "/gui.lua") dofile(minetest.get_modpath("ctf_stats") .. "/chat.lua")