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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user