From f63dd079370aca81378fd221cd545ae7fc67846b Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 10 Apr 2023 18:58:19 +0200 Subject: [PATCH 1/4] maimai: Initial Festival support --- core/data/schema/versions/SDEZ_3_rollback.sql | 31 ++ core/data/schema/versions/SDEZ_4_upgrade.sql | 31 ++ docs/game_specific_info.md | 3 +- readme.md | 4 +- titles/mai2/__init__.py | 2 +- titles/mai2/base.py | 278 ++++++++++-------- titles/mai2/const.py | 14 +- titles/mai2/festival.py | 31 ++ titles/mai2/index.py | 45 +-- titles/mai2/schema/item.py | 72 +++-- titles/mai2/schema/profile.py | 22 +- titles/mai2/schema/score.py | 2 + titles/mai2/schema/static.py | 5 +- titles/mai2/universeplus.py | 5 +- 14 files changed, 347 insertions(+), 198 deletions(-) create mode 100644 core/data/schema/versions/SDEZ_3_rollback.sql create mode 100644 core/data/schema/versions/SDEZ_4_upgrade.sql create mode 100644 titles/mai2/festival.py diff --git a/core/data/schema/versions/SDEZ_3_rollback.sql b/core/data/schema/versions/SDEZ_3_rollback.sql new file mode 100644 index 0000000..f819855 --- /dev/null +++ b/core/data/schema/versions/SDEZ_3_rollback.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +DROP COLUMN tapSe; + +ALTER TABLE mai2_score_best +DROP COLUMN extNum1; + +ALTER TABLE mai2_profile_extend +DROP COLUMN playStatusSetting; + +ALTER TABLE mai2_playlog +DROP COLUMN extNum4; + +ALTER TABLE mai2_static_event +DROP COLUMN startDate; + +ALTER TABLE mai2_item_map +CHANGE COLUMN mapId map_id INT NOT NULL, +CHANGE COLUMN isLock is_lock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN seasonId season_d INT NOT NULL, +CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL, +CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL, +CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonusId bonus_id INT NOT NULL, +CHANGE COLUMN isCurrent is_currentBoolean NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete Boolean NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_4_upgrade.sql b/core/data/schema/versions/SDEZ_4_upgrade.sql new file mode 100644 index 0000000..52d120a --- /dev/null +++ b/core/data/schema/versions/SDEZ_4_upgrade.sql @@ -0,0 +1,31 @@ +ALTER TABLE mai2_profile_option +ADD COLUMN tapSe INT NOT NULL DEFAULT 0 AFTER tapDesign; + +ALTER TABLE mai2_score_best +ADD COLUMN extNum1 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_profile_extend +ADD COLUMN playStatusSetting INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_playlog +ADD COLUMN extNum4 INT NOT NULL DEFAULT 0; + +ALTER TABLE mai2_static_event +ADD COLUMN startDate TIMESTAMP NOT NULL DEFAULT current_timestamp(); + +ALTER TABLE mai2_item_map +CHANGE COLUMN map_id mapId INT NOT NULL, +CHANGE COLUMN is_lock isLock BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_clear isClear BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0; + +ALTER TABLE mai2_item_friend_season_ranking +CHANGE COLUMN season_id seasonId INT NOT NULL, +CHANGE COLUMN reward_get rewardGet BOOLEAN NOT NULL, +CHANGE COLUMN user_name userName VARCHAR(8) NOT NULL, +CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL; + +ALTER TABLE mai2_item_login_bonus +CHANGE COLUMN bonus_id bonusId INT NOT NULL, +CHANGE COLUMN is_current isCurrent Boolean NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete Boolean NOT NULL DEFAULT 0; diff --git a/docs/game_specific_info.md b/docs/game_specific_info.md index b09d61e..1f7872c 100644 --- a/docs/game_specific_info.md +++ b/docs/game_specific_info.md @@ -114,6 +114,7 @@ Config file is located in `config/cxb.yaml`. | 3 | maimai DX Splash PLUS | | 4 | maimai DX Universe | | 5 | maimai DX Universe PLUS | +| 6 | maimai DX Festival | ### Importer @@ -126,7 +127,7 @@ python read.py --series SDEZ --version --binfolder /path/to/game/fo The importer for maimai DX will import Events, Music and Tickets. **NOTE: It is required to use the importer because the game will -crash without it!** +crash without Events!** ### Database upgrade diff --git a/readme.md b/readme.md index 4afc225..02f0c67 100644 --- a/readme.md +++ b/readme.md @@ -9,8 +9,8 @@ Games listed below have been tested and confirmed working. Only game versions ol + Crossbeats Rev + All versions + omnimix -+ Maimai - + All versions up to Universe Plus ++ maimai DX + + All versions up to Festival + Hatsune Miku Arcade + All versions diff --git a/titles/mai2/__init__.py b/titles/mai2/__init__.py index 27fba3a..810eac9 100644 --- a/titles/mai2/__init__.py +++ b/titles/mai2/__init__.py @@ -7,4 +7,4 @@ index = Mai2Servlet database = Mai2Data reader = Mai2Reader game_codes = [Mai2Constants.GAME_CODE] -current_schema_version = 3 +current_schema_version = 4 diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 741ccb6..2e7198d 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -1,5 +1,5 @@ from datetime import datetime, date, timedelta -from typing import Dict +from typing import Any, Dict import logging from core.config import CoreConfig @@ -52,6 +52,7 @@ class Mai2Base: events = self.data.static.get_enabled_events(self.version) events_lst = [] if events is None: + self.logger.warn("No enabled events, did you run the reader?") return {"type": data["type"], "length": 0, "gameEventList": []} for event in events: @@ -59,7 +60,11 @@ class Mai2Base: { "type": event["type"], "id": event["eventId"], - "startDate": "2017-12-05 07:00:00.0", + # actually use the startDate from the import so it + # properly shows all the events when new ones are imported + "startDate": datetime.strftime( + event["startDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ), "endDate": "2099-12-31 00:00:00.0", } ) @@ -79,12 +84,12 @@ class Mai2Base: return {"length": 0, "gameChargeList": []} charge_list = [] - for x in range(len(game_charge_list)): + for i, charge in enumerate(game_charge_list): charge_list.append( { - "orderId": x, - "chargeId": game_charge_list[x]["ticketId"], - "price": game_charge_list[x]["price"], + "orderId": i, + "chargeId": charge["ticketId"], + "price": charge["price"], "startDate": "2017-12-05 07:00:00.0", "endDate": "2099-12-31 00:00:00.0", } @@ -167,6 +172,20 @@ class Mai2Base: self.data.score.put_playlog(user_id, playlog) + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: + user_id = data["userId"] + charge = data["userCharge"] + + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") + self.data.item.put_charge( + user_id, + charge["chargeId"], + charge["stock"], + datetime.strptime(charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT), + datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), + ) + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -204,15 +223,21 @@ class Mai2Base: if "userChargeList" in upsert and len(upsert["userChargeList"]) > 0: for charge in upsert["userChargeList"]: + # remove the ".0" from the date string, festival only? + charge["purchaseDate"] = charge["purchaseDate"].replace(".0", "") self.data.item.put_charge( user_id, charge["chargeId"], charge["stock"], - datetime.strptime(charge["purchaseDate"], "%Y-%m-%d %H:%M:%S"), - datetime.strptime(charge["validDate"], "%Y-%m-%d %H:%M:%S") + datetime.strptime( + charge["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ), + datetime.strptime( + charge["validDate"], Mai2Constants.DATE_TIME_FORMAT + ), ) - if upsert["isNewCharacterList"] and int(upsert["isNewCharacterList"]) > 0: + if "userCharacterList" in upsert and len(upsert["userCharacterList"]) > 0: for char in upsert["userCharacterList"]: self.data.item.put_character( user_id, @@ -222,7 +247,7 @@ class Mai2Base: char["useCount"], ) - if upsert["isNewItemList"] and int(upsert["isNewItemList"]) > 0: + if "userItemList" in upsert and len(upsert["userItemList"]) > 0: for item in upsert["userItemList"]: self.data.item.put_item( user_id, @@ -232,7 +257,7 @@ class Mai2Base: item["isValid"], ) - if upsert["isNewLoginBonusList"] and int(upsert["isNewLoginBonusList"]) > 0: + if "userLoginBonusList" in upsert and len(upsert["userLoginBonusList"]) > 0: for login_bonus in upsert["userLoginBonusList"]: self.data.item.put_login_bonus( user_id, @@ -242,7 +267,7 @@ class Mai2Base: login_bonus["isComplete"], ) - if upsert["isNewMapList"] and int(upsert["isNewMapList"]) > 0: + if "userMapList" in upsert and len(upsert["userMapList"]) > 0: for map in upsert["userMapList"]: self.data.item.put_map( user_id, @@ -253,21 +278,27 @@ class Mai2Base: map["isComplete"], ) - if upsert["isNewMusicDetailList"] and int(upsert["isNewMusicDetailList"]) > 0: + if "userMusicDetailList" in upsert and len(upsert["userMusicDetailList"]) > 0: for music in upsert["userMusicDetailList"]: self.data.score.put_best_score(user_id, music) - if upsert["isNewCourseList"] and int(upsert["isNewCourseList"]) > 0: + if "userCourseList" in upsert and len(upsert["userCourseList"]) > 0: for course in upsert["userCourseList"]: self.data.score.put_course(user_id, course) - if upsert["isNewFavoriteList"] and int(upsert["isNewFavoriteList"]) > 0: + if "userFavoriteList" in upsert and len(upsert["userFavoriteList"]) > 0: for fav in upsert["userFavoriteList"]: self.data.item.put_favorite(user_id, fav["kind"], fav["itemIdList"]) - # if "isNewFriendSeasonRankingList" in upsert and int(upsert["isNewFriendSeasonRankingList"]) > 0: - # for fsr in upsert["userFriendSeasonRankingList"]: - # pass + if ( + "userFriendSeasonRankingList" in upsert + and len(upsert["userFriendSeasonRankingList"]) > 0 + ): + for fsr in upsert["userFriendSeasonRankingList"]: + fsr["recordDate"] = ( + datetime.strptime(fsr["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0"), + ) + self.data.item.put_friend_season_ranking(user_id, fsr) def handle_user_logout_api_request(self, data: Dict) -> Dict: pass @@ -311,11 +342,7 @@ class Mai2Base: def handle_get_user_card_api_request(self, data: Dict) -> Dict: user_cards = self.data.item.get_cards(data["userId"]) if user_cards is None: - return { - "userId": data["userId"], - "nextIndex": 0, - "userCardList": [] - } + return {"userId": data["userId"], "nextIndex": 0, "userCardList": []} max_ct = data["maxCount"] next_idx = data["nextIndex"] @@ -333,25 +360,23 @@ class Mai2Base: tmp.pop("id") tmp.pop("user") tmp["startDate"] = datetime.strftime( - tmp["startDate"], "%Y-%m-%d %H:%M:%S") + tmp["startDate"], Mai2Constants.DATE_TIME_FORMAT + ) tmp["endDate"] = datetime.strftime( - tmp["endDate"], "%Y-%m-%d %H:%M:%S") + tmp["endDate"], Mai2Constants.DATE_TIME_FORMAT + ) card_list.append(tmp) return { "userId": data["userId"], "nextIndex": next_idx, - "userCardList": card_list[start_idx:end_idx] + "userCardList": card_list[start_idx:end_idx], } def handle_get_user_charge_api_request(self, data: Dict) -> Dict: user_charges = self.data.item.get_charges(data["userId"]) if user_charges is None: - return { - "userId": data["userId"], - "length": 0, - "userChargeList": [] - } + return {"userId": data["userId"], "length": 0, "userChargeList": []} user_charge_list = [] for charge in user_charges: @@ -359,45 +384,46 @@ class Mai2Base: tmp.pop("id") tmp.pop("user") tmp["purchaseDate"] = datetime.strftime( - tmp["purchaseDate"], "%Y-%m-%d %H:%M:%S") + tmp["purchaseDate"], Mai2Constants.DATE_TIME_FORMAT + ) tmp["validDate"] = datetime.strftime( - tmp["validDate"], "%Y-%m-%d %H:%M:%S") + tmp["validDate"], Mai2Constants.DATE_TIME_FORMAT + ) user_charge_list.append(tmp) return { "userId": data["userId"], "length": len(user_charge_list), - "userChargeList": user_charge_list + "userChargeList": user_charge_list, } def handle_get_user_item_api_request(self, data: Dict) -> Dict: kind = int(data["nextIndex"] / 10000000000) next_idx = int(data["nextIndex"] % 10000000000) - user_items = self.data.item.get_items(data["userId"], kind) - user_item_list = [] - next_idx = 0 + user_item_list = self.data.item.get_items(data["userId"], kind) - for x in range(next_idx, data["maxCount"]): - try: - user_item_list.append({ - "itemKind": user_items[x]["itemKind"], - "itemId": user_items[x]["itemId"], - "stock": user_items[x]["stock"], - "isValid": user_items[x]["isValid"] - }) - except IndexError: + items: list[Dict[str, Any]] = [] + for i in range(next_idx, len(user_item_list)): + tmp = user_item_list[i]._asdict() + tmp.pop("user") + tmp.pop("id") + items.append(tmp) + if len(items) >= int(data["maxCount"]): break - if len(user_item_list) == data["maxCount"]: - next_idx = data["nextIndex"] + data["maxCount"] + 1 - break + xout = kind * 10000000000 + next_idx + len(items) + + if len(items) < int(data["maxCount"]): + next_idx = 0 + else: + next_idx = xout return { "userId": data["userId"], "nextIndex": next_idx, "itemKind": kind, - "userItemList": user_item_list + "userItemList": items, } def handle_get_user_character_api_request(self, data: Dict) -> Dict: @@ -479,21 +505,12 @@ class Mai2Base: tmp.pop("user") mlst.append(tmp) - return { - "userActivity": { - "playList": plst, - "musicList": mlst - } - } + return {"userActivity": {"playList": plst, "musicList": mlst}} def handle_get_user_course_api_request(self, data: Dict) -> Dict: user_courses = self.data.score.get_courses(data["userId"]) if user_courses is None: - return { - "userId": data["userId"], - "nextIndex": 0, - "userCourseList": [] - } + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": []} course_list = [] for course in user_courses: @@ -502,11 +519,7 @@ class Mai2Base: tmp.pop("id") course_list.append(tmp) - return { - "userId": data["userId"], - "nextIndex": 0, - "userCourseList": course_list - } + return {"userId": data["userId"], "nextIndex": 0, "userCourseList": course_list} def handle_get_user_portrait_api_request(self, data: Dict) -> Dict: # No support for custom pfps @@ -514,96 +527,103 @@ class Mai2Base: def handle_get_user_friend_season_ranking_api_request(self, data: Dict) -> Dict: friend_season_ranking = self.data.item.get_friend_season_ranking(data["userId"]) - friend_season_ranking_list = [] - next_index = 0 + if friend_season_ranking is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userFriendSeasonRankingList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - friend_season_ranking_list.append( - { - "mapId": friend_season_ranking_list[x]["map_id"], - "distance": friend_season_ranking_list[x]["distance"], - "isLock": friend_season_ranking_list[x]["is_lock"], - "isClear": friend_season_ranking_list[x]["is_clear"], - "isComplete": friend_season_ranking_list[x]["is_complete"], - } - ) - except: + friend_season_ranking_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(friend_season_ranking)): + tmp = friend_season_ranking[x]._asdict() + tmp.pop("user") + tmp.pop("id") + tmp["recordDate"] = datetime.strftime( + tmp["recordDate"], f"{Mai2Constants.DATE_TIME_FORMAT}.0" + ) + friend_season_ranking_list.append(tmp) + + if len(friend_season_ranking_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(friend_season_ranking_list) == data["maxCount"] - and len(friend_season_ranking) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(friend_season_ranking) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userFriendSeasonRankingList": friend_season_ranking_list, } def handle_get_user_map_api_request(self, data: Dict) -> Dict: maps = self.data.item.get_maps(data["userId"]) - map_list = [] - next_index = 0 + if maps is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userMapList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - map_list.append( - { - "mapId": maps[x]["map_id"], - "distance": maps[x]["distance"], - "isLock": maps[x]["is_lock"], - "isClear": maps[x]["is_clear"], - "isComplete": maps[x]["is_complete"], - } - ) - except: + map_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(maps)): + tmp = maps[x]._asdict() + tmp.pop("user") + tmp.pop("id") + map_list.append(tmp) + + if len(map_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(map_list) == data["maxCount"] - and len(maps) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(maps) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userMapList": map_list, } def handle_get_user_login_bonus_api_request(self, data: Dict) -> Dict: login_bonuses = self.data.item.get_login_bonuses(data["userId"]) - login_bonus_list = [] - next_index = 0 + if login_bonuses is None: + return { + "userId": data["userId"], + "nextIndex": 0, + "userLoginBonusList": [], + } - for x in range(data["nextIndex"], data["maxCount"] + data["nextIndex"]): - try: - login_bonus_list.append( - { - "bonusId": login_bonuses[x]["bonus_id"], - "point": login_bonuses[x]["point"], - "isCurrent": login_bonuses[x]["is_current"], - "isComplete": login_bonuses[x]["is_complete"], - } - ) - except: + login_bonus_list = [] + next_idx = int(data["nextIndex"]) + max_ct = int(data["maxCount"]) + + for x in range(next_idx, len(login_bonuses)): + tmp = login_bonuses[x]._asdict() + tmp.pop("user") + tmp.pop("id") + login_bonus_list.append(tmp) + + if len(login_bonus_list) >= max_ct: break - # We're capped and still have some left to go - if ( - len(login_bonus_list) == data["maxCount"] - and len(login_bonuses) > data["maxCount"] + data["nextIndex"] - ): - next_index = data["maxCount"] + data["nextIndex"] + if len(login_bonuses) >= next_idx + max_ct: + next_idx += max_ct + else: + next_idx = 0 return { "userId": data["userId"], - "nextIndex": next_index, + "nextIndex": next_idx, "userLoginBonusList": login_bonus_list, } @@ -629,5 +649,5 @@ class Mai2Base: return { "userId": data["userId"], "nextIndex": next_index, - "userMusicList": [{"userMusicDetailList": music_detail_list}] + "userMusicList": [{"userMusicDetailList": music_detail_list}], } diff --git a/titles/mai2/const.py b/titles/mai2/const.py index dd1dca0..9694aba 100644 --- a/titles/mai2/const.py +++ b/titles/mai2/const.py @@ -30,14 +30,16 @@ class Mai2Constants: VER_MAIMAI_DX_SPLASH_PLUS = 3 VER_MAIMAI_DX_UNIVERSE = 4 VER_MAIMAI_DX_UNIVERSE_PLUS = 5 + VER_MAIMAI_DX_FESTIVAL = 6 VERSION_STRING = ( - "maimai Delux", - "maimai Delux PLUS", - "maimai Delux Splash", - "maimai Delux Splash PLUS", - "maimai Delux Universe", - "maimai Delux Universe PLUS", + "maimai DX", + "maimai DX PLUS", + "maimai DX Splash", + "maimai DX Splash PLUS", + "maimai DX Universe", + "maimai DX Universe PLUS", + "maimai DX Festival" ) @classmethod diff --git a/titles/mai2/festival.py b/titles/mai2/festival.py new file mode 100644 index 0000000..4e51619 --- /dev/null +++ b/titles/mai2/festival.py @@ -0,0 +1,31 @@ +from typing import Dict + +from core.config import CoreConfig +from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.const import Mai2Constants +from titles.mai2.config import Mai2Config + + +class Mai2Festival(Mai2UniversePlus): + def __init__(self, cfg: CoreConfig, game_cfg: Mai2Config) -> None: + super().__init__(cfg, game_cfg) + self.version = Mai2Constants.VER_MAIMAI_DX_FESTIVAL + + def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict: + user_data = super().handle_cm_get_user_preview_api_request(data) + + # hardcode lastDataVersion for CardMaker 1.36 + user_data["lastDataVersion"] = "1.30.00" + return user_data + + def handle_user_login_api_request(self, data: Dict) -> Dict: + user_login = super().handle_user_login_api_request(data) + # useless? + user_login["Bearer"] = "ARTEMiSTOKEN" + return user_login + + def handle_get_user_recommend_rate_music_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "userRecommendRateMusicIdList": []} + + def handle_get_user_recommend_select_music_api_request(self, data: Dict) -> Dict: + return {"userId": data["userId"], "userRecommendSelectionMusicIdList": []} diff --git a/titles/mai2/index.py b/titles/mai2/index.py index 3cd1629..3618f4d 100644 --- a/titles/mai2/index.py +++ b/titles/mai2/index.py @@ -10,6 +10,7 @@ from os import path from typing import Tuple from core.config import CoreConfig +from core.utils import Utils from titles.mai2.config import Mai2Config from titles.mai2.const import Mai2Constants from titles.mai2.base import Mai2Base @@ -18,6 +19,7 @@ from titles.mai2.splash import Mai2Splash from titles.mai2.splashplus import Mai2SplashPlus from titles.mai2.universe import Mai2Universe from titles.mai2.universeplus import Mai2UniversePlus +from titles.mai2.festival import Mai2Festival class Mai2Servlet: @@ -30,12 +32,13 @@ class Mai2Servlet: ) self.versions = [ - Mai2Base(core_cfg, self.game_cfg), - Mai2Plus(core_cfg, self.game_cfg), - Mai2Splash(core_cfg, self.game_cfg), - Mai2SplashPlus(core_cfg, self.game_cfg), - Mai2Universe(core_cfg, self.game_cfg), - Mai2UniversePlus(core_cfg, self.game_cfg), + Mai2Base, + Mai2Plus, + Mai2Splash, + Mai2SplashPlus, + Mai2Universe, + Mai2UniversePlus, + Mai2Festival ] self.logger = logging.getLogger("mai2") @@ -97,6 +100,7 @@ class Mai2Servlet: url_split = url_path.split("/") internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) if version < 105: # 1.0 internal_ver = Mai2Constants.VER_MAIMAI_DX @@ -108,8 +112,10 @@ class Mai2Servlet: internal_ver = Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS elif version >= 120 and version < 125: # Universe internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE - elif version >= 125: # Universe Plus + elif version >= 125 and version < 130: # Universe Plus internal_ver = Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS + elif version >= 130: # Festival + internal_ver = Mai2Constants.VER_MAIMAI_DX_FESTIVAL if all(c in string.hexdigits for c in endpoint) and len(endpoint) == 32: # If we get a 32 character long hex string, it's a hash and we're @@ -128,25 +134,30 @@ class Mai2Servlet: req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" + handler_cls = self.versions[internal_ver](self.core_cfg, self.game_cfg) - if not hasattr(self.versions[internal_ver], func_to_find): + if not hasattr(handler_cls, func_to_find): self.logger.warning(f"Unhandled v{version} request {endpoint}") - return zlib.compress(b'{"returnCode": 1}') + resp = {"returnCode": 1} - try: - handler = getattr(self.versions[internal_ver], func_to_find) - resp = handler(req_data) + else: + try: + handler = getattr(handler_cls, func_to_find) + resp = handler(req_data) - except Exception as e: - self.logger.error(f"Error handling v{version} method {endpoint} - {e}") - return zlib.compress(b'{"stat": "0"}') + except Exception as e: + self.logger.error(f"Error handling v{version} method {endpoint} - {e}") + return zlib.compress(b'{"stat": "0"}') if resp == None: resp = {"returnCode": 1} - self.logger.info(f"Response {resp}") + self.logger.debug(f"Response {resp}") return zlib.compress(json.dumps(resp, ensure_ascii=False).encode("utf-8")) diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index d64d954..c6b2e7a 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -71,12 +71,12 @@ map = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("map_id", Integer, nullable=False), + Column("mapId", Integer, nullable=False), Column("distance", Integer, nullable=False), - Column("is_lock", Boolean, nullable=False, server_default="0"), - Column("is_clear", Boolean, nullable=False, server_default="0"), - Column("is_complete", Boolean, nullable=False, server_default="0"), - UniqueConstraint("user", "map_id", name="mai2_item_map_uk"), + Column("isLock", Boolean, nullable=False, server_default="0"), + Column("isClear", Boolean, nullable=False, server_default="0"), + Column("isComplete", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "mapId", name="mai2_item_map_uk"), mysql_charset="utf8mb4", ) @@ -89,11 +89,11 @@ login_bonus = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("bonus_id", Integer, nullable=False), + Column("bonusId", Integer, nullable=False), Column("point", Integer, nullable=False), - Column("is_current", Boolean, nullable=False, server_default="0"), - Column("is_complete", Boolean, nullable=False, server_default="0"), - UniqueConstraint("user", "bonus_id", name="mai2_item_login_bonus_uk"), + Column("isCurrent", Boolean, nullable=False, server_default="0"), + Column("isComplete", Boolean, nullable=False, server_default="0"), + UniqueConstraint("user", "bonusId", name="mai2_item_login_bonus_uk"), mysql_charset="utf8mb4", ) @@ -106,13 +106,15 @@ friend_season_ranking = Table( ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False, ), - Column("season_id", Integer, nullable=False), + Column("seasonId", Integer, nullable=False), Column("point", Integer, nullable=False), Column("rank", Integer, nullable=False), - Column("reward_get", Boolean, nullable=False), - Column("user_name", String(8), nullable=False), - Column("record_date", String(255), nullable=False), - UniqueConstraint("user", "season_id", "user_name", name="mai2_item_login_bonus_uk"), + Column("rewardGet", Boolean, nullable=False), + Column("userName", String(8), nullable=False), + Column("recordDate", TIMESTAMP, nullable=False), + UniqueConstraint( + "user", "seasonId", "userName", name="mai2_item_friend_season_ranking_uk" + ), mysql_charset="utf8mb4", ) @@ -293,18 +295,18 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(map).values( user=user_id, - map_id=map_id, + mapId=map_id, distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( distance=distance, - is_lock=is_lock, - is_clear=is_clear, - is_complete=is_complete, + isLock=is_lock, + isClear=is_clear, + isComplete=is_complete, ) result = self.execute(conflict) @@ -324,7 +326,7 @@ class Mai2ItemData(BaseData): return result.fetchall() def get_map(self, user_id: int, map_id: int) -> Optional[Row]: - sql = map.select(and_(map.c.user == user_id, map.c.map_id == map_id)) + sql = map.select(and_(map.c.user == user_id, map.c.mapId == map_id)) result = self.execute(sql) if result is None: @@ -341,16 +343,16 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(character).values( user=user_id, - character_id=character_id, + characterId=character_id, level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) conflict = sql.on_duplicate_key_update( level=level, awakening=awakening, - use_count=use_count, + useCount=use_count, ) result = self.execute(conflict) @@ -385,7 +387,25 @@ class Mai2ItemData(BaseData): result = self.execute(sql) if result is None: return None - return result.fetchone() + return result.fetchall() + + def put_friend_season_ranking( + self, aime_id: int, friend_season_ranking_data: Dict + ) -> Optional[int]: + sql = insert(friend_season_ranking).values( + user=aime_id, **friend_season_ranking_data + ) + + conflict = sql.on_duplicate_key_update(**friend_season_ranking_data) + result = self.execute(conflict) + + if result is None: + self.logger.warn( + f"put_friend_season_ranking: failed to insert", + f"friend_season_ranking! aime_id: {aime_id}" + ) + return None + return result.lastrowid def put_favorite( self, user_id: int, kind: int, item_id_list: List[int] diff --git a/titles/mai2/schema/profile.py b/titles/mai2/schema/profile.py index 1ce8046..b3802e5 100644 --- a/titles/mai2/schema/profile.py +++ b/titles/mai2/schema/profile.py @@ -158,6 +158,7 @@ extend = Table( Column("sortMusicSetting", Integer), Column("selectedCardList", JSON), Column("encountMapNpcList", JSON), + Column("playStatusSetting", Integer, server_default="0"), UniqueConstraint("user", "version", name="mai2_profile_extend_uk"), mysql_charset="utf8mb4", ) @@ -178,6 +179,7 @@ option = Table( Column("slideSpeed", Integer), Column("touchSpeed", Integer), Column("tapDesign", Integer), + Column("tapSe", Integer, server_default="0"), Column("holdDesign", Integer), Column("slideDesign", Integer), Column("starType", Integer), @@ -298,8 +300,8 @@ class Mai2ProfileData(BaseData): def get_profile_detail(self, user_id: int, version: int) -> Optional[Row]: sql = select(detail).where( - and_(detail.c.user == user_id, detail.c.version == version) - ) + and_(detail.c.user == user_id, detail.c.version <= version) + ).order_by(detail.c.version.desc()) result = self.execute(sql) if result is None: @@ -323,8 +325,8 @@ class Mai2ProfileData(BaseData): def get_profile_ghost(self, user_id: int, version: int) -> Optional[Row]: sql = select(ghost).where( - and_(ghost.c.user == user_id, ghost.c.version_int == version) - ) + and_(ghost.c.user == user_id, ghost.c.version_int <= version) + ).order_by(ghost.c.version.desc()) result = self.execute(sql) if result is None: @@ -348,8 +350,8 @@ class Mai2ProfileData(BaseData): def get_profile_extend(self, user_id: int, version: int) -> Optional[Row]: sql = select(extend).where( - and_(extend.c.user == user_id, extend.c.version == version) - ) + and_(extend.c.user == user_id, extend.c.version <= version) + ).order_by(extend.c.version.desc()) result = self.execute(sql) if result is None: @@ -373,8 +375,8 @@ class Mai2ProfileData(BaseData): def get_profile_option(self, user_id: int, version: int) -> Optional[Row]: sql = select(option).where( - and_(option.c.user == user_id, option.c.version == version) - ) + and_(option.c.user == user_id, option.c.version <= version) + ).order_by(option.c.version.desc()) result = self.execute(sql) if result is None: @@ -398,8 +400,8 @@ class Mai2ProfileData(BaseData): def get_profile_rating(self, user_id: int, version: int) -> Optional[Row]: sql = select(rating).where( - and_(rating.c.user == user_id, rating.c.version == version) - ) + and_(rating.c.user == user_id, rating.c.version <= version) + ).order_by(rating.c.version.desc()) result = self.execute(sql) if result is None: diff --git a/titles/mai2/schema/score.py b/titles/mai2/schema/score.py index 4d3291d..0f7f239 100644 --- a/titles/mai2/schema/score.py +++ b/titles/mai2/schema/score.py @@ -25,6 +25,7 @@ best_score = Table( Column("syncStatus", Integer), Column("deluxscoreMax", Integer), Column("scoreRank", Integer), + Column("extNum1", Integer, server_default="0"), UniqueConstraint("user", "musicId", "level", name="mai2_score_best_uk"), mysql_charset="utf8mb4", ) @@ -143,6 +144,7 @@ playlog = Table( Column("isNewFree", Boolean), Column("extNum1", Integer), Column("extNum2", Integer), + Column("extNum4", Integer, server_default="0"), Column("trialPlayAchievement", Integer), mysql_charset="utf8mb4", ) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index e40e37f..b22dcc4 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -16,6 +16,7 @@ event = Table( Column("eventId", Integer), Column("type", Integer), Column("name", String(255)), + Column("startDate", TIMESTAMP, server_default=func.now()), Column("enabled", Boolean, server_default="1"), UniqueConstraint("version", "eventId", "type", name="mai2_static_event_uk"), mysql_charset="utf8mb4", @@ -108,7 +109,7 @@ class Mai2StaticData(BaseData): return None return result.fetchall() - def toggle_game_events( + def toggle_game_event( self, version: int, event_id: int, toggle: bool ) -> Optional[List]: sql = event.update( @@ -118,7 +119,7 @@ class Mai2StaticData(BaseData): result = self.execute(sql) if result is None: self.logger.warning( - f"toggle_game_events: Failed to update event! event_id {event_id} toggle {toggle}" + f"toggle_game_event: Failed to update event! event_id {event_id} toggle {toggle}" ) return result.last_updated_params() diff --git a/titles/mai2/universeplus.py b/titles/mai2/universeplus.py index 54fe896..e45c719 100644 --- a/titles/mai2/universeplus.py +++ b/titles/mai2/universeplus.py @@ -1,7 +1,4 @@ -from typing import Any, List, Dict -from datetime import datetime, timedelta -import pytz -import json +from typing import Dict from core.config import CoreConfig from titles.mai2.universe import Mai2Universe From 28c06335b648b9f02a374feca7b471bc3170712c Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Tue, 11 Apr 2023 17:57:21 +0200 Subject: [PATCH 2/4] mai2: added upsert returns, fixed event reader, thanks @One3 Thanks to @One3 for helping with the events --- titles/mai2/base.py | 16 +++++++--- titles/mai2/read.py | 61 +++++++++++++++++++++++++++++++++++- titles/mai2/schema/item.py | 10 +++--- titles/mai2/schema/static.py | 2 +- 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/titles/mai2/base.py b/titles/mai2/base.py index 2e7198d..9f5c8af 100644 --- a/titles/mai2/base.py +++ b/titles/mai2/base.py @@ -98,16 +98,16 @@ class Mai2Base: return {"length": len(charge_list), "gameChargeList": charge_list} def handle_upsert_client_setting_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientSettingApi"} def handle_upsert_client_upload_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientUploadApi"} def handle_upsert_client_bookkeeping_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientBookkeepingApi"} def handle_upsert_client_testmode_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1, "apiName": "UpsertClientTestmodeApi"} def handle_get_user_preview_api_request(self, data: Dict) -> Dict: p = self.data.profile.get_profile_detail(data["userId"], self.version) @@ -172,6 +172,8 @@ class Mai2Base: self.data.score.put_playlog(user_id, playlog) + return {"returnCode": 1, "apiName": "UploadUserPlaylogApi"} + def handle_upsert_user_chargelog_api_request(self, data: Dict) -> Dict: user_id = data["userId"] charge = data["userCharge"] @@ -186,6 +188,8 @@ class Mai2Base: datetime.strptime(charge["validDate"], Mai2Constants.DATE_TIME_FORMAT), ) + return {"returnCode": 1, "apiName": "UpsertUserChargelogApi"} + def handle_upsert_user_all_api_request(self, data: Dict) -> Dict: user_id = data["userId"] upsert = data["upsertUserAll"] @@ -300,8 +304,10 @@ class Mai2Base: ) self.data.item.put_friend_season_ranking(user_id, fsr) + return {"returnCode": 1, "apiName": "UpsertUserAllApi"} + def handle_user_logout_api_request(self, data: Dict) -> Dict: - pass + return {"returnCode": 1} def handle_get_user_data_api_request(self, data: Dict) -> Dict: profile = self.data.profile.get_profile_detail(data["userId"], self.version) diff --git a/titles/mai2/read.py b/titles/mai2/read.py index 2c0567c..5809464 100644 --- a/titles/mai2/read.py +++ b/titles/mai2/read.py @@ -29,7 +29,7 @@ class Mai2Reader(BaseReader): f"Start importer for {Mai2Constants.game_ver_to_string(version)}" ) except IndexError: - self.logger.error(f"Invalid maidx version {version}") + self.logger.error(f"Invalid maimai DX version {version}") exit(1) def read(self) -> None: @@ -43,6 +43,7 @@ class Mai2Reader(BaseReader): for dir in data_dirs: self.logger.info(f"Read from {dir}") self.get_events(f"{dir}/event") + self.disable_events(f"{dir}/information", f"{dir}/scoreRanking") self.read_music(f"{dir}/music") self.read_tickets(f"{dir}/ticket") @@ -64,6 +65,64 @@ class Mai2Reader(BaseReader): ) self.logger.info(f"Added event {id}...") + def disable_events( + self, base_information_dir: str, base_score_ranking_dir: str + ) -> None: + self.logger.info(f"Reading disabled events from {base_information_dir}...") + + for root, dirs, files in os.walk(base_information_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/Information.xml"): + with open(f"{root}/{dir}/Information.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("name").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + for root, dirs, files in os.walk(base_score_ranking_dir): + for dir in dirs: + if os.path.exists(f"{root}/{dir}/ScoreRanking.xml"): + with open(f"{root}/{dir}/ScoreRanking.xml", encoding="utf-8") as f: + troot = ET.fromstring(f.read()) + + event_id = int(troot.find("eventName").find("id").text) + + self.data.static.toggle_game_event( + self.version, event_id, toggle=False + ) + self.logger.info(f"Disabled event {event_id}...") + + # manually disable events wich are known to be problematic + for event_id in [ + 1, + 10, + 220311, + 220312, + 220313, + 220314, + 220315, + 220316, + 220317, + 220318, + 20121821, + 21121651, + 22091511, + 22091512, + 22091513, + 22091514, + 22091515, + 22091516, + 22091517, + 22091518, + 22091519, + ]: + self.data.static.toggle_game_event(self.version, event_id, toggle=False) + self.logger.info(f"Disabled event {event_id}...") + def read_music(self, base_dir: str) -> None: self.logger.info(f"Reading music from {base_dir}...") diff --git a/titles/mai2/schema/item.py b/titles/mai2/schema/item.py index c6b2e7a..2dc5f5e 100644 --- a/titles/mai2/schema/item.py +++ b/titles/mai2/schema/item.py @@ -246,16 +246,16 @@ class Mai2ItemData(BaseData): ) -> None: sql = insert(login_bonus).values( user=user_id, - bonus_id=bonus_id, + bonusId=bonus_id, point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) conflict = sql.on_duplicate_key_update( point=point, - is_current=is_current, - is_complete=is_complete, + isCurrent=is_current, + isComplete=is_complete, ) result = self.execute(conflict) diff --git a/titles/mai2/schema/static.py b/titles/mai2/schema/static.py index b22dcc4..76b163c 100644 --- a/titles/mai2/schema/static.py +++ b/titles/mai2/schema/static.py @@ -113,7 +113,7 @@ class Mai2StaticData(BaseData): self, version: int, event_id: int, toggle: bool ) -> Optional[List]: sql = event.update( - and_(event.c.version == version, event.c.event_id == event_id) + and_(event.c.version == version, event.c.eventId == event_id) ).values(enabled=int(toggle)) result = self.execute(sql) From 97e3f1af0160a13b652d872b546018cfe3221460 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Thu, 13 Apr 2023 22:22:28 +0200 Subject: [PATCH 3/4] mai2: cardmaker festival support --- titles/cm/index.py | 7 ++++++- titles/cm/read.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/titles/cm/index.py b/titles/cm/index.py index d544e59..3a56566 100644 --- a/titles/cm/index.py +++ b/titles/cm/index.py @@ -12,6 +12,7 @@ from twisted.web.http import Request from logging.handlers import TimedRotatingFileHandler from core.config import CoreConfig +from core.utils import Utils from titles.cm.config import CardMakerConfig from titles.cm.const import CardMakerConstants from titles.cm.base import CardMakerBase @@ -82,6 +83,7 @@ class CardMakerServlet: url_split = url_path.split("/") internal_ver = 0 endpoint = url_split[len(url_split) - 1] + client_ip = Utils.get_ip_addr(request) print(f"version: {version}") @@ -107,7 +109,10 @@ class CardMakerServlet: req_data = json.loads(unzip) - self.logger.info(f"v{version} {endpoint} request - {req_data}") + self.logger.info( + f"v{version} {endpoint} request from {client_ip}" + ) + self.logger.debug(req_data) func_to_find = "handle_" + inflection.underscore(endpoint) + "_request" diff --git a/titles/cm/read.py b/titles/cm/read.py index f27b40b..93ff203 100644 --- a/titles/cm/read.py +++ b/titles/cm/read.py @@ -80,7 +80,7 @@ class CardMakerReader(BaseReader): for dir in data_dirs: self.read_chuni_card(f"{dir}/CHU/card") self.read_chuni_gacha(f"{dir}/CHU/gacha") - + self.read_mai2_card(f"{dir}/MAI/card") self.read_ongeki_gacha(f"{dir}/MU3/gacha") def read_chuni_card(self, base_dir: str) -> None: @@ -206,6 +206,7 @@ class CardMakerReader(BaseReader): "1.15": Mai2Constants.VER_MAIMAI_DX_SPLASH_PLUS, "1.20": Mai2Constants.VER_MAIMAI_DX_UNIVERSE, "1.25": Mai2Constants.VER_MAIMAI_DX_UNIVERSE_PLUS, + "1.30": Mai2Constants.VER_MAIMAI_DX_FESTIVAL, } for root, dirs, files in os.walk(base_dir): From 958471b8ebdcad0240bfddb947bd49ac725a0d86 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Wed, 19 Apr 2023 17:41:36 +0200 Subject: [PATCH 4/4] mai2: update script hotfix --- core/data/schema/versions/SDEZ_3_rollback.sql | 6 +++--- core/data/schema/versions/SDEZ_4_upgrade.sql | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/data/schema/versions/SDEZ_3_rollback.sql b/core/data/schema/versions/SDEZ_3_rollback.sql index f819855..79ca098 100644 --- a/core/data/schema/versions/SDEZ_3_rollback.sql +++ b/core/data/schema/versions/SDEZ_3_rollback.sql @@ -20,12 +20,12 @@ CHANGE COLUMN isClear is_clear BOOLEAN NOT NULL DEFAULT 0, CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; ALTER TABLE mai2_item_friend_season_ranking -CHANGE COLUMN seasonId season_d INT NOT NULL, +CHANGE COLUMN seasonId season_id INT NOT NULL, CHANGE COLUMN rewardGet reward_get BOOLEAN NOT NULL, CHANGE COLUMN userName user_name VARCHAR(8) NOT NULL, CHANGE COLUMN recordDate record_date VARCHAR(255) NOT NULL; ALTER TABLE mai2_item_login_bonus CHANGE COLUMN bonusId bonus_id INT NOT NULL, -CHANGE COLUMN isCurrent is_currentBoolean NOT NULL DEFAULT 0, -CHANGE COLUMN isComplete is_complete Boolean NOT NULL DEFAULT 0; \ No newline at end of file +CHANGE COLUMN isCurrent is_current BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN isComplete is_complete BOOLEAN NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/core/data/schema/versions/SDEZ_4_upgrade.sql b/core/data/schema/versions/SDEZ_4_upgrade.sql index 52d120a..a670d6a 100644 --- a/core/data/schema/versions/SDEZ_4_upgrade.sql +++ b/core/data/schema/versions/SDEZ_4_upgrade.sql @@ -27,5 +27,5 @@ CHANGE COLUMN record_date recordDate TIMESTAMP NOT NULL; ALTER TABLE mai2_item_login_bonus CHANGE COLUMN bonus_id bonusId INT NOT NULL, -CHANGE COLUMN is_current isCurrent Boolean NOT NULL DEFAULT 0, -CHANGE COLUMN is_complete isComplete Boolean NOT NULL DEFAULT 0; +CHANGE COLUMN is_current isCurrent BOOLEAN NOT NULL DEFAULT 0, +CHANGE COLUMN is_complete isComplete BOOLEAN NOT NULL DEFAULT 0;