using System.Text.Json; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Core.Engineering; public sealed class EngineerProjector { private readonly Dictionary engineers = []; private readonly EngineeringRules rules = EngineeringRules.Captured; public IReadOnlyList Engineers => engineers.Values.OrderByDescending(x => x.Unlocked).ThenBy(x => x.Name).ToArray(); public DateTimeOffset? UpdatedAt { get; private set; } public int VerifiedQualityRolls { get; private set; } public int MismatchedQualityRolls { get; private set; } public void Reset() { engineers.Clear(); UpdatedAt = null; VerifiedQualityRolls = MismatchedQualityRolls = 0; } public void Apply(JsonElement e, IReadOnlyList ships, long? activeShip, long? marketId = null) { var time = e.Time() ?? DateTimeOffset.MinValue; var kind = e.Text("event"); if (kind is "RedeemVoucher" or "SellExplorationData" or "MultiSellExplorationData" or "MarketSell") { // These activities can raise reputation at particular engineer workshops. Their gain formula // is not part of the crafting configuration; keep an interval until an authoritative update. foreach (var (id, market) in EngineeringCatalog.Default.EngineerMarkets) if (market == marketId && engineers.TryGetValue(id, out var access) && access.Unlocked && access.Rank < 5 && access.ObservedAt <= time) { engineers[id] = access with { ReputationMin = access.ReputationMin ?? rules.ReputationThresholds[access.Rank], ReputationMax = 100, RankProgress = null, ObservedAt = time }; UpdatedAt = time; } } if (kind == "EngineerProgress") { if (UpdatedAt > time) return; var rows = e.Get("Engineers").ValueKind == JsonValueKind.Array ? e.Get("Engineers").Items() : [e]; // Startup is authoritative; single-engineer events merge missing fields. if (e.Get("Engineers").ValueKind == JsonValueKind.Array) engineers.Clear(); foreach (var x in rows) { var id = x.Long("EngineerID"); if (id <= 0) continue; var prior = engineers.GetValueOrDefault(id) ?? new EngineerAccess { Id = id }; var rank = (int)x.Long("Rank", prior.Rank); var access = x.Text("Progress", rank > 0 ? "Unlocked" : prior.Progress); double? lo = prior.ReputationMin, hi = prior.ReputationMax, progress = x.Number("RankProgress"); if (rank is >= 1 and <= 5) { if (progress is { } p) lo = hi = rules.Absolute(rank, p); else { var floor = rules.ReputationThresholds[rank]; var ceiling = rank == 5 ? floor : rules.ReputationThresholds[rank + 1] - 1e-7; if (lo is null || hi is null || hi < floor || lo > ceiling) { lo = floor; hi = ceiling; } else { lo = Math.Max(floor, lo.Value); hi = Math.Min(ceiling, hi.Value); } if (lo == hi) progress = rules.WithinRank(lo!.Value); } } engineers[id] = prior with { Name = x.Text("Engineer", prior.Name), Progress = access, Rank = rank, RankProgress = progress, ReputationMin = lo, ReputationMax = hi, ObservedAt = time }; } UpdatedAt = time; } if (kind != "EngineerCraft") return; var engineerId = e.Long("EngineerID"); if (!engineers.TryGetValue(engineerId, out var old) || old.ObservedAt > time) return; var grade = (int)e.Long("Level"); var experiment = e.Text("ApplyExperimentalEffect").Length > 0; var remote = e.Flag("IsRemote") || e.Flag("Remote"); if (!experiment && grade is >= 1 and <= 5) { var module = ships.FirstOrDefault(s => s.ShipId == activeShip)?.Modules.FirstOrDefault(m => m.Slot == e.Text("Slot") && m.Item.Equals(e.Text("Module"), StringComparison.OrdinalIgnoreCase)); if (module is not null && old.Rank >= grade && e.Number("Quality") is { } actual) { var before = module.Engineering is { } eng && eng.Blueprint == e.Text("BlueprintName") && eng.Level == grade ? eng.Quality : 0; if (before is { } q && q < .999) { var minRank = old.ReputationMin is { } min ? rules.Rank(min) : old.Rank; var maxRank = old.ReputationMax is { } max ? rules.Rank(max) : old.Rank; if (Enumerable.Range(Math.Max(grade, minRank), Math.Max(0, maxRank - Math.Max(grade, minRank) + 1)) .Any(rank => Math.Abs(Math.Min(1, q + EngineeringRules.QualityGain(grade, rank)) - actual) < .002)) VerifiedQualityRolls++; else MismatchedQualityRolls++; } } if (!remote && old.ReputationMin is { } lo && old.ReputationMax is { } hi) { var gain = rules.Reward(grade); lo = Math.Min(100, lo + gain); hi = Math.Min(100, hi + gain); var newRank = rules.Rank(lo); engineers[engineerId] = old with { ReputationMin = lo, ReputationMax = hi, Rank = newRank, RankProgress = Math.Abs(lo - hi) < 1e-6 ? rules.WithinRank(lo) : null, ObservedAt = time }; } } UpdatedAt = time; } }