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:
gamer147
2026-05-28 20:07:05 -04:00
parent c2c6a95170
commit b50a884af9
2 changed files with 29 additions and 11 deletions

View File

@@ -72,7 +72,11 @@ public class ViewerRepository : IViewerRepository
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
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
{
AccountId = socialAccountIdentifier,
@@ -195,7 +199,7 @@ public class ViewerRepository : IViewerRepository
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
{
@@ -211,15 +215,11 @@ public class ViewerRepository : IViewerRepository
viewer.Currency.Crystals = grants.Crystals;
viewer.Currency.Rupees = grants.Rupees;
viewer.Currency.RedEther = grants.Ether;
// TUTORIAL_STEP0 (= 1) — "fresh signup, ready to play the prologue", confirmed by the
// prod capture in data_dumps/traffic_prod_tutorial.ndjson (game_start returned
// now_tutorial_step="1"). Step 0 is a pre-existence state we never want to expose
// on the wire: Wizard.Title.NextSceneSwitcher routes step==1 → Prologue scene and
// every other non-{31,41,100} step → AreaSelect at section 0, which has no chapter
// 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;
// TUTORIAL_STEP0 (= 1) is the fresh-signup default — see RegisterAnonymousViewer for
// why step==0 is unsafe. RegisterViewer (admin-import + Steam-social) passes 100 so
// those callers land at the post-tutorial baseline; import requests can still override
// via the explicit ImportViewerRequest.TutorialState field.
viewer.MissionData.TutorialState = initialTutorialState;
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
// and would otherwise be null (audit §6 #3 latent NRE — this is the one).