mirror of
https://github.com/redstrate/Kawari.git
synced 2025-04-26 16:37:46 +00:00
Move login & world database code/logic to their own modules and structs
This should remove some of the pollution I added while working on these features.
This commit is contained in:
parent
4b67b22c9f
commit
f523aa189f
7 changed files with 365 additions and 348 deletions
|
@ -1,107 +1,16 @@
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
use axum::response::{Html, Redirect};
|
use axum::response::{Html, Redirect};
|
||||||
use axum::routing::post;
|
use axum::routing::post;
|
||||||
use axum::{Form, Router, routing::get};
|
use axum::{Form, Router, routing::get};
|
||||||
use rand::Rng;
|
use kawari::login::{LoginDatabase, LoginError};
|
||||||
use rand::distributions::Alphanumeric;
|
|
||||||
use rusqlite::Connection;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
fn generate_sid() -> String {
|
|
||||||
let random_id: String = rand::thread_rng()
|
|
||||||
.sample_iter(&Alphanumeric)
|
|
||||||
.take(56)
|
|
||||||
.map(char::from)
|
|
||||||
.collect();
|
|
||||||
random_id.to_lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum LoginError {
|
|
||||||
WrongUsername,
|
|
||||||
WrongPassword,
|
|
||||||
InternalError,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct LoginServerState {
|
struct LoginServerState {
|
||||||
connection: Arc<Mutex<Connection>>,
|
database: Arc<LoginDatabase>,
|
||||||
}
|
|
||||||
|
|
||||||
impl LoginServerState {
|
|
||||||
/// Adds a new user to the database.
|
|
||||||
fn add_user(&self, username: &str, password: &str) {
|
|
||||||
let connection = self.connection.lock().unwrap();
|
|
||||||
|
|
||||||
tracing::info!("Adding user with username {username}");
|
|
||||||
|
|
||||||
let query = "INSERT INTO users VALUES (?1, ?2);";
|
|
||||||
connection
|
|
||||||
.execute(query, (username, password))
|
|
||||||
.expect("Failed to write user to database!");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Login as user, returns a session id.
|
|
||||||
fn login_user(&self, username: &str, password: &str) -> Result<String, LoginError> {
|
|
||||||
let selected_row: Result<(String, String), rusqlite::Error>;
|
|
||||||
|
|
||||||
tracing::info!("Finding user with username {username}");
|
|
||||||
|
|
||||||
{
|
|
||||||
let connection = self.connection.lock().unwrap();
|
|
||||||
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT username, password FROM users WHERE username = ?1")
|
|
||||||
.map_err(|_err| LoginError::WrongUsername)?;
|
|
||||||
selected_row = stmt.query_row((username,), |row| Ok((row.get(0)?, row.get(1)?)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok((_user, their_password)) = selected_row {
|
|
||||||
if their_password == password {
|
|
||||||
return self
|
|
||||||
.create_session(username)
|
|
||||||
.ok_or(LoginError::InternalError);
|
|
||||||
} else {
|
|
||||||
return Err(LoginError::WrongPassword);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(LoginError::WrongUsername)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new session for user, which replaces the last one (if any)
|
|
||||||
fn create_session(&self, username: &str) -> Option<String> {
|
|
||||||
let connection = self.connection.lock().unwrap();
|
|
||||||
|
|
||||||
let sid = generate_sid();
|
|
||||||
|
|
||||||
connection
|
|
||||||
.execute(
|
|
||||||
"INSERT OR REPLACE INTO sessions VALUES (?1, ?2);",
|
|
||||||
(username, &sid),
|
|
||||||
)
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
tracing::info!("Created new session for {username}: {sid}");
|
|
||||||
|
|
||||||
Some(sid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if there is a valid session for a given id
|
|
||||||
fn check_session(&self, sid: &str) -> bool {
|
|
||||||
let connection = self.connection.lock().unwrap();
|
|
||||||
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT username, sid FROM sessions WHERE sid = ?1")
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
let selected_row: Result<(String, String), rusqlite::Error> =
|
|
||||||
stmt.query_row((sid,), |row| Ok((row.get(0)?, row.get(1)?)));
|
|
||||||
|
|
||||||
selected_row.is_ok()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -134,7 +43,7 @@ async fn login_send(
|
||||||
State(state): State<LoginServerState>,
|
State(state): State<LoginServerState>,
|
||||||
Form(input): Form<Input>,
|
Form(input): Form<Input>,
|
||||||
) -> Html<String> {
|
) -> Html<String> {
|
||||||
let user = state.login_user(&input.sqexid, &input.password);
|
let user = state.database.login_user(&input.sqexid, &input.password);
|
||||||
match user {
|
match user {
|
||||||
Ok(session_id) => Html(format!(
|
Ok(session_id) => Html(format!(
|
||||||
"window.external.user(\"login=auth,ok,sid,{session_id},terms,1,region,2,etmadd,0,playable,1,ps3pkg,0,maxex,4,product,1\");"
|
"window.external.user(\"login=auth,ok,sid,{session_id},terms,1,region,2,etmadd,0,playable,1,ps3pkg,0,maxex,4,product,1\");"
|
||||||
|
@ -181,7 +90,7 @@ async fn do_register(
|
||||||
panic!("Expected password!");
|
panic!("Expected password!");
|
||||||
};
|
};
|
||||||
|
|
||||||
state.add_user(&username, &password);
|
state.database.add_user(&username, &password);
|
||||||
|
|
||||||
Redirect::to("/")
|
Redirect::to("/")
|
||||||
}
|
}
|
||||||
|
@ -196,38 +105,20 @@ async fn check_session(
|
||||||
State(state): State<LoginServerState>,
|
State(state): State<LoginServerState>,
|
||||||
Query(params): Query<CheckSessionParams>,
|
Query(params): Query<CheckSessionParams>,
|
||||||
) -> String {
|
) -> String {
|
||||||
if state.check_session(¶ms.sid) {
|
if state.database.check_session(¶ms.sid) {
|
||||||
"1".to_string()
|
"1".to_string()
|
||||||
} else {
|
} else {
|
||||||
"0".to_string()
|
"0".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_state() -> LoginServerState {
|
|
||||||
let connection = Connection::open("login.db").expect("Failed to open database!");
|
|
||||||
|
|
||||||
// Create users table
|
|
||||||
{
|
|
||||||
let query = "CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT);";
|
|
||||||
connection.execute(query, ()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create active sessions table
|
|
||||||
{
|
|
||||||
let query = "CREATE TABLE IF NOT EXISTS sessions (username TEXT PRIMARY KEY, sid TEXT);";
|
|
||||||
connection.execute(query, ()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
LoginServerState {
|
|
||||||
connection: Arc::new(Mutex::new(connection)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let state = setup_state();
|
let state = LoginServerState {
|
||||||
|
database: Arc::new(LoginDatabase::new()),
|
||||||
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/oauth/ffxivarr/login/top", get(top))
|
.route("/oauth/ffxivarr/login/top", get(top))
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use kawari::WORLD_NAME;
|
|
||||||
use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType};
|
use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType};
|
||||||
use kawari::config::get_config;
|
use kawari::config::get_config;
|
||||||
use kawari::lobby::ipc::CharacterDetails;
|
|
||||||
use kawari::lobby::{CharaMake, ClientSelectData};
|
|
||||||
use kawari::oodle::OodleNetwork;
|
use kawari::oodle::OodleNetwork;
|
||||||
use kawari::packet::{
|
use kawari::packet::{
|
||||||
CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive,
|
CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, send_keep_alive,
|
||||||
send_packet,
|
send_packet,
|
||||||
};
|
};
|
||||||
use kawari::world::PlayerData;
|
|
||||||
use kawari::world::ipc::{
|
use kawari::world::ipc::{
|
||||||
ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData,
|
ClientZoneIpcData, CommonSpawn, GameMasterCommandType, ObjectKind, ServerZoneIpcData,
|
||||||
ServerZoneIpcSegment, ServerZoneIpcType, SocialListRequestType, StatusEffect,
|
ServerZoneIpcSegment, ServerZoneIpcType, SocialListRequestType, StatusEffect,
|
||||||
|
@ -22,215 +18,13 @@ use kawari::world::{
|
||||||
Position, SocialList,
|
Position, SocialList,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use kawari::world::{PlayerData, WorldDatabase};
|
||||||
use kawari::{CHAR_NAME, CITY_STATE, CONTENT_ID, WORLD_ID, ZONE_ID, common::timestamp_secs};
|
use kawari::{CHAR_NAME, CITY_STATE, CONTENT_ID, WORLD_ID, ZONE_ID, common::timestamp_secs};
|
||||||
use physis::common::{Language, Platform};
|
use physis::common::{Language, Platform};
|
||||||
use physis::gamedata::GameData;
|
use physis::gamedata::GameData;
|
||||||
use rusqlite::Connection;
|
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
fn setup_db() -> Arc<Mutex<Connection>> {
|
|
||||||
let connection = Connection::open("world.db").expect("Failed to open database!");
|
|
||||||
|
|
||||||
// Create characters table
|
|
||||||
{
|
|
||||||
let query = "CREATE TABLE IF NOT EXISTS characters (content_id INTEGER PRIMARY KEY, service_account_id INTEGER, actor_id INTEGER);";
|
|
||||||
connection.execute(query, ()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create characters data table
|
|
||||||
{
|
|
||||||
let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING);";
|
|
||||||
connection.execute(query, ()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
Arc::new(Mutex::new(connection))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_player_data(connection: &Arc<Mutex<Connection>>, actor_id: u32) -> PlayerData {
|
|
||||||
let connection = connection.lock().unwrap();
|
|
||||||
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT content_id, service_account_id FROM characters WHERE actor_id = ?1")
|
|
||||||
.unwrap();
|
|
||||||
let (content_id, account_id) = stmt
|
|
||||||
.query_row((actor_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
PlayerData {
|
|
||||||
actor_id,
|
|
||||||
content_id,
|
|
||||||
account_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: from/to sql int
|
|
||||||
|
|
||||||
fn find_actor_id(connection: &Arc<Mutex<Connection>>, content_id: u64) -> u32 {
|
|
||||||
let connection = connection.lock().unwrap();
|
|
||||||
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT actor_id FROM characters WHERE content_id = ?1")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
stmt.query_row((content_id,), |row| row.get(0)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_character_list(
|
|
||||||
connection: &Arc<Mutex<Connection>>,
|
|
||||||
service_account_id: u32,
|
|
||||||
) -> Vec<CharacterDetails> {
|
|
||||||
let connection = connection.lock().unwrap();
|
|
||||||
|
|
||||||
let content_actor_ids: Vec<(u32, u32)>;
|
|
||||||
|
|
||||||
// find the content ids associated with the service account
|
|
||||||
{
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT content_id, actor_id FROM characters WHERE service_account_id = ?1")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
content_actor_ids = stmt
|
|
||||||
.query_map((service_account_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
|
||||||
.unwrap()
|
|
||||||
.map(|x| x.unwrap())
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut characters = Vec::new();
|
|
||||||
|
|
||||||
for (index, (content_id, actor_id)) in content_actor_ids.iter().enumerate() {
|
|
||||||
dbg!(content_id);
|
|
||||||
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (name, chara_make): (String, String) = stmt
|
|
||||||
.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let chara_make = CharaMake::from_json(&chara_make);
|
|
||||||
|
|
||||||
let select_data = ClientSelectData {
|
|
||||||
game_name_unk: "Final Fantasy".to_string(),
|
|
||||||
current_class: 2,
|
|
||||||
class_levels: [5; 30],
|
|
||||||
race: chara_make.customize.race as i32,
|
|
||||||
subrace: chara_make.customize.subrace as i32,
|
|
||||||
gender: chara_make.customize.gender as i32,
|
|
||||||
birth_month: chara_make.birth_month,
|
|
||||||
birth_day: chara_make.birth_day,
|
|
||||||
guardian: chara_make.guardian,
|
|
||||||
unk8: 0,
|
|
||||||
unk9: 0,
|
|
||||||
zone_id: ZONE_ID as i32,
|
|
||||||
unk11: 0,
|
|
||||||
customize: chara_make.customize,
|
|
||||||
unk12: 0,
|
|
||||||
unk13: 0,
|
|
||||||
unk14: [0; 10],
|
|
||||||
unk15: 0,
|
|
||||||
unk16: 0,
|
|
||||||
legacy_character: 0,
|
|
||||||
unk18: 0,
|
|
||||||
unk19: 0,
|
|
||||||
unk20: 0,
|
|
||||||
unk21: String::new(),
|
|
||||||
unk22: 0,
|
|
||||||
unk23: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
characters.push(CharacterDetails {
|
|
||||||
actor_id: *actor_id,
|
|
||||||
content_id: *content_id as u64,
|
|
||||||
index: index as u32,
|
|
||||||
unk1: [0; 16],
|
|
||||||
origin_server_id: WORLD_ID,
|
|
||||||
current_server_id: WORLD_ID,
|
|
||||||
character_name: name.clone(),
|
|
||||||
origin_server_name: WORLD_NAME.to_string(),
|
|
||||||
current_server_name: WORLD_NAME.to_string(),
|
|
||||||
character_detail_json: select_data.to_json(),
|
|
||||||
unk2: [0; 20],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dbg!(&characters);
|
|
||||||
|
|
||||||
characters
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_content_id() -> u32 {
|
|
||||||
rand::random()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_actor_id() -> u32 {
|
|
||||||
rand::random()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gives (content_id, actor_id)
|
|
||||||
fn create_player_data(
|
|
||||||
connection: &Arc<Mutex<Connection>>,
|
|
||||||
name: &str,
|
|
||||||
chara_make: &str,
|
|
||||||
) -> (u64, u32) {
|
|
||||||
let content_id = generate_content_id();
|
|
||||||
let actor_id = generate_actor_id();
|
|
||||||
|
|
||||||
let connection = connection.lock().unwrap();
|
|
||||||
|
|
||||||
// insert ids
|
|
||||||
connection
|
|
||||||
.execute(
|
|
||||||
"INSERT INTO characters VALUES (?1, ?2, ?3);",
|
|
||||||
(content_id, 0x1, actor_id),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// insert char data
|
|
||||||
connection
|
|
||||||
.execute(
|
|
||||||
"INSERT INTO character_data VALUES (?1, ?2, ?3);",
|
|
||||||
(content_id, name, chara_make),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
(content_id as u64, actor_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if `name` is in the character data table
|
|
||||||
fn check_is_name_free(connection: &Arc<Mutex<Connection>>, name: &str) -> bool {
|
|
||||||
let connection = connection.lock().unwrap();
|
|
||||||
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT content_id FROM character_data WHERE name = ?1")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
!stmt.exists((name,)).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CharacterData {
|
|
||||||
name: String,
|
|
||||||
chara_make: CharaMake, // probably not the ideal way to store this?
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_chara_make(connection: &Arc<Mutex<Connection>>, content_id: u64) -> CharacterData {
|
|
||||||
let connection = connection.lock().unwrap();
|
|
||||||
|
|
||||||
let mut stmt = connection
|
|
||||||
.prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1")
|
|
||||||
.unwrap();
|
|
||||||
let (name, chara_make_json): (String, String) = stmt
|
|
||||||
.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
CharacterData {
|
|
||||||
name,
|
|
||||||
chara_make: CharaMake::from_json(&chara_make_json),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
@ -239,12 +33,12 @@ async fn main() {
|
||||||
|
|
||||||
tracing::info!("World server started on 127.0.0.1:7100");
|
tracing::info!("World server started on 127.0.0.1:7100");
|
||||||
|
|
||||||
let db_connection = setup_db();
|
let database = Arc::new(WorldDatabase::new());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (socket, _) = listener.accept().await.unwrap();
|
let (socket, _) = listener.accept().await.unwrap();
|
||||||
|
|
||||||
let db_connection = db_connection.clone();
|
let database = database.clone();
|
||||||
|
|
||||||
let state = PacketState {
|
let state = PacketState {
|
||||||
client_key: None,
|
client_key: None,
|
||||||
|
@ -279,10 +73,8 @@ async fn main() {
|
||||||
tracing::info!("actor id to parse: {actor_id}");
|
tracing::info!("actor id to parse: {actor_id}");
|
||||||
|
|
||||||
// collect actor data
|
// collect actor data
|
||||||
connection.player_data = find_player_data(
|
connection.player_data =
|
||||||
&db_connection,
|
database.find_player_data(actor_id.parse::<u32>().unwrap());
|
||||||
actor_id.parse::<u32>().unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("player data: {:#?}", connection.player_data);
|
println!("player data: {:#?}", connection.player_data);
|
||||||
|
|
||||||
|
@ -442,10 +234,8 @@ async fn main() {
|
||||||
|
|
||||||
// Player Setup
|
// Player Setup
|
||||||
{
|
{
|
||||||
let chara_details = find_chara_make(
|
let chara_details = database
|
||||||
&db_connection,
|
.find_chara_make(connection.player_data.content_id);
|
||||||
connection.player_data.content_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
let ipc = ServerZoneIpcSegment {
|
let ipc = ServerZoneIpcSegment {
|
||||||
op_code: ServerZoneIpcType::PlayerSetup,
|
op_code: ServerZoneIpcType::PlayerSetup,
|
||||||
|
@ -515,10 +305,8 @@ async fn main() {
|
||||||
"Client has finished loading... spawning in!"
|
"Client has finished loading... spawning in!"
|
||||||
);
|
);
|
||||||
|
|
||||||
let chara_details = find_chara_make(
|
let chara_details = database
|
||||||
&db_connection,
|
.find_chara_make(connection.player_data.content_id);
|
||||||
connection.player_data.content_id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// send player spawn
|
// send player spawn
|
||||||
{
|
{
|
||||||
|
@ -925,11 +713,8 @@ async fn main() {
|
||||||
"creating character from: {name} {chara_make_json}"
|
"creating character from: {name} {chara_make_json}"
|
||||||
);
|
);
|
||||||
|
|
||||||
let (content_id, actor_id) = create_player_data(
|
let (content_id, actor_id) =
|
||||||
&db_connection,
|
database.create_player_data(name, chara_make_json);
|
||||||
name,
|
|
||||||
chara_make_json,
|
|
||||||
);
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Created new player: {content_id} {actor_id}"
|
"Created new player: {content_id} {actor_id}"
|
||||||
|
@ -960,7 +745,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CustomIpcData::GetActorId { content_id } => {
|
CustomIpcData::GetActorId { content_id } => {
|
||||||
let actor_id = find_actor_id(&db_connection, *content_id);
|
let actor_id = database.find_actor_id(*content_id);
|
||||||
|
|
||||||
tracing::info!("We found an actor id: {actor_id}");
|
tracing::info!("We found an actor id: {actor_id}");
|
||||||
|
|
||||||
|
@ -987,7 +772,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CustomIpcData::CheckNameIsAvailable { name } => {
|
CustomIpcData::CheckNameIsAvailable { name } => {
|
||||||
let is_name_free = check_is_name_free(&db_connection, name);
|
let is_name_free = database.check_is_name_free(name);
|
||||||
let is_name_free = if is_name_free { 1 } else { 0 };
|
let is_name_free = if is_name_free { 1 } else { 0 };
|
||||||
|
|
||||||
// send response
|
// send response
|
||||||
|
@ -1014,7 +799,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
CustomIpcData::RequestCharacterList { service_account_id } => {
|
CustomIpcData::RequestCharacterList { service_account_id } => {
|
||||||
let characters =
|
let characters =
|
||||||
get_character_list(&db_connection, *service_account_id);
|
database.get_character_list(*service_account_id);
|
||||||
|
|
||||||
// send response
|
// send response
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,6 +24,9 @@ pub mod world;
|
||||||
/// Everything packet parsing related.
|
/// Everything packet parsing related.
|
||||||
pub mod packet;
|
pub mod packet;
|
||||||
|
|
||||||
|
/// Logic server-specific code.
|
||||||
|
pub mod login;
|
||||||
|
|
||||||
// TODO: make this configurable
|
// TODO: make this configurable
|
||||||
/// The world ID and name for the lobby.
|
/// The world ID and name for the lobby.
|
||||||
/// See <https://ffxiv.consolegameswiki.com/wiki/Servers> for a list of possible IDs.
|
/// See <https://ffxiv.consolegameswiki.com/wiki/Servers> for a list of possible IDs.
|
||||||
|
|
120
src/login/database.rs
Normal file
120
src/login/database.rs
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
use rand::distributions::Alphanumeric;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
|
||||||
|
pub struct LoginDatabase {
|
||||||
|
connection: Mutex<Connection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum LoginError {
|
||||||
|
WrongUsername,
|
||||||
|
WrongPassword,
|
||||||
|
InternalError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoginDatabase {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let connection = Connection::open("login.db").expect("Failed to open database!");
|
||||||
|
|
||||||
|
// Create users table
|
||||||
|
{
|
||||||
|
let query =
|
||||||
|
"CREATE TABLE IF NOT EXISTS users (username TEXT PRIMARY KEY, password TEXT);";
|
||||||
|
connection.execute(query, ()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create active sessions table
|
||||||
|
{
|
||||||
|
let query =
|
||||||
|
"CREATE TABLE IF NOT EXISTS sessions (username TEXT PRIMARY KEY, sid TEXT);";
|
||||||
|
connection.execute(query, ()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
connection: Mutex::new(connection),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new user to the database.
|
||||||
|
pub fn add_user(&self, username: &str, password: &str) {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
tracing::info!("Adding user with username {username}");
|
||||||
|
|
||||||
|
let query = "INSERT INTO users VALUES (?1, ?2);";
|
||||||
|
connection
|
||||||
|
.execute(query, (username, password))
|
||||||
|
.expect("Failed to write user to database!");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Login as user, returns a session id.
|
||||||
|
pub fn login_user(&self, username: &str, password: &str) -> Result<String, LoginError> {
|
||||||
|
let selected_row: Result<(String, String), rusqlite::Error>;
|
||||||
|
|
||||||
|
tracing::info!("Finding user with username {username}");
|
||||||
|
|
||||||
|
{
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT username, password FROM users WHERE username = ?1")
|
||||||
|
.map_err(|_err| LoginError::WrongUsername)?;
|
||||||
|
selected_row = stmt.query_row((username,), |row| Ok((row.get(0)?, row.get(1)?)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok((_user, their_password)) = selected_row {
|
||||||
|
if their_password == password {
|
||||||
|
return self
|
||||||
|
.create_session(username)
|
||||||
|
.ok_or(LoginError::InternalError);
|
||||||
|
} else {
|
||||||
|
return Err(LoginError::WrongPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(LoginError::WrongUsername)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_sid() -> String {
|
||||||
|
let random_id: String = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(56)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
random_id.to_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new session for user, which replaces the last one (if any)
|
||||||
|
pub fn create_session(&self, username: &str) -> Option<String> {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let sid = Self::generate_sid();
|
||||||
|
|
||||||
|
connection
|
||||||
|
.execute(
|
||||||
|
"INSERT OR REPLACE INTO sessions VALUES (?1, ?2);",
|
||||||
|
(username, &sid),
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
tracing::info!("Created new session for {username}: {sid}");
|
||||||
|
|
||||||
|
Some(sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if there is a valid session for a given id
|
||||||
|
pub fn check_session(&self, sid: &str) -> bool {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT username, sid FROM sessions WHERE sid = ?1")
|
||||||
|
.ok()
|
||||||
|
.unwrap();
|
||||||
|
let selected_row: Result<(String, String), rusqlite::Error> =
|
||||||
|
stmt.query_row((sid,), |row| Ok((row.get(0)?, row.get(1)?)));
|
||||||
|
|
||||||
|
selected_row.is_ok()
|
||||||
|
}
|
||||||
|
}
|
2
src/login/mod.rs
Normal file
2
src/login/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod database;
|
||||||
|
pub use database::{LoginDatabase, LoginError};
|
213
src/world/database.rs
Normal file
213
src/world/database.rs
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use rusqlite::Connection;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
WORLD_ID, WORLD_NAME, ZONE_ID,
|
||||||
|
lobby::{CharaMake, ClientSelectData, ipc::CharacterDetails},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::PlayerData;
|
||||||
|
|
||||||
|
pub struct WorldDatabase {
|
||||||
|
connection: Mutex<Connection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CharacterData {
|
||||||
|
pub name: String,
|
||||||
|
pub chara_make: CharaMake, // probably not the ideal way to store this?
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorldDatabase {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let connection = Connection::open("world.db").expect("Failed to open database!");
|
||||||
|
|
||||||
|
// Create characters table
|
||||||
|
{
|
||||||
|
let query = "CREATE TABLE IF NOT EXISTS characters (content_id INTEGER PRIMARY KEY, service_account_id INTEGER, actor_id INTEGER);";
|
||||||
|
connection.execute(query, ()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create characters data table
|
||||||
|
{
|
||||||
|
let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING);";
|
||||||
|
connection.execute(query, ()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
connection: Mutex::new(connection),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_player_data(&self, actor_id: u32) -> PlayerData {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT content_id, service_account_id FROM characters WHERE actor_id = ?1")
|
||||||
|
.unwrap();
|
||||||
|
let (content_id, account_id) = stmt
|
||||||
|
.query_row((actor_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
PlayerData {
|
||||||
|
actor_id,
|
||||||
|
content_id,
|
||||||
|
account_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: from/to sql int
|
||||||
|
|
||||||
|
pub fn find_actor_id(&self, content_id: u64) -> u32 {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT actor_id FROM characters WHERE content_id = ?1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
stmt.query_row((content_id,), |row| row.get(0)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_character_list(&self, service_account_id: u32) -> Vec<CharacterDetails> {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let content_actor_ids: Vec<(u32, u32)>;
|
||||||
|
|
||||||
|
// find the content ids associated with the service account
|
||||||
|
{
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare(
|
||||||
|
"SELECT content_id, actor_id FROM characters WHERE service_account_id = ?1",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
content_actor_ids = stmt
|
||||||
|
.query_map((service_account_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
||||||
|
.unwrap()
|
||||||
|
.map(|x| x.unwrap())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut characters = Vec::new();
|
||||||
|
|
||||||
|
for (index, (content_id, actor_id)) in content_actor_ids.iter().enumerate() {
|
||||||
|
dbg!(content_id);
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (name, chara_make): (String, String) = stmt
|
||||||
|
.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let chara_make = CharaMake::from_json(&chara_make);
|
||||||
|
|
||||||
|
let select_data = ClientSelectData {
|
||||||
|
game_name_unk: "Final Fantasy".to_string(),
|
||||||
|
current_class: 2,
|
||||||
|
class_levels: [5; 30],
|
||||||
|
race: chara_make.customize.race as i32,
|
||||||
|
subrace: chara_make.customize.subrace as i32,
|
||||||
|
gender: chara_make.customize.gender as i32,
|
||||||
|
birth_month: chara_make.birth_month,
|
||||||
|
birth_day: chara_make.birth_day,
|
||||||
|
guardian: chara_make.guardian,
|
||||||
|
unk8: 0,
|
||||||
|
unk9: 0,
|
||||||
|
zone_id: ZONE_ID as i32,
|
||||||
|
unk11: 0,
|
||||||
|
customize: chara_make.customize,
|
||||||
|
unk12: 0,
|
||||||
|
unk13: 0,
|
||||||
|
unk14: [0; 10],
|
||||||
|
unk15: 0,
|
||||||
|
unk16: 0,
|
||||||
|
legacy_character: 0,
|
||||||
|
unk18: 0,
|
||||||
|
unk19: 0,
|
||||||
|
unk20: 0,
|
||||||
|
unk21: String::new(),
|
||||||
|
unk22: 0,
|
||||||
|
unk23: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
characters.push(CharacterDetails {
|
||||||
|
actor_id: *actor_id,
|
||||||
|
content_id: *content_id as u64,
|
||||||
|
index: index as u32,
|
||||||
|
unk1: [0; 16],
|
||||||
|
origin_server_id: WORLD_ID,
|
||||||
|
current_server_id: WORLD_ID,
|
||||||
|
character_name: name.clone(),
|
||||||
|
origin_server_name: WORLD_NAME.to_string(),
|
||||||
|
current_server_name: WORLD_NAME.to_string(),
|
||||||
|
character_detail_json: select_data.to_json(),
|
||||||
|
unk2: [0; 20],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
characters
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_content_id() -> u32 {
|
||||||
|
rand::random()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_actor_id() -> u32 {
|
||||||
|
rand::random()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gives (content_id, actor_id)
|
||||||
|
pub fn create_player_data(&self, name: &str, chara_make: &str) -> (u64, u32) {
|
||||||
|
let content_id = Self::generate_content_id();
|
||||||
|
let actor_id = Self::generate_actor_id();
|
||||||
|
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
// insert ids
|
||||||
|
connection
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO characters VALUES (?1, ?2, ?3);",
|
||||||
|
(content_id, 0x1, actor_id),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// insert char data
|
||||||
|
connection
|
||||||
|
.execute(
|
||||||
|
"INSERT INTO character_data VALUES (?1, ?2, ?3);",
|
||||||
|
(content_id, name, chara_make),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(content_id as u64, actor_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if `name` is in the character data table
|
||||||
|
pub fn check_is_name_free(&self, name: &str) -> bool {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT content_id FROM character_data WHERE name = ?1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
!stmt.exists((name,)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_chara_make(&self, content_id: u64) -> CharacterData {
|
||||||
|
let connection = self.connection.lock().unwrap();
|
||||||
|
|
||||||
|
let mut stmt = connection
|
||||||
|
.prepare("SELECT name, chara_make FROM character_data WHERE content_id = ?1")
|
||||||
|
.unwrap();
|
||||||
|
let (name, chara_make_json): (String, String) = stmt
|
||||||
|
.query_row((content_id,), |row| Ok((row.get(0)?, row.get(1)?)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
CharacterData {
|
||||||
|
name,
|
||||||
|
chara_make: CharaMake::from_json(&chara_make_json),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,3 +8,6 @@ pub use chat_handler::ChatHandler;
|
||||||
|
|
||||||
mod connection;
|
mod connection;
|
||||||
pub use connection::{PlayerData, ZoneConnection};
|
pub use connection::{PlayerData, ZoneConnection};
|
||||||
|
|
||||||
|
mod database;
|
||||||
|
pub use database::{CharacterData, WorldDatabase};
|
||||||
|
|
Loading…
Add table
Reference in a new issue