总览

Hello~ 我是 YinMo19. 这是一个关于 Arcaea Server Rust 版本的开发文档。我将使用 rust/rocket 进行开发。

先做一点简单的 Q&A。

  • 这是什么?

    Arcaea 是一款很好玩的游戏...... 如果你不知道的话,可以去 官网 看看。

  • 是否已经有案例?

    是的。 Lost-MSth 已经开发了一版 Arcaea Server。 如果你现在想要使用的话,请前往他的仓库查看。他的项目已经是成熟的,可以游玩。

  • 为什么还要开发?

    因为我正在学习 Rust,而且我已经想做一个这样的服务器挺久的了...... 另外,使用 rust 预期可以获得更高的性能,并且我还想要基于这个服务器再创建一个不仅仅只是管理员可用的后台,而是可以让所有玩家查询 b30、谱面信息的后台。并且我还预期在这个后台做一个小论坛、可供玩家讨论曲子。

可以看到目标还是很丰满的。当然并非空想,其中小论坛的雏形我已经写了一个简单的版本,可以在 Chatroomrelease 版本中试用。

如果不出意外的话,我将会在 2025年1月-3月之间积极开发,这个时间段我正好放寒假...... 如果你有兴趣的话可以观望...... 或者成为这个私服的测试者!帮我反馈问题也是对我很大的帮助。关于使用相关内容将不会在这里讲述。如果你想要使用本服务器,你可能需要一些简单的逆向知识。Arcaea 客户端对服务器地址进行了加密,你无法进行简单的更改。我的 博客 中简单介绍了这部分的一点点内容,如果你有兴趣的话,可以参考一下。

本 Book 使用 mdbook 进行编写,这是大部分 rust 语言文档使用的方案,所以如果你已经是一个 rusty 的用户,你应该很熟悉!

最后将要非常非常非常感谢 Lost-MSth 以及他的服务器,他真的帮助了我很多!

项目目录

https://github.com/YinMo19/Arcaea_server_rs

关于我

如果你有什么关于这个项目的想法、建议、问题,可以通过这个邮箱联系我! Arcaea@yinmo19.top

Arcaea 服务器核心

服务器核心是处理从 Arcaea 客户端发送的请求部分。这部分主要有

  • 用户登陆/注册
  • 歌曲成绩获取(世界/好友/个人排名)
  • 歌曲上传
  • 课题模式
  • 谱面下载
  • 世界模式
  • Link Play
  • ...

这些内容均涉及到数据库操作,因此这里我们需要先确定数据库类型。由于体量不大,所以我打算使用 sqlite。

在实现基础功能的基础上,我希望网页端可以轻松查询任何时候的 b30 或者其他成绩,所以我打算将每次游戏的成绩存储在数据库中。若所有用户的成绩存在同一个表单中可能会降低数据库查询速率,所以我可能会分为多个数据库,例如

// in directory "databases"
core.db
user_score.db
user_ptt_history.db
forum.db
...

而例如 user_score.db 中,我可能会将每个用户的 id 作为数据库的表名称,表内的数据为每一次游玩的数据。这样可以方便的查找所有玩家的历史数据。

注册/登陆

这一部分非常重要。如果不想要让用户随意的创建账号,那么我们需要小心谨慎。

注册

注册接口

注册账号有几种方案:

  • 直接通过
  • 通过邮件验证
  • 实现一个请求列表、管理员在后台通过
  • 第二点和第三点同时实现

我们预计将会实现这几个功能。启用这些功能通过寻找环境变量判断。如果服务器启动时在环境变量中找到关于邮件服务器的配置,那么将会启用邮件功能。

预计使用 lettre crate 实现邮件服务。用户需要在环境变量中设置

ARCSERVER_ADMIN_MAIL=M<your mail>
ARCSERVER_MAIL_SMTP_SERVER=<your smtp server>
ARCSERVER_MAIL_PASSWORD=<password>

当这三个参数同时检测到的时候才会启动邮件服务。在官方案例中

extern crate lettre;
extern crate lettre_email;
extern crate mime;

use lettre_email::Email;
use lettre::smtp::authentication::Credentials;
use lettre::{SmtpClient, Transport};

fn main() {

    let email_receiver = "YOUR_TARGET_EMAIL";
    let mine_email = "YOUR_GMAIL_ADDRESS";
    let smtp_server = "smtp.gmail.com";
    let password = "YOUR_GMAIL_APPLICATION_PASSWORD"; //需要生成应用专用密码

    let email = Email::builder()
        .to(email_receiver)
        .from(mine_email)
        .subject("subject")
        .html("<h1>Hi there</h1>")
        .text("Message send by lettre Rust")
        .build()
        .unwrap();

    let creds = Credentials::new(
        mine_email.to_string(),
        password.to_string(),
    );

    // Open connection to Gmail
    let mut mailer = SmtpClient::new_simple(smtp_server)
        .unwrap()
        .credentials(creds)
        .transport();

    // Send the email
    let result = mailer.send(email.into());

    if result.is_ok() {
        println!("Email sent");
    } else {
        println!("Could not send email: {:?}", result);
    }

    print!("{:?}", result);
    mailer.close();
}

可以看到邮件收发并不困难。

邮件发送内容会是一个 url,它应该类似于

https://<your_domain(:port)>/<api_endpoint>/auth?auth_string=<random_string>

这个随机字符串在用户注册的时候生成,并储存在用户信息中。服务器在接受到这个请求之后查询数据库,判断这个字符串是否存在、是否在规定时间内,以此实现验证。

管理员后台验证应该是默认启用的模式。如果要实现没有任何验证(这应该是别的功能测试的时候使用的,非常不建议正式使用)应该使用环境变量

ARCSERVER_NO_ADMIN_AUTH=true

来指定。否则在管理员页面将会显示一个表单,可以看到目前注册是否通过。

用户注册之后,数据库填入的字段有

user_id,
name,
password,
join_date,
user_code,
rating_ptt,
highest_rating_ptt,
character_id,
is_skill_sealed,
is_char_uncapped,
is_char_uncapped_override,
is_hide_rating,
favorite_character,
max_stamina_notification_enabled,
current_map,
ticket,
prog_boost,
email

相关接口

注册接口

('user/', methods=['POST'])

邮箱验证进度查询

('auth/verify', methods=['POST'])

邮箱验证重发

('user/email/resend_verify', methods=['POST']) 

删除账号

('user/me/request_delete', methods=['POST']) 

登陆

登陆接口

('auth/login', methods=['POST'])

需要实现的功能有

  • 验证客户端版本号
  • 验证设备信息
  • 查验 ip 地址
    • 多端登陆自动封号
  • 验证账号密码
  • ...

返回字段有

{
    "success": True,
    "token_type": "Bearer", 
    "user_id": user.user_id, 
    "access_token": user.token
}

Linkplay

课题模式

歌曲下载

歌曲购买

歌曲信息

排名信息

世界模式

礼物/兑换券

角色

网页

网页部分主要是设计前后端交互的接口。我们预计使用的前端框架是 dioxus,这是一个用 rust 语言编写的框架,可以构建出包含所有平台的 app 应用。设计上使用 tailwindcss 来设计页面,会借鉴各种模板使其更加美观。

首先是登陆与鉴权。权限控制方面需要单独设计,目前的方案是两级权限,管理员权限和普通玩家权限。网页端可操作的数据可能并不完全,所谓的超级管理员权限可能就是单杀服务器改数据库(x

需要设计的前端页面主要有

  • 登陆前的欢迎页面
  • 登陆页面
  • 登陆之后的面板
  • 查询 b30 的页面
    • 查询b30接口
    • 查询历史b30接口
  • 查询ptt历史变化
  • 查询每首歌的最高成绩
    • 歌曲下面可以插入论坛接口(以歌曲名称作为标题)
  • ...

前端要设计的好看确实很需要花时间,如果你也想要设计一个超级酷炫的前端页面,一定要联系我!可以给我发送邮件 Arcaea@yinmo19.top

欢迎页面

登陆页面

面板

b30查询

ptt历史查询

每首歌的成绩

论坛

小论坛会挂在相同的网页上,作为一个子功能。目前我已有一个相似的项目 Chatroom

核心的实现思路是,在用户第一次访问的时候获取历史信息(并缓存),之后点入特定的论坛,会获取一个 EventStream,这个事件流订阅了一个消息队列,这个消息队列的所有新增消息会实时的 Send 给用户,实现一个简单的通讯功能。

use super::database::MessageLog;
use super::models::Message;
// use super::utils::DateTimeWrapper;
use rocket::form::Form;
use rocket::response::stream::{Event, EventStream};
use rocket::serde::json::Json;
use rocket::tokio::select;
use rocket::tokio::sync::broadcast::{error::RecvError, Sender};
use rocket::{Shutdown, State};
use rocket_db_pools::Connection;
use std::net::IpAddr;

#[get("/events")]
pub async fn events(queue: &State<Sender<Message>>, mut end: Shutdown) -> EventStream![] {
    let mut rx = queue.subscribe();
    EventStream! {
        loop {
            let msg = select! {
                msg = rx.recv() => match msg {
                    Ok(msg) => msg,
                    Err(RecvError::Closed) => break,
                    Err(RecvError::Lagged(_)) => continue,
                },
                _ = &mut end => break,
            };

            yield Event::json(&msg);
        }
    }
}

#[post("/message", data = "<form>")]
pub async fn post(
    mut db: Connection<MessageLog>,
    form: Form<Message>,
    queue: &State<Sender<Message>>,
    client_ip: Option<IpAddr>,
) {
    // println!("New message from {:?}", client_ip);
    let message = form.into_inner();
    let _res = queue.send(message.clone());

    let query = r#"
        INSERT INTO messages (room, username, message, ip_addr)
        VALUES (?, ?, ?, ?)
    "#;

    let _result = sqlx::query(&query)
        .bind(&message.room)
        .bind(&message.username)
        .bind(&message.message)
        .bind(&client_ip.unwrap().to_string())
        .fetch_all(&mut **db)
        .await
        .expect("Failed to insert message");
}

#[get("/history?<room>")]
pub async fn get_history(
    mut db: Connection<MessageLog>,
    room: String,
) -> Result<Json<Vec<Message>>, rocket::response::status::Custom<String>> {
    let query = r#"
        SELECT * FROM messages WHERE room = ? ORDER BY id DESC LIMIT 500
    "#;

    let mut messages = sqlx::query_as(&query)
        .bind(room)
        .fetch_all(&mut **db)
        .await
        .map_err(|e| {
            rocket::response::status::Custom(
                rocket::http::Status::InternalServerError,
                format!("Database error: {}", e),
            )
        })?;

    messages.reverse();
    Ok(Json(messages))
}

这是之前已经实现的一些内容,可以简单的参考。

关于用户鉴权方面,直接使用核心数据库中的用户信息即可。用户鉴权方面应该放在网页端入口处实现,论坛发送消息时应该再次校验。

论坛首页

帖子

位于歌曲下的帖子