From f7aecf04c601e34cc5f276e3bbe900b955650cff Mon Sep 17 00:00:00 2001 From: 16Wuyue <16Wuyue@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:34:51 +0800 Subject: [PATCH 1/2] export equipment improvement and proficiency in replay data Replay export flattened equipment down to the plain id, dropping Level and AircraftLevel. The noro6 export reads the same SortieRecord.FleetData and carries both, so the data was there all along. Add stars/ace to ReplayShip, kept index aligned with equip, and collapse the four duplicated fleet mappings into ToReplayFleet/ToReplayShip. --- .../Replay/ReplayExtensions.cs | 61 ++++++--------- .../SortieRecordViewer/Replay/ReplayShip.cs | 12 +++ .../Replay/ReplayExportTests.cs | 76 +++++++++++++++++++ 3 files changed, 113 insertions(+), 36 deletions(-) create mode 100644 ElectronicObserverCoreTests/Replay/ReplayExportTests.cs diff --git a/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs b/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs index bc33832af..0135464fb 100644 --- a/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs +++ b/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using ElectronicObserver.Core.Types; using ElectronicObserver.Database.Sortie; @@ -31,42 +32,10 @@ public static class ReplayExtensions Fleetnum = sortie.FleetData.FleetId, World = sortie.World, Mapnum = sortie.Map, - Fleet1 = sortie.FleetData.Fleets.Skip(0).FirstOrDefault()?.Ships - .Select(s => new ReplayShip - { - ShipId = s.Id, - Level = s.Level, - Morale = s.Condition, - Kyouka = s.Kyouka, - Equip = s.EquipmentSlots.Append(s.ExpansionSlot).Select(e => ((int?)e?.Equipment?.Id) ?? 0).ToList(), - }).ToList() ?? new(), - Fleet2 = sortie.FleetData.Fleets.Skip(1).FirstOrDefault()?.Ships - .Select(s => new ReplayShip - { - ShipId = s.Id, - Level = s.Level, - Morale = s.Condition, - Kyouka = s.Kyouka, - Equip = s.EquipmentSlots.Append(s.ExpansionSlot).Select(e => ((int?)e?.Equipment?.Id) ?? 0).ToList(), - }).ToList() ?? new(), - Fleet3 = sortie.FleetData.Fleets.Skip(2).FirstOrDefault()?.Ships - .Select(s => new ReplayShip - { - ShipId = s.Id, - Level = s.Level, - Morale = s.Condition, - Kyouka = s.Kyouka, - Equip = s.EquipmentSlots.Append(s.ExpansionSlot).Select(e => ((int?)e?.Equipment?.Id) ?? 0).ToList(), - }).ToList() ?? new(), - Fleet4 = sortie.FleetData.Fleets.Skip(3).FirstOrDefault()?.Ships - .Select(s => new ReplayShip - { - ShipId = s.Id, - Level = s.Level, - Morale = s.Condition, - Kyouka = s.Kyouka, - Equip = s.EquipmentSlots.Append(s.ExpansionSlot).Select(e => ((int?)e?.Equipment?.Id) ?? 0).ToList(), - }).ToList() ?? new(), + Fleet1 = sortie.FleetData.Fleets.Skip(0).FirstOrDefault().ToReplayFleet(), + Fleet2 = sortie.FleetData.Fleets.Skip(1).FirstOrDefault().ToReplayFleet(), + Fleet3 = sortie.FleetData.Fleets.Skip(2).FirstOrDefault().ToReplayFleet(), + Fleet4 = sortie.FleetData.Fleets.Skip(3).FirstOrDefault().ToReplayFleet(), AirBases = sortie.FleetData.AirBases .Where(b => b.MapAreaId == sortie.Map) .Select(b => new ReplayAirBase @@ -95,4 +64,24 @@ public static class ReplayExtensions }).ToList(), Battles = new(), }; + + private static List ToReplayFleet(this SortieFleet? fleet) + => fleet?.Ships.Select(ToReplayShip).ToList() ?? new(); + + private static ReplayShip ToReplayShip(SortieShip ship) + { + List slots = [.. ship.EquipmentSlots, ship.ExpansionSlot]; + + return new() + { + ShipId = ship.Id, + Level = ship.Level, + Morale = ship.Condition, + Kyouka = ship.Kyouka, + // equip, stars and ace have to stay index aligned + Equip = slots.Select(s => (int)(s?.Equipment?.Id ?? EquipmentId.Unknown)).ToList(), + Stars = slots.Select(s => s?.Equipment?.Level ?? 0).ToList(), + Ace = slots.Select(s => s?.Equipment?.AircraftLevel ?? 0).ToList(), + }; + } } diff --git a/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayShip.cs b/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayShip.cs index 6659ed560..33792a056 100644 --- a/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayShip.cs +++ b/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayShip.cs @@ -20,4 +20,16 @@ public class ReplayShip [JsonPropertyName("equip")] public List Equip { get; set; } + + /// + /// Equipment improvement levels, same order as . + /// + [JsonPropertyName("stars")] + public List Stars { get; set; } = new(); + + /// + /// Equipment proficiency levels, same order as . + /// + [JsonPropertyName("ace")] + public List Ace { get; set; } = new(); } diff --git a/ElectronicObserverCoreTests/Replay/ReplayExportTests.cs b/ElectronicObserverCoreTests/Replay/ReplayExportTests.cs new file mode 100644 index 000000000..8aabc9fb8 --- /dev/null +++ b/ElectronicObserverCoreTests/Replay/ReplayExportTests.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using ElectronicObserver.Database.Sortie; +using ElectronicObserver.Window.Tools.SortieRecordViewer.Replay; +using Xunit; + +namespace ElectronicObserverCoreTests.Replay; + +public class ReplayExportTests +{ + private static string DirectoryName => Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; + private static string RelativePath => "SortieDetail"; + private static string BasePath => Path.Join(DirectoryName, RelativePath); + + private static async Task> GetSortieRecords(string fileName) + { + await using Stream stream = File.OpenRead(Path.Join(BasePath, fileName)); + List? data = await JsonSerializer.DeserializeAsync>(stream); + + Assert.NotNull(data); + Assert.NotEmpty(data); + + return data; + } + + [Theory] + [InlineData("SortieDetailTest1.json")] + [InlineData("SortieDetailTest2.json")] + [InlineData("SortieDetailTest4.json")] + public async Task EquipmentImprovementIsExported(string fileName) + { + SortieRecord sortie = (await GetSortieRecords(fileName)).First(); + + ReplayData replay = sortie.ToReplayData(); + + List ships = new[] { replay.Fleet1, replay.Fleet2, replay.Fleet3, replay.Fleet4 } + .SelectMany(f => f ?? new()) + .ToList(); + + Assert.NotEmpty(ships); + + foreach (ReplayShip ship in ships) + { + Assert.Equal(ship.Equip.Count, ship.Stars.Count); + Assert.Equal(ship.Equip.Count, ship.Ace.Count); + } + + Assert.Contains(ships, s => s.Stars.Any(v => v > 0)); + } + + [Fact] + public async Task ImprovementMatchesTheSourceRecord() + { + SortieRecord sortie = (await GetSortieRecords("SortieDetailTest2.json")).First(); + + ReplayData replay = sortie.ToReplayData(); + + List replayShips = replay.Fleet1!; + List sourceShips = sortie.FleetData.Fleets[0]!.Ships; + + Assert.Equal(sourceShips.Count, replayShips.Count); + + foreach ((SortieShip source, ReplayShip exported) in sourceShips.Zip(replayShips)) + { + List slots = [.. source.EquipmentSlots, source.ExpansionSlot]; + + Assert.Equal(slots.Select(s => (int)(s?.Equipment?.Id ?? 0)), exported.Equip); + Assert.Equal(slots.Select(s => s?.Equipment?.Level ?? 0), exported.Stars); + Assert.Equal(slots.Select(s => s?.Equipment?.AircraftLevel ?? 0), exported.Ace); + } + } +} From 6ddfb910635b91750731caaa912166237ff10a0b Mon Sep 17 00:00:00 2001 From: 16Wuyue <16Wuyue@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:37:11 +0800 Subject: [PATCH 2/2] fix air base filter comparing map area against the map number MapAreaId holds the world, not the map, so the filter dropped every air base whenever the two differed - which is most sorties. On the 58-4 fixture all three air bases disappeared from the export. --- .../SortieRecordViewer/Replay/ReplayExtensions.cs | 2 +- .../Replay/ReplayExportTests.cs | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs b/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs index 0135464fb..999c4f74f 100644 --- a/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs +++ b/ElectronicObserver/Window/Tools/SortieRecordViewer/Replay/ReplayExtensions.cs @@ -37,7 +37,7 @@ public static class ReplayExtensions Fleet3 = sortie.FleetData.Fleets.Skip(2).FirstOrDefault().ToReplayFleet(), Fleet4 = sortie.FleetData.Fleets.Skip(3).FirstOrDefault().ToReplayFleet(), AirBases = sortie.FleetData.AirBases - .Where(b => b.MapAreaId == sortie.Map) + .Where(b => b.MapAreaId == sortie.World) .Select(b => new ReplayAirBase { Rid = b.AirCorpsId, diff --git a/ElectronicObserverCoreTests/Replay/ReplayExportTests.cs b/ElectronicObserverCoreTests/Replay/ReplayExportTests.cs index 8aabc9fb8..755425132 100644 --- a/ElectronicObserverCoreTests/Replay/ReplayExportTests.cs +++ b/ElectronicObserverCoreTests/Replay/ReplayExportTests.cs @@ -73,4 +73,19 @@ public async Task ImprovementMatchesTheSourceRecord() Assert.Equal(slots.Select(s => s?.Equipment?.AircraftLevel ?? 0), exported.Ace); } } + + [Fact] + public async Task AirBasesAreExportedWhenWorldDiffersFromMap() + { + // world 58, map 4 - the filter used to compare MapAreaId against the map number + SortieRecord sortie = (await GetSortieRecords("SortieDetailTest1.json")).First(); + + Assert.NotEqual(sortie.World, sortie.Map); + Assert.NotEmpty(sortie.FleetData.AirBases); + + ReplayData replay = sortie.ToReplayData(); + + Assert.NotNull(replay.AirBases); + Assert.Equal(sortie.FleetData.AirBases.Count, replay.AirBases!.Count); + } }