mirror of
				https://github.com/9ParsonsB/Pulsar.git
				synced 2025-10-30 22:24:58 -04:00 
			
		
		
		
	Fix issues with Journal handling
Implement basic database Handle startup events only send events after the most recent LoadGame
This commit is contained in:
		| @@ -1,4 +1,5 @@ | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Runtime.Serialization; | ||||
| using DateTimeOffset = System.DateTimeOffset; | ||||
|  | ||||
| @@ -275,6 +276,7 @@ using Travel; | ||||
| public abstract class JournalBase | ||||
| { | ||||
|     [JsonPropertyName("timestamp")] | ||||
|     [Key] | ||||
|     public DateTimeOffset Timestamp { get; init; } | ||||
|  | ||||
|     /// <summary> | ||||
|   | ||||
| @@ -15,7 +15,7 @@ public class LoadGame : JournalBase | ||||
|     public bool StartLanded { get; init; } | ||||
|     public bool StartDead { get; init; } | ||||
|     public string GameMode { get; init; } | ||||
|     public string Group { get; init; } | ||||
|     public string? Group { get; init; } | ||||
|     public long Credits { get; init; } | ||||
|     public long Loan { get; init; } | ||||
|     public string ShipName { get; init; } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ public class Statistics : JournalBase | ||||
|     public Trading Trading { get; init; } | ||||
|     public Mining Mining { get; init; } | ||||
|     public ParameterTypes.Exploration Exploration { get; init; } | ||||
|     public Passengers Passengers { get; init; } | ||||
|     public ParameterTypes.Passengers Passengers { get; init; } | ||||
|     [JsonPropertyName("Search_And_Rescue")] | ||||
|     public ParameterTypes.SearchAndRescue SearchAndRescue { get; init; } | ||||
|     public Crafting Crafting { get; init; } | ||||
|   | ||||
| @@ -8,20 +8,4 @@ | ||||
|     <Nullable>enable</Nullable> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <VersionSuffix>0.1.$([System.DateTime]::UtcNow.Year.ToString().Substring(2))$([System.DateTime]::UtcNow.DayOfYear.ToString().PadLeft(3, "0")).$([System.DateTime]::UtcNow.ToString(HHmm))</VersionSuffix> | ||||
|     <AssemblyVersion Condition=" '$(VersionSuffix)' == '' ">0.0.0.1</AssemblyVersion> | ||||
|     <AssemblyVersion Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</AssemblyVersion> | ||||
|     <Version Condition=" '$(VersionSuffix)' == '' ">0.0.1.0</Version> | ||||
|     <Version Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</Version> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> | ||||
|     <DocumentationFile>ObservatoryFramework.xml</DocumentationFile> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Portable|AnyCPU'"> | ||||
|     <DocumentationFile>ObservatoryFramework.xml</DocumentationFile> | ||||
|   </PropertyGroup> | ||||
|    | ||||
| </Project> | ||||
|   | ||||
| @@ -0,0 +1,17 @@ | ||||
| namespace Pulsar.Context.Configuration; | ||||
|  | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
| using Observatory.Framework.Files.Journal.StationServices; | ||||
|  | ||||
| public class EngineerProgressConfiguration : IEntityTypeConfiguration<EngineerProgress> | ||||
| { | ||||
|     public void Configure(EntityTypeBuilder<EngineerProgress> builder) | ||||
|     { | ||||
|         builder.HasKey(x => x.Timestamp); | ||||
|         builder.OwnsMany(x => x.Engineers, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								Pulsar/Context/Configuration/LoadGameConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Pulsar/Context/Configuration/LoadGameConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| namespace Pulsar.Context.Configuration; | ||||
|  | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
|  | ||||
| public class LoadGameConfiguration : IEntityTypeConfiguration<LoadGame> | ||||
| { | ||||
|     public void Configure(EntityTypeBuilder<LoadGame> builder) | ||||
|     { | ||||
|         builder.HasKey(j => j.Timestamp); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										25
									
								
								Pulsar/Context/Configuration/MaterialsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								Pulsar/Context/Configuration/MaterialsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| namespace Pulsar.Context.Configuration; | ||||
|  | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
|  | ||||
| public class MaterialsConfiguration : IEntityTypeConfiguration<Materials> | ||||
| { | ||||
|     public void Configure(EntityTypeBuilder<Materials> builder) | ||||
|     { | ||||
|         builder.HasKey(x => x.Timestamp); | ||||
|         builder.OwnsMany(x => x.Raw, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsMany(x => x.Encoded, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsMany(x => x.Manufactured, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								Pulsar/Context/Configuration/ProgressConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Pulsar/Context/Configuration/ProgressConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| namespace Pulsar.Context.Configuration; | ||||
|  | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
|  | ||||
| public class ProgressConfiguration : IEntityTypeConfiguration<Progress> | ||||
| { | ||||
|     public void Configure(EntityTypeBuilder<Progress> builder) | ||||
|     { | ||||
|         builder.Ignore(x => x.AdditionalProperties); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										14
									
								
								Pulsar/Context/Configuration/RanksConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Pulsar/Context/Configuration/RanksConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| namespace Pulsar.Context.Configuration; | ||||
|  | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
|  | ||||
| public class RanksConfiguration : IEntityTypeConfiguration<Rank> | ||||
| { | ||||
|     public void Configure(EntityTypeBuilder<Rank> builder) | ||||
|     { | ||||
|         builder.Ignore(x => x.AdditionalProperties); | ||||
|         builder.HasKey(x => x.Timestamp); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								Pulsar/Context/Configuration/ReputationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Pulsar/Context/Configuration/ReputationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| namespace Pulsar.Context.Configuration; | ||||
|  | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
|  | ||||
| public class ReputationConfiguration : IEntityTypeConfiguration<Reputation> | ||||
| { | ||||
|     public void Configure(EntityTypeBuilder<Reputation> builder) | ||||
|     { | ||||
|         builder.HasKey(x => x.Timestamp); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										83
									
								
								Pulsar/Context/Configuration/StatisticsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								Pulsar/Context/Configuration/StatisticsConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| using Observatory.Framework.Files.ParameterTypes; | ||||
|  | ||||
| namespace Pulsar.Context.Configuration; | ||||
|  | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
|  | ||||
| public class StatisticsConfiguration : IEntityTypeConfiguration<Statistics> | ||||
| { | ||||
|     public void Configure(EntityTypeBuilder<Statistics> builder) | ||||
|     { | ||||
|         builder.HasKey(x => x.Timestamp); | ||||
|         builder.OwnsOne(x => x.BankAccount, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Combat, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Crime, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Smuggling, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Trading, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Mining, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Exploration, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Passengers, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.SearchAndRescue, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Crafting, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Crew, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Multicrew, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Thargoid, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.MaterialTrader, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.CQC, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.FleetCarrier, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|         builder.OwnsOne(x => x.Exobiology, b => | ||||
|         { | ||||
|             b.ToJson(); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										72
									
								
								Pulsar/Context/PulsarContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								Pulsar/Context/PulsarContext.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| using Microsoft.Data.Sqlite; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion; | ||||
| using Observatory.Framework.Files.Journal; | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
| using Observatory.Framework.Files.Journal.StationServices; | ||||
|  | ||||
| /// <summary> | ||||
| /// An in-memory database context for Pulsar. | ||||
| /// </summary> | ||||
| public class PulsarContext : DbContext | ||||
| { | ||||
|     public SqliteConnection Connection { get; private set; } | ||||
|      | ||||
|     // Start of game events: | ||||
|     /** | ||||
|      *  Materials | ||||
|         Rank | ||||
|         Progress | ||||
|         Reputation | ||||
|         EngineerProgress | ||||
|         LoadGame | ||||
|         --Some time later-- | ||||
|         Statistics | ||||
|      */ | ||||
|      | ||||
|     public DbSet<Materials> Materials { get; set; } | ||||
|     public DbSet<Rank> Ranks { get; set; } | ||||
|     public DbSet<Progress> Progress { get; set; } | ||||
|     public DbSet<Reputation> Reputations { get; set; } | ||||
|     public DbSet<EngineerProgress> EngineerProgress { get; set; } | ||||
|     public DbSet<LoadGame> LoadGames { get; set; } | ||||
|     public DbSet<Statistics> Statistics { get; set; } | ||||
|  | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
|         Connection = new SqliteConnection("Data Source=Journals.sqlite"); | ||||
|         optionsBuilder.UseSqlite(Connection); | ||||
|     } | ||||
|  | ||||
|     protected override void OnModelCreating(ModelBuilder modelBuilder) | ||||
|     { | ||||
|         modelBuilder.ApplyConfigurationsFromAssembly(typeof(PulsarContext).Assembly); | ||||
|         base.OnModelCreating(modelBuilder); | ||||
|  | ||||
|         if (Database.ProviderName != "Microsoft.EntityFrameworkCore.Sqlite") return; | ||||
|         // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations | ||||
|         // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations | ||||
|         // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset | ||||
|         // use the DateTimeOffsetToBinaryConverter | ||||
|         // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 | ||||
|         // This only supports millisecond precision, but should be sufficient for most use cases. | ||||
|         foreach (var entityType in modelBuilder.Model.GetEntityTypes()) | ||||
|         { | ||||
|             var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset) | ||||
|                                                                            || p.PropertyType == typeof(DateTimeOffset?)); | ||||
|             foreach (var property in properties) | ||||
|             { | ||||
|                 modelBuilder | ||||
|                     .Entity(entityType.Name) | ||||
|                     .Property(property.Name) | ||||
|                     .HasConversion(new DateTimeOffsetToBinaryConverter()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public override void Dispose() | ||||
|     { | ||||
|         Connection.Dispose(); | ||||
|         base.Dispose(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								Pulsar/Features/Backpack/BackpackService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Pulsar/Features/Backpack/BackpackService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| namespace Pulsar.Features.Backpack; | ||||
|  | ||||
| using Observatory.Framework.Files; | ||||
|  | ||||
| public interface IBackpackService : IJournalHandler<BackpackFile>; | ||||
|  | ||||
| public class BackpackService(IOptions<PulsarConfiguration> options, IEventHubContext hub, ILogger<BackpackService> logger) : IBackpackService | ||||
| { | ||||
|     public async Task<BackpackFile> Get() | ||||
|     { | ||||
|         var filePath = Path.Combine(options.Value.JournalDirectory, FileName); | ||||
|  | ||||
|         if (!FileHelper.ValidateFile(filePath)) | ||||
|         { | ||||
|             return new BackpackFile(); | ||||
|         } | ||||
|  | ||||
|         await using var file = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var backpack = await JsonSerializer.DeserializeAsync<BackpackFile>(file); | ||||
|         if (backpack != null) return backpack; | ||||
|  | ||||
|         logger.LogWarning("Failed to deserialize backpack file {File}", filePath); | ||||
|         return new BackpackFile(); | ||||
|     } | ||||
|  | ||||
|     public async Task HandleFile(string path, CancellationToken token = new ()) | ||||
|     { | ||||
|         if (!FileHelper.ValidateFile(path)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var backpack = await JsonSerializer.DeserializeAsync<BackpackFile>(file, cancellationToken: token); | ||||
|  | ||||
|         if (backpack == null) | ||||
|         { | ||||
|             logger.LogWarning("Failed to deserialize backpack {FilePath}", file); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await hub.Clients.All.BackpackUpdated(backpack); | ||||
|     } | ||||
|  | ||||
|     public string FileName => FileHandlerService.BackpackFileName; | ||||
| } | ||||
| @@ -3,8 +3,8 @@ namespace Pulsar.Features; | ||||
| using System.Collections.Concurrent; | ||||
| using Microsoft.Extensions.FileProviders; | ||||
|  | ||||
| public class FileWatcherService(IOptions<PulsarConfiguration> options, IFileHandlerService fileHandlerService) | ||||
|     : IHostedService | ||||
| public class FileWatcherService(IOptions<PulsarConfiguration> options, IFileHandlerService fileHandlerService, ILogger<FileWatcherService> logger) | ||||
|     : IHostedService, IDisposable | ||||
| { | ||||
|     private PhysicalFileProvider watcher = null!; | ||||
|  | ||||
| @@ -20,11 +20,8 @@ public class FileWatcherService(IOptions<PulsarConfiguration> options, IFileHand | ||||
|  | ||||
|         // read the journal directory to get the initial files | ||||
| #if DEBUG | ||||
|         Task.Run(() => | ||||
|         { | ||||
|         Thread.Sleep(TimeSpan.FromSeconds(2)); | ||||
|         HandleFileChanged(cancellationToken); | ||||
|         }, cancellationToken); | ||||
| #else | ||||
|         HandleFileChanged(cancellationToken); | ||||
| #endif | ||||
| @@ -37,25 +34,33 @@ public class FileWatcherService(IOptions<PulsarConfiguration> options, IFileHand | ||||
|  | ||||
|     private void HandleFileChanged(CancellationToken token = new()) | ||||
|     { | ||||
|         Watch(token); | ||||
|         var tasks = new List<Task>(); | ||||
|         try | ||||
|         { | ||||
|             foreach (var file in watcher.GetDirectoryContents("")) | ||||
|             { | ||||
|             if (file.IsDirectory || !file.Name.EndsWith(".json") && | ||||
|                 logger.LogDebug("Checking File: {File}", file.PhysicalPath); | ||||
|                 if (file.IsDirectory || (!file.Name.EndsWith(".json") && | ||||
|                                          !(file.Name.StartsWith(FileHandlerService.JournalLogFileNameStart) && | ||||
|                   file.Name.EndsWith(FileHandlerService.JournalLogFileNameEnd))) | ||||
|                                            file.Name.EndsWith(FileHandlerService.JournalLogFileNameEnd)))) | ||||
|                 { | ||||
|                 return; | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 logger.LogDebug("Has File Updated?: {File}, {LastModified}", file.PhysicalPath, file.LastModified); | ||||
|  | ||||
|                 FileDates.AddOrUpdate(file.PhysicalPath, _ => | ||||
|                 { | ||||
|                     logger.LogDebug("New File: {File}", file.PhysicalPath); | ||||
|                     tasks.Add(Task.Run(() => fileHandlerService.HandleFile(file.PhysicalPath, token), token)); | ||||
|                     return file.LastModified; | ||||
|                 }, (_, existing) => | ||||
|                 { | ||||
|                     logger.LogDebug("Existing File: {File}", file.PhysicalPath); | ||||
|                     if (existing != file.LastModified) | ||||
|                     { | ||||
|                         logger.LogDebug("File Updated: {File}", file.PhysicalPath); | ||||
|                         tasks.Add(Task.Run(() => fileHandlerService.HandleFile(file.PhysicalPath, token), token)); | ||||
|                     } | ||||
|  | ||||
| @@ -63,10 +68,13 @@ public class FileWatcherService(IOptions<PulsarConfiguration> options, IFileHand | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|         Watch(token); | ||||
|          | ||||
|             Task.WaitAll(tasks.ToArray(), token); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error handling file change"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void Watch(CancellationToken token) | ||||
|     { | ||||
| @@ -75,12 +83,24 @@ public class FileWatcherService(IOptions<PulsarConfiguration> options, IFileHand | ||||
|             HandleFileChanged(token); | ||||
|         } | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             watcher.Watch("*.*").RegisterChangeCallback(Handle, null); | ||||
|         } | ||||
|         catch (Exception ex) | ||||
|         { | ||||
|             logger.LogError(ex, "Error watching directory {Directory}", watcher.Root); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public Task StopAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|         watcher.Dispose(); | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
|  | ||||
|     public void Dispose() | ||||
|     { | ||||
|         watcher.Dispose(); | ||||
|     } | ||||
| } | ||||
| @@ -1,3 +1,5 @@ | ||||
| using Observatory.Framework.Files.Journal.Startup; | ||||
|  | ||||
| namespace Pulsar.Features.Journal; | ||||
|  | ||||
| using Observatory.Framework; | ||||
| @@ -6,7 +8,8 @@ using Observatory.Framework.Files.Journal; | ||||
| public class JournalProcessor( | ||||
|     ILogger<JournalProcessor> logger, | ||||
|     IJournalService journalService, | ||||
|     IEventHubContext hub) : IJournalProcessor, IHostedService, IDisposable | ||||
|     PulsarContext context, | ||||
|     IEventHubContext hub) : IHostedService, IDisposable | ||||
| { | ||||
|     private CancellationTokenSource tokenSource = new(); | ||||
|      | ||||
| @@ -18,7 +21,7 @@ public class JournalProcessor( | ||||
|         NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, | ||||
|     }; | ||||
|  | ||||
|     public async Task HandleFileInner(string filePath, CancellationToken token = new()) | ||||
|     public async Task<List<JournalBase>> HandleFileInner(string filePath, CancellationToken token = new()) | ||||
|     { | ||||
|         logger.LogInformation("Processing journal file: '{File}'", filePath); | ||||
|         var file = await File.ReadAllBytesAsync(filePath, token); | ||||
| @@ -49,6 +52,18 @@ public class JournalProcessor( | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 if (journal is LoadGame loadGame) | ||||
|                 { | ||||
|                     // if not existing, add | ||||
|                     if (context.LoadGames.Any(l => l.Timestamp == loadGame.Timestamp)) | ||||
|                     { | ||||
|                         //return ValueTask.CompletedTask; | ||||
|                         continue; | ||||
|                     } | ||||
|                     await context.LoadGames.AddAsync(loadGame, token); | ||||
|                     await context.SaveChangesAsync(token); | ||||
|                 } | ||||
|  | ||||
|                 newJournals.Add(journal); | ||||
|             }    | ||||
|             catch (JsonException ex) | ||||
| @@ -59,11 +74,7 @@ public class JournalProcessor( | ||||
|             //return ValueTask.CompletedTask; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         if (newJournals.Any()) | ||||
|         { | ||||
|             await hub.Clients.All.JournalUpdated(newJournals); | ||||
|         } | ||||
|         return newJournals; | ||||
|     } | ||||
|  | ||||
|     public Task StartAsync(CancellationToken cancellationToken) | ||||
| @@ -78,15 +89,36 @@ public class JournalProcessor( | ||||
|     { | ||||
|         Task.Run(async () => | ||||
|         { | ||||
|             while (!tokenSource.Token.IsCancellationRequested) | ||||
|             var token = tokenSource.Token; | ||||
|             var handled = new List<JournalBase>(); | ||||
|             while (!token.IsCancellationRequested) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     if (journalService.TryDequeue(out var file)) | ||||
|                     { | ||||
|                     await HandleFileInner(file, tokenSource.Token); | ||||
|                         handled.AddRange(await HandleFileInner(file, tokenSource.Token)); | ||||
|                     } | ||||
|                     else if (handled.Count > 0) | ||||
|                     { | ||||
|                         //get last loadgame | ||||
|                         var lastLoadGame = context.LoadGames.OrderByDescending(l => l.Timestamp).FirstOrDefault(); | ||||
|                         // only emit journals since last loadgame | ||||
|                         if (lastLoadGame != null) | ||||
|                         { | ||||
|                             handled = handled.Where(j => j.Timestamp > lastLoadGame.Timestamp).ToList(); | ||||
|                         } | ||||
|                         await hub.Clients.All.JournalUpdated(handled); | ||||
|                         handled.Clear(); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                     await Task.Delay(1000); | ||||
|                         await Task.Delay(1000, token); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     logger.LogError(ex, "Error processing journal queue"); | ||||
|                 } | ||||
|             } | ||||
|         }, tokenSource.Token); | ||||
| @@ -103,7 +135,3 @@ public class JournalProcessor( | ||||
|         tokenSource?.Dispose(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public interface IJournalProcessor | ||||
| { | ||||
| } | ||||
							
								
								
									
										46
									
								
								Pulsar/Features/Market/MarketService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Pulsar/Features/Market/MarketService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| namespace Pulsar.Features.Market; | ||||
|  | ||||
| using Observatory.Framework.Files; | ||||
|  | ||||
| public interface IMarketService : IJournalHandler<MarketFile>; | ||||
|  | ||||
| public class MarketService(IOptions<PulsarConfiguration> options, IEventHubContext hub, ILogger<MarketService> logger) : IMarketService | ||||
| { | ||||
|     public async Task<MarketFile> Get() | ||||
|     { | ||||
|         var filePath = Path.Combine(options.Value.JournalDirectory, FileName); | ||||
|  | ||||
|         if (!FileHelper.ValidateFile(filePath)) | ||||
|         { | ||||
|             return new MarketFile(); | ||||
|         } | ||||
|  | ||||
|         await using var file = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var market = await JsonSerializer.DeserializeAsync<MarketFile>(file); | ||||
|         if (market != null) return market; | ||||
|  | ||||
|         logger.LogWarning("Failed to deserialize market file {File}", filePath); | ||||
|         return new MarketFile(); | ||||
|     } | ||||
|  | ||||
|     public async Task HandleFile(string path, CancellationToken token = new ()) | ||||
|     { | ||||
|         if (!FileHelper.ValidateFile(path)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var market = await JsonSerializer.DeserializeAsync<MarketFile>(file, cancellationToken: token); | ||||
|  | ||||
|         if (market == null) | ||||
|         { | ||||
|             logger.LogWarning("Failed to deserialize market File {FilePath}", file); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await hub.Clients.All.MarketUpdated(market); | ||||
|     } | ||||
|  | ||||
|     public string FileName => FileHandlerService.MarketFileName; | ||||
| } | ||||
							
								
								
									
										46
									
								
								Pulsar/Features/NavRoute/NavRouteService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Pulsar/Features/NavRoute/NavRouteService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| namespace Pulsar.Features.NavRoute; | ||||
|  | ||||
| using Observatory.Framework.Files; | ||||
|  | ||||
| public interface INavRouteService :  IJournalHandler<NavRouteFile>; | ||||
|  | ||||
| public class NavRouteService(IOptions<PulsarConfiguration> options, ILogger<NavRouteService> logger, IEventHubContext hub) : INavRouteService | ||||
| { | ||||
|     public async Task<NavRouteFile> Get() | ||||
|     { | ||||
|         var navRouteFile = Path.Combine(options.Value.JournalDirectory, FileName); | ||||
|  | ||||
|         if (!FileHelper.ValidateFile(navRouteFile)) | ||||
|         { | ||||
|             return new NavRouteFile(); | ||||
|         } | ||||
|  | ||||
|         await using var file = File.Open(navRouteFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var shipLocker = await JsonSerializer.DeserializeAsync<NavRouteFile>(file); | ||||
|         if (shipLocker != null) return shipLocker; | ||||
|  | ||||
|         logger.LogWarning("Failed to deserialize nav route file {ShipLockerFile}", navRouteFile); | ||||
|         return new NavRouteFile(); | ||||
|     } | ||||
|  | ||||
|     public async Task HandleFile(string path, CancellationToken token = new ()) | ||||
|     { | ||||
|         if (!FileHelper.ValidateFile(path)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var navRoute = await JsonSerializer.DeserializeAsync<NavRouteFile>(file, cancellationToken: token); | ||||
|  | ||||
|         if (navRoute == null) | ||||
|         { | ||||
|             logger.LogWarning("Failed to deserialize nav route {FilePath}", file); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await hub.Clients.All.NavRouteUpdated(navRoute); | ||||
|     } | ||||
|  | ||||
|     public string FileName => FileHandlerService.NavRouteFileName; | ||||
| } | ||||
							
								
								
									
										46
									
								
								Pulsar/Features/Outfitting/OutfittingService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Pulsar/Features/Outfitting/OutfittingService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| namespace Pulsar.Features.Outfitting; | ||||
|  | ||||
| using Observatory.Framework.Files; | ||||
|  | ||||
| public interface IOutfittingService : IJournalHandler<OutfittingFile>; | ||||
|  | ||||
| public class OutfittingService(IOptions<PulsarConfiguration> options, IEventHubContext hub, ILogger<OutfittingService> logger) : IOutfittingService | ||||
| { | ||||
|     public async Task<OutfittingFile> Get() | ||||
|     { | ||||
|         var filePath = Path.Combine(options.Value.JournalDirectory, FileName); | ||||
|  | ||||
|         if (!FileHelper.ValidateFile(filePath)) | ||||
|         { | ||||
|             return new OutfittingFile(); | ||||
|         } | ||||
|  | ||||
|         await using var file = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var outfitting = await JsonSerializer.DeserializeAsync<OutfittingFile>(file); | ||||
|         if (outfitting != null) return outfitting; | ||||
|  | ||||
|         logger.LogWarning("Failed to deserialize outfitting file {File}", filePath); | ||||
|         return new OutfittingFile(); | ||||
|     } | ||||
|  | ||||
|     public async Task HandleFile(string path, CancellationToken token = new ()) | ||||
|     { | ||||
|         if (!FileHelper.ValidateFile(path)) | ||||
|         { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); | ||||
|         var outfitting = await JsonSerializer.DeserializeAsync<OutfittingFile>(file, cancellationToken: token); | ||||
|  | ||||
|         if (outfitting == null) | ||||
|         { | ||||
|             logger.LogWarning("Failed to deserialize outfitting file {FilePath}", file); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await hub.Clients.All.OutfittingUpdated(outfitting); | ||||
|     } | ||||
|  | ||||
|     public string FileName => FileHandlerService.OutfittingFileName; | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| using Lamar.Microsoft.DependencyInjection; | ||||
| using Microsoft.AspNetCore.Cors.Infrastructure; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.FileProviders; | ||||
| using Pulsar.Features; | ||||
| using Pulsar.Features.Journal; | ||||
| @@ -11,7 +12,7 @@ var builder = WebApplication.CreateBuilder(new WebApplicationOptions() | ||||
|     Args = args, WebRootPath = "static", ContentRootPath = "WebApp", ApplicationName = "Pulsar", EnvironmentName = | ||||
| #if DEBUG | ||||
|         "Development" | ||||
|     #else | ||||
| #else | ||||
|         "Production" | ||||
| #endif  | ||||
|      | ||||
| @@ -33,6 +34,7 @@ builder.Configuration.AddUserSecrets<Program>(); | ||||
|  | ||||
| builder.Services.Configure<PulsarConfiguration>(builder.Configuration.GetSection("Pulsar")); | ||||
|  | ||||
| builder.Services.AddApplicationInsightsTelemetry(); | ||||
| builder.Services.AddControllers(); | ||||
| builder.Services.AddCors(options => | ||||
| { | ||||
| @@ -60,5 +62,6 @@ app.MapControllers(); | ||||
| app.MapHub<EventsHub>("api/events"); | ||||
| app.MapFallbackToFile("index.html").AllowAnonymous(); | ||||
|  | ||||
| await app.Services.GetRequiredService<PulsarContext>().Database.EnsureCreatedAsync(); | ||||
|  | ||||
| await app.RunAsync(); | ||||
| @@ -17,8 +17,15 @@ | ||||
|     <ItemGroup> | ||||
|       <PackageReference Include="Lamar" Version="13.0.3" /> | ||||
|       <PackageReference Include="Lamar.Microsoft.DependencyInjection" Version="13.0.3" /> | ||||
|       <PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" /> | ||||
|       <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" /> | ||||
|       <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" /> | ||||
|       <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> | ||||
|         <PrivateAssets>all</PrivateAssets> | ||||
|         <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> | ||||
|       </PackageReference> | ||||
|       <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" /> | ||||
|       <PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.22.0" /> | ||||
|       <PackageReference Include="morelinq" Version="4.2.0" /> | ||||
|       <PackageReference Include="NSwag.AspNetCore" Version="14.0.7" /> | ||||
|       <PackageReference Include="NSwag.SwaggerGeneration" Version="12.3.0" /> | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| using Microsoft.Data.Sqlite; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Observatory.Framework.Files.Journal; | ||||
|  | ||||
| /// <summary> | ||||
| /// An in-memory database context for Pulsar. | ||||
| /// </summary> | ||||
| public class PulsarContext : DbContext | ||||
| { | ||||
|     public SqliteConnection Connection { get; private set;  } | ||||
|      | ||||
|     public DbSet<JournalBase> Journals { get; set; } | ||||
|      | ||||
|     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | ||||
|     { | ||||
|         Connection = new SqliteConnection("Data Source=:memory:"); | ||||
|         optionsBuilder.UseSqlite(Connection); | ||||
|     } | ||||
|  | ||||
|     public override void Dispose() | ||||
|     { | ||||
|         Connection.Dispose(); | ||||
|         base.Dispose(); | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +1,17 @@ | ||||
| namespace Pulsar; | ||||
|  | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Lamar; | ||||
| using Pulsar.Features; | ||||
| using Pulsar.Features.Cargo; | ||||
| using Pulsar.Features.ModulesInfo; | ||||
| using Pulsar.Features.Journal; | ||||
| using Pulsar.Features.ShipLocker; | ||||
| using Pulsar.Features.Shipyard; | ||||
|  | ||||
| namespace Pulsar; | ||||
| using Features; | ||||
| using Features.Backpack; | ||||
| using Features.Cargo; | ||||
| using Features.ModulesInfo; | ||||
| using Features.Journal; | ||||
| using Features.Market; | ||||
| using Features.NavRoute; | ||||
| using Features.Outfitting; | ||||
| using Features.ShipLocker; | ||||
| using Features.Shipyard; | ||||
|  | ||||
| [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] | ||||
| public class PulsarServiceRegistry : ServiceRegistry | ||||
| @@ -21,5 +25,9 @@ public class PulsarServiceRegistry : ServiceRegistry | ||||
|         For<IJournalService>().Use<JournalService>().Singleton(); | ||||
|         For<IShipLockerService>().Use<ShipLockerService>(); | ||||
|         For<IShipyardService>().Use<ShipyardService>(); | ||||
|         For<IMarketService>().Use<MarketService>(); | ||||
|         For<IBackpackService>().Use<BackpackService>(); | ||||
|         For<INavRouteService>().Use<NavRouteService>(); | ||||
|         For<IOutfittingService>().Use<OutfittingService>(); | ||||
|     } | ||||
| } | ||||
| @@ -5,6 +5,14 @@ | ||||
|       "Microsoft": "Information", | ||||
|       "Microsoft.Hosting.Lifetime": "Information", | ||||
|       "Microsoft.AspNetCore": "Information" | ||||
|     }, | ||||
|     "ApplicationInsights": { | ||||
|       "LogLevel": { | ||||
|         "Default": "Information", | ||||
|         "Microsoft.Data.SqlClient": "Warning", | ||||
|         "Microsoft.EntityFrameworkCore": "Warning", | ||||
|         "Microsoft.AspNetCore": "Warning" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "ReverseProxy": { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user