mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Update pydcs, adapt to new Point APIs.
This is briefly moving us over to my fork of pydcs while we wait for https://github.com/pydcs/dcs/pull/206 to be merged. The adaptation is invasive enough that I don't want it lingering for long.
This commit is contained in:
parent
ff12b37431
commit
9e2e4ffa74
@ -701,8 +701,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"TARGET AREA",
|
"TARGET AREA",
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
self.package.target.position.x,
|
self.package.target.position,
|
||||||
self.package.target.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
)
|
)
|
||||||
@ -906,8 +905,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"TARGET AREA",
|
"TARGET AREA",
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
self.package.target.position.x,
|
self.package.target.position,
|
||||||
self.package.target.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
)
|
)
|
||||||
@ -924,13 +922,13 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
|||||||
# Cheat in a FlightWaypoint for the split point.
|
# Cheat in a FlightWaypoint for the split point.
|
||||||
split: Point = self.package.waypoints.split
|
split: Point = self.package.waypoints.split
|
||||||
split_waypoint: FlightWaypoint = FlightWaypoint(
|
split_waypoint: FlightWaypoint = FlightWaypoint(
|
||||||
"SPLIT", FlightWaypointType.SPLIT, split.x, split.y, altitude
|
"SPLIT", FlightWaypointType.SPLIT, split, altitude
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cheat in a FlightWaypoint for the refuel point.
|
# Cheat in a FlightWaypoint for the refuel point.
|
||||||
refuel: Point = self.package.waypoints.refuel
|
refuel: Point = self.package.waypoints.refuel
|
||||||
refuel_waypoint: FlightWaypoint = FlightWaypoint(
|
refuel_waypoint: FlightWaypoint = FlightWaypoint(
|
||||||
"REFUEL", FlightWaypointType.REFUEL, refuel.x, refuel.y, altitude
|
"REFUEL", FlightWaypointType.REFUEL, refuel, altitude
|
||||||
)
|
)
|
||||||
|
|
||||||
delay_target_to_split: timedelta = self.travel_time_between_waypoints(
|
delay_target_to_split: timedelta = self.travel_time_between_waypoints(
|
||||||
|
|||||||
@ -31,12 +31,9 @@ class Navigating(InFlight):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def estimate_position(self) -> Point:
|
def estimate_position(self) -> Point:
|
||||||
x0 = self.current_waypoint.position.x
|
return self.current_waypoint.position.lerp(
|
||||||
y0 = self.current_waypoint.position.y
|
self.next_waypoint.position, self.progress()
|
||||||
x1 = self.next_waypoint.position.x
|
)
|
||||||
y1 = self.next_waypoint.position.y
|
|
||||||
progress = self.progress()
|
|
||||||
return Point(lerp(x0, x1, progress), lerp(y0, y1, progress))
|
|
||||||
|
|
||||||
def estimate_altitude(self) -> tuple[Distance, str]:
|
def estimate_altitude(self) -> tuple[Distance, str]:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -22,8 +22,7 @@ AltitudeReference = Literal["BARO", "RADIO"]
|
|||||||
class FlightWaypoint:
|
class FlightWaypoint:
|
||||||
name: str
|
name: str
|
||||||
waypoint_type: FlightWaypointType
|
waypoint_type: FlightWaypointType
|
||||||
x: float
|
position: Point
|
||||||
y: float
|
|
||||||
alt: Distance = meters(0)
|
alt: Distance = meters(0)
|
||||||
alt_type: AltitudeReference = "BARO"
|
alt_type: AltitudeReference = "BARO"
|
||||||
control_point: ControlPoint | None = None
|
control_point: ControlPoint | None = None
|
||||||
@ -50,8 +49,12 @@ class FlightWaypoint:
|
|||||||
departure_time: timedelta | None = None
|
departure_time: timedelta | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> Point:
|
def x(self) -> float:
|
||||||
return Point(self.x, self.y)
|
return self.position.x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y(self) -> float:
|
||||||
|
return self.position.y
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(id(self))
|
return hash(id(self))
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from typing import (
|
|||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point, Vector2
|
||||||
|
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
@ -71,8 +71,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"NAV",
|
"NAV",
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
description="Enter theater",
|
description="Enter theater",
|
||||||
pretty_name="Enter theater",
|
pretty_name="Enter theater",
|
||||||
@ -81,8 +80,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"TAKEOFF",
|
"TAKEOFF",
|
||||||
FlightWaypointType.TAKEOFF,
|
FlightWaypointType.TAKEOFF,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
alt_type="RADIO",
|
alt_type="RADIO",
|
||||||
description="Takeoff",
|
description="Takeoff",
|
||||||
@ -100,8 +98,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"NAV",
|
"NAV",
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
description="Exit theater",
|
description="Exit theater",
|
||||||
pretty_name="Exit theater",
|
pretty_name="Exit theater",
|
||||||
@ -110,8 +107,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"LANDING",
|
"LANDING",
|
||||||
FlightWaypointType.LANDING_POINT,
|
FlightWaypointType.LANDING_POINT,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
alt_type="RADIO",
|
alt_type="RADIO",
|
||||||
description="Land",
|
description="Land",
|
||||||
@ -143,8 +139,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"DIVERT",
|
"DIVERT",
|
||||||
FlightWaypointType.DIVERT,
|
FlightWaypointType.DIVERT,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
altitude,
|
altitude,
|
||||||
alt_type=altitude_type,
|
alt_type=altitude_type,
|
||||||
description="Divert",
|
description="Divert",
|
||||||
@ -157,8 +152,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"BULLSEYE",
|
"BULLSEYE",
|
||||||
FlightWaypointType.BULLSEYE,
|
FlightWaypointType.BULLSEYE,
|
||||||
self._bullseye.position.x,
|
self._bullseye.position,
|
||||||
self._bullseye.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
description="Bullseye",
|
description="Bullseye",
|
||||||
pretty_name="Bullseye",
|
pretty_name="Bullseye",
|
||||||
@ -173,8 +167,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"HOLD",
|
"HOLD",
|
||||||
FlightWaypointType.LOITER,
|
FlightWaypointType.LOITER,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Wait until push time",
|
description="Wait until push time",
|
||||||
@ -189,8 +182,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"JOIN",
|
"JOIN",
|
||||||
FlightWaypointType.JOIN,
|
FlightWaypointType.JOIN,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Rendezvous with package",
|
description="Rendezvous with package",
|
||||||
@ -205,8 +197,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"REFUEL",
|
"REFUEL",
|
||||||
FlightWaypointType.REFUEL,
|
FlightWaypointType.REFUEL,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Refuel from tanker",
|
description="Refuel from tanker",
|
||||||
@ -221,8 +212,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"SPLIT",
|
"SPLIT",
|
||||||
FlightWaypointType.SPLIT,
|
FlightWaypointType.SPLIT,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Depart from package",
|
description="Depart from package",
|
||||||
@ -242,8 +232,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"INGRESS",
|
"INGRESS",
|
||||||
ingress_type,
|
ingress_type,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description=f"INGRESS on {objective.name}",
|
description=f"INGRESS on {objective.name}",
|
||||||
@ -259,8 +248,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"EGRESS",
|
"EGRESS",
|
||||||
FlightWaypointType.EGRESS,
|
FlightWaypointType.EGRESS,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description=f"EGRESS from {target.name}",
|
description=f"EGRESS from {target.name}",
|
||||||
@ -284,8 +272,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
target.name,
|
target.name,
|
||||||
FlightWaypointType.TARGET_POINT,
|
FlightWaypointType.TARGET_POINT,
|
||||||
target.target.position.x,
|
target.target.position,
|
||||||
target.target.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description=description,
|
description=description,
|
||||||
@ -315,8 +302,7 @@ class WaypointBuilder:
|
|||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
name,
|
name,
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
location.position.x,
|
location.position,
|
||||||
location.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description=name,
|
description=name,
|
||||||
@ -340,8 +326,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"CAS",
|
"CAS",
|
||||||
FlightWaypointType.CAS,
|
FlightWaypointType.CAS,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
meters(60) if self.is_helo else meters(1000),
|
meters(60) if self.is_helo else meters(1000),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description="Provide CAS",
|
description="Provide CAS",
|
||||||
@ -359,8 +344,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"RACETRACK START",
|
"RACETRACK START",
|
||||||
FlightWaypointType.PATROL_TRACK,
|
FlightWaypointType.PATROL_TRACK,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
altitude,
|
altitude,
|
||||||
description="Orbit between this point and the next point",
|
description="Orbit between this point and the next point",
|
||||||
pretty_name="Race-track start",
|
pretty_name="Race-track start",
|
||||||
@ -377,8 +361,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"RACETRACK END",
|
"RACETRACK END",
|
||||||
FlightWaypointType.PATROL,
|
FlightWaypointType.PATROL,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
altitude,
|
altitude,
|
||||||
description="Orbit between this point and the previous point",
|
description="Orbit between this point and the previous point",
|
||||||
pretty_name="Race-track end",
|
pretty_name="Race-track end",
|
||||||
@ -411,8 +394,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"ORBIT",
|
"ORBIT",
|
||||||
FlightWaypointType.LOITER,
|
FlightWaypointType.LOITER,
|
||||||
start.x,
|
start,
|
||||||
start.y,
|
|
||||||
altitude,
|
altitude,
|
||||||
description="Anchor and hold at this point",
|
description="Anchor and hold at this point",
|
||||||
pretty_name="Orbit",
|
pretty_name="Orbit",
|
||||||
@ -429,8 +411,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"SWEEP START",
|
"SWEEP START",
|
||||||
FlightWaypointType.INGRESS_SWEEP,
|
FlightWaypointType.INGRESS_SWEEP,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
altitude,
|
altitude,
|
||||||
description="Proceed to the target and engage enemy aircraft",
|
description="Proceed to the target and engage enemy aircraft",
|
||||||
pretty_name="Sweep start",
|
pretty_name="Sweep start",
|
||||||
@ -447,8 +428,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"SWEEP END",
|
"SWEEP END",
|
||||||
FlightWaypointType.EGRESS,
|
FlightWaypointType.EGRESS,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
altitude,
|
altitude,
|
||||||
description="End of sweep",
|
description="End of sweep",
|
||||||
pretty_name="Sweep end",
|
pretty_name="Sweep end",
|
||||||
@ -491,8 +471,7 @@ class WaypointBuilder:
|
|||||||
return ingress_wp, FlightWaypoint(
|
return ingress_wp, FlightWaypoint(
|
||||||
"TARGET",
|
"TARGET",
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
target.position.x,
|
target.position,
|
||||||
target.position.y,
|
|
||||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Escort the package",
|
description="Escort the package",
|
||||||
@ -509,8 +488,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"PICKUP",
|
"PICKUP",
|
||||||
FlightWaypointType.PICKUP,
|
FlightWaypointType.PICKUP,
|
||||||
control_point.position.x,
|
control_point.position,
|
||||||
control_point.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description=f"Pick up cargo from {control_point}",
|
description=f"Pick up cargo from {control_point}",
|
||||||
@ -527,8 +505,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"DROP OFF",
|
"DROP OFF",
|
||||||
FlightWaypointType.PICKUP,
|
FlightWaypointType.PICKUP,
|
||||||
control_point.position.x,
|
control_point.position,
|
||||||
control_point.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
description=f"Drop off cargo at {control_point}",
|
description=f"Drop off cargo at {control_point}",
|
||||||
@ -554,8 +531,7 @@ class WaypointBuilder:
|
|||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
"NAV",
|
"NAV",
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position.x,
|
position,
|
||||||
position.y,
|
|
||||||
altitude,
|
altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="NAV",
|
description="NAV",
|
||||||
@ -617,4 +593,4 @@ class WaypointBuilder:
|
|||||||
deviation = nautical_miles(1)
|
deviation = nautical_miles(1)
|
||||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||||
return Point(point.x + x_adj, point.y + y_adj)
|
return point + Vector2(x_adj, y_adj)
|
||||||
|
|||||||
@ -1,31 +1,30 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Point
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
from game.campaignloader import CampaignAirWingConfig
|
from game.armedforces.armedforces import ArmedForces
|
||||||
|
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||||
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
||||||
from game.commander import TheaterCommander
|
from game.commander import TheaterCommander
|
||||||
from game.commander.missionscheduler import MissionScheduler
|
from game.commander.missionscheduler import MissionScheduler
|
||||||
from game.armedforces.armedforces import ArmedForces
|
|
||||||
from game.income import Income
|
from game.income import Income
|
||||||
from game.navmesh import NavMesh
|
from game.navmesh import NavMesh
|
||||||
from game.orderedset import OrderedSet
|
from game.orderedset import OrderedSet
|
||||||
from game.profiling import logged_duration, MultiEventTracer
|
from game.procurement import AircraftProcurementRequest, ProcurementAi
|
||||||
|
from game.profiling import MultiEventTracer, logged_duration
|
||||||
from game.squadrons import AirWing
|
from game.squadrons import AirWing
|
||||||
|
from game.theater.bullseye import Bullseye
|
||||||
|
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from game.transfers import PendingTransfers
|
from game.transfers import PendingTransfers
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from game.campaignloader import CampaignAirWingConfig
|
||||||
from game.data.doctrine import Doctrine
|
from game.data.doctrine import Doctrine
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from game.procurement import AircraftProcurementRequest, ProcurementAi
|
|
||||||
from game.theater.bullseye import Bullseye
|
|
||||||
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
|
||||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
|
||||||
|
|
||||||
|
|
||||||
class Coalition:
|
class Coalition:
|
||||||
@ -39,7 +38,7 @@ class Coalition:
|
|||||||
self.ato = AirTaskingOrder()
|
self.ato = AirTaskingOrder()
|
||||||
self.transit_network = TransitNetwork()
|
self.transit_network = TransitNetwork()
|
||||||
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
|
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
|
||||||
self.bullseye = Bullseye(Point(0, 0))
|
self.bullseye = Bullseye(self.game.point_in_world(0, 0))
|
||||||
self.faker = Faker(self.faction.locales)
|
self.faker = Faker(self.faction.locales)
|
||||||
self.air_wing = AirWing(player, game, self.faction)
|
self.air_wing = AirWing(player, game, self.faction)
|
||||||
self.armed_forces = ArmedForces(self.faction)
|
self.armed_forces = ArmedForces(self.faction)
|
||||||
|
|||||||
@ -64,6 +64,7 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
def _rebuild_threat_zones(self) -> None:
|
def _rebuild_threat_zones(self) -> None:
|
||||||
"""Recreates the theater's threat zones based on the current planned state."""
|
"""Recreates the theater's threat zones based on the current planned state."""
|
||||||
self.threat_zones = ThreatZones.for_threats(
|
self.threat_zones = ThreatZones.for_threats(
|
||||||
|
self.context.theater,
|
||||||
self.context.coalition.opponent.doctrine,
|
self.context.coalition.opponent.doctrine,
|
||||||
barcap_locations=self.enemy_barcaps,
|
barcap_locations=self.enemy_barcaps,
|
||||||
air_defenses=itertools.chain(self.enemy_air_defenses, self.enemy_ships),
|
air_defenses=itertools.chain(self.enemy_air_defenses, self.enemy_ships),
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import shapely.ops
|
import shapely.ops
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from shapely.geometry import Point as ShapelyPoint, Polygon, MultiPolygon
|
from shapely.geometry import MultiPolygon, Point as ShapelyPoint, Polygon
|
||||||
|
|
||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ class HoldZoneGeometry:
|
|||||||
coalition: Coalition,
|
coalition: Coalition,
|
||||||
theater: ConflictTheater,
|
theater: ConflictTheater,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._target = target
|
||||||
# Hold points are placed one of two ways. Either approach guarantees:
|
# Hold points are placed one of two ways. Either approach guarantees:
|
||||||
#
|
#
|
||||||
# * Safe hold point.
|
# * Safe hold point.
|
||||||
@ -105,4 +106,4 @@ class HoldZoneGeometry:
|
|||||||
hold, _ = shapely.ops.nearest_points(self.permissible_zones, self.home)
|
hold, _ = shapely.ops.nearest_points(self.permissible_zones, self.home)
|
||||||
else:
|
else:
|
||||||
hold, _ = shapely.ops.nearest_points(self.preferred_lines, self.join)
|
hold, _ = shapely.ops.nearest_points(self.preferred_lines, self.join)
|
||||||
return Point(hold.x, hold.y)
|
return self._target.new_in_same_map(hold.x, hold.y)
|
||||||
|
|||||||
@ -4,9 +4,9 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import shapely.ops
|
import shapely.ops
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from shapely.geometry import Point as ShapelyPoint, MultiPolygon
|
from shapely.geometry import MultiPolygon, Point as ShapelyPoint
|
||||||
|
|
||||||
from game.utils import nautical_miles, meters
|
from game.utils import meters, nautical_miles
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
@ -25,6 +25,7 @@ class IpZoneGeometry:
|
|||||||
home: Point,
|
home: Point,
|
||||||
coalition: Coalition,
|
coalition: Coalition,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._target = target
|
||||||
self.threat_zone = coalition.opponent.threat_zone.all
|
self.threat_zone = coalition.opponent.threat_zone.all
|
||||||
self.home = ShapelyPoint(home.x, home.y)
|
self.home = ShapelyPoint(home.x, home.y)
|
||||||
|
|
||||||
@ -115,4 +116,4 @@ class IpZoneGeometry:
|
|||||||
ip = self._unsafe_ip()
|
ip = self._unsafe_ip()
|
||||||
else:
|
else:
|
||||||
ip = self._safe_ip()
|
ip = self._safe_ip()
|
||||||
return Point(ip.x, ip.y)
|
return self._target.new_in_same_map(ip.x, ip.y)
|
||||||
|
|||||||
@ -5,10 +5,10 @@ from typing import TYPE_CHECKING
|
|||||||
import shapely.ops
|
import shapely.ops
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from shapely.geometry import (
|
from shapely.geometry import (
|
||||||
|
MultiLineString,
|
||||||
|
MultiPolygon,
|
||||||
Point as ShapelyPoint,
|
Point as ShapelyPoint,
|
||||||
Polygon,
|
Polygon,
|
||||||
MultiPolygon,
|
|
||||||
MultiLineString,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
@ -27,6 +27,7 @@ class JoinZoneGeometry:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self, target: Point, home: Point, ip: Point, coalition: Coalition
|
self, target: Point, home: Point, ip: Point, coalition: Coalition
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._target = target
|
||||||
# Normal join placement is based on the path from home to the IP. If no path is
|
# Normal join placement is based on the path from home to the IP. If no path is
|
||||||
# found it means that the target is on a direct path. In that case we instead
|
# found it means that the target is on a direct path. In that case we instead
|
||||||
# want to enforce that the join point is:
|
# want to enforce that the join point is:
|
||||||
@ -100,4 +101,4 @@ class JoinZoneGeometry:
|
|||||||
join, _ = shapely.ops.nearest_points(self.permissible_zones, self.ip)
|
join, _ = shapely.ops.nearest_points(self.permissible_zones, self.ip)
|
||||||
else:
|
else:
|
||||||
join, _ = shapely.ops.nearest_points(self.preferred_lines, self.home)
|
join, _ = shapely.ops.nearest_points(self.preferred_lines, self.home)
|
||||||
return Point(join.x, join.y)
|
return self._target.new_in_same_map(join.x, join.y)
|
||||||
|
|||||||
15
game/game.py
15
game/game.py
@ -143,6 +143,9 @@ class Game:
|
|||||||
yield self.blue
|
yield self.blue
|
||||||
yield self.red
|
yield self.red
|
||||||
|
|
||||||
|
def point_in_world(self, x: float, y: float) -> Point:
|
||||||
|
return Point(x, y, self.theater.terrain)
|
||||||
|
|
||||||
def ato_for(self, player: bool) -> AirTaskingOrder:
|
def ato_for(self, player: bool) -> AirTaskingOrder:
|
||||||
return self.coalition_for(player).ato
|
return self.coalition_for(player).ato
|
||||||
|
|
||||||
@ -180,9 +183,6 @@ class Game:
|
|||||||
def country_for(self, player: bool) -> str:
|
def country_for(self, player: bool) -> str:
|
||||||
return self.coalition_for(player).country_name
|
return self.coalition_for(player).country_name
|
||||||
|
|
||||||
def bullseye_for(self, player: bool) -> Bullseye:
|
|
||||||
return self.coalition_for(player).bullseye
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def neutral_country(self) -> Type[Country]:
|
def neutral_country(self) -> Type[Country]:
|
||||||
"""Return the best fitting country that can be used as neutral faction in the generated mission"""
|
"""Return the best fitting country that can be used as neutral faction in the generated mission"""
|
||||||
@ -447,10 +447,7 @@ class Game:
|
|||||||
d = cp.position.distance_to_point(cp2.position)
|
d = cp.position.distance_to_point(cp2.position)
|
||||||
if d < min_distance:
|
if d < min_distance:
|
||||||
min_distance = d
|
min_distance = d
|
||||||
cpoint = Point(
|
cpoint = cp.position.midpoint(cp2.position)
|
||||||
(cp.position.x + cp2.position.x) / 2,
|
|
||||||
(cp.position.y + cp2.position.y) / 2,
|
|
||||||
)
|
|
||||||
zones.append(cp.position)
|
zones.append(cp.position)
|
||||||
zones.append(cp2.position)
|
zones.append(cp2.position)
|
||||||
break
|
break
|
||||||
@ -473,7 +470,9 @@ class Game:
|
|||||||
self.__culling_zones = zones
|
self.__culling_zones = zones
|
||||||
|
|
||||||
def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None:
|
def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None:
|
||||||
pos = Point(cast(float, data["x"]), cast(float, data["z"]))
|
pos = Point(
|
||||||
|
cast(float, data["x"]), cast(float, data["z"]), self.theater.terrain
|
||||||
|
)
|
||||||
if self.theater.is_on_land(pos):
|
if self.theater.is_on_land(pos):
|
||||||
self.__destroyed_units.append(data)
|
self.__destroyed_units.append(data)
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
|
|
||||||
from dcs import Mission, Point
|
from dcs import Mission
|
||||||
from dcs.country import Country
|
from dcs.country import Country
|
||||||
|
from dcs.mapping import Vector2
|
||||||
from dcs.mission import StartType as DcsStartType
|
from dcs.mission import StartType as DcsStartType
|
||||||
from dcs.planes import F_14A, Su_33
|
from dcs.planes import F_14A, Su_33
|
||||||
from dcs.point import PointAction
|
from dcs.point import PointAction
|
||||||
@ -139,7 +140,7 @@ class FlightGroupSpawner:
|
|||||||
)
|
)
|
||||||
speed = self.flight.state.estimate_speed()
|
speed = self.flight.state.estimate_speed()
|
||||||
pos = self.flight.state.estimate_position()
|
pos = self.flight.state.estimate_position()
|
||||||
pos += Point(random.randint(100, 1000), random.randint(100, 1000))
|
pos += Vector2(random.randint(100, 1000), random.randint(100, 1000))
|
||||||
alt, alt_type = self.flight.state.estimate_altitude()
|
alt, alt_type = self.flight.state.estimate_altitude()
|
||||||
|
|
||||||
# We don't know where the ground is, so just make sure that any aircraft
|
# We don't know where the ground is, so just make sure that any aircraft
|
||||||
@ -197,7 +198,7 @@ class FlightGroupSpawner:
|
|||||||
alt = WARM_START_ALTITUDE
|
alt = WARM_START_ALTITUDE
|
||||||
|
|
||||||
speed = GroundSpeed.for_flight(self.flight, alt)
|
speed = GroundSpeed.for_flight(self.flight, alt)
|
||||||
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
|
pos = at + Vector2(random.randint(100, 1000), random.randint(100, 1000))
|
||||||
|
|
||||||
group = self.mission.flight_group(
|
group = self.mission.flight_group(
|
||||||
country=self.country,
|
country=self.country,
|
||||||
|
|||||||
@ -3,10 +3,9 @@ from __future__ import annotations
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Iterable, Union
|
from typing import Any, Iterable, Union
|
||||||
|
|
||||||
from dcs import Mission, Point
|
from dcs import Mission
|
||||||
from dcs.planes import AJS37, F_14B, JF_17
|
from dcs.planes import AJS37, F_14B, JF_17
|
||||||
from dcs.point import MovingPoint, PointAction
|
from dcs.point import MovingPoint, PointAction
|
||||||
from dcs.unit import Unit
|
|
||||||
from dcs.unitgroup import FlyingGroup
|
from dcs.unitgroup import FlyingGroup
|
||||||
|
|
||||||
from game.ato import Flight, FlightWaypoint
|
from game.ato import Flight, FlightWaypoint
|
||||||
@ -41,7 +40,7 @@ class PydcsWaypointBuilder:
|
|||||||
|
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = self.group.add_waypoint(
|
waypoint = self.group.add_waypoint(
|
||||||
Point(self.waypoint.x, self.waypoint.y),
|
self.waypoint.position,
|
||||||
self.waypoint.alt.meters,
|
self.waypoint.alt.meters,
|
||||||
name=self.waypoint.name,
|
name=self.waypoint.name,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from dcs import Point
|
import copy
|
||||||
|
|
||||||
from dcs.planes import B_17G, B_52H, Tu_22M3
|
from dcs.planes import B_17G, B_52H, Tu_22M3
|
||||||
from dcs.point import MovingPoint
|
from dcs.point import MovingPoint
|
||||||
from dcs.task import Bombing, OptFormation, WeaponType
|
from dcs.task import Bombing, OptFormation, WeaponType
|
||||||
@ -20,12 +21,10 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
|||||||
if not targets:
|
if not targets:
|
||||||
return
|
return
|
||||||
|
|
||||||
center = Point(0, 0)
|
center = copy.copy(targets[0].position)
|
||||||
for target in targets:
|
for target in targets[1:]:
|
||||||
center.x += target.position.x
|
center += target.position
|
||||||
center.y += target.position.y
|
center /= len(targets)
|
||||||
center.x /= len(targets)
|
|
||||||
center.y /= len(targets)
|
|
||||||
bombing = Bombing(center, weapon_type=WeaponType.Bombs)
|
bombing = Bombing(center, weapon_type=WeaponType.Bombs)
|
||||||
bombing.params["expend"] = "All"
|
bombing.params["expend"] = "All"
|
||||||
bombing.params["attackQtyLimit"] = False
|
bombing.params["attackQtyLimit"] = False
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from dcs.drawing import LineStyle, Rgba
|
from dcs.drawing import LineStyle, Rgba
|
||||||
from dcs.drawing.drawings import StandardLayer
|
from dcs.drawing.drawings import StandardLayer
|
||||||
from dcs.mission import Mission
|
from dcs.mission import Mission
|
||||||
|
|
||||||
from game import Game, VERSION
|
from game import Game
|
||||||
from game.missiongenerator.frontlineconflictdescription import (
|
from game.missiongenerator.frontlineconflictdescription import (
|
||||||
FrontLineConflictDescription,
|
FrontLineConflictDescription,
|
||||||
)
|
)
|
||||||
@ -73,7 +71,7 @@ class DrawingsGenerator:
|
|||||||
# Add shape to layer
|
# Add shape to layer
|
||||||
shape = self.player_layer.add_line_segments(
|
shape = self.player_layer.add_line_segments(
|
||||||
cp.position,
|
cp.position,
|
||||||
[Point(0, 0)]
|
[Point(0, 0, self.game.theater.terrain)]
|
||||||
+ [p - cp.position for p in convoy_route]
|
+ [p - cp.position for p in convoy_route]
|
||||||
+ [destination.position - cp.position],
|
+ [destination.position - cp.position],
|
||||||
line_thickness=6,
|
line_thickness=6,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from dcs import Mission
|
|||||||
from dcs.action import AITaskPush
|
from dcs.action import AITaskPush
|
||||||
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
|
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
|
||||||
from dcs.country import Country
|
from dcs.country import Country
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point, Vector2
|
||||||
from dcs.point import PointAction
|
from dcs.point import PointAction
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
AFAC,
|
AFAC,
|
||||||
@ -372,7 +372,7 @@ class FlotGenerator:
|
|||||||
# Then move forward OR Attack enemy base if it is not too far away
|
# Then move forward OR Attack enemy base if it is not too far away
|
||||||
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
|
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
|
||||||
if target is not None:
|
if target is not None:
|
||||||
rand_offset = Point(
|
rand_offset = Vector2(
|
||||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
)
|
)
|
||||||
@ -419,7 +419,7 @@ class FlotGenerator:
|
|||||||
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
||||||
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
|
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
|
||||||
for i, target in enumerate(targets, start=1):
|
for i, target in enumerate(targets, start=1):
|
||||||
rand_offset = Point(
|
rand_offset = Vector2(
|
||||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -766,7 +766,7 @@ class KneeboardGenerator(MissionInfoGenerator):
|
|||||||
pages: List[KneeboardPage] = [
|
pages: List[KneeboardPage] = [
|
||||||
BriefingPage(
|
BriefingPage(
|
||||||
flight,
|
flight,
|
||||||
self.game.bullseye_for(flight.friendly),
|
self.game.coalition_for(flight.friendly).bullseye,
|
||||||
self.game.theater,
|
self.game.theater,
|
||||||
self.game.conditions.weather,
|
self.game.conditions.weather,
|
||||||
zoned_time,
|
zoned_time,
|
||||||
|
|||||||
@ -129,7 +129,7 @@ class MissionGenerator:
|
|||||||
"red", bullseye=self.game.red.bullseye.to_pydcs()
|
"red", bullseye=self.game.red.bullseye.to_pydcs()
|
||||||
)
|
)
|
||||||
self.mission.coalition["neutrals"] = Coalition(
|
self.mission.coalition["neutrals"] = Coalition(
|
||||||
"neutrals", bullseye=Bullseye(Point(0, 0)).to_pydcs()
|
"neutrals", bullseye=Bullseye(Point(0, 0, self.mission.terrain)).to_pydcs()
|
||||||
)
|
)
|
||||||
|
|
||||||
p_country = self.game.blue.country_name
|
p_country = self.game.blue.country_name
|
||||||
@ -295,7 +295,7 @@ class MissionGenerator:
|
|||||||
logging.warning(f"Destroyed unit has no type: {d}")
|
logging.warning(f"Destroyed unit has no type: {d}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pos = Point(cast(float, d["x"]), cast(float, d["z"]))
|
pos = Point(cast(float, d["x"]), cast(float, d["z"]), self.mission.terrain)
|
||||||
if utype is not None and not self.game.position_culled(pos):
|
if utype is not None and not self.game.position_culled(pos):
|
||||||
self.mission.static_group(
|
self.mission.static_group(
|
||||||
country=self.mission.country(self.game.blue.country_name),
|
country=self.mission.country(self.game.blue.country_name),
|
||||||
|
|||||||
@ -537,8 +537,7 @@ class HelipadGenerator:
|
|||||||
country = self.m.country(self.game.coalition_for(self.cp.captured).country_name)
|
country = self.m.country(self.game.coalition_for(self.cp.captured).country_name)
|
||||||
name = self.cp.name + "_helipad"
|
name = self.cp.name + "_helipad"
|
||||||
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
|
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
|
||||||
sp = StaticPoint()
|
sp = StaticPoint(self.cp.position)
|
||||||
sp.position = self.cp.position
|
|
||||||
sg.add_point(sp)
|
sg.add_point(sp)
|
||||||
|
|
||||||
for i, helipad in enumerate(self.cp.helipads):
|
for i, helipad in enumerate(self.cp.helipads):
|
||||||
|
|||||||
@ -46,9 +46,8 @@ class NavPoint:
|
|||||||
point: ShapelyPoint
|
point: ShapelyPoint
|
||||||
poly: NavMeshPoly
|
poly: NavMeshPoly
|
||||||
|
|
||||||
@property
|
def world_point(self, theater: ConflictTheater) -> Point:
|
||||||
def world_point(self) -> Point:
|
return Point(self.point.x, self.point.y, theater.terrain)
|
||||||
return Point(self.point.x, self.point.y)
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(self.poly.ident)
|
return hash(self.poly.ident)
|
||||||
@ -90,8 +89,9 @@ class NavFrontier:
|
|||||||
|
|
||||||
|
|
||||||
class NavMesh:
|
class NavMesh:
|
||||||
def __init__(self, polys: List[NavMeshPoly]) -> None:
|
def __init__(self, polys: List[NavMeshPoly], theater: ConflictTheater) -> None:
|
||||||
self.polys = polys
|
self.polys = polys
|
||||||
|
self.theater = theater
|
||||||
|
|
||||||
def localize(self, point: Point) -> Optional[NavMeshPoly]:
|
def localize(self, point: Point) -> Optional[NavMeshPoly]:
|
||||||
# This is a naive implementation but it's O(n). Runs at about 10k
|
# This is a naive implementation but it's O(n). Runs at about 10k
|
||||||
@ -117,8 +117,8 @@ class NavMesh:
|
|||||||
def travel_heuristic(self, a: NavPoint, b: NavPoint) -> float:
|
def travel_heuristic(self, a: NavPoint, b: NavPoint) -> float:
|
||||||
return self.travel_cost(a, b)
|
return self.travel_cost(a, b)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def reconstruct_path(
|
def reconstruct_path(
|
||||||
|
self,
|
||||||
came_from: Dict[NavPoint, Optional[NavPoint]],
|
came_from: Dict[NavPoint, Optional[NavPoint]],
|
||||||
origin: NavPoint,
|
origin: NavPoint,
|
||||||
destination: NavPoint,
|
destination: NavPoint,
|
||||||
@ -126,14 +126,14 @@ class NavMesh:
|
|||||||
current = destination
|
current = destination
|
||||||
path: List[Point] = []
|
path: List[Point] = []
|
||||||
while current != origin:
|
while current != origin:
|
||||||
path.append(current.world_point)
|
path.append(current.world_point(self.theater))
|
||||||
previous = came_from[current]
|
previous = came_from[current]
|
||||||
if previous is None:
|
if previous is None:
|
||||||
raise NavMeshError(
|
raise NavMeshError(
|
||||||
f"Could not reconstruct path to {destination} from {origin}"
|
f"Could not reconstruct path to {destination} from {origin}"
|
||||||
)
|
)
|
||||||
current = previous
|
current = previous
|
||||||
path.append(origin.world_point)
|
path.append(origin.world_point(self.theater))
|
||||||
path.reverse()
|
path.reverse()
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@ -270,4 +270,4 @@ class NavMesh:
|
|||||||
# Triangulate the safe-region to build the navmesh.
|
# Triangulate the safe-region to build the navmesh.
|
||||||
navpolys = cls.create_navpolys(triangulate(bounds), threat_zones)
|
navpolys = cls.create_navpolys(triangulate(bounds), threat_zones)
|
||||||
cls.associate_neighbors(navpolys)
|
cls.associate_neighbors(navpolys)
|
||||||
return cls(navpolys)
|
return NavMesh(navpolys, theater)
|
||||||
|
|||||||
@ -3,21 +3,19 @@ from __future__ import annotations
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
from dcs.terrain import Terrain
|
||||||
|
|
||||||
from game.utils import Heading
|
from game.utils import Heading
|
||||||
|
|
||||||
|
|
||||||
class PointWithHeading(Point):
|
class PointWithHeading(Point):
|
||||||
def __init__(self) -> None:
|
def __init__(self, x: float, y: float, heading: Heading, terrain: Terrain) -> None:
|
||||||
super(PointWithHeading, self).__init__(0, 0)
|
super().__init__(x, y, terrain)
|
||||||
self.heading: Heading = Heading.from_degrees(0)
|
self.heading: Heading = heading
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_point(point: Point, heading: Heading) -> PointWithHeading:
|
def from_point(point: Point, heading: Heading) -> PointWithHeading:
|
||||||
p = PointWithHeading()
|
return PointWithHeading(point.x, point.y, heading, point._terrain)
|
||||||
p.x = point.x
|
|
||||||
p.y = point.y
|
|
||||||
p.heading = heading
|
|
||||||
return p
|
|
||||||
|
|
||||||
def rotate(self, origin: Point, heading: Heading) -> None:
|
def rotate(self, origin: Point, heading: Heading) -> None:
|
||||||
"""Rotates the Point by a given angle clockwise around the origin"""
|
"""Rotates the Point by a given angle clockwise around the origin"""
|
||||||
|
|||||||
@ -17,7 +17,8 @@ class ShapelyUtil:
|
|||||||
if poly.is_empty:
|
if poly.is_empty:
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
theater.point_to_ll(Point(x, y)).as_list() for x, y in poly.exterior.coords
|
Point(x, y, theater.terrain).latlng().as_list()
|
||||||
|
for x, y in poly.exterior.coords
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -34,7 +35,7 @@ class ShapelyUtil:
|
|||||||
def line_to_leaflet(
|
def line_to_leaflet(
|
||||||
line: LineString, theater: ConflictTheater
|
line: LineString, theater: ConflictTheater
|
||||||
) -> list[LeafletLatLon]:
|
) -> list[LeafletLatLon]:
|
||||||
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in line.coords]
|
return [Point(x, y, theater.terrain).latlng().as_list() for x, y in line.coords]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def lines_to_leaflet(
|
def lines_to_leaflet(
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from dcs.mapping import LatLng
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -8,7 +9,6 @@ from game.ato.flightwaypoint import FlightWaypoint
|
|||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
from game.server import GameContext
|
from game.server import GameContext
|
||||||
from game.server.waypoints.models import FlightWaypointJs
|
from game.server.waypoints.models import FlightWaypointJs
|
||||||
from game.theater import LatLon
|
|
||||||
from game.utils import meters
|
from game.utils import meters
|
||||||
|
|
||||||
router: APIRouter = APIRouter(prefix="/waypoints")
|
router: APIRouter = APIRouter(prefix="/waypoints")
|
||||||
@ -23,8 +23,7 @@ def all_waypoints_for_flight(
|
|||||||
FlightWaypoint(
|
FlightWaypoint(
|
||||||
"TAKEOFF",
|
"TAKEOFF",
|
||||||
FlightWaypointType.TAKEOFF,
|
FlightWaypointType.TAKEOFF,
|
||||||
flight.departure.position.x,
|
flight.departure.position,
|
||||||
flight.departure.position.y,
|
|
||||||
meters(0),
|
meters(0),
|
||||||
"RADIO",
|
"RADIO",
|
||||||
),
|
),
|
||||||
@ -40,7 +39,7 @@ def all_waypoints_for_flight(
|
|||||||
def set_position(
|
def set_position(
|
||||||
flight_id: UUID,
|
flight_id: UUID,
|
||||||
waypoint_idx: int,
|
waypoint_idx: int,
|
||||||
position: LatLon,
|
position: LatLng,
|
||||||
game: Game = Depends(GameContext.get),
|
game: Game = Depends(GameContext.get),
|
||||||
) -> None:
|
) -> None:
|
||||||
flight = game.db.flights.get(flight_id)
|
flight = game.db.flights.get(flight_id)
|
||||||
@ -48,9 +47,7 @@ def set_position(
|
|||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1]
|
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1]
|
||||||
point = game.theater.ll_to_point(position)
|
waypoint.position = game.theater.ll_to_point(position)
|
||||||
waypoint.x = point.x
|
|
||||||
waypoint.y = point.y
|
|
||||||
package_model = (
|
package_model = (
|
||||||
GameContext.get_model()
|
GameContext.get_model()
|
||||||
.ato_model_for(flight.blue)
|
.ato_model_for(flight.blue)
|
||||||
|
|||||||
@ -5,10 +5,10 @@ from typing import Dict, TYPE_CHECKING
|
|||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
from game.theater import LatLon
|
from .latlon import LatLon
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.theater import ConflictTheater
|
from .conflicttheater import ConflictTheater
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import LatLng, Point
|
||||||
from dcs.terrain import (
|
from dcs.terrain import (
|
||||||
caucasus,
|
caucasus,
|
||||||
marianaislands,
|
marianaislands,
|
||||||
@ -147,8 +147,8 @@ class ConflictTheater:
|
|||||||
min_distance = distance
|
min_distance = distance
|
||||||
nearest_point = pt
|
nearest_point = pt
|
||||||
assert isinstance(nearest_point, geometry.Point)
|
assert isinstance(nearest_point, geometry.Point)
|
||||||
point = Point(point.x, point.y)
|
point = Point(point.x, point.y, self.terrain)
|
||||||
nearest_point = Point(nearest_point.x, nearest_point.y)
|
nearest_point = Point(nearest_point.x, nearest_point.y, self.terrain)
|
||||||
new_point = point.point_from_heading(
|
new_point = point.point_from_heading(
|
||||||
point.heading_between_point(nearest_point),
|
point.heading_between_point(nearest_point),
|
||||||
point.distance_to_point(nearest_point) + extend_dist,
|
point.distance_to_point(nearest_point) + extend_dist,
|
||||||
@ -261,9 +261,8 @@ class ConflictTheater:
|
|||||||
lat, lon = self.point_to_ll_transformer.transform(point.x, point.y)
|
lat, lon = self.point_to_ll_transformer.transform(point.x, point.y)
|
||||||
return LatLon(lat, lon)
|
return LatLon(lat, lon)
|
||||||
|
|
||||||
def ll_to_point(self, ll: LatLon) -> Point:
|
def ll_to_point(self, ll: LatLng) -> Point:
|
||||||
x, y = self.ll_to_point_transformer.transform(ll.lat, ll.lng)
|
return Point.from_latlng(ll, self.terrain)
|
||||||
return Point(x, y)
|
|
||||||
|
|
||||||
def heading_to_conflict_from(self, position: Point) -> Optional[Heading]:
|
def heading_to_conflict_from(self, position: Point) -> Optional[Heading]:
|
||||||
# Heading for a Group to the enemy.
|
# Heading for a Group to the enemy.
|
||||||
@ -281,9 +280,8 @@ class ConflictTheater:
|
|||||||
]
|
]
|
||||||
last = len(sorted_conflicts) - 1
|
last = len(sorted_conflicts) - 1
|
||||||
|
|
||||||
conflict_center = Point(
|
conflict_center = sorted_conflicts[0].position.midpoint(
|
||||||
(sorted_conflicts[0].position.x + sorted_conflicts[last].position.x) / 2,
|
sorted_conflicts[last].position
|
||||||
(sorted_conflicts[0].position.y + sorted_conflicts[last].position.y) / 2,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return Heading.from_degrees(position.heading_between_point(conflict_center))
|
return Heading.from_degrees(position.heading_between_point(conflict_center))
|
||||||
|
|||||||
@ -7,8 +7,8 @@ from dataclasses import dataclass, field
|
|||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import Dict, Iterator, List, Optional, Set, Tuple
|
from typing import Dict, Iterator, List, Optional, Set, Tuple
|
||||||
|
|
||||||
from game.theater import ConflictTheater
|
from .conflicttheater import ConflictTheater
|
||||||
from game.theater.controlpoint import ControlPoint
|
from .controlpoint import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
class NoPathError(RuntimeError):
|
class NoPathError(RuntimeError):
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from functools import singledispatchmethod
|
from functools import singledispatchmethod
|
||||||
from typing import Optional, TYPE_CHECKING, Union, Iterable
|
from typing import Iterable, Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from dcs.mapping import Point as DcsPoint
|
from dcs.mapping import Point as DcsPoint
|
||||||
from shapely.geometry import (
|
from shapely.geometry import (
|
||||||
@ -13,11 +13,16 @@ from shapely.geometry import (
|
|||||||
from shapely.geometry.base import BaseGeometry
|
from shapely.geometry.base import BaseGeometry
|
||||||
from shapely.ops import nearest_points, unary_union
|
from shapely.ops import nearest_points, unary_union
|
||||||
|
|
||||||
from game.data.doctrine import Doctrine
|
|
||||||
from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
|
|
||||||
from game.utils import Distance, meters, nautical_miles
|
|
||||||
from game.ato.closestairfields import ObjectiveDistanceCache
|
|
||||||
from game.ato import Flight, FlightWaypoint
|
from game.ato import Flight, FlightWaypoint
|
||||||
|
from game.ato.closestairfields import ObjectiveDistanceCache
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater import (
|
||||||
|
ConflictTheater,
|
||||||
|
ControlPoint,
|
||||||
|
MissionTarget,
|
||||||
|
TheaterGroundObject,
|
||||||
|
)
|
||||||
|
from game.utils import Distance, meters, nautical_miles
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -29,10 +34,12 @@ ThreatPoly = Union[MultiPolygon, Polygon]
|
|||||||
class ThreatZones:
|
class ThreatZones:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
theater: ConflictTheater,
|
||||||
airbases: ThreatPoly,
|
airbases: ThreatPoly,
|
||||||
air_defenses: ThreatPoly,
|
air_defenses: ThreatPoly,
|
||||||
radar_sam_threats: ThreatPoly,
|
radar_sam_threats: ThreatPoly,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.theater = theater
|
||||||
self.airbases = airbases
|
self.airbases = airbases
|
||||||
self.air_defenses = air_defenses
|
self.air_defenses = air_defenses
|
||||||
self.radar_sam_threats = radar_sam_threats
|
self.radar_sam_threats = radar_sam_threats
|
||||||
@ -42,7 +49,7 @@ class ThreatZones:
|
|||||||
boundary, _ = nearest_points(
|
boundary, _ = nearest_points(
|
||||||
self.all.boundary, self.dcs_to_shapely_point(point)
|
self.all.boundary, self.dcs_to_shapely_point(point)
|
||||||
)
|
)
|
||||||
return DcsPoint(boundary.x, boundary.y)
|
return DcsPoint(boundary.x, boundary.y, self.theater.terrain)
|
||||||
|
|
||||||
def distance_to_threat(self, point: DcsPoint) -> Distance:
|
def distance_to_threat(self, point: DcsPoint) -> Distance:
|
||||||
boundary = self.closest_boundary(point)
|
boundary = self.closest_boundary(point)
|
||||||
@ -200,12 +207,13 @@ class ThreatZones:
|
|||||||
air_defenses.extend(control_point.ground_objects)
|
air_defenses.extend(control_point.ground_objects)
|
||||||
|
|
||||||
return cls.for_threats(
|
return cls.for_threats(
|
||||||
game.faction_for(player).doctrine, air_threats, air_defenses
|
game.theater, game.faction_for(player).doctrine, air_threats, air_defenses
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_threats(
|
def for_threats(
|
||||||
cls,
|
cls,
|
||||||
|
theater: ConflictTheater,
|
||||||
doctrine: Doctrine,
|
doctrine: Doctrine,
|
||||||
barcap_locations: Iterable[ControlPoint],
|
barcap_locations: Iterable[ControlPoint],
|
||||||
air_defenses: Iterable[TheaterGroundObject],
|
air_defenses: Iterable[TheaterGroundObject],
|
||||||
@ -213,6 +221,7 @@ class ThreatZones:
|
|||||||
"""Generates the threat zones projected by the given locations.
|
"""Generates the threat zones projected by the given locations.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
theater: The theater the threat zones are in.
|
||||||
doctrine: The doctrine of the owning coalition.
|
doctrine: The doctrine of the owning coalition.
|
||||||
barcap_locations: The locations that will be considered for BARCAP planning.
|
barcap_locations: The locations that will be considered for BARCAP planning.
|
||||||
air_defenses: TGOs that may have air defenses.
|
air_defenses: TGOs that may have air defenses.
|
||||||
@ -245,7 +254,8 @@ class ThreatZones:
|
|||||||
threat_zone = point.buffer(threat_range.meters)
|
threat_zone = point.buffer(threat_range.meters)
|
||||||
radar_sam_threats.append(threat_zone)
|
radar_sam_threats.append(threat_zone)
|
||||||
|
|
||||||
return cls(
|
return ThreatZones(
|
||||||
|
theater,
|
||||||
airbases=unary_union(air_threats),
|
airbases=unary_union(air_threats),
|
||||||
air_defenses=unary_union(air_defense_threats),
|
air_defenses=unary_union(air_defense_threats),
|
||||||
radar_sam_threats=unary_union(radar_sam_threats),
|
radar_sam_threats=unary_union(radar_sam_threats),
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.theater.controlpoint import ControlPointType
|
from game.ato.flightwaypoint import FlightWaypoint
|
||||||
from game.theater.theatergroundobject import IadsGroundObject, BuildingGroundObject
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
from game.utils import Distance
|
|
||||||
from game.missiongenerator.frontlineconflictdescription import (
|
from game.missiongenerator.frontlineconflictdescription import (
|
||||||
FrontLineConflictDescription,
|
FrontLineConflictDescription,
|
||||||
)
|
)
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.theater.controlpoint import ControlPointType
|
||||||
from game.ato.flightwaypoint import FlightWaypoint
|
from game.theater.theatergroundobject import BuildingGroundObject, IadsGroundObject
|
||||||
|
from game.utils import Distance
|
||||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||||
|
|
||||||
|
|
||||||
@ -72,10 +72,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
front_line, self.game.theater
|
front_line, self.game.theater
|
||||||
)[0]
|
)[0]
|
||||||
wpt = FlightWaypoint(
|
wpt = FlightWaypoint(
|
||||||
FlightWaypointType.CUSTOM,
|
FlightWaypointType.CUSTOM, pos, Distance.from_meters(800)
|
||||||
pos.x,
|
|
||||||
pos.y,
|
|
||||||
Distance.from_meters(800),
|
|
||||||
)
|
)
|
||||||
wpt.name = f"Frontline {front_line.name} [CAS]"
|
wpt.name = f"Frontline {front_line.name} [CAS]"
|
||||||
wpt.alt_type = "RADIO"
|
wpt.alt_type = "RADIO"
|
||||||
@ -94,8 +91,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
):
|
):
|
||||||
wpt = FlightWaypoint(
|
wpt = FlightWaypoint(
|
||||||
FlightWaypointType.CUSTOM,
|
FlightWaypointType.CUSTOM,
|
||||||
ground_object.position.x,
|
ground_object.position,
|
||||||
ground_object.position.y,
|
|
||||||
Distance.from_meters(0),
|
Distance.from_meters(0),
|
||||||
)
|
)
|
||||||
wpt.alt_type = "RADIO"
|
wpt.alt_type = "RADIO"
|
||||||
@ -122,8 +118,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
for j, u in enumerate(g.units):
|
for j, u in enumerate(g.units):
|
||||||
wpt = FlightWaypoint(
|
wpt = FlightWaypoint(
|
||||||
FlightWaypointType.CUSTOM,
|
FlightWaypointType.CUSTOM,
|
||||||
u.position.x,
|
u.position,
|
||||||
u.position.y,
|
|
||||||
Distance.from_meters(0),
|
Distance.from_meters(0),
|
||||||
)
|
)
|
||||||
wpt.alt_type = "RADIO"
|
wpt.alt_type = "RADIO"
|
||||||
@ -151,10 +146,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
self.include_friendly and cp.captured
|
self.include_friendly and cp.captured
|
||||||
):
|
):
|
||||||
wpt = FlightWaypoint(
|
wpt = FlightWaypoint(
|
||||||
FlightWaypointType.CUSTOM,
|
FlightWaypointType.CUSTOM, cp.position, Distance.from_meters(0)
|
||||||
cp.position.x,
|
|
||||||
cp.position.y,
|
|
||||||
Distance.from_meters(0),
|
|
||||||
)
|
)
|
||||||
wpt.alt_type = "RADIO"
|
wpt.alt_type = "RADIO"
|
||||||
wpt.name = cp.name
|
wpt.name = cp.name
|
||||||
|
|||||||
@ -4,9 +4,10 @@ from typing import Optional
|
|||||||
|
|
||||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
from dcs.mapping import LatLng
|
||||||
|
|
||||||
from game.server.leaflet import LeafletLatLon
|
from game.server.leaflet import LeafletLatLon
|
||||||
from game.theater import ConflictTheater, ControlPoint, ControlPointStatus, LatLon
|
from game.theater import ConflictTheater, ControlPoint, ControlPointStatus
|
||||||
from game.utils import meters, nautical_miles
|
from game.utils import meters, nautical_miles
|
||||||
from qt_ui.dialogs import Dialog
|
from qt_ui.dialogs import Dialog
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
@ -83,7 +84,7 @@ class ControlPointJs(QObject):
|
|||||||
|
|
||||||
@Slot(list, result=bool)
|
@Slot(list, result=bool)
|
||||||
def destinationInRange(self, destination: LeafletLatLon) -> bool:
|
def destinationInRange(self, destination: LeafletLatLon) -> bool:
|
||||||
return self.destination_in_range(self.theater.ll_to_point(LatLon(*destination)))
|
return self.destination_in_range(self.theater.ll_to_point(LatLng(*destination)))
|
||||||
|
|
||||||
@Slot(list, result=str)
|
@Slot(list, result=str)
|
||||||
def setDestination(self, destination: LeafletLatLon) -> str:
|
def setDestination(self, destination: LeafletLatLon) -> str:
|
||||||
@ -92,7 +93,7 @@ class ControlPointJs(QObject):
|
|||||||
if not self.control_point.captured:
|
if not self.control_point.captured:
|
||||||
return f"{self.control_point} is not owned by player"
|
return f"{self.control_point} is not owned by player"
|
||||||
|
|
||||||
point = self.theater.ll_to_point(LatLon(*destination))
|
point = self.theater.ll_to_point(LatLng(*destination))
|
||||||
if not self.destination_in_range(point):
|
if not self.destination_in_range(point):
|
||||||
return (
|
return (
|
||||||
f"Cannot move {self.control_point} more than "
|
f"Cannot move {self.control_point} more than "
|
||||||
|
|||||||
@ -32,7 +32,7 @@ pluggy==1.0.0
|
|||||||
pre-commit==2.17.0
|
pre-commit==2.17.0
|
||||||
py==1.11.0
|
py==1.11.0
|
||||||
pydantic==1.9.0
|
pydantic==1.9.0
|
||||||
-e git+https://github.com/pydcs/dcs@63863a88e0a43cb0a310dbab3ce2c7800a099dbb#egg=pydcs
|
-e git+https://github.com/DanAlbert/dcs@e652979adefca9f4358244cf9e42985ef58e9343#egg=pydcs
|
||||||
pyinstaller==4.9
|
pyinstaller==4.9
|
||||||
pyinstaller-hooks-contrib==2022.1
|
pyinstaller-hooks-contrib==2022.1
|
||||||
pyparsing==3.0.7
|
pyparsing==3.0.7
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user