2025-03-30 18:51:11 -04:00
|
|
|
use std::{io::Read, sync::Mutex};
|
2025-03-22 16:15:29 -04:00
|
|
|
|
|
|
|
use rusqlite::Connection;
|
2025-03-30 18:51:11 -04:00
|
|
|
use serde::Deserialize;
|
2025-03-22 16:15:29 -04:00
|
|
|
|
2025-03-22 22:01:32 -04:00
|
|
|
use crate::{
|
2025-06-27 23:27:53 -04:00
|
|
|
AETHERYTE_UNLOCK_BITMASK_SIZE, UNLOCK_BITMASK_SIZE,
|
2025-04-22 16:07:39 -04:00
|
|
|
common::{
|
2025-04-30 23:05:43 -04:00
|
|
|
CustomizeData, GameData, Position,
|
2025-04-22 16:07:39 -04:00
|
|
|
workdefinitions::{CharaMake, ClientSelectData, RemakeMode},
|
2025-03-30 08:32:46 -04:00
|
|
|
},
|
2025-06-28 13:55:11 -04:00
|
|
|
inventory::{Inventory, Item, Storage},
|
2025-05-12 15:44:39 -04:00
|
|
|
ipc::{
|
|
|
|
lobby::{CharacterDetails, CharacterFlag},
|
|
|
|
zone::GameMasterRank,
|
|
|
|
},
|
2025-03-22 22:01:32 -04:00
|
|
|
};
|
2025-03-22 16:15:29 -04:00
|
|
|
|
2025-05-02 16:15:54 -04:00
|
|
|
use super::PlayerData;
|
2025-03-22 16:15:29 -04:00
|
|
|
|
|
|
|
pub struct WorldDatabase {
|
|
|
|
connection: Mutex<Connection>,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct CharacterData {
|
|
|
|
pub name: String,
|
|
|
|
pub chara_make: CharaMake, // probably not the ideal way to store this?
|
2025-03-22 18:34:27 -04:00
|
|
|
pub city_state: u8,
|
2025-03-22 19:05:29 -04:00
|
|
|
pub position: Position,
|
|
|
|
pub zone_id: u16,
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|
|
|
|
|
2025-03-23 18:14:14 -04:00
|
|
|
impl Default for WorldDatabase {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-22 16:15:29 -04:00
|
|
|
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
|
|
|
|
{
|
2025-06-27 23:24:30 -04:00
|
|
|
let query = "CREATE TABLE IF NOT EXISTS character_data (content_id INTEGER PRIMARY KEY, name STRING, chara_make STRING, city_state INTEGER, zone_id INTEGER, pos_x REAL, pos_y REAL, pos_z REAL, rotation REAL, inventory STRING, remake_mode INTEGER, gm_rank INTEGER, classjob_id INTEGER, classjob_levels STRING, unlocks STRING, aetherytes STRING);";
|
2025-03-22 16:15:29 -04:00
|
|
|
connection.execute(query, ()).unwrap();
|
|
|
|
}
|
|
|
|
|
2025-06-23 21:14:38 -04:00
|
|
|
Self {
|
2025-03-22 16:15:29 -04:00
|
|
|
connection: Mutex::new(connection),
|
2025-06-23 21:14:38 -04:00
|
|
|
}
|
2025-03-30 18:51:11 -04:00
|
|
|
}
|
|
|
|
|
2025-06-28 13:55:11 -04:00
|
|
|
pub fn import_character(&self, game_data: &mut GameData, service_account_id: u32, path: &str) {
|
2025-05-01 15:34:01 -04:00
|
|
|
tracing::info!("Importing character backup from {path}...");
|
2025-03-30 18:51:11 -04:00
|
|
|
|
|
|
|
let file = std::fs::File::open(path).unwrap();
|
|
|
|
|
|
|
|
let mut archive = zip::ZipArchive::new(file).unwrap();
|
|
|
|
|
2025-04-01 23:25:11 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct GenericValue {
|
|
|
|
value: i32,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct NamedayValue {
|
|
|
|
day: i32,
|
|
|
|
month: i32,
|
|
|
|
}
|
|
|
|
|
2025-06-28 13:55:11 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct ClassJobLevelValue {
|
|
|
|
level: i32,
|
|
|
|
exp: Option<u32>,
|
|
|
|
value: i32,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct InventoryItem {
|
|
|
|
slot: i32,
|
|
|
|
quantity: u32,
|
2025-06-28 14:29:12 -04:00
|
|
|
condition: u16,
|
2025-06-28 13:55:11 -04:00
|
|
|
id: u32,
|
|
|
|
glamour_id: u32,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct InventoryContainer {
|
|
|
|
items: Vec<InventoryItem>,
|
|
|
|
}
|
|
|
|
|
2025-03-30 18:51:11 -04:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct CharacterJson {
|
|
|
|
name: String,
|
2025-04-01 23:25:11 -04:00
|
|
|
city_state: GenericValue,
|
|
|
|
nameday: NamedayValue,
|
|
|
|
guardian: GenericValue,
|
|
|
|
voice: i32,
|
2025-06-28 13:55:11 -04:00
|
|
|
classjob_levels: Vec<ClassJobLevelValue>,
|
|
|
|
|
|
|
|
inventory1: InventoryContainer,
|
|
|
|
inventory2: InventoryContainer,
|
|
|
|
inventory3: InventoryContainer,
|
|
|
|
inventory4: InventoryContainer,
|
|
|
|
equipped_items: InventoryContainer,
|
|
|
|
|
|
|
|
currency: InventoryContainer,
|
|
|
|
|
|
|
|
armory_off_hand: InventoryContainer,
|
|
|
|
armory_head: InventoryContainer,
|
|
|
|
armory_body: InventoryContainer,
|
|
|
|
armory_hands: InventoryContainer,
|
|
|
|
armory_legs: InventoryContainer,
|
|
|
|
armory_ear: InventoryContainer,
|
|
|
|
armory_neck: InventoryContainer,
|
|
|
|
armory_wrist: InventoryContainer,
|
|
|
|
armory_rings: InventoryContainer,
|
|
|
|
armory_soul_crystal: InventoryContainer,
|
|
|
|
armory_main_hand: InventoryContainer,
|
|
|
|
|
|
|
|
unlock_flags: Vec<u8>,
|
|
|
|
unlock_aetherytes: Vec<u8>,
|
2025-03-30 18:51:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
let character: CharacterJson;
|
|
|
|
{
|
|
|
|
let mut character_file = archive.by_name("character.json").unwrap();
|
|
|
|
|
|
|
|
let mut json_string = String::new();
|
|
|
|
character_file.read_to_string(&mut json_string).unwrap();
|
|
|
|
|
|
|
|
character = serde_json::from_str(&json_string).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
if !self.check_is_name_free(&character.name) {
|
2025-05-01 15:34:01 -04:00
|
|
|
let name = character.name;
|
|
|
|
tracing::warn!("* Skipping since {name} already exists.");
|
2025-03-30 18:51:11 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let charsave_file = archive.by_name("FFXIV_CHARA_01.dat").unwrap();
|
|
|
|
let charsave_bytes: Vec<u8> = charsave_file.bytes().map(|x| x.unwrap()).collect();
|
2025-05-09 15:25:57 -04:00
|
|
|
let charsave =
|
|
|
|
physis::savedata::chardat::CharacterData::from_existing(&charsave_bytes).unwrap();
|
2025-03-30 18:51:11 -04:00
|
|
|
|
|
|
|
let customize = CustomizeData::from(charsave.customize);
|
|
|
|
|
|
|
|
let chara_make = CharaMake {
|
|
|
|
customize,
|
2025-04-11 08:26:38 -04:00
|
|
|
voice_id: character.voice,
|
2025-04-01 23:25:11 -04:00
|
|
|
guardian: character.guardian.value,
|
|
|
|
birth_month: character.nameday.month,
|
|
|
|
birth_day: character.nameday.day,
|
2025-03-30 18:51:11 -04:00
|
|
|
classjob_id: 5,
|
|
|
|
unk2: 1,
|
|
|
|
};
|
|
|
|
|
2025-06-28 13:55:11 -04:00
|
|
|
let (_, actor_id) = self.create_player_data(
|
2025-05-01 15:34:01 -04:00
|
|
|
service_account_id,
|
2025-04-01 18:49:42 -04:00
|
|
|
&character.name,
|
|
|
|
&chara_make.to_json(),
|
2025-04-01 23:25:11 -04:00
|
|
|
character.city_state.value as u8,
|
2025-04-01 18:49:42 -04:00
|
|
|
132,
|
2025-04-01 21:37:41 -04:00
|
|
|
Inventory::default(),
|
2025-04-01 18:49:42 -04:00
|
|
|
);
|
2025-03-30 18:51:11 -04:00
|
|
|
|
2025-06-28 13:55:11 -04:00
|
|
|
let mut player_data = self.find_player_data(actor_id);
|
|
|
|
|
|
|
|
// import jobs
|
|
|
|
for classjob in &character.classjob_levels {
|
|
|
|
// find the array index of the job
|
|
|
|
let index = game_data
|
|
|
|
.get_exp_array_index(classjob.value as u16)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
player_data.classjob_levels[index as usize] = classjob.level;
|
|
|
|
if let Some(exp) = classjob.exp {
|
|
|
|
player_data.classjob_exp[index as usize] = exp;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let process_inventory_container =
|
|
|
|
|container: &InventoryContainer, target: &mut dyn Storage| {
|
|
|
|
for item in &container.items {
|
|
|
|
if item.slot as u32 > target.max_slots() {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
*target.get_slot_mut(item.slot as u16) = Item {
|
|
|
|
quantity: item.quantity,
|
|
|
|
id: item.id,
|
2025-06-28 14:29:12 -04:00
|
|
|
condition: item.condition,
|
|
|
|
glamour_catalog_id: item.glamour_id,
|
2025-06-28 13:55:11 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// import inventory
|
|
|
|
process_inventory_container(&character.inventory1, &mut player_data.inventory.pages[0]);
|
|
|
|
process_inventory_container(&character.inventory2, &mut player_data.inventory.pages[1]);
|
|
|
|
process_inventory_container(&character.inventory3, &mut player_data.inventory.pages[2]);
|
|
|
|
process_inventory_container(&character.inventory4, &mut player_data.inventory.pages[3]);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.equipped_items,
|
|
|
|
&mut player_data.inventory.equipped,
|
|
|
|
);
|
|
|
|
|
|
|
|
process_inventory_container(&character.currency, &mut player_data.inventory.currency);
|
|
|
|
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_off_hand,
|
|
|
|
&mut player_data.inventory.armoury_off_hand,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_head,
|
|
|
|
&mut player_data.inventory.armoury_head,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_body,
|
|
|
|
&mut player_data.inventory.armoury_body,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_hands,
|
|
|
|
&mut player_data.inventory.armoury_hands,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_legs,
|
|
|
|
&mut player_data.inventory.armoury_legs,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_ear,
|
|
|
|
&mut player_data.inventory.armoury_earring,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_neck,
|
|
|
|
&mut player_data.inventory.armoury_necklace,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_wrist,
|
|
|
|
&mut player_data.inventory.armoury_bracelet,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_rings,
|
|
|
|
&mut player_data.inventory.armoury_rings,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_soul_crystal,
|
|
|
|
&mut player_data.inventory.armoury_soul_crystal,
|
|
|
|
);
|
|
|
|
process_inventory_container(
|
|
|
|
&character.armory_main_hand,
|
|
|
|
&mut player_data.inventory.armoury_main_hand,
|
|
|
|
);
|
|
|
|
|
|
|
|
// import unlock flags
|
|
|
|
player_data.unlocks = character.unlock_flags;
|
|
|
|
player_data.aetherytes = character.unlock_aetherytes;
|
|
|
|
|
|
|
|
self.commit_player_data(&player_data);
|
|
|
|
|
2025-03-30 18:51:11 -04:00
|
|
|
tracing::info!("{} added to the world!", character.name);
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
2025-03-29 00:15:29 -04:00
|
|
|
stmt = connection
|
2025-06-27 23:24:30 -04:00
|
|
|
.prepare("SELECT pos_x, pos_y, pos_z, rotation, zone_id, inventory, gm_rank, classjob_id, classjob_levels, unlocks, aetherytes FROM character_data WHERE content_id = ?1")
|
2025-03-29 00:15:29 -04:00
|
|
|
.unwrap();
|
2025-06-20 19:08:53 -04:00
|
|
|
let (
|
|
|
|
pos_x,
|
|
|
|
pos_y,
|
|
|
|
pos_z,
|
|
|
|
rotation,
|
|
|
|
zone_id,
|
|
|
|
inventory_json,
|
|
|
|
gm_rank,
|
|
|
|
classjob_id,
|
|
|
|
classjob_levels,
|
2025-06-27 23:01:00 -04:00
|
|
|
unlocks,
|
2025-06-27 23:24:30 -04:00
|
|
|
aetherytes,
|
|
|
|
): (
|
|
|
|
f32,
|
|
|
|
f32,
|
|
|
|
f32,
|
|
|
|
f32,
|
|
|
|
u16,
|
|
|
|
String,
|
|
|
|
u8,
|
|
|
|
i32,
|
|
|
|
String,
|
|
|
|
String,
|
|
|
|
String,
|
|
|
|
) = stmt
|
2025-03-29 00:15:29 -04:00
|
|
|
.query_row((content_id,), |row| {
|
|
|
|
Ok((
|
|
|
|
row.get(0)?,
|
|
|
|
row.get(1)?,
|
|
|
|
row.get(2)?,
|
|
|
|
row.get(3)?,
|
|
|
|
row.get(4)?,
|
2025-04-01 18:49:42 -04:00
|
|
|
row.get(5)?,
|
2025-05-12 15:44:39 -04:00
|
|
|
row.get(6)?,
|
2025-06-20 19:08:53 -04:00
|
|
|
row.get(7)?,
|
|
|
|
row.get(8)?,
|
2025-06-27 23:01:00 -04:00
|
|
|
row.get(9)?,
|
2025-06-27 23:24:30 -04:00
|
|
|
row.get(10)?,
|
2025-03-29 00:15:29 -04:00
|
|
|
))
|
|
|
|
})
|
|
|
|
.unwrap();
|
|
|
|
|
2025-04-01 18:49:42 -04:00
|
|
|
let inventory = serde_json::from_str(&inventory_json).unwrap();
|
|
|
|
|
2025-03-22 16:15:29 -04:00
|
|
|
PlayerData {
|
|
|
|
actor_id,
|
|
|
|
content_id,
|
|
|
|
account_id,
|
2025-03-29 00:15:29 -04:00
|
|
|
position: Position {
|
|
|
|
x: pos_x,
|
|
|
|
y: pos_y,
|
|
|
|
z: pos_z,
|
|
|
|
},
|
|
|
|
rotation,
|
|
|
|
zone_id,
|
2025-04-01 18:49:42 -04:00
|
|
|
inventory,
|
2025-05-12 15:44:39 -04:00
|
|
|
gm_rank: GameMasterRank::try_from(gm_rank).unwrap(),
|
2025-06-20 19:08:53 -04:00
|
|
|
classjob_id: classjob_id as u8,
|
|
|
|
classjob_levels: serde_json::from_str(&classjob_levels).unwrap(),
|
2025-06-27 23:01:00 -04:00
|
|
|
unlocks: serde_json::from_str(&unlocks).unwrap(),
|
2025-06-27 23:24:30 -04:00
|
|
|
aetherytes: serde_json::from_str(&aetherytes).unwrap(),
|
2025-03-29 14:52:27 -04:00
|
|
|
..Default::default()
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-29 00:15:29 -04:00
|
|
|
/// Commit the dynamic player data back to the database
|
|
|
|
pub fn commit_player_data(&self, data: &PlayerData) {
|
|
|
|
let connection = self.connection.lock().unwrap();
|
|
|
|
|
|
|
|
let mut stmt = connection
|
2025-06-27 23:24:30 -04:00
|
|
|
.prepare("UPDATE character_data SET zone_id=?1, pos_x=?2, pos_y=?3, pos_z=?4, rotation=?5, inventory=?6, classjob_id=?7, classjob_levels=?8, unlocks=?9, aetherytes=?10 WHERE content_id = ?11")
|
2025-03-29 00:15:29 -04:00
|
|
|
.unwrap();
|
|
|
|
stmt.execute((
|
|
|
|
data.zone_id,
|
|
|
|
data.position.x,
|
|
|
|
data.position.y,
|
|
|
|
data.position.z,
|
|
|
|
data.rotation,
|
2025-04-01 18:49:42 -04:00
|
|
|
serde_json::to_string(&data.inventory).unwrap(),
|
2025-06-20 19:08:53 -04:00
|
|
|
data.classjob_id,
|
|
|
|
serde_json::to_string(&data.classjob_levels).unwrap(),
|
2025-06-27 23:01:00 -04:00
|
|
|
serde_json::to_string(&data.unlocks).unwrap(),
|
2025-06-27 23:24:30 -04:00
|
|
|
serde_json::to_string(&data.aetherytes).unwrap(),
|
2025-03-29 00:15:29 -04:00
|
|
|
data.content_id,
|
|
|
|
))
|
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
|
2025-03-22 16:15:29 -04:00
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
|
2025-03-22 17:00:21 -04:00
|
|
|
pub fn get_character_list(
|
|
|
|
&self,
|
|
|
|
service_account_id: u32,
|
|
|
|
world_id: u16,
|
|
|
|
world_name: &str,
|
2025-04-30 23:05:43 -04:00
|
|
|
game_data: &mut GameData,
|
2025-03-22 17:00:21 -04:00
|
|
|
) -> Vec<CharacterDetails> {
|
2025-03-22 16:15:29 -04:00
|
|
|
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() {
|
|
|
|
let mut stmt = connection
|
2025-03-22 19:05:29 -04:00
|
|
|
.prepare(
|
2025-06-20 19:08:53 -04:00
|
|
|
"SELECT name, chara_make, zone_id, inventory, remake_mode, classjob_id, classjob_levels FROM character_data WHERE content_id = ?1",
|
2025-03-22 19:05:29 -04:00
|
|
|
)
|
2025-03-22 16:15:29 -04:00
|
|
|
.unwrap();
|
|
|
|
|
2025-06-20 19:08:53 -04:00
|
|
|
let result: Result<(String, String, u16, String, i32, i32, String), rusqlite::Error> =
|
|
|
|
stmt.query_row((content_id,), |row| {
|
2025-05-02 15:36:22 -04:00
|
|
|
Ok((
|
|
|
|
row.get(0)?,
|
|
|
|
row.get(1)?,
|
|
|
|
row.get(2)?,
|
|
|
|
row.get(3)?,
|
|
|
|
row.get(4)?,
|
2025-06-20 19:08:53 -04:00
|
|
|
row.get(5)?,
|
|
|
|
row.get(6)?,
|
2025-05-02 15:36:22 -04:00
|
|
|
))
|
2025-03-22 19:05:29 -04:00
|
|
|
});
|
2025-03-22 17:32:00 -04:00
|
|
|
|
2025-06-20 19:08:53 -04:00
|
|
|
if let Ok((
|
|
|
|
name,
|
|
|
|
chara_make,
|
|
|
|
zone_id,
|
|
|
|
inventory_json,
|
|
|
|
remake_mode,
|
|
|
|
classjob_id,
|
|
|
|
classjob_levels,
|
|
|
|
)) = result
|
|
|
|
{
|
2025-03-22 17:32:00 -04:00
|
|
|
let chara_make = CharaMake::from_json(&chara_make);
|
|
|
|
|
2025-04-30 23:05:43 -04:00
|
|
|
let inventory: Inventory = serde_json::from_str(&inventory_json).unwrap();
|
|
|
|
|
2025-03-22 17:32:00 -04:00
|
|
|
let select_data = ClientSelectData {
|
2025-04-30 23:05:43 -04:00
|
|
|
character_name: name.clone(),
|
2025-06-20 19:08:53 -04:00
|
|
|
current_class: classjob_id,
|
|
|
|
class_levels: serde_json::from_str(&classjob_levels).unwrap(),
|
2025-03-22 17:32:00 -04:00
|
|
|
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,
|
2025-03-30 08:31:33 -04:00
|
|
|
unk8: 0,
|
|
|
|
unk9: 0,
|
2025-03-22 19:05:29 -04:00
|
|
|
zone_id: zone_id as i32,
|
2025-03-30 08:31:33 -04:00
|
|
|
content_finder_condition: 0,
|
2025-03-22 17:32:00 -04:00
|
|
|
customize: chara_make.customize,
|
2025-04-30 23:05:43 -04:00
|
|
|
model_main_weapon: inventory.get_main_weapon_id(game_data),
|
2025-03-30 08:32:46 -04:00
|
|
|
model_sub_weapon: 0,
|
2025-04-30 23:05:43 -04:00
|
|
|
model_ids: inventory.get_model_ids(game_data),
|
|
|
|
equip_stain: [0; 10],
|
|
|
|
glasses: [0; 2],
|
2025-05-02 15:36:22 -04:00
|
|
|
remake_mode: RemakeMode::try_from(remake_mode).unwrap(),
|
2025-03-30 08:31:33 -04:00
|
|
|
remake_minutes_remaining: 0,
|
2025-04-11 08:26:38 -04:00
|
|
|
voice_id: chara_make.voice_id,
|
2025-03-30 08:31:33 -04:00
|
|
|
unk20: 0,
|
2025-05-12 16:11:17 -04:00
|
|
|
unk21: 0,
|
2025-03-30 08:32:46 -04:00
|
|
|
world_name: String::new(),
|
2025-03-30 08:31:33 -04:00
|
|
|
unk22: 0,
|
2025-05-12 16:11:17 -04:00
|
|
|
unk23: 0,
|
2025-03-22 17:32:00 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
characters.push(CharacterDetails {
|
|
|
|
actor_id: *actor_id,
|
|
|
|
content_id: *content_id as u64,
|
2025-03-30 08:32:46 -04:00
|
|
|
index: index as u8,
|
|
|
|
flags: CharacterFlag::NONE,
|
|
|
|
unk1: [255; 6],
|
2025-03-22 17:32:00 -04:00
|
|
|
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(),
|
2025-03-30 08:32:46 -04:00
|
|
|
unk2: [255; 16],
|
|
|
|
unk3: [4; 5],
|
2025-03-22 17:32:00 -04:00
|
|
|
});
|
|
|
|
}
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
characters
|
|
|
|
}
|
|
|
|
|
|
|
|
fn generate_content_id() -> u32 {
|
2025-05-02 23:06:59 -04:00
|
|
|
fastrand::u32(..)
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn generate_actor_id() -> u32 {
|
2025-05-02 23:06:59 -04:00
|
|
|
fastrand::u32(..)
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Gives (content_id, actor_id)
|
2025-03-22 19:05:29 -04:00
|
|
|
pub fn create_player_data(
|
|
|
|
&self,
|
2025-04-05 21:36:56 -04:00
|
|
|
service_account_id: u32,
|
2025-03-22 19:05:29 -04:00
|
|
|
name: &str,
|
2025-06-20 19:08:53 -04:00
|
|
|
chara_make_str: &str,
|
2025-03-22 19:05:29 -04:00
|
|
|
city_state: u8,
|
|
|
|
zone_id: u16,
|
2025-04-01 18:49:42 -04:00
|
|
|
inventory: Inventory,
|
2025-03-22 19:05:29 -04:00
|
|
|
) -> (u64, u32) {
|
2025-03-22 16:15:29 -04:00
|
|
|
let content_id = Self::generate_content_id();
|
|
|
|
let actor_id = Self::generate_actor_id();
|
|
|
|
|
|
|
|
let connection = self.connection.lock().unwrap();
|
|
|
|
|
2025-06-20 19:08:53 -04:00
|
|
|
// fill out the initial classjob
|
|
|
|
let chara_make = CharaMake::from_json(chara_make_str);
|
|
|
|
let mut classjob_levels = [0i32; 32];
|
|
|
|
classjob_levels[chara_make.classjob_id as usize] = 1; // inital level
|
|
|
|
|
2025-06-27 23:01:00 -04:00
|
|
|
// fill out initial unlocks
|
2025-06-27 23:27:53 -04:00
|
|
|
let unlocks = vec![0u8; UNLOCK_BITMASK_SIZE];
|
2025-06-27 23:01:00 -04:00
|
|
|
|
2025-06-27 23:24:30 -04:00
|
|
|
// fill out initial aetherytes
|
2025-06-27 23:27:53 -04:00
|
|
|
let aetherytes = vec![0u8; AETHERYTE_UNLOCK_BITMASK_SIZE];
|
2025-06-27 23:24:30 -04:00
|
|
|
|
2025-03-22 16:15:29 -04:00
|
|
|
// insert ids
|
|
|
|
connection
|
|
|
|
.execute(
|
|
|
|
"INSERT INTO characters VALUES (?1, ?2, ?3);",
|
2025-04-05 21:36:56 -04:00
|
|
|
(content_id, service_account_id, actor_id),
|
2025-03-22 16:15:29 -04:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
// insert char data
|
|
|
|
connection
|
|
|
|
.execute(
|
2025-06-27 23:24:30 -04:00
|
|
|
"INSERT INTO character_data VALUES (?1, ?2, ?3, ?4, ?5, 0.0, 0.0, 0.0, 0.0, ?6, 0, 90, ?7, ?8, ?9, ?10);",
|
2025-04-01 18:49:42 -04:00
|
|
|
(
|
|
|
|
content_id,
|
|
|
|
name,
|
2025-06-20 19:08:53 -04:00
|
|
|
chara_make_str,
|
2025-04-01 18:49:42 -04:00
|
|
|
city_state,
|
|
|
|
zone_id,
|
|
|
|
serde_json::to_string(&inventory).unwrap(),
|
2025-06-20 19:08:53 -04:00
|
|
|
chara_make.classjob_id,
|
|
|
|
serde_json::to_string(&classjob_levels).unwrap(),
|
2025-06-27 23:01:00 -04:00
|
|
|
serde_json::to_string(&unlocks).unwrap(),
|
2025-06-27 23:24:30 -04:00
|
|
|
serde_json::to_string(&aetherytes).unwrap(),
|
2025-04-01 18:49:42 -04:00
|
|
|
),
|
2025-03-22 16:15:29 -04:00
|
|
|
)
|
|
|
|
.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
|
2025-03-22 18:34:27 -04:00
|
|
|
.prepare(
|
2025-03-22 19:05:29 -04:00
|
|
|
"SELECT name, chara_make, city_state, zone_id, pos_x, pos_y, pos_z FROM character_data WHERE content_id = ?1",
|
2025-03-22 18:34:27 -04:00
|
|
|
)
|
2025-03-22 16:15:29 -04:00
|
|
|
.unwrap();
|
2025-03-22 19:05:29 -04:00
|
|
|
let (name, chara_make_json, city_state, zone_id, pos_x, pos_y, pos_z): (
|
|
|
|
String,
|
|
|
|
String,
|
|
|
|
u8,
|
|
|
|
u16,
|
|
|
|
f32,
|
|
|
|
f32,
|
|
|
|
f32,
|
|
|
|
) = stmt
|
2025-03-22 18:34:27 -04:00
|
|
|
.query_row((content_id,), |row| {
|
2025-03-22 19:05:29 -04:00
|
|
|
Ok((
|
|
|
|
row.get(0)?,
|
|
|
|
row.get(1)?,
|
|
|
|
row.get(2)?,
|
|
|
|
row.get(3)?,
|
|
|
|
row.get(4)?,
|
|
|
|
row.get(5)?,
|
|
|
|
row.get(6)?,
|
|
|
|
))
|
2025-03-22 18:34:27 -04:00
|
|
|
})
|
2025-03-22 16:15:29 -04:00
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
CharacterData {
|
|
|
|
name,
|
|
|
|
chara_make: CharaMake::from_json(&chara_make_json),
|
2025-03-22 18:34:27 -04:00
|
|
|
city_state,
|
2025-03-22 19:05:29 -04:00
|
|
|
zone_id,
|
|
|
|
position: Position {
|
|
|
|
x: pos_x,
|
|
|
|
y: pos_y,
|
|
|
|
z: pos_z,
|
|
|
|
},
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|
|
|
|
}
|
2025-03-22 17:32:00 -04:00
|
|
|
|
|
|
|
/// Deletes a character and all associated data
|
|
|
|
pub fn delete_character(&self, content_id: u64) {
|
|
|
|
let connection = self.connection.lock().unwrap();
|
|
|
|
|
2025-06-28 10:43:53 -04:00
|
|
|
// delete data
|
|
|
|
{
|
|
|
|
let mut stmt = connection
|
|
|
|
.prepare("DELETE FROM character_data WHERE content_id = ?1")
|
|
|
|
.unwrap();
|
|
|
|
stmt.execute((content_id,)).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
// delete char
|
|
|
|
{
|
|
|
|
let mut stmt = connection
|
|
|
|
.prepare("DELETE FROM characters WHERE content_id = ?1")
|
|
|
|
.unwrap();
|
|
|
|
stmt.execute((content_id,)).unwrap();
|
|
|
|
}
|
2025-03-22 17:32:00 -04:00
|
|
|
}
|
2025-05-02 15:36:22 -04:00
|
|
|
|
|
|
|
/// Sets the remake mode for a character
|
|
|
|
pub fn set_remake_mode(&self, content_id: u64, mode: RemakeMode) {
|
|
|
|
let connection = self.connection.lock().unwrap();
|
|
|
|
|
|
|
|
let mut stmt = connection
|
|
|
|
.prepare("UPDATE character_data SET remake_mode=?1 WHERE content_id = ?2")
|
|
|
|
.unwrap();
|
|
|
|
stmt.execute((mode as i32, content_id)).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Sets the chara make JSON for a character
|
|
|
|
pub fn set_chara_make(&self, content_id: u64, chara_make_json: &str) {
|
|
|
|
let connection = self.connection.lock().unwrap();
|
|
|
|
|
|
|
|
let mut stmt = connection
|
|
|
|
.prepare("UPDATE character_data SET chara_make=?1 WHERE content_id = ?2")
|
|
|
|
.unwrap();
|
|
|
|
stmt.execute((chara_make_json, content_id)).unwrap();
|
|
|
|
}
|
2025-03-22 16:15:29 -04:00
|
|
|
}
|