feat(practice): serve default/trial/leader-skin lists on practice/deck_list

practice/deck_list returns the same wire shape as /deck/info (the client parses
both via DeckGroupListData), but only ever sent user decks — so a fresh account
saw no default decks and couldn't start a practice match.

Extract the /deck/info hydration into a shared IDeckListBuilder used by
/deck/info, /deck/my_list, and /practice/deck_list. Practice passes
padEmptySlots:false (deck *select*, not builder) — matches the prod practice
capture, which returns real decks unpadded plus the 8 per-class default decks
and per-class leader-skin settings. Retire the near-duplicate
PracticeDeckListResponse DTO in favor of the shared DeckListResponse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-29 12:01:36 -04:00
parent 1e53748ae3
commit 2d675aa35d
6 changed files with 203 additions and 158 deletions

View File

@@ -1,11 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using SVSim.Database.Enums;
using SVSim.EmulatedEntrypoint.Models.Dtos;
using SVSim.Database.Repositories.Deck;
using SVSim.Database.Repositories.Globals;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Practice;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Deck;
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Practice;
using SVSim.EmulatedEntrypoint.Services;
@@ -13,18 +13,18 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
public class PracticeController : SVSimController
{
private readonly IDeckRepository _deckRepository;
private readonly IGlobalsRepository _globalsRepository;
private readonly IMissionProgressService _missionProgress;
private readonly IDeckListBuilder _deckListBuilder;
public PracticeController(
IDeckRepository deckRepository,
IGlobalsRepository globalsRepository,
IMissionProgressService missionProgress)
IMissionProgressService missionProgress,
IDeckListBuilder deckListBuilder)
{
_deckRepository = deckRepository;
_globalsRepository = globalsRepository;
_missionProgress = missionProgress;
_deckListBuilder = deckListBuilder;
}
/// <summary>
@@ -53,25 +53,19 @@ public class PracticeController : SVSimController
}
/// <summary>
/// /practice/deck_list — returns viewer's decks scoped by format (always Format.All
/// per spec, server can ignore the request field). Fetched via IDeckRepository so the
/// DeckCard.Card navigation is Included; going through the heavier viewer-graph query
/// drops that ThenInclude and ships 40 zeros instead of real card ids, which then
/// NREs the client's SBattleLoad.InitPlayer (CardCreator returns null on id=0).
/// /practice/deck_list — same wire shape as /deck/info (the client parses both via
/// DeckGroupListData), so it shares <see cref="IDeckListBuilder"/>. Always All-format per spec.
/// Unlike /deck/info this is a deck *select* screen, so empty "New Deck" slots are NOT padded
/// (padEmptySlots: false) — prod's practice capture returns the viewer's real decks unpadded,
/// plus the 8 per-class default decks and per-class leader-skin settings. The builder loads
/// decks via IDeckRepository (DeckCard.Card Included), so card_id_array carries real ids rather
/// than the 40 zeros that NRE the client's SBattleLoad.InitPlayer.
/// </summary>
[HttpPost("deck_list")]
public async Task<ActionResult<PracticeDeckListResponse>> DeckList(DeckFormatRequest request)
public async Task<ActionResult<DeckListResponse>> DeckList(DeckFormatRequest request)
{
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
var byFormat = await _deckRepository.GetDecksByFormats(viewerId, new[] { Format.Rotation, Format.Unlimited });
return new PracticeDeckListResponse
{
MaintenanceCardList = new List<long>(),
UserDeckRotation = byFormat[Format.Rotation].Select(d => new UserDeck(d)).ToList(),
UserDeckUnlimited = byFormat[Format.Unlimited].Select(d => new UserDeck(d)).ToList(),
};
return await _deckListBuilder.BuildAsync(viewerId, Format.All, padEmptySlots: false);
}
/// <summary>