mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Improve Layout loading and ForceGroup generation
- fix layout not preserving the correct group index - fix ForceGroup generation merging preset_groups with generics
This commit is contained in:
parent
72682e4db3
commit
50b82f6383
@ -35,12 +35,6 @@ class ArmedForces:
|
||||
"""Initialize the ArmedForces for the given faction.
|
||||
This will create a ForceGroup for each generic Layout and PresetGroup"""
|
||||
|
||||
# Initialize with preset_groups from the faction
|
||||
self.forces = [
|
||||
preset_group.initialize_for_faction(faction)
|
||||
for preset_group in faction.preset_groups
|
||||
]
|
||||
|
||||
# Generate ForceGroup for all generic layouts by iterating over
|
||||
# all layouts which are usable by the given faction.
|
||||
for layout in LAYOUTS.layouts:
|
||||
@ -48,6 +42,10 @@ class ArmedForces:
|
||||
# Creates a faction compatible GorceGroup
|
||||
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
|
||||
|
||||
# Add all preset groups afterwards to prevent them being merged with generics
|
||||
for preset_group in faction.preset_groups:
|
||||
self.forces.append(preset_group.initialize_for_faction(faction))
|
||||
|
||||
def groups_for_task(self, group_task: GroupTask) -> Iterator[ForceGroup]:
|
||||
for force_group in self.forces:
|
||||
if group_task in force_group.tasks:
|
||||
@ -59,7 +57,7 @@ class ArmedForces:
|
||||
for group in self.groups_for_task(task):
|
||||
if group not in groups:
|
||||
groups.append(group)
|
||||
return groups
|
||||
return sorted(groups, key=lambda g: g.name)
|
||||
|
||||
def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]:
|
||||
unit_groups = list(self.groups_for_task(group_task))
|
||||
|
||||
@ -21,7 +21,7 @@ from game.theater.theatergroundobject import (
|
||||
NavalGroundObject,
|
||||
)
|
||||
from game.layout import LAYOUTS
|
||||
from game.layout.layout import TgoLayout, TgoLayoutGroup
|
||||
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
|
||||
from game.utils import escape_string_for_lua
|
||||
@ -67,10 +67,10 @@ class ForceGroup:
|
||||
"""
|
||||
units: set[UnitType[Any]] = set()
|
||||
statics: set[Type[DcsUnitType]] = set()
|
||||
for group in layout.all_groups:
|
||||
if group.optional and not group.fill:
|
||||
for unit_group in layout.all_unit_groups:
|
||||
if unit_group.optional and not unit_group.fill:
|
||||
continue
|
||||
for unit_type in group.possible_types_for_faction(faction):
|
||||
for unit_type in unit_group.possible_types_for_faction(faction):
|
||||
if issubclass(unit_type, VehicleType):
|
||||
units.add(next(GroundUnitType.for_dcs_type(unit_type)))
|
||||
elif issubclass(unit_type, ShipType):
|
||||
@ -89,11 +89,11 @@ class ForceGroup:
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def has_unit_for_layout_group(self, group: TgoLayoutGroup) -> bool:
|
||||
def has_unit_for_layout_group(self, unit_group: TgoLayoutUnitGroup) -> bool:
|
||||
for unit in self.units:
|
||||
if (
|
||||
unit.dcs_unit_type in group.unit_types
|
||||
or unit.unit_class in group.unit_classes
|
||||
unit.dcs_unit_type in unit_group.unit_types
|
||||
or unit.unit_class in unit_group.unit_classes
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@ -102,9 +102,9 @@ class ForceGroup:
|
||||
"""Initialize a ForceGroup for the given Faction.
|
||||
This adds accessible units to LayoutGroups with the fill property"""
|
||||
for layout in self.layouts:
|
||||
for group in layout.all_groups:
|
||||
if group.fill and not self.has_unit_for_layout_group(group):
|
||||
for unit_type in group.possible_types_for_faction(faction):
|
||||
for unit_group in layout.all_unit_groups:
|
||||
if unit_group.fill and not self.has_unit_for_layout_group(unit_group):
|
||||
for unit_type in unit_group.possible_types_for_faction(faction):
|
||||
if issubclass(unit_type, VehicleType):
|
||||
self.units.append(
|
||||
next(GroundUnitType.for_dcs_type(unit_type))
|
||||
@ -138,38 +138,44 @@ class ForceGroup:
|
||||
)
|
||||
|
||||
def dcs_unit_types_for_group(
|
||||
self, group: TgoLayoutGroup
|
||||
self, unit_group: TgoLayoutUnitGroup
|
||||
) -> list[Type[DcsUnitType]]:
|
||||
"""Return all available DCS Unit Types which can be used in the given
|
||||
TgoLayoutGroup"""
|
||||
unit_types = [t for t in group.unit_types if self.has_access_to_dcs_type(t)]
|
||||
unit_types = [
|
||||
t for t in unit_group.unit_types if self.has_access_to_dcs_type(t)
|
||||
]
|
||||
|
||||
alternative_types = []
|
||||
for accessible_unit in self.units:
|
||||
if accessible_unit.unit_class in group.unit_classes:
|
||||
if accessible_unit.unit_class in unit_group.unit_classes:
|
||||
unit_types.append(accessible_unit.dcs_unit_type)
|
||||
if accessible_unit.unit_class in group.fallback_classes:
|
||||
if accessible_unit.unit_class in unit_group.fallback_classes:
|
||||
alternative_types.append(accessible_unit.dcs_unit_type)
|
||||
|
||||
return unit_types or alternative_types
|
||||
|
||||
def unit_types_for_group(self, group: TgoLayoutGroup) -> Iterator[UnitType[Any]]:
|
||||
for dcs_type in self.dcs_unit_types_for_group(group):
|
||||
def unit_types_for_group(
|
||||
self, unit_group: TgoLayoutUnitGroup
|
||||
) -> Iterator[UnitType[Any]]:
|
||||
for dcs_type in self.dcs_unit_types_for_group(unit_group):
|
||||
if issubclass(dcs_type, VehicleType):
|
||||
yield next(GroundUnitType.for_dcs_type(dcs_type))
|
||||
elif issubclass(dcs_type, ShipType):
|
||||
yield next(ShipUnitType.for_dcs_type(dcs_type))
|
||||
|
||||
def statics_for_group(self, group: TgoLayoutGroup) -> Iterator[Type[DcsUnitType]]:
|
||||
for dcs_type in self.dcs_unit_types_for_group(group):
|
||||
def statics_for_group(
|
||||
self, unit_group: TgoLayoutUnitGroup
|
||||
) -> Iterator[Type[DcsUnitType]]:
|
||||
for dcs_type in self.dcs_unit_types_for_group(unit_group):
|
||||
if issubclass(dcs_type, StaticType):
|
||||
yield dcs_type
|
||||
|
||||
def random_dcs_unit_type_for_group(
|
||||
self, group: TgoLayoutGroup
|
||||
self, unit_group: TgoLayoutUnitGroup
|
||||
) -> Type[DcsUnitType]:
|
||||
"""Return random DCS Unit Type which can be used in the given TgoLayoutGroup"""
|
||||
return random.choice(self.dcs_unit_types_for_group(group))
|
||||
return random.choice(self.dcs_unit_types_for_group(unit_group))
|
||||
|
||||
def merge_group(self, new_group: ForceGroup) -> None:
|
||||
"""Merge the group with another similar group."""
|
||||
@ -204,22 +210,22 @@ class ForceGroup:
|
||||
"""Create a TheaterGroundObject for the given template"""
|
||||
go = layout.create_ground_object(name, location, control_point)
|
||||
# Generate all groups using the randomization if it defined
|
||||
for group_name, groups in layout.groups.items():
|
||||
for group in groups:
|
||||
for tgo_group in layout.groups:
|
||||
for unit_group in tgo_group.unit_groups:
|
||||
# Choose a random unit_type for the group
|
||||
try:
|
||||
unit_type = self.random_dcs_unit_type_for_group(group)
|
||||
unit_type = self.random_dcs_unit_type_for_group(unit_group)
|
||||
except IndexError:
|
||||
if group.optional:
|
||||
if unit_group.optional:
|
||||
# If group is optional it is ok when no unit_type is available
|
||||
continue
|
||||
# if non-optional this is a error
|
||||
raise RuntimeError(
|
||||
f"No accessible unit for {self.name} - {group.name}"
|
||||
f"No accessible unit for {self.name} - {unit_group.name}"
|
||||
)
|
||||
tgo_group_name = f"{name} ({group_name})"
|
||||
tgo_group_name = f"{name} ({tgo_group.group_name})"
|
||||
self.create_theater_group_for_tgo(
|
||||
go, group, tgo_group_name, game, unit_type
|
||||
go, unit_group, tgo_group_name, game, unit_type
|
||||
)
|
||||
|
||||
return go
|
||||
@ -227,7 +233,7 @@ class ForceGroup:
|
||||
def create_theater_group_for_tgo(
|
||||
self,
|
||||
ground_object: TheaterGroundObject,
|
||||
group: TgoLayoutGroup,
|
||||
unit_group: TgoLayoutUnitGroup,
|
||||
group_name: str,
|
||||
game: Game,
|
||||
unit_type: Type[DcsUnitType],
|
||||
@ -237,12 +243,12 @@ class ForceGroup:
|
||||
# Random UnitCounter if not forced
|
||||
if unit_count is None:
|
||||
# Choose a random group_size based on the layouts unit_count
|
||||
unit_count = group.group_size
|
||||
unit_count = unit_group.group_size
|
||||
if unit_count == 0:
|
||||
# No units to be created so dont create a theater group for them
|
||||
return
|
||||
# Generate Units
|
||||
units = group.generate_units(ground_object, unit_type, unit_count)
|
||||
units = unit_group.generate_units(ground_object, unit_type, unit_count)
|
||||
# Get or create the TheaterGroup
|
||||
ground_group = ground_object.group_by_name(group_name)
|
||||
if ground_group is not None:
|
||||
@ -261,9 +267,9 @@ class ForceGroup:
|
||||
):
|
||||
# Recreate the TheaterGroup as IadsGroundGroup
|
||||
ground_group = IadsGroundGroup.from_group(ground_group)
|
||||
if group.sub_task is not None:
|
||||
if unit_group.sub_task is not None:
|
||||
# Use the special sub_task of the TheaterGroup
|
||||
iads_task = group.sub_task
|
||||
iads_task = unit_group.sub_task
|
||||
else:
|
||||
# Use the primary task of the ForceGroup
|
||||
iads_task = self.tasks[0]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from .layout import TgoLayout, TgoLayoutGroup
|
||||
from .layout import TgoLayout, TgoLayoutGroup, TgoLayoutUnitGroup
|
||||
from .layoutloader import LayoutLoader
|
||||
|
||||
LAYOUTS = LayoutLoader()
|
||||
|
||||
@ -60,6 +60,21 @@ class LayoutUnit:
|
||||
|
||||
@dataclass
|
||||
class TgoLayoutGroup:
|
||||
"""The layout of a group which will generate a DCS group later. The TgoLayoutGroup has one or many TgoLayoutUnitGroup which represents a set of unit of the same type. Therefore the TgoLayoutGroup is a logical grouping of different unit_types. A TgoLayout can have one or many TgoLayoutGroup"""
|
||||
|
||||
# The group name which will be used later as the DCS group name
|
||||
group_name: str
|
||||
|
||||
# The index of the group within the TgoLayout. Used to preserve that the order of
|
||||
# the groups generated match the order defined in the layout yaml.
|
||||
group_index: int
|
||||
|
||||
# List of all connected TgoLayoutUnitGroup
|
||||
unit_groups: list[TgoLayoutUnitGroup] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TgoLayoutUnitGroup:
|
||||
"""The layout of a single type of unit within the TgoLayout
|
||||
|
||||
Each DCS group that is spawned in the mission is composed of one or more
|
||||
@ -95,6 +110,9 @@ class TgoLayoutGroup:
|
||||
unit_classes: list[UnitClass] = field(default_factory=list)
|
||||
fallback_classes: list[UnitClass] = field(default_factory=list)
|
||||
|
||||
# The index of the TgoLayoutGroup within the Layout
|
||||
unit_index: int = field(default_factory=int)
|
||||
|
||||
# Allows a group to have a special SubTask (PointDefence for example)
|
||||
sub_task: Optional[GroupTask] = None
|
||||
|
||||
@ -172,21 +190,13 @@ class TgoLayout:
|
||||
self.description = description
|
||||
self.tasks: list[GroupTask] = [] # The supported
|
||||
|
||||
# Mapping of group name and LayoutGroups for a specific TgoGroup
|
||||
# A Group can have multiple TgoLayoutGroups which get merged together
|
||||
self.groups: dict[str, list[TgoLayoutGroup]] = defaultdict(list)
|
||||
# All TgoGroups this layout has.
|
||||
self.groups: list[TgoLayoutGroup] = []
|
||||
|
||||
# A generic layout will be used to create generic ForceGroups during the
|
||||
# campaign initialization. For each generic layout a new Group will be created.
|
||||
self.generic: bool = False
|
||||
|
||||
def add_layout_group(
|
||||
self, name: str, group: TgoLayoutGroup, index: int = 0
|
||||
) -> None:
|
||||
"""Adds the layout group to the group dict at the given index"""
|
||||
# If the index is greater than the actual len it will add it after the last item
|
||||
self.groups[name].insert(min(len(self.groups[name]), index), group)
|
||||
|
||||
def usable_by_faction(self, faction: Faction) -> bool:
|
||||
# Special handling for Buildings
|
||||
if (
|
||||
@ -199,7 +209,7 @@ class TgoLayout:
|
||||
try:
|
||||
return all(
|
||||
len(group.possible_types_for_faction(faction)) > 0
|
||||
for group in self.all_groups
|
||||
for group in self.all_unit_groups
|
||||
if not group.optional
|
||||
)
|
||||
except LayoutException:
|
||||
@ -219,9 +229,9 @@ class TgoLayout:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def all_groups(self) -> Iterator[TgoLayoutGroup]:
|
||||
for groups in self.groups.values():
|
||||
yield from groups
|
||||
def all_unit_groups(self) -> Iterator[TgoLayoutUnitGroup]:
|
||||
for group in self.groups:
|
||||
yield from group.unit_groups
|
||||
|
||||
|
||||
class AntiAirLayout(TgoLayout):
|
||||
|
||||
@ -18,6 +18,7 @@ from game.data.groups import GroupRole
|
||||
from game.layout.layout import (
|
||||
TgoLayout,
|
||||
TgoLayoutGroup,
|
||||
TgoLayoutUnitGroup,
|
||||
LayoutUnit,
|
||||
AntiAirLayout,
|
||||
BuildingLayout,
|
||||
@ -29,10 +30,10 @@ from game.layout.layoutmapping import LayoutMapping
|
||||
from game.profiling import logged_duration
|
||||
from game.version import VERSION
|
||||
|
||||
TEMPLATE_DIR = "resources/layouts/"
|
||||
TEMPLATE_DUMP = "Liberation/layouts.p"
|
||||
LAYOUT_DIR = "resources/layouts/"
|
||||
LAYOUT_DUMP = "Liberation/layouts.p"
|
||||
|
||||
TEMPLATE_TYPES = {
|
||||
LAYOUT_TYPES = {
|
||||
GroupRole.AIR_DEFENSE: AntiAirLayout,
|
||||
GroupRole.BUILDING: BuildingLayout,
|
||||
GroupRole.NAVAL: NavalLayout,
|
||||
@ -62,7 +63,7 @@ class LayoutLoader:
|
||||
"""This will load all pre-loaded layouts from a pickle file.
|
||||
If pickle can not be loaded it will import and dump the layouts"""
|
||||
# We use a pickle for performance reasons. Importing takes many seconds
|
||||
file = Path(persistency.base_path()) / TEMPLATE_DUMP
|
||||
file = Path(persistency.base_path()) / LAYOUT_DUMP
|
||||
if file.is_file():
|
||||
# Load from pickle if existing
|
||||
with file.open("rb") as f:
|
||||
@ -82,7 +83,7 @@ class LayoutLoader:
|
||||
self._layouts = {}
|
||||
mappings: dict[str, list[LayoutMapping]] = defaultdict(list)
|
||||
with logged_duration("Parsing mapping yamls"):
|
||||
for file in Path(TEMPLATE_DIR).rglob("*.yaml"):
|
||||
for file in Path(LAYOUT_DIR).rglob("*.yaml"):
|
||||
if not file.is_file():
|
||||
raise RuntimeError(f"{file.name} is not a file")
|
||||
with file.open("r", encoding="utf-8") as f:
|
||||
@ -95,11 +96,17 @@ class LayoutLoader:
|
||||
with ThreadPoolExecutor() as exe:
|
||||
exe.map(self._load_from_miz, mappings.keys(), mappings.values())
|
||||
|
||||
# Sort al the LayoutGroups with the correct index
|
||||
for layout in self._layouts.values():
|
||||
layout.groups.sort(key=lambda g: g.group_index)
|
||||
for group in layout.groups:
|
||||
group.unit_groups.sort(key=lambda ug: ug.unit_index)
|
||||
|
||||
logging.info(f"Imported {len(self._layouts)} layouts")
|
||||
self._dump_templates()
|
||||
|
||||
def _dump_templates(self) -> None:
|
||||
file = Path(persistency.base_path()) / TEMPLATE_DUMP
|
||||
file = Path(persistency.base_path()) / LAYOUT_DUMP
|
||||
dump = (VERSION, self._layouts)
|
||||
with file.open("wb") as fdata:
|
||||
pickle.dump(dump, fdata)
|
||||
@ -127,7 +134,7 @@ class LayoutLoader:
|
||||
):
|
||||
|
||||
try:
|
||||
g_id, group_name, group_mapping = mapping.group_for_name(
|
||||
g_id, u_id, group_name, group_mapping = mapping.group_for_name(
|
||||
dcs_group.name
|
||||
)
|
||||
except KeyError:
|
||||
@ -143,40 +150,46 @@ class LayoutLoader:
|
||||
layout = self._layouts.get(mapping.name, None)
|
||||
if layout is None:
|
||||
# Create a new template
|
||||
layout = TEMPLATE_TYPES[mapping.primary_role](
|
||||
layout = LAYOUT_TYPES[mapping.primary_role](
|
||||
mapping.name, mapping.description
|
||||
)
|
||||
layout.generic = mapping.generic
|
||||
layout.tasks = mapping.tasks
|
||||
self._layouts[layout.name] = layout
|
||||
|
||||
for i, unit in enumerate(dcs_group.units):
|
||||
group_layout = None
|
||||
for group in layout.all_groups:
|
||||
if group.name == group_mapping.name:
|
||||
unit_group = None
|
||||
for _unit_group in layout.all_unit_groups:
|
||||
if _unit_group.name == group_mapping.name:
|
||||
# We already have a layoutgroup for this dcs_group
|
||||
group_layout = group
|
||||
if not group_layout:
|
||||
group_layout = TgoLayoutGroup(
|
||||
unit_group = _unit_group
|
||||
if not unit_group:
|
||||
unit_group = TgoLayoutUnitGroup(
|
||||
group_mapping.name,
|
||||
[],
|
||||
group_mapping.unit_count,
|
||||
group_mapping.unit_types,
|
||||
group_mapping.unit_classes,
|
||||
group_mapping.fallback_classes,
|
||||
u_id,
|
||||
)
|
||||
group_layout.optional = group_mapping.optional
|
||||
group_layout.fill = group_mapping.fill
|
||||
group_layout.sub_task = group_mapping.sub_task
|
||||
# Add the group at the correct index
|
||||
layout.add_layout_group(group_name, group_layout, g_id)
|
||||
unit_group.optional = group_mapping.optional
|
||||
unit_group.fill = group_mapping.fill
|
||||
unit_group.sub_task = group_mapping.sub_task
|
||||
tgo_group = None
|
||||
for _tgo_group in layout.groups:
|
||||
if _tgo_group.group_name == group_name:
|
||||
tgo_group = _tgo_group
|
||||
if tgo_group is None:
|
||||
tgo_group = TgoLayoutGroup(group_name, g_id)
|
||||
layout.groups.append(tgo_group)
|
||||
tgo_group.unit_groups.append(unit_group)
|
||||
layout_unit = LayoutUnit.from_unit(unit)
|
||||
if i == 0 and layout.name not in template_position:
|
||||
template_position[layout.name] = unit.position
|
||||
layout_unit.position = (
|
||||
layout_unit.position - template_position[layout.name]
|
||||
)
|
||||
group_layout.layout_units.append(layout_unit)
|
||||
unit_group.layout_units.append(layout_unit)
|
||||
|
||||
def by_name(self, name: str) -> TgoLayout:
|
||||
self.initialize()
|
||||
|
||||
@ -128,9 +128,11 @@ class LayoutMapping:
|
||||
layout_file,
|
||||
)
|
||||
|
||||
def group_for_name(self, name: str) -> tuple[int, str, GroupLayoutMapping]:
|
||||
def group_for_name(self, name: str) -> tuple[int, int, str, GroupLayoutMapping]:
|
||||
g_id = 0
|
||||
for group_name, group_mappings in self.groups.items():
|
||||
for g_id, group_mapping in enumerate(group_mappings):
|
||||
for u_id, group_mapping in enumerate(group_mappings):
|
||||
if group_mapping.name == name or name in group_mapping.statics:
|
||||
return g_id, group_name, group_mapping
|
||||
return g_id, u_id, group_name, group_mapping
|
||||
g_id += 1
|
||||
raise KeyError
|
||||
|
||||
@ -25,7 +25,7 @@ from game.data.groups import GroupRole, GroupTask
|
||||
from game.layout.layout import (
|
||||
LayoutException,
|
||||
TgoLayout,
|
||||
TgoLayoutGroup,
|
||||
TgoLayoutUnitGroup,
|
||||
)
|
||||
from game.theater import TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
@ -38,7 +38,7 @@ from qt_ui.uiconstants import EVENT_ICONS
|
||||
|
||||
@dataclass
|
||||
class QTgoLayoutGroup:
|
||||
layout: TgoLayoutGroup
|
||||
layout: TgoLayoutUnitGroup
|
||||
dcs_unit_type: Type[UnitType]
|
||||
amount: int
|
||||
unit_price: int
|
||||
@ -63,7 +63,7 @@ class QTgoLayout:
|
||||
class QTgoLayoutGroupRow(QWidget):
|
||||
group_template_changed = Signal()
|
||||
|
||||
def __init__(self, force_group: ForceGroup, group: TgoLayoutGroup) -> None:
|
||||
def __init__(self, force_group: ForceGroup, group: TgoLayoutUnitGroup) -> None:
|
||||
super().__init__()
|
||||
self.grid_layout = QGridLayout()
|
||||
self.setLayout(self.grid_layout)
|
||||
@ -170,8 +170,10 @@ class QGroundObjectTemplateLayout(QGroupBox):
|
||||
self.layout_model.groups = defaultdict(list)
|
||||
for id in range(self.template_grid.count()):
|
||||
self.template_grid.itemAt(id).widget().deleteLater()
|
||||
for group_name, groups in self.layout_model.layout.groups.items():
|
||||
self.add_theater_group(group_name, self.layout_model.force_group, groups)
|
||||
for group in self.layout_model.layout.groups:
|
||||
self.add_theater_group(
|
||||
group.group_name, self.layout_model.force_group, group.unit_groups
|
||||
)
|
||||
self.group_template_changed()
|
||||
|
||||
@property
|
||||
@ -183,7 +185,7 @@ class QGroundObjectTemplateLayout(QGroupBox):
|
||||
return self.cost <= self.game.blue.budget
|
||||
|
||||
def add_theater_group(
|
||||
self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutGroup]
|
||||
self, group_name: str, force_group: ForceGroup, groups: list[TgoLayoutUnitGroup]
|
||||
) -> None:
|
||||
group_box = QGroupBox(group_name)
|
||||
vbox_layout = QVBoxLayout()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user