diff --git a/core/adb_handlers/__init__.py b/core/adb_handlers/__init__.py index 0c96baf..9a8121f 100644 --- a/core/adb_handlers/__init__.py +++ b/core/adb_handlers/__init__.py @@ -2,5 +2,5 @@ from .base import ADBBaseRequest, ADBBaseResponse, ADBHeader, ADBHeaderException from .base import CompanyCodes, ReaderFwVer, CMD_CODE_GOODBYE, HEADER_SIZE from .lookup import ADBLookupRequest, ADBLookupResponse, ADBLookupExResponse from .campaign import ADBCampaignClearRequest, ADBCampaignClearResponse, ADBCampaignResponse, ADBOldCampaignRequest, ADBOldCampaignResponse -from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookup2Request, ADBFelicaLookup2Response +from .felica import ADBFelicaLookupRequest, ADBFelicaLookupResponse, ADBFelicaLookupExRequest, ADBFelicaLookupExResponse from .log import ADBLogExRequest, ADBLogRequest, ADBStatusLogRequest, ADBLogExResponse diff --git a/core/adb_handlers/felica.py b/core/adb_handlers/felica.py index 22c9f05..b7fdc2e 100644 --- a/core/adb_handlers/felica.py +++ b/core/adb_handlers/felica.py @@ -35,7 +35,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse): return self.head.make() + resp_struct -class ADBFelicaLookup2Request(ADBBaseRequest): +class ADBFelicaLookupExRequest(ADBBaseRequest): def __init__(self, data: bytes) -> None: super().__init__(data) self.random = struct.unpack_from("<16s", data, 0x20)[0] @@ -46,7 +46,7 @@ class ADBFelicaLookup2Request(ADBBaseRequest): self.company = CompanyCodes(int.from_bytes(company, 'little')) self.fw_ver = ReaderFwVer.from_byte(fw_ver) -class ADBFelicaLookup2Response(ADBBaseResponse): +class ADBFelicaLookupExResponse(ADBBaseResponse): def __init__(self, user_id: Union[int, None] = None, access_code: Union[str, None] = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x12, length: int = 0x130, status: int = 1) -> None: super().__init__(code, length, status, game_id, store_id, keychip_id) self.user_id = user_id if user_id is not None else -1 @@ -56,7 +56,7 @@ class ADBFelicaLookup2Response(ADBBaseResponse): self.auth_key = [0] * 256 @classmethod - def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookup2Response": + def from_req(cls, req: ADBHeader, user_id: Union[int, None] = None, access_code: Union[str, None] = None) -> "ADBFelicaLookupExResponse": c = cls(user_id, access_code, req.game_id, req.store_id, req.keychip_id) c.head.protocol_ver = req.protocol_ver return c diff --git a/core/aimedb.py b/core/aimedb.py index 88796c7..6d5bd57 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -176,6 +176,12 @@ class AimedbServlette(): async def handle_lookup(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) + if req.access_code == "00000000000000000000": + self.logger.warn(f"All-zero access code from {req.head.keychip_id}") + ret = ADBLookupResponse.from_req(req.head, -1) + ret.head.status = ADBStatus.BAN_SYS + return ret + user_id = await self.data.card.get_user_id_from_card(req.access_code) is_banned = await self.data.card.get_card_banned(req.access_code) is_locked = await self.data.card.get_card_locked(req.access_code) @@ -201,6 +207,12 @@ class AimedbServlette(): async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: req = ADBLookupRequest(data) + if req.access_code == "00000000000000000000": + self.logger.warn(f"All-zero access code from {req.head.keychip_id}") + ret = ADBLookupExResponse.from_req(req.head, -1) + ret.head.status = ADBStatus.BAN_SYS + return ret + user_id = await self.data.card.get_user_id_from_card(req.access_code) is_banned = await self.data.card.get_card_banned(req.access_code) @@ -241,6 +253,12 @@ class AimedbServlette(): """ req = ADBFelicaLookupRequest(data) idm = req.idm.zfill(16) + if idm == "0000000000000000": + self.logger.warn(f"All-zero IDm from {req.head.keychip_id}") + ret = ADBFelicaLookupResponse.from_req(req.head, "00000000000000000000") + ret.head.status = ADBStatus.BAN_SYS + return ret + card = await self.data.card.get_card_by_idm(idm) if not card: ac = self.data.card.to_access_code(idm) @@ -262,6 +280,14 @@ class AimedbServlette(): because we don't implement felica_lookup properly. """ req = ADBFelicaLookupRequest(data) + idm = req.idm.zfill(16) + + if idm == "0000000000000000": + self.logger.warn(f"All-zero IDm from {req.head.keychip_id}") + ret = ADBFelicaLookupResponse.from_req(req.head, "00000000000000000000") + ret.head.status = ADBStatus.BAN_SYS + return ret + ac = self.data.card.to_access_code(req.idm) if self.config.server.allow_user_registration: @@ -292,9 +318,16 @@ class AimedbServlette(): return ADBFelicaLookupResponse.from_req(req.head, ac) async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: - req = ADBFelicaLookup2Request(data) + req = ADBFelicaLookupExRequest(data) user_id = None idm = req.idm.zfill(16) + + if idm == "0000000000000000": + self.logger.warn(f"All-zero IDm from {req.head.keychip_id}") + ret = ADBFelicaLookupExResponse.from_req(req.head, -1, "00000000000000000000") + ret.head.status = ADBStatus.BAN_SYS + return ret + card = await self.data.card.get_card_by_idm(idm) if not card: access_code = self.data.card.to_access_code(idm) @@ -314,7 +347,7 @@ class AimedbServlette(): f"idm {idm} ipm {req.pmm} -> access_code {access_code} user_id {user_id}" ) - resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code) + resp = ADBFelicaLookupExResponse.from_req(req.head, user_id, access_code) if user_id > 0: if card['is_banned'] and card['is_locked']: @@ -347,6 +380,12 @@ class AimedbServlette(): async def handle_register(self, data: bytes, resp_code: int) -> bytes: req = ADBLookupRequest(data) user_id = -1 + + if req.access_code == "00000000000000000000": + self.logger.warn(f"All-zero access code from {req.head.keychip_id}") + ret = ADBLookupResponse.from_req(req.head, -1) + ret.head.status = ADBStatus.BAN_SYS + return ret if self.config.server.allow_user_registration: user_id = await self.data.user.create_user() diff --git a/titles/chuni/const.py b/titles/chuni/const.py index d037842..003c618 100644 --- a/titles/chuni/const.py +++ b/titles/chuni/const.py @@ -19,11 +19,13 @@ class ChuniConstants: VER_CHUNITHM_CRYSTAL = 8 VER_CHUNITHM_CRYSTAL_PLUS = 9 VER_CHUNITHM_PARADISE = 10 + VER_CHUNITHM_NEW = 11 VER_CHUNITHM_NEW_PLUS = 12 VER_CHUNITHM_SUN = 13 VER_CHUNITHM_SUN_PLUS = 14 VER_CHUNITHM_LUMINOUS = 15 + VERSION_NAMES = [ "CHUNITHM", "CHUNITHM PLUS", @@ -43,6 +45,37 @@ class ChuniConstants: "CHUNITHM LUMINOUS", ] + SCORE_RANK_INTERVALS_OLD = [ + (1007500, "SSS"), + (1000000, "SS"), + ( 975000, "S"), + ( 950000, "AAA"), + ( 925000, "AA"), + ( 900000, "A"), + ( 800000, "BBB"), + ( 700000, "BB"), + ( 600000, "B"), + ( 500000, "C"), + ( 0, "D"), + ] + + SCORE_RANK_INTERVALS_NEW = [ + (1009000, "SSS+"), # New only + (1007500, "SSS"), + (1005000, "SS+"), # New only + (1000000, "SS"), + ( 990000, "S+"), # New only + ( 975000, "S"), + ( 950000, "AAA"), + ( 925000, "AA"), + ( 900000, "A"), + ( 800000, "BBB"), + ( 700000, "BB"), + ( 600000, "B"), + ( 500000, "C"), + ( 0, "D"), + ] + @classmethod def game_ver_to_string(cls, ver: int): return cls.VERSION_NAMES[ver] diff --git a/titles/chuni/frontend.py b/titles/chuni/frontend.py index 510ae08..74f7794 100644 --- a/titles/chuni/frontend.py +++ b/titles/chuni/frontend.py @@ -13,6 +13,69 @@ from .config import ChuniConfig from .const import ChuniConstants +def pairwise(iterable): + # https://docs.python.org/3/library/itertools.html#itertools.pairwise + # but for Python < 3.10. pairwise('ABCDEFG') → AB BC CD DE EF FG + iterator = iter(iterable) + a = next(iterator, None) + for b in iterator: + yield a, b + a = b + + +def calculate_song_rank(score: int, game_version: int) -> str: + if game_version >= ChuniConstants.VER_CHUNITHM_NEW: + intervals = ChuniConstants.SCORE_RANK_INTERVALS_NEW + else: + intervals = ChuniConstants.SCORE_RANK_INTERVALS_OLD + + for (min_score, rank) in intervals: + if score >= min_score: + return rank + + return "D" + + +def calculate_song_rating(score: int, chart_constant: float, game_version: int) -> float: + is_new = game_version >= ChuniConstants.VER_CHUNITHM_NEW + + if is_new: # New and later + max_score = 1009000 + max_rating_modifier = 2.15 + else: # Up to Paradise Lost + max_score = 1007500 + max_rating_modifier = 2.0 + + if (score < 500000): + return 0.0 # D + elif (score >= max_score): + return chart_constant + max_rating_modifier # SSS/SSS+ + + # Okay, we're doing this the hard way. + # Rating goes up linearly between breakpoints listed below. + # Pick the score interval in which we are in, then calculate + # the position between possible ratings. + score_intervals = [ + ( 500000, 0.0), # C + ( 800000, max(0.0, (chart_constant - 5.0) / 2)), # BBB + ( 900000, max(0.0, (chart_constant - 5.0))), # A + ( 925000, max(0.0, (chart_constant - 3.0))), # AA + ( 975000, chart_constant), # S + (1000000, chart_constant + 1.0), # SS + (1005000, chart_constant + 1.5), # SS+ + (1007500, chart_constant + 2.0), # SSS + (1009000, chart_constant + max_rating_modifier), # SSS+! + ] + + for ((lo_score, lo_rating), (hi_score, hi_rating)) in pairwise(score_intervals): + if not (lo_score <= score < hi_score): + continue + + interval_pos = (score - lo_score) / (hi_score - lo_score) + return lo_rating + ((hi_rating - lo_rating) * interval_pos) + + + class ChuniFrontend(FE_Base): def __init__( self, cfg: CoreConfig, environment: jinja2.Environment, cfg_dir: str @@ -91,37 +154,27 @@ class ChuniFrontend(FE_Base): base_list=[] if profile and rating: song_records = [] + for song in rating: music_chart = await self.data.static.get_music_chart(usr_sesh.chunithm_version, song.musicId, song.difficultId) - if music_chart: - if (song.score < 800000): - song_rating = 0 - elif (song.score >= 800000 and song.score < 900000): - song_rating = music_chart.level / 2 - 5 - elif (song.score >= 900000 and song.score < 925000): - song_rating = music_chart.level - 5 - elif (song.score >= 925000 and song.score < 975000): - song_rating = music_chart.level - 3 - elif (song.score >= 975000 and song.score < 1000000): - song_rating = (song.score - 975000) / 2500 * 0.1 + music_chart.level - elif (song.score >= 1000000 and song.score < 1005000): - song_rating = (song.score - 1000000) / 1000 * 0.1 + 1 + music_chart.level - elif (song.score >= 1005000 and song.score < 1007500): - song_rating = (song.score - 1005000) / 500 * 0.1 + 1.5 + music_chart.level - elif (song.score >= 1007500 and song.score < 1009000): - song_rating = (song.score - 1007500) / 100 * 0.01 + 2 + music_chart.level - elif (song.score >= 1009000): - song_rating = 2.15 + music_chart.level - song_rating = int(song_rating * 10 ** 2) / 10 ** 2 - song_records.append({ - "difficultId": song.difficultId, - "musicId": song.musicId, - "title": music_chart.title, - "level": music_chart.level, - "score": song.score, - "type": song.type, - "song_rating": song_rating, - }) + if not music_chart: + continue + + rank = calculate_song_rank(song.score, profile.version) + rating = calculate_song_rating(song.score, music_chart.level, profile.version) + + song_rating = int(rating * 10 ** 2) / 10 ** 2 + song_records.append({ + "difficultId": song.difficultId, + "musicId": song.musicId, + "title": music_chart.title, + "level": music_chart.level, + "score": song.score, + "type": song.type, + "rank": rank, + "song_rating": song_rating, + }) + hot_list = [obj for obj in song_records if obj["type"] == "userRatingBaseHotList"] base_list = [obj for obj in song_records if obj["type"] == "userRatingBaseList"] return Response(template.render( @@ -243,4 +296,4 @@ class ChuniFrontend(FE_Base): resp.set_cookie("ARTEMIS_SESH", encoded_sesh) return resp else: - return RedirectResponse("/gate/", 303) \ No newline at end of file + return RedirectResponse("/gate/", 303) diff --git a/titles/chuni/index.py b/titles/chuni/index.py index dd49cc6..144f770 100644 --- a/titles/chuni/index.py +++ b/titles/chuni/index.py @@ -94,14 +94,19 @@ class ChuniServlet(BaseServlet): known_iter_counts = { ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS: 67, + f"{ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS}_int": 25, # SUPERSTAR ChuniConstants.VER_CHUNITHM_PARADISE: 44, - f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 25, + f"{ChuniConstants.VER_CHUNITHM_PARADISE}_int": 51, # SUPERSTAR PLUS ChuniConstants.VER_CHUNITHM_NEW: 54, f"{ChuniConstants.VER_CHUNITHM_NEW}_int": 49, ChuniConstants.VER_CHUNITHM_NEW_PLUS: 25, + f"{ChuniConstants.VER_CHUNITHM_NEW_PLUS}_int": 31, ChuniConstants.VER_CHUNITHM_SUN: 70, + f"{ChuniConstants.VER_CHUNITHM_SUN}_int": 35, ChuniConstants.VER_CHUNITHM_SUN_PLUS: 36, + f"{ChuniConstants.VER_CHUNITHM_SUN_PLUS}_int": 36, ChuniConstants.VER_CHUNITHM_LUMINOUS: 8, + f"{ChuniConstants.VER_CHUNITHM_LUMINOUS}_int": 8, } for version, keys in self.game_cfg.crypto.keys.items(): @@ -233,8 +238,10 @@ class ChuniServlet(BaseServlet): elif version >= 220: # LUMINOUS internal_ver = ChuniConstants.VER_CHUNITHM_LUMINOUS elif game_code == "SDGS": # Int - if version < 110: # SUPERSTAR / SUPERSTAR PLUS - internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE # SUPERSTAR / SUPERSTAR PLUS worked fine with it + if version < 105: # SUPERSTAR + internal_ver = ChuniConstants.VER_CHUNITHM_CRYSTAL_PLUS + elif version >= 105 and version < 110: # SUPERSTAR PLUS *Cursed but needed due to different encryption key + internal_ver = ChuniConstants.VER_CHUNITHM_PARADISE elif version >= 110 and version < 115: # NEW internal_ver = ChuniConstants.VER_CHUNITHM_NEW elif version >= 115 and version < 120: # NEW PLUS!! @@ -353,9 +360,9 @@ class ChuniServlet(BaseServlet): padded = pad(zipped, 16) 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]), ) return Response(crypt.encrypt(padded)) diff --git a/titles/chuni/sun.py b/titles/chuni/sun.py index 94dc1eb..4957c4b 100644 --- a/titles/chuni/sun.py +++ b/titles/chuni/sun.py @@ -16,4 +16,14 @@ class ChuniSun(ChuniNewPlus): # hardcode lastDataVersion for CardMaker 1.35 A032 user_data["lastDataVersion"] = "2.10.00" - return user_data \ No newline at end of file + return user_data + + #SDGS Exclusive + async def handle_get_user_cto_c_play_api_request(self, data: Dict) -> Dict: + return { + "userId": data["userId"], + "orderBy": "0", + "count": "0", + #game request c2c play history while login but seem unused(?) + "userCtoCPlayList": [], + } \ No newline at end of file diff --git a/titles/chuni/templates/chuni_rating.jinja b/titles/chuni/templates/chuni_rating.jinja index c094e6f..37ed6d0 100644 --- a/titles/chuni/templates/chuni_rating.jinja +++ b/titles/chuni/templates/chuni_rating.jinja @@ -18,6 +18,7 @@ Music Difficulty Score + Rank Rating {% for row in hot_list %} @@ -28,6 +29,7 @@ {{ row.level }} {{ row.score }} + {{ row.rank }} {{ row.song_rating }} @@ -48,6 +50,7 @@ Music Difficulty Score + Rank Rating {% for row in base_list %} @@ -58,6 +61,7 @@ {{ row.level }} {{ row.score }} + {{ row.rank }} {{ row.song_rating }} @@ -76,4 +80,4 @@ Login to view profile information. {% endif %} -{% endblock content %} \ No newline at end of file +{% endblock content %}