Compare commits
36 Commits
c303d3040d
...
39b38e3c80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39b38e3c80 | ||
|
|
0f44a3482c | ||
|
|
7ef5f03eb3 | ||
|
|
a5999a3e9c | ||
|
|
559a170957 | ||
|
|
f237851e42 | ||
|
|
6a03ff1bf6 | ||
|
|
529fd13668 | ||
|
|
26bb0ac268 | ||
|
|
68367db214 | ||
|
|
7be0dabf87 | ||
|
|
859980af02 | ||
|
|
c8ee1e487f | ||
|
|
f85589d208 | ||
|
|
30874c681f | ||
|
|
dffd7a9746 | ||
|
|
8e35501954 | ||
|
|
5693ec0302 | ||
|
|
640a77ec6c | ||
|
|
b65a437102 | ||
|
|
574e9ca58b | ||
|
|
df65b7a9c8 | ||
|
|
c9534d8fac | ||
|
|
aad604a589 | ||
|
|
b38be1d953 | ||
|
|
6fbf7cbc94 | ||
|
|
8fd6bc10c1 | ||
|
|
90cc5a9f5d | ||
|
|
6db800f286 | ||
|
|
5df1822217 | ||
|
|
f486c15d32 | ||
|
|
8da91783b1 | ||
|
|
6a66170677 | ||
|
|
ebba3c0eef | ||
|
|
062adefb99 | ||
|
|
9988ed85df |
638
SVSim.Bootstrap/Data/seeds/achievement-catalog.json
Normal file
638
SVSim.Bootstrap/Data/seeds/achievement-catalog.json
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"achievement_type": 1,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win 5 ranked matches as Forestcraft",
|
||||||
|
"require_number": 5,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 1,
|
||||||
|
"event_type": "ranked_win:forestcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 2,
|
||||||
|
"level": 2,
|
||||||
|
"name": "Win 20 ranked matches as Swordcraft",
|
||||||
|
"require_number": 20,
|
||||||
|
"reward_type": 1,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 2,
|
||||||
|
"event_type": "ranked_win:swordcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 3,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win 5 ranked matches as Runecraft",
|
||||||
|
"require_number": 5,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 3,
|
||||||
|
"event_type": "ranked_win:runecraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 4,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win 5 ranked matches as Dragoncraft",
|
||||||
|
"require_number": 5,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 4,
|
||||||
|
"event_type": "ranked_win:dragoncraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 5,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win 5 ranked matches as Shadowcraft",
|
||||||
|
"require_number": 5,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 5,
|
||||||
|
"event_type": "ranked_win:shadowcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 6,
|
||||||
|
"level": 3,
|
||||||
|
"name": "Win 50 ranked matches as Bloodcraft",
|
||||||
|
"require_number": 50,
|
||||||
|
"reward_type": 8,
|
||||||
|
"reward_detail_id": 106001,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 6,
|
||||||
|
"event_type": "ranked_win:bloodcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 7,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win 5 ranked matches as Havencraft",
|
||||||
|
"require_number": 5,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 7,
|
||||||
|
"event_type": "ranked_win:havencraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 8,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win 5 ranked matches as Portalcraft",
|
||||||
|
"require_number": 5,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 8,
|
||||||
|
"event_type": "ranked_win:portalcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 11,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Reach level 10 in Forestcraft",
|
||||||
|
"require_number": 10,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 9,
|
||||||
|
"event_type": "class_level_up:forestcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 12,
|
||||||
|
"level": 6,
|
||||||
|
"name": "Reach level 35 in Swordcraft",
|
||||||
|
"require_number": 35,
|
||||||
|
"reward_type": 5,
|
||||||
|
"reward_detail_id": 100211061,
|
||||||
|
"reward_number": 3,
|
||||||
|
"order_num": 10,
|
||||||
|
"event_type": "class_level_up:swordcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 12,
|
||||||
|
"level": 7,
|
||||||
|
"name": "Reach level 40 in Swordcraft",
|
||||||
|
"require_number": 40,
|
||||||
|
"reward_type": 5,
|
||||||
|
"reward_detail_id": 100214011,
|
||||||
|
"reward_number": 3,
|
||||||
|
"order_num": 10,
|
||||||
|
"event_type": "class_level_up:swordcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 13,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Reach level 10 in Runecraft",
|
||||||
|
"require_number": 10,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 11,
|
||||||
|
"event_type": "class_level_up:runecraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 14,
|
||||||
|
"level": 3,
|
||||||
|
"name": "Reach level 20 in Dragoncraft",
|
||||||
|
"require_number": 20,
|
||||||
|
"reward_type": 5,
|
||||||
|
"reward_detail_id": 100011041,
|
||||||
|
"reward_number": 3,
|
||||||
|
"order_num": 12,
|
||||||
|
"event_type": "class_level_up:dragoncraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 15,
|
||||||
|
"level": 2,
|
||||||
|
"name": "Reach level 15 in Shadowcraft",
|
||||||
|
"require_number": 15,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 30,
|
||||||
|
"order_num": 13,
|
||||||
|
"event_type": "class_level_up:shadowcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 16,
|
||||||
|
"level": 6,
|
||||||
|
"name": "Reach level 35 in Bloodcraft",
|
||||||
|
"require_number": 35,
|
||||||
|
"reward_type": 5,
|
||||||
|
"reward_detail_id": 100614011,
|
||||||
|
"reward_number": 3,
|
||||||
|
"order_num": 14,
|
||||||
|
"event_type": "class_level_up:bloodcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 17,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Reach level 10 in Havencraft",
|
||||||
|
"require_number": 10,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 15,
|
||||||
|
"event_type": "class_level_up:havencraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 18,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Reach level 10 in Portalcraft",
|
||||||
|
"require_number": 10,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 16,
|
||||||
|
"event_type": "class_level_up:portalcraft",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 28,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Cleared Chapter 8: The Morning Star with 7 leaders without skipping the battle",
|
||||||
|
"require_number": 7,
|
||||||
|
"reward_type": 8,
|
||||||
|
"reward_detail_id": 110001,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 17,
|
||||||
|
"event_type": "story_chapter_finish:main",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 29,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Cleared Chapter 12 of The Morning Star: Conclusion",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 8,
|
||||||
|
"reward_detail_id": 110006,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 71,
|
||||||
|
"event_type": "story_chapter_finish:main",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 31,
|
||||||
|
"level": 3,
|
||||||
|
"name": "Win 50 ranked matches",
|
||||||
|
"require_number": 50,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"order_num": 18,
|
||||||
|
"event_type": "ranked_win",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 32,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win 5 Challenge matches",
|
||||||
|
"require_number": 5,
|
||||||
|
"reward_type": 4,
|
||||||
|
"reward_detail_id": 10001,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 19,
|
||||||
|
"event_type": "challenge_win",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 41,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Win all 5 Challenge matches 3 times",
|
||||||
|
"require_number": 3,
|
||||||
|
"reward_type": 4,
|
||||||
|
"reward_detail_id": 10001,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 20,
|
||||||
|
"event_type": "challenge_full_clear",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 50,
|
||||||
|
"level": 3,
|
||||||
|
"name": "Achieve Beginner 3 rank (Throwback Rotation or Unlimited)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 100,
|
||||||
|
"order_num": 25,
|
||||||
|
"event_type": "rank_achieved:beginner",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 51,
|
||||||
|
"level": 4,
|
||||||
|
"name": "Achieve D3 rank (Throwback Rotation or Unlimited)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 100,
|
||||||
|
"order_num": 29,
|
||||||
|
"event_type": "rank_achieved:d",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 52,
|
||||||
|
"level": 3,
|
||||||
|
"name": "Achieve C2 rank (Throwback Rotation or Unlimited)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 100,
|
||||||
|
"order_num": 32,
|
||||||
|
"event_type": "rank_achieved:c",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 53,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Achieve B0 rank (Throwback Rotation or Unlimited)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 8,
|
||||||
|
"reward_detail_id": 201003,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 34,
|
||||||
|
"event_type": "rank_achieved:b",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 54,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Achieve A0 rank (Throwback Rotation or Unlimited)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 8,
|
||||||
|
"reward_detail_id": 201004,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 38,
|
||||||
|
"event_type": "rank_achieved:a",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 55,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Achieve AA0 rank (Throwback Rotation or Unlimited)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 8,
|
||||||
|
"reward_detail_id": 201005,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 42,
|
||||||
|
"event_type": "rank_achieved:aa",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 56,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Achieve Master rank (Throwback Rotation or Unlimited)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 8,
|
||||||
|
"reward_detail_id": 300002,
|
||||||
|
"reward_number": 1,
|
||||||
|
"order_num": 46,
|
||||||
|
"event_type": "rank_achieved:master",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 61,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Arisa on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 47,
|
||||||
|
"event_type": "practice_win:elite:arisa",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 62,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Erika on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 48,
|
||||||
|
"event_type": "practice_win:elite:erika",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 63,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Isabelle on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 49,
|
||||||
|
"event_type": "practice_win:elite:isabelle",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 64,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Rowen on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 50,
|
||||||
|
"event_type": "practice_win:elite:rowen",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 65,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Luna on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 51,
|
||||||
|
"event_type": "practice_win:elite:luna",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 66,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Urias on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 52,
|
||||||
|
"event_type": "practice_win:elite:urias",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 67,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Eris on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 53,
|
||||||
|
"event_type": "practice_win:elite:eris",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 68,
|
||||||
|
"level": 7,
|
||||||
|
"name": "Battle 7 players in Private Match (without quitting).",
|
||||||
|
"require_number": 7,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 100,
|
||||||
|
"order_num": 70,
|
||||||
|
"event_type": "private_match_distinct_opponent",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 71,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Arisa on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 55,
|
||||||
|
"event_type": "practice_win:elite2:arisa",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 72,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Erika on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 56,
|
||||||
|
"event_type": "practice_win:elite2:erika",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 73,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Isabelle on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 57,
|
||||||
|
"event_type": "practice_win:elite2:isabelle",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 74,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Rowen on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 58,
|
||||||
|
"event_type": "practice_win:elite2:rowen",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 75,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Luna on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 59,
|
||||||
|
"event_type": "practice_win:elite2:luna",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 76,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Urias on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 60,
|
||||||
|
"event_type": "practice_win:elite2:urias",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 77,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Eris on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 61,
|
||||||
|
"event_type": "practice_win:elite2:eris",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 81,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Arisa on Elite 3 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 63,
|
||||||
|
"event_type": "practice_win:elite3:arisa",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 82,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Erika on Elite 3 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 64,
|
||||||
|
"event_type": "practice_win:elite3:erika",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 83,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Isabelle on Elite 3 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 65,
|
||||||
|
"event_type": "practice_win:elite3:isabelle",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 84,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Rowen on Elite 3 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 66,
|
||||||
|
"event_type": "practice_win:elite3:rowen",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 85,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Luna on Elite 3 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 67,
|
||||||
|
"event_type": "practice_win:elite3:luna",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 86,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Urias on Elite 3 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 68,
|
||||||
|
"event_type": "practice_win:elite3:urias",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 87,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Eris on Elite 3 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 69,
|
||||||
|
"event_type": "practice_win:elite3:eris",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 168,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Yuwan on Elite difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 54,
|
||||||
|
"event_type": "practice_win:elite:yuwan",
|
||||||
|
"event_arg": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_type": 178,
|
||||||
|
"level": 1,
|
||||||
|
"name": "Defeat Yuwan on Elite 2 difficulty (Practice)",
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 200,
|
||||||
|
"order_num": 62,
|
||||||
|
"event_type": "practice_win:elite2:yuwan",
|
||||||
|
"event_arg": null
|
||||||
|
}
|
||||||
|
]
|
||||||
67
SVSim.Bootstrap/Data/seeds/bp-monthly-missions.json
Normal file
67
SVSim.Bootstrap/Data/seeds/bp-monthly-missions.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"order_num": 0,
|
||||||
|
"name": "Win 25 matches (Ranked or Arena)",
|
||||||
|
"require_number": 25,
|
||||||
|
"battle_pass_point": 1000,
|
||||||
|
"event_type": "ranked_or_arena_win",
|
||||||
|
"event_arg": null,
|
||||||
|
"reward_type": 7,
|
||||||
|
"reward_detail_id": 1215110100,
|
||||||
|
"reward_number": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"order_num": 1,
|
||||||
|
"name": "Win 50 matches (Ranked or Arena)",
|
||||||
|
"require_number": 50,
|
||||||
|
"battle_pass_point": 1000,
|
||||||
|
"event_type": "ranked_or_arena_win",
|
||||||
|
"event_arg": null,
|
||||||
|
"reward_type": 6,
|
||||||
|
"reward_detail_id": 1215110100,
|
||||||
|
"reward_number": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"order_num": 2,
|
||||||
|
"name": "Win 75 matches (Ranked or Arena)",
|
||||||
|
"require_number": 75,
|
||||||
|
"battle_pass_point": 1500,
|
||||||
|
"event_type": "ranked_or_arena_win",
|
||||||
|
"event_arg": null,
|
||||||
|
"reward_type": 7,
|
||||||
|
"reward_detail_id": 1298310100,
|
||||||
|
"reward_number": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"order_num": 3,
|
||||||
|
"name": "Win 100 matches (Ranked or Arena)",
|
||||||
|
"require_number": 100,
|
||||||
|
"battle_pass_point": 1500,
|
||||||
|
"event_type": "ranked_or_arena_win",
|
||||||
|
"event_arg": null,
|
||||||
|
"reward_type": 6,
|
||||||
|
"reward_detail_id": 1298310100,
|
||||||
|
"reward_number": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"order_num": 4,
|
||||||
|
"name": "Play 5 Challenge matches (without quitting)",
|
||||||
|
"require_number": 5,
|
||||||
|
"battle_pass_point": 500,
|
||||||
|
"event_type": "challenge_play",
|
||||||
|
"event_arg": null,
|
||||||
|
"reward_type": null,
|
||||||
|
"reward_detail_id": null,
|
||||||
|
"reward_number": null
|
||||||
|
}
|
||||||
|
]
|
||||||
38
SVSim.Bootstrap/Data/seeds/item-purchase.json
Normal file
38
SVSim.Bootstrap/Data/seeds/item-purchase.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"purchase_id": 1,
|
||||||
|
"require_item_type": 1,
|
||||||
|
"require_item_id": 0,
|
||||||
|
"require_item_num": 5000,
|
||||||
|
"purchase_item_type": 4,
|
||||||
|
"purchase_item_id": 1000,
|
||||||
|
"purchase_item_num": 1,
|
||||||
|
"purchase_name": "[b]One Time Only![/b] Seer's Globe x1",
|
||||||
|
"is_monthly_reset": false,
|
||||||
|
"purchase_limit": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"purchase_id": 100002,
|
||||||
|
"require_item_type": 4,
|
||||||
|
"require_item_id": 1001,
|
||||||
|
"require_item_num": 5,
|
||||||
|
"purchase_item_type": 4,
|
||||||
|
"purchase_item_id": 1000,
|
||||||
|
"purchase_item_num": 1,
|
||||||
|
"purchase_name": "",
|
||||||
|
"is_monthly_reset": true,
|
||||||
|
"purchase_limit": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"purchase_id": 100003,
|
||||||
|
"require_item_type": 1,
|
||||||
|
"require_item_id": 0,
|
||||||
|
"require_item_num": 30000,
|
||||||
|
"purchase_item_type": 4,
|
||||||
|
"purchase_item_id": 1000,
|
||||||
|
"purchase_item_num": 1,
|
||||||
|
"purchase_name": "",
|
||||||
|
"is_monthly_reset": true,
|
||||||
|
"purchase_limit": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
362
SVSim.Bootstrap/Data/seeds/items.json
Normal file
362
SVSim.Bootstrap/Data/seeds/items.json
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"item_id": 1,
|
||||||
|
"name": "Challenge Ticket",
|
||||||
|
"type": 1,
|
||||||
|
"thumbnail_path": "ticket_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 2,
|
||||||
|
"name": "Grand Prix Ticket",
|
||||||
|
"type": 4,
|
||||||
|
"thumbnail_path": "ticket_colosseum"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 1000,
|
||||||
|
"name": "Seer's Globe",
|
||||||
|
"type": 3,
|
||||||
|
"thumbnail_path": "thumbnail_orb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 1001,
|
||||||
|
"name": "Seer's Globe Shards",
|
||||||
|
"type": 5,
|
||||||
|
"thumbnail_path": "thumbnail_orb_piece"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 2001,
|
||||||
|
"name": "Umamusume Bingo Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_2001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 2002,
|
||||||
|
"name": "Chiikawa Bingo Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_2002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 2003,
|
||||||
|
"name": "7th Anniversary Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_2003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 2004,
|
||||||
|
"name": "Fennie Bingo Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_2004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10001,
|
||||||
|
"name": "Classic Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10002,
|
||||||
|
"name": "Darkness Evolved Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10003,
|
||||||
|
"name": "Rise of Bahamut Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10004,
|
||||||
|
"name": "Tempest of the Gods Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10005,
|
||||||
|
"name": "Wonderland Dreams Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10005"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10006,
|
||||||
|
"name": "Starforged Legends Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10006"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10007,
|
||||||
|
"name": "Chronogenesis Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10007"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10008,
|
||||||
|
"name": "Dawnbreak, Nightedge Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10008"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10009,
|
||||||
|
"name": "Brigade of the Sky Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10009"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10010,
|
||||||
|
"name": "Omen of the Ten Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10010"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10011,
|
||||||
|
"name": "Altersphere Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10012,
|
||||||
|
"name": "Steel Rebellion Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10012"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10013,
|
||||||
|
"name": "Rebirth of Glory Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10014,
|
||||||
|
"name": "Verdant Conflict Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10014"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10015,
|
||||||
|
"name": "Ultimate Colosseum Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10015"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10016,
|
||||||
|
"name": "World Uprooted Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10017,
|
||||||
|
"name": "Fortune's Hand Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10018,
|
||||||
|
"name": "Storm Over Rivayle Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10018"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10019,
|
||||||
|
"name": "Eternal Awakening Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10020,
|
||||||
|
"name": "Darkness Over Vellsar Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10020"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10021,
|
||||||
|
"name": "Renascent Chronicles Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10021"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10022,
|
||||||
|
"name": "Dawn of Calamity Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10022"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10023,
|
||||||
|
"name": "Omen of Storms Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10023"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10024,
|
||||||
|
"name": "Edge of Paradise Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10025,
|
||||||
|
"name": "Roar of the Godwyrm Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10026,
|
||||||
|
"name": "Celestial Dragonblade Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10026"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10027,
|
||||||
|
"name": "Eightfold Abyss: Azvaldt Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10027"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10028,
|
||||||
|
"name": "Academy of Ages Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10029,
|
||||||
|
"name": "Heroes of Rivenbrandt Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10029"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10030,
|
||||||
|
"name": "Order Shift Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10030"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10031,
|
||||||
|
"name": "Resurgent Legends Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10031"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 10032,
|
||||||
|
"name": "Heroes of Shadowverse Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_10032"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60001,
|
||||||
|
"name": "4th Birthday Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60019,
|
||||||
|
"name": "Eternal Awakening Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60020,
|
||||||
|
"name": "Darkness Over Vellsar Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60020"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60021,
|
||||||
|
"name": "Renascent Chronicles Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60021"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60022,
|
||||||
|
"name": "Dawn of Calamity Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60022"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60023,
|
||||||
|
"name": "Omen of Storms Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60023"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60024,
|
||||||
|
"name": "Edge of Paradise Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60025,
|
||||||
|
"name": "Roar of the Godwyrm Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60026,
|
||||||
|
"name": "Celestial Dragonblade Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60026"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60027,
|
||||||
|
"name": "Eightfold Abyss: Azvaldt Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60027"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60028,
|
||||||
|
"name": "Academy of Ages Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60028"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60029,
|
||||||
|
"name": "Heroes of Rivenbrandt Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60029"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60030,
|
||||||
|
"name": "Order Shift Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60030"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60031,
|
||||||
|
"name": "Resurgent Legends Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60031"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 60032,
|
||||||
|
"name": "Heroes of Shadowverse Temporary Deck Ticket",
|
||||||
|
"type": 6,
|
||||||
|
"thumbnail_path": "ticket_60032"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 70001,
|
||||||
|
"name": "4th Birthday Leader Ticket",
|
||||||
|
"type": 7,
|
||||||
|
"thumbnail_path": "ticket_70001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 70002,
|
||||||
|
"name": "Champion's Battle Leader Ticket",
|
||||||
|
"type": 7,
|
||||||
|
"thumbnail_path": "ticket_70002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 80001,
|
||||||
|
"name": "Throwback Rotation Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_80001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 90001,
|
||||||
|
"name": "Legendary Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_90001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_id": 92001,
|
||||||
|
"name": "4th Birthday Card Pack Ticket",
|
||||||
|
"type": 2,
|
||||||
|
"thumbnail_path": "ticket_92001"
|
||||||
|
}
|
||||||
|
]
|
||||||
4053
SVSim.Bootstrap/Data/seeds/leader-skin-shop.json
Normal file
4053
SVSim.Bootstrap/Data/seeds/leader-skin-shop.json
Normal file
File diff suppressed because it is too large
Load Diff
77
SVSim.Bootstrap/Data/seeds/mission-catalog.json
Normal file
77
SVSim.Bootstrap/Data/seeds/mission-catalog.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"name": "Win 2 ranked matches",
|
||||||
|
"lot_type": 2,
|
||||||
|
"require_number": 2,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 30,
|
||||||
|
"battle_pass_point": 50,
|
||||||
|
"default_flag": false,
|
||||||
|
"event_type": "ranked_win",
|
||||||
|
"event_arg": null,
|
||||||
|
"start_time": 1779634776,
|
||||||
|
"end_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"name": "Win 1 match as Swordcraft (Ranked, Unranked, or Arena)",
|
||||||
|
"lot_type": 2,
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"battle_pass_point": 50,
|
||||||
|
"default_flag": false,
|
||||||
|
"event_type": "ranked_win:swordcraft",
|
||||||
|
"event_arg": null,
|
||||||
|
"start_time": 1505581389,
|
||||||
|
"end_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"name": "Win 1 match as Bloodcraft (Ranked, Unranked, or Arena)",
|
||||||
|
"lot_type": 2,
|
||||||
|
"require_number": 1,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 20,
|
||||||
|
"battle_pass_point": 50,
|
||||||
|
"default_flag": false,
|
||||||
|
"event_type": "ranked_win:bloodcraft",
|
||||||
|
"event_arg": null,
|
||||||
|
"start_time": 1505659633,
|
||||||
|
"end_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 332,
|
||||||
|
"name": "Daily Mission: Win 3 matches (Ranked, Unranked, or Arena)",
|
||||||
|
"lot_type": 6,
|
||||||
|
"require_number": 3,
|
||||||
|
"reward_type": 12,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 400,
|
||||||
|
"battle_pass_point": 50,
|
||||||
|
"default_flag": true,
|
||||||
|
"event_type": "daily_match_win",
|
||||||
|
"event_arg": null,
|
||||||
|
"start_time": 1725317128,
|
||||||
|
"end_time": 1893542399
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 505,
|
||||||
|
"name": "Play 30 followers (Ranked, Unranked, or Arena)",
|
||||||
|
"lot_type": 2,
|
||||||
|
"require_number": 30,
|
||||||
|
"reward_type": 9,
|
||||||
|
"reward_detail_id": 0,
|
||||||
|
"reward_number": 50,
|
||||||
|
"battle_pass_point": 50,
|
||||||
|
"default_flag": false,
|
||||||
|
"event_type": "play_followers",
|
||||||
|
"event_arg": null,
|
||||||
|
"start_time": 1779772990,
|
||||||
|
"end_time": null
|
||||||
|
}
|
||||||
|
]
|
||||||
5918
SVSim.Bootstrap/Data/seeds/sleeve-shop.json
Normal file
5918
SVSim.Bootstrap/Data/seeds/sleeve-shop.json
Normal file
File diff suppressed because it is too large
Load Diff
3733
SVSim.Bootstrap/Data/seeds/spot-card-exchange.json
Normal file
3733
SVSim.Bootstrap/Data/seeds/spot-card-exchange.json
Normal file
File diff suppressed because it is too large
Load Diff
60
SVSim.Bootstrap/Importers/AchievementCatalogImporter.cs
Normal file
60
SVSim.Bootstrap/Importers/AchievementCatalogImporter.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of achievement-catalog rows from <c>seeds/achievement-catalog.json</c>.
|
||||||
|
/// Keyed by (AchievementType, Level) so re-running with new captures grows the ladder.
|
||||||
|
/// Rows missing from the seed are LEFT INTACT.
|
||||||
|
/// </summary>
|
||||||
|
public class AchievementCatalogImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
var seed = SeedLoader.LoadList<AchievementCatalogSeed>(Path.Combine(seedDir, "achievement-catalog.json"));
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[AchievementCatalogImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await context.AchievementCatalog
|
||||||
|
.ToDictionaryAsync(e => (e.AchievementType, e.Level));
|
||||||
|
int created = 0, updated = 0;
|
||||||
|
var unmappedTypes = new HashSet<int>();
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.AchievementType == 0 || s.Level == 0) continue;
|
||||||
|
var key = (s.AchievementType, s.Level);
|
||||||
|
var entry = existing.TryGetValue(key, out var ex) ? ex : new AchievementCatalogEntry
|
||||||
|
{
|
||||||
|
AchievementType = s.AchievementType,
|
||||||
|
Level = s.Level,
|
||||||
|
};
|
||||||
|
entry.Name = s.Name;
|
||||||
|
entry.RequireNumber = s.RequireNumber;
|
||||||
|
entry.RewardType = s.RewardType;
|
||||||
|
entry.RewardDetailId = s.RewardDetailId;
|
||||||
|
entry.RewardNumber = s.RewardNumber;
|
||||||
|
entry.OrderNum = s.OrderNum;
|
||||||
|
entry.EventType = s.EventType;
|
||||||
|
entry.EventArg = s.EventArg;
|
||||||
|
if (ex is null) { context.AchievementCatalog.Add(entry); existing[key] = entry; created++; }
|
||||||
|
else updated++;
|
||||||
|
if (s.EventType is null) unmappedTypes.Add(s.AchievementType);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"[AchievementCatalogImporter] +{created}/~{updated}");
|
||||||
|
if (unmappedTypes.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[AchievementCatalogImporter] WARN: {unmappedTypes.Count} types " +
|
||||||
|
$"with no event_type: [{string.Join(", ", unmappedTypes.OrderBy(x => x))}] — " +
|
||||||
|
"add to ACHIEVEMENT_EVENT_MAP in data_dumps/extract/extract-achievements.py");
|
||||||
|
}
|
||||||
|
return created + updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of BP monthly mission rows from <c>seeds/bp-monthly-missions.json</c>.
|
||||||
|
/// Keyed by (Year, Month, OrderNum). Rows missing from the seed are LEFT INTACT.
|
||||||
|
/// </summary>
|
||||||
|
public class BattlePassMonthlyMissionImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
var seed = SeedLoader.LoadList<BattlePassMonthlyMissionSeed>(
|
||||||
|
Path.Combine(seedDir, "bp-monthly-missions.json"));
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[BattlePassMonthlyMissionImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await context.BattlePassMonthlyMissions
|
||||||
|
.ToDictionaryAsync(e => (e.Year, e.Month, e.OrderNum));
|
||||||
|
int created = 0, updated = 0;
|
||||||
|
var unmapped = new List<string>();
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.Year == 0 || s.Month == 0) continue;
|
||||||
|
var key = (s.Year, s.Month, s.OrderNum);
|
||||||
|
var entry = existing.TryGetValue(key, out var ex)
|
||||||
|
? ex
|
||||||
|
: new BattlePassMonthlyMissionEntry
|
||||||
|
{
|
||||||
|
Year = s.Year, Month = s.Month, OrderNum = s.OrderNum,
|
||||||
|
};
|
||||||
|
entry.Name = s.Name;
|
||||||
|
entry.RequireNumber = s.RequireNumber;
|
||||||
|
entry.BattlePassPoint = s.BattlePassPoint;
|
||||||
|
entry.RewardType = s.RewardType;
|
||||||
|
entry.RewardDetailId = s.RewardDetailId;
|
||||||
|
entry.RewardNumber = s.RewardNumber;
|
||||||
|
entry.EventType = s.EventType;
|
||||||
|
entry.EventArg = s.EventArg;
|
||||||
|
if (ex is null) { context.BattlePassMonthlyMissions.Add(entry); existing[key] = entry; created++; }
|
||||||
|
else updated++;
|
||||||
|
if (s.EventType is null) unmapped.Add($"{s.Year}-{s.Month:00}/{s.OrderNum}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"[BattlePassMonthlyMissionImporter] +{created}/~{updated}");
|
||||||
|
if (unmapped.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[BattlePassMonthlyMissionImporter] WARN: {unmapped.Count} rows " +
|
||||||
|
$"with no event_type: [{string.Join(", ", unmapped)}] — add name to " +
|
||||||
|
"BP_MONTHLY_EVENT_MAP in data_dumps/extract/extract-bp-monthly-missions.py");
|
||||||
|
}
|
||||||
|
return created + updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
52
SVSim.Bootstrap/Importers/ItemImporter.cs
Normal file
52
SVSim.Bootstrap/Importers/ItemImporter.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of the item catalog from <c>seeds/items.json</c>. Source is the client's
|
||||||
|
/// <c>item_master.csv</c> + <c>itemtext.json</c> (extracted via
|
||||||
|
/// <c>data_dumps/extract/extract-items.py</c>). Rows missing from the seed are LEFT INTACT.
|
||||||
|
/// </summary>
|
||||||
|
public class ItemImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(seedDir, "items.json");
|
||||||
|
var seed = SeedLoader.LoadList<ItemSeed>(path);
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[ItemImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await context.Items.ToDictionaryAsync(e => e.Id);
|
||||||
|
int created = 0, updated = 0;
|
||||||
|
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.ItemId == 0) continue;
|
||||||
|
|
||||||
|
var entry = existing.TryGetValue(s.ItemId, out var ex)
|
||||||
|
? ex : new ItemEntry { Id = s.ItemId };
|
||||||
|
|
||||||
|
entry.Name = s.Name;
|
||||||
|
entry.Type = s.Type;
|
||||||
|
entry.ThumbnailPath = s.ThumbnailPath;
|
||||||
|
|
||||||
|
if (ex is null)
|
||||||
|
{
|
||||||
|
context.Items.Add(entry);
|
||||||
|
existing[s.ItemId] = entry;
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
else updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"[ItemImporter] +{created}/~{updated}");
|
||||||
|
return created + updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
SVSim.Bootstrap/Importers/ItemPurchaseImporter.cs
Normal file
59
SVSim.Bootstrap/Importers/ItemPurchaseImporter.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of the item-purchase catalog from <c>seeds/item-purchase.json</c>.
|
||||||
|
/// Source is the wire <c>/item_purchase/info</c> response, extracted via
|
||||||
|
/// <c>data_dumps/extract/extract-item-purchase.py</c>. Rows missing from the seed are LEFT INTACT.
|
||||||
|
/// </summary>
|
||||||
|
public class ItemPurchaseImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(seedDir, "item-purchase.json");
|
||||||
|
var seed = SeedLoader.LoadList<ItemPurchaseSeed>(path);
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[ItemPurchaseImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await context.ItemPurchaseCatalog.ToDictionaryAsync(e => e.Id);
|
||||||
|
int created = 0, updated = 0;
|
||||||
|
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.PurchaseId == 0) continue;
|
||||||
|
|
||||||
|
var entry = existing.TryGetValue(s.PurchaseId, out var ex)
|
||||||
|
? ex : new ItemPurchaseCatalogEntry { Id = s.PurchaseId };
|
||||||
|
|
||||||
|
entry.RequireItemType = s.RequireItemType;
|
||||||
|
entry.RequireItemId = s.RequireItemId;
|
||||||
|
entry.RequireItemNum = s.RequireItemNum;
|
||||||
|
entry.PurchaseItemType = s.PurchaseItemType;
|
||||||
|
entry.PurchaseItemId = s.PurchaseItemId;
|
||||||
|
entry.PurchaseItemNum = s.PurchaseItemNum;
|
||||||
|
entry.PurchaseName = s.PurchaseName;
|
||||||
|
entry.IsMonthlyReset = s.IsMonthlyReset;
|
||||||
|
entry.PurchaseLimit = s.PurchaseLimit;
|
||||||
|
entry.IsEnabled = true;
|
||||||
|
|
||||||
|
if (ex is null)
|
||||||
|
{
|
||||||
|
context.ItemPurchaseCatalog.Add(entry);
|
||||||
|
existing[s.PurchaseId] = entry;
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
else updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"[ItemPurchaseImporter] +{created}/~{updated}");
|
||||||
|
return created + updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
115
SVSim.Bootstrap/Importers/LeaderSkinShopImporter.cs
Normal file
115
SVSim.Bootstrap/Importers/LeaderSkinShopImporter.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of the leader-skin-shop catalog from <c>seeds/leader-skin-shop.json</c>.
|
||||||
|
/// Mirror of <see cref="SleeveShopImporter"/>. Source is the wire
|
||||||
|
/// <c>/leader_skin/products</c> response, extracted via
|
||||||
|
/// <c>data_dumps/extract/extract-leader-skin-shop.py</c>. Rows missing from the seed are LEFT INTACT.
|
||||||
|
/// </summary>
|
||||||
|
public class LeaderSkinShopImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(seedDir, "leader-skin-shop.json");
|
||||||
|
var seed = SeedLoader.LoadList<LeaderSkinShopSeriesSeed>(path);
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[LeaderSkinShopImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingSeries = await context.LeaderSkinShopSeries
|
||||||
|
.Include(s => s.SetCompletionRewards)
|
||||||
|
.Include(s => s.Products).ThenInclude(p => p.Rewards)
|
||||||
|
.ToDictionaryAsync(s => s.Id);
|
||||||
|
|
||||||
|
int createdSeries = 0, updatedSeries = 0, createdProducts = 0, updatedProducts = 0;
|
||||||
|
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.SeriesId == 0) continue;
|
||||||
|
|
||||||
|
if (!existingSeries.TryGetValue(s.SeriesId, out var series))
|
||||||
|
{
|
||||||
|
series = new LeaderSkinShopSeriesEntry { Id = s.SeriesId };
|
||||||
|
context.LeaderSkinShopSeries.Add(series);
|
||||||
|
existingSeries[s.SeriesId] = series;
|
||||||
|
createdSeries++;
|
||||||
|
}
|
||||||
|
else updatedSeries++;
|
||||||
|
|
||||||
|
series.IsNew = s.IsNew;
|
||||||
|
series.IsEnabled = true;
|
||||||
|
series.SetSalesStatus = s.SetSalesStatus;
|
||||||
|
series.SetPriceCrystal = s.SetPriceCrystal;
|
||||||
|
series.SetPriceRupy = s.SetPriceRupy;
|
||||||
|
series.SetPriceTicket = s.SetPriceTicket;
|
||||||
|
series.SetPriceTicketId = s.SetPriceTicketId;
|
||||||
|
// SetCompletionRewardStatus stays at the catalog default 0 — per-viewer claim state
|
||||||
|
// is computed at request time from ViewerLeaderSkinSetClaim, not from this column.
|
||||||
|
series.SetCompletionRewardStatus = 0;
|
||||||
|
|
||||||
|
// Replace owned collections wholesale on rerun.
|
||||||
|
series.SetCompletionRewards.Clear();
|
||||||
|
foreach (var r in s.SetCompletionRewards.OrderBy(r => r.OrderIndex))
|
||||||
|
{
|
||||||
|
series.SetCompletionRewards.Add(new LeaderSkinShopSeriesRewardEntry
|
||||||
|
{
|
||||||
|
OrderIndex = r.OrderIndex,
|
||||||
|
RewardType = r.RewardType,
|
||||||
|
RewardDetailId = r.RewardDetailId,
|
||||||
|
RewardNumber = r.RewardNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingProducts = series.Products.ToDictionary(p => p.Id);
|
||||||
|
foreach (var p in s.Products)
|
||||||
|
{
|
||||||
|
if (p.ProductId == 0) continue;
|
||||||
|
|
||||||
|
if (!existingProducts.TryGetValue(p.ProductId, out var product))
|
||||||
|
{
|
||||||
|
product = new LeaderSkinShopProductEntry { Id = p.ProductId };
|
||||||
|
series.Products.Add(product);
|
||||||
|
createdProducts++;
|
||||||
|
}
|
||||||
|
else updatedProducts++;
|
||||||
|
|
||||||
|
product.SeriesId = s.SeriesId;
|
||||||
|
product.LeaderSkinId = p.LeaderSkinId;
|
||||||
|
product.ProductNameKey = p.ProductNameKey;
|
||||||
|
product.IntroductionKey = p.IntroductionKey;
|
||||||
|
product.CvNameKey = p.CvNameKey;
|
||||||
|
product.SinglePriceCrystal = p.SinglePriceCrystal;
|
||||||
|
product.SinglePriceRupy = p.SinglePriceRupy;
|
||||||
|
product.SinglePriceTicket = p.SinglePriceTicket;
|
||||||
|
product.TicketNumber = p.TicketNumber;
|
||||||
|
product.TicketItemId = p.TicketItemId;
|
||||||
|
product.IsEnabled = true;
|
||||||
|
|
||||||
|
product.Rewards.Clear();
|
||||||
|
foreach (var r in p.Rewards.OrderBy(r => r.OrderIndex))
|
||||||
|
{
|
||||||
|
product.Rewards.Add(new LeaderSkinShopProductRewardEntry
|
||||||
|
{
|
||||||
|
OrderIndex = r.OrderIndex,
|
||||||
|
RewardType = r.RewardType,
|
||||||
|
RewardDetailId = r.RewardDetailId,
|
||||||
|
RewardNumber = r.RewardNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[LeaderSkinShopImporter] series +{createdSeries}/~{updatedSeries}, " +
|
||||||
|
$"products +{createdProducts}/~{updatedProducts}");
|
||||||
|
return createdSeries + updatedSeries;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
SVSim.Bootstrap/Importers/MissionCatalogImporter.cs
Normal file
57
SVSim.Bootstrap/Importers/MissionCatalogImporter.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of mission catalog rows from <c>seeds/mission-catalog.json</c>.
|
||||||
|
/// Rows missing from the seed are LEFT INTACT (so hand-added catalog rows survive).
|
||||||
|
/// </summary>
|
||||||
|
public class MissionCatalogImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
var seed = SeedLoader.LoadList<MissionCatalogSeed>(Path.Combine(seedDir, "mission-catalog.json"));
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[MissionCatalogImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await context.MissionCatalog.ToDictionaryAsync(e => e.Id);
|
||||||
|
int created = 0, updated = 0;
|
||||||
|
var unmapped = new List<int>();
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.Id == 0) continue;
|
||||||
|
var entry = existing.TryGetValue(s.Id, out var ex) ? ex : new MissionCatalogEntry { Id = s.Id };
|
||||||
|
entry.Name = s.Name;
|
||||||
|
entry.LotType = s.LotType;
|
||||||
|
entry.RequireNumber = s.RequireNumber;
|
||||||
|
entry.RewardType = s.RewardType;
|
||||||
|
entry.RewardDetailId = s.RewardDetailId;
|
||||||
|
entry.RewardNumber = s.RewardNumber;
|
||||||
|
entry.BattlePassPoint = s.BattlePassPoint;
|
||||||
|
entry.DefaultFlag = s.DefaultFlag;
|
||||||
|
entry.EventType = s.EventType;
|
||||||
|
entry.EventArg = s.EventArg;
|
||||||
|
entry.StartTime = s.StartTime;
|
||||||
|
entry.EndTime = s.EndTime;
|
||||||
|
if (ex is null) { context.MissionCatalog.Add(entry); existing[s.Id] = entry; created++; }
|
||||||
|
else updated++;
|
||||||
|
if (s.EventType is null) unmapped.Add(s.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"[MissionCatalogImporter] +{created}/~{updated}");
|
||||||
|
if (unmapped.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[MissionCatalogImporter] WARN: {unmapped.Count} mission_ids with " +
|
||||||
|
$"no event_type: [{string.Join(", ", unmapped)}] — add to MISSION_EVENT_MAP " +
|
||||||
|
"in data_dumps/extract/extract-missions.py and re-run the extractor");
|
||||||
|
}
|
||||||
|
return created + updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
89
SVSim.Bootstrap/Importers/SleeveShopImporter.cs
Normal file
89
SVSim.Bootstrap/Importers/SleeveShopImporter.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of the sleeve-shop catalog from <c>seeds/sleeve-shop.json</c>.
|
||||||
|
/// Source is the wire <c>/sleeve/info</c> response, extracted via
|
||||||
|
/// <c>data_dumps/extract/extract-sleeve-shop.py</c>. Mirror of the BuildDeck importer pattern.
|
||||||
|
/// Rows missing from the seed are LEFT INTACT (so manual test fixtures survive re-runs).
|
||||||
|
/// </summary>
|
||||||
|
public class SleeveShopImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(seedDir, "sleeve-shop.json");
|
||||||
|
var seed = SeedLoader.LoadList<SleeveShopSeriesSeed>(path);
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[SleeveShopImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingSeries = await context.SleeveShopSeries
|
||||||
|
.Include(s => s.Products).ThenInclude(p => p.Rewards)
|
||||||
|
.ToDictionaryAsync(s => s.Id);
|
||||||
|
|
||||||
|
int createdSeries = 0, updatedSeries = 0, createdProducts = 0, updatedProducts = 0;
|
||||||
|
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.SeriesId == 0) continue;
|
||||||
|
|
||||||
|
if (!existingSeries.TryGetValue(s.SeriesId, out var series))
|
||||||
|
{
|
||||||
|
series = new SleeveShopSeriesEntry { Id = s.SeriesId };
|
||||||
|
context.SleeveShopSeries.Add(series);
|
||||||
|
existingSeries[s.SeriesId] = series;
|
||||||
|
createdSeries++;
|
||||||
|
}
|
||||||
|
else updatedSeries++;
|
||||||
|
|
||||||
|
series.IsNew = s.IsNew;
|
||||||
|
series.IsEnabled = true;
|
||||||
|
|
||||||
|
var existingProducts = series.Products.ToDictionary(p => p.Id);
|
||||||
|
foreach (var p in s.Products)
|
||||||
|
{
|
||||||
|
if (p.ProductId == 0) continue;
|
||||||
|
|
||||||
|
if (!existingProducts.TryGetValue(p.ProductId, out var product))
|
||||||
|
{
|
||||||
|
product = new SleeveShopProductEntry { Id = p.ProductId };
|
||||||
|
series.Products.Add(product);
|
||||||
|
createdProducts++;
|
||||||
|
}
|
||||||
|
else updatedProducts++;
|
||||||
|
|
||||||
|
product.SeriesId = s.SeriesId;
|
||||||
|
product.NameKey = p.NameKey;
|
||||||
|
product.PriceCrystal = p.PriceCrystal;
|
||||||
|
product.PriceRupy = p.PriceRupy;
|
||||||
|
product.IsEnabled = true;
|
||||||
|
|
||||||
|
// Rewards: replace wholesale (owned collection — EF will issue DELETE+INSERT
|
||||||
|
// anyway, and the wire shape is canonical per re-extract).
|
||||||
|
product.Rewards.Clear();
|
||||||
|
foreach (var r in p.Rewards.OrderBy(r => r.OrderIndex))
|
||||||
|
{
|
||||||
|
product.Rewards.Add(new SleeveShopProductRewardEntry
|
||||||
|
{
|
||||||
|
OrderIndex = r.OrderIndex,
|
||||||
|
RewardType = r.RewardType,
|
||||||
|
RewardDetailId = r.RewardDetailId,
|
||||||
|
RewardNumber = r.RewardNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[SleeveShopImporter] series +{createdSeries}/~{updatedSeries}, " +
|
||||||
|
$"products +{createdProducts}/~{updatedProducts}");
|
||||||
|
return createdSeries + updatedSeries;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
SVSim.Bootstrap/Importers/SpotCardExchangeImporter.cs
Normal file
55
SVSim.Bootstrap/Importers/SpotCardExchangeImporter.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Bootstrap.Models.Seed;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Importers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Idempotent upsert of the spot card exchange catalog from <c>seeds/spot-card-exchange.json</c>.
|
||||||
|
/// Source is the wire <c>/spot_card_exchange/top</c> response, extracted via
|
||||||
|
/// <c>data_dumps/extract/extract-spot-card-exchange.py</c>. Rows missing from the seed are
|
||||||
|
/// LEFT INTACT.
|
||||||
|
/// </summary>
|
||||||
|
public class SpotCardExchangeImporter
|
||||||
|
{
|
||||||
|
public async Task<int> ImportAsync(SVSimDbContext context, string seedDir)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(seedDir, "spot-card-exchange.json");
|
||||||
|
var seed = SeedLoader.LoadList<SpotCardExchangeSeed>(path);
|
||||||
|
if (seed.Count == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[SpotCardExchangeImporter] No seed rows; skipping.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await context.SpotCardExchangeCatalog.ToDictionaryAsync(e => e.Id);
|
||||||
|
int created = 0, updated = 0;
|
||||||
|
|
||||||
|
foreach (var s in seed)
|
||||||
|
{
|
||||||
|
if (s.CardId == 0) continue;
|
||||||
|
|
||||||
|
var entry = existing.TryGetValue(s.CardId, out var ex)
|
||||||
|
? ex : new SpotCardExchangeEntry { Id = s.CardId };
|
||||||
|
|
||||||
|
entry.ClassId = s.ClassId;
|
||||||
|
entry.ExchangePoint = s.ExchangePoint;
|
||||||
|
entry.TsRotationId = s.TsRotationId;
|
||||||
|
entry.IsPreRelease = s.IsPreRelease;
|
||||||
|
entry.IsEnabled = true;
|
||||||
|
|
||||||
|
if (ex is null)
|
||||||
|
{
|
||||||
|
context.SpotCardExchangeCatalog.Add(entry);
|
||||||
|
existing[s.CardId] = entry;
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
else updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
Console.WriteLine($"[SpotCardExchangeImporter] +{created}/~{updated}");
|
||||||
|
return created + updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
SVSim.Bootstrap/Models/Seed/AchievementCatalogSeed.cs
Normal file
17
SVSim.Bootstrap/Models/Seed/AchievementCatalogSeed.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
public sealed class AchievementCatalogSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("achievement_type")] public int AchievementType { get; set; }
|
||||||
|
[JsonPropertyName("level")] public int Level { get; set; }
|
||||||
|
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||||
|
[JsonPropertyName("require_number")] public int RequireNumber { get; set; }
|
||||||
|
[JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[JsonPropertyName("reward_detail_id")] public long RewardDetailId { get; set; }
|
||||||
|
[JsonPropertyName("reward_number")] public int RewardNumber { get; set; }
|
||||||
|
[JsonPropertyName("order_num")] public int OrderNum { get; set; }
|
||||||
|
[JsonPropertyName("event_type")] public string? EventType { get; set; }
|
||||||
|
[JsonPropertyName("event_arg")] public int? EventArg { get; set; }
|
||||||
|
}
|
||||||
18
SVSim.Bootstrap/Models/Seed/BattlePassMonthlyMissionSeed.cs
Normal file
18
SVSim.Bootstrap/Models/Seed/BattlePassMonthlyMissionSeed.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
public sealed class BattlePassMonthlyMissionSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("year")] public int Year { get; set; }
|
||||||
|
[JsonPropertyName("month")] public int Month { get; set; }
|
||||||
|
[JsonPropertyName("order_num")] public int OrderNum { get; set; }
|
||||||
|
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||||
|
[JsonPropertyName("require_number")] public int RequireNumber { get; set; }
|
||||||
|
[JsonPropertyName("battle_pass_point")] public int BattlePassPoint { get; set; }
|
||||||
|
[JsonPropertyName("reward_type")] public int? RewardType { get; set; }
|
||||||
|
[JsonPropertyName("reward_detail_id")] public long? RewardDetailId { get; set; }
|
||||||
|
[JsonPropertyName("reward_number")] public int? RewardNumber { get; set; }
|
||||||
|
[JsonPropertyName("event_type")] public string? EventType { get; set; }
|
||||||
|
[JsonPropertyName("event_arg")] public int? EventArg { get; set; }
|
||||||
|
}
|
||||||
17
SVSim.Bootstrap/Models/Seed/ItemPurchaseSeed.cs
Normal file
17
SVSim.Bootstrap/Models/Seed/ItemPurchaseSeed.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
public sealed class ItemPurchaseSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("purchase_id")] public int PurchaseId { get; set; }
|
||||||
|
[JsonPropertyName("require_item_type")] public int RequireItemType { get; set; }
|
||||||
|
[JsonPropertyName("require_item_id")] public long RequireItemId { get; set; }
|
||||||
|
[JsonPropertyName("require_item_num")] public int RequireItemNum { get; set; }
|
||||||
|
[JsonPropertyName("purchase_item_type")] public int PurchaseItemType { get; set; }
|
||||||
|
[JsonPropertyName("purchase_item_id")] public long PurchaseItemId { get; set; }
|
||||||
|
[JsonPropertyName("purchase_item_num")] public int PurchaseItemNum { get; set; }
|
||||||
|
[JsonPropertyName("purchase_name")] public string PurchaseName { get; set; } = "";
|
||||||
|
[JsonPropertyName("is_monthly_reset")] public bool IsMonthlyReset { get; set; }
|
||||||
|
[JsonPropertyName("purchase_limit")] public int PurchaseLimit { get; set; }
|
||||||
|
}
|
||||||
11
SVSim.Bootstrap/Models/Seed/ItemSeed.cs
Normal file
11
SVSim.Bootstrap/Models/Seed/ItemSeed.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
public sealed class ItemSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("item_id")] public int ItemId { get; set; }
|
||||||
|
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||||
|
[JsonPropertyName("type")] public int Type { get; set; }
|
||||||
|
[JsonPropertyName("thumbnail_path")] public string ThumbnailPath { get; set; } = "";
|
||||||
|
}
|
||||||
39
SVSim.Bootstrap/Models/Seed/LeaderSkinShopSeed.cs
Normal file
39
SVSim.Bootstrap/Models/Seed/LeaderSkinShopSeed.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
public sealed class LeaderSkinShopSeriesSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("series_id")] public int SeriesId { get; set; }
|
||||||
|
[JsonPropertyName("is_new")] public bool IsNew { get; set; }
|
||||||
|
[JsonPropertyName("set_sales_status")] public int SetSalesStatus { get; set; }
|
||||||
|
[JsonPropertyName("set_price_crystal")] public int? SetPriceCrystal { get; set; }
|
||||||
|
[JsonPropertyName("set_price_rupy")] public int? SetPriceRupy { get; set; }
|
||||||
|
[JsonPropertyName("set_price_ticket")] public int? SetPriceTicket { get; set; }
|
||||||
|
[JsonPropertyName("set_price_ticket_id")] public long? SetPriceTicketId { get; set; }
|
||||||
|
[JsonPropertyName("set_completion_rewards")] public List<LeaderSkinShopRewardSeed> SetCompletionRewards { get; set; } = new();
|
||||||
|
[JsonPropertyName("products")] public List<LeaderSkinShopProductSeed> Products { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LeaderSkinShopProductSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("product_id")] public int ProductId { get; set; }
|
||||||
|
[JsonPropertyName("leader_skin_id")] public int LeaderSkinId { get; set; }
|
||||||
|
[JsonPropertyName("product_name_key")] public string ProductNameKey { get; set; } = "";
|
||||||
|
[JsonPropertyName("introduction_key")] public string IntroductionKey { get; set; } = "";
|
||||||
|
[JsonPropertyName("cv_name_key")] public string CvNameKey { get; set; } = "";
|
||||||
|
[JsonPropertyName("single_price_crystal")] public int? SinglePriceCrystal { get; set; }
|
||||||
|
[JsonPropertyName("single_price_rupy")] public int? SinglePriceRupy { get; set; }
|
||||||
|
[JsonPropertyName("single_price_ticket")] public int? SinglePriceTicket { get; set; }
|
||||||
|
[JsonPropertyName("ticket_number")] public int? TicketNumber { get; set; }
|
||||||
|
[JsonPropertyName("ticket_item_id")] public long? TicketItemId { get; set; }
|
||||||
|
[JsonPropertyName("rewards")] public List<LeaderSkinShopRewardSeed> Rewards { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LeaderSkinShopRewardSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("order_index")] public int OrderIndex { get; set; }
|
||||||
|
[JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[JsonPropertyName("reward_detail_id")] public long RewardDetailId { get; set; }
|
||||||
|
[JsonPropertyName("reward_number")] public int RewardNumber { get; set; }
|
||||||
|
}
|
||||||
21
SVSim.Bootstrap/Models/Seed/MissionCatalogSeed.cs
Normal file
21
SVSim.Bootstrap/Models/Seed/MissionCatalogSeed.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
/// <summary>Mirrors a single entry in <c>seeds/mission-catalog.json</c>.</summary>
|
||||||
|
public sealed class MissionCatalogSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public int Id { get; set; }
|
||||||
|
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||||
|
[JsonPropertyName("lot_type")] public int LotType { get; set; }
|
||||||
|
[JsonPropertyName("require_number")] public int RequireNumber { get; set; }
|
||||||
|
[JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[JsonPropertyName("reward_detail_id")] public long RewardDetailId { get; set; }
|
||||||
|
[JsonPropertyName("reward_number")] public int RewardNumber { get; set; }
|
||||||
|
[JsonPropertyName("battle_pass_point")] public int BattlePassPoint { get; set; }
|
||||||
|
[JsonPropertyName("default_flag")] public bool DefaultFlag { get; set; }
|
||||||
|
[JsonPropertyName("event_type")] public string? EventType { get; set; }
|
||||||
|
[JsonPropertyName("event_arg")] public int? EventArg { get; set; }
|
||||||
|
[JsonPropertyName("start_time")] public long StartTime { get; set; }
|
||||||
|
[JsonPropertyName("end_time")] public long? EndTime { get; set; }
|
||||||
|
}
|
||||||
27
SVSim.Bootstrap/Models/Seed/SleeveShopSeed.cs
Normal file
27
SVSim.Bootstrap/Models/Seed/SleeveShopSeed.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
public sealed class SleeveShopSeriesSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("series_id")] public int SeriesId { get; set; }
|
||||||
|
[JsonPropertyName("is_new")] public bool IsNew { get; set; }
|
||||||
|
[JsonPropertyName("products")] public List<SleeveShopProductSeed> Products { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SleeveShopProductSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("product_id")] public int ProductId { get; set; }
|
||||||
|
[JsonPropertyName("name_key")] public string NameKey { get; set; } = "";
|
||||||
|
[JsonPropertyName("price_crystal")] public int? PriceCrystal { get; set; }
|
||||||
|
[JsonPropertyName("price_rupy")] public int? PriceRupy { get; set; }
|
||||||
|
[JsonPropertyName("rewards")] public List<SleeveShopRewardSeed> Rewards { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SleeveShopRewardSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("order_index")] public int OrderIndex { get; set; }
|
||||||
|
[JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[JsonPropertyName("reward_detail_id")] public long RewardDetailId { get; set; }
|
||||||
|
[JsonPropertyName("reward_number")] public int RewardNumber { get; set; }
|
||||||
|
}
|
||||||
12
SVSim.Bootstrap/Models/Seed/SpotCardExchangeSeed.cs
Normal file
12
SVSim.Bootstrap/Models/Seed/SpotCardExchangeSeed.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.Bootstrap.Models.Seed;
|
||||||
|
|
||||||
|
public sealed class SpotCardExchangeSeed
|
||||||
|
{
|
||||||
|
[JsonPropertyName("card_id")] public long CardId { get; set; }
|
||||||
|
[JsonPropertyName("class")] public int ClassId { get; set; }
|
||||||
|
[JsonPropertyName("exchange_point")] public int ExchangePoint { get; set; }
|
||||||
|
[JsonPropertyName("ts_rotation_id")] public long TsRotationId { get; set; }
|
||||||
|
[JsonPropertyName("is_pre_release")] public bool IsPreRelease { get; set; }
|
||||||
|
}
|
||||||
@@ -87,6 +87,9 @@ public static class Program
|
|||||||
await new BattlePassImporter().ImportAsync(context, opts.SeedDir);
|
await new BattlePassImporter().ImportAsync(context, opts.SeedDir);
|
||||||
await new BattlePassSeasonImporter().ImportAsync(context, opts.SeedDir);
|
await new BattlePassSeasonImporter().ImportAsync(context, opts.SeedDir);
|
||||||
await new BattlePassRewardImporter().ImportAsync(context, opts.SeedDir);
|
await new BattlePassRewardImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new MissionCatalogImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new AchievementCatalogImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new BattlePassMonthlyMissionImporter().ImportAsync(context, opts.SeedDir);
|
||||||
await new DailyLoginBonusImporter().ImportAsync(context, opts.SeedDir);
|
await new DailyLoginBonusImporter().ImportAsync(context, opts.SeedDir);
|
||||||
await new PreReleaseInfoImporter().ImportAsync(context, opts.SeedDir);
|
await new PreReleaseInfoImporter().ImportAsync(context, opts.SeedDir);
|
||||||
await new CardListsImporter().ImportAsync(context, opts.SeedDir);
|
await new CardListsImporter().ImportAsync(context, opts.SeedDir);
|
||||||
@@ -94,6 +97,11 @@ public static class Program
|
|||||||
|
|
||||||
await new PracticeOpponentImporter().ImportAsync(context, opts.SeedDir);
|
await new PracticeOpponentImporter().ImportAsync(context, opts.SeedDir);
|
||||||
await new PaymentItemImporter().ImportAsync(context, opts.SeedDir);
|
await new PaymentItemImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new ItemImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new SleeveShopImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new ItemPurchaseImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new LeaderSkinShopImporter().ImportAsync(context, opts.SeedDir);
|
||||||
|
await new SpotCardExchangeImporter().ImportAsync(context, opts.SeedDir);
|
||||||
var puzzleImporter = new PuzzleImporter();
|
var puzzleImporter = new PuzzleImporter();
|
||||||
await puzzleImporter.ImportGroupsAsync(context, opts.SeedDir);
|
await puzzleImporter.ImportGroupsAsync(context, opts.SeedDir);
|
||||||
await puzzleImporter.ImportPuzzlesAsync(context, opts.SeedDir);
|
await puzzleImporter.ImportPuzzlesAsync(context, opts.SeedDir);
|
||||||
|
|||||||
3262
SVSim.Database/Migrations/20260527140924_AddMissionsAndAchievements.Designer.cs
generated
Normal file
3262
SVSim.Database/Migrations/20260527140924_AddMissionsAndAchievements.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddMissionsAndAchievements : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AchievementCatalog",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
AchievementType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Level = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
RequireNumber = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardDetailId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
RewardNumber = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
OrderNum = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
EventType = table.Column<string>(type: "text", nullable: true),
|
||||||
|
EventArg = table.Column<int>(type: "integer", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AchievementCatalog", x => new { x.AchievementType, x.Level });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "BattlePassMonthlyMissions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Year = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Month = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
OrderNum = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
RequireNumber = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
BattlePassPoint = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardType = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
RewardDetailId = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
RewardNumber = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
EventType = table.Column<string>(type: "text", nullable: true),
|
||||||
|
EventArg = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_BattlePassMonthlyMissions", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "MissionCatalog",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
LotType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RequireNumber = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardDetailId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
RewardNumber = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
BattlePassPoint = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
DefaultFlag = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
EventType = table.Column<string>(type: "text", nullable: true),
|
||||||
|
EventArg = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
StartTime = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
EndTime = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_MissionCatalog", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ViewerAchievements",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ViewerId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
AchievementType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Level = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
AchievementStatus = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
NowAchievedLevel = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ResultAnnounceSawLevel = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ViewerAchievements", x => new { x.ViewerId, x.AchievementType });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ViewerAchievements_Viewers_ViewerId",
|
||||||
|
column: x => x.ViewerId,
|
||||||
|
principalTable: "Viewers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ViewerEventCounters",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ViewerId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
EventKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Period = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Count = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ViewerEventCounters", x => new { x.ViewerId, x.EventKey, x.Period });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ViewerEventCounters_Viewers_ViewerId",
|
||||||
|
column: x => x.ViewerId,
|
||||||
|
principalTable: "Viewers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ViewerMissions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
ViewerId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
MissionCatalogId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Slot = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
AssignedAt = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ClaimedAt = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
MissionStatus = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ViewerMissions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ViewerMissions_Viewers_ViewerId",
|
||||||
|
column: x => x.ViewerId,
|
||||||
|
principalTable: "Viewers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AchievementCatalog_AchievementType",
|
||||||
|
table: "AchievementCatalog",
|
||||||
|
column: "AchievementType");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AchievementCatalog_EventType_EventArg",
|
||||||
|
table: "AchievementCatalog",
|
||||||
|
columns: new[] { "EventType", "EventArg" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BattlePassMonthlyMissions_Year_Month",
|
||||||
|
table: "BattlePassMonthlyMissions",
|
||||||
|
columns: new[] { "Year", "Month" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_BattlePassMonthlyMissions_Year_Month_OrderNum",
|
||||||
|
table: "BattlePassMonthlyMissions",
|
||||||
|
columns: new[] { "Year", "Month", "OrderNum" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MissionCatalog_EventType_EventArg",
|
||||||
|
table: "MissionCatalog",
|
||||||
|
columns: new[] { "EventType", "EventArg" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_MissionCatalog_LotType",
|
||||||
|
table: "MissionCatalog",
|
||||||
|
column: "LotType");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ViewerEventCounters_ViewerId_Period",
|
||||||
|
table: "ViewerEventCounters",
|
||||||
|
columns: new[] { "ViewerId", "Period" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ViewerMissions_ViewerId",
|
||||||
|
table: "ViewerMissions",
|
||||||
|
column: "ViewerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ViewerMissions_ViewerId_Slot",
|
||||||
|
table: "ViewerMissions",
|
||||||
|
columns: new[] { "ViewerId", "Slot" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AchievementCatalog");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "BattlePassMonthlyMissions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "MissionCatalog");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ViewerAchievements");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ViewerEventCounters");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ViewerMissions");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3268
SVSim.Database/Migrations/20260527175337_AddViewerUdid.Designer.cs
generated
Normal file
3268
SVSim.Database/Migrations/20260527175337_AddViewerUdid.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
SVSim.Database/Migrations/20260527175337_AddViewerUdid.cs
Normal file
40
SVSim.Database/Migrations/20260527175337_AddViewerUdid.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddViewerUdid : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "Udid",
|
||||||
|
table: "Viewers",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Viewers_Udid",
|
||||||
|
table: "Viewers",
|
||||||
|
column: "Udid",
|
||||||
|
unique: true,
|
||||||
|
filter: "\"Udid\" IS NOT NULL");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Viewers_Udid",
|
||||||
|
table: "Viewers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Udid",
|
||||||
|
table: "Viewers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3271
SVSim.Database/Migrations/20260527184324_AddSocialAccountConnectionUniqueIndex.Designer.cs
generated
Normal file
3271
SVSim.Database/Migrations/20260527184324_AddSocialAccountConnectionUniqueIndex.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSocialAccountConnectionUniqueIndex : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SocialAccountConnection_AccountType_AccountId",
|
||||||
|
table: "SocialAccountConnection",
|
||||||
|
columns: new[] { "AccountType", "AccountId" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_SocialAccountConnection_AccountType_AccountId",
|
||||||
|
table: "SocialAccountConnection");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3278
SVSim.Database/Migrations/20260528013825_AddItemTypeAndThumbnail.Designer.cs
generated
Normal file
3278
SVSim.Database/Migrations/20260528013825_AddItemTypeAndThumbnail.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddItemTypeAndThumbnail : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ThumbnailPath",
|
||||||
|
table: "Items",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Type",
|
||||||
|
table: "Items",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ThumbnailPath",
|
||||||
|
table: "Items");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Type",
|
||||||
|
table: "Items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3383
SVSim.Database/Migrations/20260528015716_AddSleeveShop.Designer.cs
generated
Normal file
3383
SVSim.Database/Migrations/20260528015716_AddSleeveShop.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
96
SVSim.Database/Migrations/20260528015716_AddSleeveShop.cs
Normal file
96
SVSim.Database/Migrations/20260528015716_AddSleeveShop.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSleeveShop : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SleeveShopSeries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
IsNew = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SleeveShopSeries", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SleeveShopProducts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SeriesId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
NameKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
PriceCrystal = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
PriceRupy = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SleeveShopProducts", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SleeveShopProducts_SleeveShopSeries_SeriesId",
|
||||||
|
column: x => x.SeriesId,
|
||||||
|
principalTable: "SleeveShopSeries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SleeveShopProductRewardEntry",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
SleeveShopProductEntryId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
OrderIndex = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardDetailId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
RewardNumber = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SleeveShopProductRewardEntry", x => new { x.SleeveShopProductEntryId, x.Id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SleeveShopProductRewardEntry_SleeveShopProducts_SleeveShopP~",
|
||||||
|
column: x => x.SleeveShopProductEntryId,
|
||||||
|
principalTable: "SleeveShopProducts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SleeveShopProducts_SeriesId",
|
||||||
|
table: "SleeveShopProducts",
|
||||||
|
column: "SeriesId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SleeveShopProductRewardEntry");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SleeveShopProducts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SleeveShopSeries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3430
SVSim.Database/Migrations/20260528021818_AddItemPurchaseCatalog.Designer.cs
generated
Normal file
3430
SVSim.Database/Migrations/20260528021818_AddItemPurchaseCatalog.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddItemPurchaseCatalog : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ItemPurchaseCatalog",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RequireItemType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RequireItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
RequireItemNum = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
PurchaseItemType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
PurchaseItemId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
PurchaseItemNum = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
PurchaseName = table.Column<string>(type: "text", nullable: false),
|
||||||
|
IsMonthlyReset = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
PurchaseLimit = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ItemPurchaseCatalog", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ItemPurchaseCatalog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3627
SVSim.Database/Migrations/20260528024430_AddLeaderSkinShop.Designer.cs
generated
Normal file
3627
SVSim.Database/Migrations/20260528024430_AddLeaderSkinShop.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
155
SVSim.Database/Migrations/20260528024430_AddLeaderSkinShop.cs
Normal file
155
SVSim.Database/Migrations/20260528024430_AddLeaderSkinShop.cs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddLeaderSkinShop : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "LeaderSkinShopSeries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
IsNew = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
SetSalesStatus = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SetPriceCrystal = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
SetPriceRupy = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
SetPriceTicket = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
SetPriceTicketId = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
SetCompletionRewardStatus = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_LeaderSkinShopSeries", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ViewerLeaderSkinSetClaims",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ViewerId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
SeriesId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ClaimedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ViewerLeaderSkinSetClaims", x => new { x.ViewerId, x.SeriesId });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "LeaderSkinShopProducts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SeriesId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
LeaderSkinId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ProductNameKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
IntroductionKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CvNameKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
SinglePriceCrystal = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
SinglePriceRupy = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
SinglePriceTicket = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
TicketNumber = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
TicketItemId = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_LeaderSkinShopProducts", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_LeaderSkinShopProducts_LeaderSkinShopSeries_SeriesId",
|
||||||
|
column: x => x.SeriesId,
|
||||||
|
principalTable: "LeaderSkinShopSeries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "LeaderSkinShopSeriesRewardEntry",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
LeaderSkinShopSeriesEntryId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
OrderIndex = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardDetailId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
RewardNumber = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_LeaderSkinShopSeriesRewardEntry", x => new { x.LeaderSkinShopSeriesEntryId, x.Id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_LeaderSkinShopSeriesRewardEntry_LeaderSkinShopSeries_Leader~",
|
||||||
|
column: x => x.LeaderSkinShopSeriesEntryId,
|
||||||
|
principalTable: "LeaderSkinShopSeries",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "LeaderSkinShopProductRewardEntry",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
LeaderSkinShopProductEntryId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
OrderIndex = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
RewardDetailId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
RewardNumber = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_LeaderSkinShopProductRewardEntry", x => new { x.LeaderSkinShopProductEntryId, x.Id });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_LeaderSkinShopProductRewardEntry_LeaderSkinShopProducts_Lea~",
|
||||||
|
column: x => x.LeaderSkinShopProductEntryId,
|
||||||
|
principalTable: "LeaderSkinShopProducts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_LeaderSkinShopProducts_SeriesId",
|
||||||
|
table: "LeaderSkinShopProducts",
|
||||||
|
column: "SeriesId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ViewerLeaderSkinSetClaims_ViewerId",
|
||||||
|
table: "ViewerLeaderSkinSetClaims",
|
||||||
|
column: "ViewerId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "LeaderSkinShopProductRewardEntry");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "LeaderSkinShopSeriesRewardEntry");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ViewerLeaderSkinSetClaims");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "LeaderSkinShopProducts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "LeaderSkinShopSeries");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3685
SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.Designer.cs
generated
Normal file
3685
SVSim.Database/Migrations/20260528030221_AddSpotCardExchange.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SVSim.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSpotCardExchange : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "Currency_SpotPoints",
|
||||||
|
table: "Viewers",
|
||||||
|
type: "numeric(20,0)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SpotCardExchangeCatalog",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
CardId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ClassId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ExchangePoint = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TsRotationId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
IsPreRelease = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
DateCreated = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
DateUpdated = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SpotCardExchangeCatalog", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ViewerSpotCardExchanges",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
ViewerId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
CardId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
IsPreRelease = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
ExchangedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ViewerSpotCardExchanges", x => new { x.ViewerId, x.CardId });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ViewerSpotCardExchanges_ViewerId",
|
||||||
|
table: "ViewerSpotCardExchanges",
|
||||||
|
column: "ViewerId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SpotCardExchangeCatalog");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ViewerSpotCardExchanges");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Currency_SpotPoints",
|
||||||
|
table: "Viewers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -370,6 +370,48 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("ViewerStoryProgress");
|
b.ToTable("ViewerStoryProgress");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.AchievementCatalogEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AchievementType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Level")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("EventArg")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RequireNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long>("RewardDetailId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("RewardNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RewardType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("AchievementType", "Level");
|
||||||
|
|
||||||
|
b.HasIndex("AchievementType");
|
||||||
|
|
||||||
|
b.HasIndex("EventType", "EventArg");
|
||||||
|
|
||||||
|
b.ToTable("AchievementCatalog");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.ArenaSeasonConfig", b =>
|
modelBuilder.Entity("SVSim.Database.Models.ArenaSeasonConfig", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -516,6 +558,64 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("BattlePassLevels");
|
b.ToTable("BattlePassLevels");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.BattlePassMonthlyMissionEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("BattlePassPoint")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int?>("EventArg")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Month")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("OrderNum")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RequireNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long?>("RewardDetailId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int?>("RewardNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("RewardType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Year")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Year", "Month");
|
||||||
|
|
||||||
|
b.HasIndex("Year", "Month", "OrderNum")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("BattlePassMonthlyMissions");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.BattlePassRewardEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -995,11 +1095,65 @@ namespace SVSim.Database.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ThumbnailPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Items");
|
b.ToTable("Items");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ItemPurchaseCatalogEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsMonthlyReset")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<long>("PurchaseItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("PurchaseItemNum")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("PurchaseItemType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("PurchaseLimit")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("PurchaseName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<long>("RequireItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("RequireItemNum")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RequireItemType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ItemPurchaseCatalog");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -1028,6 +1182,100 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("LeaderSkins");
|
b.ToTable("LeaderSkins");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("CvNameKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("IntroductionKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("LeaderSkinId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ProductNameKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SinglePriceCrystal")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SinglePriceRupy")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SinglePriceTicket")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long?>("TicketItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int?>("TicketNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("LeaderSkinShopProducts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsNew")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<int>("SetCompletionRewardStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SetPriceCrystal")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SetPriceRupy")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("SetPriceTicket")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long?>("SetPriceTicketId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("SetSalesStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("LeaderSkinShopSeries");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.LoadingExclusionCardEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.LoadingExclusionCardEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -1094,6 +1342,63 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("MasterPointRankingPeriods");
|
b.ToTable("MasterPointRankingPeriods");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.MissionCatalogEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("BattlePassPoint")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("DefaultFlag")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<long?>("EndTime")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int?>("EventArg")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("EventType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("LotType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("RequireNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long>("RewardDetailId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("RewardNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("RewardType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long>("StartTime")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LotType");
|
||||||
|
|
||||||
|
b.HasIndex("EventType", "EventArg");
|
||||||
|
|
||||||
|
b.ToTable("MissionCatalog");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.MyPageBackgroundEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.MyPageBackgroundEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -1800,6 +2105,62 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("Sleeves");
|
b.ToTable("Sleeves");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("NameKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("PriceCrystal")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("PriceRupy")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SeriesId");
|
||||||
|
|
||||||
|
b.ToTable("SleeveShopProducts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsNew")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SleeveShopSeries");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.SpecialDeckFormatEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.SpecialDeckFormatEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -1845,6 +2206,40 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("SpotCards");
|
b.ToTable("SpotCards");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.SpotCardExchangeEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("CardId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("ClassId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("ExchangePoint")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPreRelease")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<long>("TsRotationId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("SpotCardExchangeCatalog");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -1895,13 +2290,44 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("ShortUdid"), "ShortUdidSequence");
|
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("ShortUdid"), "ShortUdidSequence");
|
||||||
|
|
||||||
|
b.Property<Guid?>("Udid")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ShortUdid");
|
b.HasIndex("ShortUdid");
|
||||||
|
|
||||||
|
b.HasIndex("Udid")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Viewers");
|
b.ToTable("Viewers");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ViewerId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("AchievementType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("AchievementStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Level")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("NowAchievedLevel")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ResultAnnounceSawLevel")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("ViewerId", "AchievementType");
|
||||||
|
|
||||||
|
b.ToTable("ViewerAchievements");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassClaimEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.ViewerBattlePassClaimEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -1981,6 +2407,87 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("ViewerBattlePassProgress");
|
b.ToTable("ViewerBattlePassProgress");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ViewerId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("EventKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Period")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Count")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("ViewerId", "EventKey", "Period");
|
||||||
|
|
||||||
|
b.HasIndex("ViewerId", "Period");
|
||||||
|
|
||||||
|
b.ToTable("ViewerEventCounters");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerLeaderSkinSetClaim", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ViewerId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("SeriesId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ClaimedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("ViewerId", "SeriesId");
|
||||||
|
|
||||||
|
b.HasIndex("ViewerId");
|
||||||
|
|
||||||
|
b.ToTable("ViewerLeaderSkinSetClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<long>("AssignedAt")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long?>("ClaimedAt")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("DateCreated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DateUpdated")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("MissionCatalogId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("MissionStatus")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Slot")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long>("ViewerId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ViewerId");
|
||||||
|
|
||||||
|
b.HasIndex("ViewerId", "Slot")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ViewerMissions");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.ViewerPuzzleClear", b =>
|
modelBuilder.Entity("SVSim.Database.Models.ViewerPuzzleClear", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("ViewerId")
|
b.Property<long>("ViewerId")
|
||||||
@@ -2000,6 +2507,27 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("ViewerPuzzleClears");
|
b.ToTable("ViewerPuzzleClears");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerSpotCardExchange", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("ViewerId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("CardId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExchangedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPreRelease")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("ViewerId", "CardId");
|
||||||
|
|
||||||
|
b.HasIndex("ViewerId");
|
||||||
|
|
||||||
|
b.ToTable("ViewerSpotCardExchanges");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SleeveEntryViewer", b =>
|
modelBuilder.Entity("SleeveEntryViewer", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("SleevesId")
|
b.Property<int>("SleevesId")
|
||||||
@@ -2353,6 +2881,86 @@ namespace SVSim.Database.Migrations
|
|||||||
b.Navigation("Class");
|
b.Navigation("Class");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopProductEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SVSim.Database.Models.LeaderSkinShopSeriesEntry", "Series")
|
||||||
|
.WithMany("Products")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.OwnsMany("SVSim.Database.Models.LeaderSkinShopProductRewardEntry", "Rewards", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("LeaderSkinShopProductEntryId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||||
|
|
||||||
|
b1.Property<int>("OrderIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<long>("RewardDetailId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("RewardNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<int>("RewardType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("LeaderSkinShopProductEntryId", "Id");
|
||||||
|
|
||||||
|
b1.ToTable("LeaderSkinShopProductRewardEntry");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("LeaderSkinShopProductEntryId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Rewards");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b =>
|
||||||
|
{
|
||||||
|
b.OwnsMany("SVSim.Database.Models.LeaderSkinShopSeriesRewardEntry", "SetCompletionRewards", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("LeaderSkinShopSeriesEntryId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||||
|
|
||||||
|
b1.Property<int>("OrderIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<long>("RewardDetailId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("RewardNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<int>("RewardType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("LeaderSkinShopSeriesEntryId", "Id");
|
||||||
|
|
||||||
|
b1.ToTable("LeaderSkinShopSeriesRewardEntry");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("LeaderSkinShopSeriesEntryId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("SetCompletionRewards");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.PackConfigEntry", b =>
|
||||||
{
|
{
|
||||||
b.OwnsMany("SVSim.Database.Models.PackBannerEntry", "Banners", b1 =>
|
b.OwnsMany("SVSim.Database.Models.PackBannerEntry", "Banners", b1 =>
|
||||||
@@ -2570,6 +3178,50 @@ namespace SVSim.Database.Migrations
|
|||||||
b.Navigation("Sleeve");
|
b.Navigation("Sleeve");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.SleeveShopProductEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SVSim.Database.Models.SleeveShopSeriesEntry", "Series")
|
||||||
|
.WithMany("Products")
|
||||||
|
.HasForeignKey("SeriesId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.OwnsMany("SVSim.Database.Models.SleeveShopProductRewardEntry", "Rewards", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<int>("SleeveShopProductEntryId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
|
||||||
|
|
||||||
|
b1.Property<int>("OrderIndex")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<long>("RewardDetailId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b1.Property<int>("RewardNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.Property<int>("RewardType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b1.HasKey("SleeveShopProductEntryId", "Id");
|
||||||
|
|
||||||
|
b1.ToTable("SleeveShopProductRewardEntry");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("SleeveShopProductEntryId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("Rewards");
|
||||||
|
|
||||||
|
b.Navigation("Series");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.Viewer", b =>
|
modelBuilder.Entity("SVSim.Database.Models.Viewer", b =>
|
||||||
{
|
{
|
||||||
b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 =>
|
b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 =>
|
||||||
@@ -2672,6 +3324,9 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b1.HasKey("ViewerId", "Id");
|
b1.HasKey("ViewerId", "Id");
|
||||||
|
|
||||||
|
b1.HasIndex("AccountType", "AccountId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b1.ToTable("SocialAccountConnection");
|
b1.ToTable("SocialAccountConnection");
|
||||||
|
|
||||||
b1.WithOwner("Viewer")
|
b1.WithOwner("Viewer")
|
||||||
@@ -2790,6 +3445,9 @@ namespace SVSim.Database.Migrations
|
|||||||
b1.Property<decimal>("Rupees")
|
b1.Property<decimal>("Rupees")
|
||||||
.HasColumnType("numeric(20,0)");
|
.HasColumnType("numeric(20,0)");
|
||||||
|
|
||||||
|
b1.Property<decimal>("SpotPoints")
|
||||||
|
.HasColumnType("numeric(20,0)");
|
||||||
|
|
||||||
b1.Property<decimal>("SteamCrystals")
|
b1.Property<decimal>("SteamCrystals")
|
||||||
.HasColumnType("numeric(20,0)");
|
.HasColumnType("numeric(20,0)");
|
||||||
|
|
||||||
@@ -2931,6 +3589,33 @@ namespace SVSim.Database.Migrations
|
|||||||
b.Navigation("SocialAccountConnections");
|
b.Navigation("SocialAccountConnections");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerAchievement", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SVSim.Database.Models.Viewer", null)
|
||||||
|
.WithMany("Achievements")
|
||||||
|
.HasForeignKey("ViewerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerEventCounter", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SVSim.Database.Models.Viewer", null)
|
||||||
|
.WithMany("EventCounters")
|
||||||
|
.HasForeignKey("ViewerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.ViewerMission", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SVSim.Database.Models.Viewer", null)
|
||||||
|
.WithMany("Missions")
|
||||||
|
.HasForeignKey("ViewerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SleeveEntryViewer", b =>
|
modelBuilder.Entity("SleeveEntryViewer", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("SVSim.Database.Models.SleeveEntry", null)
|
b.HasOne("SVSim.Database.Models.SleeveEntry", null)
|
||||||
@@ -2961,6 +3646,11 @@ namespace SVSim.Database.Migrations
|
|||||||
b.Navigation("LeaderSkins");
|
b.Navigation("LeaderSkins");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinShopSeriesEntry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Products");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.PuzzleGroupEntry", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Puzzles");
|
b.Navigation("Puzzles");
|
||||||
@@ -2971,9 +3661,20 @@ namespace SVSim.Database.Migrations
|
|||||||
b.Navigation("Cards");
|
b.Navigation("Cards");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SVSim.Database.Models.SleeveShopSeriesEntry", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Products");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.Viewer", b =>
|
modelBuilder.Entity("SVSim.Database.Models.Viewer", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Achievements");
|
||||||
|
|
||||||
b.Navigation("Decks");
|
b.Navigation("Decks");
|
||||||
|
|
||||||
|
b.Navigation("EventCounters");
|
||||||
|
|
||||||
|
b.Navigation("Missions");
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
|
|||||||
23
SVSim.Database/Models/AchievementCatalogEntry.cs
Normal file
23
SVSim.Database/Models/AchievementCatalogEntry.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One tier of an achievement. PK is composite (AchievementType, Level). Rows are seeded from
|
||||||
|
/// <c>seeds/achievement-catalog.json</c>. The captured tier IS the max tier in our world —
|
||||||
|
/// max_level on the wire is computed as MAX(Level) per AchievementType at /mission/info time.
|
||||||
|
/// Inherits Id from BaseEntity but the Id is unused; PK is configured in DbContext.
|
||||||
|
/// </summary>
|
||||||
|
public class AchievementCatalogEntry
|
||||||
|
{
|
||||||
|
public int AchievementType { get; set; }
|
||||||
|
public int Level { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int RequireNumber { get; set; }
|
||||||
|
public int RewardType { get; set; }
|
||||||
|
public long RewardDetailId { get; set; }
|
||||||
|
public int RewardNumber { get; set; }
|
||||||
|
public int OrderNum { get; set; }
|
||||||
|
public string? EventType { get; set; }
|
||||||
|
public int? EventArg { get; set; }
|
||||||
|
}
|
||||||
28
SVSim.Database/Models/BattlePassMonthlyMissionEntry.cs
Normal file
28
SVSim.Database/Models/BattlePassMonthlyMissionEntry.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row of the BP monthly mission list, keyed to a specific (Year, Month).
|
||||||
|
/// `RewardType` is nullable because some monthly missions only award BP points (capture shows
|
||||||
|
/// the "Play 5 Challenge matches" entry has no reward_info block on wire).
|
||||||
|
/// Id is auto-generated — override BaseEntity's [DatabaseGenerated(None)] default.
|
||||||
|
/// </summary>
|
||||||
|
public class BattlePassMonthlyMissionEntry : BaseEntity<int>
|
||||||
|
{
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public override int Id { get; set; }
|
||||||
|
|
||||||
|
public int Year { get; set; }
|
||||||
|
public int Month { get; set; }
|
||||||
|
public int OrderNum { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int RequireNumber { get; set; }
|
||||||
|
public int BattlePassPoint { get; set; }
|
||||||
|
public int? RewardType { get; set; }
|
||||||
|
public long? RewardDetailId { get; set; }
|
||||||
|
public int? RewardNumber { get; set; }
|
||||||
|
public string? EventType { get; set; }
|
||||||
|
public int? EventArg { get; set; }
|
||||||
|
}
|
||||||
@@ -2,7 +2,21 @@ using SVSim.Database.Common;
|
|||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item master row. Mirrors the client's <c>item_master.csv</c> + <c>itemtext.json</c>
|
||||||
|
/// (under <c>data_dumps/client_master_csv/</c>): <see cref="Type"/> matches the client-side
|
||||||
|
/// item_type enum (1 = challenge ticket, 2 = card-pack ticket, 3 = premium orb,
|
||||||
|
/// 4 = colosseum ticket, 5 = orb piece, 6 = skin/event ticket, 7 = other);
|
||||||
|
/// <see cref="ThumbnailPath"/> is the client-resolved sprite key.
|
||||||
|
/// </summary>
|
||||||
public class ItemEntry : BaseEntity<int>
|
public class ItemEntry : BaseEntity<int>
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Client-side item_type enum (1-7). Drives shop categorisation, e.g.
|
||||||
|
/// <c>user_card_pack_ticket_list</c> in /item_purchase/info filters on Type == 2.</summary>
|
||||||
|
public int Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Sprite key, e.g. <c>"ticket_10032"</c>. Empty when unknown.</summary>
|
||||||
|
public string ThumbnailPath { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
39
SVSim.Database/Models/ItemPurchaseCatalogEntry.cs
Normal file
39
SVSim.Database/Models/ItemPurchaseCatalogEntry.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row of the /item_purchase/info catalog — an exchange the user can perform N times per
|
||||||
|
/// period (monthly or lifetime) by spending <c>RequireItem*</c> to acquire <c>PurchaseItem*</c>.
|
||||||
|
/// PK = wire <c>purchase_id</c>.
|
||||||
|
/// <para>
|
||||||
|
/// Both sides reference <see cref="Enums.UserGoodsType"/>. Captures show the common shape is
|
||||||
|
/// currency-for-item (RedEther 5000 → Seer's Globe ×1) or item-for-item (Orb Shard ×5 →
|
||||||
|
/// Seer's Globe ×1). Per-viewer remaining quota lives in
|
||||||
|
/// <see cref="ViewerEventCounter"/> keyed by <c>"item_purchase:{Id}"</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class ItemPurchaseCatalogEntry : BaseEntity<int>
|
||||||
|
{
|
||||||
|
public int RequireItemType { get; set; }
|
||||||
|
public long RequireItemId { get; set; }
|
||||||
|
public int RequireItemNum { get; set; }
|
||||||
|
|
||||||
|
public int PurchaseItemType { get; set; }
|
||||||
|
public long PurchaseItemId { get; set; }
|
||||||
|
public int PurchaseItemNum { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SystemText-ready display name. May be empty — the client falls back to a templated name
|
||||||
|
/// built from <c>UserGoods.getUserGoodsName + count</c> via SystemText key "Shop_0132".
|
||||||
|
/// </summary>
|
||||||
|
public string PurchaseName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>True → quota resets at the start of each JST month. False → lifetime quota.</summary>
|
||||||
|
public bool IsMonthlyReset { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Per-period purchase cap. Wire <c>rest</c> = max(0, PurchaseLimit - counter).</summary>
|
||||||
|
public int PurchaseLimit { get; set; }
|
||||||
|
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
}
|
||||||
36
SVSim.Database/Models/LeaderSkinShopProductEntry.cs
Normal file
36
SVSim.Database/Models/LeaderSkinShopProductEntry.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One purchasable leader-skin product. PK = wire product_id (small ints in captures — e.g. 31,
|
||||||
|
/// 165, 166). FK <see cref="SeriesId"/>. <see cref="LeaderSkinId"/> points at the
|
||||||
|
/// <see cref="LeaderSkinEntry"/> the buyer ends up owning.
|
||||||
|
/// </summary>
|
||||||
|
public class LeaderSkinShopProductEntry : BaseEntity<int>
|
||||||
|
{
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
public int LeaderSkinId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>SystemText keys — resolved client-side via Data.Master.GetLeaderSkinProductText.</summary>
|
||||||
|
public string ProductNameKey { get; set; } = string.Empty;
|
||||||
|
public string IntroductionKey { get; set; } = string.Empty;
|
||||||
|
public string CvNameKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-product price for solo buy. Captures consistently show crystal/rupy parity for
|
||||||
|
/// regular skins (500c / 500r single, 400 unit-price when bought as set). Nullable so
|
||||||
|
/// promotions can offer one currency without the other.
|
||||||
|
/// </summary>
|
||||||
|
public int? SinglePriceCrystal { get; set; }
|
||||||
|
public int? SinglePriceRupy { get; set; }
|
||||||
|
public int? SinglePriceTicket { get; set; }
|
||||||
|
public int? TicketNumber { get; set; }
|
||||||
|
public long? TicketItemId { get; set; }
|
||||||
|
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
|
||||||
|
public List<LeaderSkinShopProductRewardEntry> Rewards { get; set; } = new();
|
||||||
|
|
||||||
|
public LeaderSkinShopSeriesEntry? Series { get; set; }
|
||||||
|
}
|
||||||
17
SVSim.Database/Models/LeaderSkinShopProductRewardEntry.cs
Normal file
17
SVSim.Database/Models/LeaderSkinShopProductRewardEntry.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One per-buy reward attached to a leader-skin product. Owned by
|
||||||
|
/// <see cref="LeaderSkinShopProductEntry"/>. Captures show each skin product bundles 3 rewards:
|
||||||
|
/// the skin itself (type=10), the matching emblem (type=7), and the matching sleeve (type=6).
|
||||||
|
/// </summary>
|
||||||
|
[Owned]
|
||||||
|
public class LeaderSkinShopProductRewardEntry
|
||||||
|
{
|
||||||
|
public int OrderIndex { get; set; }
|
||||||
|
public int RewardType { get; set; } // Wizard.UserGoods.Type
|
||||||
|
public long RewardDetailId { get; set; }
|
||||||
|
public int RewardNumber { get; set; }
|
||||||
|
}
|
||||||
33
SVSim.Database/Models/LeaderSkinShopSeriesEntry.cs
Normal file
33
SVSim.Database/Models/LeaderSkinShopSeriesEntry.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One leader-skin-shop series (a themed collection — e.g. "7th Anniversary Skins").
|
||||||
|
/// PK = wire series_id. <see cref="SetSalesStatus"/> controls whether the per-series
|
||||||
|
/// "buy whole set" UI is offered: 0=none (single-skin purchases only), non-zero=set sale active.
|
||||||
|
/// When set-active, the set-price + set-completion-reward fields are populated.
|
||||||
|
/// </summary>
|
||||||
|
public class LeaderSkinShopSeriesEntry : BaseEntity<int>
|
||||||
|
{
|
||||||
|
public bool IsNew { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>SkinSeriesPurchaseInfo.eSetSalesStatus — 0=None.</summary>
|
||||||
|
public int SetSalesStatus { get; set; }
|
||||||
|
|
||||||
|
public int? SetPriceCrystal { get; set; }
|
||||||
|
public int? SetPriceRupy { get; set; }
|
||||||
|
public int? SetPriceTicket { get; set; }
|
||||||
|
public long? SetPriceTicketId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SkinSeriesPurchaseInfo.RewardStatus — 0=none. The per-VIEWER claim state is computed
|
||||||
|
/// at request time from <see cref="ViewerLeaderSkinSetClaim"/>; this column is the catalog
|
||||||
|
/// default surfaced when no viewer is in context (or when set_sales_status==0).
|
||||||
|
/// </summary>
|
||||||
|
public int SetCompletionRewardStatus { get; set; }
|
||||||
|
|
||||||
|
public List<LeaderSkinShopProductEntry> Products { get; set; } = new();
|
||||||
|
public List<LeaderSkinShopSeriesRewardEntry> SetCompletionRewards { get; set; } = new();
|
||||||
|
}
|
||||||
18
SVSim.Database/Models/LeaderSkinShopSeriesRewardEntry.cs
Normal file
18
SVSim.Database/Models/LeaderSkinShopSeriesRewardEntry.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One set-completion bonus item attached to a leader-skin series. Owned by
|
||||||
|
/// <see cref="LeaderSkinShopSeriesEntry"/>. Granted by /leader_skin/buy_set_item once the
|
||||||
|
/// viewer owns every skin in the series. Wire shape: entries inside
|
||||||
|
/// <c>rewards.items[]</c> on the per-series block of /leader_skin/products.
|
||||||
|
/// </summary>
|
||||||
|
[Owned]
|
||||||
|
public class LeaderSkinShopSeriesRewardEntry
|
||||||
|
{
|
||||||
|
public int OrderIndex { get; set; }
|
||||||
|
public int RewardType { get; set; }
|
||||||
|
public long RewardDetailId { get; set; }
|
||||||
|
public int RewardNumber { get; set; }
|
||||||
|
}
|
||||||
26
SVSim.Database/Models/MissionCatalogEntry.cs
Normal file
26
SVSim.Database/Models/MissionCatalogEntry.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One mission template. Id = wire mission_id. Rows are seeded from
|
||||||
|
/// <c>seeds/mission-catalog.json</c> (extracted from /mission/info captures).
|
||||||
|
/// LotType 2 = weekly rotation slot; LotType 6 = daily slot (per UserMission.GEM_MISSION_TYPE).
|
||||||
|
/// EventType is the catalog-side key the progress service matches against; NULL means the row
|
||||||
|
/// was captured but no event mapping has been added yet (importer logs a warning).
|
||||||
|
/// </summary>
|
||||||
|
public class MissionCatalogEntry : BaseEntity<int>
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public int LotType { get; set; }
|
||||||
|
public int RequireNumber { get; set; }
|
||||||
|
public int RewardType { get; set; }
|
||||||
|
public long RewardDetailId { get; set; }
|
||||||
|
public int RewardNumber { get; set; }
|
||||||
|
public int BattlePassPoint { get; set; }
|
||||||
|
public bool DefaultFlag { get; set; }
|
||||||
|
public string? EventType { get; set; }
|
||||||
|
public int? EventArg { get; set; }
|
||||||
|
public long StartTime { get; set; }
|
||||||
|
public long? EndTime { get; set; }
|
||||||
|
}
|
||||||
32
SVSim.Database/Models/SleeveShopProductEntry.cs
Normal file
32
SVSim.Database/Models/SleeveShopProductEntry.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One purchasable sleeve product. PK = wire product_id (e.g. 301901). FK SeriesId.
|
||||||
|
/// <para>
|
||||||
|
/// Both <see cref="PriceCrystal"/> and <see cref="PriceRupy"/> are nullable. At least one must be
|
||||||
|
/// populated for an enabled product (both zero = free, both null = invalid). Sleeves don't have
|
||||||
|
/// the two-tier intro/regular pricing that BuildDeck products use — one price per currency.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="Rewards"/> drives both the catalog display (in /sleeve/info) and the actual grant
|
||||||
|
/// list (in /sleeve/buy). The capture shows each sleeve product grants a sleeve (type=6) and an
|
||||||
|
/// emblem (type=7) — both faithful reward_detail_ids that exist in the cosmetic catalogs.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class SleeveShopProductEntry : BaseEntity<int>
|
||||||
|
{
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
/// <summary>Wire `name` field — SystemText key like "sleeve_138". Localised client-side.</summary>
|
||||||
|
public string NameKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? PriceCrystal { get; set; }
|
||||||
|
public int? PriceRupy { get; set; }
|
||||||
|
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
|
||||||
|
public List<SleeveShopProductRewardEntry> Rewards { get; set; } = new();
|
||||||
|
|
||||||
|
public SleeveShopSeriesEntry? Series { get; set; }
|
||||||
|
}
|
||||||
17
SVSim.Database/Models/SleeveShopProductRewardEntry.cs
Normal file
17
SVSim.Database/Models/SleeveShopProductRewardEntry.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One per-buy reward attached to a sleeve product. Owned by <see cref="SleeveShopProductEntry"/>.
|
||||||
|
/// Wire shape: one entry of the product-level `rewards` array in /sleeve/info. Order is
|
||||||
|
/// preserved by <see cref="OrderIndex"/> since the wire shape is an ordered array, not a dict.
|
||||||
|
/// </summary>
|
||||||
|
[Owned]
|
||||||
|
public class SleeveShopProductRewardEntry
|
||||||
|
{
|
||||||
|
public int OrderIndex { get; set; }
|
||||||
|
public int RewardType { get; set; } // Wizard.UserGoods.Type
|
||||||
|
public long RewardDetailId { get; set; }
|
||||||
|
public int RewardNumber { get; set; }
|
||||||
|
}
|
||||||
16
SVSim.Database/Models/SleeveShopSeriesEntry.cs
Normal file
16
SVSim.Database/Models/SleeveShopSeriesEntry.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One sleeve-shop series (a themed collection — e.g. series 3019 "BattlePass sleeves",
|
||||||
|
/// series 3004 "Granblue Fantasy collab"). PK = wire series_id. IsEnabled gates whether
|
||||||
|
/// /sleeve/info renders this series.
|
||||||
|
/// </summary>
|
||||||
|
public class SleeveShopSeriesEntry : BaseEntity<int>
|
||||||
|
{
|
||||||
|
public bool IsNew { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
|
||||||
|
public List<SleeveShopProductEntry> Products { get; set; } = new();
|
||||||
|
}
|
||||||
30
SVSim.Database/Models/SpotCardExchangeEntry.cs
Normal file
30
SVSim.Database/Models/SpotCardExchangeEntry.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One catalog entry of the /spot_card_exchange/top shop — a card the viewer can buy with
|
||||||
|
/// spot points. PK = wire card_id. Distinct from <see cref="SpotCardEntry"/> (which is the
|
||||||
|
/// /load/index data.spot_cards rental-cost list — a different concept).
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="TsRotationId"/> matches the card_set_id; cards cycle out of the exchange when
|
||||||
|
/// their set rotates. <see cref="IsPreRelease"/> distinguishes the pre-release-pool subset
|
||||||
|
/// gated by <c>pre_release_spot_card_exchange_limit</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class SpotCardExchangeEntry : BaseEntity<long>
|
||||||
|
{
|
||||||
|
public long CardId { get => Id; set => Id = value; }
|
||||||
|
|
||||||
|
/// <summary>Wire <c>class</c> field — clan id (0=Neutral, 1=Forestcraft, ..., 8).</summary>
|
||||||
|
public int ClassId { get; set; }
|
||||||
|
|
||||||
|
public int ExchangePoint { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Wire <c>ts_rotation_id</c> — card_set_id this card belongs to.</summary>
|
||||||
|
public long TsRotationId { get; set; }
|
||||||
|
|
||||||
|
public bool IsPreRelease { get; set; }
|
||||||
|
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ namespace SVSim.Database.Models;
|
|||||||
/// A user within the game system.
|
/// A user within the game system.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Index(nameof(ShortUdid))]
|
[Index(nameof(ShortUdid))]
|
||||||
|
[Index(nameof(Udid), IsUnique = true)]
|
||||||
public class Viewer : BaseEntity<long>
|
public class Viewer : BaseEntity<long>
|
||||||
{
|
{
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
@@ -17,11 +18,18 @@ public class Viewer : BaseEntity<long>
|
|||||||
/// This user's name displayed in game.
|
/// This user's name displayed in game.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DisplayName { get; set; } = String.Empty;
|
public string DisplayName { get; set; } = String.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This user's short identifier.
|
/// This user's short identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long ShortUdid { get; set; }
|
public long ShortUdid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The client's full UDID (AES key for the wire protocol). Set when the viewer is created
|
||||||
|
/// via <c>/tool/signup</c>; null for viewers created via the admin Steam-import path. Unique
|
||||||
|
/// when present — the partial filter is declared in the migration.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? Udid { get; set; }
|
||||||
|
|
||||||
public DateTime LastLogin { get; set; }
|
public DateTime LastLogin { get; set; }
|
||||||
|
|
||||||
@@ -59,6 +67,12 @@ public class Viewer : BaseEntity<long>
|
|||||||
|
|
||||||
public List<ViewerBuildDeckProductPurchase> BuildDeckPurchases { get; set; } = new List<ViewerBuildDeckProductPurchase>();
|
public List<ViewerBuildDeckProductPurchase> BuildDeckPurchases { get; set; } = new List<ViewerBuildDeckProductPurchase>();
|
||||||
|
|
||||||
|
public List<ViewerMission> Missions { get; set; } = new List<ViewerMission>();
|
||||||
|
|
||||||
|
public List<ViewerAchievement> Achievements { get; set; } = new List<ViewerAchievement>();
|
||||||
|
|
||||||
|
public List<ViewerEventCounter> EventCounters { get; set; } = new List<ViewerEventCounter>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Navigation Properties
|
#region Navigation Properties
|
||||||
|
|||||||
17
SVSim.Database/Models/ViewerAchievement.cs
Normal file
17
SVSim.Database/Models/ViewerAchievement.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-viewer state for one achievement type. Composite PK (ViewerId, AchievementType) configured
|
||||||
|
/// in DbContext. <c>Level</c> is the viewer's current tier; <c>max_level</c> on the wire is
|
||||||
|
/// derived from catalog as MAX(Level) per type. Lazy-created at /load/index time — one row per
|
||||||
|
/// AchievementCatalogEntries.AchievementType that the viewer doesn't yet have a row for.
|
||||||
|
/// </summary>
|
||||||
|
public class ViewerAchievement
|
||||||
|
{
|
||||||
|
public long ViewerId { get; set; }
|
||||||
|
public int AchievementType { get; set; }
|
||||||
|
public int Level { get; set; } = 1;
|
||||||
|
public int AchievementStatus { get; set; }
|
||||||
|
public int NowAchievedLevel { get; set; }
|
||||||
|
public int ResultAnnounceSawLevel { get; set; }
|
||||||
|
}
|
||||||
@@ -14,4 +14,11 @@ public class ViewerCurrency
|
|||||||
public ulong LifeTotalCrystals { get; set; }
|
public ulong LifeTotalCrystals { get; set; }
|
||||||
public ulong RedEther { get; set; }
|
public ulong RedEther { get; set; }
|
||||||
public ulong Rupees { get; set; }
|
public ulong Rupees { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spot card points — currency earned from battles/missions, spent at /spot_card_exchange/exchange.
|
||||||
|
/// Wire field <c>spot_point</c> in /load/index and /spot_card_exchange/top; reward_type 12
|
||||||
|
/// (<see cref="Enums.UserGoodsType.SpotCardPoint"/>) in reward_list entries.
|
||||||
|
/// </summary>
|
||||||
|
public ulong SpotPoints { get; set; }
|
||||||
}
|
}
|
||||||
15
SVSim.Database/Models/ViewerEventCounter.cs
Normal file
15
SVSim.Database/Models/ViewerEventCounter.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-viewer "how many times has this happened" counter. Composite PK
|
||||||
|
/// (ViewerId, EventKey, Period). Period strings: "all-time", "month:YYYY-MM",
|
||||||
|
/// "week:YYYY-W##", "day:YYYY-MM-DD" — all JST-anchored with 02:00 day-boundary.
|
||||||
|
/// Single source of truth for total_count / done_number on every wire shape.
|
||||||
|
/// </summary>
|
||||||
|
public class ViewerEventCounter
|
||||||
|
{
|
||||||
|
public long ViewerId { get; set; }
|
||||||
|
public string EventKey { get; set; } = "";
|
||||||
|
public string Period { get; set; } = "";
|
||||||
|
public int Count { get; set; }
|
||||||
|
}
|
||||||
14
SVSim.Database/Models/ViewerLeaderSkinSetClaim.cs
Normal file
14
SVSim.Database/Models/ViewerLeaderSkinSetClaim.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row per (viewer, leader-skin series) marking that the viewer has claimed the
|
||||||
|
/// series-completion bonus via /leader_skin/buy_set_item. Composite PK (ViewerId, SeriesId).
|
||||||
|
/// Standalone table (not a Viewer owned collection) to avoid the cartesian-explode pitfall
|
||||||
|
/// when loading the viewer graph — claim state is checked per-series, not per-viewer-load.
|
||||||
|
/// </summary>
|
||||||
|
public class ViewerLeaderSkinSetClaim
|
||||||
|
{
|
||||||
|
public long ViewerId { get; set; }
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
public DateTime ClaimedAt { get; set; }
|
||||||
|
}
|
||||||
24
SVSim.Database/Models/ViewerMission.cs
Normal file
24
SVSim.Database/Models/ViewerMission.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One assigned mission slot for a viewer. <c>Id</c> is the wire <c>UserMission.id</c> — echoed
|
||||||
|
/// back as the retire-request parameter, auto-generated. Slot 0 = daily (lot_type=6),
|
||||||
|
/// Slots 1..3 = weekly (lot_type=2). Progress (<c>total_count</c> on the wire) is NOT stored
|
||||||
|
/// here — it's read from <see cref="ViewerEventCounter"/> at response-build time, keyed by the
|
||||||
|
/// catalog row's EventType.
|
||||||
|
/// </summary>
|
||||||
|
public class ViewerMission : BaseEntity<long>
|
||||||
|
{
|
||||||
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
|
public override long Id { get; set; }
|
||||||
|
|
||||||
|
public long ViewerId { get; set; }
|
||||||
|
public int MissionCatalogId { get; set; }
|
||||||
|
public int Slot { get; set; }
|
||||||
|
public long AssignedAt { get; set; }
|
||||||
|
public long? ClaimedAt { get; set; }
|
||||||
|
public int MissionStatus { get; set; } = 1;
|
||||||
|
}
|
||||||
16
SVSim.Database/Models/ViewerSpotCardExchange.cs
Normal file
16
SVSim.Database/Models/ViewerSpotCardExchange.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row per (viewer, exchanged card). Composite PK (ViewerId, CardId). Standalone table
|
||||||
|
/// (not a Viewer owned collection) to avoid cartesian-explode on viewer-graph reads.
|
||||||
|
/// <see cref="IsPreRelease"/> snapshot at exchange time so the pre-release counter can be
|
||||||
|
/// computed without joining back to <see cref="SpotCardExchangeEntry"/> (and to survive
|
||||||
|
/// catalog edits that re-classify a card).
|
||||||
|
/// </summary>
|
||||||
|
public class ViewerSpotCardExchange
|
||||||
|
{
|
||||||
|
public long ViewerId { get; set; }
|
||||||
|
public long CardId { get; set; }
|
||||||
|
public bool IsPreRelease { get; set; }
|
||||||
|
public DateTime ExchangedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Repositories.Mission;
|
||||||
|
|
||||||
|
public interface IMissionCatalogRepository
|
||||||
|
{
|
||||||
|
Task<List<MissionCatalogEntry>> GetByLotTypeAsync(int lotType, CancellationToken ct);
|
||||||
|
Task<List<MissionCatalogEntry>> GetByIdsAsync(IReadOnlyCollection<int> ids, CancellationToken ct);
|
||||||
|
Task<MissionCatalogEntry?> GetByIdAsync(int id, CancellationToken ct);
|
||||||
|
|
||||||
|
Task<List<MissionCatalogEntry>> GetByEventTypesAsync(IReadOnlyCollection<string> eventTypes, CancellationToken ct);
|
||||||
|
Task<List<AchievementCatalogEntry>> GetAchievementsByEventTypesAsync(IReadOnlyCollection<string> eventTypes, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>All distinct achievement_type values present in the catalog. Used by /load/index materialization.</summary>
|
||||||
|
Task<List<int>> GetAllAchievementTypesAsync(CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>MIN(Level) per achievement_type — the "starting tier" for new viewers when the
|
||||||
|
/// catalog doesn't contain a level-1 row. With our captured-data-is-catalog model, a fresh
|
||||||
|
/// viewer starts at whatever the lowest captured tier is for that type.</summary>
|
||||||
|
Task<IReadOnlyDictionary<int, int>> GetMinLevelByAchievementTypeAsync(CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>MAX(Level) per achievement_type — cached. Used to compute wire max_level.</summary>
|
||||||
|
Task<IReadOnlyDictionary<int, int>> GetMaxLevelByAchievementTypeAsync(CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>Catalog row at (type, level), or null if no such tier has been captured.</summary>
|
||||||
|
Task<AchievementCatalogEntry?> GetAchievementAsync(int achievementType, int level, CancellationToken ct);
|
||||||
|
|
||||||
|
Task<List<BattlePassMonthlyMissionEntry>> GetMonthlyMissionsAsync(int year, int month, CancellationToken ct);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Repositories.Mission;
|
||||||
|
|
||||||
|
public interface IViewerMissionRepository
|
||||||
|
{
|
||||||
|
Task<List<ViewerMission>> GetMissionsAsync(long viewerId, CancellationToken ct);
|
||||||
|
Task<ViewerMission?> GetMissionByIdAsync(long viewerId, long missionId, CancellationToken ct);
|
||||||
|
|
||||||
|
Task<List<ViewerAchievement>> GetAchievementsAsync(long viewerId, CancellationToken ct);
|
||||||
|
Task<ViewerAchievement?> GetAchievementAsync(long viewerId, int achievementType, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>Reads counter rows for (viewerId, eventKey IN list, period IN list). Empty inputs return [].</summary>
|
||||||
|
Task<List<ViewerEventCounter>> GetCountersAsync(
|
||||||
|
long viewerId,
|
||||||
|
IReadOnlyCollection<string> eventKeys,
|
||||||
|
IReadOnlyCollection<string> periods,
|
||||||
|
CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>Single-row counter read. Returns 0 if no row exists.</summary>
|
||||||
|
Task<int> GetCounterAsync(long viewerId, string eventKey, string period, CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>Add a viewer mission row (in-memory; caller saves).</summary>
|
||||||
|
void AddMission(ViewerMission row);
|
||||||
|
|
||||||
|
/// <summary>Remove a viewer mission row (in-memory; caller saves).</summary>
|
||||||
|
void RemoveMission(ViewerMission row);
|
||||||
|
|
||||||
|
/// <summary>Add a viewer achievement row (in-memory; caller saves).</summary>
|
||||||
|
void AddAchievement(ViewerAchievement row);
|
||||||
|
|
||||||
|
/// <summary>Upsert a counter delta (in-memory; caller saves). Creates the row if missing.</summary>
|
||||||
|
Task UpsertCounterAsync(long viewerId, string eventKey, string period, int delta, CancellationToken ct);
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Repositories.Mission;
|
||||||
|
|
||||||
|
public sealed class MissionCatalogRepository : IMissionCatalogRepository
|
||||||
|
{
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
|
||||||
|
// Process-level cache for the derived MAX(Level) lookup. Cleared on host restart
|
||||||
|
// (re-bootstrap is the only legitimate way to mutate the catalog at runtime).
|
||||||
|
private static IReadOnlyDictionary<int, int>? _maxLevelCache;
|
||||||
|
private static readonly SemaphoreSlim _maxLevelLock = new(1, 1);
|
||||||
|
|
||||||
|
public MissionCatalogRepository(SVSimDbContext db) { _db = db; }
|
||||||
|
|
||||||
|
public Task<List<MissionCatalogEntry>> GetByLotTypeAsync(int lotType, CancellationToken ct) =>
|
||||||
|
_db.MissionCatalog.AsNoTracking().Where(e => e.LotType == lotType).ToListAsync(ct);
|
||||||
|
|
||||||
|
public Task<List<MissionCatalogEntry>> GetByIdsAsync(IReadOnlyCollection<int> ids, CancellationToken ct) =>
|
||||||
|
_db.MissionCatalog.AsNoTracking().Where(e => ids.Contains(e.Id)).ToListAsync(ct);
|
||||||
|
|
||||||
|
public Task<MissionCatalogEntry?> GetByIdAsync(int id, CancellationToken ct) =>
|
||||||
|
_db.MissionCatalog.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id, ct);
|
||||||
|
|
||||||
|
public Task<List<MissionCatalogEntry>> GetByEventTypesAsync(IReadOnlyCollection<string> eventTypes, CancellationToken ct) =>
|
||||||
|
_db.MissionCatalog.AsNoTracking()
|
||||||
|
.Where(e => e.EventType != null && eventTypes.Contains(e.EventType))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
public Task<List<AchievementCatalogEntry>> GetAchievementsByEventTypesAsync(IReadOnlyCollection<string> eventTypes, CancellationToken ct) =>
|
||||||
|
_db.AchievementCatalog.AsNoTracking()
|
||||||
|
.Where(e => e.EventType != null && eventTypes.Contains(e.EventType))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
public Task<List<int>> GetAllAchievementTypesAsync(CancellationToken ct) =>
|
||||||
|
_db.AchievementCatalog.AsNoTracking()
|
||||||
|
.Select(e => e.AchievementType).Distinct()
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyDictionary<int, int>> GetMaxLevelByAchievementTypeAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_maxLevelCache is not null) return _maxLevelCache;
|
||||||
|
await _maxLevelLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_maxLevelCache is null)
|
||||||
|
{
|
||||||
|
var pairs = await _db.AchievementCatalog.AsNoTracking()
|
||||||
|
.GroupBy(e => e.AchievementType)
|
||||||
|
.Select(g => new { Type = g.Key, Max = g.Max(e => e.Level) })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
_maxLevelCache = pairs.ToDictionary(p => p.Type, p => p.Max);
|
||||||
|
}
|
||||||
|
return _maxLevelCache;
|
||||||
|
}
|
||||||
|
finally { _maxLevelLock.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyDictionary<int, int>> GetMinLevelByAchievementTypeAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var pairs = await _db.AchievementCatalog.AsNoTracking()
|
||||||
|
.GroupBy(e => e.AchievementType)
|
||||||
|
.Select(g => new { Type = g.Key, Min = g.Min(e => e.Level) })
|
||||||
|
.ToListAsync(ct);
|
||||||
|
return pairs.ToDictionary(p => p.Type, p => p.Min);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AchievementCatalogEntry?> GetAchievementAsync(int achievementType, int level, CancellationToken ct) =>
|
||||||
|
_db.AchievementCatalog.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(e => e.AchievementType == achievementType && e.Level == level, ct);
|
||||||
|
|
||||||
|
public Task<List<BattlePassMonthlyMissionEntry>> GetMonthlyMissionsAsync(int year, int month, CancellationToken ct) =>
|
||||||
|
_db.BattlePassMonthlyMissions.AsNoTracking()
|
||||||
|
.Where(e => e.Year == year && e.Month == month)
|
||||||
|
.OrderBy(e => e.OrderNum).ToListAsync(ct);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
namespace SVSim.Database.Repositories.Mission;
|
||||||
|
|
||||||
|
public sealed class ViewerMissionRepository : IViewerMissionRepository
|
||||||
|
{
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
|
||||||
|
public ViewerMissionRepository(SVSimDbContext db) { _db = db; }
|
||||||
|
|
||||||
|
public Task<List<ViewerMission>> GetMissionsAsync(long viewerId, CancellationToken ct) =>
|
||||||
|
_db.ViewerMissions.Where(e => e.ViewerId == viewerId).OrderBy(e => e.Slot).ToListAsync(ct);
|
||||||
|
|
||||||
|
public Task<ViewerMission?> GetMissionByIdAsync(long viewerId, long missionId, CancellationToken ct) =>
|
||||||
|
_db.ViewerMissions.FirstOrDefaultAsync(e => e.ViewerId == viewerId && e.Id == missionId, ct);
|
||||||
|
|
||||||
|
public Task<List<ViewerAchievement>> GetAchievementsAsync(long viewerId, CancellationToken ct) =>
|
||||||
|
_db.ViewerAchievements.Where(e => e.ViewerId == viewerId).ToListAsync(ct);
|
||||||
|
|
||||||
|
public Task<ViewerAchievement?> GetAchievementAsync(long viewerId, int achievementType, CancellationToken ct) =>
|
||||||
|
_db.ViewerAchievements.FirstOrDefaultAsync(
|
||||||
|
e => e.ViewerId == viewerId && e.AchievementType == achievementType, ct);
|
||||||
|
|
||||||
|
public Task<List<ViewerEventCounter>> GetCountersAsync(
|
||||||
|
long viewerId,
|
||||||
|
IReadOnlyCollection<string> eventKeys,
|
||||||
|
IReadOnlyCollection<string> periods,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (eventKeys.Count == 0 || periods.Count == 0) return Task.FromResult(new List<ViewerEventCounter>());
|
||||||
|
return _db.ViewerEventCounters.AsNoTracking()
|
||||||
|
.Where(e => e.ViewerId == viewerId
|
||||||
|
&& eventKeys.Contains(e.EventKey)
|
||||||
|
&& periods.Contains(e.Period))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetCounterAsync(long viewerId, string eventKey, string period, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var row = await _db.ViewerEventCounters.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
e => e.ViewerId == viewerId && e.EventKey == eventKey && e.Period == period, ct);
|
||||||
|
return row?.Count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMission(ViewerMission row) => _db.ViewerMissions.Add(row);
|
||||||
|
public void RemoveMission(ViewerMission row) => _db.ViewerMissions.Remove(row);
|
||||||
|
public void AddAchievement(ViewerAchievement row) => _db.ViewerAchievements.Add(row);
|
||||||
|
|
||||||
|
public async Task UpsertCounterAsync(long viewerId, string eventKey, string period, int delta, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var row = await _db.ViewerEventCounters.FirstOrDefaultAsync(
|
||||||
|
e => e.ViewerId == viewerId && e.EventKey == eventKey && e.Period == period, ct);
|
||||||
|
if (row is null)
|
||||||
|
{
|
||||||
|
_db.ViewerEventCounters.Add(new ViewerEventCounter
|
||||||
|
{
|
||||||
|
ViewerId = viewerId, EventKey = eventKey, Period = period, Count = delta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
row.Count += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,10 @@ public interface IViewerRepository
|
|||||||
Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId);
|
Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId);
|
||||||
Task<Models.Viewer?> GetViewerWithSocials(long id);
|
Task<Models.Viewer?> GetViewerWithSocials(long id);
|
||||||
Task<Models.Viewer?> GetViewerByShortUdid(long shortUdid);
|
Task<Models.Viewer?> GetViewerByShortUdid(long shortUdid);
|
||||||
|
Task<Models.Viewer?> GetViewerByUdid(Guid udid);
|
||||||
|
|
||||||
Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
||||||
ulong socialAccountIdentifier, ulong? shortUdid = null);
|
ulong socialAccountIdentifier, ulong? shortUdid = null);
|
||||||
}
|
Task<Models.Viewer> RegisterAnonymousViewer(Guid udid);
|
||||||
|
Task LinkSteamToViewer(long viewerId, ulong steamId);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Npgsql;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using SVSim.Database.Models.Config;
|
using SVSim.Database.Models.Config;
|
||||||
@@ -70,6 +71,100 @@ public class ViewerRepository : IViewerRepository
|
|||||||
|
|
||||||
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
||||||
ulong socialAccountIdentifier, ulong? shortUdid = null)
|
ulong socialAccountIdentifier, ulong? shortUdid = null)
|
||||||
|
{
|
||||||
|
var viewer = await BuildDefaultViewer(displayName);
|
||||||
|
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
||||||
|
{
|
||||||
|
AccountId = socialAccountIdentifier,
|
||||||
|
AccountType = socialType
|
||||||
|
});
|
||||||
|
_dbContext.Set<Models.Viewer>().Add(viewer);
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
return viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Models.Viewer?> GetViewerByUdid(Guid udid)
|
||||||
|
{
|
||||||
|
if (udid == Guid.Empty) return null;
|
||||||
|
return await _dbContext.Set<Models.Viewer>()
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(v => v.Udid == udid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Models.Viewer> RegisterAnonymousViewer(Guid udid)
|
||||||
|
{
|
||||||
|
if (udid == Guid.Empty)
|
||||||
|
throw new InvalidOperationException("Cannot register viewer for empty UDID.");
|
||||||
|
|
||||||
|
var viewer = await BuildDefaultViewer("Player");
|
||||||
|
viewer.Udid = udid;
|
||||||
|
_dbContext.Set<Models.Viewer>().Add(viewer);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (IsUniqueViolation(ex))
|
||||||
|
{
|
||||||
|
// Concurrent signup for the same UDID raced us to the unique index. The other request
|
||||||
|
// already committed a viewer with this UDID — re-read and return it. Detach the local
|
||||||
|
// entity first so EF doesn't keep trying to insert the now-orphaned graph.
|
||||||
|
//
|
||||||
|
// Cross-engine: Postgres surfaces this as Npgsql.PostgresException SqlState "23505";
|
||||||
|
// SQLite (test backend) surfaces it as Microsoft.Data.Sqlite.SqliteException with
|
||||||
|
// SqliteErrorCode 19 (SQLITE_CONSTRAINT). Matched by type-name to avoid pulling a
|
||||||
|
// Sqlite package dep into SVSim.Database.
|
||||||
|
_dbContext.Entry(viewer).State = EntityState.Detached;
|
||||||
|
var existing = await GetViewerByUdid(udid)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
$"Got unique-violation on Udid={udid} insert but subsequent lookup found no row. " +
|
||||||
|
"This shouldn't happen — likely transaction isolation issue.");
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
return viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the given <see cref="DbUpdateException"/> wraps a backend-level unique-
|
||||||
|
/// constraint violation. Postgres → SqlState "23505"; SQLite → SqliteErrorCode 19.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsUniqueViolation(DbUpdateException ex)
|
||||||
|
{
|
||||||
|
if (ex.InnerException is Npgsql.PostgresException pgEx && pgEx.SqlState == "23505")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Match SQLite by type name so this assembly doesn't take a dep on Microsoft.Data.Sqlite.
|
||||||
|
// Test backend (SQLite in-memory) raises SqliteException with SqliteErrorCode 19 on UNIQUE
|
||||||
|
// constraint violations.
|
||||||
|
var inner = ex.InnerException;
|
||||||
|
if (inner is not null && inner.GetType().FullName == "Microsoft.Data.Sqlite.SqliteException")
|
||||||
|
{
|
||||||
|
var prop = inner.GetType().GetProperty("SqliteErrorCode");
|
||||||
|
if (prop?.GetValue(inner) is int code && code == 19) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LinkSteamToViewer(long viewerId, ulong steamId)
|
||||||
|
{
|
||||||
|
var viewer = await _dbContext.Set<Models.Viewer>()
|
||||||
|
.Include(v => v.SocialAccountConnections)
|
||||||
|
.FirstOrDefaultAsync(v => v.Id == viewerId)
|
||||||
|
?? throw new InvalidOperationException($"Viewer {viewerId} not found for Steam link.");
|
||||||
|
|
||||||
|
bool alreadyLinked = viewer.SocialAccountConnections.Any(sac =>
|
||||||
|
sac.AccountType == SocialAccountType.Steam && sac.AccountId == steamId);
|
||||||
|
if (alreadyLinked) return;
|
||||||
|
|
||||||
|
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
||||||
|
{
|
||||||
|
AccountId = steamId,
|
||||||
|
AccountType = SocialAccountType.Steam
|
||||||
|
});
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Models.Viewer> BuildDefaultViewer(string displayName)
|
||||||
{
|
{
|
||||||
Models.Viewer viewer = new Models.Viewer
|
Models.Viewer viewer = new Models.Viewer
|
||||||
{
|
{
|
||||||
@@ -79,12 +174,6 @@ public class ViewerRepository : IViewerRepository
|
|||||||
var grants = _config.Get<DefaultGrantsConfig>();
|
var grants = _config.Get<DefaultGrantsConfig>();
|
||||||
var loadout = _config.Get<DefaultLoadoutConfig>();
|
var loadout = _config.Get<DefaultLoadoutConfig>();
|
||||||
|
|
||||||
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
|
||||||
{
|
|
||||||
AccountId = socialAccountIdentifier,
|
|
||||||
AccountType = socialType
|
|
||||||
});
|
|
||||||
|
|
||||||
viewer.Info.MaxFriends = player.MaxFriends;
|
viewer.Info.MaxFriends = player.MaxFriends;
|
||||||
viewer.Info.CountryCode = "KOR";
|
viewer.Info.CountryCode = "KOR";
|
||||||
viewer.Info.BirthDate = DateTime.UtcNow;
|
viewer.Info.BirthDate = DateTime.UtcNow;
|
||||||
@@ -133,8 +222,6 @@ public class ViewerRepository : IViewerRepository
|
|||||||
.ToList();
|
.ToList();
|
||||||
viewer.LeaderSkins.AddRange(grantedSkins);
|
viewer.LeaderSkins.AddRange(grantedSkins);
|
||||||
|
|
||||||
_dbContext.Set<Models.Viewer>().Add(viewer);
|
|
||||||
await _dbContext.SaveChangesAsync();
|
|
||||||
return viewer;
|
return viewer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ public class SVSimDbContext : DbContext
|
|||||||
public DbSet<BattlePassRewardEntry> BattlePassRewards => Set<BattlePassRewardEntry>();
|
public DbSet<BattlePassRewardEntry> BattlePassRewards => Set<BattlePassRewardEntry>();
|
||||||
public DbSet<ViewerBattlePassProgressEntry> ViewerBattlePassProgress => Set<ViewerBattlePassProgressEntry>();
|
public DbSet<ViewerBattlePassProgressEntry> ViewerBattlePassProgress => Set<ViewerBattlePassProgressEntry>();
|
||||||
public DbSet<ViewerBattlePassClaimEntry> ViewerBattlePassClaims => Set<ViewerBattlePassClaimEntry>();
|
public DbSet<ViewerBattlePassClaimEntry> ViewerBattlePassClaims => Set<ViewerBattlePassClaimEntry>();
|
||||||
|
public DbSet<MissionCatalogEntry> MissionCatalog => Set<MissionCatalogEntry>();
|
||||||
|
public DbSet<AchievementCatalogEntry> AchievementCatalog => Set<AchievementCatalogEntry>();
|
||||||
|
public DbSet<BattlePassMonthlyMissionEntry> BattlePassMonthlyMissions => Set<BattlePassMonthlyMissionEntry>();
|
||||||
|
public DbSet<ViewerMission> ViewerMissions => Set<ViewerMission>();
|
||||||
|
public DbSet<ViewerAchievement> ViewerAchievements => Set<ViewerAchievement>();
|
||||||
|
public DbSet<ViewerEventCounter> ViewerEventCounters => Set<ViewerEventCounter>();
|
||||||
public DbSet<DailyLoginBonusEntry> DailyLoginBonuses => Set<DailyLoginBonusEntry>();
|
public DbSet<DailyLoginBonusEntry> DailyLoginBonuses => Set<DailyLoginBonusEntry>();
|
||||||
public DbSet<BannerEntry> Banners => Set<BannerEntry>();
|
public DbSet<BannerEntry> Banners => Set<BannerEntry>();
|
||||||
public DbSet<ColosseumConfig> Colosseums => Set<ColosseumConfig>();
|
public DbSet<ColosseumConfig> Colosseums => Set<ColosseumConfig>();
|
||||||
@@ -64,6 +70,14 @@ public class SVSimDbContext : DbContext
|
|||||||
public DbSet<PackConfigEntry> Packs => Set<PackConfigEntry>();
|
public DbSet<PackConfigEntry> Packs => Set<PackConfigEntry>();
|
||||||
public DbSet<BuildDeckSeriesEntry> BuildDeckSeries => Set<BuildDeckSeriesEntry>();
|
public DbSet<BuildDeckSeriesEntry> BuildDeckSeries => Set<BuildDeckSeriesEntry>();
|
||||||
public DbSet<BuildDeckProductEntry> BuildDeckProducts => Set<BuildDeckProductEntry>();
|
public DbSet<BuildDeckProductEntry> BuildDeckProducts => Set<BuildDeckProductEntry>();
|
||||||
|
public DbSet<SleeveShopSeriesEntry> SleeveShopSeries => Set<SleeveShopSeriesEntry>();
|
||||||
|
public DbSet<SleeveShopProductEntry> SleeveShopProducts => Set<SleeveShopProductEntry>();
|
||||||
|
public DbSet<ItemPurchaseCatalogEntry> ItemPurchaseCatalog => Set<ItemPurchaseCatalogEntry>();
|
||||||
|
public DbSet<LeaderSkinShopSeriesEntry> LeaderSkinShopSeries => Set<LeaderSkinShopSeriesEntry>();
|
||||||
|
public DbSet<LeaderSkinShopProductEntry> LeaderSkinShopProducts => Set<LeaderSkinShopProductEntry>();
|
||||||
|
public DbSet<ViewerLeaderSkinSetClaim> ViewerLeaderSkinSetClaims => Set<ViewerLeaderSkinSetClaim>();
|
||||||
|
public DbSet<SpotCardExchangeEntry> SpotCardExchangeCatalog => Set<SpotCardExchangeEntry>();
|
||||||
|
public DbSet<ViewerSpotCardExchange> ViewerSpotCardExchanges => Set<ViewerSpotCardExchange>();
|
||||||
public DbSet<MaintenanceCardEntry> MaintenanceCards => Set<MaintenanceCardEntry>();
|
public DbSet<MaintenanceCardEntry> MaintenanceCards => Set<MaintenanceCardEntry>();
|
||||||
public DbSet<FeatureMaintenanceEntry> FeatureMaintenances => Set<FeatureMaintenanceEntry>();
|
public DbSet<FeatureMaintenanceEntry> FeatureMaintenances => Set<FeatureMaintenanceEntry>();
|
||||||
public DbSet<PreReleaseInfo> PreReleaseInfos => Set<PreReleaseInfo>();
|
public DbSet<PreReleaseInfo> PreReleaseInfos => Set<PreReleaseInfo>();
|
||||||
@@ -151,6 +165,17 @@ public class SVSimDbContext : DbContext
|
|||||||
b.HasIndex("ViewerId", "ProductId").IsUnique();
|
b.HasIndex("ViewerId", "ProductId").IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A given social account links to exactly one viewer — two viewers cannot share the same
|
||||||
|
// Steam (or Facebook, etc.) account. This is the dedup backstop the auth handler's find-
|
||||||
|
// or-link path (SteamSessionAuthenticationHandler) relies on: two concurrent first-Steam-
|
||||||
|
// touch requests can both pass the .Any(...) check in LinkSteamToViewer, but the second
|
||||||
|
// SaveChanges() throws unique-violation and surfaces a clean 500 instead of silently
|
||||||
|
// appending duplicate connections.
|
||||||
|
modelBuilder.Entity<Viewer>().OwnsMany(v => v.SocialAccountConnections, b =>
|
||||||
|
{
|
||||||
|
b.HasIndex("AccountType", "AccountId").IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<BuildDeckSeriesEntry>().OwnsMany(s => s.SeriesRewards);
|
modelBuilder.Entity<BuildDeckSeriesEntry>().OwnsMany(s => s.SeriesRewards);
|
||||||
modelBuilder.Entity<BuildDeckProductEntry>().OwnsMany(p => p.Cards);
|
modelBuilder.Entity<BuildDeckProductEntry>().OwnsMany(p => p.Cards);
|
||||||
modelBuilder.Entity<BuildDeckProductEntry>().OwnsMany(p => p.Rewards);
|
modelBuilder.Entity<BuildDeckProductEntry>().OwnsMany(p => p.Rewards);
|
||||||
@@ -163,6 +188,35 @@ public class SVSimDbContext : DbContext
|
|||||||
|
|
||||||
modelBuilder.Entity<BuildDeckProductEntry>().HasIndex(p => p.SeriesId);
|
modelBuilder.Entity<BuildDeckProductEntry>().HasIndex(p => p.SeriesId);
|
||||||
|
|
||||||
|
modelBuilder.Entity<SleeveShopProductEntry>().OwnsMany(p => p.Rewards);
|
||||||
|
modelBuilder.Entity<SleeveShopProductEntry>()
|
||||||
|
.HasOne(p => p.Series)
|
||||||
|
.WithMany(s => s.Products)
|
||||||
|
.HasForeignKey(p => p.SeriesId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<SleeveShopProductEntry>().HasIndex(p => p.SeriesId);
|
||||||
|
|
||||||
|
modelBuilder.Entity<LeaderSkinShopSeriesEntry>().OwnsMany(s => s.SetCompletionRewards);
|
||||||
|
modelBuilder.Entity<LeaderSkinShopProductEntry>().OwnsMany(p => p.Rewards);
|
||||||
|
modelBuilder.Entity<LeaderSkinShopProductEntry>()
|
||||||
|
.HasOne(p => p.Series)
|
||||||
|
.WithMany(s => s.Products)
|
||||||
|
.HasForeignKey(p => p.SeriesId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
modelBuilder.Entity<LeaderSkinShopProductEntry>().HasIndex(p => p.SeriesId);
|
||||||
|
|
||||||
|
modelBuilder.Entity<ViewerLeaderSkinSetClaim>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(c => new { c.ViewerId, c.SeriesId });
|
||||||
|
b.HasIndex(c => c.ViewerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ViewerSpotCardExchange>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(e => new { e.ViewerId, e.CardId });
|
||||||
|
b.HasIndex(e => e.ViewerId);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<CardCosmeticReward>(b =>
|
modelBuilder.Entity<CardCosmeticReward>(b =>
|
||||||
{
|
{
|
||||||
b.HasKey(r => new { r.CardId, r.Type, r.CosmeticId });
|
b.HasKey(r => new { r.CardId, r.Type, r.CosmeticId });
|
||||||
@@ -242,6 +296,46 @@ public class SVSimDbContext : DbContext
|
|||||||
b.HasIndex(e => new { e.ViewerId, e.SeasonId });
|
b.HasIndex(e => new { e.ViewerId, e.SeasonId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<MissionCatalogEntry>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(e => e.Id);
|
||||||
|
b.Property(e => e.Id).ValueGeneratedNever();
|
||||||
|
b.HasIndex(e => e.LotType);
|
||||||
|
b.HasIndex(e => new { e.EventType, e.EventArg });
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AchievementCatalogEntry>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(e => new { e.AchievementType, e.Level });
|
||||||
|
b.HasIndex(e => e.AchievementType);
|
||||||
|
b.HasIndex(e => new { e.EventType, e.EventArg });
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<BattlePassMonthlyMissionEntry>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(e => e.Id);
|
||||||
|
b.HasIndex(e => new { e.Year, e.Month, e.OrderNum }).IsUnique();
|
||||||
|
b.HasIndex(e => new { e.Year, e.Month });
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ViewerMission>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(e => e.Id);
|
||||||
|
b.HasIndex(e => new { e.ViewerId, e.Slot }).IsUnique();
|
||||||
|
b.HasIndex(e => e.ViewerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ViewerAchievement>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(e => new { e.ViewerId, e.AchievementType });
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ViewerEventCounter>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(e => new { e.ViewerId, e.EventKey, e.Period });
|
||||||
|
b.HasIndex(e => new { e.ViewerId, e.Period });
|
||||||
|
});
|
||||||
|
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ public sealed record GrantedReward(int RewardType, long RewardId, int RewardNum)
|
|||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>DO NOT reimplement reward dispatch in a controller or new helper.</b> This service handles
|
/// <b>DO NOT reimplement reward dispatch in a controller or new helper.</b> This service handles
|
||||||
/// RedEther, Crystal, Item, Card (with <see cref="CardCosmeticReward"/> cascade), Sleeve, Emblem,
|
/// RedEther, Crystal, SpotCardPoint, Item, Card (with <see cref="CardCosmeticReward"/> cascade),
|
||||||
/// Degree, Rupy, Skin, MyPageBG — everything except SpotCard (TODO). Endpoint code that takes a
|
/// Sleeve, Emblem, Degree, Rupy, Skin, MyPageBG — everything except the dead-letter SpotCard /
|
||||||
|
/// SpotCardOnlyLatestCardPack slots (use Card=5 instead). Endpoint code that takes a
|
||||||
/// list of <c>(type, id, num)</c> tuples should iterate and call <see cref="ApplyAsync"/>
|
/// list of <c>(type, id, num)</c> tuples should iterate and call <see cref="ApplyAsync"/>
|
||||||
/// per tuple — never switch on type yourself, never filter to "only card-typed rewards", never
|
/// per tuple — never switch on type yourself, never filter to "only card-typed rewards", never
|
||||||
/// build a second dispatch table. Past duplicate implementations (ICardAcquisitionService in the
|
/// build a second dispatch table. Past duplicate implementations (ICardAcquisitionService in the
|
||||||
@@ -87,6 +88,10 @@ public sealed class RewardGrantService
|
|||||||
viewer.Currency.RedEther += (ulong)num;
|
viewer.Currency.RedEther += (ulong)num;
|
||||||
return Single(type, detailId, checked((int)viewer.Currency.RedEther));
|
return Single(type, detailId, checked((int)viewer.Currency.RedEther));
|
||||||
|
|
||||||
|
case UserGoodsType.SpotCardPoint:
|
||||||
|
viewer.Currency.SpotPoints += (ulong)num;
|
||||||
|
return Single(type, detailId, checked((int)viewer.Currency.SpotPoints));
|
||||||
|
|
||||||
case UserGoodsType.Item:
|
case UserGoodsType.Item:
|
||||||
{
|
{
|
||||||
var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId);
|
var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId);
|
||||||
@@ -106,11 +111,11 @@ public sealed class RewardGrantService
|
|||||||
|
|
||||||
case UserGoodsType.SpotCard:
|
case UserGoodsType.SpotCard:
|
||||||
case UserGoodsType.SpotCardOnlyLatestCardPack:
|
case UserGoodsType.SpotCardOnlyLatestCardPack:
|
||||||
// TODO: spot cards are currently global in our seed data; the existence of these
|
// Spot-card-typed grants don't appear in captures — emitters always use Card=5
|
||||||
// reward types suggests there's a mix of global + per-player spot cards. Revisit
|
// with the spot-card-specific id. These two enum slots remain unimplemented; if a
|
||||||
// when per-player spot-card infrastructure lands.
|
// capture ever shows one in a reward_list we'll know to wire them up here.
|
||||||
throw new NotSupportedException(
|
throw new NotSupportedException(
|
||||||
$"{type} rewards are not yet supported — see SpotCard TODO in RewardGrantService.");
|
$"{type} rewards are not yet supported — emitters use Card=5 instead.");
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new NotSupportedException($"UserGoodsType {type} not yet handled by RewardGrantService");
|
throw new NotSupportedException($"UserGoodsType {type} not yet handled by RewardGrantService");
|
||||||
|
|||||||
133
SVSim.EmulatedEntrypoint/Controllers/AchievementController.cs
Normal file
133
SVSim.EmulatedEntrypoint/Controllers/AchievementController.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Repositories.Mission;
|
||||||
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Achievement;
|
||||||
|
using SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /achievement/* — claim achievement rewards. Wire shape mirrors AchievementReceiveRewardTask.cs.
|
||||||
|
/// </summary>
|
||||||
|
[Route("achievement")]
|
||||||
|
public class AchievementController : SVSimController
|
||||||
|
{
|
||||||
|
private const int FailureResultCode = 2;
|
||||||
|
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
private readonly IMissionCatalogRepository _catalog;
|
||||||
|
private readonly IViewerMissionStateService _state;
|
||||||
|
private readonly IMissionAssembler _assembler;
|
||||||
|
private readonly RewardGrantService _grantService;
|
||||||
|
|
||||||
|
public AchievementController(
|
||||||
|
SVSimDbContext db,
|
||||||
|
IMissionCatalogRepository catalog,
|
||||||
|
IViewerMissionStateService state,
|
||||||
|
IMissionAssembler assembler,
|
||||||
|
RewardGrantService grantService)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_catalog = catalog;
|
||||||
|
_state = state;
|
||||||
|
_assembler = assembler;
|
||||||
|
_grantService = grantService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("receive_reward")]
|
||||||
|
public async Task<IActionResult> ReceiveReward(
|
||||||
|
AchievementReceiveRewardRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
// Load viewer with all the collections RewardGrantService may need to mutate.
|
||||||
|
var viewer = await _db.Viewers
|
||||||
|
.Include(v => v.MissionData)
|
||||||
|
.Include(v => v.Currency)
|
||||||
|
.Include(v => v.Cards)
|
||||||
|
.Include(v => v.Items)
|
||||||
|
.Include(v => v.Sleeves)
|
||||||
|
.Include(v => v.Emblems)
|
||||||
|
.Include(v => v.Degrees)
|
||||||
|
.Include(v => v.LeaderSkins)
|
||||||
|
.Include(v => v.MyPageBackgrounds)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.FirstAsync(v => v.Id == viewerId, ct);
|
||||||
|
|
||||||
|
await _state.EnsureCurrentAsync(viewer.Id, ct);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
// Re-read viewer's achievement for this type after state-service materialization.
|
||||||
|
var ach = await _db.ViewerAchievements
|
||||||
|
.FirstOrDefaultAsync(a => a.ViewerId == viewerId && a.AchievementType == request.AchievementType, ct);
|
||||||
|
if (ach is null || ach.Level != request.Level)
|
||||||
|
{
|
||||||
|
return Ok(new { result_code = FailureResultCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
var catalogRow = await _catalog.GetAchievementAsync(request.AchievementType, request.Level, ct);
|
||||||
|
if (catalogRow is null)
|
||||||
|
{
|
||||||
|
return Ok(new { result_code = FailureResultCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant via the canonical RewardGrantService primitive.
|
||||||
|
var granted = await _grantService.ApplyAsync(
|
||||||
|
viewer,
|
||||||
|
(UserGoodsType)catalogRow.RewardType,
|
||||||
|
catalogRow.RewardDetailId,
|
||||||
|
catalogRow.RewardNumber,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
// Advance viewer's level by 1. If no catalog row exists at the new level (i.e. just
|
||||||
|
// claimed the highest captured tier), max_level on the wire stays the same and the
|
||||||
|
// UI shows "claimed at max" until catalog grows.
|
||||||
|
ach.Level += 1;
|
||||||
|
var maxLevelByType = await _catalog.GetMaxLevelByAchievementTypeAsync(ct);
|
||||||
|
if (maxLevelByType.TryGetValue(request.AchievementType, out int maxLevel)
|
||||||
|
&& ach.Level > maxLevel)
|
||||||
|
{
|
||||||
|
ach.AchievementStatus = 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ach.AchievementStatus = 0;
|
||||||
|
}
|
||||||
|
ach.NowAchievedLevel = request.Level;
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
var dto = await _assembler.BuildAsync(viewer, ct);
|
||||||
|
var resp = new AchievementReceiveRewardResponse
|
||||||
|
{
|
||||||
|
UserMissionList = dto.UserMissionList,
|
||||||
|
UserAchievementList = dto.UserAchievementList,
|
||||||
|
BattlePassMonthlyMission = dto.BattlePassMonthlyMission,
|
||||||
|
IsChangeMission = dto.IsChangeMission,
|
||||||
|
CanChangeMissionTime = dto.CanChangeMissionTime,
|
||||||
|
IsChangeReceiveType = dto.IsChangeReceiveType,
|
||||||
|
CanChangeReceiveTypeTime = dto.CanChangeReceiveTypeTime,
|
||||||
|
MissionReceiveType = dto.MissionReceiveType,
|
||||||
|
RewardList = granted.Select(g => new RewardGrantDto
|
||||||
|
{
|
||||||
|
RewardType = g.RewardType,
|
||||||
|
RewardId = g.RewardId,
|
||||||
|
RewardNum = g.RewardNum,
|
||||||
|
}).ToList(),
|
||||||
|
TotalReceiveCountList = granted.Select(g => new TotalReceiveCountDto
|
||||||
|
{
|
||||||
|
RewardType = g.RewardType,
|
||||||
|
RewardDetailId = g.RewardId,
|
||||||
|
RewardCount = g.RewardNum,
|
||||||
|
ItemType = 0,
|
||||||
|
IsUsable = true,
|
||||||
|
}).ToList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,10 +30,6 @@ public class CheckController : SVSimController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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")]
|
[HttpPost("game_start")]
|
||||||
public async Task<GameStartResponse> GameStart(GameStartRequest request)
|
public async Task<GameStartResponse> GameStart(GameStartRequest request)
|
||||||
{
|
{
|
||||||
|
|||||||
216
SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs
Normal file
216
SVSim.EmulatedEntrypoint/Controllers/ItemPurchaseController.cs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.ItemPurchase;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.ItemPurchase;
|
||||||
|
using SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /item_purchase/* — the generic item shop where viewers spend item-or-currency to acquire
|
||||||
|
/// other items (e.g. RedEther → Seer's Globe, Orb Shards → Seer's Globe). Per-viewer monthly
|
||||||
|
/// or lifetime quota tracked via <see cref="ViewerEventCounter"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Route("item_purchase")]
|
||||||
|
public class ItemPurchaseController : SVSimController
|
||||||
|
{
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
private readonly RewardGrantService _rewards;
|
||||||
|
private readonly TimeProvider _time;
|
||||||
|
|
||||||
|
public ItemPurchaseController(SVSimDbContext db, RewardGrantService rewards, TimeProvider time)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_rewards = rewards;
|
||||||
|
_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("info")]
|
||||||
|
public async Task<ActionResult<ItemPurchaseInfoResponse>> Info(BaseRequest _)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
var catalog = await _db.ItemPurchaseCatalog
|
||||||
|
.Where(c => c.IsEnabled)
|
||||||
|
.OrderBy(c => c.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var now = _time.GetUtcNow();
|
||||||
|
var monthKey = JstPeriod.MonthKey(now);
|
||||||
|
var keys = catalog.Select(c => CounterKey(c.Id)).ToList();
|
||||||
|
var counters = await _db.ViewerEventCounters
|
||||||
|
.Where(c => c.ViewerId == viewerId && keys.Contains(c.EventKey))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var info = new List<ItemPurchaseEntryDto>(catalog.Count);
|
||||||
|
foreach (var c in catalog)
|
||||||
|
{
|
||||||
|
int count = CounterCount(counters, c, monthKey);
|
||||||
|
info.Add(new ItemPurchaseEntryDto
|
||||||
|
{
|
||||||
|
PurchaseId = c.Id,
|
||||||
|
RequireItemType = c.RequireItemType,
|
||||||
|
RequireItemId = c.RequireItemId,
|
||||||
|
RequireItemNum = c.RequireItemNum,
|
||||||
|
PurchaseItemType = c.PurchaseItemType,
|
||||||
|
PurchaseItemId = c.PurchaseItemId,
|
||||||
|
PurchaseItemNum = c.PurchaseItemNum,
|
||||||
|
PurchaseName = c.PurchaseName,
|
||||||
|
IsMonthlyReset = c.IsMonthlyReset ? 1 : 0,
|
||||||
|
Rest = Math.Max(0, c.PurchaseLimit - count),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// user_card_pack_ticket_list: every item with Type == 2 paired with the viewer's count
|
||||||
|
// (zero counts included — the client unconditionally calls UpdateItemNum per entry).
|
||||||
|
var ticketItems = await _db.Items
|
||||||
|
.Where(i => i.Type == 2)
|
||||||
|
.OrderByDescending(i => i.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
var ownedByItemId = (await _db.Viewers
|
||||||
|
.Where(v => v.Id == viewerId)
|
||||||
|
.SelectMany(v => v.Items)
|
||||||
|
.Select(oi => new { oi.Item.Id, oi.Count })
|
||||||
|
.ToListAsync())
|
||||||
|
.ToDictionary(x => x.Id, x => x.Count);
|
||||||
|
|
||||||
|
var ticketList = ticketItems.Select(i => new UserCardPackTicketDto
|
||||||
|
{
|
||||||
|
ItemId = i.Id,
|
||||||
|
Number = ownedByItemId.TryGetValue(i.Id, out var cnt) ? cnt : 0,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new ItemPurchaseInfoResponse
|
||||||
|
{
|
||||||
|
ItemPurchaseInfo = info,
|
||||||
|
UserCardPackTicketList = ticketList,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("purchase")]
|
||||||
|
public async Task<ActionResult<ItemPurchasePurchaseResponse>> Purchase(ItemPurchasePurchaseRequest request)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
var entry = await _db.ItemPurchaseCatalog.FindAsync(request.PurchaseId);
|
||||||
|
if (entry is null || !entry.IsEnabled)
|
||||||
|
return BadRequest(new { error = "unknown_purchase" });
|
||||||
|
|
||||||
|
var now = _time.GetUtcNow();
|
||||||
|
var period = entry.IsMonthlyReset ? JstPeriod.MonthKey(now) : JstPeriod.AllTime;
|
||||||
|
var key = CounterKey(entry.Id);
|
||||||
|
|
||||||
|
var counter = await _db.ViewerEventCounters
|
||||||
|
.FirstOrDefaultAsync(c => c.ViewerId == viewerId && c.EventKey == key && c.Period == period);
|
||||||
|
int currentCount = counter?.Count ?? 0;
|
||||||
|
int rest = entry.PurchaseLimit - currentCount;
|
||||||
|
if (rest <= 0)
|
||||||
|
return BadRequest(new { error = "sold_out" });
|
||||||
|
|
||||||
|
var viewer = await LoadViewerGraphAsync(viewerId);
|
||||||
|
var rewardList = new List<RewardListEntry>();
|
||||||
|
|
||||||
|
// Debit the require side. RewardGrantService is grant-only, so handle this inline.
|
||||||
|
var debit = TryDebit(viewer, (UserGoodsType)entry.RequireItemType, entry.RequireItemId, entry.RequireItemNum);
|
||||||
|
if (debit.Error is not null) return BadRequest(new { error = debit.Error });
|
||||||
|
if (debit.PostState is not null) rewardList.Add(debit.PostState);
|
||||||
|
|
||||||
|
// Grant the purchase side through the central dispatcher.
|
||||||
|
var granted = await _rewards.ApplyAsync(viewer,
|
||||||
|
(UserGoodsType)entry.PurchaseItemType, entry.PurchaseItemId, entry.PurchaseItemNum);
|
||||||
|
foreach (var g in granted)
|
||||||
|
{
|
||||||
|
rewardList.Add(new RewardListEntry
|
||||||
|
{
|
||||||
|
RewardType = g.RewardType,
|
||||||
|
RewardId = g.RewardId,
|
||||||
|
RewardNum = g.RewardNum,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the per-period counter.
|
||||||
|
if (counter is null)
|
||||||
|
{
|
||||||
|
_db.ViewerEventCounters.Add(new ViewerEventCounter
|
||||||
|
{
|
||||||
|
ViewerId = viewerId,
|
||||||
|
EventKey = key,
|
||||||
|
Period = period,
|
||||||
|
Count = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
counter.Count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new ItemPurchasePurchaseResponse { RewardList = rewardList };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debit <paramref name="num"/> of (<paramref name="type"/>, <paramref name="detailId"/>)
|
||||||
|
/// from the viewer, returning a post-state-aware <see cref="RewardListEntry"/> the client
|
||||||
|
/// uses to refresh its cached count. Returns an error string on insufficient balance.
|
||||||
|
/// </summary>
|
||||||
|
private static (RewardListEntry? PostState, string? Error) TryDebit(
|
||||||
|
Viewer viewer, UserGoodsType type, long detailId, int num)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case UserGoodsType.RedEther:
|
||||||
|
if (viewer.Currency.RedEther < (ulong)num)
|
||||||
|
return (null, "insufficient_red_ether");
|
||||||
|
viewer.Currency.RedEther -= (ulong)num;
|
||||||
|
return (new RewardListEntry { RewardType = 1, RewardId = 0, RewardNum = (int)viewer.Currency.RedEther }, null);
|
||||||
|
|
||||||
|
case UserGoodsType.Crystal:
|
||||||
|
if (viewer.Currency.Crystals < (ulong)num)
|
||||||
|
return (null, "insufficient_crystals");
|
||||||
|
viewer.Currency.Crystals -= (ulong)num;
|
||||||
|
return (new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)viewer.Currency.Crystals }, null);
|
||||||
|
|
||||||
|
case UserGoodsType.Rupy:
|
||||||
|
if (viewer.Currency.Rupees < (ulong)num)
|
||||||
|
return (null, "insufficient_rupees");
|
||||||
|
viewer.Currency.Rupees -= (ulong)num;
|
||||||
|
return (new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)viewer.Currency.Rupees }, null);
|
||||||
|
|
||||||
|
case UserGoodsType.Item:
|
||||||
|
var owned = viewer.Items.FirstOrDefault(i => i.Item.Id == (int)detailId);
|
||||||
|
if (owned is null || owned.Count < num)
|
||||||
|
return (null, "insufficient_item");
|
||||||
|
owned.Count -= num;
|
||||||
|
return (new RewardListEntry { RewardType = 4, RewardId = detailId, RewardNum = owned.Count }, null);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (null, $"debit_type_not_supported:{type}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CounterKey(int purchaseId) => $"item_purchase:{purchaseId}";
|
||||||
|
|
||||||
|
private static int CounterCount(List<ViewerEventCounter> counters, ItemPurchaseCatalogEntry entry, string monthKey)
|
||||||
|
{
|
||||||
|
var period = entry.IsMonthlyReset ? monthKey : JstPeriod.AllTime;
|
||||||
|
return counters.FirstOrDefault(c => c.EventKey == CounterKey(entry.Id) && c.Period == period)?.Count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Viewer> LoadViewerGraphAsync(long viewerId) => _db.Viewers
|
||||||
|
.Include(v => v.Items).ThenInclude(i => i.Item)
|
||||||
|
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||||
|
.Include(v => v.Sleeves)
|
||||||
|
.Include(v => v.Emblems)
|
||||||
|
.Include(v => v.LeaderSkins)
|
||||||
|
.Include(v => v.Degrees)
|
||||||
|
.Include(v => v.MyPageBackgrounds)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.FirstAsync(v => v.Id == viewerId);
|
||||||
|
}
|
||||||
@@ -1,24 +1,41 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SVSim.Database;
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.LeaderSkin;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.LeaderSkin;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.LeaderSkin;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.LeaderSkin;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// /leader_skin/* — per-class "active leader skin" preference. The per-CLASS setting is the
|
/// /leader_skin/* — the leader-skin shop family.
|
||||||
/// fallback used when a deck has <c>leader_skin_id == 0</c>; per-deck overrides go through
|
/// <list type="bullet">
|
||||||
/// /deck/update_leader_skin instead.
|
/// <item><c>/set</c>: per-class equipped-skin preference (the fallback when a deck has
|
||||||
|
/// <c>leader_skin_id == 0</c>). Per-deck overrides go through /deck/update_leader_skin.</item>
|
||||||
|
/// <item><c>/products</c>: shop catalog (dict-keyed by series_id).</item>
|
||||||
|
/// <item><c>/buy</c>: single-skin purchase. Currency dispatch crystal/rupy/ticket(501).</item>
|
||||||
|
/// <item><c>/buy_set</c>: whole-series purchase at set discount.</item>
|
||||||
|
/// <item><c>/buy_set_item</c>: claim series-completion bonus (idempotent via
|
||||||
|
/// <see cref="ViewerLeaderSkinSetClaim"/>).</item>
|
||||||
|
/// <item><c>/ids</c>: flat list of owned skin ids for badge refresh.</item>
|
||||||
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("leader_skin")]
|
[Route("leader_skin")]
|
||||||
public class LeaderSkinController : SVSimController
|
public class LeaderSkinController : SVSimController
|
||||||
{
|
{
|
||||||
private readonly SVSimDbContext _db;
|
private readonly SVSimDbContext _db;
|
||||||
|
private readonly RewardGrantService _rewards;
|
||||||
|
private readonly TimeProvider _time;
|
||||||
|
|
||||||
public LeaderSkinController(SVSimDbContext db)
|
public LeaderSkinController(SVSimDbContext db, RewardGrantService rewards, TimeProvider time)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_rewards = rewards;
|
||||||
|
_time = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("set")]
|
[HttpPost("set")]
|
||||||
@@ -28,8 +45,6 @@ public class LeaderSkinController : SVSimController
|
|||||||
|
|
||||||
if (request.IsRandomLeaderSkin)
|
if (request.IsRandomLeaderSkin)
|
||||||
{
|
{
|
||||||
// Random-skin mode needs a per-viewer per-class shuffle pool, which we don't
|
|
||||||
// persist yet (ViewerClassData has no list field for it). Punt for now.
|
|
||||||
return StatusCode(StatusCodes.Status501NotImplemented,
|
return StatusCode(StatusCodes.Status501NotImplemented,
|
||||||
new { error = "random_leader_skin_not_implemented" });
|
new { error = "random_leader_skin_not_implemented" });
|
||||||
}
|
}
|
||||||
@@ -44,7 +59,6 @@ public class LeaderSkinController : SVSimController
|
|||||||
var classData = viewer.Classes.FirstOrDefault(c => c.Class.Id == request.ClassId);
|
var classData = viewer.Classes.FirstOrDefault(c => c.Class.Id == request.ClassId);
|
||||||
if (classData is null) return BadRequest(new { error = "unknown_class" });
|
if (classData is null) return BadRequest(new { error = "unknown_class" });
|
||||||
|
|
||||||
// Skin must (a) exist in the catalog, (b) match the target class, (c) be owned by the viewer.
|
|
||||||
var skin = await _db.LeaderSkins.FindAsync(request.LeaderSkinId);
|
var skin = await _db.LeaderSkins.FindAsync(request.LeaderSkinId);
|
||||||
if (skin is null) return BadRequest(new { error = "unknown_skin" });
|
if (skin is null) return BadRequest(new { error = "unknown_skin" });
|
||||||
if (skin.ClassId != request.ClassId) return BadRequest(new { error = "skin_class_mismatch" });
|
if (skin.ClassId != request.ClassId) return BadRequest(new { error = "skin_class_mismatch" });
|
||||||
@@ -61,4 +75,345 @@ public class LeaderSkinController : SVSimController
|
|||||||
LeaderSkinIdList = new(),
|
LeaderSkinIdList = new(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("ids")]
|
||||||
|
public async Task<ActionResult<LeaderSkinIdsResponse>> Ids(BaseRequest _)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
var ids = await _db.Viewers
|
||||||
|
.Where(v => v.Id == viewerId)
|
||||||
|
.SelectMany(v => v.LeaderSkins.Select(s => s.Id))
|
||||||
|
.OrderBy(id => id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new LeaderSkinIdsResponse { UserLeaderSkinIds = ids };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("products")]
|
||||||
|
public async Task<ActionResult<Dictionary<string, SkinSeriesDto>>> Products(BaseRequest _)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
var ownedSkinIds = (await _db.Viewers
|
||||||
|
.Where(v => v.Id == viewerId)
|
||||||
|
.SelectMany(v => v.LeaderSkins.Select(s => s.Id))
|
||||||
|
.ToListAsync()).ToHashSet();
|
||||||
|
|
||||||
|
var claimedSeries = (await _db.ViewerLeaderSkinSetClaims
|
||||||
|
.Where(c => c.ViewerId == viewerId)
|
||||||
|
.Select(c => c.SeriesId)
|
||||||
|
.ToListAsync()).ToHashSet();
|
||||||
|
|
||||||
|
var series = await _db.LeaderSkinShopSeries
|
||||||
|
.Where(s => s.IsEnabled)
|
||||||
|
.Include(s => s.SetCompletionRewards)
|
||||||
|
.Include(s => s.Products.Where(p => p.IsEnabled)).ThenInclude(p => p.Rewards)
|
||||||
|
.OrderBy(s => s.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var result = new Dictionary<string, SkinSeriesDto>();
|
||||||
|
foreach (var s in series)
|
||||||
|
{
|
||||||
|
var products = s.Products.OrderBy(p => p.Id).Select(p => ToProductDto(p, ownedSkinIds)).ToList();
|
||||||
|
bool seriesCompleted = products.Count > 0 && products.All(p => p.IsPurchased);
|
||||||
|
int rewardStatus = ComputeRewardStatus(s, seriesCompleted, claimedSeries.Contains(s.Id));
|
||||||
|
|
||||||
|
result[s.Id.ToString()] = new SkinSeriesDto
|
||||||
|
{
|
||||||
|
SeriesId = s.Id,
|
||||||
|
IsCompleted = seriesCompleted,
|
||||||
|
IsNew = s.IsNew,
|
||||||
|
SetSalesStatus = s.SetSalesStatus,
|
||||||
|
Rewards = new SkinSeriesRewardsDto
|
||||||
|
{
|
||||||
|
Status = rewardStatus,
|
||||||
|
Items = s.SetCompletionRewards.OrderBy(r => r.OrderIndex).Select(r => new SkinSeriesRewardItemDto
|
||||||
|
{
|
||||||
|
RewardType = r.RewardType,
|
||||||
|
RewardDetailId = r.RewardDetailId,
|
||||||
|
RewardNumber = r.RewardNumber,
|
||||||
|
}).ToList(),
|
||||||
|
},
|
||||||
|
SetPrices = new SkinSeriesSetPricesDto
|
||||||
|
{
|
||||||
|
SetPriceCrystal = s.SetPriceCrystal,
|
||||||
|
SetPriceRupy = s.SetPriceRupy,
|
||||||
|
SetPriceTicket = s.SetPriceTicket,
|
||||||
|
TicketId = s.SetPriceTicketId,
|
||||||
|
},
|
||||||
|
Products = products,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("buy")]
|
||||||
|
public async Task<ActionResult<LeaderSkinBuyResponse>> Buy(LeaderSkinBuyRequest request)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
if (request.SalesType is 3)
|
||||||
|
return StatusCode(StatusCodes.Status501NotImplemented,
|
||||||
|
new { error = "ticket_currency_path_not_implemented" });
|
||||||
|
if (request.SalesType is < 0 or > 3)
|
||||||
|
return BadRequest(new { error = "invalid_sales_type" });
|
||||||
|
|
||||||
|
var product = await _db.LeaderSkinShopProducts
|
||||||
|
.Include(p => p.Rewards)
|
||||||
|
.Include(p => p.Series)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == request.ProductId);
|
||||||
|
if (product is null) return NotFound(new { error = "unknown_product" });
|
||||||
|
if (!product.IsEnabled || product.Series is not { IsEnabled: true })
|
||||||
|
return BadRequest(new { error = "product_not_available" });
|
||||||
|
|
||||||
|
var viewer = await LoadViewerGraphAsync(viewerId);
|
||||||
|
|
||||||
|
// Already-purchased = viewer owns the leader_skin this product grants.
|
||||||
|
if (viewer.LeaderSkins.Any(s => s.Id == product.LeaderSkinId))
|
||||||
|
return BadRequest(new { error = "already_purchased" });
|
||||||
|
|
||||||
|
var rewardList = new List<RewardListEntry>();
|
||||||
|
var debit = DebitProductPrice(viewer, product, request.SalesType);
|
||||||
|
if (debit.Error is not null) return BadRequest(new { error = debit.Error });
|
||||||
|
if (debit.PostState is not null) rewardList.Add(debit.PostState);
|
||||||
|
|
||||||
|
await ApplyRewardsAsync(viewer, product.Rewards, rewardList);
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return new LeaderSkinBuyResponse { RewardList = rewardList };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("buy_set")]
|
||||||
|
public async Task<ActionResult<LeaderSkinBuyResponse>> BuySet(LeaderSkinBuySetRequest request)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
if (request.SalesType is 3)
|
||||||
|
return StatusCode(StatusCodes.Status501NotImplemented,
|
||||||
|
new { error = "ticket_currency_path_not_implemented" });
|
||||||
|
if (request.SalesType is < 0 or > 3)
|
||||||
|
return BadRequest(new { error = "invalid_sales_type" });
|
||||||
|
|
||||||
|
var series = await _db.LeaderSkinShopSeries
|
||||||
|
.Include(s => s.Products.Where(p => p.IsEnabled)).ThenInclude(p => p.Rewards)
|
||||||
|
.FirstOrDefaultAsync(s => s.Id == request.SeriesId);
|
||||||
|
if (series is null) return NotFound(new { error = "unknown_series" });
|
||||||
|
if (!series.IsEnabled || series.SetSalesStatus == 0)
|
||||||
|
return BadRequest(new { error = "set_sale_not_active" });
|
||||||
|
|
||||||
|
var viewer = await LoadViewerGraphAsync(viewerId);
|
||||||
|
|
||||||
|
var rewardList = new List<RewardListEntry>();
|
||||||
|
var debit = DebitSetPrice(viewer, series, request.SalesType);
|
||||||
|
if (debit.Error is not null) return BadRequest(new { error = debit.Error });
|
||||||
|
if (debit.PostState is not null) rewardList.Add(debit.PostState);
|
||||||
|
|
||||||
|
// Grant every product's rewards; RewardGrantService is idempotent on already-owned
|
||||||
|
// cosmetics, so partial-set buyers don't double-add.
|
||||||
|
foreach (var p in series.Products.OrderBy(p => p.Id))
|
||||||
|
{
|
||||||
|
await ApplyRewardsAsync(viewer, p.Rewards, rewardList);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return new LeaderSkinBuyResponse { RewardList = rewardList };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("buy_set_item")]
|
||||||
|
public async Task<ActionResult<LeaderSkinBuyResponse>> BuySetItem(LeaderSkinBuySetItemRequest request)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
var series = await _db.LeaderSkinShopSeries
|
||||||
|
.Include(s => s.SetCompletionRewards)
|
||||||
|
.Include(s => s.Products.Where(p => p.IsEnabled))
|
||||||
|
.FirstOrDefaultAsync(s => s.Id == request.SeriesId);
|
||||||
|
if (series is null) return NotFound(new { error = "unknown_series" });
|
||||||
|
|
||||||
|
// Check claim hasn't been made already (idempotent — returns empty reward_list rather
|
||||||
|
// than 400 so the client doesn't error if it retries).
|
||||||
|
var existingClaim = await _db.ViewerLeaderSkinSetClaims
|
||||||
|
.FirstOrDefaultAsync(c => c.ViewerId == viewerId && c.SeriesId == series.Id);
|
||||||
|
if (existingClaim is not null)
|
||||||
|
return new LeaderSkinBuyResponse { RewardList = new() };
|
||||||
|
|
||||||
|
var viewer = await LoadViewerGraphAsync(viewerId);
|
||||||
|
|
||||||
|
// Must own every skin in the series to claim the bonus.
|
||||||
|
var ownedSkinIds = viewer.LeaderSkins.Select(s => s.Id).ToHashSet();
|
||||||
|
bool ownsAll = series.Products.Count > 0 && series.Products.All(p => ownedSkinIds.Contains(p.LeaderSkinId));
|
||||||
|
if (!ownsAll)
|
||||||
|
return BadRequest(new { error = "series_not_completed" });
|
||||||
|
|
||||||
|
var rewardList = new List<RewardListEntry>();
|
||||||
|
await ApplyRewardsAsync(viewer, series.SetCompletionRewards, rewardList);
|
||||||
|
|
||||||
|
_db.ViewerLeaderSkinSetClaims.Add(new ViewerLeaderSkinSetClaim
|
||||||
|
{
|
||||||
|
ViewerId = viewerId,
|
||||||
|
SeriesId = series.Id,
|
||||||
|
ClaimedAt = _time.GetUtcNow().UtcDateTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return new LeaderSkinBuyResponse { RewardList = rewardList };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Computes the per-viewer <c>rewards.status</c> for a series:
|
||||||
|
/// 0=none — set_sales_status==0 OR no bonus items configured (matches prod, which ships
|
||||||
|
/// status=0 for series where items[] is empty even when set_sales_status==1)
|
||||||
|
/// 1=not_got — bonus exists, series completed by viewer, bonus unclaimed
|
||||||
|
/// 2=got — viewer claimed the bonus
|
||||||
|
/// 1 (effectively "available later") when set sale active with bonus and viewer hasn't
|
||||||
|
/// completed the series.
|
||||||
|
/// The 1/2 distinction matches the client enum (RewardStatus.not_got vs .got).
|
||||||
|
/// <para>
|
||||||
|
/// Important: emitting status=1 when items[] is empty triggers the client's
|
||||||
|
/// <c>is_completed && not_got</c> branch in SkinPurchaseInfoTask.CreateSetSaleInfo,
|
||||||
|
/// which marks the set sale as FREE and renders a useless "claim" button for a
|
||||||
|
/// nonexistent bonus. Always return 0 when there's nothing to claim.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private static int ComputeRewardStatus(LeaderSkinShopSeriesEntry series, bool seriesCompleted, bool claimed)
|
||||||
|
{
|
||||||
|
if (series.SetSalesStatus == 0) return 0;
|
||||||
|
if (series.SetCompletionRewards.Count == 0) return 0;
|
||||||
|
if (claimed) return 2;
|
||||||
|
if (seriesCompleted) return 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SkinProductDto ToProductDto(LeaderSkinShopProductEntry p, HashSet<int> ownedSkinIds)
|
||||||
|
{
|
||||||
|
bool isPurchased = ownedSkinIds.Contains(p.LeaderSkinId);
|
||||||
|
return new SkinProductDto
|
||||||
|
{
|
||||||
|
ProductId = p.Id,
|
||||||
|
LeaderSkinId = p.LeaderSkinId,
|
||||||
|
ProductName = p.ProductNameKey,
|
||||||
|
Introduction = p.IntroductionKey,
|
||||||
|
CvName = p.CvNameKey,
|
||||||
|
IsPurchased = isPurchased,
|
||||||
|
Sale = new SkinProductSaleDto
|
||||||
|
{
|
||||||
|
SinglePriceCrystal = p.SinglePriceCrystal,
|
||||||
|
SinglePriceRupy = p.SinglePriceRupy,
|
||||||
|
SinglePriceTicket = p.SinglePriceTicket,
|
||||||
|
TicketNumber = p.TicketNumber,
|
||||||
|
ItemId = p.TicketItemId,
|
||||||
|
},
|
||||||
|
Rewards = p.Rewards.OrderBy(r => r.OrderIndex).Select(r => new SkinProductRewardDto
|
||||||
|
{
|
||||||
|
RewardType = r.RewardType,
|
||||||
|
RewardDetailId = r.RewardDetailId,
|
||||||
|
RewardNumber = r.RewardNumber,
|
||||||
|
IsOwned = IsRewardOwned(r, ownedSkinIds),
|
||||||
|
}).ToList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A bundled reward shows as "owned" when the viewer already has the cosmetic. For now we
|
||||||
|
/// only flag the Skin reward (type==10) against the viewer's skin collection — the cascaded
|
||||||
|
/// emblem/sleeve typically come with the skin, so the heuristic is "skin owned → all three
|
||||||
|
/// bundle items are de-facto owned." Refine later if a capture shows independent state.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsRewardOwned(LeaderSkinShopProductRewardEntry r, HashSet<int> ownedSkinIds)
|
||||||
|
{
|
||||||
|
// Skin reward: direct check.
|
||||||
|
if (r.RewardType == (int)UserGoodsType.Skin)
|
||||||
|
return ownedSkinIds.Contains((int)r.RewardDetailId);
|
||||||
|
// Other types: we don't have the full cosmetic-owned graph in scope here. The product's
|
||||||
|
// sibling Skin reward tells us whether the bundle was purchased; piggy-back on that by
|
||||||
|
// letting the caller pre-compute IsPurchased. Conservative default: not owned.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (RewardListEntry? PostState, string? Error) DebitProductPrice(
|
||||||
|
Viewer viewer, LeaderSkinShopProductEntry product, int salesType)
|
||||||
|
{
|
||||||
|
return salesType switch
|
||||||
|
{
|
||||||
|
0 when product.SinglePriceCrystal == 0 && product.SinglePriceRupy == 0 => (null, null),
|
||||||
|
0 => (null, "price_not_available_for_currency"),
|
||||||
|
1 => product.SinglePriceCrystal is null
|
||||||
|
? (null, "price_not_available_for_currency")
|
||||||
|
: DebitCrystal(viewer, product.SinglePriceCrystal.Value),
|
||||||
|
2 => product.SinglePriceRupy is null
|
||||||
|
? (null, "price_not_available_for_currency")
|
||||||
|
: DebitRupy(viewer, product.SinglePriceRupy.Value),
|
||||||
|
_ => (null, "invalid_sales_type"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private (RewardListEntry? PostState, string? Error) DebitSetPrice(
|
||||||
|
Viewer viewer, LeaderSkinShopSeriesEntry series, int salesType)
|
||||||
|
{
|
||||||
|
return salesType switch
|
||||||
|
{
|
||||||
|
0 when series.SetPriceCrystal == 0 && series.SetPriceRupy == 0 => (null, null),
|
||||||
|
0 => (null, "price_not_available_for_currency"),
|
||||||
|
1 => series.SetPriceCrystal is null
|
||||||
|
? (null, "price_not_available_for_currency")
|
||||||
|
: DebitCrystal(viewer, series.SetPriceCrystal.Value),
|
||||||
|
2 => series.SetPriceRupy is null
|
||||||
|
? (null, "price_not_available_for_currency")
|
||||||
|
: DebitRupy(viewer, series.SetPriceRupy.Value),
|
||||||
|
_ => (null, "invalid_sales_type"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (RewardListEntry?, string?) DebitCrystal(Viewer viewer, int amount)
|
||||||
|
{
|
||||||
|
if (viewer.Currency.Crystals < (ulong)amount) return (null, "insufficient_crystals");
|
||||||
|
viewer.Currency.Crystals -= (ulong)amount;
|
||||||
|
return (new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)viewer.Currency.Crystals }, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (RewardListEntry?, string?) DebitRupy(Viewer viewer, int amount)
|
||||||
|
{
|
||||||
|
if (viewer.Currency.Rupees < (ulong)amount) return (null, "insufficient_rupees");
|
||||||
|
viewer.Currency.Rupees -= (ulong)amount;
|
||||||
|
return (new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)viewer.Currency.Rupees }, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyRewardsAsync<T>(
|
||||||
|
Viewer viewer, IEnumerable<T> rewards, List<RewardListEntry> rewardList) where T : notnull
|
||||||
|
{
|
||||||
|
foreach (var r in rewards)
|
||||||
|
{
|
||||||
|
var (type, detailId, number) = ExtractTuple(r);
|
||||||
|
var granted = await _rewards.ApplyAsync(viewer, (UserGoodsType)type, detailId, number);
|
||||||
|
foreach (var g in granted)
|
||||||
|
{
|
||||||
|
rewardList.Add(new RewardListEntry
|
||||||
|
{
|
||||||
|
RewardType = g.RewardType,
|
||||||
|
RewardId = g.RewardId,
|
||||||
|
RewardNum = g.RewardNum,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int Type, long Id, int Num) ExtractTuple(object reward) => reward switch
|
||||||
|
{
|
||||||
|
LeaderSkinShopProductRewardEntry p => (p.RewardType, p.RewardDetailId, p.RewardNumber),
|
||||||
|
LeaderSkinShopSeriesRewardEntry s => (s.RewardType, s.RewardDetailId, s.RewardNumber),
|
||||||
|
_ => throw new InvalidOperationException($"unexpected reward type {reward.GetType().Name}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private Task<Viewer> LoadViewerGraphAsync(long viewerId) => _db.Viewers
|
||||||
|
.Include(v => v.LeaderSkins)
|
||||||
|
.Include(v => v.Sleeves)
|
||||||
|
.Include(v => v.Emblems)
|
||||||
|
.Include(v => v.Degrees)
|
||||||
|
.Include(v => v.MyPageBackgrounds)
|
||||||
|
.Include(v => v.Items).ThenInclude(i => i.Item)
|
||||||
|
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.FirstAsync(v => v.Id == viewerId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SVSim.Database;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using SVSim.Database.Models.Config;
|
using SVSim.Database.Models.Config;
|
||||||
@@ -47,11 +48,14 @@ public class LoadController : SVSimController
|
|||||||
private readonly ICardAcquisitionService _acquisition;
|
private readonly ICardAcquisitionService _acquisition;
|
||||||
private readonly IGameConfigService _config;
|
private readonly IGameConfigService _config;
|
||||||
private readonly IBattlePassService _battlePass;
|
private readonly IBattlePassService _battlePass;
|
||||||
|
private readonly IViewerMissionStateService _missionState;
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
|
||||||
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
||||||
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository,
|
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository,
|
||||||
ICardAcquisitionService acquisition, IGameConfigService config,
|
ICardAcquisitionService acquisition, IGameConfigService config,
|
||||||
IBattlePassService battlePass)
|
IBattlePassService battlePass, IViewerMissionStateService missionState,
|
||||||
|
SVSimDbContext db)
|
||||||
{
|
{
|
||||||
_viewerRepository = viewerRepository;
|
_viewerRepository = viewerRepository;
|
||||||
_cardRepository = cardRepository;
|
_cardRepository = cardRepository;
|
||||||
@@ -60,6 +64,8 @@ public class LoadController : SVSimController
|
|||||||
_acquisition = acquisition;
|
_acquisition = acquisition;
|
||||||
_config = config;
|
_config = config;
|
||||||
_battlePass = battlePass;
|
_battlePass = battlePass;
|
||||||
|
_missionState = missionState;
|
||||||
|
_db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("index")]
|
[HttpPost("index")]
|
||||||
@@ -83,6 +89,11 @@ public class LoadController : SVSimController
|
|||||||
// (on a separate tracked instance) won't appear on this snapshot. Without the re-fetch,
|
// (on a separate tracked instance) won't appear on this snapshot. Without the re-fetch,
|
||||||
// the response payload would be one /load/index behind on newly-granted cosmetics.
|
// the response payload would be one /load/index behind on newly-granted cosmetics.
|
||||||
await _acquisition.BackfillCosmeticsAsync(viewer.Id);
|
await _acquisition.BackfillCosmeticsAsync(viewer.Id);
|
||||||
|
|
||||||
|
// Lazy-materialize mission/achievement state. Idempotent — safe to call every /load/index.
|
||||||
|
await _missionState.EnsureCurrentAsync(viewer.Id);
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
viewer = await _viewerRepository.GetViewerByShortUdid(shortUdid);
|
viewer = await _viewerRepository.GetViewerByShortUdid(shortUdid);
|
||||||
if (viewer is null)
|
if (viewer is null)
|
||||||
{
|
{
|
||||||
@@ -170,6 +181,7 @@ public class LoadController : SVSimController
|
|||||||
UserInfo = new UserInfo(deviceType, viewer),
|
UserInfo = new UserInfo(deviceType, viewer),
|
||||||
UserCurrency = new UserCurrency(viewer),
|
UserCurrency = new UserCurrency(viewer),
|
||||||
UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(),
|
UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(),
|
||||||
|
SpotPoint = checked((int)viewer.Currency.SpotPoints),
|
||||||
UserRotationDecks = new UserFormatDeckInfo
|
UserRotationDecks = new UserFormatDeckInfo
|
||||||
{
|
{
|
||||||
UserDecks = viewer.Decks.Where(d => d.Format == Format.Rotation)
|
UserDecks = viewer.Decks.Where(d => d.Format == Format.Rotation)
|
||||||
|
|||||||
124
SVSim.EmulatedEntrypoint/Controllers/MissionController.cs
Normal file
124
SVSim.EmulatedEntrypoint/Controllers/MissionController.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Repositories.Mission;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Mission;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
using SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /mission/* — daily/weekly mission slots + achievement claim flow. Wire shapes mirror
|
||||||
|
/// MissionInfoDetail.cs + Wizard/Mission*Task.cs.
|
||||||
|
/// </summary>
|
||||||
|
[Route("mission")]
|
||||||
|
public class MissionController : SVSimController
|
||||||
|
{
|
||||||
|
private const int RetireCooldownSeconds = 75600; // 21h per capture
|
||||||
|
private const int FailureResultCode = 2;
|
||||||
|
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
private readonly IViewerMissionStateService _state;
|
||||||
|
private readonly IMissionAssembler _assembler;
|
||||||
|
private readonly IMissionCatalogRepository _catalog;
|
||||||
|
private readonly IViewerMissionRepository _viewerRepo;
|
||||||
|
private readonly TimeProvider _time;
|
||||||
|
|
||||||
|
public MissionController(
|
||||||
|
SVSimDbContext db,
|
||||||
|
IViewerMissionStateService state,
|
||||||
|
IMissionAssembler assembler,
|
||||||
|
IMissionCatalogRepository catalog,
|
||||||
|
IViewerMissionRepository viewerRepo,
|
||||||
|
TimeProvider time)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_state = state;
|
||||||
|
_assembler = assembler;
|
||||||
|
_catalog = catalog;
|
||||||
|
_viewerRepo = viewerRepo;
|
||||||
|
_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("info")]
|
||||||
|
public async Task<IActionResult> Info(BaseRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
var viewer = await LoadViewer(viewerId, ct);
|
||||||
|
|
||||||
|
await _state.EnsureCurrentAsync(viewer.Id, ct);
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
var dto = await _assembler.BuildAsync(viewer, ct);
|
||||||
|
return Ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("retire")]
|
||||||
|
public async Task<IActionResult> Retire(MissionRetireRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
var viewer = await LoadViewer(viewerId, ct);
|
||||||
|
|
||||||
|
var missions = await _viewerRepo.GetMissionsAsync(viewerId, ct);
|
||||||
|
var target = missions.FirstOrDefault(m => m.Id == request.Id);
|
||||||
|
if (target is null)
|
||||||
|
{
|
||||||
|
return Ok(new { result_code = FailureResultCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
var catalogRow = await _catalog.GetByIdAsync(target.MissionCatalogId, ct);
|
||||||
|
if (catalogRow is null || catalogRow.LotType != 2)
|
||||||
|
{
|
||||||
|
return Ok(new { result_code = FailureResultCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
var pool = await _catalog.GetByLotTypeAsync(2, ct);
|
||||||
|
var assignedIds = missions
|
||||||
|
.Where(m => m.Slot != target.Slot)
|
||||||
|
.Select(m => m.MissionCatalogId).ToHashSet();
|
||||||
|
var candidates = pool.Where(p => p.Id != target.MissionCatalogId && !assignedIds.Contains(p.Id)).ToList();
|
||||||
|
if (candidates.Count == 0)
|
||||||
|
{
|
||||||
|
return Ok(new { result_code = FailureResultCode });
|
||||||
|
}
|
||||||
|
var pick = candidates[Random.Shared.Next(candidates.Count)];
|
||||||
|
|
||||||
|
var now = _time.GetUtcNow();
|
||||||
|
_viewerRepo.RemoveMission(target);
|
||||||
|
_viewerRepo.AddMission(new ViewerMission
|
||||||
|
{
|
||||||
|
ViewerId = viewerId,
|
||||||
|
MissionCatalogId = pick.Id,
|
||||||
|
Slot = target.Slot,
|
||||||
|
AssignedAt = now.ToUnixTimeSeconds(),
|
||||||
|
MissionStatus = 1,
|
||||||
|
});
|
||||||
|
viewer.MissionData.MissionChangeTime = now.AddSeconds(RetireCooldownSeconds).UtcDateTime;
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
var dto = await _assembler.BuildAsync(viewer, ct);
|
||||||
|
return Ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("change_receive_setting")]
|
||||||
|
public async Task<IActionResult> ChangeReceiveSetting(MissionChangeReceiveSettingRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
var viewer = await LoadViewer(viewerId, ct);
|
||||||
|
|
||||||
|
viewer.MissionData.MissionReceiveType = request.MissionReceiveType;
|
||||||
|
await _db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
var dto = await _assembler.BuildAsync(viewer, ct);
|
||||||
|
return Ok(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Viewer> LoadViewer(long viewerId, CancellationToken ct) =>
|
||||||
|
_db.Viewers
|
||||||
|
.Include(v => v.MissionData)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.FirstAsync(v => v.Id == viewerId, ct);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
|||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Common;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Practice;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Practice;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Practice;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Practice;
|
||||||
|
using SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Controllers;
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
@@ -14,11 +15,16 @@ public class PracticeController : SVSimController
|
|||||||
{
|
{
|
||||||
private readonly IDeckRepository _deckRepository;
|
private readonly IDeckRepository _deckRepository;
|
||||||
private readonly IGlobalsRepository _globalsRepository;
|
private readonly IGlobalsRepository _globalsRepository;
|
||||||
|
private readonly IMissionProgressService _missionProgress;
|
||||||
|
|
||||||
public PracticeController(IDeckRepository deckRepository, IGlobalsRepository globalsRepository)
|
public PracticeController(
|
||||||
|
IDeckRepository deckRepository,
|
||||||
|
IGlobalsRepository globalsRepository,
|
||||||
|
IMissionProgressService missionProgress)
|
||||||
{
|
{
|
||||||
_deckRepository = deckRepository;
|
_deckRepository = deckRepository;
|
||||||
_globalsRepository = globalsRepository;
|
_globalsRepository = globalsRepository;
|
||||||
|
_missionProgress = missionProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,15 +89,29 @@ public class PracticeController : SVSimController
|
|||||||
/// XP / no rewards. Class XP bookkeeping is deferred until a per-class XP store exists.
|
/// XP / no rewards. Class XP bookkeeping is deferred until a per-class XP store exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("finish")]
|
[HttpPost("finish")]
|
||||||
public Task<PracticeFinishResponse> Finish(PracticeFinishRequest request)
|
public async Task<PracticeFinishResponse> Finish(PracticeFinishRequest request)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new PracticeFinishResponse
|
// Mission/achievement progress hook. Catalog rows for practice_win achievements use
|
||||||
|
// opponent NAMES (e.g. "practice_win:elite:arisa") — we only have numeric class_id /
|
||||||
|
// difficulty here, so we emit numeric forms. Bridging numeric→name to match captured
|
||||||
|
// catalog rows is a follow-up; the infrastructure is in place.
|
||||||
|
if (request.IsWin == 1 && TryGetViewerId(out long viewerId))
|
||||||
|
{
|
||||||
|
await _missionProgress.RecordEventAsync(viewerId, new[]
|
||||||
|
{
|
||||||
|
"practice_win",
|
||||||
|
$"practice_win:{request.Difficulty}",
|
||||||
|
$"practice_win:{request.Difficulty}:{request.EnemyClassId}",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PracticeFinishResponse
|
||||||
{
|
{
|
||||||
GetClassExperience = 0,
|
GetClassExperience = 0,
|
||||||
ClassExperience = 0,
|
ClassExperience = 0,
|
||||||
ClassLevel = 1,
|
ClassLevel = 1,
|
||||||
AchievedInfo = new Dictionary<string, object>(),
|
AchievedInfo = new Dictionary<string, object>(),
|
||||||
RewardList = new List<Models.Dtos.Common.Reward>()
|
RewardList = new List<Models.Dtos.Common.Reward>()
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
190
SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs
Normal file
190
SVSim.EmulatedEntrypoint/Controllers/SleeveController.cs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Sleeve;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.Sleeve;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /sleeve/* — the sleeve shop. Catalog + single-product purchase. No series-completion bonus
|
||||||
|
/// (sleeves are sold individually; the leader-skin shop is the family with set-buys).
|
||||||
|
/// </summary>
|
||||||
|
[Route("sleeve")]
|
||||||
|
public class SleeveController : SVSimController
|
||||||
|
{
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
private readonly RewardGrantService _rewards;
|
||||||
|
|
||||||
|
public SleeveController(SVSimDbContext db, RewardGrantService rewards)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_rewards = rewards;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("info")]
|
||||||
|
public async Task<ActionResult<SleeveInfoResponse>> Info(BaseRequest _)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
// is_purchased_product is "viewer owns at least one sleeve granted by this product".
|
||||||
|
// Loading the viewer's sleeve-id set once and checking each product against it avoids
|
||||||
|
// an N+1 over products.
|
||||||
|
var ownedSleeveIds = (await _db.Viewers
|
||||||
|
.Where(v => v.Id == viewerId)
|
||||||
|
.SelectMany(v => v.Sleeves.Select(s => (long)s.Id))
|
||||||
|
.ToListAsync()).ToHashSet();
|
||||||
|
|
||||||
|
var series = await _db.SleeveShopSeries
|
||||||
|
.Where(s => s.IsEnabled)
|
||||||
|
.Include(s => s.Products.Where(p => p.IsEnabled)).ThenInclude(p => p.Rewards)
|
||||||
|
.OrderBy(s => s.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var sleeveList = new Dictionary<string, SleeveSeriesDto>();
|
||||||
|
foreach (var s in series)
|
||||||
|
{
|
||||||
|
var products = new Dictionary<string, SleeveProductDto>();
|
||||||
|
foreach (var p in s.Products.OrderBy(p => p.Id))
|
||||||
|
{
|
||||||
|
products[p.Id.ToString()] = new SleeveProductDto
|
||||||
|
{
|
||||||
|
ProductId = p.Id,
|
||||||
|
Name = p.NameKey,
|
||||||
|
PriceCrystal = p.PriceCrystal,
|
||||||
|
PriceRupy = p.PriceRupy,
|
||||||
|
IsPurchasedProduct = IsProductPurchased(p, ownedSleeveIds),
|
||||||
|
Rewards = p.Rewards.OrderBy(r => r.OrderIndex).Select(r => new SleeveProductRewardDto
|
||||||
|
{
|
||||||
|
RewardType = r.RewardType,
|
||||||
|
RewardDetailId = r.RewardDetailId,
|
||||||
|
RewardNumber = r.RewardNumber,
|
||||||
|
}).ToList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sleeveList[s.Id.ToString()] = new SleeveSeriesDto
|
||||||
|
{
|
||||||
|
SeriesId = s.Id,
|
||||||
|
IsNew = s.IsNew,
|
||||||
|
ProductInfo = products,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SleeveInfoResponse { SleeveList = sleeveList };
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("buy")]
|
||||||
|
public async Task<ActionResult<SleeveBuyResponse>> Buy(SleeveBuyRequest request)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
if (request.SalesType is 3)
|
||||||
|
return StatusCode(StatusCodes.Status501NotImplemented,
|
||||||
|
new { error = "ticket_currency_path_not_implemented" });
|
||||||
|
if (request.SalesType is < 0 or > 3)
|
||||||
|
return BadRequest(new { error = "invalid_sales_type" });
|
||||||
|
|
||||||
|
var product = await _db.SleeveShopProducts
|
||||||
|
.Include(p => p.Rewards)
|
||||||
|
.Include(p => p.Series)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == request.ProductId);
|
||||||
|
if (product is null) return NotFound(new { error = "unknown_product" });
|
||||||
|
|
||||||
|
if (!product.IsEnabled || product.Series is not { IsEnabled: true })
|
||||||
|
return BadRequest(new { error = "product_not_available" });
|
||||||
|
|
||||||
|
// Defence-in-depth: client also sends series_id; reject mismatches so a misencoded
|
||||||
|
// request can't accidentally bypass per-series state we'll later add (e.g. series-new flag).
|
||||||
|
if (product.SeriesId != request.SeriesId)
|
||||||
|
return BadRequest(new { error = "series_product_mismatch" });
|
||||||
|
|
||||||
|
var viewer = await LoadViewerGraphAsync(viewerId);
|
||||||
|
|
||||||
|
if (IsProductPurchased(product, viewer.Sleeves.Select(s => (long)s.Id).ToHashSet()))
|
||||||
|
return BadRequest(new { error = "already_purchased" });
|
||||||
|
|
||||||
|
// Pricing: capture-confirmed shape is single-price-per-currency (no intro/regular tiers
|
||||||
|
// like BuildDeck). At least one of crystal/rupy must match the chosen sales_type;
|
||||||
|
// sales_type==0 means "free", which requires both prices == 0.
|
||||||
|
var rewardList = new List<RewardListEntry>();
|
||||||
|
switch (request.SalesType)
|
||||||
|
{
|
||||||
|
case 0: // free
|
||||||
|
if (!(product.PriceCrystal == 0 && product.PriceRupy == 0))
|
||||||
|
return BadRequest(new { error = "price_not_available_for_currency" });
|
||||||
|
break;
|
||||||
|
case 1: // crystal
|
||||||
|
if (product.PriceCrystal is null)
|
||||||
|
return BadRequest(new { error = "price_not_available_for_currency" });
|
||||||
|
var crystalCost = (ulong)product.PriceCrystal.Value;
|
||||||
|
if (viewer.Currency.Crystals < crystalCost)
|
||||||
|
return BadRequest(new { error = "insufficient_crystals" });
|
||||||
|
viewer.Currency.Crystals -= crystalCost;
|
||||||
|
rewardList.Add(new RewardListEntry { RewardType = 2, RewardId = 0, RewardNum = (int)viewer.Currency.Crystals });
|
||||||
|
break;
|
||||||
|
case 2: // rupy
|
||||||
|
if (product.PriceRupy is null)
|
||||||
|
return BadRequest(new { error = "price_not_available_for_currency" });
|
||||||
|
var rupyCost = (ulong)product.PriceRupy.Value;
|
||||||
|
if (viewer.Currency.Rupees < rupyCost)
|
||||||
|
return BadRequest(new { error = "insufficient_rupees" });
|
||||||
|
viewer.Currency.Rupees -= rupyCost;
|
||||||
|
rewardList.Add(new RewardListEntry { RewardType = 9, RewardId = 0, RewardNum = (int)viewer.Currency.Rupees });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant each catalog reward through the central dispatcher — covers sleeve (6), emblem
|
||||||
|
// (7), and any future bundled grants. ApplyAsync returns post-state-aware reward entries
|
||||||
|
// suitable for emission as-is.
|
||||||
|
foreach (var r in product.Rewards.OrderBy(r => r.OrderIndex))
|
||||||
|
{
|
||||||
|
var granted = await _rewards.ApplyAsync(viewer, (UserGoodsType)r.RewardType, r.RewardDetailId, r.RewardNumber);
|
||||||
|
foreach (var g in granted)
|
||||||
|
{
|
||||||
|
rewardList.Add(new RewardListEntry
|
||||||
|
{
|
||||||
|
RewardType = g.RewardType,
|
||||||
|
RewardId = g.RewardId,
|
||||||
|
RewardNum = g.RewardNum,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new SleeveBuyResponse { RewardList = rewardList };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A product is "purchased" once the viewer owns at least one of its sleeve-typed reward
|
||||||
|
/// grants. Emblem/other grants aren't load-bearing for this check — a viewer who somehow
|
||||||
|
/// ended up with the emblem but not the sleeve (e.g. partial gift) should still be allowed
|
||||||
|
/// to buy the product to pick up the sleeve.
|
||||||
|
/// </summary>
|
||||||
|
private static bool IsProductPurchased(SleeveShopProductEntry product, HashSet<long> ownedSleeveIds)
|
||||||
|
{
|
||||||
|
foreach (var r in product.Rewards)
|
||||||
|
{
|
||||||
|
if (r.RewardType == (int)UserGoodsType.Sleeve && ownedSleeveIds.Contains(r.RewardDetailId))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Viewer> LoadViewerGraphAsync(long viewerId) => _db.Viewers
|
||||||
|
.Include(v => v.Sleeves)
|
||||||
|
.Include(v => v.Emblems)
|
||||||
|
.Include(v => v.LeaderSkins)
|
||||||
|
.Include(v => v.Degrees)
|
||||||
|
.Include(v => v.MyPageBackgrounds)
|
||||||
|
.Include(v => v.Items).ThenInclude(i => i.Item)
|
||||||
|
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.FirstAsync(v => v.Id == viewerId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SVSim.Database;
|
||||||
|
using SVSim.Database.Enums;
|
||||||
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.Database.Services;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests.SpotCardExchange;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses.SpotCardExchange;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /spot_card_exchange/* — trade spot points for individual cards from the rotating exchange
|
||||||
|
/// pool. Spot points are earned from battles/missions (not implemented here — earners live in
|
||||||
|
/// battle/mission finish reward emitters via <see cref="RewardGrantService"/> +
|
||||||
|
/// <see cref="UserGoodsType.SpotCardPoint"/>).
|
||||||
|
/// </summary>
|
||||||
|
[Route("spot_card_exchange")]
|
||||||
|
public class SpotCardExchangeController : SVSimController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pre-release exchange cap. Captures show "2" — global limit, not per-card. When
|
||||||
|
/// IsPreRelease is active on the catalog level we honour this; otherwise the cap is
|
||||||
|
/// effectively unbounded (UI never shows the warning).
|
||||||
|
/// </summary>
|
||||||
|
private const int PreReleaseLimit = 2;
|
||||||
|
|
||||||
|
private readonly SVSimDbContext _db;
|
||||||
|
private readonly RewardGrantService _rewards;
|
||||||
|
private readonly TimeProvider _time;
|
||||||
|
|
||||||
|
public SpotCardExchangeController(SVSimDbContext db, RewardGrantService rewards, TimeProvider time)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_rewards = rewards;
|
||||||
|
_time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("top")]
|
||||||
|
public async Task<ActionResult<SpotCardExchangeTopResponse>> Top(BaseRequest _)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
var viewer = await _db.Viewers
|
||||||
|
.Where(v => v.Id == viewerId)
|
||||||
|
.Select(v => new { v.Currency.SpotPoints })
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (viewer is null) return Unauthorized();
|
||||||
|
|
||||||
|
var catalog = await _db.SpotCardExchangeCatalog
|
||||||
|
.Where(c => c.IsEnabled)
|
||||||
|
.OrderBy(c => c.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var exchanges = await _db.ViewerSpotCardExchanges
|
||||||
|
.Where(e => e.ViewerId == viewerId)
|
||||||
|
.ToListAsync();
|
||||||
|
var exchangedIds = exchanges.Select(e => e.CardId).ToHashSet();
|
||||||
|
int preReleaseExchangedCount = exchanges.Count(e => e.IsPreRelease);
|
||||||
|
bool preReleaseActive = catalog.Any(c => c.IsPreRelease);
|
||||||
|
bool preReleaseLimitHit = preReleaseExchangedCount >= PreReleaseLimit;
|
||||||
|
|
||||||
|
// Build the 9-clan-bucket dict-of-arrays. Every clan slot is present even when empty;
|
||||||
|
// the inner dict always uses key "1" matching the captured prod shape.
|
||||||
|
var byClan = new List<Dictionary<string, List<SpotCardExchangeCardDto>>>(9);
|
||||||
|
for (int clan = 0; clan < 9; clan++)
|
||||||
|
{
|
||||||
|
byClan.Add(new Dictionary<string, List<SpotCardExchangeCardDto>>
|
||||||
|
{
|
||||||
|
["1"] = new List<SpotCardExchangeCardDto>(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var c in catalog)
|
||||||
|
{
|
||||||
|
int clanIdx = Math.Clamp(c.ClassId, 0, 8);
|
||||||
|
byClan[clanIdx]["1"].Add(new SpotCardExchangeCardDto
|
||||||
|
{
|
||||||
|
CardId = c.Id,
|
||||||
|
ExchangeStatus = ComputeExchangeStatus(c, exchangedIds, preReleaseLimitHit),
|
||||||
|
ExchangePoint = c.ExchangePoint.ToString(),
|
||||||
|
Class = c.ClassId.ToString(),
|
||||||
|
IsPreRelease = c.IsPreRelease,
|
||||||
|
TsRotationId = c.TsRotationId.ToString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SpotCardExchangeTopResponse
|
||||||
|
{
|
||||||
|
SpotPoint = checked((int)viewer.SpotPoints),
|
||||||
|
ExchangeableCardList = byClan,
|
||||||
|
SoonCycleOutCardSetId = string.Empty, // No captured value to derive; spec allows ""
|
||||||
|
PreReleaseInfo = new PreReleaseInfoDto
|
||||||
|
{
|
||||||
|
IsPreRelease = preReleaseActive,
|
||||||
|
PreReleaseSpotCardExchangeCount = preReleaseExchangedCount,
|
||||||
|
PreReleaseSpotCardExchangeLimit = PreReleaseLimit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("exchange")]
|
||||||
|
public async Task<ActionResult<SpotCardExchangeResponse>> Exchange(SpotCardExchangeRequest request)
|
||||||
|
{
|
||||||
|
if (!TryGetViewerId(out long viewerId)) return Unauthorized();
|
||||||
|
|
||||||
|
var entry = await _db.SpotCardExchangeCatalog.FindAsync((long)request.CardId);
|
||||||
|
if (entry is null || !entry.IsEnabled)
|
||||||
|
return BadRequest(new { error = "unknown_card" });
|
||||||
|
|
||||||
|
// Already-exchanged guard — each catalog row is one card per viewer.
|
||||||
|
var existingExchange = await _db.ViewerSpotCardExchanges
|
||||||
|
.FirstOrDefaultAsync(e => e.ViewerId == viewerId && e.CardId == entry.Id);
|
||||||
|
if (existingExchange is not null)
|
||||||
|
return BadRequest(new { error = "already_exchanged" });
|
||||||
|
|
||||||
|
if (entry.IsPreRelease)
|
||||||
|
{
|
||||||
|
int prCount = await _db.ViewerSpotCardExchanges
|
||||||
|
.CountAsync(e => e.ViewerId == viewerId && e.IsPreRelease);
|
||||||
|
if (prCount >= PreReleaseLimit)
|
||||||
|
return BadRequest(new { error = "pre_release_limit_reached" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewer = await LoadViewerGraphAsync(viewerId);
|
||||||
|
|
||||||
|
var rewardList = new List<RewardListEntry>();
|
||||||
|
|
||||||
|
// Debit spot points. Client-supplied exchange_point isn't authoritative — server uses
|
||||||
|
// catalog price. Mirroring the build_deck/sleeve convention: post-state currency entry
|
||||||
|
// first, then grants.
|
||||||
|
if (viewer.Currency.SpotPoints < (ulong)entry.ExchangePoint)
|
||||||
|
return BadRequest(new { error = "insufficient_spot_points" });
|
||||||
|
viewer.Currency.SpotPoints -= (ulong)entry.ExchangePoint;
|
||||||
|
rewardList.Add(new RewardListEntry
|
||||||
|
{
|
||||||
|
RewardType = (int)UserGoodsType.SpotCardPoint,
|
||||||
|
RewardId = 0,
|
||||||
|
RewardNum = checked((int)viewer.Currency.SpotPoints),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Grant the card itself via the existing card dispatcher (handles cosmetic cascade).
|
||||||
|
var granted = await _rewards.ApplyAsync(viewer, UserGoodsType.Card, entry.Id, 1);
|
||||||
|
foreach (var g in granted)
|
||||||
|
{
|
||||||
|
rewardList.Add(new RewardListEntry
|
||||||
|
{
|
||||||
|
RewardType = g.RewardType,
|
||||||
|
RewardId = g.RewardId,
|
||||||
|
RewardNum = g.RewardNum,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.ViewerSpotCardExchanges.Add(new ViewerSpotCardExchange
|
||||||
|
{
|
||||||
|
ViewerId = viewerId,
|
||||||
|
CardId = entry.Id,
|
||||||
|
IsPreRelease = entry.IsPreRelease,
|
||||||
|
ExchangedAt = _time.GetUtcNow().UtcDateTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
return new SpotCardExchangeResponse { RewardList = rewardList };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps to <see cref="Wizard.SpotCardExchangeInfo.ExchangeStatus"/>:
|
||||||
|
/// 0 = EnableExchange
|
||||||
|
/// 1 = AlreadyExchange (viewer has already exchanged this card)
|
||||||
|
/// 2 = LimitOver (pre-release card and viewer hit the global pre-release cap)
|
||||||
|
/// Insufficient-balance is NOT surfaced here — the client greys those out by comparing
|
||||||
|
/// <c>spot_point</c> to <c>exchange_point</c>.
|
||||||
|
/// </summary>
|
||||||
|
private static int ComputeExchangeStatus(SpotCardExchangeEntry c, HashSet<long> exchangedIds, bool preReleaseLimitHit)
|
||||||
|
{
|
||||||
|
if (exchangedIds.Contains(c.Id)) return 1;
|
||||||
|
if (c.IsPreRelease && preReleaseLimitHit) return 2;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Viewer> LoadViewerGraphAsync(long viewerId) => _db.Viewers
|
||||||
|
.Include(v => v.Cards).ThenInclude(c => c.Card)
|
||||||
|
.Include(v => v.Sleeves)
|
||||||
|
.Include(v => v.Emblems)
|
||||||
|
.Include(v => v.LeaderSkins)
|
||||||
|
.Include(v => v.Degrees)
|
||||||
|
.Include(v => v.MyPageBackgrounds)
|
||||||
|
.Include(v => v.Items).ThenInclude(i => i.Item)
|
||||||
|
.AsSplitQuery()
|
||||||
|
.FirstAsync(v => v.Id == viewerId);
|
||||||
|
}
|
||||||
@@ -11,7 +11,12 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
|
|||||||
public class StoryController : SVSimController
|
public class StoryController : SVSimController
|
||||||
{
|
{
|
||||||
private readonly IStoryService _service;
|
private readonly IStoryService _service;
|
||||||
public StoryController(IStoryService service) { _service = service; }
|
private readonly IMissionProgressService _missionProgress;
|
||||||
|
public StoryController(IStoryService service, IMissionProgressService missionProgress)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
_missionProgress = missionProgress;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("/story/section")]
|
[HttpPost("/story/section")]
|
||||||
[HttpPost("/main_story/section")]
|
[HttpPost("/main_story/section")]
|
||||||
@@ -65,7 +70,28 @@ public class StoryController : SVSimController
|
|||||||
public async Task<ActionResult<FinishResponse>> Finish(FinishRequest req)
|
public async Task<ActionResult<FinishResponse>> Finish(FinishRequest req)
|
||||||
{
|
{
|
||||||
if (!TryGetViewerId(out long vid)) return Unauthorized();
|
if (!TryGetViewerId(out long vid)) return Unauthorized();
|
||||||
return await _service.FinishAsync(ResolveApiType(), req, vid);
|
var result = await _service.FinishAsync(ResolveApiType(), req, vid);
|
||||||
|
|
||||||
|
// Emit story-chapter-finish events for mission/achievement progress.
|
||||||
|
var apiType = ResolveApiType();
|
||||||
|
var prefix = apiType switch
|
||||||
|
{
|
||||||
|
StoryApiType.Main => "main",
|
||||||
|
StoryApiType.Limited => "limited",
|
||||||
|
StoryApiType.Event => "event",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
if (prefix is not null && req.StoryId != 0)
|
||||||
|
{
|
||||||
|
await _missionProgress.RecordEventAsync(vid, new[]
|
||||||
|
{
|
||||||
|
"story_chapter_finish",
|
||||||
|
$"story_chapter_finish:{prefix}",
|
||||||
|
$"story_chapter_finish:{prefix}:{req.StoryId}",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("/main_story/all_finish")]
|
[HttpPost("/main_story/all_finish")]
|
||||||
|
|||||||
51
SVSim.EmulatedEntrypoint/Controllers/ToolController.cs
Normal file
51
SVSim.EmulatedEntrypoint/Controllers/ToolController.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SVSim.Database.Repositories.Viewer;
|
||||||
|
using SVSim.EmulatedEntrypoint.Extensions;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
|
public class ToolController : SVSimController
|
||||||
|
{
|
||||||
|
private readonly ILogger<ToolController> _logger;
|
||||||
|
private readonly IViewerRepository _viewerRepository;
|
||||||
|
|
||||||
|
public ToolController(ILogger<ToolController> logger, IViewerRepository viewerRepository)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_viewerRepository = viewerRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>POST /tool/signup</c> — the client's first request on a fresh boot. Creates (or returns
|
||||||
|
/// the existing) Viewer keyed on the request's UDID. The interesting outputs (viewer_id,
|
||||||
|
/// short_udid, udid) all flow back via <c>data_headers</c>, populated by the translation
|
||||||
|
/// middleware after this action returns — we just need to stash the viewer on HttpContext so
|
||||||
|
/// the middleware picks it up the same way the auth handler does for logged-in endpoints.
|
||||||
|
///
|
||||||
|
/// Spec: <c>docs/api-spec/endpoints/pre-login/tool-signup.md</c>.
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("signup")]
|
||||||
|
public async Task<SignupResponse> Signup([FromBody] SignupRequest request)
|
||||||
|
{
|
||||||
|
Guid? maybeUdid = HttpContext.GetUdid();
|
||||||
|
if (maybeUdid is not Guid udid || udid == Guid.Empty)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Cannot register viewer: request has no resolvable UDID (missing UDID/SID headers, or " +
|
||||||
|
"SessionidMappingMiddleware couldn't decode the UDID header).");
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewer = await _viewerRepository.GetViewerByUdid(udid)
|
||||||
|
?? await _viewerRepository.RegisterAnonymousViewer(udid);
|
||||||
|
|
||||||
|
HttpContext.SetViewer(viewer);
|
||||||
|
_logger.LogInformation("Signup resolved for udid={Udid} → viewer_id={ViewerId}, short_udid={ShortUdid}.",
|
||||||
|
udid, viewer.Id, viewer.ShortUdid);
|
||||||
|
|
||||||
|
return new SignupResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
|
using SVSim.EmulatedEntrypoint.Constants;
|
||||||
|
using SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Extensions;
|
namespace SVSim.EmulatedEntrypoint.Extensions;
|
||||||
|
|
||||||
public static class HttpContextExtensions
|
public static class HttpContextExtensions
|
||||||
{
|
{
|
||||||
private const string ViewerItemName = "SVSimViewer";
|
private const string ViewerItemName = "SVSimViewer";
|
||||||
|
|
||||||
public static Viewer? GetViewer(this HttpContext context)
|
public static Viewer? GetViewer(this HttpContext context)
|
||||||
{
|
{
|
||||||
if (context.Items.TryGetValue(ViewerItemName, out object? viewer))
|
if (context.Items.TryGetValue(ViewerItemName, out object? viewer))
|
||||||
@@ -21,4 +23,18 @@ public static class HttpContextExtensions
|
|||||||
context.Items[ViewerItemName] = viewer;
|
context.Items[ViewerItemName] = viewer;
|
||||||
return viewer;
|
return viewer;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the client's UDID for this request by looking up the SID header in the
|
||||||
|
/// in-memory SID→UDID dict that <see cref="Middlewares.SessionidMappingMiddleware"/>
|
||||||
|
/// populates from the UDID header. Returns null when the SID isn't mapped (e.g. the
|
||||||
|
/// request didn't carry a UDID header at all, or carried an undecodable one).
|
||||||
|
/// </summary>
|
||||||
|
public static Guid? GetUdid(this HttpContext context)
|
||||||
|
{
|
||||||
|
string? sid = context.Request.Headers[NetworkConstants.SessionIdHeaderName];
|
||||||
|
if (sid is null) return null;
|
||||||
|
var sessionService = context.RequestServices.GetService<ShadowverseSessionService>();
|
||||||
|
return sessionService?.GetUdidFromSessionId(sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,7 +100,19 @@ public class ShadowverseTranslationMiddleware : IMiddleware
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
Type requestType = endpointDescriptor.Parameters.FirstOrDefault().ParameterType;
|
var firstParam = endpointDescriptor.Parameters.FirstOrDefault();
|
||||||
|
if (firstParam is null)
|
||||||
|
{
|
||||||
|
// Action method has no parameters — middleware can't bind the (encrypted+msgpacked)
|
||||||
|
// body to anything. The codebase convention is to take a BaseRequest even for body-
|
||||||
|
// less endpoints (see e.g. PuzzleController.Info(BaseRequest _)). Fail loud with a
|
||||||
|
// specific message rather than NREing below on .ParameterType.
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Action {endpointDescriptor.DisplayName} has no parameters; the SV translation " +
|
||||||
|
"middleware needs at least one to bind the decrypted body. Add a BaseRequest parameter " +
|
||||||
|
"(or a derived DTO) — see other *Info/*Top actions for the convention.");
|
||||||
|
}
|
||||||
|
Type requestType = firstParam.ParameterType;
|
||||||
object? data;
|
object? data;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -166,7 +178,11 @@ public class ShadowverseTranslationMiddleware : IMiddleware
|
|||||||
// populated (prod sends real numbers for the title check too, but 0 / 0 satisfies
|
// populated (prod sends real numbers for the title check too, but 0 / 0 satisfies
|
||||||
// the client's BaseTask.Parse which only reads result_code + servertime here).
|
// the client's BaseTask.Parse which only reads result_code + servertime here).
|
||||||
ShortUdid = viewer?.ShortUdid ?? 0,
|
ShortUdid = viewer?.ShortUdid ?? 0,
|
||||||
ViewerId = viewer?.Id ?? 0
|
ViewerId = viewer?.Id ?? 0,
|
||||||
|
// Echo the decrypted-against UDID. Most clients ignore this field; SignUpTask.Parse
|
||||||
|
// requires it (validates against Certification.Udid on the response). Comes from
|
||||||
|
// mappedUdid (the value used for AES); never from controller state.
|
||||||
|
Udid = mappedUdid?.ToString() ?? ""
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Achievement;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class AchievementReceiveRewardRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[Key("achievement_type")]
|
||||||
|
[JsonPropertyName("achievement_type")]
|
||||||
|
public int AchievementType { get; set; }
|
||||||
|
|
||||||
|
[Key("level")]
|
||||||
|
[JsonPropertyName("level")]
|
||||||
|
public int Level { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Achievement;
|
||||||
|
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class TotalReceiveCountDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[Key(1)][JsonPropertyName("reward_detail_id")] public long RewardDetailId { get; set; }
|
||||||
|
[Key(2)][JsonPropertyName("reward_count")] public int RewardCount { get; set; }
|
||||||
|
[Key(3)][JsonPropertyName("item_type")] public int ItemType { get; set; }
|
||||||
|
[Key(4)][JsonPropertyName("is_usable")] public bool IsUsable { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class RewardGrantDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[Key(1)][JsonPropertyName("reward_id")] public long RewardId { get; set; }
|
||||||
|
[Key(2)][JsonPropertyName("reward_num")] public int RewardNum { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /achievement/receive_reward response — MissionInfoDataDto + two extras consumed by
|
||||||
|
/// PlayerStaticData.UpdateHaveUserGoodsNumByJsonData per AchievementReceiveRewardTask.cs:33.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AchievementReceiveRewardResponse : MissionInfoDataDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("total_receive_count_list")] public List<TotalReceiveCountDto> TotalReceiveCountList { get; set; } = new();
|
||||||
|
[JsonPropertyName("reward_list")] public List<RewardGrantDto> RewardList { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inner reward block. STRING-typed on wire (capture confirms reward_type/reward_detail_id/
|
||||||
|
/// reward_number all serialize as JSON strings here, unlike UserMission where they're int).
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class BPMonthlyMissionRewardInfoDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("reward_type")] public string RewardType { get; set; } = "";
|
||||||
|
[Key(1)][JsonPropertyName("reward_detail_id")] public string RewardDetailId { get; set; } = "";
|
||||||
|
[Key(2)][JsonPropertyName("reward_number")] public string RewardNumber { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One BP monthly mission. reward_info is OPTIONAL — capture shows "Play 5 Challenge matches"
|
||||||
|
/// has no reward_info block (only BP points). Global WhenWritingNull policy omits when null.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class BPMonthlyMissionDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||||
|
[Key(1)][JsonPropertyName("is_cleared")] public bool IsCleared { get; set; }
|
||||||
|
[Key(2)][JsonPropertyName("require_number")] public int RequireNumber { get; set; }
|
||||||
|
[Key(3)][JsonPropertyName("done_number")] public int DoneNumber { get; set; }
|
||||||
|
[Key(4)][JsonPropertyName("battle_pass_point")] public int BattlePassPoint { get; set; }
|
||||||
|
[Key(5)][JsonPropertyName("reward_info")] public BPMonthlyMissionRewardInfoDto? RewardInfo { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Outer block. Date strings use the capture's space-separated JST format
|
||||||
|
/// ("2026-05-01 02:00:00"). The whole block is omitted from /mission/info when no monthly
|
||||||
|
/// missions are seeded for the current month.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class BPMonthlyMissionsDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("start_date")] public string StartDate { get; set; } = "";
|
||||||
|
[Key(1)][JsonPropertyName("end_date")] public string EndDate { get; set; } = "";
|
||||||
|
[Key(2)][JsonPropertyName("mission_list")] public List<BPMonthlyMissionDto> MissionList { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Top-level payload for /mission/info responses (also reused by /mission/retire,
|
||||||
|
/// /mission/change_receive_setting; /achievement/receive_reward adds reward_list +
|
||||||
|
/// total_receive_count_list to this shape via inheritance).
|
||||||
|
///
|
||||||
|
/// CanChangeMissionTime is wire-required to be present (capture shows null when active).
|
||||||
|
/// Override [JsonIgnore(Condition = Never)] per memory project_wire_null_policy.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class MissionInfoDataDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("user_mission_list")] public List<UserMissionDto> UserMissionList { get; set; } = new();
|
||||||
|
[Key(1)][JsonPropertyName("is_change_mission")] public bool IsChangeMission { get; set; }
|
||||||
|
|
||||||
|
[Key(2)]
|
||||||
|
[JsonPropertyName("can_change_mission_time")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||||
|
public long? CanChangeMissionTime { get; set; }
|
||||||
|
|
||||||
|
[Key(3)][JsonPropertyName("is_change_receive_type")] public bool IsChangeReceiveType { get; set; }
|
||||||
|
|
||||||
|
[Key(4)]
|
||||||
|
[JsonPropertyName("can_change_receive_type_time")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||||
|
public long? CanChangeReceiveTypeTime { get; set; }
|
||||||
|
|
||||||
|
[Key(5)][JsonPropertyName("user_achievement_list")] public List<UserAchievementDto> UserAchievementList { get; set; } = new();
|
||||||
|
[Key(6)][JsonPropertyName("mission_receive_type")] public string MissionReceiveType { get; set; } = "0";
|
||||||
|
|
||||||
|
[Key(7)]
|
||||||
|
[JsonPropertyName("battle_pass_monthly_mission")]
|
||||||
|
public BPMonthlyMissionsDto? BattlePassMonthlyMission { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wire shape of UserAchievement (per MissionInfoDetail.cs:98-116). ios/android are always
|
||||||
|
/// empty strings in our world. max_level is computed from catalog (MAX(Level) per type).
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class UserAchievementDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("achievement_type")] public int AchievementType { get; set; }
|
||||||
|
[Key(1)][JsonPropertyName("achievement_status")] public int AchievementStatus { get; set; }
|
||||||
|
[Key(2)][JsonPropertyName("level")] public int Level { get; set; }
|
||||||
|
[Key(3)][JsonPropertyName("now_achieved_level")] public int NowAchievedLevel { get; set; }
|
||||||
|
[Key(4)][JsonPropertyName("result_announce_saw_level")] public int ResultAnnounceSawLevel { get; set; }
|
||||||
|
[Key(5)][JsonPropertyName("total_count")] public int TotalCount { get; set; }
|
||||||
|
[Key(6)][JsonPropertyName("achievement_name")] public string AchievementName { get; set; } = "";
|
||||||
|
[Key(7)][JsonPropertyName("require_number")] public int RequireNumber { get; set; }
|
||||||
|
[Key(8)][JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[Key(9)][JsonPropertyName("reward_detail_id")] public long RewardDetailId { get; set; }
|
||||||
|
[Key(10)][JsonPropertyName("reward_number")] public int RewardNumber { get; set; }
|
||||||
|
[Key(11)][JsonPropertyName("max_level")] public int MaxLevel { get; set; }
|
||||||
|
[Key(12)][JsonPropertyName("order_num")] public int OrderNum { get; set; }
|
||||||
|
[Key(13)][JsonPropertyName("ios")] public string Ios { get; set; } = "";
|
||||||
|
[Key(14)][JsonPropertyName("android")] public string Android { get; set; } = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Common.Mission;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wire shape of UserMission (per MissionInfoDetail.cs:75-95). lot_type and battle_pass_point
|
||||||
|
/// are STRING-typed on wire (client uses .ToInt() but emits as string in capture). All other
|
||||||
|
/// scalar fields are int. end_time omitted when null per UserMission.Parse() optional read.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject(keyAsPropertyName: true)]
|
||||||
|
public class UserMissionDto
|
||||||
|
{
|
||||||
|
[Key(0)][JsonPropertyName("id")] public long Id { get; set; }
|
||||||
|
[Key(1)][JsonPropertyName("mission_id")] public int MissionId { get; set; }
|
||||||
|
[Key(2)][JsonPropertyName("total_count")] public int TotalCount { get; set; }
|
||||||
|
[Key(3)][JsonPropertyName("mission_status")] public int MissionStatus { get; set; }
|
||||||
|
[Key(4)][JsonPropertyName("display_order")] public int DisplayOrder { get; set; }
|
||||||
|
[Key(5)][JsonPropertyName("mission_name")] public string MissionName { get; set; } = "";
|
||||||
|
[Key(6)][JsonPropertyName("lot_type")] public string LotType { get; set; } = "";
|
||||||
|
[Key(7)][JsonPropertyName("battle_pass_point")] public string BattlePassPoint { get; set; } = "";
|
||||||
|
[Key(8)][JsonPropertyName("require_number")] public int RequireNumber { get; set; }
|
||||||
|
[Key(9)][JsonPropertyName("reward_type")] public int RewardType { get; set; }
|
||||||
|
[Key(10)][JsonPropertyName("reward_detail_id")] public long RewardDetailId { get; set; }
|
||||||
|
[Key(11)][JsonPropertyName("reward_number")] public int RewardNumber { get; set; }
|
||||||
|
[Key(12)][JsonPropertyName("default_flag")] public bool DefaultFlag { get; set; }
|
||||||
|
[Key(13)][JsonPropertyName("start_time")] public long StartTime { get; set; }
|
||||||
|
[Key(14)][JsonPropertyName("end_time")] public long? EndTime { get; set; }
|
||||||
|
}
|
||||||
@@ -21,4 +21,15 @@ public class DataHeaders
|
|||||||
[JsonPropertyName("result_code")]
|
[JsonPropertyName("result_code")]
|
||||||
[Key("result_code")]
|
[Key("result_code")]
|
||||||
public int ResultCode { get; set; }
|
public int ResultCode { get; set; }
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Echoed UDID. Read by <c>SignUpTask.Parse</c> to validate response identity (client logs
|
||||||
|
/// <c>udid一致しません</c> and discards the response on mismatch); ignored by every other
|
||||||
|
/// client task. Always set by <c>ShadowverseTranslationMiddleware</c> from the request's
|
||||||
|
/// resolved UDID — never from controller state. Empty string when the SID→UDID lookup misses
|
||||||
|
/// (request without UDID/SID headers).
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("udid")]
|
||||||
|
[Key("udid")]
|
||||||
|
public string Udid { get; set; } = "";
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Mission;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class MissionChangeReceiveSettingRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[Key("mission_receive_type")]
|
||||||
|
[JsonPropertyName("mission_receive_type")]
|
||||||
|
public int MissionReceiveType { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Mission;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public class MissionRetireRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[Key("id")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.ItemPurchase;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /item_purchase/purchase request body. <c>rest</c> is the client's locally-cached remaining
|
||||||
|
/// quota — used as an optional optimistic-concurrency check on the server. Not authoritative;
|
||||||
|
/// the server's own counter is canonical.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class ItemPurchasePurchaseRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("purchase_id")]
|
||||||
|
[Key("purchase_id")]
|
||||||
|
public int PurchaseId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("rest")]
|
||||||
|
[Key("rest")]
|
||||||
|
public int Rest { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.LeaderSkin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /leader_skin/buy request body. sales_type is ShopCommonUtility.SalesType:
|
||||||
|
/// 0=free, 1=crystal, 2=rupy, 3=ticket (v1: 3 returns 501 — no ticket-priced skin captured).
|
||||||
|
/// <see cref="ItemId"/> is the ticket item id when paying with a ticket, null otherwise.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class LeaderSkinBuyRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("product_id")]
|
||||||
|
[Key("product_id")]
|
||||||
|
public int ProductId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sales_type")]
|
||||||
|
[Key("sales_type")]
|
||||||
|
public int SalesType { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("item_id")]
|
||||||
|
[Key("item_id")]
|
||||||
|
public long? ItemId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.LeaderSkin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /leader_skin/buy_set_item — claim the series-completion bonus once every skin in the series
|
||||||
|
/// is owned. <c>sales_type</c> field exists on the client's param class but is never set; server
|
||||||
|
/// ignores it.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class LeaderSkinBuySetItemRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("series_id")]
|
||||||
|
[Key("series_id")]
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.LeaderSkin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /leader_skin/buy_set — purchase every skin in a series in one call (cheaper per-skin).
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class LeaderSkinBuySetRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("series_id")]
|
||||||
|
[Key("series_id")]
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sales_type")]
|
||||||
|
[Key("sales_type")]
|
||||||
|
public int SalesType { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("item_id")]
|
||||||
|
[Key("item_id")]
|
||||||
|
public long? ItemId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MessagePack;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>POST /tool/signup</c> request body. Spec:
|
||||||
|
/// <c>docs/api-spec/endpoints/pre-login/tool-signup.md</c>. Client source:
|
||||||
|
/// <c>Shadowverse_Code_2026-05-23/Cute/SignUpTask.cs</c> (LoginPostParams).
|
||||||
|
///
|
||||||
|
/// All fields are device telemetry; the server doesn't use them in v1 but still binds them so
|
||||||
|
/// the request shape matches the spec exactly.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class SignupRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("device_name")]
|
||||||
|
[Key("device_name")]
|
||||||
|
public string DeviceName { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("client_type")]
|
||||||
|
[Key("client_type")]
|
||||||
|
public string ClientType { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("os_version")]
|
||||||
|
[Key("os_version")]
|
||||||
|
public string OsVersion { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("app_version")]
|
||||||
|
[Key("app_version")]
|
||||||
|
public string AppVersion { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("resource_version")]
|
||||||
|
[Key("resource_version")]
|
||||||
|
public string ResourceVersion { get; set; } = "";
|
||||||
|
|
||||||
|
[JsonPropertyName("carrier")]
|
||||||
|
[Key("carrier")]
|
||||||
|
public string Carrier { get; set; } = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.Sleeve;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /sleeve/buy request body. sales_type is ShopCommonUtility.SalesType:
|
||||||
|
/// 0=free, 1=crystal, 2=rupy, 3=ticket (v1: 3 returns 501, no ticket-priced sleeve captured).
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class SleeveBuyRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("series_id")]
|
||||||
|
[Key("series_id")]
|
||||||
|
public int SeriesId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("product_id")]
|
||||||
|
[Key("product_id")]
|
||||||
|
public int ProductId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sales_type")]
|
||||||
|
[Key("sales_type")]
|
||||||
|
public int SalesType { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MessagePack;
|
||||||
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests.SpotCardExchange;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /spot_card_exchange/exchange request — trade <see cref="ExchangePoint"/> spot points for
|
||||||
|
/// the card identified by <see cref="CardId"/>. The exchange_point field is the client's view
|
||||||
|
/// of the price (sanity-check it against the catalog server-side).
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class SpotCardExchangeRequest : BaseRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("card_id")]
|
||||||
|
[Key("card_id")]
|
||||||
|
public int CardId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("exchange_point")]
|
||||||
|
[Key("exchange_point")]
|
||||||
|
public int ExchangePoint { get; set; }
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user