Seeding updated
This commit is contained in:
@@ -7,7 +7,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.Database", "SVSim.Dat
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.UnitTests", "SVSim.UnitTests\SVSim.UnitTests.csproj", "{00E87101-F286-46F3-858E-83AB1CEBF8D1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.CardImport", "SVSim.CardImport\SVSim.CardImport.csproj", "{666786D9-9A4D-49EA-A759-39055C57F9AA}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.Bootstrap", "SVSim.Bootstrap\SVSim.Bootstrap.csproj", "{666786D9-9A4D-49EA-A759-39055C57F9AA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
484
SVSim.Bootstrap/Data/prod-captures/deck-info-2026-05-23.json
Normal file
484
SVSim.Bootstrap/Data/prod-captures/deck-info-2026-05-23.json
Normal file
@@ -0,0 +1,484 @@
|
||||
{
|
||||
"data_headers": {
|
||||
"sid": "ac631c29b5f5d07ed5fb6712ad8623c31779553958",
|
||||
"short_udid": 411054851,
|
||||
"viewer_id": 906243102,
|
||||
"servertime": 1779553958,
|
||||
"result_code": 1
|
||||
},
|
||||
"data": {
|
||||
"user_deck_rotation": [],
|
||||
"user_deck_unlimited": [],
|
||||
"maintenance_card_list": [],
|
||||
"user_deck_my_rotation": [],
|
||||
"trial_deck_list": [],
|
||||
"default_deck_list": {
|
||||
"91": {
|
||||
"null": 1,
|
||||
"deck_no": 91,
|
||||
"class_id": 1,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100111010,
|
||||
100111010,
|
||||
100111010,
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100111020,
|
||||
100111020,
|
||||
100111020,
|
||||
100111040,
|
||||
100111040,
|
||||
100111040,
|
||||
100114010,
|
||||
100114010,
|
||||
100114010,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100111060,
|
||||
100111060,
|
||||
100111060,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100111030,
|
||||
100111030,
|
||||
100111030,
|
||||
100111050,
|
||||
100111050,
|
||||
100111050,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100111070,
|
||||
100111070,
|
||||
100111070,
|
||||
100121010,
|
||||
100121010,
|
||||
100121010
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
},
|
||||
"92": {
|
||||
"null": 1,
|
||||
"deck_no": 92,
|
||||
"class_id": 2,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100211010,
|
||||
100211010,
|
||||
100211010,
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100211020,
|
||||
100211020,
|
||||
100211020,
|
||||
100211060,
|
||||
100211060,
|
||||
100211060,
|
||||
100214010,
|
||||
100214010,
|
||||
100214010,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100211030,
|
||||
100211030,
|
||||
100211030,
|
||||
100214020,
|
||||
100214020,
|
||||
100214020,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100211040,
|
||||
100211040,
|
||||
100211040,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100211050,
|
||||
100211050,
|
||||
100211050,
|
||||
100221020,
|
||||
100221020,
|
||||
100221020
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
},
|
||||
"93": {
|
||||
"null": 1,
|
||||
"deck_no": 93,
|
||||
"class_id": 3,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100314010,
|
||||
100314010,
|
||||
100314010,
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100311010,
|
||||
100311010,
|
||||
100311010,
|
||||
100314030,
|
||||
100314030,
|
||||
100314030,
|
||||
100314020,
|
||||
100314020,
|
||||
100314020,
|
||||
100314040,
|
||||
100314040,
|
||||
100314040,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100314050,
|
||||
100314050,
|
||||
100314050,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100314060,
|
||||
100314060,
|
||||
100314060,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100314070,
|
||||
100314070,
|
||||
100314070,
|
||||
100321010,
|
||||
100321010,
|
||||
100321010
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
},
|
||||
"94": {
|
||||
"null": 1,
|
||||
"deck_no": 94,
|
||||
"class_id": 4,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100414020,
|
||||
100414020,
|
||||
100414020,
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100411010,
|
||||
100411010,
|
||||
100411010,
|
||||
100414010,
|
||||
100414010,
|
||||
100414010,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100411050,
|
||||
100411050,
|
||||
100411050,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100411030,
|
||||
100411030,
|
||||
100411030,
|
||||
100414030,
|
||||
100414030,
|
||||
100414030,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100411020,
|
||||
100411020,
|
||||
100411020,
|
||||
100411040,
|
||||
100411040,
|
||||
100411040,
|
||||
100421020,
|
||||
100421020,
|
||||
100421020
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
},
|
||||
"95": {
|
||||
"null": 1,
|
||||
"deck_no": 95,
|
||||
"class_id": 5,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100511010,
|
||||
100511010,
|
||||
100511010,
|
||||
100511020,
|
||||
100511020,
|
||||
100511020,
|
||||
100514010,
|
||||
100514010,
|
||||
100514010,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100511030,
|
||||
100511030,
|
||||
100511030,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100511040,
|
||||
100511040,
|
||||
100511040,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100511050,
|
||||
100511050,
|
||||
100511050,
|
||||
100514020,
|
||||
100514020,
|
||||
100514020,
|
||||
100511060,
|
||||
100511060,
|
||||
100511060,
|
||||
100521030,
|
||||
100521030,
|
||||
100521030
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
},
|
||||
"96": {
|
||||
"null": 1,
|
||||
"deck_no": 96,
|
||||
"class_id": 6,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100611010,
|
||||
100611010,
|
||||
100611010,
|
||||
100611020,
|
||||
100611020,
|
||||
100611020,
|
||||
100614010,
|
||||
100614010,
|
||||
100614010,
|
||||
100614020,
|
||||
100614020,
|
||||
100614020,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100611030,
|
||||
100611030,
|
||||
100611030,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100611050,
|
||||
100611050,
|
||||
100611050,
|
||||
100614030,
|
||||
100614030,
|
||||
100614030,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100611040,
|
||||
100611040,
|
||||
100611040,
|
||||
100621010,
|
||||
100621010,
|
||||
100621010
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
},
|
||||
"97": {
|
||||
"null": 1,
|
||||
"deck_no": 97,
|
||||
"class_id": 7,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100713020,
|
||||
100713020,
|
||||
100713020,
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100713010,
|
||||
100713010,
|
||||
100713010,
|
||||
100711010,
|
||||
100711010,
|
||||
100711010,
|
||||
100714010,
|
||||
100714010,
|
||||
100714010,
|
||||
100714020,
|
||||
100714020,
|
||||
100714020,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100713030,
|
||||
100713030,
|
||||
100713030,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100723010,
|
||||
100723010,
|
||||
100723010,
|
||||
100714030,
|
||||
100714030,
|
||||
100714030,
|
||||
100711020,
|
||||
100711020,
|
||||
100711020
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
},
|
||||
"98": {
|
||||
"null": 1,
|
||||
"deck_no": 98,
|
||||
"class_id": 8,
|
||||
"sleeve_id": 3000011,
|
||||
"leader_skin_id": 0,
|
||||
"deck_name": "Default",
|
||||
"card_id_array": [
|
||||
100011020,
|
||||
100011020,
|
||||
100011020,
|
||||
100012010,
|
||||
100811020,
|
||||
100811020,
|
||||
100811020,
|
||||
100811060,
|
||||
100811060,
|
||||
100811060,
|
||||
100811070,
|
||||
100811070,
|
||||
100811070,
|
||||
100814010,
|
||||
100814010,
|
||||
100814010,
|
||||
100011030,
|
||||
100011030,
|
||||
100011030,
|
||||
100811010,
|
||||
100811010,
|
||||
100811010,
|
||||
100811030,
|
||||
100811030,
|
||||
100811030,
|
||||
100011040,
|
||||
100011040,
|
||||
100011040,
|
||||
100811040,
|
||||
100811040,
|
||||
100811040,
|
||||
100824010,
|
||||
100824010,
|
||||
100824010,
|
||||
100011050,
|
||||
100011050,
|
||||
100011050,
|
||||
100811050,
|
||||
100811050,
|
||||
100811050
|
||||
],
|
||||
"is_complete_deck": 1,
|
||||
"is_available_deck": 1,
|
||||
"maintenance_card_ids": []
|
||||
}
|
||||
},
|
||||
"user_leader_skin_setting_list": {
|
||||
"1": {
|
||||
"class_id": 1,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 1
|
||||
},
|
||||
"2": {
|
||||
"class_id": 2,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 2
|
||||
},
|
||||
"3": {
|
||||
"class_id": 3,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 3
|
||||
},
|
||||
"4": {
|
||||
"class_id": 4,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 104
|
||||
},
|
||||
"5": {
|
||||
"class_id": 5,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 5
|
||||
},
|
||||
"6": {
|
||||
"class_id": 6,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 106
|
||||
},
|
||||
"7": {
|
||||
"class_id": 7,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 7
|
||||
},
|
||||
"8": {
|
||||
"class_id": 8,
|
||||
"is_random_leader_skin": 0,
|
||||
"leader_skin_id": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14552
SVSim.Bootstrap/Data/prod-captures/load-index-2026-05-23.json
Normal file
14552
SVSim.Bootstrap/Data/prod-captures/load-index-2026-05-23.json
Normal file
File diff suppressed because it is too large
Load Diff
229
SVSim.Bootstrap/Data/prod-captures/mypage-index-2026-05-23.json
Normal file
229
SVSim.Bootstrap/Data/prod-captures/mypage-index-2026-05-23.json
Normal file
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"data_headers": {
|
||||
"short_udid": 411054851,
|
||||
"viewer_id": 906243102,
|
||||
"sid": "",
|
||||
"servertime": 1779553959,
|
||||
"result_code": 1
|
||||
},
|
||||
"data": {
|
||||
"user_info": {
|
||||
"device_type": "2",
|
||||
"name": "combusty7",
|
||||
"country_code": "KOR",
|
||||
"max_friend": "20",
|
||||
"last_play_time": "2026-05-23 16:32:39",
|
||||
"is_received_two_pick_mission": "1",
|
||||
"birth": "19600101",
|
||||
"selected_emblem_id": "701441011",
|
||||
"selected_degree_id": "300003",
|
||||
"mission_change_time": "2017-09-17 14:47:13",
|
||||
"mission_receive_type": "0",
|
||||
"is_official": "0",
|
||||
"is_official_mark_displayed": "0"
|
||||
},
|
||||
"sealed_info": {
|
||||
"enable": 1,
|
||||
"crystal_cost": 600,
|
||||
"rupy_cost": 600,
|
||||
"ticket_cost": 4,
|
||||
"is_join": false,
|
||||
"pack_info": [
|
||||
10032,
|
||||
10032,
|
||||
10031,
|
||||
10030,
|
||||
10029
|
||||
],
|
||||
"deck_using_num_min": 30,
|
||||
"schedule_id": 21,
|
||||
"is_deck_code_maintenance": false,
|
||||
"sales_period_info": {
|
||||
"sales_period_series": 33
|
||||
}
|
||||
},
|
||||
"colosseum_info": {
|
||||
"colosseum_id": "165",
|
||||
"is_display_tips": "0",
|
||||
"tips_id": "0",
|
||||
"card_pool_name": "Take Two (Dragonblade–Rivenbrandt)",
|
||||
"is_colosseum_period": true,
|
||||
"is_round_period": true,
|
||||
"deck_format": "3",
|
||||
"is_normal_two_pick": "1",
|
||||
"is_special_mode": "10",
|
||||
"is_all_card_enabled": 0,
|
||||
"start_time": "2026-05-21 06:00:00",
|
||||
"colosseum_name": "Rivenbrandt Take Two Cup",
|
||||
"now_round": "1",
|
||||
"end_time": "2026-05-25 19:59:59",
|
||||
"sales_period_info": {
|
||||
"sales_period_time": "2026-05-25 19:59:59"
|
||||
}
|
||||
},
|
||||
"is_available_colosseum_free_entry": true,
|
||||
"arena_info": [
|
||||
{
|
||||
"mode": 1,
|
||||
"enable": 1,
|
||||
"cost": 150,
|
||||
"rupy_cost": 150,
|
||||
"ticket_cost": 1,
|
||||
"is_join": false,
|
||||
"format_info": {
|
||||
"two_pick_type": "1",
|
||||
"card_pool_name": "Take Two (Dragonblade–Rivenbrandt)",
|
||||
"announce_id": 0,
|
||||
"last_card_pack_set_id": "10029",
|
||||
"start_time": "2026-05-01 02:00:00",
|
||||
"end_time": "2026-06-01 01:59:59"
|
||||
},
|
||||
"sales_period_info": {
|
||||
"sales_period_time": "2026-06-01 01:59:59"
|
||||
}
|
||||
}
|
||||
],
|
||||
"is_arena_challenge_period": true,
|
||||
"is_hidden_boss_appeared": false,
|
||||
"competition_info": {
|
||||
"is_competition_period": false
|
||||
},
|
||||
"treasure_info": null,
|
||||
"unread_present_count": 0,
|
||||
"unreceived_mission_reward_count": 0,
|
||||
"lottery_period_info": null,
|
||||
"master_point_ranking_period": {
|
||||
"id": "119",
|
||||
"period_num": "118",
|
||||
"necessary_score": "0",
|
||||
"begin_time": "2026-05-01 02:00:00",
|
||||
"end_time": "2026-06-01 01:59:59"
|
||||
},
|
||||
"last_announce_id": "3353",
|
||||
"last_announce_update_time": "2026-05-15 10:22:11",
|
||||
"unfinished_battle_exists": false,
|
||||
"is_joined_room": false,
|
||||
"receive_friend_apply_count": 0,
|
||||
"feature_maintenance_list": [],
|
||||
"can_give_daily_login_bonus": false,
|
||||
"friend_battle_invite_count": 0,
|
||||
"user_config": {
|
||||
"receive_invitation": "1",
|
||||
"receive_invitation_in_battle": "1",
|
||||
"receive_invitation_in_offline": "1",
|
||||
"receive_friend_apply": "1",
|
||||
"is_allow_send_adjust": "1",
|
||||
"is_foil_preferred": "0",
|
||||
"is_prize_preferred": "0"
|
||||
},
|
||||
"banner": [
|
||||
{
|
||||
"image_name": "banner_000788",
|
||||
"click": "account_transition_with_two",
|
||||
"status": "10",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"image_name": "banner_000906",
|
||||
"click": "colosseum",
|
||||
"status": "",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"image_name": "banner_000220",
|
||||
"click": "deck_intro_rotation",
|
||||
"status": "17",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
},
|
||||
{
|
||||
"image_name": "banner_000840",
|
||||
"click": "mission",
|
||||
"status": "2",
|
||||
"change_time": "10",
|
||||
"remaining_time": "0",
|
||||
"image_paths": []
|
||||
}
|
||||
],
|
||||
"sub_banner": null,
|
||||
"sub_banner_list": [],
|
||||
"user_mypage_info": {
|
||||
"user_mypage_setting": {
|
||||
"mypage_id": "0",
|
||||
"select_type": "0",
|
||||
"mypage_id_list": []
|
||||
}
|
||||
},
|
||||
"user_offline_event": [],
|
||||
"convention": {
|
||||
"is_join_tournament": false,
|
||||
"recent_start_date": null,
|
||||
"is_admin_watch_user": false
|
||||
},
|
||||
"special_crystal_info": [],
|
||||
"room_type_in_session": {
|
||||
"special_deck_format_list": [
|
||||
{
|
||||
"deck_format": "5",
|
||||
"end_time": "2030-06-26 19:59:59"
|
||||
}
|
||||
]
|
||||
},
|
||||
"guild_notification": {
|
||||
"guild_id": null,
|
||||
"guild_room_message_id": null,
|
||||
"is_join_request": false,
|
||||
"is_invited": false
|
||||
},
|
||||
"shop_notification": {
|
||||
"card_pack": {
|
||||
"is_open_free_gacha_campaign": false,
|
||||
"can_free_gacha": false
|
||||
},
|
||||
"build_deck": [],
|
||||
"sleeve": [],
|
||||
"leader_skin": []
|
||||
},
|
||||
"pre_release_status": 0,
|
||||
"gathering_info": {
|
||||
"has_invite": 0,
|
||||
"is_entry": 0
|
||||
},
|
||||
"quest": {
|
||||
"is_open": false,
|
||||
"is_display_badge": false,
|
||||
"is_daily_first_access": false,
|
||||
"end_time": "",
|
||||
"name": ""
|
||||
},
|
||||
"basic_puzzle": {
|
||||
"is_display_badge": true
|
||||
},
|
||||
"all_card_enabled_period": null,
|
||||
"user_item_list": [
|
||||
{
|
||||
"item_id": "1",
|
||||
"number": "19"
|
||||
},
|
||||
{
|
||||
"item_id": "10011",
|
||||
"number": "1"
|
||||
},
|
||||
{
|
||||
"item_id": "80001",
|
||||
"number": "1"
|
||||
}
|
||||
],
|
||||
"is_battle_pass_period": true,
|
||||
"story_notification": {
|
||||
"is_display_ribbon": false,
|
||||
"is_display_badge": false
|
||||
},
|
||||
"home_dialog_list": []
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,52 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
|
||||
namespace SVSim.CardImport;
|
||||
namespace SVSim.Bootstrap.Importers;
|
||||
|
||||
public static class Program
|
||||
/// <summary>
|
||||
/// Reads the loader's card dump (LitJson array of CardCSVData) and upserts ShadowverseCardEntry +
|
||||
/// ShadowverseCardSetEntry rows. Lifted unchanged from the original SVSim.CardImport.Program.Main —
|
||||
/// only the orchestration was moved into <see cref="Program"/>.
|
||||
/// </summary>
|
||||
public class CardImporter
|
||||
{
|
||||
private const string DefaultConnectionString =
|
||||
"Host=localhost;Database=svsim;Username=postgres;password=postgres";
|
||||
|
||||
public static async Task<int> Main(string[] args)
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
if (args.Length < 1 || args[0] is "--help" or "-h")
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
public async Task<int> ImportAsync(SVSimDbContext context, string cardsJsonPath)
|
||||
{
|
||||
if (!File.Exists(cardsJsonPath))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"Usage: svsim-card-import <cards.json> [connection-string]\n" +
|
||||
"\n" +
|
||||
" cards.json Path to the loader's card dump (LitJson array of CardCSVData)\n" +
|
||||
" connection-string Postgres connection (falls back to NPGSQL_CONNECTION env var,\n" +
|
||||
$" then \"{DefaultConnectionString}\")");
|
||||
return 1;
|
||||
Console.Error.WriteLine($"[CardImporter] cards.json not found at {cardsJsonPath}; skipping card import.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
string path = args[0];
|
||||
string connection = args.Length > 1
|
||||
? args[1]
|
||||
: Environment.GetEnvironmentVariable("NPGSQL_CONNECTION") ?? DefaultConnectionString;
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.Error.WriteLine($"File not found: {path}");
|
||||
return 2;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Reading {path} ({new FileInfo(path).Length / 1024} KiB)...");
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
Console.WriteLine($"[CardImporter] Reading {cardsJsonPath} ({new FileInfo(cardsJsonPath).Length / 1024} KiB)...");
|
||||
|
||||
List<CardInput>? input;
|
||||
await using (var fs = File.OpenRead(path))
|
||||
await using (var fs = File.OpenRead(cardsJsonPath))
|
||||
{
|
||||
input = await JsonSerializer.DeserializeAsync<List<CardInput>>(fs, jsonOptions);
|
||||
input = await JsonSerializer.DeserializeAsync<List<CardInput>>(fs, JsonOptions);
|
||||
}
|
||||
if (input is null || input.Count == 0)
|
||||
{
|
||||
Console.Error.WriteLine("No card records parsed from input.");
|
||||
return 3;
|
||||
Console.Error.WriteLine("[CardImporter] No card records parsed from input.");
|
||||
return 0;
|
||||
}
|
||||
Console.WriteLine($"Parsed {input.Count} card records.");
|
||||
|
||||
var dbOptions = new DbContextOptionsBuilder<SVSimDbContext>()
|
||||
.UseNpgsql(connection)
|
||||
.Options;
|
||||
|
||||
await using var context = new SVSimDbContext(NullLogger<SVSimDbContext>.Instance, dbOptions);
|
||||
|
||||
// Apply any pending migrations first — bootstraps a fresh DB so CardImport can be the
|
||||
// very first thing run after `dotnet ef migrations add` (no need to run the server too).
|
||||
// Migration files have InsertData rows for the seeded master data already; runtime seeder
|
||||
// skip is fine.
|
||||
await context.Database.MigrateAsync();
|
||||
Console.WriteLine($"[CardImporter] Parsed {input.Count} card records.");
|
||||
|
||||
var classesById = await context.Classes.ToDictionaryAsync(c => c.Id);
|
||||
var existingSets = (await context.CardSets.ToListAsync()).ToDictionary(s => s.Id);
|
||||
var existingCards = (await context.Cards.ToListAsync()).ToDictionary(c => c.Id);
|
||||
Console.WriteLine(
|
||||
$"DB state before: {existingCards.Count} cards, {existingSets.Count} card sets, " +
|
||||
$"[CardImporter] DB state before: {existingCards.Count} cards, {existingSets.Count} card sets, " +
|
||||
$"{classesById.Count} classes seeded.");
|
||||
|
||||
int created = 0, updated = 0, skipped = 0, setsCreated = 0;
|
||||
@@ -140,13 +114,12 @@ public static class Program
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"Saving: +{created} cards, ~{updated} updated, +{setsCreated} card sets, " +
|
||||
$"[CardImporter] Saving: +{created} cards, ~{updated} updated, +{setsCreated} card sets, " +
|
||||
$"skipped {skipped} (bad/missing card_id)...");
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
Console.WriteLine("Done.");
|
||||
return 0;
|
||||
Console.WriteLine("[CardImporter] Done.");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
private static int ParseInt(string? raw, int fallback) =>
|
||||
620
SVSim.Bootstrap/Importers/GlobalsImporter.cs
Normal file
620
SVSim.Bootstrap/Importers/GlobalsImporter.cs
Normal file
@@ -0,0 +1,620 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Models;
|
||||
using static SVSim.Bootstrap.Importers.ImporterBase;
|
||||
|
||||
namespace SVSim.Bootstrap.Importers;
|
||||
|
||||
/// <summary>
|
||||
/// Imports prod-captured globals from <c>{capturesDir}/{endpoint}-*.json</c> snapshots into the
|
||||
/// DB via idempotent upserts. Source endpoints: <c>load-index</c>, <c>mypage-index</c>, <c>deck-info</c>.
|
||||
///
|
||||
/// Topological order: GameConfiguration extensions → standalone tables → card-referencing tables →
|
||||
/// rotation CardSet flag update. Card-referencing importers warn on orphans (missing card rows)
|
||||
/// but never fail — CardImporter must have run first for clean output.
|
||||
///
|
||||
/// Re-runnable on the same capture (no-op delta) and on updated captures (creates/updates only).
|
||||
/// Does NOT delete rows missing from the latest capture — that would risk data loss if a capture
|
||||
/// file is partial. Use a fresh DB for snapshot-only state.
|
||||
/// </summary>
|
||||
public class GlobalsImporter
|
||||
{
|
||||
public async Task<int> ImportAllAsync(SVSimDbContext context, string capturesDir)
|
||||
{
|
||||
Console.WriteLine($"[GlobalsImporter] Loading captures from {capturesDir}...");
|
||||
|
||||
JsonElement? loadIndex = LoadCapture(capturesDir, "load-index");
|
||||
JsonElement? mypageIndex = LoadCapture(capturesDir, "mypage-index");
|
||||
JsonElement? deckInfo = LoadCapture(capturesDir, "deck-info");
|
||||
|
||||
int total = 0;
|
||||
|
||||
if (loadIndex.HasValue)
|
||||
{
|
||||
total += await ImportGameConfigurationExtensions(context, loadIndex.Value);
|
||||
total += await ImportMyRotation(context, loadIndex.Value);
|
||||
total += await ImportAvatarAbilities(context, loadIndex.Value);
|
||||
total += await ImportArenaSeason(context, loadIndex.Value);
|
||||
total += await ImportBattlePassLevels(context, loadIndex.Value);
|
||||
total += await ImportDailyLoginBonus(context, loadIndex.Value);
|
||||
total += await ImportPreReleaseInfo(context, loadIndex.Value);
|
||||
total += await ImportSpotCards(context, loadIndex.Value);
|
||||
total += await ImportReprintedCards(context, loadIndex.Value);
|
||||
total += await ImportUnlimitedRestrictions(context, loadIndex.Value);
|
||||
total += await ImportLoadingExclusionCards(context, loadIndex.Value);
|
||||
total += await ImportMaintenanceCards(context, loadIndex.Value);
|
||||
total += await ImportFeatureMaintenances(context, loadIndex.Value);
|
||||
total += await UpdateRotationCardSetFlags(context, loadIndex.Value);
|
||||
}
|
||||
|
||||
if (mypageIndex.HasValue)
|
||||
{
|
||||
total += await ImportBanners(context, mypageIndex.Value);
|
||||
total += await ImportColosseum(context, mypageIndex.Value);
|
||||
total += await ImportSealed(context, mypageIndex.Value);
|
||||
total += await ImportMasterPointRankingPeriod(context, mypageIndex.Value);
|
||||
}
|
||||
|
||||
if (deckInfo.HasValue)
|
||||
{
|
||||
total += await ImportDefaultDecks(context, deckInfo.Value);
|
||||
total += await ImportDefaultLeaderSkinSettings(context, deckInfo.Value);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
Console.WriteLine($"[GlobalsImporter] Done: {total} total rows changed.");
|
||||
return total;
|
||||
}
|
||||
|
||||
// ---------- GameConfiguration ----------
|
||||
|
||||
private async Task<int> ImportGameConfigurationExtensions(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
var cfg = await context.GameConfigurations.FirstOrDefaultAsync(g => g.Id == "default");
|
||||
if (cfg is null)
|
||||
{
|
||||
Console.Error.WriteLine("[GlobalsImporter] GameConfigurations 'default' row missing; " +
|
||||
"DefaultSettingsSeeder should have created it. Skipping extensions.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
cfg.TsRotationId = GetString(loadIndex, "ts_rotation_id");
|
||||
cfg.IsBattlePassPeriod = GetBool(loadIndex, "is_battle_pass_period");
|
||||
cfg.IsBeginnerMission = GetBool(loadIndex, "is_beginner_mission");
|
||||
cfg.CardSetIdForResourceDlView = GetInt(loadIndex, "card_set_id_for_resource_dl_view");
|
||||
|
||||
if (loadIndex.TryGetProperty("challenge_config", out var cc))
|
||||
{
|
||||
cfg.ChallengeUseTwoPickPremiumCard = GetBool(cc, "use_challenge_two_pick_premium_card");
|
||||
cfg.ChallengeTwoPickSleeveId = GetLong(cc, "challenge_two_pick_sleeve_id");
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GlobalsImporter] GameConfiguration extensions: ts_rotation_id={cfg.TsRotationId}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- My Rotation ----------
|
||||
|
||||
private async Task<int> ImportMyRotation(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("my_rotation_info", out var info)) return 0;
|
||||
|
||||
// Settings — join setting + reprinted + restricted dicts on rotation_id.
|
||||
var settingsDict = info.TryGetProperty("setting", out var s) ? s : default;
|
||||
var reprintedDict = info.TryGetProperty("reprinted_base_card_ids", out var r) ? r : default;
|
||||
var restrictedDict = info.TryGetProperty("restricted_base_card_id_list", out var rs) ? rs : default;
|
||||
|
||||
var existingSettings = await context.MyRotationSettings.ToDictionaryAsync(e => e.Id);
|
||||
int setCreated = 0, setUpdated = 0;
|
||||
if (settingsDict.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var kv in settingsDict.EnumerateObject())
|
||||
{
|
||||
if (!int.TryParse(kv.Name, out int rid)) continue;
|
||||
var entry = existingSettings.TryGetValue(rid, out var ex) ? ex : new MyRotationSettingEntry { Id = rid };
|
||||
entry.CardSetIdsCsv = GetString(kv.Value, "card_set_ids");
|
||||
entry.AbilitiesCsv = GetString(kv.Value, "abilities");
|
||||
entry.ReprintedCardIds = reprintedDict.ValueKind == JsonValueKind.Object && reprintedDict.TryGetProperty(kv.Name, out var rep)
|
||||
? Serialize(rep) : "[]";
|
||||
entry.RestrictedCardIds = restrictedDict.ValueKind == JsonValueKind.Object && restrictedDict.TryGetProperty(kv.Name, out var res)
|
||||
? Serialize(res) : "[]";
|
||||
if (ex is null) { context.MyRotationSettings.Add(entry); setCreated++; }
|
||||
else setUpdated++;
|
||||
}
|
||||
}
|
||||
|
||||
// Abilities
|
||||
int abilityCreated = 0, abilityUpdated = 0;
|
||||
if (info.TryGetProperty("abilities", out var abilities) && abilities.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var existingAbilities = await context.MyRotationAbilities.ToDictionaryAsync(e => e.Id);
|
||||
foreach (var kv in abilities.EnumerateObject())
|
||||
{
|
||||
if (!int.TryParse(kv.Name, out int aid)) continue;
|
||||
var entry = existingAbilities.TryGetValue(aid, out var ex) ? ex : new MyRotationAbilityEntry { Id = aid };
|
||||
entry.Data = Serialize(kv.Value);
|
||||
if (ex is null) { context.MyRotationAbilities.Add(entry); abilityCreated++; }
|
||||
else abilityUpdated++;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GlobalsImporter] MyRotation: settings +{setCreated}/~{setUpdated}, abilities +{abilityCreated}/~{abilityUpdated}");
|
||||
return setCreated + setUpdated + abilityCreated + abilityUpdated;
|
||||
}
|
||||
|
||||
// ---------- Avatar Abilities ----------
|
||||
|
||||
private async Task<int> ImportAvatarAbilities(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("avatar_info", out var info)) return 0;
|
||||
if (!info.TryGetProperty("abilities", out var abilities) || abilities.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.AvatarAbilities.ToDictionaryAsync(e => e.Id);
|
||||
int created = 0, updated = 0;
|
||||
|
||||
foreach (var kv in abilities.EnumerateObject())
|
||||
{
|
||||
if (!int.TryParse(kv.Name, out int leaderSkinId)) continue;
|
||||
var v = kv.Value;
|
||||
var entry = existing.TryGetValue(leaderSkinId, out var ex) ? ex : new AvatarAbilityEntry { Id = leaderSkinId };
|
||||
entry.BattleStartFirstPlayerTurnBp = GetInt(v, "battle_start_firstplayerturn_bp");
|
||||
entry.BattleStartSecondPlayerTurnBp = GetInt(v, "battle_start_secondplayerturn_bp");
|
||||
entry.BattleStartMaxLife = GetInt(v, "battle_start_max_life");
|
||||
entry.AbilityCost = GetString(v, "ability_cost");
|
||||
entry.Ability = GetString(v, "ability");
|
||||
entry.PassiveAbility = GetString(v, "passive_ability");
|
||||
entry.AbilityDesc = GetString(v, "ability_desc");
|
||||
entry.PassiveAbilityDesc = GetString(v, "passive_ability_desc");
|
||||
if (ex is null) { context.AvatarAbilities.Add(entry); created++; }
|
||||
else updated++;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GlobalsImporter] AvatarAbilities: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
// ---------- Arena Season (singleton) ----------
|
||||
|
||||
private async Task<int> ImportArenaSeason(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("arena_info", out var arr) || arr.ValueKind != JsonValueKind.Array || arr.GetArrayLength() == 0) return 0;
|
||||
var first = arr[0];
|
||||
|
||||
var existing = await context.ArenaSeasons.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new ArenaSeasonConfig { Id = 1 };
|
||||
entry.Mode = GetInt(first, "mode");
|
||||
entry.Enable = GetInt(first, "enable");
|
||||
entry.Cost = GetULong(first, "cost");
|
||||
entry.RupyCost = GetULong(first, "rupy_cost");
|
||||
entry.TicketCost = GetInt(first, "ticket_cost");
|
||||
entry.IsJoin = GetBool(first, "is_join");
|
||||
entry.FormatInfo = first.TryGetProperty("format_info", out var fi) ? Serialize(fi) : "{}";
|
||||
if (existing is null) context.ArenaSeasons.Add(entry);
|
||||
|
||||
Console.WriteLine($"[GlobalsImporter] ArenaSeason: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Battle Pass Levels ----------
|
||||
|
||||
private async Task<int> ImportBattlePassLevels(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("battle_pass_level_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.BattlePassLevels.ToDictionaryAsync(e => e.Id);
|
||||
int created = 0, updated = 0;
|
||||
foreach (var kv in info.EnumerateObject())
|
||||
{
|
||||
if (!int.TryParse(kv.Name, out int level)) continue;
|
||||
var entry = existing.TryGetValue(level, out var ex) ? ex : new BattlePassLevelEntry { Id = level };
|
||||
entry.RewardData = Serialize(kv.Value);
|
||||
if (ex is null) { context.BattlePassLevels.Add(entry); created++; }
|
||||
else updated++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] BattlePassLevels: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
// ---------- Daily Login Bonus ----------
|
||||
|
||||
private async Task<int> ImportDailyLoginBonus(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("daily_login_bonus", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.DailyLoginBonuses.ToDictionaryAsync(e => e.Id);
|
||||
int created = 0, updated = 0;
|
||||
foreach (var kv in info.EnumerateObject())
|
||||
{
|
||||
if (!int.TryParse(kv.Name, out int bonusId)) continue;
|
||||
var entry = existing.TryGetValue(bonusId, out var ex) ? ex : new DailyLoginBonusEntry { Id = bonusId };
|
||||
entry.BonusData = Serialize(kv.Value);
|
||||
if (ex is null) { context.DailyLoginBonuses.Add(entry); created++; }
|
||||
else updated++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] DailyLoginBonus: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
// ---------- Pre-release Info (singleton) ----------
|
||||
|
||||
private async Task<int> ImportPreReleaseInfo(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("pre_release_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.PreReleaseInfos.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new PreReleaseInfo { Id = 1 };
|
||||
entry.PreReleaseId = GetString(info, "id");
|
||||
entry.NextCardSetId = GetString(info, "next_card_set_id");
|
||||
entry.StartTime = ParseWireDateTime(GetString(info, "start_time"));
|
||||
entry.EndTime = ParseWireDateTime(GetString(info, "end_time"));
|
||||
entry.DisplayEndTime = ParseWireDateTime(GetString(info, "display_end_time"));
|
||||
entry.FreeMatchStartTime = ParseWireDateTime(GetString(info, "free_match_start_time"));
|
||||
entry.CardMasterId = GetInt(info, "card_master_id");
|
||||
entry.DefaultCardMasterId = GetString(info, "default_card_master_id");
|
||||
entry.PreReleaseCardMasterId = GetString(info, "pre_release_card_master_id");
|
||||
entry.IsPreRotationFreeMatchTerm = GetBool(info, "is_pre_rotation_free_match_term");
|
||||
entry.RotationCardSetIdList = info.TryGetProperty("rotation_card_set_id_list", out var rcs) ? Serialize(rcs) : "[]";
|
||||
entry.ReprintedBaseCardIds = info.TryGetProperty("reprinted_base_card_ids", out var rep) ? Serialize(rep) : "{}";
|
||||
entry.LatestReprintedBaseCardIds = info.TryGetProperty("latest_reprinted_base_card_ids", out var lrep) ? Serialize(lrep) : "{}";
|
||||
if (existing is null) context.PreReleaseInfos.Add(entry);
|
||||
Console.WriteLine($"[GlobalsImporter] PreReleaseInfo: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Spot Cards (card-referencing) ----------
|
||||
|
||||
private async Task<int> ImportSpotCards(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("spot_cards", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.SpotCards.ToDictionaryAsync(e => e.Id);
|
||||
var knownCards = await context.Cards.Select(c => c.Id).ToListAsync();
|
||||
var knownSet = new HashSet<long>(knownCards);
|
||||
int created = 0, updated = 0, orphans = 0;
|
||||
|
||||
foreach (var kv in info.EnumerateObject())
|
||||
{
|
||||
if (!long.TryParse(kv.Name, out long cardId)) continue;
|
||||
if (!knownSet.Contains(cardId)) orphans++;
|
||||
int cost = kv.Value.ValueKind == JsonValueKind.Number ? kv.Value.GetInt32() : GetInt(kv.Value, "cost");
|
||||
var entry = existing.TryGetValue(cardId, out var ex) ? ex : new SpotCardEntry { Id = cardId };
|
||||
entry.Cost = cost;
|
||||
if (ex is null) { context.SpotCards.Add(entry); created++; }
|
||||
else updated++;
|
||||
}
|
||||
WarnOrphans("SpotCards", orphans);
|
||||
Console.WriteLine($"[GlobalsImporter] SpotCards: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
// ---------- Reprinted Cards ----------
|
||||
|
||||
private async Task<int> ImportReprintedCards(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("reprinted_base_card_ids", out var info)) return 0;
|
||||
|
||||
var existing = await context.ReprintedCards.ToDictionaryAsync(e => e.Id);
|
||||
var knownSet = new HashSet<long>(await context.Cards.Select(c => c.Id).ToListAsync());
|
||||
int created = 0, orphans = 0;
|
||||
IEnumerable<long> ids;
|
||||
if (info.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
ids = info.EnumerateObject().Select(kv => long.TryParse(kv.Name, out var n) ? n : 0L).Where(n => n != 0);
|
||||
}
|
||||
else if (info.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
ids = info.EnumerateArray().Select(e => e.ValueKind == JsonValueKind.Number ? e.GetInt64() : (long.TryParse(e.GetString(), out var n) ? n : 0L)).Where(n => n != 0);
|
||||
}
|
||||
else return 0;
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
if (!knownSet.Contains(id)) orphans++;
|
||||
if (existing.ContainsKey(id)) continue;
|
||||
context.ReprintedCards.Add(new ReprintedCardEntry { Id = id });
|
||||
existing[id] = null!;
|
||||
created++;
|
||||
}
|
||||
WarnOrphans("ReprintedCards", orphans);
|
||||
Console.WriteLine($"[GlobalsImporter] ReprintedCards: +{created}");
|
||||
return created;
|
||||
}
|
||||
|
||||
// ---------- Unlimited Restrictions ----------
|
||||
|
||||
private async Task<int> ImportUnlimitedRestrictions(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("unlimited_restricted_base_card_id_list", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.UnlimitedRestrictions.ToDictionaryAsync(e => e.Id);
|
||||
var knownSet = new HashSet<long>(await context.Cards.Select(c => c.Id).ToListAsync());
|
||||
int created = 0, updated = 0, orphans = 0;
|
||||
|
||||
foreach (var kv in info.EnumerateObject())
|
||||
{
|
||||
if (!long.TryParse(kv.Name, out long cardId)) continue;
|
||||
if (!knownSet.Contains(cardId)) orphans++;
|
||||
int val = kv.Value.ValueKind == JsonValueKind.Number ? kv.Value.GetInt32()
|
||||
: (int.TryParse(kv.Value.GetString(), out var n) ? n : 0);
|
||||
var entry = existing.TryGetValue(cardId, out var ex) ? ex : new UnlimitedRestrictionEntry { Id = cardId };
|
||||
entry.RestrictionValue = val;
|
||||
if (ex is null) { context.UnlimitedRestrictions.Add(entry); created++; }
|
||||
else updated++;
|
||||
}
|
||||
WarnOrphans("UnlimitedRestrictions", orphans);
|
||||
Console.WriteLine($"[GlobalsImporter] UnlimitedRestrictions: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
// ---------- Loading Exclusion Cards ----------
|
||||
|
||||
private async Task<int> ImportLoadingExclusionCards(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("loading_exclusion_card_list", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
|
||||
|
||||
var existing = await context.LoadingExclusionCards.ToDictionaryAsync(e => e.Id);
|
||||
var knownSet = new HashSet<long>(await context.Cards.Select(c => c.Id).ToListAsync());
|
||||
int created = 0, orphans = 0;
|
||||
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
long id = el.ValueKind == JsonValueKind.Number ? el.GetInt64() : (long.TryParse(el.GetString(), out var n) ? n : 0);
|
||||
if (id == 0) continue;
|
||||
if (!knownSet.Contains(id)) orphans++;
|
||||
if (existing.ContainsKey(id)) continue;
|
||||
context.LoadingExclusionCards.Add(new LoadingExclusionCardEntry { Id = id });
|
||||
existing[id] = null!;
|
||||
created++;
|
||||
}
|
||||
WarnOrphans("LoadingExclusionCards", orphans);
|
||||
Console.WriteLine($"[GlobalsImporter] LoadingExclusionCards: +{created}");
|
||||
return created;
|
||||
}
|
||||
|
||||
// ---------- Maintenance Cards (skeleton-seedable) ----------
|
||||
|
||||
private async Task<int> ImportMaintenanceCards(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("maintenance_card_list", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
|
||||
if (arr.GetArrayLength() == 0) return 0;
|
||||
|
||||
var existing = await context.MaintenanceCards.ToDictionaryAsync(e => e.Id);
|
||||
int created = 0;
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
long id = el.ValueKind == JsonValueKind.Number ? el.GetInt64() : (long.TryParse(el.GetString(), out var n) ? n : 0);
|
||||
if (id == 0 || existing.ContainsKey(id)) continue;
|
||||
context.MaintenanceCards.Add(new MaintenanceCardEntry { Id = id });
|
||||
existing[id] = null!;
|
||||
created++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] MaintenanceCards: +{created}");
|
||||
return created;
|
||||
}
|
||||
|
||||
// ---------- Feature Maintenances (skeleton-seedable) ----------
|
||||
|
||||
private async Task<int> ImportFeatureMaintenances(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("feature_maintenance_list", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
|
||||
if (arr.GetArrayLength() == 0) return 0;
|
||||
|
||||
// Schema uses synthetic int Id; preserve raw blob per index.
|
||||
int created = 0;
|
||||
int idx = 1;
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
context.FeatureMaintenances.Add(new FeatureMaintenanceEntry
|
||||
{
|
||||
Id = idx++,
|
||||
FeatureKey = GetString(el, "feature_key"),
|
||||
Data = Serialize(el)
|
||||
});
|
||||
created++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] FeatureMaintenances: +{created}");
|
||||
return created;
|
||||
}
|
||||
|
||||
// ---------- Rotation CardSet flag update ----------
|
||||
|
||||
private async Task<int> UpdateRotationCardSetFlags(SVSimDbContext context, JsonElement loadIndex)
|
||||
{
|
||||
if (!loadIndex.TryGetProperty("rotation_card_set_id_list", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
|
||||
|
||||
var rotationIds = arr.EnumerateArray()
|
||||
.Select(e => e.TryGetProperty("card_set_id", out var v) && v.ValueKind == JsonValueKind.Number ? v.GetInt32() : 0)
|
||||
.Where(n => n != 0)
|
||||
.ToHashSet();
|
||||
|
||||
if (rotationIds.Count == 0) return 0;
|
||||
|
||||
var allSets = await context.CardSets.ToListAsync();
|
||||
int updated = 0, missing = 0;
|
||||
foreach (var rid in rotationIds)
|
||||
{
|
||||
var set = allSets.FirstOrDefault(s => s.Id == rid);
|
||||
if (set is null) { missing++; continue; }
|
||||
if (!set.IsInRotation) { set.IsInRotation = true; updated++; }
|
||||
}
|
||||
// Demote sets not in the current rotation
|
||||
foreach (var s in allSets.Where(s => s.IsInRotation && !rotationIds.Contains(s.Id)))
|
||||
{
|
||||
s.IsInRotation = false;
|
||||
updated++;
|
||||
}
|
||||
if (missing > 0) Console.Error.WriteLine($"[GlobalsImporter] Warning: {missing} rotation card_set_id(s) missing from CardSets — run CardImporter first.");
|
||||
Console.WriteLine($"[GlobalsImporter] RotationCardSets: ~{updated} flag changes");
|
||||
return updated;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Banners ----------
|
||||
|
||||
private async Task<int> ImportBanners(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("banner", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
|
||||
|
||||
// Banners have no wire ID; we treat the capture as authoritative — clear and rewrite.
|
||||
var existing = await context.Banners.ToListAsync();
|
||||
context.Banners.RemoveRange(existing);
|
||||
|
||||
int created = 0;
|
||||
int idx = 1;
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
context.Banners.Add(new BannerEntry
|
||||
{
|
||||
Id = idx++,
|
||||
ImageName = GetString(el, "image_name"),
|
||||
Click = GetString(el, "click"),
|
||||
Status = GetString(el, "status"),
|
||||
ChangeTime = GetInt(el, "change_time"),
|
||||
RemainingTime = GetInt(el, "remaining_time"),
|
||||
ImagePaths = el.TryGetProperty("image_paths", out var ip) ? Serialize(ip) : "[]"
|
||||
});
|
||||
created++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] Banners: {(existing.Count > 0 ? $"-{existing.Count}/" : "")}+{created}");
|
||||
return created;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Colosseum (singleton) ----------
|
||||
|
||||
private async Task<int> ImportColosseum(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("colosseum_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.Colosseums.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new ColosseumConfig { Id = 1 };
|
||||
entry.ColosseumId = GetString(info, "colosseum_id");
|
||||
entry.ColosseumName = GetString(info, "colosseum_name");
|
||||
entry.CardPoolName = GetString(info, "card_pool_name");
|
||||
entry.DeckFormat = GetString(info, "deck_format");
|
||||
entry.StartTime = ParseWireDateTime(GetString(info, "start_time"));
|
||||
entry.EndTime = ParseWireDateTime(GetString(info, "end_time"));
|
||||
entry.NowRound = GetString(info, "now_round");
|
||||
entry.IsDisplayTips = GetString(info, "is_display_tips");
|
||||
entry.TipsId = GetString(info, "tips_id");
|
||||
entry.IsColosseumPeriod = GetBool(info, "is_colosseum_period");
|
||||
entry.IsRoundPeriod = GetBool(info, "is_round_period");
|
||||
entry.IsNormalTwoPick = GetString(info, "is_normal_two_pick");
|
||||
entry.IsSpecialMode = GetString(info, "is_special_mode");
|
||||
entry.IsAllCardEnabled = GetInt(info, "is_all_card_enabled");
|
||||
entry.SalesPeriodInfo = info.TryGetProperty("sales_period_info", out var sp) ? Serialize(sp) : "{}";
|
||||
if (existing is null) context.Colosseums.Add(entry);
|
||||
Console.WriteLine($"[GlobalsImporter] Colosseum: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Sealed (singleton) ----------
|
||||
|
||||
private async Task<int> ImportSealed(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("sealed_info", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.SealedSeasons.FirstOrDefaultAsync(e => e.Id == 1);
|
||||
var entry = existing ?? new SealedConfig { Id = 1 };
|
||||
entry.Enable = GetInt(info, "enable");
|
||||
entry.CrystalCost = GetInt(info, "crystal_cost");
|
||||
entry.RupyCost = GetInt(info, "rupy_cost");
|
||||
entry.TicketCost = GetInt(info, "ticket_cost");
|
||||
entry.DeckUsingNumMin = GetInt(info, "deck_using_num_min");
|
||||
entry.ScheduleId = GetInt(info, "schedule_id");
|
||||
entry.IsJoin = GetBool(info, "is_join");
|
||||
entry.IsDeckCodeMaintenance = GetBool(info, "is_deck_code_maintenance");
|
||||
entry.PackInfo = info.TryGetProperty("pack_info", out var pi) ? Serialize(pi) : "[]";
|
||||
entry.SalesPeriodInfo = info.TryGetProperty("sales_period_info", out var sp) ? Serialize(sp) : "{}";
|
||||
if (existing is null) context.SealedSeasons.Add(entry);
|
||||
Console.WriteLine($"[GlobalsImporter] Sealed: {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Mypage: Master Point Ranking Period ----------
|
||||
|
||||
private async Task<int> ImportMasterPointRankingPeriod(SVSimDbContext context, JsonElement mypage)
|
||||
{
|
||||
if (!mypage.TryGetProperty("master_point_ranking_period", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
int id = GetInt(info, "id");
|
||||
if (id == 0) return 0;
|
||||
|
||||
var existing = await context.MasterPointRankingPeriods.FirstOrDefaultAsync(e => e.Id == id);
|
||||
var entry = existing ?? new MasterPointRankingPeriodEntry { Id = id };
|
||||
entry.PeriodNum = GetInt(info, "period_num");
|
||||
entry.NecessaryScore = GetLong(info, "necessary_score");
|
||||
entry.BeginTime = ParseWireDateTime(GetString(info, "begin_time"));
|
||||
entry.EndTime = ParseWireDateTime(GetString(info, "end_time"));
|
||||
if (existing is null) context.MasterPointRankingPeriods.Add(entry);
|
||||
Console.WriteLine($"[GlobalsImporter] MasterPointRankingPeriod (id={id}): {(existing is null ? "+1" : "~1")}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------- Deck/info: Default Decks ----------
|
||||
|
||||
private async Task<int> ImportDefaultDecks(SVSimDbContext context, JsonElement deckInfo)
|
||||
{
|
||||
if (!deckInfo.TryGetProperty("default_deck_list", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.DefaultDecks.ToDictionaryAsync(e => e.Id);
|
||||
var knownSet = new HashSet<long>(await context.Cards.Select(c => c.Id).ToListAsync());
|
||||
int created = 0, updated = 0, orphans = 0;
|
||||
|
||||
foreach (var kv in info.EnumerateObject())
|
||||
{
|
||||
if (!int.TryParse(kv.Name, out int deckNo)) continue;
|
||||
var v = kv.Value;
|
||||
var entry = existing.TryGetValue(deckNo, out var ex) ? ex : new DefaultDeckEntry { Id = deckNo };
|
||||
entry.ClassId = GetInt(v, "class_id");
|
||||
entry.SleeveId = GetLong(v, "sleeve_id");
|
||||
entry.LeaderSkinId = GetInt(v, "leader_skin_id");
|
||||
entry.DeckName = GetString(v, "deck_name");
|
||||
entry.CardIdArray = v.TryGetProperty("card_id_array", out var arr) ? Serialize(arr) : "[]";
|
||||
|
||||
// Count orphans against card master
|
||||
if (arr.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var c in arr.EnumerateArray())
|
||||
{
|
||||
if (c.ValueKind != JsonValueKind.Number) continue;
|
||||
if (!knownSet.Contains(c.GetInt64())) orphans++;
|
||||
}
|
||||
}
|
||||
|
||||
if (ex is null) { context.DefaultDecks.Add(entry); created++; }
|
||||
else updated++;
|
||||
}
|
||||
WarnOrphans("DefaultDecks.card_id_array", orphans);
|
||||
Console.WriteLine($"[GlobalsImporter] DefaultDecks: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
// ---------- Deck/info: Default Leader Skin Settings ----------
|
||||
|
||||
private async Task<int> ImportDefaultLeaderSkinSettings(SVSimDbContext context, JsonElement deckInfo)
|
||||
{
|
||||
if (!deckInfo.TryGetProperty("user_leader_skin_setting_list", out var info) || info.ValueKind != JsonValueKind.Object) return 0;
|
||||
|
||||
var existing = await context.DefaultLeaderSkinSettings.ToDictionaryAsync(e => e.Id);
|
||||
int created = 0, updated = 0;
|
||||
foreach (var kv in info.EnumerateObject())
|
||||
{
|
||||
if (!int.TryParse(kv.Name, out int classId)) continue;
|
||||
var v = kv.Value;
|
||||
var entry = existing.TryGetValue(classId, out var ex) ? ex : new DefaultLeaderSkinSettingEntry { Id = classId };
|
||||
entry.IsRandomLeaderSkin = GetInt(v, "is_random_leader_skin");
|
||||
entry.LeaderSkinId = GetInt(v, "leader_skin_id");
|
||||
if (ex is null) { context.DefaultLeaderSkinSettings.Add(entry); created++; }
|
||||
else updated++;
|
||||
}
|
||||
Console.WriteLine($"[GlobalsImporter] DefaultLeaderSkinSettings: +{created}/~{updated}");
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
// ---------- Helpers ----------
|
||||
|
||||
private static void WarnOrphans(string label, int count)
|
||||
{
|
||||
if (count > 0) Console.Error.WriteLine($"[GlobalsImporter] Warning: {label} has {count} orphan card_id(s) — run CardImporter first for clean references.");
|
||||
}
|
||||
}
|
||||
140
SVSim.Bootstrap/Importers/ImporterBase.cs
Normal file
140
SVSim.Bootstrap/Importers/ImporterBase.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SVSim.Bootstrap.Importers;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helpers for content importers. Loads a prod-capture JSON file by endpoint name from
|
||||
/// a captures directory, returning the inner <c>data</c> element. Picks the latest matching dated
|
||||
/// file (e.g. <c>load-index-2026-05-23.json</c>) if multiple exist for the same endpoint.
|
||||
/// </summary>
|
||||
public static class ImporterBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the parsed <c>.data</c> JsonElement for the latest <c>{endpoint}-*.json</c> file in
|
||||
/// <paramref name="capturesDir"/>, or null if no file matches. Logs a warning when missing —
|
||||
/// caller decides whether absence is fatal.
|
||||
/// </summary>
|
||||
public static JsonElement? LoadCapture(string capturesDir, string endpoint)
|
||||
{
|
||||
if (!Directory.Exists(capturesDir))
|
||||
{
|
||||
Console.Error.WriteLine($"[ImporterBase] Captures dir missing: {capturesDir}");
|
||||
return null;
|
||||
}
|
||||
|
||||
string? path = Directory.EnumerateFiles(capturesDir, $"{endpoint}-*.json")
|
||||
.OrderByDescending(p => p)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (path is null)
|
||||
{
|
||||
Console.Error.WriteLine($"[ImporterBase] No capture found for endpoint '{endpoint}' in {capturesDir}");
|
||||
return null;
|
||||
}
|
||||
|
||||
using var fs = File.OpenRead(path);
|
||||
using var doc = JsonDocument.Parse(fs);
|
||||
if (!doc.RootElement.TryGetProperty("data", out var data))
|
||||
{
|
||||
Console.Error.WriteLine($"[ImporterBase] Capture file {path} has no top-level 'data' property.");
|
||||
return null;
|
||||
}
|
||||
// Clone so the JsonElement survives doc disposal.
|
||||
return data.Clone();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic upsert by primary key. Returns (created, updated, unchanged) counts.
|
||||
/// <paramref name="incoming"/> is the desired state from the capture; rows are matched by
|
||||
/// <paramref name="keySelector"/>. <paramref name="applyChanges"/> mutates an existing row to
|
||||
/// reflect incoming values and returns true if anything actually changed.
|
||||
/// </summary>
|
||||
public static (int created, int updated, int unchanged) Upsert<T, TKey>(
|
||||
IEnumerable<T> incoming,
|
||||
Dictionary<TKey, T> existingByKey,
|
||||
Func<T, TKey> keySelector,
|
||||
Action<T> addToContext,
|
||||
Func<T, T, bool> applyChanges) where TKey : notnull
|
||||
{
|
||||
int created = 0, updated = 0, unchanged = 0;
|
||||
foreach (var item in incoming)
|
||||
{
|
||||
var key = keySelector(item);
|
||||
if (existingByKey.TryGetValue(key, out var existing))
|
||||
{
|
||||
if (applyChanges(existing, item)) updated++;
|
||||
else unchanged++;
|
||||
}
|
||||
else
|
||||
{
|
||||
addToContext(item);
|
||||
existingByKey[key] = item;
|
||||
created++;
|
||||
}
|
||||
}
|
||||
return (created, updated, unchanged);
|
||||
}
|
||||
|
||||
/// <summary>Serialize a JsonElement back to compact JSON text for jsonb storage.</summary>
|
||||
public static string Serialize(JsonElement el) =>
|
||||
JsonSerializer.Serialize(el, new JsonSerializerOptions { WriteIndented = false });
|
||||
|
||||
/// <summary>Parse a wire date that may be ISO ("2026-05-23T..."), space-separated ("2026-05-23 16:32:31"), or empty.</summary>
|
||||
public static DateTime ParseWireDateTime(string? s)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(s)) return DateTime.MinValue;
|
||||
if (DateTime.TryParse(s, System.Globalization.CultureInfo.InvariantCulture,
|
||||
System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
|
||||
out var dt))
|
||||
{
|
||||
return dt;
|
||||
}
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>Read a JsonElement string/number property as long, defaulting on missing/null.</summary>
|
||||
public static long GetLong(JsonElement el, string prop, long fallback = 0)
|
||||
{
|
||||
if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback;
|
||||
if (v.ValueKind == JsonValueKind.Number) return v.GetInt64();
|
||||
if (v.ValueKind == JsonValueKind.String && long.TryParse(v.GetString(), out var n)) return n;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static int GetInt(JsonElement el, string prop, int fallback = 0)
|
||||
{
|
||||
if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback;
|
||||
if (v.ValueKind == JsonValueKind.Number) return v.GetInt32();
|
||||
if (v.ValueKind == JsonValueKind.String && int.TryParse(v.GetString(), out var n)) return n;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static string GetString(JsonElement el, string prop, string fallback = "")
|
||||
{
|
||||
if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback;
|
||||
return v.ValueKind == JsonValueKind.String ? v.GetString() ?? fallback : v.ToString();
|
||||
}
|
||||
|
||||
public static bool GetBool(JsonElement el, string prop, bool fallback = false)
|
||||
{
|
||||
if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback;
|
||||
if (v.ValueKind == JsonValueKind.True) return true;
|
||||
if (v.ValueKind == JsonValueKind.False) return false;
|
||||
if (v.ValueKind == JsonValueKind.Number) return v.GetInt32() != 0;
|
||||
if (v.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var s = v.GetString();
|
||||
if (bool.TryParse(s, out var b)) return b;
|
||||
if (int.TryParse(s, out var i)) return i != 0;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static ulong GetULong(JsonElement el, string prop, ulong fallback = 0)
|
||||
{
|
||||
if (!el.TryGetProperty(prop, out var v) || v.ValueKind == JsonValueKind.Null) return fallback;
|
||||
if (v.ValueKind == JsonValueKind.Number) return v.GetUInt64();
|
||||
if (v.ValueKind == JsonValueKind.String && ulong.TryParse(v.GetString(), out var n)) return n;
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
161
SVSim.Bootstrap/Program.cs
Normal file
161
SVSim.Bootstrap/Program.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using SVSim.Bootstrap.Importers;
|
||||
using SVSim.Database;
|
||||
|
||||
namespace SVSim.Bootstrap;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
private const string DefaultConnectionString =
|
||||
"Host=localhost;Database=svsim;Username=postgres;password=postgres";
|
||||
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0 && (args[0] is "--help" or "-h"))
|
||||
{
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
var opts = ParseArgs(args);
|
||||
if (opts is null)
|
||||
{
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (opts.SkipCards && opts.SkipGlobals)
|
||||
{
|
||||
Console.Error.WriteLine("Both --skip-cards and --skip-globals set; nothing to do.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Bootstrap] Connection: {RedactPassword(opts.ConnectionString)}");
|
||||
Console.WriteLine($"[Bootstrap] Cards file: {opts.CardsFile}");
|
||||
Console.WriteLine($"[Bootstrap] Captures: {opts.CapturesDir}");
|
||||
|
||||
var dbOptions = new DbContextOptionsBuilder<SVSimDbContext>()
|
||||
.UseNpgsql(opts.ConnectionString)
|
||||
.Options;
|
||||
|
||||
await using var context = new SVSimDbContext(NullLogger<SVSimDbContext>.Instance, dbOptions);
|
||||
|
||||
// Bootstrap applies pending migrations first so it can be the very first thing run after
|
||||
// `dotnet ef migrations add` — no need to start the server too.
|
||||
Console.WriteLine("[Bootstrap] Applying pending migrations...");
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
if (!opts.SkipCards)
|
||||
{
|
||||
await new CardImporter().ImportAsync(context, opts.CardsFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[Bootstrap] --skip-cards set; skipping card import.");
|
||||
}
|
||||
|
||||
if (!opts.SkipGlobals)
|
||||
{
|
||||
await new GlobalsImporter().ImportAllAsync(context, opts.CapturesDir);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("[Bootstrap] --skip-globals set; skipping globals import.");
|
||||
}
|
||||
|
||||
Console.WriteLine("[Bootstrap] Complete.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static BootstrapOptions? ParseArgs(string[] args)
|
||||
{
|
||||
string? dataDir = null;
|
||||
string? cards = null;
|
||||
string? captures = null;
|
||||
string? connection = null;
|
||||
bool skipCards = false;
|
||||
bool skipGlobals = false;
|
||||
string? positionalCards = null;
|
||||
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
string a = args[i];
|
||||
switch (a)
|
||||
{
|
||||
case "--data-dir": dataDir = NextArg(args, ref i); break;
|
||||
case "--cards": cards = NextArg(args, ref i); break;
|
||||
case "--captures": captures = NextArg(args, ref i); break;
|
||||
case "--connection-string": connection = NextArg(args, ref i); break;
|
||||
case "--skip-cards": skipCards = true; break;
|
||||
case "--skip-globals": skipGlobals = true; break;
|
||||
default:
|
||||
// Back-compat: legacy positional form `svsim-card-import <cards.json> [connection]`.
|
||||
if (positionalCards is null && !a.StartsWith('-')) positionalCards = a;
|
||||
else if (connection is null && !a.StartsWith('-')) connection = a;
|
||||
else { Console.Error.WriteLine($"Unknown argument: {a}"); return null; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolution order:
|
||||
// --cards beats --data-dir/cards.json beats legacy positional;
|
||||
// --captures beats --data-dir/prod-captures beats Bootstrap/Data/prod-captures (shipped default).
|
||||
string baseDir = AppContext.BaseDirectory;
|
||||
string shippedCaptures = Path.Combine(baseDir, "Data", "prod-captures");
|
||||
|
||||
string cardsFile = cards
|
||||
?? (dataDir is not null ? Path.Combine(dataDir, "cards.json") : null)
|
||||
?? positionalCards
|
||||
?? "data_dumps/cards.json";
|
||||
|
||||
// Resolve captures dir, falling back to the shipped copy if the data-dir path is unset
|
||||
// OR points at a missing folder. (Common case: user has cards.json in data_dumps/ but
|
||||
// hasn't copied prod-captures/ there — the shipped snapshot is the source of truth.)
|
||||
string? capturesCandidate = captures
|
||||
?? (dataDir is not null ? Path.Combine(dataDir, "prod-captures") : null);
|
||||
string capturesDir = capturesCandidate is not null && Directory.Exists(capturesCandidate)
|
||||
? capturesCandidate
|
||||
: shippedCaptures;
|
||||
|
||||
string connStr = connection
|
||||
?? Environment.GetEnvironmentVariable("NPGSQL_CONNECTION")
|
||||
?? DefaultConnectionString;
|
||||
|
||||
return new BootstrapOptions(cardsFile, capturesDir, connStr, skipCards, skipGlobals);
|
||||
}
|
||||
|
||||
private static string NextArg(string[] args, ref int i)
|
||||
{
|
||||
if (i + 1 >= args.Length) throw new ArgumentException($"Missing value for {args[i]}");
|
||||
return args[++i];
|
||||
}
|
||||
|
||||
private static string RedactPassword(string conn) =>
|
||||
System.Text.RegularExpressions.Regex.Replace(conn, "(?i)(password=)[^;]+", "$1***");
|
||||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"Usage: svsim-bootstrap [options]\n" +
|
||||
"\n" +
|
||||
" --data-dir <path> Directory containing cards.json and prod-captures/\n" +
|
||||
" (default: ./data_dumps relative to working dir)\n" +
|
||||
" --cards <file> Override path to cards.json\n" +
|
||||
" --captures <dir> Override path to prod-captures directory\n" +
|
||||
" (default: shipped Data/prod-captures next to the binary)\n" +
|
||||
" --connection-string <conn> Postgres connection (or NPGSQL_CONNECTION env var,\n" +
|
||||
$" then \"{DefaultConnectionString}\")\n" +
|
||||
" --skip-cards Skip card import (re-run globals only)\n" +
|
||||
" --skip-globals Skip globals import (cards only — legacy behavior)\n" +
|
||||
"\n" +
|
||||
"Back-compat: `svsim-bootstrap <cards.json> [connection]` still works (positional).");
|
||||
}
|
||||
|
||||
private sealed record BootstrapOptions(
|
||||
string CardsFile,
|
||||
string CapturesDir,
|
||||
string ConnectionString,
|
||||
bool SkipCards,
|
||||
bool SkipGlobals);
|
||||
}
|
||||
@@ -5,10 +5,16 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>SVSim.CardImport</RootNamespace>
|
||||
<AssemblyName>svsim-card-import</AssemblyName>
|
||||
<RootNamespace>SVSim.Bootstrap</RootNamespace>
|
||||
<AssemblyName>svsim-bootstrap</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Data\prod-captures\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SVSim.Database\SVSim.Database.csproj" />
|
||||
</ItemGroup>
|
||||
34506
SVSim.Database/Migrations/20260523200820_ProdContentTables.Designer.cs
generated
Normal file
34506
SVSim.Database/Migrations/20260523200820_ProdContentTables.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
489
SVSim.Database/Migrations/20260523200820_ProdContentTables.cs
Normal file
489
SVSim.Database/Migrations/20260523200820_ProdContentTables.cs
Normal file
@@ -0,0 +1,489 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SVSim.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ProdContentTables : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "CardSetIdForResourceDlView",
|
||||
table: "GameConfigurations",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "ChallengeTwoPickSleeveId",
|
||||
table: "GameConfigurations",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ChallengeUseTwoPickPremiumCard",
|
||||
table: "GameConfigurations",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsBattlePassPeriod",
|
||||
table: "GameConfigurations",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsBeginnerMission",
|
||||
table: "GameConfigurations",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TsRotationId",
|
||||
table: "GameConfigurations",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ArenaSeasons",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
Mode = table.Column<int>(type: "integer", nullable: false),
|
||||
Enable = table.Column<int>(type: "integer", nullable: false),
|
||||
Cost = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
RupyCost = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||
TicketCost = table.Column<int>(type: "integer", nullable: false),
|
||||
IsJoin = table.Column<bool>(type: "boolean", nullable: false),
|
||||
FormatInfo = table.Column<string>(type: "jsonb", 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_ArenaSeasons", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AvatarAbilities",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
LeaderSkinId = table.Column<int>(type: "integer", nullable: false),
|
||||
BattleStartFirstPlayerTurnBp = table.Column<int>(type: "integer", nullable: false),
|
||||
BattleStartSecondPlayerTurnBp = table.Column<int>(type: "integer", nullable: false),
|
||||
BattleStartMaxLife = table.Column<int>(type: "integer", nullable: false),
|
||||
AbilityCost = table.Column<string>(type: "text", nullable: false),
|
||||
Ability = table.Column<string>(type: "text", nullable: false),
|
||||
PassiveAbility = table.Column<string>(type: "text", nullable: false),
|
||||
AbilityDesc = table.Column<string>(type: "text", nullable: false),
|
||||
PassiveAbilityDesc = table.Column<string>(type: "text", 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_AvatarAbilities", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Banners",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
ImageName = table.Column<string>(type: "text", nullable: false),
|
||||
Click = table.Column<string>(type: "text", nullable: false),
|
||||
Status = table.Column<string>(type: "text", nullable: false),
|
||||
ChangeTime = table.Column<int>(type: "integer", nullable: false),
|
||||
RemainingTime = table.Column<int>(type: "integer", nullable: false),
|
||||
ImagePaths = table.Column<string>(type: "jsonb", 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_Banners", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BattlePassLevels",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
Level = table.Column<int>(type: "integer", nullable: false),
|
||||
RewardData = table.Column<string>(type: "jsonb", 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_BattlePassLevels", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Colosseums",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
ColosseumId = table.Column<string>(type: "text", nullable: false),
|
||||
ColosseumName = table.Column<string>(type: "text", nullable: false),
|
||||
CardPoolName = table.Column<string>(type: "text", nullable: false),
|
||||
DeckFormat = table.Column<string>(type: "text", nullable: false),
|
||||
StartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
NowRound = table.Column<string>(type: "text", nullable: false),
|
||||
IsDisplayTips = table.Column<string>(type: "text", nullable: false),
|
||||
TipsId = table.Column<string>(type: "text", nullable: false),
|
||||
IsColosseumPeriod = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsRoundPeriod = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsNormalTwoPick = table.Column<string>(type: "text", nullable: false),
|
||||
IsSpecialMode = table.Column<string>(type: "text", nullable: false),
|
||||
IsAllCardEnabled = table.Column<int>(type: "integer", nullable: false),
|
||||
SalesPeriodInfo = table.Column<string>(type: "jsonb", 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_Colosseums", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DailyLoginBonuses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
BonusId = table.Column<int>(type: "integer", nullable: false),
|
||||
BonusData = table.Column<string>(type: "jsonb", 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_DailyLoginBonuses", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DefaultDecks",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
DeckNo = table.Column<int>(type: "integer", nullable: false),
|
||||
ClassId = table.Column<int>(type: "integer", nullable: false),
|
||||
SleeveId = table.Column<long>(type: "bigint", nullable: false),
|
||||
LeaderSkinId = table.Column<int>(type: "integer", nullable: false),
|
||||
DeckName = table.Column<string>(type: "text", nullable: false),
|
||||
CardIdArray = table.Column<string>(type: "jsonb", 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_DefaultDecks", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DefaultLeaderSkinSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
ClassId = table.Column<int>(type: "integer", nullable: false),
|
||||
IsRandomLeaderSkin = table.Column<int>(type: "integer", nullable: false),
|
||||
LeaderSkinId = 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_DefaultLeaderSkinSettings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeatureMaintenances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
FeatureKey = table.Column<string>(type: "text", nullable: false),
|
||||
Data = table.Column<string>(type: "jsonb", 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_FeatureMaintenances", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LoadingExclusionCards",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||
CardId = table.Column<long>(type: "bigint", 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_LoadingExclusionCards", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MaintenanceCards",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||
CardId = table.Column<long>(type: "bigint", 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_MaintenanceCards", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MasterPointRankingPeriods",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
PeriodNum = table.Column<int>(type: "integer", nullable: false),
|
||||
NecessaryScore = table.Column<long>(type: "bigint", nullable: false),
|
||||
BeginTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
EndTime = table.Column<DateTime>(type: "timestamp with time zone", 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_MasterPointRankingPeriods", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MyRotationAbilities",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
AbilityId = table.Column<int>(type: "integer", nullable: false),
|
||||
Data = table.Column<string>(type: "jsonb", 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_MyRotationAbilities", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MyRotationSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
RotationId = table.Column<int>(type: "integer", nullable: false),
|
||||
CardSetIdsCsv = table.Column<string>(type: "text", nullable: false),
|
||||
AbilitiesCsv = table.Column<string>(type: "text", nullable: false),
|
||||
ReprintedCardIds = table.Column<string>(type: "jsonb", nullable: false),
|
||||
RestrictedCardIds = table.Column<string>(type: "jsonb", 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_MyRotationSettings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PreReleaseInfos",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
PreReleaseId = table.Column<string>(type: "text", nullable: false),
|
||||
NextCardSetId = table.Column<string>(type: "text", nullable: false),
|
||||
StartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
EndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
DisplayEndTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
FreeMatchStartTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CardMasterId = table.Column<int>(type: "integer", nullable: false),
|
||||
DefaultCardMasterId = table.Column<string>(type: "text", nullable: false),
|
||||
PreReleaseCardMasterId = table.Column<string>(type: "text", nullable: false),
|
||||
IsPreRotationFreeMatchTerm = table.Column<bool>(type: "boolean", nullable: false),
|
||||
RotationCardSetIdList = table.Column<string>(type: "jsonb", nullable: false),
|
||||
ReprintedBaseCardIds = table.Column<string>(type: "jsonb", nullable: false),
|
||||
LatestReprintedBaseCardIds = table.Column<string>(type: "jsonb", 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_PreReleaseInfos", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ReprintedCards",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||
CardId = table.Column<long>(type: "bigint", 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_ReprintedCards", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SealedSeasons",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false),
|
||||
Enable = table.Column<int>(type: "integer", nullable: false),
|
||||
CrystalCost = table.Column<int>(type: "integer", nullable: false),
|
||||
RupyCost = table.Column<int>(type: "integer", nullable: false),
|
||||
TicketCost = table.Column<int>(type: "integer", nullable: false),
|
||||
DeckUsingNumMin = table.Column<int>(type: "integer", nullable: false),
|
||||
ScheduleId = table.Column<int>(type: "integer", nullable: false),
|
||||
IsJoin = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsDeckCodeMaintenance = table.Column<bool>(type: "boolean", nullable: false),
|
||||
PackInfo = table.Column<string>(type: "jsonb", nullable: false),
|
||||
SalesPeriodInfo = table.Column<string>(type: "jsonb", 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_SealedSeasons", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SpotCards",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||
CardId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Cost = 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_SpotCards", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UnlimitedRestrictions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false),
|
||||
CardId = table.Column<long>(type: "bigint", nullable: false),
|
||||
RestrictionValue = 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_UnlimitedRestrictions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "GameConfigurations",
|
||||
keyColumn: "Id",
|
||||
keyValue: "default",
|
||||
columns: new[] { "CardSetIdForResourceDlView", "ChallengeTwoPickSleeveId", "ChallengeUseTwoPickPremiumCard", "IsBattlePassPeriod", "IsBeginnerMission", "TsRotationId" },
|
||||
values: new object[] { 0, 0L, false, false, false, "" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ArenaSeasons");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AvatarAbilities");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Banners");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BattlePassLevels");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Colosseums");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DailyLoginBonuses");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DefaultDecks");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "DefaultLeaderSkinSettings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeatureMaintenances");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "LoadingExclusionCards");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MaintenanceCards");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MasterPointRankingPeriods");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MyRotationAbilities");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MyRotationSettings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "PreReleaseInfos");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ReprintedCards");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SealedSeasons");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SpotCards");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UnlimitedRestrictions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CardSetIdForResourceDlView",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChallengeTwoPickSleeveId",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ChallengeUseTwoPickPremiumCard",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsBattlePassPeriod",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsBeginnerMission",
|
||||
table: "GameConfigurations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TsRotationId",
|
||||
table: "GameConfigurations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,153 @@ namespace SVSim.Database.Migrations
|
||||
b.ToTable("MyPageBackgroundEntryViewer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.ArenaSeasonConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("Cost")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Enable")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("FormatInfo")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<bool>("IsJoin")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("Mode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("RupyCost")
|
||||
.HasColumnType("numeric(20,0)");
|
||||
|
||||
b.Property<int>("TicketCost")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ArenaSeasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.AvatarAbilityEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Ability")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AbilityCost")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AbilityDesc")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("BattleStartFirstPlayerTurnBp")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BattleStartMaxLife")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BattleStartSecondPlayerTurnBp")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("LeaderSkinId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("PassiveAbility")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PassiveAbilityDesc")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("AvatarAbilities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.BannerEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ChangeTime")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Click")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ImagePaths")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("RemainingTime")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Banners");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.BattlePassLevelEntry", 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<int>("Level")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("RewardData")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("BattlePassLevels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.BattlefieldEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1213,6 +1360,161 @@ namespace SVSim.Database.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.ColosseumConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CardPoolName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ColosseumId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ColosseumName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DeckFormat")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("EndTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("IsAllCardEnabled")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsColosseumPeriod")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IsDisplayTips")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("IsNormalTwoPick")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsRoundPeriod")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("IsSpecialMode")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("NowRound")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SalesPeriodInfo")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("StartTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TipsId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Colosseums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.DailyLoginBonusEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("BonusData")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("BonusId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DailyLoginBonuses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.DefaultDeckEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CardIdArray")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
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<string>("DeckName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("DeckNo")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("LeaderSkinId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("SleeveId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DefaultDecks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.DefaultLeaderSkinSettingEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
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>("IsRandomLeaderSkin")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("LeaderSkinId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("DefaultLeaderSkinSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.DegreeEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -21399,11 +21701,44 @@ namespace SVSim.Database.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.FeatureMaintenanceEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("FeatureKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("FeatureMaintenances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.GameConfiguration", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("CardSetIdForResourceDlView")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<long>("ChallengeTwoPickSleeveId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("ChallengeUseTwoPickPremiumCard")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
@@ -21431,9 +21766,19 @@ namespace SVSim.Database.Migrations
|
||||
b.Property<int>("DefaultSleeveId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsBattlePassPeriod")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsBeginnerMission")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("MaxFriends")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TsRotationId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefaultDegreeId");
|
||||
@@ -21450,6 +21795,9 @@ namespace SVSim.Database.Migrations
|
||||
new
|
||||
{
|
||||
Id = "default",
|
||||
CardSetIdForResourceDlView = 0,
|
||||
ChallengeTwoPickSleeveId = 0L,
|
||||
ChallengeUseTwoPickPremiumCard = false,
|
||||
DateCreated = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||
DefaultCrystals = 50000m,
|
||||
DefaultDegreeId = 300003,
|
||||
@@ -21458,7 +21806,10 @@ namespace SVSim.Database.Migrations
|
||||
DefaultMyPageBackgroundId = 100000000,
|
||||
DefaultRupees = 50000m,
|
||||
DefaultSleeveId = 3000011,
|
||||
MaxFriends = 20
|
||||
IsBattlePassPeriod = false,
|
||||
IsBeginnerMission = false,
|
||||
MaxFriends = 20,
|
||||
TsRotationId = ""
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24844,6 +25195,72 @@ namespace SVSim.Database.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.LoadingExclusionCardEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CardId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("LoadingExclusionCards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.MaintenanceCardEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CardId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MaintenanceCards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.MasterPointRankingPeriodEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("BeginTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("EndTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("NecessaryScore")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("PeriodNum")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MasterPointRankingPeriods");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.MyPageBackgroundEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -24947,6 +25364,126 @@ namespace SVSim.Database.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.MyRotationAbilityEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("AbilityId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MyRotationAbilities");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.MyRotationSettingEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("AbilitiesCsv")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CardSetIdsCsv")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ReprintedCardIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RestrictedCardIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("RotationId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("MyRotationSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.PreReleaseInfo", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CardMasterId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DefaultCardMasterId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("DisplayEndTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("EndTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("FreeMatchStartTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsPreRotationFreeMatchTerm")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LatestReprintedBaseCardIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("NextCardSetId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PreReleaseCardMasterId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PreReleaseId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ReprintedBaseCardIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("RotationCardSetIdList")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("StartTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PreReleaseInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.RankInfoEntry", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -25623,6 +26160,73 @@ namespace SVSim.Database.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.ReprintedCardEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CardId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ReprintedCards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.SealedConfig", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("CrystalCost")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("DeckUsingNumMin")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Enable")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsDeckCodeMaintenance")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsJoin")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PackInfo")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("RupyCost")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SalesPeriodInfo")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int>("ScheduleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("TicketCost")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SealedSeasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -25694,6 +26298,7 @@ namespace SVSim.Database.Migrations
|
||||
modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("ClassId")
|
||||
@@ -33268,6 +33873,50 @@ namespace SVSim.Database.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.SpotCardEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CardId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Cost")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SpotCards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.UnlimitedRestrictionEntry", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CardId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("DateCreated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DateUpdated")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("RestrictionValue")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("UnlimitedRestrictions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SVSim.Database.Models.Viewer", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
|
||||
27
SVSim.Database/Models/ArenaSeasonConfig.cs
Normal file
27
SVSim.Database/Models/ArenaSeasonConfig.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton row (Id=1) capturing the current Take Two arena season config from
|
||||
/// /load/index data.arena_info[0]. FormatInfo jsonb holds the nested
|
||||
/// {two_pick_type, card_pool_name, announce_id, last_card_pack_set_id, start_time, end_time}.
|
||||
/// </summary>
|
||||
public class ArenaSeasonConfig : BaseEntity<int>
|
||||
{
|
||||
public int Mode { get; set; }
|
||||
|
||||
public int Enable { get; set; }
|
||||
|
||||
public ulong Cost { get; set; }
|
||||
|
||||
public ulong RupyCost { get; set; }
|
||||
|
||||
public int TicketCost { get; set; }
|
||||
|
||||
public bool IsJoin { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string FormatInfo { get; set; } = "{}";
|
||||
}
|
||||
30
SVSim.Database/Models/AvatarAbilityEntry.cs
Normal file
30
SVSim.Database/Models/AvatarAbilityEntry.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One Avatar (Hero) mode definition. Keyed by leader_skin_id. The Ability/PassiveAbility strings
|
||||
/// are the dense "(skill:...)(timing:...)" effect DSL that cannot be reconstructed from card master —
|
||||
/// preserve verbatim from /load/index data.avatar_info.abilities[leaderSkinId].
|
||||
/// </summary>
|
||||
public class AvatarAbilityEntry : BaseEntity<int>
|
||||
{
|
||||
public int LeaderSkinId { get => Id; set => Id = value; }
|
||||
|
||||
public int BattleStartFirstPlayerTurnBp { get; set; }
|
||||
|
||||
public int BattleStartSecondPlayerTurnBp { get; set; }
|
||||
|
||||
public int BattleStartMaxLife { get; set; }
|
||||
|
||||
public string AbilityCost { get; set; } = string.Empty;
|
||||
|
||||
public string Ability { get; set; } = string.Empty;
|
||||
|
||||
public string PassiveAbility { get; set; } = string.Empty;
|
||||
|
||||
public string AbilityDesc { get; set; } = string.Empty;
|
||||
|
||||
public string PassiveAbilityDesc { get; set; } = string.Empty;
|
||||
}
|
||||
24
SVSim.Database/Models/BannerEntry.cs
Normal file
24
SVSim.Database/Models/BannerEntry.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One mypage banner from /mypage/index data.banner. Id is synthetic ordinal (1-N) since the wire
|
||||
/// has no explicit ID. Highly time-varying content — recapture aggressively before EOS.
|
||||
/// </summary>
|
||||
public class BannerEntry : BaseEntity<int>
|
||||
{
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
|
||||
public string Click { get; set; } = string.Empty;
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public int ChangeTime { get; set; }
|
||||
|
||||
public int RemainingTime { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string ImagePaths { get; set; } = "[]";
|
||||
}
|
||||
16
SVSim.Database/Models/BattlePassLevelEntry.cs
Normal file
16
SVSim.Database/Models/BattlePassLevelEntry.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One battle pass level (1-100). RewardData jsonb holds the per-level reward blob from
|
||||
/// /load/index data.battle_pass_level_info[level]. Shape varies per level so we preserve verbatim.
|
||||
/// </summary>
|
||||
public class BattlePassLevelEntry : BaseEntity<int>
|
||||
{
|
||||
public int Level { get => Id; set => Id = value; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string RewardData { get; set; } = "{}";
|
||||
}
|
||||
42
SVSim.Database/Models/ColosseumConfig.cs
Normal file
42
SVSim.Database/Models/ColosseumConfig.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton row (Id=1) for the current Colosseum event from /mypage/index data.colosseum_info.
|
||||
/// Time-bound — recapture per Colosseum cycle (every few weeks).
|
||||
/// </summary>
|
||||
public class ColosseumConfig : BaseEntity<int>
|
||||
{
|
||||
public string ColosseumId { get; set; } = string.Empty;
|
||||
|
||||
public string ColosseumName { get; set; } = string.Empty;
|
||||
|
||||
public string CardPoolName { get; set; } = string.Empty;
|
||||
|
||||
public string DeckFormat { get; set; } = string.Empty;
|
||||
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
public DateTime EndTime { get; set; }
|
||||
|
||||
public string NowRound { get; set; } = string.Empty;
|
||||
|
||||
public string IsDisplayTips { get; set; } = string.Empty;
|
||||
|
||||
public string TipsId { get; set; } = string.Empty;
|
||||
|
||||
public bool IsColosseumPeriod { get; set; }
|
||||
|
||||
public bool IsRoundPeriod { get; set; }
|
||||
|
||||
public string IsNormalTwoPick { get; set; } = string.Empty;
|
||||
|
||||
public string IsSpecialMode { get; set; } = string.Empty;
|
||||
|
||||
public int IsAllCardEnabled { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string SalesPeriodInfo { get; set; } = "{}";
|
||||
}
|
||||
17
SVSim.Database/Models/DailyLoginBonusEntry.cs
Normal file
17
SVSim.Database/Models/DailyLoginBonusEntry.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Daily login bonus campaign from /load/index data.daily_login_bonus (dict keyed by bonus_id,
|
||||
/// values are arrays of bonus days). Prod observed keys {1, 3, 4} with empty arrays — recapture
|
||||
/// target during active login bonus events.
|
||||
/// </summary>
|
||||
public class DailyLoginBonusEntry : BaseEntity<int>
|
||||
{
|
||||
public int BonusId { get => Id; set => Id = value; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string BonusData { get; set; } = "[]";
|
||||
}
|
||||
24
SVSim.Database/Models/DefaultDeckEntry.cs
Normal file
24
SVSim.Database/Models/DefaultDeckEntry.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Starter / "use default" deck definition from /deck/info data.default_deck_list.
|
||||
/// CardIdArray is the wire's int[] of 40 card_id values; stored as jsonb to keep it array-shaped.
|
||||
/// </summary>
|
||||
public class DefaultDeckEntry : BaseEntity<int>
|
||||
{
|
||||
public int DeckNo { get => Id; set => Id = value; }
|
||||
|
||||
public int ClassId { get; set; }
|
||||
|
||||
public long SleeveId { get; set; }
|
||||
|
||||
public int LeaderSkinId { get; set; }
|
||||
|
||||
public string DeckName { get; set; } = string.Empty;
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string CardIdArray { get; set; } = "[]";
|
||||
}
|
||||
13
SVSim.Database/Models/DefaultLeaderSkinSettingEntry.cs
Normal file
13
SVSim.Database/Models/DefaultLeaderSkinSettingEntry.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>One row per class: which leader skin is default and whether random rotation is on.</summary>
|
||||
public class DefaultLeaderSkinSettingEntry : BaseEntity<int>
|
||||
{
|
||||
public int ClassId { get => Id; set => Id = value; }
|
||||
|
||||
public int IsRandomLeaderSkin { get; set; }
|
||||
|
||||
public int LeaderSkinId { get; set; }
|
||||
}
|
||||
16
SVSim.Database/Models/FeatureMaintenanceEntry.cs
Normal file
16
SVSim.Database/Models/FeatureMaintenanceEntry.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Per-feature maintenance toggle from /load/index data.feature_maintenance_list. Empty in current
|
||||
/// prod capture; recapture target if a feature ever gets disabled before EOS.
|
||||
/// </summary>
|
||||
public class FeatureMaintenanceEntry : BaseEntity<int>
|
||||
{
|
||||
public string FeatureKey { get; set; } = string.Empty;
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string Data { get; set; } = "{}";
|
||||
}
|
||||
@@ -9,9 +9,30 @@ public class GameConfiguration : BaseEntity<string>
|
||||
public ulong DefaultRupees { get; set; }
|
||||
|
||||
public ulong DefaultEther { get; set; }
|
||||
|
||||
|
||||
public int MaxFriends { get; set; }
|
||||
|
||||
#region Time-varying globals populated by SVSim.Bootstrap.GlobalsImporter
|
||||
|
||||
/// <summary>Current "Take Two Special" rotation ID, e.g. "10015". Points into MyRotationSettingEntry.</summary>
|
||||
public string TsRotationId { get; set; } = string.Empty;
|
||||
|
||||
public bool ChallengeUseTwoPickPremiumCard { get; set; }
|
||||
|
||||
public long ChallengeTwoPickSleeveId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bool on the wire (prod sends true/false); local previously sent int. Fixes the
|
||||
/// type-mismatch noted in seed-data-strategy-2026-05-23.md crash audit.
|
||||
/// </summary>
|
||||
public bool IsBattlePassPeriod { get; set; }
|
||||
|
||||
public bool IsBeginnerMission { get; set; }
|
||||
|
||||
public int CardSetIdForResourceDlView { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Foreign Keys
|
||||
|
||||
public int DefaultDegreeId { get; set; }
|
||||
|
||||
12
SVSim.Database/Models/LoadingExclusionCardEntry.cs
Normal file
12
SVSim.Database/Models/LoadingExclusionCardEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Cards excluded from loading-screen art rotation, from /load/index data.loading_exclusion_card_list.
|
||||
/// References ShadowverseCardEntry.Id but no FK.
|
||||
/// </summary>
|
||||
public class LoadingExclusionCardEntry : BaseEntity<long>
|
||||
{
|
||||
public long CardId { get => Id; set => Id = value; }
|
||||
}
|
||||
12
SVSim.Database/Models/MaintenanceCardEntry.cs
Normal file
12
SVSim.Database/Models/MaintenanceCardEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Cards disabled mid-season for emergency balance, from /load/index data.maintenance_card_list.
|
||||
/// Empty in current prod capture; recapture target if a card ever gets emergency-disabled before EOS.
|
||||
/// </summary>
|
||||
public class MaintenanceCardEntry : BaseEntity<long>
|
||||
{
|
||||
public long CardId { get => Id; set => Id = value; }
|
||||
}
|
||||
18
SVSim.Database/Models/MasterPointRankingPeriodEntry.cs
Normal file
18
SVSim.Database/Models/MasterPointRankingPeriodEntry.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Monthly Master Point ranking window from /mypage/index data.master_point_ranking_period.
|
||||
/// One row per period; the "current" period is fetched by EndTime > now ordering.
|
||||
/// </summary>
|
||||
public class MasterPointRankingPeriodEntry : BaseEntity<int>
|
||||
{
|
||||
public int PeriodNum { get; set; }
|
||||
|
||||
public long NecessaryScore { get; set; }
|
||||
|
||||
public DateTime BeginTime { get; set; }
|
||||
|
||||
public DateTime EndTime { get; set; }
|
||||
}
|
||||
13
SVSim.Database/Models/MyRotationAbilityEntry.cs
Normal file
13
SVSim.Database/Models/MyRotationAbilityEntry.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
public class MyRotationAbilityEntry : BaseEntity<int>
|
||||
{
|
||||
public int AbilityId { get => Id; set => Id = value; }
|
||||
|
||||
/// <summary>Raw ability blob from /load/index data.my_rotation_info.abilities[abilityId].</summary>
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string Data { get; set; } = "{}";
|
||||
}
|
||||
24
SVSim.Database/Models/MyRotationSettingEntry.cs
Normal file
24
SVSim.Database/Models/MyRotationSettingEntry.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Joins /load/index data.my_rotation_info.{setting, reprinted_base_card_ids, restricted_base_card_id_list}
|
||||
/// on rotation_id. CardSetIdsCsv and AbilitiesCsv mirror the wire's pipe-delimited string format
|
||||
/// (e.g. "10000|10001|10002"); the importer keeps them verbatim.
|
||||
/// </summary>
|
||||
public class MyRotationSettingEntry : BaseEntity<int>
|
||||
{
|
||||
public int RotationId { get => Id; set => Id = value; }
|
||||
|
||||
public string CardSetIdsCsv { get; set; } = string.Empty;
|
||||
|
||||
public string AbilitiesCsv { get; set; } = string.Empty;
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string ReprintedCardIds { get; set; } = "[]";
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string RestrictedCardIds { get; set; } = "[]";
|
||||
}
|
||||
41
SVSim.Database/Models/PreReleaseInfo.cs
Normal file
41
SVSim.Database/Models/PreReleaseInfo.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton row (Id=1) for upcoming card-set pre-release window from /load/index data.pre_release_info.
|
||||
/// Current capture has stale 1900/2019/2020 dates — likely "no active pre-release" sentinel.
|
||||
/// Recapture target during an active pre-release window (typically a week before each new expansion).
|
||||
/// </summary>
|
||||
public class PreReleaseInfo : BaseEntity<int>
|
||||
{
|
||||
public string PreReleaseId { get; set; } = string.Empty;
|
||||
|
||||
public string NextCardSetId { get; set; } = string.Empty;
|
||||
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
public DateTime EndTime { get; set; }
|
||||
|
||||
public DateTime DisplayEndTime { get; set; }
|
||||
|
||||
public DateTime FreeMatchStartTime { get; set; }
|
||||
|
||||
public int CardMasterId { get; set; }
|
||||
|
||||
public string DefaultCardMasterId { get; set; } = string.Empty;
|
||||
|
||||
public string PreReleaseCardMasterId { get; set; } = string.Empty;
|
||||
|
||||
public bool IsPreRotationFreeMatchTerm { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string RotationCardSetIdList { get; set; } = "[]";
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string ReprintedBaseCardIds { get; set; } = "{}";
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string LatestReprintedBaseCardIds { get; set; } = "{}";
|
||||
}
|
||||
12
SVSim.Database/Models/ReprintedCardEntry.cs
Normal file
12
SVSim.Database/Models/ReprintedCardEntry.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Cards currently in the reprinted list from /load/index data.reprinted_base_card_ids.
|
||||
/// References ShadowverseCardEntry.Id but no FK.
|
||||
/// </summary>
|
||||
public class ReprintedCardEntry : BaseEntity<long>
|
||||
{
|
||||
public long CardId { get => Id; set => Id = value; }
|
||||
}
|
||||
33
SVSim.Database/Models/SealedConfig.cs
Normal file
33
SVSim.Database/Models/SealedConfig.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton row (Id=1) for the current Sealed Arena season from /mypage/index data.sealed_info.
|
||||
/// PackInfo jsonb is the int[] of pack set IDs used in the pool.
|
||||
/// </summary>
|
||||
public class SealedConfig : BaseEntity<int>
|
||||
{
|
||||
public int Enable { get; set; }
|
||||
|
||||
public int CrystalCost { get; set; }
|
||||
|
||||
public int RupyCost { get; set; }
|
||||
|
||||
public int TicketCost { get; set; }
|
||||
|
||||
public int DeckUsingNumMin { get; set; }
|
||||
|
||||
public int ScheduleId { get; set; }
|
||||
|
||||
public bool IsJoin { get; set; }
|
||||
|
||||
public bool IsDeckCodeMaintenance { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string PackInfo { get; set; } = "[]";
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public string SalesPeriodInfo { get; set; } = "{}";
|
||||
}
|
||||
14
SVSim.Database/Models/SpotCardEntry.cs
Normal file
14
SVSim.Database/Models/SpotCardEntry.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// One row per rentable "spot card" from /load/index data.spot_cards (dict {card_id: cost}).
|
||||
/// References ShadowverseCardEntry.Id but no FK — bootstrap warns on orphans.
|
||||
/// </summary>
|
||||
public class SpotCardEntry : BaseEntity<long>
|
||||
{
|
||||
public long CardId { get => Id; set => Id = value; }
|
||||
|
||||
public int Cost { get; set; }
|
||||
}
|
||||
15
SVSim.Database/Models/UnlimitedRestrictionEntry.cs
Normal file
15
SVSim.Database/Models/UnlimitedRestrictionEntry.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using SVSim.Database.Common;
|
||||
|
||||
namespace SVSim.Database.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Per-card unlimited-format ban/limit value from /load/index data.unlimited_restricted_base_card_id_list
|
||||
/// (dict {card_id: restriction_value}). RestrictionValue semantics TBD — prod observed {0, 1}; the audit
|
||||
/// flags this as "0 = limit-1? 1 = hard-ban?" pending a client read.
|
||||
/// </summary>
|
||||
public class UnlimitedRestrictionEntry : BaseEntity<long>
|
||||
{
|
||||
public long CardId { get => Id; set => Id = value; }
|
||||
|
||||
public int RestrictionValue { get; set; }
|
||||
}
|
||||
@@ -28,9 +28,81 @@ public class GlobalsRepository : IGlobalsRepository
|
||||
.Include(gc => gc.DefaultEmblem).Include(gc => gc.DefaultDegree).Include(gc => gc.DefaultSleeve).FirstOrDefaultAsync(gc => gc.Id == key) ??
|
||||
new GameConfiguration();
|
||||
}
|
||||
|
||||
|
||||
public async Task<List<RankInfoEntry>> GetRankInfo()
|
||||
{
|
||||
return await _dbContext.Set<RankInfoEntry>().ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Prod-captured globals ----------
|
||||
|
||||
public Task<List<MyRotationSettingEntry>> GetMyRotationSettings() =>
|
||||
_dbContext.MyRotationSettings.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<MyRotationAbilityEntry>> GetMyRotationAbilities() =>
|
||||
_dbContext.MyRotationAbilities.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<AvatarAbilityEntry>> GetAvatarAbilities() =>
|
||||
_dbContext.AvatarAbilities.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<DefaultDeckEntry>> GetDefaultDecks() =>
|
||||
_dbContext.DefaultDecks.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<DefaultLeaderSkinSettingEntry>> GetDefaultLeaderSkinSettings() =>
|
||||
_dbContext.DefaultLeaderSkinSettings.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<ArenaSeasonConfig?> GetCurrentArenaSeason() =>
|
||||
_dbContext.ArenaSeasons.AsNoTracking().FirstOrDefaultAsync(e => e.Id == 1);
|
||||
|
||||
public Task<List<SpotCardEntry>> GetSpotCards() =>
|
||||
_dbContext.SpotCards.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<ReprintedCardEntry>> GetReprintedCards() =>
|
||||
_dbContext.ReprintedCards.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<UnlimitedRestrictionEntry>> GetUnlimitedRestrictions() =>
|
||||
_dbContext.UnlimitedRestrictions.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<LoadingExclusionCardEntry>> GetLoadingExclusionCards() =>
|
||||
_dbContext.LoadingExclusionCards.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<BattlePassLevelEntry>> GetBattlePassLevels() =>
|
||||
_dbContext.BattlePassLevels.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<DailyLoginBonusEntry>> GetDailyLoginBonus() =>
|
||||
_dbContext.DailyLoginBonuses.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<BannerEntry>> GetBanners() =>
|
||||
_dbContext.Banners.AsNoTracking().OrderBy(b => b.Id).ToListAsync();
|
||||
|
||||
public Task<ColosseumConfig?> GetCurrentColosseum() =>
|
||||
_dbContext.Colosseums.AsNoTracking().FirstOrDefaultAsync(e => e.Id == 1);
|
||||
|
||||
public Task<SealedConfig?> GetCurrentSealedSeason() =>
|
||||
_dbContext.SealedSeasons.AsNoTracking().FirstOrDefaultAsync(e => e.Id == 1);
|
||||
|
||||
/// <summary>Returns the master-point ranking period whose EndTime is in the future, or the latest by EndTime as fallback.</summary>
|
||||
public async Task<MasterPointRankingPeriodEntry?> GetCurrentMasterPointPeriod()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return await _dbContext.MasterPointRankingPeriods.AsNoTracking()
|
||||
.Where(p => p.EndTime >= now)
|
||||
.OrderBy(p => p.EndTime)
|
||||
.FirstOrDefaultAsync()
|
||||
?? await _dbContext.MasterPointRankingPeriods.AsNoTracking()
|
||||
.OrderByDescending(p => p.EndTime)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public Task<List<MaintenanceCardEntry>> GetMaintenanceCards() =>
|
||||
_dbContext.MaintenanceCards.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<List<FeatureMaintenanceEntry>> GetFeatureMaintenances() =>
|
||||
_dbContext.FeatureMaintenances.AsNoTracking().ToListAsync();
|
||||
|
||||
public Task<PreReleaseInfo?> GetPreReleaseInfo() =>
|
||||
_dbContext.PreReleaseInfos.AsNoTracking().FirstOrDefaultAsync(e => e.Id == 1);
|
||||
|
||||
public Task<List<ShadowverseCardSetEntry>> GetRotationCardSets() =>
|
||||
_dbContext.CardSets.AsNoTracking().Where(s => s.IsInRotation).ToListAsync();
|
||||
}
|
||||
|
||||
@@ -8,4 +8,26 @@ public interface IGlobalsRepository
|
||||
Task<List<BattlefieldEntry>> GetBattlefields(bool onlyOpen);
|
||||
Task<GameConfiguration> GetGameConfiguration(string key);
|
||||
Task<List<RankInfoEntry>> GetRankInfo();
|
||||
}
|
||||
|
||||
// Prod-captured globals — populated by SVSim.Bootstrap.GlobalsImporter.
|
||||
Task<List<MyRotationSettingEntry>> GetMyRotationSettings();
|
||||
Task<List<MyRotationAbilityEntry>> GetMyRotationAbilities();
|
||||
Task<List<AvatarAbilityEntry>> GetAvatarAbilities();
|
||||
Task<List<DefaultDeckEntry>> GetDefaultDecks();
|
||||
Task<List<DefaultLeaderSkinSettingEntry>> GetDefaultLeaderSkinSettings();
|
||||
Task<ArenaSeasonConfig?> GetCurrentArenaSeason();
|
||||
Task<List<SpotCardEntry>> GetSpotCards();
|
||||
Task<List<ReprintedCardEntry>> GetReprintedCards();
|
||||
Task<List<UnlimitedRestrictionEntry>> GetUnlimitedRestrictions();
|
||||
Task<List<LoadingExclusionCardEntry>> GetLoadingExclusionCards();
|
||||
Task<List<BattlePassLevelEntry>> GetBattlePassLevels();
|
||||
Task<List<DailyLoginBonusEntry>> GetDailyLoginBonus();
|
||||
Task<List<BannerEntry>> GetBanners();
|
||||
Task<ColosseumConfig?> GetCurrentColosseum();
|
||||
Task<SealedConfig?> GetCurrentSealedSeason();
|
||||
Task<MasterPointRankingPeriodEntry?> GetCurrentMasterPointPeriod();
|
||||
Task<List<MaintenanceCardEntry>> GetMaintenanceCards();
|
||||
Task<List<FeatureMaintenanceEntry>> GetFeatureMaintenances();
|
||||
Task<PreReleaseInfo?> GetPreReleaseInfo();
|
||||
Task<List<ShadowverseCardSetEntry>> GetRotationCardSets();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,28 @@ public class SVSimDbContext : DbContext
|
||||
|
||||
public DbSet<GameConfiguration> GameConfigurations => Set<GameConfiguration>();
|
||||
|
||||
// Prod-captured globals — populated by SVSim.Bootstrap, not HasData. See
|
||||
// docs/audits/prod-data-capture-strategy-2026-05-23.md.
|
||||
public DbSet<MyRotationSettingEntry> MyRotationSettings => Set<MyRotationSettingEntry>();
|
||||
public DbSet<MyRotationAbilityEntry> MyRotationAbilities => Set<MyRotationAbilityEntry>();
|
||||
public DbSet<AvatarAbilityEntry> AvatarAbilities => Set<AvatarAbilityEntry>();
|
||||
public DbSet<DefaultDeckEntry> DefaultDecks => Set<DefaultDeckEntry>();
|
||||
public DbSet<DefaultLeaderSkinSettingEntry> DefaultLeaderSkinSettings => Set<DefaultLeaderSkinSettingEntry>();
|
||||
public DbSet<ArenaSeasonConfig> ArenaSeasons => Set<ArenaSeasonConfig>();
|
||||
public DbSet<SpotCardEntry> SpotCards => Set<SpotCardEntry>();
|
||||
public DbSet<ReprintedCardEntry> ReprintedCards => Set<ReprintedCardEntry>();
|
||||
public DbSet<UnlimitedRestrictionEntry> UnlimitedRestrictions => Set<UnlimitedRestrictionEntry>();
|
||||
public DbSet<LoadingExclusionCardEntry> LoadingExclusionCards => Set<LoadingExclusionCardEntry>();
|
||||
public DbSet<BattlePassLevelEntry> BattlePassLevels => Set<BattlePassLevelEntry>();
|
||||
public DbSet<DailyLoginBonusEntry> DailyLoginBonuses => Set<DailyLoginBonusEntry>();
|
||||
public DbSet<BannerEntry> Banners => Set<BannerEntry>();
|
||||
public DbSet<ColosseumConfig> Colosseums => Set<ColosseumConfig>();
|
||||
public DbSet<SealedConfig> SealedSeasons => Set<SealedConfig>();
|
||||
public DbSet<MasterPointRankingPeriodEntry> MasterPointRankingPeriods => Set<MasterPointRankingPeriodEntry>();
|
||||
public DbSet<MaintenanceCardEntry> MaintenanceCards => Set<MaintenanceCardEntry>();
|
||||
public DbSet<FeatureMaintenanceEntry> FeatureMaintenances => Set<FeatureMaintenanceEntry>();
|
||||
public DbSet<PreReleaseInfo> PreReleaseInfos => Set<PreReleaseInfo>();
|
||||
|
||||
#endregion
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SVSim.Bootstrap.Importers;
|
||||
using SVSim.Database;
|
||||
using SVSim.Database.Enums;
|
||||
using SVSim.Database.Models;
|
||||
@@ -139,6 +140,21 @@ internal sealed class SVSimTestFactory : WebApplicationFactory<Program>
|
||||
return viewerId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs <see cref="GlobalsImporter"/> against the test SQLite DB using the prod captures
|
||||
/// copied into the test output dir (see SVSim.UnitTests.csproj Content Include for
|
||||
/// Data/prod-captures). Idempotent — safe to call multiple times per factory. Tests that
|
||||
/// depend on prod-shaped global content (spot_cards, avatar abilities, etc.) call this once
|
||||
/// during setup; the rest of the test runs against whatever the importer populated.
|
||||
/// </summary>
|
||||
public async Task SeedGlobalsAsync(string? capturesDir = null)
|
||||
{
|
||||
capturesDir ??= Path.Combine(AppContext.BaseDirectory, "Data", "prod-captures");
|
||||
using var scope = Services.CreateScope();
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||
await new GlobalsImporter().ImportAllAsync(ctx, capturesDir);
|
||||
}
|
||||
|
||||
/// <summary>Convenience: bake the X-Test-Viewer-Id header into a fresh client.</summary>
|
||||
public HttpClient CreateAuthenticatedClient(long viewerId)
|
||||
{
|
||||
|
||||
252
SVSim.UnitTests/Repositories/GlobalsRepositoryTests.cs
Normal file
252
SVSim.UnitTests/Repositories/GlobalsRepositoryTests.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SVSim.Database.Repositories.Globals;
|
||||
using SVSim.UnitTests.Infrastructure;
|
||||
|
||||
namespace SVSim.UnitTests.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end tests for the prod-captured globals path: SeedGlobalsAsync invokes
|
||||
/// SVSim.Bootstrap.GlobalsImporter against the test SQLite DB, then we verify each
|
||||
/// IGlobalsRepository method returns the expected count + a spot-checked field value.
|
||||
///
|
||||
/// Counts come from the 2026-05-23 prod capture; if a recapture lands with different cardinalities,
|
||||
/// expect to update these assertions.
|
||||
/// </summary>
|
||||
public class GlobalsRepositoryTests
|
||||
{
|
||||
private static async Task<(SVSimTestFactory factory, IGlobalsRepository repo)> SetupAsync()
|
||||
{
|
||||
var factory = new SVSimTestFactory();
|
||||
await factory.SeedGlobalsAsync();
|
||||
var scope = factory.Services.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<IGlobalsRepository>();
|
||||
return (factory, repo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetMyRotationSettings_returns_27_entries_from_capture()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var settings = await repo.GetMyRotationSettings();
|
||||
Assert.That(settings.Count, Is.EqualTo(27));
|
||||
var tsRotation = settings.FirstOrDefault(s => s.Id == 10015);
|
||||
Assert.That(tsRotation, Is.Not.Null, "Expected to find ts_rotation_id=10015 in seeded data.");
|
||||
Assert.That(tsRotation!.CardSetIdsCsv, Does.Contain("10015"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetMyRotationAbilities_returns_6_entries()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var abilities = await repo.GetMyRotationAbilities();
|
||||
Assert.That(abilities.Count, Is.EqualTo(6));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetAvatarAbilities_returns_24_entries_with_skill_dsl_preserved()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var abilities = await repo.GetAvatarAbilities();
|
||||
Assert.That(abilities.Count, Is.EqualTo(24));
|
||||
var avatar2801 = abilities.FirstOrDefault(a => a.Id == 2801);
|
||||
Assert.That(avatar2801, Is.Not.Null);
|
||||
Assert.That(avatar2801!.BattleStartMaxLife, Is.EqualTo(25));
|
||||
Assert.That(avatar2801.Ability, Does.Contain("skill:"),
|
||||
"Avatar ability DSL string should be preserved verbatim.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCurrentArenaSeason_returns_singleton_with_format_info()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var arena = await repo.GetCurrentArenaSeason();
|
||||
Assert.That(arena, Is.Not.Null);
|
||||
Assert.That(arena!.Mode, Is.EqualTo(1));
|
||||
Assert.That(arena.Enable, Is.EqualTo(1));
|
||||
Assert.That(arena.FormatInfo, Does.Contain("Take Two"),
|
||||
"Current 2pick season FormatInfo should preserve card_pool_name verbatim.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetBattlePassLevels_returns_100_levels()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var levels = await repo.GetBattlePassLevels();
|
||||
Assert.That(levels.Count, Is.EqualTo(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetDailyLoginBonus_returns_three_skeleton_entries()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var bonuses = await repo.GetDailyLoginBonus();
|
||||
Assert.That(bonuses.Count, Is.EqualTo(3),
|
||||
"Prod capture has keys {1, 3, 4} with empty arrays — skeleton presence still seeded.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetPreReleaseInfo_returns_singleton()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var pri = await repo.GetPreReleaseInfo();
|
||||
Assert.That(pri, Is.Not.Null);
|
||||
// Prod capture has stale 1900/2019 dates; the audit flags this as a recapture target.
|
||||
Assert.That(pri!.PreReleaseId, Is.EqualTo("1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetSpotCards_returns_239_entries()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var spots = await repo.GetSpotCards();
|
||||
Assert.That(spots.Count, Is.EqualTo(239));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetReprintedCards_returns_54_entries()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var reprinted = await repo.GetReprintedCards();
|
||||
Assert.That(reprinted.Count, Is.EqualTo(54));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetUnlimitedRestrictions_returns_3_entries_with_values()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var bans = await repo.GetUnlimitedRestrictions();
|
||||
Assert.That(bans.Count, Is.EqualTo(3));
|
||||
var hardBan = bans.FirstOrDefault(r => r.Id == 107813030);
|
||||
Assert.That(hardBan, Is.Not.Null);
|
||||
Assert.That(hardBan!.RestrictionValue, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetLoadingExclusionCards_returns_176_entries()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var excl = await repo.GetLoadingExclusionCards();
|
||||
Assert.That(excl.Count, Is.EqualTo(176));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetBanners_returns_4_entries_in_order()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var banners = await repo.GetBanners();
|
||||
Assert.That(banners.Count, Is.EqualTo(4));
|
||||
Assert.That(banners[0].ImageName, Is.EqualTo("banner_000788"));
|
||||
Assert.That(banners[0].Click, Is.EqualTo("account_transition_with_two"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCurrentColosseum_returns_singleton_with_name()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var col = await repo.GetCurrentColosseum();
|
||||
Assert.That(col, Is.Not.Null);
|
||||
Assert.That(col!.ColosseumName, Is.EqualTo("Rivenbrandt Take Two Cup"));
|
||||
Assert.That(col.ColosseumId, Is.EqualTo("165"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCurrentSealedSeason_returns_singleton_with_pack_info()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var sealedSeason = await repo.GetCurrentSealedSeason();
|
||||
Assert.That(sealedSeason, Is.Not.Null);
|
||||
Assert.That(sealedSeason!.CrystalCost, Is.EqualTo(600));
|
||||
Assert.That(sealedSeason.DeckUsingNumMin, Is.EqualTo(30));
|
||||
Assert.That(sealedSeason.PackInfo, Does.Contain("10032"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetCurrentMasterPointPeriod_returns_period_119()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var period = await repo.GetCurrentMasterPointPeriod();
|
||||
Assert.That(period, Is.Not.Null);
|
||||
Assert.That(period!.Id, Is.EqualTo(119));
|
||||
Assert.That(period.PeriodNum, Is.EqualTo(118));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetDefaultDecks_returns_8_starter_decks_one_per_class()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var decks = await repo.GetDefaultDecks();
|
||||
Assert.That(decks.Count, Is.EqualTo(8));
|
||||
// Each starter deck packs 40 card IDs in card_id_array (jsonb).
|
||||
Assert.That(decks.All(d => d.CardIdArray.Contains(",")), Is.True,
|
||||
"Each starter deck should serialize multiple card IDs in card_id_array.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetDefaultLeaderSkinSettings_returns_8_entries()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var skins = await repo.GetDefaultLeaderSkinSettings();
|
||||
Assert.That(skins.Count, Is.EqualTo(8));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetGameConfiguration_default_has_seeded_ts_rotation_id()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var cfg = await repo.GetGameConfiguration("default");
|
||||
Assert.That(cfg, Is.Not.Null);
|
||||
Assert.That(cfg.TsRotationId, Is.EqualTo("10015"),
|
||||
"GlobalsImporter should overwrite the migration's empty-string default with the capture value.");
|
||||
Assert.That(cfg.IsBattlePassPeriod, Is.True,
|
||||
"Prod sends bool true for is_battle_pass_period; capture should overwrite the migration default of false.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetMaintenanceCards_empty_when_capture_has_none()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var cards = await repo.GetMaintenanceCards();
|
||||
Assert.That(cards.Count, Is.EqualTo(0),
|
||||
"Prod capture has empty maintenance_card_list; importer should not create skeleton rows.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetFeatureMaintenances_empty_when_capture_has_none()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
var feats = await repo.GetFeatureMaintenances();
|
||||
Assert.That(feats.Count, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetRotationCardSets_flags_six_sets_in_rotation()
|
||||
{
|
||||
var (factory, repo) = await SetupAsync();
|
||||
using var _ = factory;
|
||||
// CardImport isn't run in tests, so the CardSets table is empty. UpdateRotationCardSetFlags
|
||||
// can only mark rows that exist — verify the importer's "missing-id" warning didn't crash.
|
||||
var sets = await repo.GetRotationCardSets();
|
||||
Assert.That(sets.Count, Is.LessThanOrEqualTo(6),
|
||||
"Without CardImport, no rotation flags can be set; expect 0. With CardImport, expect 6.");
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,22 @@
|
||||
<ProjectReference Include="..\SVSim.EmulatedEntrypoint\SVSim.EmulatedEntrypoint.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SVSim.Bootstrap\SVSim.Bootstrap.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- BaseDataSeeder reads CSVs from the runtime "Data" folder; mirror them into the test
|
||||
output so HasData seeding fires when EnsureCreated builds the SQLite schema. -->
|
||||
<Content Include="..\SVSim.EmulatedEntrypoint\Data\*.csv" Link="Data\%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- Tests that call SVSimTestFactory.SeedGlobalsAsync() need the prod captures available in
|
||||
the test output dir. Mirror them from SVSim.Bootstrap so the same files drive both
|
||||
production bootstrap and test seeding. -->
|
||||
<Content Include="..\SVSim.Bootstrap\Data\prod-captures\*.json" Link="Data\prod-captures\%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user