Skip to content

Commit

Permalink
Feat/seed vip (#682)
Browse files Browse the repository at this point in the history
This is a pretty straight forward port of https://github.com/cemathey/hll_seed_vip to run as a service inside of CRCON itself.

Made some minor tweaks since it can access info internally instead of using the API.

This runs as a separate service (like all the other plugins)
Has a user config and updated menu

For future reference, this possible languages that can be used when using `humanize` to nicely format date/time quantities: https://github.com/MarechJ/hll_rcon_tool/wiki/Seed-VIP-Localization
  • Loading branch information
cemathey authored Oct 16, 2024
1 parent 8bf716f commit 80ff605
Show file tree
Hide file tree
Showing 29 changed files with 1,762 additions and 74 deletions.
7 changes: 7 additions & 0 deletions config/supervisord.conf
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ environment=LOGGING_FILENAME=expiring_vips_%(ENV_SERVER_NUMBER)s.log
startretries=10
autorestart=true

[program:seed_vip]
command=/code/manage.py seed_vip
environment=LOGGING_FILENAME=seed_vip_%(ENV_SERVER_NUMBER)s.log
startretries=10
autostart=true
autorestart=true


[program:log_event_loop]
command=/code/manage.py log_loop
Expand Down
53 changes: 48 additions & 5 deletions rcon/api_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
from rcon.user_config.rcon_server_settings import RconServerSettingsUserConfig
from rcon.user_config.real_vip import RealVipUserConfig
from rcon.user_config.scorebot import ScorebotUserConfig
from rcon.user_config.seed_vip import SeedVIPUserConfig
from rcon.user_config.standard_messages import (
StandardBroadcastMessagesUserConfig,
StandardPunishmentMessagesUserConfig,
Expand Down Expand Up @@ -81,20 +82,23 @@
def parameter_aliases(alias_to_param: Dict[str, str]):
"""Specify parameter aliases of a function. This might be useful to preserve backwards
compatibility or to handle parameters named after a Python reserved keyword.
Takes a mapping of aliases to their parameter name."""

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for alias, param in alias_to_param.items():
if alias in kwargs:
kwargs[param] = kwargs.pop(alias)
return func(*args, **kwargs)

wrapper._parameter_aliases = alias_to_param
return wrapper

return decorator


def get_rcon_api(credentials: ServerInfoType | None = None) -> "RconAPI":
"""Return a initialized Rcon connection to the game server
Expand Down Expand Up @@ -578,9 +582,11 @@ def get_online_mods(self) -> list[AdminUserType]:
def get_ingame_mods(self) -> list[AdminUserType]:
return ingame_mods()

@parameter_aliases({
"from": "from_",
})
@parameter_aliases(
{
"from": "from_",
}
)
def get_historical_logs(
self,
player_name: str | None = None,
Expand Down Expand Up @@ -1010,6 +1016,43 @@ def set_real_vip_config(
reset_to_default=reset_to_default,
)

def get_seed_vip_config(
self,
) -> SeedVIPUserConfig:
return SeedVIPUserConfig.load_from_db()

def validate_seed_vip_config(
self,
by: str,
config: dict[str, Any] | BaseUserConfig | None = None,
reset_to_default: bool = False,
**kwargs,
) -> bool:
return self._validate_user_config(
command_name=inspect.currentframe().f_code.co_name, # type: ignore
by=by,
model=SeedVIPUserConfig,
data=config or kwargs,
dry_run=True,
reset_to_default=reset_to_default,
)

def set_seed_vip_config(
self,
by: str,
config: dict[str, Any] | BaseUserConfig | None = None,
reset_to_default: bool = False,
**kwargs,
) -> bool:
return self._validate_user_config(
command_name=inspect.currentframe().f_code.co_name, # type: ignore
by=by,
model=SeedVIPUserConfig,
data=config or kwargs,
dry_run=False,
reset_to_default=reset_to_default,
)

def get_camera_notification_config(self) -> CameraNotificationUserConfig:
return CameraNotificationUserConfig.load_from_db()

Expand Down
5 changes: 2 additions & 3 deletions rcon/automods/level_thresholds.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@
WatchStatus,
)
from rcon.automods.num_or_inf import num_or_inf
from rcon.types import GameState, GetDetailedPlayer
from rcon.types import GameStateType, GetDetailedPlayer
from rcon.user_config.auto_mod_level import AutoModLevelUserConfig, Roles


LEVEL_THRESHOLDS_RESET_SECS = 120
AUTOMOD_USERNAME = "LevelThresholdsAutomod"

Expand Down Expand Up @@ -261,7 +260,7 @@ def punitions_to_apply(
squad_name: str,
team: Literal["axis", "allies"],
squad: dict,
game_state: GameState,
game_state: GameStateType,
) -> PunitionsToApply:
"""
Observe all squads/players
Expand Down
5 changes: 2 additions & 3 deletions rcon/automods/no_leader.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@
WatchStatus,
)
from rcon.automods.num_or_inf import num_or_inf
from rcon.types import GameState
from rcon.types import GameStateType
from rcon.user_config.auto_mod_no_leader import AutoModNoLeaderUserConfig


LEADER_WATCH_RESET_SECS = 120
AUTOMOD_USERNAME = "NoLeaderWatch"

Expand Down Expand Up @@ -137,7 +136,7 @@ def punitions_to_apply(
squad_name: str,
team: Literal["axis", "allies"],
squad: dict,
game_state: GameState,
game_state: GameStateType,
) -> PunitionsToApply:
"""
Observe all squads/players
Expand Down
5 changes: 2 additions & 3 deletions rcon/automods/no_solotank.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@
WatchStatus,
)
from rcon.automods.num_or_inf import num_or_inf
from rcon.types import GameState
from rcon.types import GameStateType
from rcon.user_config.auto_mod_solo_tank import AutoModNoSoloTankUserConfig


SOLO_TANK_RESET_SECS = 120
AUTOMOD_USERNAME = "NoSoloTank"

Expand Down Expand Up @@ -134,7 +133,7 @@ def punitions_to_apply(
squad_name: str,
team: Literal["axis", "allies"],
squad: dict,
game_state: GameState,
game_state: GameStateType,
) -> PunitionsToApply:
"""
Observe all squads/players
Expand Down
5 changes: 2 additions & 3 deletions rcon/automods/seeding_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@
from rcon.game_logs import on_match_start
from rcon.maps import GameMode, parse_layer
from rcon.rcon import StructuredLogLineType
from rcon.types import GameState, GetDetailedPlayer, Roles
from rcon.types import GameStateType, GetDetailedPlayer, Roles
from rcon.user_config.auto_mod_seeding import AutoModSeedingUserConfig


SEEDING_RULES_RESET_SECS = 120
AUTOMOD_USERNAME = "SeedingRulesAutomod"
SEEDING_RULE_NAMES = ["disallowed_roles", "disallowed_weapons", "enforce_cap_fight"]
Expand Down Expand Up @@ -296,7 +295,7 @@ def punitions_to_apply(
squad_name: str,
team: Literal["axis", "allies"],
squad: dict,
game_state: GameState,
game_state: GameStateType,
) -> PunitionsToApply:
"""
Observe all squads/players
Expand Down
11 changes: 11 additions & 0 deletions rcon/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from sqlalchemy import select, text, update

import rcon.expiring_vips.service
import rcon.seed_vip.service
import rcon.user_config
import rcon.user_config.utils
from rcon import auto_settings, broadcast, game_logs, routines
Expand All @@ -31,6 +32,7 @@
from rcon.steam_utils import enrich_db_users
from rcon.user_config.auto_settings import AutoSettingsConfig
from rcon.user_config.log_stream import LogStreamUserConfig
from rcon.user_config.seed_vip import SeedVIPUserConfig
from rcon.user_config.webhooks import (
BaseMentionWebhookUserConfig,
BaseUserConfig,
Expand Down Expand Up @@ -126,6 +128,15 @@ def run_expiring_vips():
rcon.expiring_vips.service.run()


@cli.command(name="seed_vip")
def run_seed_vip():
config = SeedVIPUserConfig.load_from_db()
if config.enabled:
rcon.seed_vip.service.run()
else:
logger.info("Seed VIP is not enabled")


@cli.command(name="automod")
def run_automod():
automod.run()
Expand Down
36 changes: 21 additions & 15 deletions rcon/rcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
AdminType,
GameLayoutRandomConstraints,
GameServerBanType,
GameState,
GameStateType,
GetDetailedPlayer,
GetDetailedPlayers,
GetPlayersType,
Expand Down Expand Up @@ -757,7 +757,7 @@ def message_player(
)
return res

def get_gamestate(self) -> GameState:
def get_gamestate(self) -> GameStateType:
"""
Returns player counts, team scores, remaining match time and current/next map
Expand Down Expand Up @@ -1321,21 +1321,22 @@ def set_maprotation(self, map_names: list[str]) -> list[Layer]:
super().remove_map_from_rotation(current[0], 1)

return self.get_map_rotation()

@ttl_cache(ttl=10)
def get_objective_row(self, row: int):
return super().get_objective_row(row)

def get_objective_rows(self) -> List[List[str]]:
return [
self.get_objective_row(row)
for row in range(5)
]
return [self.get_objective_row(row) for row in range(5)]

def set_game_layout(self, objectives: Sequence[str | int | None], random_constraints: GameLayoutRandomConstraints = 0):
def set_game_layout(
self,
objectives: Sequence[str | int | None],
random_constraints: GameLayoutRandomConstraints = 0,
):
if len(objectives) != 5:
raise ValueError("5 objectives must be provided")

obj_rows = self.get_objective_rows()
parsed_objs: list[str] = []
for row, (obj, obj_row) in enumerate(zip(objectives, obj_rows)):
Expand All @@ -1350,12 +1351,17 @@ def set_game_layout(self, objectives: Sequence[str | int | None], random_constra
elif obj in ("right", "bottom"):
parsed_objs.append(obj_row[2])
else:
raise ValueError("Objective %s does not exist in row %s" % (obj, row))

raise ValueError(
"Objective %s does not exist in row %s" % (obj, row)
)

elif isinstance(obj, int):
# Use index of the objective
if not (0 <= obj <= 2):
raise ValueError("Objective index %s is out of range 0-2 in row %s" % (obj, row + 1))
raise ValueError(
"Objective index %s is out of range 0-2 in row %s"
% (obj, row + 1)
)
parsed_objs.append(obj_row[obj])

elif obj is None:
Expand Down Expand Up @@ -1385,7 +1391,7 @@ def set_game_layout(self, objectives: Sequence[str | int | None], random_constra
neighbors.append(obj_rows[row - 1].index(parsed_objs[row - 1]))
if row < 4 and parsed_objs[row + 1] is not None:
neighbors.append(obj_rows[row + 1].index(parsed_objs[row + 1]))

# Skip this row for now if neither of its neighbors had their objective determined yet
if not neighbors:
continue
Expand All @@ -1404,7 +1410,7 @@ def set_game_layout(self, objectives: Sequence[str | int | None], random_constra
if random_constraints & GameLayoutRandomConstraints.ALWAYS_DIAGONAL:
# Cannot have two objectives in a straight row
obj_choices[neighbor_idx] = None

# Pick an objective. If none are viable, discard constraints.
parsed_objs[row] = random.choice(
[c for c in obj_choices if c is not None] or obj_row
Expand Down
Empty file added rcon/seed_vip/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions rcon/seed_vip/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from datetime import datetime, timedelta
from logging import getLogger

import pydantic

from rcon.maps import Layer

logger = getLogger(__name__)


class BaseCondition(pydantic.BaseModel):
def is_met(self):
raise NotImplementedError


class PlayerCountCondition(BaseCondition):
faction: str
min_players: int = pydantic.Field(ge=0, le=50)
max_players: int = pydantic.Field(ge=0, le=50)

current_players: int = pydantic.Field(ge=0, le=50)

def is_met(self):
return self.min_players <= self.current_players <= self.max_players


class PlayTimeCondition(BaseCondition):
# This is constrained on the user config side
min_time_secs: int = pydantic.Field()
# This should be constrained to ge=0 but CRCON will sometimes
# report players with negative play time
current_time_secs: int = pydantic.Field()

def is_met(self):
return self.current_time_secs >= self.min_time_secs


class GameState(pydantic.BaseModel):
num_allied_players: int
num_axis_players: int
allied_score: int
axis_score: int
raw_time_remaining: str
time_remaining: timedelta
current_map: Layer
next_map: Layer


class Player(pydantic.BaseModel):
name: str
player_id: str
current_playtime_seconds: int


class VipPlayer(pydantic.BaseModel):
player: Player
expiration_date: datetime | None


class ServerPopulation(pydantic.BaseModel):
players: dict[str, Player]
Loading

0 comments on commit 80ff605

Please sign in to comment.