fix(viewer): RegisterViewer defaults to post-tutorial TutorialState=100
BuildDefaultViewer hardcoded TutorialState=1 — correct for fresh anonymous signups (RegisterAnonymousViewer) but wrong for AdminController.ImportViewer and Steam-social signups, which both go through RegisterViewer and expect a prod-replica viewer that boots to the home screen. Add an initialTutorialState parameter (default 1 preserves RegisterAnonymousViewer behavior); RegisterViewer passes 100. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,11 @@ public class ViewerRepository : IViewerRepository
|
|||||||
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
||||||
ulong socialAccountIdentifier, ulong? shortUdid = null)
|
ulong socialAccountIdentifier, ulong? shortUdid = null)
|
||||||
{
|
{
|
||||||
var viewer = await BuildDefaultViewer(displayName);
|
// RegisterViewer is the import / Steam-social path. Default to the post-tutorial baseline
|
||||||
|
// (state 100) so AdminController.ImportViewer materializes prod-replicas at the home screen
|
||||||
|
// unless the import request explicitly overrides via request.TutorialState. The anonymous
|
||||||
|
// signup path (RegisterAnonymousViewer) uses the parameter default of 1.
|
||||||
|
var viewer = await BuildDefaultViewer(displayName, initialTutorialState: 100);
|
||||||
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
||||||
{
|
{
|
||||||
AccountId = socialAccountIdentifier,
|
AccountId = socialAccountIdentifier,
|
||||||
@@ -195,7 +199,7 @@ public class ViewerRepository : IViewerRepository
|
|||||||
await _dbContext.SaveChangesAsync();
|
await _dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Models.Viewer> BuildDefaultViewer(string displayName)
|
private async Task<Models.Viewer> BuildDefaultViewer(string displayName, int initialTutorialState = 1)
|
||||||
{
|
{
|
||||||
Models.Viewer viewer = new Models.Viewer
|
Models.Viewer viewer = new Models.Viewer
|
||||||
{
|
{
|
||||||
@@ -211,15 +215,11 @@ public class ViewerRepository : IViewerRepository
|
|||||||
viewer.Currency.Crystals = grants.Crystals;
|
viewer.Currency.Crystals = grants.Crystals;
|
||||||
viewer.Currency.Rupees = grants.Rupees;
|
viewer.Currency.Rupees = grants.Rupees;
|
||||||
viewer.Currency.RedEther = grants.Ether;
|
viewer.Currency.RedEther = grants.Ether;
|
||||||
// TUTORIAL_STEP0 (= 1) — "fresh signup, ready to play the prologue", confirmed by the
|
// TUTORIAL_STEP0 (= 1) is the fresh-signup default — see RegisterAnonymousViewer for
|
||||||
// prod capture in data_dumps/traffic_prod_tutorial.ndjson (game_start returned
|
// why step==0 is unsafe. RegisterViewer (admin-import + Steam-social) passes 100 so
|
||||||
// now_tutorial_step="1"). Step 0 is a pre-existence state we never want to expose
|
// those callers land at the post-tutorial baseline; import requests can still override
|
||||||
// on the wire: Wizard.Title.NextSceneSwitcher routes step==1 → Prologue scene and
|
// via the explicit ImportViewerRequest.TutorialState field.
|
||||||
// every other non-{31,41,100} step → AreaSelect at section 0, which has no chapter
|
viewer.MissionData.TutorialState = initialTutorialState;
|
||||||
// data (the prologue's chapters are baked into Wizard/Prologue.cs, not served) and
|
|
||||||
// crashes AreaSelectUI.SelectChapter with a "Sequence contains no matching element"
|
|
||||||
// LINQ Single() failure.
|
|
||||||
viewer.MissionData.TutorialState = 1;
|
|
||||||
|
|
||||||
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
|
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
|
||||||
// and would otherwise be null (audit §6 #3 latent NRE — this is the one).
|
// and would otherwise be null (audit §6 #3 latent NRE — this is the one).
|
||||||
|
|||||||
@@ -141,6 +141,24 @@ public class ViewerRepositoryTests
|
|||||||
await repo.RegisterAnonymousViewer(Guid.Empty));
|
await repo.RegisterAnonymousViewer(Guid.Empty));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task RegisterViewer_starts_at_post_tutorial_state()
|
||||||
|
{
|
||||||
|
using var factory = new SVSimTestFactory();
|
||||||
|
using var scope = factory.Services.CreateScope();
|
||||||
|
var repo = scope.ServiceProvider.GetRequiredService<IViewerRepository>();
|
||||||
|
|
||||||
|
var viewer = await repo.RegisterViewer(
|
||||||
|
"Imported Viewer",
|
||||||
|
SVSim.Database.Enums.SocialAccountType.Steam,
|
||||||
|
socialAccountIdentifier: 76_561_198_000_000_999UL);
|
||||||
|
|
||||||
|
Assert.That(viewer.MissionData.TutorialState, Is.EqualTo(100),
|
||||||
|
"RegisterViewer (admin-import + Steam-social signup) must produce a post-tutorial " +
|
||||||
|
"viewer by default. Import requests can override via request.TutorialState; absence " +
|
||||||
|
"means 'a prod-replica viewer ready for the home screen', NOT 'replay tutorial'.");
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task GetViewerByUdid_returns_viewer_or_null()
|
public async Task GetViewerByUdid_returns_viewer_or_null()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user