From 50e0dde7de3173b688df27ea0c8af99e6a1ffcf1 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Mon, 13 May 2024 08:45:19 +0000 Subject: [PATCH 01/11] Added team user points --- titles/chuni/schema/profile.py | 35 ++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 2f8bce3..9b4d645 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -1,3 +1,4 @@ +import json from typing import Dict, List, Optional from sqlalchemy import Table, Column, UniqueConstraint, and_ from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger @@ -389,6 +390,7 @@ team = Table( Column("id", Integer, primary_key=True, nullable=False), Column("teamName", String(255)), Column("teamPoint", Integer), + Column("userTeamPoint", JSON), mysql_charset="utf8mb4", ) @@ -693,12 +695,36 @@ class ChuniProfileData(BaseData): # Return the rank if found, or a default rank otherwise return rank if rank is not None else 0 - # RIP scaled team ranking. Gone, but forgotten - # def get_team_rank_scaled(self, team_id: int) -> int: - - async def update_team(self, team_id: int, team_data: Dict) -> bool: + async def update_team(self, team_id: int, team_data: Dict, user_id: str, user_point_delta: int) -> bool: + # Update the team data team_data["id"] = team_id + existing_team = self.get_team_by_id(team_id) + if existing_team is None or "userTeamPoint" not in existing_team: + self.logger.warn( + f"update_team: Failed to update team! team id: {team_id}. Existing team data not found." + ) + return False + user_team_point_data = [] + if existing_team["userTeamPoint"] is not None and existing_team["userTeamPoint"] is not "": + user_team_point_data = json.loads(existing_team["userTeamPoint"]) + updated = False + + # Try to find the user in the existing data and update their points + for user_point_data in user_team_point_data: + if user_point_data["user"] == user_id: + user_point_data["userPoint"] = str(int(user_point_delta)) + updated = True + break + + # If the user was not found, add them to the data with the new points + if not updated: + user_team_point_data.append({"user": user_id, "userPoint": str(user_point_delta)}) + + # Update the team's userTeamPoint field in the team data + team_data["userTeamPoint"] = json.dumps(user_team_point_data) + + # Update the team in the database sql = insert(team).values(**team_data) conflict = sql.on_duplicate_key_update(**team_data) @@ -710,6 +736,7 @@ class ChuniProfileData(BaseData): ) return False return True + async def get_rival(self, rival_id: int) -> Optional[Row]: sql = select(profile).where(profile.c.user == rival_id) result = await self.execute(sql) From b3c1dceec9755ac73423f3136c36081fe6409ac6 Mon Sep 17 00:00:00 2001 From: EmmyHeart Date: Mon, 13 May 2024 08:48:01 +0000 Subject: [PATCH 02/11] Add team user points --- titles/chuni/base.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 9e8a634..e53c2d2 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -720,9 +720,14 @@ class ChuniBase: team_id = 65535 team_name = self.game_cfg.team.team_name team_rank = 0 + team_user_point = 0 # Get user profile profile = await self.data.profile.get_profile_data(data["userId"], self.version) + + if profile is None: + return {"userId": data["userId"], "teamId": 0} + if profile and profile["teamId"]: # Get team by id team = await self.data.profile.get_team_by_id(profile["teamId"]) @@ -731,7 +736,12 @@ class ChuniBase: team_id = team["id"] team_name = team["teamName"] team_rank = await self.data.profile.get_team_rank(team["id"]) - + team_point = team["teamPoint"] + if team["userTeamPoint"] is not None and team["userTeamPoint"] is not "": + user_team_point_data = json.loads(team["userTeamPoint"]) + for user_point_data in user_team_point_data: + if user_point_data["user"] == data["userId"]: + team_user_point = int(user_point_data["userPoint"]) # Don't return anything if no team name has been defined for defaults and there is no team set for the player if not profile["teamId"] and team_name == "": return {"userId": data["userId"], "teamId": 0} @@ -741,11 +751,12 @@ class ChuniBase: "teamId": team_id, "teamRank": team_rank, "teamName": team_name, + "assaultTimeRate": 1, # TODO: Figure out assaultTime, which might be team point boost? "userTeamPoint": { "userId": data["userId"], "teamId": team_id, - "orderId": 1, - "teamPoint": 1, + "orderId": 0, + "teamPoint": team_user_point, "aggrDate": data["playDate"], }, } From c3efc36be2556551afaa8a88504bfe6b52eb8a7c Mon Sep 17 00:00:00 2001 From: beerpsi Date: Wed, 12 Jun 2024 16:08:12 +0700 Subject: [PATCH 03/11] [chuni] Add correct endpoint `iter_count`s for all versions with encryption --- titles/chuni/index.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index f0f1eac..e030004 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -107,8 +107,23 @@ class ChuniServlet(BaseServlet): iter_count = 36 elif version == ChuniConstants.VER_CHUNITHM_SUN: iter_count = 70 - else: + elif version == ChuniConstants.VER_CHUNITHM_NEW_PLUS: + iter_count = 25 + elif version == ChuniConstants.VER_CHUNITHM_NEW: + iter_count = 54 + elif version == ChuniConstants.VER_CHUNITHM_PARADISE: iter_count = 44 + elif version == ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: + iter_count = 67 + else: + disp_version = ( + ChuniConstants.VERSION_NAMES[version] + if version in ChuniConstants.VERSION_NAMES + else str(version) + ) + + self.logger.warning("Version %s does not support encryption", disp_version) + continue hash = PBKDF2( method_fixed, @@ -313,4 +328,4 @@ class ChuniServlet(BaseServlet): bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), ) - return Response(crypt.encrypt(padded)) \ No newline at end of file + return Response(crypt.encrypt(padded)) From bf8631448a6ed63bd16e40d9f63a59951fc3a758 Mon Sep 17 00:00:00 2001 From: beerpsi Date: Sat, 15 Jun 2024 22:22:07 +0700 Subject: [PATCH 04/11] support SDGS encryption --- titles/chuni/index.py | 88 +++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index e030004..726f02a 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -39,7 +39,7 @@ class ChuniServlet(BaseServlet): def __init__(self, core_cfg: CoreConfig, cfg_dir: str) -> None: super().__init__(core_cfg, cfg_dir) self.game_cfg = ChuniConfig() - self.hash_table: Dict[Dict[str, str]] = {} + self.hash_table: Dict[str, Dict[str, str]] = {} if path.exists(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}"): self.game_cfg.update( yaml.safe_load(open(f"{cfg_dir}/{ChuniConstants.CONFIG_NAME}")) @@ -89,45 +89,59 @@ class ChuniServlet(BaseServlet): ) self.logger.inited = True + known_iter_counts = { + ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67, + ChuniConstants.VER_CHUNITHM_PARADISE: 44, + f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 25, + ChuniConstants.VER_CHUNITHM_NEW: 54, + f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49, + ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, + ChuniConstants.VER_CHUNITHM_SUN: 70, + ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, + } + for version, keys in self.game_cfg.crypto.keys.items(): if len(keys) < 3: continue - self.hash_table[version] = {} + if isinstance(version, int): + version_idx = version + else: + version_idx = int(version.split("_")[0]) + + salt = bytes.fromhex(keys[2]) + if len(keys) >= 4: + iter_count = keys[3] + elif (iter_count := known_iter_counts.get(version)) is None: + self.logger.error( + "Number of iteration rounds for version %s is not known, but it is not specified in the config", + version, + ) + continue + + self.hash_table[version] = {} method_list = [ method - for method in dir(self.versions[version]) + for method in dir(self.versions[version_idx]) if not method.startswith("__") ] + for method in method_list: method_fixed = inflection.camelize(method)[6:-7] - # number of iterations was changed to 70 in SUN and then to 36 - if version == ChuniConstants.VER_CHUNITHM_SUN_PLUS: - iter_count = 36 - elif version == ChuniConstants.VER_CHUNITHM_SUN: - iter_count = 70 - elif version == ChuniConstants.VER_CHUNITHM_NEW_PLUS: - iter_count = 25 - elif version == ChuniConstants.VER_CHUNITHM_NEW: - iter_count = 54 - elif version == ChuniConstants.VER_CHUNITHM_PARADISE: - iter_count = 44 - elif version == ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: - iter_count = 67 - else: - disp_version = ( - ChuniConstants.VERSION_NAMES[version] - if version in ChuniConstants.VERSION_NAMES - else str(version) - ) - - self.logger.warning("Version %s does not support encryption", disp_version) - continue + + # This only applies for CHUNITHM NEW International and later for some reason. + # CHUNITHM SUPERSTAR (PLUS) did not add "Exp" to the endpoint when hashing. + if ( + isinstance(version, str) + and version.endswith("_int") + and version_idx >= ChuniConstants.VER_CHUNITHM_NEW + ): + method_fixed += "C3Exp" hash = PBKDF2( method_fixed, - bytes.fromhex(keys[2]), + salt, 128, count=iter_count, hmac_hash_module=SHA1, @@ -137,7 +151,7 @@ class ChuniServlet(BaseServlet): self.hash_table[version][hashed_name] = method_fixed self.logger.debug( - f"Hashed v{version} method {method_fixed} with {bytes.fromhex(keys[2])} to get {hash.hex()}" + f"Hashed v{version} method {method_fixed} with {salt} to get {hashed_name}" ) @classmethod @@ -226,31 +240,39 @@ class ChuniServlet(BaseServlet): if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're - # doing encrypted. The likelyhood of false positives is low but + # doing encrypted. The likelihood of false positives is low but # technically not 0 + + if game_code == "SDGS": + crypto_cfg_key = f"{internal_ver}_int" + hash_table_key = f"{internal_ver}_int" + else: + crypto_cfg_key = internal_ver + hash_table_key = internal_ver + if internal_ver < ChuniConstants.VER_CHUNITHM_NEW: endpoint = request.headers.get("User-Agent").split("#")[0] else: - if internal_ver not in self.hash_table: + if hash_table_key not in self.hash_table: self.logger.error( f"v{version} does not support encryption or no keys entered" ) return Response(zlib.compress(b'{"stat": "0"}')) - elif endpoint.lower() not in self.hash_table[internal_ver]: + elif endpoint.lower() not in self.hash_table[hash_table_key]: self.logger.error( f"No hash found for v{version} endpoint {endpoint}" ) return Response(zlib.compress(b'{"stat": "0"}')) - endpoint = self.hash_table[internal_ver][endpoint.lower()] + endpoint = self.hash_table[hash_table_key][endpoint.lower()] try: crypt = AES.new( - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][0]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][0]), AES.MODE_CBC, - bytes.fromhex(self.game_cfg.crypto.keys[internal_ver][1]), + bytes.fromhex(self.game_cfg.crypto.keys[crypto_cfg_key][1]), ) req_raw = crypt.decrypt(req_raw) From 243d40cfd84d14cba581c7dcc65303b1c0e818dc Mon Sep 17 00:00:00 2001 From: beerpsi Date: Tue, 18 Jun 2024 09:39:14 +0700 Subject: [PATCH 05/11] Add changelog and docs --- changelog.md | 4 ++++ docs/game_specific_info.md | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 3f8c6ba..a2447e3 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,10 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20240616 +### CHUNITHM ++ Support network encryption for Export/International versions + ## 20240530 ### DIVA + Fix reader for when dificulty is not a int diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index a8e63c5..8e56322 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -87,13 +87,19 @@ Config file is located in `config/chuni.yaml`. | `crypto` | This option is used to enable the TLS Encryption | -**If you would like to use network encryption, the following will be required underneath but key, iv and hash are required:** +If you would like to use network encryption, add the keys to the `keys` section under `crypto`, where the key +is the version ID for Japanese (SDHD) versions and `"{versionID}_int"` for Export (SDGS) versions, and the value +is an array containing `[key, iv, salt, iter_count]` in order. + +`iter_count` is optional for all Japanese (SDHD) versions but may be required for some Export (SDGS) versions. +You will receive an error in the logs if it needs to be specified. ```yaml crypto: encrypted_only: False keys: 13: ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000"] + "13_int": ["0000000000000000000000000000000000000000000000000000000000000000", "00000000000000000000000000000000", "0000000000000000", 42] ``` ### Database upgrade From 7242a187ab4c5b27e14ad703a906dd2ae75db7af Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 28 Jun 2024 12:25:33 -0400 Subject: [PATCH 06/11] sao: fix sao_hero_log_data nullability --- .../versions/680789dabab3_sao_player_changes.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/data/alembic/versions/680789dabab3_sao_player_changes.py b/core/data/alembic/versions/680789dabab3_sao_player_changes.py index 9cc60d4..3e4aef7 100644 --- a/core/data/alembic/versions/680789dabab3_sao_player_changes.py +++ b/core/data/alembic/versions/680789dabab3_sao_player_changes.py @@ -64,6 +64,12 @@ def upgrade(): op.add_column('sao_hero_log_data', sa.Column('property4_value1', sa.INTEGER(), server_default='0', nullable=False)) op.add_column('sao_hero_log_data', sa.Column('property4_value2', sa.INTEGER(), server_default='0', nullable=False)) op.add_column('sao_hero_log_data', sa.Column('converted_card_num', sa.INTEGER(), server_default='0', nullable=False)) + op.alter_column('sao_hero_log_data', 'main_weapon', + existing_type=mysql.INTEGER(), + nullable=True) + op.alter_column('sao_hero_log_data', 'sub_equipment', + existing_type=mysql.INTEGER(), + nullable=True) op.alter_column('sao_hero_log_data', 'skill_slot1_skill_id', existing_type=mysql.INTEGER(), type_=sa.BIGINT(), @@ -235,12 +241,10 @@ def downgrade(): type_=mysql.INTEGER(), nullable=False) op.alter_column('sao_hero_log_data', 'sub_equipment', - existing_type=sa.BIGINT(), - type_=mysql.INTEGER(), + existing_type=mysql.INTEGER(), nullable=False) op.alter_column('sao_hero_log_data', 'main_weapon', - existing_type=sa.BIGINT(), - type_=mysql.INTEGER(), + existing_type=mysql.INTEGER(), nullable=False) op.drop_column('sao_hero_log_data', 'converted_card_num') op.drop_column('sao_hero_log_data', 'property4_value2') From 4446ff1f21071f2aa6b58345bc04fcbec5e65676 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 28 Jun 2024 15:48:27 -0400 Subject: [PATCH 07/11] mai2: add present support --- .../versions/5ea363686347_mai2_presents.py | 41 ++++++++++++ docs/game_specific_info.md | 8 +++ titles/mai2/base.py | 20 +++++- titles/mai2/dx.py | 37 ++++++++--- titles/mai2/schema/item.py | 66 ++++++++++++++++++- titles/mai2/schema/profile.py | 2 +- 6 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 core/data/alembic/versions/5ea363686347_mai2_presents.py diff --git a/core/data/alembic/versions/5ea363686347_mai2_presents.py b/core/data/alembic/versions/5ea363686347_mai2_presents.py new file mode 100644 index 0000000..8e07a5c --- /dev/null +++ b/core/data/alembic/versions/5ea363686347_mai2_presents.py @@ -0,0 +1,41 @@ +"""mai2_presents + +Revision ID: 5ea363686347 +Revises: 680789dabab3 +Create Date: 2024-06-28 14:49:07.666879 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '5ea363686347' +down_revision = '680789dabab3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('mai2_item_present', + sa.Column('id', sa.BIGINT(), nullable=False), + sa.Column('version', sa.INTEGER(), nullable=True), + sa.Column('user', sa.Integer(), nullable=True), + sa.Column('itemKind', sa.INTEGER(), nullable=False), + sa.Column('itemId', sa.INTEGER(), nullable=False), + sa.Column('stock', sa.INTEGER(), server_default='1', nullable=False), + sa.Column('startDate', sa.TIMESTAMP(), nullable=True), + sa.Column('endDate', sa.TIMESTAMP(), nullable=True), + sa.ForeignKeyConstraint(['user'], ['aime_user.id'], onupdate='cascade', ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('version', 'user', 'itemKind', 'itemId', name='mai2_item_present_uk'), + mysql_charset='utf8mb4' + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('mai2_item_present') + # ### end Alembic commands ### diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index e81a3c6..9fadf80 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -182,6 +182,14 @@ Config file is located in `config/cxb.yaml`. ## maimai DX +### Presents +Presents are items given to the user when they login, with a little animation (for example, the KOP song was given to the finalists as a present). To add a present, you must insert it into the `mai2_item_present` table. In that table, a NULL version means any version, a NULL user means any user, a NULL start date means always open, and a NULL end date means it never expires. Below is a list of presents one might wish to add: + +| Game Version | Item ID | Item Kind | Item Description | Present Description | +|--------------|---------|-----------|-------------------------------------------------|------------------------------------------------| +| BUDDiES (21) | 409505 | Icon (3) | 旅行スタンプ(月面基地) (Travel Stamp - Moon Base) | Officially obtained on the webui with a serial | +| | | | | number, for project raputa | + ### Versions | Game Code | Version ID | Version Name | diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 4d31213..79905ff 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -471,7 +471,25 @@ class Mai2Base: } async def handle_get_user_present_api_request(self, data: Dict) -> Dict: - return { "userId": data.get("userId", 0), "length": 0, "userPresentList": []} + items: List[Dict[str, Any]] = [] + user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) + if user_pres_list: + for present in user_pres_list: + if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): + self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") + continue # present period hasn't started yet, move onto the next one + + if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()): + self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed") + continue # present period ended, move onto the next one + + test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) + if not test: # Don't send presents for items the user already has + items.append({"itemId": present['itemId'], "itemKind": present['itemKind'], "stock": present['stock'], "isValid": True}) + self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") + await self.data.item.put_item(data["userId"], present['itemKind'], present['itemId'], present['stock'], True) + + return { "userId": data.get("userId", 0), "length": len(items), "userPresentList": items} async def handle_get_transfer_friend_api_request(self, data: Dict) -> Dict: return {} diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 04c9172..5ca03f7 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -327,16 +327,35 @@ class Mai2DX(Mai2Base): async def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) - user_item_list = await self.data.item.get_items(data["userId"], kind) - items: List[Dict[str, Any]] = [] - for i in range(next_idx, len(user_item_list)): - tmp = user_item_list[i]._asdict() - tmp.pop("user") - tmp.pop("id") - items.append(tmp) - if len(items) >= int(data["maxCount"]): - break + + if kind == 4: # presents + user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) + if user_pres_list: + for present in user_pres_list: + if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): + self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") + continue # present period hasn't started yet, move onto the next one + + if (present['endDate'] and present['endDate'].timestamp() < datetime.now().timestamp()): + self.logger.warn(f"Present {present['id']} ended on {present['endDate']} and should be removed") + continue # present period ended, move onto the next one + + test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) + if not test: # Don't send presents for items the user already has + items.append({"itemId": present['itemId'], "itemKind": present['itemKind'], "stock": present['stock'], "isValid": True}) + self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") + await self.data.item.put_item(data["userId"], present['itemKind'], present['itemId'], present['stock'], True) + + else: + user_item_list = await self.data.item.get_items(data["userId"], kind) + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): + break xout = kind * 10000000000 + next_idx + len(items) diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index f22cccd..65e129f 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -2,8 +2,8 @@ from core.data.schema import BaseData, metadata from datetime import datetime from typing import Optional, Dict, List -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON +from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_, or_ +from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BIGINT, INTEGER from sqlalchemy.schema import ForeignKey from sqlalchemy.sql import func, select from sqlalchemy.dialects.mysql import insert @@ -198,6 +198,20 @@ print_detail = Table( mysql_charset="utf8mb4", ) +present = Table( + "mai2_item_present", + metadata, + Column('id', BIGINT, primary_key=True, nullable=False), + Column('version', INTEGER), + Column("user", Integer, ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")), + Column("itemKind", INTEGER, nullable=False), + Column("itemId", INTEGER, nullable=False), + Column("stock", INTEGER, nullable=False, server_default="1"), + Column("startDate", TIMESTAMP), + Column("endDate", TIMESTAMP), + UniqueConstraint("version", "user", "itemKind", "itemId", name="mai2_item_present_uk"), + mysql_charset="utf8mb4", +) class Mai2ItemData(BaseData): async def put_item( @@ -476,7 +490,7 @@ class Mai2ItemData(BaseData): musicId = music_id ) - conflict = sql.on_duplicate_key_do_nothing() + conflict = sql.on_duplicate_key_update(musicId = music_id) result = await self.execute(conflict) if result: @@ -586,3 +600,49 @@ class Mai2ItemData(BaseData): ) return None return result.lastrowid + + async def put_present(self, item_kind: int, item_id: int, version: int = None, user_id: int = None, start_date: datetime = None, end_date: datetime = None) -> Optional[int]: + sql = insert(present).values( + version = version, + user = user_id, + itemKind = item_kind, + itemId = item_id, + startDate = start_date, + endDate = end_date + ) + + conflict = sql.on_duplicate_key_update( + startDate = start_date, + endDate = end_date + ) + + result = await self.execute(conflict) + if result: + return result.lastrowid + + self.logger.error(f"Failed to add present item {item_id}!") + + async def get_presents_by_user(self, user_id: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select(or_(present.c.user == user_id, present.c.user is None))) + if result: + return result.fetchall() + + async def get_presents_by_version(self, ver: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select(or_(present.c.version == ver, present.c.version is None))) + if result: + return result.fetchall() + + async def get_presents_by_version_user(self, ver: int = None, user_id: int = None) -> Optional[List[Row]]: + result = await self.execute(present.select( + and_( + or_(present.c.user == user_id, present.c.user is None)), + or_(present.c.version == ver, present.c.version is None) + ) + ) + if result: + return result.fetchall() + + async def get_present_by_id(self, present_id: int) -> Optional[Row]: + result = await self.execute(present.select(present.c.id == present_id)) + if result: + return result.fetchone() diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 1b76b07..c191a1a 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -892,7 +892,7 @@ class Mai2ProfileData(BaseData): rival = rival_id ) - conflict = sql.on_duplicate_key_do_nothing() + conflict = sql.on_duplicate_key_update(rival = rival_id) result = await self.execute(conflict) if result: From e9afaaf56cd42fd1c8f823d41c728d1375ac4896 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 28 Jun 2024 15:51:28 -0400 Subject: [PATCH 08/11] update changelog --- changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/changelog.md b/changelog.md index f9c6563..17fef90 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,14 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20240628 +### maimai ++ Add present support + +## 20240627 +### SAO ++ Fix ghost items, character and player XP, EX Bonuses, unlocks, and much much more + ## 20240620 ### CHUNITHM + CHUNITHM LUMINOUS support From af8bd1d1c08be6f9e2fc50987cf0934b29091c65 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 28 Jun 2024 23:29:31 -0400 Subject: [PATCH 09/11] mai2: fix presents --- titles/mai2/base.py | 5 ++++- titles/mai2/dx.py | 5 ++++- titles/mai2/schema/item.py | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 79905ff..16b2039 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -474,6 +474,7 @@ class Mai2Base: items: List[Dict[str, Any]] = [] user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) if user_pres_list: + self.logger.debug(f"Found {len(user_pres_list)} possible presents") for present in user_pres_list: if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") @@ -485,7 +486,9 @@ class Mai2Base: test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) if not test: # Don't send presents for items the user already has - items.append({"itemId": present['itemId'], "itemKind": present['itemKind'], "stock": present['stock'], "isValid": True}) + pres_id = present['itemKind'] * 1000000 + pres_id += present['itemId'] + items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") await self.data.item.put_item(data["userId"], present['itemKind'], present['itemId'], present['stock'], True) diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 5ca03f7..3b28f30 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -332,6 +332,7 @@ class Mai2DX(Mai2Base): if kind == 4: # presents user_pres_list = await self.data.item.get_presents_by_version_user(self.version, data["userId"]) if user_pres_list: + self.logger.debug(f"Found {len(user_pres_list)} possible presents") for present in user_pres_list: if (present['startDate'] and present['startDate'].timestamp() > datetime.now().timestamp()): self.logger.debug(f"Present {present['id']} distribution hasn't started yet (begins {present['startDate']})") @@ -343,7 +344,9 @@ class Mai2DX(Mai2Base): test = await self.data.item.get_item(data["userId"], present['itemKind'], present['itemId']) if not test: # Don't send presents for items the user already has - items.append({"itemId": present['itemId'], "itemKind": present['itemKind'], "stock": present['stock'], "isValid": True}) + pres_id = present['itemKind'] * 1000000 + pres_id += present['itemId'] + items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") await self.data.item.put_item(data["userId"], present['itemKind'], present['itemId'], present['stock'], True) diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index 65e129f..d53ebbc 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -635,10 +635,10 @@ class Mai2ItemData(BaseData): async def get_presents_by_version_user(self, ver: int = None, user_id: int = None) -> Optional[List[Row]]: result = await self.execute(present.select( and_( - or_(present.c.user == user_id, present.c.user is None)), - or_(present.c.version == ver, present.c.version is None) + or_(present.c.user == user_id, present.c.user == None), + or_(present.c.version == ver, present.c.version == None) ) - ) + )) if result: return result.fetchall() From ecb2e9ec75035f0733e59491f9939980b1a7d203 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Fri, 28 Jun 2024 23:55:23 -0400 Subject: [PATCH 10/11] mai2: properly add present items --- titles/mai2/base.py | 1 - titles/mai2/dx.py | 16 +++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 16b2039..b3c6a1d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -490,7 +490,6 @@ class Mai2Base: pres_id += present['itemId'] items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") - await self.data.item.put_item(data["userId"], present['itemKind'], present['itemId'], present['stock'], True) return { "userId": data.get("userId", 0), "length": len(items), "userPresentList": items} diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index 3b28f30..7a067d7 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -196,10 +196,17 @@ class Mai2DX(Mai2Base): if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: + if item["itemKind"] == 4: + item_id = item["itemId"] % 1000000 + item_kind = item["itemId"] // 1000000 + else: + item_id = item["itemId"] + item_kind = item["itemKind"] + await self.data.item.put_item( user_id, - int(item["itemKind"]), - item["itemId"], + item_kind, + item_id, item["stock"], item["isValid"], ) @@ -325,8 +332,8 @@ class Mai2DX(Mai2Base): } async def handle_get_user_item_api_request(self, data: Dict) -> Dict: - kind = int(data["nextIndex"] / 10000000000) - next_idx = int(data["nextIndex"] % 10000000000) + kind = data["nextIndex"] // 10000000000 + next_idx = data["nextIndex"] % 10000000000 items: List[Dict[str, Any]] = [] if kind == 4: # presents @@ -348,7 +355,6 @@ class Mai2DX(Mai2Base): pres_id += present['itemId'] items.append({"itemId": pres_id, "itemKind": 4, "stock": present['stock'], "isValid": True}) self.logger.info(f"Give user {data['userId']} {present['stock']}x item {present['itemId']} (kind {present['itemKind']}) as present") - await self.data.item.put_item(data["userId"], present['itemKind'], present['itemId'], present['stock'], True) else: user_item_list = await self.data.item.get_items(data["userId"], kind) From 44fb6037cf5102294f603ddffe1294ab2646a440 Mon Sep 17 00:00:00 2001 From: Kevin Trocolli Date: Sat, 29 Jun 2024 00:08:11 -0400 Subject: [PATCH 11/11] chuni: add missing alembic script --- changelog.md | 4 +++ .../745448d83696_chuni_team_points.py | 28 +++++++++++++++++++ titles/chuni/base.py | 2 +- titles/chuni/schema/profile.py | 2 +- 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 core/data/alembic/versions/745448d83696_chuni_team_points.py diff --git a/changelog.md b/changelog.md index 17fef90..8f60130 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,10 @@ # Changelog Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to. +## 20240629 +### CHUNITHM ++ Add team points + ## 20240628 ### maimai + Add present support diff --git a/core/data/alembic/versions/745448d83696_chuni_team_points.py b/core/data/alembic/versions/745448d83696_chuni_team_points.py new file mode 100644 index 0000000..2a8465a --- /dev/null +++ b/core/data/alembic/versions/745448d83696_chuni_team_points.py @@ -0,0 +1,28 @@ +"""chuni_team_points + +Revision ID: 745448d83696 +Revises: 5ea363686347 +Create Date: 2024-06-29 00:05:22.479187 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '745448d83696' +down_revision = '5ea363686347' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('chuni_profile_team', sa.Column('userTeamPoint', sa.JSON(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('chuni_profile_team', 'userTeamPoint') + # ### end Alembic commands ### diff --git a/titles/chuni/base.py b/titles/chuni/base.py index d9d35cc..9be4bf7 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -737,7 +737,7 @@ class ChuniBase: team_name = team["teamName"] team_rank = await self.data.profile.get_team_rank(team["id"]) team_point = team["teamPoint"] - if team["userTeamPoint"] is not None and team["userTeamPoint"] is not "": + if team["userTeamPoint"] is not None and team["userTeamPoint"] != "": user_team_point_data = json.loads(team["userTeamPoint"]) for user_point_data in user_team_point_data: if user_point_data["user"] == data["userId"]: diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index daa7f4a..f0b8c0f 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -718,7 +718,7 @@ class ChuniProfileData(BaseData): ) return False user_team_point_data = [] - if existing_team["userTeamPoint"] is not None and existing_team["userTeamPoint"] is not "": + if existing_team["userTeamPoint"] is not None and existing_team["userTeamPoint"] != "": user_team_point_data = json.loads(existing_team["userTeamPoint"]) updated = False