From 4446ff1f21071f2aa6b58345bc04fcbec5e65676 Mon Sep 17 00:00:00 2001 From: Hay1tsme Date: Fri, 28 Jun 2024 15:48:27 -0400 Subject: [PATCH] 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: