Story leader fixes

This commit is contained in:
gamer147
2026-05-25 17:10:08 -04:00
parent a33bfad3bc
commit ce8d80559b
2 changed files with 56 additions and 37 deletions

View File

@@ -250,33 +250,31 @@ public class StoryService : IStoryService
} }
public async Task<LeaderSelectResponse> GetLeaderSelectAsync(StoryApiType apiType, int sectionId, long viewerId) public async Task<LeaderSelectResponse> GetLeaderSelectAsync(StoryApiType apiType, int sectionId, long viewerId)
{ {
// For section's chara list we use a fixed 1-8 enumeration for leader-select sections. // Leader list comes from whatever chara_ids the section's chapter catalog actually contains —
// Non-leader-select sections are not expected to call this endpoint; returning leader_count=8 // NOT a fixed 1..8 enumeration. Sections in prod range from standard class subsets
// matches the client's default sentinel. // (e.g. section 5: charas 3,5,6,8) to custom-leader sections (e.g. section 17: chara_ids
var resp = new LeaderSelectResponse { LeaderCount = 8 }; // 500901-500904). Order is by ascending min(story_id) per chara, which reproduces prod's
var charaIds = new[] { 1, 2, 3, 4, 5, 6, 7, 8 }; // ordering for every captured section (including section 17's 500901,500903,500904,500902
// and section 15's 500701,500732,500704). Verified against traffic_prod_626_story.ndjson.
var chapters = await _master.GetChaptersBySectionsAsync(new[] { sectionId });
var charaGroups = chapters
.GroupBy(c => c.CharaId)
.Select(g => (CharaId: g.Key, Chapters: g.ToList(), MinStoryId: g.Min(c => c.StoryId)))
.OrderBy(g => g.MinStoryId)
.ToList();
// Pre-collect all story_ids across charas in this section to do one progress query. var resp = new LeaderSelectResponse { LeaderCount = charaGroups.Count };
var perCharaChapters = new Dictionary<int, List<StoryChapter>>(); if (charaGroups.Count == 0) return resp;
foreach (var c in charaIds)
{ var allStoryIds = chapters.Select(c => c.StoryId).ToList();
perCharaChapters[c] = await _master.GetChaptersBySectionCharaAsync(sectionId, c);
}
var allStoryIds = perCharaChapters.SelectMany(kv => kv.Value).Select(c => c.StoryId).ToList();
var progress = await _viewer.GetProgressForChaptersAsync(viewerId, allStoryIds); var progress = await _viewer.GetProgressForChaptersAsync(viewerId, allStoryIds);
foreach (var c in charaIds) foreach (var (charaId, chaps, _) in charaGroups)
{ {
var chapters = perCharaChapters[c];
if (chapters.Count == 0)
{
resp.LeaderList.Add(new LeaderEntry { CharaId = c, CurrentChapter = 1 });
continue;
}
int highest = 0; int highest = 0;
bool anySkipped = false; bool anySkipped = false;
int clearedCount = 0; int clearedCount = 0;
foreach (var ch in chapters) foreach (var ch in chaps)
{ {
if (progress.TryGetValue(ch.StoryId, out var p) && (p.IsFinish || p.IsSkipped)) if (progress.TryGetValue(ch.StoryId, out var p) && (p.IsFinish || p.IsSkipped))
{ {
@@ -288,9 +286,9 @@ public class StoryService : IStoryService
} }
resp.LeaderList.Add(new LeaderEntry resp.LeaderList.Add(new LeaderEntry
{ {
CharaId = c, CharaId = charaId,
IsSkipped = anySkipped, IsSkipped = anySkipped,
IsFinished = clearedCount == chapters.Count, IsFinished = clearedCount == chaps.Count,
CurrentChapter = (highest == 0) ? 1 : highest + 1, CurrentChapter = (highest == 0) ? 1 : highest + 1,
}); });
} }

View File

@@ -183,13 +183,10 @@ public class StoryServiceTests
[Test] [Test]
public async Task GetLeaderSelectAsync_untouched_chara_has_current_chapter_1() public async Task GetLeaderSelectAsync_untouched_chara_has_current_chapter_1()
{ {
_master.Setup(m => m.GetSectionsByFamilyAsync(StoryApiType.Main)) var allChapters = new[] { 1, 2, 3, 4, 5, 6, 7, 8 }
.ReturnsAsync(new List<StorySection> { new() { Id = 1, IsLeaderSelect = true } }); .Select(chara => Ch(100 + chara, 1, chara, "1", "2")).ToList();
foreach (int chara in new[] { 1, 2, 3, 4, 5, 6, 7, 8 }) _master.Setup(m => m.GetChaptersBySectionsAsync(It.Is<IEnumerable<int>>(ids => ids.Contains(1))))
{ .ReturnsAsync(allChapters);
_master.Setup(m => m.GetChaptersBySectionCharaAsync(1, chara))
.ReturnsAsync(new List<StoryChapter> { Ch(100 + chara, 1, chara, "1", "2") });
}
_viewer.Setup(v => v.GetProgressForChaptersAsync(7L, It.IsAny<IEnumerable<int>>())) _viewer.Setup(v => v.GetProgressForChaptersAsync(7L, It.IsAny<IEnumerable<int>>()))
.ReturnsAsync(new Dictionary<int, ViewerStoryProgress>()); .ReturnsAsync(new Dictionary<int, ViewerStoryProgress>());
@@ -203,14 +200,12 @@ public class StoryServiceTests
[Test] [Test]
public async Task GetLeaderSelectAsync_after_clearing_chapter5_current_chapter_is_6() public async Task GetLeaderSelectAsync_after_clearing_chapter5_current_chapter_is_6()
{ {
_master.Setup(m => m.GetSectionsByFamilyAsync(StoryApiType.Main)) // Section only seeded with chara=2 here — new impl returns only chara_ids actually present.
.ReturnsAsync(new List<StorySection> { new() { Id = 1, IsLeaderSelect = true } }); _master.Setup(m => m.GetChaptersBySectionsAsync(It.Is<IEnumerable<int>>(ids => ids.Contains(1))))
_master.Setup(m => m.GetChaptersBySectionCharaAsync(1, 2)).ReturnsAsync(new List<StoryChapter> { .ReturnsAsync(new List<StoryChapter> {
Ch(101, 1, 2, "1", "2"), Ch(102, 1, 2, "2", "3"), Ch(103, 1, 2, "3", "4"), Ch(101, 1, 2, "1", "2"), Ch(102, 1, 2, "2", "3"), Ch(103, 1, 2, "3", "4"),
Ch(104, 1, 2, "4", "5"), Ch(105, 1, 2, "5", "6"), Ch(106, 1, 2, "6", "0"), Ch(104, 1, 2, "4", "5"), Ch(105, 1, 2, "5", "6"), Ch(106, 1, 2, "6", "0"),
}); });
foreach (int chara in new[] { 1, 3, 4, 5, 6, 7, 8 })
_master.Setup(m => m.GetChaptersBySectionCharaAsync(1, chara)).ReturnsAsync(new List<StoryChapter>());
_viewer.Setup(v => v.GetProgressForChaptersAsync(7L, It.IsAny<IEnumerable<int>>())) _viewer.Setup(v => v.GetProgressForChaptersAsync(7L, It.IsAny<IEnumerable<int>>()))
.ReturnsAsync(new Dictionary<int, ViewerStoryProgress> { .ReturnsAsync(new Dictionary<int, ViewerStoryProgress> {
{ 101, new ViewerStoryProgress { StoryId = 101, IsFinish = true } }, { 101, new ViewerStoryProgress { StoryId = 101, IsFinish = true } },
@@ -227,6 +222,32 @@ public class StoryServiceTests
Assert.That(chara2.IsFinished, Is.False); // chapter 6 not done yet Assert.That(chara2.IsFinished, Is.False); // chapter 6 not done yet
} }
[Test]
public async Task GetLeaderSelectAsync_section_with_custom_leaders_returns_only_those_charas_in_min_story_id_order()
{
// Section 17 in prod offers 4 custom leaders (chara_ids 500901-500904), not the default
// 8 classes. Ordering is by ascending min(story_id) of each chara's chapters:
// 500901 (569), 500903 (591), 500904 (594), 500902 (597) — non-numeric chara_id sequence.
// Verified against data_dumps/traffic_prod_626_story.ndjson section_id=17 leader_select.
var s17chapters = new List<StoryChapter> {
Ch(569, 17, 500901, "1", "2"), Ch(570, 17, 500901, "2", "3"),
Ch(591, 17, 500903, "1", "2"), Ch(592, 17, 500903, "2", "3"),
Ch(594, 17, 500904, "1", "2"), Ch(595, 17, 500904, "2", "3"),
Ch(597, 17, 500902, "1", "2"), Ch(598, 17, 500902, "2", "3"),
};
_master.Setup(m => m.GetChaptersBySectionsAsync(It.Is<IEnumerable<int>>(ids => ids.Contains(17))))
.ReturnsAsync(s17chapters);
_viewer.Setup(v => v.GetProgressForChaptersAsync(7L, It.IsAny<IEnumerable<int>>()))
.ReturnsAsync(new Dictionary<int, ViewerStoryProgress>());
var resp = await _service.GetLeaderSelectAsync(StoryApiType.Main, 17, viewerId: 7L);
Assert.That(resp.LeaderCount, Is.EqualTo(4));
Assert.That(resp.LeaderList.Select(l => l.CharaId),
Is.EqualTo(new[] { 500901, 500903, 500904, 500902 }));
Assert.That(resp.LeaderList.All(l => l.CurrentChapter == 1), Is.True);
}
[Test] [Test]
public async Task StartAsync_returns_sbs_payload_for_chapter_with_sbs_id() public async Task StartAsync_returns_sbs_payload_for_chapter_with_sbs_id()
{ {