Skip to content

Commit

Permalink
HLL U15.2 support - Game layout change & new maps (#634)
Browse files Browse the repository at this point in the history
* Add backend support for game layout changes
* fix teamview infinite interval, nav menu refactored
* header responsivness, map rotation page styling
* votemap styling changes
* Add blacklist permissions to default groups
* Add game layout permission
* Votemap-config page
* Add missing HTTP methods for new endpoints
* Add django migration for new permissions
* Change map page
* update gamelayout command and error when game mode does not support cap layouts
* Handle get objectiverow_N during skirmish errors
* Fix error when using get_objective_row via API
* Have set_game_layout error message use zero-based indexing
* Add orientation to maps
* Objectives page (no backend connection)
* Objectives page added backend connection
* Log only map names not full Layer types
* Trigger votemap reset when enabled is toggled (previously in UI)
* Fixed selecting players to blacklist GameView
---------

Co-authored-by: Dorfieeee <[email protected]>
Co-authored-by: C. Eric Mathey <[email protected]>
Co-authored-by: Florian Schmidt <[email protected]>
  • Loading branch information
4 people authored Aug 29, 2024
1 parent 04279c6 commit 33a7de5
Show file tree
Hide file tree
Showing 59 changed files with 2,346 additions and 1,493 deletions.
15 changes: 14 additions & 1 deletion rcon/api_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,9 @@ def set_votemap_config(
reset_to_default: bool = False,
**kwargs,
) -> bool:
return self._validate_user_config(
old_config = VoteMapUserConfig.load_from_db()

res = self._validate_user_config(
by=by,
command_name=inspect.currentframe().f_code.co_name, # type: ignore
model=VoteMapUserConfig,
Expand All @@ -698,6 +700,14 @@ def set_votemap_config(
reset_to_default=reset_to_default,
)

new_config = VoteMapUserConfig.load_from_db()

# on -> off or off -> on
if old_config.enabled != new_config:
self.reset_votemap_state()

return True

def get_auto_broadcasts_config(self) -> AutoBroadcastUserConfig:
return AutoBroadcastUserConfig.load_from_db()

Expand Down Expand Up @@ -1736,3 +1746,6 @@ def get_date_scoreboard(self, start: int, end: int):
result = {}

return result

def get_objective_row(self, row: int):
return super().get_objective_row(int(row))
39 changes: 37 additions & 2 deletions rcon/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import time
from contextlib import contextmanager, nullcontext
from functools import wraps
from typing import Generator, List
from typing import Generator, List, Sequence

from rcon.connection import HLLConnection
from rcon.types import ServerInfoType, VipId
Expand Down Expand Up @@ -366,12 +366,30 @@ def _read_list(self, raw: bytes, conn: HLLConnection) -> list[str]:

@_auto_retry
def _get_list(
self, item: str, can_fail=True, conn: HLLConnection | None = None
self,
item: str,
can_fail=True,
conn: HLLConnection | None = None,
fail_msgs: Sequence[str] = [],
) -> list[str]:
if conn is None:
raise ValueError("conn parameter should never be None")

res = self._bytes_request(item, can_fail=can_fail, conn=conn)

# Some commands do not return "FAIL" when they fail. Normally we can handle them in the command implementation,
# but not with lists, where we need to do it here.
try:
decoded_res = res.decode()
except UnicodeDecodeError:
pass
else:
if decoded_res in fail_msgs:
if can_fail:
raise CommandFailedError(decoded_res)
else:
raise HLLServerError(f"Got FAIL for {item}: {decoded_res}")

return self._read_list(res, conn)

def get_profanities(self) -> list[str]:
Expand Down Expand Up @@ -719,6 +737,23 @@ def get_gamestate(self) -> list[str]:

return self._str_request("get gamestate", can_fail=False).split("\n")

def get_objective_row(self, row: int):
if not (0 <= row <= 4):
raise ValueError('Row must be between 0 and 4')

return self._get_list(
f'get objectiverow_{row}',
fail_msgs="Cannot execute command for this gamemode."
)

def set_game_layout(self, objectives: Sequence[str]):
if len(objectives) != 5:
raise ValueError("5 objectives must be provided")
self._str_request(
f'gamelayout "{objectives[0]}" "{objectives[1]}" "{objectives[2]}" "{objectives[3]}" "{objectives[4]}"', log_info=True,
can_fail=False
)
return list(objectives)

if __name__ == "__main__":
from rcon.settings import SERVER_INFO
Expand Down
25 changes: 25 additions & 0 deletions rcon/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class MapType(typing_extensions.TypedDict):
shortname: str
allies: "Faction"
axis: "Faction"
orientation: str


class LayerType(typing_extensions.TypedDict):
Expand All @@ -57,6 +58,10 @@ class FactionType(typing_extensions.TypedDict):
# interacts with the CRCON API and enables much easier parsing of results
# for example where result is a plain python dictionary containing a serialized Layer:
# Layer.model_validate(result)
class Orientation(str, Enum):
HORIZONTAL = "horizontal"
VERTICAL = "vertical"

class GameMode(str, Enum):
WARFARE = "warfare"
OFFENSIVE = "offensive"
Expand Down Expand Up @@ -121,6 +126,7 @@ class Map(pydantic.BaseModel):
shortname: str
allies: "Faction"
axis: "Faction"
orientation: Orientation

def __str__(self) -> str:
return self.id
Expand Down Expand Up @@ -226,6 +232,7 @@ def image_name(self) -> str:
shortname=UNKNOWN_MAP_NAME,
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="stmereeglise",
Expand All @@ -235,6 +242,7 @@ def image_name(self) -> str:
shortname="SME",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.HORIZONTAL,
),
Map(
id="stmariedumont",
Expand All @@ -244,6 +252,7 @@ def image_name(self) -> str:
shortname="SMDM",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="utahbeach",
Expand All @@ -253,6 +262,7 @@ def image_name(self) -> str:
shortname="Utah",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="omahabeach",
Expand All @@ -262,6 +272,7 @@ def image_name(self) -> str:
shortname="Omaha",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="purpleheartlane",
Expand All @@ -271,6 +282,7 @@ def image_name(self) -> str:
shortname="PHL",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="carentan",
Expand All @@ -280,6 +292,7 @@ def image_name(self) -> str:
shortname="Carentan",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.HORIZONTAL,
),
Map(
id="hurtgenforest",
Expand All @@ -289,6 +302,7 @@ def image_name(self) -> str:
shortname="Hurtgen",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.HORIZONTAL,
),
Map(
id="hill400",
Expand All @@ -298,6 +312,7 @@ def image_name(self) -> str:
shortname="Hill 400",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.HORIZONTAL,
),
Map(
id="foy",
Expand All @@ -307,6 +322,7 @@ def image_name(self) -> str:
shortname="Foy",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="kursk",
Expand All @@ -316,6 +332,7 @@ def image_name(self) -> str:
shortname="Kursk",
allies=Faction(name=FactionName.RUS.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="stalingrad",
Expand All @@ -325,6 +342,7 @@ def image_name(self) -> str:
shortname="Stalingrad",
allies=Faction(name=FactionName.RUS.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.HORIZONTAL,
),
Map(
id="remagen",
Expand All @@ -334,6 +352,7 @@ def image_name(self) -> str:
shortname="Remagen",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="kharkov",
Expand All @@ -343,6 +362,7 @@ def image_name(self) -> str:
shortname="Kharkov",
allies=Faction(name=FactionName.RUS.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="driel",
Expand All @@ -352,6 +372,7 @@ def image_name(self) -> str:
shortname="Driel",
allies=Faction(name=FactionName.GB.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
),
Map(
id="elalamein",
Expand All @@ -361,6 +382,7 @@ def image_name(self) -> str:
shortname="Alamein",
allies=Faction(name=FactionName.GB.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.HORIZONTAL,
),
Map(
id="mortain",
Expand All @@ -370,6 +392,7 @@ def image_name(self) -> str:
shortname="MOR",
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.HORIZONTAL,
),
)
}
Expand Down Expand Up @@ -920,6 +943,7 @@ def parse_layer(layer_name: str | Layer) -> Layer:
shortname=tag,
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
)

try:
Expand Down Expand Up @@ -967,6 +991,7 @@ def _parse_legacy_layer(layer_name: str):
shortname=name.capitalize(),
allies=Faction(name=FactionName.US.value, team=Team.ALLIES),
axis=Faction(name=FactionName.GER.value, team=Team.AXIS),
orientation=Orientation.VERTICAL,
)

result = Layer(id=layer_name, map=map_, game_mode=GameMode.WARFARE)
Expand Down
94 changes: 93 additions & 1 deletion rcon/rcon.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import logging
import os
import random
import re
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta
from functools import cached_property
from itertools import chain
from time import sleep
from typing import Any, Iterable, Literal, Optional, overload
from typing import Any, Iterable, List, Literal, Optional, Sequence, overload

from dateutil import parser

Expand All @@ -19,6 +20,7 @@
from rcon.settings import SERVER_INFO
from rcon.types import (
AdminType,
GameLayoutRandomConstraints,
GameServerBanType,
GameState,
GetDetailedPlayer,
Expand Down Expand Up @@ -1309,6 +1311,96 @@ 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)
]

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)):
if isinstance(obj, str):
# Verify whether the objective exists, or if it can be logically determined
if obj in obj_row:
parsed_objs.append(obj)
elif obj in ("left", "top"):
parsed_objs.append(obj_row[0])
elif obj in ("center", "mid"):
parsed_objs.append(obj_row[1])
elif obj in ("right", "bottom"):
parsed_objs.append(obj_row[2])
else:
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))
parsed_objs.append(obj_row[obj])

elif obj is None:
# Choose randomly
if random_constraints:
# Sort later
parsed_objs.append(None)
else:
# No constraints, no need for a special algorithm
parsed_objs.append(random.choice(obj_row))

# Special algorithm to apply randomness with constraints. Prioritizes rows with already
# determined neighbors to avoid conflicts in situations with two neighbors.
while None in parsed_objs:
# If no rows are predetermined, determine the middle row first
if all(obj is None for obj in parsed_objs):
parsed_objs[2] = random.choice(obj_rows[2])

for row, obj in enumerate(parsed_objs):
# Skip if row is already determined
if obj is not None:
continue

# Get the indices of the objectives of neighboring rows, if they are already determined
neighbors = []
if row > 0 and parsed_objs[row - 1] is not None:
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

# Create a list of available objectives
obj_row = obj_rows[row]
obj_choices = obj_row.copy()
for neighbor_idx in neighbors:
# Apply constraints
if random_constraints & GameLayoutRandomConstraints.ALWAYS_ADJACENT:
# Cannot have two objectives follow each other up on opposite sides of the map
if neighbor_idx == 0:
obj_choices[2] = None
elif neighbor_idx == 2:
obj_choices[0] = None
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
)

return super().set_game_layout(parsed_objs)

@staticmethod
def parse_log_line(raw_line: str) -> StructuredLogLineType:
Expand Down
Loading

0 comments on commit 33a7de5

Please sign in to comment.