feat(config): wire /config/update + /config/update_challenge_config (closes family 4/4)
is_skip_gacha_effect, use_challenge_two_pick_premium_card, and challenge_two_pick_sleeve_id all persist on ViewerInfo and round-trip through /load/index. The two challenge fields move from the global ChallengeConfig section to ViewerInfo — they're viewer preferences, not server-wide policy. Premium card defaults to 0; sleeve falls back to DefaultLoadoutConfig.SleeveId (3000011) when unset. MatchContextBuilder's TK2 sleeve read switches to the same viewer-scoped path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"use_two_pick_premium_card": false,
|
||||
"two_pick_sleeve_id": 3000011,
|
||||
"last_card_pack_set_id": 10015,
|
||||
"card_pool_name": "Throwback Rotation",
|
||||
"card_pool_url": "",
|
||||
|
||||
@@ -40,8 +40,6 @@ public class RotationConfigImporter
|
||||
{
|
||||
await UpsertSection<ChallengeConfig>(context, ChallengeConfig.ShippedDefaults, c =>
|
||||
{
|
||||
c.UseTwoPickPremiumCard = cc.UseTwoPickPremiumCard;
|
||||
c.TwoPickSleeveId = cc.TwoPickSleeveId;
|
||||
c.LastCardPackSetId = cc.LastCardPackSetId;
|
||||
c.CardPoolName = cc.CardPoolName;
|
||||
c.CardPoolUrl = cc.CardPoolUrl;
|
||||
|
||||
@@ -19,9 +19,6 @@ public sealed class RotationConfigSeed
|
||||
/// <summary>Mirrors <c>seeds/challenge-config.json</c>. Drives the Challenge <c>GameConfigSection</c>.</summary>
|
||||
public sealed class ChallengeConfigSeed
|
||||
{
|
||||
[JsonPropertyName("use_two_pick_premium_card")] public bool UseTwoPickPremiumCard { get; set; }
|
||||
[JsonPropertyName("two_pick_sleeve_id")] public long TwoPickSleeveId { get; set; }
|
||||
|
||||
[JsonPropertyName("last_card_pack_set_id")] public int LastCardPackSetId { get; set; }
|
||||
[JsonPropertyName("card_pool_name")] public string CardPoolName { get; set; } = "";
|
||||
[JsonPropertyName("card_pool_url")] public string CardPoolUrl { get; set; } = "";
|
||||
|
||||
4656
SVSim.Database/Migrations/20260613152024_ConfigViewerOverrides.Designer.cs
generated
Normal file
4656
SVSim.Database/Migrations/20260613152024_ConfigViewerOverrides.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SVSim.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ConfigViewerOverrides : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "Info_ChallengeTwoPickSleeveId",
|
||||
table: "Viewers",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Info_IsSkipGachaEffect",
|
||||
table: "Viewers",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Info_UseChallengeTwoPickPremiumCard",
|
||||
table: "Viewers",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Info_ChallengeTwoPickSleeveId",
|
||||
table: "Viewers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Info_IsSkipGachaEffect",
|
||||
table: "Viewers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Info_UseChallengeTwoPickPremiumCard",
|
||||
table: "Viewers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4336,6 +4336,9 @@ namespace SVSim.Database.Migrations
|
||||
b1.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b1.Property<long>("ChallengeTwoPickSleeveId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b1.Property<string>("CountryCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -4352,6 +4355,9 @@ namespace SVSim.Database.Migrations
|
||||
b1.Property<bool>("IsPrizePreferred")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<bool>("IsSkipGachaEffect")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.Property<int>("MaxFriends")
|
||||
.HasColumnType("integer");
|
||||
|
||||
@@ -4361,6 +4367,9 @@ namespace SVSim.Database.Migrations
|
||||
b1.Property<int>("SelectedEmblemId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b1.Property<bool>("UseChallengeTwoPickPremiumCard")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b1.HasKey("ViewerId");
|
||||
|
||||
b1.HasIndex("SelectedDegreeId");
|
||||
|
||||
@@ -3,9 +3,6 @@ namespace SVSim.Database.Models.Config;
|
||||
[ConfigSection("Challenge")]
|
||||
public class ChallengeConfig
|
||||
{
|
||||
public bool UseTwoPickPremiumCard { get; set; }
|
||||
public long TwoPickSleeveId { get; set; }
|
||||
|
||||
// Wire-mirrored fields from format_info (ChallengeData.cs parser)
|
||||
public int LastCardPackSetId { get; set; }
|
||||
public string CardPoolName { get; set; } = "";
|
||||
|
||||
@@ -12,6 +12,9 @@ public class ViewerInfo
|
||||
public bool IsOfficialMarkDisplayed { get; set; }
|
||||
public bool IsFoilPreferred { get; set; }
|
||||
public bool IsPrizePreferred { get; set; }
|
||||
public bool IsSkipGachaEffect { get; set; }
|
||||
public bool UseChallengeTwoPickPremiumCard { get; set; }
|
||||
public long ChallengeTwoPickSleeveId { get; set; }
|
||||
|
||||
#region Navigation Properties
|
||||
|
||||
|
||||
@@ -39,4 +39,29 @@ public class ConfigController : SVSimController
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
[HttpPost("update")]
|
||||
public async Task<ActionResult<EmptyResponse>> Update(
|
||||
ConfigUpdateRequest request, CancellationToken ct)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var viewer = await _db.Viewers.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
||||
if (viewer is null) return Unauthorized();
|
||||
viewer.Info.IsSkipGachaEffect = request.IsSkipGachaEffect != 0;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
[HttpPost("update_challenge_config")]
|
||||
public async Task<ActionResult<EmptyResponse>> UpdateChallengeConfig(
|
||||
ConfigUpdateChallengeConfigRequest request, CancellationToken ct)
|
||||
{
|
||||
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||
var viewer = await _db.Viewers.FirstOrDefaultAsync(v => v.Id == viewerId, ct);
|
||||
if (viewer is null) return Unauthorized();
|
||||
viewer.Info.UseChallengeTwoPickPremiumCard = request.UseChallengeTwoPickPremiumCard != 0;
|
||||
viewer.Info.ChallengeTwoPickSleeveId = request.ChallengeTwoPickSleeveId;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,8 +248,10 @@ public class LoadController : SVSimController
|
||||
}).ToList(),
|
||||
ArenaConfig = new ArenaConfig
|
||||
{
|
||||
UseChallengePickTwoPremiumCard = challenge.UseTwoPickPremiumCard ? 1 : 0,
|
||||
ChallengePickTwoCardSleeve = (int)challenge.TwoPickSleeveId,
|
||||
UseChallengePickTwoPremiumCard = viewer.Info.UseChallengeTwoPickPremiumCard ? 1 : 0,
|
||||
ChallengePickTwoCardSleeve = (int)(viewer.Info.ChallengeTwoPickSleeveId != 0
|
||||
? viewer.Info.ChallengeTwoPickSleeveId
|
||||
: defaultLoadout.SleeveId),
|
||||
},
|
||||
ArenaInfos = await BuildArenaInfosAsync(viewer.Id),
|
||||
RotationSets = rotationSets,
|
||||
@@ -257,6 +259,7 @@ public class LoadController : SVSimController
|
||||
{
|
||||
IsFoilPreferred = viewer.Info.IsFoilPreferred ? 1 : 0,
|
||||
IsPrizePreferred = viewer.Info.IsPrizePreferred ? 1 : 0,
|
||||
IsSkipGachaEffect = viewer.Info.IsSkipGachaEffect ? 1 : 0,
|
||||
},
|
||||
OpenBattlefieldIds = (await _globalsRepository.GetBattlefields(true))
|
||||
.Select(bf => bf.Id.ToString()).ToList(),
|
||||
|
||||
@@ -115,6 +115,7 @@ public class MyPageController : SVSimController
|
||||
{
|
||||
IsFoilPreferred = viewer.Info.IsFoilPreferred ? 1 : 0,
|
||||
IsPrizePreferred = viewer.Info.IsPrizePreferred ? 1 : 0,
|
||||
IsSkipGachaEffect = viewer.Info.IsSkipGachaEffect ? 1 : 0,
|
||||
},
|
||||
Quest = new Quest(), // TODO(mypage-stub): active Quest event + viewer flags
|
||||
MasterPointRankingPeriod = BuildMasterPointRankingPeriod(masterPointPeriod),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using MessagePack;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
|
||||
[MessagePackObject]
|
||||
public class ConfigUpdateChallengeConfigRequest : BaseRequest
|
||||
{
|
||||
[JsonPropertyName("use_challenge_two_pick_premium_card")]
|
||||
[Key("use_challenge_two_pick_premium_card")]
|
||||
public int UseChallengeTwoPickPremiumCard { get; set; }
|
||||
|
||||
[JsonPropertyName("challenge_two_pick_sleeve_id")]
|
||||
[Key("challenge_two_pick_sleeve_id")]
|
||||
public long ChallengeTwoPickSleeveId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using MessagePack;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||
|
||||
[MessagePackObject]
|
||||
public class ConfigUpdateRequest : BaseRequest
|
||||
{
|
||||
[JsonPropertyName("is_skip_gacha_effect")]
|
||||
[Key("is_skip_gacha_effect")]
|
||||
public int IsSkipGachaEffect { get; set; }
|
||||
}
|
||||
@@ -27,4 +27,7 @@ public class UserConfig
|
||||
[JsonPropertyName("is_prize_preferred")]
|
||||
[Key("is_prize_preferred")]
|
||||
public int IsPrizePreferred { get; set; }
|
||||
[JsonPropertyName("is_skip_gacha_effect")]
|
||||
[Key("is_skip_gacha_effect")]
|
||||
public int IsSkipGachaEffect { get; set; }
|
||||
}
|
||||
@@ -39,7 +39,6 @@ public class MatchContextBuilder : IMatchContextBuilder
|
||||
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
|
||||
@@ -51,6 +50,11 @@ public class MatchContextBuilder : IMatchContextBuilder
|
||||
var charaId = run.LeaderSkinId != 0
|
||||
? run.LeaderSkinId.ToString()
|
||||
: run.ClassId.ToString();
|
||||
// TK2-specific cosmetic source; falls back to the global default sleeve if the
|
||||
// viewer hasn't set a challenge-specific one via /config/update_challenge_config.
|
||||
var twoPickSleeveId = viewer.Info.ChallengeTwoPickSleeveId != 0
|
||||
? viewer.Info.ChallengeTwoPickSleeveId
|
||||
: defaults.SleeveId;
|
||||
|
||||
return new MatchContext(
|
||||
SelfDeckCardIds: deck,
|
||||
@@ -61,7 +65,7 @@ public class MatchContextBuilder : IMatchContextBuilder
|
||||
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(),
|
||||
SleeveId: twoPickSleeveId.ToString(),
|
||||
EmblemId: emblemId,
|
||||
DegreeId: degreeId,
|
||||
// Hardcoded v1; needs equipped-MyPageBackground lookup (see spec §Deferred).
|
||||
|
||||
@@ -53,4 +53,74 @@ public class ConfigControllerPreferencesTests
|
||||
var cfg = doc.RootElement.GetProperty("user_config");
|
||||
Assert.That(cfg.GetProperty("is_prize_preferred").GetInt32(), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Update_persists_is_skip_gacha_effect_and_appears_in_load_index()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync(tutorialState: 0);
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var setJson = """{"is_skip_gacha_effect":1,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
|
||||
var setResp = await client.PostAsync("/config/update",
|
||||
new StringContent(setJson, Encoding.UTF8, "application/json"));
|
||||
var setBody = await setResp.Content.ReadAsStringAsync();
|
||||
Assert.That(setResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), setBody);
|
||||
|
||||
var loadJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"none","card_master_hash":""}""";
|
||||
var loadResp = await client.PostAsync("/load/index",
|
||||
new StringContent(loadJson, Encoding.UTF8, "application/json"));
|
||||
var body = await loadResp.Content.ReadAsStringAsync();
|
||||
Assert.That(loadResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var cfg = doc.RootElement.GetProperty("user_config");
|
||||
Assert.That(cfg.GetProperty("is_skip_gacha_effect").GetInt32(), Is.EqualTo(1), body);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task UpdateChallengeConfig_round_trips_through_load_index()
|
||||
{
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync(tutorialState: 0);
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var setJson = """{"use_challenge_two_pick_premium_card":1,"challenge_two_pick_sleeve_id":3000099,"viewer_id":"0","steam_id":0,"steam_session_ticket":""}""";
|
||||
var setResp = await client.PostAsync("/config/update_challenge_config",
|
||||
new StringContent(setJson, Encoding.UTF8, "application/json"));
|
||||
var setBody = await setResp.Content.ReadAsStringAsync();
|
||||
Assert.That(setResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), setBody);
|
||||
|
||||
var loadJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"none","card_master_hash":""}""";
|
||||
var loadResp = await client.PostAsync("/load/index",
|
||||
new StringContent(loadJson, Encoding.UTF8, "application/json"));
|
||||
var body = await loadResp.Content.ReadAsStringAsync();
|
||||
Assert.That(loadResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var challenge = doc.RootElement.GetProperty("challenge_config");
|
||||
Assert.That(challenge.GetProperty("use_challenge_two_pick_premium_card").GetInt32(), Is.EqualTo(1), body);
|
||||
Assert.That(challenge.GetProperty("challenge_two_pick_sleeve_id").GetInt32(), Is.EqualTo(3000099), body);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LoadIndex_challenge_config_falls_back_to_default_sleeve_when_viewer_unset()
|
||||
{
|
||||
// Fresh viewer never touched /config/update_challenge_config — premium card defaults
|
||||
// to 0 and sleeve falls back to DefaultLoadoutConfig.SleeveId (3000011).
|
||||
using var factory = new SVSimTestFactory();
|
||||
long viewerId = await factory.SeedViewerAsync(tutorialState: 0);
|
||||
using var client = factory.CreateAuthenticatedClient(viewerId);
|
||||
|
||||
var loadJson = """{"viewer_id":"0","steam_id":0,"steam_session_ticket":"","carrier":"none","card_master_hash":""}""";
|
||||
var loadResp = await client.PostAsync("/load/index",
|
||||
new StringContent(loadJson, Encoding.UTF8, "application/json"));
|
||||
var body = await loadResp.Content.ReadAsStringAsync();
|
||||
Assert.That(loadResp.StatusCode, Is.EqualTo(HttpStatusCode.OK), body);
|
||||
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var challenge = doc.RootElement.GetProperty("challenge_config");
|
||||
Assert.That(challenge.GetProperty("use_challenge_two_pick_premium_card").GetInt32(), Is.EqualTo(0), body);
|
||||
Assert.That(challenge.GetProperty("challenge_two_pick_sleeve_id").GetInt32(), Is.EqualTo(3000011), body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,9 @@ public class MatchContextBuilderTests
|
||||
// Hardcoded v1 fixtures (see spec §Deferred plumbing)
|
||||
Assert.That(ctx.CardMasterName, Is.EqualTo("card_master_node_10015"));
|
||||
Assert.That(ctx.FieldId, Is.EqualTo(43));
|
||||
// Sleeve comes from ChallengeConfig.TwoPickSleeveId — ShippedDefaults is 0.
|
||||
Assert.That(ctx.SleeveId, Is.EqualTo("0"));
|
||||
// Sleeve falls back to DefaultLoadoutConfig.SleeveId when the viewer hasn't set
|
||||
// a challenge-specific one via /config/update_challenge_config.
|
||||
Assert.That(ctx.SleeveId, Is.EqualTo("3000011"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
Reference in New Issue
Block a user