Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions AscNet.GameServer/Handlers/CourseModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@ private static int ChapterPoint(Player player, CourseChapterTable chapter) =>
private static int TotalLessonPoint(Player player) => Chapters.Value.Values
.Where(chapter => chapter.StageType == 1).Sum(chapter => ChapterPoint(player, chapter));

private static bool HasCompletionRule(CourseChapterTable chapter) =>
chapter.StageIds.Count > 0 && (chapter.StageType == 1 || chapter.StageType == 2 && chapter.ClearPoint is > 0);

internal static bool TryGetChapterComplete(Player player, int chapterId, out bool complete)
{
complete = false;
if (!Chapters.Value.TryGetValue(chapterId, out CourseChapterTable? chapter)) return true;
if (chapter.ClearPoint is not > 0) return false;
complete = chapter.StageIds.Count > 0
&& chapter.StageIds.All(id => player.Course.Stages.Any(stage => stage.Id == id))
&& ChapterPoint(player, chapter) >= chapter.ClearPoint.Value;
if (!HasCompletionRule(chapter)) return false;
complete = chapter.StageIds.All(id => player.Course.Stages.Any(stage => stage.Id == id))
&& (chapter.StageType == 1 || ChapterPoint(player, chapter) >= chapter.ClearPoint!.Value);
return true;
}

Expand All @@ -72,8 +74,7 @@ internal static bool IsChapterComplete(Player player, int chapterId) =>
TotalLessonPoint = TotalLessonPoint(player),
MaxTotalLessonPoint = player.Course.MaxTotalLessonPoint,
ChapterDataList = Chapters.Value.Values
// Unknown lesson IsClear cannot be represented by the authoritative boolean wire field.
.Where(chapter => chapter.ClearPoint is > 0
.Where(chapter => HasCompletionRule(chapter)
&& player.Course.Stages.Any(stage => chapter.StageIds.Contains(stage.Id)))
.Select(chapter => ChapterData(player, chapter)).ToList(),
StageDataDict = player.Course.Stages.Where(stage => Stages.Value.ContainsKey(stage.Id))
Expand Down
45 changes: 37 additions & 8 deletions AscNet.Test/Program.CourseCompatibility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ private static void ValidateCourseCompatibility()
List<CourseChapterTable> chapters = TableReaderV2.Parse<CourseChapterTable>();
Dictionary<int, CourseStageTable> stages = TableReaderV2.Parse<CourseStageTable>().ToDictionary(row => row.StageId);
CourseChapterTable lesson = chapters.First(row => row.StageType == 1 && row.StageIds.Count > 1);
CourseChapterTable secondLesson = chapters.Where(row => row.StageType == 1 && row.StageIds.Count > 1)
.OrderBy(row => row.ChapterId).Skip(1).First();
CourseChapterTable exam = chapters.First(row => row.StageType == 2 && row.PrevChapterIds is not > 0);
CourseRewardTable reward = TableReaderV2.Parse<CourseRewardTable>()
.Where(row => row.ChapterId == lesson.ChapterId && row.Point > 0).OrderBy(row => row.Point).First();
Expand Down Expand Up @@ -183,13 +185,39 @@ void RejectFightResult(int stageId, bool wrongFightId)
player = BsonSerializer.Deserialize<Player>(player.ToBson());
harness.Session.player = player;
AssertEqual(firstStage, player.Course.PendingResult?.Id ?? 0, "Pending lesson result survives BSON relog");
byte[] blockedLesson = player.Course.ToBson();
AssertEqual(1, Save(true).Code, "Lesson save blocks while retail completion evidence is unavailable");
AssertEqual(Convert.ToHexString(blockedLesson), Convert.ToHexString(player.Course.ToBson()),
"Blocked lesson save retains pending result without mutating saved progression");
AssertEqual(0, Login().Data.ChapterDataList.Count, "Login does not invent lesson chapter completion");
CourseSaveResultResponse firstSaved = Save(true);
AssertEqual(0, firstSaved.Code, "C1 lesson result saves after BSON relog");
AssertEqual(null, player.Course.PendingResult, "C1 lesson save consumes pending result");
AssertEqual(false, firstSaved.ChapterData!.IsClear, "C1 remains incomplete after one distinct stage");
AssertEqual(false, IsComplete(lesson.ChapterId), "C1 mission predicate rejects an incomplete lesson");
AssertEqual(false, Login().Data.ChapterDataList.Single(row => row.Id == lesson.ChapterId).IsClear,
"C1 partial progress is represented in login data");
foreach (int stageId in lesson.StageIds.Skip(1))
{
Fight(stageId, Mask(stageId));
CourseSaveResultResponse saved = Save();
AssertEqual(null, player.Course.PendingResult, "C1 stage save consumes pending result");
AssertEqual(stageId == lesson.StageIds[^1], saved.ChapterData!.IsClear,
"C1 completes only after every configured stage is saved");
}
AssertEqual(true, IsComplete(lesson.ChapterId), "C1 completion satisfies the mission chapter predicate");
foreach (int stageId in secondLesson.StageIds)
{
Fight(stageId, Mask(stageId));
CourseSaveResultResponse saved = Save();
AssertEqual(null, player.Course.PendingResult, "C2 stage save consumes pending result");
AssertEqual(stageId == secondLesson.StageIds[^1], saved.ChapterData!.IsClear,
"C2 completes only after every configured stage is saved");
}
player = BsonSerializer.Deserialize<Player>(player.ToBson());
harness.Session.player = player;
AssertEqual(true, Login().Data.ChapterDataList.Single(row => row.Id == lesson.ChapterId).IsClear,
"C1 completion survives BSON relog");
AssertEqual(true, Login().Data.ChapterDataList.Single(row => row.Id == secondLesson.ChapterId).IsClear,
"C2 completion survives BSON relog");
AssertEqual(true, IsComplete(secondLesson.ChapterId), "C2 completion satisfies the mission chapter predicate");

// Explicit persisted earned-state fixture: lesson saving is blocked, not simulated or authorized here.
// Explicit persisted earned-state fixture for reward and aggregate-point validation.
player.Course.PendingResult = null;
player.Course.Stages = [new CourseStageState { Id = firstStage, StarsFlag = 0 }];
Claim([reward.Id], 20175011);
Expand All @@ -201,7 +229,8 @@ void RejectFightResult(int stageId, bool wrongFightId)
harness.Session.player = player;
AssertEqual(expectedLesson, Login().Data.TotalLessonPoint, "Persisted lesson stars derive current points independently of completion evidence");
AssertEqual(player.Course.Stages.Count, Login().Data.StageDataDict.Count, "Login preserves earned lesson stages");
AssertEqual(0, Login().Data.ChapterDataList.Count, "Login omits unsupported lesson chapter clear flags");
AssertEqual(chapters.Count(row => row.StageType == 1), Login().Data.ChapterDataList.Count,
"Login includes authoritative completion state for persisted lesson chapters");
Claim([reward.Id, int.MaxValue], 20175008);
CourseGetRewardResponse awarded = Claim([reward.Id], 0);
AssertEqual(true, awarded.SuccessRewardIds.SequenceEqual([reward.Id]), "Reward response identifies exact successful claim");
Expand Down Expand Up @@ -279,6 +308,6 @@ void RejectFightResult(int stageId, bool wrongFightId)
harness.Session.player = player;
AssertEqual(true, Login().Data.ChapterDataList.Single(row => row.Id == dependentExam.ChapterId).IsClear,
"Dependent exam completion survives BSON relog");
Console.WriteLine("Course compatibility: blocked unsupported lesson save, persisted lesson fixtures, real exams, pending/save, downgrade, rewards and BSON relog passed.");
Console.WriteLine("Course compatibility: C1/C2 lesson saves, persisted lesson state, real exams, pending/save, downgrade, rewards and BSON relog passed.");
}
}