Ready for ex update

This commit is contained in:
UnitedAirforce
2025-08-18 19:47:32 +08:00
parent b900f0ca3e
commit 989981a777
8 changed files with 110 additions and 55 deletions

View File

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

View File

@@ -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>
<summary>Details</summary>
<br>
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.
</details>
#### 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里的脚本进行更正但是本地存档将无法更正。唯一解决方法为清空本地和云存档。敬请注意。
<details>
<summary>细节</summary>
<br>
由于一首歌曲重复两首AC Alt误放至4MAX正常曲目中歌曲列表发生了偏移。你的游玩记录会跟着偏移。其实有一个解决办法就是把这三首位置留空然后在本地文件和服务器端隐藏他们。不过鉴于总曲目限制所剩无几仅剩12首以及所带来的代码更新会将4MAX服务器陷入自己的鸽子洞造成开发的不愉快我选择了牺牲老用户的存档。非常抱歉。
</details>
#### 更新
`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`.

View File

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

View File

@@ -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)
return file.read().format(text=text, img=mode)
def safe_int(val):
try:
return int(val)
except (TypeError, ValueError):
return None

View File

@@ -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"""<div class="f90 a_center pt50">Play Music to level up and unlock free songs!<br>Songs can only be unlocked when you play online.</div><div class='mission-list'>"""

View File

@@ -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"""
<button style="width: 170px; height: 170px; margin: 10px; background-size: cover; background-image: url('/files/image/icon/shop/{i}.jpg');"
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}'">
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}&return_page={page}'">
</button>
"""
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"""
<button style="width: 170px; height: 170px; margin: 10px; background-color: black; background-size: contain; background-repeat: no-repeat; background-position: center center; background-image: url('/files/image/icon/avatar/{i}.png');"
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}'">
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}&return_page={page}'">
</button>
"""
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"""
<div class="text-content">
<p>Brace the Ultimate - Extra - Challenge.</p>
<p>170+ Arcade Extra difficulty charts await you.</p>
<p>Are you looking for a bad time?</p>
<p>If so, this is the Ultimate - Extra - Challenge.</p>
<p>180+ Arcade Extra difficulty charts await you.</p>
<p>You have been warned.</p>
</div>
@@ -333,7 +338,7 @@ async def web_shop_detail(request: Request):
<br>
<div class="buttons" style="margin-top: 20px;">
<a href="wwic://web_purchase_coin?cnt_type={cnt_type}&cnt_id={cnt_id}&num=1" class="bt_bg01" >Buy</a><br>
<a href="wwic://web_shop?cnt_type={cnt_type}" class="bt_bg01" >Go Back</a>
<a href="wwic://web_shop?cnt_type={cnt_type}&page={return_page}" class="bt_bg01" >Go Back</a>
</div>
"""

View File

@@ -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("""<response><code>403</code><message>Access denied.</message></response>""", 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]:

View File

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