mirror of
https://github.com/renorris/openfsd
synced 2026-04-04 22:36:52 +08:00
Compare commits
14 Commits
v1.0-beta.
...
v1.0-beta.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e09ff9e64e | ||
|
|
933edc0478 | ||
|
|
6e3a179a3a | ||
|
|
f63d89eb3e | ||
|
|
bc7a37e490 | ||
|
|
7d17066289 | ||
|
|
2943735be6 | ||
|
|
c0155e78d4 | ||
|
|
2f423ed824 | ||
|
|
456503ef84 | ||
|
|
fa51997b33 | ||
|
|
cfab03a90f | ||
|
|
a4210f836a | ||
|
|
7adfae6a23 |
@@ -4,3 +4,4 @@ mkdocs.yml
|
|||||||
README.md
|
README.md
|
||||||
docs
|
docs
|
||||||
**tmp**
|
**tmp**
|
||||||
|
build-and-push.sh
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
.vscode
|
.vscode
|
||||||
*.db
|
*.db
|
||||||
**tmp**
|
**tmp**
|
||||||
|
build-and-push.sh
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# openfsd
|
# 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.
|
**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
|
## About
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ type Client struct {
|
|||||||
cancelCtx func()
|
cancelCtx func()
|
||||||
sendChan chan string
|
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
|
flightPlan atomic.String
|
||||||
assignedBeaconCode atomic.String
|
assignedBeaconCode atomic.String
|
||||||
@@ -31,12 +33,17 @@ type Client struct {
|
|||||||
facilityType int // ATC facility type. This value is only relevant for ATC
|
facilityType int // ATC facility type. This value is only relevant for ATC
|
||||||
loginData
|
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) {
|
func newClient(ctx context.Context, conn net.Conn, scanner *bufio.Scanner, loginData loginData) (client *Client) {
|
||||||
clientCtx, cancel := context.WithCancel(ctx)
|
clientCtx, cancel := context.WithCancel(ctx)
|
||||||
return &Client{
|
client = &Client{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
scanner: scanner,
|
scanner: scanner,
|
||||||
ctx: clientCtx,
|
ctx: clientCtx,
|
||||||
@@ -44,6 +51,8 @@ func newClient(ctx context.Context, conn net.Conn, scanner *bufio.Scanner, login
|
|||||||
sendChan: make(chan string, 32),
|
sendChan: make(chan string, 32),
|
||||||
loginData: loginData,
|
loginData: loginData,
|
||||||
}
|
}
|
||||||
|
client.setLatLon(0, 0)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) senderWorker() {
|
func (c *Client) senderWorker() {
|
||||||
@@ -120,5 +129,10 @@ func (s *Server) eventLoop(client *Client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) latLon() [2]float64 {
|
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})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,6 +163,21 @@ func (s *Server) handlePilotPosition(client *Client, packet []byte) {
|
|||||||
client.heading.Store(int32(heading))
|
client.heading.Store(int32(heading))
|
||||||
|
|
||||||
client.lastUpdated.Store(time.Now())
|
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
|
// handleFastPilotPosition handles logic for fast `^`, stopped `#ST`, and slow `#SL` pilot position updates
|
||||||
|
|||||||
@@ -110,13 +110,14 @@ func (s *Server) handleGetOnlineUsers(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, client := range clientMap {
|
for _, client := range clientMap {
|
||||||
|
latLon := client.latLon()
|
||||||
genData := OnlineUserGeneralData{
|
genData := OnlineUserGeneralData{
|
||||||
Callsign: client.callsign,
|
Callsign: client.callsign,
|
||||||
CID: client.cid,
|
CID: client.cid,
|
||||||
Name: client.realName,
|
Name: client.realName,
|
||||||
NetworkRating: int(client.networkRating),
|
NetworkRating: int(client.networkRating),
|
||||||
Latitude: client.lat.Load(),
|
Latitude: latLon[0],
|
||||||
Longitude: client.lon.Load(),
|
Longitude: latLon[1],
|
||||||
LogonTime: client.loginTime,
|
LogonTime: client.loginTime,
|
||||||
LastUpdated: client.lastUpdated.Load(),
|
LastUpdated: client.lastUpdated.Load(),
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ func (s *Server) handleGetOnlineUsers(c *gin.Context) {
|
|||||||
OnlineUserGeneralData: genData,
|
OnlineUserGeneralData: genData,
|
||||||
Frequency: client.frequency.Load(),
|
Frequency: client.frequency.Load(),
|
||||||
Facility: client.facilityType,
|
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)
|
resData.ATC = append(resData.ATC, atc)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ func (p *postOffice) updatePosition(client *Client, newCenter [2]float64, newVis
|
|||||||
oldMin, oldMax := calculateBoundingBox(client.latLon(), client.visRange.Load())
|
oldMin, oldMax := calculateBoundingBox(client.latLon(), client.visRange.Load())
|
||||||
newMin, newMax := calculateBoundingBox(newCenter, newVisRange)
|
newMin, newMax := calculateBoundingBox(newCenter, newVisRange)
|
||||||
|
|
||||||
client.lat.Store(newCenter[0])
|
client.setLatLon(newCenter[0], newCenter[1])
|
||||||
client.lon.Store(newCenter[1])
|
|
||||||
client.visRange.Store(newVisRange)
|
client.visRange.Store(newVisRange)
|
||||||
|
|
||||||
// Avoid redundant updates
|
// Avoid redundant updates
|
||||||
@@ -85,15 +84,29 @@ func (p *postOffice) updatePosition(client *Client, newCenter [2]float64, newVis
|
|||||||
return
|
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) {
|
func (p *postOffice) search(client *Client, callback func(recipient *Client) bool) {
|
||||||
clientMin, clientMax := calculateBoundingBox(client.latLon(), client.visRange.Load())
|
clientMin, clientMax := calculateBoundingBox(client.latLon(), client.visRange.Load())
|
||||||
|
|
||||||
|
client.closestVelocityClientDistance = math.MaxFloat64
|
||||||
|
|
||||||
p.treeLock.RLock()
|
p.treeLock.RLock()
|
||||||
p.tree.Search(clientMin, clientMax, func(foundMin [2]float64, foundMax [2]float64, foundClient *Client) bool {
|
p.tree.Search(clientMin, clientMax, func(foundMin [2]float64, foundMax [2]float64, foundClient *Client) bool {
|
||||||
if foundClient == client {
|
if foundClient == client {
|
||||||
return true // Ignore self
|
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)
|
return callback(foundClient)
|
||||||
})
|
})
|
||||||
p.treeLock.RUnlock()
|
p.treeLock.RUnlock()
|
||||||
|
|||||||
27
fsd/util.go
27
fsd/util.go
@@ -391,3 +391,30 @@ func pitchBankHeading(packed uint32) (pitch float64, bank float64, heading float
|
|||||||
func strPtr(str string) *string {
|
func strPtr(str string) *string {
|
||||||
return &str
|
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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 ."
|
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 ."
|
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 ."
|
||||||
|
|||||||
97
web/data.go
97
web/data.go
@@ -145,6 +145,15 @@ func (s *Server) handleGetServersJSON(c *gin.Context) {
|
|||||||
ClientsConnectionAllowed: 99,
|
ClientsConnectionAllowed: 99,
|
||||||
IsSweatbox: isSweatbox,
|
IsSweatbox: isSweatbox,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Ident: "AUTOMATIC",
|
||||||
|
HostnameOrIp: serverHostname,
|
||||||
|
Location: serverLocation,
|
||||||
|
Name: serverIdent,
|
||||||
|
ClientConnectionsAllowed: true,
|
||||||
|
ClientsConnectionAllowed: 99,
|
||||||
|
IsSweatbox: isSweatbox,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := json.Marshal(&dataJson)
|
res, err := json.Marshal(&dataJson)
|
||||||
@@ -190,6 +199,15 @@ func (s *Server) generateServersTxt() (txt string, err error) {
|
|||||||
ClientsConnectionAllowed: 99,
|
ClientsConnectionAllowed: 99,
|
||||||
IsSweatbox: false,
|
IsSweatbox: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Ident: "AUTOMATIC",
|
||||||
|
HostnameOrIp: serverHostname,
|
||||||
|
Location: serverLocation,
|
||||||
|
Name: serverIdent,
|
||||||
|
ClientConnectionsAllowed: true,
|
||||||
|
ClientsConnectionAllowed: 99,
|
||||||
|
IsSweatbox: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
@@ -250,8 +268,49 @@ func (s *Server) getBaseURLOrErr(c *gin.Context) (baseURL string, ok bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Datafeed struct {
|
type Datafeed struct {
|
||||||
Pilots []fsd.OnlineUserPilot `json:"pilots"`
|
General DatafeedGeneral `json:"general"`
|
||||||
ATC []fsd.OnlineUserATC `json:"atc"`
|
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 {
|
type DatafeedCache struct {
|
||||||
@@ -294,10 +353,38 @@ func (s *Server) generateDatafeed() (feed *DatafeedCache, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
dataFeed := Datafeed{
|
dataFeed := Datafeed{
|
||||||
Pilots: onlineUsers.Pilots,
|
General: DatafeedGeneral{
|
||||||
ATC: onlineUsers.ATC,
|
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{}
|
buf := bytes.Buffer{}
|
||||||
encoder := json.NewEncoder(&buf)
|
encoder := json.NewEncoder(&buf)
|
||||||
if err = encoder.Encode(&dataFeed); err != nil {
|
if err = encoder.Encode(&dataFeed); err != nil {
|
||||||
@@ -306,7 +393,7 @@ func (s *Server) generateDatafeed() (feed *DatafeedCache, err error) {
|
|||||||
|
|
||||||
feed = &DatafeedCache{
|
feed = &DatafeedCache{
|
||||||
jsonStr: buf.String(),
|
jsonStr: buf.String(),
|
||||||
lastUpdated: time.Now(),
|
lastUpdated: now,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ CONNECTED CLIENTS = 1
|
|||||||
;
|
;
|
||||||
;
|
;
|
||||||
!SERVERS:
|
!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
|
; END
|
||||||
|
|||||||
Reference in New Issue
Block a user