reworked app startup
This commit is contained in:
		| @@ -4,6 +4,7 @@ using Avalonia.Markup.Xaml; | |||||||
| using StarsAssistant.ViewModels; | using StarsAssistant.ViewModels; | ||||||
| using StarsAssistant.Views; | using StarsAssistant.Views; | ||||||
| using Splat; | using Splat; | ||||||
|  | using StarsAssistant.Services; | ||||||
|  |  | ||||||
| namespace StarsAssistant; | namespace StarsAssistant; | ||||||
|  |  | ||||||
| @@ -18,15 +19,21 @@ public partial class App : Application | |||||||
|     { |     { | ||||||
|         if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) |         if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) | ||||||
|         { |         { | ||||||
|  |             Game game = new(desktop.Args); | ||||||
|  |             game.RegisterServicesForGame(); | ||||||
|  |  | ||||||
|             desktop.MainWindow = new MainWindow |             desktop.MainWindow = new MainWindow | ||||||
|             { |             { | ||||||
|                 DataContext = new MainWindowViewModel(), |                 DataContext = new MainWindowViewModel(), | ||||||
|             }; |             }; | ||||||
|         } |  | ||||||
|  |  | ||||||
|         var csvloader = Locator.Current.GetService<Services.CSVDataLoader>()!; |  | ||||||
|         csvloader.StartPlanetCSVWatcher(); |  | ||||||
|  |  | ||||||
|             base.OnFrameworkInitializationCompleted(); |             base.OnFrameworkInitializationCompleted(); | ||||||
|  |  | ||||||
|  |             game.StartBackgroundServices(); | ||||||
|  |         } | ||||||
|  |         else | ||||||
|  |         { | ||||||
|  |             throw new InvalidOperationException("This app supports only Desktop mode."); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -13,7 +13,7 @@ public class Planet | |||||||
|     public string? Owner { get; set; } |     public string? Owner { get; set; } | ||||||
|      |      | ||||||
|     [Index(2)] |     [Index(2)] | ||||||
|     public string? StarbaseType { get; set; } |     public string StarbaseType { get; set; } = String.Empty; | ||||||
|      |      | ||||||
|     [Index(3)] |     [Index(3)] | ||||||
|     public int ReportAge { get; set; } = 0; |     public int ReportAge { get; set; } = 0; | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ public class Planet | |||||||
|  |  | ||||||
|     public string? OwnerId { get; set; } |     public string? OwnerId { get; set; } | ||||||
|      |      | ||||||
|     public string? StarbaseType { get; set; } |     public string StarbaseType { get; set; } = String.Empty; | ||||||
|      |      | ||||||
|     public int ReportAge { get; set; } = 0; |     public int ReportAge { get; set; } = 0; | ||||||
|      |      | ||||||
|   | |||||||
| @@ -25,37 +25,6 @@ sealed class Program | |||||||
|         var logger = new ConsoleLogger() { Level = LogLevel.Debug }; |         var logger = new ConsoleLogger() { Level = LogLevel.Debug }; | ||||||
|         Locator.CurrentMutable.RegisterConstant(logger, typeof(ILogger)); |         Locator.CurrentMutable.RegisterConstant(logger, typeof(ILogger)); | ||||||
|  |  | ||||||
|         bool newGame = false; |  | ||||||
|         string dbPath = "/home/torben/Nextcloud/Documents/Stars!/Games/goingth/GOINGTH.sqlite"; |  | ||||||
|         Services.Game gameSvc; |  | ||||||
|  |  | ||||||
|         using (StarsDatabase starsDB = new(dbPath)) |  | ||||||
|         { |  | ||||||
|             if (Path.Exists(dbPath)) |  | ||||||
|             { |  | ||||||
|                 Model.Game dbGame = starsDB.Game.First(); |  | ||||||
|                 gameSvc = new Services.Game(dbGame); |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 starsDB.Database.EnsureCreated(); |  | ||||||
|                 gameSvc = new() |  | ||||||
|                 { |  | ||||||
|                     BaseName = "GOINGTH", |  | ||||||
|                     GamePath = "/home/torben/Nextcloud/Documents/Stars!/Games/goingth/" |  | ||||||
|                 }; |  | ||||||
|                 gameSvc.SaveToDatabase(starsDB); |  | ||||||
|                 newGame = true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         Locator.CurrentMutable.RegisterConstant(gameSvc, typeof(Services.Game)); |  | ||||||
|         Locator.CurrentMutable.RegisterConstant(new Services.CSVDataLoader(), typeof(Services.CSVDataLoader)); |  | ||||||
|         Locator.CurrentMutable.Register(() => new StarsDatabase(gameSvc.DatabaseFileName), typeof(StarsDatabase)); |  | ||||||
|              |  | ||||||
|         if (newGame) |  | ||||||
|             __createTestData(); |  | ||||||
|  |  | ||||||
|         BuildAvaloniaApp() |         BuildAvaloniaApp() | ||||||
|             .StartWithClassicDesktopLifetime(args); |             .StartWithClassicDesktopLifetime(args); | ||||||
|     } |     } | ||||||
| @@ -68,93 +37,4 @@ sealed class Program | |||||||
|             .LogToTrace() |             .LogToTrace() | ||||||
|             .UseReactiveUI(); |             .UseReactiveUI(); | ||||||
|  |  | ||||||
|  |  | ||||||
|     public static void __createTestData () |  | ||||||
|     { |  | ||||||
|         using var db = Locator.Current.GetService<StarsDatabase>()!; |  | ||||||
|  |  | ||||||
|         // Note: This sample requires the database to be created before running. |  | ||||||
|         // Console.WriteLine($"Database path: {db.DbPath}."); |  | ||||||
|  |  | ||||||
|         Race r = new() |  | ||||||
|         { |  | ||||||
|             Name = "Atlantis", |  | ||||||
|             PlayerRace = true, |  | ||||||
|             PlayerFileId = 1, |  | ||||||
|             ColonistsPerResource = 1000, |  | ||||||
|             GrowthRatePercent = 19, |  | ||||||
|             PRT = PRT.Other, |  | ||||||
|             HasOBRM = true, |  | ||||||
|             FactoryCost3 = false, |  | ||||||
|             FactoryNumberPer10k = 8, |  | ||||||
|             FactoryResCost = 8, |  | ||||||
|             FactoryResPer10 = 15, |  | ||||||
|             MineResCost = 3, |  | ||||||
|             MineMineralsPer10 = 10, |  | ||||||
|             MineNumberPer10k = 10 |  | ||||||
|         }; |  | ||||||
|         db.Add(r); |  | ||||||
|         db.SaveChanges(); |  | ||||||
|  |  | ||||||
|         var loader = new CSV.PlanetLoader(); |  | ||||||
|         loader.ImportForRace(Services.Game.Player); |  | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /* |  | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using CsvHelper.Configuration; |  | ||||||
| using CsvHelper; |  | ||||||
| using StarsAssistant.model; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| using var db = new StarsDatabase("stars.sqlite"); |  | ||||||
|  |  | ||||||
| db.Database.EnsureDeleted(); |  | ||||||
| db.Database.EnsureCreated(); |  | ||||||
|  |  | ||||||
| // Note: This sample requires the database to be created before running. |  | ||||||
| Console.WriteLine($"Database path: {db.DbPath}."); |  | ||||||
|  |  | ||||||
| var config = CsvConfiguration.FromAttributes<Planet>(CultureInfo.InvariantCulture); |  | ||||||
| config.Delimiter = "\t"; |  | ||||||
| config.Escape = '\0'; |  | ||||||
| config.MissingFieldFound = null; |  | ||||||
|  |  | ||||||
| using (var reader = new StreamReader("/home/torben/goingth/GOINGTH.p1", Encoding.Latin1)) |  | ||||||
| using (var csv = new CsvReader(reader, config)) |  | ||||||
| { |  | ||||||
|     List<Planet> records = csv.GetRecords<Planet>().ToList(); |  | ||||||
|     foreach (Planet p in records) |  | ||||||
|     { |  | ||||||
|         db.Add(p); |  | ||||||
|     } |  | ||||||
|     db.SaveChanges(); |  | ||||||
| } |  | ||||||
| */ |  | ||||||
|  |  | ||||||
| /* |  | ||||||
| // Create |  | ||||||
| Console.WriteLine("Inserting a new blog"); |  | ||||||
| db.Add(new Blog { Url = "http://blogs.msdn.com/adonet" }); |  | ||||||
| db.SaveChanges(); |  | ||||||
|  |  | ||||||
| // Read |  | ||||||
| Console.WriteLine("Querying for a blog"); |  | ||||||
| var blog = db.Blogs |  | ||||||
|     .OrderBy(b => b.BlogId) |  | ||||||
|     .First(); |  | ||||||
|  |  | ||||||
| // Update |  | ||||||
| Console.WriteLine("Updating the blog and adding a post"); |  | ||||||
| blog.Url = "https://devblogs.microsoft.com/dotnet"; |  | ||||||
| blog.Posts.Add( |  | ||||||
|     new Post { Title = "Hello World", Content = "I wrote an app using EF Core!" }); |  | ||||||
| db.SaveChanges(); |  | ||||||
|  |  | ||||||
| // Delete |  | ||||||
| // Console.WriteLine("Delete the blog"); |  | ||||||
| // db.Remove(blog); |  | ||||||
| // db.SaveChanges(); |  | ||||||
| */ |  | ||||||
| @@ -16,30 +16,58 @@ public partial class CSVDataLoader : IEnableLogger | |||||||
|     /// </summary> |     /// </summary> | ||||||
|     protected Services.Game Game = Locator.Current.GetService<Services.Game>()!; |     protected Services.Game Game = Locator.Current.GetService<Services.Game>()!; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Regex to match fs watcher results to Stars file types. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <returns>Precompiled RegEx</returns> | ||||||
|     [GeneratedRegex(@".*\.(?<type>[pf])(?<player>\d)+$")] |     [GeneratedRegex(@".*\.(?<type>[pf])(?<player>\d)+$")] | ||||||
|     private static partial Regex MyRegex(); |     private static partial Regex MyRegex(); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// RegEx Instance to match Stars file types. | ||||||
|  |     /// </summary> | ||||||
|     protected Regex FileTypeRegEx = MyRegex(); |     protected Regex FileTypeRegEx = MyRegex(); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Instance of the FSWatcher, to which we subscribe. | ||||||
|  |     /// </summary> | ||||||
|  |     protected IObservable<FileSystemEventArgs> Watcher; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Active subscription to FsWatcher. | ||||||
|  |     /// </summary> | ||||||
|  |     protected IDisposable? Subscription; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Construct the instance and prepare our FS Watcher. | ||||||
|  |     /// </summary> | ||||||
|     public CSVDataLoader()  |     public CSVDataLoader()  | ||||||
|     { |  | ||||||
|          |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void StartPlanetCSVWatcher()  |  | ||||||
|     { |     { | ||||||
|         // string[] filters = { "*.p*", "*.f*", "*.map" }; |         // string[] filters = { "*.p*", "*.f*", "*.map" }; | ||||||
|  |         Watcher = FsWatcher | ||||||
|         var watcher = FsWatcher |  | ||||||
|             .ObserveFileSystem(Game.GamePath, [ Game.PlanetFileSearchPattern ]) |             .ObserveFileSystem(Game.GamePath, [ Game.PlanetFileSearchPattern ]) | ||||||
|             .ThrottleAndDistinct(2, RxApp.TaskpoolScheduler) |             .ThrottleAndDistinct(2, RxApp.TaskpoolScheduler) | ||||||
|             .Log(this, $"{DateTime.Now.ToLongTimeString()} FsEvent", fsEvent => $"{fsEvent.FullPath} {fsEvent.ChangeType}") |             .Log(this, $"{DateTime.Now.ToLongTimeString()} FsEvent", fsEvent => $"{fsEvent.FullPath} {fsEvent.ChangeType}") | ||||||
|             .ObserveOn(RxApp.TaskpoolScheduler); |             .ObserveOn(RxApp.TaskpoolScheduler); | ||||||
|          |  | ||||||
|         watcher.Subscribe(fsEvent => this.LoadPlanetFile(fsEvent.FullPath)); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     protected void LoadPlanetFile(string fileName) |     /// <summary> | ||||||
|  |     /// Start the CSV watcher, and capture our disposable, so that we can shut  | ||||||
|  |     /// down the watcher if needed. | ||||||
|  |     /// </summary> | ||||||
|  |     public void StartCSVWatcher()  | ||||||
|  |     { | ||||||
|  |         if (Subscription != null) | ||||||
|  |             throw new InvalidOperationException("CSV Watcher is active, can't start it again."); | ||||||
|  |  | ||||||
|  |         Subscription = Watcher.Subscribe(fsEvent => this.LoadFile(fsEvent.FullPath)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Subscription to File Processing, called by our subscription to the watcher. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="fileName">The File to process</param> | ||||||
|  |     protected void LoadFile(string fileName) | ||||||
|     { |     { | ||||||
|         Match m = FileTypeRegEx.Match(fileName); |         Match m = FileTypeRegEx.Match(fileName); | ||||||
|         if (! m.Success) |         if (! m.Success) | ||||||
| @@ -50,9 +78,18 @@ public partial class CSVDataLoader : IEnableLogger | |||||||
|  |  | ||||||
|         string type = m.Groups["type"].Value; |         string type = m.Groups["type"].Value; | ||||||
|         string player = m.Groups["player"].Value; |         string player = m.Groups["player"].Value; | ||||||
|  |  | ||||||
|         this.Log().Debug($"Got file type {type} for player {player}"); |         this.Log().Debug($"Got file type {type} for player {player}"); | ||||||
|  |  | ||||||
|  |         switch (type) | ||||||
|  |         { | ||||||
|  |             case "p": | ||||||
|                 var loader = new CSV.PlanetLoader(); |                 var loader = new CSV.PlanetLoader(); | ||||||
|                 loader.ImportForRace(Services.Game.Player); |                 loader.ImportForRace(Services.Game.Player); | ||||||
|  |                 break; | ||||||
|  |  | ||||||
|  |             default: | ||||||
|  |                 this.Log().Warn($"Planet loader got unknown file type ${type}. Ignoring file."); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| }    | }    | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| using System.Reflection.PortableExecutable; | using System.Reflection.PortableExecutable; | ||||||
| using Splat; | using Splat; | ||||||
|  | using StarsAssistant.Model; | ||||||
|  |  | ||||||
| namespace StarsAssistant.Services; | namespace StarsAssistant.Services; | ||||||
|  |  | ||||||
| @@ -22,26 +23,26 @@ public class Game | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Save this record in the database. Uses the service locator to access the  |     /// Helper to construct the game from a command line. | ||||||
|     /// database unless you specify an instance. This is needed during initial |  | ||||||
|     /// game creation, where the services are not yet established. |  | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     /// <param name="db">Optional DB instance if services are yet unavailable.</param> |     /// <param name="args">The command line to parse.</param> | ||||||
|     public void SaveToDatabase(Model.StarsDatabase? db = null)  |     public Game(string[]? args) | ||||||
|     { |     { | ||||||
|         db ??= Locator.Current.GetService<Model.StarsDatabase>()!; |         GamePath = "/home/torben/Nextcloud/Documents/Stars!/Games/goingth/"; | ||||||
|  |         BaseName = "GOINGTH"; | ||||||
|  |         string dbPath = DatabaseFileName; | ||||||
|  |  | ||||||
|         Model.Game? dbGame = db.Game.FirstOrDefault(); |         using StarsDatabase starsDB = new(dbPath); | ||||||
|         if (dbGame == null) |         if (Path.Exists(dbPath)) | ||||||
|         { |         { | ||||||
|             dbGame = new Model.Game(); |             Model.Game dbGame = starsDB.Game.First(); | ||||||
|             db.Add(dbGame); |         } | ||||||
|             db.SaveChanges(); |         else | ||||||
|  |         { | ||||||
|  |             starsDB.Database.EnsureCreated(); | ||||||
|  |             SaveToDatabase(starsDB); | ||||||
|  |             __doCreateTestData = true; | ||||||
|         } |         } | ||||||
|         dbGame.GamePath = GamePath; |  | ||||||
|         dbGame.BaseName = BaseName;  |  | ||||||
|         db.Update(dbGame); |  | ||||||
|         db.SaveChanges(); |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     /// <summary> |     /// <summary> | ||||||
| @@ -68,9 +69,9 @@ public class Game | |||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Internal helper to lazily load the current player from the DB. |     /// Internal helper to lazily load the current player from the DB. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     private readonly static Lazy<Model.Race>LazyPlayer = new ( () => { |     private readonly static Lazy<Race>LazyPlayer = new ( () => { | ||||||
|         using var db = Locator.Current.GetService<Model.StarsDatabase>()!; |         using var db = Locator.Current.GetService<StarsDatabase>()!; | ||||||
|         Model.Race result = db.Race |         Race result = db.Race | ||||||
|             .Where(r => r.PlayerRace == true) |             .Where(r => r.PlayerRace == true) | ||||||
|             .First(); |             .First(); | ||||||
|         return result; |         return result; | ||||||
| @@ -79,13 +80,99 @@ public class Game | |||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Get the Race object for the current player, lazy initialized. |     /// Get the Race object for the current player, lazy initialized. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public static Model.Race Player => LazyPlayer.Value; |     public static Race Player => LazyPlayer.Value; | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// Get the name of a planet file for a given race. |     /// Get the name of a planet file for a given race. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     /// <param name="r">The race to load.</param> |     /// <param name="r">The race to load.</param> | ||||||
|     /// <returns>Fully qualified file path.</returns> |     /// <returns>Fully qualified file path.</returns> | ||||||
|     public string PlanetFileForRace (Model.Race r) => Path.Combine(GamePath, $"{BaseName}.p{r.PlayerFileId}"); |     public string PlanetFileForRace (Race r) => Path.Combine(GamePath, $"{BaseName}.p{r.PlayerFileId}"); | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Save this record in the database. Uses the service locator to access the  | ||||||
|  |     /// database unless you specify an instance. This is needed during initial | ||||||
|  |     /// game creation, where the services are not yet established. | ||||||
|  |     /// </summary> | ||||||
|  |     /// <param name="db">Optional DB instance if services are yet unavailable.</param> | ||||||
|  |     public void SaveToDatabase(StarsDatabase? db = null)  | ||||||
|  |     { | ||||||
|  |         db ??= Locator.Current.GetService<StarsDatabase>()!; | ||||||
|  |          | ||||||
|  |         Model.Game? dbGame = db.Game.FirstOrDefault(); | ||||||
|  |         if (dbGame == null) | ||||||
|  |         { | ||||||
|  |             dbGame = new Model.Game(); | ||||||
|  |             db.Add(dbGame); | ||||||
|  |             db.SaveChanges(); | ||||||
|  |         } | ||||||
|  |         dbGame.GamePath = GamePath; | ||||||
|  |         dbGame.BaseName = BaseName;  | ||||||
|  |         db.Update(dbGame); | ||||||
|  |         db.SaveChanges(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Registers all services depending on this Game, in the order required. | ||||||
|  |     /// This is called once the framework is initialized but before the main  | ||||||
|  |     /// window is active and has its models. | ||||||
|  |     /// </summary> | ||||||
|  |     public void RegisterServicesForGame() | ||||||
|  |     { | ||||||
|  |         Locator.CurrentMutable.RegisterConstant(this, typeof(Services.Game)); | ||||||
|  |         Locator.CurrentMutable.RegisterConstant(new CSVDataLoader(), typeof(CSVDataLoader)); | ||||||
|  |         Locator.CurrentMutable.Register(() => new StarsDatabase(DatabaseFileName), typeof(StarsDatabase)); | ||||||
|  |  | ||||||
|  |         // TESTING HELPER | ||||||
|  |         if (__doCreateTestData) | ||||||
|  |             __createTestData(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// Starts all required background servies, so that we are in normal UI | ||||||
|  |     /// operation mode. This is called after the UI is up and running. | ||||||
|  |     /// </summary> | ||||||
|  |     public void StartBackgroundServices() | ||||||
|  |     { | ||||||
|  |         CSVDataLoader csvloader = Locator.Current.GetService<Services.CSVDataLoader>()!; | ||||||
|  |         csvloader.StartCSVWatcher(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private bool __doCreateTestData = false; | ||||||
|  |  | ||||||
|  |     /// <summary> | ||||||
|  |     /// TESTING HELPER | ||||||
|  |     /// </summary> | ||||||
|  |     private void __createTestData () | ||||||
|  |     { | ||||||
|  |         using var db = Locator.Current.GetService<StarsDatabase>()!; | ||||||
|  |  | ||||||
|  |         // Note: This sample requires the database to be created before running. | ||||||
|  |         // Console.WriteLine($"Database path: {db.DbPath}."); | ||||||
|  |  | ||||||
|  |         Race r = new() | ||||||
|  |         { | ||||||
|  |             Name = "Atlantis", | ||||||
|  |             PlayerRace = true, | ||||||
|  |             PlayerFileId = 1, | ||||||
|  |             ColonistsPerResource = 1000, | ||||||
|  |             GrowthRatePercent = 19, | ||||||
|  |             PRT = PRT.Other, | ||||||
|  |             HasOBRM = true, | ||||||
|  |             FactoryCost3 = false, | ||||||
|  |             FactoryNumberPer10k = 8, | ||||||
|  |             FactoryResCost = 8, | ||||||
|  |             FactoryResPer10 = 15, | ||||||
|  |             MineResCost = 3, | ||||||
|  |             MineMineralsPer10 = 10, | ||||||
|  |             MineNumberPer10k = 10 | ||||||
|  |         }; | ||||||
|  |         db.Add(r); | ||||||
|  |         db.SaveChanges(); | ||||||
|  |  | ||||||
|  |         var loader = new CSV.PlanetLoader(); | ||||||
|  |         loader.ImportForRace(Services.Game.Player); | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user