Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using DragaliaAPI.Database.Entities;
using DragaliaAPI.Features.Shared.Reward.Handlers;
using DragaliaAPI.Shared.Definitions.Enums.Summon;
using DragaliaAPI.Shared.MasterAsset;
using DragaliaAPI.Shared.MasterAsset.Models.Story;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace DragaliaAPI.Integration.Test.Features.Reward;
Expand Down Expand Up @@ -99,4 +102,57 @@ public void NoDuplicateSupportedTypes()

supportedTypes.Should().OnlyHaveUniqueItems();
}

[Fact]
public async Task GrantArsene_AddsDragonStories()
{
(
await this.ApiContext.PlayerStoryState.CountAsync(
x => x.StoryType == StoryTypes.Dragon,
cancellationToken: TestContext.Current.CancellationToken
)
)
.Should()
.Be(0);

DbPlayerPresent present = new()
{
EntityType = EntityTypes.Dragon,
EntityQuantity = 1,
EntityId = (int)DragonId.Arsene,
};

await this.AddToDatabase(present);

await this.Client.PostMsgpack(
"/present/receive",
new PresentReceiveRequest() { PresentIdList = [(ulong)present.PresentId] },
cancellationToken: TestContext.Current.CancellationToken
);

StoryData arseneStories = MasterAsset.DragonStories[(int)DragonId.Arsene];

List<DbPlayerStoryState> dragonStories = await this
.ApiContext.PlayerStoryState.Where(x => x.StoryType == StoryTypes.Dragon)
.ToListAsync(cancellationToken: TestContext.Current.CancellationToken);

dragonStories
.Should()
.BeEquivalentTo([
new DbPlayerStoryState()
{
ViewerId = this.ViewerId,
StoryType = StoryTypes.Dragon,
State = 0,
StoryId = arseneStories.StoryIds[0],
},
new DbPlayerStoryState()
{
ViewerId = this.ViewerId,
StoryType = StoryTypes.Dragon,
State = 0,
StoryId = arseneStories.StoryIds[1],
},
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public ISavefileUpdateTest(CustomWebApplicationFactory factory, ITestOutputHelpe
public void ISavefileUpdate_HasExpectedCount()
{
// Update this test when adding a new update.
this.updates.Should().HaveCount(27);
this.updates.Should().HaveCount(28);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using DragaliaAPI.Database.Entities;
using DragaliaAPI.Shared.MasterAsset;
using DragaliaAPI.Shared.MasterAsset.Models.Story;

namespace DragaliaAPI.Integration.Test.Features.SavefileUpdate;

public class V28UpdateTest : SavefileUpdateTestFixture
{
protected override int StartingVersion => 27;

public V28UpdateTest(CustomWebApplicationFactory factory, ITestOutputHelper outputHelper)
: base(factory, outputHelper) { }

[Fact]
public async Task V28Update_UnlocksBothStoriesForDragonAtLevel30()
{
this.ApiContext.PlayerStoryState.Count(x => x.StoryType == StoryTypes.Dragon)
.Should()
.Be(0);

await this.AddRangeToDatabase([
new DbPlayerDragonReliability() { DragonId = DragonId.Arsene, Level = 30 },
]);

StoryData arseneStories = MasterAsset.DragonStories[(int)DragonId.Arsene];

this.ApiContext.ChangeTracker.Clear();

await this.LoadIndex();

List<DbPlayerStoryState> dragonStories = ApiContext
.PlayerStoryState.Where(x => x.StoryType == StoryTypes.Dragon)
.ToList();

dragonStories
.Should()
.Contain(x => x.StoryId == arseneStories.StoryIds[0])
.Which.Should()
.BeEquivalentTo(
new DbPlayerStoryState()
{
ViewerId = this.ViewerId,
StoryId = arseneStories.StoryIds[0],
StoryType = StoryTypes.Dragon,
State = StoryState.Unlocked,
}
);

dragonStories
.Should()
.Contain(x => x.StoryId == arseneStories.StoryIds[1])
.Which.Should()
.BeEquivalentTo(
new DbPlayerStoryState()
{
ViewerId = this.ViewerId,
StoryId = arseneStories.StoryIds[1],
StoryType = StoryTypes.Dragon,
State = StoryState.Unlocked,
}
);
}

[Fact]
public async Task V28Update_DoesNotDuplicateExistingStories()
{
await this.AddRangeToDatabase([
new DbPlayerDragonReliability() { DragonId = DragonId.Arsene, Level = 30 },
]);

StoryData arseneStories = MasterAsset.DragonStories[(int)DragonId.Arsene];

await this.AddRangeToDatabase([
new DbPlayerStoryState()
{
StoryId = arseneStories.StoryIds[0],
StoryType = StoryTypes.Dragon,
},
new DbPlayerStoryState()
{
StoryId = arseneStories.StoryIds[1],
StoryType = StoryTypes.Dragon,
},
]);

this.ApiContext.ChangeTracker.Clear();

await this.LoadIndex();

ApiContext
.PlayerStoryState.Where(x =>
x.StoryType == StoryTypes.Dragon
&& (
x.StoryId == arseneStories.StoryIds[0] || x.StoryId == arseneStories.StoryIds[1]
)
)
.Should()
.HaveCount(2);
}
}
67 changes: 67 additions & 0 deletions DragaliaAPI/DragaliaAPI/Features/Login/SavefileUpdate/V28Update.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using DragaliaAPI.Database;
using DragaliaAPI.Database.Entities;
using DragaliaAPI.Shared.Definitions.Enums;
using DragaliaAPI.Shared.MasterAsset;
using DragaliaAPI.Shared.MasterAsset.Models.Story;
using DragaliaAPI.Shared.PlayerDetails;
using LinqToDB;
using Microsoft.EntityFrameworkCore;

namespace DragaliaAPI.Features.Login.SavefileUpdate;

/// <summary>
/// Fixes missing dragon stories for Arsene, whose default reliability level of 30 meant that
/// stories were not unlocked on receipt (unlike level-up, which is handled by <see cref="V25Update"/>).
/// </summary>
public partial class V28Update(
ApiContext apiContext,
IPlayerIdentityService playerIdentityService,
ILogger<V28Update> logger
) : ISavefileUpdate
{
public int SavefileVersion => 28;

public async Task Apply()
Comment thread
SapiensAnatis marked this conversation as resolved.
{
DbPlayerDragonReliability? arsene =
await apiContext.PlayerDragonReliability.FirstOrDefaultAsync(x =>
x.DragonId == DragonId.Arsene
);

if (
arsene is null
|| !MasterAsset.DragonStories.TryGetValue((int)DragonId.Arsene, out StoryData? data)
)
{
Log.AddedNewStories(logger, 0);
return;
}

// Arsene's default reliability level is 30, so any existing reliability entry implies
// both stories should be unlocked -- there is no need to check the level.
List<DbPlayerStoryState> intendedStoryStates = data
.StoryIds.Select(storyId => new DbPlayerStoryState()
{
ViewerId = playerIdentityService.ViewerId,
StoryId = storyId,
State = StoryState.Unlocked,
StoryType = StoryTypes.Dragon,
})
.ToList();

int rowsAffected = await apiContext
.PlayerStoryState.Merge()
.Using(intendedStoryStates)
.OnTargetKey()
.InsertWhenNotMatched()
.MergeAsync();

Log.AddedNewStories(logger, rowsAffected);
}

private static partial class Log
{
[LoggerMessage(LogLevel.Information, "V28Update added {Count} missing dragon stories")]
public static partial void AddedNewStories(ILogger logger, int count);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System.Diagnostics;
using DragaliaAPI.Database;
using DragaliaAPI.Database.Entities;
using DragaliaAPI.Shared.Definitions.Enums;
using DragaliaAPI.Shared.MasterAsset;
using DragaliaAPI.Shared.MasterAsset.Models.Story;
using DragaliaAPI.Shared.PlayerDetails;
using Microsoft.EntityFrameworkCore;

Expand Down Expand Up @@ -47,7 +51,9 @@ public async Task<GrantReturn> Grant(Entity entity)
&& !await apiContext.PlayerDragonReliability.AnyAsync(x => x.DragonId == dragon)
)
{
apiContext.PlayerDragonReliability.Add(new(playerIdentityService.ViewerId, dragon));
DbPlayerDragonReliability reliability = new(playerIdentityService.ViewerId, dragon);
apiContext.PlayerDragonReliability.Add(reliability);
this.AddDefaultLevelStories(dragon, reliability.Level);
}

return GrantReturn.Added();
Expand Down Expand Up @@ -99,8 +105,10 @@ IDictionary<TKey, Entity> entities
&& !apiContext.PlayerDragonReliability.Local.Any(x => x.DragonId == dragon)
)
{
apiContext.PlayerDragonReliability.Add(new(playerIdentityService.ViewerId, dragon));
DbPlayerDragonReliability reliability = new(playerIdentityService.ViewerId, dragon);
apiContext.PlayerDragonReliability.Add(reliability);
ownedReliabilities.Add(dragon);
this.AddDefaultLevelStories(dragon, reliability.Level);
}

resultDict.Add(key, GrantReturn.Added());
Expand All @@ -110,6 +118,36 @@ IDictionary<TKey, Entity> entities
return resultDict;
}

private void AddDefaultLevelStories(DragonId dragon, int reliabilityLevel)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for this in the present tests

{
if (
reliabilityLevel < 5
|| !MasterAsset.DragonStories.TryGetValue((int)dragon, out StoryData? storyData)
)
{
return;
}

Debug.Assert(
storyData.StoryIds.Length == 2,
"Expected all dragons to have exactly two stories"
);

int storiesToUnlock = reliabilityLevel >= 15 ? 2 : 1;

for (int i = 0; i < storiesToUnlock; i++)
{
apiContext.PlayerStoryState.Add(
new DbPlayerStoryState()
{
ViewerId = playerIdentityService.ViewerId,
StoryType = StoryTypes.Dragon,
StoryId = storyData.StoryIds[i],
}
);
}
}

private static partial class Log
{
[LoggerMessage(
Expand Down