diff --git a/rcon/api_commands.py b/rcon/api_commands.py index 8179a2c22..ecaf970ae 100644 --- a/rcon/api_commands.py +++ b/rcon/api_commands.py @@ -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, @@ -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() @@ -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)) diff --git a/rcon/commands.py b/rcon/commands.py index d3d390133..c7cb0a934 100644 --- a/rcon/commands.py +++ b/rcon/commands.py @@ -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 @@ -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]: @@ -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 diff --git a/rcon/maps.py b/rcon/maps.py index 0380e2faf..b9b73c059 100644 --- a/rcon/maps.py +++ b/rcon/maps.py @@ -32,6 +32,7 @@ class MapType(typing_extensions.TypedDict): shortname: str allies: "Faction" axis: "Faction" + orientation: str class LayerType(typing_extensions.TypedDict): @@ -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" @@ -121,6 +126,7 @@ class Map(pydantic.BaseModel): shortname: str allies: "Faction" axis: "Faction" + orientation: Orientation def __str__(self) -> str: return self.id @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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, ), ) } @@ -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: @@ -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) diff --git a/rcon/rcon.py b/rcon/rcon.py index 36723ea30..65d6fc54e 100644 --- a/rcon/rcon.py +++ b/rcon/rcon.py @@ -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 @@ -19,6 +20,7 @@ from rcon.settings import SERVER_INFO from rcon.types import ( AdminType, + GameLayoutRandomConstraints, GameServerBanType, GameState, GetDetailedPlayer, @@ -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: diff --git a/rcon/types.py b/rcon/types.py index ec25b80cc..f84ceeba9 100644 --- a/rcon/types.py +++ b/rcon/types.py @@ -770,3 +770,8 @@ class DjangoUserPermissions(TypedDict): permissions: list[DjangoPermission] groups: list[DjangoGroup] is_superuser: bool + + +class GameLayoutRandomConstraints(enum.IntFlag): + ALWAYS_ADJACENT = enum.auto() + ALWAYS_DIAGONAL = enum.auto() diff --git a/rcon/vote_map.py b/rcon/vote_map.py index f6d3be036..8771000e2 100644 --- a/rcon/vote_map.py +++ b/rcon/vote_map.py @@ -127,9 +127,15 @@ def _suggest_next_maps( last_n_maps = set(m for m in maps_history[:exclude_last_n]) else: last_n_maps: set[maps.Layer] = set() - logger.info("Excluding last %s player maps: %s", exclude_last_n, last_n_maps) + logger.info( + "Excluding last %s player maps: %s", + exclude_last_n, + [m.pretty_name for m in last_n_maps], + ) remaining_maps = [maps.parse_layer(m) for m in allowed_maps - last_n_maps] - logger.info("Remaining maps to suggest from: %s", remaining_maps) + logger.info( + "Remaining maps to suggest from: %s", [m.pretty_name for m in remaining_maps] + ) current_side = current_map.attackers @@ -142,7 +148,10 @@ def _suggest_next_maps( remaining_maps = [ maps.parse_layer(m) for m in remaining_maps if m.map not in map_ids ] - logger.info("Remaining maps to suggest from: %s", remaining_maps) + logger.info( + "Remaining maps to suggest from: %s", + [m.pretty_name for m in remaining_maps], + ) if not allow_consecutive_offensives_of_opposite_side and current_side: # TODO: make sure this is correct @@ -155,7 +164,10 @@ def _suggest_next_maps( "Not allowing consecutive offensive with opposite side: %s", maps.get_opposite_side(current_side), ) - logger.info("Remaining maps to suggest from: %s", remaining_maps) + logger.info( + "Remaining maps to suggest from: %s", + [m.pretty_name for m in remaining_maps], + ) # Handle case if all maps got excluded categorized_maps = categorize_maps(remaining_maps) @@ -197,7 +209,7 @@ def _suggest_next_maps( logger.error("No maps can be suggested with the given parameters.") raise RestrictiveFilterError("Unable to suggest map") - logger.info("Suggestion %s", selection) + logger.info("Suggestion %s", [m.pretty_name for m in selection]) return selection @@ -684,7 +696,7 @@ def gen_selection(self): ) self.set_selection(selection=selection) - logger.info("Saved new selection: %s", selection) + logger.info("Saved new selection: %s", [m.pretty_name for m in selection]) def _get_selection(self) -> list[str]: """Return the current map suggestions""" diff --git a/rcongui/package-lock.json b/rcongui/package-lock.json index 7f2b0beda..803664daf 100644 --- a/rcongui/package-lock.json +++ b/rcongui/package-lock.json @@ -23,6 +23,7 @@ "@testing-library/user-event": "^7.1.2", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", + "clsx": "^2.1.1", "country-list": "^2.2.0", "emoji-mart": "^3.0.0", "faker": "^4.1.0", @@ -2565,6 +2566,14 @@ } } }, + "node_modules/@material-ui/core/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@material-ui/icons": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz", @@ -2615,6 +2624,14 @@ } } }, + "node_modules/@material-ui/lab/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@material-ui/pickers": { "version": "3.2.10", "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz", @@ -2635,6 +2652,14 @@ "react-dom": "^16.8.4" } }, + "node_modules/@material-ui/pickers/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@material-ui/styles": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.2.tgz", @@ -2676,6 +2701,14 @@ } } }, + "node_modules/@material-ui/styles/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@material-ui/system": { "version": "4.11.2", "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.2.tgz", @@ -3678,9 +3711,9 @@ "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" }, "node_modules/clsx": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", - "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -5041,6 +5074,14 @@ "react-dom": "^16.8.0" } }, + "node_modules/mui-datatables/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -5637,6 +5678,14 @@ "react-dom": "^15.3.0 || ^16.0.0-alpha" } }, + "node_modules/react-virtualized/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/rcongui/package.json b/rcongui/package.json index 1f907b972..00d8ac29b 100644 --- a/rcongui/package.json +++ b/rcongui/package.json @@ -18,6 +18,7 @@ "@testing-library/user-event": "^7.1.2", "chart.js": "^3.9.1", "chartjs-adapter-moment": "^1.0.0", + "clsx": "^2.1.1", "country-list": "^2.2.0", "emoji-mart": "^3.0.0", "faker": "^4.1.0", diff --git a/rcongui/public/tac-maps/carentan.webp b/rcongui/public/tac-maps/carentan.webp new file mode 100644 index 000000000..f4c309622 Binary files /dev/null and b/rcongui/public/tac-maps/carentan.webp differ diff --git a/rcongui/public/tac-maps/driel.webp b/rcongui/public/tac-maps/driel.webp new file mode 100644 index 000000000..e0bc11449 Binary files /dev/null and b/rcongui/public/tac-maps/driel.webp differ diff --git a/rcongui/public/tac-maps/elalamein.webp b/rcongui/public/tac-maps/elalamein.webp new file mode 100644 index 000000000..5a78b7e5f Binary files /dev/null and b/rcongui/public/tac-maps/elalamein.webp differ diff --git a/rcongui/public/tac-maps/foy.webp b/rcongui/public/tac-maps/foy.webp new file mode 100644 index 000000000..a512ecbb5 Binary files /dev/null and b/rcongui/public/tac-maps/foy.webp differ diff --git a/rcongui/public/tac-maps/hill400.webp b/rcongui/public/tac-maps/hill400.webp new file mode 100644 index 000000000..6e95b2fc2 Binary files /dev/null and b/rcongui/public/tac-maps/hill400.webp differ diff --git a/rcongui/public/tac-maps/hurtgenforrest.webp b/rcongui/public/tac-maps/hurtgenforrest.webp new file mode 100644 index 000000000..fcdf30c7b Binary files /dev/null and b/rcongui/public/tac-maps/hurtgenforrest.webp differ diff --git a/rcongui/public/tac-maps/kharkov.webp b/rcongui/public/tac-maps/kharkov.webp new file mode 100644 index 000000000..06f61364c Binary files /dev/null and b/rcongui/public/tac-maps/kharkov.webp differ diff --git a/rcongui/public/tac-maps/kursk.webp b/rcongui/public/tac-maps/kursk.webp new file mode 100644 index 000000000..85a7f26d6 Binary files /dev/null and b/rcongui/public/tac-maps/kursk.webp differ diff --git a/rcongui/public/tac-maps/mortain.webp b/rcongui/public/tac-maps/mortain.webp new file mode 100644 index 000000000..323cdefcd Binary files /dev/null and b/rcongui/public/tac-maps/mortain.webp differ diff --git a/rcongui/public/tac-maps/omahabeach.webp b/rcongui/public/tac-maps/omahabeach.webp new file mode 100644 index 000000000..436f0239a Binary files /dev/null and b/rcongui/public/tac-maps/omahabeach.webp differ diff --git a/rcongui/public/tac-maps/purpleheartlane.webp b/rcongui/public/tac-maps/purpleheartlane.webp new file mode 100644 index 000000000..c3a3103ef Binary files /dev/null and b/rcongui/public/tac-maps/purpleheartlane.webp differ diff --git a/rcongui/public/tac-maps/remagen.webp b/rcongui/public/tac-maps/remagen.webp new file mode 100644 index 000000000..279dd548b Binary files /dev/null and b/rcongui/public/tac-maps/remagen.webp differ diff --git a/rcongui/public/tac-maps/stalingrad.webp b/rcongui/public/tac-maps/stalingrad.webp new file mode 100644 index 000000000..03fde2c91 Binary files /dev/null and b/rcongui/public/tac-maps/stalingrad.webp differ diff --git a/rcongui/public/tac-maps/stmariedumont.webp b/rcongui/public/tac-maps/stmariedumont.webp new file mode 100644 index 000000000..5b6ccd654 Binary files /dev/null and b/rcongui/public/tac-maps/stmariedumont.webp differ diff --git a/rcongui/public/tac-maps/stmereeglise.webp b/rcongui/public/tac-maps/stmereeglise.webp new file mode 100644 index 000000000..44012c066 Binary files /dev/null and b/rcongui/public/tac-maps/stmereeglise.webp differ diff --git a/rcongui/public/tac-maps/utahbeach.webp b/rcongui/public/tac-maps/utahbeach.webp new file mode 100644 index 000000000..45a814052 Binary files /dev/null and b/rcongui/public/tac-maps/utahbeach.webp differ diff --git a/rcongui/src/App.css b/rcongui/src/App.css index 74b5e0534..ad1a806f0 100644 --- a/rcongui/src/App.css +++ b/rcongui/src/App.css @@ -1,5 +1,7 @@ .App { - text-align: center; + min-height: 100vh; + display: flex; + flex-direction: column; } .App-logo { diff --git a/rcongui/src/App.js b/rcongui/src/App.js index 65754e888..3a212e54f 100644 --- a/rcongui/src/App.js +++ b/rcongui/src/App.js @@ -57,6 +57,7 @@ import { } from "./components/UserSettings/miscellaneous"; import BlacklistRecords from "./components/Blacklist/BlacklistRecords"; import BlacklistLists from "./components/Blacklist/BlacklistLists"; +import { MapManager } from "./components/MapManager/map-manager"; const Live = ({ classes }) => { const [mdSize, setMdSize] = React.useState(6); @@ -399,7 +400,7 @@ function App() { const Router = isEmbed ? BrowserRouter : HashRouter; return ( -
+
{isEmbed ? "" : } @@ -412,56 +413,56 @@ function App() { )} - - - - - - - - - - - - - - - - - - - - - - - {!process.env.REACT_APP_PUBLIC_BUILD ? ( - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + {!process.env.REACT_APP_PUBLIC_BUILD ? ( + + + + + + + + + + + + + + - - - - - - - - + - + @@ -476,6 +477,7 @@ function App() { + - - - - - + + + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - - - - + + + + + + + + + + + + + Players + + + + + + Historical Logs + + + - - - - - - - Players - - - - - - Historical Logs - - - - - - - ) : ( - "" - )} - + + + ) : ( + "" + )} + +
{isEmbed ? "" :