using CapySR.Data; using CapySR.Data.Excel; using CapySR.GameServer.Game; using CapySR.GameServer.Net; using CapySR.Protocol; using Microsoft.Extensions.Logging; namespace CapySR.GameServer.Handlers; [Handlers] public static class ChallengeHandlers { // MOC floor N is locked until floor N-1 is cleared, and PF/AS want a score on the floor // before. reporting a full clear for anything unplayed keeps every floor reachable. private const uint AllStars = 7; public static Task OnGetChallenge(PlayerSession session, GetChallengeCsReq request) { var data = session.World.Data; var records = session.Player.ChallengeRecords; var unlockAll = session.Config.GameServer.UnlockAllChallenges; var response = new GetChallengeScRsp { Retcode = 0 }; foreach (var challenge in data.Challenges.Values.OrderBy(c => c.ID)) { var record = records.GetValueOrDefault(challenge.ID); var clearScore = unlockAll ? ClearScore(data, challenge) : 0; response.ChallengeList.Add(new Challenge { ChallengeId = challenge.ID, Star = record?.Stars ?? (unlockAll ? AllStars : 0), ScoreId = record?.Score ?? clearScore, ScoreTwo = record?.ScoreTwo ?? clearScore, TakenReward = 0, }); } foreach (var groupId in data.ChallengeGroups.Keys.Order()) { response.ChallengeGroupList.Add(new ChallengeGroup { GroupId = groupId, TakenStarsCountReward = 0 }); } response.MaxLevelList.Add(new ChallengeHistoryMaxLevel { RewardDisplayType = 1, Level = 12 }); response.MaxLevelList.Add(new ChallengeHistoryMaxLevel { RewardDisplayType = 2, Level = 4 }); response.MaxLevelList.Add(new ChallengeHistoryMaxLevel { RewardDisplayType = 3, Level = 4 }); return session.SendAsync(response); } // the score a PF or AS floor counts as fully cleared, straight out of its target rows private static uint ClearScore(GameData data, ChallengeMazeExcel challenge) { if (challenge.Kind == ChallengeKind.MemoryOfChaos) { return 0; } return challenge.ChallengeTargetID .Select(id => data.ChallengeTargets.GetValueOrDefault(id)) .Where(t => t?.ChallengeTargetType == "TOTAL_SCORE") .Select(t => t!.ChallengeTargetParam1) .DefaultIfEmpty(0u) .Max(); } public static Task OnGetCurChallenge(PlayerSession session, GetCurChallengeCsReq request) { var state = session.Challenge; var response = new GetCurChallengeScRsp { Retcode = 0 }; if (state.Active && !state.IsPeak) { response.CurChallenge = ChallengeFlow.BuildCurChallenge(session); response.LineupList.Add(session.BuildChallengeLineup(0)); if (state.NodeCount > 1) { response.LineupList.Add(session.BuildChallengeLineup(1)); } } return session.SendAsync(response); } public static Task OnGetChallengeGroupStatistics(PlayerSession session, GetChallengeGroupStatisticsCsReq request) => session.SendAsync(new GetChallengeGroupStatisticsScRsp { Retcode = 0, GroupId = request.GroupId }); public static Task OnTakeChallengeReward(PlayerSession session, TakeChallengeRewardCsReq request) => session.SendAsync(new TakeChallengeRewardScRsp { Retcode = 0, GroupId = request.GroupId }); public static async Task OnStartChallenge(PlayerSession session, StartChallengeCsReq request) { var world = session.World; var challenge = world.Data.GetChallenge(request.ChallengeId); if (challenge is null) { session.Logger.LogWarning("unknown challenge {Id}", request.ChallengeId); await session.SendAsync(new StartChallengeScRsp { Retcode = (uint)Retcode.RetChallengeNotExist }); return; } var first = Lineup(request.AvatarLineupFirst, request.FirstLineup); var second = Lineup(request.AvatarLineupSecond, request.SecondLineup); if (first.Count == 0) { first = second.Count > 0 ? second : session.DefaultLineup(); } // a two node floor wants a team per half; run the first one twice rather than refuse if (challenge.NodeCount > 1 && second.Count == 0) { second = first; } var (buffOne, buffTwo) = challenge.Kind switch { ChallengeKind.PureFiction => (request.StageInfo?.StoryInfo?.BuffOne ?? 0u, request.StageInfo?.StoryInfo?.BuffTwo ?? 0u), ChallengeKind.ApocalypticShadow => (request.StageInfo?.BossInfo?.BuffOne ?? 0u, request.StageInfo?.BossInfo?.BuffTwo ?? 0u), _ => (0u, 0u), }; var state = session.Challenge; if (!state.Start(world.Data, challenge, first, second, buffOne, buffTwo)) { session.Logger.LogWarning("challenge {Id} has no usable scene", challenge.ID); await session.SendAsync(new StartChallengeScRsp { Retcode = (uint)Retcode.RetChallengeNotExist }); return; } session.Battles.Quit(); var book = session.Player.Lineups; book.ClearExtra(); book.SetExtra(ExtraLineupType.LineupChallenge, state.Lineups[0], activate: false); if (state.NodeCount > 1) { book.SetExtra(ExtraLineupType.LineupChallenge2, state.Lineups[1], activate: false); } await ChallengeFlow.ActivateNodeAsync(session); session.Logger.LogInformation( "challenge {Id} ({Kind}) started: {Nodes} node(s), stage {Stage}, floor {Floor}, monster {Monster}, buffs [{Buffs}]", state.ChallengeId, state.Kind, state.NodeCount, state.StageId, state.FloorId, state.MonsterId, string.Join(", ", state.Blessings)); var scene = world.Scenes.BuildChallengeScene(state); session.Scene.Load(scene, isChallenge: true); var response = new StartChallengeScRsp { Retcode = 0, CurChallenge = ChallengeFlow.BuildCurChallenge(session), Scene = scene, StageInfo = ChallengeFlow.BuildStageInfo(session), }; response.LineupList.Add(session.BuildChallengeLineup(0)); if (state.NodeCount > 1) { response.LineupList.Add(session.BuildChallengeLineup(1)); } await session.SendAsync(response); await session.NotifySpawnPositionAsync(scene); await session.SyncLineupAsync(); } public static async Task OnLeaveChallenge(PlayerSession session, LeaveChallengeCsReq request) { await session.SendAsync(new LeaveChallengeScRsp { Retcode = 0 }); await LeaveAsync(session); } // AS: the second boss half, requested once the phase settle screen is dismissed public static async Task OnEnterChallengeNextPhase(PlayerSession session, EnterChallengeNextPhaseCsReq request) { var state = session.Challenge; if (!state.Active || state.IsPeak || state.IsLastNode) { await session.SendAsync(new EnterChallengeNextPhaseScRsp { Retcode = (uint)Retcode.RetChallengeNotDoing }); return; } await ChallengeFlow.AdvanceNodeAsync(session); await session.SendAsync(new EnterChallengeNextPhaseScRsp { Retcode = 0, Scene = session.CurrentScene() }); } public static async Task OnRestartChallengePhase(PlayerSession session, RestartChallengePhaseCsReq request) { if (!session.Challenge.Active || session.Challenge.IsPeak) { await session.SendAsync(new RestartChallengePhaseScRsp { Retcode = (uint)Retcode.RetChallengeNotDoing }); return; } session.Battles.Quit(); await ChallengeFlow.RestartNodeAsync(session); await session.SendAsync(new RestartChallengePhaseScRsp { Retcode = 0, Scene = session.CurrentScene() }); } // ---- Anomaly Arbitration (Challenge Peak) ---- public static Task OnGetChallengePeakData(PlayerSession session, GetChallengePeakDataCsReq request) { var data = session.World.Data; var response = new GetChallengePeakDataScRsp { Retcode = 0 }; foreach (var group in data.ChallengePeakGroups.Values.OrderBy(g => g.ID)) { response.ChallengePeakGroups.Add(ChallengeFlow.BuildPeakGroup(session, group)); response.CurrentPeakGroupId = group.ID; } return session.SendAsync(response); } public static Task OnGetCurChallengePeak(PlayerSession session, GetCurChallengePeakCsReq request) { var state = session.Challenge; var response = new GetCurChallengePeakScRsp { Retcode = 0 }; if (state.Active && state.IsPeak) { response.PeakId = state.ChallengeId; response.BossBuffId = state.SelectedBuffs[0]; response.HasPassed = state.Status == ChallengeStatus.ChallengeFinish; } return session.SendAsync(response); } public static Task OnSetChallengePeakBossHardMode(PlayerSession session, SetChallengePeakBossHardModeCsReq request) { session.Challenge.HardMode = request.IsHardMode; return session.SendAsync(new SetChallengePeakBossHardModeScRsp { Retcode = 0, PeakGroupId = request.PeakGroupId, IsHardMode = request.IsHardMode, }); } public static async Task OnSetChallengePeakMobLineupAvatar(PlayerSession session, SetChallengePeakMobLineupAvatarCsReq request) { foreach (var lineup in request.LineupList) { session.Player.PeakLineups[lineup.PeakId] = [.. lineup.PeakAvatarIdList.Where(session.OwnsBase)]; } session.Player.Touch(); if (session.World.Data.ChallengePeakGroups.TryGetValue(request.PeakGroupId, out var group)) { await session.SendAsync(new ChallengePeakGroupDataUpdateScNotify { ChallengePeakGroup = ChallengeFlow.BuildPeakGroup(session, group), }); } await session.SendAsync(new SetChallengePeakMobLineupAvatarScRsp { Retcode = 0 }); } public static async Task OnStartChallengePeak(PlayerSession session, StartChallengePeakCsReq request) { var world = session.World; if (!world.Data.ChallengePeaks.TryGetValue(request.PeakId, out var peak)) { session.Logger.LogWarning("unknown AA stage {Id}", request.PeakId); await session.SendAsync(new StartChallengePeakScRsp { Retcode = (uint)Retcode.RetChallengeNotExist }); return; } world.Data.ChallengePeakBosses.TryGetValue(peak.ID, out var boss); var lineup = request.PeakAvatarIdList.Where(session.OwnsBase).ToList(); if (lineup.Count == 0) { lineup = session.Player.PeakLineups.GetValueOrDefault(peak.ID)?.Where(session.OwnsBase).ToList() ?? []; } if (lineup.Count == 0) { lineup = session.DefaultLineup(); } if (!session.Challenge.StartPeak(world.Data, peak, boss, request.BossBuffId, lineup)) { session.Logger.LogWarning("AA stage {Id} has no usable scene", peak.ID); await session.SendAsync(new StartChallengePeakScRsp { Retcode = (uint)Retcode.RetChallengeNotExist }); return; } session.Battles.Quit(); var state = session.Challenge; var book = session.Player.Lineups; book.ClearExtra(); book.SetExtra(ExtraLineupType.LineupChallenge, state.Lineups[0], activate: false); await ChallengeFlow.ActivateNodeAsync(session); session.Logger.LogInformation( "AA stage {Id}{Hard}: stage {Stage}, floor {Floor}, monster {Monster}, tags [{Tags}]", state.ChallengeId, state.HardMode && boss is not null ? " (hard)" : string.Empty, state.StageId, state.FloorId, state.MonsterId, string.Join(", ", state.Blessings)); // AA delivers the scene out of band; the response itself is only a retcode await session.EnterChallengeSceneAsync(); await session.SendAsync(new StartChallengePeakScRsp { Retcode = 0 }); await session.SyncLineupAsync(); } public static async Task OnReStartChallengePeak(PlayerSession session, ReStartChallengePeakCsReq request) { var state = session.Challenge; if (!state.Active || !state.IsPeak || state.Peak is null) { await session.SendAsync(new ReStartChallengePeakScRsp { Retcode = (uint)Retcode.RetChallengeNotDoing }); return; } session.Battles.Quit(); state.StartPeak(session.World.Data, state.Peak, state.Boss, state.SelectedBuffs[0], state.Lineup.ToList()); await ChallengeFlow.ActivateNodeAsync(session); await session.EnterChallengeSceneAsync(); await session.SendAsync(new ReStartChallengePeakScRsp { Retcode = 0 }); await session.SyncLineupAsync(); } public static async Task OnLeaveChallengePeak(PlayerSession session, LeaveChallengePeakCsReq request) { await session.SendAsync(new LeaveChallengePeakScRsp { Retcode = 0 }); await LeaveAsync(session); } public static Task OnConfirmChallengePeakSettle(PlayerSession session, ConfirmChallengePeakSettleCsReq request) => session.SendAsync(new ConfirmChallengePeakSettleScRsp { Retcode = 0, PeakId = request.PeakId, IOBCIMIPEOM = request.IOBCIMIPEOM, }); public static Task OnTakeChallengePeakReward(PlayerSession session, TakeChallengePeakRewardCsReq request) => session.SendAsync(new TakeChallengePeakRewardScRsp { Retcode = 0, PeakGroupId = request.PeakGroupId }); // back to the overworld the player left from private static Task LeaveAsync(PlayerSession session) { if (!session.Challenge.Active && !session.Scene.IsChallenge) { return Task.CompletedTask; } var player = session.Player; return session.EnterWorldSceneAsync(player.EntryId, player.TeleportId); } private static List Lineup(IEnumerable identifiers, IEnumerable plain) { var ids = identifiers.Select(a => a.Id).Where(id => id != 0).Distinct().ToList(); return ids.Count > 0 ? ids : [.. plain.Where(id => id != 0).Distinct()]; } }