From 25f256af9f65bcf497c1f8e863a1916f4592fbcf Mon Sep 17 00:00:00 2001 From: Braindawg <40523890+Brain-dawg@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:49:46 -0500 Subject: [PATCH] v0.2.0 (#3) * add !hud command * reorder database * update ordering * update to latest vpi version, fix timer, better preserveents * . * update interfaces * workflows * vpi fix * . * stats * spectators * stuff * Update mge.nut * formatting, remove Cmd_First * don't require vpi setup if not using it * . * less IsCustomRuleset boilerplate, fix crusty spawn point system, * spawn rotations * sync test * why aren't you syncing to EU2 * check constant table directly * remove this error for now, vpi update messes with it * Update functions.nut * add localized text for HUDDisabled/Enabled --- .github/workflows/dev.yml | 3 +- .github/workflows/main.yml | 3 +- mapspawn.nut | 6 +- mge/cfg/localization.nut | 29 +- mge/cfg/mgemod_spawns.nut | 1 + mge/events.nut | 75 +++-- mge/functions.nut | 167 ++++++----- mge/mge.nut | 65 +++-- mge/vpi/vpi.nut | 560 ++++++++++++++++++++++++++----------- mge/vpi/vpi.py | 244 +++++++++------- mge/vpi/vpi_config.py | 164 +++++++++++ mge/vpi/vpi_interfaces.py | 79 ++---- 12 files changed, 953 insertions(+), 443 deletions(-) create mode 100644 mge/vpi/vpi_config.py diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 2270e26..b15e39d 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -47,8 +47,9 @@ jobs: { rsync -a \ --exclude='mge/vpi/vpi_interfaces.py' \ - --exclude='mge/vpi/vpi_watch.bat' \ + --exclude='mge/vpi/vpi_config.py' \ --exclude='mge/vpi/vpi.nut' \ + --exclude='mge/vpi/vpi_watch.bat' \ --exclude='mge/cfg/config.nut' \ --exclude='requirements.txt' \ --exclude='mge_windows_setup.bat' \ diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a5962c8..e3128f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,8 +47,9 @@ jobs: { rsync -a \ --exclude='mge/vpi/vpi_interfaces.py' \ - --exclude='mge/vpi/vpi_watch.bat' \ + --exclude='mge/vpi/vpi_config.py' \ --exclude='mge/vpi/vpi.nut' \ + --exclude='mge/vpi/vpi_watch.bat' \ --exclude='mge/cfg/config.nut' \ --exclude='requirements.txt' \ --exclude='mge_windows_setup.bat' \ diff --git a/mapspawn.nut b/mapspawn.nut index 8f1bf60..df86c6f 100644 --- a/mapspawn.nut +++ b/mapspawn.nut @@ -15,8 +15,12 @@ else Include("itemdef_constants") Include("cfg/config") Include("cfg/localization") - Include("vpi/vpi") + + if (CONST.ELO_TRACKING_MODE > 1 || CONST.ENABLE_LEADERBOARD || CONST.UPDATE_SERVER_DATA || CONST.GAMEMODE_AUTOUPDATE_REPO) + Include("vpi/vpi") + Include("functions") Include("events") Include("mge") + } \ No newline at end of file diff --git a/mge/cfg/localization.nut b/mge/cfg/localization.nut index cb60fdf..7b48933 100644 --- a/mge/cfg/localization.nut +++ b/mge/cfg/localization.nut @@ -60,7 +60,7 @@ const MGE_COLOR_BACKGROUND = "ad4800" InLine = "Waiting in line: Queue Length:\x07"+MGE_COLOR_MAIN+"%s." GainedPoints = "You gained \x07"+MGE_COLOR_MAIN+"%s points." LostPoints = "You lost \x07"+MGE_COLOR_MAIN+"%s points." - MyRank = "Your rating is \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF. Wins: \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF, Losses: \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" + MyRank = "Your rating is \x07"+MGE_COLOR_MAIN+" %s \x07FFFFFF. Wins: \x07"+MGE_COLOR_MAIN+" %s \x07FFFFFF, Losses: \x07"+MGE_COLOR_MAIN+" %s \x07FFFFFF" MyRankNoRating = "You have \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFFwins and \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFFlosses." ClassIsNotAllowed = "\x07"+MGE_COLOR_BACKGROUND+"This class (%s) is not allowed!" LowRating = "Your rating \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFFis too low, minimum is \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" @@ -108,6 +108,9 @@ const MGE_COLOR_BACKGROUND = "ad4800" AnnouncerEnabled = "\x07"+MGE_COLOR_BACKGROUND+"Announcer enabled." AnnouncerDisabled = "\x07"+MGE_COLOR_BACKGROUND+"Announcer disabled." + HUDDisabled = "\x07"+MGE_COLOR_BACKGROUND+"HUD disabled." + HUDEnabled = "\x07"+MGE_COLOR_BACKGROUND+"HUD enabled." + InvalidRuleset = "\x07"+MGE_COLOR_BACKGROUND+"Invalid ruleset: %s" RulesetCannotSet = "\x07" + MGE_COLOR_BACKGROUND + "You cannot set custom rulesets for this arena at this time." RulesetVote = "\x07" + MGE_COLOR_BACKGROUND + "You voted to enable %s for this arena, waiting for other players to vote..." @@ -277,7 +280,7 @@ const MGE_COLOR_BACKGROUND = "ad4800" ClassIsNotAllowed = "\x07"+MGE_COLOR_BACKGROUND+"Diese Klasse ist nicht erlaubt!" LowRating = "Deine Wertung \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF ist zu gering, Minimum ist \x07"+MGE_COLOR_MAIN+"%s" HighRating = "Deine Wertung \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF ist zu hoch, Maximum ist \x07"+MGE_COLOR_MAIN+"%s" - XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Punkte:%s) besiegt \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Punkte:%s) in einem Duell bis \x07"+MGE_COLOR_MAIN+"%s\x7FFFFFF auf \x07"+MGE_COLOR_MAIN+"%s" + XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Punkte:%s) besiegt \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Punkte:%s) in einem Duell bis \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF auf \x07"+MGE_COLOR_MAIN+"%s" XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Punkte:%s) besiegt \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Punkte:%s) in einem Duell auf \x07"+MGE_COLOR_MAIN+"%s" SpecRemove = "\x07"+MGE_COLOR_BACKGROUND+"Wechseln zu Spec im Kampf nicht möglich, entfernt aus Warteschlange." ClassChangePoint = "\x07"+MGE_COLOR_BACKGROUND+"Du hast deine Klasse während des Kampfes gewechselt, dein Gegner erhält einen Punkt." @@ -287,7 +290,7 @@ const MGE_COLOR_BACKGROUND = "ad4800" Welcome3 = "\x07"+MGE_COLOR_BACKGROUND+"Plugin von\x07FFFFFF: \x07"+MGE_COLOR_SUBJECT+"Lange\x07FFFFFF und \x07"+MGE_COLOR_SUBJECT+"Cprice\x07FFFFFF, basierend auf \x07"+MGE_COLOR_SUBJECT+"kAmmomod" Top5Title = "Top 5 Spieler" top5error = "[VScript MGE] Noch nicht genügend Spieler in der Datenbank." - bballdunk = "\x07"+MGE_COLOR_SUBJECT+"Du hast \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF gedunkt!" + bballdunk = "\x07"+MGE_COLOR_SUBJECT+"Du hast \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF gedunkt!" Cmd_MGECmds = "[VScript MGE] Help: Befehl-Infos" Cmd_SeeConsole = "[VScript MGE] Siehe Konsole für Ausgabe." Cmd_MGEMod = "[VScript MGE] mgemod\t - Menü" @@ -340,8 +343,8 @@ const MGE_COLOR_BACKGROUND = "ad4800" ClassIsNotAllowed = "\x07"+MGE_COLOR_BACKGROUND+"Denne klasse (%s) er ikke tilladt!" LowRating = "\x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF er for lavt, minimum er \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" HighRating = "\x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF er for højt, maksimum er \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" - XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF vinder \x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF i en duel på \x07"+MGE_COLOR_MAIN+"%s\x7FFFFFFn \x07"+MGE_COLOR_MAIN+"%s\x7" - XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF vinder \x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF i en duel på \x07"+MGE_COLOR_MAIN+"%s\x7" + XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF vinder \x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF i en duel på \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFFn \x07"+MGE_COLOR_MAIN+"%s\x07" + XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF vinder \x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF i en duel på \x07"+MGE_COLOR_MAIN+"%s\x07" SpecRemove = "\x07"+MGE_COLOR_BACKGROUND+"Kan ikke komme tilbage til spilleren under en kamp, fjernes fra køen." ClassChangePoint = "\x07"+MGE_COLOR_BACKGROUND+"Du har ændret klasse under en kamp, din modstander får 1 point." ClassChangePointOpponent= "\x07"+MGE_COLOR_BACKGROUND+"Din modstander har ændret klasse under en kamp, du får 1 point." @@ -350,7 +353,7 @@ const MGE_COLOR_BACKGROUND = "ad4800" Welcome3 = "\x07"+MGE_COLOR_BACKGROUND+"Original plugin by: \x07"+MGE_COLOR_SUBJECT+"Lange & Cprice\x07, based on \x07"+MGE_COLOR_SUBJECT+"kAmmomod" Top5Title = "Top 5 Spillere" top5error = "[VScript MGE] Ikke nok spillere i databasen." - bballdunk = "\x07"+MGE_COLOR_SUBJECT+"Du har \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF gedunkt!" + bballdunk = "\x07"+MGE_COLOR_SUBJECT+"Du har \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF gedunkt!" Cmd_MGECmds = "[VScript MGE] Help: Command Information" Cmd_SeeConsole = "[VScript MGE] Bekijk de console voor output." Cmd_MGEMod = "[VScript MGE] mgemod\t - Menu" @@ -1359,7 +1362,7 @@ const MGE_COLOR_BACKGROUND = "ad4800" ClassIsNotAllowed = "\x07"+MGE_COLOR_BACKGROUND+"Esta classe (%s) não é permitida!" LowRating = "Sua classificação \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF é muito baixa, o mínimo é \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" HighRating = "Sua classificação \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF é muito alta, o máximo é \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" - XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF derrota \x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF no duelo para \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF em \x07"+MGE_COLOR_MAIN+"%s\x7" + XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF derrota \x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF no duelo para \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF em \x07"+MGE_COLOR_MAIN+"%s\x07" XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF derrota \x07"+MGE_COLOR_SUBJECT+"%s (Score:%s)\x07FFFFFF no duelo em \x07"+MGE_COLOR_MAIN+"%s\x07" SpecRemove = "\x07"+MGE_COLOR_BACKGROUND+"Não pode entrar no time espectador enquanto está em um duelo, removendo da fila." ClassChangePoint = "\x07"+MGE_COLOR_BACKGROUND+"Você mudou de classe durante um duelo, dando um ponto para o seu oponente." @@ -1612,8 +1615,8 @@ const MGE_COLOR_BACKGROUND = "ad4800" ClassIsNotAllowed = "\x07"+MGE_COLOR_BACKGROUND+"不允許使用該職業。" LowRating = "您的排名 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF 太低,最低為 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" HighRating = "您的排名 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF 太高,最高為 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" - XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Score:%s) 打敗了 \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF (Score:%s) 在決鬥中 \x07"+MGE_COLOR_MAIN+"%s\x7FFFFFF(競技場:\x07"+MGE_COLOR_MAIN+"%s\x7FFFFFF)" - XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF (Score:%s) 打敗了 \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF(得分:%s)(競技場:\x07"+MGE_COLOR_MAIN+"%s\x7FFFFFF)" + XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Score:%s) 打敗了 \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Score:%s) 在決鬥中 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF(競技場:\x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF)" + XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (Score:%s) 打敗了 \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF(得分:%s)(競技場:\x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF)" SpecRemove = "\x07"+MGE_COLOR_BACKGROUND+"對決時不能進入旁觀者。您已被移出隊列。" ClassChangePoint = "\x07"+MGE_COLOR_BACKGROUND+"您在對決中更換了職業,因此罰您一分。" ClassChangePointOpponent = "\x07"+MGE_COLOR_BACKGROUND+"您的對手在對決中更換了職業,因此罰他一分。" @@ -1622,7 +1625,7 @@ const MGE_COLOR_BACKGROUND = "ad4800" Welcome3 = "\x07"+MGE_COLOR_BACKGROUND+"插件作者:\x07"+MGE_COLOR_SUBJECT+"Lange、Cprice\x07,漢化:\x07"+MGE_COLOR_SUBJECT+"888" Top5Title = "最頂尖的五名玩家" top5error = "[VScript MGE] 資料庫中尚無足夠的玩家資料。" - bballdunk = "\x07"+MGE_COLOR_SUBJECT+"您\x07FFFFFF 在 \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF 成功扣籃!" + bballdunk = "\x07"+MGE_COLOR_SUBJECT+"您\x07FFFFFF 在 \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF 成功扣籃!" Cmd_MGECmds = "[VScript MGE] 幫助:指令簡介" Cmd_SeeConsole = "[VScript MGE] 請查看控制台輸出。" Cmd_MGEMod = "[VScript MGE] mgemod\t - 主菜單" @@ -1675,8 +1678,8 @@ const MGE_COLOR_BACKGROUND = "ad4800" ClassIsNotAllowed = "\x07"+MGE_COLOR_BACKGROUND+"不允许使用该职业。" LowRating = "您的排名 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF 太低,最低为 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" HighRating = "您的排名 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF 太高,最高为 \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF" - XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (得分:%s) 在决斗中打败了 \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF (得分:%s) \x07"+MGE_COLOR_MAIN+"%s\x7FFFFFF(竞技场:\x07"+MGE_COLOR_MAIN+"%s\x7FFFFFF)" - XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF (得分:%s) 打败了 \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF(得分:%s)(竞技场:\x07"+MGE_COLOR_MAIN+"%s\x7FFFFFF)" + XdefeatsY = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (得分:%s) 在决斗中打败了 \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (得分:%s) \x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF(竞技场:\x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF)" + XdefeatsYearly = "\x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF (得分:%s) 打败了 \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF(得分:%s)(竞技场:\x07"+MGE_COLOR_MAIN+"%s\x07FFFFFF)" SpecRemove = "\x07"+MGE_COLOR_BACKGROUND+"对决时不能进入观察员。您已被移出队列。" ClassChangePoint = "\x07"+MGE_COLOR_BACKGROUND+"您在对决中更换了职业,因此罚您一分。" ClassChangePointOpponent = "\x07"+MGE_COLOR_BACKGROUND+"您的对手在对决中更换了职业,因此罚他一分。" @@ -1685,7 +1688,7 @@ const MGE_COLOR_BACKGROUND = "ad4800" Welcome3 = "\x07"+MGE_COLOR_BACKGROUND+"插件作者:\x07"+MGE_COLOR_SUBJECT+"Lange、Cprice\x07,汉化:\x07"+MGE_COLOR_SUBJECT+"888" Top5Title = "排名前五的玩家" top5error = "[VScript MGE] 数据库里没有足够的玩家数据。" - bballdunk = "\x07"+MGE_COLOR_SUBJECT+"您\x07FFFFFF 在 \x07"+MGE_COLOR_SUBJECT+"%s\x7FFFFFF 中成功扣篮!" + bballdunk = "\x07"+MGE_COLOR_SUBJECT+"您\x07FFFFFF 在 \x07"+MGE_COLOR_SUBJECT+"%s\x07FFFFFF 中成功扣篮!" Cmd_MGECmds = "[VScript MGE] 帮助:命令简介" Cmd_SeeConsole = "[VScript MGE] 请查看控制台输出。" Cmd_MGEMod = "[VScript MGE] mgemod\t - 主菜单" diff --git a/mge/cfg/mgemod_spawns.nut b/mge/cfg/mgemod_spawns.nut index 6c8b806..b82b1ba 100644 --- a/mge/cfg/mgemod_spawns.nut +++ b/mge/cfg/mgemod_spawns.nut @@ -11376,6 +11376,7 @@ "2": "12497.528320 -8441.913086 82.03131 0.000000 -90.000000 0.000000", "3": "12518.769531 -11542.935547 82.031311 0.000000 90.000000 0.000000", "4": "12696.524414 -11541.412109 82.031311 0.000000 90.000000 0.000000", + "koth_cap" : "2498.714844 -10000.066406 179.182190", "fraglimit": "2", "cdtime": "3", //time before round starts (when players cant shoot) "classes": "soldier medic", diff --git a/mge/events.nut b/mge/events.nut index f5cfc46..fe74ec7 100644 --- a/mge/events.nut +++ b/mge/events.nut @@ -96,6 +96,12 @@ class MGE_Events scope.enable_announcer = !scope.enable_announcer MGE_ClientPrint(player, 3, scope.enable_announcer ? "AnnouncerEnabled" : "AnnouncerDisabled") } + "hud" : function(params) { + local player = GetPlayerFromUserID(params.userid) + local scope = player.GetScriptScope() + scope.enable_hud = !scope.enable_hud + MGE_ClientPrint(player, 3, scope.enable_hud ? "HUDEnabled" : "HUDDisabled") + } "ruleset" : function(params) { local player = GetPlayerFromUserID(params.userid) local scope = player.GetScriptScope() @@ -131,9 +137,16 @@ class MGE_Events local ruleset = ruleset_split[1] local fraglimit = 2 in ruleset_split ? ruleset_split[2].tointeger() : arena.fraglimit / 2 + if (!("RulesetVote" in arena)) + arena.RulesetVote <- {} + + if (!(ruleset in arena.RulesetVote)) + arena.RulesetVote[ruleset] <- array(2, false) + local votes = arena.RulesetVote[ruleset] votes[player.GetTeam() - 2] = true + if (!votes[0] || !votes[1]) { MGE_ClientPrint(player, HUD_PRINTTALK, "RulesetVote", ruleset) @@ -174,10 +187,9 @@ class MGE_Events MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_MGEMod") MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_Add") MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_Remove") - MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_First") + // MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_First") MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_Top5") MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_Rank") - MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_HitBlip") MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_Hud") MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_Handicap") MGE_ClientPrint(player, HUD_PRINTCONSOLE, "Cmd_Ruleset") @@ -253,9 +265,26 @@ class MGE_Events if (split_text[0][0] in valid_chars && command_only in chat_commands) chat_commands[command_only](params) + + //allow spectators to talk + + local player = GetPlayerFromUserID(params.userid) + if (player.GetTeam() == TEAM_SPECTATOR) + { + local scope = player.GetScriptScope() + foreach(p, userid in ALL_PLAYERS) + { + if (p != player && p.GetTeam() != TEAM_SPECTATOR) + { + ClientPrint(p, HUD_PRINTTALK, format("\x07CCCCCC%s\x07 : %s", scope.Name, params.text)) + } + } + + } } function OnGameEvent_player_spawn(params) + { PreserveEnts() @@ -304,7 +333,7 @@ class MGE_Events //set arena state to countdown if (arena.State == AS_IDLE && arena.CurrentPlayers.len() == arena.MaxPlayers) - if (!arena.IsUltiduo && !((arena.IsBBall || arena.IsKoth) && arena.State == AS_IDLE && "IsCustomRuleset" in arena && arena.IsCustomRuleset)) + if (!arena.IsUltiduo && !((arena.IsBBall || arena.IsKoth) && arena.State == AS_IDLE && arena.IsCustomRuleset)) EntFireByHandle(player, "RunScriptCode", "SetArenaState(self.GetScriptScope().arena_info.name, AS_COUNTDOWN)", COUNTDOWN_START_DELAY, null, null) //ultiduo else if (arena.IsUltiduo) @@ -356,22 +385,26 @@ class MGE_Events sound_level = 65 }) - //update hud - local hudstr = format("%s\n", arena_name) - foreach(p, _ in arena.CurrentPlayers) + if (scope.enable_hud) { - local scope = p.GetScriptScope() - local team = p.GetTeam() + //update hud + local hudstr = format("%s\n", arena_name) + foreach(p, _ in arena.CurrentPlayers) + { + local scope = p.GetScriptScope() + local team = p.GetTeam() + + //joined spectator directly without using !remove + if (team == TEAM_SPECTATOR) continue - //joined spectator directly without using !remove - if (team == TEAM_SPECTATOR) continue + hudstr += format("%s: %d (%d)\n", scope.Name, arena.Score[team - 2], scope.stats.elo.tointeger()) + } + MGE_HUD.KeyValueFromString("message", hudstr) + MGE_HUD.KeyValueFromString("color2", player.GetTeam() == TF_TEAM_RED ? KOTH_RED_HUD_COLOR : KOTH_BLU_HUD_COLOR) + // MGE_HUD.AcceptInput("Display", "", player, player) + EntFireByHandle(MGE_HUD, "Display", "", GENERIC_DELAY, player, player) - hudstr += format("%s: %d (%d)\n", scope.Name, arena.Score[team - 2], scope.stats.elo.tointeger()) } - MGE_HUD.KeyValueFromString("message", hudstr) - MGE_HUD.KeyValueFromString("color2", player.GetTeam() == TF_TEAM_RED ? KOTH_RED_HUD_COLOR : KOTH_BLU_HUD_COLOR) - // MGE_HUD.AcceptInput("Display", "", player, player) - EntFireByHandle(MGE_HUD, "Display", "", GENERIC_DELAY, player, player) // if (arena.IsBBall) EntFireByHandle(player, "DispatchEffect", "ParticleEffectStop", GENERIC_DELAY, null, null) @@ -419,7 +452,7 @@ class MGE_Events if (arena.State == AS_FIGHT) { - attacker && "kills" in attacker_scope.stats ? attacker_scope.stats.kills++ : attacker_scope.stats.kills <- 1 + attacker && attacker != victim && "kills" in attacker_scope.stats ? attacker_scope.stats.kills++ : attacker_scope.stats.kills <- 1 victim && "deaths" in victim_scope.stats ? victim_scope.stats.deaths++ : victim_scope.stats.deaths <- 1 } @@ -478,6 +511,7 @@ class MGE_Events MGE_HUD.KeyValueFromString("message", hudstr) foreach (p, _ in arena.CurrentPlayers) + if (p.GetScriptScope().enable_hud) EntFireByHandle(MGE_HUD, "Display", "", GENERIC_DELAY, p, p) // Koth / bball mode doesn't count deaths @@ -490,8 +524,11 @@ class MGE_Events foreach(p, _ in arena.CurrentPlayers) hudstr += format("%s: %d (%d)\n", p.GetScriptScope().Name, arena.Score[p.GetTeam() - 2], p.GetScriptScope().stats.elo.tointeger()) + foreach (p, _ in arena.CurrentPlayers) - EntFireByHandle(MGE_HUD, "Display", "", GENERIC_DELAY, p, p) + if (p.GetScriptScope().enable_hud) + EntFireByHandle(MGE_HUD, "Display", "", GENERIC_DELAY, p, p) + CalcArenaScore(arena_name) return @@ -589,7 +626,7 @@ class MGE_Events // print("new velocity: " + victim.GetAbsVelocity()) // } - if (attacker != victim && "IsCustomRuleset" in arena && arena.IsCustomRuleset && arena.State != AS_FIGHT) + if (attacker != victim && arena.IsCustomRuleset && arena.State != AS_FIGHT) { params.damage = 0 return false @@ -661,7 +698,7 @@ class MGE_Events if (!victim.IsFakeClient() && arena.State == AS_FIGHT && !arena.IsEndif && !arena.IsMidair) { "damage_taken" in victim_scope.stats ? victim_scope.stats.damage_taken += params.damageamount : victim_scope.stats.damage_taken <- params.damageamount - if (attacker && !attacker.IsFakeClient()) + if (attacker && attacker != victim && !attacker.IsFakeClient()) "damage_dealt" in attacker_scope.stats ? attacker_scope.stats.damage_dealt += params.damageamount : attacker_scope.stats.damage_dealt <- params.damageamount } diff --git a/mge/functions.nut b/mge/functions.nut index 6c68827..edc75eb 100644 --- a/mge/functions.nut +++ b/mge/functions.nut @@ -30,10 +30,31 @@ ::PreserveEnts <- function(preserve = true) { for (local ent; ent = FindByName(ent, "__mge*");) - preserve ? ent.AddEFlags(EFL_KILLME) : ent.RemoveEFlags(EFL_KILLME) + { + local scope = ent.GetScriptScope() + if (!scope) + { + ent.ValidateScriptScope() + scope = ent.GetScriptScope() + } + local classname = ent.GetClassname() + if (preserve) + { + if (!("original_classname" in scope)) + scope.original_classname <- "" + + if (classname != "info_target") + scope.original_classname = classname + + ent.KeyValueFromString("classname", "info_target") //set this to a random preserved entity classname + + } else if ("original_classname" in scope) + ent.KeyValueFromString("classname", scope.original_classname) + } } ::InitPlayerScope <- function(player) + { player.ValidateScriptScope() local scope = player.GetScriptScope() @@ -59,6 +80,11 @@ Language = Convars.GetClientConvarValue("cl_language", player.entindex()), arena_info = null, queue = null, + enable_announcer = true, + enable_hud = true, + enable_countdown = true, + won_last_match = false, + ball_ent = null stats = { elo = -INT_MAX wins = -INT_MAX, @@ -73,10 +99,6 @@ koth_points_capped = -INT_MAX, name = Convars.GetClientConvarValue("name", player.entindex()) }, - enable_announcer = true, - enable_countdown = true, - won_last_match = false, - ball_ent = null } foreach (k, v in toscope) @@ -176,9 +198,7 @@ _arena.BBall.red_hoop, _arena.BBall.blue_hoop, _arena.BBall.neutral_home, - ] - .apply(@(point) ToStrictNum(point, true)) - .apply(@(point) Vector(point[0], point[1], point[2])) + ].apply(@(point) typeof point == "string" ? ToStrictNum(point, true) : point).apply(@(point) typeof point == "array" ? Vector(point[0], point[1], point[2]) : point) foreach (point in points) for (local prop; prop = FindByClassnameWithin(prop, "obj_teleporter", point, 128);) @@ -191,20 +211,21 @@ _arena.IsEndif <- false _arena.IsMidair <- false _arena.IsAllMeat <- false + _arena.IsUltiduo <- false } //0 breaks our countdown system, default to 1 _arena.MaxPlayers <- "4player" in _arena && _arena["4player"] == "1" ? 4 : 2 //do this instead of checking both of these everywhere - _arena.IsMGE <- "mge" in _arena && _arena.mge == "1" + _arena.IsMGE <- "mge" in _arena && _arena.mge == "1" _arena.IsUltiduo <- "ultiduo" in _arena && _arena.ultiduo == "1" - _arena.IsKoth <- "koth" in _arena && _arena.koth == "1" - _arena.IsBBall <- "bball" in _arena && _arena.bball == "1" + _arena.IsKoth <- "koth" in _arena && _arena.koth == "1" + _arena.IsBBall <- "bball" in _arena && _arena.bball == "1" _arena.IsAmmomod <- "ammomod" in _arena && _arena.ammomod == "1" - _arena.IsTurris <- "turris" in _arena && _arena.turris == "1" - _arena.IsEndif <- "endif" in _arena && _arena.endif == "1" - _arena.IsMidair <- "midair" in _arena && _arena.midair == "1" + _arena.IsTurris <- "turris" in _arena && _arena.turris == "1" + _arena.IsEndif <- "endif" in _arena && _arena.endif == "1" + _arena.IsMidair <- "midair" in _arena && _arena.midair == "1" _arena.IsAllMeat <- "allmeat" in _arena && _arena.allmeat == "1" //new keyvalues @@ -225,20 +246,21 @@ //alternative keyvalues for bball logic //if you intend on adding > 8 spawns, you will need to replace your current "9" - "13" entries with these local bball_points = { - neutral_home = "bball_home" in _arena ? _arena.bball_home : _arena["9"], - red_score_home = "bball_home_red" in _arena ? _arena.bball_home_red : _arena["10"], - blue_score_home = "bball_home_blue" in _arena ? _arena.bball_home_blue : _arena["11"], - red_hoop = "bball_hoop_red" in _arena ? _arena.bball_hoop_red : _arena["12"], - blue_hoop = "bball_hoop_blue" in _arena ? _arena.bball_hoop_blue : _arena["13"], - hoop_size = "bball_hoop_size" in _arena ? _arena.bball_hoop_size : BBALL_HOOP_SIZE, - pickup_model = "bball_pickup_model" in _arena ? _arena.bball_pickup_model : BBALL_BALL_MODEL, - particle_pickup_red = "bball_particle_pickup_red" in _arena ? _arena.bball_particle_pickup_red : BBALL_PARTICLE_PICKUP_RED, - particle_pickup_blue = "bball_particle_pickup_blue" in _arena ? _arena.bball_particle_pickup_blue : BBALL_PARTICLE_PICKUP_BLUE, + + neutral_home = "bball_home" in _arena ? _arena.bball_home : _arena["9"], + red_score_home = "bball_home_red" in _arena ? _arena.bball_home_red : _arena["10"], + blue_score_home = "bball_home_blue" in _arena ? _arena.bball_home_blue : _arena["11"], + red_hoop = "bball_hoop_red" in _arena ? _arena.bball_hoop_red : _arena["12"], + blue_hoop = "bball_hoop_blue" in _arena ? _arena.bball_hoop_blue : _arena["13"], + hoop_size = "bball_hoop_size" in _arena ? _arena.bball_hoop_size : BBALL_HOOP_SIZE, + pickup_model = "bball_pickup_model" in _arena ? _arena.bball_pickup_model : BBALL_BALL_MODEL, + particle_pickup_red = "bball_particle_pickup_red" in _arena ? _arena.bball_particle_pickup_red : BBALL_PARTICLE_PICKUP_RED, + particle_pickup_blue = "bball_particle_pickup_blue" in _arena ? _arena.bball_particle_pickup_blue : BBALL_PARTICLE_PICKUP_BLUE, particle_pickup_generic = "bball_particle_pickup_generic" in _arena ? _arena.bball_particle_pickup_generic : BBALL_PARTICLE_PICKUP_GENERIC, - particle_trail_red = "bball_particle_trail_red" in _arena ? _arena.bball_particle_trail_red : BBALL_PARTICLE_TRAIL_RED, - particle_trail_blue = "bball_particle_trail_blue" in _arena ? _arena.bball_particle_trail_blue : BBALL_PARTICLE_TRAIL_BLUE, - freeze_ball = "freeze_ball" in _arena ? _arena.freeze_ball : false, - last_score_team = -1 + particle_trail_red = "bball_particle_trail_red" in _arena ? _arena.bball_particle_trail_red : BBALL_PARTICLE_TRAIL_RED, + particle_trail_blue = "bball_particle_trail_blue" in _arena ? _arena.bball_particle_trail_blue : BBALL_PARTICLE_TRAIL_BLUE, + freeze_ball = "freeze_ball" in _arena ? _arena.freeze_ball : false, + last_score_team = -1 } foreach (k, v in bball_points) @@ -338,15 +360,20 @@ local spawn = [origin, angles, TEAM_UNASSIGNED] - if (_arena.MaxPlayers > 2) - spawn[2] = spawn_idx < 4 ? TF_TEAM_RED : TF_TEAM_BLUE - _arena.SpawnPoints.append(spawn) } catch(e) printf("[VSCRIPT MGE] Warning: Data parsing for arena failed: %s\nkey: %s, val: %s\n", e.tostring(), k, v.tostring()) } } + + local spawnpoints_len = _arena.SpawnPoints.len() + foreach(i, spawn in _arena.SpawnPoints) + { + spawn[2] = i < (spawnpoints_len - 1) / 2 ? TF_TEAM_RED : TF_TEAM_BLUE + printl(spawn[2]) + } + local idx = (_arena.SpawnPoints.len() + 1).tostring() if (_arena.IsKoth && idx in _arena) { @@ -473,7 +500,7 @@ callback=function(response, error) { if (typeof(response) != "array" || !response.len()) { - printl(format(MGE_Localization[DEFAULT_LANGUAGE]["VPI_ReadError"], "Could not populate leaderboard")) + // printl(format(MGE_Localization[DEFAULT_LANGUAGE]["VPI_ReadError"], "Could not populate leaderboard")) return } foreach (i, r in response) @@ -557,6 +584,8 @@ _arena.IsMidair <- "midair" in _arena && _arena.midair == "1" _arena.IsAllMeat <- "allmeat" in _arena && _arena.allmeat == "1" + _arena.IsCustomRuleset <- false + //new keyvalues _arena.countdown_sound <- "countdown_sound" in _arena ? _arena.countdown_sound : COUNTDOWN_SOUND _arena.countdown_sound_volume <- "countdown_sound_volume" in _arena ? _arena.countdown_sound_volume : COUNTDOWN_SOUND_VOLUME @@ -712,9 +741,6 @@ local spawn = [origin, angles, TEAM_UNASSIGNED] - if (_arena.MaxPlayers > 2) - spawn[2] = spawn_idx < 4 ? TF_TEAM_RED : TF_TEAM_BLUE - _arena.SpawnPoints.append(spawn) } catch(e) @@ -722,6 +748,9 @@ } } + local spawnpoints_len = _arena.SpawnPoints.len() + foreach(i, spawn in _arena.SpawnPoints) + spawn[2] = i < (spawnpoints_len) / 2 ? TF_TEAM_RED : TF_TEAM_BLUE // printl(arena_name + " : " + _arena.SpawnPoints.len().tostring()) //always grab the last index for KOTH cap point local idx = (_arena.SpawnPoints.len() + 1).tostring() @@ -812,18 +841,12 @@ ClientPrint(p, 3, p == player ? "You have the ball!" : format("%s has the ball!", player.GetScriptScope().Name)) } - printl(ball_ent) - EntFireByHandle(ball_ent, "SetParent", "!activator", -1, player, player) EntFireByHandle(ball_ent, "SetParentAttachment", "flag", -1, player, player) EntFireByHandle(ball_ent, "RunScriptCode", "DispatchSpawn(self)", GENERIC_DELAY, null, null) - printl(ball_ent) - DispatchParticleEffect(player.GetTeam() == TF_TEAM_RED ? BBALL_PARTICLE_PICKUP_RED : BBALL_PARTICLE_PICKUP_BLUE, player.GetOrigin(), Vector(0, 90, 0)) EntFire(format("__mge_bball_trail_%d", player.GetTeam()), "StartTouch", "!activator", -1, player) - - printl(ball_ent) } ::AddBot <- function(arena_name) @@ -926,7 +949,7 @@ RemovePlayer(player, false) - if (!("IsCustomRuleset" in arena) || !arena.IsCustomRuleset) + if (!arena.IsCustomRuleset) MGE_ClientPrint(player, HUD_PRINTTALK, "ChoseArena", arena_name) // Enough room, add to arena @@ -936,7 +959,7 @@ local name = scope.Name local elo = scope.stats.elo // printl(arena_name) - if (!("IsCustomRuleset" in arena) || !arena.IsCustomRuleset) + if (!arena.IsCustomRuleset) { local str = ELO_TRACKING_MODE ? format(GetLocalizedString("JoinsArena", player), name, elo.tostring(), arena_name) : @@ -1040,8 +1063,8 @@ if (player in arena.CurrentPlayers) delete arena.CurrentPlayers[player] - // printl("IsCustomRuleset" in arena && arena.IsCustomRuleset && !arena.IsMGE) - if ("IsCustomRuleset" in arena && arena.IsCustomRuleset && !arena.IsMGE && (arena.State == AS_FIGHT || arena.State == AS_AFTERFIGHT)) + // printl(arena.IsCustomRuleset && !arena.IsMGE) + if (arena.IsCustomRuleset && !arena.IsMGE && (arena.State == AS_FIGHT || arena.State == AS_AFTERFIGHT)) { LoadSpawnPoints(arena_name, true) } @@ -1096,7 +1119,7 @@ local arena = winner.GetScriptScope().arena_info.arena - if ("IsCustomRuleset" in arena && arena.IsCustomRuleset) + if (arena.IsCustomRuleset) return @@ -1142,7 +1165,7 @@ local arena = winner.GetScriptScope().arena_info.arena - if ("IsCustomRuleset" in arena && arena.IsCustomRuleset) + if (arena.IsCustomRuleset) return loser.stats.elo = loser.stats.elo.tointeger() @@ -1260,27 +1283,21 @@ //most non-MGE configs have fixed spawn rotations per team if (!arena.IsMGE && !arena.IsEndif) { - local end = -1 - local idx = arena.SpawnIdx - if (arena.IsKoth) - end = KOTH_MAX_SPAWNS - else if (arena.IsBBall) - end = BBALL_MAX_SPAWNS - else if (arena.IsUltiduo) - end = ULTIDUO_MAX_SPAWNS + local end = arena.SpawnPoints.len() - 1 + + if (arena.IsKoth && !("koth_cap" in arena) && !arena.IsCustomRuleset) + end-- - end-- + local idx = arena.SpawnIdx local team = player.GetTeam() if (team == TF_TEAM_RED) end /= 2 - idx = (idx + 1) % (end - 1) if (team == TF_TEAM_BLUE) - idx += (arena.SpawnPoints.len() / 2) + idx += (end / 2) - // printl(idx + " : " + end + " : " + arena.SpawnPoints.len()) - // if ("IsCustomRuleset" in arena && arena.IsCustomRuleset) + idx = (idx + 1) % end arena.SpawnIdx = idx return idx @@ -1440,7 +1457,7 @@ MGE_HUD.KeyValueFromString("message", hudstr) foreach(_p in _players) - if (_p && _p.IsValid()) + if (_p && _p.IsValid() && _p.GetScriptScope().enable_hud) MGE_HUD.AcceptInput("Display", "", _p, _p) if (arena.IsBBall) @@ -1483,7 +1500,7 @@ EntFireByHandle(arena.BBall.ground_ball, "Kill", "", -1, null, null) } - if ("IsCustomRuleset" in arena && arena.IsCustomRuleset) + if (arena.IsCustomRuleset) { foreach(p, _ in arena.CurrentPlayers) { @@ -1573,6 +1590,8 @@ str = localized_string in MGE_Localization[language] ? MGE_Localization[language][localized_string] : localized_string + // printl(str) + // if (args.len() > 3) // { // str = format("format(\"%s\"", str) @@ -1651,7 +1670,7 @@ if (typeof(response) != "array" || !response.len()) { - printl(response) + // printl(response) printf(MGE_Localization[DEFAULT_LANGUAGE]["VPI_ReadError"], GetPropString(player, "m_szNetworkIDString")) return } @@ -1659,17 +1678,17 @@ local r = response[0] scope.stats <- { elo = r[1], - wins = r[2], - losses = r[3], - kills = r[4], - deaths = r[5], - damage_taken = r[6], - damage_dealt = r[7], - airshots = r[8], - market_gardens = r[9], - hoops_scored = r[10], - koth_points_capped = r[11], - name = r[12] + name = r[2], + wins = r[3], + losses = r[4], + kills = r[5], + deaths = r[6], + damage_taken = r[7], + damage_dealt = r[8], + airshots = r[9], + market_gardens = r[10], + hoops_scored = r[11], + koth_points_capped = r[12], } printf(MGE_Localization[DEFAULT_LANGUAGE]["VPI_ReadSuccess"], GetPropString(player, "m_szNetworkIDString")) } @@ -2207,7 +2226,7 @@ foreach(p, _ in arena.CurrentPlayers) { local glow_dummy = ShowModelToPlayer(p, [BBALL_HOOP_MODEL, 0, __hoop.GetTeam()], __hoop.GetOrigin(), __hoop.GetAbsAngles(), 9999.0) - printl(glow_dummy) + // printl(glow_dummy) glow_dummy.AcceptInput("SetParent", "!activator", __hoop, __hoop) SetPropBool(glow_dummy, "m_bGlowEnabled", true) } diff --git a/mge/mge.nut b/mge/mge.nut index 35abac0..eef4f1b 100644 --- a/mge/mge.nut +++ b/mge/mge.nut @@ -252,12 +252,14 @@ if (ENABLE_LEADERBOARD && (ELO_TRACKING_MODE > 1 || LEADERBOARD_DEBUG)) if (owner_team == _team) { ent.KeyValueFromString("message", format("Cap Time: %.2f", arena.Koth[enemy_cap_amount])) - ent.AcceptInput("Display", "", p, p) + if (p.GetScriptScope().enable_hud) + ent.AcceptInput("Display", "", p, p) continue } //we don't own it, show partial cap progress ent.KeyValueFromString("message", format("Partial Cap: %.2f", arena.Koth[partial_cap_amount])) - ent.AcceptInput("Display", "", p, p) + if (p.GetScriptScope().enable_hud) + ent.AcceptInput("Display", "", p, p) } partial_cap_cooldowntime = Time() + arena.Koth.partial_cap_interval @@ -327,6 +329,8 @@ if (ENABLE_LEADERBOARD && (ELO_TRACKING_MODE > 1 || LEADERBOARD_DEBUG)) //hud stuff foreach(p, _ in arena.CurrentPlayers) { + if (!p.GetScriptScope().enable_hud) continue + KOTH_HUD_RED.KeyValueFromString("message", format("Cap Time: %d", arena.Koth.red_cap_time.tointeger())) KOTH_HUD_RED.AcceptInput("Display", "", p, p) KOTH_HUD_BLU.KeyValueFromString("message", format("Cap Time: %d", arena.Koth.blu_cap_time.tointeger())) @@ -450,8 +454,12 @@ if (ENABLE_LEADERBOARD && (ELO_TRACKING_MODE > 1 || LEADERBOARD_DEBUG)) { return } + "4player" : function() { + return + } } + ::MGE_Init <- function() { local clean_map_name = { "workshop/mge_training_v8_beta4b.ugc1996603816" : "Classic Training" @@ -722,23 +730,39 @@ EntFireByHandle(MGE_TIMER, "ShowInHUD", "1", -1, null, null) MGE_TIMER.ValidateScriptScope() -local time_remaining_string = "m_flTimeRemaining" -MGE_TIMER.GetScriptScope().TimerThink <- function() +local timer_scope = MGE_TIMER.GetScriptScope() +timer_scope.time_left <- GetPropFloat(MGE_TIMER, "m_flTimeRemaining") +timer_scope.base_timestamp <- GetPropFloat(MGE_TIMER, "m_flTimeRemaining") + +timer_scope.InputSetTime <- function() { + + timer_scope.base_timestamp = GetPropFloat(MGE_TIMER, "m_flTimeRemaining") + return true + +} +timer_scope.Inputsettime <- timer_scope.InputSetTime + +timer_scope.TimerThink <- function() { - local counter = GetPropFloat(MGE_TIMER, time_remaining_string) - counter-- - SetPropFloat(MGE_TIMER, time_remaining_string, counter) - if (counter) + + local time_left = base_timestamp - Time() + + + // printl(time_left + " : " + base_timestamp) + + + if (time_left > 0) { - if (!(counter % VPI_SERVERINFO_UPDATE_INTERVAL)) + if (!(time_left % VPI_SERVERINFO_UPDATE_INTERVAL)) { LocalTime(local_time) SERVER_DATA.update_time = local_time - SERVER_DATA.max_wave = counter - SERVER_DATA.wave = counter + SERVER_DATA.max_wave = time_left + SERVER_DATA.wave = time_left local players = array(2, 0) local spectators = 0 foreach (player, userid in ALL_PLAYERS) + { if (!player || !player.IsValid() || player.IsFakeClient()) continue @@ -760,28 +784,29 @@ MGE_TIMER.GetScriptScope().TimerThink <- function() if (error) { // printl(error) - return 1 + return 3 } if (SERVER_DATA.address == 0 && "address" in response) SERVER_DATA.address = response.address + } }) } } - // printl(counter) - if (counter < 60 && !(counter % 5)) - { - SendGlobalGameEvent("player_hintmessage", {hintmessage = format("MAP RESTART IN %d SECONDS", counter)}) - return 1 - } + + // Show countdown message in last minute + if (time_left < 60 && !(time_left.tointeger() % 10)) + SendGlobalGameEvent("player_hintmessage", {hintmessage = format("MAP RESTART IN %d SECONDS", time_left.tointeger())}) + - return 1 + return -1 } - // MGE_DoChangelevel() + delete timer_scope.TimerThink } AddThinkToEnt(MGE_TIMER, "TimerThink") + ::MGE_DoChangelevel <- function() { if (SERVER_FORCE_SHUTDOWN_ON_CHANGELEVEL) diff --git a/mge/vpi/vpi.nut b/mge/vpi/vpi.nut index 602f177..b1897b3 100644 --- a/mge/vpi/vpi.nut +++ b/mge/vpi/vpi.nut @@ -3,33 +3,46 @@ // Made by Mince (STEAM_0:0:41588292) -////////////////////////////////////////// SCRIPT VARS ////////////////////////////////////////// +local VERSION = "1.0.0"; + +////////////////////////////////////////// SCRIPT VARS ////////////////////////////////////////// // Server owners modify this section +/* +// Uncomment and use this on a listen server to generate a secret before you do anything +// ent_fire !self callscriptfunction GenerateSecret +::GenerateSecret <- function(n=128) { + local s = ""; + for (local i = 0; i < n; ++i) + { + // 35 instead of 32 so user doesn't have to deal with quotations + s += randomint(35, 126).tochar(); + } + printl(s); + return s; +}; +*/ + // Token used to verify the identity of the functions we expose to the public to prevent tampering // If they do not return this secret when prompted the program will abort -// Do not put this token into a variable as error locals traces can give away its value +// Also used to prove our identity to server +// Avoid putting this token into a variable as error locals traces can give away its value local function GetSecret() { - return ""; + return @""; } - -if (ELO_TRACKING_MODE < 2 && !GAMEMODE_AUTOUPDATE_REPO && !UPDATE_SERVER_DATA) - return - // Note: This only works to ensure security if vpi.nut is executed within mapspawn.nut // Do not set this to false unless you handle wrapping the file functions elsewhere local PROTECTED_FILE_FUNCTIONS = true; // Stores which source files are allowed to use what interface functions, if the table is empty whitelist is disabled - // You may match against interface function names with regexp, to do so, start and end the string with forward slash / // and create any pattern defined by squirrel: http://squirrel-lang.org/squirreldoc/stdlib/stdstringlib.html#the-regexp-class // { "source.nut" : [ "VPI_InterfaceFunctionName", @"/VPI_DB_User.*/" ] } local SOURCE_WHITELIST = { "vpi.nut": null, // Null or empty list denotes uninhibited access - "mge.nut": null, - "functions.nut": null, + "functions.nut": ["VPI_MGE_ReadWritePlayerStats", "VPI_MGE_PopulateLeaderboard"], + "mge.nut": ["VPI_MGE_DBInit", "VPI_MGE_AutoUpdate", "VPI_MGE_UpdateServerData"], }; // How often we normally write to file (in ticks) @@ -48,37 +61,110 @@ local expecting_iters = null; local URGENT_WRITE_MAX_COUNT = 3; // How many urgent calls per WRITE_INTERVAL are allowed local urgent_write_count = 0; +// How many seconds to wait for response before call times out +local CALLBACK_TIMEOUT = 3.0; +// How often we check on timeouts (in ticks) +local CALLBACK_TIMEOUT_CHECK_INTERVAL = 33; // 0.5s + + +// Bit flags for what messages to output to users +// 0 - Silent +// 1 - Debug +// 2 - Errors +// 4 - Warnings +// 8 - Misc +// -------------- +// 14 - (ALL - DEBUG) +// 15 - (ALL) +local LOG_MSG_LEVEL = 14; + /////////////////////////////////////////////////////////////////////////////////////////////////// -if (GetSecret() == "") throw "[VPI ERROR] Please set your secret token"; +local MSG_DEBUG = 1; +local MSG_ERROR = 2; +local MSG_WARNING = 4; +local MSG_MISC = 8; + +local NOTIFY_CONSOLE = 1; +local NOTIFY_CHAT = 2; +local NOTIFY_CENTER = 4; + +local function PrintMessage(player, msg, level=MSG_MISC, notify=NOTIFY_CONSOLE) +{ + if (LOG_MSG_LEVEL <= 0) return; + if (!(LOG_MSG_LEVEL & level)) return; + + if (level == MSG_ERROR) msg = "[VPI] -- ERROR -- " + msg; + else if (level == MSG_WARNING) msg = "[VPI] -- WARNING -- " + msg; + else if (level == MSG_DEBUG) msg = "[VPI] -- DEBUG -- " + msg; + else msg = "[VPI] -- " + msg; + + if (notify & NOTIFY_CONSOLE) ClientPrint(player, 2, msg); + if (notify & NOTIFY_CENTER) ClientPrint(player, 4, msg); + if (notify & NOTIFY_CHAT) + { + local chatmsg = msg; + if (level == MSG_ERROR) + chatmsg = "\x07ff5757" + msg; + else if (level == MSG_WARNING) + chatmsg = "\x07ffeb52" + msg; + else + chatmsg = "\x07D9F4FC" + msg; + + ClientPrint(player, 3, chatmsg); + } + + if (level == MSG_ERROR) + throw msg; +} + +if (!GetSecret().len()) + PrintMessage(null, "Please set your secret token", MSG_ERROR, NOTIFY_CHAT); local lateload = (Entities.FindByName(null, "bignet") != null); if (lateload) - throw "[VPI ERROR] Late loading is not permitted as it is a security risk, please load in mapspawn.nut" + PrintMessage(null, "Late loading is not permitted as it is a security risk, please load in mapspawn.nut", MSG_ERROR, NOTIFY_CHAT); local ROOT = getroottable(); local stringtofile = ::StringToFile; local filetostring = ::FileToString; +local randomint = ::RandomInt; +local challenge_response; +local should_write_before_destroy = true; local function ValidateIntegrity() { + local function Validate(challenge) + { + local response = challenge_response; + challenge_response = null; + + return ( response == GetSecret() ); + } + try { if (PROTECTED_FILE_FUNCTIONS) { - if (::StringToFile(null, null, true) != GetSecret()) throw null; - if (::FileToString(null, true) != GetSecret()) throw null; + if ( !Validate(::StringToFile(null, null, true)) ) throw null; + if ( !Validate(::FileToString(null, true)) ) throw null; } if ("VPI" in ROOT) { - if (VPI.Call(null, null, null, null, true) != GetSecret()) throw null; - if (VPI.AsyncCall(null, true) != GetSecret()) throw null; - if (VPI.ChainCall(null, null, null, true) != GetSecret()) throw null; + if ( !Validate(VPI.Call(null, null, null, null, null, true)) ) throw null; + if ( !Validate(VPI.AsyncCall(null, true)) ) throw null; } + + if (::RandomInt.tostring().find("native function") == null) throw null; + } + catch (e) + { + challenge_response = null; + should_write_before_destroy = false; + PrintMessage(null, "*** PROTECTED FUNCTION TAMPERING DETECTED; ABORTING ***", MSG_ERROR, NOTIFY_CHAT); } - catch (e) { throw "[VPI ERROR] *** POSSIBLE VPI FUNCTION TAMPERING, ABORTING ***" } } if (PROTECTED_FILE_FUNCTIONS) @@ -113,8 +199,9 @@ if (PROTECTED_FILE_FUNCTIONS) local callinfo = getstackinfos(2); if (__challenge) { - if (callinfo.src != "vpi.nut") return; - else return GetSecret(); + if (callinfo.src == "vpi.nut") + challenge_response = GetSecret(); + return; } if (typeof(file) != "string") return; @@ -128,8 +215,9 @@ if (PROTECTED_FILE_FUNCTIONS) local callinfo = getstackinfos(2); if (__challenge) { - if (callinfo.src != "vpi.nut") return; - else return GetSecret(); + if (callinfo.src == "vpi.nut") + challenge_response = GetSecret(); + return; } if (typeof(file) != "string") return; @@ -144,38 +232,44 @@ if (PROTECTED_FILE_FUNCTIONS) local call_list = { normal = { async=[], - chain=[], }, urgent = { async=[], - chain=[], }, }; local callbacks = {}; local used_tokens = {}; -// Strip hostname of characters other than [a-z0-9_] -local hostname = @() Convars.GetStr("hostname").tolower(); -try +// We delay sending calls until this is true so hostname can have the proper value +local server_cfg_execd = false; +local HOSTNAME; + +local function GetSanitizedHostname() { - local str = ""; - foreach(code in hostname()) + // Strip hostname of characters other than [a-z0-9_] + try { - if (code < 33 && !endswith(hostname(), "_")) + local hostname = Convars.GetStr("hostname").tolower(); + local str = ""; + foreach (code in hostname) { - str += "_"; - continue; - } - if (code < 48 || (code > 57 && code < 97) || code > 122) continue; + if (code < 33 && !endswith(hostname, "_")) + { + str += "_"; + continue; + } + if (code < 48 || (code > 57 && code < 97) || code > 122) continue; - str += code.tochar(); + str += code.tochar(); + } + return str; } - hostname = str; + catch (e) + return "team_fortress" } -catch (e) {} -local INPUT_FILE = hostname + "_vpi_input.interface"; +local INPUT_FILE; local MAX_FILE_SIZE = 16000; local INT_MAX = 2147483647; @@ -190,7 +284,7 @@ local EPOCH = { }; -////////////////////////////////////////////// JSON ///////////////////////////////////////////// +////////////////////////////////////////////// JSON ///////////////////////////////////////////// // Based on implementation: https://github.com/electricimp/JSONEncoder/blob/v2.0.0/JSONEncoder.class.nut // Max depth for encoding objects @@ -271,6 +365,66 @@ local function Escape(str) return res; } +local function UnEscape(str) +{ + local res = ""; + local i = 0; + + while (i < str.len()) + { + local ch1 = str[i].tochar(); + + if (ch1 == "\\" && i + 1 < str.len()) + { + ++i; // Skip the backslash + + ch1 = str[i].tochar(); + + // Handle escape sequences + if (ch1 == "\"") + res += "\""; + else if (ch1 == "\\") + res += "\\"; + else if (ch1 == "/") + res += "/"; + else if (ch1 == "b") + res += "\b"; + else if (ch1 == "f") + res += "\f"; + else if (ch1 == "n") + res += "\n"; + else if (ch1 == "r") + res += "\r"; + else if (ch1 == "t") + res += "\t"; + else if (ch1 == "u") + { + // Handle Unicode escape sequences \uXXXX + if (i + 5 < str.len()) + { + local hex = str.slice(i + 1, i + 5); + local uni = hex.tointeger(16); + res += format("%c", uni); + i += 4; // Skip past the 4 hex digits + } + } + else + { + res += "\\" + ch1; + } + } + else + { + // Add non-escaped character to result + res += ch1; + } + + ++i; + } + + return res; +} + local function Tokenize(str) { local tokens = []; @@ -361,7 +515,7 @@ ParseTokens = function(tokens, start_index=0) obj = false; // String else if (token[0] == '"' && token[token.len()-1] == '"') - obj = token.slice(1, -1); + obj = UnEscape(token.slice(1, -1)); // Float else if (token.find(".") != null || token.find("e") != null) { @@ -606,6 +760,62 @@ local function Timestamp(time=null, epoch=null, timezone={dir=1,hour=5,minute=0} return seconds; } +// Simple encryption algorithm based on timestamp, time, and a key +local function Encrypt(str) +{ + local timestamp = Timestamp(); + local time = (Time() / 0.015).tointeger(); + + // Add a bit of randomness + local t = (timestamp + time) % 1024; // Sin doesn't give good output for large values, keep things small + local f = fabs(sin(16 * t)); // Give our time a bit of variance + local h = floor(f * 127 + 0.5); // Get a hash value from 0 - 127 (really this could be any number though) + + // Initialization vector to provide true randomness since we always use the same key + // Without this the output tends to repeat quite often + local iv = ""; + foreach (ch in str) + iv += randomint(35, 126).tochar(); + + local enc = ""; + foreach (i, ch in str) + { + local key_index = i % GetSecret().len(); // Corresponding index in our key, loop if necessary + local key_char = GetSecret()[key_index]; + + // Encode the character; shifted using hash and key_char; limited to 32 - 127 ASCII + enc += (32 + (ch + h + iv[i] + key_char) % 95).tochar(); + } + + return { + enc = enc, + iv = iv, + timestamp = timestamp, + ticks = time, + }; +} +// Decryption +local function Decrypt(enc, iv, timestamp, ticks) +{ + local t = (timestamp + ticks) % 1024; + local f = fabs(sin(16 * t)); + local h = floor(f * 127 + 0.5); + + local dec = ""; + foreach (i, ch in enc) + { + local key_index = i % GetSecret().len(); + local key_char = GetSecret()[key_index]; + + local dec_char = (ch - 32 - h - iv[i] - key_char) % 95; + if (dec_char < 32) + dec_char += 95 * ceil((32 - dec_char) / 95.0); + dec += dec_char.tochar(); + } + + return dec; +} + local function SetDestroyCallback(entity, callback) { entity.ValidateScriptScope(); @@ -690,13 +900,14 @@ local VPICallInfo = class kwargs = null; callback = null; urgent = null; + timeout = null; GetScript = null; - constructor(secret, s=null, f=null, k=null, c=null, u=null) + constructor(secret, s=null, f=null, k=null, c=null, u=null, t=null) { if (secret != GetSecret()) - throw "[VPI ERROR] *** POSSIBLE VPI FUNCTION TAMPERING, ABORTING ***"; + PrintMessage(null, "*** PROTECTED FUNCTION TAMPERING DETECTED; ABORTING ***", MSG_ERROR, NOTIFY_CHAT); token = UniqueString(); @@ -705,10 +916,12 @@ local VPICallInfo = class local script = s; GetScript = function() { return script }; - func = f; - urgent = u; - callback = c; - kwargs = k; + func = f; + urgent = u; + callback = c; + kwargs = k; + + timeout = t; } } @@ -721,16 +934,15 @@ local VPICallInfo = class {...}, {...} ], - "chain": [ - [{...}, {...}], - [{...}] - ] } */ local function EncodeOutput(list) { // This structure gets turned into JSON - local table = { "Calls":{"async":[], "chain":[]} }; + local table = { "Calls":{"async":[]} }; + + // Encrypt our secret and send it to the server for verification + table.Identity <- Encrypt(GetSecret()); // We don't want every member of VPICallInfo to be sent to server // Make a table of only what's needed @@ -741,7 +953,7 @@ local function EncodeOutput(list) foreach (k, v in call.getclass()) { if (typeof(v) == "function" && k != "callback") continue; - if (k == "urgent") continue; + if (k == "urgent" || k == "timeout") continue; t[k] <- call[k]; } @@ -760,23 +972,15 @@ local function EncodeOutput(list) foreach (call in list.async) table.Calls.async.append(GetCallTable(call)); - foreach (calls in list.chain) - { - local list = []; - foreach (call in calls) - list.append(GetCallTable(call)); - - table.Calls.chain.append(list); - } - return JSON.Encode(table); } -// Write interface call to file as JSON +// Write interface calls to file as JSON local last_write_time = null; local function WriteCallList(list, combined=false) { - if (!list.async.len() && !list.chain.len()) return 0; + + if (!list.async.len()) return 0; // Our write file name's uniqueness is based on tick count // Don't write if we already wrote this tick @@ -786,28 +990,34 @@ local function WriteCallList(list, combined=false) // Reading files seems to be about 3x as expensive as writing // If we used a single output file we would have to read to see if we can write, // so the simple solution is to base file name off timestamp and tick count and let the server handle the hard work - local output_file = format("%s_vpi_%d_%d_output.interface", hostname, Timestamp(), time / 0.015); + local output_file = format("%s_vpi_%d_%d_output.interface", HOSTNAME, Timestamp(), time / 0.015); StringToFile(output_file, EncodeOutput(list)); + // Document the write time for current callbacks + foreach (call in list.async) + { + if (!(call.token in callbacks)) continue; + + local cbt = callbacks[call.token]; + cbt.calltime = time; + } + // Clear calls if (combined) { call_list = { normal = { async=[], - chain=[], }, urgent = { async=[], - chain=[], }, }; } else { list.async = []; - list.chain = []; } last_write_time = time; @@ -818,50 +1028,65 @@ local function WriteCallList(list, combined=false) return 1; } +local function TryExecCallback(token, data, error) +{ + if (token in callbacks) + { + local cbt = callbacks[token]; + + try { cbt.callback(data, error); } + catch (e) { + PrintMessage(null, format("User callback '%s' threw error '%s'", token, e), MSG_WARNING); + } + + delete callbacks[token]; + } +} + // Read callbacks results from the server local function HandleCallbacks() { - // Don't bother reading if we don't have anything to look for if (!callbacks.len()) return; - // The good thing about a single input file is that it seems VScript stores the - // modify time of the file and skips trying to read it if it hasn't changed - // As a result reading an unchanged file is much faster, and we can have a relatively - // small WATCH_INTERVAL without much complication local contents = FileToString(INPUT_FILE); - if (!contents || contents == "") return; + try { local table = JSON.Decode(contents); - // Look to see if any of our callbacks have results + local id = table.Identity; + id = Decrypt(id.enc, id.iv, id.timestamp, id.ticks) + if (id != GetSecret()) + { + PrintMessage(null, format("Invalid identification received from file: '%s'", INPUT_FILE), MSG_WARNING); + throw null; + } + local calls = table.Calls; - foreach (token, data in calls) + + // Look to see if any of our callbacks have results + foreach (token, cbt in callbacks) { - if (!(token in callbacks)) continue; + if (!(token in calls)) continue; + + local calldata = calls[token]; - // Peek at data and print if error + // Peek at calldata and print if error local error = false; - if (typeof(data) == "string" && startswith(data, "[VPI ERROR]")) + if (typeof(calldata) == "string" && startswith(calldata, "[VPI ERROR]")) { - printl(data); - data = null; + PrintMessage(null, format("Server returned error for call -\ntoken: %s\nerror: %s\n", token, calldata), MSG_WARNING); error = true; } - try { callbacks[token](data, error); } - catch (e) { - printl(format("[VPI ERROR] Callback %s failed with error: %s", callbacks[token].tostring(), e)); - } - - delete callbacks[token]; + TryExecCallback(token, calldata, error); } + } catch (e) - { - printl("[VPI] INVALID INPUT RECEIVED FROM SERVER"); - } + if (e != null) + PrintMessage(null, format("Invalid input from file: '%s'", INPUT_FILE), MSG_WARNING); // Wipe the file to let the server know we've handled its contents // and it can send anything else it's waiting to write @@ -879,12 +1104,13 @@ local function GetCallFromArg(src, arg) if (arg instanceof VPICallInfo) return arg; else if (typeof(arg) == "table") { - local func = arg.func; - local kwargs = ("kwargs" in arg) ? arg.kwargs : null; + local func = arg.func; + local kwargs = ("kwargs" in arg) ? arg.kwargs : null; local callback = ("callback" in arg) ? arg.callback : null; - local urgent = ("urgent" in arg) ? arg.urgent : null; + local timeout = ("timeout" in arg) ? arg.timeout : CALLBACK_TIMEOUT; + local urgent = ("urgent" in arg) ? arg.urgent : null; - return VPICallInfo(GetSecret(), src, func, kwargs, callback, urgent); + return VPICallInfo(GetSecret(), src, func, kwargs, callback, urgent, timeout); } } catch (e) {} @@ -893,33 +1119,54 @@ local function GetCallFromArg(src, arg) // Public interface for user scripts ::VPI <- { // Create a VPICallInfo instance (we don't want the actual class visible for security) - function Call(func, kwargs=null, callback=null, urgent=false, __challenge=false) + function Call(func, kwargs=null, callback=null, urgent=false, timeout=CALLBACK_TIMEOUT, __challenge=false) { local callinfo = getstackinfos(2); if (__challenge) { - if (callinfo.src != "vpi.nut") return; - else return GetSecret(); + if (callinfo.src == "vpi.nut") + challenge_response = GetSecret(); + return; } - if (!ValidateCaller(callinfo.src, func)) return; - return VPICallInfo(GetSecret(), callinfo.src, func, kwargs, callback, urgent); + if (!ValidateCaller(callinfo.src, func)) + { + PrintMessage(null, format("VPI.Call interface call for func '%s' from script '%s' failed validation", func, callinfo.src), MSG_DEBUG); + return; + } + + if (kwargs != null && typeof(kwargs) != "table") kwargs = null; + if (callback != null && typeof(callback) != "function") callback = null; + if (typeof(timeout) != "integer" || typeof(timeout) != "float") timeout = CALLBACK_TIMEOUT; + + local call = VPICallInfo(GetSecret(), callinfo.src, func, kwargs, callback, urgent, timeout); + + PrintMessage(null, format("Created VPICallInfo instance -\ntoken: %s\nfunc: %s\nurgent: %d\ntimeout: %.2f\n\n", + call.token, func, urgent, timeout), MSG_DEBUG); + + return call; }, // Queue a call to be sent to the server which will be interpreted asynchronously function AsyncCall(table_or_call, __challenge=false) { local callinfo = getstackinfos(2); + if (__challenge) { - if (callinfo.src != "vpi.nut") return; - else return GetSecret(); + if (callinfo.src == "vpi.nut") + challenge_response = GetSecret(); + return; } local call = GetCallFromArg(callinfo.src, table_or_call); if (!call || !call.token || callinfo.src != call.GetScript()) return; - if (!ValidateCaller(callinfo.src, call.func)) return; + if (!ValidateCaller(callinfo.src, call.func)) + { + PrintMessage(null, format("VPI.AsyncCall interface call for func '%s' from script '%s' failed validation", call.func, callinfo.src), MSG_DEBUG); + return; + } // Calls are one time use for the life of the VM, do not re-use them if (call.token in used_tokens) return; @@ -929,72 +1176,28 @@ local function GetCallFromArg(src, arg) list.append(call); if (typeof(call.callback) == "function") - callbacks[call.token] <- call.callback; + callbacks[call.token] <- { callback=call.callback, calltime=null, timeout=call.timeout, func=call.func }; return true; }, - // Queue a list of calls to be sent to the server which will be interpreted synchronously - function ChainCall(calls, callback=null, urgent=false, __challenge=false) + function OnGameEvent_server_cvar(params) { - local callinfo = getstackinfos(2); - if (__challenge) - { - if (callinfo.src != "vpi.nut") return; - else return GetSecret(); - } - - if (typeof(calls) != "array" || !calls.len()) return; - - local new_calls = []; - foreach (el in calls) - { - local call = GetCallFromArg(callinfo.src, el); - if (!call || !call.token || callinfo.src != call.GetScript()) return; - - if (!ValidateCaller(callinfo.src, call.func)) return; - - // Calls are one time use for the life of the VM, do not re-use them - if (call.token in used_tokens) return; - used_tokens[call.token] <- null; - - // We handle these a few lines down - call.token = null; - call.callback = false; - - new_calls.append(call); - } - - local list = (urgent) ? call_list.urgent.chain : call_list.normal.chain; - list.append(new_calls); - - // Generate a token for the whole chain call if we need one - local token; - if (typeof(callback) == "function") - { - token = UniqueString(); - callbacks[token] <- callback; - } - - // The server uses the last call's info to determine if it needs to send back results - local last = new_calls.top(); - last.token = token; - last.callback = (last.token) ? true : false; - - // Consume the input call list - calls = []; - - return true; + // We check in the script think for this bool, once it's true we'll set the hostname next tick + if (!server_cfg_execd) + server_cfg_execd = true; }, }; +__CollectGameEventCallbacks(VPI); + /////////////////////////////////////////////////////////////////////////////////////////////////// local function CombineCallLists() { - local combined = {async=[], chain=[]}; + local combined = {async=[]}; foreach (urgency, table in call_list) foreach (sync, list in table) combined[sync].extend(list); @@ -1009,7 +1212,8 @@ if (!SCRIPT_ENTITY) SCRIPT_ENTITY.ValidateScriptScope(); local SCRIPT_SCOPE = SCRIPT_ENTITY.GetScriptScope(); -SCRIPT_SCOPE.tickcount <- 0; +SCRIPT_SCOPE.readwritetick <- 0; +SCRIPT_SCOPE.ticks <- 0; SCRIPT_SCOPE.Think <- function() { // Check for tampering try { ValidateIntegrity(); } @@ -1020,6 +1224,20 @@ SCRIPT_SCOPE.Think <- function() { throw e; } + // If we didn't lateload idle until server.cfg executes so we can start with correct hostname + if (!lateload && !server_cfg_execd) return -1; + if (!HOSTNAME) + { + HOSTNAME = GetSanitizedHostname(); + INPUT_FILE = HOSTNAME + "_vpi_input.interface"; + + // Tell the server to clear out any callbacks it might be waiting to write + // from the previous map / script load + StringToFile(HOSTNAME + "_vpi_restart.interface", ""); + // Clear any left over responses from the server + StringToFile(INPUT_FILE, ""); + } + // Read input if (callbacks.len()) { @@ -1027,7 +1245,7 @@ SCRIPT_SCOPE.Think <- function() { // Read more frequently if (expecting_iters != null && expecting_iters < MAX_EXPECTING_ITERS) { - if (tickcount % EXPECTING_INTERVAL == 0) + if (readwritetick % EXPECTING_INTERVAL == 0) { ++expecting_iters; HandleCallbacks(); @@ -1035,7 +1253,7 @@ SCRIPT_SCOPE.Think <- function() { } // Normal read interval else - if (tickcount % WATCH_INTERVAL == 0 && callbacks.len()) + if (readwritetick % WATCH_INTERVAL == 0 && callbacks.len()) HandleCallbacks(); } @@ -1044,21 +1262,35 @@ SCRIPT_SCOPE.Think <- function() { // We can only have one write call per tick (filename is based on tick count) // Urgent calls get handled immediately if we aren't over the rate limit - if ( urgent_write_count < URGENT_WRITE_MAX_COUNT && - ( call_list.urgent.async.len() || call_list.urgent.chain.len()) ) + if ( urgent_write_count < URGENT_WRITE_MAX_COUNT && call_list.urgent.async.len() ) { result = WriteCallList(call_list.urgent); if (result) ++urgent_write_count; // Only increment if we actually wrote to file } // Write everything we've accumulated - else if (tickcount % WRITE_INTERVAL == 0) + else if (readwritetick % WRITE_INTERVAL == 0) { urgent_write_count = 0; result = WriteCallList(CombineCallLists(), true); } + // Check for callback timeout + if (ticks % CALLBACK_TIMEOUT_CHECK_INTERVAL == 0) + { + local time = Time(); + foreach (token, cbt in callbacks) + { + if (cbt.calltime == null || time < (cbt.calltime + cbt.timeout)) continue; + + PrintMessage(null, format("User callback '%s' for func '%s' timed out after %.2f seconds of no response", token, cbt.func, cbt.timeout), MSG_WARNING); + TryExecCallback(token, "[VPI ERROR] TIMEOUT", true); + } + } + // Don't increment if we failed to write because we already wrote this tick - if (result != -1) ++tickcount; + if (result != -1) ++readwritetick; + + ++ticks; return -1; }; @@ -1066,7 +1298,8 @@ AddThinkToEnt(SCRIPT_ENTITY, "Think"); // Make sure we get any pending calls out to the server SetDestroyCallback(SCRIPT_ENTITY, function() { - WriteCallList(CombineCallLists(), true); + if (should_write_before_destroy) + WriteCallList(CombineCallLists(), true); // Clean up after ourselves if ("VPI" in ROOT) @@ -1076,6 +1309,5 @@ SetDestroyCallback(SCRIPT_ENTITY, function() { ::FileToString <- filetostring; }); -// Tell the server to clear out any callbacks it might be waiting to write -// from the previous map / script load -StringToFile(hostname + "_vpi_restart.interface", ""); \ No newline at end of file +// We use printl instead of ClientPrint since mapspawn runs before client connect +printl(format("[VPI] -- Finished loading VScript-Python Interface Client Version %s", VERSION)); \ No newline at end of file diff --git a/mge/vpi/vpi.py b/mge/vpi/vpi.py index e4719e4..635cd60 100644 --- a/mge/vpi/vpi.py +++ b/mge/vpi/vpi.py @@ -3,74 +3,38 @@ # Made by Mince (STEAM_0:0:41588292) +VERSION = "1.0.0" + import os import datetime import time +import math import json -import importlib import asyncio -import aiomysql -import argparse -# from dotenv import load_dotenv +import importlib +from random import randint +import vpi_config import vpi_interfaces -PARSER = argparse.ArgumentParser() -PARSER.add_argument("--host", help="Hostname for database connection", type=str) -PARSER.add_argument("-u", "--user", help="User for database connection", type=str) -PARSER.add_argument("-p", "--port", help="Port for database connection", type=int) -PARSER.add_argument("-db", "--database", help="Database to use", type=str) -PARSER.add_argument("--password", help="Password for database connection", type=str) - -args = PARSER.parse_args() - -############################################ ENV VARS ############################################# -# Server owners modify this section - -# if os.path.exists("../.env"): - # load_dotenv('../.env') - -genv = os.environ.get - -# Modify VPI_* with your environment variables if you named them something else -# DB_HOST = args.host if args.host else genv("VPI_HOST", "localhost") -# DB_USER = args.user if args.user else genv("VPI_USER", "root") -# DB_PORT = args.port if args.port else int(genv("VPI_PORT", 3306)) -# DB_DATABASE = args.database if args.database else genv("VPI_INTERFACE", "mge") -# DB_PASSWORD = args.password if args.password else genv("VPI_PASSWORD", "") -DB_HOST = args.host if args.host else genv("VPI_HOST", "localhost") -DB_USER = args.user if args.user else genv("VPI_USER", "root") -DB_PORT = args.port if args.port else int(genv("VPI_PORT", 3306)) -DB_DATABASE = args.database if args.database else genv("VPI_NAME", "mge") -DB_PASSWORD = args.password if args.password else genv("VPI_PASS", "") - -SCRIPTDATA_DIR = genv("VPI_SCRIPTDATA_DIR", r"../../../../scriptdata") -# SCRIPTDATA_DIR = r"..\..\..\..\scriptdata" -# ---- - -# Validation -for env in [DB_HOST, DB_USER, DB_PORT, DB_DATABASE, SCRIPTDATA_DIR]: - assert env is not None - -if (DB_PASSWORD is None): - DB_PASSWORD = input(f"Enter password for {DB_USER}@{DB_HOST}:{DB_PORT} >>> ") - print() - -if (not os.path.exists(SCRIPTDATA_DIR)): raise RuntimeError("SCRIPTDATA_DIR does not exist") +LOGGER = vpi_config.LOGGER ################################################################################################### # { -# "": { -# "async": [ {...}, {...} ], -# "chain": [ -# [ {...}, {...} ], -# [] -# ] -# } +# "": { +# "restart_modtime": , +# "paths": { +# "": { +# "modtime": , +# "async": [ {...}, {...} ], +# } +# } +# } # } calls = {} + # { # "": { # "": @@ -92,6 +56,57 @@ def default(self, o): else: return super().default(o) +# Emulate behavior of modulus in C / Squirrel +def mod(a, b): + return (a % b + b) % b + +# Simple encryption algorithm based on timestamp, time, and a key +def Encrypt(string): + timestamp = int(round(time.time())) + + # Add a bit of randomness + t = mod(timestamp, 1024) # Sin doesn't give good output for large values, keep things small + f = math.fabs(math.sin(16 * t)) # Give our time a bit of variance + h = math.floor(f * 127 + 0.5) # Get a hash value from 0 - 127 (really this could be any number though) + + # Initialization vector to provide true randomness since we always use the same key + # Without this the output tends to repeat quite often + iv = "" + for ch in string: + iv += chr(randint(35, 126)) + + enc = "" + for i, ch in enumerate(string): + key_index = mod(i, len(vpi_config.SECRET)) # Corresponding index in our key, loop if necessary + key_char = vpi_config.SECRET[key_index] + + # Encode the character; shifted using hash and key_char; limited to 32 - 127 ASCII + enc += chr(32 + mod(ord(ch) + h + ord(iv[i]) + ord(key_char), 95)) + + return { + "enc" : enc, + "iv" : iv, + "timestamp" : timestamp, + "ticks" : 0, + } + +# Decryption +def Decrypt(enc, iv, timestamp, ticks): + t = mod(timestamp + ticks, 1024) + f = math.fabs(math.sin(16 * t)) + h = math.floor(f * 127 + 0.5) + + dec = "" + for i, ch in enumerate(enc): + key_index = mod(i, len(vpi_config.SECRET)) + key_char = vpi_config.SECRET[key_index] + + dec_char = mod(ord(ch) - 32 - h - ord(iv[i]) - ord(key_char), 95) + if (dec_char < 32): + dec_char += 95 * math.ceil((32 - dec_char) / 95.0) + dec += chr(dec_char) + + return dec # Grab the hostname from a path def GetHostname(path): @@ -108,7 +123,7 @@ def WriteCallbacksToFile(): delete = [] for host, info in callbacks.items(): - path = os.path.join(SCRIPTDATA_DIR, f"{host}_vpi_input.interface") + path = os.path.join(vpi_config.SCRIPTDATA_DIR, f"{host}_vpi_input.interface") with open(path, "a+") as f: # "a+" file mode seeks to the end of the file, need to go back to the beginning f.seek(0) @@ -122,7 +137,9 @@ def WriteCallbacksToFile(): # Wipe the file f.truncate(0) - table = {"Calls": info} + table = {"Calls": info} + table["Identity"] = Encrypt(vpi_config.SECRET) + string = json.dumps(table, cls=Encoder) overflow = {} @@ -171,45 +188,49 @@ async def ExecCalls(): tasks = [] # Tasks to gather contexts = [] # Needed context for parsing task results - # Execute calls in call chain synchronously - async def ExecCallChain(call_chain): - result = None - for call in call_chain: - func = call["func"] - if (not func.startswith("VPI_")): continue - try: - func = getattr(vpi_interfaces, func) - result = await func(call, POOL) - except: - continue - - return result + db_connected = False + if (vpi_config.DB_SUPPORT): + db_connected = await vpi_config.PingDB() + if (not db_connected): + LOGGER.warning("Could not establish connection to database! DB functions will be postponed") # Prepare calls - for host, table in calls.items(): - # Async calls can just be added to tasks directly - for call in table["async"]: - func = call["func"] - if (not func.startswith("VPI_")): continue - try: - func = getattr(vpi_interfaces, func) - tasks.append(func(call, POOL)) - contexts.append({"host":host, "call":call}) - except: - continue + for host, t1 in calls.items(): + restart_modtime = t1["restart_modtime"] + for path, t2 in t1["paths"].copy().items(): + modtime = t2["modtime"] - # Calls in call chains should be executed synchronously, but still add that to tasks - for call_chain in table["chain"]: - if (not len(call_chain)): continue - last = call_chain[-1] - tasks.append(ExecCallChain(call_chain)) - contexts.append({"host":host, "call":last}) + for call in t2["async"].copy(): + func = call["func"] + + if (func.startswith("VPI_")): + func = getattr(vpi_interfaces, func) + + # We don't have a connection to the DB so don't bother + if (not db_connected and hasattr(func, "__WrapDB__")): + if (not vpi_config.DB_SUPPORT): + LOGGER.error("Database call received from client but server does not support DB operations! Discarding call to %s", func) + t2["async"].remove(call) + continue + + tasks.append(func(call)) + contexts.append({"host":host, "call":call} if (modtime >= restart_modtime) else None) + + t2["async"].remove(call) + + # No more calls to handle + if (not len(t2["async"])): + del t1["paths"][path] + continue # Go results = await asyncio.gather(*tasks) # Set callbacks (to return results to client later) for result, context in zip(results, contexts): + # We don't send a response to client for calls from stale files + if (context is None): continue + host = context["host"] call = context["call"] token = call["token"] @@ -231,22 +252,33 @@ def ExtractCallsFromFile(path): data = json.loads(contents) + ident = Decrypt(**data["Identity"]) + if (ident != vpi_config.SECRET): + LOGGER.warning("Invalid identification in file: %s; ignoring", path) + return + host = GetHostname(path) - if (host not in calls): - calls[host] = {"async":[], "chain":[]} - calls[host]["async"].extend(data["Calls"]["async"]) - calls[host]["chain"].extend(data["Calls"]["chain"]) + calls[host]["paths"][path]["async"].extend(data["Calls"]["async"]) except Exception as e: - print(f"Invalid input received from client in: \"{path}\"") + LOGGER.warning("Invalid structure in file: %s; ignoring", path) -POOL = None async def main(): - global POOL - POOL = await aiomysql.create_pool(host=DB_HOST, user=DB_USER, password=DB_PASSWORD, port=DB_PORT, db=DB_DATABASE, autocommit=False) - print(str(POOL) + "\n") + LOGGER.info("VScript-Python Interface Server version %s startup", VERSION) + + try: + if (vpi_config.DB_TYPE == "mysql"): + vpi_config.DB = await vpi_config.aiomysql.create_pool(host=vpi_config.DB_HOST, user=vpi_config.DB_USER, password=vpi_config.DB_PASSWORD, port=vpi_config.DB_PORT, db=vpi_config.DB_DATABASE, autocommit=False) + elif (vpi_config.DB_TYPE == "sqlite"): + vpi_config.DB = await vpi_config.aiosqlite.connect(vpi_config.DB_LITE) + + if (vpi_config.DB is not None): + LOGGER.info("Connected to %s database using %s", vpi_config.DB_TYPE, str(vpi_config.DB)) + except Exception as e: + LOGGER.critical(e) + return global calls global callbacks @@ -264,23 +296,38 @@ async def main(): last_interface_modtime = last_modtime try: importlib.reload(vpi_interfaces) + LOGGER.info("Successfully hot-loaded changes to vpi_interfaces.py") except: - print("INVALID INTERFACE MODULE CODE!") + LOGGER.error("Failed to hot-load changes to vpi_interfaces.py due to error:", exc_info=True) - files = os.listdir(SCRIPTDATA_DIR) + files = os.listdir(vpi_config.SCRIPTDATA_DIR) for file in files: - path = os.path.join(SCRIPTDATA_DIR, file) + path = os.path.join(vpi_config.SCRIPTDATA_DIR, file) host = GetHostname(path) if (not host): continue + if (host not in calls): + calls[host] = { "restart_modtime": 0, "paths": {} } + + path_mtime = os.path.getmtime(path) + # Client tells us our callbacks list is outdated (e.g. map change) if (file.endswith("_restart.interface")): + mtime = calls[host]["restart_modtime"] + + if (path_mtime >= mtime): + calls[host]["restart_modtime"] = path_mtime + if (host in callbacks): del callbacks[host] os.remove(path) + # Grab info from clients elif (file.endswith("_output.interface")): + if (path not in calls[host]["paths"]): + calls[host]["paths"][path] = { "modtime": path_mtime, "async": [] } + ExtractCallsFromFile(path) os.remove(path) @@ -290,7 +337,4 @@ async def main(): # Send results to clients WriteCallbacksToFile() - calls = {} - - asyncio.run(main()) diff --git a/mge/vpi/vpi_config.py b/mge/vpi/vpi_config.py new file mode 100644 index 0000000..f05f081 --- /dev/null +++ b/mge/vpi/vpi_config.py @@ -0,0 +1,164 @@ +import os +import sys +import logging +from logging.handlers import TimedRotatingFileHandler + +genv = os.environ.get + +USE_COLOR = True +try: + from colorama import Fore, Back, Style +except: + USE_COLOR = False + +# Environment Variables: +# VPI_SCRIPTDATA_DIR - tf/scriptdata directory +# If MySQL Database: +# VPI_HOST - hostname +# VPI_USER - user +# VPI_PORT - port +# VPI_INTERFACE - database name +# VPI_PASSWORD - password + +# If you don't want to set environment variables feel free to simply set the default values below instead +# They're mainly for when you host your source code publicly + +# ====================================================================================================================== # + +# This should be the same token returned in the GetSecret function in vpi.nut +# It's used to identify files created by VPI +SECRET = r"" +if (not SECRET): + raise RuntimeError("Please set your secret token") + +# Change this to your scriptdata directory +SCRIPTDATA_DIR = genv("VPI_SCRIPTDATA_DIR", r"C:\Program Files (x86)\Steam\steamapps\common\Team Fortress 2\tf\scriptdata") +if (not os.path.exists(SCRIPTDATA_DIR)): raise RuntimeError("SCRIPTDATA_DIR does not exist") + +# Are you going to be interacting with a database? +DB_SUPPORT = False +if (DB_SUPPORT): + DB = None + + # What type? + DB_TYPE = "mysql" # mysql or sqlite + + DB_TYPE = DB_TYPE.lower() + if (DB_TYPE == "mysql"): + import aiomysql + import argparse + + # An alternative to using environment variables or setting the default values in this file is to + # specify them with command line options when you run vpi.py (ideally in a service) + PARSER = argparse.ArgumentParser() + PARSER.add_argument("--host", help="Hostname for database connection", type=str) + PARSER.add_argument("-u", "--user", help="User for database connection", type=str) + PARSER.add_argument("-p", "--port", help="Port for database connection", type=int) + PARSER.add_argument("-db", "--database", help="Database to use", type=str) + PARSER.add_argument("--password", help="Password for database connection", type=str) + + args = PARSER.parse_args() + + # Change to your database info + DB_HOST = args.host if args.host else genv("VPI_HOST", "localhost") + DB_USER = args.user if args.user else genv("VPI_USER", "user") + DB_PORT = args.port if args.port else int(genv("VPI_PORT", 3306)) + DB_DATABASE = args.database if args.database else genv("VPI_INTERFACE", "interface") + DB_PASSWORD = args.password if args.password else genv("VPI_PASSWORD") + + # Validation + for env in [DB_HOST, DB_USER, DB_PORT, DB_DATABASE, SCRIPTDATA_DIR]: + assert env is not None + + if (DB_PASSWORD is None): + DB_PASSWORD = input(f"Enter password for {DB_USER}@{DB_HOST}:{DB_PORT} >>> ") + print() + + elif (DB_TYPE == "sqlite"): + import aiosqlite + + # Put the path to your .db file here + DB_LITE = "test.db" + + else: + raise RuntimeError("DB_TYPE must be either 'mysql' or 'sqlite'") + + # Get a connection to the current database + async def _GetDBConnection(): + if (DB_TYPE == "mysql"): + return await DB.acquire() # Pool + elif (DB_TYPE == "sqlite"): + return DB # Connection + else: + return + + # Ping the database to see if we're connected + async def PingDB(): + try: + conn = await _GetDBConnection() + try: + cursor = await conn.cursor() + await cursor.execute("SELECT 1") + return True + except: + return False + finally: + if (DB_TYPE == "mysql"): + DB.release(conn) + except: + return False + +# ====================================================================================================================== # + +# Logging + +# Should we send messages to console? +LOG_USE_CONSOLE = True +# Should we send messages to log files? +LOG_USE_FILE = True + +# Levels: DEBUG, INFO, WARNING, ERROR, CRITICAL +# What min level of messages should reach the console? +LOG_MIN_CONSOLE_LEVEL = logging.INFO +# What min level of messages should reach our log files? +LOG_MIN_FILE_LEVEL = logging.WARNING + +# ====================================================================================================================== # + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +FILE_FORMATTER = logging.Formatter("{asctime} - {levelname} - {message}", style="{") +if (USE_COLOR): + class ColoredConsoleFormatter(logging.Formatter): + def __init__(self, fmt, style="%", *args, **kwargs): + super().__init__(fmt, *args, style=style, **kwargs) + self.fmt = fmt + self.style = style + self.FORMATS = { + logging.DEBUG: Back.LIGHTBLACK_EX + Fore.WHITE, + logging.WARNING: Back.BLACK + Fore.YELLOW, + logging.ERROR: Back.BLACK + Fore.RED, + logging.CRITICAL: Back.RED + Fore.WHITE, + } + + def format(self, record): + fmt = self.FORMATS[record.levelno] if record.levelno in self.FORMATS else "" + fmt += self.fmt + Style.RESET_ALL + return logging.Formatter(fmt, style=self.style).format(record) + + CONSOLE_FORMATTER = ColoredConsoleFormatter("{asctime} - {levelname} - {message}", style="{") +else: + CONSOLE_FORMATTER = FILE_FORMATTER + +CONSOLE_HANDLER = logging.StreamHandler(stream=sys.stdout) +CONSOLE_HANDLER.setLevel(LOG_MIN_CONSOLE_LEVEL) +CONSOLE_HANDLER.setFormatter(CONSOLE_FORMATTER) +CONSOLE_HANDLER.addFilter(lambda _: LOG_USE_CONSOLE) +LOGGER.addHandler(CONSOLE_HANDLER) + +FILE_HANDLER = TimedRotatingFileHandler("vpi.log", when="W0", encoding="utf-8", backupCount=5, delay=True) +FILE_HANDLER.setLevel(LOG_MIN_FILE_LEVEL) +FILE_HANDLER.setFormatter(FILE_FORMATTER) +FILE_HANDLER.addFilter(lambda _: LOG_USE_FILE) +LOGGER.addHandler(FILE_HANDLER) diff --git a/mge/vpi/vpi_interfaces.py b/mge/vpi/vpi_interfaces.py index 2cc3748..f80a248 100644 --- a/mge/vpi/vpi_interfaces.py +++ b/mge/vpi/vpi_interfaces.py @@ -1,42 +1,14 @@ -import asyncio import functools import re -import git -import os -import tempfile -import shutil -import datetime -import sys -import requests -import logging - -os.system('') # enables ansi escape characters in terminal - -# Setup logging at the top of the file -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - stream=sys.stdout -) -logger = logging.getLogger('VPI_MGE') - -COLOR = { - 'CYAN': '\033[96m', - 'HEADER': '\033[95m', - 'GREEN2': '\033[32m', - 'YELLOW': '\033[93m', - 'GREEN': '\033[92m', - 'RED': '\033[91m', - "ENDC": '\033[0m', -} + +import vpi_config + +logger = vpi_config.LOGGER + # Note: # All interface functions should be decorated with either WrapDB or WrapInterface # Otherwise any errors that occur will not be handled gracefully and brick the entire program -ACCESS_TOKEN = os.environ.get("ACCESS_TOKEN", "") -WEBAPI_KEY = os.environ.get("WEBAPI_KEY", "") -POTATO_API_KEY = os.environ.get("POTATO_API_KEY", "") - # Remove problematic characters from strings (return copy) def SanitizeString(string): sanitized = re.sub("[\0\x1a;]", "", string) @@ -83,51 +55,60 @@ def ValidateUserTable(info, table): # Wrapper for DB interface functions def WrapDB(func): @functools.wraps(func) - async def inner(info, pool): - conn = await pool.acquire() - cursor = await conn.cursor() + async def inner(info): + try: + conn = await vpi_config._GetDBConnection() + cursor = await conn.cursor() + except Exception as e: + LOGGER.error("Failed to establish connection to database in WrapDB due to error:", exc_info=True) + error = f"[VPI ERROR] ({func.__name__}) :: {type(e).__name__}" + return error result = None error = None try: result = await func(info, cursor) + LOGGER.debug("Executing interface function with info: %s", info) except Exception as e: - # Client expects error responses to start with [VPI ERROR] + LOGGER.error("Failed to execute interface function due to error:", exc_info=True) error = f"[VPI ERROR] ({func.__name__}) :: {type(e).__name__}" - raise Exception(error) finally: await cursor.close() if (error is None): await conn.commit() - pool.release(conn) + + if (vpi_config.DB_TYPE == "mysql"): + vpi_config.DB.release(conn) if (error is None): return result else: return error + # So we can check elsewhere if a specified function was a result of this wrapper + inner.__WrapDB__ = True + return inner # Wrapper for generic interface functions def WrapInterface(func): @functools.wraps(func) - async def inner(*args, **kwargs): + async def inner(info): result = None error = None try: - result = await func(*args, **kwargs) + result = await func(info) + LOGGER.debug("Executing interface function with info: %s", info) except Exception as e: - # Client expects this to start with [VPI ERROR] + LOGGER.error("Failed to execute interface function due to error:", exc_info=True) error = f"[VPI ERROR] ({func.__name__}) :: {type(e).__name__}" - print(error) - print(e) finally: if (error is None): return result else: return error return inner -player_data_columns = "steam_id, elo, wins, losses, kills, deaths, damage_taken, damage_dealt, airshots, market_gardens, hoops_scored, koth_points_capped, name" +player_data_columns = "steam_id, name, elo, wins, losses, kills, deaths, damage_taken, damage_dealt, airshots, market_gardens, hoops_scored, koth_points_capped" @WrapDB async def VPI_MGE_DBInit(info, cursor): print(COLOR['HEADER'], "Initializing MGE database...", COLOR['ENDC']) @@ -139,6 +120,7 @@ async def VPI_MGE_DBInit(info, cursor): print(COLOR['YELLOW'], "No mge_playerdata table found, creating...", COLOR['ENDC']) await cursor.execute("""CREATE TABLE IF NOT EXISTS mge_playerdata ( steam_id INTEGER PRIMARY KEY, + name VARCHAR(255), elo BIGINT, wins BIGINT, losses BIGINT, @@ -149,8 +131,7 @@ async def VPI_MGE_DBInit(info, cursor): airshots BIGINT, market_gardens BIGINT, hoops_scored BIGINT, - koth_points_capped BIGINT, - name VARCHAR(255))""" + koth_points_capped BIGINT)""" ) finally: print(COLOR['GREEN'], "MGE database initialized, check server console for '[VPI]: Database initialized successfully'", COLOR['ENDC']) @@ -166,7 +147,7 @@ async def VPI_MGE_PopulateLeaderboard(info, cursor): return await cursor.fetchall() -default_zeroes = ", ".join(["0"] * (len(player_data_columns.split(",")) - 3)) +default_zeroes = ", ".join(["0"] * (len(player_data_columns.split(",")) - 2)) @WrapDB async def VPI_MGE_ReadWritePlayerStats(info, cursor): kwargs = info["kwargs"] @@ -387,5 +368,3 @@ async def VPI_MGE_UpdateServerDataDB(info, cursor): kwargs["campaign_name"] )) return server - -