Skip to content

Commit 8a83cd0

Browse files
dharmabclaude
andcommitted
Address PR review: return structs, validate locations, fix docs
- Refactor Vector/Bullseye/BRA/BRAA from interfaces to public structs following idiomatic Go "return structs, accept interfaces" - Remove redundant conf.Location; use locations.Location directly - Load locations from file and pass to parser and controller - Validate reserved location names ("tanker", "bullseye") at startup - Mark --locations-file as filename flag for shell completion - Remove unnecessary loc import alias - Add missing godocs on public symbols - Fix double comment in vector.go - Update LOCATIONS.md wording for non-server users Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9c5cf94 commit 8a83cd0

21 files changed

Lines changed: 135 additions & 125 deletions

File tree

cmd/skyeye/main.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ import (
2525
"github.com/spf13/pflag"
2626
"github.com/spf13/viper"
2727

28+
"encoding/json"
29+
2830
"github.com/dharmab/skyeye/internal/application"
2931
"github.com/dharmab/skyeye/internal/cli"
3032
"github.com/dharmab/skyeye/internal/conf"
3133
"github.com/dharmab/skyeye/pkg/coalitions"
34+
"github.com/dharmab/skyeye/pkg/locations"
3235
"github.com/dharmab/skyeye/pkg/synthesizer/voices"
3336
"github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper"
3437
)
@@ -154,6 +157,9 @@ func init() {
154157
skyeye.Flags().Float64Var(&mandatoryThreatRadiusNM, "mandatory-threat-radius", 25, "Briefed radius for mandatory THREAT calls, in nautical miles")
155158
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")
156159
skyeye.Flags().StringVar(&locationsFile, "locations-file", "", "Path to file containing additional locations that may be referenced in ALPHA CHECK and VECTOR calls.")
160+
if err := skyeye.MarkFlagFilename("locations-file", "json"); err != nil {
161+
log.Fatal().Err(err).Msg("failed to mark flag as filename")
162+
}
157163

158164
// Tracing
159165
skyeye.Flags().BoolVar(&enableTracing, "enable-tracing", false, "Enable tracing")
@@ -337,6 +343,27 @@ func loadVoiceVolume() float64 {
337343
return clamped
338344
}
339345

346+
func loadLocations() []locations.Location {
347+
if locationsFile == "" {
348+
return nil
349+
}
350+
data, err := os.ReadFile(locationsFile)
351+
if err != nil {
352+
log.Fatal().Err(err).Str("path", locationsFile).Msg("failed to read locations file")
353+
}
354+
var locs []locations.Location
355+
if err := json.Unmarshal(data, &locs); err != nil {
356+
log.Fatal().Err(err).Str("path", locationsFile).Msg("failed to parse locations file")
357+
}
358+
for _, loc := range locs {
359+
if err := loc.Validate(); err != nil {
360+
log.Fatal().Err(err).Msg("invalid location in locations file")
361+
}
362+
}
363+
log.Info().Int("count", len(locs)).Msg("loaded custom locations")
364+
return locs
365+
}
366+
340367
func preRun(cmd *cobra.Command, _ []string) error {
341368
if err := initializeConfig(cmd); err != nil {
342369
return fmt.Errorf("failed to initialize config: %w", err)
@@ -384,6 +411,7 @@ func run(_ *cobra.Command, _ []string) {
384411
voiceLock := loadLock(voiceLockPath)
385412
recognizerLock := loadLock(recognizerLockPath)
386413
volume := loadVoiceVolume()
414+
locs := loadLocations()
387415

388416
config := conf.Configuration{
389417
ACMIFile: acmiFile,
@@ -425,6 +453,7 @@ func run(_ *cobra.Command, _ []string) {
425453
GRPCAddress: grpcAddress,
426454
GRPCAPIKey: grpcAPIKey,
427455
EnableTerrainDetection: enableTerrainDetection,
456+
Locations: locs,
428457
}
429458

430459
log.Info().Msg("starting application")

docs/LOCATIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Custom Locations
22

3-
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.
3+
You 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.
44

55
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:
66

internal/application/app.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/dharmab/skyeye/pkg/commands"
1818
"github.com/dharmab/skyeye/pkg/composer"
1919
"github.com/dharmab/skyeye/pkg/controller"
20-
loc "github.com/dharmab/skyeye/pkg/locations"
2120
"github.com/dharmab/skyeye/pkg/parser"
2221
"github.com/dharmab/skyeye/pkg/radar"
2322
"github.com/dharmab/skyeye/pkg/recognizer"
@@ -169,8 +168,13 @@ func NewApplication(config conf.Configuration) (*Application, error) {
169168
return nil, fmt.Errorf("failed to construct application: unrecognized recognizer %q", config.Recognizer)
170169
}
171170

171+
locationNames := make([]string, 0)
172+
for _, loc := range config.Locations {
173+
locationNames = append(locationNames, loc.Names...)
174+
}
175+
172176
log.Info().Msg("constructing request parser")
173-
requestParser := parser.New(config.Callsign, []string{}, config.EnableTranscriptionLogging)
177+
requestParser := parser.New(config.Callsign, locationNames, config.EnableTranscriptionLogging)
174178

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

@@ -185,7 +189,7 @@ func NewApplication(config conf.Configuration) (*Application, error) {
185189
config.EnableThreatMonitoring,
186190
config.ThreatMonitoringInterval,
187191
config.ThreatMonitoringRequiresSRS,
188-
[]loc.Location{},
192+
config.Locations,
189193
)
190194

191195
log.Info().Msg("constructing response composer")

internal/conf/configuration.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"time"
55

66
"github.com/dharmab/skyeye/pkg/coalitions"
7+
"github.com/dharmab/skyeye/pkg/locations"
78
"github.com/dharmab/skyeye/pkg/simpleradio"
89
"github.com/dharmab/skyeye/pkg/synthesizer/voices"
910
"github.com/ggerganov/whisper.cpp/bindings/go/pkg/whisper"
@@ -94,7 +95,7 @@ type Configuration struct {
9495
// for debugging.
9596
ThreatMonitoringRequiresSRS bool
9697
// Locations is a slice of named locations that can be referenced in ALPHA CHECK and VECTOR calls.
97-
Locations []*Location
98+
Locations []locations.Location
9899
// EnableTracing controls whether to publish traces
99100
EnableTracing bool
100101
// DiscordWebhookID is the ID of the Discord webhook
@@ -108,13 +109,6 @@ type Configuration struct {
108109
EnableTerrainDetection bool
109110
}
110111

111-
type Location struct {
112-
// Names of the location
113-
Names []string `json:"names"`
114-
// Coordinates of the location as a GeoJSON coordinates array with a single member
115-
Coordinates [][]float64 `json:"coordinates"`
116-
}
117-
118112
var DefaultCallsigns = []string{"Sky Eye", "Thunderhead", "Eagle Eye", "Ghost Eye", "Sky Keeper", "Bandog", "Long Caster", "Galaxy"}
119113

120114
var DefaultPictureRadius = 300 * unit.NauticalMile

pkg/brevity/alphacheck.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ type AlphaCheckResponse struct {
2020
// Status is true if the ALPHA CHECK was correlated to an aircraft on frequency, otherwise false.
2121
Status bool
2222
// Location of the friendly aircraft. If Status is false, this may be nil.
23-
Location Bullseye
23+
Location *Bullseye
2424
}

pkg/brevity/braa.go

Lines changed: 25 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,102 +11,75 @@ import (
1111

1212
// BRAA provides target bearing, range, altitude and aspect relative to a specified friendly aircraft.
1313
// Reference: ATP 3-52.4 Chapter IV section 4 subsection b.
14-
type BRAA interface {
14+
type BRAA struct {
1515
BRA
16-
// Aspect of the contact.
17-
Aspect() Aspect
16+
// aspect of the contact.
17+
aspect Aspect
1818
}
1919

2020
// BRA is an abbreviated form of BRAA without aspect.
21-
type BRA interface {
21+
type BRA struct {
2222
Vector
23-
// Altitude of the contact above sea level, rounded to the nearest thousands of feet.
24-
Altitude() unit.Length
25-
// Altitude STACKS of the contact above sea level, rounded to the nearest thousands of feet.
26-
Stacks() []Stack
27-
}
28-
29-
type bra struct {
30-
vector
3123
stacks []Stack
3224
}
3325

3426
// NewBRA creates a new [BRA].
35-
func NewBRA(b bearings.Bearing, r unit.Length, a ...unit.Length) BRA {
27+
func NewBRA(b bearings.Bearing, r unit.Length, a ...unit.Length) *BRA {
3628
if !b.IsMagnetic() {
3729
log.Warn().Stringer("bearing", b).Msg("bearing provided to NewBRA should be magnetic")
3830
}
39-
return &bra{
40-
vector: vector{
31+
return &BRA{
32+
Vector: Vector{
4133
bearing: b,
4234
distance: r,
4335
},
4436
stacks: Stacks(a...),
4537
}
4638
}
4739

48-
// Altitude implements [BRA.Altitude].
49-
func (b *bra) Altitude() unit.Length {
40+
// Altitude returns the highest altitude of the contact above sea level, rounded to the nearest thousands of feet.
41+
func (b *BRA) Altitude() unit.Length {
5042
if len(b.stacks) == 0 {
5143
return 0
5244
}
5345
return spatial.NormalizeAltitude(b.stacks[0].Altitude)
5446
}
5547

56-
// Stacks implements [BRA.Stacks].
57-
func (b *bra) Stacks() []Stack {
48+
// Stacks returns the altitude STACKS of the contact.
49+
func (b *BRA) Stacks() []Stack {
5850
return b.stacks
5951
}
6052

61-
func (b *bra) String() string {
53+
func (b *BRA) String() string {
6254
s := fmt.Sprintf("BRA %s/%.0f %.0f", b.Bearing(), b.Range().NauticalMiles(), b.Altitude().Feet())
6355
if len(b.Stacks()) > 1 {
6456
s += fmt.Sprintf(" (%v)", b.Stacks())
6557
}
6658
return s
6759
}
6860

69-
type braa struct {
70-
bra BRA
71-
aspect Aspect
72-
}
73-
7461
// NewBRAA creates a new [BRAA].
75-
func NewBRAA(b bearings.Bearing, r unit.Length, altitudes []unit.Length, aspect Aspect) BRAA {
62+
func NewBRAA(b bearings.Bearing, r unit.Length, altitudes []unit.Length, aspect Aspect) *BRAA {
7663
if !b.IsMagnetic() {
7764
log.Warn().Stringer("bearing", b).Msg("bearing provided to NewBRAA should be magnetic")
7865
}
79-
return &braa{
80-
bra: NewBRA(b, r, altitudes...),
66+
return &BRAA{
67+
BRA: BRA{
68+
Vector: Vector{
69+
bearing: b,
70+
distance: r,
71+
},
72+
stacks: Stacks(altitudes...),
73+
},
8174
aspect: aspect,
8275
}
8376
}
8477

85-
// Bearing implements [BRA.Bearing].
86-
func (b *braa) Bearing() bearings.Bearing {
87-
return b.bra.Bearing()
88-
}
89-
90-
// Range implements [BRA.Range].
91-
func (b *braa) Range() unit.Length {
92-
return b.bra.Range()
93-
}
94-
95-
// Altitude implements [BRA.Altitude].
96-
func (b *braa) Altitude() unit.Length {
97-
return b.bra.Altitude()
98-
}
99-
100-
// Stacks implements [BRA.Stacks].
101-
func (b *braa) Stacks() []Stack {
102-
return b.bra.Stacks()
103-
}
104-
105-
// Aspect implements [BRAA.Aspect].
106-
func (b *braa) Aspect() Aspect {
78+
// Aspect returns the aspect of the contact.
79+
func (b *BRAA) Aspect() Aspect {
10780
return b.aspect
10881
}
10982

110-
func (b *braa) String() string {
111-
return fmt.Sprintf("%s %s", b.bra, b.Aspect())
83+
func (b *BRAA) String() string {
84+
return fmt.Sprintf("%s %s", &b.BRA, b.Aspect())
11285
}

pkg/brevity/bullseye.go

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,28 @@ import (
1010

1111
// Bullseye is a magnetic bearing and distance from a reference point called the BULLSEYE.
1212
// Reference: ATP 3-52.4 Chapter IV section 4 subsection a.
13-
type Bullseye interface {
14-
Bearing() bearings.Bearing
15-
Distance() unit.Length
16-
}
17-
18-
type bullseye struct {
19-
vector
13+
type Bullseye struct {
14+
Vector
2015
}
2116

2217
// NewBullseye creates a new [Bullseye].
23-
func NewBullseye(bearing bearings.Bearing, distance unit.Length) Bullseye {
18+
func NewBullseye(bearing bearings.Bearing, distance unit.Length) *Bullseye {
2419
if !bearing.IsMagnetic() {
2520
log.Warn().Stringer("bearing", bearing).Msg("bearing provided to NewBullseye should be magnetic")
2621
}
27-
return &bullseye{
28-
vector: vector{
22+
return &Bullseye{
23+
Vector: Vector{
2924
bearing: bearing,
3025
distance: distance,
3126
},
3227
}
3328
}
3429

35-
// Bearing from the BULLSEYE to the contact, rounded to the nearest degree.
36-
func (b *bullseye) Bearing() bearings.Bearing {
37-
return b.vector.Bearing()
38-
}
39-
4030
// Distance from the BULLSEYE to the contact, rounded to the nearest nautical mile.
41-
func (b *bullseye) Distance() unit.Length {
31+
func (b *Bullseye) Distance() unit.Length {
4232
return b.Range()
4333
}
4434

45-
func (b *bullseye) String() string {
35+
func (b *Bullseye) String() string {
4636
return fmt.Sprintf("%s/%.0f", b.Bearing(), b.Distance().NauticalMiles())
4737
}

pkg/brevity/declare.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type DeclareRequest struct {
5252
// were providing Bullseye or BRAA coordinates.
5353
IsAmbiguous bool
5454
// Bullseye of the contact, if provided using Bullseye.
55-
Bullseye Bullseye
55+
Bullseye *Bullseye
5656
// Bearing of the contact, if provided using BRAA.
5757
Bearing bearings.Bearing
5858
/// Range to the contact, if provided using BRAA.
@@ -92,8 +92,7 @@ type DeclareResponse struct {
9292
// because the friendly aircraft did not provide coordinates for the
9393
// contact.
9494
Sour bool
95-
// If readback is not nil, the controller should read back the coordinate
96-
// in the response.
95+
// Readback is the bullseye coordinate to read back in the response. May be nil.
9796
Readback *Bullseye
9897
// Declaration of the contact.
9998
Declaration Declaration

pkg/brevity/group.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type Group interface {
1919
// Contacts is the number of contacts in the group.
2020
Contacts() int
2121
// Bullseye is the location of the group. This may be nil for BOGEY DOPE, SNAPLOCK, and THREAT calls.
22-
Bullseye() Bullseye
22+
Bullseye() *Bullseye
2323
// Altitude is the group's highest altitude. This may be zero for BOGEY DOPE, SNAPLOCK, and THREAT calls.
2424
Altitude() unit.Length
2525
// Stacks are the group's altitude STACKS, ordered from highest to lowest in intervals of at least 10,000 feet.
@@ -30,7 +30,7 @@ type Group interface {
3030
// Aspect is the group's aspect angle relative to another aircraft. This may be nil for BOGEY DOPE, SNAPLOCK, SPIKED, STROBE and some THREAT calls.
3131
Aspect() Aspect
3232
// BRAA is an alternate format for the group's location. This is nil except for BOGEY DOPE, SNAPLOCK, SPIKED, STROBE and some THREAT calls.
33-
BRAA() BRAA
33+
BRAA() *BRAA
3434
// Declaration of the group's friend or foe status.
3535
Declaration() Declaration
3636
// SetDeclaration sets the group's friend or foe status.

pkg/brevity/snaplock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type SnaplockRequest struct {
99
// Callsign of the friendly aircraft requesting the SNAPLOCK.
1010
Callsign string
1111
// BRA is the location of the contact.
12-
BRA BRA
12+
BRA *BRA
1313
}
1414

1515
func (r SnaplockRequest) String() string {

0 commit comments

Comments
 (0)