using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Engineering;
public sealed class EngineerProjector
{
private readonly Dictionary<long, EngineerAccess> engineers = [];
private readonly EngineeringRules rules = EngineeringRules.Captured;
public IReadOnlyList<EngineerAccess> 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<ShipRecord> 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;
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Engineering;
public sealed class EngineerProjector
{
private readonly Dictionary<long, EngineerAccess> engineers = [];
private readonly EngineeringRules rules = EngineeringRules.Captured;
public IReadOnlyList<EngineerAccess> 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<ShipRecord> 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;
}
}