From 989981a777210f9732ae2051d5a124415e9ce565 Mon Sep 17 00:00:00 2001 From: UnitedAirforce Date: Mon, 18 Aug 2025 19:47:32 +0800 Subject: [PATCH] Ready for ex update --- 7002.py | 6 +++- README.md | 26 ++++++++++++++++ api/database.py | 2 +- api/misc.py | 8 ++++- api/ranking.py | 80 ++++++++++++++++++++++++------------------------- api/shop.py | 15 ++++++---- api/user.py | 4 +-- config.py | 24 +++++++++++---- 8 files changed, 110 insertions(+), 55 deletions(-) diff --git a/7002.py b/7002.py index 50b8de7..45e43e3 100644 --- a/7002.py +++ b/7002.py @@ -14,8 +14,9 @@ from api.user import routes as user_routes from api.ranking import routes as rank_routes from api.shop import routes as shop_routes from api.play import routes as play_routes +from api.batch import routes as batch_routes -from config import DEBUG, SSL_CERT, SSL_KEY, ROOT_FOLDER, ACTUAL_HOST, ACTUAL_PORT +from config import DEBUG, SSL_CERT, SSL_KEY, ROOT_FOLDER, ACTUAL_HOST, ACTUAL_PORT, BATCH_DOWNLOAD_ENABLED if (os.path.isfile('./files/dlc_4max.html')): get_4max_version_string() @@ -36,6 +37,9 @@ routes = [] routes = routes + user_routes + rank_routes + shop_routes + play_routes +if BATCH_DOWNLOAD_ENABLED: + routes = routes + batch_routes + routes.append(Route("/{path:path}", serve_file)) app = Starlette(debug=DEBUG, routes=routes) diff --git a/README.md b/README.md index 5113e3d..c5b7ea8 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,16 @@ Edit `config.py`'s `MODEL`, `TUNEFILE`, and `SKIN` value to match the `pak`'s ti Important: You must use the `common.zip` inside the `4max expansion` folder, not the `common.zip` in the root directory. +⚠️For players/servers already equipped with the 4MAX expansion, updating to the EX expansion means that your play results will be offsetted. While the server side can be corrected with a script in `various-tools`, your local save can't. The only way to fix this would be to clear your local and cloud save. Please beware. + +
+Details +
+ +Because of a duplicate song, and 2 AC Alts being misplaced into 4MAX normal roster, the song list has changed order. As a result, your old play result would no longer match the song list. There is another solution - that is, to leave the 3 songs blank and hide them on both client and server side. However, since the remaining song list capacity is minimal (12 left), and the resulting code changes will pigeon-hole the 4max server code, resulting in discomfort in development later down the line. As a result, I chose to sacrifice the old user's save file. Sorry about that. + +
+ #### Updates The `GC4MAX` expansion will receive updates to fix bugs. You can check the latest version by going to the ingame `shop` - `songs page` - `GC4MAX banner` after purchasing the expansion. A server restart is required to fetch the latest update. @@ -325,6 +335,16 @@ Since this game features tons of downloadable music and stage files that cannot 重要: 必须使用`4max expansion`文件夹里的`common.zip`,而不是根目录里的`common.zip`. +⚠️针对已安装游玩4MAX扩展包的服务器/用户,更新至EX扩展包会造成歌曲游玩记录偏移。服务器端可使用tools里的脚本进行更正,但是本地存档将无法更正。唯一解决方法为清空本地和云存档。敬请注意。 + +
+细节 +
+ +由于一首歌曲重复,两首AC Alt误放至4MAX正常曲目中,歌曲列表发生了偏移。你的游玩记录会跟着偏移。其实有一个解决办法,就是把这三首位置留空,然后在本地文件和服务器端隐藏他们。不过,鉴于总曲目限制所剩无几(仅剩12首),以及所带来的代码更新会将4MAX服务器陷入自己的鸽子洞,造成开发的不愉快,我选择了牺牲老用户的存档。非常抱歉。 + +
+ #### 更新 `GC4MAX` 扩展包会不定期接受bug修复更新。你可以在购买扩展包之后,通过游戏内的 `shop` - `songs 页面` - `GC4MAX 标题图片` 来查看最新的版本。为获取最新的版本更新,服务器应当不定期重启。 @@ -463,6 +483,12 @@ PC用文本编辑器打开服务器文件夹的 `config.env`,将`IPV4`填写 下载网盘里`install packages`里的`apk`文件。安装。`安卓14+` 设备可能需要用`幸运破解器`重构APK (`Menu of Patches` - `Create Modified APK File` - `APK with changed permissions and activities` - 打开 `Removes integrity check and signature verification` 和 `Re-sign with original signature for android patch "Disable .apk Signature Verification"` 有人反馈中文路径如下:点 `破解菜单` 然后点 `已更改权限和活动项的APK文件` )。 +![](https://studio.code.org/v3/assets/ywLEIWMnUvIOCJAgDvB6Qi2WCialf3EiqCW4qy_vrsM/w1.jpg) + +![](https://studio.code.org/v3/assets/ywLEIWMnUvIOCJAgDvB6Qi2WCialf3EiqCW4qy_vrsM/w2.jpg) + +(已获得授权,感谢@SaltNyaako) + 此`apk`被修改过。若想自己修改,请看最后一段。 打开游戏的`obb`,密码是`eiprblFFv69R83J5`。提取全部文件。或者,从网盘下载`install packages/main.76.jp.co.taito.groovecoasterzero.obb`. diff --git a/api/database.py b/api/database.py index c30760a..5b29181 100644 --- a/api/database.py +++ b/api/database.py @@ -32,7 +32,7 @@ user = Table( Column("data", String, nullable=True), Column("save_id", String, nullable=True), Column("crc", Integer, nullable=True), - Column("timestamp", DateTime, default=datetime.datetime.utcnow), + Column("timestamp", DateTime, default=None), Column("coin_mp", Integer, default=1), ) diff --git a/api/misc.py b/api/misc.py index e5ce581..fb93ffe 100644 --- a/api/misc.py +++ b/api/misc.py @@ -124,4 +124,10 @@ def inform_page(text, mode): elif mode == 3: mode = "/files/web/ttl_title.png" with open("files/inform.html", "r") as file: - return file.read().format(text=text, img=mode) \ No newline at end of file + return file.read().format(text=text, img=mode) + +def safe_int(val): + try: + return int(val) + except (TypeError, ValueError): + return None \ No newline at end of file diff --git a/api/ranking.py b/api/ranking.py index 014af05..028a63d 100644 --- a/api/ranking.py +++ b/api/ranking.py @@ -7,10 +7,10 @@ from sqlalchemy import select, update from config import AUTHORIZATION_NEEDED, USE_REDIS_CACHE -import api.database +from api.database import database, check_whitelist, check_blacklist, redis, result, daily_reward, user from api.crypt import decrypt_fields, encryptAES from api.templates import EXP_UNLOCKED_SONGS, TITLE_LISTS, SONG_LIST -from api.misc import inform_page +from api.misc import inform_page, safe_int async def ranking(request: Request): decrypted_fields, _ = await decrypt_fields(request) @@ -19,7 +19,7 @@ async def ranking(request: Request): should_serve = True if AUTHORIZATION_NEEDED: - should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields) + should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() @@ -61,7 +61,7 @@ async def ranking_detail(request: Request): should_serve = True if AUTHORIZATION_NEEDED: - should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields) + should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() @@ -111,7 +111,7 @@ async def ranking_detail(request: Request): device_result = None if USE_REDIS_CACHE: cache_key = f"{song_id}-{mode}" - cached = await api.database.redis.get(cache_key) + cached = await redis.get(cache_key) else: cached = False @@ -129,19 +129,19 @@ async def ranking_detail(request: Request): sorted_players = json.loads(cached) else: - query = select(api.database.result.c.vid, api.database.result.c.sid, api.database.result.c.mode, api.database.result.c.avatar, api.database.result.c.score) - play_results = await api.database.database.fetch_all(query) + query = select(result.c.vid, result.c.sid, result.c.mode, result.c.avatar, result.c.score) + play_results = await database.fetch_all(query) - query = select(api.database.daily_reward.c.device_id, api.database.daily_reward.c.title, api.database.daily_reward.c.avatar) - device_results_raw = await api.database.database.fetch_all(query) + query = select(daily_reward.c.device_id, daily_reward.c.title, daily_reward.c.avatar) + device_results_raw = await database.fetch_all(query) device_results = {row["device_id"]: {"title": row["title"], "avatar": row["avatar"]} for row in device_results_raw} - query = select(api.database.user.c.id, api.database.user.c.username, api.database.user.c.device_id) - user_results_raw = await api.database.database.fetch_all(query) + query = select(user.c.id, user.c.username, user.c.device_id) + user_results_raw = await database.fetch_all(query) user_results = {row["id"]: {"username": row["username"], "device_id": row["device_id"]} for row in user_results_raw} - query = select(api.database.user).where(api.database.user.c.device_id == device_id) - cur_user = await api.database.database.fetch_one(query) + query = select(user).where(user.c.device_id == device_id) + cur_user = await database.fetch_one(query) player_scores = {} @@ -174,7 +174,7 @@ async def ranking_detail(request: Request): sorted_players = sorted(player_scores.items(), key=lambda x: x[1]["score"], reverse=True) if USE_REDIS_CACHE: - await api.database.redis.set(cache_key, json.dumps(sorted_players), ex=300) + await redis.set(cache_key, json.dumps(sorted_players), ex=300) username = cur_user[1] if cur_user else f"Guest({device_id[-6:]})" @@ -231,25 +231,25 @@ async def ranking_detail(request: Request): play_results = json.loads(cached) else: - query = select(api.database.result).where((api.database.result.c.id == song_id) & (api.database.result.c.mode == mode)) - play_results = await api.database.database.fetch_all(query) + query = select(result).where((result.c.id == song_id) & (result.c.mode == mode)) + play_results = await database.fetch_all(query) play_results = sorted(play_results, key=lambda x: int(x[8]), reverse=True) if USE_REDIS_CACHE: - await api.database.redis.set(cache_key, json.dumps(play_results), ex=300) + await redis.set(cache_key, json.dumps(play_results), ex=300) - query = select(api.database.user).where(api.database.user.c.device_id == device_id) - user_result = await api.database.database.fetch_one(query) + query = select(user).where(user.c.device_id == device_id) + user_result = await database.fetch_one(query) - query = select(api.database.daily_reward).where(api.database.daily_reward.c.device_id == device_id) - device_result = await api.database.database.fetch_one(query) + query = select(daily_reward).where(daily_reward.c.device_id == device_id) + device_result = await database.fetch_one(query) user_id = user_result[0] if user_result else None username = user_result[1] if user_result else f"Guest({device_id[-6:]})" play_record = None if user_id: - play_record = play_record = next( - (record for record in play_results if record[3] not in (None, '') and int(record[3]) == user_id), + play_record = next( + (record for record in play_results if safe_int(record[3]) == user_id), None ) @@ -260,7 +260,7 @@ async def ranking_detail(request: Request): avatar_index = str(play_record[7]) if play_record else "1" user_score = play_record[8] if play_record else 0 for rank, result_obj in enumerate(play_results, start=1): - if user_result and result_obj[3] not in (None, '') and int(result_obj[3]) == user_id: + if user_result and safe_int(result_obj[3]) == user_id: player_rank = rank break elif result_obj[1] == device_id and result_obj[3] in (None, ''): @@ -287,13 +287,13 @@ async def ranking_detail(request: Request): username = f"Guest({record[1][-6:]})" device_info = None if record[3]: - query = select(api.database.user.c.username).where(api.database.user.c.id == record[3]) - user_data = await api.database.database.fetch_one(query) + query = select(user.c.username).where(user.c.id == record[3]) + user_data = await database.fetch_one(query) if user_data: username = user_data["username"] - query = select(api.database.daily_reward.c.title).where(api.database.daily_reward.c.device_id == record[1]) - device_title = await api.database.database.fetch_one(query) + query = select(daily_reward.c.title).where(daily_reward.c.device_id == record[1]) + device_title = await database.fetch_one(query) if device_title: device_info = device_title["title"] else: @@ -343,7 +343,7 @@ async def status(request: Request): should_serve = True if AUTHORIZATION_NEEDED: - should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields) + should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() @@ -352,19 +352,19 @@ async def status(request: Request): if set_title: update_query = ( - update(api.database.daily_reward) - .where(api.database.daily_reward.c.device_id == device_id) + update(daily_reward) + .where(daily_reward.c.device_id == device_id) .values(title=set_title) ) - await api.database.execute(update_query) + await database.execute(update_query) - query = select(api.database.daily_reward).where(api.database.daily_reward.c.device_id == device_id) - user_data = await api.database.database.fetch_one(query) + query = select(daily_reward).where(daily_reward.c.device_id == device_id) + user_data = await database.fetch_one(query) user_name = f"Guest({device_id[-6:]})" if user_data: - query = select(api.database.user.c.username).where(api.database.user.c.device_id == device_id) - user_result = await api.database.database.fetch_one(query) + query = select(user.c.username).where(user.c.device_id == device_id) + user_result = await database.fetch_one(query) if user_result: user_name = user_result["username"] @@ -438,7 +438,7 @@ async def set_title(request: Request): should_serve = True if AUTHORIZATION_NEEDED: - should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields) + should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: device_id = decrypted_fields[b'vid'][0].decode() @@ -446,8 +446,8 @@ async def set_title(request: Request): title_id = decrypted_fields[b'title_id'][0].decode() current_title = 1 - query = select(api.database.daily_reward.c.title).where(api.database.daily_reward.c.device_id == device_id) - row = await api.database.database.fetch_one(query) + query = select(daily_reward.c.title).where(daily_reward.c.device_id == device_id) + row = await database.fetch_one(query) if row: current_title = row["title"] @@ -480,7 +480,7 @@ async def mission(request: Request): should_serve = True if AUTHORIZATION_NEEDED: - should_serve = await api.database.check_whitelist(decrypted_fields) and not await api.database.check_blacklist(decrypted_fields) + should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields) if should_serve: html = f"""
Play Music to level up and unlock free songs!
Songs can only be unlocked when you play online.
""" diff --git a/api/shop.py b/api/shop.py index 3ab5c15..f2ac43f 100644 --- a/api/shop.py +++ b/api/shop.py @@ -91,7 +91,7 @@ async def web_shop(request: Request): if i not in my_stage and i not in EXCLUDE_STAGE_EXP: buttons_html += f""" """ inc += 1 @@ -133,7 +133,7 @@ async def web_shop(request: Request): if i not in my_avatar and i not in EXCLUDE_STAGE_EXP: buttons_html += f""" """ inc += 1 @@ -193,6 +193,10 @@ async def web_shop_detail(request: Request): cnt_type = decrypted_fields[b'cnt_type'][0].decode() cnt_id = int(decrypted_fields[b'cnt_id'][0].decode()) device_id = decrypted_fields[b'vid'][0].decode() + if b'return_page' in decrypted_fields: + return_page = decrypted_fields[b'return_page'][0].decode() + else: + return_page = "1" query = select(daily_reward.c.coin).where(daily_reward.c.device_id == device_id) result = await database.fetch_one(query) @@ -245,8 +249,9 @@ async def web_shop_detail(request: Request): elif cnt_id == -2: html = f"""
-

Brace the Ultimate - Extra - Challenge.

-

170+ Arcade Extra difficulty charts await you.

+

Are you looking for a bad time?

+

If so, this is the Ultimate - Extra - Challenge.

+

180+ Arcade Extra difficulty charts await you.

You have been warned.

@@ -333,7 +338,7 @@ async def web_shop_detail(request: Request):
""" diff --git a/api/user.py b/api/user.py index d5b7c91..136b45b 100644 --- a/api/user.py +++ b/api/user.py @@ -8,7 +8,7 @@ import secrets from sqlalchemy import select, update, insert import xml.etree.ElementTree as ET -from config import ROOT_FOLDER, START_COIN, AUTHORIZATION_NEEDED, HOST, PORT +from config import ROOT_FOLDER, START_COIN, AUTHORIZATION_NEEDED, HOST, PORT, OVERRIDE_HOST from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, get_model_pak, get_tune_pak, get_skin_pak, get_m4a_path, get_stage_path, get_stage_zero from api.database import database, user, daily_reward, get_user_data, set_user_data, check_blacklist, check_whitelist @@ -331,7 +331,7 @@ async def start(request: Request): if not should_serve: return Response("""403Access denied.""", media_type="application/xml") - host_string = "http://" + HOST + ":" + str(PORT) + "/" + host_string = OVERRIDE_HOST if OVERRIDE_HOST is not None else ("http://" + HOST + ":" + str(PORT) + "/") device_id = decrypted_fields[b'vid'][0].decode() for generator in [get_model_pak, get_tune_pak, get_skin_pak, get_m4a_path, get_stage_path]: diff --git a/config.py b/config.py index 241a257..2eee4cf 100644 --- a/config.py +++ b/config.py @@ -5,12 +5,15 @@ Do not change the name of this file. 不要改动这个文件的名称。 ''' ''' -IP and port of the server. -服务器的IP和端口。 +IP and port of the server FOR THE DOWNLOAD LINKS. +下载链接:服务器的IP和端口。 +If you want to use a domain name, set it in OVERRIDE_HOST +若想使用域名,请在OVERRIDE_HOST中设置。 ''' HOST = "192.168.0.106" -PORT = 9070 +PORT = 9068 +OVERRIDE_HOST = None ACTUAL_HOST = "192.168.0.106" ACTUAL_PORT = 9068 @@ -29,7 +32,7 @@ Datecode of the 3 pak files. ''' MODEL = "202504125800" -TUNEFILE = "202504125800" +TUNEFILE = "202507315816" SKIN = "202404191149" ''' @@ -42,6 +45,9 @@ ITEM_PRICE = 2 COIN_REWARD = 1 START_COIN = 10 +FMAX_PRICE = 300 +EX_PRICE = 150 + ''' Only the whitelisted playerID can use the service. Blacklist has priority over whitelist. 只有白名单的玩家ID才能使用服务。黑名单优先于白名单。 @@ -56,11 +62,19 @@ SSL certificate path. If left blank, use HTTP. SSL_CERT = None SSL_KEY = None +''' +是否开启批量下载功能 +Whether to enable batch download functionality. +''' + +BATCH_DOWNLOAD_ENABLED = True +THREAD_COUNT = 3 + ''' Starlette default debug Starlette内置Debug ''' -DEBUG = False +DEBUG = True ROOT_FOLDER = os.path.dirname(os.path.abspath(__file__)) \ No newline at end of file