diff --git a/DragaliaAPI.Database/Entities/DbPlayerCharaData.cs b/DragaliaAPI.Database/Entities/DbPlayerCharaData.cs index c221df0b0..47f6344dd 100644 --- a/DragaliaAPI.Database/Entities/DbPlayerCharaData.cs +++ b/DragaliaAPI.Database/Entities/DbPlayerCharaData.cs @@ -173,8 +173,10 @@ public DbPlayerCharaData() { } /// User-facing constructor. /// /// Primary key. + /// + /// [SetsRequiredMembers] - public DbPlayerCharaData(string deviceAccountId, Charas id) + public DbPlayerCharaData(string deviceAccountId, Charas id, bool isTemporary = false) { CharaData data = MasterAsset.CharaData.Get(id); @@ -210,5 +212,6 @@ public DbPlayerCharaData(string deviceAccountId, Charas id) this.Ability2Level = (byte)data.DefaultAbility2Level; this.Ability3Level = (byte)data.DefaultAbility3Level; this.IsUnlockEditSkill = data.Availability == CharaAvailabilities.Story; + this.IsTemporary = false; } } diff --git a/DragaliaAPI.Database/Repositories/IUnitRepository.cs b/DragaliaAPI.Database/Repositories/IUnitRepository.cs index 105b3203b..de19deabb 100644 --- a/DragaliaAPI.Database/Repositories/IUnitRepository.cs +++ b/DragaliaAPI.Database/Repositories/IUnitRepository.cs @@ -17,9 +17,12 @@ public interface IUnitRepository Task CheckHasDragons(IEnumerable idList); - Task> AddCharas(IEnumerable idList); + Task> AddCharas( + IEnumerable idList, + bool isTemporary = false + ); - Task AddCharas(Charas id); + Task AddCharas(Charas id, bool isTemporary = false); Task> AddDragons(IEnumerable idList); @@ -36,4 +39,5 @@ public interface IUnitRepository Task>> GetCharaSets(IEnumerable charaId); Task FindCharaAsync(Charas chara); + Task ClearIsTemporary(Charas id); } diff --git a/DragaliaAPI.Database/Repositories/UnitRepository.cs b/DragaliaAPI.Database/Repositories/UnitRepository.cs index 7667a2146..f0e9dad3c 100644 --- a/DragaliaAPI.Database/Repositories/UnitRepository.cs +++ b/DragaliaAPI.Database/Repositories/UnitRepository.cs @@ -85,11 +85,15 @@ public async Task CheckHasDragons(IEnumerable idList) /// /// Add a list of characters to the database. Will only add the first instance of any new character. /// - /// /// + /// + /// /// A list of tuples which adds an additional dimension onto the input list, /// where the second item shows whether the given character id was a duplicate. - public async Task> AddCharas(IEnumerable idList) + public async Task> AddCharas( + IEnumerable idList, + bool isTemporary = false + ) { List addedChars = idList.ToList(); @@ -106,7 +110,7 @@ public async Task CheckHasDragons(IEnumerable idList) if (newCharas.Any()) { IEnumerable dbEntries = newCharas.Select( - id => new DbPlayerCharaData(this.playerIdentityService.AccountId, id) + id => new DbPlayerCharaData(this.playerIdentityService.AccountId, id, isTemporary) ); await apiContext.PlayerCharaData.AddRangeAsync(dbEntries); @@ -147,9 +151,22 @@ out StoryData? story return newMapping; } - public async Task AddCharas(Charas id) + public async Task AddCharas(Charas id, bool isTemporary = false) { - return (await this.AddCharas(new[] { id })).First().isNew; + return (await this.AddCharas(new[] { id }, isTemporary)).First().isNew; + } + + public async Task ClearIsTemporary(Charas id) + { + DbPlayerCharaData? chara = await this.Charas.FirstOrDefaultAsync(x => x.CharaId == id); + if (chara is null) + { + logger.LogWarning("Attempted to update temp flag of unowned character: {id}", id); + return; + } + + logger.LogDebug("Set {id}.IsTemporary = false", id); + chara.IsTemporary = false; } public async Task> AddDragons(IEnumerable idList) diff --git a/DragaliaAPI.Shared/MasterAsset/Models/Event/EventData.cs b/DragaliaAPI.Shared/MasterAsset/Models/Event/EventData.cs index c607c3876..d664eb178 100644 --- a/DragaliaAPI.Shared/MasterAsset/Models/Event/EventData.cs +++ b/DragaliaAPI.Shared/MasterAsset/Models/Event/EventData.cs @@ -18,5 +18,7 @@ public record EventData( EntityTypes ViewEntityType4, int ViewEntityId4, EntityTypes ViewEntityType5, - int ViewEntityId5 + int ViewEntityId5, + Charas EventCharaId, + int GuestJoinStoryId ); diff --git a/DragaliaAPI.Test/Services/DragonServiceTest.cs b/DragaliaAPI.Test/Services/DragonServiceTest.cs index d6b777e5c..ef0e20149 100644 --- a/DragaliaAPI.Test/Services/DragonServiceTest.cs +++ b/DragaliaAPI.Test/Services/DragonServiceTest.cs @@ -378,7 +378,8 @@ public async Task DoDragonResetPlusCount_ResetsPlusCount() 50, null, null, - null + null, + false ) ) ) diff --git a/DragaliaAPI.Test/Services/StoryServiceTest.cs b/DragaliaAPI.Test/Services/StoryServiceTest.cs index c90d19b3d..399f9e827 100644 --- a/DragaliaAPI.Test/Services/StoryServiceTest.cs +++ b/DragaliaAPI.Test/Services/StoryServiceTest.cs @@ -234,7 +234,15 @@ public async Task ReadQuestStory_DragonReward_ReceivesReward() .Setup( x => x.GrantReward( - new Entity(EntityTypes.Dragon, (int)Dragons.Brunhilda, 1, null, null, null) + new Entity( + EntityTypes.Dragon, + (int)Dragons.Brunhilda, + 1, + null, + null, + null, + false + ) ) ) .ReturnsAsync(RewardGrantResult.Added); diff --git a/DragaliaAPI/Features/Event/EventRepository.cs b/DragaliaAPI/Features/Event/EventRepository.cs index 63d5be5ce..fed221013 100644 --- a/DragaliaAPI/Features/Event/EventRepository.cs +++ b/DragaliaAPI/Features/Event/EventRepository.cs @@ -1,5 +1,6 @@ using DragaliaAPI.Database; using DragaliaAPI.Database.Entities; +using DragaliaAPI.Database.Repositories; using DragaliaAPI.Models; using DragaliaAPI.Services.Exceptions; using DragaliaAPI.Shared.PlayerDetails; @@ -7,12 +8,23 @@ namespace DragaliaAPI.Features.Event; -public class EventRepository(ApiContext apiContext, IPlayerIdentityService playerIdentityService) - : IEventRepository +public class EventRepository( + ApiContext apiContext, + IPlayerIdentityService playerIdentityService, + IUserDataRepository userDataRepository +) : IEventRepository { public IQueryable EventData => apiContext.PlayerEventData.Where(x => x.DeviceAccountId == playerIdentityService.AccountId); + public IQueryable MemoryEventData => + EventData.Join( + userDataRepository.UserData, + eventData => eventData.EventId, + userData => userData.ActiveMemoryEventId, + (eventData, userData) => eventData + ); + public IQueryable Rewards => apiContext.PlayerEventRewards.Where( x => x.DeviceAccountId == playerIdentityService.AccountId diff --git a/DragaliaAPI/Features/Event/EventService.cs b/DragaliaAPI/Features/Event/EventService.cs index 5ae17cf4d..1c187e1dc 100644 --- a/DragaliaAPI/Features/Event/EventService.cs +++ b/DragaliaAPI/Features/Event/EventService.cs @@ -16,7 +16,8 @@ public class EventService( ILogger logger, IEventRepository eventRepository, IRewardService rewardService, - IQuestRepository questRepository + IQuestRepository questRepository, + IUnitRepository unitRepository ) : IEventService { public async Task GetCustomEventFlag(int eventId) @@ -191,6 +192,32 @@ public async Task CreateEventData(int eventId) if (neededEventPassiveIds.Count > 0) eventRepository.CreateEventPassives(eventId, neededEventPassiveIds); + + // Memory events give their characters as temporary on starting + if (data.EventCharaId != Charas.Empty && data.IsMemoryEvent) + { + await rewardService.GrantReward( + new Entity(Type: EntityTypes.Chara, Id: (int)data.EventCharaId, IsTemporary: true) + ); + } + } + + public async Task GetMemoryEventAssetData() + { + int? memoryEventId = await eventRepository.MemoryEventData + .Select(x => x.EventId) + .Cast() + .FirstOrDefaultAsync(); + + if ( + memoryEventId is null + || MasterAsset.EventData.TryGetValue(memoryEventId.Value, out EventData? result) + ) + { + return null; + } + + return result; } private async Task GetEventData(int eventId) diff --git a/DragaliaAPI/Features/Event/IEventRepository.cs b/DragaliaAPI/Features/Event/IEventRepository.cs index 612230792..16086657f 100644 --- a/DragaliaAPI/Features/Event/IEventRepository.cs +++ b/DragaliaAPI/Features/Event/IEventRepository.cs @@ -5,6 +5,7 @@ namespace DragaliaAPI.Features.Event; public interface IEventRepository { IQueryable EventData { get; } + IQueryable MemoryEventData { get; } IQueryable Rewards { get; } IQueryable Items { get; } IQueryable Passives { get; } diff --git a/DragaliaAPI/Features/Event/IEventService.cs b/DragaliaAPI/Features/Event/IEventService.cs index 850ae840d..41badb42e 100644 --- a/DragaliaAPI/Features/Event/IEventService.cs +++ b/DragaliaAPI/Features/Event/IEventService.cs @@ -1,4 +1,5 @@ using DragaliaAPI.Models.Generated; +using DragaliaAPI.Shared.MasterAsset.Models.Event; namespace DragaliaAPI.Features.Event; @@ -37,4 +38,6 @@ int locationId Task GetSimpleEventUserData(int eventId); #endregion + + Task GetMemoryEventAssetData(); } diff --git a/DragaliaAPI/Features/Reward/Entity.cs b/DragaliaAPI/Features/Reward/Entity.cs index 20a42a012..deb701fbb 100644 --- a/DragaliaAPI/Features/Reward/Entity.cs +++ b/DragaliaAPI/Features/Reward/Entity.cs @@ -9,7 +9,8 @@ public record Entity( int Quantity = 1, int? LimitBreakCount = null, int? BuildupCount = null, - int? EquipableCount = null + int? EquipableCount = null, + bool? IsTemporary = false // TODO: int? Level = null ) { diff --git a/DragaliaAPI/Features/Reward/RewardService.cs b/DragaliaAPI/Features/Reward/RewardService.cs index 8c9e1a7fd..ffdae1022 100644 --- a/DragaliaAPI/Features/Reward/RewardService.cs +++ b/DragaliaAPI/Features/Reward/RewardService.cs @@ -37,7 +37,7 @@ public async Task GrantReward(Entity entity) switch (entity.Type) { case EntityTypes.Chara: - return await RewardCharacter(entity); + return await RewardCharacter(entity, entity.IsTemporary ?? false); case EntityTypes.Dragon: for (int i = 0; i < entity.Quantity; i++) await unitRepository.AddDragons((Dragons)entity.Id); @@ -90,7 +90,7 @@ await inventoryRepository.GetMaterial((Materials)entity.Id) return RewardGrantResult.Added; } - private async Task RewardCharacter(Entity entity) + private async Task RewardCharacter(Entity entity, bool isTemporary) { if (entity.Type != EntityTypes.Chara) throw new ArgumentException("Entity was not a character", nameof(entity)); @@ -109,7 +109,7 @@ private async Task RewardCharacter(Entity entity) // TODO: Support EntityLevel/LimitBreak/etc here logger.LogDebug("Granted new character entity: {@entity}", entity); - await unitRepository.AddCharas(chara); + await unitRepository.AddCharas(chara, isTemporary); newEntities.Add(entity); return RewardGrantResult.Added; } diff --git a/DragaliaAPI/Services/Game/StoryService.cs b/DragaliaAPI/Services/Game/StoryService.cs index 314cb4abe..9db461bfe 100644 --- a/DragaliaAPI/Services/Game/StoryService.cs +++ b/DragaliaAPI/Services/Game/StoryService.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using DragaliaAPI.Database.Entities; using DragaliaAPI.Database.Repositories; +using DragaliaAPI.Features.Event; using DragaliaAPI.Features.Fort; using DragaliaAPI.Features.Missions; using DragaliaAPI.Features.Reward; @@ -8,12 +9,25 @@ using DragaliaAPI.Models.Generated; using DragaliaAPI.Shared.Definitions.Enums; using DragaliaAPI.Shared.MasterAsset; +using DragaliaAPI.Shared.MasterAsset.Models.Event; using DragaliaAPI.Shared.MasterAsset.Models.Story; using Microsoft.EntityFrameworkCore; namespace DragaliaAPI.Services.Game; -public class StoryService : IStoryService +public class StoryService( + IStoryRepository storyRepository, + ILogger logger, + IUserDataRepository userDataRepository, + IInventoryRepository inventoryRepository, + ITutorialService tutorialService, + IFortRepository fortRepository, + IMissionProgressionService missionProgressionService, + IRewardService rewardService, + IPaymentService paymentService, + IEventService eventService, + IUnitRepository unitRepository +) : IStoryService { private const int DragonStoryWyrmite = 25; private const int CastleStoryWyrmite = 50; @@ -21,39 +35,6 @@ public class StoryService : IStoryService private const int CharaStoryWyrmite2 = 10; private const int QuestStoryWyrmite = 25; - private readonly IStoryRepository storyRepository; - private readonly ILogger logger; - private readonly IUserDataRepository userDataRepository; - private readonly IInventoryRepository inventoryRepository; - private readonly ITutorialService tutorialService; - private readonly IFortRepository fortRepository; - private readonly IMissionProgressionService missionProgressionService; - private readonly IRewardService rewardService; - private readonly IPaymentService paymentService; - - public StoryService( - IStoryRepository storyRepository, - ILogger logger, - IUserDataRepository userDataRepository, - IInventoryRepository inventoryRepository, - ITutorialService tutorialService, - IFortRepository fortRepository, - IMissionProgressionService missionProgressionService, - IRewardService rewardService, - IPaymentService paymentService - ) - { - this.storyRepository = storyRepository; - this.logger = logger; - this.userDataRepository = userDataRepository; - this.inventoryRepository = inventoryRepository; - this.tutorialService = tutorialService; - this.fortRepository = fortRepository; - this.missionProgressionService = missionProgressionService; - this.rewardService = rewardService; - this.paymentService = paymentService; - } - #region Eligibility check methods public async Task CheckStoryEligibility(StoryTypes type, int storyId) { @@ -247,7 +228,26 @@ await rewardService.GrantReward( } } - logger.LogInformation("Granted rewards for reading new story: {rewards}", rewardList); + EventData? memoryEventData = await eventService.GetMemoryEventAssetData(); + if (memoryEventData?.GuestJoinStoryId == storyId) + { + logger.LogDebug( + "Setting chara {id} to permanently owned", + memoryEventData.EventCharaId + ); + + await unitRepository.ClearIsTemporary(memoryEventData.EventCharaId); + rewardList.Add( + new AtgenBuildEventRewardEntityList() + { + entity_id = (int)memoryEventData.EventCharaId, + entity_quantity = 1, + entity_type = EntityTypes.Chara + } + ); + } + + logger.LogInformation("Granted rewards for reading new story: {@rewards}", rewardList); return rewardList; }