Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9673ace
init working version
Dorfieeee Jul 18, 2025
6732da4
feat: add /vm add commands
Dorfieeee Jul 18, 2025
9088edf
feat: vm add based on flags, offensive handling, helper texts, remove…
Dorfieeee Jul 22, 2025
cdeebef
removed singleton pattern, parametrized init and several methods
Dorfieeee Jul 24, 2025
5d7485b
new tests for votemap
Dorfieeee Jul 24, 2025
fd24916
update votemap settings ui - vote ban flags and vip vote count
Dorfieeee Jul 24, 2025
23ed652
fix tests after changing votemap config conditions
Dorfieeee Jul 24, 2025
647611b
Test for empty selection, extracted get_next_map method from apply_re…
Dorfieeee Jul 25, 2025
799171c
fix: addressing timraay pr code review
Dorfieeee Aug 7, 2025
478af7c
fix: remove zero vote count test
Dorfieeee Aug 7, 2025
52e9ccc
feat: send reminder api, status response change
Dorfieeee Aug 7, 2025
ba002d3
test: votemap mock rcon dynamic rotation
Dorfieeee Aug 8, 2025
be49e91
feat: configurable vote reminders
Dorfieeee Aug 8, 2025
4c56e21
feat: moved next_map class attribute to redis
Dorfieeee Aug 8, 2025
4320f9f
fix: rename redis vote reminder
Dorfieeee Aug 8, 2025
acec8f8
fix: add votemap state versioning
Dorfieeee Aug 9, 2025
c7b4062
fix: version redis key
Dorfieeee Aug 9, 2025
10ce3c9
refactor: replace map_id str for Layer
Dorfieeee Aug 22, 2025
0caf95b
feat: vip only & flag only votemap allowance
Dorfieeee Aug 22, 2025
7ffd993
Update apply_results logic, discord audit on match start
Dorfieeee Sep 3, 2025
b2e36cb
Adjustment to U18 changes
Dorfieeee Oct 7, 2025
c336bc7
Adjust tests to modified get_map_rotation
Dorfieeee Jan 12, 2026
82d62fe
Init votemap result history
Dorfieeee Mar 25, 2026
d67fc47
added history and some refactor
Dorfieeee Mar 27, 2026
d81bed0
exposed other votemap methods to api
Dorfieeee Mar 27, 2026
6f35b77
updated votemap UI
Dorfieeee Mar 27, 2026
66d4cbd
reworked admin set next map
Dorfieeee Mar 30, 2026
feb0430
fix votemap toggle mutation fn
Dorfieeee Mar 30, 2026
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
62 changes: 38 additions & 24 deletions rcon/api_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
PlayerFlagType,
PlayerProfileTypeEnriched,
ServerInfoType,
VoteMapStatusType,
)
from rcon.user_config.auto_broadcast import AutoBroadcastUserConfig
from rcon.user_config.auto_kick import AutoVoteKickUserConfig
Expand Down Expand Up @@ -651,61 +650,72 @@ def get_recent_logs(
inclusive_filter=inclusive_filter,
)

def get_votemap_status(self) -> list[VoteMapStatusType]:
def get_votemap_status(self):
v = VoteMap()
return v.get_status(sort_by_vote=False)

votes = v.get_votes()
votes_by_map: dict[maps.Layer, list[str]] = defaultdict(list)
for player, map_ in votes.items():
votes_by_map[map_].append(player)
def get_votemap_results(self):
v = VoteMap()
return [x for x in v.get_results()]

selection = v.get_selection()
def remove_map_from_votemap(self, map_name: str):
v = VoteMap()
v.remove_map_from_selection(maps.parse_layer(map_name))

result = []
for map_ in selection:
result.append({"map": map_, "voters": votes_by_map[map_]})
def add_map_to_votemap(self, map_name: str):
v = VoteMap()
v.add_map_to_selection(maps.parse_layer(map_name))

return sorted(result, key=lambda m: len(m["voters"]), reverse=True)
def set_votemap_winner(self, map_name: str):
v = VoteMap()
v.guarantee_next_map(maps.parse_layer(map_name))

def reset_votemap_state(self) -> list[VoteMapStatusType]:
def add_votemap_vote(self, player_id: str, player_name: str, map_name: str, vote_count: int | None = None):
v = VoteMap()
v.clear_votes()
v.gen_selection()
v.apply_results()
v.add_vote(maps.parse_layer(map_name), player_id, player_name, vote_count)

return self.get_votemap_status()
def send_votemap_reminder(self):
v = VoteMap()
result = v.send_reminder(force=True)
if not result.ok:
raise Exception(result.message)

def reset_votemap_state(self):
v = VoteMap()
v.restart()
return v.get_status()

def get_votemap_whitelist(self) -> list[str]:
v = VoteMap()

return [str(map) for map in v.get_map_whitelist()]

def add_map_to_votemap_whitelist(self, map_name: str):
v = VoteMap()
v.add_map_to_whitelist(map_name=map_name)
v.add_map_to_whitelist(maps.parse_layer(map_name))

def add_maps_to_votemap_whitelist(self, map_names: Iterable[str]):
v = VoteMap()
v.add_maps_to_whitelist(map_names=map_names)
v.add_maps_to_whitelist([maps.parse_layer(map) for map in map_names])

def remove_map_from_votemap_whitelist(self, map_name: str):
v = VoteMap()
v.remove_map_from_whitelist(map_name=map_name)
v.remove_map_from_whitelist(maps.parse_layer(map_name))

def remove_maps_from_votemap_whitelist(self, map_names: Iterable[str]):
v = VoteMap()
v.remove_maps_from_whitelist(map_names=map_names)
v.remove_maps_from_whitelist(map_names)

def reset_map_votemap_whitelist(self):
v = VoteMap()
v.reset_map_whitelist()

def set_votemap_whitelist(self, map_names: Iterable[str]):
v = VoteMap()
v.set_map_whitelist(map_layers=set([maps.parse_layer(m) for m in map_names]))
v.set_map_whitelist([maps.parse_layer(map) for map in map_names])

def get_votemap_config(self) -> VoteMapUserConfig:
return VoteMapUserConfig.load_from_db()
v = VoteMap()
return v.config

def validate_votemap_config(
self,
Expand Down Expand Up @@ -745,7 +755,11 @@ def set_votemap_config(

# on -> off or off -> on
if old_config.enabled != new_config.enabled:
self.reset_votemap_state()
vm = VoteMap()
if new_config.enabled:
vm.restart()
else:
vm.reset()

return True

Expand Down
6 changes: 4 additions & 2 deletions rcon/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def send_to_discord_audit(
webhookurls: list[HttpUrl | None] | None = None,
md_escape_message: bool = True,
md_escape_author: bool = True,
flatten: bool = True,
):
by = by or "CRCON"
config = None
Expand All @@ -131,8 +132,9 @@ def send_to_discord_audit(
server_config = RconServerSettingsUserConfig.load_from_db()

# Flatten messages with newlines
message = message.replace("\n", " ")
logger.info("Audit: [%s] %s, %s", by, command_name, message)
if flatten:
message = message.replace("\n", " ")
logger.info("Audit: [%s] %s, %s", by, command_name, message.replace("\n", " "))
if not webhookurls:
logger.debug("No webhooks set for audit log")
return
Expand Down
54 changes: 25 additions & 29 deletions rcon/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,25 +74,8 @@


@on_chat
def count_vote(rcon: Rcon, struct_log: StructuredLogLineWithMetaData):
enabled = VoteMap().handle_vote_command(rcon=rcon, struct_log=struct_log)
if enabled and (match := re.match(r"\d\s*$", struct_log["sub_content"].strip())):
rcon.message_player(
player_id=struct_log["player_id_1"],
message=f"INVALID VOTE\n\nUse: !votemap {match.group()}",
)


def initialise_vote_map(struct_log):
logger.info("New match started initializing vote map. %s", struct_log)
try:
vote_map = VoteMap()
vote_map.clear_votes()
vote_map.gen_selection()
vote_map.reset_last_reminder_time()
vote_map.apply_results()
except Exception as ex:
logger.exception("Something went wrong in vote map init", ex)
def count_map_vote(_, struct_log: StructuredLogLineWithMetaData):
VoteMap().handle_vote_command(struct_log)


@on_chat
Expand Down Expand Up @@ -264,11 +247,12 @@ def chat_help_command(rcon: Rcon, command: BaseChatCommand, ctx: dict[str, str])


@on_match_end
def remind_vote_map(rcon: Rcon, struct_log):
logger.info("Match ended reminding to vote map. %s", struct_log)
vote_map = VoteMap()
vote_map.apply_with_retry()
vote_map.vote_map_reminder(rcon, force=True)
def remind_vote_map(_, struct_log):
vm = VoteMap()
if vm.enabled:
logger.info("Match ended reminding to vote map. %s", struct_log)
vm.apply_results()
vm.send_reminder(force=vm.config.remind_on_match_end)


@on_match_start
Expand All @@ -282,7 +266,7 @@ def reset_watch_killrate_cooldown(rcon: Rcon, struct_log: StructuredLogLineWithM
@on_match_start
def handle_new_match_start(rcon: Rcon, struct_log):
try:
logger.info("New match started recording map %s", struct_log)
logger.info("MATCH START: Started recording map %s", struct_log)
with invalidates(Rcon.get_map, Rcon.get_next_map):
try:
# Don't use the current_map property and clear the cache to pull the new map name
Expand Down Expand Up @@ -341,11 +325,23 @@ def handle_new_match_start(rcon: Rcon, struct_log):
except:
raise
finally:
initialise_vote_map(struct_log)
prev_map = MapsHistory()[1]
vm = VoteMap()
if vm.enabled:
logger.info("MATCH START: Restarting votemap")
vm.record_result(prev_map)
send_to_discord_audit(
command_name="on_match_start",
message=vm.get_result_message(),
md_escape_message=False,
flatten=False,
)
vm.restart()
vm.send_reminder(force=vm.config.remind_on_match_start)
try:
record_stats_worker(MapsHistory()[1])
except Exception:
logger.exception("Unexpected error while running stats worker")
record_stats_worker(prev_map)
except Exception as e:
logger.exception("Unexpected error while running stats worker\n%s", e)


@on_match_end
Expand Down
9 changes: 3 additions & 6 deletions rcon/message_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,9 @@ def format_winning_map(
)


def vote_status() -> list[tuple[Layer, int]]:
def vote_status():
logger.info(f"Crunching vote_status")
vote_results = VoteMap().get_vote_overview()
if vote_results:
return [(m, v) for m, v in vote_results.items()]
else:
return []
return VoteMap().get_vote_overview()


def format_by_line_length(possible_votes, max_length=50):
Expand Down Expand Up @@ -217,6 +213,7 @@ def format_map_vote(format_type="line"):
if not selection:
return ""

selection = [maps.parse_layer(map) for map in selection]
# 0: map 1, 1: map 2, etc.
vote_dict = numbered_maps(selection)
# map 1: 0, map 2: 1, etc.
Expand Down
4 changes: 2 additions & 2 deletions rcon/routines.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ def run():
while True:
try:
toggle_votekick(rcon)
VoteMap().vote_map_reminder(rcon)
except HLLCommandFailedError:
VoteMap().send_reminder()
except CommandFailedError:
max_fails -= 1
if max_fails <= 0:
logger.exception("Routines 5 failures in a row. Stopping")
Expand Down
43 changes: 33 additions & 10 deletions rcon/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ class DBLogLineType(TypedDict):
server: str
weapon: Optional[str]

class Player(TypedDict):
id: str
name: str

class PlayerTeamConfidence(enum.Enum):
STRONG = "strong"
Expand Down Expand Up @@ -746,22 +749,42 @@ class VipId(TypedDict):
player_id: str
name: str


class VoteMapPlayerVoteType(TypedDict):
class VoteMapVote(TypedDict):
player_id: str
player_name: str
map_name: str
map_id: str
vote_count: int

class VoteMapVoter(TypedDict):
player_id: str
player_name: str
count: int

class VoteMapResultType(TypedDict):
class VoteMapMapResult(TypedDict):
map: Layer
num_votes: int
voters: list[VoteMapVoter]
votes_count: int

class VoteMapPlayerChoice(TypedDict):
player_name: str
player_id: str

# TODO: finish this typing
class VoteMapStatusType(TypedDict):
map: Layer
voters: dict[Layer, list[str]]
class VoteMapStatus(TypedDict):
enabled: bool
paused: bool
results: list[VoteMapMapResult]
next_map: Optional[str]
last_reminder: Optional[datetime.datetime]
player_choice: Optional[VoteMapPlayerChoice]

class VoteMapHistoryResult(TypedDict):
map_id: str
votes_count: int

class VoteMapHistory(TypedDict):
ts: int
map_id: str
results: list[VoteMapHistoryResult]

# Have to inherit from str to allow for JSON serialization w/ pydantic
class AllLogTypes(str, enum.Enum):
Expand Down Expand Up @@ -854,7 +877,7 @@ class PublicInfoType(TypedDict):
player_count_by_team: PublicInfoPlayerType
score: PublicInfoScoreType
time_remaining: float
vote_status: list[VoteMapStatusType]
vote_status: VoteMapStatus
name: PublicInfoNameType


Expand Down
Loading
Loading