This commit is contained in:
UnitedAirforce
2025-11-27 14:52:43 +08:00
parent 10bbd03bb6
commit aacbea2096
16 changed files with 1059 additions and 296 deletions

View File

@@ -4,8 +4,8 @@ from starlette.routing import Route
from datetime import datetime
import secrets
from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, read_user_save_file, write_user_save_file, should_serve, generate_salt
from api.database import check_blacklist, user_name_to_user_info, decrypt_fields_to_user_info, set_user_data_using_decrypted_fields, get_user_from_save_id, create_user, logout_user, login_user, get_bind
from api.misc import is_alphanumeric, inform_page, verify_password, hash_password, crc32_decimal, should_serve, generate_salt
from api.database import check_blacklist, user_name_to_user_info, decrypt_fields_to_user_info, set_user_data_using_decrypted_fields, get_user_from_save_id, create_user, logout_user, login_user, get_bind, read_user_save_file, write_user_save_file
from api.crypt import decrypt_fields
from config import AUTHORIZATION_MODE
@@ -76,7 +76,6 @@ async def password_reset(request: Request):
return inform_page("FAILED:<br>Password must have 6 or more characters.", 0)
old_hash = user_info['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)
@@ -228,7 +227,7 @@ async def login(request: Request):
user_id = user_record['id']
password_hash_record = user_record['password_hash']
if password_hash_record and verify_password(password, password_hash_record[0]):
if password_hash_record and verify_password(password, password_hash_record):
device_id = decrypted_fields[b'vid'][0].decode()
await login_user(user_id, device_id)

View File

@@ -5,8 +5,8 @@ import sqlalchemy
import json
import datetime
from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin
from api.misc import crc32_decimal, read_user_save_file, write_user_save_file
from api.database import player_database, accounts, results, devices, whitelists, blacklists, batch_tokens, binds, webs, logs, is_admin, read_user_save_file, write_user_save_file
from api.misc import crc32_decimal
TABLE_MAP = {
"accounts": (accounts, ["id", "username", "password_hash", "save_crc", "save_timestamp", "save_id", "coin_mp", "title", "avatar", "created_at", "updated_at"]),
@@ -14,9 +14,9 @@ TABLE_MAP = {
"devices": (devices, ["device_id", "user_id", "my_stage", "my_avatar", "item", "daily_day", "coin", "lvl", "title", "avatar", "mobile_sum", "arcade_sum", "total_sum", "created_at", "updated_at", "last_login_at"]),
"whitelist": (whitelists, ["id", "device_id"]),
"blacklist": (blacklists, ["id", "ban_terms", "reason"]),
"batch_tokens": (batch_tokens, ["id", "bind_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]),
"binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "bind_token", "created_at", "updated_at"]),
"webs": (webs, ["id", "user_id", "permission", "web_token", "created_at", "updated_at"]),
"batch_tokens": (batch_tokens, ["id", "batch_token", "expire_at", "uses_left", "auth_id", "created_at", "updated_at"]),
"binds": (binds, ["id", "user_id", "bind_account", "bind_code", "is_verified", "auth_token", "created_at", "updated_at"]),
"webs": (webs, ["id", "user_id", "permission", "web_token", "last_save_export", "created_at", "updated_at"]),
"logs": (logs, ["id", "user_id", "filename", "filesize", "timestamp"]),
}

View File

@@ -1,6 +1,7 @@
from starlette.responses import HTMLResponse
from starlette.requests import Request
from starlette.routing import Route
from datetime import datetime
import os
import json
import time
@@ -27,6 +28,15 @@ async def batch_handler(request: Request):
if result['expire_at'] < int(time.time()):
return HTMLResponse(content="Token expired", status_code=400)
if result['uses_left'] <= 0:
return HTMLResponse(content="No uses left for this token", status_code=400)
update_query = batch_tokens.update().where(batch_tokens.c.token == token).values(
uses_left=result['uses_left'] - 1,
updated_at=datetime.utcnow()
)
await player_database.execute(update_query)
with open(os.path.join('api/config/', 'download_manifest.json'), 'r', encoding='utf-8') as f:
stage_manifest = json.load(f)

View File

@@ -1,8 +1,10 @@
import sqlalchemy
from sqlalchemy import Table, Column, Integer, String, DateTime, JSON, ForeignKey, Index
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import select, update, insert
from sqlalchemy import select, update
import base64
import aiofiles
import json
from config import START_COIN, SIMULTANEOUS_LOGINS
from api.template import START_AVATARS, START_STAGES
@@ -82,8 +84,7 @@ results = Table(
Column("os", String(8), nullable=False),
Column("os_ver", String(16), nullable=False),
Column("ver", String(8), nullable=False),
Column("created_at", DateTime, default=datetime.utcnow),
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
Column("created_at", DateTime, default=datetime.utcnow)
)
Index(
@@ -100,6 +101,7 @@ webs = Table(
Column("user_id", Integer, ForeignKey("accounts.id")),
Column("permission", Integer, default=1),
Column("web_token", String(128), unique=True, nullable=False),
Column("last_save_export", Integer, nullable=True),
Column("created_at", DateTime, default=datetime.utcnow),
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
@@ -108,7 +110,7 @@ batch_tokens = Table(
"batch_tokens",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("bind_token", String(128), unique=True, nullable=False),
Column("batch_token", String(128), unique=True, nullable=False),
Column("expire_at", DateTime, nullable=False),
Column("uses_left", Integer, default=1),
Column("auth_id", String(64), nullable=False),
@@ -139,7 +141,7 @@ binds = Table(
Column("bind_account", String(128), unique=True, nullable=False),
Column("bind_code", String(6), nullable=False),
Column("is_verified", Integer, default=0),
Column("bind_token", String(64), unique=True, nullable=False),
Column("auth_token", String(64), unique=True),
Column("bind_date", DateTime, default=datetime.utcnow)
)
@@ -215,7 +217,7 @@ async def refresh_bind(user_id):
if existing_bind and existing_bind['is_verified'] == 1:
new_auth_token = base64.urlsafe_b64encode(os.urandom(64)).decode("utf-8")
update_query = update(binds).where(binds.c.id == existing_bind['id']).values(
bind_token=new_auth_token
auth_token=new_auth_token
)
await player_database.execute(update_query)
@@ -425,7 +427,7 @@ async def create_device(device_id, current_time):
async def is_admin(token):
if not token:
return False
query = webs.select().where(webs.c.token == token)
query = webs.select().where(webs.c.web_token == token)
web_data = await player_database.fetch_one(query)
if not web_data:
return False
@@ -468,3 +470,59 @@ async def get_rank_cache(key):
return None
return dict(result)['value']
return None
async def get_user_export_data(user_id):
user_data = {}
user_info, device_list = await user_id_to_user_info(user_id)
user_save = await read_user_save_file(user_id)
user_info['save_data'] = user_save
user_data['account'] = [user_info]
user_data['devices'] = device_list
all_results_query = results.select().where(results.c.user_id == user_id)
all_results = await player_database.fetch_all(all_results_query)
user_data['results'] = [dict(result) for result in all_results]
user_binds_query = binds.select().where(binds.c.user_id == user_id)
user_binds = await player_database.fetch_all(user_binds_query)
user_data['binds'] = [dict(bind) for bind in user_binds]
# Convert JSON fields to strings
for key, value in user_data.items():
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
for field, field_value in item.items():
if isinstance(field_value, (dict, list)):
item[field] = json.dumps(field_value)
return user_data
async def read_user_save_file(user_id):
if user_id is None:
return ""
elif type(user_id) != int:
return ""
else:
try:
async with aiofiles.open(f"./save/{user_id}.dat", "rb") as file:
result = await file.read()
result = result.decode("utf-8")
return result
except FileNotFoundError:
return ""
async def write_user_save_file(user_id, data):
if user_id is None:
return
elif type(user_id) != int:
return
else:
try:
async with aiofiles.open(f"./save/{user_id}.dat", "wb") as file:
await file.write(data.encode("utf-8"))
except Exception as e:
print(f"An error occurred while writing the file: {e}")

View File

@@ -2,6 +2,8 @@ from starlette.responses import Response, FileResponse
from starlette.requests import Request
from starlette.routing import Route
from sqlalchemy import select
import openpyxl
from io import BytesIO
import os
from api.database import player_database, devices, binds, batch_tokens, log_download, get_downloaded_bytes
@@ -18,7 +20,7 @@ async def serve_file(request: Request):
if not filename.endswith(".zip") and not filename.endswith(".pak"):
return Response("Unauthorized", status_code=403)
existing_batch_token = select(batch_tokens).where(batch_tokens.c.bind_token == auth_token)
existing_batch_token = select(batch_tokens).where(batch_tokens.c.batch_token == auth_token)
batch_result = await player_database.fetch_one(existing_batch_token)
if batch_result:
pass
@@ -72,6 +74,37 @@ async def serve_public_file(request: Request):
else:
return Response("File not found", status_code=404)
async def convert_user_export_data(data):
wb = openpyxl.Workbook()
# Remove default sheet
default_sheet = wb.active
wb.remove(default_sheet)
# Create sheet for each top-level key
for sheet_name, rows in data.items():
ws = wb.create_sheet(title=sheet_name)
# rows expected to be list[dict]
if isinstance(rows, list):
await write_dict_list_to_sheet(ws, rows)
stream = BytesIO()
wb.save(stream)
stream.seek(0)
return stream
async def write_dict_list_to_sheet(ws, rows):
if not rows:
return
# headers
headers = list(rows[0].keys())
ws.append(headers)
# rows
for row in rows:
ws.append([row.get(h, "") for h in headers])
routes = [
Route("/files/gc2/{auth_token}/{folder}/{filename}", serve_file, methods=["GET"]),

View File

@@ -65,6 +65,8 @@ def hash_password(password):
def verify_password(password, hashed_password):
if type(hashed_password) == str:
hashed_password = hashed_password.encode('utf-8')
print("hashed_password:", hashed_password)
return bcrypt.checkpw(password.encode('utf-8'), hashed_password)
def is_alphanumeric(username):
@@ -246,33 +248,6 @@ def check_email(email):
STRICT_EMAIL_REGEX = r"^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*@[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*(?:\.[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)*\.[A-Za-z]{2,}$"
return re.match(STRICT_EMAIL_REGEX, email) is not None
async def read_user_save_file(user_id):
if user_id is None:
return ""
elif type(user_id) != int:
return ""
else:
try:
async with aiofiles.open(f"./save/{user_id}.dat", "rb") as file:
result = await file.read()
result = result.decode("utf-8")
return result
except FileNotFoundError:
return ""
async def write_user_save_file(user_id, data):
if user_id is None:
return
elif type(user_id) != int:
return
else:
try:
async with aiofiles.open(f"./save/{user_id}.dat", "wb") as file:
await file.write(data.encode("utf-8"))
except Exception as e:
print(f"An error occurred while writing the file: {e}")
async def should_serve(decrypted_fields):
should_serve = True
if AUTHORIZATION_NEEDED:

View File

@@ -101,7 +101,8 @@ async def result_request(request: Request):
item=item,
os=device_os,
os_ver=os_ver,
ver=ver
ver=ver,
created_at=datetime.utcnow()
)
await player_database.execute(update_query)
@@ -123,8 +124,7 @@ async def result_request(request: Request):
os=device_os,
os_ver=os_ver,
ver=ver,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
created_at=datetime.utcnow()
)
result = await player_database.execute(insert_query)
target_row_id = result

View File

@@ -215,7 +215,7 @@ async def user_ranking_individual(request: Request):
ranking_list = []
player_ranking = {"username": user_info["username"] if user_info else "Guest (Not Ranked)", "score": 0, "position": -1, "title": user_info["title"] if user_info else device_info['title'], "avatar": device_info["avatar"]}
cache_key = f"{song_id}-{mode}"
cache_key = f"{str(song_id)}-{str(mode)}"
cached_data = await get_rank_cache(cache_key)
if cached_data:
@@ -232,13 +232,14 @@ async def user_ranking_individual(request: Request):
for index, record in enumerate(records):
if index >= page_number * page_count and index < (page_number + 1) * page_count:
rank_user = await user_id_to_user_info_simple(record["user_id"])
ranking_list.append({
"position": index + 1,
"username": rank_user["username"],
"score": record["score"],
"title": rank_user["title"],
"avatar": record["avatar"]
})
if rank_user:
ranking_list.append({
"position": index + 1,
"username": rank_user["username"],
"score": record["score"],
"title": rank_user["title"],
"avatar": record["avatar"]
})
if user_id and record["user_id"] == user_id:
player_ranking = {
"username": user_info["username"],
@@ -258,6 +259,8 @@ async def user_ranking_individual(request: Request):
}
}
print("toital_count: " + str(total_count))
return JSONResponse(payload)
async def user_ranking_total(request: Request):
@@ -290,7 +293,7 @@ async def user_ranking_total(request: Request):
score_obj = ["total_delta", "mobile_delta", "arcade_delta"]
cache_key = f"0-{mode}"
cache_key = f"0-{str(mode)}"
cached_data = await get_rank_cache(cache_key)
if cached_data:
records = cached_data
@@ -302,24 +305,29 @@ async def user_ranking_total(request: Request):
accounts.c[score_obj[mode]],
accounts.c.title,
accounts.c.avatar,
).order_by(accounts.c[score_obj[mode]].desc())
).where(
accounts.c[score_obj[mode]] > 0
).order_by(
accounts.c[score_obj[mode]].desc()
)
records = await player_database.fetch_all(query)
records = [dict(record) for record in records]
await write_rank_cache(cache_key, records, expire_seconds=120)
total_count = len(records)
print("total_count fetched: " + str(total_count))
for index, record in enumerate(records):
print(record)
if index >= page_number * page_count and index < (page_number + 1) * page_count:
rank_user = await user_id_to_user_info_simple(record["id"])
ranking_list.append({
"position": index + 1,
"username": rank_user["username"],
"score": record[score_obj[mode]],
"title": rank_user["title"],
"avatar": record["avatar"]
})
if rank_user:
ranking_list.append({
"position": index + 1,
"username": rank_user["username"],
"score": record[score_obj[mode]],
"title": rank_user["title"],
"avatar": record["avatar"]
})
if user_id and record["id"] == user_id:
player_ranking = {
"username": user_info["username"],

View File

@@ -1,28 +1,33 @@
from starlette.requests import Request
from starlette.routing import Route
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse, StreamingResponse
import secrets
from datetime import datetime
import time
from api.database import player_database, webs, is_admin, user_name_to_user_info, user_id_to_user_info_simple
from api.misc import read_user_save_file, verify_password, should_serve_web
from config import AUTHORIZATION_MODE
from api.database import player_database, webs, is_admin, user_name_to_user_info, user_id_to_user_info_simple, get_user_export_data
from api.misc import verify_password, should_serve_web
from api.file import convert_user_export_data
from config import AUTHORIZATION_MODE, SAVE_EXPORT_COOLDOWN
async def is_user(request: Request):
token = request.cookies.get("token")
if not token:
return False
query = webs.select().where(webs.c.token == token)
return False, None
query = webs.select().where(webs.c.web_token == token)
web_data = await player_database.fetch_one(query)
if not web_data:
return False
return False, None
if web_data['permission'] < 1:
return False
return False, None
if AUTHORIZATION_MODE > 0:
return await should_serve_web(web_data['user_id'])
result = await should_serve_web(web_data['user_id'])
if not result:
return False, None
return True
return True, web_data
async def web_login_page(request: Request):
with open("web/login.html", "r", encoding="utf-8") as file:
@@ -53,13 +58,15 @@ async def web_login_login(request: Request):
return JSONResponse({"status": "failed", "message": "Access denied."}, status_code=403)
query = webs.update().where(webs.c.user_id == user_info['id']).values(
token=token
web_token=token,
updated_at=datetime.utcnow()
)
else:
query = webs.insert().values(
user_id=user_info['id'],
permission=1,
web_token=token,
last_save_export=0,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
@@ -75,7 +82,7 @@ async def user_center_api(request: Request):
if not token:
return JSONResponse({"status": "failed", "message": "Token is required."}, status_code=400)
query = webs.select().where(webs.c.token == token)
query = webs.select().where(webs.c.web_token == token)
web_record = await player_database.fetch_one(query)
if not web_record:
return JSONResponse({"status": "failed", "message": "Invalid token."}, status_code=403)
@@ -96,7 +103,7 @@ async def user_center_api(request: Request):
response_data = {
"username": user_info['username'],
"last_save_export": web_record['last_save_export'].isoformat() if web_record['last_save_export'] else "None",
"next_save_export": web_record['last_save_export'] + SAVE_EXPORT_COOLDOWN,
}
return JSONResponse({"status": "success", "data": response_data})
@@ -105,28 +112,64 @@ async def user_center_api(request: Request):
return JSONResponse({"status": "failed", "message": "Invalid action."}, status_code=400)
async def user_center_page(request: Request):
usr = await is_user(request)
if not usr:
serve, web_data = await is_user(request)
if not serve:
response = RedirectResponse(url="/login")
response.delete_cookie(key="token")
return response
with open("web/user.html", "r", encoding="utf-8") as file:
html_template = file.read()
is_adm = await is_admin(request)
is_adm = await is_admin(request.cookies.get("token"))
if is_adm:
admin_button = f"""
<div class="container mt-4">
<button class="btn btn-light" onclick="window.location.href='/admin'">Admin Panel</button>
<button class="btn btn-light" onclick="window.location.href='/admin'">Admin Panel</button>
</div>
"""
html_template += admin_button
return HTMLResponse(content=html_template)
async def user_center_export_data(request: Request):
serve, web_data = await is_user(request)
if not serve:
response = RedirectResponse(url="/login")
response.delete_cookie(key="token")
return response
user_id = web_data['user_id']
last_save_export = web_data['last_save_export']
current_time = time.time()
if current_time - last_save_export < SAVE_EXPORT_COOLDOWN:
wait_time = int(SAVE_EXPORT_COOLDOWN - (current_time - last_save_export))
return JSONResponse({"status": "failed", "message": f"Please wait {wait_time} seconds before exporting again."}, status_code=429)
user_json_data_set = await get_user_export_data(user_id)
user_xlsx_stream = await convert_user_export_data(user_json_data_set)
headers = {
"Content-Disposition": 'attachment; filename="export.xlsx"'
}
update_query = webs.update().where(webs.c.user_id == user_id).values(
last_save_export=int(current_time),
updated_at=datetime.utcnow()
)
await player_database.execute(update_query)
return StreamingResponse(
user_xlsx_stream,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers=headers,
)
routes = [
Route("/login", web_login_page, methods=["GET"]),
Route("/login/", web_login_page, methods=["GET"]),
Route("/login/login", web_login_login, methods=["POST"]),
Route("/usercenter", user_center_page, methods=["GET"]),
Route("/usercenter/api", user_center_api, methods=["POST"])
Route("/usercenter/api", user_center_api, methods=["POST"]),
Route("/usercenter/export_data", user_center_export_data, methods=["GET"]),
]

492
new_server_7003/db-conv.py Normal file
View File

@@ -0,0 +1,492 @@
import sqlalchemy
from sqlalchemy import Table, Column, Integer, String, DateTime, JSON, ForeignKey, Index
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy import select, update, insert
import json
import os
START_COIN = 10
import os
import databases
from datetime import datetime, timedelta
DB_NAME = "player.db"
DB_PATH = os.path.join(os.getcwd(), DB_NAME)
DATABASE_URL = f"sqlite+aiosqlite:///{DB_PATH}"
OLD_DB_NAME = "player_d.db"
OLD_DB_PATH = os.path.join(os.getcwd(), OLD_DB_NAME)
OLD_DATABASE_URL = f"sqlite+aiosqlite:///{OLD_DB_PATH}"
old_database = databases.Database(OLD_DATABASE_URL)
old_metadata = sqlalchemy.MetaData()
player_database = databases.Database(DATABASE_URL)
player_metadata = sqlalchemy.MetaData()
#----------------------- Old Table definitions -----------------------#
user = Table(
"user",
old_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=None),
Column("coin_mp", Integer, default=1)
)
daily_reward = Table(
"daily_reward",
old_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("device_id", String(512)),
Column("timestamp", DateTime, default=datetime.utcnow),
Column("my_stage", JSON),
Column("my_avatar", JSON),
Column("item", JSON),
Column("day", Integer),
Column("coin", Integer),
Column("lvl", Integer),
Column("title", Integer),
Column("avatar", Integer),
)
result = Table(
"result",
old_metadata,
Column("rid", Integer, primary_key=True, autoincrement=True),
Column("vid", String(512), nullable=False),
Column("tid", String(512), nullable=False),
Column("sid", Integer, nullable=False),
Column("stts", String(64)),
Column("id", Integer),
Column("mode", Integer),
Column("avatar", Integer),
Column("score", Integer),
Column("high_score", String(128)),
Column("play_rslt", String(128)),
Column("item", Integer),
Column("os", String(16)),
Column("os_ver", String(16)),
Column("ver", String(16)),
Column("mike", Integer),
)
whitelist = Table(
"whitelist",
old_metadata,
Column("id", String(512), primary_key=True)
)
blacklist = Table(
"blacklist",
old_metadata,
Column("id", String(512), primary_key=True),
Column("reason", String(256))
)
bind = Table(
"bind",
old_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
Column("bind_acc", String(256), unique=True, nullable=False),
Column("bind_code", String(16), nullable=False),
Column("is_verified", Integer, default=0),
Column("auth_token", String(256), nullable=True),
Column("bind_date", DateTime, default=datetime.utcnow)
)
logs = Table(
"logs",
old_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
Column("filename", String(256)),
Column("filesize", Integer),
Column("timestamp", DateTime, default=datetime.utcnow)
)
device_list = Table(
"device_list",
old_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("user_id", Integer, sqlalchemy.ForeignKey("user.id")),
Column("device_id", String(512), unique=True, nullable=False),
Column("last_login", DateTime, default=datetime.utcnow)
)
#----------------------- End of old Table definitions -----------------------#
#----------------------- New Table definitions -----------------------#
accounts = Table(
"accounts",
player_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("save_crc", String(24), nullable=True),
Column("save_timestamp", DateTime, nullable=True),
Column("save_id", String(24), nullable=True),
Column("coin_mp", Integer, default=0),
Column("title", Integer, default=1),
Column("avatar", Integer, default=1),
Column("mobile_delta", Integer, default=0),
Column("arcade_delta", Integer, default=0),
Column("total_delta", Integer, default=0),
Column("created_at", DateTime, default=datetime.utcnow),
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
devices = Table(
"devices",
player_metadata,
Column("device_id", String(64), primary_key=True),
Column("user_id", Integer, ForeignKey("accounts.id")),
Column("my_stage", JSON, default=[]),
Column("my_avatar", JSON, default=[]),
Column("item", JSON, default=[]),
Column("daily_day", Integer, default=0),
Column("daily_timestamp", DateTime, default=datetime.min),
Column("coin", Integer, default=START_COIN),
Column("lvl", Integer, default=1),
Column("title", Integer, default=1),
Column("avatar", Integer, default=1),
Column("created_at", DateTime, default=datetime.utcnow),
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow),
Column("last_login_at", DateTime, default=None)
)
results = Table(
"results",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("device_id", String(64), ForeignKey("devices.device_id")),
Column("user_id", Integer, ForeignKey("accounts.id")),
Column("stts", JSON, nullable=False),
Column("song_id", Integer, nullable=False),
Column("mode", Integer, nullable=False),
Column("avatar", Integer, nullable=False),
Column("score", Integer, nullable=False),
Column("high_score", JSON, nullable=False),
Column("play_rslt", JSON, nullable=False),
Column("item", Integer, nullable=False),
Column("os", String(8), nullable=False),
Column("os_ver", String(16), nullable=False),
Column("ver", String(8), nullable=False),
Column("created_at", DateTime, default=datetime.utcnow)
)
Index(
"idx_results_song_mode_score",
results.c.song_id,
results.c.mode,
results.c.score.desc(),
)
webs = Table(
"webs",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("user_id", Integer, ForeignKey("accounts.id")),
Column("permission", Integer, default=1),
Column("web_token", String(128), unique=True, nullable=False),
Column("last_save_export", Integer, nullable=True),
Column("created_at", DateTime, default=datetime.utcnow),
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
batch_tokens = Table(
"batch_tokens",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("batch_token", String(128), unique=True, nullable=False),
Column("expire_at", DateTime, nullable=False),
Column("uses_left", Integer, default=1),
Column("auth_id", String(64), nullable=False),
Column("created_at", DateTime, default=datetime.utcnow),
Column("updated_at", DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
whitelists = Table(
"whitelists",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("device_id", String(64), ForeignKey("devices.device_id")),
)
blacklists = Table(
"blacklists",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("ban_terms", String(128), unique=True, nullable=False),
Column("reason", String(255), nullable=True)
)
binds = Table(
"binds",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("user_id", Integer, ForeignKey("accounts.id")),
Column("bind_account", String(128), unique=True, nullable=False),
Column("bind_code", String(6), nullable=False),
Column("is_verified", Integer, default=0),
Column("auth_token", String(64), unique=True),
Column("bind_date", DateTime, default=datetime.utcnow)
)
logs = Table(
"logs",
player_metadata,
Column("id", Integer, primary_key=True, autoincrement=True),
Column("user_id", Integer, ForeignKey("accounts.id")),
Column("filename", String(255), nullable=False),
Column("filesize", Integer, nullable=False),
Column("timestamp", DateTime, default=datetime.utcnow)
)
#----------------------- End of new Table definitions -----------------------#
async def init_db():
if not os.path.exists(DB_PATH):
print("[DB] Creating new database:", DB_PATH)
if not os.path.exists(OLD_DB_PATH):
print("[DB] Old db must exist as player_d.db")
old_engine = create_async_engine(OLD_DATABASE_URL, echo=False)
async with old_engine.begin() as conn:
await conn.run_sync(old_metadata.create_all)
await old_engine.dispose()
engine = create_async_engine(DATABASE_URL, echo=False)
async with engine.begin() as conn:
await conn.run_sync(player_metadata.create_all)
await engine.dispose()
print("[DB] Database initialized successfully.")
async def save_user_data(user_id, user_data):
save_dir = "save"
save_file = os.path.join(save_dir, f"{user_id}.dat")
if not os.path.exists(save_dir):
os.makedirs(save_dir)
with open(save_file, "w") as file:
file.write(user_data)
async def convert_db():
await old_database.connect()
await player_database.connect()
# Conversion logic goes here
print("[DB] Starting database conversion...")
# Convert user -> accounts
print("[DB] Converting users to accounts...")
query = select(old_metadata.tables["user"])
all_users = await old_database.fetch_all(query)
all_users = [dict(u) for u in all_users]
for user in all_users:
user_device_id = user['device_id']
user_data = user.get('data') if 'data' in user else None
if user_data:
user_id = user['id']
await save_user_data(user_id, user_data)
user_old_device = await old_database.fetch_one(
select(old_metadata.tables["daily_reward"]).where(old_metadata.tables["daily_reward"].c.device_id == user_device_id)
)
user_old_device = dict(user_old_device) if user_old_device else None
insert_query = insert(accounts).values(
username=user['username'],
password_hash=user['password_hash'],
save_crc=user['crc'],
save_timestamp=user['timestamp'],
save_id=user['save_id'],
coin_mp=user['coin_mp'],
title=user_old_device['title'] if user_old_device else 1,
avatar=user_old_device['avatar'] if user_old_device else 1,
mobile_delta=0,
arcade_delta=0,
total_delta=0,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow()
)
await player_database.execute(insert_query)
# Compute score deltas
print("[DB] Computing score deltas for accounts...")
all_new_users = await player_database.fetch_all(select(accounts))
all_new_users = [dict(u) for u in all_new_users]
for new_user in all_new_users:
username = new_user['username']
old_user_object = await old_database.fetch_one(
select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.username == username)
)
old_user_id = old_user_object['id']
old_user_results_mobile = await old_database.fetch_all(
select(old_metadata.tables["result"]).where(
(old_metadata.tables["result"].c.sid == old_user_id) &
(old_metadata.tables["result"].c.mode.in_([1, 2, 3]))
)
)
old_user_mobile_sum = sum([int(r['score']) for r in old_user_results_mobile])
old_user_results_arcade = await old_database.fetch_all(
select(old_metadata.tables["result"]).where(
(old_metadata.tables["result"].c.sid == old_user_id) &
(old_metadata.tables["result"].c.mode.in_([11, 12, 13]))
)
)
old_user_arcade_sum = sum([int(r['score']) for r in old_user_results_arcade])
old_user_results_total = old_user_mobile_sum + old_user_arcade_sum
update_query = (
update(accounts).where(accounts.c.id == new_user['id']).values(
mobile_delta=old_user_mobile_sum,
arcade_delta=old_user_arcade_sum,
total_delta=old_user_results_total
)
)
await player_database.execute(update_query)
# Convert daily_reward -> devices
print("[DB] Converting daily rewards to devices...")
all_devices = await old_database.fetch_all(select(old_metadata.tables["daily_reward"]))
all_devices = [dict(d) for d in all_devices]
for device in all_devices:
connected_user = await old_database.fetch_one(
select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.device_id == device['device_id'])
)
insert_query = insert(devices).values(
device_id=device['device_id'],
user_id=connected_user['id'] if connected_user else None,
my_stage=device['my_stage'],
my_avatar=device['my_avatar'],
item=device['item'],
daily_day=device['day'],
daily_timestamp=device['timestamp'],
coin=device['coin'],
lvl=device['lvl'],
title=device['title'],
avatar=device['avatar'],
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
last_login_at=datetime.utcnow()
)
await player_database.execute(insert_query)
# convert result -> results
print("[DB] Converting results to results...")
all_results = await old_database.fetch_all(select(old_metadata.tables["result"]))
all_results = [dict(r) for r in all_results]
for result in all_results:
if result['sid'] is not None and result['sid'] != 0:
# Skip guests, they are no longer ranked
old_user_object = await old_database.fetch_one(
select(old_metadata.tables["user"]).where(old_metadata.tables["user"].c.id == result['sid'])
)
old_user_username = old_user_object['username'] if old_user_object else None
if old_user_username:
new_user_object = await player_database.fetch_one(
select(accounts).where(accounts.c.username == old_user_username)
)
new_user_id = new_user_object['id']
insert_query = insert(results).values(
device_id=result['vid'],
user_id=new_user_id,
stts=convert_array(result['stts']),
song_id=result['id'],
mode=result['mode'],
avatar=result['avatar'],
score=result['score'],
high_score=convert_array(result['high_score']),
play_rslt=convert_array(result['play_rslt']),
item=result['item'],
os=result['os'],
os_ver=result['os_ver'],
ver=result['ver'],
created_at=datetime.utcnow(),
)
await player_database.execute(insert_query)
# convert bwlist
print("[DB] Converting blacklists and whitelists...")
old_blacklists = await old_database.fetch_all(select(old_metadata.tables["blacklist"]))
old_blacklists = [dict(b) for b in old_blacklists]
for bl in old_blacklists:
insert_query = insert(blacklists).values(
ban_terms=bl['id'],
reason=bl['reason']
)
await player_database.execute(insert_query)
old_whitelists = await old_database.fetch_all(select(old_metadata.tables["whitelist"]))
old_whitelists = [dict(w) for w in old_whitelists]
for wl in old_whitelists:
insert_query = insert(whitelists).values(
device_id=wl['id']
)
await player_database.execute(insert_query)
# convert binds (optional)
if 'bind' in old_metadata.tables:
print("[DB] Converting binds...")
old_binds = await old_database.fetch_all(select(old_metadata.tables["bind"]))
old_binds = [dict(b) for b in old_binds]
for b in old_binds:
insert_query = insert(binds).values(
user_id=b['user_id'],
bind_account=b['bind_acc'],
bind_code=b['bind_code'],
is_verified=b['is_verified'],
auth_token=b['auth_token'],
bind_date=b['bind_date']
)
await player_database.execute(insert_query)
# convert logs (optional)
if 'logs' in old_metadata.tables:
print("[DB] Converting logs...")
old_logs = await old_database.fetch_all(select(old_metadata.tables["logs"]))
old_logs = [dict(l) for l in old_logs]
for l in old_logs:
insert_query = insert(logs).values(
user_id=l['user_id'],
filename=l['filename'],
filesize=l['filesize'],
timestamp=l['timestamp']
)
await player_database.execute(insert_query)
# tables not converted: batch_tokens (new field added), webs (auto created)
await old_database.disconnect()
await player_database.disconnect()
print("[DB] Database conversion completed successfully.")
def convert_array(old_array):
old_array = "[" + old_array + "]"
old_array = json.loads(old_array)
return old_array
if __name__ == "__main__":
import asyncio
asyncio.run(init_db())
asyncio.run(convert_db())

View File

@@ -168,9 +168,11 @@ function showDetailTotal(mode, page) {
// Mode boxes
html = `<div>${songName}</div>`;
html = `<div id="ranking_content">
<div class="song-name">${songName}</div>
`;
rowStart = '<div class="button-row">'
rowStart = '<div class="button-row mode-buttons">'
rowEnd = '</div>'
rowContent = []
@@ -182,7 +184,7 @@ function showDetailTotal(mode, page) {
rowContent.push(`<div class="bt_bg01_ac">${label}</div>`);
} else {
rowContent.push(
`<a onclick="showDetailTotal(${modeValue});" class="bt_bg01_xnarrow">${label}</a>`
`<a onclick="showDetailTotal(${modeValue}, 0);" class="bt_bg01_xnarrow">${label}</a>`
);
}
}
@@ -210,25 +212,29 @@ function showDetailTotal(mode, page) {
`;
// generate pagination buttons
if (generatePrevButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailTotal(${mode}, ${page - 1})">
Prev Page
</button>
`;
}
html += `
<div class="pagination-container">
${generatePrevButton ? `
<button class="pagination-button prev-button"
onclick="showDetailTotal(${mode}, ${page - 1})">
Prev Page
</button>
` : '<div class="placeholder"></div>'} <!-- Placeholder for alignment -->
if (generateNextButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailTotal(${mode}, ${page + 1})">
Next Page
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' class="pagination-button">
Back
</button>
`;
}
${generateNextButton ? `
<button class="pagination-button next-button"
onclick="showDetailTotal(${mode}, ${page + 1})">
Next Page
</button>
` : ''}
</div>
`;
// Loop leaderboard ranks
html += `<div class="leaderboard-players">`;
for (let i = 0; i < rankingList.length; i++) {
userData = rankingList[i];
@@ -246,31 +252,7 @@ function showDetailTotal(mode, page) {
}
// Generate pagination buttons again
if (generatePrevButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailTotal(${mode}, ${page - 1})">
Prev Page
</button>
`;
}
if (generateNextButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailTotal(${mode}, ${page + 1})">
Next Page
</button>
`;
}
html += `
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
Back
</button>
`;
html += `</div>`;
document.getElementById('ranking_box').innerHTML = html;
@@ -358,6 +340,7 @@ function showDetailIndividual(songId, mode, page = 0) {
if (page > 0) {
generatePrevButton = true;
}
if ((page + 1) * pageCount < totalCount) {
generateNextButton = true;
}
@@ -413,25 +396,29 @@ function showDetailIndividual(songId, mode, page = 0) {
`;
// generate pagination buttons
if (generatePrevButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
Prev Page
</button>
`;
}
html += `
<div class="pagination-container">
${generatePrevButton ? `
<button class="pagination-button prev-button"
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
Prev Page
</button>
` : '<div class="placeholder"></div>'}
if (generateNextButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
Next Page
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' class="pagination-button">
Back
</button>
`;
}
${generateNextButton ? `
<button class="pagination-button next-button"
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
Next Page
</button>
` : '<div class="placeholder"></div>'}
</div>
`;
// Loop leaderboard ranks
html += `<div class="leaderboard-players">`;
for (let i = 0; i < rankingList.length; i++) {
userData = rankingList[i];
@@ -449,31 +436,7 @@ function showDetailIndividual(songId, mode, page = 0) {
}
// Generate pagination buttons again
if (generatePrevButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailIndividual(${songId}, ${mode}, ${page - 1})">
Prev Page
</button>
`;
}
if (generateNextButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
onclick="showDetailIndividual(${songId}, ${mode}, ${page + 1})">
Next Page
</button>
`;
}
html += `
<button onclick='restoreBaseStructure();loadUI();filterList();loadList();' style="margin-top: 20px;" class="bt_bg01">
Back
</button>
`;
html += `</div>`;
document.getElementById('ranking_box').innerHTML = html;

View File

@@ -20,8 +20,8 @@
padding: 20px;
}
.dlc_logo {
width: 100%;
max-width: 500px;
width: 70vw;
max-width: 1000px;
margin-bottom: 10px;
}
.dlc_extra_body {
@@ -34,19 +34,19 @@
padding: 0;
}
.text-content {
font-size: 18px;
font-size: 36px;
line-height: 1.6;
margin-bottom: 30px;
}
.buy-button {
display: inline-block;
width: 160px;
height: 110px;
width: 30vw;
height: 20vw;
background: url('/files/web/frame_buy.png') no-repeat center center;
background-size: contain;
border: none;
color: white;
font-size: 22px;
font-size: 30px;
font-weight: bold;
padding: 15px;
cursor: pointer;
@@ -54,26 +54,26 @@
}
.quit-button {
display: inline-block;
width: 150px;
height: 30px;
width: 33vw;
height: 10vw;
background: url('/files/web/quit_button.png') no-repeat center center;
background-size: contain;
border: none;
color: white;
font-size: 20px;
font-size: 30px;
font-weight: bold;
cursor: pointer;
position: relative;
}
.buy-button-extra {
display: inline-block;
width: 160px;
height: 110px;
width: 30vw;
height: 20vw;
background: url('/files/web/frame_buy_extra.png') no-repeat center center;
background-size: contain;
border: none;
color: white;
font-size: 22px;
font-size: 30px;
font-weight: bold;
padding: 15px;
cursor: pointer;
@@ -81,13 +81,13 @@
}
.quit-button-extra {
display: inline-block;
width: 150px;
height: 30px;
width: 33vw;
height: 10vw;
background: url('/files/web/quit_button_extra.png') no-repeat center center;
background-size: contain;
border: none;
color: white;
font-size: 20px;
font-size: 30px;
font-weight: bold;
cursor: pointer;
position: relative;
@@ -99,8 +99,13 @@
margin-top: 10px;
}
.coin-icon {
width: 30px;
height: 30px;
width: 7vw;
height: 7vw;
margin-left: 5px;
}
.coin-price {
font-size: 30px;
font-weight: bold;
margin-left: 5px;
}
body {
@@ -131,7 +136,12 @@ html {
.d_ib{display:inline-block;}
.mb10p{margin-bottom:10%;}
.f90{font-size:90%;}
.f80{font-size:70%;}
.pt50{padding-top:50px;}
.f20{font-size:20%;}
.f30{font-size:30%;}
.f40{font-size:40%;}
.f50{font-size:50%;}
.f60{font-size:60%;}
.f70{font-size:70%;}
.plr25{padding-left:25px;padding-right:25px;}
@@ -156,7 +166,6 @@ div#wrapper{
.wrapper_box{
text-align:center;
margin-top:5%;
margin-bottom:-10%;
display: inline-block;
position: relative;
width: 100%;
@@ -261,6 +270,12 @@ div#wrapper{
list-style: none;
padding: 0;
margin: 0;
flex-grow: 1;
overflow-y: auto;
height: 0;
margin: 0;
padding: 0;
list-style: none;
}
.song-item {
@@ -295,6 +310,11 @@ div#wrapper{
.song-name {
font-size: 26px;
flex-shrink: 0;
}
.mode-buttons {
flex-shrink: 0;
}
.composer-name-owned {
@@ -319,7 +339,6 @@ div#wrapper{
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.player-element {
display: flex;
@@ -333,6 +352,7 @@ div#wrapper{
height: 100px;
width: 98%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
flex-shrink: 0;
}
.player-info {
@@ -376,14 +396,6 @@ div#wrapper{
border-radius: 8px;
padding: 20px;
}
.leaderboard-container {
height: calc(53vh);
overflow-y: auto;
border: 1px solid #444;
margin: 20px auto;
padding: 10px;
background-color: #111;
}
.leaderboard-player {
display: flex;
@@ -460,3 +472,76 @@ div#wrapper{
cursor: pointer;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.2);
}
#ranking_content {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.song-list {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 0;
margin: 0;
list-style: none;
}
#ranking_searchbar,
#ranking_sort {
flex-shrink: 0;
}
#ranking_box {
display: flex;
flex-direction: column;
height: 90vh;
overflow: hidden;
}
.leaderboard-players {
flex-grow: 1;
height: 90%;
overflow-y: auto;
margin: 0;
list-style: none;
}
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin: 10px 0;
}
.pagination-button {
width: 170px;
height: 40px;
background-color: #000000;
color: #FFFFFF;
border: 1px solid #808080;
cursor: pointer;
font-size: 30px;
text-align: center;
}
.pagination-button:hover {
background-color: #333333;
}
.placeholder {
flex: 0.25;
}
.pagination-button-shop {
width: 170px;
height: 40px;
margin: 10px;
background-color: #000000;
color: #FFFFFF;
font-size: 24px;
}

View File

@@ -101,17 +101,17 @@ function generateMenuContent() {
if (shopMode === 0 && stagePage === 0) {
html += `
<a onclick="showFMax()">
<img src="/files/web/dlc_4max.jpg" style="width: 90%; margin-bottom: 110px; margin-top: -100px;" />
<img src="/files/web/dlc_4max.jpg" style="width: 90%; margin-bottom: 110px; margin-top: -80px;" />
</a><br>
<a onclick="showExtra()">
<img src="/files/web/dlc_extra.jpg" style="width: 90%; margin-bottom: 20px; margin-top: -100px;" />
<img src="/files/web/dlc_extra.jpg" style="width: 90%; margin-bottom: 20px; margin-top: -80px;" />
</a><br>
`;
}
if (generatePrevButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
<button class="pagination-button-shop"
onclick="prevPage(${shopMode})">
Prev Page
</button>
@@ -120,7 +120,7 @@ function generateMenuContent() {
if (generateNextButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
<button class="pagination-button-shop"
onclick="nextPage(${shopMode})">
Next Page
</button>
@@ -144,7 +144,7 @@ function generateMenuContent() {
if (generatePrevButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
<button class="pagination-button-shop"
onclick="prevPage(${shopMode})">
Prev Page
</button>
@@ -153,7 +153,7 @@ function generateMenuContent() {
if (generateNextButton) {
html += `
<button style="width: 170px; height: 40px; margin: 10px; background-color: #000000; color: #FFFFFF;"
<button class="pagination-button-shop"
onclick="nextPage(${shopMode})">
Next Page
</button>
@@ -231,10 +231,10 @@ function showItemDetail(mode, itemId) {
<div class="image-container">
<img src="/files/image/icon/shop/${itemId}.jpg" alt="Item Image" style="width: 180px; height: 180px;" />
</div>
<p>Would you like to purchase this song?</p>
<p class="f80">Would you like to purchase this song?</p>
<div>
<p>${payload.data.property_first} - ${payload.data.property_second}</p>
<p>Difficulty Levels: ${payload.data.property_third}</p>
<p class="f80">${payload.data.property_first} - ${payload.data.property_second}</p>
<p class="f80">Difficulty Levels: ${payload.data.property_third}</p>
</div>
<div>
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
@@ -247,10 +247,10 @@ function showItemDetail(mode, itemId) {
<div class="image-container">
<img src="/files/image/icon/avatar/${itemId}.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>
<p class="f80">Would you like to purchase this avatar?</p>
<div>
<p>${payload.data.property_first}</p>
<p>Effect: ${payload.data.property_second}</p>
<p class="f80">${payload.data.property_first}</p>
<p class="f80">Effect: ${payload.data.property_second}</p>
</div>
<div>
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
@@ -263,10 +263,10 @@ function showItemDetail(mode, itemId) {
<div class="image-container">
<img src="/files/image/icon/item/${itemId}.png" alt="Item Image" style="width: 180px; height: 180px;" />
</div>
<p>Would you like to purchase this item?</p>
<p class="f80">Would you like to purchase this item?</p>
<div>
<p>${payload.data.property_first}</p>
<p>Effect: ${payload.data.property_second}</p>
<p class="f80">${payload.data.property_first}</p>
<p class="f80">Effect: ${payload.data.property_second}</p>
</div>
<div>
<img src="/files/web/coin_icon.png" class="coin-icon" style="width: 40px; height: 40px;" alt="Coin Icon" />
@@ -329,19 +329,17 @@ function purchaseItem(mode, itemId) {
if (payload.state === 1) {
contentElement.innerHTML = `
<p>${payload.message}</p>
<button style="margin-top: 20px;" class="bt_bg01"
onclick="on_initialize();">
Back
</button>
<div style="margin-top: 20px;">
<a onclick="on_initialize();" class="bt_bg01">Back</a><br>
</div>
`;
document.getElementById('coinCounter').innerText = payload.data.coin;
} else {
contentElement.innerHTML = `
<p>Purchase failed: ${payload.message}</p>
<button style="margin-top: 20px;" class="bt_bg01"
onclick="on_initialize();">
Back
</button>
<div style="margin-top: 20px;">
<a onclick="on_initialize();" class="bt_bg01">Back</a><br>
</div>
`;
}
})
@@ -349,10 +347,9 @@ function purchaseItem(mode, itemId) {
console.error('Error processing purchase:', error);
contentElement.innerHTML = `
<p>Failed to process your purchase. Please try again later.</p>
<button style="margin-top: 20px;" class="bt_bg01"
onclick="on_initialize();">
Back
</button>
<div style="margin-top: 20px;">
<a onclick="on_initialize();" class="bt_bg01">Back</a><br>
</div>
`;
});
}
@@ -373,7 +370,7 @@ function restoreBaseStructure() {
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<ul style="list-style-type: none; padding: 0; margin-top: 100px; text-align: center;" id="menuList">
<ul style="list-style-type: none; padding: 0; margin-top: 60px; text-align: center;" id="menuList">
</ul>
<div class="f90 a_center pt50" id="content">
Loading...
@@ -446,13 +443,13 @@ function showFMax() {
<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>
<p>After purchasing, you will have access to support information and update logs.</p>
</div>
<button class="buy-button" onclick="purchaseItem(3, 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;">${payload.data.price}</span>
<span class="coin-price">${payload.data.price}</span>
</div>
</button>
<br><br>
@@ -537,7 +534,7 @@ function showExtra() {
let html = '';
if (extraPurchased) {
html = `
<br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br>
<div class="text-content">
<p>You have unlocked the EXTRA Challenge!</p>
<p>Please report bugs/missing tracks to Discord: #AnTcfgss, or QQ 3421587952.</p>
@@ -548,7 +545,7 @@ function showExtra() {
`;
} else {
html = `
<br><br><br><br><br><br><br>
<div style="margin-top: 30vh;"></div>
<div class="text-content">
<p>Are you looking for a bad time?</p>
<p>If so, this is the Ultimate - Extra - Challenge.</p>
@@ -560,7 +557,7 @@ function showExtra() {
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;">${payload.data.price}</span>
<span class="coin-price">${payload.data.price}</span>
</div>
</button>
<br><br>
@@ -570,7 +567,12 @@ function showExtra() {
`;
}
document.body.innerHTML = html;
document.body.innerHTML = `
<div class="dlc_container">
${html}
</div>
`;;
} else {
document.body.innerHTML = `
<div class="dlc_container">

View File

@@ -13,18 +13,18 @@
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<div class="f90 a_center pt50">This is a Private Server</div>
<div class="f50 a_center pt50">This is a Private Server</div>
<hr />
<div class="f70 plr25">Your text here</div>
<div class="f30 plr25">Your text here</div>
</div>
</div>
</div>
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<div class="f90 a_center pt50">About Coins</div>
<div class="f50 a_center pt50">About Coins</div>
<hr />
<div class="f70 plr25">Coins are an in-game currency used within GROOVE COASTER ZERO.
<div class="f30 plr25">Coins are an in-game currency used within GROOVE COASTER ZERO.
They may be used to purchase special tracks, Avatars, and Items unavailable through regular game play.
Spent Coins will not be refunded under any circumstances.
Please note that Coin-related records are stored online, so a network connection is required for Coin use.
@@ -35,9 +35,9 @@
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<div class="f90 a_center pt50">Regarding Data Backup</div>
<div class="f50 a_center pt50">Regarding Data Backup</div>
<hr />
<div class="f70 plr25">Please note that while the Backup/TAITO ID Registration option may be used to save play data.
<div class="f30 plr25">Please note that while the Backup/TAITO ID Registration option may be used to save play data.
Item stock information and replays are not saved.
Contrary to the official server, you can log out of the TAITO ID, change username, and password.
However, each account can only be linked to one device at a time.
@@ -50,9 +50,9 @@
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<div class="f90 a_center pt50">Regarding Online Use</div>
<div class="f50 a_center pt50">Regarding Online Use</div>
<hr />
<div class="f70 plr25">Once the application's tutorial is complete the game may be played offline (without a network connection).
<div class="f30 plr25">Once the application's tutorial is complete the game may be played offline (without a network connection).
However, please note that a network connection is required for certain operations, including track data downloads, SHOP use, save backup/restore, etc.
</div>
</div>
@@ -61,36 +61,36 @@
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<div class="f90 a_center pt50">About HELP and OPTIONS</div>
<div class="f4 0 a_center pt50 f60">About HELP and OPTIONS</div>
<hr />
<div class="f70 plr25">Tapping on the question mark (?) in the top left of the screen will bring up the application's help and settings.</div>
<div class="f30 plr25">Tapping on the question mark (?) in the top left of the screen will bring up the application's help and settings.</div>
<br />
<div class="f70 plr25">There are many different settings under Options in the Help menu for you to customize to your liking.</div>
<div class="f30 plr25">There are many different settings under Options in the Help menu for you to customize to your liking.</div>
<br />
<div class="f70 plr25">- SCREEN SETTINGS</div>
<div class="f60 plr50">This menu is where you may adjust the visuals if you feel like the frame rate is unstable, or you want to disable AD-LIBS or Hit SFX when there's no TARGET to hit.</div>
<div class="f30 plr25">- SCREEN SETTINGS</div>
<div class="f30 plr50">This menu is where you may adjust the visuals if you feel like the frame rate is unstable, or you want to disable AD-LIBS or Hit SFX when there's no TARGET to hit.</div>
<br />
<div class="f70 plr25">- SOUND AND VISUAL TIMING ADJUSTMENT</div>
<div class="f60 plr50">Adjust the settings here when you feel like the visuals or audio is not in sync.</div>
<div class="f60 plr50">If you feel the visuals are ahead of the sound press the - button, if they're behind then use the + button. These buttons will adjust the visual display timing with the audio.</div>
<div class="f30 plr25">- SOUND AND VISUAL TIMING ADJUSTMENT</div>
<div class="f30 plr50">Adjust the settings here when you feel like the visuals or audio is not in sync.</div>
<div class="f30 plr50">If you feel the visuals are ahead of the sound press the - button, if they're behind then use the + button. These buttons will adjust the visual display timing with the audio.</div>
<br />
<div class="f70 plr25">- INPUT REGISTRY ADJUSTMENT</div>
<div class="f60 plr50">Adjust these settings if even though you feel like you're right in time with the beat, you still don't get a GREAT rating.</div>
<div class="f60 plr50">
<div class="f30 plr25">- INPUT REGISTRY ADJUSTMENT</div>
<div class="f30 plr50">Adjust these settings if even though you feel like you're right in time with the beat, you still don't get a GREAT rating.</div>
<div class="f30 plr50">
Press the - button if you feel like you have to press earlier than you should for a GREAT rating, or press the + button if you feel like you have to press later than you should for a GREAT rating.
</div>
<br />
<div class="f70 plr25">■MICROPHONE INPUT SENSITIVITY</div>
<div class="f60 plr50">This setting will help to make playing ORIGINAL STYLE more smooth.</div>
<div class="f30 plr25">■MICROPHONE INPUT SENSITIVITY</div>
<div class="f30 plr50">This setting will help to make playing ORIGINAL STYLE more smooth.</div>
</div>
</div>
</div>
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<p class="a_center">About Arcade Mode</p>
<p class="a_center f60">About Arcade Mode</p>
<hr />
<p class="f60 plr50">
<p class="f30 plr50">
Now you can play Arcade Mode which lets you enjoy a the same experience you'd have on a Groove Coaster arcade machine!<br />
<br />
*Please be aware that you will not be able to play Arcade Mode, even if you own it, if you have not yet cleared the NORMAL difficulty on the specified track.
@@ -101,9 +101,9 @@
<div id="wrapper">
<div class="wrapper_box">
<div class="a_left w100 d_ib mb10p">
<p class="a_center">Play the game by making sounds!</p>
<p class="a_center f60">Play the game by making sounds!</p>
<hr />
<p class="f60 plr50">
<p class="f30 plr50">
★What's Original Style?<br />
<img src="./files/web/Info1.gif" class="w90 d_bl m_auto" /><br />
It gives you a brand new way to play the game by making sounds!<br />

View File

@@ -33,7 +33,7 @@
<body>
<div class="login-card">
<div class="d-flex justify-content-center align-items-center mb-4">
<h3 class="text-center mb-0">Admin Login</h3>
<h3 class="text-center mb-0">GC2OS Login</h3>
<button type="button" class="btn btn-link ms-2 p-0" id="infoBtn" aria-label="Info" style="font-size:1.5rem;">
<span class="bi bi-info-circle" style="vertical-align:middle;"></span>
</button>
@@ -95,7 +95,12 @@
document.getElementById('infoBtn').addEventListener('click', function() {
document.getElementById('loginModalLabel').innerText = "User Center Login";
document.getElementById('loginModalBody').innerHTML =
`<p>Don't do anything funny! Don't even think about it!</p>`;
`
<p>Don't do anything funny! Don't even think about it!</p>
<p>Jokes aside, this login is for accessing your user center where you can view your stats and manage your account.</p>
<p>If you don't have an account yet, please register in-game first.</p>
<p>Depending on binding settings, you might need to bind your account to email/discord before the user center becomes accessible.</p>
`;
const modal = new bootstrap.Modal(document.getElementById('loginModal'));
modal.show();
});

View File

@@ -20,60 +20,150 @@
<body>
<div class="container mt-5">
<div class="text-center">
<!-- Title -->
<h1 id="welcome-title">Hello, {username}</h1>
<!-- Export Data Button -->
<button id="export-button" class="btn btn-primary mt-3">Export Data</button>
<button id="export-button" class="btn btn-primary mt-3" onclick="openExportModal()">Export Data</button>
</div>
</div>
<script>
// Function to get a cookie value by name
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
<!-- Export Save Data Modal -->
<div class="modal fade" id="exportModal" tabindex="-1" aria-labelledby="exportModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Export Save Data</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>You can export your save data to back it up or transfer it to another server.</p>
<p id="next-export-time"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="export-data-button" onclick="exportSaveData()">Export Data</button>
</div>
</div>
</div>
</div>
<script>
let nextSaveExportTimestamp = 0;
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
async function fetchUserData() {
const token = getCookie("token");
if (!token) {
console.error("Token not found in cookies.");
return;
}
// Fetch user data from the API
async function fetchUserData() {
const token = getCookie("token"); // Read the token from the cookie
if (!token) {
console.error("Token not found in cookies.");
try {
const response = await fetch("/usercenter/api", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: token,
action: "basic"
})
});
if (!response.ok) {
console.error("Failed to fetch user data:", response.statusText);
return;
}
try {
// Query the API
const response = await fetch("/usercenter/api", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
token: token,
action: "basic"
})
});
// Parse the response data
const response_data = await response.json();
const username = response_data.data.username || "User";
nextSaveExportTimestamp = response_data.data.next_save_export || 0;
if (!response.ok) {
console.error("Failed to fetch user data:", response.statusText);
return;
}
// Parse the response data
const response_data = await response.json();
const username = response_data.data.username || "User";
document.getElementById("welcome-title").textContent = `Hello, ${username}`;
} catch (error) {
console.error("Error fetching user data:", error);
}
}
// Update the username in the title
document.getElementById("welcome-title").textContent = `Hello, ${username}`;
} catch (error) {
console.error("Error fetching user data:", error);
}
async function openExportModal() {
const nextExportTime = new Date(nextSaveExportTimestamp * 1000); // Convert UNIX timestamp to milliseconds
const now = new Date();
// Check if the export is available
if (nextSaveExportTimestamp > Math.floor(now.getTime() / 1000)) {
const timeRemaining = Math.ceil((nextSaveExportTimestamp - now.getTime() / 1000) / 60);
document.getElementById("next-export-time").textContent = `You can export your data again in approximately ${timeRemaining} minutes.`;
document.getElementById("export-data-button").disabled = true; // Disable the button if export is not available
} else {
document.getElementById("next-export-time").textContent = "You can export your data now.";
document.getElementById("export-data-button").disabled = false; // Enable the button
}
// Call the function to fetch user data when the page loads
document.addEventListener("DOMContentLoaded", fetchUserData);
</script>
// Show the modal
const exportModal = new bootstrap.Modal(document.getElementById("exportModal"));
exportModal.show();
}
async function exportSaveData() {
const token = getCookie("token");
if (!token) {
console.error("Token not found in cookies.");
return;
}
try {
const response = await fetch("/usercenter/export_data", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
});
if (!response.ok) {
console.error("Failed to export data:", response.statusText);
return;
}
// Check if the response is JSON or a file stream
const contentType = response.headers.get("Content-Type");
if (contentType && contentType.includes("application/json")) {
const responseData = await response.json();
if (responseData.status === "failed") {
alert(responseData.message); // Show the failure message
}
} else {
// Handle file download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "save_export.xlsx"; // Default filename
document.body.appendChild(a);
a.click();
a.remove();
const exportModal = bootstrap.Modal.getInstance(document.getElementById("exportModal"));
if (exportModal) {
exportModal.hide();
}
fetchUserData();
}
} catch (error) {
console.error("Error exporting save data:", error);
}
}
document.addEventListener("DOMContentLoaded", fetchUserData);
</script>
</body>
</html>