Behavior-preserving; 271 BattleNode/Matching/Services tests green, full solution builds. "BattleType" meant two things: the Sessions.BattleType enum (Pvp/Bot) and an int "mode id" field. Renamed the int field on MatchContext AND the BattleStartBody wire DTO to BattleModeId (wire key stays "battleType" via JsonPropertyName), so BattleType now means only the enum project-wide. New Bridge/BattleModes.cs (TakeTwo = 11) replaces every 11 literal — both prod MatchContextBuilder sites and the test fixtures/assertions. The arbitrary-passthrough 42 and bot 0 stay literal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
4.9 KiB
C#
120 lines
4.9 KiB
C#
using System.Text.Json;
|
|
using SVSim.BattleNode.Bridge;
|
|
using SVSim.Database.Enums;
|
|
using SVSim.Database.Models.Config;
|
|
using SVSim.Database.Repositories.Deck;
|
|
using SVSim.Database.Repositories.Viewer;
|
|
using SVSim.Database.Services;
|
|
|
|
namespace SVSim.EmulatedEntrypoint.Services;
|
|
|
|
public class MatchContextBuilder : IMatchContextBuilder
|
|
{
|
|
private readonly IArenaTwoPickRunRepository _runs;
|
|
private readonly IViewerRepository _viewers;
|
|
private readonly IDeckRepository _decks;
|
|
private readonly IGameConfigService _config;
|
|
|
|
public MatchContextBuilder(
|
|
IArenaTwoPickRunRepository runs,
|
|
IViewerRepository viewers,
|
|
IDeckRepository decks,
|
|
IGameConfigService config)
|
|
{
|
|
_runs = runs;
|
|
_viewers = viewers;
|
|
_decks = decks;
|
|
_config = config;
|
|
}
|
|
|
|
public async Task<MatchContext> BuildForTwoPickAsync(long viewerId)
|
|
{
|
|
var run = await _runs.GetByViewerIdAsync(viewerId)
|
|
?? throw new ArenaTwoPickException("arena_two_pick_no_active_run");
|
|
|
|
var deck = JsonSerializer.Deserialize<List<long>>(run.SelectedCardIdsJson) ?? new();
|
|
if (deck.Count < 30)
|
|
throw new ArenaTwoPickException("arena_two_pick_draft_incomplete");
|
|
|
|
var viewer = await _viewers.LoadForMatchContextAsync(viewerId)
|
|
?? throw new ArenaTwoPickException("arena_two_pick_no_active_run");
|
|
|
|
var challenge = _config.Get<ChallengeConfig>();
|
|
var defaults = _config.Get<DefaultLoadoutConfig>();
|
|
|
|
var emblemId = viewer.Info.SelectedEmblem.Id != 0
|
|
? viewer.Info.SelectedEmblem.Id.ToString()
|
|
: defaults.EmblemId.ToString();
|
|
var degreeId = viewer.Info.SelectedDegree.Id != 0
|
|
? viewer.Info.SelectedDegree.Id.ToString()
|
|
: defaults.DegreeId.ToString();
|
|
var charaId = run.LeaderSkinId != 0
|
|
? run.LeaderSkinId.ToString()
|
|
: run.ClassId.ToString();
|
|
|
|
return new MatchContext(
|
|
SelfDeckCardIds: deck,
|
|
ClassId: run.ClassId.ToString(),
|
|
CharaId: charaId,
|
|
// Hardcoded v1; see spec §Deferred plumbing.
|
|
CardMasterName: "card_master_node_10015",
|
|
CountryCode: viewer.Info.CountryCode ?? string.Empty,
|
|
UserName: viewer.DisplayName,
|
|
// TK2-specific cosmetic source; other modes will use the deck row's SleeveId.
|
|
SleeveId: challenge.TwoPickSleeveId.ToString(),
|
|
EmblemId: emblemId,
|
|
DegreeId: degreeId,
|
|
// Hardcoded v1; needs equipped-MyPageBackground lookup (see spec §Deferred).
|
|
FieldId: 43,
|
|
IsOfficial: viewer.Info.IsOfficial ? 1 : 0,
|
|
BattleModeId: BattleModes.TakeTwo);
|
|
}
|
|
|
|
public async Task<MatchContext> BuildForRankBattleAsync(long viewerId, Format format, int deckNo)
|
|
{
|
|
var viewer = await _viewers.LoadForMatchContextAsync(viewerId)
|
|
?? throw new InvalidOperationException($"viewer {viewerId} not found");
|
|
|
|
// IDeckRepository is the right path here — viewer-graph nav refs (DeckCard.Card)
|
|
// don't auto-load (see project_ef_nav_include_pitfall memory), which would
|
|
// silently ship card_id=0.
|
|
var deck = await _decks.GetDeck(viewerId, format, deckNo)
|
|
?? throw new InvalidOperationException(
|
|
$"viewer {viewerId} has no deck #{deckNo} for format {format}");
|
|
|
|
var defaults = _config.Get<DefaultLoadoutConfig>();
|
|
var emblemId = viewer.Info.SelectedEmblem.Id != 0
|
|
? viewer.Info.SelectedEmblem.Id.ToString()
|
|
: defaults.EmblemId.ToString();
|
|
var degreeId = viewer.Info.SelectedDegree.Id != 0
|
|
? viewer.Info.SelectedDegree.Id.ToString()
|
|
: defaults.DegreeId.ToString();
|
|
var charaId = deck.LeaderSkin.Id != 0
|
|
? deck.LeaderSkin.Id.ToString()
|
|
: deck.Class.Id.ToString();
|
|
var sleeveId = deck.Sleeve.Id != 0
|
|
? deck.Sleeve.Id.ToString()
|
|
: defaults.SleeveId.ToString();
|
|
// DeckCard is count-based (one row per unique card + a Count). The node's deck
|
|
// is one entry PER PHYSICAL CARD (idx 1..N), so expand each row by its Count —
|
|
// otherwise a 3-copy card ships as a single in-battle card.
|
|
var deckCardIds = deck.Cards
|
|
.SelectMany(c => Enumerable.Repeat(c.Card.Id, c.Count))
|
|
.ToList();
|
|
|
|
return new MatchContext(
|
|
SelfDeckCardIds: deckCardIds,
|
|
ClassId: deck.Class.Id.ToString(),
|
|
CharaId: charaId,
|
|
CardMasterName: "card_master_node_10015",
|
|
CountryCode: viewer.Info.CountryCode ?? string.Empty,
|
|
UserName: viewer.DisplayName,
|
|
SleeveId: sleeveId,
|
|
EmblemId: emblemId,
|
|
DegreeId: degreeId,
|
|
FieldId: 43,
|
|
IsOfficial: viewer.Info.IsOfficial ? 1 : 0,
|
|
BattleModeId: BattleModes.TakeTwo);
|
|
}
|
|
}
|