More features

This commit is contained in:
gamer147
2026-05-23 14:18:01 -04:00
parent b2024af852
commit 6b70850b7b
59 changed files with 862 additions and 42033 deletions

View File

@@ -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
};
}
}

View File

@@ -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
};
}
}
}

View File

@@ -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