mirror of
https://github.com/qwerfd2/Groove_Coaster_2_Server.git
synced 2026-09-22 22:38:01 +03:00
New server open source
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
from starlette.applications import Starlette
|
||||
from starlette.responses import FileResponse, Response
|
||||
from starlette.routing import Route
|
||||
import os
|
||||
|
||||
# stupid loading sequence
|
||||
from api.templates import init_templates
|
||||
init_templates()
|
||||
|
||||
from api.database import database, init_db
|
||||
from api.misc import get_4max_version_string
|
||||
|
||||
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 config import HOST, PORT, DEBUG, SSL_CERT, SSL_KEY, ROOT_FOLDER, ACTUAL_HOST, ACTUAL_PORT
|
||||
|
||||
if (os.path.isfile('./files/dlc_4max.html')):
|
||||
get_4max_version_string()
|
||||
|
||||
allowed_folders = ["files"]
|
||||
|
||||
async def serve_file(request):
|
||||
path = request.path_params['path']
|
||||
for folder in allowed_folders:
|
||||
if path.startswith(folder):
|
||||
file_path = os.path.join(ROOT_FOLDER, path)
|
||||
if os.path.isfile(file_path):
|
||||
return FileResponse(file_path)
|
||||
|
||||
return Response("", status_code=404)
|
||||
|
||||
routes = []
|
||||
|
||||
routes = routes + user_routes + rank_routes + shop_routes
|
||||
|
||||
routes.append(Route("/{path:path}", serve_file))
|
||||
|
||||
app = Starlette(debug=DEBUG, routes=routes)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await database.connect()
|
||||
await init_db()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await database.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
ssl_context = (SSL_CERT, SSL_KEY) if SSL_CERT and SSL_KEY else None
|
||||
uvicorn.run(app, host=ACTUAL_HOST, port=ACTUAL_PORT, ssl_certfile=SSL_CERT, ssl_keyfile=SSL_KEY)
|
||||
|
||||
# Made By Tony 2025.5.10
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A small local server for `Groove Coaster 2: Original Style`, implemented with `Python` and `Flask`.
|
||||
|
||||
一个基于`Python`和`Flask`的微型`Groove Coaster 2: Original Style`本地服务器。
|
||||
一个基于`Python`和~~`Flask`~~`Starlette`的微型`Groove Coaster 2: Original Style`本地服务器。
|
||||
|
||||
<details>
|
||||
<summary>English</summary>
|
||||
@@ -14,7 +14,9 @@ This project is for game preservation purposes only. Creative liberty and conven
|
||||
|
||||
You shall bare all the responsibility for any potential consequences as a result of running this server. If you do not agree to these requirements, you are not allowed to replicate or run this program.
|
||||
|
||||
It is designed as a **local** server, as Flask face issues with high concurrency. There is an optimized, `async` server, but the code is not open source. Only vetted server owners that will not violate the license terms will be given access. Contact the repo owner for more information.
|
||||
~~It is designed as a **local** server, as Flask face issues with high concurrency. There is an optimized, `async` server, but the code is not open source. Only vetted server owners that will not violate the license terms will be given access. Contact the repo owner for more information.~~
|
||||
|
||||
The async server is now open source under the GPLv2 license. It has superceded the old server in terms of functionality and performance.
|
||||
|
||||
Inspiration: [Lost-MSth/Arcaea-server](https://github.com/Lost-MSth/Arcaea-server)
|
||||
|
||||
@@ -65,7 +67,7 @@ The server owner must install the update on their instance. They can download it
|
||||
|
||||
- Python
|
||||
|
||||
- Flask
|
||||
- ~~Flask~~ `Starlette`
|
||||
|
||||
- Crypto (pycryptodome)
|
||||
|
||||
@@ -99,13 +101,13 @@ Note that MAC uses `python3`. Code examples in this document will use the defaul
|
||||
|
||||
Open command on Windows (MAC open terminal). Type `ipconfig` (MAC `ifconfig`), and obtain your IPV4 address. This assumes that you are connected to a WIFI, and it should start with 192 or 172.
|
||||
|
||||
Open the `config_old.py` of the private server, and change the `IP` accordingly.
|
||||
Open the `config.env` of the private server, and change the `IP` accordingly.
|
||||
|
||||
Type `cmd` in the file directory on the top of the file explorer, and press enter. A command prompt will be opened for that directory.
|
||||
|
||||
Type `pip install -r requirements.txt` to install all the dependencies.
|
||||
|
||||
Type `python 7001.py` to start the server. If an error pops up, resolve it now – did you install all the dependencies? Is the IP correct?
|
||||
Type `python 7002.py` to start the server. If an error pops up, resolve it now – did you install all the dependencies? Is the IP correct?
|
||||
|
||||
### Android (Harder)
|
||||
|
||||
@@ -125,7 +127,7 @@ Use
|
||||
|
||||
`pip install ...`
|
||||
|
||||
to install `rust`, `flask`, `passlib`, `pycryptodome`, `requests`.
|
||||
to install `rust`, `starlette`, `passlib`, `pycryptodome`, `requests`.
|
||||
|
||||
If ssl errors pop up, you might need to ``pkg up ssl -y``.
|
||||
|
||||
@@ -135,7 +137,7 @@ change `config.py`'s `IP` to `127.0.0.1` (this is `loopback`. Feel free to use y
|
||||
|
||||
`cd storage/shared/.... (server location on android file system)`
|
||||
|
||||
`python 7001.py` to start the server.
|
||||
`python 7002.py` to start the server.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -263,7 +265,9 @@ Note that this data is for analytics only, and the functionality to embed this d
|
||||
|
||||
你应对因运行本服务器而产生的任何潜在后果承担全部责任。如果您不同意这些要求,则不允许您复制或运行该程序。
|
||||
|
||||
此服务器仅为**本地**运行设计,鉴于Flask糟糕的并发性能。一个高效,`异步`的服务器可供使用,不过代码并非开源。只有经过审核,不会违反许可条款的服务器所有者才能获得访问权限。请联系repo所有者了解更多信息。
|
||||
~~此服务器仅为**本地**运行设计,鉴于Flask糟糕的并发性能。一个高效,`异步`的服务器可供使用,不过代码并非开源。只有经过审核,不会违反许可条款的服务器所有者才能获得访问权限。请联系repo所有者了解更多信息。~~
|
||||
|
||||
基于`Starlette`的异步服务器已经在功能和性能上超越了老服务器,现在以`GPLv2`许可证开源。
|
||||
|
||||
灵感: [Lost-MSth/Arcaea-server](https://github.com/Lost-MSth/Arcaea-server)
|
||||
|
||||
@@ -313,7 +317,7 @@ Note that this data is for analytics only, and the functionality to embed this d
|
||||
|
||||
- Python
|
||||
|
||||
- Flask
|
||||
- ~~Flask~~`Starlette`
|
||||
|
||||
- Crypto (pycryptodome)
|
||||
|
||||
@@ -348,11 +352,11 @@ PC/MAC安装 `python`,安装 `pip`。
|
||||
|
||||
PC打开 `cmd` 输入 `ipconfig`。MAC 打开 `terminal` 输入 `ifconfig`。获得你的`IPV4`,一串为192或172开头的数字。
|
||||
|
||||
PC用文本编辑器打开服务器文件夹的 `config.py`,将`IPV4`填写至`IP`。`PORT`(端口)也可以更改。
|
||||
PC用文本编辑器打开服务器文件夹的 `config.env`,将`IPV4`填写至`IP`。`PORT`(端口)也可以更改。
|
||||
|
||||
文件管理器上方的文件夹路径清空,输入 `cmd`。命令行窗口会弹出。
|
||||
|
||||
输入 `python 7001.py`来开启服务器。如果出现错误,就解决他们吧。检查依赖项是否安装,网络配置是否正确。
|
||||
输入 `python 7002.py`来开启服务器。如果出现错误,就解决他们吧。检查依赖项是否安装,网络配置是否正确。
|
||||
|
||||
### 安卓(稍难)
|
||||
|
||||
@@ -372,7 +376,7 @@ PC用文本编辑器打开服务器文件夹的 `config.py`,将`IPV4`填写至
|
||||
|
||||
`pip install ...`
|
||||
|
||||
来安装 `rust`, `flask`, `bcrypt`, `pycryptodome`, `requests`.
|
||||
来安装 `rust`, `starlette`, `bcrypt`, `pycryptodome`, `requests`.
|
||||
|
||||
如果出现ssl问题,可能需要``pkg up ssl -y``.
|
||||
|
||||
@@ -382,7 +386,7 @@ PC用文本编辑器打开服务器文件夹的 `config.py`,将`IPV4`填写至
|
||||
|
||||
`cd storage/shared/.... (服务器在安卓文件系统的位置)`
|
||||
|
||||
`python 7001.py` 来运行服务器。
|
||||
`python 7002.py` 来运行服务器。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -516,8 +520,11 @@ server/
|
||||
│ │ └─ title
|
||||
│ ├─ web/ (found in common.zip)
|
||||
│ │ └─ webpage assets
|
||||
├─ 7001.py (main script)
|
||||
├─ 7002.py (main script)
|
||||
├─ api/ (API scripts)
|
||||
│ ├─ config/ (various configuration files)
|
||||
├─ getCrypt.py (debug purpose only)
|
||||
├─ old_server (Flask old server (depricated))
|
||||
└─ config.py (configuration script)
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from Crypto.Cipher import AES
|
||||
import re
|
||||
import urllib.parse
|
||||
from starlette.requests import Request
|
||||
|
||||
# Found in: aesManager::initialize()
|
||||
# Used for: Crypting parameter bytes sent by client
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
AES_CBC_KEY = b"oLxvgCJjMzYijWIldgKLpUx5qhUhguP1"
|
||||
|
||||
# Found in: aesManager::decryptCBC() and aesManager::encryptCBC()
|
||||
# Used for: Crypting parameter bytes sent by client
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
AES_CBC_IV = b"6NrjyFU04IO9j9Yo"
|
||||
|
||||
# Decrypt AES encrypted data, takes in a hex string
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
def decryptAES(data, key=AES_CBC_KEY, iv=AES_CBC_IV):
|
||||
return AES.new(key, AES.MODE_CBC, iv).decrypt(bytes.fromhex(data))
|
||||
|
||||
# Encrypt data with AES, takes in a bytes object
|
||||
# Credit: https://github.com/Walter-o/gcm-downloader
|
||||
def encryptAES(data, key=AES_CBC_KEY, iv=AES_CBC_IV):
|
||||
while len(data) % 16 != 0:
|
||||
data += b"\x00"
|
||||
encryptedData = AES.new(key, AES.MODE_CBC, iv).encrypt(data)
|
||||
return encryptedData.hex()
|
||||
|
||||
async def decrypt_fields(request: Request):
|
||||
url = str(request.url)
|
||||
match = re.search(r'\?(.*)', url)
|
||||
if match:
|
||||
original_field = match.group(1)
|
||||
decrypted_fields = urllib.parse.parse_qs(decryptAES(match.group(1))[:-1])
|
||||
|
||||
return decrypted_fields, original_field
|
||||
else:
|
||||
return None, None
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
from starlette.responses import JSONResponse, Response
|
||||
from starlette.requests import Request
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy import Table, Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from sqlalchemy import select, update
|
||||
|
||||
import os
|
||||
import databases
|
||||
import datetime
|
||||
|
||||
DB_NAME = "player.db"
|
||||
DB_PATH = os.path.join(os.getcwd(), DB_NAME)
|
||||
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"
|
||||
|
||||
database = databases.Database(DATABASE_URL)
|
||||
metadata = sqlalchemy.MetaData()
|
||||
|
||||
user = Table(
|
||||
"user",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("username", String(20), unique=True, nullable=False),
|
||||
Column("password_hash", String(255), nullable=False),
|
||||
Column("device_id", String(512)),
|
||||
Column("data", String, nullable=True),
|
||||
Column("save_id", String, nullable=True),
|
||||
Column("crc", Integer, nullable=True),
|
||||
Column("timestamp", DateTime, default=datetime.datetime.utcnow),
|
||||
Column("coin_mp", Integer, default=1),
|
||||
)
|
||||
|
||||
daily_reward = Table(
|
||||
"daily_reward",
|
||||
metadata,
|
||||
Column("id", Integer, primary_key=True, autoincrement=True),
|
||||
Column("device_id", String(512)),
|
||||
Column("timestamp", DateTime, default=datetime.datetime.utcnow),
|
||||
Column("my_stage", String),
|
||||
Column("my_avatar", String),
|
||||
Column("item", String),
|
||||
Column("day", Integer),
|
||||
Column("coin", Integer),
|
||||
Column("lvl", Integer),
|
||||
Column("title", Integer),
|
||||
Column("avatar", Integer),
|
||||
)
|
||||
|
||||
result = Table(
|
||||
"result",
|
||||
metadata,
|
||||
Column("rid", Integer, primary_key=True, autoincrement=True),
|
||||
Column("vid", String(512), nullable=False),
|
||||
Column("tid", String(512), nullable=False),
|
||||
Column("sid", String(512), nullable=False),
|
||||
Column("stts", String(64)),
|
||||
Column("id", String(8)),
|
||||
Column("mode", String(4)),
|
||||
Column("avatar", String(4)),
|
||||
Column("score", String(16)),
|
||||
Column("high_score", String(128)),
|
||||
Column("play_rslt", String(128)),
|
||||
Column("item", String(16)),
|
||||
Column("os", String(16)),
|
||||
Column("os_ver", String(16)),
|
||||
Column("ver", String(16)),
|
||||
Column("mike", String(8)),
|
||||
|
||||
)
|
||||
|
||||
whitelist = Table(
|
||||
"whitelist",
|
||||
metadata,
|
||||
Column("id", String(512), primary_key=True)
|
||||
)
|
||||
blacklist = Table(
|
||||
"blacklist",
|
||||
metadata,
|
||||
Column("id", String(512), primary_key=True),
|
||||
Column("reason", String(256))
|
||||
)
|
||||
|
||||
async def init_db():
|
||||
|
||||
if not os.path.exists(DB_PATH):
|
||||
print("[DB] Creating new database:", DB_PATH)
|
||||
|
||||
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(metadata.create_all)
|
||||
|
||||
await engine.dispose()
|
||||
print("[DB] Database initialized successfully.")
|
||||
|
||||
async def get_user_data(uid, data_field):
|
||||
query = select(user.c[data_field]).where(user.c.device_id == uid[b'vid'][0].decode())
|
||||
async with database.transaction():
|
||||
result = await database.fetch_one(query)
|
||||
return result[data_field] if result else None
|
||||
|
||||
async def set_user_data(uid, data_field, new_data):
|
||||
query = (
|
||||
update(user)
|
||||
.where(user.c.device_id == uid[b'vid'][0].decode())
|
||||
.values({data_field: new_data})
|
||||
)
|
||||
async with database.transaction():
|
||||
await database.execute(query)
|
||||
|
||||
async def check_whitelist(uid):
|
||||
query = select(whitelist.c.id).where(whitelist.c.id == uid[b'vid'][0].decode())
|
||||
async with database.transaction():
|
||||
result = await database.fetch_one(query)
|
||||
return result is not None
|
||||
|
||||
async def check_blacklist(uid):
|
||||
device_id = uid[b'vid'][0].decode()
|
||||
user_data = await get_user_data(uid, "username")
|
||||
username = user_data[0] if user_data else None
|
||||
|
||||
query = select(blacklist.c.id).where(
|
||||
(blacklist.c.id == device_id) | (blacklist.c.id == username)
|
||||
)
|
||||
async with database.transaction():
|
||||
result = await database.fetch_one(query)
|
||||
return result is None
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import requests
|
||||
import json
|
||||
import binascii
|
||||
import bcrypt
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from config import MODEL, TUNEFILE, SKIN
|
||||
|
||||
FMAX_VER = None
|
||||
FMAX_RES = None
|
||||
|
||||
def get_4max_version_string():
|
||||
url = "https://studio.code.org/v3/sources/3-aKHy16Y5XaAPXQHI95RnFOKlyYT2O95ia2HN2jKIs/main.json"
|
||||
global FMAX_VER
|
||||
try:
|
||||
with open("./files/4max_ver.txt", 'r') as file:
|
||||
FMAX_VER = file.read().strip()
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred when loading files/4max_ver.txt: {e}")
|
||||
|
||||
def fetch():
|
||||
global FMAX_RES
|
||||
try:
|
||||
response = requests.get(url)
|
||||
if 200 <= response.status_code <= 207:
|
||||
try:
|
||||
FMAX_RES = json.loads(json.loads(response.text)['source'])
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
FMAX_RES = 500
|
||||
else:
|
||||
FMAX_RES = response.status_code
|
||||
except requests.RequestException:
|
||||
FMAX_RES = 400
|
||||
|
||||
fetch()
|
||||
|
||||
def parse_res(res):
|
||||
parsed_data = []
|
||||
if isinstance(res, int):
|
||||
return "Failed to fetch version info: Error " + str(res)
|
||||
|
||||
for item in res:
|
||||
if item.get("isOpen"):
|
||||
version = item.get("version", "Unknown Version")
|
||||
changelog = "<br>".join(item.get("changeLog", {}).get("en", []))
|
||||
parsed_data.append(f"<strong>Version: {version}</strong><p><strong>Changelog:</strong><br>{changelog}</p>")
|
||||
return "".join(parsed_data)
|
||||
|
||||
def crc32_decimal(data):
|
||||
crc32_hex = binascii.crc32(data.encode())
|
||||
return int(crc32_hex & 0xFFFFFFFF)
|
||||
|
||||
def hash_password(password):
|
||||
salt = bcrypt.gensalt()
|
||||
hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt)
|
||||
return hashed_password
|
||||
|
||||
def verify_password(password, hashed_password):
|
||||
if type(hashed_password) == str:
|
||||
hashed_password = hashed_password.encode('utf-8')
|
||||
return bcrypt.checkpw(password.encode('utf-8'), hashed_password)
|
||||
|
||||
def is_alphanumeric(username):
|
||||
pattern = r"^[a-zA-Z0-9]+$"
|
||||
return bool(re.match(pattern, username))
|
||||
|
||||
def get_model_pak(host):
|
||||
mid = ET.Element("model_pak")
|
||||
rid = ET.Element("date")
|
||||
uid = ET.Element("url")
|
||||
rid.text = MODEL
|
||||
uid.text = host + "files/gc2/model" + MODEL + ".pak"
|
||||
mid.append(rid)
|
||||
mid.append(uid)
|
||||
return mid
|
||||
|
||||
def get_tune_pak(host):
|
||||
mid = ET.Element("tuneFile_pak")
|
||||
rid = ET.Element("date")
|
||||
uid = ET.Element("url")
|
||||
rid.text = TUNEFILE
|
||||
uid.text = host + "files/gc2/tuneFile" + TUNEFILE + ".pak"
|
||||
mid.append(rid)
|
||||
mid.append(uid)
|
||||
return mid
|
||||
|
||||
def get_skin_pak(host):
|
||||
mid = ET.Element("skin_pak")
|
||||
rid = ET.Element("date")
|
||||
uid = ET.Element("url")
|
||||
rid.text = SKIN
|
||||
uid.text = host + "files/gc2/skin" + SKIN + ".pak"
|
||||
mid.append(rid)
|
||||
mid.append(uid)
|
||||
return mid
|
||||
|
||||
def get_m4a_path(host):
|
||||
mid = ET.Element("m4a_path")
|
||||
mid.text = host + "files/gc2/audio/"
|
||||
return mid
|
||||
|
||||
def get_stage_path(host):
|
||||
mid = ET.Element("stage_path")
|
||||
mid.text = host + "files/gc2/stage/"
|
||||
return mid
|
||||
|
||||
def get_stage_zero():
|
||||
sid = ET.Element("my_stage")
|
||||
did = ET.Element("stage_id")
|
||||
cid = ET.Element("ac_mode")
|
||||
did.text = "0"
|
||||
cid.text = "0"
|
||||
sid.append(did)
|
||||
sid.append(cid)
|
||||
return sid
|
||||
|
||||
def inform_page(text, mode):
|
||||
if mode == 0:
|
||||
mode = "/files/web/ttl_taitoid.png"
|
||||
elif mode == 1:
|
||||
mode = "/files/web/ttl_information.png"
|
||||
elif mode == 2:
|
||||
mode = "/files/web/ttl_buy.png"
|
||||
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)
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
from starlette.responses import HTMLResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
import os
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from config import AUTHORIZATION_NEEDED
|
||||
|
||||
from api.database import database, user, result, daily_reward, check_blacklist, check_whitelist
|
||||
from api.crypt import decrypt_fields, encryptAES
|
||||
from api.templates import EXP_UNLOCKED_SONGS, TITLE_LISTS, SONG_LIST
|
||||
from api.misc import inform_page
|
||||
|
||||
async def ranking(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
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()
|
||||
|
||||
html = "<ul class='song-list'>"
|
||||
encrypted_mass = encryptAES(("vid=" + device_id + "&song_id=-1&mode=1&dummy=").encode("utf-8"))
|
||||
href = f"/ranking_detail.php?{encrypted_mass}"
|
||||
html += f'''
|
||||
<li class="song-item">
|
||||
<a href="{href}" class="song-button">Total Score</a>
|
||||
</li>
|
||||
'''
|
||||
for index, song in enumerate(SONG_LIST):
|
||||
encrypted_mass = encryptAES(("vid=" + device_id + "&song_id=" + str(index) + "&mode=3&dummy=").encode("utf-8"))
|
||||
song_name = song.get("name_en", "Unknown")
|
||||
href = f"/ranking_detail.php?{encrypted_mass}"
|
||||
html += f'''
|
||||
<li class="song-item">
|
||||
<a href="{href}" class="song-button">{song_name}</a>
|
||||
</li>
|
||||
'''
|
||||
html += "</ul>"
|
||||
|
||||
file_path = os.path.join("files", "ranking.html")
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(text=html)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse("""<html><body><h1>Ranking file not found</h1></body></html>""", status_code=500)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
else:
|
||||
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
|
||||
|
||||
async def ranking_detail(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
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()
|
||||
song_id = int(decrypted_fields[b'song_id'][0].decode())
|
||||
mode = int(decrypted_fields[b'mode'][0].decode())
|
||||
button_labels = []
|
||||
difficulty_levels = []
|
||||
song_name = ""
|
||||
if (song_id == -1):
|
||||
song_name = "Total Score"
|
||||
difficulty_levels = []
|
||||
button_labels = ["All", "Mobile", "Arcade"]
|
||||
else:
|
||||
song_name = SONG_LIST[song_id]["name_en"]
|
||||
difficulty_levels = SONG_LIST[song_id]["difficulty_levels"]
|
||||
button_labels = ["Easy", "Normal", "Hard"]
|
||||
|
||||
html = f"""<div style="text-align: center; font-size: 36px; margin-bottom: 20px;">{song_name}</div>"""
|
||||
|
||||
button_modes = [1, 2, 3]
|
||||
print(len(difficulty_levels))
|
||||
|
||||
if (len(difficulty_levels) == 6):
|
||||
button_labels.extend(["AC-Easy", "AC-Normal", "AC-Hard"])
|
||||
button_modes.extend([11, 12, 13])
|
||||
|
||||
if song_id > 615:
|
||||
button_modes = [x for x in button_modes if x not in [1, 2, 3]]
|
||||
button_labels = [x for x in button_labels if x not in ["Easy", "Normal", "Hard"]]
|
||||
|
||||
row_start = '<div class="button-row">'
|
||||
row_end = '</div>'
|
||||
row_content = []
|
||||
|
||||
for i, (label, mode_value) in enumerate(zip(button_labels, button_modes)):
|
||||
if mode_value == mode:
|
||||
row_content.append(f"""<div class="bt_bg01_ac">{label}</div>""")
|
||||
else:
|
||||
encrypted_mass = encryptAES(("vid=" + device_id + "&song_id=" + str(song_id) + "&mode=" + str(mode_value) + "&dummy=").encode("utf-8"))
|
||||
row_content.append(f"""<a href="/ranking_detail.php?{encrypted_mass}" class="bt_bg01_xnarrow">{label}</a>""")
|
||||
|
||||
if len(row_content) == 3:
|
||||
html += row_start + ''.join(row_content) + row_end
|
||||
row_content = [] # Reset row content
|
||||
|
||||
play_results = None
|
||||
user_result = None
|
||||
device_result = None
|
||||
|
||||
if (song_id == -1):
|
||||
# Filter out the mobile/AC modes
|
||||
if (mode == 1):
|
||||
exclude = []
|
||||
elif mode == 2:
|
||||
exclude = [11, 12, 13]
|
||||
else:
|
||||
exclude = [1, 2, 3]
|
||||
|
||||
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(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(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(user).where(user.c.device_id == device_id)
|
||||
cur_user = await database.fetch_one(query)
|
||||
|
||||
player_scores = {}
|
||||
|
||||
filtered_play_results = [play for play in play_results if int(play[2]) not in exclude]
|
||||
|
||||
for play in filtered_play_results:
|
||||
did = play[0]
|
||||
sid = play[1]
|
||||
avatar = play[3]
|
||||
score = play[4]
|
||||
username, title = None, None
|
||||
|
||||
if sid:
|
||||
sid = int(sid)
|
||||
if sid in user_results:
|
||||
username = user_results[sid]["username"]
|
||||
did = user_results[sid]["device_id"]
|
||||
else: # Guest
|
||||
username = f"Guest({did[-6:]})"
|
||||
|
||||
# title is device-specific
|
||||
title = device_results.get(did, {}).get("title", "1")
|
||||
|
||||
if username in player_scores:
|
||||
player_scores[username]["score"] += int(score)
|
||||
player_scores[username]["avatar"] = avatar # But avatar is based on latest play submission
|
||||
player_scores[username]["title"] = title
|
||||
else:
|
||||
player_scores[username] = {"score": int(score), "avatar": avatar, "title": title}
|
||||
|
||||
sorted_players = sorted(player_scores.items(), key=lambda x: x[1]["score"], reverse=True)
|
||||
|
||||
username = cur_user[1] if cur_user else f"Guest({device_id[-6:]})"
|
||||
|
||||
player_rank = None
|
||||
user_score = 0
|
||||
avatar = "1"
|
||||
title = "1"
|
||||
|
||||
for rank, (player_name, data) in enumerate(sorted_players, start=1):
|
||||
if player_name == username:
|
||||
player_rank = rank
|
||||
user_score = data["score"]
|
||||
avatar = data["avatar"]
|
||||
title = data["title"]
|
||||
break
|
||||
|
||||
if player_rank is None:
|
||||
device_data = next((device for device in device_results if device[1] == device_id), None)
|
||||
if device_data:
|
||||
avatar = device_data["avatar"]
|
||||
title = device_data["title"]
|
||||
|
||||
html += f"""
|
||||
<div class="player-element">
|
||||
<span class="rank">You<br>{"#" + str(player_rank) if player_rank else "N/A"}</span>
|
||||
<img src="/files/image/icon/avatar/{avatar}.png" class="avatar" alt="Player Avatar">
|
||||
<div class="player-info">
|
||||
<div class="name">{username}</div>
|
||||
<img src="/files/image/title/{title}.png" class="title" alt="Player Title">
|
||||
</div>
|
||||
<div class="player-score">{user_score}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
<div class="leaderboard-container">
|
||||
"""
|
||||
# Loop leaderboard
|
||||
for rank, (username, data) in enumerate(sorted_players, start=1):
|
||||
html += f"""
|
||||
<div class="leaderboard-player">
|
||||
<div class="rank">#{rank}</div>
|
||||
<img class="avatar" src="/files/image/icon/avatar/{data['avatar']}.png" alt="Avatar">
|
||||
<div class="leaderboard-info">
|
||||
<div class="name">{username}</div>
|
||||
<div class="title"><img src="/files/image/title/{data['title']}.png" alt="Title"></div>
|
||||
</div>
|
||||
<div class="leaderboard-score">{data['score']}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
else:
|
||||
query = select(result).where(
|
||||
(result.c.id == song_id) & (result.c.mode == mode)
|
||||
).order_by(result.c.score.desc())
|
||||
play_results = await database.fetch_all(query)
|
||||
|
||||
query = select(user).where(user.c.device_id == device_id)
|
||||
user_result = await 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 = next((record for record in play_results if int(record[3]) == user_id), None)
|
||||
|
||||
if not play_record:
|
||||
play_record = next((record for record in play_results if record[1] == device_id and record[3] is None), None)
|
||||
|
||||
player_rank = None
|
||||
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 int(result_obj[3]) == user_id:
|
||||
player_rank = rank
|
||||
break
|
||||
elif result_obj[1] == device_id and result_obj[3] is None:
|
||||
player_rank = rank
|
||||
break
|
||||
|
||||
html += f"""
|
||||
<div class="player-element">
|
||||
<span class="rank">You<br>{"#" + str(player_rank) if player_rank else "N/A"}</span>
|
||||
<img src="/files/image/icon/avatar/{avatar_index}.png" class="avatar" alt="Player Avatar">
|
||||
<div class="player-info">
|
||||
<div class="name">{username}</div>
|
||||
<img src="/files/image/title/{device_result[9]}.png" class="title" alt="Player Title">
|
||||
</div>
|
||||
<div class="player-score">{user_score}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
<div class="leaderboard-container">
|
||||
"""
|
||||
|
||||
for rank, record in enumerate(play_results, start=1):
|
||||
username = f"Guest({record[1][-6:]})"
|
||||
device_info = None
|
||||
if record[3]:
|
||||
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(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:
|
||||
device_info = "1"
|
||||
|
||||
avatar_id = record[7] if record[7] else 1
|
||||
avatar_url = f"/files/image/icon/avatar/{avatar_id}.png"
|
||||
|
||||
score = record[8]
|
||||
|
||||
html += f"""
|
||||
<div class="leaderboard-player">
|
||||
<div class="rank">#{rank}</div>
|
||||
<img class="avatar" src="{avatar_url}" alt="Avatar">
|
||||
<div class="leaderboard-info">
|
||||
<div class="name">{username}</div>
|
||||
<div class="title"><img src="/files/image/title/{device_info}.png" alt="Title"></div>
|
||||
</div>
|
||||
<div class="leaderboard-score">{score}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += "</div>"
|
||||
|
||||
encrypted_mass = encryptAES(("vid=" + device_id + "&dummy=").encode("utf-8"))
|
||||
html += f"""
|
||||
<a href="/ranking.php?{encrypted_mass}" class="bt_bg01" style="margin: 20px auto; display: block; text-align: center;">
|
||||
Go Back
|
||||
</a>
|
||||
"""
|
||||
|
||||
file_path = os.path.join("files", "ranking.html")
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(text=html)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse("""<html><body><h1>Ranking file not found</h1></body></html>""", status_code=500)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
else:
|
||||
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
|
||||
|
||||
async def status(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
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()
|
||||
set_title = int(decrypted_fields[b'set_title'][0].decode()) if b'set_title' in decrypted_fields else None
|
||||
page_id = int(decrypted_fields[b'page_id'][0].decode()) if b'page_id' in decrypted_fields else 0
|
||||
|
||||
if set_title:
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(title=set_title)
|
||||
)
|
||||
await database.execute(update_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(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"]
|
||||
|
||||
html = ""
|
||||
if user_data:
|
||||
player_element = f"""
|
||||
<div class="player-element">
|
||||
<img src="/files/image/icon/avatar/{user_data['avatar']}.png" class="avatar" alt="Player Avatar">
|
||||
<div class="player-info">
|
||||
<div class="name">{user_name}</div>
|
||||
<img src="/files/image/title/{user_data['title']}.png" class="title" alt="Player Title">
|
||||
</div>
|
||||
<div class="player-score">Level {user_data['lvl']}</div>
|
||||
</div>
|
||||
"""
|
||||
html += player_element
|
||||
|
||||
page_name = ["Special", "Normal", "Master", "God"]
|
||||
buttons_html = '<div class="button-row">'
|
||||
for i, name in enumerate(page_name):
|
||||
if i == page_id:
|
||||
buttons_html += f"""
|
||||
<div class="bt_bg01_ac">{name}</div>
|
||||
"""
|
||||
else:
|
||||
encrypted_mass = encryptAES(f"vid={device_id}&page_id={i}&dummy=".encode("utf-8"))
|
||||
buttons_html += f"""
|
||||
<a href="/status.php?{encrypted_mass}" class="bt_bg01_xnarrow">{name}</a>
|
||||
"""
|
||||
buttons_html += '</div>'
|
||||
html += f"<div style='text-align: center; margin-top: 20px;'>{buttons_html}</div>"
|
||||
|
||||
selected_titles = TITLE_LISTS.get(page_id, [])
|
||||
titles_html = '<div class="title-list">'
|
||||
for index, num in enumerate(selected_titles):
|
||||
if index % 2 == 0:
|
||||
if index != 0:
|
||||
titles_html += '</div>'
|
||||
titles_html += '<div class="title-row">'
|
||||
|
||||
if num == user_data["title"]:
|
||||
titles_html += f"""
|
||||
<img src="/files/image/title/{num}.png" alt="Title {num}" class="title-image-selected">
|
||||
"""
|
||||
else:
|
||||
encrypted_mass = encryptAES(f"vid={device_id}&title_id={num}&page_id={page_id}&dummy=".encode("utf-8"))
|
||||
titles_html += f"""
|
||||
<a href="/set_title.php?{encrypted_mass}" class="title-link">
|
||||
<img src="/files/image/title/{num}.png" alt="Title {num}" class="title-image">
|
||||
</a>
|
||||
"""
|
||||
titles_html += '</div></div>'
|
||||
html += titles_html
|
||||
|
||||
file_path = os.path.join("files", "status.html")
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(text=html)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse("""<html><body><h1>Status file not found</h1></body></html>""", status_code=500)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
else:
|
||||
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
|
||||
|
||||
async def set_title(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
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()
|
||||
page_id = decrypted_fields[b'page_id'][0].decode()
|
||||
title_id = decrypted_fields[b'title_id'][0].decode()
|
||||
current_title = 1
|
||||
|
||||
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"]
|
||||
|
||||
confirm_url = encryptAES(
|
||||
f"vid={device_id}&page_id={page_id}&set_title={title_id}&dummy=".encode("utf-8")
|
||||
)
|
||||
go_back_url = encryptAES(
|
||||
f"vid={device_id}&page_id={page_id}&dummy=".encode("utf-8")
|
||||
)
|
||||
|
||||
html = f"""
|
||||
<p>Would you like to change your title?<br>Current Title:</p>
|
||||
<img src="/files/image/title/{current_title}.png" alt="Current Title" class="title-image">
|
||||
<p>New Title:</p>
|
||||
<img src="/files/image/title/{title_id}.png" alt="New Title" class="title-image">
|
||||
<div class="button-container">
|
||||
<a href="/status.php?{confirm_url}" class="bt_bg01">Confirm</a>
|
||||
<a href="/status.php?{go_back_url}" class="bt_bg01">Go back</a>
|
||||
</div>
|
||||
"""
|
||||
return HTMLResponse(inform_page(html, 1))
|
||||
|
||||
else:
|
||||
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
|
||||
|
||||
async def mission(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
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'>"""
|
||||
|
||||
for song in EXP_UNLOCKED_SONGS:
|
||||
song_id = song["id"]
|
||||
level_required = song["lvl"]
|
||||
song_name = SONG_LIST[song_id]["name_en"] if song_id < len(SONG_LIST) else "Unknown Song"
|
||||
|
||||
html += f"""
|
||||
<div class="mission-row">
|
||||
<div class="mission-level">Level {level_required}</div>
|
||||
<div class="mission-song">{song_name}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += "</div>"
|
||||
file_path = os.path.join("files", "mission.html")
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(text=html)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse("""<html><body><h1>Mission file not found</h1></body></html>""", status_code=500)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
else:
|
||||
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
|
||||
|
||||
routes = [
|
||||
Route('/ranking.php', ranking, methods=['GET']),
|
||||
Route('/ranking_detail.php', ranking_detail, methods=['GET']),
|
||||
Route('/set_title.php', set_title, methods=['GET']),
|
||||
Route('/mission.php', mission, methods=['GET']),
|
||||
Route('/status.php', status, methods=['GET']),
|
||||
]
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
from starlette.responses import Response, FileResponse, HTMLResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
import os
|
||||
import json
|
||||
import math
|
||||
from sqlalchemy import select, update
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from config import START_COIN, AUTHORIZATION_NEEDED, STAGE_PRICE, START_COIN, AVATAR_PRICE, ITEM_PRICE
|
||||
|
||||
from api.crypt import decrypt_fields
|
||||
from api.misc import inform_page, parse_res, FMAX_VER, FMAX_RES
|
||||
from api.database import database, daily_reward, check_blacklist, check_whitelist
|
||||
from api.templates import START_AVATARS, START_STAGES, EXCLUDE_STAGE_EXP, SONG_LIST, AVATAR_LIST, ITEM_LIST
|
||||
|
||||
async def web_shop(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
if should_serve:
|
||||
cnt_type = decrypted_fields[b'cnt_type'][0].decode()
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
if (decrypted_fields.get(b'page')):
|
||||
page = int(decrypted_fields.get(b'page')[0].decode())
|
||||
else:
|
||||
page = 0
|
||||
|
||||
inc = 0
|
||||
fmax_inc = 0
|
||||
buttons_html = ""
|
||||
spawn_prev_page = False
|
||||
spawn_next_page = False
|
||||
|
||||
query = select(daily_reward.c.my_stage, daily_reward.c.my_avatar, daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
|
||||
result = await database.fetch_one(query)
|
||||
|
||||
my_stage = set(json.loads(result["my_stage"])) if result and result["my_stage"] else START_STAGES
|
||||
my_avatar = set(json.loads(result["my_avatar"])) if result and result["my_avatar"] else START_AVATARS
|
||||
coin = result["coin"] if result and result["coin"] else START_COIN
|
||||
|
||||
if cnt_type == "1":
|
||||
low_range = 100
|
||||
up_range = 616
|
||||
|
||||
if page < 0 or page > math.ceil(up_range - low_range) / 80:
|
||||
return HTMLResponse("""<html><body><h1>Invalid page number</h1></body></html>""", status_code=400)
|
||||
|
||||
if page > 0:
|
||||
spawn_prev_page = True
|
||||
if page < math.ceil(up_range - low_range) / 80 - 1:
|
||||
spawn_next_page = True
|
||||
|
||||
low_range = low_range + page * 80
|
||||
up_range = min(up_range, low_range + 80)
|
||||
|
||||
if 700 not in my_stage and os.path.isfile('./files/dlc_4max.html'):
|
||||
buttons_html += """
|
||||
<a href="wwic://web_shop_detail?&cnt_type=1&cnt_id=-1">
|
||||
<img src="/files/web/dlc_4max.jpg" style="width: 84%; margin-bottom: 20px; margin-top: -100px;" />
|
||||
</a><br>
|
||||
"""
|
||||
fmax_inc = 1
|
||||
elif 700 in my_stage and os.path.isfile('./files/dlc_4max.html'):
|
||||
buttons_html += """
|
||||
<a href="wwic://web_shop_detail?&cnt_type=1&cnt_id=-2">
|
||||
<img src="/files/web/dlc_4max.jpg" style="width: 84%; margin-bottom: 20px; margin-top: -100px;" />
|
||||
</a><br>
|
||||
"""
|
||||
|
||||
for i in range(low_range, up_range):
|
||||
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}'">
|
||||
</button>
|
||||
"""
|
||||
inc += 1
|
||||
if inc % 4 == 0:
|
||||
buttons_html += "<br>"
|
||||
|
||||
if spawn_prev_page:
|
||||
buttons_html += """<br>
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="window.location.href='wwic://web_shop?&cnt_type=1&page={}'">
|
||||
Prev Page
|
||||
</button>
|
||||
""".format(page - 1)
|
||||
|
||||
if spawn_next_page:
|
||||
buttons_html += """<br>
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="window.location.href='wwic://web_shop?&cnt_type=1&page={}'">
|
||||
Next Page
|
||||
</button>
|
||||
""".format(page + 1)
|
||||
|
||||
elif cnt_type == "2":
|
||||
low_range = 15
|
||||
up_range = 173 if FMAX_VER == 0 else 267
|
||||
|
||||
if page < 0 or page > math.ceil(up_range - low_range) / 80:
|
||||
return HTMLResponse("""<html><body><h1>Invalid page number</h1></body></html>""", status_code=400)
|
||||
|
||||
if page > 0:
|
||||
spawn_prev_page = True
|
||||
if page < math.ceil(up_range - low_range) / 80 - 1:
|
||||
spawn_next_page = True
|
||||
|
||||
low_range = low_range + page * 80
|
||||
up_range = min(up_range, low_range + 80)
|
||||
|
||||
for i in range(low_range, up_range):
|
||||
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}'">
|
||||
</button>
|
||||
"""
|
||||
inc += 1
|
||||
if inc % 4 == 0:
|
||||
buttons_html += "<br>"
|
||||
|
||||
if spawn_prev_page:
|
||||
buttons_html += """<br>
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="window.location.href='wwic://web_shop?&cnt_type=2&page={}'">
|
||||
Prev Page
|
||||
</button>
|
||||
""".format(page - 1)
|
||||
|
||||
if spawn_next_page:
|
||||
buttons_html += """<br>
|
||||
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
|
||||
onclick="window.location.href='wwic://web_shop?&cnt_type=2&page={}'">
|
||||
Next Page
|
||||
</button>
|
||||
""".format(page + 1)
|
||||
|
||||
elif cnt_type == "3":
|
||||
for i in range(1, 11):
|
||||
buttons_html += f"""
|
||||
<button style="width: 170px; height: 170px; margin: 10px; background-size: cover; background-image: url('/files/image/icon/item/{i}.png');"
|
||||
onclick="window.location.href='wwic://web_shop_detail?&cnt_type={cnt_type}&cnt_id={i}'">
|
||||
</button>
|
||||
"""
|
||||
if i % 4 == 0:
|
||||
buttons_html += "<br>"
|
||||
|
||||
if inc == 0 and fmax_inc == 0 and cnt_type != "3":
|
||||
buttons_html += """<div>Everything has been purchased!</div>"""
|
||||
|
||||
html_path = f"files/web_shop_{cnt_type}.html"
|
||||
try:
|
||||
with open(html_path, "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(text=buttons_html, coin=coin)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse("""<html><body><h1>Shop template not found</h1></body></html>""", status_code=500)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
else:
|
||||
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
|
||||
|
||||
async def web_shop_detail(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
if should_serve:
|
||||
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()
|
||||
|
||||
query = select(daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
|
||||
result = await database.fetch_one(query)
|
||||
coin = result["coin"] if result and result["coin"] else 0
|
||||
html = ""
|
||||
|
||||
if cnt_type == "1":
|
||||
if cnt_id > -1:
|
||||
song = SONG_LIST[cnt_id]
|
||||
difficulty_levels = "/".join(map(str, song.get("difficulty_levels", [])))
|
||||
song_stage_price = STAGE_PRICE * 2 if len(song["difficulty_levels"]) == 6 else STAGE_PRICE
|
||||
html = f"""
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/shop/{cnt_id}.jpg" alt="Item Image" style="width: 180px; height: 180px;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this song?</p>
|
||||
<div>
|
||||
<p>{song.get("name_en")} - {song.get("author_en")}</p>
|
||||
<p>Difficulty Levels: {difficulty_levels}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
<span style="color: #FFFFFF; font-size: 44px; font-family: Hiragino Kaku Gothic ProN, sans-serif;">{song_stage_price}</span>
|
||||
</div>
|
||||
"""
|
||||
elif cnt_id == -2:
|
||||
log = parse_res(FMAX_RES)
|
||||
html = f"""
|
||||
<div class="text-content">
|
||||
<p>You have unlocked the GC4MAX expansion!</p>
|
||||
<p>Please report bugs/missing tracks to Discord: #AnTcfgss, or QQ 3421587952.</p>
|
||||
<button class="quit-button" onclick="window.location.href='wwic://web_shop?&cnt_type=1'">
|
||||
Go Back
|
||||
</button><br>
|
||||
<strong>This server has version {FMAX_VER}.</strong>
|
||||
<p>Update log: </p>
|
||||
<p>{log}<p><br>
|
||||
</div>
|
||||
"""
|
||||
elif cnt_id == -1:
|
||||
html = f"""
|
||||
<div class="text-content">
|
||||
<p>Experience the arcade with the GC4MAX expansion! This DLC unlocks 320+ exclusive songs for your 2OS experience.</p>
|
||||
<p>Note that these songs don't have mobile difficulties. A short placeholder is used, and GCoin reward is not available for playing them. You must clear the Normal difficulty to unlock AC content.</p>
|
||||
<p>Due to technical limitations, Extra level charts cannot be ported as of now. After purchasing, you will have access to support information and update logs.</p>
|
||||
</div>
|
||||
|
||||
<button class="buy-button" onclick="window.location.href='wwic://web_purchase_coin?&cnt_type=1&cnt_id=-1&num=1'">
|
||||
Buy
|
||||
<div class="coin-container">
|
||||
<img src="/files/web/coin_icon.png" alt="Coin Icon" class="coin-icon">
|
||||
<span style="font-size: 22px; font-weight: bold;"> 300</span>
|
||||
</div>
|
||||
</button>
|
||||
<br><br>
|
||||
<button class="quit-button" onclick="window.location.href='wwic://web_shop?&cnt_type=1'">
|
||||
Go Back
|
||||
</button>
|
||||
"""
|
||||
|
||||
elif cnt_type == "2":
|
||||
avatar = next((item for item in AVATAR_LIST if item.get("id") == cnt_id), None)
|
||||
if avatar:
|
||||
html = f"""
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/avatar/{cnt_id}.png" alt="Item Image" style="width: 180px; height: 180px; background-color: black; object-fit: contain;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this avatar?</p>
|
||||
<div>
|
||||
<p>{avatar.get("name")}</p>
|
||||
<p>Effect: {avatar.get("effect")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
<span>{AVATAR_PRICE}</span>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
html = "<p>Avatar not found.</p>"
|
||||
|
||||
elif cnt_type == "3":
|
||||
item = next((item for item in ITEM_LIST if item.get("id") == cnt_id), None)
|
||||
if item:
|
||||
html = f"""
|
||||
<div class="image-container">
|
||||
<img src="/files/image/icon/item/{cnt_id}.png" alt="Item Image" style="width: 180px; height: 180px;" />
|
||||
</div>
|
||||
<p>Would you like to purchase this item?</p>
|
||||
<div>
|
||||
<p>{item.get("name")}</p>
|
||||
<p>Effect: {item.get("effect")}</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
|
||||
<span>{ITEM_PRICE}</span>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
html = "<p>Item not found.</p>"
|
||||
|
||||
if cnt_type == "1" and cnt_id < 0:
|
||||
source_html = f"files/dlc_4max.html"
|
||||
else:
|
||||
source_html = f"files/web_shop_detail.html"
|
||||
html += f"""
|
||||
<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>
|
||||
</div>
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(source_html, "r", encoding="utf-8") as file:
|
||||
html_content = file.read().format(text=html, coin=coin)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse("""<html><body><h1>Shop detail template not found</h1></body></html>""", status_code=500)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
else:
|
||||
return HTMLResponse("""<html><body><h1>Access denied</h1></body></html>""", status_code=403)
|
||||
|
||||
async def buy_by_coin(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<?xml version="1.0" encoding="UTF-8"?><response><code>1</code><result_url>coin_error.php</result_url></response>""", media_type="application/xml")
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
if should_serve:
|
||||
cnt_type = decrypted_fields[b'cnt_type'][0].decode()
|
||||
cnt_id = int(decrypted_fields[b'cnt_id'][0].decode())
|
||||
num = int(decrypted_fields[b'num'][0].decode())
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
|
||||
fail_url = """<?xml version="1.0" encoding="UTF-8"?><response><code>1</code><result_url>coin_error.php</result_url></response>"""
|
||||
|
||||
query = select(daily_reward.c.my_stage, daily_reward.c.my_avatar, daily_reward.c.coin, daily_reward.c.item).where(daily_reward.c.device_id == device_id)
|
||||
result = await database.fetch_one(query)
|
||||
|
||||
if not result:
|
||||
return Response(fail_url, media_type="application/xml")
|
||||
|
||||
my_stage = set(json.loads(result["my_stage"])) if result["my_stage"] else set()
|
||||
my_avatar = set(json.loads(result["my_avatar"])) if result["my_avatar"] else set()
|
||||
coin = int(result["coin"]) if result["coin"] else 0
|
||||
item = json.loads(result["item"]) if result["item"] else []
|
||||
|
||||
if cnt_type == "1":
|
||||
if cnt_id == -1:
|
||||
song_stage_price = 300
|
||||
if coin < song_stage_price:
|
||||
return Response(fail_url, media_type="application/xml")
|
||||
|
||||
for i in range(616, 950):
|
||||
my_stage.add(i)
|
||||
coin -= song_stage_price
|
||||
else:
|
||||
song_stage_price = STAGE_PRICE * 2 if len(SONG_LIST[cnt_id]["difficulty_levels"]) == 6 else STAGE_PRICE
|
||||
if coin < song_stage_price or cnt_id in my_stage:
|
||||
return Response(fail_url, media_type="application/xml")
|
||||
|
||||
coin -= song_stage_price
|
||||
my_stage.add(cnt_id)
|
||||
|
||||
elif cnt_type == "2":
|
||||
if coin < AVATAR_PRICE or cnt_id in my_avatar:
|
||||
return Response(fail_url, media_type="application/xml")
|
||||
|
||||
coin -= AVATAR_PRICE
|
||||
my_avatar.add(cnt_id)
|
||||
|
||||
elif cnt_type == "3":
|
||||
if coin < ITEM_PRICE:
|
||||
return Response(fail_url, media_type="application/xml")
|
||||
|
||||
coin -= ITEM_PRICE
|
||||
item.append(cnt_id)
|
||||
|
||||
else:
|
||||
return Response(fail_url, media_type="application/xml")
|
||||
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(
|
||||
my_stage=json.dumps(list(my_stage)),
|
||||
my_avatar=json.dumps(list(my_avatar)),
|
||||
coin=coin,
|
||||
item=json.dumps(item)
|
||||
)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
response = ET.Element("response")
|
||||
ET.SubElement(response, "code").text = "0"
|
||||
ET.SubElement(response, "result_url").text = "web_shop_result.php"
|
||||
ET.SubElement(response, "cnt_type").text = cnt_type
|
||||
ET.SubElement(response, "cnt_id").text = str(cnt_id)
|
||||
ET.SubElement(response, "num").text = str(num)
|
||||
|
||||
if cnt_type == "1":
|
||||
ET.SubElement(response, "stage_id").text = str(cnt_id)
|
||||
|
||||
response_string = ET.tostring(response, encoding="utf-8", method="xml").decode("utf-8")
|
||||
return Response(response_string, media_type="application/xml")
|
||||
else:
|
||||
return Response("""<?xml version="1.0" encoding="UTF-8"?><response><code>1</code><result_url>coin_error.php</result_url></response>""", media_type="application/xml")
|
||||
|
||||
async def web_shop_result(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
cnt_type = decrypted_fields[b'cnt_type'][0].decode()
|
||||
return HTMLResponse(inform_page(f"""SUCCESS:<br>Purchase successful.<br>Please close this page and the reward will arrive shortly.<br>If it took too long, try restarting the game.<br><a href='wwic://web_shop?cnt_type={cnt_type}' class='bt_bg01' >Go Back</a>""", 2))
|
||||
|
||||
async def coin_error(request: Request):
|
||||
return HTMLResponse(inform_page(f"""FAILED:<br>Either you don't have enough coin,<br>or there were a duplicate order, and the reward will arrive shortly.""", 2))
|
||||
|
||||
|
||||
routes = [
|
||||
Route('/web_shop.php', web_shop, methods=['GET', 'POST']),
|
||||
Route('/web_shop_detail.php', web_shop_detail, methods=['GET', 'POST']),
|
||||
Route('/buy_by_coin.php', buy_by_coin, methods=['GET']),
|
||||
Route('/web_shop_result.php', web_shop_result, methods=['GET']),
|
||||
Route('/coin_error.php', coin_error, methods=['GET']),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
SONG_LIST = []
|
||||
AVATAR_LIST = []
|
||||
ITEM_LIST = []
|
||||
EXP_UNLOCKED_SONGS = []
|
||||
|
||||
START_STAGES = [7,23,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,88,89,90,91,92,93,94,95,96,97,98,99,214]
|
||||
# 214 is tutorial song.
|
||||
|
||||
START_AVATARS = []
|
||||
|
||||
EXCLUDE_STAGE_EXP = [121,134,166,167,168,169,170,213,214,215,225,277,397] # 134 and 170 unoccupied dummy tracks (filled with Departure -Remix-),
|
||||
#121 (and 93-96 lady gaga songs) removed (can be enabled by patching stageParam:isAvailable, or change the last byte before next song's name - 1 from 01 to 03 in stage_param.dat.
|
||||
# Rest are exp unlocked songs.
|
||||
EXCLUDE_AVATAR_EXP = [28,29]
|
||||
|
||||
SPECIAL_TITLES = [1, 2, 4431, 4432, 4601, 4602, 4611, 4612, 4621, 4622, 4631, 4632, 5111, 5112, 5121, 5122, 5131, 5132, 10001, 10002, 20001, 20002, 20003, 20004, 20005, 20006, 30001, 30002, 40001, 40002, 50001, 50002, 60001, 60002, 70001, 70002, 80001, 80002, 90001, 90002, 100001, 100002, 110001, 110002, 120001, 120002, 130001, 130002, 140001, 140002, 140003, 140004, 150001, 150002, 150003, 150004, 160001, 160002, 160003, 160004, 170001, 170002, 170003, 170004, 180001, 180002, 180003, 180004, 190001, 190002, 190003, 190004, 200001, 200002, 200003, 200004, 210001, 210002, 210003, 210004, 210005, 210006, 210007, 210008, 210009, 210010, 210011, 210012, 210013, 210014, 240001, 240002, 240003, 240004, 240005, 240006, 240007, 240008, 240009, 240010, 240011, 240012]
|
||||
|
||||
GOD_TITLES = [220001, 220002, 220003, 220004, 220005, 220006, 220007, 220008, 220009, 220010, 220011, 220012, 220013, 220014, 220015, 220016, 220017, 220018, 220019, 220020, 220021, 220022, 220023, 220024, 220025, 220026, 220027, 220028, 220029, 220030, 220031, 220032, 220033, 220034, 220035, 220036, 220037, 220038, 220039, 220040, 220041, 220042, 220043, 220044, 220045, 220046, 220047, 220048, 220049, 220050, 220051, 220052, 220053, 220054, 220055, 220056, 220057, 220058, 220059, 220060, 220061, 220062, 220063, 220064, 220065, 220066, 220067, 220068, 220069, 220070, 220071, 220072, 220073, 220074, 220075, 220076, 220077, 220078, 220079, 220080, 220081, 220082, 220083, 220084, 220085, 220086, 220087, 220088, 220089, 220090, 220091, 220092, 220093, 220094, 220095, 220096, 220097, 220098, 220099, 220100, 220101, 220102]
|
||||
|
||||
MASTER_TITLES = [12, 22, 32, 42, 52, 62, 72, 82, 92, 102, 112, 122, 132, 142, 152, 162, 172, 182, 192, 202, 212, 222, 232, 242, 252, 262, 272, 282, 292, 302, 312, 322, 332, 342, 352, 362, 372, 382, 392, 402, 412, 422, 432, 442, 452, 462, 472, 482, 492, 502, 512, 522, 532, 542, 552, 562, 572, 582, 592, 602, 612, 622, 632, 642, 652, 662, 672, 682, 692, 702, 712, 722, 732, 742, 752, 762, 772, 782, 792, 802, 812, 822, 832, 842, 852, 862, 872, 882, 892, 902, 912, 922, 972, 982, 992, 1002, 1012, 1022, 1032, 1042, 1052, 1062, 1072, 1082, 1092, 1102, 1112, 1122, 1132, 1142, 1152, 1162, 1172, 1182, 1192, 1202, 1222, 1232, 1242, 1252, 1262, 1272, 1282, 1292, 1302, 1312, 1322, 1332, 1342, 1352, 1362, 1372, 1382, 1392, 1402, 1412, 1422, 1432, 1442, 1452, 1462, 1472, 1482, 1492, 1502, 1512, 1522, 1532, 1542, 1552, 1562, 1572, 1582, 1592, 1602, 1612, 1622, 1632, 1642, 1652, 1662, 1672, 1682, 1692, 1702, 1712, 1722, 1732, 1742, 1752, 1762, 1772, 1782, 1792, 1802, 1812, 1822, 1832, 1842, 1852, 1862, 1872, 1882, 1892, 1902, 1912, 1922, 1932, 1942, 1952, 1962, 1972, 1982, 1992, 2002, 2012, 2022, 2032, 2042, 2052, 2062, 2072, 2082, 2092, 2102, 2112, 2122, 2132, 2152, 2162, 2172, 2182, 2192, 2202, 2212, 2222, 2232, 2242, 2252, 2262, 2272, 2282, 2292, 2302, 2312, 2322, 2332, 2342, 2352, 2362, 2372, 2382, 2392, 2402, 2412, 2422, 2432, 2442, 2452, 2462, 2472, 2482, 2492, 2502, 2512, 2522, 2532, 2542, 2552, 2562, 2572, 2582, 2592, 2602, 2612, 2622, 2632, 2642, 2652, 2662, 2672, 2682, 2692, 2702, 2712, 2722, 2732, 2742, 2752, 2762, 2782, 2792, 2802, 2812, 2822, 2832, 2842, 2852, 2862, 2872, 2882, 2892, 2902, 2912, 2922, 2932, 2942, 2952, 2962, 2972, 2982, 2992, 3002, 3012, 3022, 3032, 3042, 3052, 3062, 3072, 3082, 3092, 3102, 3112, 3122, 3132, 3142, 3152, 3162, 3172, 3182, 3192, 3202, 3212, 3222, 3232, 3242, 3252, 3262, 3272, 3282, 3292, 3302, 3312, 3322, 3332, 3342, 3352, 3362, 3372, 3382, 3392, 3402, 3412, 3422, 3432, 3442, 3452, 3462, 3472, 3482, 3492, 3502, 3512, 3522, 3532, 3542, 3552, 3562, 3572, 3582, 3592, 3602, 3612, 3622, 3632, 3642, 3652, 3662, 3672, 3682, 3692, 3702, 3712, 3722, 3732, 3742, 3752, 3762, 3772, 3782, 3792, 3802, 3812, 3822, 3832, 3842, 3852, 3862, 3872, 3882, 3892, 3902, 3912, 3922, 3932, 3942, 3952, 3962, 3982, 3992, 4002, 4012, 4022, 4032, 4042, 4052, 4062, 4072, 4082, 4092, 4102, 4112, 4122, 4132, 4142, 4152, 4162, 4172, 4182, 4192, 4202, 4212, 4222, 4232, 4242, 4252, 4262, 4272, 4282, 4292, 4302, 4312, 4322, 4332, 4342, 4352, 4362, 4372, 4382, 4392, 4402, 4412, 4422, 4442, 4452, 4462, 4472, 4482, 4492, 4502, 4512, 4522, 4532, 4542, 4552, 4562, 4572, 4582, 4592, 4642, 4652, 4662, 4672, 4682, 4692, 4702, 4712, 4722, 4732, 4742, 4752, 4762, 4772, 4782, 4792, 4802, 4812, 4822, 4832, 4842, 4862, 4872, 4882, 4892, 4902, 4912, 4922, 4932, 4942, 4952, 4962, 4972, 4982, 4992, 5002, 5012, 5022, 5032, 5042, 5052, 5062, 5072, 5082, 5092, 5102, 5142, 5152, 5162, 5172, 5182, 5192, 5202, 5212, 5222, 5232, 5242, 5252, 5262, 5272, 5282, 5292, 5302, 5312, 5322, 5332, 5342, 5352, 5362, 5372, 5382, 5392, 5402, 5412, 5422, 5432, 5442, 5452, 5462, 5472, 5482, 5492, 5502, 5512, 5522, 5532, 5542, 5552, 5562, 5572, 5582, 5592, 5602, 5612, 5622, 5632, 5642, 5652, 5662, 5672, 5682, 5692, 5702, 5712, 5722, 5732, 5742, 5752, 5762, 5772, 5782, 5792, 5802, 5812, 5822, 5832, 5842, 5852, 5862, 5872, 5882, 5892, 5902, 5912, 5922, 5932, 5942, 5952, 5962, 5972, 5982, 5992, 6002, 6012, 6022, 6032, 6042, 6052, 6062, 6072, 6082, 6092, 6102, 6112, 6122, 6132, 6142, 6152]
|
||||
|
||||
NORMAL_TITLES = [11, 21, 31, 41, 51, 61, 71, 81, 91, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191, 201, 211, 221, 231, 241, 251, 261, 271, 281, 291, 301, 311, 321, 331, 341, 351, 361, 371, 381, 391, 401, 411, 421, 431, 441, 451, 461, 471, 481, 491, 501, 511, 521, 531, 541, 551, 561, 571, 581, 591, 601, 611, 621, 631, 641, 651, 661, 671, 681, 691, 701, 711, 721, 731, 741, 751, 761, 771, 781, 791, 801, 811, 821, 831, 841, 851, 861, 871, 881, 891, 901, 911, 921, 971, 981, 991, 1001, 1011, 1021, 1031, 1041, 1051, 1061, 1071, 1081, 1091, 1101, 1111, 1121, 1131, 1141, 1151, 1161, 1171, 1181, 1191, 1201, 1221, 1231, 1241, 1251, 1261, 1271, 1281, 1291, 1301, 1311, 1321, 1331, 1341, 1351, 1361, 1371, 1381, 1391, 1401, 1411, 1421, 1431, 1441, 1451, 1461, 1471, 1481, 1491, 1501, 1511, 1521, 1531, 1541, 1551, 1561, 1571, 1581, 1591, 1601, 1611, 1621, 1631, 1641, 1651, 1661, 1671, 1681, 1691, 1701, 1711, 1721, 1731, 1741, 1751, 1761, 1771, 1781, 1791, 1801, 1811, 1821, 1831, 1841, 1851, 1861, 1871, 1881, 1891, 1901, 1911, 1921, 1931, 1941, 1951, 1961, 1971, 1981, 1991, 2001, 2011, 2021, 2031, 2041, 2051, 2061, 2071, 2081, 2091, 2101, 2111, 2121, 2131, 2151, 2161, 2171, 2181, 2191, 2201, 2211, 2221, 2231, 2241, 2251, 2261, 2271, 2281, 2291, 2301, 2311, 2321, 2331, 2341, 2351, 2361, 2371, 2381, 2391, 2401, 2411, 2421, 2431, 2441, 2451, 2461, 2471, 2481, 2491, 2501, 2511, 2521, 2531, 2541, 2551, 2561, 2571, 2581, 2591, 2601, 2611, 2621, 2631, 2641, 2651, 2661, 2671, 2681, 2691, 2701, 2711, 2721, 2731, 2741, 2751, 2761, 2781, 2791, 2801, 2811, 2821, 2831, 2841, 2851, 2861, 2871, 2881, 2891, 2901, 2911, 2921, 2931, 2941, 2951, 2961, 2971, 2981, 2991, 3001, 3011, 3021, 3031, 3041, 3051, 3061, 3071, 3081, 3091, 3101, 3111, 3121, 3131, 3141, 3151, 3161, 3171, 3181, 3191, 3201, 3211, 3221, 3231, 3241, 3251, 3261, 3271, 3281, 3291, 3301, 3311, 3321, 3331, 3341, 3351, 3361, 3371, 3381, 3391, 3401, 3411, 3421, 3431, 3441, 3451, 3461, 3471, 3481, 3491, 3501, 3511, 3521, 3531, 3541, 3551, 3561, 3571, 3581, 3591, 3601, 3611, 3621, 3631, 3641, 3651, 3661, 3671, 3681, 3691, 3701, 3711, 3721, 3731, 3741, 3751, 3761, 3771, 3781, 3791, 3801, 3811, 3821, 3831, 3841, 3851, 3861, 3871, 3881, 3891, 3901, 3911, 3921, 3931, 3941, 3951, 3961, 3981, 3991, 4001, 4011, 4021, 4031, 4041, 4051, 4061, 4071, 4081, 4091, 4101, 4111, 4121, 4131, 4141, 4151, 4161, 4171, 4181, 4191, 4201, 4211, 4221, 4231, 4241, 4251, 4261, 4271, 4281, 4291, 4301, 4311, 4321, 4331, 4341, 4351, 4361, 4371, 4381, 4391, 4401, 4411, 4421, 4441, 4451, 4461, 4471, 4481, 4491, 4501, 4511, 4521, 4531, 4541, 4551, 4561, 4571, 4581, 4591, 4641, 4651, 4661, 4671, 4681, 4691, 4701, 4711, 4721, 4731, 4741, 4751, 4761, 4771, 4781, 4791, 4801, 4811, 4821, 4831, 4841, 4861, 4871, 4881, 4891, 4901, 4911, 4921, 4931, 4941, 4951, 4961, 4971, 4981, 4991, 5001, 5011, 5021, 5031, 5041, 5051, 5061, 5071, 5081, 5091, 5101, 5141, 5151, 5161, 5171, 5181, 5191, 5201, 5211, 5221, 5231, 5241, 5251, 5261, 5271, 5281, 5291, 5301, 5311, 5321, 5331, 5341, 5351, 5361, 5371, 5381, 5391, 5401, 5411, 5421, 5431, 5441, 5451, 5461, 5471, 5481, 5491, 5501, 5511, 5521, 5531, 5541, 5551, 5561, 5571, 5581, 5591, 5601, 5611, 5621, 5631, 5641, 5651, 5661, 5671, 5681, 5691, 5701, 5711, 5721, 5731, 5741, 5751, 5761, 5771, 5781, 5791, 5801, 5811, 5821, 5831, 5841, 5851, 5861, 5871, 5881, 5891, 5901, 5911, 5921, 5931, 5941, 5951, 5961, 5971, 5981, 5991, 6001, 6011, 6021, 6031, 6041, 6051, 6061, 6071, 6081, 6091, 6101, 6111, 6121, 6131, 6141, 6151]
|
||||
|
||||
TITLE_LISTS = {
|
||||
0: SPECIAL_TITLES,
|
||||
1: NORMAL_TITLES,
|
||||
2: MASTER_TITLES,
|
||||
3: GOD_TITLES,
|
||||
}
|
||||
|
||||
def init_templates():
|
||||
global SONG_LIST, AVATAR_LIST, ITEM_LIST, EXP_UNLOCKED_SONGS
|
||||
|
||||
base_path = 'api/config/'
|
||||
print("[TEMPLATES] Initializing templates...")
|
||||
|
||||
try:
|
||||
with open(os.path.join(base_path, 'song_list.json'), 'r', encoding='utf-8') as f:
|
||||
SONG_LIST = json.load(f)
|
||||
|
||||
with open(os.path.join(base_path, 'avatar_list.json'), 'r', encoding='utf-8') as f:
|
||||
AVATAR_LIST = json.load(f)
|
||||
|
||||
with open(os.path.join(base_path, 'item_list.json'), 'r', encoding='utf-8') as f:
|
||||
ITEM_LIST = json.load(f)
|
||||
|
||||
with open(os.path.join(base_path, 'exp_unlocked_songs.json'), 'r', encoding='utf-8') as f:
|
||||
EXP_UNLOCKED_SONGS = json.load(f)
|
||||
|
||||
print("[TEMPLATES] Templates initialized successfully.")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"Error: {e}")
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error decoding JSON: {e}")
|
||||
+825
@@ -0,0 +1,825 @@
|
||||
from starlette.responses import Response, FileResponse, HTMLResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.routing import Route
|
||||
import os
|
||||
from datetime import datetime
|
||||
import json
|
||||
import secrets
|
||||
from sqlalchemy import select, update, insert
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from config import ROOT_FOLDER, START_COIN, COIN_REWARD, AUTHORIZATION_NEEDED, HOST, PORT
|
||||
|
||||
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, result, get_user_data, set_user_data, check_blacklist, check_whitelist
|
||||
from api.crypt import decrypt_fields
|
||||
from api.templates import START_AVATARS, START_STAGES, EXP_UNLOCKED_SONGS
|
||||
|
||||
async def info(request: Request):
|
||||
file_path = os.path.join(ROOT_FOLDER, "files/history.html")
|
||||
return FileResponse(file_path)
|
||||
|
||||
async def history(request: Request):
|
||||
file_path = os.path.join(ROOT_FOLDER, "files/history.html")
|
||||
return FileResponse(file_path)
|
||||
|
||||
async def delete_account(request):
|
||||
# This only tricks the client to clear its local data for now
|
||||
return Response(
|
||||
"""<?xml version="1.0" encoding="UTF-8"?><response><code>0</code><taito_id></taito_id></response>""",
|
||||
media_type="application/xml"
|
||||
)
|
||||
|
||||
async def tier(request: Request):
|
||||
file_path = os.path.join(ROOT_FOLDER, "files/tier.xml")
|
||||
return FileResponse(file_path)
|
||||
|
||||
async def reg(request: Request):
|
||||
return Response("", status_code=200)
|
||||
|
||||
async def name_reset(request: Request):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Missing username or password.", 0))
|
||||
|
||||
if len(username) < 6 or len(username) > 20:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username must be between 6 and 20 characters long.", 0))
|
||||
|
||||
if not is_alphanumeric(username):
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username must consist entirely of alphanumeric characters.", 0))
|
||||
|
||||
if username == password:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username cannot be the same as password.", 0))
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
|
||||
|
||||
if not await check_blacklist(decrypted_fields):
|
||||
return HTMLResponse(inform_page("FAILED:<br>Your account is banned and you are not allowed to perform this action.", 0))
|
||||
|
||||
user_exist = await get_user_data(decrypted_fields, "username")
|
||||
if user_exist:
|
||||
|
||||
query = select(user.c.id).where(user.c.username == username)
|
||||
existing_user = await database.fetch_one(query)
|
||||
if existing_user:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Another user already has this name.", 0))
|
||||
|
||||
password_hash = await get_user_data(decrypted_fields, "password_hash")
|
||||
if password_hash:
|
||||
if verify_password(password, password_hash):
|
||||
await set_user_data(decrypted_fields, "username", username)
|
||||
return HTMLResponse(inform_page("SUCCESS:<br>Username updated.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Password is not correct.<br>Please try again.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>User has no password hash.<br>This should not happen.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>User does not exist.<br>This should not happen.", 0))
|
||||
|
||||
|
||||
async def password_reset(request: Request):
|
||||
form = await request.form()
|
||||
old_password = form.get("old")
|
||||
new_password = form.get("new")
|
||||
|
||||
if not old_password or not new_password:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Missing old or new password.", 0))
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
|
||||
|
||||
username = await get_user_data(decrypted_fields, "username")
|
||||
if username:
|
||||
if username == new_password:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username cannot be the same as password.", 0))
|
||||
if len(new_password) < 6:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Password must have 6 or more characters.", 0))
|
||||
|
||||
old_hash = await get_user_data(decrypted_fields, "password_hash")
|
||||
print("hash type", type(old_hash))
|
||||
if old_hash:
|
||||
if verify_password(old_password, old_hash):
|
||||
hashed_new_password = hash_password(new_password)
|
||||
await set_user_data(decrypted_fields, "password_hash", hashed_new_password)
|
||||
return HTMLResponse(inform_page("SUCCESS:<br>Password updated.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Old password is not correct.<br>Please try again.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>User has no password hash.<br>This should not happen.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>User does not exist.<br>This should not happen.", 0))
|
||||
|
||||
|
||||
async def coin_mp(request: Request):
|
||||
form = await request.form()
|
||||
mp = int(form.get("coin_mp"))
|
||||
|
||||
if not mp:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Missing multiplier.", 0))
|
||||
|
||||
if mp < 0 or mp > 5:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Multiplier not acceptable.", 0))
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
|
||||
|
||||
user_exist = await get_user_data(decrypted_fields, "username")
|
||||
if user_exist:
|
||||
await set_user_data(decrypted_fields, "coin_mp", mp)
|
||||
return HTMLResponse(inform_page("SUCCESS:<br>Coin multiplier set to " + str(mp) + ".", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>User does not exist.", 0))
|
||||
|
||||
|
||||
async def save_migration(request: Request):
|
||||
form = await request.form()
|
||||
save_id = form.get("save_id")
|
||||
|
||||
if not save_id:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Missing save_id.", 0))
|
||||
|
||||
if len(save_id) != 24 or not all(c in '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' for c in save_id):
|
||||
return HTMLResponse(inform_page("FAILED:<br>Save ID not acceptable format.", 0))
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
|
||||
|
||||
user_exist = await get_user_data(decrypted_fields, "username")
|
||||
if user_exist:
|
||||
query = select(user.c.data, user.c.crc).where(user.c.save_id == save_id)
|
||||
existing_save_data = await database.fetch_one(query)
|
||||
|
||||
if existing_save_data:
|
||||
query = update(user).where(user.c.device_id == decrypted_fields[b'vid'][0].decode()).values(
|
||||
data=existing_save_data["data"],
|
||||
crc=existing_save_data["crc"],
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
await database.execute(query)
|
||||
return HTMLResponse(inform_page("SUCCESS:<br>Save migration was applied. If this was done by mistake, press the Save button now.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Save ID is not associated with a save file.", 0))
|
||||
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>User does not exist.", 0))
|
||||
|
||||
|
||||
async def register(request: Request):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Missing username or password.", 0))
|
||||
|
||||
if username == password:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username cannot be the same as password.", 0))
|
||||
|
||||
if len(username) < 6 or len(username) > 20:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username must be between 6 and 20<br>characters long.", 0))
|
||||
|
||||
if len(password) < 6:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Password must have<br>6 or above characters.", 0))
|
||||
|
||||
if not is_alphanumeric(username):
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username must consist entirely of<br>alphanumeric characters.", 0))
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
|
||||
|
||||
query = select(user.c.id).where(user.c.username == username)
|
||||
existing_user = await database.fetch_one(query)
|
||||
if existing_user:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Another user already has this name.", 0))
|
||||
|
||||
insert_query = insert(user).values(
|
||||
username=username,
|
||||
password_hash=hash_password(password),
|
||||
device_id=decrypted_fields[b'vid'][0].decode(),
|
||||
data="",
|
||||
crc=0,
|
||||
coin_mp=1,
|
||||
)
|
||||
await database.execute(insert_query)
|
||||
|
||||
return HTMLResponse(inform_page("SUCCESS:<br>Account is registered.<br>You can now backup/restore your save file.<br>You can only log into one device at a time.", 0))
|
||||
|
||||
async def logout(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
|
||||
|
||||
if not await check_blacklist(decrypted_fields):
|
||||
return HTMLResponse(inform_page("FAILED:<br>Your account is banned and you are<br>not allowed to perform this action.", 0))
|
||||
|
||||
await set_user_data(decrypted_fields, "device_id", "")
|
||||
return HTMLResponse(inform_page("Logout success.", 0))
|
||||
|
||||
async def login(request: Request):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
|
||||
if not username or not password:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Missing username or password.", 0))
|
||||
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Invalid request data.", 0))
|
||||
|
||||
query = select(user.c.id).where(user.c.username == username)
|
||||
user_record = await database.fetch_one(query)
|
||||
if user_record:
|
||||
user_id = user_record[0]
|
||||
|
||||
query = select(user.c.password_hash).where(user.c.id == user_id)
|
||||
password_hash_record = await database.fetch_one(query)
|
||||
if password_hash_record and verify_password(password, password_hash_record[0]):
|
||||
update_query = (
|
||||
update(user)
|
||||
.where(user.c.id == user_id)
|
||||
.values(device_id=decrypted_fields[b'vid'][0].decode())
|
||||
)
|
||||
await database.execute(update_query)
|
||||
return HTMLResponse(inform_page("SUCCESS:<br>You are logged in.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username or password incorrect.", 0))
|
||||
else:
|
||||
return HTMLResponse(inform_page("FAILED:<br>Username or password incorrect.", 0))
|
||||
|
||||
async def load(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message><ja>この機能を使用するには、まずアカウントを登録する必要があります。</ja><en>You need to register an account first before this feature can be used.</en><fr>Vous devez d'abord créer un compte avant de pouvoir utiliser cette fonctionnalité.</fr><it>È necessario registrare un account prima di poter utilizzare questa funzione.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
data = await get_user_data(decrypted_fields, "data")
|
||||
if data and data != "":
|
||||
crc = await get_user_data(decrypted_fields, "crc")
|
||||
timestamp = await get_user_data(decrypted_fields, "timestamp")
|
||||
xml_data = f"""<?xml version="1.0" encoding="UTF-8"?><response><code>0</code>
|
||||
<data>{data}</data>
|
||||
<crc>{crc}</crc>
|
||||
<date>{timestamp}</date>
|
||||
</response>"""
|
||||
return Response(xml_data, media_type="application/xml")
|
||||
else:
|
||||
return Response( """<response><code>12</code><message><ja>セーブデータが無いか、セーブデータが破損しているため、ロードできませんでした。</ja><en>Unable to load; either no save data exists, or the save data is corrupted.</en><fr>Chargement impossible : les données de sauvegarde sont absentes ou corrompues.</fr><it>Impossibile caricare. Non esistono dati salvati o quelli esistenti sono danneggiati.</it></message></response>""", media_type="application/xml")
|
||||
|
||||
async def save(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code></response>""", media_type="application/xml")
|
||||
|
||||
data = await request.body()
|
||||
data = data.decode("utf-8")
|
||||
|
||||
username = await get_user_data(decrypted_fields, "username")
|
||||
if username:
|
||||
crc = crc32_decimal(data)
|
||||
formatted_time = datetime.now()
|
||||
is_save_id_unique = False
|
||||
while not is_save_id_unique:
|
||||
save_id = ''.join(secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(24))
|
||||
|
||||
check_query = select(user.c.id).where(user.c.save_id == save_id)
|
||||
existing_user = await database.fetch_one(check_query)
|
||||
if not existing_user:
|
||||
is_save_id_unique = True
|
||||
|
||||
update_query = (
|
||||
update(user)
|
||||
.where(user.c.device_id == decrypted_fields[b'vid'][0].decode())
|
||||
.values(data=data, crc=crc, save_id=save_id, timestamp=formatted_time)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
return Response("""<response><code>0</code></response>""", media_type="application/xml")
|
||||
else:
|
||||
return Response("""<response><code>10</code></response>""", media_type="application/xml")
|
||||
|
||||
async def start(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message><ja>Invalid request data.</ja><en>Invalid request data.</en></message></response>""", media_type="application/xml"
|
||||
)
|
||||
|
||||
file_path = os.path.join(ROOT_FOLDER, "files/start.xml")
|
||||
try:
|
||||
tree = ET.parse(file_path)
|
||||
root = tree.getroot()
|
||||
except Exception as e:
|
||||
return Response(f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""", media_type="application/xml")
|
||||
|
||||
username = await get_user_data(decrypted_fields, "username")
|
||||
user_id = await get_user_data(decrypted_fields, "id")
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
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) + "/"
|
||||
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]:
|
||||
try:
|
||||
root.append(generator(host_string))
|
||||
except Exception as e:
|
||||
return Response(f"""<response><code>500</code><message>Error generating XML element: {str(e)}</message></response>""", media_type="application/xml")
|
||||
|
||||
daily_reward_elem = root.find(".//login_bonus")
|
||||
if daily_reward_elem is None:
|
||||
return Response("""<response><code>500</code><message>Missing <login_bonus> element in XML.</message></response>""", media_type="application/xml")
|
||||
|
||||
last_count_elem = daily_reward_elem.find("last_count")
|
||||
if last_count_elem is None or not last_count_elem.text.isdigit():
|
||||
return Response("""<response><code>500</code><message>Invalid or missing last_count in XML.</message></response>""", media_type="application/xml")
|
||||
last_count = int(last_count_elem.text)
|
||||
now_count = 1
|
||||
|
||||
query = select(daily_reward.c.day, daily_reward.c.timestamp).where(daily_reward.c.device_id == device_id)
|
||||
row = await database.fetch_one(query)
|
||||
|
||||
if row:
|
||||
current_day = row["day"]
|
||||
last_timestamp = row["timestamp"]
|
||||
current_date = datetime.now()
|
||||
|
||||
if (current_date.date() - last_timestamp.date()).days >= 1:
|
||||
now_count = current_day + 1
|
||||
if now_count > last_count:
|
||||
now_count = 1
|
||||
else:
|
||||
now_count = current_day
|
||||
|
||||
now_count_elem = daily_reward_elem.find("now_count")
|
||||
if now_count_elem is None:
|
||||
now_count_elem = ET.Element("now_count")
|
||||
daily_reward_elem.append(now_count_elem)
|
||||
now_count_elem.text = str(now_count)
|
||||
|
||||
query = select(daily_reward.c.my_avatar, daily_reward.c.my_stage, daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
|
||||
result_obj = await database.fetch_one(query)
|
||||
|
||||
if result_obj:
|
||||
my_avatar = set(json.loads(result_obj[0])) if result_obj[0] else START_AVATARS
|
||||
my_stage = set(json.loads(result_obj[1])) if result_obj[1] else START_STAGES
|
||||
coin = result_obj[2] if result_obj[2] is not None else START_COIN
|
||||
else:
|
||||
my_avatar = START_AVATARS
|
||||
my_stage = START_STAGES
|
||||
coin = START_COIN
|
||||
|
||||
coin_elem = ET.Element("my_coin")
|
||||
coin_elem.text = str(coin)
|
||||
root.append(coin_elem)
|
||||
|
||||
for avatar_id in my_avatar:
|
||||
avatar_elem = ET.Element("my_avatar")
|
||||
avatar_elem.text = str(avatar_id)
|
||||
root.append(avatar_elem)
|
||||
|
||||
for stage_id in my_stage:
|
||||
stage_elem = ET.Element("my_stage")
|
||||
stage_id_elem = ET.Element("stage_id")
|
||||
stage_id_elem.text = str(stage_id)
|
||||
stage_elem.append(stage_id_elem)
|
||||
|
||||
ac_mode_elem = ET.Element("ac_mode")
|
||||
ac_mode_elem.text = "1"
|
||||
stage_elem.append(ac_mode_elem)
|
||||
root.append(stage_elem)
|
||||
|
||||
if username:
|
||||
tid = ET.Element("taito_id")
|
||||
tid.text = username
|
||||
root.append(tid)
|
||||
|
||||
sid_elem = ET.Element("sid")
|
||||
sid_elem.text = str(user_id)
|
||||
root.append(sid_elem)
|
||||
|
||||
try:
|
||||
sid = get_stage_zero()
|
||||
root.append(sid)
|
||||
except Exception as e:
|
||||
return Response(f"""<response><code>500</code><message>Error retrieving stage zero: {str(e)}</message></response>""", media_type="application/xml")
|
||||
|
||||
xml_response = ET.tostring(root, encoding='unicode')
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
async def sync(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response(
|
||||
"""<response><code>10</code><message>Invalid request data.</message></response>""",
|
||||
media_type="application/xml"
|
||||
)
|
||||
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
file_path = os.path.join(ROOT_FOLDER, "files/sync.xml")
|
||||
try:
|
||||
tree = ET.parse(file_path)
|
||||
root = tree.getroot()
|
||||
except Exception as e:
|
||||
return Response(
|
||||
f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""",
|
||||
media_type="application/xml"
|
||||
)
|
||||
|
||||
username = await get_user_data(decrypted_fields, "username")
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
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) + "/"
|
||||
root.append(get_model_pak(host_string))
|
||||
root.append(get_tune_pak(host_string))
|
||||
root.append(get_skin_pak(host_string))
|
||||
root.append(get_m4a_path(host_string))
|
||||
root.append(get_stage_path(host_string))
|
||||
|
||||
query = select(daily_reward.c.my_avatar, daily_reward.c.my_stage, daily_reward.c.coin, daily_reward.c.item).where(daily_reward.c.device_id == device_id)
|
||||
result_obj = await database.fetch_one(query)
|
||||
|
||||
if result_obj:
|
||||
my_avatar = set(json.loads(result_obj[0])) if result_obj[0] else START_AVATARS
|
||||
my_stage = set(json.loads(result_obj[1])) if result_obj[1] else START_STAGES
|
||||
coin = result_obj[2] if result_obj[2] is not None else START_COIN
|
||||
items = json.loads(result_obj[3]) if result_obj[3] else []
|
||||
else:
|
||||
my_avatar = START_AVATARS
|
||||
my_stage = START_STAGES
|
||||
coin = START_COIN
|
||||
items = []
|
||||
|
||||
coin_elem = ET.Element("my_coin")
|
||||
coin_elem.text = str(coin)
|
||||
root.append(coin_elem)
|
||||
|
||||
for item in items:
|
||||
item_elem = ET.Element("add_item")
|
||||
item_id_elem = ET.Element("id")
|
||||
item_id_elem.text = str(item)
|
||||
item_elem.append(item_id_elem)
|
||||
item_num_elem = ET.Element("num")
|
||||
item_num_elem.text = "9"
|
||||
item_elem.append(item_num_elem)
|
||||
root.append(item_elem)
|
||||
|
||||
if items:
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(item="[]")
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
for avatar_id in my_avatar:
|
||||
avatar_elem = ET.Element("my_avatar")
|
||||
avatar_elem.text = str(avatar_id)
|
||||
root.append(avatar_elem)
|
||||
|
||||
for stage_id in my_stage:
|
||||
stage_elem = ET.Element("my_stage")
|
||||
stage_id_elem = ET.Element("stage_id")
|
||||
stage_id_elem.text = str(stage_id)
|
||||
stage_elem.append(stage_id_elem)
|
||||
|
||||
ac_mode_elem = ET.Element("ac_mode")
|
||||
ac_mode_elem.text = "1"
|
||||
stage_elem.append(ac_mode_elem)
|
||||
root.append(stage_elem)
|
||||
|
||||
if username:
|
||||
tid = ET.Element("taito_id")
|
||||
tid.text = username
|
||||
root.append(tid)
|
||||
|
||||
sid = get_stage_zero()
|
||||
root.append(sid)
|
||||
|
||||
kid = ET.Element("friend_num")
|
||||
kid.text = "9"
|
||||
root.append(kid)
|
||||
|
||||
xml_response = ET.tostring(root, encoding='unicode')
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
async def ttag(request: Request):
|
||||
decrypted_fields, original_field = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return HTMLResponse("""<html><body><h1>Invalid request data</h1></body></html>""", status_code=400)
|
||||
|
||||
username = await get_user_data(decrypted_fields, "username")
|
||||
if username:
|
||||
gcoin_mp = await get_user_data(decrypted_fields, "coin_mp")
|
||||
savefile_id = await get_user_data(decrypted_fields, "save_id")
|
||||
with open("files/profile.html", "r") as file:
|
||||
html_content = file.read().format(
|
||||
pid=original_field,
|
||||
user=username,
|
||||
gcoin_mp_0='selected' if gcoin_mp == 0 else '',
|
||||
gcoin_mp_1='selected' if gcoin_mp == 1 else '',
|
||||
gcoin_mp_2='selected' if gcoin_mp == 2 else '',
|
||||
gcoin_mp_3='selected' if gcoin_mp == 3 else '',
|
||||
gcoin_mp_4='selected' if gcoin_mp == 4 else '',
|
||||
gcoin_mp_5='selected' if gcoin_mp == 5 else '',
|
||||
savefile_id=savefile_id
|
||||
)
|
||||
else:
|
||||
with open("files/register.html", "r") as file:
|
||||
html_content = file.read().format(pid=original_field)
|
||||
|
||||
return HTMLResponse(html_content)
|
||||
|
||||
async def bonus(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message>Invalid request data.</message></response>""", media_type="application/xml")
|
||||
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
file_path = os.path.join(ROOT_FOLDER, "files/start.xml")
|
||||
|
||||
try:
|
||||
tree = ET.parse(file_path)
|
||||
root = tree.getroot()
|
||||
except Exception as e:
|
||||
return Response(f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""", media_type="application/xml")
|
||||
|
||||
daily_reward_elem = root.find(".//login_bonus")
|
||||
last_count_elem = daily_reward_elem.find("last_count")
|
||||
if last_count_elem is None or not last_count_elem.text.isdigit():
|
||||
return Response("""<response><code>500</code><message>Invalid or missing last_count in XML.</message></response>""", media_type="application/xml")
|
||||
last_count = int(last_count_elem.text)
|
||||
|
||||
query = select(daily_reward.c.day, daily_reward.c.timestamp, daily_reward.c.my_avatar, daily_reward.c.my_stage).where(daily_reward.c.device_id == device_id)
|
||||
row = await database.fetch_one(query)
|
||||
|
||||
time = datetime.now()
|
||||
|
||||
if row:
|
||||
current_day = row["day"]
|
||||
last_timestamp = row["timestamp"]
|
||||
my_avatar = set(json.loads(row["my_avatar"])) if row["my_avatar"] else set()
|
||||
my_stage = set(json.loads(row["my_stage"])) if row["my_stage"] else set()
|
||||
|
||||
if (time.date() - last_timestamp.date()).days >= 1:
|
||||
current_day += 1
|
||||
if current_day > last_count:
|
||||
current_day = 1
|
||||
reward_elem = daily_reward_elem.find(f".//reward[count='{current_day}']")
|
||||
if reward_elem is not None:
|
||||
cnt_type = int(reward_elem.find("cnt_type").text)
|
||||
cnt_id = int(reward_elem.find("cnt_id").text)
|
||||
|
||||
if cnt_type == 1:
|
||||
stages = set(json.loads(my_stage)) if my_stage else set()
|
||||
if cnt_id not in stages:
|
||||
stages.add(cnt_id)
|
||||
my_stage = json.dumps(list(stages))
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(timestamp=time, day=current_day, my_stage=my_stage)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
elif cnt_type == 2:
|
||||
avatars = set(json.loads(my_avatar)) if my_avatar else set()
|
||||
if cnt_id not in avatars:
|
||||
avatars.add(cnt_id)
|
||||
my_avatar = json.dumps(list(avatars))
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(timestamp=time, day=current_day, my_avatar=my_avatar)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
else:
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(timestamp=time, day=current_day)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
else:
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(timestamp=time, day=current_day)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
xml_response = "<response><code>0</code></response>"
|
||||
else:
|
||||
xml_response = "<response><code>1</code></response>"
|
||||
else:
|
||||
insert_query = insert(daily_reward).values(
|
||||
device_id=device_id,
|
||||
day=1,
|
||||
timestamp=time,
|
||||
my_avatar=json.dumps(START_AVATARS),
|
||||
my_stage=json.dumps(START_STAGES),
|
||||
coin=START_COIN,
|
||||
item="[]",
|
||||
lvl=1,
|
||||
title=1,
|
||||
avatar=1
|
||||
)
|
||||
await database.execute(insert_query)
|
||||
xml_response = "<response><code>0</code></response>"
|
||||
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
async def result_request(request: Request):
|
||||
decrypted_fields, _ = await decrypt_fields(request)
|
||||
if not decrypted_fields:
|
||||
return Response("""<response><code>10</code><message>Invalid request data.</message></response>""", media_type="application/xml")
|
||||
|
||||
should_serve = True
|
||||
if AUTHORIZATION_NEEDED:
|
||||
should_serve = await check_whitelist(decrypted_fields) and not await check_blacklist(decrypted_fields)
|
||||
|
||||
if not should_serve:
|
||||
return Response("""<response><code>403</code><message>Access denied.</message></response>""", media_type="application/xml")
|
||||
|
||||
device_id = decrypted_fields[b'vid'][0].decode()
|
||||
file_path = os.path.join(ROOT_FOLDER, "files/result.xml")
|
||||
try:
|
||||
tree = ET.parse(file_path)
|
||||
root = tree.getroot()
|
||||
except Exception as e:
|
||||
return Response(f"""<response><code>500</code><message>Error parsing XML: {str(e)}</message></response>""", media_type="application/xml")
|
||||
|
||||
vid = decrypted_fields[b'vid'][0].decode()
|
||||
stts = decrypted_fields[b'stts'][0].decode()
|
||||
track_id = decrypted_fields[b'id'][0].decode()
|
||||
mode = decrypted_fields[b'mode'][0].decode()
|
||||
avatar = decrypted_fields[b'avatar'][0].decode()
|
||||
score = int(decrypted_fields[b'score'][0].decode())
|
||||
high_score = decrypted_fields[b'high_score'][0].decode()
|
||||
play_rslt = decrypted_fields[b'play_rslt'][0].decode()
|
||||
item = decrypted_fields[b'item'][0].decode()
|
||||
device_os = decrypted_fields[b'os'][0].decode()
|
||||
os_ver = decrypted_fields[b'os_ver'][0].decode()
|
||||
tid = decrypted_fields[b'tid'][0].decode()
|
||||
ver = decrypted_fields[b'ver'][0].decode()
|
||||
mike = decrypted_fields[b'mike'][0].decode()
|
||||
|
||||
if int(track_id) not in range(616, 1024) or int(mode) not in range(0, 4):
|
||||
query = select(daily_reward.c.coin).where(daily_reward.c.device_id == device_id)
|
||||
row = await database.fetch_one(query)
|
||||
query = select(user.c.coin_mp).where(user.c.device_id == device_id)
|
||||
coin_mp_row = await database.fetch_one(query)
|
||||
current_coin = row["coin"] if row and row["coin"] else START_COIN
|
||||
updated_coin = current_coin + COIN_REWARD * coin_mp_row["coin_mp"]
|
||||
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(coin=updated_coin)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
query = select(user.c.id).where(user.c.device_id == vid)
|
||||
user_row = await database.fetch_one(query)
|
||||
sid = user_row["id"] if user_row else ""
|
||||
|
||||
do_insert = False
|
||||
do_update_sid = False
|
||||
do_update_vid = False
|
||||
last_row_id = 0
|
||||
|
||||
if sid:
|
||||
query = select(result.c.rid, result.c.score).where(
|
||||
(result.c.id == track_id) &
|
||||
(result.c.mode == mode) &
|
||||
(result.c.sid == sid)
|
||||
).order_by(result.c.score.desc())
|
||||
records = await database.fetch_all(query)
|
||||
if records:
|
||||
last_row_id = records[0]["rid"]
|
||||
if score > int(records[0]["score"]):
|
||||
do_update_sid = True
|
||||
else:
|
||||
do_insert = True
|
||||
else:
|
||||
query = select(result.c.rid, result.c.score).where(
|
||||
(result.c.id == track_id) &
|
||||
(result.c.mode == mode) &
|
||||
(result.c.sid == "") &
|
||||
(result.c.vid == vid)
|
||||
).order_by(result.c.score.desc())
|
||||
records = await database.fetch_all(query)
|
||||
if records:
|
||||
last_row_id = records[0]["rid"]
|
||||
if score > records[0]["score"]:
|
||||
do_update_vid = True
|
||||
else:
|
||||
do_insert = True
|
||||
|
||||
if do_insert:
|
||||
insert_query = insert(result).values(
|
||||
vid=vid, stts=stts, id=track_id, mode=mode, avatar=avatar,
|
||||
score=score, high_score=high_score, play_rslt=play_rslt, item=item,
|
||||
os=device_os, os_ver=os_ver, tid=tid, sid=sid, ver=ver, mike=mike
|
||||
)
|
||||
result_obj = await database.execute(insert_query)
|
||||
last_row_id = result_obj
|
||||
elif do_update_sid:
|
||||
update_query = (
|
||||
update(result)
|
||||
.where((result.c.sid == sid) & (result.c.id == track_id) & (result.c.mode == mode))
|
||||
.values(
|
||||
stts=stts, avatar=avatar, score=score, high_score=high_score,
|
||||
play_rslt=play_rslt, item=item, os=device_os, os_ver=os_ver,
|
||||
tid=tid, ver=ver, mike=mike, vid=vid
|
||||
)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
elif do_update_vid:
|
||||
update_query = (
|
||||
update(result)
|
||||
.where((result.c.vid == vid) & (result.c.id == track_id) & (result.c.mode == mode))
|
||||
.values(
|
||||
stts=stts, avatar=avatar, score=score, high_score=high_score,
|
||||
play_rslt=play_rslt, item=item, os=device_os, os_ver=os_ver,
|
||||
sid=sid, ver=ver, mike=mike
|
||||
)
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
query = select(daily_reward.c.my_stage).where(daily_reward.c.device_id == device_id)
|
||||
row = await database.fetch_one(query)
|
||||
my_stage = set(json.loads(row["my_stage"])) if row and row["my_stage"] else set(START_STAGES)
|
||||
|
||||
current_exp = int(stts.split(",")[0])
|
||||
for song in EXP_UNLOCKED_SONGS:
|
||||
if song["lvl"] <= current_exp:
|
||||
my_stage.add(song["id"])
|
||||
|
||||
my_stage = sorted(my_stage)
|
||||
update_query = (
|
||||
update(daily_reward)
|
||||
.where(daily_reward.c.device_id == device_id)
|
||||
.values(lvl=current_exp, avatar=int(avatar), my_stage=json.dumps(my_stage))
|
||||
)
|
||||
await database.execute(update_query)
|
||||
|
||||
query = select(result.c.rid, result.c.score).where(
|
||||
(result.c.id == track_id) & (result.c.mode == mode)
|
||||
).order_by(result.c.score.desc())
|
||||
records = await database.fetch_all(query)
|
||||
|
||||
rank = None
|
||||
for idx, record in enumerate(records, start=1):
|
||||
if record["rid"] == last_row_id:
|
||||
rank = idx
|
||||
break
|
||||
|
||||
after_element = root.find('.//after')
|
||||
after_element.text = str(rank)
|
||||
xml_response = ET.tostring(tree.getroot(), encoding='unicode')
|
||||
return Response(xml_response, media_type="application/xml")
|
||||
|
||||
|
||||
routes = [
|
||||
Route('/info.php', info, methods=['GET']),
|
||||
Route('/history.php', history, methods=['GET']),
|
||||
Route('/delete_account.php', delete_account, methods=['GET']),
|
||||
Route('/confirm_tier.php', tier, methods=['GET']),
|
||||
Route('/gcm/php/register.php', reg, methods=['GET']),
|
||||
Route('/name_reset/', name_reset, methods=['POST']),
|
||||
Route('/password_reset/', password_reset, methods=['POST']),
|
||||
Route('/coin_mp/', coin_mp, methods=['POST']),
|
||||
Route('/save_migration/', save_migration, methods=['POST']),
|
||||
Route('/register/', register, methods=['POST']),
|
||||
Route('/logout/', logout, methods=['POST']),
|
||||
Route('/login/', login, methods=['POST']),
|
||||
Route('/load.php', load, methods=['GET']),
|
||||
Route('/save.php', save, methods=['POST']),
|
||||
Route('/start.php', start, methods=['GET']),
|
||||
Route('/sync.php', sync, methods=['GET', 'POST']),
|
||||
Route('/ttag.php', ttag, methods=['GET']),
|
||||
Route('/login_bonus.php', bonus, methods=['GET']),
|
||||
Route('/result.php', result_request, methods=['GET']),
|
||||
]
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
HOST = 192.168.0.106
|
||||
PORT = 9068
|
||||
ACTUAL_HOST = 192.168.0.106
|
||||
ACTUAL_PORT = 9068
|
||||
|
||||
MODEL = 202504125800
|
||||
TUNEFILE = 202504125800
|
||||
SKIN = 202404191149
|
||||
|
||||
STAGE_PRICE = 1
|
||||
AVATAR_PRICE = 1
|
||||
ITEM_PRICE = 2
|
||||
COIN_REWARD = 1
|
||||
START_COIN = 10
|
||||
|
||||
AUTHORIZATION_NEEDED = False
|
||||
|
||||
DEBUG = True
|
||||
@@ -0,0 +1,63 @@
|
||||
from starlette.config import Config
|
||||
import os
|
||||
'''
|
||||
Do not change the name of this file.
|
||||
不要改动这个文件的名称。
|
||||
'''
|
||||
|
||||
config = Config("config.env")
|
||||
|
||||
'''
|
||||
IP and port of the server.
|
||||
服务器的IP和端口。
|
||||
'''
|
||||
|
||||
HOST = config("HOST", default="192.168.0.106")
|
||||
PORT = int(config("PORT", default=9070))
|
||||
|
||||
ACTUAL_HOST = config("ACTUAL_HOST", default="192.168.0.106")
|
||||
ACTUAL_PORT = int(config("ACTUAL_PORT", default=9070))
|
||||
|
||||
'''
|
||||
Datecode of the 3 pak files.
|
||||
三个pak文件的时间戳。
|
||||
'''
|
||||
|
||||
MODEL = config("MODEL", cast=str, default="202504125800")
|
||||
TUNEFILE = config("TUNEFILE", cast=str, default="202504125800")
|
||||
SKIN = config("SKIN", cast=str, default="202404191149")
|
||||
|
||||
'''
|
||||
Groove Coin-related settings.
|
||||
GCoin相关设定。
|
||||
'''
|
||||
|
||||
STAGE_PRICE = config("STAGE_PRICE", cast=int, default=1)
|
||||
AVATAR_PRICE = config("AVATAR_PRICE", cast=int, default=1)
|
||||
ITEM_PRICE = config("ITEM_PRICE", cast=int, default=2)
|
||||
COIN_REWARD = config("COIN_REWARD", cast=int, default=1)
|
||||
START_COIN = config("START_COIN", cast=int, default=10)
|
||||
|
||||
'''
|
||||
Only the whitelisted playerID can use the service. Blacklist has priority over whitelist.
|
||||
只有白名单的玩家ID才能使用服务。黑名单优先于白名单。
|
||||
'''
|
||||
|
||||
AUTHORIZATION_NEEDED = config("AUTHORIZATION_NEEDED", cast=bool, default=False)
|
||||
|
||||
'''
|
||||
SSL证书路径 - 留空则使用HTTP
|
||||
SSL certificate path. If left blank, use HTTP.
|
||||
'''
|
||||
|
||||
SSL_CERT = config("SSL_CERT", default=None)
|
||||
SSL_KEY = config("SSL_KEY", default=None)
|
||||
|
||||
'''
|
||||
Flask default debug
|
||||
Flask内置Debug
|
||||
'''
|
||||
|
||||
DEBUG = config("DEBUG", cast=bool, default=False)
|
||||
|
||||
ROOT_FOLDER = os.path.dirname(os.path.abspath(__file__))
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
flask
|
||||
starlette
|
||||
bcrypt
|
||||
pycryptodome
|
||||
requests
|
||||
Reference in New Issue
Block a user