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
+Add
-
+ {% if "{:08b}".format(sesh.permissions)[6] == "1" %}
+
+
+ {% endif %}
+ {% if "{:08b}".format(sesh.permissions)[5] == "1" %}
+
+
+ {% 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()