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 =