Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update watch_killrate.py #952

Merged
merged 3 commits into from
Feb 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 56 additions & 33 deletions rcon/watch_killrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@

from discord_webhook import DiscordEmbed, DiscordWebhook

from rcon.api_commands import RconAPI, get_rcon_api
from rcon.cache_utils import invalidates, ttl_cache
from rcon.player_history import get_player_profile, player_has_flag
from rcon.player_stats import current_game_stats
from rcon.rcon import SERVER_INFO
from rcon.user_config.rcon_server_settings import RconServerSettingsUserConfig
from rcon.user_config.watch_killrate import WatchKillRateUserConfig
from rcon.utils import get_server_number
Expand Down Expand Up @@ -83,7 +82,6 @@
"D-5T 85MM [IS-1]": True,
"COAXIAL DT [IS-1]": True,
"HULL DT [IS-1]": True,
"M2 Browning [M3 Half-track]": True,
"QF 2-POUNDER [Daimler]": True,
"COAXIAL BESA [Daimler]": True,
"QF 2-POUNDER [Tetrarch]": True,
Expand Down Expand Up @@ -126,7 +124,6 @@
"T70": True,
"T34/76": True,
"IS-1 ": True,
"M3 Half-track": True,
"ZIS-5 (Transport)": True,
"ZIS-5 (Supply)": True,
"GAZ-67": True,
Expand All @@ -138,7 +135,6 @@
"Firefly": True,
"Churchill Mk.III": True,
"Churchill Mk.VII": True,
"M3 Half-track": True,
"Bedford OYD (Transport)": True,
"Bedford OYD (Supply)": True,
}
Expand All @@ -147,18 +143,21 @@
_LAST_REPORTED_CACHE: defaultdict[str, datetime | None] = defaultdict(lambda: None)


def reset_cache():
def reset_cache() -> None:
"""Reset cache"""
global _LAST_REPORTED_CACHE
with invalidates(get_cache_value):
_LAST_REPORTED_CACHE = defaultdict(lambda: None)


@ttl_cache(ttl=10, cache_falsy=False)
def get_cache_value(player_id: str) -> datetime | None:
"""Get cache value"""
return _LAST_REPORTED_CACHE[player_id]


def set_cache_value(player_id: str, last_reported: datetime):
def set_cache_value(player_id: str, last_reported: datetime) -> None:
"""Set cache value"""
with invalidates(get_cache_value):
_LAST_REPORTED_CACHE[player_id] = last_reported

Expand All @@ -177,6 +176,7 @@ def make_embed(
server_name: str,
author_name: str,
):
"""Prepare Discord message embed"""
embed = DiscordEmbed()
embed.set_author(name=author_name)
embed.timestamp = str(timestamp)
Expand All @@ -185,17 +185,16 @@ def make_embed(
embed.add_embed_field(
name="Playtime", value=str(timedelta(seconds=playtime_secs)), inline=False
)
embed.add_embed_field(name="Kills", value=str(kills), inline=False)
embed.add_embed_field(name="Overall KPM", value=f"{kpm:.1f}", inline=True)
embed.add_embed_field(name="Kills", value=str(kills), inline=True)
embed.add_embed_field(name="Overall KPM", value=f"{kpm:.1f}", inline=False)
if armor_kpm > 0.0:
embed.add_embed_field(name="Armor KPM", value=f"{armor_kpm:.1f}", inline=False)
embed.add_embed_field(name="Armor KPM", value=f"{armor_kpm:.1f}", inline=True)
if artillery_kpm > 0.0:
embed.add_embed_field(
name="Artillery KPM", value=f"{artillery_kpm:.1f}", inline=False
name="Artillery KPM", value=f"{artillery_kpm:.1f}", inline=True
)
if mg_kpm > 0.0:
embed.add_embed_field(name="MG KPM", value=f"{mg_kpm:.1f}", inline=False)

embed.add_embed_field(
name="Weapons",
value="\n".join(
Expand All @@ -207,8 +206,9 @@ def make_embed(
return embed


def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: str):
player_stats = current_game_stats()
def watch_killrate(config: WatchKillRateUserConfig, server_name: str) -> None:
"""Observe all players and report them if they hit k/r thresholds"""
player_stats: dict = current_game_stats()

# Create a dict of all the weapons that are whitelisted so that we can
# recalculate our base kill rate for the player after filtering them out
Expand All @@ -227,30 +227,40 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
return

for player_name, stats in player_stats.items():
player_id = stats["player_id"]
player_id: str = stats["player_id"]

# Skip whitelisted players
whitelisted: bool = False
for flag in config.whitelist_flags:
if stats["profile"] and flag in stats["profile"]["flags"]:
continue
if player_has_flag((get_player_profile(player_id, 0)), flag):
logger.info("Skipping %s/%s - Whitelist flag", player_name, player_id)
whitelisted = True
break
if whitelisted:
continue

# There is some wonkiness in player stat calculation and this can be negative sometimes
playtime_secs: int = (
int(stats["time_seconds"]) if stats["time_seconds"] > 0 else 0
)
kills = stats["kills"]

# Skip players with less than minimum playtime
if playtime_secs < config.watch_interval_secs:
logger.debug(
"Skipping %s/%s did not meet %s minimum play time of %s",
"Skipping %s/%s - Did not meet minimum play time %s/%s",
player_name,
player_id,
playtime_secs,
config.watch_interval_secs,
)
continue

kills: int = stats["kills"]

# Skip players with less than minimum kills
if kills < config.min_kills:
logger.debug(
"Skipping %s/%s %s kills < %s minimum kills",
"Skipping %s/%s - Did not meet minimum kills %s/%s",
player_name,
player_id,
kills,
Expand All @@ -259,7 +269,13 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
continue

kpm: float = stats["kills_per_minute"]
logger.info(f"{player_id=} {playtime_secs=} {kills=} {kpm=}")
logger.info(
"player_id=%s playtime_secs=%s kills=%s kpm=%s",
player_id,
playtime_secs,
kills,
kpm,
)
used_weapons: Counter = Counter()
# If the players unfiltered KPM doesn't meet any of the thresholds
# skip all the calculations because this is the highest possible KPM
Expand Down Expand Up @@ -307,6 +323,8 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
),
1,
)

# Artillery
artillery_kpm: float = round(
(
(
Expand All @@ -318,11 +336,13 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
/ playtime_secs
* 60
)
if not config.whitelist_armor
if not config.whitelist_artillery
else 0.0
),
1,
)

# Machinegun
mg_kpm: float = round(
(
(
Expand All @@ -334,7 +354,7 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
/ playtime_secs
* 60
)
if not config.whitelist_armor
if not config.whitelist_mg
else 0.0
),
1,
Expand All @@ -354,7 +374,9 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
if not conditions_met:
continue

# KPM exceeded at this point, make discord embed/log
# Threshold exceeded

# Only report once per match
last_reported = get_cache_value(player_id)
if config.only_report_once_per_match and last_reported:
logger.info(
Expand All @@ -364,12 +386,15 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
last_reported,
)
continue
elif (

# Player has not been reported or cooldown has passed
if (
not last_reported
or (timestamp - last_reported).total_seconds()
> config.report_cooldown_mins * 60
):
set_cache_value(player_id, timestamp)

logger.debug(
"Creating embed %s/%s/%s kpm %s armor: %s arty: %s mg: %s",
player_name,
Expand All @@ -380,6 +405,7 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
artillery_kpm,
mg_kpm,
)

embed: DiscordEmbed = make_embed(
timestamp=timestamp,
player_name=player_name,
Expand All @@ -394,6 +420,7 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
server_name=server_name,
author_name=config.author,
)

for hook in config.webhooks:
wh = DiscordWebhook(url=str(hook.url))
wh.add_embed(embed)
Expand All @@ -418,20 +445,16 @@ def watch_killrate(api: RconAPI, config: WatchKillRateUserConfig, server_name: s
)


def run():
api: RconAPI = get_rcon_api(SERVER_INFO)
api.get_detailed_players

server_config = RconServerSettingsUserConfig.load_from_db()

def run() -> None:
"""Main process (loop)"""
while True:
server_config = RconServerSettingsUserConfig.load_from_db()
config = WatchKillRateUserConfig.load_from_db()

if not config.enabled:
break
break # The service will gracefully exit

watch_killrate(
api=api,
config=config,
server_name=server_config.short_name,
)
Expand Down