Skip to content
Merged

V2 #10

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dcbbc07
Refactor MainWindow and Introduce MVVM Pattern
AussieScorcher Aug 10, 2025
c24280c
Refactor AirportWebSocketManager to include Disconnected event; remov…
AussieScorcher Aug 10, 2025
7b1a435
Enhance MSFS Simulator Connector and Scenery Service
AussieScorcher Aug 11, 2025
ae65241
Enhance airport package selection logic in AirportStateHub and MainWi…
AussieScorcher Aug 11, 2025
b34d321
Add dynamic pruning capability to MsfsPointController; enable conditi…
AussieScorcher Aug 12, 2025
150f95d
Refactor logging in MsfsPointController; implement conditional loggin…
AussieScorcher Aug 12, 2025
6d9cf04
Add Discord presence service; integrate with existing services for en…
AussieScorcher Aug 21, 2025
8f42a7d
Enhance project configuration and build scripts; add versioning, upda…
AussieScorcher Aug 21, 2025
5b51ca9
Enhance stopbar crossing detection; update SimConnect.NET version, ad…
AussieScorcher Sep 4, 2025
516c953
Update SimConnect.NET version to 0.1.14-beta for improved functionali…
AussieScorcher Sep 4, 2025
0b54438
Enhance scenery package management; add event for package changes, im…
AussieScorcher Sep 6, 2025
91bf074
Notify SceneryService of selected package changes for hot-reloading t…
AussieScorcher Sep 6, 2025
27f6ba4
Refactor spawn management in MsfsPointController; reduce concurrency …
AussieScorcher Sep 15, 2025
80ed4be
Enhance airport package selection logic; cache available packages for…
AussieScorcher Sep 23, 2025
970b015
Update SimConnect.NET to version 0.1.15-beta
AussieScorcher Sep 24, 2025
80458b0
Improve app startup and shutdown robustness
AussieScorcher Sep 24, 2025
cf7ed89
Enhance connection resilience and error handling in simulator service…
AussieScorcher Sep 24, 2025
4c7facf
Refactor DiscordPresenceService for improved clarity and maintainability
AussieScorcher Sep 24, 2025
6c08924
Update README.md (#9)
frankeet11 Sep 24, 2025
97f2dd2
Update build/publish-framework-dependent.ps1
AussieScorcher Sep 24, 2025
6ee942c
Merge branch 'main' into v2
AussieScorcher Sep 24, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,6 @@ appsettings.*.json
.Trashes
ehthumbs.db
Thumbs.db

# Build
dist/
5 changes: 2 additions & 3 deletions App.xaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
<Application x:Class="BARS_Client_V2.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BARS_Client_V2"
StartupUri="MainWindow.xaml">
xmlns:local="clr-namespace:BARS_Client_V2">
<Application.Resources>

</Application.Resources>
</Application>
98 changes: 91 additions & 7 deletions App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,98 @@
using System.Configuration;
using System.Data;
using System.Windows;
using System.Windows;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Http;
using BARS_Client_V2.Domain;
using BARS_Client_V2.Application; // Contains SimulatorManager; no conflict if fully qualified below
using BARS_Client_V2.Presentation.ViewModels;
using BARS_Client_V2.Services;
using System.Windows.Threading;

namespace BARS_Client_V2
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
public partial class App : System.Windows.Application
{
private IHost? _host;

protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
_host = Host.CreateDefaultBuilder()
.ConfigureLogging(lb =>
{
lb.ClearProviders();
lb.AddConsole(); // Console (visible if app started from console / debug output window)
lb.AddDebug(); // VS Debug Output window
lb.AddEventSourceLogger(); // ETW / PerfView if needed
lb.SetMinimumLevel(LogLevel.Trace);
})
.ConfigureServices(services =>
{
services.AddSingleton<ISimulatorConnector, Infrastructure.Simulators.Msfs.MsfsSimulatorConnector>();
services.AddSingleton<IAirportRepository, Infrastructure.Networking.HttpAirportRepository>();
services.AddSingleton<ISettingsStore, Infrastructure.Settings.JsonSettingsStore>();
services.AddSingleton<SimulatorManager>();
services.AddHostedService(sp => sp.GetRequiredService<SimulatorManager>()); // background stream
services.AddHttpClient();
services.AddSingleton<INearestAirportService, NearestAirportService>();
services.AddSingleton<BARS_Client_V2.Infrastructure.Networking.AirportWebSocketManager>();
services.AddHostedService(sp => sp.GetRequiredService<BARS_Client_V2.Infrastructure.Networking.AirportWebSocketManager>());
services.AddHostedService<BARS_Client_V2.Services.DiscordPresenceService>();
services.AddSingleton<BARS_Client_V2.Infrastructure.Networking.AirportStateHub>();
services.AddSingleton<BARS_Client_V2.Infrastructure.Simulators.Msfs.MsfsPointController>(sp =>
{
var connectors = sp.GetServices<ISimulatorConnector>();
var logger = sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<BARS_Client_V2.Infrastructure.Simulators.Msfs.MsfsPointController>>();
var hub = sp.GetRequiredService<BARS_Client_V2.Infrastructure.Networking.AirportStateHub>();
var simManager = sp.GetRequiredService<SimulatorManager>();
return new BARS_Client_V2.Infrastructure.Simulators.Msfs.MsfsPointController(connectors, logger, hub, simManager, null);
});
services.AddHostedService(sp => sp.GetRequiredService<BARS_Client_V2.Infrastructure.Simulators.Msfs.MsfsPointController>());
services.AddSingleton<MainWindowViewModel>();
services.AddTransient<MainWindow>();
})
.Build();


var mainWindow = _host.Services.GetRequiredService<MainWindow>();
var vm = _host.Services.GetRequiredService<MainWindowViewModel>();
mainWindow.DataContext = vm;
var wsMgr = _host.Services.GetRequiredService<BARS_Client_V2.Infrastructure.Networking.AirportWebSocketManager>();
var hub = _host.Services.GetRequiredService<BARS_Client_V2.Infrastructure.Networking.AirportStateHub>();
wsMgr.AttachHub(hub);
wsMgr.Connected += () => vm.NotifyServerConnected();
wsMgr.ConnectionError += code => vm.NotifyServerError(code);
wsMgr.MessageReceived += msg => { vm.NotifyServerMessage(); _ = hub.ProcessAsync(msg); };
var pointController = _host.Services.GetRequiredService<BARS_Client_V2.Infrastructure.Simulators.Msfs.MsfsPointController>();
wsMgr.Disconnected += reason => { pointController.Suspend(); _ = pointController.DespawnAllAsync(); };
wsMgr.Connected += () => pointController.Resume();
mainWindow.Show();
Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, async () =>
{
try
{
if (_host != null)
{
await _host.StartAsync();
}
}
catch
{
// Swallow to avoid UI crash;
}
});
}

protected override async void OnExit(ExitEventArgs e)
{
if (_host != null)
{
try { await _host.StopAsync(); } catch { }
_host.Dispose();
}
base.OnExit(e);
}
}

}
11 changes: 11 additions & 0 deletions Application/IAirportRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using BARS_Client_V2.Domain;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace BARS_Client_V2.Application;

public interface IAirportRepository
{
Task<(IReadOnlyList<Airport> Items, int TotalCount)> SearchAsync(string? search, int page, int pageSize, CancellationToken ct = default);
}
15 changes: 15 additions & 0 deletions Application/ISettingsStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Threading.Tasks;
using System.Collections.Generic;

namespace BARS_Client_V2.Application;

public interface ISettingsStore
{
Task<ClientSettings> LoadAsync();
Task SaveAsync(ClientSettings settings);
}

public sealed record ClientSettings(string? ApiToken, IDictionary<string, string>? AirportPackages = null)
{
public static ClientSettings Empty => new(null, new Dictionary<string, string>());
}
93 changes: 93 additions & 0 deletions Application/SimulatorManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BARS_Client_V2.Domain;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace BARS_Client_V2.Application;

public sealed class SimulatorManager : BackgroundService
{
private readonly IEnumerable<ISimulatorConnector> _connectors;
private readonly ILogger<SimulatorManager> _logger;
private readonly object _lock = new();
private ISimulatorConnector? _active;
private FlightState? _latest;

public SimulatorManager(IEnumerable<ISimulatorConnector> connectors, ILogger<SimulatorManager> logger)
{
_connectors = connectors;
_logger = logger;
}

public FlightState? LatestState { get { lock (_lock) return _latest; } }
public ISimulatorConnector? ActiveConnector { get { lock (_lock) return _active; } }

public async Task<bool> ActivateAsync(string simulatorId, CancellationToken ct = default)
{
var connector = _connectors.FirstOrDefault(c => string.Equals(c.SimulatorId, simulatorId, StringComparison.OrdinalIgnoreCase));
if (connector == null) return false;
if (connector == _active && connector.IsConnected) return true;

if (_active != null && _active.IsConnected)
{
try { await _active.DisconnectAsync(ct); } catch (Exception ex) { _logger.LogWarning(ex, "Error disconnecting previous simulator"); }
}

if (await connector.ConnectAsync(ct))
{
lock (_lock) _active = connector;
_logger.LogInformation("Activated simulator {sim}", connector.DisplayName);
return true;
}
return false;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var first = _connectors.FirstOrDefault();
if (first != null)
{
await ActivateAsync(first.SimulatorId, stoppingToken);
}

while (!stoppingToken.IsCancellationRequested)
{
var active = ActiveConnector;
if (active == null || !active.IsConnected)
{
// Attempt reconnect periodically when disconnected
if (first != null)
{
try
{
await ActivateAsync(first.SimulatorId, stoppingToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Reconnect attempt failed");
}
}
await Task.Delay(2000, stoppingToken);
continue;
}
try
{
await foreach (var raw in active.StreamRawAsync(stoppingToken))
{
lock (_lock) _latest = new FlightState(raw.Latitude, raw.Longitude, raw.OnGround);
}
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
_logger.LogError(ex, "Error streaming flight state");
// small backoff
await Task.Delay(2000, stoppingToken);
}
}
}
}
28 changes: 28 additions & 0 deletions BARS-Client-V2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,40 @@
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<ApplicationIcon>appicon.ico</ApplicationIcon>
<AssemblyName>BARS Client</AssemblyName>
<Name>BARS Client</Name>
<Version>2.0.0</Version>
<FileVersion>2.0.0</FileVersion>
<AssemblyVersion>2.0.0</AssemblyVersion>
<PackageId>BARS-Client</PackageId>
<!-- Deployment / Publish settings -->
<SelfContained>false</SelfContained>
<PublishReadyToRun>true</PublishReadyToRun>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<DebugType>none</DebugType>
<Deterministic>true</Deterministic>
<PublishTrimmed>false</PublishTrimmed>
<PublishSingleFile>false</PublishSingleFile>
<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>
</PropertyGroup>

<ItemGroup>
<Content Include="appicon.ico" />
</ItemGroup>

<!-- Exclude PDB files from publish output explicitly (belt & suspenders) -->
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<None Remove="**/*.pdb" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageReference Include="SimConnect.NET" Version="0.1.15-beta" />
</ItemGroup>

<ItemGroup>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
Expand Down
5 changes: 5 additions & 0 deletions Domain/Airport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace BARS_Client_V2.Domain;

public sealed record SceneryPackage(string Name);

public sealed record Airport(string ICAO, IReadOnlyList<SceneryPackage> SceneryPackages);
3 changes: 3 additions & 0 deletions Domain/FlightState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace BARS_Client_V2.Domain;

public sealed record FlightState(double Latitude, double Longitude, bool OnGround);
8 changes: 8 additions & 0 deletions Domain/IPointStateListener.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Threading.Tasks;

namespace BARS_Client_V2.Domain;

public interface IPointStateListener
{
void OnPointStateChanged(PointState state);
}
17 changes: 17 additions & 0 deletions Domain/ISimulatorConnector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace BARS_Client_V2.Domain;

public sealed record RawFlightSample(double Latitude, double Longitude, bool OnGround);

public interface ISimulatorConnector
{
string SimulatorId { get; }
string DisplayName { get; }
bool IsConnected { get; }
Task<bool> ConnectAsync(CancellationToken ct = default);
Task DisconnectAsync(CancellationToken ct = default);
IAsyncEnumerable<RawFlightSample> StreamRawAsync(CancellationToken ct = default);
}
17 changes: 17 additions & 0 deletions Domain/Point.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace BARS_Client_V2.Domain;

public sealed record PointMetadata(
string Id,
string AirportId,
string Type,
string Name,
double Latitude,
double Longitude,
string? Directionality,
string? Orientation,
string? Color,
bool Elevated,
bool Ihp
);

public sealed record PointState(PointMetadata Metadata, bool IsOn, long TimestampMs);
Loading
Loading