Deck list work
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Globals;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.EmulatedEntrypoint.Constants;
|
||||
using SVSim.EmulatedEntrypoint.Infrastructure;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||
@@ -11,6 +14,10 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
public class MyPageController : SVSimController
|
||||
{
|
||||
/// <summary>"yyyy-MM-dd HH:mm:ss" — prod's PHP convention. Used for wire-formatting DateTime
|
||||
/// columns that the client parses via DateTime.Parse on its side.</summary>
|
||||
private const string WireDateFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
private readonly IViewerRepository _viewerRepository;
|
||||
private readonly IGlobalsRepository _globalsRepository;
|
||||
|
||||
@@ -38,9 +45,15 @@ public class MyPageController : SVSimController
|
||||
var deviceHeader = Request.Headers["DEVICE"].FirstOrDefault();
|
||||
int deviceType = int.TryParse(deviceHeader, out int parsed) ? parsed : 0;
|
||||
|
||||
// Stubs below are tagged TODO(mypage-stub). See the "Current server implementation"
|
||||
// section of docs/api-spec/endpoints/post-login/mypage-index.md for the table of what
|
||||
// each one would source from. Grep for "mypage-stub" to enumerate them.
|
||||
// Hydrate all the globals slices in parallel-ish — they're independent reads.
|
||||
var cfg = await _globalsRepository.GetGameConfiguration("default");
|
||||
var colosseum = await _globalsRepository.GetCurrentColosseum();
|
||||
var sealedSeason = await _globalsRepository.GetCurrentSealedSeason();
|
||||
var masterPointPeriod = await _globalsRepository.GetCurrentMasterPointPeriod();
|
||||
var bannerEntries = await _globalsRepository.GetBanners();
|
||||
var specialDeckFormats = await _globalsRepository.GetActiveSpecialDeckFormats();
|
||||
|
||||
// Remaining stubs are tagged TODO(mypage-stub) — see docs/api-spec/endpoints/post-login/mypage-index.md.
|
||||
return new MyPageIndexResponse
|
||||
{
|
||||
UserInfo = new UserInfo(deviceType, viewer),
|
||||
@@ -55,6 +68,19 @@ public class MyPageController : SVSimController
|
||||
ArenaInfo = await BuildArenaInfosAsync(),
|
||||
IsArenaChallengePeriod = false, // TODO(mypage-stub): globals/ArenaSeason flag
|
||||
IsAvailableColosseumFreeEntry = false, // TODO(mypage-stub): viewer + globals free-entry quota
|
||||
ColosseumInfo = BuildColosseumInfo(colosseum),
|
||||
SealedInfo = BuildSealedInfo(sealedSeason),
|
||||
Banner = bannerEntries.Select(BuildBannerInfo).ToList(),
|
||||
RoomTypeInSession = new RoomTypeInSession
|
||||
{
|
||||
SpecialDeckFormatList = specialDeckFormats
|
||||
.Select(e => new SpecialDeckFormat
|
||||
{
|
||||
DeckFormat = e.DeckFormat,
|
||||
EndTime = e.EndTime.ToString(WireDateFormat, CultureInfo.InvariantCulture)
|
||||
})
|
||||
.ToList()
|
||||
},
|
||||
Convention = new Convention // TODO(mypage-stub): viewer offline-event participation
|
||||
{
|
||||
IsJoinTournament = false,
|
||||
@@ -62,36 +88,61 @@ public class MyPageController : SVSimController
|
||||
},
|
||||
UserConfig = new UserConfig(), // TODO(mypage-stub): persist viewer UserConfig
|
||||
Quest = new Quest(), // TODO(mypage-stub): active Quest event + viewer flags
|
||||
MasterPointRankingPeriod = new MasterPointRankingPeriod
|
||||
{
|
||||
// TODO(mypage-stub): source begin_time/end_time/period_num/necessary_score from the
|
||||
// current Master Points season row in globals. Far-future fallback so the client's
|
||||
// DateTime.Parse(end_time) succeeds and _masterResetNextTime gets seeded.
|
||||
EndTime = "2030-01-01 00:00:00",
|
||||
},
|
||||
MasterPointRankingPeriod = BuildMasterPointRankingPeriod(masterPointPeriod),
|
||||
PreReleaseStatus = 0, // TODO(mypage-stub): derive from PreReleaseInfo
|
||||
UserMyPageInfo = new UserMyPageInfo // TODO(mypage-stub): viewer mypage BG selection
|
||||
{
|
||||
UserMyPageSetting = new MyPageBgSetting(),
|
||||
},
|
||||
BasicPuzzle = new BasicPuzzle { IsDisplayBadge = false }, // TODO(mypage-stub): viewer practice-puzzle progress
|
||||
IsBattlePassPeriod = (await _globalsRepository.GetGameConfiguration("default")).IsBattlePassPeriod,
|
||||
IsBattlePassPeriod = cfg.IsBattlePassPeriod,
|
||||
SpecialCrystalInfo = new(), // TODO(mypage-stub): same shape/source as /load/index
|
||||
// ColosseumInfo, ShopNotification, StoryNotification, IsHiddenBossAppeared all
|
||||
// default-constructed by MyPageIndexResponse's field initializers.
|
||||
// TODO(mypage-stub): wire colosseum_info from current Colosseum cup row.
|
||||
// CompetitionInfo, ShopNotification, StoryNotification, GuildNotification, GatheringInfo,
|
||||
// IsHiddenBossAppeared, SubBanner/SubBannerList/HomeDialogList/UserOfflineEvent/UserItemList,
|
||||
// and the three explicit-null fields (TreasureInfo, LotteryPeriodInfo, AllCardEnabledPeriod)
|
||||
// all rely on MyPageIndexResponse field initializers.
|
||||
// TODO(mypage-stub): wire competition_info from active tournament row (default false fine until tournaments exist).
|
||||
// TODO(mypage-stub): wire shop_notification from per-product shop-appeal state.
|
||||
// TODO(mypage-stub): wire story_notification from viewer story progress.
|
||||
// TODO(mypage-stub): wire is_hidden_boss_appeared from globals event flag.
|
||||
// TODO(mypage-stub): per-viewer state for user_item_list, gathering_info.is_entry, guild_notification, user_offline_event, home_dialog_list.
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Same shape as LoadController.BuildArenaInfosAsync, but /mypage/index has no
|
||||
/// Keys.Contains("arena_info") guard on the client (ArenaData(jsonData["arena_info"])
|
||||
/// at MyPageTask.cs:55 indexes [0] unconditionally). When no current Take Two season is
|
||||
/// seeded we fall back to a minimal one-entry list so the client's ArenaData ctor doesn't
|
||||
/// crash with IndexOutOfRange.
|
||||
/// Slim notification-delta endpoint — see MyPageRefreshResponse for the 3-field contract.
|
||||
/// Client fires this once after main-menu UI settles (and a second time shortly after; both
|
||||
/// calls get the same response). No state changes happen here; everything is read-only.
|
||||
/// </summary>
|
||||
[HttpPost("refresh")]
|
||||
public async Task<ActionResult<MyPageRefreshResponse>> Refresh(MyPageRefreshRequest request)
|
||||
{
|
||||
var shortUdidClaim = User.Claims.FirstOrDefault(c => c.Type == ShadowverseClaimTypes.ShortUdidClaim)?.Value;
|
||||
if (shortUdidClaim is null || !long.TryParse(shortUdidClaim, out long shortUdid))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
Viewer? viewer = await _viewerRepository.GetViewerByShortUdid(shortUdid);
|
||||
if (viewer is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return new MyPageRefreshResponse
|
||||
{
|
||||
FriendBattleInviteCount = 0, // TODO(mypage-stub): viewer room-invite count
|
||||
ShopNotification = new ShopNotification(), // TODO(mypage-stub): per-product shop-appeal state
|
||||
GatheringNotification = new GatheringNotification(), // empty matching message — correct for fresh viewers
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors LoadController.BuildArenaInfosAsync. /mypage/index has no Keys.Contains("arena_info")
|
||||
/// guard (ArenaData(jsonData["arena_info"]) at MyPageTask.cs:55 indexes [0] unconditionally), and
|
||||
/// the post-parse UI consumer (ChallengeEntry.SetChallengeInfo at ChallengeEntry.cs:35) reads
|
||||
/// _twoPickData.ChallengeData which is only built when arena_info[0].format_info is present.
|
||||
/// So we always populate format_info from the same ArenaSeason.FormatInfo jsonb /load/index uses.
|
||||
/// </summary>
|
||||
private async Task<List<ArenaInfo>> BuildArenaInfosAsync()
|
||||
{
|
||||
@@ -112,6 +163,12 @@ public class MyPageController : SVSimController
|
||||
};
|
||||
}
|
||||
|
||||
ArenaFormatInfo? format = null;
|
||||
if (!string.IsNullOrEmpty(season.FormatInfo) && season.FormatInfo != "{}")
|
||||
{
|
||||
format = JsonSerializer.Deserialize<ArenaFormatInfo>(season.FormatInfo, JsonbReadOptions.Instance);
|
||||
}
|
||||
|
||||
return new List<ArenaInfo>
|
||||
{
|
||||
new ArenaInfo
|
||||
@@ -122,10 +179,115 @@ public class MyPageController : SVSimController
|
||||
RupeeCost = season.RupyCost,
|
||||
TicketCost = season.TicketCost,
|
||||
IsJoin = season.IsJoin,
|
||||
// format_info is intentionally omitted here — /mypage/index's ArenaData
|
||||
// ctor only needs the top-level fields. /load/index round-trips it via
|
||||
// JsonbReadOptions; pull it in if a downstream check ever needs it.
|
||||
FormatInfo = format,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ColosseumInfo BuildColosseumInfo(ColosseumConfig? row)
|
||||
{
|
||||
if (row is null) return new ColosseumInfo();
|
||||
|
||||
ColosseumSalesPeriodInfo sales = new();
|
||||
if (!string.IsNullOrEmpty(row.SalesPeriodInfo) && row.SalesPeriodInfo != "{}")
|
||||
{
|
||||
sales = JsonSerializer.Deserialize<ColosseumSalesPeriodInfo>(row.SalesPeriodInfo, JsonbReadOptions.Instance)
|
||||
?? new ColosseumSalesPeriodInfo();
|
||||
}
|
||||
|
||||
return new ColosseumInfo
|
||||
{
|
||||
ColosseumId = row.ColosseumId,
|
||||
IsDisplayTips = row.IsDisplayTips,
|
||||
TipsId = row.TipsId,
|
||||
CardPoolName = row.CardPoolName,
|
||||
IsColosseumPeriod = row.IsColosseumPeriod,
|
||||
IsRoundPeriod = row.IsRoundPeriod,
|
||||
DeckFormat = row.DeckFormat,
|
||||
IsNormalTwoPick = row.IsNormalTwoPick,
|
||||
IsSpecialMode = row.IsSpecialMode,
|
||||
IsAllCardEnabled = row.IsAllCardEnabled,
|
||||
StartTime = row.StartTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
|
||||
ColosseumName = row.ColosseumName,
|
||||
NowRound = row.NowRound,
|
||||
EndTime = row.EndTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
|
||||
SalesPeriodInfo = sales,
|
||||
};
|
||||
}
|
||||
|
||||
private SealedInfo BuildSealedInfo(SealedConfig? row)
|
||||
{
|
||||
if (row is null) return new SealedInfo();
|
||||
|
||||
List<int> packInfo = new();
|
||||
if (!string.IsNullOrEmpty(row.PackInfo) && row.PackInfo != "[]")
|
||||
{
|
||||
packInfo = JsonSerializer.Deserialize<List<int>>(row.PackInfo, JsonbReadOptions.Instance) ?? new();
|
||||
}
|
||||
|
||||
SealedSalesPeriodInfo sales = new();
|
||||
if (!string.IsNullOrEmpty(row.SalesPeriodInfo) && row.SalesPeriodInfo != "{}")
|
||||
{
|
||||
sales = JsonSerializer.Deserialize<SealedSalesPeriodInfo>(row.SalesPeriodInfo, JsonbReadOptions.Instance)
|
||||
?? new SealedSalesPeriodInfo();
|
||||
}
|
||||
|
||||
return new SealedInfo
|
||||
{
|
||||
Enable = row.Enable,
|
||||
CrystalCost = row.CrystalCost,
|
||||
RupyCost = row.RupyCost,
|
||||
TicketCost = row.TicketCost,
|
||||
IsJoin = row.IsJoin,
|
||||
PackInfo = packInfo,
|
||||
DeckUsingNumMin = row.DeckUsingNumMin,
|
||||
ScheduleId = row.ScheduleId,
|
||||
IsDeckCodeMaintenance = row.IsDeckCodeMaintenance,
|
||||
SalesPeriodInfo = sales,
|
||||
};
|
||||
}
|
||||
|
||||
private static BannerInfo BuildBannerInfo(BannerEntry row)
|
||||
{
|
||||
List<string> imagePaths = new();
|
||||
if (!string.IsNullOrEmpty(row.ImagePaths) && row.ImagePaths != "[]")
|
||||
{
|
||||
imagePaths = JsonSerializer.Deserialize<List<string>>(row.ImagePaths, JsonbReadOptions.Instance) ?? new();
|
||||
}
|
||||
|
||||
return new BannerInfo
|
||||
{
|
||||
ImageName = row.ImageName,
|
||||
Click = row.Click,
|
||||
Status = row.Status,
|
||||
// DB stores numeric, wire is string. PHP convention.
|
||||
ChangeTime = row.ChangeTime.ToString(CultureInfo.InvariantCulture),
|
||||
RemainingTime = row.RemainingTime.ToString(CultureInfo.InvariantCulture),
|
||||
ImagePaths = imagePaths,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Far-future fallback EndTime so the client's DateTime.Parse(end_time) succeeds and
|
||||
/// Data.Load.data._masterResetNextTime gets seeded even when no globals row is present.
|
||||
/// </summary>
|
||||
private static MasterPointRankingPeriod BuildMasterPointRankingPeriod(MasterPointRankingPeriodEntry? row)
|
||||
{
|
||||
if (row is null)
|
||||
{
|
||||
return new MasterPointRankingPeriod
|
||||
{
|
||||
EndTime = "2030-01-01 00:00:00",
|
||||
};
|
||||
}
|
||||
|
||||
return new MasterPointRankingPeriod
|
||||
{
|
||||
Id = row.Id,
|
||||
PeriodNum = row.PeriodNum,
|
||||
NecessaryScore = row.NecessaryScore,
|
||||
BeginTime = row.BeginTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
|
||||
EndTime = row.EndTime.ToString(WireDateFormat, CultureInfo.InvariantCulture),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user