forked from Hay1tsme/artemis
- Add Ranking Event Support
- Add Technical Challenge Event Support - Fix Event Enumeration for EVT_TYPES as Enum starts with 1, to be in spec with what game expects, also add missing Max EVT_TYPE - Add documentation on how to properly configure and run Events for ONGEKI
This commit is contained in:
parent
4bedf71d3d
commit
4da886a083
@ -336,6 +336,42 @@ perform all previous updates as well:
|
|||||||
python dbutils.py --game SDDT upgrade
|
python dbutils.py --game SDDT upgrade
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Controlling Events (Ranking Event, Technical Challenge Event, Mission Event)
|
||||||
|
|
||||||
|
Events are controlled by 2 types of enabled events:
|
||||||
|
- RankingEvent (type 6), TechChallengeEvent (type 17)
|
||||||
|
- AcceptRankingEvent (type 7), AcceptTechChallengeEvent (type 18)
|
||||||
|
|
||||||
|
Both Ranking and Accept must be enabled for event to function properly
|
||||||
|
Event will run for the time specified in startDate and endDate
|
||||||
|
AcceptRankingEvent and AcceptTechChallengeEvent are reward period for events, which specify from what startDate until endDate you can collect the rewards for attending the event, so the reward period must start in the future, e.g. :
|
||||||
|
|
||||||
|
- RankingEvent startDate 2023-12-01 - endDate 2023-12-30 - period in which whole event is running
|
||||||
|
- AcceptRankingEvent startDate 2023-12-23 - endDate 2023-12-30 - period in which you can collect rewards for the event
|
||||||
|
|
||||||
|
If player misses the AcceptRankingEvent period - ranking will be invalidated and receive lowest reward from the event (typically 500x money)
|
||||||
|
|
||||||
|
Technical Challenge Song List:
|
||||||
|
Songs that are used for Technical Challenge are not stored anywhere in data files, so you need to fill the database table by yourself, you can gather all songs that should be in Technical Challenges from ONGEKI japanese wikis, or, you can create your own sets:
|
||||||
|
|
||||||
|
Database table : `ongeki_tech_music_list`
|
||||||
|
```
|
||||||
|
id: Id in table, just increment for each entry
|
||||||
|
eventId: Id of the event in ongeki_static_events, insert the Id of the TechChallengeEvent (type 17) you want the song be assigned to
|
||||||
|
musicId: Id of the song you want to add, use songId from ongeki_static_music table
|
||||||
|
level: Difficulty of the song you want to track during the event, from 0(basic) to 3(master)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Current implementation of Ranking and Technical Challenge Events are updated on every profile save to the Network, and Ranked on each player login, in official specification, calculation for current rank on the network should be done in the maintenance window
|
||||||
|
|
||||||
|
Mission Event (type 13) is a monthly type of event, which is used when another event doesn't have it's own Ranking or Technical Challenge Event running, only one Mission Event should be running at a time, so enable only the specific Mission you want to run currently on the Network
|
||||||
|
|
||||||
|
If you're often trying fresh cards, registering new profiles etc., you can also consider disabling all Announcement Events (type 1), as it will disable all the banners that pop up on login (they show up only once though, so if you click through them once they won't show again)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Card Maker
|
## Card Maker
|
||||||
|
|
||||||
### SDED
|
### SDED
|
||||||
@ -587,4 +623,4 @@ python dbutils.py --game SDEW upgrade
|
|||||||
|
|
||||||
- Midorica - Limited Network Support
|
- Midorica - Limited Network Support
|
||||||
- Dniel97 - Helping with network base
|
- Dniel97 - Helping with network base
|
||||||
- tungnotpunk - Source
|
- tungnotpunk - Source
|
||||||
|
@ -228,7 +228,21 @@ class OngekiBase:
|
|||||||
return {"length": 0, "gameSaleList": []}
|
return {"length": 0, "gameSaleList": []}
|
||||||
|
|
||||||
def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict:
|
def handle_get_game_tech_music_api_request(self, data: Dict) -> Dict:
|
||||||
return {"length": 0, "gameTechMusicList": []}
|
music_list = self.data.item.get_tech_music()
|
||||||
|
|
||||||
|
prep_music_list = []
|
||||||
|
for music in music_list:
|
||||||
|
tmp = music._asdict()
|
||||||
|
tmp.pop("id")
|
||||||
|
prep_music_list.append(tmp)
|
||||||
|
|
||||||
|
if prep_music_list is None:
|
||||||
|
return {"length": 0, "gameTechMusicList": []}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"length": len(prep_music_list),
|
||||||
|
"gameTechMusicList": prep_music_list,
|
||||||
|
}
|
||||||
|
|
||||||
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict:
|
||||||
return {"returnCode": 1, "apiName": "UpsertClientSettingApi"}
|
return {"returnCode": 1, "apiName": "UpsertClientSettingApi"}
|
||||||
@ -283,7 +297,7 @@ class OngekiBase:
|
|||||||
"endDate": "2099-12-31 00:00:00.0",
|
"endDate": "2099-12-31 00:00:00.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"type": data["type"],
|
"type": data["type"],
|
||||||
"length": len(evt_list),
|
"length": len(evt_list),
|
||||||
@ -403,15 +417,24 @@ class OngekiBase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_tech_event_ranking_api_request(self, data: Dict) -> Dict:
|
||||||
# user_event_ranking_list = self.data.item.get_tech_event_ranking(data["userId"])
|
user_tech_event_ranks = self.data.item.get_tech_event_ranking(data["userId"])
|
||||||
# if user_event_ranking_list is None: return {}
|
if user_tech_event_ranks is None:
|
||||||
|
return {
|
||||||
|
"userId": data["userId"],
|
||||||
|
"length": 0,
|
||||||
|
"userTechEventRankingList": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# collect the whole table and clear other players, to preserve proper ranking
|
||||||
evt_ranking = []
|
evt_ranking = []
|
||||||
# for evt in user_event_ranking_list:
|
for evt in user_tech_event_ranks:
|
||||||
# tmp = evt._asdict()
|
tmp = evt._asdict()
|
||||||
# tmp.pop("id")
|
if tmp["user"] != data["userId"]:
|
||||||
# tmp.pop("user")
|
tmp.clear()
|
||||||
# evt_ranking.append(tmp)
|
else:
|
||||||
|
tmp.pop("id")
|
||||||
|
tmp.pop("user")
|
||||||
|
evt_ranking.append(tmp)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
@ -533,20 +556,26 @@ class OngekiBase:
|
|||||||
return {"userId": data["userId"], "userData": user_data}
|
return {"userId": data["userId"], "userData": user_data}
|
||||||
|
|
||||||
def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_event_ranking_api_request(self, data: Dict) -> Dict:
|
||||||
# user_event_ranking_list = self.data.item.get_event_ranking(data["userId"])
|
user_event_ranking_list = self.data.item.get_ranking_event_ranks(data["userId"])
|
||||||
# if user_event_ranking_list is None: return {}
|
if user_event_ranking_list is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
evt_ranking = []
|
# We collect the whole ranking table, and clear out any not needed data, this way we preserve the proper ranking
|
||||||
# for evt in user_event_ranking_list:
|
# In official spec this should be done server side, in maintenance period
|
||||||
# tmp = evt._asdict()
|
prep_event_ranking = []
|
||||||
# tmp.pop("id")
|
for evt in user_event_ranking_list:
|
||||||
# tmp.pop("user")
|
tmp = evt._asdict()
|
||||||
# evt_ranking.append(tmp)
|
if tmp["user"] != data["userId"]:
|
||||||
|
tmp.clear()
|
||||||
|
else:
|
||||||
|
tmp.pop("id")
|
||||||
|
tmp.pop("user")
|
||||||
|
prep_event_ranking.append(tmp)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(evt_ranking),
|
"length": len(prep_event_ranking),
|
||||||
"userEventRankingList": evt_ranking,
|
"userEventRankingList": prep_event_ranking,
|
||||||
}
|
}
|
||||||
|
|
||||||
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict:
|
||||||
@ -788,6 +817,7 @@ class OngekiBase:
|
|||||||
tmp.pop("user")
|
tmp.pop("user")
|
||||||
mission_point_list.append(tmp)
|
mission_point_list.append(tmp)
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"userId": data["userId"],
|
"userId": data["userId"],
|
||||||
"length": len(mission_point_list),
|
"length": len(mission_point_list),
|
||||||
@ -804,6 +834,10 @@ class OngekiBase:
|
|||||||
tmp = evt_music._asdict()
|
tmp = evt_music._asdict()
|
||||||
tmp.pop("id")
|
tmp.pop("id")
|
||||||
tmp.pop("user")
|
tmp.pop("user")
|
||||||
|
# pop other stuff event_point doesn't want
|
||||||
|
tmp.pop("rank")
|
||||||
|
tmp.pop("type")
|
||||||
|
tmp.pop("date")
|
||||||
event_point_list.append(tmp)
|
event_point_list.append(tmp)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -987,6 +1021,9 @@ class OngekiBase:
|
|||||||
for x in upsert["userTechEventList"]:
|
for x in upsert["userTechEventList"]:
|
||||||
self.data.item.put_tech_event(user_id, x)
|
self.data.item.put_tech_event(user_id, x)
|
||||||
|
|
||||||
|
# This should be updated once a day in maintenance window, but for time being we will push the update on each upsert
|
||||||
|
self.data.item.put_tech_event_ranking(user_id, x)
|
||||||
|
|
||||||
if "userKopList" in upsert:
|
if "userKopList" in upsert:
|
||||||
for x in upsert["userKopList"]:
|
for x in upsert["userKopList"]:
|
||||||
self.data.profile.put_kop(user_id, x)
|
self.data.profile.put_kop(user_id, x)
|
||||||
|
@ -19,7 +19,6 @@ class OngekiConstants:
|
|||||||
EVT_TYPES: Enum = Enum(
|
EVT_TYPES: Enum = Enum(
|
||||||
"EVT_TYPES",
|
"EVT_TYPES",
|
||||||
[
|
[
|
||||||
"None",
|
|
||||||
"Announcement",
|
"Announcement",
|
||||||
"Movie",
|
"Movie",
|
||||||
"AddMyList",
|
"AddMyList",
|
||||||
@ -39,6 +38,8 @@ class OngekiConstants:
|
|||||||
"TechChallengeEvent",
|
"TechChallengeEvent",
|
||||||
"AcceptTechChallengeEvent",
|
"AcceptTechChallengeEvent",
|
||||||
"SilverJewelEvent",
|
"SilverJewelEvent",
|
||||||
|
"Max",
|
||||||
|
"None",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Dict, Optional, List
|
from typing import Dict, Optional, List
|
||||||
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
|
from sqlalchemy import Table, Column, UniqueConstraint, PrimaryKeyConstraint, and_
|
||||||
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
|
from sqlalchemy.types import Integer, String, TIMESTAMP, Boolean, JSON
|
||||||
@ -172,6 +173,9 @@ event_point = Table(
|
|||||||
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
|
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade")),
|
||||||
Column("eventId", Integer),
|
Column("eventId", Integer),
|
||||||
Column("point", Integer),
|
Column("point", Integer),
|
||||||
|
Column("rank", Integer),
|
||||||
|
Column("type", Integer),
|
||||||
|
Column("date", String(25)),
|
||||||
Column("isRankingRewarded", Boolean),
|
Column("isRankingRewarded", Boolean),
|
||||||
UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"),
|
UniqueConstraint("user", "eventId", name="ongeki_user_event_point_uk"),
|
||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
@ -247,6 +251,31 @@ tech_event = Table(
|
|||||||
mysql_charset="utf8mb4",
|
mysql_charset="utf8mb4",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tech_music = Table(
|
||||||
|
"ongeki_tech_music_list",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("eventId", Integer),
|
||||||
|
Column("musicId", Integer),
|
||||||
|
Column("level", Integer),
|
||||||
|
UniqueConstraint("musicId", name="ongeki_tech_music_list_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
|
tech_ranking = Table(
|
||||||
|
"ongeki_tech_event_ranking",
|
||||||
|
metadata,
|
||||||
|
Column("id", Integer, primary_key=True, nullable=False),
|
||||||
|
Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False),
|
||||||
|
Column("date", String(25)),
|
||||||
|
Column("eventId", Integer),
|
||||||
|
Column("rank", Integer),
|
||||||
|
Column("totalPlatinumScore", Integer),
|
||||||
|
Column("totalTechScore", Integer),
|
||||||
|
UniqueConstraint("user", "eventId", name="ongeki_tech_event_ranking_uk"),
|
||||||
|
mysql_charset="utf8mb4",
|
||||||
|
)
|
||||||
|
|
||||||
gacha = Table(
|
gacha = Table(
|
||||||
"ongeki_user_gacha",
|
"ongeki_user_gacha",
|
||||||
metadata,
|
metadata,
|
||||||
@ -534,7 +563,12 @@ class OngekiItemData(BaseData):
|
|||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
def put_event_point(self, aime_id: int, event_point_data: Dict) -> Optional[int]:
|
def put_event_point(self, aime_id: int, event_point_data: Dict) -> Optional[int]:
|
||||||
|
# We update only the newest (type: 1) entry, in official spec game watches for both latest(type:1) and previous (type:2) entries to give an additional info how many ranks has player moved up or down
|
||||||
|
# This fully featured is on TODO list, at the moment we just update the tables as data comes and give out rank as request comes
|
||||||
event_point_data["user"] = aime_id
|
event_point_data["user"] = aime_id
|
||||||
|
event_point_data["type"] = 1
|
||||||
|
event_point_time = datetime.now()
|
||||||
|
event_point_data["date"] = datetime.strftime(event_point_time, "%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
sql = insert(event_point).values(**event_point_data)
|
sql = insert(event_point).values(**event_point_data)
|
||||||
conflict = sql.on_duplicate_key_update(**event_point_data)
|
conflict = sql.on_duplicate_key_update(**event_point_data)
|
||||||
@ -613,6 +647,14 @@ class OngekiItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.fetchall()
|
return result.fetchall()
|
||||||
|
|
||||||
|
def get_tech_music(self) -> Optional[List[Dict]]:
|
||||||
|
sql = select(tech_music)
|
||||||
|
result = self.execute(sql)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
def put_tech_event(self, aime_id: int, tech_event_data: Dict) -> Optional[int]:
|
def put_tech_event(self, aime_id: int, tech_event_data: Dict) -> Optional[int]:
|
||||||
tech_event_data["user"] = aime_id
|
tech_event_data["user"] = aime_id
|
||||||
|
|
||||||
@ -625,6 +667,22 @@ class OngekiItemData(BaseData):
|
|||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
|
def put_tech_event_ranking(self, aime_id: int, tech_event_data: Dict) -> Optional[int]:
|
||||||
|
tech_event_data["user"] = aime_id
|
||||||
|
tech_event_data.pop("isRankingRewarded")
|
||||||
|
tech_event_data.pop("isTotalTechNewRecord")
|
||||||
|
tech_event_data["date"] = tech_event_data.pop("techRecordDate")
|
||||||
|
tech_event_data["rank"] = 0
|
||||||
|
|
||||||
|
sql = insert(tech_ranking).values(**tech_event_data)
|
||||||
|
conflict = sql.on_duplicate_key_update(**tech_event_data)
|
||||||
|
result = self.execute(conflict)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
self.logger.warning(f"put_tech_event_ranking: Failed to update ranking! aime_id {aime_id}")
|
||||||
|
return None
|
||||||
|
return result.lastrowid
|
||||||
|
|
||||||
def get_tech_event(self, aime_id: int) -> Optional[List[Dict]]:
|
def get_tech_event(self, aime_id: int) -> Optional[List[Dict]]:
|
||||||
sql = select(tech_event).where(tech_event.c.user == aime_id)
|
sql = select(tech_event).where(tech_event.c.user == aime_id)
|
||||||
result = self.execute(sql)
|
result = self.execute(sql)
|
||||||
@ -714,3 +772,21 @@ class OngekiItemData(BaseData):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
return result.lastrowid
|
return result.lastrowid
|
||||||
|
|
||||||
|
|
||||||
|
def get_ranking_event_ranks(self, aime_id: int) -> Optional[List[Dict]]:
|
||||||
|
# Calculates player rank on GameRequest from server, and sends it back, official spec would rank players in maintenance period, on TODO list
|
||||||
|
sql = select(event_point.c.id, event_point.c.user, event_point.c.eventId, event_point.c.type, func.row_number().over(partition_by=event_point.c.eventId, order_by=event_point.c.point.desc()).label('rank'), event_point.c.date, event_point.c.point)
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.error(f"failed to rank aime_id: {aime_id} ranking event positions")
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
|
||||||
|
def get_tech_event_ranking(self, aime_id: int) -> Optional[List[Dict]]:
|
||||||
|
sql = select(tech_ranking.c.id, tech_ranking.c.user, tech_ranking.c.date, tech_ranking.c.eventId, func.row_number().over(partition_by=tech_ranking.c.eventId, order_by=[tech_ranking.c.totalTechScore.desc(),tech_ranking.c.totalPlatinumScore.desc()]).label('rank'), tech_ranking.c.totalTechScore, tech_ranking.c.totalPlatinumScore)
|
||||||
|
result = self.execute(sql)
|
||||||
|
if result is None:
|
||||||
|
self.logger.warning(f"aime_id: {aime_id} has no tech ranking ranks")
|
||||||
|
return None
|
||||||
|
return result.fetchall()
|
||||||
|
Loading…
Reference in New Issue
Block a user