Practice/deck editing mostly there

This commit is contained in:
gamer147
2026-05-24 00:17:28 -04:00
parent 21b97269ff
commit bdff142d16
14 changed files with 588 additions and 59 deletions

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using SVSim.EmulatedEntrypoint.Models.Dtos.Common;
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
namespace SVSim.EmulatedEntrypoint.Controllers;
/// <summary>
/// /config/* — viewer-scoped preference toggles. Fired by the client as fire-and-forget
/// after the deck-builder save flow (and from settings screens). Currently acknowledge-only:
/// UserConfig is stubbed in load/index + mypage/index until a viewer-config persistence
/// layer lands. Mirrors the set_deck_redis precedent (no-op accept).
/// </summary>
public class ConfigController : SVSimController
{
[HttpPost("update_foil_preferred")]
public ActionResult<EmptyResponse> UpdateFoilPreferred(ConfigUpdateFoilPreferredRequest request)
{
if (!TryGetViewerId(out long _)) return Unauthorized();
// TODO(viewer-config-persist): write request.IsFoilPreferred onto the viewer's config row
// and read it back in load/index + mypage/index instead of `new UserConfig()`.
return new EmptyResponse();
}
[HttpPost("update_prize_preferred")]
public ActionResult<EmptyResponse> UpdatePrizePreferred(ConfigUpdatePrizePreferredRequest request)
{
if (!TryGetViewerId(out long _)) return Unauthorized();
// TODO(viewer-config-persist): see UpdateFoilPreferred.
return new EmptyResponse();
}
}

View File

@@ -1,10 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using SVSim.Database;
using SVSim.Database.Enums;
using SVSim.Database.Models;
using SVSim.Database.Repositories.Deck;
using SVSim.Database.Repositories.Globals;
using SVSim.EmulatedEntrypoint.Configuration;
using SVSim.EmulatedEntrypoint.Constants;
using SVSim.EmulatedEntrypoint.Extensions;
using SVSim.EmulatedEntrypoint.Models.Dtos;
@@ -20,6 +22,7 @@ public class DeckController : SVSimController
private readonly IDeckRepository _deckRepository;
private readonly IGlobalsRepository _globalsRepository;
private readonly SVSimDbContext _dbContext;
private readonly DeckOptions _deckOptions;
private static readonly System.Text.Json.JsonSerializerOptions JsonbReadOptions = new()
{
@@ -27,11 +30,32 @@ public class DeckController : SVSimController
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
};
public DeckController(IDeckRepository deckRepository, IGlobalsRepository globalsRepository, SVSimDbContext dbContext)
public DeckController(IDeckRepository deckRepository, IGlobalsRepository globalsRepository, SVSimDbContext dbContext, IOptions<DeckOptions> deckOptions)
{
_deckRepository = deckRepository;
_globalsRepository = globalsRepository;
_dbContext = dbContext;
_deckOptions = deckOptions.Value;
}
/// <summary>
/// Pads a viewer's real deck list with empty-slot placeholders up to <see cref="DeckOptions.MaxDeckSlots"/>.
/// Required because the client's <c>DeckUI.DeckViewData.CreateDeckViewList</c> only renders
/// a "New Deck" tile when the response contains an entry whose <c>card_id_array</c> is empty —
/// without padding, the player cannot create additional decks once any exist.
/// </summary>
private List<UserDeck> PadEmptySlots(List<UserDeck> realDecks)
{
var taken = realDecks.Select(d => d.DeckNumber).ToHashSet();
var result = new List<UserDeck>(realDecks);
for (int slot = 1; slot <= _deckOptions.MaxDeckSlots; slot++)
{
if (!taken.Contains(slot))
{
result.Add(UserDeck.CreateEmptySlot(slot));
}
}
return result;
}
// Request deck_format fields arrive as wire ints (MessagePack-CSharp doesn't honor STJ
@@ -106,9 +130,9 @@ public class DeckController : SVSimController
// for our profile; we mirror that omission and leave the nullable DTO fields unset.
var formats = new[] { Format.Rotation, Format.Unlimited, Format.MyRotation };
var byFormat = await _deckRepository.GetDecksByFormats(viewerId, formats);
response.UserDeckRotation = byFormat[Format.Rotation].Select(d => new UserDeck(d)).ToList();
response.UserDeckUnlimited = byFormat[Format.Unlimited].Select(d => new UserDeck(d)).ToList();
response.UserDeckMyRotation = byFormat[Format.MyRotation].Select(d => new UserDeck(d)).ToList();
response.UserDeckRotation = PadEmptySlots(byFormat[Format.Rotation].Select(d => new UserDeck(d)).ToList());
response.UserDeckUnlimited = PadEmptySlots(byFormat[Format.Unlimited].Select(d => new UserDeck(d)).ToList());
response.UserDeckMyRotation = PadEmptySlots(byFormat[Format.MyRotation].Select(d => new UserDeck(d)).ToList());
// trial_deck_list is prod-emitted on /deck/info (All format) but omitted on /deck/my_list
// (specific format). Empty array in the 2026-05-23 prod capture.
response.TrialDeckList = new();
@@ -116,7 +140,7 @@ public class DeckController : SVSimController
else
{
var decks = await _deckRepository.GetDecks(viewerId, requestFormat);
response.UserDeckList = decks.Select(d => new UserDeck(d)).ToList();
response.UserDeckList = PadEmptySlots(decks.Select(d => new UserDeck(d)).ToList());
}
return response;
@@ -163,7 +187,7 @@ public class DeckController : SVSimController
var decks = await _deckRepository.GetDecks(viewerId, format);
return new DeckUpdateResponse
{
UserDeckList = decks.Select(d => new UserDeck(d)).ToList()
UserDeckList = PadEmptySlots(decks.Select(d => new UserDeck(d)).ToList())
};
}