diff --git a/changelog.md b/changelog.md index 3aa0559..437a3b2 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. +## 20240318 +### CXB ++ Fixing handle_data_shop_list_detail_request for Sunrise S1 + ## 20240302 ### SAO + Fixing new profile creation with right heroes and start VP diff --git a/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py b/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py new file mode 100644 index 0000000..2d4074a --- /dev/null +++ b/core/data/alembic/versions/6a7e8277763b_gekichu_rating_tables.py @@ -0,0 +1,56 @@ +"""GekiChu rating tables + +Revision ID: 6a7e8277763b +Revises: d8950c7ce2fc +Create Date: 2024-03-13 12:18:53.210018 + +""" +from alembic import op +from sqlalchemy import Column, Integer, String + + +# revision identifiers, used by Alembic. +revision = '6a7e8277763b' +down_revision = 'd8950c7ce2fc' +branch_labels = None +depends_on = None + +GEKICHU_RATING_TABLE_NAMES = [ + "chuni_profile_rating", + "ongeki_profile_rating", +] + +def upgrade(): + for table_name in GEKICHU_RATING_TABLE_NAMES: + op.create_table( + table_name, + Column("id", Integer, primary_key=True, nullable=False), + Column("user", Integer, nullable=False), + Column("version", Integer, nullable=False), + Column("type", String(255), nullable=False), + Column("index", Integer, nullable=False), + Column("musicId", Integer), + Column("difficultId", Integer), + Column("romVersionCode", Integer), + Column("score", Integer), + mysql_charset="utf8mb4", + ) + op.create_foreign_key( + None, + table_name, + "aime_user", + ["user"], + ["id"], + ondelete="cascade", + onupdate="cascade", + ) + op.create_unique_constraint( + f"{table_name}_uk", + table_name, + ["user", "version", "type", "index"], + ) + + +def downgrade(): + for table_name in GEKICHU_RATING_TABLE_NAMES: + op.drop_table(table_name) diff --git a/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py new file mode 100644 index 0000000..04d2217 --- /dev/null +++ b/core/data/alembic/versions/81e44dd6047a_mai2_buddies_support.py @@ -0,0 +1,68 @@ +"""mai2_buddies_support + +Revision ID: 81e44dd6047a +Revises: d8950c7ce2fc +Create Date: 2024-03-12 19:10:37.063907 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "81e44dd6047a" +down_revision = "6a7e8277763b" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "mai2_playlog_2p", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user", sa.Integer(), nullable=False), + sa.Column("userId1", sa.Integer(), nullable=True), + sa.Column("userId2", sa.Integer(), nullable=True), + sa.Column("userName1", sa.String(length=25), nullable=True), + sa.Column("userName2", sa.String(length=25), nullable=True), + sa.Column("regionId", sa.Integer(), nullable=True), + sa.Column("placeId", sa.Integer(), nullable=True), + sa.Column("user2pPlaylogDetailList", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint( + ["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade" + ), + sa.PrimaryKeyConstraint("id"), + mysql_charset="utf8mb4", + ) + + op.add_column( + "mai2_playlog", + sa.Column( + "extBool1", sa.Boolean(), nullable=True, server_default=sa.text("NULL") + ), + ) + + op.add_column( + "mai2_profile_detail", + sa.Column( + "renameCredit", sa.Integer(), nullable=True, server_default=sa.text("NULL") + ), + ) + op.add_column( + "mai2_profile_detail", + sa.Column( + "currentPlayCount", + sa.Integer(), + nullable=True, + server_default=sa.text("NULL"), + ), + ) + + +def downgrade(): + op.drop_table("mai2_playlog_2p") + + op.drop_column("mai2_playlog", "extBool1") + op.drop_column("mai2_profile_detail", "renameCredit") + op.drop_column("mai2_profile_detail", "currentPlayCount") diff --git a/core/data/schema/arcade.py b/core/data/schema/arcade.py index 03d0915..680f827 100644 --- a/core/data/schema/arcade.py +++ b/core/data/schema/arcade.py @@ -94,7 +94,7 @@ class ArcadeData(BaseData): return None return result.fetchone() - async def put_machine( + async def create_machine( self, arcade_id: int, serial: str = "", @@ -102,12 +102,12 @@ class ArcadeData(BaseData): game: str = None, is_cab: bool = False, ) -> Optional[int]: - if arcade_id: + if not arcade_id: self.logger.error(f"{__name__ }: Need arcade id!") return None sql = machine.insert().values( - arcade=arcade_id, keychip=serial, board=board, game=game, is_cab=is_cab + arcade=arcade_id, serial=serial, board=board, game=game, is_cab=is_cab ) result = await self.execute(sql) @@ -148,15 +148,15 @@ class ArcadeData(BaseData): return None return result.fetchall() - async def put_arcade( + async def create_arcade( self, - name: str, + name: str = None, nickname: str = None, country: str = "JPN", country_id: int = 1, state: str = "", city: str = "", - regional_id: int = 1, + region_id: int = 1, ) -> Optional[int]: if nickname is None: nickname = name @@ -168,7 +168,7 @@ class ArcadeData(BaseData): country_id=country_id, state=state, city=city, - regional_id=regional_id, + region_id=region_id, ) result = await self.execute(sql) @@ -206,8 +206,8 @@ class ArcadeData(BaseData): return None return result.lastrowid - async def format_serial( - self, platform_code: str, platform_rev: int, serial_num: int, append: int = 4152 + def format_serial( # TODO: Actual serial stuff + self, platform_code: str, platform_rev: int, serial_num: int, append: int = 8888 ) -> str: return f"{platform_code}{platform_rev:02d}A{serial_num:04d}{append:04d}" # 0x41 = A, 0x52 = R diff --git a/core/frontend.py b/core/frontend.py index 8e480b0..9a00ca5 100644 --- a/core/frontend.py +++ b/core/frontend.py @@ -10,6 +10,9 @@ import bcrypt import re import jwt import yaml +import secrets +import string +import random from base64 import b64decode from enum import Enum from datetime import datetime, timezone @@ -131,6 +134,10 @@ class FrontendServlet(): Route("/", self.system.render_GET, methods=['GET']), Route("/lookup.user", self.system.lookup_user, methods=['GET']), Route("/lookup.shop", self.system.lookup_shop, methods=['GET']), + Route("/add.user", self.system.add_user, methods=['POST']), + Route("/add.card", self.system.add_card, methods=['POST']), + Route("/add.shop", self.system.add_shop, methods=['POST']), + Route("/add.cab", self.system.add_cab, methods=['POST']), ]), Mount("/shop", routes=[ Route("/", self.arcade.render_GET, methods=['GET']), @@ -551,10 +558,16 @@ class FE_System(FE_Base): if not usr_sesh or not self.test_perm_minimum(usr_sesh.permissions, PermissionOffset.USERMOD): return RedirectResponse("/gate/", 303) + if request.query_params.get("e", None): + err = int(request.query_params.get("e")) + else: + err = 0 + return Response(template.render( title=f"{self.core_config.server.name} | System", sesh=vars(usr_sesh), usrlist=[], + error = err ), media_type="text/html; charset=utf-8") async def lookup_user(self, request: Request): @@ -661,6 +674,113 @@ class FE_System(FE_Base): shoplist=shoplist, ), media_type="text/html; charset=utf-8") + async def add_user(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + username = frm.get("userName", None) + email = frm.get("userEmail", None) + perm = frm.get("usrPerm", "1") + passwd = "".join( + secrets.choice(string.ascii_letters + string.digits) for i in range(20) + ) + hash = bcrypt.hashpw(passwd.encode(), bcrypt.gensalt()) + + if not email: + return RedirectResponse("/sys/?e=4", 303) + + uid = await self.data.user.create_user(username=username if username else None, email=email, password=hash.decode(), permission=int(perm)) + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + usradd={"id": uid, "username": username, "password": passwd}, + ), media_type="text/html; charset=utf-8") + + async def add_card(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + userid = frm.get("cardUsr", None) + access_code = frm.get("cardAc", None) + idm = frm.get("cardIdm", None) + + if userid is None or access_code is None or not userid.isdigit() or not len(access_code) == 20 or not access_code.isdigit: + return RedirectResponse("/sys/?e=4", 303) + + cardid = await self.data.card.create_card(int(userid), access_code) + if not cardid: + return RedirectResponse("/sys/?e=99", 303) + + if idm is not None: + # TODO: save IDM + pass + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + cardadd={"id": cardid, "user": userid, "access_code": access_code}, + ), media_type="text/html; charset=utf-8") + + async def add_shop(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + name = frm.get("shopName", None) + country = frm.get("shopCountry", "JPN") + ip = frm.get("shopIp", None) + + acid = await self.data.arcade.create_arcade(name if name else None, name if name else None, country) + if not acid: + return RedirectResponse("/sys/?e=99", 303) + + if ip: + # TODO: set IP + pass + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + shopadd={"id": acid}, + ), media_type="text/html; charset=utf-8") + + async def add_cab(self, request: Request): + template = self.environment.get_template("core/templates/sys/index.jinja") + + usr_sesh = self.validate_session(request) + if not usr_sesh or not self.test_perm(usr_sesh.permissions, PermissionOffset.ACMOD): + return RedirectResponse("/gate/", 303) + + frm = await request.form() + shopid = frm.get("cabShop", None) + serial = frm.get("cabSerial", None) + game_code = frm.get("cabGame", None) + + if not shopid or not shopid.isdigit(): + return RedirectResponse("/sys/?e=4", 303) + + if not serial: + serial = self.data.arcade.format_serial("A69E", 1, random.randint(1, 9999)) + + cab_id = await self.data.arcade.create_machine(int(shopid), serial, None, game_code if game_code else None) + + return Response(template.render( + title=f"{self.core_config.server.name} | System", + sesh=vars(usr_sesh), + cabadd={"id": cab_id, "serial": serial}, + ), media_type="text/html; charset=utf-8") + class FE_Arcade(FE_Base): async def render_GET(self, request: Request): template = self.environment.get_template("core/templates/arcade/index.jinja") diff --git a/core/templates/arcade/index.jinja b/core/templates/arcade/index.jinja index 1de4301..393443a 100644 --- a/core/templates/arcade/index.jinja +++ b/core/templates/arcade/index.jinja @@ -10,7 +10,7 @@ Cab added successfully {% endif %} {% else %} diff --git a/core/templates/sys/index.jinja b/core/templates/sys/index.jinja index 92dd864..b589d90 100644 --- a/core/templates/sys/index.jinja +++ b/core/templates/sys/index.jinja @@ -4,6 +4,7 @@ {% if error is defined %} {% include "core/templates/widgets/err_banner.jinja" %} {% endif %} +

Search

{% if "{:08b}".format(sesh.permissions)[6] == "1" %}
@@ -21,7 +22,12 @@ OR
- + +
+ OR +
+ +

@@ -63,7 +69,121 @@
{% endif %}
+

Add

- + {% if "{:08b}".format(sesh.permissions)[6] == "1" %} +
+
+

Add User

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+

Add Card

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ {% endif %} + {% if "{:08b}".format(sesh.permissions)[5] == "1" %} +
+
+

Add Shop

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+

Add Machine

+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ {% endif %} +
+
+ {% if "{:08b}".format(sesh.permissions)[6] == "1" %} +
+ {% if usradd is defined %} +
Added user {{ usradd.username if usradd.username is not none else "with no name"}} with id {{usradd.id}} and password {{ usradd.password }}
+ {% endif %} +
+
+ {% if cardadd is defined %} +
Added {{ cardadd.access_code }} with id {{cardadd.id}} to user {{ cardadd.user }}
+ {% endif %} +
+ {% endif %} + {% if "{:08b}".format(sesh.permissions)[5] == "1" %} +
+ {% if shopadd is defined %} +
Added Shop {{ shopadd.id }}
+ {% endif %} +
+
+ {% if cabadd is defined %} +
Added Machine {{ cabadd.id }} with serial {{ cabadd.serial }}
+ {% endif %} +
+ {% endif %}
{% endblock content %} \ No newline at end of file diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index a936546..fcb6128 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -194,6 +194,7 @@ Config file is located in `config/cxb.yaml`. | SDEZ | 18 | maimai DX UNiVERSE PLUS | | SDEZ | 19 | maimai DX FESTiVAL | | SDEZ | 20 | maimai DX FESTiVAL PLUS | +| SDEZ | 21 | maimai DX BUDDiES | ### Importer @@ -408,6 +409,7 @@ After that, on next login the present should be received (or whenever it suppose * UNiVERSE PLUS: Yes * FESTiVAL: Yes (added in A031) * FESTiVAL PLUS: Yes (added in A035) + * BUDDiES: Yes (added in A039) * O.N.G.E.K.I. bright MEMORY: Yes diff --git a/readme.md b/readme.md index 1226784..114fef5 100644 --- a/readme.md +++ b/readme.md @@ -47,6 +47,7 @@ Games listed below have been tested and confirmed working. Only game versions ol + UNiVERSE PLUS + FESTiVAL + FESTiVAL PLUS + + BUDDiES + O.N.G.E.K.I. + SUMMER diff --git a/requirements.txt b/requirements.txt index 1bfd667..fe5b4ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,24 @@ -mypy -wheel -pytz -pyyaml -sqlalchemy==1.4.46 -mysqlclient -pyopenssl -service_identity -PyCryptodome -inflection -coloredlogs -pylibmc; platform_system != "Windows" -wacky -bcrypt -jinja2 -protobuf -pillow -pyjwt==2.8.0 -websockets -starlette -asyncio -uvicorn -alembic +mypy +wheel +pytz +pyyaml +sqlalchemy==1.4.46 +mysqlclient +pyopenssl +service_identity +PyCryptodome +inflection +coloredlogs +pylibmc; platform_system != "Windows" +wacky +bcrypt +jinja2 +protobuf +pillow +pyjwt==2.8.0 +websockets +starlette +asyncio +uvicorn +alembic +python-multipart \ No newline at end of file diff --git a/titles/chuni/base.py b/titles/chuni/base.py index 72d665e..c3b9ca2 100644 --- a/titles/chuni/base.py +++ b/titles/chuni/base.py @@ -925,6 +925,17 @@ class ChuniBase: for rp in upsert["userRecentPlayerList"]: pass + for rating_type in {"userRatingBaseList", "userRatingBaseHotList", "userRatingBaseNextList"}: + if rating_type not in upsert: + continue + + await self.data.profile.put_profile_rating( + user_id, + self.version, + rating_type, + upsert[rating_type], + ) + return {"returnCode": "1"} async def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: diff --git a/titles/chuni/schema/profile.py b/titles/chuni/schema/profile.py index 849a5b3..9864928 100644 --- a/titles/chuni/schema/profile.py +++ b/titles/chuni/schema/profile.py @@ -1,10 +1,9 @@ from typing import Dict, List, Optional -from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_ -from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON, BigInteger -from sqlalchemy.engine.base import Connection +from sqlalchemy import Table, Column, UniqueConstraint, and_ +from sqlalchemy.types import Integer, String, Boolean, JSON, BigInteger from sqlalchemy.schema import ForeignKey from sqlalchemy.engine import Row -from sqlalchemy.sql import func, select +from sqlalchemy.sql import select, delete from sqlalchemy.dialects.mysql import insert from core.data.schema import BaseData, metadata @@ -393,6 +392,26 @@ team = Table( mysql_charset="utf8mb4", ) +rating = Table( + "chuni_profile_rating", + 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("type", String(255), nullable=False), + Column("index", Integer, nullable=False), + Column("musicId", Integer), + Column("difficultId", Integer), + Column("romVersionCode", Integer), + Column("score", Integer), + UniqueConstraint("user", "version", "type", "index", name="chuni_profile_rating_best_uk"), + mysql_charset="utf8mb4", +) + class ChuniProfileData(BaseData): async def update_name(self, user_id: int, new_name: str) -> bool: @@ -714,3 +733,27 @@ class ChuniProfileData(BaseData): return { "total_play_count": total_play_count } + + async def put_profile_rating( + self, + aime_id: int, + version: int, + rating_type: str, + rating_data: List[Dict], + ): + inserted_values = [ + {"user": aime_id, "version": version, "type": rating_type, "index": i, **x} + for (i, x) in enumerate(rating_data) + ] + sql = insert(rating).values(inserted_values) + update_dict = {x.name: x for x in sql.inserted if x.name != "id"} + sql = sql.on_duplicate_key_update(**update_dict) + result = await self.execute(sql) + + if result is None: + self.logger.warn( + f"put_profile_rating: Could not insert {rating_type}, aime_id: {aime_id}", + ) + return + + return result.lastrowid diff --git a/titles/cm/read.py b/titles/cm/read.py index 0e2c841..2b5ec8a 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -206,6 +206,7 @@ class CardMakerReader(BaseReader): "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, "1.35": Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS, + "1.40": Mai2Constants.VER_MAIMAI_DX_BUDDIES, } for root, dirs, files in os.walk(base_dir): @@ -225,12 +226,6 @@ class CardMakerReader(BaseReader): True if troot.find("disable").text == "false" else False ) - # check if a date is part of the name and disable the - # card if it is - enabled = ( - False if re.search(r"\d{2}/\d{2}/\d{2}", name) else enabled - ) - await self.mai2_data.static.put_card( version, card_id, name, enabled=enabled ) diff --git a/titles/cxb/data/rss1/Shop/ShopList_Sale.csv b/titles/cxb/data/rss1/Shop/ShopList_Sale.csv deleted file mode 100644 index 5e9fb72..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_Sale.csv +++ /dev/null @@ -1,3 +0,0 @@ -saleID.,�J�n��,�I����,ShopID,Price, -0,1411696799,1443236400,0,7000, -1,1411783199,1443322800,1,7000, diff --git a/titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv b/titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv deleted file mode 100644 index 44c4843..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv +++ /dev/null @@ -1,4 +0,0 @@ -shopID.,pNo.,Ver.,otO,otOQID,,o,Ŏ,ItemCode,i,\^Cv,Text,Type,Value(op),Value,Ώۋ,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,vC,n, -3000,1,1.00.00,1,-1,-,1411697520.0288,1443233520.0288,skb0000,10,1,MASTERȏ2S+ȏtR{NAB,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -3001,2,1.00.00,1,-1,-,1411697520.0288,1443233520.0288,skb0001,10,1,Next Frontier (MasterjNA,0,-1,-1,bleeze,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -3002,3,1.00.00,1,-1,-,1412103600.0288,1443639598.992,skb0002,10,2,Masterȏ1ȂS+ȏŃNAB,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1, diff --git a/titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv b/titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv deleted file mode 100644 index bb6486e..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv +++ /dev/null @@ -1,11 +0,0 @@ -shopID.,�����pNo.,Ver.,�o���t���O,�o���t���O�Q��ID,����,�o������,���Ŏ���,ItemCode,���i,�\���^�C�v,Text,Type,Value(op),Value,�Ώۋ�,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,�v���C��,�n��, -5000,1,10000,1,-1,-,1411697520,1443233520,ske0000,10,1,MASTER�ȏ��2��S+�ȏ�t���R���{�N���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5001,2,10000,1,-1,-,1411697520,1443233520,ske0001,10,1,Next Frontier (Master�j���N���A,0,-1,-1,megaro,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5002,3,10000,1,-1,-,1412103600,1443639598,ske0002,10,2,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1, -5003,4,10000,1,-1,-,1412103600,1443639598,ske0003,10,0,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5004,5,10000,1,-1,-,1412103600,1443639598,ske0004,10,2,2�ȃN���A,1,1,2,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5005,5,10000,1,-1,-,1412103600,1443639598,ske0005,10,2,3�ȃN���A,1,1,3,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5006,5,10000,1,-1,-,1412103600,1443639598,ske0006,10,2,4�ȃN���A,1,1,4,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5007,5,10000,1,-1,-,1412103600,1443639598,ske0007,10,2,5�ȃN���A,1,1,5,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5008,5,10000,1,-1,-,1412103600,1443639598,ske0008,10,2,6�ȃN���A,1,1,6,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -5009,5,10000,1,-1,-,1412103600,1443639598,ske0009,10,2,7�ȃN���A,1,1,7,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, diff --git a/titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv b/titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv deleted file mode 100644 index 0b496f2..0000000 --- a/titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv +++ /dev/null @@ -1,6 +0,0 @@ -shopID.,�����pNo.,Ver.,�o���t���O,�o���t���O�Q��ID,����,�o������,���Ŏ���,ItemCode,���i,�\���^�C�v,Text,Type,Value(op),Value,�Ώۋ�,Difficulty(op),Difficulty,Level(op),Level,Grade(Op),Grade,GaugeType(op),GaugeType,HS(op)i,HS,APP,DAP,F-V,F-H,FullCombo,Combo(op),Combo,ClearRate(op),ClearRate,�v���C��,�n��, -4000,1,10000,1,-1,-,1411697520,4096483201,skt0000,10,1,MASTER�ȏ��2��S+�ȏ�t���R���{�N���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -4001,2,10000,1,-1,-,1411697520,4096483201,skt0001,10,1,Next Frontier (Master�j���N���A,0,-1,-1,megaro,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -4002,3,10000,1,-1,-,1412103600,4096483201,skt0002,10,2,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,1, -4003,4,10000,1,-1,-,1412103600,4096483201,skt0003,10,0,Master�ȏ��1�Ȃ�S+�ȏ�ŃN���A����B,1,1,1,-,1,2,-1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, -4004,5,10000,1,-1,-,1412103600,4096483201,skt0004,10,2,aaaaaaaaaaaaaaaaa,1,1,20,-,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,,, diff --git a/titles/cxb/rss1.py b/titles/cxb/rss1.py index cce0315..4999cc5 100644 --- a/titles/cxb/rss1.py +++ b/titles/cxb/rss1.py @@ -73,42 +73,6 @@ class CxbRevSunriseS1(CxbBase): for line in lines: ret_str += f"{line[:-1]}\r\n" - # ShopListSale load - ret_str += "\r\n#ShopListSale\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_Sale.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - - # ShopListSkinBg load - ret_str += "\r\n#ShopListSkinBg\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_SkinBg.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - - # ShopListSkinEffect load - ret_str += "\r\n#ShopListSkinEffect\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_SkinEffect.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - - # ShopListSkinNotes load - ret_str += "\r\n#ShopListSkinNotes\r\n" - with open( - r"titles/cxb/data/rss1/Shop/ShopList_SkinNotes.csv", encoding="shift-jis" - ) as shop: - lines = shop.readlines() - for line in lines: - ret_str += f"{line[:-1]}\r\n" - # ShopListTitle load ret_str += "\r\n#ShopListTitle\r\n" with open( diff --git a/titles/mai2/buddies.py b/titles/mai2/buddies.py new file mode 100644 index 0000000..f04b215 --- /dev/null +++ b/titles/mai2/buddies.py @@ -0,0 +1,32 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.festivalplus import Mai2FestivalPlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2Buddies(Mai2FestivalPlus): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_BUDDIES + + async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = await super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker + user_data["lastDataVersion"] = "1.40.00" + return user_data + + async def handle_get_user_new_item_api_request(self, data: Dict) -> Dict: + # TODO: Added in 1.41, implement this? + user_id = data["userId"] + version = data.get("version", 1041000) + user_playlog_list = data.get("userPlaylogList", []) + + return { + "userId": user_id, + "itemKind": -1, + "itemId": -1, + } + diff --git a/titles/mai2/const.py b/titles/mai2/const.py index a4c29db..92e00e7 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -53,6 +53,7 @@ class Mai2Constants: VER_MAIMAI_DX_UNIVERSE_PLUS = 18 VER_MAIMAI_DX_FESTIVAL = 19 VER_MAIMAI_DX_FESTIVAL_PLUS = 20 + VER_MAIMAI_DX_BUDDIES = 21 VERSION_STRING = ( "maimai", @@ -76,6 +77,7 @@ class Mai2Constants: "maimai DX UNiVERSE PLUS", "maimai DX FESTiVAL", "maimai DX FESTiVAL PLUS", + "maimai DX BUDDiES" ) @classmethod diff --git a/titles/mai2/dx.py b/titles/mai2/dx.py index fff39f2..837fb8f 100644 --- a/titles/mai2/dx.py +++ b/titles/mai2/dx.py @@ -212,6 +212,9 @@ class Mai2DX(Mai2Base): ), ) await self.data.item.put_friend_season_ranking(user_id, fsr) + + if "user2pPlaylog" in upsert: + await self.data.score.put_playlog_2p(user_id, upsert["user2pPlaylog"]) return {"returnCode": 1, "apiName": "UpsertUserAllApi"} diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 2ee4cae..df167f0 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -25,6 +25,7 @@ from .universe import Mai2Universe from .universeplus import Mai2UniversePlus from .festival import Mai2Festival from .festivalplus import Mai2FestivalPlus +from .buddies import Mai2Buddies class Mai2Servlet(BaseServlet): @@ -58,6 +59,7 @@ class Mai2Servlet(BaseServlet): Mai2UniversePlus, Mai2Festival, Mai2FestivalPlus, + Mai2Buddies ] self.logger = logging.getLogger("mai2") @@ -257,8 +259,10 @@ class Mai2Servlet(BaseServlet): internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS elif version >= 130 and version < 135: # FESTiVAL internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL - elif version >= 135: # FESTiVAL PLUS + elif version >= 135 and version < 140: # FESTiVAL PLUS internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL_PLUS + elif version >= 140: # BUDDiES + internal_ver = Mai2Constants.VER_MAIMAI_DX_BUDDIES if ( request.headers.get("Mai-Encoding") is not None diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index b4cdbd8..8f1d5f3 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -40,6 +40,8 @@ detail = Table( Column("charaLockSlot", JSON), Column("contentBit", BigInteger), Column("playCount", Integer), + Column("currentPlayCount", Integer), # new with buddies + Column("renameCredit", Integer), # new with buddies Column("mapStock", Integer), # new with fes+ Column("eventWatchedDate", String(25)), Column("lastGameId", String(25)), diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 51dcc18..d13faae 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -145,11 +145,34 @@ playlog = Table( Column("isNewFree", Boolean), Column("extNum1", Integer), Column("extNum2", Integer), - Column("extNum4", Integer, server_default="0"), + Column("extNum4", Integer), + Column("extBool1", Boolean), # new with buddies Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) +# new with buddies +playlog_2p = Table( + "mai2_playlog_2p", + metadata, + Column("id", Integer, primary_key=True, nullable=False), + Column( + "user", + ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), + nullable=False, + ), + # TODO: ForeignKey to aime_user? + Column("userId1", Integer), + Column("userId2", Integer), + # TODO: ForeignKey to mai2_profile_detail? + Column("userName1", String(25)), + Column("userName2", String(25)), + Column("regionId", Integer), + Column("placeId", Integer), + Column("user2pPlaylogDetailList", JSON), + mysql_charset="utf8mb4", +) + course = Table( "mai2_score_course", metadata, @@ -343,6 +366,18 @@ class Mai2ScoreData(BaseData): self.logger.error(f"put_playlog: Failed to insert! user_id {user_id} is_dx {is_dx}") return None return result.lastrowid + + async def put_playlog_2p(self, user_id: int, playlog_2p_data: Dict) -> Optional[int]: + playlog_2p_data["user"] = user_id + sql = insert(playlog_2p).values(**playlog_2p_data) + + conflict = sql.on_duplicate_key_update(**playlog_2p_data) + + result = await self.execute(conflict) + if result is None: + self.logger.error(f"put_playlog_2p: Failed to insert! user_id {user_id}") + return None + return result.lastrowid async def put_course(self, user_id: int, course_data: Dict) -> Optional[int]: course_data["user"] = user_id diff --git a/titles/ongeki/base.py b/titles/ongeki/base.py index d40ece8..ca5a38c 100644 --- a/titles/ongeki/base.py +++ b/titles/ongeki/base.py @@ -1067,6 +1067,24 @@ class OngekiBase: if "userKopList" in upsert: for x in upsert["userKopList"]: await self.data.profile.put_kop(user_id, x) + + for rating_type in { + "userRatingBaseBestList", + "userRatingBaseBestNewList", + "userRatingBaseHotList", + "userRatingBaseNextList", + "userRatingBaseNextNewList", + "userRatingBaseHotNextList", + }: + if rating_type not in upsert: + continue + + await self.data.profile.put_profile_rating( + user_id, + self.version, + rating_type, + upsert[rating_type], + ) return {"returnCode": 1, "apiName": "upsertUserAll"} diff --git a/titles/ongeki/schema/profile.py b/titles/ongeki/schema/profile.py index 1f6bcab..b42a0a3 100644 --- a/titles/ongeki/schema/profile.py +++ b/titles/ongeki/schema/profile.py @@ -246,6 +246,26 @@ rival = Table( mysql_charset="utf8mb4", ) +rating = Table( + "ongeki_profile_rating", + 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("type", String(255), nullable=False), + Column("index", Integer, nullable=False), + Column("musicId", Integer), + Column("difficultId", Integer), + Column("romVersionCode", Integer), + Column("score", Integer), + UniqueConstraint("user", "version", "type", "index", name="ongeki_profile_rating_best_uk"), + mysql_charset="utf8mb4", +) + class OngekiProfileData(BaseData): def __init__(self, cfg: CoreConfig, conn: Connection) -> None: @@ -508,10 +528,35 @@ class OngekiProfileData(BaseData): ) return None return result.lastrowid + async def delete_rival(self, aime_id: int, rival_id: int) -> Optional[int]: sql = delete(rival).where(rival.c.user==aime_id, rival.c.rivalUserId==rival_id) result = await self.execute(sql) if result is None: self.logger.error(f"delete_rival: failed to delete! aime_id: {aime_id}, rival_id: {rival_id}") else: - return result.rowcount \ No newline at end of file + return result.rowcount + + async def put_profile_rating( + self, + aime_id: int, + version: int, + rating_type: str, + rating_data: List[Dict], + ): + inserted_values = [ + {"user": aime_id, "version": version, "type": rating_type, "index": i, **x} + for (i, x) in enumerate(rating_data) + ] + sql = insert(rating).values(inserted_values) + update_dict = {x.name: x for x in sql.inserted if x.name != "id"} + sql = sql.on_duplicate_key_update(**update_dict) + result = await self.execute(sql) + + if result is None: + self.logger.warn( + f"put_profile_rating_{rating_type}: Could not insert rating entries, aime_id: {aime_id}", + ) + return + + return result.lastrowid diff --git a/titles/wacca/s.py b/titles/wacca/s.py index aab2659..4e43237 100644 --- a/titles/wacca/s.py +++ b/titles/wacca/s.py @@ -11,25 +11,24 @@ from titles.wacca.handlers import * class WaccaS(WaccaBase): - allowed_stages = [ - (1513, 13), - (1512, 12), - (1511, 11), - (1510, 10), - (1509, 9), - (1508, 8), - (1507, 7), - (1506, 6), - (1505, 5), - (1514, 4), - (1513, 3), - (1512, 2), - (1511, 1), - ] - def __init__(self, cfg: CoreConfig, game_cfg: WaccaConfig) -> None: super().__init__(cfg, game_cfg) self.version = WaccaConstants.VER_WACCA_S + self.allowed_stages = [ + (1513, 13), + (1512, 12), + (1511, 11), + (1510, 10), + (1509, 9), + (1508, 8), + (1507, 7), + (1506, 6), + (1505, 5), + (1504, 4), + (1503, 3), + (1502, 2), + (1501, 1), + ] async def handle_advertise_GetNews_request(self, data: Dict) -> Dict: resp = GetNewsResponseV2()