import_viewer: round-trip ViewerStoryProgress with per-family offsets
This commit is contained in:
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Entities.Story;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
@@ -417,6 +418,52 @@ public class AdminController : SVSimController
|
||||
}
|
||||
}
|
||||
|
||||
// Pass D: StoryProgress with per-family offsets
|
||||
var skippedStoryIds = new HashSet<long>();
|
||||
if (request.StoryProgress is { } storyRows)
|
||||
{
|
||||
var existingStory = await _dbContext.ViewerStoryProgress
|
||||
.Where(p => p.ViewerId == viewer.Id).ToListAsync();
|
||||
_dbContext.ViewerStoryProgress.RemoveRange(existingStory);
|
||||
|
||||
// Compute effective ids and bulk-load referenced catalog rows.
|
||||
var effectiveIds = new List<int>(storyRows.Count);
|
||||
var rowEffective = new List<(ImportStoryProgress row, int effective)>(storyRows.Count);
|
||||
foreach (var r in storyRows)
|
||||
{
|
||||
long offset = r.StoryApiType switch
|
||||
{
|
||||
1 => 0L,
|
||||
2 => 10_000_000L,
|
||||
3 => 20_000_000L,
|
||||
_ => -1L
|
||||
};
|
||||
if (offset < 0) { skippedStoryIds.Add(r.StoryId); continue; }
|
||||
int wireId = r.SubChapterId ?? r.StoryId;
|
||||
int effective = checked((int)(wireId + offset));
|
||||
effectiveIds.Add(effective);
|
||||
rowEffective.Add((r, effective));
|
||||
}
|
||||
var present = (await _dbContext.StoryWorlds
|
||||
.Where(s => effectiveIds.Contains(s.Id))
|
||||
.Select(s => s.Id).ToListAsync()).ToHashSet();
|
||||
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
foreach (var (r, eff) in rowEffective)
|
||||
{
|
||||
if (!present.Contains(eff)) { skippedStoryIds.Add(eff); continue; }
|
||||
_dbContext.ViewerStoryProgress.Add(new ViewerStoryProgress
|
||||
{
|
||||
ViewerId = viewer.Id,
|
||||
StoryId = eff,
|
||||
IsFinish = r.IsFinish,
|
||||
IsSkipped = r.IsSkipped,
|
||||
FinishedAt = r.IsFinish ? nowUtc : null,
|
||||
SkippedAt = r.IsSkipped ? nowUtc : null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
if (skippedCardIds.Count > 0)
|
||||
@@ -455,6 +502,13 @@ public class AdminController : SVSimController
|
||||
request.SteamId, viewer.Id, skippedAchievementCounterTypes.Count,
|
||||
string.Join(", ", skippedAchievementCounterTypes.Take(10)));
|
||||
}
|
||||
if (skippedStoryIds.Count > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"ImportViewer (steam_id={SteamId}, viewer_id={ViewerId}): skipped {Count} unknown story_id(s). Sample: [{Sample}]",
|
||||
request.SteamId, viewer.Id, skippedStoryIds.Count,
|
||||
string.Join(", ", skippedStoryIds.Take(10)));
|
||||
}
|
||||
|
||||
return new ImportViewerResponse
|
||||
{
|
||||
@@ -466,6 +520,7 @@ public class AdminController : SVSimController
|
||||
SkippedMissionCounterCount = skippedMissionCounterIds.Count,
|
||||
SkippedAchievementCount = skippedAchievementTypes.Count,
|
||||
SkippedAchievementCounterCount = skippedAchievementCounterTypes.Count,
|
||||
SkippedStoryCount = skippedStoryIds.Count,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ public class ImportViewerRequest
|
||||
|
||||
[JsonPropertyName("achievements")]
|
||||
public List<ImportAchievement>? Achievements { get; set; }
|
||||
|
||||
[JsonPropertyName("story_progress")]
|
||||
public List<ImportStoryProgress>? StoryProgress { get; set; }
|
||||
}
|
||||
|
||||
public class ImportDeck
|
||||
@@ -110,6 +113,15 @@ public class ImportAchievement
|
||||
[JsonPropertyName("total_count")] public int TotalCount { get; set; }
|
||||
}
|
||||
|
||||
public class ImportStoryProgress
|
||||
{
|
||||
[JsonPropertyName("story_api_type")] public int StoryApiType { get; set; }
|
||||
[JsonPropertyName("story_id")] public int StoryId { get; set; }
|
||||
[JsonPropertyName("sub_chapter_id")] public int? SubChapterId { get; set; }
|
||||
[JsonPropertyName("is_finish")] public bool IsFinish { get; set; }
|
||||
[JsonPropertyName("is_skipped")] public bool IsSkipped { get; set; }
|
||||
}
|
||||
|
||||
public class ImportMissionMeta
|
||||
{
|
||||
[JsonPropertyName("has_received_pick_two_mission")]
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Entities.Story;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Admin;
|
||||
@@ -796,4 +797,142 @@ public class AdminControllerTests
|
||||
.SingleAsync(m => m.ViewerId == viewerId && m.MissionCatalogId == 9005);
|
||||
Assert.That(mission.Slot, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ImportViewer_StoryProgress_OffsetsByFamily()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using (var seedScope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.StoryWorlds.Add(new StoryWorld { Id = 5 }); // Main
|
||||
db.StoryWorlds.Add(new StoryWorld { Id = 10_000_005 }); // Limited
|
||||
db.StoryWorlds.Add(new StoryWorld { Id = 20_000_005 }); // Event
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
var client = factory.CreateClient();
|
||||
ulong steamId = 70000000000000007UL;
|
||||
|
||||
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
|
||||
{
|
||||
SteamId = steamId,
|
||||
StoryProgress = new List<ImportStoryProgress>
|
||||
{
|
||||
new() { StoryApiType = 1, StoryId = 5, IsFinish = true, IsSkipped = false },
|
||||
new() { StoryApiType = 2, StoryId = 5, IsFinish = false, IsSkipped = true },
|
||||
new() { StoryApiType = 3, StoryId = 5, IsFinish = true, IsSkipped = true }
|
||||
}
|
||||
});
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var body = await resp.Content.ReadFromJsonAsync<ImportViewerResponse>(JsonOptions);
|
||||
Assert.That(body!.SkippedStoryCount, Is.EqualTo(0));
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var verifyDb = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewerId = await verifyDb.Viewers
|
||||
.Where(v => v.SocialAccountConnections
|
||||
.Any(s => s.AccountType == SocialAccountType.Steam && s.AccountId == steamId))
|
||||
.Select(v => v.Id).SingleAsync();
|
||||
|
||||
var ids = verifyDb.ViewerStoryProgress.Where(p => p.ViewerId == viewerId).Select(p => p.StoryId).ToList();
|
||||
Assert.That(ids, Is.EquivalentTo(new[] { 5, 10_000_005, 20_000_005 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ImportViewer_StoryProgress_SubChapterUsesSubId()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using (var seedScope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.StoryWorlds.Add(new StoryWorld { Id = 10 }); // parent chapter
|
||||
db.StoryWorlds.Add(new StoryWorld { Id = 11 }); // sub_chapter (own row)
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
var client = factory.CreateClient();
|
||||
ulong steamId = 70000000000000008UL;
|
||||
|
||||
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
|
||||
{
|
||||
SteamId = steamId,
|
||||
StoryProgress = new List<ImportStoryProgress>
|
||||
{
|
||||
new() { StoryApiType = 1, StoryId = 10, IsFinish = true, IsSkipped = false },
|
||||
new() { StoryApiType = 1, StoryId = 10, SubChapterId = 11, IsFinish = true, IsSkipped = true }
|
||||
}
|
||||
});
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var verifyDb = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewerId = await verifyDb.Viewers
|
||||
.Where(v => v.SocialAccountConnections
|
||||
.Any(s => s.AccountType == SocialAccountType.Steam && s.AccountId == steamId))
|
||||
.Select(v => v.Id).SingleAsync();
|
||||
var ids = verifyDb.ViewerStoryProgress.Where(p => p.ViewerId == viewerId).Select(p => p.StoryId).ToList();
|
||||
Assert.That(ids, Is.EquivalentTo(new[] { 10, 11 }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ImportViewer_StoryProgress_UnknownStoryIdSkipped()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
var client = factory.CreateClient();
|
||||
ulong steamId = 70000000000000009UL;
|
||||
|
||||
var resp = await client.PostAsJsonAsync("/admin/import_viewer", new ImportViewerRequest
|
||||
{
|
||||
SteamId = steamId,
|
||||
StoryProgress = new List<ImportStoryProgress>
|
||||
{
|
||||
new() { StoryApiType = 1, StoryId = 9999, IsFinish = true, IsSkipped = false }
|
||||
}
|
||||
});
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var body = await resp.Content.ReadFromJsonAsync<ImportViewerResponse>(JsonOptions);
|
||||
Assert.That(body!.SkippedStoryCount, Is.EqualTo(1));
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var verifyDb = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
Assert.That(verifyDb.ViewerStoryProgress.Any(), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ImportViewer_AllNewSections_AreIdempotentOnReImport()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
using (var seedScope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = seedScope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
db.MissionCatalog.Add(new MissionCatalogEntry { Id = 8001, LotType = 6, EventType = "x", EventArg = null });
|
||||
db.AchievementCatalog.Add(new AchievementCatalogEntry { AchievementType = 800, Level = 1, EventType = "y", EventArg = null });
|
||||
db.StoryWorlds.Add(new StoryWorld { Id = 50 });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
var client = factory.CreateClient();
|
||||
ulong steamId = 70000000000000010UL;
|
||||
|
||||
var req = new ImportViewerRequest
|
||||
{
|
||||
SteamId = steamId,
|
||||
MissionMeta = new ImportMissionMeta { HasReceivedPickTwoMission = true, MissionReceiveType = 1, MissionChangeTime = 1L },
|
||||
Missions = new List<ImportMission> { new() { MissionId = 8001, MissionStatus = 1, TotalCount = 5 } },
|
||||
Achievements = new List<ImportAchievement> { new() { AchievementType = 800, Level = 1, NowAchievedLevel = 1, ResultAnnounceSawLevel = 0, TotalCount = 9 } },
|
||||
StoryProgress = new List<ImportStoryProgress> { new() { StoryApiType = 1, StoryId = 50, IsFinish = true } }
|
||||
};
|
||||
(await client.PostAsJsonAsync("/admin/import_viewer", req)).EnsureSuccessStatusCode();
|
||||
(await client.PostAsJsonAsync("/admin/import_viewer", req)).EnsureSuccessStatusCode();
|
||||
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var verifyDb = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
var viewerId = await verifyDb.Viewers
|
||||
.Where(v => v.SocialAccountConnections
|
||||
.Any(s => s.AccountType == SocialAccountType.Steam && s.AccountId == steamId))
|
||||
.Select(v => v.Id).SingleAsync();
|
||||
|
||||
Assert.That(verifyDb.ViewerMissions.Count(m => m.ViewerId == viewerId), Is.EqualTo(1));
|
||||
Assert.That(verifyDb.ViewerAchievements.Count(a => a.ViewerId == viewerId), Is.EqualTo(1));
|
||||
Assert.That(verifyDb.ViewerStoryProgress.Count(p => p.ViewerId == viewerId), Is.EqualTo(1));
|
||||
Assert.That(verifyDb.ViewerEventCounters.Count(c => c.ViewerId == viewerId), Is.EqualTo(2));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user