14 Commits

Author SHA1 Message Date
Reese Norris
e09ff9e64e rename atc to controllers in datafeed 2025-05-25 14:54:31 -07:00
Reese Norris
933edc0478 update datafeed to be more vatsim-compliant 2025-05-25 14:51:45 -07:00
Reese Norris
6e3a179a3a convert visual range unit for ATC datafeed to nautical miles 2025-05-25 14:29:30 -07:00
Reese Norris
f63d89eb3e only do velocity distance calculation for relevant clients 2025-05-25 14:27:07 -07:00
Reese Norris
bc7a37e490 properly store sendfast state 2025-05-25 14:19:08 -07:00
Reese Norris
7d17066289 properly initialize Client latlon 2025-05-25 14:00:14 -07:00
Reese Norris
2943735be6 add AUTOMATIC entry to server list API for client compatibility 2025-05-25 13:52:19 -07:00
Reese Norris
c0155e78d4 store client coords as a single atomic unit 2025-05-25 11:57:06 -07:00
Reese Norris
2f423ed824 update gitignore 2025-05-25 11:56:38 -07:00
Reese Norris
456503ef84 remove build-and-push.sh 2025-05-25 11:36:54 -07:00
Reese Norris
fa51997b33 support velocity SendFast 2025-05-25 11:33:04 -07:00
Reese Norris
cfab03a90f add license badge 2025-05-24 08:51:00 -07:00
Reese Norris
a4210f836a add LICENSE 2025-05-24 08:47:26 -07:00
Reese Norris
7adfae6a23 make batch file waiter silent 2025-05-23 22:55:46 -07:00
12 changed files with 201 additions and 18 deletions

View File

@@ -3,4 +3,5 @@ docker-compose.yml
mkdocs.yml
README.md
docs
**tmp**
**tmp**
build-and-push.sh

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
.vscode
*.db
**tmp**
build-and-push.sh

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Reese Norris
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,5 +1,7 @@
# openfsd
[![license](https://img.shields.io/github/license/renorris/openfsd)](https://github.com/renorris/openfsd/blob/main/LICENSE)
**openfsd** is an open-source multiplayer flight simulation server implementing the modern VATSIM FSD protocol. It connects pilots and air traffic controllers in a shared virtual environment.
## About

View File

@@ -16,7 +16,9 @@ type Client struct {
cancelCtx func()
sendChan chan string
lat, lon, visRange atomic.Float64
coords atomic.Value
visRange atomic.Float64
closestVelocityClientDistance float64 // The closest Velocity-compatible client in meters
flightPlan atomic.String
assignedBeaconCode atomic.String
@@ -31,12 +33,17 @@ type Client struct {
facilityType int // ATC facility type. This value is only relevant for ATC
loginData
authState vatsimAuthState
authState vatsimAuthState
sendFastEnabled bool
}
type LatLon struct {
lat, lon float64
}
func newClient(ctx context.Context, conn net.Conn, scanner *bufio.Scanner, loginData loginData) (client *Client) {
clientCtx, cancel := context.WithCancel(ctx)
return &Client{
client = &Client{
conn: conn,
scanner: scanner,
ctx: clientCtx,
@@ -44,6 +51,8 @@ func newClient(ctx context.Context, conn net.Conn, scanner *bufio.Scanner, login
sendChan: make(chan string, 32),
loginData: loginData,
}
client.setLatLon(0, 0)
return
}
func (c *Client) senderWorker() {
@@ -120,5 +129,10 @@ func (s *Server) eventLoop(client *Client) {
}
func (c *Client) latLon() [2]float64 {
return [2]float64{c.lat.Load(), c.lon.Load()}
latLon := c.coords.Load().(LatLon)
return [2]float64{latLon.lat, latLon.lon}
}
func (c *Client) setLatLon(lat, lon float64) {
c.coords.Store(LatLon{lat: lat, lon: lon})
}

View File

@@ -163,6 +163,21 @@ func (s *Server) handlePilotPosition(client *Client, packet []byte) {
client.heading.Store(int32(heading))
client.lastUpdated.Store(time.Now())
// Check if we need to update the sendfast state
if client.protoRevision == 101 {
if client.sendFastEnabled {
if (client.closestVelocityClientDistance / 1852.0) > 5.0 { // 5.0 nautical miles
client.sendFastEnabled = false
sendDisableSendFastPacket(client)
}
} else {
if (client.closestVelocityClientDistance / 1852.0) < 5.0 { // 5.0 nautical miles
client.sendFastEnabled = true
sendEnableSendFastPacket(client)
}
}
}
}
// handleFastPilotPosition handles logic for fast `^`, stopped `#ST`, and slow `#SL` pilot position updates

View File

@@ -110,13 +110,14 @@ func (s *Server) handleGetOnlineUsers(c *gin.Context) {
}
for _, client := range clientMap {
latLon := client.latLon()
genData := OnlineUserGeneralData{
Callsign: client.callsign,
CID: client.cid,
Name: client.realName,
NetworkRating: int(client.networkRating),
Latitude: client.lat.Load(),
Longitude: client.lon.Load(),
Latitude: latLon[0],
Longitude: latLon[1],
LogonTime: client.loginTime,
LastUpdated: client.lastUpdated.Load(),
}
@@ -126,7 +127,7 @@ func (s *Server) handleGetOnlineUsers(c *gin.Context) {
OnlineUserGeneralData: genData,
Frequency: client.frequency.Load(),
Facility: client.facilityType,
VisRange: int(client.visRange.Load()),
VisRange: int(client.visRange.Load() * 0.000539957), // Convert meters to nautical miles
}
resData.ATC = append(resData.ATC, atc)
} else {

View File

@@ -68,8 +68,7 @@ func (p *postOffice) updatePosition(client *Client, newCenter [2]float64, newVis
oldMin, oldMax := calculateBoundingBox(client.latLon(), client.visRange.Load())
newMin, newMax := calculateBoundingBox(newCenter, newVisRange)
client.lat.Store(newCenter[0])
client.lon.Store(newCenter[1])
client.setLatLon(newCenter[0], newCenter[1])
client.visRange.Store(newVisRange)
// Avoid redundant updates
@@ -85,15 +84,29 @@ func (p *postOffice) updatePosition(client *Client, newCenter [2]float64, newVis
return
}
// search calls `callback` for every other Client within geographical range of the provided Client
// search calls `callback` for every other Client within geographical range of the provided Client.
//
// It automatically resets and populates the Client.nearbyClients and Client.closestVelocityClientDistance values
func (p *postOffice) search(client *Client, callback func(recipient *Client) bool) {
clientMin, clientMax := calculateBoundingBox(client.latLon(), client.visRange.Load())
client.closestVelocityClientDistance = math.MaxFloat64
p.treeLock.RLock()
p.tree.Search(clientMin, clientMax, func(foundMin [2]float64, foundMax [2]float64, foundClient *Client) bool {
if foundClient == client {
return true // Ignore self
}
if !client.isAtc && client.protoRevision == 101 && foundClient.protoRevision == 101 {
clientLatLon := client.latLon()
foundClientLatLon := foundClient.latLon()
dist := distance(clientLatLon[0], clientLatLon[1], foundClientLatLon[0], foundClientLatLon[1])
if dist < client.closestVelocityClientDistance {
client.closestVelocityClientDistance = dist
}
}
return callback(foundClient)
})
p.treeLock.RUnlock()

View File

@@ -391,3 +391,30 @@ func pitchBankHeading(packed uint32) (pitch float64, bank float64, heading float
func strPtr(str string) *string {
return &str
}
// sendEnableSendFastPacket sends an 'enable' $SF Send Fast packet to the client
func sendEnableSendFastPacket(client *Client) {
sendSendFastPacket(client, true)
}
// sendDisableSendFastPacket sends a 'disable' $SF Send Fast packet to the client
func sendDisableSendFastPacket(client *Client) {
sendSendFastPacket(client, false)
}
// sendSendFastPacket sends a $SF Send Fast packet to the client
func sendSendFastPacket(client *Client, enabled bool) {
builder := strings.Builder{}
builder.Grow(32)
builder.WriteString("$SFSERVER:")
builder.WriteString(client.callsign)
builder.WriteByte(':')
if enabled {
builder.WriteByte('1')
} else {
builder.WriteByte('0')
}
builder.WriteString("\r\n")
client.send(builder.String())
}

View File

@@ -4,6 +4,6 @@ title openfsd
start /b cmd /c "set DATABASE_AUTO_MIGRATE=true&& set DATABASE_SOURCE_NAME=openfsd.db?_pragma=busy_timeout(5000)^&_pragma=journal_mode(WAL)&& go run ."
powershell -Command "while (-not (Test-NetConnection -ComputerName localhost -Port 13618 -InformationLevel Quiet)) { Start-Sleep -Seconds 1 }"
powershell -Command "$ProgressPreference = 'SilentlyContinue'; while (-not (Test-NetConnection -ComputerName localhost -Port 13618 -InformationLevel Quiet)) { Start-Sleep -Seconds 1 }" >nul 2>&1
cmd /c "cd web&& set FSD_HTTP_SERVICE_ADDRESS=http://localhost:13618&& set DATABASE_SOURCE_NAME=../openfsd.db?_pragma=busy_timeout(5000)^&_pragma=journal_mode(WAL)&& go run ."

View File

@@ -145,6 +145,15 @@ func (s *Server) handleGetServersJSON(c *gin.Context) {
ClientsConnectionAllowed: 99,
IsSweatbox: isSweatbox,
},
{
Ident: "AUTOMATIC",
HostnameOrIp: serverHostname,
Location: serverLocation,
Name: serverIdent,
ClientConnectionsAllowed: true,
ClientsConnectionAllowed: 99,
IsSweatbox: isSweatbox,
},
}
res, err := json.Marshal(&dataJson)
@@ -190,6 +199,15 @@ func (s *Server) generateServersTxt() (txt string, err error) {
ClientsConnectionAllowed: 99,
IsSweatbox: false,
},
{
Ident: "AUTOMATIC",
HostnameOrIp: serverHostname,
Location: serverLocation,
Name: serverIdent,
ClientConnectionsAllowed: true,
ClientsConnectionAllowed: 99,
IsSweatbox: false,
},
}
buf := bytes.Buffer{}
@@ -250,8 +268,49 @@ func (s *Server) getBaseURLOrErr(c *gin.Context) (baseURL string, ok bool) {
}
type Datafeed struct {
Pilots []fsd.OnlineUserPilot `json:"pilots"`
ATC []fsd.OnlineUserATC `json:"atc"`
General DatafeedGeneral `json:"general"`
Pilots []DatafeedPilot `json:"pilots"`
ATC []DatafeedATC `json:"controllers"`
}
type DatafeedGeneral struct {
Version int `json:"version"`
UpdateTimestamp time.Time `json:"update_timestamp"`
ConnectedClients int `json:"connected_clients"`
UniqueUsers int `json:"unique_users"`
}
type DatafeedPilot struct {
fsd.OnlineUserPilot
Server string `json:"server"`
PilotRating int `json:"pilot_rating"` // INOP placeholder
MilitaryRating int `json:"military_rating"` // INOP placeholder
QnhIHg float64 `json:"qnh_i_hg"` // INOP placeholder
QnhMb int `json:"qnh_mb"` // INOP placeholder
FlightPlan *DatafeedFlightplan `json:"flight_plan,omitempty"` // INOP placeholder
}
type DatafeedFlightplan struct {
FlightRules string `json:"flight_rules"`
Aircraft string `json:"aircraft"`
AircraftFAA string `json:"aircraft_faa"`
AircraftShort string `json:"aircraft_short"`
Departure string `json:"departure"`
Arrival string `json:"arrival"`
Alternate string `json:"alternate"`
DepTime string `json:"deptime"`
EnrouteTime string `json:"enroute_time"`
FuelTime string `json:"fuel_time"`
Remarks string `json:"remarks"`
Route string `json:"route"`
RevisionID int `json:"revision_id"`
AssignedTransponder string `json:"assigned_transponder"`
}
type DatafeedATC struct {
fsd.OnlineUserATC
Server string `json:"server"`
TextATIS []string `json:"text_atis"` // INOP placeholder
}
type DatafeedCache struct {
@@ -294,10 +353,38 @@ func (s *Server) generateDatafeed() (feed *DatafeedCache, err error) {
return
}
now := time.Now()
dataFeed := Datafeed{
Pilots: onlineUsers.Pilots,
ATC: onlineUsers.ATC,
General: DatafeedGeneral{
Version: 3,
UpdateTimestamp: now,
ConnectedClients: len(onlineUsers.Pilots) + len(onlineUsers.ATC),
UniqueUsers: len(onlineUsers.Pilots) + len(onlineUsers.ATC),
},
Pilots: []DatafeedPilot{},
ATC: []DatafeedATC{},
}
for _, pilot := range onlineUsers.Pilots {
dataFeed.Pilots = append(dataFeed.Pilots, DatafeedPilot{
OnlineUserPilot: pilot,
Server: "OPENFSD",
PilotRating: 1,
MilitaryRating: 1,
QnhIHg: 29.92,
QnhMb: 1013,
})
}
for _, atc := range onlineUsers.ATC {
dataFeed.ATC = append(dataFeed.ATC, DatafeedATC{
OnlineUserATC: atc,
Server: "OPENFSD",
TextATIS: []string{},
})
}
buf := bytes.Buffer{}
encoder := json.NewEncoder(&buf)
if err = encoder.Encode(&dataFeed); err != nil {
@@ -306,7 +393,7 @@ func (s *Server) generateDatafeed() (feed *DatafeedCache, err error) {
feed = &DatafeedCache{
jsonStr: buf.String(),
lastUpdated: time.Now(),
lastUpdated: now,
}
return
}

View File

@@ -7,6 +7,7 @@ CONNECTED CLIENTS = 1
;
;
!SERVERS:
{{ range . }}{{ .Ident }}:{{ .HostnameOrIp }}:{{ .Location }}:{{ .Ident }}:{{ .ClientsConnectionAllowed }}:{{ end }}
{{ range $index, $element := . }}{{ if $index }}
{{ end }}{{ $element.Ident }}:{{ $element.HostnameOrIp }}:{{ $element.Location }}:{{ $element.Name }}:{{ $element.ClientsConnectionAllowed }}:{{ end }}
;
; END