using Observatory.Framework.Files.Journal.Combat; using Observatory.Framework.Files.Journal.Exploration; using Observatory.Framework.Files.Journal.Odyssey; using Observatory.Framework.Files.Journal.Other; using Observatory.Framework.Files.Journal.Powerplay; using Observatory.Framework.Files.Journal.Startup; using Observatory.Framework.Files.Journal.StationServices; using Observatory.Framework.Files.Journal.Trade; using Observatory.Framework.Files.Journal.Travel; namespace Pulsar.Utils; using Observatory.Framework.Files.Journal; [Flags] public enum JournalReaderState { /// /// Have read the first character of the object /// Start, /// /// Have read the timestamp. Generally the first property in a journal entry. /// Timestamp, /// /// have read the event name. Generally the second property in a journal entry. /// Event, /// /// Have read the last character of the object, the next character should be a newline, whitespace, EOF, or another object. /// End, } /// /// A JournalFile contains a collection of journal entries. /// Each journal entry is a JSON object, delimited by a newline character. /// all Journals can be deserialized into a JournalBase object for identification /// and then deserialized into their respective types. /// public class JournalJsonConverter(ILogger logger) : JsonConverter { private JournalReaderState state = JournalReaderState.Start; public override JournalBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { Utf8JsonReader clone = reader; DateTimeOffset? timestamp = null; string? eventName = null; // for debug int depth = 0; do { depth++; switch (clone.TokenType) { case JsonTokenType.StartObject: state = JournalReaderState.Start; break; case JsonTokenType.EndObject: state = JournalReaderState.End; break; case JsonTokenType.PropertyName: var propertyName = clone.GetString(); // if we have not started reading the body, and we have not read the (timestamp or event name) if ((state & JournalReaderState.Timestamp) == 0 || (state & JournalReaderState.Event) == 0) { switch (propertyName) { case "timestamp": clone.Read(); timestamp = clone.GetDateTimeOffset(); state = JournalReaderState.Timestamp; break; case "event": clone.Read(); eventName = clone.GetString(); state = JournalReaderState.Event; break; } } if ((state & JournalReaderState.Event) != 0) { // create destination type return GetDestinationType(ref reader, eventName!); } break; case JsonTokenType.Comment: continue; case JsonTokenType.None: case JsonTokenType.StartArray: case JsonTokenType.EndArray: case JsonTokenType.String: case JsonTokenType.Number: case JsonTokenType.True: case JsonTokenType.False: case JsonTokenType.Null: logger.LogWarning("Unexpected token type {TokenType} at depth {Depth}", clone.TokenType, depth); break; default: throw new ArgumentOutOfRangeException(); } } while (clone.Read()); logger.LogWarning("Failed to deserialize journal entry at depth: {Depth}. Event?: {EventName}, Timestamp?: {Timestamp}", depth, eventName, timestamp); return null; // TODO: handle inf (invalid data) in the journal files // else if (typeof(TJournal) == typeof(Scan) && json.Contains("\"RotationPeriod\":inf")) // { // deserialized = JsonSerializer.Deserialize(json.Replace("\"RotationPeriod\":inf,", "")); // } } private JournalBase GetDestinationType(ref Utf8JsonReader reader, string eventName) { switch (eventName) { case "Fileheader": return JsonSerializer.Deserialize(ref reader)!; case "Commander": return JsonSerializer.Deserialize(ref reader)!; case "Materials": return JsonSerializer.Deserialize(ref reader)!; case "Rank": return JsonSerializer.Deserialize(ref reader)!; case "Music": return JsonSerializer.Deserialize(ref reader)!; case "Cargo": return JsonSerializer.Deserialize(ref reader)!; case "Loadout": return JsonSerializer.Deserialize(ref reader)!; case "Missions": return JsonSerializer.Deserialize(ref reader)!; case "FSSSignalDiscovered": return JsonSerializer.Deserialize(ref reader)!; case "Reputation": return JsonSerializer.Deserialize(ref reader)!; case "LoadGame": return JsonSerializer.Deserialize(ref reader)!; case "ReceiveText": return JsonSerializer.Deserialize(ref reader)!; case "ShipLocker": return JsonSerializer.Deserialize(ref reader)!; case "Location": return JsonSerializer.Deserialize(ref reader)!; case "Powerplay": return JsonSerializer.Deserialize(ref reader)!; case "ReservoirReplenished": return JsonSerializer.Deserialize(ref reader)!; case "Statistics": return JsonSerializer.Deserialize(ref reader)!; case "Scan": return JsonSerializer.Deserialize(ref reader)!; case "Shipyard": return JsonSerializer.Deserialize(ref reader)!; case "Docked": return JsonSerializer.Deserialize(ref reader)!; case "LeaveBody": return JsonSerializer.Deserialize(ref reader)!; case "Progress": return JsonSerializer.Deserialize(ref reader)!; case "SupercruiseExit": return JsonSerializer.Deserialize(ref reader)!; case "EngineerProgress": return JsonSerializer.Deserialize(ref reader)!; case "DockingRequested": return JsonSerializer.Deserialize(ref reader)!; case "NpcCrewPaidWage": return JsonSerializer.Deserialize(ref reader)!; case "SupercruiseEntry": return JsonSerializer.Deserialize(ref reader)!; case "DockingGranted": return JsonSerializer.Deserialize(ref reader)!; case "StartJump": return JsonSerializer.Deserialize(ref reader)!; case "FSSAllBodiesFound": return JsonSerializer.Deserialize(ref reader)!; case "FSSBodySignals": return JsonSerializer.Deserialize(ref reader)!; case "Liftoff": return JsonSerializer.Deserialize(ref reader)!; case "SupercruiseDestinationDrop": return JsonSerializer.Deserialize(ref reader)!; case "FSDTarget": return JsonSerializer.Deserialize(ref reader)!; case "FSDJump": return JsonSerializer.Deserialize(ref reader)!; case "CodexEntry": return JsonSerializer.Deserialize(ref reader)!; case "HullDamage": return JsonSerializer.Deserialize(ref reader)!; case "MaterialCollected": return JsonSerializer.Deserialize(ref reader)!; case "NavRoute": return JsonSerializer.Deserialize(ref reader)!; case "NavRouteClear": return JsonSerializer.Deserialize(ref reader)!; case "ScanBaryCentre": return JsonSerializer.Deserialize(ref reader)!; case "JetConeBoost": return JsonSerializer.Deserialize(ref reader)!; case "Shutdown": return JsonSerializer.Deserialize(ref reader)!; case "FuelScoop": return JsonSerializer.Deserialize(ref reader)!; case "FSSDiscoveryScan": return JsonSerializer.Deserialize(ref reader)!; case "ModuleInfo": return JsonSerializer.Deserialize(ref reader)!; case "ShipTargeted": return JsonSerializer.Deserialize(ref reader)!; case "AfmuRepairs": return JsonSerializer.Deserialize(ref reader)!; case "HeatWarning": return JsonSerializer.Deserialize(ref reader)!; case "ModuleBuy": return JsonSerializer.Deserialize(ref reader)!; case "BuyDrones": return JsonSerializer.Deserialize(ref reader)!; case "ShieldState": return JsonSerializer.Deserialize(ref reader)!; case "BuyAmmo": return JsonSerializer.Deserialize(ref reader)!; case "EjectCargo": return JsonSerializer.Deserialize(ref reader)!; case "ApproachBody": return JsonSerializer.Deserialize(ref reader)!; case "DockSRV": return JsonSerializer.Deserialize(ref reader)!; case "Touchdown": return JsonSerializer.Deserialize(ref reader)!; case "SAASignalsFound": return JsonSerializer.Deserialize(ref reader)!; case "EngineerCraft": return JsonSerializer.Deserialize(ref reader)!; case "MaterialTrade": return JsonSerializer.Deserialize(ref reader)!; case "Repair": return JsonSerializer.Deserialize(ref reader)!; case "RefuelAll": return JsonSerializer.Deserialize(ref reader)!; case "StoredModules": return JsonSerializer.Deserialize(ref reader)!; case "Synthesis": return JsonSerializer.Deserialize(ref reader)!; case "Scanned": return JsonSerializer.Deserialize(ref reader)!; case "SendText": return JsonSerializer.Deserialize(ref reader)!; case "Embark": return JsonSerializer.Deserialize(ref reader)!; case "MultiSellExplorationData": return JsonSerializer.Deserialize(ref reader)!; case "Backpack": return JsonSerializer.Deserialize(ref reader)!; case "ModuleSell": return JsonSerializer.Deserialize(ref reader)!; case "Undocked": return JsonSerializer.Deserialize(ref reader)!; case "RepairAll": return JsonSerializer.Deserialize(ref reader)!; case "Outfitting": return JsonSerializer.Deserialize(ref reader)!; case "PowerplaySalary": return JsonSerializer.Deserialize(ref reader)!; case "RedeemVoucher": return JsonSerializer.Deserialize(ref reader)!; case "SAAScanComplete": return JsonSerializer.Deserialize(ref reader)!; case "Friends": return JsonSerializer.Deserialize(ref reader)!; case "LaunchSRV": return JsonSerializer.Deserialize(ref reader)!; case "SuitLoadout": return JsonSerializer.Deserialize(ref reader)!; case "Disembark": return JsonSerializer.Deserialize(ref reader)!; case "MaterialDiscovered": return JsonSerializer.Deserialize(ref reader)!; case "StoredShips": return JsonSerializer.Deserialize(ref reader)!; case "ScanOrganic": return JsonSerializer.Deserialize(ref reader)!; case "Market": return JsonSerializer.Deserialize(ref reader)!; case "MissionCompleted": return JsonSerializer.Deserialize(ref reader)!; case "SellShipOnRebuy": return JsonSerializer.Deserialize(ref reader)!; case "MissionAccepted": return JsonSerializer.Deserialize(ref reader)!; case "ApproachSettlement": return JsonSerializer.Deserialize(ref reader)!; case "Screenshot": return JsonSerializer.Deserialize(ref reader)!; case "ModuleSwap": return JsonSerializer.Deserialize(ref reader)!; case "UnderAttack": return JsonSerializer.Deserialize(ref reader)!; case "DataScanned": return JsonSerializer.Deserialize(ref reader)!; case "DockingDenied": return JsonSerializer.Deserialize(ref reader)!; case "FetchRemoteModule": return JsonSerializer.Deserialize(ref reader)!; case "EngineerContribution": return JsonSerializer.Deserialize(ref reader)!; case "CollectCargo": return JsonSerializer.Deserialize(ref reader)!; case "ModuleRetrieve": return JsonSerializer.Deserialize(ref reader)!; case "MarketBuy": return JsonSerializer.Deserialize(ref reader)!; case "SellDrones": return JsonSerializer.Deserialize(ref reader)!; case "Interdicted": return JsonSerializer.Deserialize(ref reader)!; case "SellOrganicData": return JsonSerializer.Deserialize(ref reader)!; case "WingAdd": return JsonSerializer.Deserialize(ref reader)!; case "WingInvite": return JsonSerializer.Deserialize(ref reader)!; case "WingJoin": return JsonSerializer.Deserialize(ref reader)!; case "WingLeave": return JsonSerializer.Deserialize(ref reader)!; case "Bounty": return JsonSerializer.Deserialize(ref reader)!; case "CommitCrime": return JsonSerializer.Deserialize(ref reader)!; case "ModuleStore": return JsonSerializer.Deserialize(ref reader)!; case "FactionKillBond": return JsonSerializer.Deserialize(ref reader)!; case "RebootRepair": return JsonSerializer.Deserialize(ref reader)!; case "LaunchDrone": return JsonSerializer.Deserialize(ref reader)!; case "SellMicroResources": return JsonSerializer.Deserialize(ref reader)!; case "NavBeaconScan": return JsonSerializer.Deserialize(ref reader)!; case "SearchAndRescue": return JsonSerializer.Deserialize(ref reader)!; case "MarketSell": return JsonSerializer.Deserialize(ref reader)!; default: logger.LogWarning("Unknown Journal event type {EventName}", eventName); return JsonSerializer.Deserialize(ref reader)!; } } public override void Write(Utf8JsonWriter writer, JournalBase value, JsonSerializerOptions options) { throw new NotSupportedException(); } }