2
0
mirror of https://github.com/9ParsonsB/Pulsar.git synced 2025-07-01 16:33:43 -04:00

Add Startup Events to Database

Now emit startup events on conneciton
Some events still to add
This commit is contained in:
2024-05-25 19:41:38 +10:00
parent 579b2b115d
commit 68eff73dbd
80 changed files with 586 additions and 229 deletions

View File

@ -0,0 +1,17 @@
using JasperFx.Core;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Observatory.Framework.Files;
namespace Pulsar.Context.Configuration;
public class BackpackCofiguration : IEntityTypeConfiguration<BackpackFile>
{
public void Configure(EntityTypeBuilder<BackpackFile> builder)
{
builder.OwnsMany(b => b.Components, b => b.ToJson());
builder.OwnsMany(b => b.Consumables, b => b.ToJson());
builder.OwnsMany(b => b.Items, b => b.ToJson());
builder.OwnsMany(b => b.Data, b => b.ToJson());
}
}

View File

@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Observatory.Framework.Files.Journal.Startup;
namespace Pulsar.Context.Configuration;
public class CargoConfiguration : IEntityTypeConfiguration<Cargo>
{
public void Configure(EntityTypeBuilder<Cargo> builder)
{
builder.OwnsMany(c => c.Inventory, c => c.ToJson());
}
}

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Observatory.Framework.Files.Journal.Startup;
namespace Pulsar.Context.Configuration;
public class LoadoutConfiguration : IEntityTypeConfiguration<Loadout>
{
public void Configure(EntityTypeBuilder<Loadout> builder)
{
builder.OwnsMany(l => l.Modules, lb =>
{
lb.OwnsOne(m => m.Engineering, mb =>
{
mb.OwnsMany(e => e.Modifiers, eb =>
{
eb.OwnsOne(em => em.Value, emb => emb.ToJson());
eb.ToJson();
});
mb.ToJson();
});
lb.ToJson();
});
builder.OwnsOne(l => l.FuelCapacity, b=>b.ToJson());
}
}

View File

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Observatory.Framework.Files.Journal.Travel;
namespace Pulsar.Context.Configuration;
public class LocationConfiguration : IEntityTypeConfiguration<Location>
{
public void Configure(EntityTypeBuilder<Location> builder)
{
builder.OwnsOne(l => l.StarPos, b => b.ToJson());
builder.OwnsMany(l => l.Conflicts, b =>
{
b.OwnsOne(c => c.FirstFaction, c => c.ToJson());
b.OwnsOne(c => c.SecondFaction, c => c.ToJson());
b.ToJson();
});
builder.OwnsOne(l => l.StationFaction, b => b.ToJson());
builder.OwnsOne(l => l.SystemFaction, b =>
{
b.OwnsMany(s => s.ActiveStates, sb => sb.ToJson());
b.OwnsMany(s => s.RecoveringStates, rb => rb.ToJson());
b.ToJson();
});
builder.OwnsMany(l => l.Factions, b =>
{
b.OwnsMany(f => f.ActiveStates, fb => fb.ToJson());
b.OwnsMany(f => f.RecoveringStates, rb => rb.ToJson());
b.ToJson();
});
builder.OwnsMany(l => l.StationEconomies, sb => sb.ToJson());
builder.OwnsOne(l => l.ThargoidWar, tb => tb.ToJson());
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Observatory.Framework.Files.Journal.Odyssey;
namespace Pulsar.Context.Configuration;
public class ShipLockerConfiguration : IEntityTypeConfiguration<ShipLockerMaterials>
{
public void Configure(EntityTypeBuilder<ShipLockerMaterials> builder)
{
builder.OwnsMany(b => b.Items, b => b.ToJson());
builder.OwnsMany(b => b.Components, b => b.ToJson());
builder.OwnsMany(b => b.Consumables, b => b.ToJson());
builder.OwnsMany(b => b.Data, b => b.ToJson());
}
}

View File

@ -2,8 +2,11 @@ using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Files.Journal.Odyssey;
using Observatory.Framework.Files.Journal.Startup;
using Observatory.Framework.Files.Journal.StationServices;
using Observatory.Framework.Files.Journal.Travel;
using Pulsar.Features.ShipLocker;
/// <summary>
/// An in-memory database context for Pulsar.
@ -11,26 +14,23 @@ using Observatory.Framework.Files.Journal.StationServices;
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<Commander> Commander { get; set; }
public DbSet<Materials> Materials { get; set; }
public DbSet<Rank> Ranks { get; set; }
public DbSet<Rank> Rank { get; set; }
public DbSet<Progress> Progress { get; set; }
public DbSet<Reputation> Reputations { get; set; }
public DbSet<Reputation> Reputation { get; set; }
public DbSet<EngineerProgress> EngineerProgress { get; set; }
public DbSet<LoadGame> LoadGames { get; set; }
public DbSet<Statistics> Statistics { get; set; }
public DbSet<Location> Locations { get; set; }
public DbSet<Powerplay> PowerPlay { get; set; }
public DbSet<ShipLockerMaterials> ShipLocker { get; set; }
public DbSet<Missions> Missions { get; set; }
public DbSet<Loadout> Loadout { get; set; }
public DbSet<Cargo> Cargo { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

View File

@ -1,11 +1,19 @@
using Pulsar.Features.Journal;
namespace Pulsar.Features;
using Observatory.Framework.Files;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Files.Journal.Odyssey;
public class EventsHub : Hub<IEventsHub>
public class EventsHub(IJournalService journalService) : Hub<IEventsHub>
{
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
await Clients.Caller.JournalUpdated(await journalService.GetLastStartupEvents());
}
public async Task Status([FromServices] IStatusService statusService)
{
var status = await statusService.Get();

View File

@ -19,12 +19,7 @@ public class FileWatcherService(IOptions<PulsarConfiguration> options, IFileHand
Watch(cancellationToken);
// read the journal directory to get the initial files
#if DEBUG
Thread.Sleep(TimeSpan.FromSeconds(2));
HandleFileChanged(cancellationToken);
#else
HandleFileChanged(cancellationToken);
#endif
return Task.CompletedTask;

View File

@ -1,18 +1,19 @@
using Observatory.Framework.Files.Journal.Startup;
using Observatory.Framework.Files.Journal.StationServices;
namespace Pulsar.Features.Journal;
using Observatory.Framework;
using Observatory.Framework.Files.Journal;
using Observatory.Framework.Files.Journal.Startup;
public class JournalProcessor(
ILogger<JournalProcessor> logger,
IJournalService journalService,
IJournalStore journalStore,
PulsarContext context,
IEventHubContext hub) : IHostedService, IDisposable
{
private CancellationTokenSource tokenSource = new();
readonly JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true,
@ -52,24 +53,74 @@ public class JournalProcessor(
continue;
}
if (journal is LoadGame loadGame)
switch (journal)
{
// if not existing, add
if (context.LoadGames.Any(l => l.Timestamp == loadGame.Timestamp))
{
//return ValueTask.CompletedTask;
case Commander commander when context.Commander.Any(c => c.Timestamp == commander.Timestamp):
continue;
}
await context.LoadGames.AddAsync(loadGame, token);
await context.SaveChangesAsync(token);
case Commander commander:
await context.Commander.AddAsync(commander, token);
await context.SaveChangesAsync(token);
break;
case Materials materials when context.Materials.Any(m => m.Timestamp == materials.Timestamp):
continue;
case Materials materials:
await context.Materials.AddAsync(materials, token);
await context.SaveChangesAsync(token);
break;
case Rank rank when context.Rank.Any(r => r.Timestamp == rank.Timestamp):
continue;
case Rank rank:
await context.Rank.AddAsync(rank, token);
await context.SaveChangesAsync(token);
break;
case Progress progress when context.Progress.Any(p => p.Timestamp == progress.Timestamp):
continue;
case Progress progress:
await context.Progress.AddAsync(progress, token);
await context.SaveChangesAsync(token);
break;
case Reputation reputation when context.Reputation.Any(r => r.Timestamp == reputation.Timestamp):
continue;
case Reputation reputation:
await context.Reputation.AddAsync(reputation, token);
await context.SaveChangesAsync(token);
break;
case EngineerProgress engineerProgress
when context.EngineerProgress.Any(e => e.Timestamp == engineerProgress.Timestamp):
continue;
case EngineerProgress engineerProgress:
await context.EngineerProgress.AddAsync(engineerProgress, token);
await context.SaveChangesAsync(token);
break;
case LoadGame loadGame when context.LoadGames.Any(l => l.Timestamp == loadGame.Timestamp):
continue;
case LoadGame loadGame:
await context.LoadGames.AddAsync(loadGame, token);
await context.SaveChangesAsync(token);
break;
case Statistics statistics when context.Statistics.Any(s => s.Timestamp == statistics.Timestamp):
continue;
case Statistics statistics:
await context.Statistics.AddAsync(statistics, token);
await context.SaveChangesAsync(token);
break;
}
newJournals.Add(journal);
}
}
catch (JsonException ex)
{
logger.LogError(ex, "Error deserializing journal file: '{File}', line: {Line}", filePath, line);
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing journal file: '{File}', line# {LineNumber}, line: {Line}",
filePath, index, Encoding.UTF8.GetString(line.ToArray()));
}
//return ValueTask.CompletedTask;
}
@ -95,7 +146,7 @@ public class JournalProcessor(
{
try
{
if (journalService.TryDequeue(out var file))
if (journalStore.TryDequeue(out var file))
{
handled.AddRange(await HandleFileInner(file, tokenSource.Token));
}
@ -108,6 +159,7 @@ public class JournalProcessor(
{
handled = handled.Where(j => j.Timestamp > lastLoadGame.Timestamp).ToList();
}
await hub.Clients.All.JournalUpdated(handled);
handled.Clear();
}
@ -134,4 +186,4 @@ public class JournalProcessor(
{
tokenSource?.Dispose();
}
}
}

View File

@ -1,31 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Observatory.Framework.Files.Journal.Startup;
using Observatory.Framework.Files.Journal.StationServices;
namespace Pulsar.Features.Journal;
using System.Collections.Concurrent;
using Observatory.Framework.Files.Journal;
public interface IJournalService : IJournalHandler<List<JournalBase>>
{
public bool TryDequeue(out string filePath);
Task<List<JournalBase>> GetLastStartupEvents();
}
public class JournalService(
ILogger<JournalService> logger
ILogger<JournalService> logger,
IJournalStore store,
PulsarContext context
) : IJournalService
{
public string FileName => FileHandlerService.JournalLogFileName;
private readonly ConcurrentQueue<string> JournalFileQueue = new();
public void EnqueueFile(string filePath)
{
JournalFileQueue.Enqueue(filePath);
}
public bool TryDequeue(out string filePath)
{
return JournalFileQueue.TryDequeue(out filePath);
}
public Task HandleFile(string filePath, CancellationToken token = new())
{
if (!FileHelper.ValidateFile(filePath))
@ -33,10 +26,102 @@ public class JournalService(
return Task.CompletedTask;
}
EnqueueFile(filePath);
store.EnqueueFile(filePath);
return Task.CompletedTask;
}
// Start of game events/order:
/** Commander
* Materials
Rank
Progress
Reputation
EngineerProgress
LoadGame
--Some time later--
Statistics
-- Game Events (e.g. FSSSignalDiscovered) --
Location
Powerplay
ShipLocker
Missions
Loadout
Cargo
*/
// StartupEvents:
// Commander
// Materials
// Rank
// Progress
// Reputation
// EngineerProgress
// LoadGame
// StateEvents:
// Location
// Powerplay
// Music
// ShipLocker
// Missions
// Loadout
// Cargo
public async Task<List<JournalBase>> GetLastStartupEvents()
{
//TODO: add other state events
var commanderTask = context.Commander.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
var materialsTask = context.Materials.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
var rankTask = context.Rank.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
var progressTask = context.Progress.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
var reputationTask = context.Reputation.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
var engineerProgressTask = context.EngineerProgress.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
var loadGameTask = context.LoadGames.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
var statisticsTask = context.Statistics.OrderByDescending(x => x.Timestamp).FirstOrDefaultAsync();
await Task.WhenAll(
commanderTask,
materialsTask,
rankTask,
progressTask,
reputationTask,
engineerProgressTask,
loadGameTask,
statisticsTask);
var commander = await commanderTask;
var materials = await materialsTask;
var rank = await rankTask;
var progress = await progressTask;
var reputation = await reputationTask;
var engineerProgress = await engineerProgressTask;
var loadGame = await loadGameTask;
var statistics = await statisticsTask;
// if any null, return empty list
if (materials == null || rank == null || progress == null || reputation == null || engineerProgress == null ||
loadGame == null || statistics == null || commander == null)
{
return [];
}
// dont check the time of statistics as it may occur a few moments after
if (commander.Timestamp != materials.Timestamp ||
materials.Timestamp != rank.Timestamp ||
rank.Timestamp != progress.Timestamp ||
progress.Timestamp != reputation.Timestamp ||
reputation.Timestamp != engineerProgress.Timestamp ||
engineerProgress.Timestamp != loadGame.Timestamp ||
statistics.Timestamp < materials.Timestamp)
{
throw new InvalidOperationException("Timestamps do not match");
}
return [commander, materials, rank, progress, reputation, engineerProgress, loadGame, statistics];
}
public async Task<List<JournalBase>> Get()
{
return [];

View File

@ -0,0 +1,24 @@
namespace Pulsar.Features.Journal;
using System.Collections.Concurrent;
public interface IJournalStore
{
void EnqueueFile(string filePath);
bool TryDequeue(out string filePath);
}
public class JournalStore : IJournalStore
{
private readonly ConcurrentQueue<string> JournalFileQueue = new();
public void EnqueueFile(string filePath)
{
JournalFileQueue.Enqueue(filePath);
}
public bool TryDequeue(out string filePath)
{
return JournalFileQueue.TryDequeue(out filePath);
}
}

View File

@ -4,7 +4,11 @@ using Observatory.Framework.Files;
public interface INavRouteService : IJournalHandler<NavRouteFile>;
public class NavRouteService(IOptions<PulsarConfiguration> options, ILogger<NavRouteService> logger, IEventHubContext hub) : INavRouteService
public class NavRouteService(
IOptions<PulsarConfiguration> options,
ILogger<NavRouteService> logger,
IEventHubContext hub)
: INavRouteService
{
public async Task<NavRouteFile> Get()
{

View File

@ -4,7 +4,11 @@ using Observatory.Framework.Files;
public interface IOutfittingService : IJournalHandler<OutfittingFile>;
public class OutfittingService(IOptions<PulsarConfiguration> options, IEventHubContext hub, ILogger<OutfittingService> logger) : IOutfittingService
public class OutfittingService(
IOptions<PulsarConfiguration> options,
IEventHubContext hub,
ILogger<OutfittingService> logger)
: IOutfittingService
{
public async Task<OutfittingFile> Get()
{

View File

@ -4,7 +4,9 @@ using Observatory.Framework.Files.Journal.Odyssey;
public interface IShipLockerService : IJournalHandler<ShipLockerMaterials>;
public class ShipLockerService(ILogger<ShipLockerService> logger, IOptions<PulsarConfiguration> options,
public class ShipLockerService(
ILogger<ShipLockerService> logger,
IOptions<PulsarConfiguration> options,
IEventHubContext hub)
: IShipLockerService
{

View File

@ -22,7 +22,8 @@ public class PulsarServiceRegistry : ServiceRegistry
For<IStatusService>().Use<StatusService>();
For<IModulesInfoService>().Use<ModulesInfoService>();
For<ICargoService>().Use<CargoService>();
For<IJournalService>().Use<JournalService>().Singleton();
For<IJournalService>().Use<JournalService>();
For<IJournalStore>().Use<JournalStore>().Singleton();
For<IShipLockerService>().Use<ShipLockerService>();
For<IShipyardService>().Use<ShipyardService>();
For<IMarketService>().Use<MarketService>();