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

Feat/seed vip #682

Merged
merged 10 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
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 @@ -260,7 +259,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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won't this result in automatic restarts when the seed vip feature is disabled? (I mean supervisor restarting the service)

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 @@ -747,7 +747,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 @@ -1311,21 +1311,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 @@ -1340,12 +1341,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 @@ -1375,7 +1381,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 @@ -1394,7 +1400,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