From 93791727911fc072463a405dc69790908c9f3f1a Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 1 Apr 2024 20:19:37 +0200 Subject: [PATCH] idac: battle gift event, tips, QoL improvements added --- ...89c9b02_idac_battle_gift_and_tips_added.py | 83 ++++ docs/game_specific_info.md | 15 +- example_config/idac.yaml | 8 + titles/idac/config.py | 38 ++ titles/idac/data/battle_gift/touhou_1st.json | 82 ++++ .../touhou_flandre_scarlet.json | 0 .../touhou_remilia_scarlet.json | 0 .../touhou_sakuya_izayoi.json | 0 .../timetrial/touhou_flandre_scarlet.json | 1 - .../timetrial/touhou_remilia_scarlet.json | 1 - .../data/timetrial/touhou_sakuya_izayoi.json | 1 - titles/idac/schema/item.py | 46 ++ titles/idac/schema/profile.py | 52 +++ titles/idac/season2.py | 395 +++++++++++++++--- 14 files changed, 669 insertions(+), 53 deletions(-) create mode 100644 core/data/alembic/versions/e4e8d89c9b02_idac_battle_gift_and_tips_added.py create mode 100644 titles/idac/data/battle_gift/touhou_1st.json rename titles/idac/data/{stamps => stamp}/touhou_flandre_scarlet.json (100%) rename titles/idac/data/{stamps => stamp}/touhou_remilia_scarlet.json (100%) rename titles/idac/data/{stamps => stamp}/touhou_sakuya_izayoi.json (100%) diff --git a/core/data/alembic/versions/e4e8d89c9b02_idac_battle_gift_and_tips_added.py b/core/data/alembic/versions/e4e8d89c9b02_idac_battle_gift_and_tips_added.py new file mode 100644 index 0000000..015835f --- /dev/null +++ b/core/data/alembic/versions/e4e8d89c9b02_idac_battle_gift_and_tips_added.py @@ -0,0 +1,83 @@ +"""IDAC Battle Gift and Tips added + +Revision ID: e4e8d89c9b02 +Revises: 81e44dd6047a +Create Date: 2024-04-01 17:49:50.009718 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "e4e8d89c9b02" +down_revision = "81e44dd6047a" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "idac_user_battle_gift", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user", sa.Integer(), nullable=False), + sa.Column("battle_gift_event_id", sa.Integer(), nullable=True), + sa.Column("gift_id", sa.Integer(), nullable=True), + sa.Column("gift_status", sa.Integer(), nullable=True), + sa.Column( + "received_date", + sa.TIMESTAMP(), + server_default=sa.text("now()"), + nullable=True, + ), + sa.ForeignKeyConstraint( + ["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade" + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint( + "user", "battle_gift_event_id", "gift_id", name="idac_user_battle_gift_uk" + ), + mysql_charset="utf8mb4", + ) + + op.create_table( + "idac_profile_tips", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user", sa.Integer(), nullable=False), + sa.Column("version", sa.Integer(), nullable=False), + sa.Column( + "tips_list", + sa.String(length=16), + server_default="QAAAAAAAAAAAAAAA", + nullable=True, + ), + sa.Column( + "timetrial_play_count", sa.Integer(), server_default="0", nullable=True + ), + sa.Column("story_play_count", sa.Integer(), server_default="0", nullable=True), + sa.Column( + "store_battle_play_count", sa.Integer(), server_default="0", nullable=True + ), + sa.Column( + "online_battle_play_count", sa.Integer(), server_default="0", nullable=True + ), + sa.Column( + "special_play_count", sa.Integer(), server_default="0", nullable=True + ), + sa.Column( + "challenge_play_count", sa.Integer(), server_default="0", nullable=True + ), + sa.Column("theory_play_count", sa.Integer(), server_default="0", nullable=True), + sa.ForeignKeyConstraint( + ["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade" + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("user", "version", name="idac_profile_tips_uk"), + mysql_charset="utf8mb4", + ) + + +def downgrade(): + op.drop_table("idac_user_battle_gift") + op.drop_table("idac_profile_tips") diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index fcb6128..633c7ca 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -775,7 +775,7 @@ python dbutils.py --game SDGT upgrade | Display ID | Description | | ---------------------------------------- | ------------------------------------------------------------------------------------------------- | -| 1 | ADV image in the size 1280x720, shown during attract | +| 1 | ADV image in the size 1920x1080, shown during attract | | 2 | Start image in the size 1280x720, shown in the Main Menu after selection the corresponding banner | | 3 | Banner image in the size 640×120, shown in the Main Menu | | 5 | Stamp Background image in the size 1780x608 | @@ -809,6 +809,19 @@ python dbutils.py --game SDGT upgrade }, ``` +### Battle Gift + +- `gift_id`: unique gift index (starts from 0 f.e.) +- `battle_gift_event_id`: unique event id +- `reward_category`: item category (f.e. 21 = Chat Stamp) +- `reward_type`: item id (f.e. 483 = Remilia Scarlet) +- `reward_name`: name of the reward +- `rarity`: 2 Golden, 1 Silver, 3 Bronze +- `cash_rate`: ? +- `customize_point_rate`: ? +- `avatar_point_rate`: ? +- `first_distribution_rate`: only used server side for the probability of the first distribution + ### Credits: - Bottersnike: For the HUGE Reverse Engineering help diff --git a/example_config/idac.yaml b/example_config/idac.yaml index 626565e..7d3bd93 100644 --- a/example_config/idac.yaml +++ b/example_config/idac.yaml @@ -10,6 +10,10 @@ server: port_echo2: 20002 port_matching_p2p: 20003 +timerelease: + timerelease_no: 3 + timerelease_avatar_gacha_no: 3 + stamp: enable: True enabled_stamps: # max 3 play stamps @@ -20,3 +24,7 @@ stamp: timetrial: enable: True enabled_timetrial: "touhou_remilia_scarlet" + +battle_event: + enabled: True + enabled_battle_event: "touhou_1st" diff --git a/titles/idac/config.py b/titles/idac/config.py index 2f97219..c411d47 100644 --- a/titles/idac/config.py +++ b/titles/idac/config.py @@ -66,6 +66,22 @@ class IDACServerConfig: return CoreConfig.get_config_field( self.__config, "idac", "server", "port_matching_p2p", default=20003 ) + +class IDACTimereleaseConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def timerelease_no(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "timerelease", "timerelease_no", default=1 + ) + + @property + def timerelease_avatar_gacha_no(self) -> int: + return CoreConfig.get_config_field( + self.__config, "idac", "timerelease", "timerelease_avatar_gacha_no", default=1 + ) class IDACStampConfig: @@ -112,10 +128,32 @@ class IDACTimetrialConfig: "enabled_timetrial", default="touhou_remilia_scarlet", ) + +class IDACTBattleGiftConfig: + def __init__(self, parent: "IDACConfig") -> None: + self.__config = parent + + @property + def enable(self) -> bool: + return CoreConfig.get_config_field( + self.__config, "idac", "battle_gift", "enable", default=True + ) + + @property + def enabled_battle_gift(self) -> str: + return CoreConfig.get_config_field( + self.__config, + "idac", + "battle_gift", + "enabled_battle_gift", + default="touhou_1st", + ) class IDACConfig(dict): def __init__(self) -> None: self.server = IDACServerConfig(self) + self.timerelease = IDACTimereleaseConfig(self) self.stamp = IDACStampConfig(self) self.timetrial = IDACTimetrialConfig(self) + self.battle_gift = IDACTBattleGiftConfig(self) diff --git a/titles/idac/data/battle_gift/touhou_1st.json b/titles/idac/data/battle_gift/touhou_1st.json new file mode 100644 index 0000000..20704ad --- /dev/null +++ b/titles/idac/data/battle_gift/touhou_1st.json @@ -0,0 +1,82 @@ +{ + "battle_gift_event_id": 2, + "event_nm": "東方Projectコラボ", + "start_dt": "2024-04-01", + "end_dt": "2024-06-03", + "mode_id": 1, + "delivery_type": 1, + "gift_data": [ + { + "gift_id": 0, + "battle_gift_event_id": 2, + "reward_category": 21, + "reward_type": 418, + "reward_name": "素敵なお賽銭箱はそこよ", + "rarity": 3, + "cash_rate": 0, + "customize_point_rate": 0, + "avatar_point_rate": 0, + "first_distribution_rate": 60 + }, + { + "gift_id": 1, + "battle_gift_event_id": 2, + "reward_category": 21, + "reward_type": 419, + "reward_name": "面倒な種族ね", + "rarity": 2, + "cash_rate": 0, + "customize_point_rate": 0, + "avatar_point_rate": 0, + "first_distribution_rate": 20 + }, + { + "gift_id": 2, + "battle_gift_event_id": 2, + "reward_category": 21, + "reward_type": 423, + "reward_name": "調子がそこそこだぜ", + "rarity": 3, + "cash_rate": 0, + "customize_point_rate": 0, + "avatar_point_rate": 0, + "first_distribution_rate": 60 + }, + { + "gift_id": 3, + "battle_gift_event_id": 2, + "reward_category": 21, + "reward_type": 422, + "reward_name": "あー?", + "rarity": 2, + "cash_rate": 0, + "customize_point_rate": 0, + "avatar_point_rate": 0, + "first_distribution_rate": 20 + }, + { + "gift_id": 4, + "battle_gift_event_id": 2, + "reward_category": 21, + "reward_type": 427, + "reward_name": "さ、記事にするわよー", + "rarity": 3, + "cash_rate": 0, + "customize_point_rate": 0, + "avatar_point_rate": 0, + "first_distribution_rate": 60 + }, + { + "gift_id": 5, + "battle_gift_event_id": 2, + "reward_category": 21, + "reward_type": 426, + "reward_name": "号外~、号外だよ~", + "rarity": 2, + "cash_rate": 0, + "customize_point_rate": 0, + "avatar_point_rate": 0, + "first_distribution_rate": 20 + } + ] +} \ No newline at end of file diff --git a/titles/idac/data/stamps/touhou_flandre_scarlet.json b/titles/idac/data/stamp/touhou_flandre_scarlet.json similarity index 100% rename from titles/idac/data/stamps/touhou_flandre_scarlet.json rename to titles/idac/data/stamp/touhou_flandre_scarlet.json diff --git a/titles/idac/data/stamps/touhou_remilia_scarlet.json b/titles/idac/data/stamp/touhou_remilia_scarlet.json similarity index 100% rename from titles/idac/data/stamps/touhou_remilia_scarlet.json rename to titles/idac/data/stamp/touhou_remilia_scarlet.json diff --git a/titles/idac/data/stamps/touhou_sakuya_izayoi.json b/titles/idac/data/stamp/touhou_sakuya_izayoi.json similarity index 100% rename from titles/idac/data/stamps/touhou_sakuya_izayoi.json rename to titles/idac/data/stamp/touhou_sakuya_izayoi.json diff --git a/titles/idac/data/timetrial/touhou_flandre_scarlet.json b/titles/idac/data/timetrial/touhou_flandre_scarlet.json index 0db5ed7..474c4cd 100644 --- a/titles/idac/data/timetrial/touhou_flandre_scarlet.json +++ b/titles/idac/data/timetrial/touhou_flandre_scarlet.json @@ -30,7 +30,6 @@ 180, 180, 180, - 200, 200 ], "reward": [ diff --git a/titles/idac/data/timetrial/touhou_remilia_scarlet.json b/titles/idac/data/timetrial/touhou_remilia_scarlet.json index 6b50a2b..a5dfa17 100644 --- a/titles/idac/data/timetrial/touhou_remilia_scarlet.json +++ b/titles/idac/data/timetrial/touhou_remilia_scarlet.json @@ -30,7 +30,6 @@ 180, 180, 180, - 200, 200 ], "reward": [ diff --git a/titles/idac/data/timetrial/touhou_sakuya_izayoi.json b/titles/idac/data/timetrial/touhou_sakuya_izayoi.json index c31d391..89e8dd8 100644 --- a/titles/idac/data/timetrial/touhou_sakuya_izayoi.json +++ b/titles/idac/data/timetrial/touhou_sakuya_izayoi.json @@ -30,7 +30,6 @@ 180, 180, 180, - 200, 200 ], "reward": [ diff --git a/titles/idac/schema/item.py b/titles/idac/schema/item.py index 93b0e7f..0c7e490 100644 --- a/titles/idac/schema/item.py +++ b/titles/idac/schema/item.py @@ -321,6 +321,23 @@ timetrial_event = Table( mysql_charset="utf8mb4", ) +battle_gift = Table( + "idac_user_battle_gift", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("battle_gift_event_id", Integer), + Column("gift_id", Integer), + Column("gift_status", Integer), + Column("received_date", TIMESTAMP, server_default=func.now()), + UniqueConstraint("user", "battle_gift_event_id", "gift_id", name="idac_user_battle_gift_uk"), + mysql_charset="utf8mb4", +) + class IDACItemData(BaseData): async def get_random_user_car(self, aime_id: int, version: int) -> Optional[List[Row]]: @@ -843,6 +860,19 @@ class IDACItemData(BaseData): if result is None: return None return result.fetchone() + + async def get_battle_gifts(self, aime_id: int, battle_gift_event_id: int) -> Optional[Row]: + sql = select(battle_gift).where( + and_( + battle_gift.c.user == aime_id, + battle_gift.c.battle_gift_event_id == battle_gift_event_id, + ) + ) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchall() async def put_car(self, aime_id: int, version: int, car_data: Dict) -> Optional[int]: car_data["user"] = aime_id @@ -1074,3 +1104,19 @@ class IDACItemData(BaseData): ) return None return result.lastrowid + + async def put_battle_gift( + self, aime_id: int, battle_gift_data: Dict + ) -> Optional[int]: + battle_gift_data["user"] = aime_id + + sql = insert(battle_gift).values(**battle_gift_data) + conflict = sql.on_duplicate_key_update(**battle_gift_data) + result = await self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_battle_gift: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid \ No newline at end of file diff --git a/titles/idac/schema/profile.py b/titles/idac/schema/profile.py index bb6593b..32759c3 100644 --- a/titles/idac/schema/profile.py +++ b/titles/idac/schema/profile.py @@ -244,6 +244,28 @@ theory = Table( mysql_charset="utf8mb4", ) +tips = Table( + "idac_profile_tips", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + Column("version", Integer, nullable=False), + Column("tips_list", String(16), server_default="QAAAAAAAAAAAAAAA"), + Column("timetrial_play_count", Integer, server_default="0"), + Column("story_play_count", Integer, server_default="0"), + Column("store_battle_play_count", Integer, server_default="0"), + Column("online_battle_play_count", Integer, server_default="0"), + Column("special_play_count", Integer, server_default="0"), + Column("challenge_play_count", Integer, server_default="0"), + Column("theory_play_count", Integer, server_default="0"), + UniqueConstraint("user", "version", name="idac_profile_tips_uk"), + mysql_charset="utf8mb4", +) + class IDACProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: @@ -348,6 +370,19 @@ class IDACProfileData(BaseData): if result is None: return None return result.fetchone() + + async def get_profile_tips(self, aime_id: int, version: int) -> Optional[Row]: + sql = select(tips).where( + and_( + tips.c.user == aime_id, + tips.c.version == version, + ) + ) + + result = await self.execute(sql) + if result is None: + return None + return result.fetchone() async def put_profile( self, aime_id: int, version: int, profile_data: Dict @@ -438,3 +473,20 @@ class IDACProfileData(BaseData): ) return None return result.lastrowid + + async def put_profile_tips( + self, aime_id: int, version: int, tips_data: Dict + ) -> Optional[int]: + tips_data["user"] = aime_id + tips_data["version"] = version + + sql = insert(tips).values(**tips_data) + conflict = sql.on_duplicate_key_update(**tips_data) + result = await self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_profile_tips: Failed to update! aime_id: {aime_id}" + ) + return None + return result.lastrowid \ No newline at end of file diff --git a/titles/idac/season2.py b/titles/idac/season2.py index dc3dd1b..04a4a32 100644 --- a/titles/idac/season2.py +++ b/titles/idac/season2.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta import os -from random import choice +from random import choice, randint from typing import Any, Dict, List import json import logging @@ -17,20 +17,27 @@ class IDACSeason2(IDACBase): super().__init__(core_cfg, game_cfg) self.version = IDACConstants.VER_IDAC_SEASON_2 - # load the play stamps and timetrial events into memory + # load timerelease numbers from config + self.timerelease_no = self.game_config.timerelease.timerelease_no + self.timerelease_avatar_gacha_no = ( + self.game_config.timerelease.timerelease_avatar_gacha_no + ) + + # load the user configured play stamps (up to 3) self.stamp_info = [] if self.game_config.stamp.enable: for stamp in self.game_config.stamp.enabled_stamps: - if not os.path.exists(f"./titles/idac/data/stamps/{stamp}.json"): + if not os.path.exists(f"./titles/idac/data/stamp/{stamp}.json"): self.logger.warning(f"Stamp {stamp} is enabled but does not exist!") continue with open( - f"./titles/idac/data/stamps/{stamp}.json", encoding="UTF-8" + f"./titles/idac/data/stamp/{stamp}.json", encoding="UTF-8" ) as f: self.logger.debug(f"Loading stamp {stamp}") self.stamp_info.append(self._fix_dates(json.load(f))) + # load the user configured time trials (only one) self.timetrial_event = {} self.timetrial_event_id = None if self.game_config.timetrial.enable: @@ -53,6 +60,25 @@ class IDACSeason2(IDACBase): "timetrial_event_id" ) + # load the user configured battle gifts (only one) + self.battle_gift_event = None + if self.game_config.battle_gift.enable: + battle_gift = self.game_config.battle_gift.enabled_battle_gift + if battle_gift is not None: + if not os.path.exists( + f"./titles/idac/data/battle_gift/{battle_gift}.json" + ): + self.logger.warning( + f"Battle gift {battle_gift} is enabled but does not exist!" + ) + else: + self.logger.debug(f"Loading battle gift {battle_gift}") + with open( + f"./titles/idac/data/battle_gift/{battle_gift}.json", + encoding="UTF-8", + ) as f: + self.battle_gift_event = self._fix_dates(json.load(f)) + async def handle_alive_get_request(self, data: Dict, headers: Dict): return { "status_code": "0", @@ -122,7 +148,6 @@ class IDACSeason2(IDACBase): "difference_time_to_jp": 0, # has to match the game asset version to show theory of street "asset_version": "1", - # option version? MV01? "optional_version": "1", "disconnect_offset": 0, "boost_balance_version": "0", @@ -130,8 +155,8 @@ class IDACSeason2(IDACBase): "play_stamp_enable": 1, "play_stamp_bonus_coin": 1, "gacha_chara_needs": 1, - "both_win_system_control": 1, - "subcard_system_congrol": 1, + "both_win_system_control": 0, + "subcard_system_congrol": 0, "server_maintenance_start_hour": 0, "server_maintenance_start_minutes": 0, "server_maintenance_end_hour": 0, @@ -141,7 +166,10 @@ class IDACSeason2(IDACBase): "domain_echo1": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo1}", "domain_echo2": f"{self.core_cfg.server.hostname}:{self.game_config.server.echo1}", "domain_ping": f"{self.core_cfg.server.hostname}", - "battle_gift_event_master": [], + "battle_gift_event_master": ( + [self.battle_gift_event] if self.battle_gift_event else [] + ), + # online battle round event "round_event": [ { "round_event_id": 30, @@ -180,7 +208,7 @@ class IDACSeason2(IDACBase): "reward_upper_limit": 180, "reward_lower_limit": 180, "reward": [{"reward_category": 21, "reward_type": 462}], - } + }, ], "rank": [], "point": [], @@ -245,26 +273,148 @@ class IDACSeason2(IDACBase): "round_event_exp": [], "stamp_info": self.stamp_info, # 0 = use default data, 1+ = server version of timereleasedata response - "timerelease_no": 3, + "timerelease_no": self.timerelease_no, # 0 = use default data, 1+ = server version of gachadata response - "timerelease_avatar_gacha_no": 3, - "takeover_reward": [], + "timerelease_avatar_gacha_no": self.timerelease_avatar_gacha_no, + # takover reward from other games such as 1=IDZ and 3=SWDC + "takeover_reward": [ + { + "takeover_reward_type": 0, + "takeover_reward_data": [ + {"reward_no": 1, "reward_category": 1, "reward_type": 5000}, + {"reward_no": 2, "reward_category": 25, "reward_type": 1}, + ], + }, + { + "takeover_reward_type": 1, + "takeover_reward_data": [ + {"reward_no": 1, "reward_category": 15, "reward_type": 82}, + {"reward_no": 1, "reward_category": 15, "reward_type": 190}, + {"reward_no": 2, "reward_category": 3, "reward_type": 2}, + {"reward_no": 2, "reward_category": 5, "reward_type": 2}, + {"reward_no": 3, "reward_category": 3, "reward_type": 3}, + {"reward_no": 3, "reward_category": 5, "reward_type": 3}, + {"reward_no": 4, "reward_category": 3, "reward_type": 5}, + {"reward_no": 4, "reward_category": 5, "reward_type": 5}, + {"reward_no": 5, "reward_category": 15, "reward_type": 104}, + {"reward_no": 5, "reward_category": 15, "reward_type": 212}, + ], + }, + { + "takeover_reward_type": 2, + "takeover_reward_data": [ + {"reward_no": 1, "reward_category": 24, "reward_type": 4052}, + {"reward_no": 2, "reward_category": 24, "reward_type": 4053}, + {"reward_no": 3, "reward_category": 24, "reward_type": 4054}, + {"reward_no": 4, "reward_category": 24, "reward_type": 4055}, + {"reward_no": 5, "reward_category": 24, "reward_type": 4056}, + {"reward_no": 6, "reward_category": 24, "reward_type": 4057}, + {"reward_no": 7, "reward_category": 24, "reward_type": 4058}, + ], + }, + { + "takeover_reward_type": 3, + "takeover_reward_data": [ + {"reward_no": 1, "reward_category": 15, "reward_type": 81}, + {"reward_no": 1, "reward_category": 15, "reward_type": 189}, + {"reward_no": 2, "reward_category": 25, "reward_type": 1}, + {"reward_no": 2, "reward_category": 15, "reward_type": 103}, + {"reward_no": 2, "reward_category": 15, "reward_type": 211}, + ], + }, + ], "subcard_judge": [ { "condition_id": 1, - "lower_rank": 0, - "higher_rank": 10, - "condition_start": 2, - "condition_end": 3, - } - ], - "special_promote": [{"counter": 1, "online_rank_id": 1}], - "matching_id": 1, - "matching_group": [ + "lower_rank": 1, + "higher_rank": 7, + "condition_start": 17, + "condition_end": 20, + "calc": 3, + }, { - "group_id": 1, - "group_percent": 1, - } + "condition_id": 2, + "lower_rank": 1, + "higher_rank": 15, + "condition_start": 10, + "condition_end": 10, + "calc": 20, + }, + { + "condition_id": 3, + "lower_rank": 11, + "higher_rank": 15, + "condition_start": 80, + "condition_end": 89, + "calc": 7, + }, + { + "condition_id": 3, + "lower_rank": 11, + "higher_rank": 15, + "condition_start": 90, + "condition_end": 100, + "calc": 10, + }, + { + "condition_id": 3, + "lower_rank": 1, + "higher_rank": 15, + "condition_start": 0, + "condition_end": 40, + "calc": -100, + }, + { + "condition_id": 1, + "lower_rank": 1, + "higher_rank": 15, + "condition_start": 21, + "condition_end": 24, + "calc": 7, + }, + { + "condition_id": 1, + "lower_rank": 1, + "higher_rank": 15, + "condition_start": 25, + "condition_end": 25, + "calc": 10, + }, + { + "condition_id": 3, + "lower_rank": 16, + "higher_rank": 20, + "condition_start": 70, + "condition_end": 89, + "calc": 6, + }, + { + "condition_id": 3, + "lower_rank": 16, + "higher_rank": 20, + "condition_start": 90, + "condition_end": 100, + "calc": 12, + }, + { + "condition_id": 3, + "lower_rank": 16, + "higher_rank": 20, + "condition_start": 0, + "condition_end": 50, + "calc": -100, + }, + ], + "special_promote": [ + {"counter": 50, "online_rank_id": 8}, + {"counter": 200, "online_rank_id": 16}, + {"counter": 255, "online_rank_id": 21}, + ], + "matching_id": 0, + "matching_group": [ + {"group_id": 1, "group_percent": 30}, + {"group_id": 2, "group_percent": 50}, + {"group_id": 3, "group_percent": 100}, ], "timetrial_disp_date": int( datetime.strptime("2023-10-01", "%Y-%m-%d").timestamp() @@ -272,21 +422,22 @@ class IDACSeason2(IDACBase): # price for every car "buy_car_need_cash": 5000, # number of buyable shop/customization time limits - "time_extension_limit": 1, + "time_extension_limit": 5, "collabo_id": 0, "driver_debut_end_date": int( datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp() ), - "online_battle_param1": 1, - "online_battle_param2": 1, - "online_battle_param3": 1, - "online_battle_param4": 1, - "online_battle_param5": 1, - "online_battle_param6": 1, - "online_battle_param7": 1, - "online_battle_param8": 1, - "theory_open_version": "1.30", - "theory_close_version": "1.50", + "online_battle_param1": 0, + "online_battle_param2": 0, + "online_battle_param3": 0, + "online_battle_param4": 0, + "online_battle_param5": 0, + "online_battle_param6": 2, + "online_battle_param7": 0, + "online_battle_param8": 3900, + "theory_open_version": "1.30.00", + "theory_close_version": "9.99.99", + # unlocks teh version specific special mode "special_mode_data": { "start_dt": int( datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp() @@ -692,6 +843,20 @@ class IDACSeason2(IDACBase): vs_info["course_select_priority"] = data.get("course_select_priority") return vs_info + def _choose_gift_id(self) -> Dict: + gift_data = self.battle_gift_event["gift_data"] + # calculate the total_rate based on the first_distribution_rate + total_rate = sum(gift["first_distribution_rate"] for gift in gift_data) + + # randomly choose a number between 1 and the total rate + rand_num = randint(1, total_rate) + cumulative_rate = 0 + for gift in gift_data: + # if the random number is less than the cumulative rate, return the gift + cumulative_rate += gift["first_distribution_rate"] + if rand_num <= cumulative_rate: + return gift + async def handle_user_getdata_request(self, data: Dict, headers: Dict): user_id = int(headers["session"]) @@ -926,6 +1091,74 @@ class IDACSeason2(IDACBase): "point": timetrial["point"], } + # check if the battle gift event is active + if self.battle_gift_event: + # get the users battle gifts, for the current active battle gift event + battle_gifts = await self.data.item.get_battle_gifts( + user_id, self.battle_gift_event.get("battle_gift_event_id") + ) + + if battle_gifts: + # just return all aquired gifts + battle_gift_data = [ + { + "first_distribution_flag": 0, + "gift_data": [ + { + "gift_id": gift["gift_id"], + # 1 = acquired + "gift_get_status": gift["gift_status"], + } + for gift in battle_gifts + ], + } + ] + else: + # get a random gift from the active battle gift event + gift_id = self._choose_gift_id()["gift_id"] + + # save the battle_gift inside the database + await self.data.item.put_battle_gift( + user_id, + { + "battle_gift_event_id": self.battle_gift_event.get( + "battle_gift_event_id" + ), + "gift_id": gift_id, + "gift_status": 1, # aquired + }, + ) + + battle_gift_data = [ + { + # trigger the first gift animation + "first_distribution_flag": 1, + "gift_data": [ + { + # return a random selected gift_id + "gift_id": gift_id, + # set the status to 2 = new gift + "gift_get_status": 2, + } + ], + } + ] + + # get the tips from the database + tips = await self.data.profile.get_profile_tips(user_id, self.version) + + # if there are no tips, create a new one + if tips is None: + await self.data.profile.put_profile_tips(user_id, self.version, {}) + + # get the tips from the database + tips = await self.data.profile.get_profile_tips(user_id, self.version) + + tips_info = tips._asdict() + del tips_info["id"] + del tips_info["user"] + del tips_info["version"] + return { "status_code": "0", "user_base_data": user_data, @@ -969,7 +1202,9 @@ class IDACSeason2(IDACBase): "frozen_data": {"frozen_status": 2}, "penalty_data": {"penalty_flag": 0, "penalty_2_level": 0}, "config_data": config_data, - "battle_gift_data": [], + # gift_get_status 2 = new gift, 1 = acquired + # first_distribution related are useless since this is all handled by server + "battle_gift_data": battle_gift_data, "ticket_data": ticket_data, "round_event": [], "last_round_event": [], @@ -984,17 +1219,16 @@ class IDACSeason2(IDACBase): "car_use_count": [], "maker_use_count": [], "story_course": [{"course_id": 0, "count": 1}], - # TODO! - # "driver_debut": { - # "play_count": 137, - # "daily_play": 5, - # "last_play_dt": 0, - # "use_start_date": 0, - # "use_end_date": 0, - # "use_dt": 0, - # "ticket_cnt": 0, - # "ticket_get_bit": 0, - # }, + "driver_debut": { + # "play_count": 5, + # "daily_play": 5, + # "last_play_dt": datetime.now().timestamp() - 86400, + # "use_start_date": datetime.now().timestamp() - 86400, + # "use_end_date": datetime.now().timestamp() + 86400, + # "use_dt": datetime.now().timestamp(), + # "ticket_cnt": 1, + # "ticket_get_bit": (2 ** 5) - 1, + }, "theory_data": theory_data, "theory_course_data": theory_course_data, "theory_partner_data": theory_partner_data, @@ -1004,6 +1238,7 @@ class IDACSeason2(IDACBase): "season_rewards_data": [], "timetrial_event_data": timetrial_event_data, "special_mode_hint_data": {"story_type": 0, "hint_display_flag": 0}, + "tips_info": tips_info, } async def handle_timetrial_getbestrecordpreta_request( @@ -1596,6 +1831,12 @@ class IDACSeason2(IDACBase): user_id, self.timetrial_event_id, event_point ) + # update the tips play count + tips = await self.data.profile.get_profile_tips(user_id, self.version) + await self.data.profile.put_profile_tips( + user_id, self.version, {"timetrial_play_count": tips["timetrial_play_count"] + 1} + ) + return { "status_code": "0", "course_rank": course_rank, @@ -1733,6 +1974,12 @@ class IDACSeason2(IDACBase): }, ) + # update the tips play count + tips = await self.data.profile.get_profile_tips(user_id, self.version) + await self.data.profile.put_profile_tips( + user_id, self.version, {"story_play_count": tips["story_play_count"] + 1} + ) + return { "status_code": "0", "story_data": await self._generate_story_data(user_id), @@ -1806,6 +2053,12 @@ class IDACSeason2(IDACBase): # finally save the special mode with story_type=4 in database await self.data.item.put_challenge(user_id, data) + # update the tips play count + tips = await self.data.profile.get_profile_tips(user_id, self.version) + await self.data.profile.put_profile_tips( + user_id, self.version, {"special_play_count": tips["special_play_count"] + 1} + ) + return { "status_code": "0", "special_mode_data": await self._generate_special_data(user_id), @@ -1886,6 +2139,12 @@ class IDACSeason2(IDACBase): # finally save the challenge mode with story_type=3 in database await self.data.item.put_challenge(user_id, data) + # update the tips play count + tips = await self.data.profile.get_profile_tips(user_id, self.version) + await self.data.profile.put_profile_tips( + user_id, self.version, {"challenge_play_count": tips["challenge_play_count"] + 1} + ) + return { "status_code": "0", "challenge_mode_data": await self._generate_challenge_data(user_id), @@ -2118,7 +2377,12 @@ class IDACSeason2(IDACBase): # not required? mode_id = data.pop("mode_id") standby_play_flag = data.pop("standby_play_flag") + + # save the tips list in database tips_list = data.pop("tips_list") + await self.data.profile.put_profile_tips( + user_id, self.version, {"tips_list": tips_list} + ) # save stock data in database await self._save_stock_data(user_id, stock_data) @@ -2522,6 +2786,12 @@ class IDACSeason2(IDACBase): }, ) + # update the tips play count + tips = await self.data.profile.get_profile_tips(user_id, self.version) + await self.data.profile.put_profile_tips( + user_id, self.version, {"theory_play_count": tips["theory_play_count"] + 1} + ) + return { "status_code": "0", "played_powerhouse_lv": data.get("powerhouse_lv"), @@ -2584,7 +2854,9 @@ class IDACSeason2(IDACBase): "bothwin_penalty": 1, } - async def handle_user_updateonlinebattleresult_request(self, data: Dict, headers: Dict): + async def handle_user_updateonlinebattleresult_request( + self, data: Dict, headers: Dict + ): user_id = headers["session"] stock_data: Dict = data.pop("stock_obj") @@ -2640,7 +2912,15 @@ class IDACSeason2(IDACBase): }, ) - vs_info = self._update_vs_info(user_id, IDACConstants.BATTLE_MODE_ONLINE, data) + vs_info = await self._update_vs_info( + user_id, IDACConstants.BATTLE_MODE_ONLINE, data + ) + + # update the tips play count + tips = await self.data.profile.get_profile_tips(user_id, self.version) + await self.data.profile.put_profile_tips( + user_id, self.version, {"online_battle_play_count": tips["online_battle_play_count"] + 1} + ) return { "status_code": "0", @@ -2671,9 +2951,20 @@ class IDACSeason2(IDACBase): # no idea? result = data.pop("result") + + # save the received battle gift in database, hopefully that works battle_gift_event_id = data.pop("battle_gift_event_id") gift_id = data.pop("gift_id") + await self.data.item.put_battle_gift( + user_id, + { + "battle_gift_event_id": battle_gift_event_id, + "gift_id": gift_id, + "gift_status": 1, # aquired + }, + ) + # save stock data in database await self._save_stock_data(user_id, stock_data) @@ -2726,6 +3017,12 @@ class IDACSeason2(IDACBase): user_id, IDACConstants.BATTLE_MODE_OFFLINE, data ) + # update the tips play count + tips = await self.data.profile.get_profile_tips(user_id, self.version) + await self.data.profile.put_profile_tips( + user_id, self.version, {"store_battle_play_count": tips["store_battle_play_count"] + 1} + ) + return { "status_code": "0", "vsinfo_data": vs_info,