diff --git a/README.md b/README.md index 830a008..1560107 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The biggest obstacle that obviously cannot be worked around is the lack of a pro ## Installation - Drop the `mapspawn.nut` file and `mge` folder in your `tf/scripts/vscripts` directory. That's it - If you know github/git, I recommend cloning the repository to this directory so you're always up to date. +- Alternatively, if you are not using any database integration, you can rename mapspawn.nut to something else and add `script_execute new_filename_here` to your server.cfg ## Don't pack this into your map - I generally don't recommend you do this, this will receive live regular updates like a sourcemod plugin, and will conflict with server configs @@ -40,8 +41,6 @@ The biggest obstacle that obviously cannot be worked around is the lack of a pro | Database tracking (SQLite) | ⚠️ | | Custom rulesets | ⚠️ | | Arbitrary team sizes | ❌ | -| Custom spawn ordering | ❌ | -| In-Game map configuration tool | ❌ | ⚠️KOTH works but the logic is super janky right now, partial cap time can go negative and you can just trade the point back and forth (doesn't contest/pause) @@ -153,7 +152,7 @@ All chat commands can be prefixed with any of these characters: `/\.!?` | help/mgehelp | view the help menu | stats | view your stats breakdown | language | change your language, this will read your `cl_language` setting by default -| handicap | set a handicap for yourself, example: `!handicap 100` will set your HP to 100 +| handicap | set a handicap for yourself, `!handicap 100` will set your HP to 100 | top5 | NOT IMPLEMENTED | leaderboard | NOT IMPLEMENTED @@ -175,20 +174,19 @@ Support [This github issue](https://github.com/ValveSoftware/Source-1-Games/issu - No leaderboard support currently. ### Database -- Database tracking uses [VScript-Python Interface](https://github.com/potato-tf/VPI) to send data from vscript to python through the filesystem. +- Database tracking uses [VScript-Python Interface](https://github.com/Mince1844/VPI) to send data from vscript to python through the filesystem. - Open `tf/scripts/vscripts/mge/cfg/config.nut` and set `ELO_TRACKING_MODE` from 1 to 2 - Open `tf/scripts/vscripts/mge/vpi/vpi.nut` and update line 13, change `return "";` to a random unique string. Treat this like a password. + - Install MySQL (recommended) or SQLite and create a database - Install Python 3.10 or newer if you don't already have it - - Install MySQL (recommended) or SQLite - - Install the `aiomysql` module - - SQLite uses `aiosqlite` - - Add your database credentials to `tf/scripts/mge_python/vpi.py` (use env vars) and run this script constantly in the background, this is your database connection + - Install the `aiomysql` module, SQLite uses `aiosqlite` + - Add your database credentials to `tf/scripts/vscripts/mge/vpi/vpi.py` (use env vars) and run this script constantly in the background, this is your database connection - You should create a systemd service for this on linux, or whatever the windows equivalent is - Check server console for any VPI related errors when you join/leave the server. - This will automatically create the `mge_playerdata` table in your database ## GitHub Auto Updates -- If configured in `cfg/constants.nut`, the python script that handles database connections will also periodically git clone this repo to a specified directory and shorten the map restart timer. +- If configured in `cfg/config.nut`, the python script that handles database connections will also periodically git clone this repo to a specified directory and shorten the map restart timer. ## NavMesh generation diff --git a/mge/cfg/config.nut b/mge/cfg/config.nut index 59c794b..c5e0c4e 100644 --- a/mge/cfg/config.nut +++ b/mge/cfg/config.nut @@ -7,7 +7,7 @@ const DEFAULT_LANGUAGE = "english" * if this cvar is not set it will simply switch to whatever map is listed next in your `mapcyclefile` * * make sure you configure your mapcycle.txt correctly so your MGE server doesn't switch to cp_granary or something * ********************************************************************************************************************/ -const MAP_RESTART_TIMER = 7200 +const MAP_RESTART_TIMER = 7200 /*************************************************************************************************************** * setting this to true will send a retry command to every player and kill worldspawn * @@ -79,7 +79,7 @@ const MAX_CLEAR_SPAWN_RETRIES = 10 //announcer const ENABLE_ANNOUNCER = true //enable announcer quips (first blood airshots etc) const ANNOUNCER_VOLUME = 0.5 //volume of announcer quips -const KILLSTREAK_ANNOUNCER_INTERVAL = 5 //airshot announcer will play every KILLSTREAK_ANNOUNCER_INTERVAL number of kills +const KILLSTREAK_ANNOUNCER_INTERVAL = 5 //killstreak announcer will play every KILLSTREAK_ANNOUNCER_INTERVAL number of kills //round misc const DEFAULT_CDTIME = 3 //default countdown time diff --git a/mge/events.nut b/mge/events.nut index 95c6e01..34eb5a1 100644 --- a/mge/events.nut +++ b/mge/events.nut @@ -115,6 +115,19 @@ class MGE_Events local arena_name = scope.arena_info.name local ruleset_split = split(params.text, " ") + + if (ruleset_split.len() == 1 || !(ruleset_split[1] in special_arenas)) + { + MGE_ClientPrint(player, HUD_PRINTTALK, "InvalidRuleset", ruleset_split.len() == 1 ? "" : ruleset_split[1]) + + local valid_rulesets = "" + foreach (ruleset, _ in special_arenas) + valid_rulesets += format(", %s ", ruleset) + + valid_rulesets = valid_rulesets.slice(1) + ClientPrint(player, HUD_PRINTTALK, format("\x07%sValid Rulesets:\x07%s %s", MGE_COLOR_MAIN, MGE_COLOR_SUBJECT, valid_rulesets)) + return + } local ruleset = ruleset_split[1] local fraglimit = 2 in ruleset_split ? ruleset_split[2].tointeger() : arena.fraglimit / 2 @@ -442,6 +455,9 @@ class MGE_Events ValidatePlayerClass(player, params["class"], true) local scope = player.GetScriptScope() + + if (player.IsFakeClient()) return + local arena = scope.arena_info.arena if (arena.State != AS_FIGHT || arena.IsBBall || arena.IsKoth) return @@ -642,7 +658,7 @@ class MGE_Events params.damage = 0 return false } - if (arena.IsAllMeat) + if ("IsAllMeat" in arena && arena.IsAllMeat) { local weapon = params.weapon if (!weapon) return @@ -680,6 +696,8 @@ class MGE_Events } + if (victim.IsFakeClient() && !("IsEndif" in arena)) return + if (victim_scope && victim.IsPlayer() && attacker != victim && (arena.IsEndif || arena.IsMidair) && params.damage_type & DMG_BLAST && !(victim.GetFlags() & FL_ONGROUND)) { local trace_dist = arena.IsEndif ? arena.Endif.height_threshold : arena.IsMidair ? arena.Midair.height_threshold : AIRSHOT_HEIGHT_THRESHOLD @@ -704,10 +722,14 @@ class MGE_Events local attacker = GetPlayerFromUserID(params.attacker) local attacker_scope = attacker ? attacker.GetScriptScope() : {} - if (arena.State == AS_FIGHT && !arena.IsEndif && !arena.IsMidair) + if (!("State" in arena) && victim.IsFakeClient()) + { + victim.ForceChangeTeam(TEAM_SPECTATOR, true) + } + 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) + if (attacker && !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 a0501df..cf04784 100644 --- a/mge/functions.nut +++ b/mge/functions.nut @@ -505,7 +505,7 @@ { if (!user_info) { - think_override = 0.2 + think_override = 1 user_info = ["NONE", -INT_MAX] } else { think_override = LEADERBOARD_UPDATE_INTERVAL diff --git a/mge/mge.nut b/mge/mge.nut index 7f76bd7..16c6eb9 100644 --- a/mge/mge.nut +++ b/mge/mge.nut @@ -708,16 +708,22 @@ AddOutput(MGE_TIMER, "OnFinished", "!self", "CallScriptFunction", "MGE_DoChangel DispatchSpawn(MGE_TIMER) MGE_TIMER.AcceptInput("Resume", "", null, null) +MGE_TIMER.AcceptInput("ShowInHUD", "1", null, null) +// EntFireByHandle(MGE_TIMER, "ShowInHUD", "1", 1.0, null, null) MGE_TIMER.ValidateScriptScope() -MGE_TIMER.GetScriptScope().counter <- MAP_RESTART_TIMER +// MGE_TIMER.GetScriptScope().counter <- MAP_RESTART_TIMER +local time_remining_string = "m_flTimeRemaining" +SetPropFloat(MGE_TIMER, time_remining_string, MAP_RESTART_TIMER) MGE_TIMER.GetScriptScope().TimerThink <- function() { + local counter = GetPropFloat(MGE_TIMER, time_remaining_string) counter-- + SetPropFloat(MGE_TIMER, time_remaining_string, counter) if (counter) { - if (!HLTV_TEST && !(counter % VPI_SERVERINFO_UPDATE_INTERVAL)) + if (!(counter % VPI_SERVERINFO_UPDATE_INTERVAL)) { LocalTime(local_time) SERVER_DATA.update_time = local_time @@ -744,15 +750,16 @@ MGE_TIMER.GetScriptScope().TimerThink <- function() func = "VPI_MGE_UpdateServerData", kwargs = SERVER_DATA, callback = function(response, error) { - if (error) - { - // printl(error) - return 1 + if (error) + { + // printl(error) + return 1 + } + if (SERVER_DATA.address == 0 && "address" in response) + SERVER_DATA.address = response.address } - if (SERVER_DATA.address == 0 && "address" in response) - SERVER_DATA.address = response.address - } - }) + }) + } } // printl(counter) // if (counter < 60 && !(counter % 5)) diff --git a/mge/vpi/vpi.nut b/mge/vpi/vpi.nut index d613f5e..cd50666 100644 --- a/mge/vpi/vpi.nut +++ b/mge/vpi/vpi.nut @@ -151,13 +151,13 @@ local callbacks = {}; local used_tokens = {}; // Strip hostname of characters other than [a-z0-9_] -local hostname = Convars.GetStr("hostname").tolower(); +local hostname = @() Convars.GetStr("hostname").tolower(); try { local str = ""; - foreach (code in hostname) + foreach(code in hostname()) { - if (code < 33 && !endswith(hostname, "_")) + if (code < 33 && !endswith(hostname(), "_")) { str += "_"; continue; diff --git a/mge/vpi/vpi_interfaces.py b/mge/vpi/vpi_interfaces.py index bd8cf4a..2fb0584 100644 --- a/mge/vpi/vpi_interfaces.py +++ b/mge/vpi/vpi_interfaces.py @@ -166,46 +166,48 @@ async def VPI_MGE_PopulateLeaderboard(info, cursor): return await cursor.fetchall() -default_zeroes = ", ".join(["0"] * (len(player_data_columns.split(",")) - 2)) +default_zeroes = ", ".join(["0"] * (len(player_data_columns.split(",")) - 3)) @WrapDB async def VPI_MGE_ReadWritePlayerStats(info, cursor): kwargs = info["kwargs"] query_mode = kwargs["query_mode"] network_id = kwargs["network_id"] - - print("name" in kwargs) - name = kwargs["name"] + name = kwargs["name"] # This should be properly escaped if network_id == "BOT": return - - default_elo = kwargs["default_elo"] if "default_elo" in kwargs else 1000 - if (query_mode == "read" or query_mode == 0): - - print(COLOR['CYAN'], f"Fetching player data for steam ID {network_id}", COLOR['ENDC']) - await cursor.execute(f"SELECT * FROM mge_playerdata WHERE steam_id = {network_id}") + default_elo = kwargs.get("default_elo", 1000) + + if query_mode in ("read", 0): + # Use parameterized query + await cursor.execute("SELECT * FROM mge_playerdata WHERE steam_id = %s", (network_id,)) result = await cursor.fetchall() - # If no record exists, create one with default values if not result: - print(COLOR['YELLOW'], f"No record exists for steam ID {network_id}, adding...", COLOR['ENDC']) - # await cursor.execute(f"INSERT INTO mge_playerdata ({player_data_columns}) VALUES ({network_id}, {default_elo}, {default_zeroes})") - await cursor.execute(f"INSERT INTO mge_playerdata ({player_data_columns}) VALUES ({network_id}, {default_elo}, {default_zeroes}, {name})") - await cursor.execute(f"SELECT * FROM mge_playerdata WHERE steam_id = {network_id}") + # Parameterized INSERT with proper value ordering + await cursor.execute( + f"INSERT INTO mge_playerdata ({player_data_columns}) VALUES (%s, %s, {default_zeroes}, %s)", + (network_id, default_elo, name) + ) + await cursor.execute("SELECT * FROM mge_playerdata WHERE steam_id = %s", (network_id,)) result = await cursor.fetchall() return result - elif (query_mode == "write" or query_mode == 1): - # Build SET clause from stats dictionary + + elif query_mode in ("write", 1): + # Parameterized UPDATE set_clauses = [] + params = [] for key, value in kwargs['stats'].items(): - set_clauses.append(f"{key} = {value}") - set_clause = ", ".join(set_clauses) + set_clauses.append(f"{key} = %s") + params.append(value) - print(COLOR['CYAN'], f"Updating player data for steam ID {network_id} with stats: {set_clause}", COLOR['ENDC']) - await cursor.execute(f"UPDATE mge_playerdata SET {set_clause} WHERE steam_id = {network_id}") - return await cursor.fetchall() + params.append(network_id) # Add WHERE clause param + query = f"UPDATE mge_playerdata SET {', '.join(set_clauses)} WHERE steam_id = %s" + await cursor.execute(query, params) + return await cursor.fetchall() + banned_files = [".gitignore", ".git", ".vscode", "README.md", "mge_windows_setup.bat", "config.nut"] @WrapInterface async def VPI_MGE_AutoUpdate(info, test=False):