Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/skyeye/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ var (
discordWebhookID string
discordWebhookToken string
exitAfter time.Duration
locationsFile string
)

func init() {
Expand Down Expand Up @@ -142,6 +143,7 @@ func init() {
skyeye.Flags().DurationVar(&threatMonitoringInterval, "threat-monitoring-interval", 3*time.Minute, "How often to broadcast THREAT")
skyeye.Flags().Float64Var(&mandatoryThreatRadiusNM, "mandatory-threat-radius", 25, "Briefed radius for mandatory THREAT calls, in nautical miles")
skyeye.Flags().BoolVar(&threatMonitoringRequiresSRS, "threat-monitoring-requires-srs", true, "Require aircraft to be on SRS to receive THREAT calls. Only useful to disable when debugging")
skyeye.Flags().StringVar(&locationsFile, "locations-file", "", "Path to file containing additional locations that may be referenced in ALPHA CHECK and VECTOR calls.")

// Tracing
skyeye.Flags().BoolVar(&enableTracing, "enable-tracing", false, "Enable tracing")
Expand Down
28 changes: 28 additions & 0 deletions docs/LOCATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Custom Locations

Server administrators can define custom locations which players can reference in ALPHA CHECK and VECTOR requests. You can define useful locations like friendly bases or fix points.

To use this feature, create a `locations.json` file. The content of the file should be a JSON array of objects. Each object should have the following properties:

- `names`: An array of strings, which are the names of the location. These names are used in the ALPHA CHECK and VECTOR requests. You can define multiple names for the same location - for example, you might have a location with both the names "Incirlik" and "Home plate". Note that the names "Bullseye" and "Tanker" are reserved and cannot be used as custom location names.
- `latitude`: A floating point number, which is the latitude of the location in decimal degrees. This should be a number between -90 and 90.
- `longitude`: A floating point number, which is the longitude of the location in decimal degrees. This should be a number between -180 and 180.

Example:

```json
[
{
"names": ["Incirlik", "Home plate"],
"latitude": 37.001166662,
"longitude": 35.422164978
},
{
"names": ["Hatay", "Divert option"],
"latitude": 36.362778,
"longitude": 36.282222
}
]
```

Set the path to the `locations.json` file in the `locations-file` setting in SkyEye's configuration.
25 changes: 24 additions & 1 deletion docs/PLAYER.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Tips:

Keyword: `ALPHA`

Function: The GCI will check if they see you on scope and tell you your approximate current location in bullseye format.
Function: The GCI will check if they see you on scope and tell you your approximate current location as a bearing and range from a given point. You can ask for an Alpha Check from bullseye, or from a [named location](LOCATIONS.md). The bullseye is used if no point is specified.

Use: You can use this to coarsely check your INS navigation system in an aircraft without GPS. It is accurate to within several miles (accounting for potential lag time between when the bot checks the scope and when the response is sent on the radio).

Expand All @@ -189,6 +189,29 @@ YELLOW 13: "Goliath Yellow One Three alpha"
GOLIATH: "Yellow One Three, Goliath, contact, alpha check bullseye 088/5"
```

### VECTOR

Keyword: `VECTOR`

Function: The GCI will check if you are on scope and tell you the approximate bearing and range from you to a given point. You can ask for a vector to bullseye, "tanker", or to a [named location](LOCATIONS.md). The bullseye is used if no point is specified.

Asking for a vector to "tanker" requests a vector to the nearest tanker aircraft which is compatible with your aircraft.

Use: This is the reciprocal of an ALPHA CHECK, and is useful for navigation assistance.

Examples:

```
MOBIUS 1: "Thunderhead Mobius One vector to home plate."
THUNDERHEAD: "Mobius One, Thunderhead, vector to HOME PLATE 010/122"
```

```
WARDOG 14: "Magic, Wardog One Four, vector to tanker"
MAGIC: "Wardog One Four, vector to Shell One One 099/75"
```


### BOGEY DOPE

Keyword: `BOGEY`
Expand Down
4 changes: 3 additions & 1 deletion internal/application/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/dharmab/skyeye/pkg/commands"
"github.com/dharmab/skyeye/pkg/composer"
"github.com/dharmab/skyeye/pkg/controller"
loc "github.com/dharmab/skyeye/pkg/locations"
"github.com/dharmab/skyeye/pkg/parser"
"github.com/dharmab/skyeye/pkg/radar"
"github.com/dharmab/skyeye/pkg/recognizer"
Expand Down Expand Up @@ -167,7 +168,7 @@ func NewApplication(config conf.Configuration) (*Application, error) {
}

log.Info().Msg("constructing request parser")
requestParser := parser.New(config.Callsign, config.EnableTranscriptionLogging)
requestParser := parser.New(config.Callsign, []string{}, config.EnableTranscriptionLogging)

log.Info().Msg("constructing radar scope")

Expand All @@ -182,6 +183,7 @@ func NewApplication(config conf.Configuration) (*Application, error) {
config.EnableThreatMonitoring,
config.ThreatMonitoringInterval,
config.ThreatMonitoringRequiresSRS,
[]loc.Location{},
)

log.Info().Msg("constructing response composer")
Expand Down
2 changes: 2 additions & 0 deletions internal/application/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ func (a *Application) composeCall(ctx context.Context, call any, out chan<- Mess
response = a.composer.ComposeSpikedResponse(c)
case brevity.TripwireResponse:
response = a.composer.ComposeTripwireResponse(c)
case brevity.VectorResponse:
response = a.composer.ComposeVectorResponse(c)
case brevity.SunriseCall:
response = a.composer.ComposeSunriseCall(c)
case brevity.ThreatCall:
Expand Down
2 changes: 2 additions & 0 deletions internal/application/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func (a *Application) handleRequest(ctx context.Context, r any) {
a.controller.HandleSpiked(ctx, request)
case *brevity.TripwireRequest:
a.controller.HandleTripwire(ctx, request)
case *brevity.VectorRequest:
a.controller.HandleVector(ctx, request)
case *brevity.UnableToUnderstandRequest:
a.controller.HandleUnableToUnderstand(ctx, request)
default:
Expand Down
9 changes: 9 additions & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ type Configuration struct {
// ThreatMonitoringRequiresSRS controls whether threat calls are issued to aircraft that are not on an SRS frequency. This is mostly
// for debugging.
ThreatMonitoringRequiresSRS bool
// Locations is a slice of named locations that can be referenced in ALPHA CHECK and VECTOR calls.
Locations []*Location
// EnableTracing controls whether to publish traces
EnableTracing bool
// DiscordWebhookID is the ID of the Discord webhook
Expand All @@ -101,6 +103,13 @@ type Configuration struct {
ExitAfter time.Duration
}

type Location struct {
// Names of the location
Names []string `json:"names"`
// Coordinates of the location as a GeoJSON coordinates array with a single member
Coordinates [][]float64 `json:"coordinates"`
}

var DefaultCallsigns = []string{"Sky Eye", "Thunderhead", "Eagle Eye", "Ghost Eye", "Sky Keeper", "Bandog", "Long Caster", "Galaxy"}

var DefaultPictureRadius = 300 * unit.NauticalMile
Expand Down
29 changes: 8 additions & 21 deletions pkg/brevity/braa.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package brevity

import (
"fmt"
"math"

"github.com/dharmab/skyeye/pkg/bearings"
"github.com/dharmab/skyeye/pkg/spatial"
Expand All @@ -20,20 +19,16 @@ type BRAA interface {

// BRA is an abbreviated form of BRAA without aspect.
type BRA interface {
// Bearing is the heading from the fighter to the contact, rounded to the nearest degree.
Bearing() bearings.Bearing
// Range is the distance from the fighter to the contact, rounded to the nearest nautical mile.
Range() unit.Length
Vector
// Altitude of the contact above sea level, rounded to the nearest thousands of feet.
Altitude() unit.Length
// Altitude STACKS of the contact above sea level, rounded to the nearest thousands of feet.
Stacks() []Stack
}

type bra struct {
bearing bearings.Bearing
_range unit.Length
stacks []Stack
vector
stacks []Stack
}

// NewBRA creates a new [BRA].
Expand All @@ -42,22 +37,14 @@ func NewBRA(b bearings.Bearing, r unit.Length, a ...unit.Length) BRA {
log.Warn().Stringer("bearing", b).Msg("bearing provided to NewBRA should be magnetic")
}
return &bra{
bearing: b,
_range: r,
stacks: Stacks(a...),
vector: vector{
bearing: b,
distance: r,
},
stacks: Stacks(a...),
}
}

// Bearing implements [BRA.Bearing].
func (b *bra) Bearing() bearings.Bearing {
return b.bearing
}

// Range implements [BRA.Range].
func (b *bra) Range() unit.Length {
return unit.Length(math.Round(b._range.NauticalMiles())) * unit.NauticalMile
}

// Altitude implements [BRA.Altitude].
func (b *bra) Altitude() unit.Length {
if len(b.stacks) == 0 {
Expand Down
33 changes: 19 additions & 14 deletions pkg/brevity/bullseye.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package brevity

import (
"fmt"
"math"

"github.com/dharmab/skyeye/pkg/bearings"
"github.com/martinlindhe/unit"
Expand All @@ -11,32 +10,38 @@ import (

// Bullseye is a magnetic bearing and distance from a reference point called the BULLSEYE.
// Reference: ATP 3-52.4 Chapter IV section 4 subsection a.
type Bullseye struct {
bearing bearings.Bearing
distance unit.Length
type Bullseye interface {
Bearing() bearings.Bearing
Distance() unit.Length
}

type bullseye struct {
vector
}

// NewBullseye creates a new [Bullseye].
func NewBullseye(bearing bearings.Bearing, distance unit.Length) *Bullseye {
func NewBullseye(bearing bearings.Bearing, distance unit.Length) Bullseye {
if !bearing.IsMagnetic() {
log.Warn().Stringer("bearing", bearing).Msg("bearing provided to NewBullseye should be magnetic")
}
return &Bullseye{
bearing: bearing,
distance: distance,
return &bullseye{
vector: vector{
bearing: bearing,
distance: distance,
},
}
}

// Bearing from the BULLSEYE to the contact, rounded to the nearest degree.
func (b *Bullseye) Bearing() bearings.Bearing {
return b.bearing
func (b *bullseye) Bearing() bearings.Bearing {
return b.vector.Bearing()
}

// Distance from the BULLSEYE to the contact, rounded to the nearest nautical mile.
func (b *Bullseye) Distance() unit.Length {
return unit.Length(math.Round(b.distance.NauticalMiles())) * unit.NauticalMile
func (b *bullseye) Distance() unit.Length {
return b.Range()
}

func (b *Bullseye) String() string {
return fmt.Sprintf("%s/%.0f", b.bearing, b.distance.NauticalMiles())
func (b *bullseye) String() string {
return fmt.Sprintf("%s/%.0f", b.Bearing(), b.Distance().NauticalMiles())
}
2 changes: 1 addition & 1 deletion pkg/brevity/declare.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (r DeclareRequest) String() string {
s += fmt.Sprintf(", altitude %.0f", r.Altitude.Feet())
}
} else {
s += fmt.Sprintf("bullseye %s", &r.Bullseye)
s += fmt.Sprintf("bullseye %s", r.Bullseye)
}
if r.Track != UnknownDirection {
s += fmt.Sprintf(", track %s", r.Track)
Expand Down
2 changes: 1 addition & 1 deletion pkg/brevity/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Group interface {
// Contacts is the number of contacts in the group.
Contacts() int
// Bullseye is the location of the group. This may be nil for BOGEY DOPE, SNAPLOCK, and THREAT calls.
Bullseye() *Bullseye
Bullseye() Bullseye
// Altitude is the group's highest altitude. This may be zero for BOGEY DOPE, SNAPLOCK, and THREAT calls.
Altitude() unit.Length
// Stacks are the group's altitude STACKS, ordered from highest to lowest in intervals of at least 10,000 feet.
Expand Down
62 changes: 62 additions & 0 deletions pkg/brevity/vector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package brevity

import (
"math"

"github.com/dharmab/skyeye/pkg/bearings"
"github.com/martinlindhe/unit"
)

type Vector interface {
// Bearing is the heading from the fighter to the target location, rounded to the nearest degree.
Bearing() bearings.Bearing
// Range is the distance from the fighter to the target location, rounded to the nearest nautical mile.
Range() unit.Length
}

type vector struct {
bearing bearings.Bearing
distance unit.Length
}

func NewVector(bearing bearings.Bearing, distance unit.Length) Vector {
return &vector{
bearing: bearing,
distance: distance,
}
}

// Bearing implements [Vector.Bearing].
func (v *vector) Bearing() bearings.Bearing {
return v.bearing
}

// Range implements [Vector.Range].
func (v *vector) Range() unit.Length {
return unit.Length(math.Round(v.distance.NauticalMiles())) * unit.NauticalMile
}

type VectorRequest struct {
// Callsign of the friendly aircraft requesting the vector.
Callsign string
// Location to which the friendly aircraft is requesting a vector.
Location string
}

func (r VectorRequest) String() string {
return "VECTOR to " + r.Location + " for " + r.Callsign
}

type VectorResponse struct {
// Callsign of the friendly aircraft requesting the vector.
Callsign string
// Location which the friendly aircraft is requesting a vector.
Location string
// Contact is true if the callsign was correlated to an aircraft on frequency, otherwise false.
Contact bool
// Status is true if the vector was successfully computed, otherwise false.
Status bool
// Vector is the computed vector to the target location, if available.
// // If Status is false, this may be nil.
Vector Vector
}
2 changes: 1 addition & 1 deletion pkg/composer/faded.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func (c *Composer) ComposeFadedCall(call brevity.FadedCall) (response NaturalLan
}

if bullseye := call.Group.Bullseye(); bullseye != nil {
bullseye := c.composeBullseye(*bullseye)
bullseye := c.composeBullseye(bullseye)
response.WriteResponse(bullseye)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/composer/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (c *Composer) composeGroup(group brevity.Group) (response NaturalLanguageRe
stacks := group.Stacks()
isTrackKnown := group.Track() != brevity.UnknownDirection
if group.Bullseye() != nil {
bullseye := c.composeBullseye(*group.Bullseye())
bullseye := c.composeBullseye(group.Bullseye())
altitude := c.composeAltitudeStacks(stacks, group.Declaration())
response.Write(
fmt.Sprintf("%s %s, %s", label, bullseye.Speech, altitude),
Expand Down
Loading
Loading