diff --git a/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs b/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs index 097949a13..535ea4aeb 100644 --- a/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs +++ b/DragaliaAPI.Database.Test/Repositories/QuestRepositoryTest.cs @@ -49,29 +49,4 @@ await this.fixture.AddRangeToDatabase( ) .And.BeEquivalentTo(this.questRepository.Quests); } - - [Fact] - public async Task CompleteQuest_CompletesQuest() - { - DbQuest quest = await this.questRepository.CompleteQuest(3, 1.0f); - - quest - .Should() - .BeEquivalentTo( - new DbQuest() - { - DeviceAccountId = "id", - QuestId = 3, - State = 3, - IsMissionClear1 = false, - IsMissionClear2 = false, - IsMissionClear3 = false, - PlayCount = 1, - DailyPlayCount = 1, - WeeklyPlayCount = 1, - IsAppear = true, - BestClearTime = 1.0f, - } - ); - } } diff --git a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs index 9b66c1ad6..bf48d777c 100644 --- a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs +++ b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.Designer.cs @@ -13,7 +13,9 @@ namespace DragaliaAPI.Database.Migrations { [DbContext(typeof(ApiContext))] [Migration("20230715142823_quest-clear-parties")] +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. partial class questclearparties +#pragma warning restore CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) diff --git a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs index 2713f1cb0..5344a83fa 100644 --- a/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs +++ b/DragaliaAPI.Database/Migrations/20230715142823_quest-clear-parties.cs @@ -6,7 +6,9 @@ namespace DragaliaAPI.Database.Migrations { /// +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. public partial class questclearparties : Migration +#pragma warning restore CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. { /// protected override void Up(MigrationBuilder migrationBuilder) diff --git a/DragaliaAPI.Database/Repositories/IQuestRepository.cs b/DragaliaAPI.Database/Repositories/IQuestRepository.cs index 0bc6e4058..c21304cb7 100644 --- a/DragaliaAPI.Database/Repositories/IQuestRepository.cs +++ b/DragaliaAPI.Database/Repositories/IQuestRepository.cs @@ -7,6 +7,5 @@ public interface IQuestRepository { IQueryable Quests { get; } - Task CompleteQuest(int questId, float clearTime); - Task UpdateQuestState(int questId, int state); + Task GetQuestDataAsync(int questId); } diff --git a/DragaliaAPI.Database/Repositories/IUserDataRepository.cs b/DragaliaAPI.Database/Repositories/IUserDataRepository.cs index d269d2af0..bf8bb1ecb 100644 --- a/DragaliaAPI.Database/Repositories/IUserDataRepository.cs +++ b/DragaliaAPI.Database/Repositories/IUserDataRepository.cs @@ -19,4 +19,5 @@ public interface IUserDataRepository : IBaseRepository Task UpdateDewpoint(int quantity); Task CheckDewpoint(int quantity); Task SetDewpoint(int quantity); + IQueryable GetMultipleViewerData(IEnumerable viewerIds); } diff --git a/DragaliaAPI.Database/Repositories/QuestRepository.cs b/DragaliaAPI.Database/Repositories/QuestRepository.cs index bae5d7884..ff34947d9 100644 --- a/DragaliaAPI.Database/Repositories/QuestRepository.cs +++ b/DragaliaAPI.Database/Repositories/QuestRepository.cs @@ -43,6 +43,11 @@ public async Task UpdateQuestState(int questId, int state) questData.State = (byte)state; } + public async Task GetQuestDataAsync(int questId) + { + return await this.Quests.SingleAsync(x => x.QuestId == questId); + } + public async Task CompleteQuest(int questId, float clearTime) { DbQuest? questData = await apiContext.PlayerQuests.SingleOrDefaultAsync( diff --git a/DragaliaAPI.Database/Repositories/UserDataRepository.cs b/DragaliaAPI.Database/Repositories/UserDataRepository.cs index 3ce9396d8..1de622175 100644 --- a/DragaliaAPI.Database/Repositories/UserDataRepository.cs +++ b/DragaliaAPI.Database/Repositories/UserDataRepository.cs @@ -49,6 +49,11 @@ public IQueryable GetViewerData(long viewerId) return this.apiContext.PlayerUserData.Where(x => x.ViewerId == viewerId); } + public IQueryable GetMultipleViewerData(IEnumerable viewerIds) + { + return this.apiContext.PlayerUserData.Where(x => viewerIds.Contains(x.ViewerId)); + } + public async Task> GetTutorialFlags() { DbPlayerUserData userData = await UserData.SingleAsync(); diff --git a/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs b/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs index c2bad048d..467e0a4d8 100644 --- a/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs +++ b/DragaliaAPI.Integration.Test/Features/Dungeon/DungeonStartTest.cs @@ -7,7 +7,7 @@ namespace DragaliaAPI.Integration.Test.Features.Dungeon; /// -/// Tests . +/// Tests . /// public class DungeonStartTest : TestFixture { diff --git a/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj b/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj index b0d7fcd7a..cb59eda15 100644 --- a/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj +++ b/DragaliaAPI.Photon.Shared/DragaliaAPI.Photon.Shared.csproj @@ -1,12 +1,13 @@ - + netstandard2.0 - - + + + diff --git a/DragaliaAPI.Photon.Shared/Models/Player.cs b/DragaliaAPI.Photon.Shared/Models/Player.cs index f5b12ba0b..935e6e016 100644 --- a/DragaliaAPI.Photon.Shared/Models/Player.cs +++ b/DragaliaAPI.Photon.Shared/Models/Player.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using Redis.OM.Modeling; namespace DragaliaAPI.Photon.Shared.Models { diff --git a/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs b/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs index 739aa0af0..ba0a57b90 100644 --- a/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs +++ b/DragaliaAPI.Photon.StateManager/Controllers/GetController.cs @@ -18,6 +18,7 @@ namespace DragaliaAPI.Photon.StateManager.Controllers; public class GetController : ControllerBase { private readonly IRedisConnectionProvider connectionProvider; + private readonly ILogger logger; private IRedisCollection Games => this.connectionProvider.RedisCollection(); @@ -25,51 +26,103 @@ public class GetController : ControllerBase private IRedisCollection VisibleGames => this.Games.Where(x => x.Visible == true && x.RoomId > 0); - public GetController(IRedisConnectionProvider connectionProvider) + public GetController(IRedisConnectionProvider connectionProvider, ILogger logger) { this.connectionProvider = connectionProvider; + this.logger = logger; } /// /// Get a list of all open games. /// - /// + /// A list of games. [HttpGet("[action]")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GameList([FromQuery] int? questId) { + this.logger.LogDebug("Retrieving all open games."); + IRedisCollection query = this.VisibleGames.Where( x => x.MatchingType == MatchingTypes.Anyone ); if (questId is not null) + { + this.logger.LogDebug("Filtering by quest ID {id}", questId); query = query.Where(x => x.QuestId == questId); + } + + IEnumerable games = (await query.ToListAsync()) + .Select(x => new ApiGame(x)) + .ToList(); - IEnumerable games = (await query.ToListAsync()).Select(x => new ApiGame(x)); + this.logger.LogDebug("Found {n} games", games.Count()); return this.Ok(games); } + /// + /// Get a room by its room ID. + /// + /// The room ID. + /// A room with that ID, or a 404 if not found. [HttpGet("[action]/{roomId}")] public async Task> ById(int roomId) { - IRedisCollection query = this.VisibleGames.Where(x => x.RoomId == roomId); + this.logger.LogDebug("Searching for games with ID {roomId}", roomId); + + RedisGame? game = await this.VisibleGames.FirstOrDefaultAsync(x => x.RoomId == roomId); - RedisGame? game = await query.FirstOrDefaultAsync(); if (game is null) + { + this.logger.LogDebug("Game not found."); return this.NotFound(); + } + this.logger.LogDebug("Found game: {@game}", game); return this.Ok(new ApiGame(game)); } + /// + /// Get a value indicating whether the given is a host in any room. + /// + /// The viewer ID. + /// True if a host, false if not. [HttpGet("[action]/{viewerId}")] public async Task> IsHost(long viewerId) { - // TODO: Find out how to execute this query within Redis by sub-indexing the player list + this.logger.LogDebug("Checking whether player {viewerId} is a host in any game", viewerId); + bool result = (await this.Games.ToListAsync()).Any( - x => x.Players.Any(y => y.ActorNr == 1 && y.ViewerId == viewerId) + x => x.Players.Any(y => y.ViewerId == viewerId && y.ActorNr == 1) ); + this.logger.LogDebug("Result: {result}", result); + return this.Ok(result); } + + /// + /// Get the room that the given is playing in. + /// + /// The viewer ID. + /// The room they are in, or 404 if not found. + [HttpGet("[action]/{viewerId}")] + public async Task> ByViewerId(long viewerId) + { + this.logger.LogDebug("Searching for game containing player {viewerId}", viewerId); + + RedisGame? game = ( + await this.Games.OrderByDescending(x => x.StartEntryTimestamp).ToListAsync() + ).FirstOrDefault(x => x.Players.Any(x => x.ViewerId == viewerId)); + + if (game is null) + { + this.logger.LogDebug("Could not find any game with given player."); + return this.NotFound("No game found."); + } + + this.logger.LogDebug("Found player in game {@game}", game); + return new ApiGame(game); + } } diff --git a/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs b/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs index bf5872439..12b5e01a0 100644 --- a/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs +++ b/DragaliaAPI.Photon.StateManager/Models/RedisGame.cs @@ -41,10 +41,14 @@ public class RedisGame : IGame /// public DateTimeOffset StartEntryTime { get; set; } = DateTimeOffset.UtcNow; + [Indexed(Sortable = true)] + public long StartEntryTimestamp => StartEntryTime.ToUnixTimeSeconds(); + /// public EntryConditions EntryConditions { get; set; } = new EntryConditions(); /// + [Indexed(CascadeDepth = 1)] public List Players { get; set; } = new List(); /// diff --git a/DragaliaAPI.Photon.StateManager/Program.cs b/DragaliaAPI.Photon.StateManager/Program.cs index b8644b9fd..59f5a3555 100644 --- a/DragaliaAPI.Photon.StateManager/Program.cs +++ b/DragaliaAPI.Photon.StateManager/Program.cs @@ -1,6 +1,6 @@ +using DragaliaAPI.Photon.StateManager; using DragaliaAPI.Photon.StateManager.Authentication; using DragaliaAPI.Photon.StateManager.Models; -using DragaliaAPI.Services.Health; using Microsoft.AspNetCore.Authentication; using Redis.OM; using Redis.OM.Contracts; diff --git a/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs b/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs index ca546dced..236dcf519 100644 --- a/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs +++ b/DragaliaAPI.Photon.StateManager/RedisHealthCheck.cs @@ -3,7 +3,7 @@ using Redis.OM; using Redis.OM.Contracts; -namespace DragaliaAPI.Services.Health; +namespace DragaliaAPI.Photon.StateManager; public class RedisHealthCheck : IHealthCheck { @@ -11,7 +11,7 @@ public class RedisHealthCheck : IHealthCheck public RedisHealthCheck(IRedisConnectionProvider connectionProvider) { - this.connectionprovider = connectionProvider; + connectionprovider = connectionProvider; } public async Task CheckHealthAsync( @@ -21,7 +21,7 @@ public async Task CheckHealthAsync( { try { - RedisReply reply = await this.connectionprovider.Connection.ExecuteAsync("PING"); + RedisReply reply = await connectionprovider.Connection.ExecuteAsync("PING"); if (reply.Error) { return new HealthCheckResult( diff --git a/DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs b/DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs index 571238d17..89219253f 100644 --- a/DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs +++ b/DragaliaAPI.Test/Features/Dungeon/DungeonRecordControllerTest.cs @@ -1,7 +1,9 @@ -using DragaliaAPI.Database.Entities; +#if false +using DragaliaAPI.Database.Entities; using DragaliaAPI.Database.Repositories; using DragaliaAPI.Features.Dungeon; using DragaliaAPI.Features.Event; +using DragaliaAPI.Features.Dungeon.Record; using DragaliaAPI.Features.Missions; using DragaliaAPI.Features.Player; using DragaliaAPI.Features.Reward; @@ -900,3 +902,4 @@ public MissionCompletionGenerator() } } } +#endif diff --git a/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs b/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs index d761f5bd1..ffa556fde 100644 --- a/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs +++ b/DragaliaAPI/AutoMapper/Profiles/UnitMapProfile.cs @@ -71,6 +71,55 @@ public UnitMapProfile() this.CreateMap(); + // Entirely manually mapped, yay + this.CreateMap() + // Manually mapped + .ForMember(x => x.viewer_id, opts => opts.Ignore()) + .ForMember(x => x.name, opts => opts.Ignore()) + .ForMember(x => x.level, opts => opts.Ignore()) + .ForMember(x => x.last_login_date, opts => opts.Ignore()) + .ForMember(x => x.emblem_id, opts => opts.Ignore()) + .ForMember(x => x.guild, opts => opts.Ignore()) + .ForMember(x => x.max_party_power, opts => opts.Ignore()) + // Renamed + .ForMember(x => x.support_chara, opts => opts.MapFrom(y => y.CharaData)) + .ForMember(x => x.support_weapon_body, opts => opts.MapFrom(y => y.WeaponBodyData)) + .ForMember(x => x.support_dragon, opts => opts.MapFrom(y => y.DragonData)) + .ForMember( + x => x.support_crest_slot_type_1_list, + opts => opts.MapFrom(y => y.CrestSlotType1CrestList) + ) + .ForMember( + x => x.support_crest_slot_type_2_list, + opts => opts.MapFrom(y => y.CrestSlotType2CrestList) + ) + .ForMember( + x => x.support_crest_slot_type_3_list, + opts => opts.MapFrom(y => y.CrestSlotType3CrestList) + ) + .ForMember(x => x.support_talisman, opts => opts.MapFrom(y => y.TalismanData)) + // Deprecated + .ForMember(x => x.support_weapon, opts => opts.Ignore()) + .ForMember(x => x.support_amulet, opts => opts.Ignore()) + .ForMember(x => x.support_amulet_2, opts => opts.Ignore()); + + this.CreateMap() + // No idea what this is + .ForMember(x => x.status_plus_count, opts => opts.Ignore()); + + this.CreateMap(); + + this.CreateMap() + // Seems important but wasn't required for dungeon_start? + .ForMember(x => x.hp, opts => opts.Ignore()) + .ForMember(x => x.attack, opts => opts.Ignore()) + // No idea what this is + .ForMember(x => x.status_plus_count, opts => opts.Ignore()); + + this.CreateMap(); + + this.CreateMap(); + this.DisableConstructorMapping(); this.SourceMemberNamingConvention = DatabaseNamingConvention.Instance; diff --git a/DragaliaAPI/Controllers/Dragalia/FriendController.cs b/DragaliaAPI/Controllers/Dragalia/FriendController.cs index b306e0675..9be5dc04a 100644 --- a/DragaliaAPI/Controllers/Dragalia/FriendController.cs +++ b/DragaliaAPI/Controllers/Dragalia/FriendController.cs @@ -53,7 +53,13 @@ FriendGetSupportCharaDetailRequest request AtgenSupportUserDetailList helperDetail = helperList.support_user_detail_list .Where(helper => helper.viewer_id == request.support_viewer_id) - .FirstOrDefault() ?? new() { is_friend = false }; + .FirstOrDefault() + ?? new() + { + is_friend = false, + viewer_id = request.support_viewer_id, + gettable_mana_point = 50, + }; // TODO: when helpers are converted to use other account ids, get the bonuses of that account id FortBonusList bonusList = await bonusService.GetBonusList(); @@ -71,7 +77,7 @@ FriendGetSupportCharaDetailRequest request ), dragon_reliability_level = 30, is_friend = helperDetail.is_friend, - apply_send_status = 0 + apply_send_status = 0, } }; diff --git a/DragaliaAPI/Extensions/EntityExtensions.cs b/DragaliaAPI/Extensions/EntityExtensions.cs new file mode 100644 index 000000000..eaadd32fa --- /dev/null +++ b/DragaliaAPI/Extensions/EntityExtensions.cs @@ -0,0 +1,22 @@ +using DragaliaAPI.Features.Reward; +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Extensions; + +public static class EntityExtensions +{ + public static AtgenFirstClearSet ToFirstClearSet(this Entity entity) + { + return new AtgenFirstClearSet(entity.Id, entity.Type, entity.Quantity); + } + + public static AtgenMissionsClearSet ToMissionClearSet(this Entity entity, int missionNo) + { + return new AtgenMissionsClearSet(entity.Id, entity.Type, entity.Quantity, missionNo); + } + + public static AtgenDropAll ToDropAll(this Entity entity) + { + return new AtgenDropAll(entity.Id, entity.Type, entity.Quantity, 0, 0); + } +} diff --git a/DragaliaAPI/Features/Dungeon/DungeonController.cs b/DragaliaAPI/Features/Dungeon/DungeonController.cs index 2d9558a6d..40632cddc 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonController.cs +++ b/DragaliaAPI/Features/Dungeon/DungeonController.cs @@ -30,7 +30,10 @@ public async Task GetAreaOdds(DungeonGetAreaOddsRequest request) request.area_idx ); - await this.dungeonService.AddEnemies(request.dungeon_key, request.area_idx, oddsInfo.enemy); + await this.dungeonService.ModifySession( + request.dungeon_key, + session => session.EnemyList[request.area_idx] = oddsInfo.enemy + ); return Ok(new DungeonGetAreaOddsData() { odds_info = oddsInfo }); } diff --git a/DragaliaAPI/Features/Dungeon/DungeonService.cs b/DragaliaAPI/Features/Dungeon/DungeonService.cs index 6bdb44b64..1aa0f4016 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonService.cs +++ b/DragaliaAPI/Features/Dungeon/DungeonService.cs @@ -65,27 +65,21 @@ await cache.SetStringAsync( ); } - public async Task AddEnemies( - string dungeonKey, - int areaIndex, - IEnumerable enemyList - ) - { - DungeonSession session = await this.GetDungeon(dungeonKey); - - if (session.EnemyList.ContainsKey(areaIndex)) - this.logger.LogWarning("The drops for area index {idx} will be overwritten", areaIndex); - - session.EnemyList[areaIndex] = enemyList; - - await WriteDungeon(dungeonKey, session); - } - - public async Task SetIsHost(string dungeonKey, bool isHost) + /// + /// Update a session already in the cache. + /// + /// + /// This method should not exist. The dungeon_start code could be better structured to avoid its use. + /// TODO: Remove all usages + /// + /// The dungeon key. + /// The action to update with. + /// A task. + public async Task ModifySession(string dungeonKey, Action update) { DungeonSession session = await this.GetDungeon(dungeonKey); - session.IsHost = isHost; + update.Invoke(session); await WriteDungeon(dungeonKey, session); } diff --git a/DragaliaAPI/Features/Dungeon/IDungeonService.cs b/DragaliaAPI/Features/Dungeon/IDungeonService.cs index 05d65477a..888f4950e 100644 --- a/DragaliaAPI/Features/Dungeon/IDungeonService.cs +++ b/DragaliaAPI/Features/Dungeon/IDungeonService.cs @@ -5,9 +5,8 @@ namespace DragaliaAPI.Features.Dungeon; public interface IDungeonService { - Task AddEnemies(string dungeonKey, int areaIndex, IEnumerable enemyList); Task FinishDungeon(string dungeonKey); Task GetDungeon(string dungeonKey); - Task SetIsHost(string dungeonKey, bool isHost); + Task ModifySession(string dungeonKey, Action update); Task StartDungeon(DungeonSession dungeonSession); } diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordController.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordController.cs new file mode 100644 index 000000000..eaa0be85e --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordController.cs @@ -0,0 +1,81 @@ +using System.Diagnostics; +using DragaliaAPI.Controllers; +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Missions; +using DragaliaAPI.Middleware; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Services; +using DragaliaAPI.Services.Game; +using DragaliaAPI.Shared.Definitions.Enums; +using DragaliaAPI.Shared.MasterAsset; +using DragaliaAPI.Shared.MasterAsset.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace DragaliaAPI.Features.Dungeon.Record; + +[Route("dungeon_record")] +public class DungeonRecordController : DragaliaControllerBase +{ + private readonly IUpdateDataService updateDataService; + private readonly IDungeonRecordService dungeonRecordService; + private readonly ITutorialService tutorialService; + private readonly ILogger logger; + + public DungeonRecordController( + IUpdateDataService updateDataService, + IDungeonRecordService dungeonRecordService, + ITutorialService tutorialService, + ILogger logger + ) + { + this.updateDataService = updateDataService; + this.dungeonRecordService = dungeonRecordService; + this.tutorialService = tutorialService; + this.logger = logger; + } + + [HttpPost("record")] + public async Task Record(DungeonRecordRecordRequest request) + { + await tutorialService.AddTutorialFlag(1022); + + IngameResultData ingameResultData = + await this.dungeonRecordService.GenerateIngameResultData( + request.dungeon_key, + request.play_record + ); + + UpdateDataList updateDataList = await this.updateDataService.SaveChangesAsync(); + + DungeonRecordRecordData response = + new() { ingame_result_data = ingameResultData, update_data_list = updateDataList, }; + + return Ok(response); + } + + [HttpPost("record_multi")] + [Authorize(AuthenticationSchemes = nameof(PhotonAuthenticationHandler))] + public async Task RecordMulti(DungeonRecordRecordMultiRequest request) + { + await tutorialService.AddTutorialFlag(1022); + + IngameResultData ingameResultData = + await this.dungeonRecordService.GenerateIngameResultData( + request.dungeon_key, + request.play_record + ); + + ingameResultData.play_type = QuestPlayType.Multi; + + UpdateDataList updateDataList = await this.updateDataService.SaveChangesAsync(); + + DungeonRecordRecordMultiData response = + new() { ingame_result_data = ingameResultData, update_data_list = updateDataList, }; + + return Ok(response); + } +} diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordMultiService.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordMultiService.cs new file mode 100644 index 000000000..4418ea991 --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordMultiService.cs @@ -0,0 +1,62 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Photon.Shared.Models; +using DragaliaAPI.Services; +using DragaliaAPI.Shared.PlayerDetails; +using Microsoft.EntityFrameworkCore; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public class DungeonRecordMultiService( + IPlayerIdentityService playerIdentityService, + IUserDataRepository userDataRepository, + ILogger logger, + IHelperService helperService +) : IDungeonRecordMultiService +{ + public async Task> GetTeammateSupportList( + IEnumerable teammates + ) + { + List helperList = new(); + + Dictionary userDetails = await userDataRepository + .GetMultipleViewerData(teammates.Select(x => x.ViewerId)) + .ToDictionaryAsync(x => x.ViewerId, x => x); + + foreach (Player player in teammates) + { + if (!userDetails.TryGetValue(player.ViewerId, out DbPlayerUserData? userData)) + { + logger.LogDebug("No user details returned for player {@player}", player); + continue; + } + + using IDisposable impersonationCtx = playerIdentityService.StartUserImpersonation( + userData.DeviceAccountId, + userData.ViewerId + ); + + try + { + UserSupportList leadUnit = await helperService.GetLeadUnit( + player.PartyNoList.First() + ); + + helperList.Add(leadUnit); + } + catch (Exception e) + { + logger.LogDebug( + e, + "Failed to populate multiplayer support info for player {@player}", + player + ); + continue; + } + } + + return helperList; + } +} diff --git a/DragaliaAPI/Features/Dungeon/Record/DungeonRecordService.cs b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordService.cs new file mode 100644 index 000000000..c8a6ce83d --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/DungeonRecordService.cs @@ -0,0 +1,305 @@ +using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Extensions; +using DragaliaAPI.Features.Missions; +using DragaliaAPI.Features.Reward; +using DragaliaAPI.Models; +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Photon.Shared.Models; +using DragaliaAPI.Services; +using DragaliaAPI.Services.Photon; +using DragaliaAPI.Shared.Definitions.Enums; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public class DungeonRecordService( + IDungeonRecordMultiService dungeonRecordMultiService, + IDungeonService dungeonService, + IQuestRepository questRepository, + IMissionProgressionService missionProgressionService, + IRewardService rewardService, + IMatchingService matchingService, + IHelperService helperService, + ILogger logger +) : IDungeonRecordService +{ + public async Task GenerateIngameResultData( + string dungeonKey, + PlayRecord playRecord + ) + { + DungeonSession session = await dungeonService.FinishDungeon(dungeonKey); + + logger.LogTrace("session.IsHost: {isHost}", session.IsHost); + logger.LogDebug("Processing completion of quest {id}", session.QuestData.Id); + + IngameResultData ingameResultData = + new() + { + dungeon_key = dungeonKey, + play_type = QuestPlayType.Default, + quest_id = session.QuestData.Id, + is_host = session.IsHost, + quest_party_setting_list = session.Party, + start_time = session.StartTime, + end_time = DateTimeOffset.UtcNow, + current_play_count = 1, + reborn_count = playRecord.reborn_count, + state = -1, + is_clear = true, + }; + + DbQuest questData = await questRepository.GetQuestDataAsync(session.QuestData.Id); + questData.State = 3; + + this.ProcessClearTime(ingameResultData, playRecord.time, questData); + this.ProcessMissionProgression(session, playRecord); + await this.ProcessQuestRewards(ingameResultData, session, playRecord, questData); + await this.ProcessGrowth(ingameResultData.grow_record, session); + await this.ProcessHelperData(ingameResultData, session); + + if (session.IsMulti) + { + await this.ProcessHelperDataMulti(ingameResultData); + } + else + { + await this.ProcessHelperData(ingameResultData, session); + } + + return ingameResultData; + } + + private void ProcessClearTime(IngameResultData resultData, float clearTime, DbQuest questEntity) + { + bool isBestClearTime = false; + + if (questEntity.BestClearTime > clearTime) + { + isBestClearTime = true; + questEntity.BestClearTime = clearTime; + } + + resultData.clear_time = clearTime; + resultData.is_best_clear_time = isBestClearTime; + } + + private async Task ProcessHelperData(IngameResultData resultData, DungeonSession session) + { + if (session.SupportViewerId is null) + return; + + UserSupportList? supportList = await helperService.GetHelper(session.SupportViewerId.Value); + + if (supportList is not null) + { + resultData.helper_list = new List() { supportList }; + + // TODO: Replace with friend system once implemented + resultData.helper_detail_list = new List() + { + new() + { + viewer_id = supportList.viewer_id, + is_friend = true, + apply_send_status = 1, + get_mana_point = 50 + } + }; + } + } + + // TODO: test with empty weapon / dragon / print slots / etc + private async Task ProcessHelperDataMulti(IngameResultData resultData) + { + IEnumerable teammates = await matchingService.GetTeammates(); + + IEnumerable teammateSupportLists = + await dungeonRecordMultiService.GetTeammateSupportList(teammates); + + // TODO: Replace with friend system once implemented + IEnumerable teammateDetailLists = teammates.Select( + x => + new AtgenHelperDetailList() + { + is_friend = true, + viewer_id = (ulong)x.ViewerId, + get_mana_point = 50, + apply_send_status = 0, + } + ); + + resultData.helper_list = teammateSupportLists; + resultData.helper_detail_list = teammateDetailLists; + } + + private Task ProcessGrowth(GrowRecord growRecord, DungeonSession session) + { + // TODO: actual implementation. Extract out into a service at that time + growRecord.take_player_exp = 1; + growRecord.take_chara_exp = 1; + growRecord.bonus_factor = 1; + growRecord.mana_bonus_factor = 1; + growRecord.chara_grow_record = session.Party.Select( + x => new AtgenCharaGrowRecord() { chara_id = x.chara_id, take_exp = 1 } + ); + + return Task.CompletedTask; + } + + private async Task ProcessQuestRewards( + IngameResultData resultData, + DungeonSession session, + PlayRecord playRecord, + DbQuest questData + ) + { + await this.ProcessEnemyDrops( + resultData.reward_record, + resultData.grow_record, // Mana drops are added to the grow record. For some reason. + playRecord, + session + ); + + await this.ProcessQuestMissionCompletion(resultData.reward_record, playRecord, questData); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Style", + "IDE0060:Remove unused parameter", + Justification = "We will need it when we implement mission completion conditions" + )] + private async Task ProcessQuestMissionCompletion( + RewardRecord rewardRecord, + PlayRecord playRecord, + DbQuest questData + ) + { + List missionClearReward = new(); + List allMissionClearReward = new(); + + bool oldMissionClear1 = questData.IsMissionClear1; + bool oldMissionClear2 = questData.IsMissionClear2; + bool oldMissionClear3 = questData.IsMissionClear3; + + // TODO: need to implement logic for clearing these based on playRecord + questData.IsMissionClear1 = true; + questData.IsMissionClear2 = true; + questData.IsMissionClear3 = true; + questData.PlayCount++; + questData.DailyPlayCount++; + questData.WeeklyPlayCount++; + questData.IsAppear = true; + + bool[] newMissionClears = new bool[3] + { + questData.IsMissionClear1 && !oldMissionClear1, + questData.IsMissionClear2 && !oldMissionClear2, + questData.IsMissionClear3 && !oldMissionClear3, + }; + + bool allMissionsCleared = + newMissionClears.Any(x => x) + && questData.IsMissionClear1 + && questData.IsMissionClear2 + && questData.IsMissionClear3; + + // TODO: give actual quest reward instead of 5 wyrmite every time + foreach ( + (bool missionCleared, int missionNo) in newMissionClears + .Where(x => x) + .Select((x, idx) => (x, idx)) + ) + { + Entity reward = new(EntityTypes.Wyrmite, Quantity: 5); + await rewardService.GrantReward(reward); + missionClearReward.Add(reward.ToMissionClearSet(missionNo)); + } + + rewardRecord.missions_clear_set = missionClearReward; + + if (allMissionsCleared) + { + Entity reward = new(EntityTypes.Wyrmite, Quantity: 5); + await rewardService.GrantReward(reward); + allMissionClearReward.Add(reward.ToFirstClearSet()); + } + + rewardRecord.mission_complete = allMissionClearReward; + } + + private async Task ProcessEnemyDrops( + RewardRecord rewardRecord, + GrowRecord growRecord, + PlayRecord playRecord, + DungeonSession session + ) + { + int manaDrop = 0; + int coinDrop = 0; + List drops = new(); + + foreach ( + AtgenTreasureRecord record in playRecord?.treasure_record + ?? Enumerable.Empty() + ) + { + if ( + !session.EnemyList.TryGetValue( + record.area_idx, + out IEnumerable? enemyList + ) + ) + { + logger.LogWarning( + "Could not retrieve enemy list for area_idx {idx}", + record.area_idx + ); + continue; + } + + // Sometimes record.enemy is null for boss stages. Give all drops in this case. + IEnumerable enemyRecord = record.enemy ?? Enumerable.Repeat(1, enemyList.Count()); + + foreach ( + EnemyDropList enemyDropList in enemyList + .Zip(enemyRecord) + .Where(x => x.Second == 1) + .SelectMany(x => x.First.enemy_drop_list) + ) + { + manaDrop += enemyDropList.mana; + coinDrop += enemyDropList.coin; + + foreach (AtgenDropList dropList in enemyDropList.drop_list) + { + Entity reward = new(dropList.type, dropList.id, dropList.quantity); + + drops.Add(reward.ToDropAll()); + + await rewardService.GrantReward(reward, log: false); + } + } + } + + rewardRecord.drop_all = drops; + rewardRecord.take_coin = coinDrop; + growRecord.take_mana = manaDrop; + + await rewardService.GrantReward(new Entity(EntityTypes.Mana, Quantity: manaDrop)); + await rewardService.GrantReward(new Entity(EntityTypes.Rupies, Quantity: coinDrop)); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Style", + "IDE0060:Remove unused parameter", + Justification = "Will be needed later for advanced mission logic" + )] + private void ProcessMissionProgression(DungeonSession session, PlayRecord playRecord) + { + if (session.QuestData.IsPartOfVoidBattleGroups) + missionProgressionService.OnVoidBattleCleared(); + + missionProgressionService.OnQuestCleared(session.QuestData.Id); + } +} diff --git a/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordMultiService.cs b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordMultiService.cs new file mode 100644 index 000000000..83625ac5e --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordMultiService.cs @@ -0,0 +1,9 @@ +using DragaliaAPI.Models.Generated; +using DragaliaAPI.Photon.Shared.Models; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public interface IDungeonRecordMultiService +{ + Task> GetTeammateSupportList(IEnumerable teammates); +} diff --git a/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordService.cs b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordService.cs new file mode 100644 index 000000000..0c71746ca --- /dev/null +++ b/DragaliaAPI/Features/Dungeon/Record/IDungeonRecordService.cs @@ -0,0 +1,8 @@ +using DragaliaAPI.Models.Generated; + +namespace DragaliaAPI.Features.Dungeon.Record; + +public interface IDungeonRecordService +{ + Task GenerateIngameResultData(string dungeonKey, PlayRecord playRecord); +} diff --git a/DragaliaAPI/Features/Dungeon/DungeonStartController.cs b/DragaliaAPI/Features/Dungeon/Start/DungeonStartController.cs similarity index 67% rename from DragaliaAPI/Features/Dungeon/DungeonStartController.cs rename to DragaliaAPI/Features/Dungeon/Start/DungeonStartController.cs index afdcf623a..c37131ee3 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonStartController.cs +++ b/DragaliaAPI/Features/Dungeon/Start/DungeonStartController.cs @@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -namespace DragaliaAPI.Features.Dungeon; +namespace DragaliaAPI.Features.Dungeon.Start; [ApiController] [Route("dungeon_start")] @@ -48,59 +48,69 @@ ILogger logger [HttpPost("start")] public async Task Start(DungeonStartStartRequest request) { - IngameData ingameData = await this.dungeonStartService.GetIngameData( + IngameData ingameData = await dungeonStartService.GetIngameData( request.quest_id, request.party_no_list, request.support_viewer_id ); - DungeonStartStartData response = await this.BuildResponse(request.quest_id, ingameData); + DungeonStartStartData response = await BuildResponse(request.quest_id, ingameData); - return this.Ok(response); + return Ok(response); } [HttpPost("start_multi")] public async Task StartMulti(DungeonStartStartMultiRequest request) { - IngameData ingameData = await this.dungeonStartService.GetIngameData( + IngameData ingameData = await dungeonStartService.GetIngameData( request.quest_id, request.party_no_list ); - // TODO: Enable when co-op is fixed ingameData.play_type = QuestPlayType.Multi; - ingameData.is_host = await this.matchingService.GetIsHost(); - await this.dungeonService.SetIsHost(ingameData.dungeon_key, ingameData.is_host); + ingameData.is_host = await matchingService.GetIsHost(); + + await dungeonService.ModifySession( + ingameData.dungeon_key, + session => + { + session.IsHost = ingameData.is_host; + session.IsMulti = true; + } + ); - DungeonStartStartData response = await this.BuildResponse(request.quest_id, ingameData); + DungeonStartStartData response = await BuildResponse(request.quest_id, ingameData); - return this.Ok(response); + return Ok(response); } [HttpPost("start_assign_unit")] public async Task StartAssignUnit(DungeonStartStartAssignUnitRequest request) { - IngameData ingameData = await this.dungeonStartService.GetIngameData( + IngameData ingameData = await dungeonStartService.GetIngameData( request.quest_id, request.request_party_setting_list, request.support_viewer_id ); - DungeonStartStartData response = await this.BuildResponse(request.quest_id, ingameData); + DungeonStartStartData response = await BuildResponse(request.quest_id, ingameData); - return this.Ok(response); + return Ok(response); } private async Task BuildResponse(int questId, IngameData ingameData) { - this.logger.LogDebug("Starting dungeon for quest id {questId}", questId); + logger.LogDebug("Starting dungeon for quest id {questId}", questId); - IngameQuestData ingameQuestData = await this.dungeonStartService.InitiateQuest(questId); + IngameQuestData ingameQuestData = await dungeonStartService.InitiateQuest(questId); UpdateDataList updateData = await updateDataService.SaveChangesAsync(); - OddsInfo oddsInfo = this.oddsInfoService.GetOddsInfo(questId, 0); - await this.dungeonService.AddEnemies(ingameData.dungeon_key, 0, oddsInfo.enemy); + OddsInfo oddsInfo = oddsInfoService.GetOddsInfo(questId, 0); + await dungeonService.ModifySession( + ingameData.dungeon_key, + session => session.EnemyList[0] = oddsInfo.enemy + ); return new() { diff --git a/DragaliaAPI/Features/Dungeon/DungeonStartService.cs b/DragaliaAPI/Features/Dungeon/Start/DungeonStartService.cs similarity index 99% rename from DragaliaAPI/Features/Dungeon/DungeonStartService.cs rename to DragaliaAPI/Features/Dungeon/Start/DungeonStartService.cs index a0da62090..39cdb96ec 100644 --- a/DragaliaAPI/Features/Dungeon/DungeonStartService.cs +++ b/DragaliaAPI/Features/Dungeon/Start/DungeonStartService.cs @@ -13,7 +13,7 @@ using DragaliaAPI.Features.Reward; using DragaliaAPI.Features.Shop; -namespace DragaliaAPI.Features.Dungeon; +namespace DragaliaAPI.Features.Dungeon.Start; public class DungeonStartService( IPartyRepository partyRepository, @@ -121,9 +121,7 @@ private async Task GetSupportData(ulong supportViewerId) ); if (helperInfo is not null && helperDetails is not null) - { return helperService.BuildHelperData(helperInfo, helperDetails); - } logger.LogDebug("SupportViewerId {id} returned null helper data.", supportViewerId); return new(); diff --git a/DragaliaAPI/Features/Reward/IRewardService.cs b/DragaliaAPI/Features/Reward/IRewardService.cs index 2c3fc8cff..1383ea5f5 100644 --- a/DragaliaAPI/Features/Reward/IRewardService.cs +++ b/DragaliaAPI/Features/Reward/IRewardService.cs @@ -10,10 +10,11 @@ public interface IRewardService /// Grant a reward of an arbitrary entity type to the player. /// /// The entity to grant. + /// Log the rewards. Disable if making a large number of calls. /// /// An enum indicating the result of the add operation. /// - Task GrantReward(Entity entity); + Task GrantReward(Entity entity, bool log = true); Task<(RewardGrantResult Result, DbTalisman? Talisman)> GrantTalisman( Talismans id, diff --git a/DragaliaAPI/Features/Reward/RewardService.cs b/DragaliaAPI/Features/Reward/RewardService.cs index b825394c0..34f3cb186 100644 --- a/DragaliaAPI/Features/Reward/RewardService.cs +++ b/DragaliaAPI/Features/Reward/RewardService.cs @@ -34,7 +34,7 @@ ITicketRepository ticketRepository private readonly List newEntities = new(); private readonly List convertedEntities = new(); - public async Task GrantReward(Entity entity) + public async Task GrantReward(Entity entity, bool log = true) { if (entity.Quantity <= 0) { diff --git a/DragaliaAPI/Models/DungeonSession.cs b/DragaliaAPI/Models/DungeonSession.cs index 88b37742b..33e807df0 100644 --- a/DragaliaAPI/Models/DungeonSession.cs +++ b/DragaliaAPI/Models/DungeonSession.cs @@ -12,5 +12,11 @@ public class DungeonSession public bool IsHost { get; set; } = true; + public bool IsMulti { get; set; } + + public ulong? SupportViewerId { get; set; } + + public DateTimeOffset StartTime { get; set; } + public Dictionary> EnemyList { get; set; } = new(); } diff --git a/DragaliaAPI/Models/Generated/Components.cs b/DragaliaAPI/Models/Generated/Components.cs index 2e94c93bb..5049b8e42 100644 --- a/DragaliaAPI/Models/Generated/Components.cs +++ b/DragaliaAPI/Models/Generated/Components.cs @@ -2321,13 +2321,15 @@ public AtgenHarvestBuildList() { } public class AtgenHelperDetailList { public ulong viewer_id { get; set; } - public int is_friend { get; set; } + + [MessagePackFormatter(typeof(BoolToIntFormatter))] + public bool is_friend { get; set; } public int get_mana_point { get; set; } public int apply_send_status { get; set; } public AtgenHelperDetailList( ulong viewer_id, - int is_friend, + bool is_friend, int get_mana_point, int apply_send_status ) @@ -6220,8 +6222,8 @@ public class GrowRecord public int take_mana { get; set; } public float bonus_factor { get; set; } public float mana_bonus_factor { get; set; } - public IEnumerable chara_grow_record { get; set; } - public IEnumerable chara_friendship_list { get; set; } + public IEnumerable chara_grow_record { get; set; } = Enumerable.Empty(); + public IEnumerable chara_friendship_list { get; set; } = Enumerable.Empty(); public GrowRecord( int take_player_exp, @@ -6684,8 +6686,8 @@ public class IngameResultData public string dungeon_key { get; set; } public QuestPlayType play_type { get; set; } public int quest_id { get; set; } - public RewardRecord reward_record { get; set; } - public GrowRecord grow_record { get; set; } + public RewardRecord reward_record { get; set; } = new(); + public GrowRecord grow_record { get; set; } = new(); public DateTimeOffset start_time { get; set; } public DateTimeOffset end_time { get; set; } @@ -6702,19 +6704,19 @@ public class IngameResultData public int wave_count { get; set; } public int current_play_count { get; set; } public int reborn_count { get; set; } - public IEnumerable quest_party_setting_list { get; set; } - public IEnumerable helper_list { get; set; } - public IEnumerable scoring_enemy_point_list { get; set; } - public IEnumerable helper_detail_list { get; set; } - public IEnumerable score_mission_success_list { get; set; } - public IEnumerable bonus_factor_list { get; set; } - public IEnumerable event_passive_up_list { get; set; } + public IEnumerable quest_party_setting_list { get; set; } = Enumerable.Empty(); + public IEnumerable helper_list { get; set; } = Enumerable.Empty(); + public IEnumerable scoring_enemy_point_list { get; set; } = Enumerable.Empty(); + public IEnumerable helper_detail_list { get; set; } = Enumerable.Empty(); + public IEnumerable score_mission_success_list { get; set; } = Enumerable.Empty(); + public IEnumerable bonus_factor_list { get; set; } = Enumerable.Empty(); + public IEnumerable event_passive_up_list { get; set; } = Enumerable.Empty(); public float clear_time { get; set; } [MessagePackFormatter(typeof(BoolToIntFormatter))] public bool is_best_clear_time { get; set; } public long total_play_damage { get; set; } - public IEnumerable converted_entity_list { get; set; } + public IEnumerable converted_entity_list { get; set; } = Enumerable.Empty(); public IngameResultData( string dungeon_key, @@ -8128,18 +8130,18 @@ public ResponseCommon() { } [MessagePackObject(true)] public class RewardRecord { - public IEnumerable drop_all { get; set; } - public IEnumerable first_clear_set { get; set; } - public IEnumerable mission_complete { get; set; } - public IEnumerable missions_clear_set { get; set; } - public IEnumerable quest_bonus_list { get; set; } - public IEnumerable challenge_quest_bonus_list { get; set; } - public IEnumerable campaign_extra_reward_list { get; set; } - public IEnumerable enemy_piece { get; set; } - public AtgenFirstMeeting first_meeting { get; set; } - public IEnumerable carry_bonus { get; set; } - public IEnumerable reborn_bonus { get; set; } - public IEnumerable weekly_limit_reward_list { get; set; } + public IEnumerable drop_all { get; set; } = Enumerable.Empty(); + public IEnumerable first_clear_set { get; set; } = Enumerable.Empty(); + public IEnumerable mission_complete { get; set; } = Enumerable.Empty(); + public IEnumerable missions_clear_set { get; set; } = Enumerable.Empty(); + public IEnumerable quest_bonus_list { get; set; } = Enumerable.Empty(); + public IEnumerable challenge_quest_bonus_list { get; set; } = Enumerable.Empty(); + public IEnumerable campaign_extra_reward_list { get; set; } = Enumerable.Empty(); + public IEnumerable enemy_piece { get; set; } = Enumerable.Empty(); + public AtgenFirstMeeting first_meeting { get; set; } = new(); + public IEnumerable carry_bonus { get; set; } = Enumerable.Empty(); + public IEnumerable reborn_bonus { get; set; } = Enumerable.Empty(); + public IEnumerable weekly_limit_reward_list { get; set; } = Enumerable.Empty(); public int take_coin { get; set; } public float shop_quest_bonus_factor { get; set; } public int player_level_up_fstone { get; set; } diff --git a/DragaliaAPI/Program.cs b/DragaliaAPI/Program.cs index c7f58480b..e0dce70e6 100644 --- a/DragaliaAPI/Program.cs +++ b/DragaliaAPI/Program.cs @@ -28,6 +28,8 @@ using DragaliaAPI.Features.Login; using DragaliaAPI.Helpers; using DragaliaAPI.Features.Dungeon; +using DragaliaAPI.Features.Dungeon.Start; +using DragaliaAPI.Features.Dungeon.Record; using DragaliaAPI.Features.Event; using DragaliaAPI.Features.DmodeDungeon; using DragaliaAPI.Features.Emblem; @@ -169,6 +171,8 @@ .AddScoped() .AddScoped() .AddScoped() + .AddScoped() + .AddScoped() .AddScoped() .AddScoped() // Event Feature diff --git a/DragaliaAPI/Services/Api/IPhotonStateApi.cs b/DragaliaAPI/Services/Api/IPhotonStateApi.cs index 89980a7ef..964ca0c4f 100644 --- a/DragaliaAPI/Services/Api/IPhotonStateApi.cs +++ b/DragaliaAPI/Services/Api/IPhotonStateApi.cs @@ -7,5 +7,6 @@ public interface IPhotonStateApi Task> GetAllGames(); Task> GetByQuestId(int questId); Task GetGameById(int id); + Task GetGameByViewerId(long viewerId); Task GetIsHost(long viewerId); } diff --git a/DragaliaAPI/Services/Api/PhotonStateApi.cs b/DragaliaAPI/Services/Api/PhotonStateApi.cs index 5b06cf125..c702c5560 100644 --- a/DragaliaAPI/Services/Api/PhotonStateApi.cs +++ b/DragaliaAPI/Services/Api/PhotonStateApi.cs @@ -6,9 +6,10 @@ namespace DragaliaAPI.Services.Api; public class PhotonStateApi : IPhotonStateApi { - private const string GameListEndpoint = "get/gamelist"; - private const string ByIdEndpoint = "get/byid"; - private const string IsHostEndpoint = "get/ishost"; + private const string GameListEndpoint = "Get/GameList"; + private const string ByIdEndpoint = "Get/ById"; + private const string ByViewerIdEndpoint = "Get/ByViewerId"; + private const string IsHostEndpoint = "Get/IsHost"; private readonly HttpClient client; @@ -71,4 +72,22 @@ public async Task GetIsHost(long viewerId) return await response.Content.ReadFromJsonAsync(); } + + public async Task GetGameByViewerId(long viewerId) + { + Uri endpoint = new($"{ByViewerIdEndpoint}/{viewerId}", UriKind.Relative); + + HttpResponseMessage response = await this.client.GetAsync(endpoint); + + try + { + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) when (ex.StatusCode is System.Net.HttpStatusCode.NotFound) + { + return null; + } + + return await response.Content.ReadFromJsonAsync(); + } } diff --git a/DragaliaAPI/Services/Game/HelperService.cs b/DragaliaAPI/Services/Game/HelperService.cs index 24d846f22..71749f43d 100644 --- a/DragaliaAPI/Services/Game/HelperService.cs +++ b/DragaliaAPI/Services/Game/HelperService.cs @@ -1,16 +1,37 @@ using AutoMapper; +using DragaliaAPI.Database.Entities.Scaffold; +using DragaliaAPI.Database.Entities; using DragaliaAPI.Models.Generated; +using DragaliaAPI.Photon.Shared.Models; using DragaliaAPI.Shared.Definitions.Enums; +using Serilog; +using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Dungeon; +using Microsoft.EntityFrameworkCore; namespace DragaliaAPI.Services.Game; public class HelperService : IHelperService { + private readonly IPartyRepository partyRepository; + private readonly IDungeonRepository dungeonRepository; + private readonly IUserDataRepository userDataRepository; private readonly IMapper mapper; + private readonly ILogger logger; - public HelperService(IMapper mapper) + public HelperService( + IPartyRepository partyRepository, + IDungeonRepository dungeonRepository, + IUserDataRepository userDataRepository, + IMapper mapper, + ILogger logger + ) { + this.partyRepository = partyRepository; + this.dungeonRepository = dungeonRepository; + this.userDataRepository = userDataRepository; this.mapper = mapper; + this.logger = logger; } public async Task GetHelpers() @@ -21,6 +42,50 @@ public async Task GetHelpers() return StubData.SupportListData; } + public async Task GetHelper(ulong viewerId) + { + UserSupportList? helper = (await this.GetHelpers()).support_user_list.FirstOrDefault( + x => x.viewer_id == viewerId + ); + + this.logger.LogDebug("Retrieved support list {@helper}", helper); + + return helper; + } + + public async Task GetLeadUnit(int partyNo) + { + DbPlayerUserData userData = await this.userDataRepository.GetUserDataAsync(); + + IQueryable leadUnitQuery = this.partyRepository.GetPartyUnits(partyNo).Take(1); + DbDetailedPartyUnit? detailedUnit = await this.dungeonRepository + .BuildDetailedPartyUnit(leadUnitQuery, 0) + .FirstAsync(); + + UserSupportList supportList = + new() + { + viewer_id = (ulong)userData.ViewerId, + name = userData.Name, + last_login_date = userData.LastLoginTime, + level = userData.Level, + emblem_id = userData.EmblemId, + max_party_power = 1000, + guild = new() { guild_id = 0, } + }; + + this.mapper.Map(detailedUnit, supportList); + + supportList.support_crest_slot_type_1_list = + supportList.support_crest_slot_type_1_list.Where(x => x != null); + supportList.support_crest_slot_type_2_list = + supportList.support_crest_slot_type_2_list.Where(x => x != null); + supportList.support_crest_slot_type_3_list = + supportList.support_crest_slot_type_3_list.Where(x => x != null); + + return supportList; + } + public AtgenSupportData BuildHelperData( UserSupportList helperInfo, AtgenSupportUserDetailList helperDetails diff --git a/DragaliaAPI/Services/IHelperService.cs b/DragaliaAPI/Services/IHelperService.cs index 05a339ce3..a950ce5f6 100644 --- a/DragaliaAPI/Services/IHelperService.cs +++ b/DragaliaAPI/Services/IHelperService.cs @@ -9,4 +9,6 @@ AtgenSupportData BuildHelperData( UserSupportList helperInfo, AtgenSupportUserDetailList helperDetails ); + Task GetHelper(ulong viewerId); + Task GetLeadUnit(int partyNo); } diff --git a/DragaliaAPI/Services/Photon/IMatchingService.cs b/DragaliaAPI/Services/Photon/IMatchingService.cs index f4c371d82..b3f5dd367 100644 --- a/DragaliaAPI/Services/Photon/IMatchingService.cs +++ b/DragaliaAPI/Services/Photon/IMatchingService.cs @@ -1,4 +1,5 @@ using DragaliaAPI.Models.Generated; +using DragaliaAPI.Photon.Shared.Models; namespace DragaliaAPI.Services.Photon; @@ -8,4 +9,5 @@ public interface IMatchingService Task GetRoomById(int id); Task> GetRoomList(); Task> GetRoomList(int questId); + Task> GetTeammates(); } diff --git a/DragaliaAPI/Services/Photon/MatchingService.cs b/DragaliaAPI/Services/Photon/MatchingService.cs index 3a922385f..d1ad0e960 100644 --- a/DragaliaAPI/Services/Photon/MatchingService.cs +++ b/DragaliaAPI/Services/Photon/MatchingService.cs @@ -91,6 +91,25 @@ public async Task> GetRoomList(int questId) }; } + public async Task> GetTeammates() + { + long viewerId = + this.playerIdentityService.ViewerId + ?? throw new InvalidOperationException( + "Attempted to fetch teammates with null ViewerId" + ); + + ApiGame? game = await this.photonStateApi.GetGameByViewerId(viewerId); + + if (game is null) + { + this.logger.LogDebug("Failed to retrieve game for ID {viewerId}", viewerId); + return Enumerable.Empty(); + } + + return game.Players.Where(x => x.ViewerId != viewerId); + } + public async Task GetIsHost() { long viewerId =