idac: added support for basic 1.70

- Added special mode based on device version
- Added number plate lottery
- Updated online battle rounds handling
This commit is contained in:
Dniel97 2024-06-12 23:43:42 +02:00
parent d5b2074945
commit ba5591cd81
Signed by untrusted user: Dniel97
GPG Key ID: 6180B3C768FB2E08
12 changed files with 277 additions and 127 deletions

View File

@ -1,6 +1,12 @@
# Changelog
Documenting updates to ARTEMiS, to be updated every time the master branch is pushed to.
## 20240612
+ Support Initial D THE ARCADE v1.70
+ Added special mode based on device version
+ Added number plate lottery
+ Updated online battle rounds handling
## 20240526
+ Fixed missing awaits causing coroutine error

View File

@ -1,7 +1,7 @@
"""idac rounds event info added
Revision ID: 202d1ada1b39
Revises: e4e8d89c9b02
Revises: 7dc13e364e53
Create Date: 2024-05-03 15:51:02.384863
"""
@ -12,7 +12,7 @@ from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '202d1ada1b39'
down_revision = 'e4e8d89c9b02'
down_revision = '7dc13e364e53'
branch_labels = None
depends_on = None
@ -22,7 +22,7 @@ def upgrade():
"idac_round_info",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("round_id_in_json", sa.Integer(), nullable=True),
sa.Column("name", String(64), nullable=True),
sa.Column("name", sa.String(64), nullable=True),
sa.Column("season", sa.Integer(), nullable=True),
sa.Column(
"start_dt",

View File

@ -1,4 +1,4 @@
"""idac plate number lottery added
"""IDAC plate number lottery added
Revision ID: 7e98c2c328b1
Revises: 202d1ada1b39
@ -24,6 +24,12 @@ def upgrade():
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("saved_value", sa.Integer(), nullable=False),
sa.Column("lottery_count", sa.Integer(), nullable=False),
sa.Column(
"create_date",
sa.TIMESTAMP(),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["user"], ["aime_user.id"], onupdate="cascade", ondelete="cascade"
),

View File

@ -0,0 +1,26 @@
"""IDAC device_version added
Revision ID: aeb6b1e28354
Revises: 7e98c2c328b1
Create Date: 2024-05-29 17:40:45.123656
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = 'aeb6b1e28354'
down_revision = '7e98c2c328b1'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('idac_profile', sa.Column('device_version', sa.String(length=7), server_default='1.50.00', nullable=True))
op.drop_column('idac_profile', 'asset_version')
def downgrade():
op.add_column('idac_profile', sa.Column('asset_version', mysql.INTEGER(), server_default="1", nullable=True))
op.drop_column('idac_profile', 'device_version')

View File

@ -733,10 +733,11 @@ python dbutils.py upgrade
### TimeRelease Chapter:
1. Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11?)
1. Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11)
2. MF Ghost: 10, 11, 12, 13, 14, 15
3. Bunta: 15, 16, 17, 18, 20, 21, 21, 22
4. Special Event: 23, 24, 25, 26, 27, 28 (Touhou Project)
4. Touhou Project Special Event: 23, 24, 25, 26, 27, 28
5. Hatsune Miku Special Event: 36, 37, 38
### TimeRelease Courses:
@ -762,6 +763,8 @@ python dbutils.py upgrade
| 58 | Momiji Line(もみじライン) | Hillclimb(上り) |
| 24 | Happogahara(八方ヶ原) | Outbound(往路) |
| 26 | Happogahara(八方ヶ原) | Inbound(復路) |
| 28 | Nagao(長尾) | Downhill(下り) |
| 30 | Nagao(長尾) | Hillclimb(上り) |
| 40 | Sadamine(定峰) | Downhill(下り) |
| 42 | Sadamine(定峰) | Hillclimb(上り) |
| 44 | Tsuchisaka(土坂) | Outbound(往路) |

View File

@ -166,7 +166,7 @@ class IDACRoundConfig:
"idac",
"round_event",
"enabled_round",
default="S1R1",
default="S2R2",
)
@property
@ -176,7 +176,7 @@ class IDACRoundConfig:
"idac",
"round_event",
"last_round",
default="S1R1",
default="S2R1",
)

View File

@ -1,13 +1,12 @@
from typing import Dict, Optional, List
from datetime import datetime
from typing import Dict, Optional
from sqlalchemy import (
Table,
Column,
UniqueConstraint,
PrimaryKeyConstraint,
and_,
update,
)
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
from sqlalchemy.types import Integer, TIMESTAMP
from sqlalchemy.schema import ForeignKey
from sqlalchemy.engine import Row
from sqlalchemy.sql import func, select
@ -23,6 +22,7 @@ lottery = Table(
Column("version", Integer, nullable=False),
Column("saved_value", Integer, nullable=False),
Column("lottery_count", Integer, nullable=False),
Column("create_date", TIMESTAMP, server_default=func.now()),
UniqueConstraint("user", "version", name="idac_user_lottery_uk"),
mysql_charset="utf8mb4",
)
@ -41,14 +41,17 @@ class IDACFactoryData(BaseData):
return result.fetchone()
async def put_lottery(
self, aime_id: int, version: int, saved_value: int, lottery_count: int
self, aime_id: int, version: int, saved_value: int, lottery_count: int, create_date: datetime
) -> Optional[int]:
lottery_data = {}
lottery_data["user"] = aime_id
lottery_data["version"] = version
lottery_data["saved_value"] = saved_value
lottery_data["lottery_count"] = lottery_count
lottery_data = {
"user": aime_id,
"version": version,
"saved_value": saved_value,
"lottery_count": lottery_count,
"create_date": create_date
}
sql = insert(lottery).values(**lottery_data)
conflict = sql.on_duplicate_key_update(**lottery_data)

View File

@ -249,30 +249,6 @@ vs_course_info = Table(
mysql_charset="utf8mb4",
)
round_infos = Table(
"idac_round_info",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("name", String(64)),
Column("season", Integer),
Column("start_dt", TIMESTAMP, server_default=func.now()),
Column("end_dt", TIMESTAMP, server_default=func.now()),
mysql_charset="utf8mb4",
)
round_info = Table(
"idac_user_round_info",
metadata,
Column("id", Integer, primary_key=True, nullable=False),
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
Column("round_id", Integer),
Column("count", Integer),
Column("win", Integer),
Column("points", Integer),
UniqueConstraint("user", "round_id", name="idac_user_round_info_uk"),
mysql_charset="utf8mb4",
)
stamp = Table(
"idac_user_stamp",
metadata,
@ -354,7 +330,7 @@ class IDACItemData(BaseData):
return result.fetchone()
async def get_random_car(self, version: int) -> Optional[List[Row]]:
sql = select(car).where(car.c.version == version).order_by(func.rand()).limit(1)
sql = select(car).where(car.c.version <= version).order_by(func.rand()).limit(1)
result = await self.execute(sql)
if result is None:
@ -367,7 +343,7 @@ class IDACItemData(BaseData):
sql = select(car).where(
and_(
car.c.user == aime_id,
car.c.version == version,
car.c.version <= version,
car.c.style_car_id == style_car_id,
)
)
@ -384,7 +360,7 @@ class IDACItemData(BaseData):
sql = select(car).where(
and_(
car.c.user == aime_id,
car.c.version == version,
car.c.version <= version,
car.c.pickup_seq != 0,
)
)

View File

@ -28,7 +28,7 @@ profile = Table(
Column("daily_play", Integer, server_default="0"),
Column("day_play", Integer, server_default="0"),
Column("mileage", Integer, server_default="0"),
Column("asset_version", Integer, server_default="1"),
Column("device_version", String(7), server_default="1.50.00"),
Column("last_play_date", TIMESTAMP, server_default=func.now()),
Column("mytitle_id", Integer, server_default="0"),
Column("mytitle_efffect_id", Integer, server_default="0"),
@ -279,7 +279,7 @@ class IDACProfileData(BaseData):
sql = select(profile).where(
and_(
profile.c.user == aime_id,
profile.c.version == version,
profile.c.version <= version,
)
)
@ -336,7 +336,7 @@ class IDACProfileData(BaseData):
sql = select(rank).where(
and_(
rank.c.user == aime_id,
rank.c.version == version,
rank.c.version <= version,
)
)
@ -349,7 +349,7 @@ class IDACProfileData(BaseData):
sql = select(stock).where(
and_(
stock.c.user == aime_id,
stock.c.version == version,
stock.c.version <= version,
)
)
@ -362,7 +362,7 @@ class IDACProfileData(BaseData):
sql = select(theory).where(
and_(
theory.c.user == aime_id,
theory.c.version == version,
theory.c.version <= version,
)
)
@ -375,7 +375,7 @@ class IDACProfileData(BaseData):
sql = select(tips).where(
and_(
tips.c.user == aime_id,
tips.c.version == version,
tips.c.version <= version,
)
)

View File

@ -119,11 +119,13 @@ class IDACOnlineRounds(BaseData):
async def _try_load_round_event(
self, round_id: int, version: int, round_data: Dict
) -> Optional[int]:
sql = select(round_details).where(
round_details.c.round_id_in_json == round_id
)
result = await self.execute(sql)
rid = result.fetchone()
if rid is None:
tmp = {}
tmp["round_id_in_json"] = round_id
@ -131,8 +133,11 @@ class IDACOnlineRounds(BaseData):
tmp["season"] = version
tmp["start_dt"] = datetime.datetime.fromtimestamp(round_data["start_dt"])
tmp["end_dt"] = datetime.datetime.fromtimestamp(round_data["end_dt"])
sql = insert(round_details).values(**tmp)
result = await self.execute(sql)
return result.lastrowid
return rid["id"]
#TODO: get top five players of last round event for Boot/GetConfigData
# TODO: get top five players of last round event for Boot/GetConfigData

View File

@ -93,12 +93,11 @@ class IDACSeason2(IDACBase):
# Load current round event
round = self.game_config.round_event.enabled_round
if round is not None:
if not os.path.exists(f"./titles/idac/data/rounds/season{self.version+1}/{round}.json"):
path = f"./titles/idac/data/rounds/season{self.version+1}/{round}.json"
if not os.path.exists(path):
self.logger.warning(f"Round info {round} is enabled but json file does not exist!")
else:
with open(
f"./titles/idac/data/rounds/season{self.version+1}/{round}.json", encoding="UTF-8"
) as f:
with open(path, encoding="UTF-8") as f:
self.logger.debug(f"Loading round info {round}...")
tmp = json.load(f)
self.round_event_id = await self.data.rounds._try_load_round_event(tmp["round_event_id"], self.version, tmp)
@ -108,12 +107,11 @@ class IDACSeason2(IDACBase):
# Load last round event
round = self.game_config.round_event.last_round
if round is not None:
if not os.path.exists(f"./titles/idac/data/rounds/season{self.version+1}/{round}.json"):
path = f"./titles/idac/data/rounds/season{self.version+1}/{round}.json"
if not os.path.exists(path):
self.logger.warning(f"Round info {round} is enabled but json file does not exist!")
else:
with open(
f"./titles/idac/data/rounds/season{self.version+1}/{round}.json", encoding="UTF-8"
) as f:
with open(path, encoding="UTF-8") as f:
self.logger.debug(f"Loading round info {round}...")
tmp = json.load(f)
self.last_round_event_id = await self.data.rounds._try_load_round_event(tmp["round_event_id"], self.version, tmp)
@ -217,6 +215,16 @@ class IDACSeason2(IDACBase):
output[key] = value
return output
def _special_story_type(self, headers: Dict):
version = headers["device_version"]
ver_str = version.replace(".", "")[:3]
# 4 = touhou project, 5 = hatsune miku
return {
"150": 4,
"170": 5
}.get(ver_str, 0)
async def handle_boot_getconfigdata_request(self, data: Dict, headers: Dict):
"""
category:
@ -255,6 +263,18 @@ class IDACSeason2(IDACBase):
else:
domain_api_game = f"http://{self.core_cfg.server.hostname}:{Utils.get_title_port(self.core_cfg)}/{ver_str}/"
# sets the special mode data if the version supports the special mode
special_mode_data = {}
special_story_type = self._special_story_type(headers)
if special_story_type != 0:
special_mode_data = {
"start_dt": int(
datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp()
),
"end_dt": int(datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp()),
"story_type": special_story_type
}
return {
"status_code": "0",
"free_continue_enable": 1,
@ -456,14 +476,90 @@ class IDACSeason2(IDACBase):
"theory_open_version": "1.30.00",
"theory_close_version": "9.99.99",
# unlocks the version specific special mode
"special_mode_data": {
"start_dt": int(
datetime.strptime("2023-01-01", "%Y-%m-%d").timestamp()
),
"end_dt": int(datetime.strptime("2029-01-01", "%Y-%m-%d").timestamp()),
"story_type": 4, # touhou special event
},
"special_mode_data": special_mode_data,
"timetrial_event_data": self.timetrial_event,
"number_lottery_data": [
{
"m_number_lottery_win_number_no": 10,
"m_number_lottery_schedule_no": 1,
"win_number": 0,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 11,
"m_number_lottery_schedule_no": 1,
"win_number": 1111,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 12,
"m_number_lottery_schedule_no": 1,
"win_number": 2222,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 13,
"m_number_lottery_schedule_no": 1,
"win_number": 3333,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 14,
"m_number_lottery_schedule_no": 1,
"win_number": 4444,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 15,
"m_number_lottery_schedule_no": 1,
"win_number": 5555,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 16,
"m_number_lottery_schedule_no": 1,
"win_number": 6666,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 17,
"m_number_lottery_schedule_no": 1,
"win_number": 7777,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 18,
"m_number_lottery_schedule_no": 1,
"win_number": 8888,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
{
"m_number_lottery_win_number_no": 19,
"m_number_lottery_schedule_no": 1,
"win_number": 9999,
"reward_category": 34,
"reward_type": 1,
"end_dt": 0,
},
],
}
async def handle_boot_bookkeep_request(self, data: Dict, headers: Dict):
@ -524,7 +620,7 @@ class IDACSeason2(IDACBase):
timerelease chapter:
1 = Story: 1, 2, 3, 4, 5, 6, 7, 8, 9, 19 (Chapter 10), (29 Chapter 11 lol?)
2 = MF Ghost: 10, 11, 12, 13, 14, 15
3 = Bunta: 15, 16, 17, 18, 20, (21, 21, 22?)
3 = Bunta: 15, 16, 17, 18, 20, 21, 21, 22
4 = Special Event: 23, 24, 25, 26, 27, 28 (Touhou Project)
"""
path = "./titles/idac/data/"
@ -613,6 +709,16 @@ class IDACSeason2(IDACBase):
"rank_management_flag": 0,
}
def _is_valid_version(self, db_version: str, cl_version: str) -> bool:
if len(db_version) < 7 or len(cl_version) < 7:
return False
# convert the trings to int and compare
db_version = int(db_version.replace(".", "")[:7])
cl_version = int(cl_version.replace(".", "")[:7])
return cl_version >= db_version
async def handle_login_checklock_request(self, data: Dict, headers: Dict):
user_id = data["id"]
access_code = data["accesscode"]
@ -625,6 +731,13 @@ class IDACSeason2(IDACBase):
# check if an IDAC profile already exists
p = await self.data.profile.get_profile(user_id, self.version)
is_new_player = 1 if p is None else 0
db_version = p["device_version"] if p is not None else "1.00.00"
cl_version = headers["device_version"]
# check if the client version is valid
if not self._is_valid_version(db_version, cl_version):
lock_result = 2
else:
lock_result = 0
user_id = ""
@ -706,10 +819,10 @@ class IDACSeason2(IDACBase):
return story_data
async def _generate_special_data(self, user_id: int) -> Dict:
# 4 = special mode
async def _generate_special_data(self, user_id: int, headers: Dict) -> Dict:
# 4 = touhou project, 5 = hatsune miku
specials = await self.data.item.get_best_challenges_by_vs_type(
user_id, story_type=4
user_id, story_type=self._special_story_type(headers)
)
special_data = []
@ -1321,7 +1434,7 @@ class IDACSeason2(IDACBase):
"theory_course_data": theory_course_data,
"theory_partner_data": theory_partner_data,
"theory_running_pram_data": theory_running_pram_data,
"special_mode_data": await self._generate_special_data(user_id),
"special_mode_data": await self._generate_special_data(user_id, headers),
"challenge_mode_data": await self._generate_challenge_data(user_id),
"season_rewards_data": [],
"timetrial_event_data": timetrial_event_data,
@ -1519,7 +1632,7 @@ class IDACSeason2(IDACBase):
# save profile in database
data["store"] = headers.get("a_store", 0)
data["country"] = headers.get("a_country", 0)
data["asset_version"] = headers.get("asset_version", 1)
data["device_version"] = headers.get("device_version", "1.50.00")
await self.data.profile.put_profile(user_id, self.version, data)
# save rank data in database
@ -1925,7 +2038,9 @@ class IDACSeason2(IDACBase):
# update the tips play count
tips = await self.data.profile.get_profile_tips(user_id, self.version)
await self.data.profile.put_profile_tips(
user_id, self.version, {"timetrial_play_count": tips["timetrial_play_count"] + 1}
user_id,
self.version,
{"timetrial_play_count": tips["timetrial_play_count"] + 1},
)
return {
@ -2147,12 +2262,14 @@ class IDACSeason2(IDACBase):
# update the tips play count
tips = await self.data.profile.get_profile_tips(user_id, self.version)
await self.data.profile.put_profile_tips(
user_id, self.version, {"special_play_count": tips["special_play_count"] + 1}
user_id,
self.version,
{"special_play_count": tips["special_play_count"] + 1},
)
return {
"status_code": "0",
"special_mode_data": await self._generate_special_data(user_id),
"special_mode_data": await self._generate_special_data(user_id, headers),
"car_use_count": [],
"maker_use_count": [],
}
@ -2233,7 +2350,9 @@ class IDACSeason2(IDACBase):
# update the tips play count
tips = await self.data.profile.get_profile_tips(user_id, self.version)
await self.data.profile.put_profile_tips(
user_id, self.version, {"challenge_play_count": tips["challenge_play_count"] + 1}
user_id,
self.version,
{"challenge_play_count": tips["challenge_play_count"] + 1},
)
return {
@ -3011,7 +3130,9 @@ class IDACSeason2(IDACBase):
# update the tips play count
tips = await self.data.profile.get_profile_tips(user_id, self.version)
await self.data.profile.put_profile_tips(
user_id, self.version, {"online_battle_play_count": tips["online_battle_play_count"] + 1}
user_id,
self.version,
{"online_battle_play_count": tips["online_battle_play_count"] + 1},
)
round_info = await self._update_round_info(user_id, self.round_event_id, data.pop("round_point"), data.pop("win_flg"))
@ -3103,7 +3224,9 @@ class IDACSeason2(IDACBase):
# update the tips play count
tips = await self.data.profile.get_profile_tips(user_id, self.version)
await self.data.profile.put_profile_tips(
user_id, self.version, {"store_battle_play_count": tips["store_battle_play_count"] + 1}
user_id,
self.version,
{"store_battle_play_count": tips["store_battle_play_count"] + 1},
)
return {
@ -3118,23 +3241,25 @@ class IDACSeason2(IDACBase):
win_list = []
lottery_count = 0
p = await self.data.factory.get_lottery(user_id, self.version)
if p is not None:
lottery_data = p._asdict()
lottery_count = lottery_data["lottery_count"]
number = 0
while number < 10:
if int(lottery_data["saved_value"]) & 1:
win_list_data = {
# retrieve the lottery data for the user
l = await self.data.factory.get_lottery(user_id, self.version)
if l:
# check if create_data is younger than 24 hours
if datetime.now() - l["create_date"] < timedelta(days=1):
lottery_count = l["lottery_count"]
saved_value = int(l["saved_value"])
# check each of the first 10 bits in saved_value
for number in range(10):
if saved_value & 1:
# if the least significant bit is 1, add to the win_list
win_list.append({
"m_number_lottery_schedule_no": 1,
"win_number": 0
}
"win_number": number * 1111
})
win_list_data["win_number"] = number*1111
win_list.append(win_list_data)
lottery_data["saved_value"] = lottery_data["saved_value"] / 2
number = number + 1
# right shift saved_value to check the next bit
saved_value >>= 1
return {
"status_code": "0",
@ -3148,43 +3273,43 @@ class IDACSeason2(IDACBase):
user_id = headers["session"]
win_number = data.pop("win_number")
lottery_count = data.pop("lottery_count")
style_car_id = data.pop("style_car_id")
license_no = data.pop("l_no")
is_end = data.pop("isEnd")
ticket_data = data.pop("ticket_data")
# retrieve the lottery data for the user
l = await self.data.factory.get_lottery(user_id, self.version)
create_date = l["create_date"] if l else datetime.now()
saved_value = l["saved_value"] if l else 0
# save count if not a lucky number otherwise save all
if win_number != 10000:
shifted = win_number / 1111
number = 1 << int(shifted)
p = await self.data.factory.get_lottery(user_id, self.version)
if p is not None:
lottery_data = p._asdict()
saved_value = lottery_data["saved_value"] + number
else:
saved_value = number
# calculate the bit position to set based on the win_number
shifted = win_number // 1111
saved_value += 1 << shifted
await self.data.factory.put_lottery(user_id, self.version, saved_value, lottery_count)
else:
p = await self.data.factory.get_lottery(user_id, self.version)
if p is not None:
lottery_data = p._asdict()
saved_value = lottery_data["saved_value"]
else:
saved_value = 0
await self.data.factory.put_lottery(user_id, self.version, saved_value, lottery_count)
# update the create_date timestamp when the last create_date is older than 24 hours
if l and datetime.now() - l["create_date"] > timedelta(days=1):
create_date = datetime.now()
# save car data if lottery ended
car = {}
car["style_car_id"] = data.pop("style_car_id")
car["l_no"] = data.pop("l_no")
if data.pop("isEnd") == 1:
await self.data.item.put_car(user_id, self.version, car)
# update the lottery data with the new saved_value and lottery_count
await self.data.factory.put_lottery(user_id, self.version, saved_value, lottery_count, create_date)
# save ticket data
for ticket in data.pop("ticket_data"):
if license_no != 10000 and is_end == 1:
# ithe lottery is ended, save car data
await self.data.item.put_car(user_id, self.version, {
"style_car_id": style_car_id,
"l_no": license_no
})
# save each ticket data
for ticket in ticket_data:
await self.data.item.put_ticket(user_id, ticket)
# save cash
# save remaining profile data
await self.data.profile.put_profile(user_id, self.version, data)
return {
"status_code": "0"
}

File diff suppressed because one or more lines are too long