Skip to content

Commit

Permalink
Enable random go arounds from windshear; fix wake separation indicato…
Browse files Browse the repository at this point in the history
…rs not showing for approaches without localizer; increase contact sector look-forward from 30s to 60s
  • Loading branch information
Bombbird2001 committed Mar 30, 2024
1 parent 96b6657 commit 97cf13b
Show file tree
Hide file tree
Showing 11 changed files with 57 additions and 11 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ configure(subprojects - project(':android')) {
subprojects {
version = '2.2.0-beta'
ext.appName = 'Terminal Control 2'
ext.buildVersion = 26
ext.buildVersion = 27
repositories {
mavenCentral()
maven { url 'https://s01.oss.sonatype.org' }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,15 @@ data class TrailInfo(val positions: Queue<Position> = Queue(MAX_TRAIL_DOTS)): Co
fun initialise() = InitializeCompanionObjectOnStart.initialise(this::class)
}
}

/** Component for tagging if an aircraft will go around due to windshear */
@JsonClass(generateAdapter = true)
data class WindshearGoAround(var goAround: Boolean = false): Component, BaseComponentJSONInterface {
override val componentType = BaseComponentJSONInterface.ComponentType.WINDSHEAR_GO_AROUND

companion object {
val mapper = object: Mapper<WindshearGoAround>() {}.mapper

fun initialise() = InitializeCompanionObjectOnStart.initialise(this::class)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,5 @@ fun loadAllComponents() {
WakeTolerance.initialise()
WakeTrail.initialise()
WaypointInfo.initialise()
WindshearGoAround.initialise()
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const val TRANS_PREFIX = "Via "
const val NO_TRANS_SELECTION = "..."

/** Control state constants */
const val TRACK_EXTRAPOLATE_TIME_S = 30f
const val TRACK_EXTRAPOLATE_TIME_S = 60f

/** Aircraft initial spawn */
const val ARRIVAL_SPAWN_EXTEND_DIST_NM = 0.5f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ val AVAIL_AIRPORTS = GdxArrayMap<String, String?>().apply {
put("TCBD", null)
put("TCMD", null)
put("TCPG", null)
put("TCSF", null)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface BaseComponentJSONInterface {
RANDOM_AIRLINE_DATA, ACTIVE_RUNWAY_CONFIG, ARRIVAL_CLOSED, TIME_SINCE_LAST_DEPARTURE, DEPARTURE_INFO, AIRPORT_NEXT_DEPARTURE,
RUNWAY_PREVIOUS_ARRIVAL, RUNWAY_PREVIOUS_DEPARTURE, RUNWAY_OCCUPIED, WAKE_TRAIL, WAKE_INFO, INITIAL_CLIENT_DATATAG_POSITION,
TRAIL_INFO, TTS_VOICE, EMERGENCY_PENDING, RUNNING_CHECKLISTS, REQUIRES_FUEL_DUMP, IMMOBILIZE_ON_LANDING, RUNWAY_CLOSED,
ON_GO_AROUND_ROUTE, WAKE_TOLERANCE, ACC_TEMP_ALTITUDE
ON_GO_AROUND_ROUTE, WAKE_TOLERANCE, ACC_TEMP_ALTITUDE, WINDSHEAR_GO_AROUND
}

val componentType: ComponentType
Expand Down Expand Up @@ -155,6 +155,7 @@ private fun getPolymorphicComponentAdapter(): PolymorphicJsonAdapterFactory<Base
.withSubtype(OnGoAroundRoute::class.java, BaseComponentJSONInterface.ComponentType.ON_GO_AROUND_ROUTE.name)
.withSubtype(WakeTolerance::class.java, BaseComponentJSONInterface.ComponentType.WAKE_TOLERANCE.name)
.withSubtype(ACCTempAltitude::class.java, BaseComponentJSONInterface.ComponentType.ACC_TEMP_ALTITUDE.name)
.withSubtype(WindshearGoAround::class.java, BaseComponentJSONInterface.ComponentType.WINDSHEAR_GO_AROUND.name)
}

/** Interface for implementing JSON serialization for subclasses of Leg */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,14 @@ class AISystem: EntitySystem() {
if (it.crosswindKt > 25) return@apply initiateGoAround(this, RecentGoAround.STRONG_CROSSWIND)
}

// Check windshear go around
if (hasNot(WindshearGoAround.mapper)) {
this += WindshearGoAround(generateRandomWindshearGoAround(rwyObj, arptMetar))
}
if (get(WindshearGoAround.mapper)?.goAround == true) {
return@apply initiateGoAround(this, RecentGoAround.WINDSHEAR)
}

// Check runway occupancy or runway closed
// For all approaches, go around if runway is still occupied or closed by the time aircraft reaches 150 feet AGL
val rwyAlt = rwyObj[Altitude.mapper]?.altitudeFt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.bombbird.terminalcontrol2.utilities.*
import ktx.ashley.allOf
import ktx.ashley.get
import ktx.collections.GdxArray
import ktx.math.unaryMinus
import kotlin.math.roundToInt

/**
Expand Down Expand Up @@ -72,25 +73,27 @@ class DataSystemClient: EntitySystem() {
val locCap = get(LocalizerCaptured.mapper)
val arrAirport = get(ArrivalAirport.mapper)
val clearance = get(ClearanceAct.mapper)
val refApp = (if (arrAirport != null && clearance != null) {
val refAppAndDir = (if (arrAirport != null && clearance != null) {
val approaches = GAME.gameClientScreen?.airports?.get(arrAirport.arptId)?.entity?.get(ApproachChildren.mapper)?.approachMap
val reqApp = approaches?.get(clearance.actingClearance.clearanceState.clearedApp)?.entity
// We will also check if the aircraft is heading towards the approach (i.e. within 90 degrees of the approach heading)
val acDir = radar.direction
val appDir = reqApp?.get(Direction.mapper)
val appDirUnitVector = reqApp?.get(Direction.mapper)?.trackUnitVector
?: reqApp?.get(ApproachInfo.mapper)?.rwyObj?.entity?.getOrLogMissing(Direction.mapper)?.let { -it.trackUnitVector }
// Since the app's direction is the opposite of the track, we need to check if the dot product is <= 0 before adding
if (locCap == null && (appDir == null || acDir.trackUnitVector.dot(appDir.trackUnitVector) > 0)) null
else reqApp
if (locCap == null && (appDirUnitVector == null || acDir.trackUnitVector.dot(appDirUnitVector) > 0)) null
else Pair(reqApp, appDirUnitVector)
} else null) ?: return@apply
val refApp = refAppAndDir.first ?: return@apply
val refAppDir = refAppAndDir.second ?: return@apply
val alt = get(Altitude.mapper)?.altitudeFt ?: return@apply
val rwyAlt = refApp[ApproachInfo.mapper]?.rwyObj?.entity?.get(Altitude.mapper)?.altitudeFt ?: return@apply
if (alt < rwyAlt + 10) return@apply // Aircraft has touched down
val refPos = refApp[Position.mapper] ?: return@apply
val dist = calculateDistanceBetweenPoints(acPos.x, acPos.y, refPos.x, refPos.y)
val maxDistNm = refApp[Localizer.mapper]?.maxDistNm ?: 15
val appDir = refApp[Direction.mapper]?.trackUnitVector ?: return@apply
// Not within 25 degrees of localizer centerline
if (!checkInArc(refPos.x, refPos.y, convertWorldAndRenderDeg(appDir.angleDeg()), nmToPx(maxDistNm.toInt()),
if (!checkInArc(refPos.x, refPos.y, convertWorldAndRenderDeg(refAppDir.angleDeg()), nmToPx(maxDistNm.toInt()),
25f, acPos.x, acPos.y)
) return@apply
refApp[ApproachWakeSequence.mapper]?.aircraftDist?.add(Pair(this, dist))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,8 @@ class RenderingSystemClient(private val shapeRenderer: ShapeRendererBoundingBox,
for (i in 0 until wakeRenderEntities.size()) {
wakeRenderEntities[i]?.apply {
val position = getOrLogMissing(Position.mapper) ?: return@apply
val appOppDirUnitVector = getOrLogMissing(Direction.mapper)?.trackUnitVector ?: return@apply
val appOppDirUnitVector = get(Direction.mapper)?.trackUnitVector
?: get(ApproachInfo.mapper)?.rwyObj?.entity?.getOrLogMissing(Direction.mapper)?.let { -it.trackUnitVector } ?: return@apply
val acDistances = getOrLogMissing(ApproachWakeSequence.mapper)?.aircraftDist ?: return@apply
for (j in 0 until acDistances.size - 1) {
val ac1 = acDistances[j].first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ fun removeAllApproachComponents(aircraft: Entity) {
remove<AppDecelerateTo190kts>()
remove<DecelerateToAppSpd>()
remove<ContactToTower>()
remove<WindshearGoAround>()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ private fun generateRandomWsForAllRwy(airport: Entity): String {
val prob = (1 / (1 + exp(-b0 - b1 * speed.toDouble()))).toFloat()

var landingRwyCount = 0
var landingWsRwyCount = 0
val stringBuilder = StringBuilder()
val rwyEntries = Entries(airport[RunwayChildren.mapper]?.rwyMap ?: return "")
rwyEntries.mapNotNull { it.value.entity }.forEach { rwy ->
Expand All @@ -263,9 +264,26 @@ private fun generateRandomWsForAllRwy(airport: Entity): String {
stringBuilder.append("R")
stringBuilder.append(rwy[RunwayInfo.mapper]?.rwyName)
stringBuilder.append(" ")
landingWsRwyCount++
}
}
return if (stringBuilder.length > 3 && stringBuilder.length == landingRwyCount * 3)"ALL RWY" else stringBuilder.toString()
return if (landingWsRwyCount == landingRwyCount) "ALL RWY" else stringBuilder.toString()
}

/**
* Randomly generates whether an aircraft will go around for the input [rwy] due to the presence of windshear,
* using the airport's [metar] information
*
* Returns true if the aircraft will go around, false otherwise
*/
fun generateRandomWindshearGoAround(rwy: Entity, metar: MetarInfo): Boolean {
val rwyName = "R${rwy[RunwayInfo.mapper]?.rwyName ?: return false}"
val ws = metar.windshear
if (ws != "ALL RWY" && !ws.contains(rwyName)) return false
var baseChance = 0.2f
val gusts = metar.windGustKt
if (gusts > 0) baseChance += (1 - baseChance) * MathUtils.clamp(1.2f * (gusts - 15) / 100, 0f, 1f)
return MathUtils.randomBoolean(baseChance)
}

/**
Expand Down

0 comments on commit 97cf13b

Please sign in to comment.