More features
This commit is contained in:
@@ -1,11 +1,5 @@
|
||||
using System.Buffers.Text;
|
||||
using System.Text;
|
||||
using MessagePack;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.EmulatedEntrypoint.Extensions;
|
||||
@@ -13,64 +7,68 @@ using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Controllers
|
||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
public class CheckController : SVSimController
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class CheckController : SVSimController
|
||||
private readonly ILogger _logger;
|
||||
private readonly IViewerRepository _viewerRepository;
|
||||
|
||||
public CheckController(ILogger<CheckController> logger, IViewerRepository viewerRepository)
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IViewerRepository _viewerRepository;
|
||||
|
||||
public CheckController(ILogger<CheckController> logger, IViewerRepository viewerRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_viewerRepository = viewerRepository;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("special_title")]
|
||||
public async Task<SpecialTitleCheckResponse> SpecialTitleCheck(SpecialTitleCheckRequest request)
|
||||
{
|
||||
int titleId = Random.Shared.Next(8, 33);
|
||||
var res = new SpecialTitleCheckResponse
|
||||
{
|
||||
TitleImageId = titleId,
|
||||
TitleSoundId = titleId
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
[HttpPost("game_start")]
|
||||
public async Task<GameStartResponse> GameStart(GameStartRequest request)
|
||||
{
|
||||
Viewer? viewer = await _viewerRepository.GetViewerWithSocials(HttpContext.GetViewer().Id);
|
||||
return new GameStartResponse()
|
||||
{
|
||||
IsSetTransitionPassword = true,
|
||||
KorAuthorityId = default,
|
||||
KorAuthorityState = default,
|
||||
NowRank = new Dictionary<string, string>()
|
||||
{
|
||||
{ "1", "RankName_010" },
|
||||
{ "2", "RankName_010" },
|
||||
{ "4", "RankName_017" }
|
||||
},
|
||||
NowName = viewer.DisplayName,
|
||||
PolicyState = default,
|
||||
PolicyId = default,
|
||||
NowTutorialStep = "100",
|
||||
NowViewerId = viewer.Id,
|
||||
TosId = default,
|
||||
TosState = default,
|
||||
TransitionAccountData = viewer.SocialAccountConnections.Select(sac => new TransitionAccountData
|
||||
{
|
||||
ConnectedViewerId = viewer.Id.ToString(),
|
||||
SocialAccountId = sac.AccountId.ToString(),
|
||||
SocialAccountType = ((int)sac.AccountType).ToString()
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
_logger = logger;
|
||||
_viewerRepository = viewerRepository;
|
||||
}
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("special_title")]
|
||||
public Task<SpecialTitleCheckResponse> SpecialTitleCheck(SpecialTitleCheckRequest request)
|
||||
{
|
||||
return Task.FromResult(new SpecialTitleCheckResponse
|
||||
{
|
||||
TitleImageId = "0"
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: spec lists this as anonymous (identity from SHORT_UDID), but the base controller's
|
||||
// [Authorize] still applies. For now requires a Steam-linked viewer; new-user bootstrap (where
|
||||
// the server creates a viewer + returns rewrite_viewer_id) is deferred until the boot flow is
|
||||
// exercised end-to-end with a real client.
|
||||
[HttpPost("game_start")]
|
||||
public async Task<GameStartResponse> GameStart(GameStartRequest request)
|
||||
{
|
||||
Viewer viewer = HttpContext.GetViewer()
|
||||
?? throw new InvalidOperationException("Auth handler must set viewer in context.");
|
||||
Viewer fullViewer = await _viewerRepository.GetViewerWithSocials(viewer.Id) ?? viewer;
|
||||
|
||||
return new GameStartResponse
|
||||
{
|
||||
NowViewerId = fullViewer.Id,
|
||||
NowName = fullViewer.DisplayName,
|
||||
NowTutorialStep = fullViewer.MissionData.TutorialState.ToString(),
|
||||
IsSetTransitionPassword = true,
|
||||
// Stub rank map until per-format ranks are persisted (prod observed: "1"/"2"/"4"
|
||||
// keys mapping to RankName_010 / RankName_017). Empty dict here may be safe but
|
||||
// we don't yet know which client paths read this — match prod stub.
|
||||
NowRank = new Dictionary<string, string>
|
||||
{
|
||||
{ "1", "RankName_010" },
|
||||
{ "2", "RankName_010" },
|
||||
{ "4", "RankName_017" }
|
||||
},
|
||||
TransitionAccountData = fullViewer.SocialAccountConnections
|
||||
.Select(sac => new TransitionAccountData
|
||||
{
|
||||
SocialAccountId = sac.AccountId.ToString(),
|
||||
SocialAccountType = ((int)sac.AccountType).ToString(),
|
||||
ConnectedViewerId = fullViewer.Id.ToString()
|
||||
}).ToList(),
|
||||
TosState = 1,
|
||||
PolicyState = 1,
|
||||
KorAuthorityState = 0,
|
||||
TosId = 1,
|
||||
PolicyId = 1,
|
||||
KorAuthorityId = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,29 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||
|
||||
public class LoadController : SVSimController
|
||||
{
|
||||
// Per-format rank entries the wire expects (5 entries, in deck_format discriminator order).
|
||||
// Hard-coded until viewer rank-state is persisted (see audit §6 #1).
|
||||
private static readonly Format[] RankFormats =
|
||||
{
|
||||
Format.Rotation, Format.Unlimited, Format.MyRotation, Format.Avatar, Format.Crossover
|
||||
};
|
||||
|
||||
// Until ShadowverseCardSetEntry is seeded by CardImport, hard-code a stub so the client
|
||||
// doesn't crash on RotationCardSetList[1] / [Count-1] (LoadDetail.cs:184).
|
||||
private static readonly List<CardSetIdentifier> StubRotationSets = new()
|
||||
{
|
||||
new CardSetIdentifier { SetId = 10000 },
|
||||
new CardSetIdentifier { SetId = 10005 },
|
||||
new CardSetIdentifier { SetId = 10010 }
|
||||
};
|
||||
|
||||
private readonly IViewerRepository _viewerRepository;
|
||||
private readonly ICardRepository _cardRepository;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IGlobalsRepository _globalsRepository;
|
||||
|
||||
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository, ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository)
|
||||
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
||||
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository)
|
||||
{
|
||||
_viewerRepository = viewerRepository;
|
||||
_cardRepository = cardRepository;
|
||||
@@ -30,29 +47,30 @@ public class LoadController : SVSimController
|
||||
[HttpPost("index")]
|
||||
public async Task<ActionResult<IndexResponse>> Index(IndexRequest request)
|
||||
{
|
||||
Viewer? viewer = await _viewerRepository.GetViewerByShortUdid(long.Parse(User.Claims
|
||||
.FirstOrDefault(claim => claim.Type == ShadowverseClaimTypes.ShortUdidClaim).Value));
|
||||
|
||||
if (viewer == null)
|
||||
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();
|
||||
}
|
||||
|
||||
// Cards. Empty until CardImport lands (audit §3 — user_card_list is blocked).
|
||||
List<ShadowverseCardEntry> allCollectibleCards = await _cardRepository.GetAll(true);
|
||||
List<ShadowverseCardEntry> allBasicCards = await _cardRepository.GetAllBasic();
|
||||
List<OwnedCardEntry> ownedCards = viewer.Cards;
|
||||
List<OwnedCardEntry> allCardsAsOwned = allCollectibleCards.GroupJoin(ownedCards, card => card.Id,
|
||||
List<OwnedCardEntry> allCardsAsOwned = allCollectibleCards.GroupJoin(ownedCards,
|
||||
card => card.Id,
|
||||
ownedCard => ownedCard.Card.Id,
|
||||
(card, foundOwnedCards) =>
|
||||
(card, foundOwnedCards) => foundOwnedCards.DefaultIfEmpty().FirstOrDefault() ?? new OwnedCardEntry
|
||||
{
|
||||
OwnedCardEntry ownedCard = foundOwnedCards.DefaultIfEmpty().FirstOrDefault() ?? new OwnedCardEntry
|
||||
{
|
||||
Card = card,
|
||||
Count = 0,
|
||||
IsProtected = false
|
||||
};
|
||||
|
||||
return ownedCard;
|
||||
Card = card,
|
||||
Count = 0,
|
||||
IsProtected = false
|
||||
}).ToList();
|
||||
allCardsAsOwned = allCardsAsOwned.Union(allBasicCards.Select(bc => new OwnedCardEntry
|
||||
{
|
||||
@@ -60,14 +78,11 @@ public class LoadController : SVSimController
|
||||
Count = 3,
|
||||
IsProtected = true
|
||||
})).ToList();
|
||||
|
||||
List<LeaderSkinEntry> allLeaderSkins = await _collectionRepository.GetLeaderSkins();
|
||||
|
||||
Dictionary<string, ClassExp> classExp = new Dictionary<string, ClassExp>();
|
||||
List<LeaderSkinEntry> allLeaderSkins = await _collectionRepository.GetLeaderSkins();
|
||||
var classExpCurve = await _globalsRepository.GetClassExpCurve();
|
||||
|
||||
List<ClassExp> classExps = new List<ClassExp>();
|
||||
|
||||
List<ClassExp> classExps = new();
|
||||
int accumulateExp = 0;
|
||||
int? prevNecessaryExp = null;
|
||||
foreach (var entry in classExpCurve)
|
||||
@@ -75,7 +90,7 @@ public class LoadController : SVSimController
|
||||
accumulateExp += entry.NecessaryExp;
|
||||
classExps.Add(new ClassExp
|
||||
{
|
||||
Level = entry.Id, // You need to specify the level value based on your logic
|
||||
Level = entry.Id,
|
||||
NecessaryExp = entry.NecessaryExp,
|
||||
DiffExp = prevNecessaryExp.HasValue ? entry.NecessaryExp - prevNecessaryExp.Value : entry.NecessaryExp,
|
||||
AccumulateExp = accumulateExp
|
||||
@@ -83,37 +98,53 @@ public class LoadController : SVSimController
|
||||
prevNecessaryExp = entry.NecessaryExp;
|
||||
}
|
||||
|
||||
List<CardSetIdentifier> rotationSets = (await _cardRepository.GetCardSets(true))
|
||||
.Select(set => new CardSetIdentifier { SetId = set.Id })
|
||||
.ToList();
|
||||
if (rotationSets.Count < 2)
|
||||
{
|
||||
rotationSets = StubRotationSets;
|
||||
}
|
||||
|
||||
var deviceHeader = Request.Headers["DEVICE"].FirstOrDefault();
|
||||
int deviceType = int.TryParse(deviceHeader, out int parsed) ? parsed : 0;
|
||||
|
||||
return new IndexResponse
|
||||
{
|
||||
UserTutorial = new UserTutorial
|
||||
{
|
||||
TutorialStep = viewer.MissionData.TutorialState
|
||||
},
|
||||
UserInfo = new UserInfo(int.Parse(Request.Headers["DEVICE"].FirstOrDefault()), viewer),
|
||||
UserInfo = new UserInfo(deviceType, viewer),
|
||||
UserCurrency = new UserCurrency(viewer),
|
||||
UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(),
|
||||
UserRotationDecks = new UserFormatDeckInfo
|
||||
{
|
||||
UserDecks = viewer.Decks.Where(deck => deck.Format == Format.Rotation).Select(deck => new UserDeck(deck)).ToList()
|
||||
UserDecks = viewer.Decks.Where(d => d.Format == Format.Rotation)
|
||||
.Select(d => new UserDeck(d)).ToList()
|
||||
},
|
||||
UserUnlimitedDecks = new UserFormatDeckInfo
|
||||
{
|
||||
UserDecks = viewer.Decks.Where(deck => deck.Format == Format.Unlimited).Select(deck => new UserDeck(deck)).ToList()
|
||||
UserDecks = viewer.Decks.Where(d => d.Format == Format.Unlimited)
|
||||
.Select(d => new UserDeck(d)).ToList()
|
||||
},
|
||||
UserMyRotationDecks = new UserFormatDeckInfo
|
||||
{
|
||||
UserDecks = viewer.Decks.Where(deck => deck.Format == Format.MyRotation).Select(deck => new UserDeck(deck)).ToList()
|
||||
UserDecks = viewer.Decks.Where(d => d.Format == Format.MyRotation)
|
||||
.Select(d => new UserDeck(d)).ToList()
|
||||
},
|
||||
UserCards = allCardsAsOwned.Select(card => new UserCard(card)).ToList(),
|
||||
UserClasses = viewer.Classes.Select(viewerClass => new UserClass(viewerClass)).ToList(),
|
||||
Sleeves = viewer.Sleeves.ToDictionary(sleeve => sleeve.Id.ToString(), sleeve => new SleeveIdentifier { SleeveId = sleeve.Id }),
|
||||
UserEmblems = viewer.Emblems.Select(emblem => new EmblemIdentifier { EmblemId = emblem.Id }).ToList(),
|
||||
UserDegrees = viewer.Degrees.Select(degree => new DegreeIdentifier { DegreeId = degree.Id }).ToList(),
|
||||
LeaderSkins = allLeaderSkins.ToDictionary(skin => skin.Id.ToString(), skin => new UserLeaderSkin(skin, viewer.LeaderSkins.Any(vs => vs.Id == skin.Id))),
|
||||
MyPageBackgrounds = viewer.MyPageBackgrounds.Select(mpbg => mpbg.Id).ToList(),
|
||||
UserClasses = viewer.Classes.Select(vc => new UserClass(vc)).ToList(),
|
||||
Sleeves = viewer.Sleeves.Select(s => new SleeveIdentifier { SleeveId = s.Id }).ToList(),
|
||||
UserEmblems = viewer.Emblems.Select(e => new EmblemIdentifier { EmblemId = e.Id }).ToList(),
|
||||
UserDegrees = viewer.Degrees.Select(d => new DegreeIdentifier { DegreeId = d.Id }).ToList(),
|
||||
LeaderSkins = allLeaderSkins
|
||||
.Select(skin => new UserLeaderSkin(skin, viewer.LeaderSkins.Any(vs => vs.Id == skin.Id)))
|
||||
.ToList(),
|
||||
MyPageBackgrounds = viewer.MyPageBackgrounds.Select(mpbg => mpbg.Id.ToString()).ToList(),
|
||||
LootBoxRegulations = new LootBoxRegulations(),
|
||||
GatheringInfo = new GatheringInfo(),
|
||||
IsBattlePassPeriod = false,
|
||||
IsBattlePassPeriod = 0,
|
||||
BattlePassLevelInfo = null,
|
||||
SpecialCrystalInfos = new List<SpecialCrystalInfo>(),
|
||||
AvatarRotationInfo = null,
|
||||
@@ -121,22 +152,35 @@ public class LoadController : SVSimController
|
||||
FeatureMaintenances = new List<FeatureMaintenance>(),
|
||||
PreReleaseInfo = null,
|
||||
SpotCards = new Dictionary<string, int>(),
|
||||
ReprintedCards = new Dictionary<string, long>(),
|
||||
ReprintedCards = new List<long>(),
|
||||
UnlimitedBanList = new Dictionary<string, int>(),
|
||||
LoadingTipCardExclusions = new List<long>(),
|
||||
MaintenanceCards = new List<CardIdentifier>(),
|
||||
MaintenanceCards = new List<long>(),
|
||||
RedEtherOverrides = new List<RedEtherOverride>(),
|
||||
DailyLoginBonus = new DailyLoginBonus(),
|
||||
UserRankedMatches = new List<UserRankedMatches>(),
|
||||
UserRankInfo = new Dictionary<string, UserRankInfo>(),
|
||||
UserRankInfo = RankFormats.Select(f => new UserRankInfo
|
||||
{
|
||||
DeckFormat = (int)f,
|
||||
Rank = 1,
|
||||
BattlePoints = 0,
|
||||
WinStreak = 0,
|
||||
IsPromotion = 0,
|
||||
IsMasterRank = 0,
|
||||
IsGrandMasterRank = 0,
|
||||
MasterPoints = 0
|
||||
}).ToList(),
|
||||
ArenaConfig = new ArenaConfig(),
|
||||
ArenaInfos = new List<ArenaInfo>(),
|
||||
RotationSets = (await _cardRepository.GetCardSets(true)).Select(set => new CardSetIdentifier { SetId = set.Id }).ToList(),
|
||||
RotationSets = rotationSets,
|
||||
UserConfig = new UserConfig(),
|
||||
OpenBattlefieldIds = (await _globalsRepository.GetBattlefields(true)).ToDictionary(bf => bf.Id.ToString(), bf => bf.Id),
|
||||
OpenBattlefieldIds = (await _globalsRepository.GetBattlefields(true))
|
||||
.Select(bf => bf.Id.ToString()).ToList(),
|
||||
DefaultSettings = new DefaultSettings(await _globalsRepository.GetGameConfiguration("default")),
|
||||
ClassExp = classExps.ToDictionary(kv => kv.Level.ToString(), kv => kv),
|
||||
RankInfo = (await _globalsRepository.GetRankInfo()).Select(ri => new RankInfo(ri)).ToDictionary(ri => ri.RankId.ToString(), ri => ri)
|
||||
ClassExp = classExps,
|
||||
RankInfo = (await _globalsRepository.GetRankInfo()).Select(ri => new RankInfo(ri)).ToList(),
|
||||
DeckFormat = 1,
|
||||
CardSetIdForResourceDlView = rotationSets.Last().SetId
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace SVSim.EmulatedEntrypoint.Controllers
|
||||
/// <summary>
|
||||
/// A base controller for SVSim with helpers for getting some values.
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[Route("[controller]")]
|
||||
[ApiController]
|
||||
[Authorize(AuthenticationSchemes = SteamAuthenticationConstants.SchemeName)]
|
||||
public abstract class SVSimController : ControllerBase
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Conventions;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures controllers go to the correct swagger definition.
|
||||
/// </summary>
|
||||
public class SwaggerDefinitionConvention : IControllerModelConvention
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
var controllerNamespace = controller.ControllerType.Namespace; // eg. Controllers.V1
|
||||
var swaggerDefinition = controllerNamespace.Split('.').Last().ToLower();
|
||||
const string defaultRoute = "api/[controller]";
|
||||
|
||||
controller.ApiExplorer.GroupName = swaggerDefinition;
|
||||
foreach (SelectorModel selector in controller.Selectors)
|
||||
{
|
||||
if (selector.AttributeRouteModel != null && selector.AttributeRouteModel.Template == defaultRoute)
|
||||
{
|
||||
selector.AttributeRouteModel.Template = $"api/{swaggerDefinition}/[controller]";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,5 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
public class EmblemIdentifier
|
||||
{
|
||||
[Key("emblem_id")]
|
||||
public int EmblemId { get; set; }
|
||||
public long EmblemId { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
|
||||
[MessagePackObject]
|
||||
public class IndexRequest : BaseRequest
|
||||
{
|
||||
[Key("carrier")]
|
||||
public string Carrier { get; set; }
|
||||
|
||||
[Key("card_master_hash")]
|
||||
public string CardMasterHash { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,31 +2,83 @@ using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// Wire-shape mirrors production's <c>/check/game_start</c> response. Several fields here are
|
||||
/// NOT read by <c>Cute/GameStartCheckTask.Parse</c> (<c>now_viewer_id</c>, <c>now_name</c>,
|
||||
/// <c>now_rank</c> — those are consumed by sibling tasks); they're included because prod sends
|
||||
/// them and the boot worked when we matched prod exactly. Removing them is a regression risk
|
||||
/// even though the parse-time decompile says they're unused.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class GameStartResponse
|
||||
{
|
||||
/// <summary>The signed-in viewer's internal id. Prod always sends.</summary>
|
||||
[Key("now_viewer_id")]
|
||||
public long NowViewerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user has set a data-transfer password. Prod sends a non-null bool;
|
||||
/// <c>GameStartCheckTask.Parse</c> gates the read with <c>Keys.Contains</c>.
|
||||
/// </summary>
|
||||
[Key("is_set_transition_password")]
|
||||
public bool IsSetTransitionPassword { get; set; }
|
||||
|
||||
/// <summary>Viewer display name. Not read by GameStartCheckTask but sent by prod.</summary>
|
||||
[Key("now_name")]
|
||||
public string NowName { get; set; }
|
||||
public string NowName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Per-format rank-name map keyed by deck-format id ("1", "2", "4" observed in prod).
|
||||
/// Stub for now until rank state is persisted; pinned to RankName_010 / RankName_017
|
||||
/// (matches prod's shape).
|
||||
/// </summary>
|
||||
[Key("now_rank")]
|
||||
public Dictionary<string, string> NowRank { get; set; }
|
||||
public Dictionary<string, string> NowRank { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Tutorial progress — **sent as a string on the wire** ("100" = tutorial complete).
|
||||
/// <c>GameStartCheckTask.Parse</c> calls <c>.ToInt()</c> so LitJson coerces.
|
||||
/// </summary>
|
||||
[Key("now_tutorial_step")]
|
||||
public string NowTutorialStep { get; set; }
|
||||
public string NowTutorialStep { get; set; } = "100";
|
||||
|
||||
/// <summary>
|
||||
/// Linked social accounts. Per-entry shape in <see cref="TransitionAccountData"/>.
|
||||
/// </summary>
|
||||
[Key("transition_account_data")]
|
||||
public List<TransitionAccountData> TransitionAccountData { get; set; }
|
||||
public List<TransitionAccountData> TransitionAccountData { get; set; } = new();
|
||||
|
||||
// INTENTIONALLY OMITTED: `rewrite_viewer_id` and `account_delete_reservation_status`.
|
||||
// Both are presence-checked by the client via `Keys.Contains(...)` + `.ToInt()` with no
|
||||
// null guard. MessagePack-CSharp writes [Key] properties unconditionally (null → Nil),
|
||||
// and the System.Text.Json `WhenWritingNull` ignore only affects the plain-JSON path.
|
||||
// So including these as nullable properties is a guaranteed NRE on the encrypted client
|
||||
// path. We don't need them — the client tolerates their absence — so don't declare them.
|
||||
// Re-add only if we have a real value to send.
|
||||
|
||||
// --- Agreement / consent state (all required) ---
|
||||
|
||||
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
|
||||
[Key("tos_state")]
|
||||
public int TosState { get; set; }
|
||||
[Key("tos_id")]
|
||||
public int TosId { get; set; }
|
||||
|
||||
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
|
||||
[Key("policy_state")]
|
||||
public int PolicyState { get; set; }
|
||||
[Key("policy_id")]
|
||||
public int PolicyId { get; set; }
|
||||
[Key("kor_authority_id")]
|
||||
public int KorAuthorityId { get; set; }
|
||||
|
||||
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
|
||||
[Key("kor_authority_state")]
|
||||
public int KorAuthorityState { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Current Terms of Service document id.</summary>
|
||||
[Key("tos_id")]
|
||||
public int TosId { get; set; }
|
||||
|
||||
/// <summary>Current Privacy Policy document id.</summary>
|
||||
[Key("policy_id")]
|
||||
public int PolicyId { get; set; }
|
||||
|
||||
/// <summary>Current Korean authority consent document id.</summary>
|
||||
[Key("kor_authority_id")]
|
||||
public int KorAuthorityId { get; set; }
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ public class IndexResponse
|
||||
{
|
||||
#region Primitive Returns
|
||||
|
||||
[Key("ts_card_rotation")]
|
||||
public string TsCardRotation { get; set; } = string.Empty;
|
||||
[Key("is_beginner_mission")]
|
||||
public int IsBeginnerMission { get; set; }
|
||||
[Key("spot_point")]
|
||||
public int SpotPoint { get; set; }
|
||||
[Key("is_available_colosseum_free_entry")]
|
||||
@@ -23,29 +19,22 @@ public class IndexResponse
|
||||
[Key("room_recovery_status")]
|
||||
public int RoomRecoveryStatus { get; set; }
|
||||
[Key("is_battle_pass_period")]
|
||||
public bool IsBattlePassPeriod { get; set; }
|
||||
public int IsBattlePassPeriod { get; set; }
|
||||
[Key("card_set_id_for_resource_dl_view")]
|
||||
public int CardSetIdForResourceDlView { get; set; }
|
||||
[Key("deck_format")]
|
||||
public int DeckFormat { get; set; } = 1;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic User Data
|
||||
|
||||
/// <summary>
|
||||
/// The user's tutorial progress state.
|
||||
/// </summary>
|
||||
[Key("user_tutorial")]
|
||||
public UserTutorial UserTutorial { get; set; } = new UserTutorial();
|
||||
|
||||
/// <summary>
|
||||
/// Basic information about the user.
|
||||
/// </summary>
|
||||
|
||||
[Key("user_info")]
|
||||
public UserInfo UserInfo { get; set; } = new UserInfo();
|
||||
|
||||
/// <summary>
|
||||
/// The in-game currency information for this user.
|
||||
/// </summary>
|
||||
[Key("user_crystal_count")]
|
||||
public UserCurrency UserCurrency { get; set; } = new UserCurrency();
|
||||
|
||||
@@ -53,234 +42,163 @@ public class IndexResponse
|
||||
|
||||
#region Inventory Data
|
||||
|
||||
/// <summary>
|
||||
/// Items that the user has and how many of each.
|
||||
/// </summary>
|
||||
[Key("user_item_list")]
|
||||
public List<UserItem> UserItems { get; set; } = new List<UserItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Decks for the rotation format.
|
||||
/// </summary>
|
||||
[Key("user_item_list")]
|
||||
public List<UserItem> UserItems { get; set; } = new();
|
||||
|
||||
[Key("user_deck_rotation")]
|
||||
public UserFormatDeckInfo UserRotationDecks { get; set; } = new UserFormatDeckInfo();
|
||||
public UserFormatDeckInfo UserRotationDecks { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Decks for the unlimited format.
|
||||
/// </summary>
|
||||
[Key("user_deck_unlimited")]
|
||||
public UserFormatDeckInfo UserUnlimitedDecks { get; set; } = new UserFormatDeckInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Decks for the unlimited format.
|
||||
/// </summary>
|
||||
public UserFormatDeckInfo UserUnlimitedDecks { get; set; } = new();
|
||||
|
||||
[Key("user_deck_my_rotation")]
|
||||
public UserFormatDeckInfo UserMyRotationDecks { get; set; } = new UserFormatDeckInfo();
|
||||
public UserFormatDeckInfo UserMyRotationDecks { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The list of cards and how many of each this user has.
|
||||
/// </summary>
|
||||
[Key("user_card_list")]
|
||||
public List<UserCard> UserCards { get; set; } = new List<UserCard>();
|
||||
public List<UserCard> UserCards { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The classes a user has and their stats.
|
||||
/// </summary>
|
||||
[Key("user_class_list")]
|
||||
public List<UserClass> UserClasses { get; set; } = new List<UserClass>();
|
||||
public List<UserClass> UserClasses { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Mapping of SleeveId to a <see cref="SleeveIdentifier"/> object.
|
||||
/// Wire is an array; parser iterates by index (LoadDetail.cs:358-360).
|
||||
/// </summary>
|
||||
[Key("user_sleeve_list")]
|
||||
public Dictionary<string, SleeveIdentifier> Sleeves { get; set; } = new Dictionary<string, SleeveIdentifier>();
|
||||
public List<SleeveIdentifier> Sleeves { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The emblems available to this user.
|
||||
/// </summary>
|
||||
[Key("user_emblem_list")]
|
||||
public List<EmblemIdentifier> UserEmblems { get; set; } = new List<EmblemIdentifier>();
|
||||
public List<EmblemIdentifier> UserEmblems { get; set; } = new();
|
||||
|
||||
[Key("user_degree_list")]
|
||||
public List<DegreeIdentifier> UserDegrees { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The degrees available to this user.
|
||||
/// </summary>
|
||||
[Key("degree_id")]
|
||||
public List<DegreeIdentifier> UserDegrees { get; set; } = new List<DegreeIdentifier>();
|
||||
|
||||
/// <summary>
|
||||
/// Leader skins available to the leader.
|
||||
/// Wire is an array; parser iterates by index (LoadDetail.cs:348-356).
|
||||
/// </summary>
|
||||
[Key("user_leader_skin_list")]
|
||||
public Dictionary<string, UserLeaderSkin> LeaderSkins { get; set; } = new Dictionary<string, UserLeaderSkin>();
|
||||
|
||||
public List<UserLeaderSkin> LeaderSkins { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Backgrounds for 'My Page' the user has collected.
|
||||
/// Wire is string[]; parser calls .ToString() on each element (LoadDetail.cs:387-392).
|
||||
/// </summary>
|
||||
[Key("user_mypage_list")]
|
||||
public List<int> MyPageBackgrounds { get; set; } = new List<int>();
|
||||
public List<string> MyPageBackgrounds { get; set; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Advanced Player Data
|
||||
|
||||
/// <summary>
|
||||
/// Maps a deck format (as a string) to info about ranked for that format.
|
||||
/// Wire is an array of 5 entries; parser uses deck_format as discriminator
|
||||
/// (LoadDetail.cs:527-538).
|
||||
/// </summary>
|
||||
[Key("user_rank")]
|
||||
public Dictionary<string, UserRankInfo> UserRankInfo { get; set; } = new Dictionary<string, UserRankInfo>();
|
||||
public List<UserRankInfo> UserRankInfo { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The number of ranked matches for each class the user has played.
|
||||
/// </summary>
|
||||
[Key("user_rank_match_list")]
|
||||
public List<UserRankedMatches> UserRankedMatches { get; set; } = new List<UserRankedMatches>();
|
||||
public List<UserRankedMatches> UserRankedMatches { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The daily login bonuses currently going on, including if the player should receive the next reward for any.
|
||||
/// </summary>
|
||||
[Key("daily_login_bonus")]
|
||||
public DailyLoginBonus DailyLoginBonus { get; set; } = new DailyLoginBonus();
|
||||
public DailyLoginBonus DailyLoginBonus { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// User configuration for the arena.
|
||||
/// </summary>
|
||||
[Key("challenge_config")]
|
||||
public ArenaConfig ArenaConfig { get; set; } = new ArenaConfig();
|
||||
public ArenaConfig ArenaConfig { get; set; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Global Data
|
||||
|
||||
/// <summary>
|
||||
/// Cards that have had their red ether values overriden.
|
||||
/// </summary>
|
||||
[Key("red_ether_overwrite_list")]
|
||||
public List<RedEtherOverride> RedEtherOverrides { get; set; } = new List<RedEtherOverride>();
|
||||
public List<RedEtherOverride> RedEtherOverrides { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Cards that are currently undergoing maintenance.
|
||||
/// Wire is a flat number[]; parser passes it straight to SetMaintenanceCardIds
|
||||
/// (LoadDetail.cs:165).
|
||||
/// </summary>
|
||||
[Key("maintenance_card_list")]
|
||||
public List<CardIdentifier> MaintenanceCards { get; set; } = new List<CardIdentifier>();
|
||||
|
||||
/// <summary>
|
||||
/// The arena formats currently available.
|
||||
/// </summary>
|
||||
public List<long> MaintenanceCards { get; set; } = new();
|
||||
|
||||
[Key("arena_info")]
|
||||
public List<ArenaInfo> ArenaInfos { get; set; } = new List<ArenaInfo>();
|
||||
public List<ArenaInfo> ArenaInfos { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary of rank id to information about that rank.
|
||||
/// Wire is an array; client uses POSITIONAL logic (index >= 24 = master ranks,
|
||||
/// LoadDetail.cs:417-422). Order must match repository's ordering.
|
||||
/// </summary>
|
||||
[Key("rank_info")]
|
||||
public Dictionary<string, RankInfo> RankInfo { get; set; } = new Dictionary<string, RankInfo>();
|
||||
public List<RankInfo> RankInfo { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary mapping a class level to information about that level.
|
||||
/// Wire is an array; parser iterates by index (LoadDetail.cs:425-434).
|
||||
/// </summary>
|
||||
[Key("class_exp")]
|
||||
public Dictionary<string, ClassExp> ClassExp { get; set; } = new Dictionary<string, ClassExp>();
|
||||
public List<ClassExp> ClassExp { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Card ids that should not show up on loading screen tips.
|
||||
/// </summary>
|
||||
[Key("loading_exclusion_card_list")]
|
||||
public List<long> LoadingTipCardExclusions { get; set; } = new List<long>();
|
||||
public List<long> LoadingTipCardExclusions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The default settings for every user.
|
||||
/// </summary>
|
||||
[Key("default_setting")]
|
||||
public DefaultSettings DefaultSettings { get; set; } = new DefaultSettings();
|
||||
public DefaultSettings DefaultSettings { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Any cards that are restricted in unlimited, and the maximum count that can be run of the card.
|
||||
/// </summary>
|
||||
[Key("unlimited_restricted_base_card_id_list")]
|
||||
public Dictionary<string, int> UnlimitedBanList { get; set; } = new Dictionary<string, int>();
|
||||
public Dictionary<string, int> UnlimitedBanList { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets currently available in rotation.
|
||||
/// Client unconditionally accesses [1] and [Count-1] (LoadDetail.cs:184) — list MUST
|
||||
/// have at least 2 entries or the client crashes.
|
||||
/// </summary>
|
||||
[Key("rotation_card_set_id_list")]
|
||||
public List<CardSetIdentifier> RotationSets { get; set; } = new List<CardSetIdentifier>();
|
||||
public List<CardSetIdentifier> RotationSets { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Allows cards out of your 'My Rotation' to still be used. TODO investigate
|
||||
/// Wire is a flat number[]; parser iterates and reads .ToInt() (LoadDetail.cs:463-468).
|
||||
/// </summary>
|
||||
[Key("reprinted_base_card_ids")]
|
||||
public Dictionary<string, long> ReprintedCards { get; set; } = new Dictionary<string, long>();
|
||||
public List<long> ReprintedCards { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Something to do with destroying cards. TODO investigate
|
||||
/// </summary>
|
||||
[Key("spot_cards")]
|
||||
public Dictionary<string, int> SpotCards { get; set; } = new Dictionary<string, int>();
|
||||
public Dictionary<string, int> SpotCards { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Info about the next set to be released? TODO investigate
|
||||
/// </summary>
|
||||
[Key("pre_release_info")]
|
||||
public PreReleaseInfo? PreReleaseInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Information for the 'My Rotation' mode.
|
||||
/// </summary>
|
||||
|
||||
[Key("my_rotation_info")]
|
||||
public MyRotationInfo? MyRotationInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about some avatar mode? TODO investigate
|
||||
/// </summary>
|
||||
|
||||
[Key("avatar_info")]
|
||||
public MyRotationInfo? AvatarRotationInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of features that are undergoing maintenance.
|
||||
/// </summary>
|
||||
|
||||
[Key("feature_maintenance_list")]
|
||||
public List<FeatureMaintenance> FeatureMaintenances { get; set; } = new List<FeatureMaintenance>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Special deals on crystal purchases.
|
||||
/// </summary>
|
||||
public List<FeatureMaintenance> FeatureMaintenances { get; set; } = new();
|
||||
|
||||
[Key("special_crystal_info")]
|
||||
public List<SpecialCrystalInfo> SpecialCrystalInfos { get; set; } = new List<SpecialCrystalInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Current battle pass levels and required points for each.
|
||||
/// </summary>
|
||||
public List<SpecialCrystalInfo> SpecialCrystalInfos { get; set; } = new();
|
||||
|
||||
[Key("battle_pass_level_info")]
|
||||
public Dictionary<string, BattlePassLevel>? BattlePassLevelInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Battlefields that can currently be picked.
|
||||
/// Wire is string[]; parser calls .ToString() on each element (LoadDetail.cs:493-499).
|
||||
/// </summary>
|
||||
[Key("open_battle_field_id_list")]
|
||||
public Dictionary<string, int> OpenBattlefieldIds { get; set; } = new Dictionary<string, int>();
|
||||
public List<string> OpenBattlefieldIds { get; set; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Misc Data
|
||||
|
||||
/// <summary>
|
||||
/// What loot box features are disabled for this user.
|
||||
/// </summary>
|
||||
[Key("loot_box_regulation")]
|
||||
public LootBoxRegulations LootBoxRegulations { get; set; } = new LootBoxRegulations();
|
||||
public LootBoxRegulations LootBoxRegulations { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Something about whether the user has an invite notification.
|
||||
/// </summary>
|
||||
[Key("gathering_info")]
|
||||
public GatheringInfo GatheringInfo { get; set; } = new GatheringInfo();
|
||||
public GatheringInfo GatheringInfo { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// User configuration.
|
||||
/// Spec is unclear whether this is returned at /load/index or only at /config/* endpoints
|
||||
/// (load-index.md line 390). Pending live-capture confirmation; harmless extra.
|
||||
/// </summary>
|
||||
[Key("user_config")]
|
||||
public UserConfig UserConfig { get; set; } = new UserConfig();
|
||||
public UserConfig UserConfig { get; set; } = new();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||
[MessagePackObject]
|
||||
public class SpecialTitleCheckResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Numeric string. "0"/"1" are the built-in default title screens; any other value
|
||||
/// is treated as an asset-bundle id. When omitted, the client defaults to "0".
|
||||
/// </summary>
|
||||
[Key("title_image_id")]
|
||||
public int TitleImageId { get; set; }
|
||||
[Key("title_sound_id")]
|
||||
public int TitleSoundId { get; set; }
|
||||
}
|
||||
public string? TitleImageId { get; set; }
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@ public class SleeveIdentifier
|
||||
/// The id of the sleeve.
|
||||
/// </summary>
|
||||
[Key("sleeve_id")]
|
||||
public int SleeveId { get; set; }
|
||||
public long SleeveId { get; set; }
|
||||
}
|
||||
@@ -2,15 +2,35 @@ using MessagePack;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||
|
||||
/// <summary>
|
||||
/// Per-link entry in <c>transition_account_data</c>. Production sends three string fields per
|
||||
/// entry even though <c>GameStartCheckTask.Parse</c> only reads <c>social_account_type</c>.
|
||||
/// The extra two are read by adjacent tasks (<c>GetGameDataByTransitionCode</c>,
|
||||
/// <c>GetGameDataBySocialAccountTask</c>) — kept here so the wire matches prod regardless of
|
||||
/// which task ends up consuming the payload.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class TransitionAccountData
|
||||
{
|
||||
/// <summary>
|
||||
/// The social provider's account id (e.g. SteamID as a string). Sent as string on the wire.
|
||||
/// </summary>
|
||||
[Key("social_account_id")]
|
||||
public string SocialAccountId { get; set; }
|
||||
|
||||
public string? SocialAccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <c>Cute/CuteNetworkDefine.ACCOUNT_TYPE</c> enum, **sent as string** on the wire even
|
||||
/// though it's numeric. <c>GameStartCheckTask.Parse</c> calls <c>.ToInt()</c> on it so
|
||||
/// LitJson coerces transparently — but matching prod's string form makes us safer against
|
||||
/// future client paths that might compare it as a literal.
|
||||
/// 1=GooglePlay, 2=GameCenter, 3=Facebook, 4=DMM, 5=Steam, 6=AppleID.
|
||||
/// </summary>
|
||||
[Key("social_account_type")]
|
||||
public string SocialAccountType { get; set; }
|
||||
|
||||
public string? SocialAccountType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The viewer id this social connection is linked to. Sent as string.
|
||||
/// </summary>
|
||||
[Key("connected_viewer_id")]
|
||||
public string ConnectedViewerId { get; set; }
|
||||
}
|
||||
public string? ConnectedViewerId { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@ public class UserInfo
|
||||
public DateTime LastPlayTime { get; set; }
|
||||
[Key("is_received_two_pick_mission")]
|
||||
public int HasReceivedPickTwoMission { get; set; }
|
||||
/// <summary>
|
||||
/// Birth date as yyyy-MM-dd. Parser does .ToString() on this field (LoadDetail.cs:203).
|
||||
/// Format verified against live capture pending.
|
||||
/// </summary>
|
||||
[Key("birth")]
|
||||
public long Birthday { get; set; }
|
||||
public string Birthday { get; set; } = string.Empty;
|
||||
[Key("selected_emblem_id")]
|
||||
public long SelectedEmblemId { get; set; }
|
||||
[Key("selected_degree_id")]
|
||||
@@ -45,7 +49,7 @@ public class UserInfo
|
||||
this.MaxFriend = viewer.Info.MaxFriends;
|
||||
this.LastPlayTime = viewer.LastLogin;
|
||||
this.HasReceivedPickTwoMission = viewer.MissionData.HasReceivedPickTwoMission ? 1 : 0;
|
||||
this.Birthday = viewer.Info.BirthDate.Ticks;
|
||||
this.Birthday = viewer.Info.BirthDate.ToString("yyyy-MM-dd");
|
||||
this.SelectedEmblemId = viewer.Info.SelectedEmblem.Id;
|
||||
this.SelectedDegreeId = viewer.Info.SelectedDegree.Id;
|
||||
this.MissionChangeTime = viewer.MissionData.MissionChangeTime;
|
||||
@@ -53,4 +57,4 @@ public class UserInfo
|
||||
this.IsOfficial = viewer.Info.IsOfficial ? 1 : 0;
|
||||
this.IsOfficialMarkDisplayed = viewer.Info.IsOfficialMarkDisplayed ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ public class UserLeaderSkin
|
||||
{
|
||||
this.Id = leaderSkin.Id;
|
||||
this.Name = leaderSkin.Name;
|
||||
this.ClassId = leaderSkin.Class.Id;
|
||||
// Class is nullable — BaseDataSeeder maps CSV class_chara_id=0 to null. Fall back to
|
||||
// the FK column (also nullable) and finally 0 for class-agnostic skins.
|
||||
this.ClassId = leaderSkin.Class?.Id ?? leaderSkin.ClassId ?? 0;
|
||||
this.EmoteId = leaderSkin.EmoteId;
|
||||
this.IsOwned = isOwned;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class UserRankInfo
|
||||
[Key("is_master_rank")]
|
||||
public int IsMasterRank { get; set; }
|
||||
[Key("is_grand_master_rank")]
|
||||
public bool IsGrandMasterRank { get; set; }
|
||||
public int IsGrandMasterRank { get; set; }
|
||||
[Key("master_point")]
|
||||
public int MasterPoints { get; set; }
|
||||
[Key("period_grand_master_point")]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
using System.Reflection;
|
||||
using DCGEngine.Database.Configuration;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using SVSim.Database.Repositories.Card;
|
||||
using SVSim.Database.Repositories.Collectibles;
|
||||
using SVSim.Database.Repositories.Deck;
|
||||
using SVSim.Database.Repositories.Globals;
|
||||
using SVSim.Database.Repositories.Viewer;
|
||||
using SVSim.EmulatedEntrypoint.Middlewares;
|
||||
@@ -21,7 +20,14 @@ public class Program
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers().AddJsonOptions(opt =>
|
||||
{
|
||||
// Production omits null/optional fields entirely; the client uses
|
||||
// `Keys.Contains(name)` as a presence check and calls `.ToInt()` (etc.) on the
|
||||
// value without a null guard. Emitting `"key":null` makes Contains return true and
|
||||
// crashes the client. Match prod by dropping nulls during serialization.
|
||||
opt.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
});
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
@@ -38,16 +44,14 @@ public class Program
|
||||
});
|
||||
builder.Services.AddTransient<IViewerRepository, ViewerRepository>();
|
||||
builder.Services.AddTransient<ICardRepository, CardRepository>();
|
||||
builder.Services.AddTransient<ICollectionRepository, CollectionRepository>();
|
||||
builder.Services.AddTransient<IGlobalsRepository, GlobalsRepository>();
|
||||
builder.Services.AddTransient<IDeckRepository, DeckRepository>();
|
||||
|
||||
#endregion
|
||||
|
||||
builder.Services.AddTransient<ShadowverseTranslationMiddleware>();
|
||||
builder.Services.AddTransient<SessionidMappingMiddleware>();
|
||||
builder.Services.Configure<DCGEDatabaseConfiguration>(opt =>
|
||||
{
|
||||
opt.DbSetSearchAssemblies = new List<Assembly> { Assembly.GetAssembly(typeof(SVSimDbContext)) };
|
||||
});
|
||||
builder.Services.AddSingleton<ShadowverseSessionService>();
|
||||
builder.Services.AddSingleton<SteamSessionService>();
|
||||
builder.Services.AddAuthentication()
|
||||
@@ -60,11 +64,16 @@ public class Program
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Update database
|
||||
// Update database (skipped for non-relational providers, e.g. InMemory in tests, and
|
||||
// skipped under the "Testing" environment where the test fixture has already called
|
||||
// EnsureCreated against a SQLite in-memory DB — the Postgres migrations would fail there).
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
dbContext.UpdateDatabase();
|
||||
if (dbContext.Database.IsRelational() && !app.Environment.IsEnvironment("Testing"))
|
||||
{
|
||||
dbContext.UpdateDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
app.UseHttpLogging();
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Configuration\" />
|
||||
<Folder Include="Data\" />
|
||||
<Folder Include="Utility\" />
|
||||
<Content Include="Data\*.csv">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Steamworks;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Services;
|
||||
|
||||
public class SteamSessionService : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ulong> _validatedSessionTickets;
|
||||
private readonly ConcurrentDictionary<string, ulong> _validatedSessionTickets = new();
|
||||
private readonly object _initLock = new();
|
||||
private bool _steamInitialized;
|
||||
|
||||
private const int ShadowVerseAppId = 453480;
|
||||
|
||||
public SteamSessionService()
|
||||
{
|
||||
_validatedSessionTickets = new ConcurrentDictionary<string, ulong>();
|
||||
SteamServer.Init(ShadowVerseAppId, new SteamServerInit
|
||||
{
|
||||
GamePort = default,
|
||||
QueryPort = default
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates if a given session ticket is valid, and matches up with the given steamid.
|
||||
@@ -34,6 +24,8 @@ public class SteamSessionService : IDisposable
|
||||
return storedSteamId == steamId;
|
||||
}
|
||||
|
||||
EnsureSteamInitialized();
|
||||
|
||||
List<byte> ticketBytes = new List<byte>();
|
||||
for (int i = 0; i < ticket.Length; i += 2)
|
||||
{
|
||||
@@ -45,12 +37,30 @@ public class SteamSessionService : IDisposable
|
||||
{
|
||||
_validatedSessionTickets.TryAdd(ticket, steamId);
|
||||
}
|
||||
|
||||
|
||||
return steamCheckResults;
|
||||
}
|
||||
|
||||
private void EnsureSteamInitialized()
|
||||
{
|
||||
if (_steamInitialized) return;
|
||||
lock (_initLock)
|
||||
{
|
||||
if (_steamInitialized) return;
|
||||
SteamServer.Init(ShadowVerseAppId, new SteamServerInit
|
||||
{
|
||||
GamePort = default,
|
||||
QueryPort = default
|
||||
});
|
||||
_steamInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SteamServer.Shutdown();
|
||||
if (_steamInitialized)
|
||||
{
|
||||
SteamServer.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user