develop #11

Merged
Kayori merged 7 commits from Hay1tsme/artemis:develop into develop 2024-07-19 12:22:25 +00:00
8 changed files with 189 additions and 43 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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]

View File

@ -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)
return RedirectResponse("/gate/", 303)

View File

@ -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))

View File

@ -16,4 +16,14 @@ class ChuniSun(ChuniNewPlus):
# hardcode lastDataVersion for CardMaker 1.35 A032
user_data["lastDataVersion"] = "2.10.00"
return user_data
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": [],
}

View File

@ -18,6 +18,7 @@
<th>Music</th>
<th>Difficulty</th>
<th>Score</th>
<th>Rank</th>
<th>Rating</th>
</tr>
{% for row in hot_list %}
@ -28,6 +29,7 @@
{{ row.level }}
</td>
<td>{{ row.score }}</td>
<td>{{ row.rank }}</td>
<td class="{% if row.song_rating >= 16 %}rainbow{% endif %}">
{{ row.song_rating }}
</td>
@ -48,6 +50,7 @@
<th>Music</th>
<th>Difficulty</th>
<th>Score</th>
<th>Rank</th>
<th>Rating</th>
</tr>
{% for row in base_list %}
@ -58,6 +61,7 @@
{{ row.level }}
</td>
<td>{{ row.score }}</td>
<td>{{ row.rank }}</td>
<td class="{% if row.song_rating >= 16 %}rainbow{% endif %}">
{{ row.song_rating }}
</td>
@ -76,4 +80,4 @@
Login to view profile information.
{% endif %}
</div>
{% endblock content %}
{% endblock content %}