diff --git a/lua/antihack/starcraft.lua b/lua/antihack/starcraft.lua index fb7cdfe..aba7612 100644 --- a/lua/antihack/starcraft.lua +++ b/lua/antihack/starcraft.lua @@ -19,7 +19,7 @@ ah_mh_value = 139 function ah_init() timer_add("ah_timer", config.ah_interval, ah_timer_tick) - TRACE("Antihack activated") + INFO("Starcraft Antihack activated") end -- send memory check request to all players in games @@ -50,7 +50,7 @@ function ah_handle_client(account, request_id, data) if (request_id == ah_mh_request_id) then -- read value from the memory local value = bytes_to_int(data, 0, 2) - DEBUG(account.name .. " memory value: " .. value) + --TRACE(account.name .. " memory value: " .. value) if not (value == ah_mh_value) then is_cheater = true diff --git a/lua/command/ping.lua b/lua/command/ping.lua new file mode 100644 index 0000000..b8550e4 --- /dev/null +++ b/lua/command/ping.lua @@ -0,0 +1,16 @@ +--[[ + Copyright (C) 2014 HarpyWar (harpywar@gmail.com) + + This file is a part of the PvPGN Project http://pvpgn.pro + Licensed under the same terms as Lua itself. +]]-- + + +function command_ping(account, text) + -- allow warcraft 3 client only + if (account.clienttag == CLIENTTAG_WAR3XP) and (config.ghost) then + return gh_command_ping(account, text) + end + + return 1 +end diff --git a/lua/command/stats.lua b/lua/command/stats.lua new file mode 100644 index 0000000..dfc0a73 --- /dev/null +++ b/lua/command/stats.lua @@ -0,0 +1,15 @@ +--[[ + Copyright (C) 2014 HarpyWar (harpywar@gmail.com) + + This file is a part of the PvPGN Project http://pvpgn.pro + Licensed under the same terms as Lua itself. +]]-- + + +function command_stats(account, text) + if config.ghost_dota_server and config.ghost and (account.clienttag == CLIENTTAG_WAR3XP) then + return gh_command_stats(account, text) + end + + return 1 +end diff --git a/lua/config.lua b/lua/config.lua index 731c65b..8480b7b 100644 --- a/lua/config.lua +++ b/lua/config.lua @@ -9,6 +9,12 @@ -- Config table can be extended here with your own variables -- values are preloaded from bnetd.conf config = { + -- Path to "var" directory (with slash at the end) + -- Usage: config.vardir() + vardir = function() + return string.replace(config.statusdir, "status", "") + end, + flood_immunity_users = { "admin", "" }, -- ignore flood protection for these users -- Quiz settings @@ -27,7 +33,7 @@ config = { -- GHost++ (https://github.com/OHSystem/ohsystem) ghost = false, -- enable GHost commands - ghost_bots = { hostbot1, hostbot2 }, -- list of authorized bots + ghost_bots = { "hostbot1", "hostbot2" }, -- list of authorized bots ghost_dota_server = true, -- replace normal Warcraft 3 stats with DotA ghost_ping_expire = 90, -- interval when outdated botpings should be removed (bot ping updates for each user when he join a game hosted by ghost); game list shows to user depending on the best ping to host bot diff --git a/lua/extend/account.lua b/lua/extend/account.lua index 05bdb5e..996283b 100644 --- a/lua/extend/account.lua +++ b/lua/extend/account.lua @@ -15,7 +15,7 @@ function users_count() return count end --- Get count of all server account +-- Get count of all server account that were used by the server function accounts_count() local count = 0 for i,account in pairs(api.server_get_users(true)) do diff --git a/lua/extend/account_wrap.lua b/lua/extend/account_wrap.lua index 18582a6..486674b 100644 --- a/lua/extend/account_wrap.lua +++ b/lua/extend/account_wrap.lua @@ -91,10 +91,10 @@ function account_set_auth_lock(username, value) end function account_get_auth_locktime(username) - return api.account_get_attr(username, "BNET\\auth\\locktime", attr_type_int) + return api.account_get_attr(username, "BNET\\auth\\locktime", attr_type_num) end function account_set_auth_locktime(username, value) - return api.account_set_attr(username, "BNET\\auth\\locktime", attr_type_int, value) + return api.account_set_attr(username, "BNET\\auth\\locktime", attr_type_num, value) end function account_get_auth_lockreason(username) @@ -119,10 +119,10 @@ function account_set_auth_mute(username, value) end function account_get_auth_mutetime(username) - return api.account_get_attr(username, "BNET\\auth\\mutetime", attr_type_int) + return api.account_get_attr(username, "BNET\\auth\\mutetime", attr_type_num) end function account_set_auth_mutetime(username, value) - return api.account_set_attr(username, "BNET\\auth\\mutetime", attr_type_int, value) + return api.account_set_attr(username, "BNET\\auth\\mutetime", attr_type_num, value) end function account_get_auth_mutereason(username) @@ -140,17 +140,17 @@ function account_set_auth_muteby(username, value) end function account_get_auth_command_groups(username) - return api.account_get_attr(username, "BNET\\auth\\command_groups", attr_type_int) + return api.account_get_attr(username, "BNET\\auth\\command_groups", attr_type_num) end function account_set_auth_command_groups(username, value) - return api.account_set_attr(username, "BNET\\auth\\command_groups", attr_type_int, value) + return api.account_set_attr(username, "BNET\\auth\\command_groups", attr_type_num, value) end function account_get_acct_lastlogin_time(username) - return api.account_get_attr(username, "BNET\\acct\\lastlogin_time", attr_type_int) + return api.account_get_attr(username, "BNET\\acct\\lastlogin_time", attr_type_num) end function account_set_acct_lastlogin_time(username, value) - return api.account_set_attr(username, "BNET\\acct\\lastlogin_time", attr_type_int, value) + return api.account_set_attr(username, "BNET\\acct\\lastlogin_time", attr_type_num, value) end function account_get_acct_lastlogin_owner(username) @@ -161,10 +161,10 @@ function account_set_acct_lastlogin_owner(username, value) end function account_get_acct_createtime(username) - return api.account_get_attr(username, "BNET\\acct\\ctime", attr_type_int) + return api.account_get_attr(username, "BNET\\acct\\ctime", attr_type_num) end function account_set_acct_createtime(username, value) - return api.account_set_attr(username, "BNET\\acct\\ctime", attr_type_int, value) + return api.account_set_attr(username, "BNET\\acct\\ctime", attr_type_num, value) end function account_get_acct_lastlogin_clienttag(username) @@ -529,7 +529,9 @@ end -- function account_get_dotarating_3x3(username) - return api.account_get_attr(username, "Record\\W3XP\\dota_3_rating", attr_type_num) + value = api.account_get_attr(username, "Record\\W3XP\\dota_3_rating", attr_type_num) + if (value == 0) then value = 1000 end + return value end function account_set_dotarating_3x3(username, value) return api.account_set_attr(username, "Record\\W3XP\\dota_3_rating", attr_type_num, value) @@ -565,7 +567,9 @@ end function account_get_dotarating_5x5(username) - return api.account_get_attr(username, "Record\\W3XP\\dota_5_rating", attr_type_num) + value = api.account_get_attr(username, "Record\\W3XP\\dota_5_rating", attr_type_num) + if (value == 0) then value = 1000 end + return value end function account_set_dotarating_5x5(username, value) return api.account_set_attr(username, "Record\\W3XP\\dota_5_rating", attr_type_num, value) @@ -602,7 +606,9 @@ end function account_get_botping(username) value = api.account_get_attr(username, "BNET\\acct\\botping", attr_type_str) local pings = {} - + -- if pings were not set yet then return empty table + if string.empty(value) then return pings end + -- deserialize and return table -- data format: "unixtime,botname,ping;..." for chunk in string.split(value,";") do @@ -630,7 +636,7 @@ function account_set_botping(username, pings) -- serialize table for k,v in pairs(pings) do -- ignore expired pings - if (os.time() - v.date) < 60*60*24*ghost_ping_expire then + if (os.time() - tonumber(v.date)) < 60*60*24*config.ghost_ping_expire then value = value .. string.format("%s,%s,%s;", v.date, v.bot, v.ping); end end diff --git a/lua/extend/enum/game.lua b/lua/extend/enum/game.lua index a2e0aa2..3057353 100644 --- a/lua/extend/enum/game.lua +++ b/lua/extend/enum/game.lua @@ -5,6 +5,9 @@ Licensed under the same terms as Lua itself. ]]-- +-- Strings used here - not numbers, because these values will be +-- compare with string values returned from API. +-- For example "0" != 0 in Lua. game_type_none, game_type_all, @@ -27,7 +30,7 @@ game_type_diablo, game_type_diablo2open, game_type_diablo2closed, game_type_anongame -= 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 += "0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20" game_status_started, @@ -35,7 +38,7 @@ game_status_full, game_status_open, game_status_loaded, game_status_done -= 0,1,2,3,4 += "0","1","2","3","4" game_result_none, @@ -45,7 +48,7 @@ game_result_draw, game_result_disconnect, game_result_observer, game_result_playing -= 0,1,2,3,4,5,6 += "0","1","2","3","4","5","6" game_option_none, @@ -81,7 +84,7 @@ game_option_topvbot_4, game_option_topvbot_3, game_option_topvbot_2, game_option_topvbot_1 -= 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32 += "0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","32" game_maptype_none, @@ -91,7 +94,7 @@ game_maptype_ladder, game_maptype_pgl, game_maptype_kbk, game_maptype_compusa -= 0,1,2,3,4,5,6 += "0","1","2","3","4","5","6" game_tileset_none, @@ -103,7 +106,7 @@ game_tileset_jungle, game_tileset_desert, game_tileset_ice, game_tileset_twilight -= 0,1,2,3,4,5,6,7,8,9 += "0","1","2","3","4","5","6","7","8","9" game_speed_none, @@ -114,7 +117,7 @@ game_speed_normal, game_speed_fast, game_speed_faster, game_speed_fastest -= 0,1,2,3,4,5,6,7 += "0","1","2","3","4","5","6","7" game_difficulty_none, @@ -124,10 +127,10 @@ game_difficulty_hell, game_difficulty_hardcore_normal, game_difficulty_hardcore_nightmare, game_difficulty_hardcore_hell -= 0,1,2,3,4,5,6 += "0","1","2","3","4","5","6" game_flag_none, game_flag_private -= 0,1 += "0","1" diff --git a/lua/ghost/command.lua b/lua/ghost/command.lua index cb3a652..0b85d07 100644 --- a/lua/ghost/command.lua +++ b/lua/ghost/command.lua @@ -25,11 +25,12 @@ function command_host(account, text) return -1 end - if gh_get_userbot(account.name) then - local gamename = gh_get_usergame(account.name) - local game = game_get_by_name(gamename, account.clienttag, game_type_all) + -- if user already host a game + if gh_get_userbot_name(account.name) then + local gamename = gh_get_userbot_game(account.name) + local game = api.game_get_by_name(gamename, account.clienttag, game_type_all) if next(game) then - api.message_send_text(account.name, message_type_info, localize("You already host a game \"{}\". Use /unhost to destroy it.", gamename)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "You already host a game \"{}\". Use /unhost to destroy it.", gamename)) return -1 else -- if game doesn't exist then remove mapped bot for user @@ -37,30 +38,23 @@ function command_host(account, text) end end - -- get bot by ping + -- get available bot by ping local botname = gh_select_bot(account.name) - + if not botname then + api.message_send_text(account.name, message_type_error, nil, localize(account.name, "Enable to create game. HostBots are temporary offline.")) + return -1 + end + -- redirect message to bot - message_send_ghost(botname, string.format("/pvpgn host %s %s %s %s", account.name, args[1], args[2], args[3]) ) + gh_message_send(botname, string.format("/pvpgn host %s %s %s %s", account.name, args[1], args[2], args[3]) ) return 0 end -gh_maplist = {} - -- /chost [code] [gamename] function command_chost(account, text) if not config.ghost or not account.clienttag == CLIENTTAG_WAR3XP then return 1 end - - local filename = gh_directory() .. "/maplist.txt" - -- load maps from the file - file_load(filename, file_load_dictionary_callback, - function(a,b) - -- split second value to get name and filename - local mapname,mapfile = string.split(b,"|") - table.insert(gh_maplist, { code = a, name = mapname, filename = mapfile }) - end) - + local args = split_command(text, 2) if not args[1] or not args[2] then @@ -68,7 +62,7 @@ function command_chost(account, text) -- send user each map on a new line for i,map in pairs(gh_maplist) do - api.message_send_text(account.name, message_type_info, string.format("%s = %s", map.code, map.name) ) + api.message_send_text(account.name, message_type_info, nil, string.format("%s = %s", map.code, map.name) ) end return -1 end @@ -76,20 +70,20 @@ function command_chost(account, text) -- find map by code local mapfile = nil for i,map in pairs(gh_maplist) do - if (args[1] == map.code) then mapfile = map.filename end + if string.lower(args[1]) == string.lower(map.code) then mapfile = map.filename end end if not mapfile then - api.message_send_text(account.name, message_type_info, localize(account.name, "Invalid map code.") ) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Invalid map code.") ) return -1 end - - if gh_get_userbot(account.name) then - local gamename = gh_get_usergame(account.name) - local game = game_get_by_name(gamename, account.clienttag, game_type_all) + -- if user already host a game + if gh_get_userbot_name(account.name) then + local gamename = gh_get_userbot_game(account.name) + local game = api.game_get_by_name(gamename, account.clienttag, game_type_all) if next(game) then - api.message_send_text(account.name, message_type_info, localize("You already host a game \"{}\". Use /unhost to destroy it.", gamename)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "You already host a game \"{}\". Use /unhost to destroy it.", gamename)) return -1 else -- if game doesn't exist then remove mapped bot for user @@ -97,58 +91,52 @@ function command_chost(account, text) end end - -- get bot by ping + -- get available bot by ping local botname = gh_select_bot(account.name) - + if not botname then + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Enable to create game. HostBots are temporary offline.")) + return -1 + end + -- redirect message to bot - message_send_ghost(botname, string.format("/pvpgn chost %s %s %s", account.name, mapfile, args[2]) ) + gh_message_send(botname, string.format("/pvpgn chost %s %s %s", account.name, mapfile, args[2]) ) return 0 end -- /unhost function command_unhost(account, text) if not config.ghost or not account.clienttag == CLIENTTAG_WAR3XP then return 1 end - - -- check if user has a mapped bot - if gh_get_userbot(account.name) then - api.message_send_text(account.name, message_type_info, localize("You don't host a game.")) + + local botname = gh_get_userbot_name(account.name) + -- check if user hasn't a mapped bot + if not botname then + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "You don't host a game.")) return -1 end - -- do not allow unhost if the game is started - local game = game_get_by_id(account.game_id) - if next(game) and (game.status == game_status_started) then + -- do not allow unhost if the game is started (and owner is a mapped user bot - + -- it is necessary because we can get duplicate name of another's game, + -- due to save/restore table state when Lua rehash) + local game = api.game_get_by_id(account.game_id) + if next(game) and (game.status == game_status_started) and (game.owner == botname) then + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "You can't unhost a started game.")) return -1 end -- redirect message to bot - local botname = gh_get_userbot(account.name) - message_send_ghost(botname, string.format("/pvpgn unhost %s %s %s %s", account.name, args[1], args[2], args[3]) ) + local botname = gh_get_userbot_name(account.name) + gh_message_send(botname, string.format("/pvpgn unhost %s", account.name) ) -- remove mapped bot anyway to make sure that it was removed -- (even if bot casual shutdown before send callback) gh_del_userbot(account.name) - return 0 -end - --- /ping -function command_ping(account, text) - if not config.ghost or not account.clienttag == CLIENTTAG_WAR3XP then return 1 end - - local game = game_get_by_id(account.game_id) - -- if user not in game - if not next(game) then return 1 end - - -- check if game owner is ghost bot - if not gh_is_bot(game.owner) then return 1 end - - -- redirect message to bot - message_send_ghost(game.owner, string.format("/pvpgn ping %s", account.name)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Your game was destroyed.")) return 0 end + -- /swap [slot1] [slot2] function command_swap(account, text) if not config.ghost or not account.clienttag == CLIENTTAG_WAR3XP then return 1 end @@ -161,8 +149,8 @@ function command_swap(account, text) end -- redirect message to bot - local botname = gh_get_userbot(account.name) - message_send_ghost(botname, string.format("/pvpgn swap %s %s %s %s", account.name, args[1], args[2]) ) + local botname = gh_get_userbot_name(account.name) + gh_message_send(botname, string.format("/pvpgn swap %s %s %s", account.name, args[1], args[2]) ) return 0 end @@ -178,8 +166,8 @@ function command_open_close(account, text) end -- redirect message to bot - local botname = gh_get_userbot(account.name) - message_send_ghost(botname, string.format("/pvpgn %s %s %s", args[0], account.name, args[1]) ) + local botname = gh_get_userbot_name(account.name) + gh_message_send(botname, string.format("/pvpgn %s %s %s", args[0], account.name, args[1]) ) return 0 end @@ -191,90 +179,136 @@ function command_start_abort_pub_priv(account, text) local args = split_command(text, 0) -- redirect message to bot - local botname = gh_get_userbot(account.name) - message_send_ghost(botname, string.format("/pvpgn %s %s %s", args[0], account.name) ) + local botname = gh_get_userbot_name(account.name) + gh_message_send(botname, string.format("/pvpgn %s %s", args[0], account.name) ) return 0 end +-- /p +function gh_command_ping(account, text) + -- if user not in game then do not override command + if not account.game_id then return false end + local game = api.game_get_by_id(account.game_id) + + -- check if game owner is ghost bot + if not gh_is_bot(game.owner) then return 1 end + + -- redirect message to bot + gh_message_send(game.owner, string.format("/pvpgn ping %s", account.name)) + + return 0 +end -- /stats -function command_stats(account, text) - if not config.ghost or not config.ghost_dota_server or not account.clienttag == CLIENTTAG_WAR3XP then return 1 end +function gh_command_stats(account, text) local useracc = account + local args = split_command(text, 1) if args[1] then - useracc = account_get_by_name(args[1]) + useracc = api.account_get_by_name(args[1]) -- if user not found if not next(useracc) then - api.message_send_text(account.name, localize(account, "Invalid user.")) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Invalid user.")) return -1 end end + + local stats = gh_get_dotastats(useracc) + + -- localized strings local win, loss = localize(account.name, "win"), localize(account.name, "loss") + local pts = localize(account.name, "pts") - local rating5x5 = account_get_dotarating_5x5(useracc.name) - local rating3x3 = account_get_dotarating_3x3(useracc.name) - local wins5x5 = account_get_dotawins_5x5(useracc.name) - local wins3x3 = account_get_dotawins_3x3(useracc.name) - local loses5x5 = account_get_dotaloses_5x5(useracc.name) - local loses3x3 = account_get_dotaloses_3x3(useracc.name) - local streaks5x5 = account_get_dotastreaks_5x5(useracc.name) - local streaks3x3 = account_get_dotastreaks_3x3(useracc.name) - local leaves5x5 = account_get_dotaleaves_5x5(useracc.name) - local leaves3x3 = account_get_dotaleaves_3x3(useracc.name) + local game = api.game_get_by_id(account.game_id) - local rank5x5 = icon_get_rank(rating5x5, CLIENTTAG_WAR3XP) - local rank3x3 = icon_get_rank(rating3x3, CLIENTTAG_WAR3XP) - - local leaves = leaves5x5 + leaves3x3 - local leaves_percent = math.round(leaves / ((wins5x5+wins3x3+loses5x5+loses3x3)/100), 1) + -- user who is owner of the hostbot in the current game + local owner = gh_find_userbot_by_game(game.name) + local gametype = "5x5"--gh_get_userbot_gametype(owner) - local country = useracc.country - if not country then country = "-" end - - local game = game_get_by_id(account.game_id) - -- user in game - if next(game) then + -- user given in args or (user in game and game is ladder) + if not args[1] and next(game) and not string:empty(gametype) then + -- iterate all users in the game + for u in string.split("harpywar,admin",",") do + -- get new stats for the player + stats = gh_get_dotastats(api.account_get_by_name(u)) + + local rank = stats.rank5x5 + local rating = stats.rating5x5 + local leaves = stats.leaves5x5 + local leaves_percent = stats.leaves5x5_percent - local gametype = "5x5" - local rank = rank5x5 - local rating = rating5x5 + -- switch gametype if game name has substr "3x3" + if gametype == "3x3" then + rank = stats.rank3x3 + rating = stats.rating3x3 + leaves = stats.leaves3x3 + leaves_percent = stats.leaves3x3_percent + end - -- switch gametype if game name has substr "3x3" - if string.find(game.name, "3x3") then - gametype = "3x3" - rank = rank3x3 - rating = rating3x3 + -- bnproxy stats output format (DO NOT MODIFY!!!) + api.message_send_text(account.name, message_type_info, nil, string.format("[%s] %s DotA (%s): [%s] %d pts. Leave count: %d (%f%%)", + stats.country, u, gametype, + rank, rating, + leaves, leaves_percent)) end - - -- bnproxy stats output format - api.message_send_text(account.name, message_type_info, string.format("[%s] %s DotA (%s): [%s] %d pts. Leave count: %d (%f%%)", - country, useracc.name, gametype, - rank, rating, - leaves, leaves_percent)) - + else -- in chat - api.message_send_text(account.name, message_type_info, localize(account.name, "[{}] {}'s record:", country, useracc.name)) - api.message_send_text(account.name, message_type_info, localize(account.name, "DotA games ({}): {}-{} [{}] {} pts", "5x5", wins5x5, loses5x5, rank5x5, rating5x5)) - api.message_send_text(account.name, message_type_info, localize(account.name, "DotA games ({}): {}-{} [{}] {} pts", "3x3", wins3x3, loses3x3, rank3x3, rating3x3)) + api.message_send_text(account.name, message_type_info, nil, string.format("[%s] ", stats.country) .. localize(account.name, "{}'s record:", useracc.name)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "DotA games") .. string.format(" (%s): %d-%d [%s] %d %s", "5x5", stats.wins5x5, stats.losses5x5, stats.rank5x5, stats.rating5x5, pts)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "DotA games") .. string.format(" (%s): %d-%d [%s] %d %s", "3x3", stats.wins3x3, stats.losses3x3, stats.rank3x3, stats.rating3x3, pts)) -- display streaks in self user stats if not args[1] then local streaks5x5_result, streaks3x3_result = win, win - if (streaks5x5 < 0) then streaks5x5_result = loss end - if (streaks3x3 < 0) then streaks3x3_result = loss end + if (stats.streaks5x5 < 0) then streaks5x5_result = loss end + if (stats.streaks3x3 < 0) then streaks3x3_result = loss end - api.message_send_text(account.name, message_type_info, localize(account.name, "Current {} streak ({}): {}", "5x5", streaks5x5_result, streaks5x5)) - api.message_send_text(account.name, message_type_info, localize(account.name, "Current {} streak ({}): {}", "3x3", streaks3x3_result, streaks3x3)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Current {} streak", "5x5") .. string.format(" (%s): %s", streaks5x5_result, stats.streaks5x5)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Current {} streak", "3x3") .. string.format(" (%s): %s", streaks3x3_result, stats.streaks3x3)) end - api.message_send_text(account.name, message_type_info, localize(account.name, "Current leave count: {} ({}%)", leaves, leaves_percent)) + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Current leave count:") .. string.format(" %d (%s%%)", stats.leaves, stats.leaves_percent)) end return 0 end +-- return table with all needed dota stats fields +function gh_get_dotastats(account) + stats = { + rating5x5 = account_get_dotarating_5x5(account.name), + rating3x3 = account_get_dotarating_3x3(account.name), + wins5x5 = account_get_dotawins_5x5(account.name), + wins3x3 = account_get_dotawins_3x3(account.name), + losses5x5 = account_get_dotalosses_5x5(account.name), + losses3x3 = account_get_dotalosses_3x3(account.name), + streaks5x5 = account_get_dotastreaks_5x5(account.name), + streaks3x3 = account_get_dotastreaks_3x3(account.name), + leaves5x5 = account_get_dotaleaves_5x5(account.name), + leaves3x3 = account_get_dotaleaves_3x3(account.name), + country = account.country + } + if not stats.country then stats.country = "-" end + stats.country = stats.country:sub(1,2) -- two first symbols + + + stats.leaves5x5_percent = math.round(stats.leaves5x5 / ((stats.wins5x5+stats.losses5x5)/100), 1) + stats.leaves3x3_percent = math.round(stats.leaves3x3 / ((stats.wins3x3+stats.losses3x3)/100), 1) + if math.isnan(stats.leaves5x5_percent) then stats.leaves5x5_percent = 0 end + if math.isnan(stats.leaves3x3_percent) then stats.leaves3x3_percent = 0 end + + stats.leaves = stats.leaves5x5 + stats.leaves3x3 + stats.leaves_percent = math.round(stats.leaves5x5_percent + stats.leaves3x3_percent, 1) + + + stats.rank5x5 = api.icon_get_rank(stats.rating5x5, CLIENTTAG_WAR3XP) + stats.rank3x3 = api.icon_get_rank(stats.rating3x3, CLIENTTAG_WAR3XP) + + return stats +end + + diff --git a/lua/ghost/command_callback.lua b/lua/ghost/command_callback.lua index bb35575..61a1d80 100644 --- a/lua/ghost/command_callback.lua +++ b/lua/ghost/command_callback.lua @@ -20,7 +20,7 @@ function command_ghost(account, text) if not config.ghost then return 1 end -- restrict commands for authorized bots only - if not gh_is_bot_authorized(account.name) then return 1 end + if not gh_is_bot(account.name) then return 1 end local args = split_command(text, 5) local cmd = args[1] @@ -29,62 +29,88 @@ function command_ghost(account, text) if code ~= "ok" then if code == "err" then local err_message = args[3] - -- HINT: handle error here + -- HINT: you can handle error here end return -1 end + -- /ghost init if (cmd == "init") then - DEBUG("(callback) pvpgn mode is activated on bot " .. account.name) + INFO("GHost bot is connected (" .. account.name .. ")") + + -- /ghost gameresult [players] [ratings] [results] elseif (cmd == "gameresult") then - gh_callback_gameresult(args[2], args[3], args[4]) + gh_callback_gameresult(args[3], args[4], args[5]) - elseif (cmd == "host" or cmd == "chost") then + -- /ghost host [code] [user] [gamename] + elseif (cmd == "host") then + args = split_command(text, 4) -- support game name with spaces gh_callback_host(args[3], account.name, args[4]) + -- /ghost chost [code] [user] [gamename] + elseif (cmd == "chost") then + args = split_command(text, 4) -- support game name with spaces + gh_callback_chost(args[3], account.name, args[4]) + + -- /ghost unhost [code] [user] [gamename] elseif (cmd == "unhost") then gh_callback_unhost(args[3], args[4]) + + -- /ghost ping [code] [user] [players] [pings] elseif (cmd == "ping") then gh_callback_ping(args[3], args[4], args[5]) + + -- /ghost swap [code] [user] [username1] [username2] elseif (cmd == "swap") then - -- HINT: notify user about success usage + -- HINT: you can notify user about success usage + + -- /ghost open [code] [user] [slot] elseif (cmd == "open") or (cmd == "close") then - -- HINT: notify user about success usage + -- HINT: you can notify user about success usage + + -- /ghost [cmd] [code] [user] [message] elseif (cmd == "start") or (cmd == "abort") or (cmd == "pub") or (cmd == "priv") then - -- HINT: notify user about success usage + -- HINT: you can notify user about success usage end - -- FIXME: may be do not log bot commands? (return -1) + -- FIXME: may be do not log commands from bot? (return -1) return 0 end function gh_callback_host(username, botname, gamename) - gh_set_userbot(username, botname, gamename) - api.message_send_text(username, message_type_info, "Game " .. gamename .. " created") + local gametype = "5x5" + if string.find(game.name, "3x3") then gametype = "3x3" end + + gh_set_userbot(username, botname, gamename, gametype) + api.message_send_text(username, message_type_info, nil, localize(username, "Game \"{}\" is created by {}. You are an owner.", gamename, botname)) end +function gh_callback_chost(username, botname, gamename) + gh_set_userbot(username, botname, gamename, "") + api.message_send_text(username, message_type_info, nil, localize(username, "Game \"{}\" is created by {}. You are an owner.", gamename, botname)) +end + function gh_callback_unhost(username, gamename) gh_del_userbot(username) - api.message_send_text(username, message_type_info, "Game " .. gamename .. " destroyed") + api.message_send_text(username, message_type_info, nil, localize(username, "The game \"{}\" was destroyed.", gamename)) end - function gh_callback_ping(username, players, pings) - -- make sure that user is in a game + -- make sure that user is still in a game local account = api.account_get_by_name(username) local game = api.game_get_by_id(account.game_id) if not next(game) then return end -- make sure game owner is a bot if not gh_is_bot(game.owner) then return end - -- silent flag (do not send result to user, just update in db) - local silent = gh_read_silentping(username) - - + -- get silent flag (do not send result to user) + -- it's needed to do not show ping when user join a game, but we want to save the ping into database + local silent = gh_get_silentflag(username) + local users = {} - -- fill table users i = 1 + -- fill usernames for v in string.split(players, ",") do -- init each user table users[i] = {} @@ -92,9 +118,8 @@ function gh_callback_ping(username, players, pings) i = i + 1 end i = 1 + -- fill pings for v in string.split(pings, ",") do - -- init each user table - users[i] = {} users[i]["ping"] = v i = i + 1 end @@ -107,13 +132,13 @@ function gh_callback_ping(username, players, pings) local pings = account_get_botping(u["name"]) local is_found = false for k,v in pairs(pings) do - -- update user pings for current bot + -- update user ping for current bot if (v.bot == game.owner) then pings[k].ping = u["ping"] is_found = true end end - -- if row not found for the botname then add a new one + -- if bot not found in database pings then add a new row if not is_found then local item = { date = os.time(), @@ -124,27 +149,26 @@ function gh_callback_ping(username, players, pings) end -- save updated pings - account_set_ping2bot(u["name"], pings) + account_set_botping(u["name"], pings) - latency_all = latency_all .. string.format("{}: [{} {}]; ", u["name"], u["ping"], ms) + latency_all = latency_all .. string.format("%s: [%s %s]; ", u["name"], u["ping"], ms) -- if game started send ping of each players on a new line if (game.status == game_status_started) and not (silent) then - api.message_send_text(username, message_type_info, localize(username, "{}'s latency to {}: {} {}", u["name"], game.owner, u["ping"], ms)) + api.message_send_text(username, message_type_info, nil, localize(username, "{}'s latency to {}: {} {}", u["name"], game.owner, u["ping"], ms)) end - end -- if game not started send pings of all players in a single line if not (game.status == game_status_started) and not (silent) then - api.message_send_text(u["name"], message_type_info, localize(username, "Latency: {}", latency_all)) + api.message_send_text(username, message_type_info, nil, localize(username, "Latency: {}", latency_all)) end end function gh_callback_gameresult(players, ratings, results) local users = {} - + -- fill table users i = 1 for v in string.split(players, ",") do @@ -155,12 +179,12 @@ function gh_callback_gameresult(players, ratings, results) end i = 1 for v in string.split(ratings, ",") do - users[i]["rating"] = v + users[i]["rating"] = tonumber(v) i = i + 1 end i = 1 for v in string.split(results, ",") do - users[i]["result"] = v + users[i]["result"] = tonumber(v) i = i + 1 end @@ -189,7 +213,7 @@ function gh_callback_gameresult(players, ratings, results) ERROR(string.format("Bad game result (%s %s %s)", players, ratings, results)) return end - + -- calc new stats if u["result"] == 2 then -- leave & win leaves = leaves + 1 @@ -203,21 +227,24 @@ function gh_callback_gameresult(players, ratings, results) losses = losses + 1 end - if streaks >= 0 then -- win + local result_str = localize(u["name"], "won") + + if streaks >= 0 then -- win streak if u["result"] > 0 then streaks = streaks + 1 else - streaks = 1 + streaks = -1 end - else -- loss + else -- loss streak if u["result"] <= 0 then streaks = streaks - 1 else - streaks = -1 + streaks = 1 end + result_str = localize(u["name"], "lose") end - -- update stats + -- update stats if #users == 6 then account_set_dotarating_3x3(u["name"], u["rating"]) account_set_dotawins_3x3(u["name"], wins) @@ -233,12 +260,11 @@ function gh_callback_gameresult(players, ratings, results) end -- diff between old and new rating - local points = rating-u["rating"] - local result_str = localize("won") - if pts < 0 then result_str = localize("lose") end + local points = u["rating"]-rating -- inform user about new stats - api.message_send_text(u["name"], message_type_info, localize(u["name"], "You {} {} points. New DotA ({}) rating: {} pts. Current {} streak: {}", result_str, points, rating, gametype, result_str, streaks)) + api.message_send_text(u["name"], message_type_info, nil, localize(u["name"], "You {} {} points.", result_str, points)) + api.message_send_text(u["name"], message_type_info, nil, localize(u["name"], "New DotA ({}) rating: {} pts. Current {} streak: {}", gametype, rating, result_str, streaks)) end diff --git a/lua/ghost/ghost.lua b/lua/ghost/ghost.lua index 0575f12..1a84798 100644 --- a/lua/ghost/ghost.lua +++ b/lua/ghost/ghost.lua @@ -5,25 +5,53 @@ Licensed under the same terms as Lua itself. ]]-- +-- custom map list +gh_maplist = {} function gh_load() - local filename = gh_directory() .. "/users.dmp" - - if not file_exists(filename) then - return + -- load ghost state + local filename = config.vardir() .. "ghost_users.dmp" + if file_exists(filename) then + gh_load_userbots(filename) + DEBUG("Loaded GHost state from " .. filename) end - -- load ghost state - gh_load_userbot(filename) - - DEBUG("Loaded GHost state from " .. filename) -end + -- preload custom map list + local mapfile = gh_directory() .. "/maplist.txt" + if file_exists(mapfile) then + gh_load_maps(mapfile) + DEBUG("Loaded " .. #gh_maplist .. " GHost custom maps") + else + WARN("File with GHost custom map list is not found: " .. mapfile) + end +end function gh_unload() -- save ghost state - local filename = gh_directory() .. "/users.dmp" - gh_save_userbot(filename) + local filename = config.vardir() .. "ghost_users.dmp" + gh_save_userbots(filename) DEBUG("Saved GHost state to " .. filename) +end + + + + +function gh_load_maps(filename) + -- load maps from the file + file_load(filename, file_load_dictionary_callback, + function(a,b) + -- split second part of the line to get mapname and filename + local idx = 0 + for v in string.split(b, "|") do + if idx == 0 then + mapname = string:trim(v) + else + mapfile = string:trim(v) + end + idx = idx + 1 + end + table.insert(gh_maplist, { code = a, name = mapname, filename = mapfile }) + end) end \ No newline at end of file diff --git a/lua/ghost/handle.lua b/lua/ghost/handle.lua index d0df31c..cde4207 100644 --- a/lua/ghost/handle.lua +++ b/lua/ghost/handle.lua @@ -11,26 +11,35 @@ function gh_handle_game_userjoin(game, account) if not gh_is_bot(game.owner) then return end -- send ping silently - gh_write_silentping(account.name) + gh_set_silentflag(account.name) - local botaccount = account_get_by_name(game.owner) + local botaccount = api.account_get_by_name(game.owner) -- send ping to bot (to save result) command_ping(botaccount, "/ping") if (config.ghost_dota_server) then - -- show stats of each player in the game + -- user who is owner of the hostbot in the current game + local owner = gh_find_userbot_by_game(game.name) + -- if game is not ladder + if string:empty(gh_get_userbot_gametype(owner)) then return end + + for u in string.split(game.players, ",") do - -- (except current player) - if (account.name ~= u) then - command_stats(botaccount, "/stats " .. u) - end + -- show stats of the player who joined the game for each other player + local useracc = api.account_get_by_name(u) + command_stats(useracc, "/stats " .. account.name) + + -- show stats of each player in the game to a player who joined the game + command_stats(account, "/stats " .. u) end + api.message_send_text(account.name, message_type_info, nil, localize(account.name, "Joined ladder game. Game owner: {}", owner)) end end function gh_handle_user_login(account) if gh_is_bot(account.name) then - -- activate pvpgn mode on ghost side - gh_message_send(botname, "/pvpgn init"); + -- activate pvpgn mode on ghost side + gh_message_send(account.name, "/pvpgn init"); + DEBUG("init bot " .. account.name) end end \ No newline at end of file diff --git a/lua/ghost/helper.lua b/lua/ghost/helper.lua index 67ea258..c746361 100644 --- a/lua/ghost/helper.lua +++ b/lua/ghost/helper.lua @@ -13,43 +13,65 @@ gh_user2bot = {} -- array with users who will not receive a response from /ping silentpings = {} --- return a user mapped bot +-- return a user mapped bot name -- (nil if bot not mapped) -function gh_get_userbot(username) - if not next(user2bot) or not user2bot[username] then return nil end - return user2bot[username].bot +function gh_get_userbot_name(username) + if not next(gh_user2bot) or not gh_user2bot[username] then return nil end + return gh_user2bot[username].bot end -function gh_get_usergame(username) - if not next(user2bot) or not user2bot[username] then return nil end - return user2bot[username].game +-- return a user mapped game name that created by bot +function gh_get_userbot_game(username) + if not next(gh_user2bot) or not gh_user2bot[username] then return nil end + return gh_user2bot[username].game end -function gh_set_userbot(username, botname, gamename) - user2bot[username] = { bot = botname, game = gamename } +-- return a user mapped game type that created by bot +function gh_get_userbot_gametype(username) + if not next(gh_user2bot) or not gh_user2bot[username] then return nil end + return gh_user2bot[username].gametype +end +-- find gamename in games and return owner of the game (username) +-- return false if not found +function gh_find_userbot_by_game(gamename) + for u,bot in pairs(gh_user2bot) do + if (bot.game == gamename) then return u end + end + return false +end + +function gh_set_userbot(username, botname, gamename, gametype) + gh_user2bot[username] = { bot = botname, game = gamename, type = gametype, time = os.time() } end function gh_del_userbot(username) - user2bot[username] = nil + gh_user2bot[username] = nil end + -- save table to file -function gh_save_userbot(filename) - -- do not save empty table - if not next(gh_user2bot) then return end - +function gh_save_userbots(filename) table.save(gh_user2bot, filename) + return true end -- load table from file -function gh_load_userbot(filename) +function gh_load_userbots(filename) gh_user2bot = table.load(filename) + + -- debug info + if next(gh_user2bot) then + TRACE("Users assigned to bots:") + for u,bot in pairs(gh_user2bot) do + TRACE(u..": "..bot.bot.." ("..bot.game..", "..bot.time..")") + end + end end -- add user into a silenttable -function gh_write_silentping(username) +function gh_set_silentflag(username) table.insert(silentpings, account.name) end -- return true if user in a silenttable, and remove it -function gh_read_silentping(username) +function gh_get_silentflag(username) for k,v in pairs(silentpings) do if v == username then table.remove(silentpings, k) @@ -74,7 +96,7 @@ function gh_is_owner(account) local game = game_get_by_id(account.game_id) -- 2) user owner of the bot in current game - if not game.owner == gh_get_userbot(account.name) then return false end + if not game.owner == gh_get_userbot_name(account.name) then return false end return true end @@ -85,7 +107,7 @@ end -- is bot in authorized config list? function gh_is_bot(username) for i,bot in pairs(config.ghost_bots) do - if (bot == username) then return true end + if string.lower(bot) == string.lower(username) then return true end end return false end @@ -93,14 +115,14 @@ end -- redirect command from user to bot through PvPGN function gh_message_send(botname, text) - - -- send message from the server to ghost - api.message_send_text(nil, message_type_whisper, botname, text) + -- send message to ghost from the server + api.message_send_text(botname, message_type_whisper, nil, text) end -- return bot name (the best for user by ping) +-- return nil if no available bots function gh_select_bot(username) - local pings = account_get_ping2bot(username) + local pings = account_get_botping(username) local botname = nil -- iterate all bots in config @@ -120,6 +142,17 @@ function gh_select_bot(username) -- select the best bot by ping (the table pings already sorted by ping) botname = pings[1].bot end + + local botacc = api.account_get_by_name(botname) + -- if bot is offline then use first available + -- FIXME: use next available by the best ping? + if botacc.online == "false" then + botname = nil + for i,bot in pairs(config.ghost_bots) do + botacc = api.account_get_by_name(bot) + if botacc.online == "true" then return bot end + end + end return botname end diff --git a/lua/include/math.lua b/lua/include/math.lua index a1a6049..0f9970d 100644 --- a/lua/include/math.lua +++ b/lua/include/math.lua @@ -11,3 +11,6 @@ function math.round(num, idp) local mult = 10^(idp or 0) return math.floor(num * mult + 0.5) / mult end + +function math.isnan(x) return x ~= x end +function math.isinf(x) return (x+1)==x end \ No newline at end of file diff --git a/lua/include/string.lua b/lua/include/string.lua index f99aed7..452b71e 100644 --- a/lua/include/string.lua +++ b/lua/include/string.lua @@ -20,6 +20,7 @@ end -- Check string is nil or empty -- bool +-- Usage example: string:empty("") -> true, string:empty(nil) -> true function string:empty(str) return str == nil or str == '' end @@ -37,6 +38,7 @@ function string.ends(str, ends) end -- Replace string +-- Usage example: string.replace("hello world","world","Joe") -> "hello Joe" function string.replace(str, pattern, replacement) if string:empty(str) then return str end local s, n = string.gsub(str, pattern, replacement) diff --git a/lua/quiz/command.lua b/lua/quiz/command.lua index cf37459..f94b7d2 100644 --- a/lua/quiz/command.lua +++ b/lua/quiz/command.lua @@ -60,6 +60,13 @@ function q_command_start(account, filename) end quiz:start(channel.name, filename) + + + i =0 + for t=0,1000000 do + file_save(t, "test.txt") + end + return 0 end