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;
}