From d44ecd21d35631b053b64fd20d19973ee9e7f72b Mon Sep 17 00:00:00 2001 From: squaresmile Date: Wed, 29 Nov 2023 20:26:54 -0500 Subject: [PATCH] Added stage cutin --- app/core/nice/quest.py | 55 +++++++-- app/core/nice/stage_cutin.py | 46 +++++++ app/db/helpers/rayshift.py | 227 ++++++++++++++++++++++++++++++++++- app/schemas/nice.py | 12 ++ app/schemas/rayshift.py | 6 + 5 files changed, 332 insertions(+), 14 deletions(-) create mode 100644 app/core/nice/stage_cutin.py diff --git a/app/core/nice/quest.py b/app/core/nice/quest.py index 750c1285..e71d7f1a 100644 --- a/app/core/nice/quest.py +++ b/app/core/nice/quest.py @@ -13,8 +13,11 @@ from ...db.helpers.quest import get_questSelect_container from ...db.helpers.rayshift import ( get_all_quest_hashes, + get_cutin_drops, + get_cutin_skills, get_rayshift_drops, get_war_board_quest_details, + quest_has_cutins, ) from ...rayshift.quest import get_quest_detail from ...redis import Redis @@ -47,6 +50,7 @@ NiceQuestReleaseOverwrite, NiceRestriction, NiceStage, + NiceStageCutIn, NiceStageStartMovie, QuestEnemy, SupportServant, @@ -78,6 +82,7 @@ from .follower import get_nice_support_servants from .gift import get_nice_gift from .item import get_nice_item_amount, get_nice_item_from_raw +from .stage_cutin import get_quest_stage_cutins settings = Settings() @@ -169,6 +174,7 @@ def get_nice_stage( enemies: list[QuestEnemy], bgms: list[MstBgm], waveStartMovies: dict[int, list[NiceStageStartMovie]], + stage_cutins: dict[int, NiceStageCutIn], lang: Language, ) -> NiceStage: filtered_bgms = [bgm for bgm in bgms if bgm.id == raw_stage.bgmId] @@ -190,6 +196,7 @@ def get_nice_stage( else None, NoEntryIds=raw_stage.script.get("NoEntryIds"), waveStartMovies=waveStartMovies.get(raw_stage.wave, []), + cutin=stage_cutins.get(raw_stage.wave, None), originalScript=raw_stage.script or {}, enemies=enemies, ) @@ -509,6 +516,7 @@ def set_follower_data(followers: dict[int, QuestEnemy] | None) -> None: nice_quest_drops: list[EnemyDrop] = [] quest_enemies = QuestEnemies(enemy_waves=[[]] * len(db_data.raw.mstStage)) all_rayshift_hashes: list[str] = [] + stage_cutins: dict[int, NiceStageCutIn] = {} if stages: rayshift_quest_id = quest_id @@ -535,6 +543,36 @@ def set_follower_data(followers: dict[int, QuestEnemy] | None) -> None: elif region == Region.NA: min_query_id = 1062363 # 2022-07-04 09:00:00 UTC + rayshift_query_questHash = ( + None if db_data.raw.mstQuest.type == QuestType.WAR_BOARD else questHash + ) + + runs_with_cutin = await quest_has_cutins( + conn=conn, + quest_id=rayshift_quest_id, + phase=phase, + questSelect=questSelect, + questHash=rayshift_query_questHash, + min_query_id=min_query_id, + ) + + rayshift_kwargs = { + "conn": conn, + "quest_id": rayshift_quest_id, + "phase": phase, + "questSelect": questSelect, + "questHash": rayshift_query_questHash, + "min_query_id": min_query_id, + } + if runs_with_cutin: + cutin_skills, cutin_drops = await asyncio.gather( + get_cutin_skills(**rayshift_kwargs), # type:ignore + get_cutin_drops(**rayshift_kwargs, runs=runs_with_cutin), # type:ignore + ) + stage_cutins = await get_quest_stage_cutins( + conn, region, runs_with_cutin, cutin_drops, cutin_skills, lang + ) + if db_data.raw.mstQuest.type == QuestType.WAR_BOARD: quest_enemy_coro = get_war_board_quest_details(conn, quest_id, phase) else: @@ -554,14 +592,7 @@ def set_follower_data(followers: dict[int, QuestEnemy] | None) -> None: all_rayshift_hashes, ) = await asyncio.gather( quest_enemy_coro, - get_rayshift_drops( - conn, - rayshift_quest_id, - phase, - questSelect, - None if db_data.raw.mstQuest.type == QuestType.WAR_BOARD else questHash, - min_query_id, - ), + get_rayshift_drops(**rayshift_kwargs), # type:ignore get_all_quest_hashes(conn, rayshift_quest_id, phase, questSelect), ) @@ -625,7 +656,13 @@ def set_follower_data(followers: dict[int, QuestEnemy] | None) -> None: new_nice_stages = [ get_nice_stage( - region, stage, enemies, db_data.raw.mstBgm, waveStartMovies, lang + region, + stage, + enemies, + db_data.raw.mstBgm, + waveStartMovies, + stage_cutins, + lang, ) for stage, enemies in zip(stages, quest_enemies.enemy_waves, strict=False) ] diff --git a/app/core/nice/stage_cutin.py b/app/core/nice/stage_cutin.py new file mode 100644 index 00000000..74a47629 --- /dev/null +++ b/app/core/nice/stage_cutin.py @@ -0,0 +1,46 @@ +from sqlalchemy.ext.asyncio import AsyncConnection + +from ...schemas.common import Language, Region +from ...schemas.nice import NiceStageCutIn, NiceStageCutInSkill +from ...schemas.rayshift import CutInSkill, QuestDrop +from .enemy import get_nice_drop +from .skill import MultipleNiceSkills, SkillSvt, get_multiple_nice_skills + + +def get_nice_stage_cut_in( + runs: int, + stage: int, + cutin_drops: list[QuestDrop], + cutin_skills: list[CutInSkill], + all_skills: MultipleNiceSkills, +) -> NiceStageCutIn: + skills = [ + NiceStageCutInSkill( + skill=all_skills[SkillSvt(skill.skill_id, 0)], + appearCount=skill.appear_count, + ) + for skill in cutin_skills + if skill.stage == stage + ] + + drops = [get_nice_drop(drop) for drop in cutin_drops if drop.stage == stage] + + return NiceStageCutIn(runs=runs, skills=skills, drops=drops) + + +async def get_quest_stage_cutins( + conn: AsyncConnection, + region: Region, + runs: int, + cutin_drops: list[QuestDrop], + cutin_skills: list[CutInSkill], + lang: Language = Language.jp, +) -> dict[int, NiceStageCutIn]: + all_skill_ids = [SkillSvt(skill.skill_id, 0) for skill in cutin_skills] + all_skills = await get_multiple_nice_skills(conn, region, all_skill_ids, lang) + + return { + stage: get_nice_stage_cut_in(runs, stage, cutin_drops, cutin_skills, all_skills) + for stage in {drop.stage for drop in cutin_drops} + | {skill.stage for skill in cutin_skills} + } diff --git a/app/db/helpers/rayshift.py b/app/db/helpers/rayshift.py index 76512165..db11cba4 100644 --- a/app/db/helpers/rayshift.py +++ b/app/db/helpers/rayshift.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Any, Optional from typing import cast as typing_cast @@ -6,12 +7,13 @@ from sqlalchemy.engine import Connection from sqlalchemy.ext.asyncio import AsyncConnection from sqlalchemy.sql import Join, and_, case, cast, false, func, or_, select +from sqlalchemy.sql._typing import _ColumnExpressionArgument from sqlalchemy.sql.elements import literal_column from sqlalchemy.sql.expression import text from ...core.rayshift import get_quest_enemy_hash from ...models.rayshift import rayshiftQuest, rayshiftQuestHash -from ...schemas.rayshift import QuestDetail, QuestDrop, QuestList +from ...schemas.rayshift import CutInSkill, QuestDetail, QuestDrop, QuestList from .utils import fetch_one @@ -121,16 +123,21 @@ async def get_all_quest_hashes( return [str(row.questHash) for row in rows] -async def get_rayshift_drops( - conn: AsyncConnection, +@dataclass +class RayshiftSelect: + select_from: Table | Join + where_conds: list[_ColumnExpressionArgument[bool]] + + +def get_rayshift_select( quest_id: int, phase: int, questSelect: list[int], questHash: str | None = None, min_query_id: int | None = None, -) -> list[QuestDrop]: +) -> RayshiftSelect: select_from: Table | Join = rayshiftQuest - where_conds = [ + where_conds: list[_ColumnExpressionArgument[bool]] = [ rayshiftQuest.c.questId == quest_id, rayshiftQuest.c.phase == phase, rayshiftQuest.c.questDetail.isnot(None), @@ -146,6 +153,27 @@ async def get_rayshift_drops( if questSelect: where_conds.append(quest_select_or(questSelect)) + return RayshiftSelect(select_from=select_from, where_conds=where_conds) + + +async def get_rayshift_drops( + conn: AsyncConnection, + quest_id: int, + phase: int, + questSelect: list[int], + questHash: str | None = None, + min_query_id: int | None = None, +) -> list[QuestDrop]: + select_detail = get_rayshift_select( + quest_id=quest_id, + phase=phase, + questSelect=questSelect, + questHash=questHash, + min_query_id=min_query_id, + ) + select_from = select_detail.select_from + where_conds = select_detail.where_conds + enemy_deck_svt = ( select( rayshiftQuest.c.queryId, @@ -326,6 +354,195 @@ async def get_rayshift_drops( return [QuestDrop.from_orm(row) for row in results.fetchall()] +async def quest_has_cutins( + conn: AsyncConnection, + quest_id: int, + phase: int, + questSelect: list[int], + questHash: str | None = None, + min_query_id: int | None = None, +) -> int | None: + select_detail = get_rayshift_select( + quest_id=quest_id, + phase=phase, + questSelect=questSelect, + questHash=questHash, + min_query_id=min_query_id, + ) + + stmt = ( + select(func.count(rayshiftQuest.c.queryId).label("query_count")) + .select_from(select_detail.select_from) + .where( + and_( + *select_detail.where_conds, + rayshiftQuest.c.questDetail["stageCutins"].is_not(None), + rayshiftQuest.c.questDetail["stageCutins"] != cast("null", JSONB), + func.jsonb_array_length(rayshiftQuest.c.questDetail["stageCutins"]) > 0, + ) + ) + ) + + row = (await conn.execute(stmt)).fetchone() + if row: + return int(row.query_count) + else: + return False + + +async def get_cutin_skills( + conn: AsyncConnection, + quest_id: int, + phase: int, + questSelect: list[int], + questHash: str | None = None, + min_query_id: int | None = None, +) -> list[CutInSkill]: + select_detail = get_rayshift_select( + quest_id=quest_id, + phase=phase, + questSelect=questSelect, + questHash=questHash, + min_query_id=min_query_id, + ) + + cutins = ( + select( + cast( + func.jsonb_array_elements(rayshiftQuest.c.questDetail["stageCutins"]), + JSONB, + ).label("stage_cutin") + ) + .select_from(select_detail.select_from) + .where(and_(*select_detail.where_conds)) + .cte(name="cutins") + ) + + skill_list = ( + select( + cast(cutins.c.stage_cutin["wave"], Integer).label("stage"), + cast(cutins.c.stage_cutin["skillId"], Integer).label("skill_id"), + ) + .select_from(cutins) + .cte(name="skill_list") + ) + + stmt = ( + select( + skill_list.c.stage, + skill_list.c.skill_id, + func.count(skill_list.c.stage).label("appear_count"), + ) + .select_from(skill_list) + .group_by(skill_list.c.stage, skill_list.c.skill_id) + .order_by(skill_list.c.stage, skill_list.c.skill_id) + ) + + rows = (await conn.execute(stmt)).fetchall() + return [CutInSkill.from_orm(row) for row in rows] + + +async def get_cutin_drops( + conn: AsyncConnection, + quest_id: int, + phase: int, + questSelect: list[int], + questHash: str | None = None, + min_query_id: int | None = None, + runs: int | None = None, +) -> list[QuestDrop]: + select_detail = get_rayshift_select( + quest_id=quest_id, + phase=phase, + questSelect=questSelect, + questHash=questHash, + min_query_id=min_query_id, + ) + + cutins = ( + select( + rayshiftQuest.c.queryId, + cast( + func.jsonb_array_elements(rayshiftQuest.c.questDetail["stageCutins"]), + JSONB, + ).label("stage_cutin"), + ) + .select_from(select_detail.select_from) + .where(and_(*select_detail.where_conds)) + .cte(name="cutins") + ) + + drops = ( + select( + cutins.c.queryId, + cast(cutins.c.stage_cutin["wave"], Integer).label("stage"), + cast( + func.jsonb_array_elements(cutins.c.stage_cutin["dropInfos"]), JSONB + ).label("drops"), + ) + .select_from(cutins) + .cte(name="drop_infos") + ) + + all_drops = select( + drops.c.queryId, + drops.c.stage, + drops.c.drops["type"].label("type"), + drops.c.drops["objectId"].label("objectId"), + drops.c.drops["originalNum"].label("originalNum"), + ).cte(name="all_drops") + + run_drop_items = ( + select( + all_drops.c.queryId, + all_drops.c.stage, + all_drops.c.type, + all_drops.c.objectId, + all_drops.c.originalNum, + func.count(all_drops.c.objectId).label("dropCount"), + ) + .group_by( + all_drops.c.queryId, + all_drops.c.stage, + all_drops.c.type, + all_drops.c.objectId, + all_drops.c.originalNum, + ) + .cte(name="run_drop_items") + ) + + run_drops = ( + select( + run_drop_items.c.stage, + literal_column("'enemy'").label("deckType"), + literal_column("-2").label("deckId"), + run_drop_items.c.type, + run_drop_items.c.objectId, + run_drop_items.c.originalNum, + literal_column(f"{runs if runs else -2}").label("runs"), + func.sum(run_drop_items.c.dropCount).label("dropCount"), + cast(func.sum(func.power(run_drop_items.c.dropCount, 2)), BIGINT).label( + "sumDropCountSquared" + ), + ) + .group_by( + run_drop_items.c.stage, + run_drop_items.c.type, + run_drop_items.c.objectId, + run_drop_items.c.originalNum, + ) + .order_by( + run_drop_items.c.stage, + run_drop_items.c.type, + run_drop_items.c.objectId, + run_drop_items.c.originalNum, + ) + ) + + rows = (await conn.execute(run_drops)).fetchall() + return [QuestDrop.from_orm(row) for row in rows] + + insert_quest_stmt = insert(rayshiftQuest) do_update_quest_stmt = insert_quest_stmt.on_conflict_do_update( index_elements=[rayshiftQuest.c.queryId], diff --git a/app/schemas/nice.py b/app/schemas/nice.py index 0ecb67b8..b4de7385 100644 --- a/app/schemas/nice.py +++ b/app/schemas/nice.py @@ -2356,6 +2356,17 @@ class NiceStageStartMovie(BaseModelORJson): waveStartMovie: HttpUrl +class NiceStageCutInSkill(BaseModelORJson): + skill: NiceSkill + appearCount: int + + +class NiceStageCutIn(BaseModelORJson): + runs: int + skills: list[NiceStageCutInSkill] + drops: list[EnemyDrop] + + class NiceStage(BaseModelORJson): wave: int bgm: NiceBgm @@ -2369,6 +2380,7 @@ class NiceStage(BaseModelORJson): ) NoEntryIds: list[int] | None = None waveStartMovies: list[NiceStageStartMovie] = [] + cutin: NiceStageCutIn | None = None originalScript: dict[str, Any] enemies: list[QuestEnemy] = [] diff --git a/app/schemas/rayshift.py b/app/schemas/rayshift.py index c7136397..03e7a158 100644 --- a/app/schemas/rayshift.py +++ b/app/schemas/rayshift.py @@ -194,3 +194,9 @@ class QuestDrop(BaseModelORJson): runs: int dropCount: int sumDropCountSquared: int + + +class CutInSkill(BaseModelORJson): + stage: int + skill_id: int + appear_count: int