diff --git a/Cargo.toml b/Cargo.toml index 1537cfd..4720390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ mio-extras = "2.0.5" crc = "^1.0.0" bcrypt = "0.4" threadpool = "1.0" -chrono = "*" +chrono = "0.4.11" serde = "*" serde_json = "*" ron = "*" @@ -29,4 +29,8 @@ thiserror = "1.0.15" ages-prs = "0.1" async-trait = "0.1.31" lazy_static = "1.4.0" +barrel = { version = "0.6.5", features = ["pg"] } +postgres = "0.17.5" +refinery = { version = "0.3.0", features = ["postgres"] } +sqlx = { version = "0.4.0-beta.1", features = ["postgres", "json", "chrono"] } diff --git a/data/item_stats/mag_stats.toml b/data/item_stats/mag_stats.toml index 04e93e1..6fb9d47 100644 --- a/data/item_stats/mag_stats.toml +++ b/data/item_stats/mag_stats.toml @@ -3,6 +3,7 @@ feed_table = 0 [Varuna] feed_table = 1 +photon_blast = "Farlla" [Mitra] feed_table = 3 @@ -82,6 +83,7 @@ photon_blast = "Pilla" [Ribhava] feed_table = 5 +photon_blast = "Farlla" [Soma] feed_table = 5 @@ -120,6 +122,7 @@ photon_blast = "MyllaYoulla" [Ravana] feed_table = 6 +photon_blast = "Farlla" [Marica] feed_table = 6 diff --git a/src/bin/main.rs b/src/bin/main.rs index 6db053a..fe379e0 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -8,7 +8,7 @@ use elseware::login::character::CharacterServerState; use elseware::ship::ship::ShipServerState; use elseware::ship::ship::ShipServerStateBuilder; use elseware::entity::account::{NewUserAccountEntity, NewUserSettingsEntity}; -use elseware::entity::gateway::{EntityGateway, InMemoryGateway}; +use elseware::entity::gateway::{EntityGateway, InMemoryGateway, PostgresGateway}; use elseware::entity::character::NewCharacterEntity; use elseware::entity::item::{NewItemEntity, ItemDetail, ItemLocation}; @@ -48,17 +48,19 @@ fn setup_logger() { fn main() { setup_logger(); async_std::task::block_on(async move { + //let mut entity_gateway = PostgresGateway::new("localhost", "elsewhere", "elsewhere", ""); let mut entity_gateway = InMemoryGateway::new(); for i in 0..5 { let fake_user = NewUserAccountEntity { + email: format!("fake{}@email.com", i), username: if i == 0 { "hi".to_string() } else { format!("hi{}", i+1) }, password: bcrypt::hash("qwer", 5).unwrap(), guildcard: i + 1, team_id: None, - banned: false, - muted_until: SystemTime::now(), - created_at: SystemTime::now(), + banned_until: None, + muted_until: None, + created_at: chrono::Utc::now(), flags: 0, }; let fake_user = entity_gateway.create_user(fake_user).await.unwrap(); diff --git a/src/common/mainloop/client.rs b/src/common/mainloop/client.rs index ed32e73..ff9a71c 100644 --- a/src/common/mainloop/client.rs +++ b/src/common/mainloop/client.rs @@ -119,7 +119,7 @@ async fn send_pkt(socket: Arc Result<(), NetworkError> { let buf = pkt.as_bytes(); - println!("sndbuf: {:?}", buf); + //println!("sndbuf: {:?}", buf); let cbuf = cipher.lock().await.encrypt(&buf)?; let mut ssock = &*socket; ssock.write_all(&cbuf).await?; diff --git a/src/common/mainloop/interserver.rs b/src/common/mainloop/interserver.rs index 059e27e..8d8124b 100644 --- a/src/common/mainloop/interserver.rs +++ b/src/common/mainloop/interserver.rs @@ -139,27 +139,32 @@ where } async fn interserver_send_loop(server_id: ServerId, - mut socket: async_std::net::TcpStream, - output_loop_receiver: async_std::sync::Receiver) + mut socket: async_std::net::TcpStream, + output_loop_receiver: async_std::sync::Receiver) where S: Serialize + std::fmt::Debug + Send + 'static, { async_std::task::spawn(async move { loop { info!("login send loop"); - let msg = output_loop_receiver.recv().await.unwrap(); - - let payload = serde_json::to_string(&msg); - if let Ok(payload) = payload { - let len_bytes = u32::to_le_bytes(payload.len() as u32); - - match socket.write_all(&len_bytes).await { - Ok(_) => {}, - Err(err) => warn!("send failed: {:?}", err), - } - match socket.write_all(&payload.as_bytes()).await { - Ok(_) => {}, - Err(err) => warn!("send failed: {:?}", err), + match output_loop_receiver.recv().await { + Ok(msg) => { + let payload = serde_json::to_string(&msg); + if let Ok(payload) = payload { + let len_bytes = u32::to_le_bytes(payload.len() as u32); + + match socket.write_all(&len_bytes).await { + Ok(_) => {}, + Err(err) => warn!("send failed: {:?}", err), + } + match socket.write_all(&payload.as_bytes()).await { + Ok(_) => {}, + Err(err) => warn!("send failed: {:?}", err), + } + } + }, + Err(err) => { + warn!("error in send_loop: {:?}, {:?}", server_id, err) } } } @@ -179,7 +184,7 @@ pub fn login_listen_mainloop(state: Arc, - pub banned: bool, - pub muted_until: SystemTime, - pub created_at: SystemTime, + pub banned_until: Option>, + pub muted_until: Option>, + pub created_at: chrono::DateTime, pub flags: u32, } @@ -34,9 +35,9 @@ pub struct UserAccountEntity { pub password: String, pub guildcard: u32, pub team_id: Option, - pub banned: bool, - pub muted_until: SystemTime, - pub created_at: SystemTime, + pub banned_until: Option>, + pub muted_until: Option>, + pub created_at: chrono::DateTime, pub flags: u32, // TODO: is this used for anything other than character creation? } diff --git a/src/entity/character.rs b/src/entity/character.rs index 3c4d87a..c4f97de 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -7,7 +7,7 @@ use libpso::character::character::{DEFAULT_PALETTE_CONFIG, DEFAULT_TECH_MENU}; use crate::entity::item::tech::Technique; use crate::entity::account::UserAccountId; -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize)] pub enum CharacterClass { HUmar, HUnewearl, @@ -64,7 +64,7 @@ impl Into for CharacterClass { } -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, derive_more::Display)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize)] pub enum SectionID { Viridia, Greenill, @@ -134,7 +134,7 @@ pub struct TechLevel(pub u8); #[derive(Clone, Debug)] pub struct CharacterTechniques { - techs: HashMap + pub techs: HashMap } impl CharacterTechniques { @@ -163,7 +163,7 @@ impl CharacterTechniques { #[derive(Clone)] pub struct CharacterConfig { - raw_data: [u8; 0xE8], + pub raw_data: [u8; 0xE8], } impl CharacterConfig { @@ -184,7 +184,7 @@ impl CharacterConfig { #[derive(Clone)] pub struct CharacterInfoboard { - board: [u16; 172], + pub board: [u16; 172], } impl CharacterInfoboard { diff --git a/src/entity/gateway/entitygateway.rs b/src/entity/gateway/entitygateway.rs index 9cf4d4d..71c23fd 100644 --- a/src/entity/gateway/entitygateway.rs +++ b/src/entity/gateway/entitygateway.rs @@ -2,6 +2,7 @@ use crate::entity::account::*; use crate::entity::character::*; use crate::entity::item::*; +// TODO: all these Options should be Results #[async_trait::async_trait] pub trait EntityGateway: Send + Sync + Clone { async fn create_user(&mut self, _user: NewUserAccountEntity) -> Option { @@ -28,7 +29,7 @@ pub trait EntityGateway: Send + Sync + Clone { unimplemented!(); } - fn save_user_settings(&mut self, _settings: &UserSettingsEntity) { + async fn save_user_settings(&mut self, _settings: &UserSettingsEntity) { unimplemented!(); } @@ -36,6 +37,7 @@ pub trait EntityGateway: Send + Sync + Clone { unimplemented!(); } + // TODO: just make this a vec sorted by slot order? async fn get_characters_by_user(&self, _user: &UserAccountEntity) -> [Option; 4] { unimplemented!(); } diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index 56da621..2801eb1 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -43,7 +43,7 @@ impl EntityGateway for InMemoryGateway { password: user.password, guildcard: user.guildcard, team_id: user.team_id, - banned: user.banned, + banned_until: user.banned_until, muted_until: user.muted_until, created_at: user.created_at, flags: user.flags, diff --git a/src/entity/gateway/mod.rs b/src/entity/gateway/mod.rs index 214bdfd..528dcea 100644 --- a/src/entity/gateway/mod.rs +++ b/src/entity/gateway/mod.rs @@ -1,5 +1,7 @@ pub mod entitygateway; pub mod inmemory; +pub mod postgres; pub use entitygateway::EntityGateway; pub use inmemory::InMemoryGateway; +pub use self::postgres::PostgresGateway; diff --git a/src/entity/gateway/postgres/migrations/V0001__initial.sql b/src/entity/gateway/postgres/migrations/V0001__initial.sql new file mode 100644 index 0000000..c5805f1 --- /dev/null +++ b/src/entity/gateway/postgres/migrations/V0001__initial.sql @@ -0,0 +1,121 @@ +create table user_accounts ( + id serial primary key not null, + email varchar(128) not null unique, + username varchar(128) not null unique, + password varchar(128) not null, + banned timestamptz, + muted timestamptz, + created_at timestamptz default current_timestamp not null, + flags integer default 0 not null, + activated boolean default false not null, + game_session integer, + interserver_session integer +); + +create table user_settings ( + id serial primary key not null, + user_account integer references user_accounts (id) not null, + blocked_users bytea not null, + key_config bytea not null, + joystick_config bytea not null, + option_flags integer not null, + shortcuts bytea not null, + symbol_chats bytea not null, + team_name bytea not null +); + +/* TODO: guild card data */ + +create table player_character ( + id serial primary key not null, + user_account integer references user_accounts (id) not null, + slot smallint not null, + name varchar(12) not null, + exp integer not null, + class varchar(12) null, + section_id varchar(12) not null, + + costume smallint not null, + skin smallint not null, + face smallint not null, + head smallint not null, + hair smallint not null, + hair_r smallint not null, + hair_g smallint not null, + hair_b smallint not null, + prop_x real not null, + prop_y real not null, + + techs bytea not null, + + config bytea not null, + infoboard varchar(172) not null, + guildcard varchar(172) not null, + option_flags integer not null, + + power smallint not null, + mind smallint not null, + def smallint not null, + evade smallint not null, + luck smallint not null, + hp smallint not null, + tp smallint not null, + + tech_menu bytea not null, + meseta integer not null, + bank_meseta integer not null + +); + + +create table item ( + id serial primary key not null, + item jsonb not null +); + +create table item_location ( + item integer references item (id) not null, + location jsonb not null, + created_at timestamptz default current_timestamp not null +); + +create table inventory_slots ( + pchar integer references player_character not null, + items integer[30] /* references item (id) */ +); + +create table weapon_modifier ( + weapon integer references item (id) not null, + modifier jsonb not null, + created_at timestamptz default current_timestamp not null +); + +create table armor_modifier ( + armor integer references item (id) not null, + modifier jsonb not null, + created_at timestamptz default current_timestamp not null +); + +create table shield_modifier ( + shield integer references item (id) not null, + modifier jsonb not null, + created_at timestamptz default current_timestamp not null +); + +create table unit_modifier ( + unit integer references item (id) not null, + modifier jsonb not null, + created_at timestamptz default current_timestamp not null +); + +create table esweapon_modifier ( + esweapon integer references item (id) not null, + modifier jsonb not null, + created_at timestamptz default current_timestamp not null +); + +create table mag_modifier ( + mag integer references item (id) not null, + modifier jsonb not null, + created_at timestamptz default current_timestamp not null +); diff --git a/src/entity/gateway/postgres/migrations/mod.rs b/src/entity/gateway/postgres/migrations/mod.rs new file mode 100644 index 0000000..533298a --- /dev/null +++ b/src/entity/gateway/postgres/migrations/mod.rs @@ -0,0 +1,3 @@ +use refinery::include_migration_mods; + +include_migration_mods!("src/entity/gateway/postgres/migrations"); diff --git a/src/entity/gateway/postgres/mod.rs b/src/entity/gateway/postgres/mod.rs new file mode 100644 index 0000000..7ba478c --- /dev/null +++ b/src/entity/gateway/postgres/mod.rs @@ -0,0 +1,5 @@ +pub mod postgres; +pub mod migrations; +pub mod models; + +pub use self::postgres::PostgresGateway; diff --git a/src/entity/gateway/postgres/models.rs b/src/entity/gateway/postgres/models.rs new file mode 100644 index 0000000..7d508d6 --- /dev/null +++ b/src/entity/gateway/postgres/models.rs @@ -0,0 +1,691 @@ +use std::collections::HashMap; +use std::convert::Into; +use serde::{Serialize, Deserialize}; +use futures::TryStreamExt; +use libpso::character::{settings, guildcard}; +use libpso::util::vec_to_array; +use crate::entity::account::*; +use crate::entity::character::*; +use crate::entity::gateway::EntityGateway; +use crate::entity::item::*; +use crate::ship::map::MapArea; + +use sqlx::postgres::PgPoolOptions; +use sqlx::Row; +use sqlx::Execute; +use postgres::{Client, NoTls}; + + +#[derive(Debug, sqlx::FromRow)] +pub struct PgUserAccount { + id: i32, + username: String, + password: String, + banned: Option>, + muted: Option>, + created_at: chrono::DateTime, + flags: i32, +} + +impl Into for PgUserAccount { + fn into(self) -> UserAccountEntity { + UserAccountEntity { + id: UserAccountId(self.id as u32), + username: self.username, + password: self.password, + banned_until: self.banned, + muted_until: self.muted, + created_at: self.created_at, + flags: self.flags as u32, + // TOOD + guildcard: self.id as u32 + 1, + team_id: None, + } + } +} + +#[derive(Debug, sqlx::FromRow)] +pub struct PgUserSettings { + id: i32, + user_account: i32, + blocked_users: Vec, //[u32; 0x1E], + key_config: Vec, //[u8; 0x16C], + joystick_config: Vec, //[u8; 0x38], + option_flags: i32, + shortcuts: Vec, //[u8; 0xA40], + symbol_chats: Vec, //[u8; 0x4E0], + team_name: Vec, //[u16; 0x10], +} + +impl Into for PgUserSettings { + fn into(self) -> UserSettingsEntity { + UserSettingsEntity { + id: UserSettingsId(self.id as u32), + user_id: UserAccountId(self.user_account as u32), + settings: settings::UserSettings { + blocked_users: vec_to_array(self.blocked_users.chunks(4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect()), + key_config: vec_to_array(self.key_config), + joystick_config: vec_to_array(self.joystick_config), + option_flags: self.option_flags as u32, + shortcuts: vec_to_array(self.shortcuts), + symbol_chats: vec_to_array(self.symbol_chats), + team_name: vec_to_array(self.team_name.chunks(2).map(|b| u16::from_le_bytes([b[0], b[1]])).collect()), + } + } + } +} + +#[derive(sqlx::Type, Debug)] +#[sqlx(rename_all = "lowercase")] +pub enum PgCharacterClass { + HUmar, + HUnewearl, + HUcast, + HUcaseal, + RAmar, + RAmarl, + RAcast, + RAcaseal, + FOmar, + FOmarl, + FOnewm, + FOnewearl, +} + +impl Into for PgCharacterClass { + fn into(self) -> CharacterClass { + match self { + PgCharacterClass::HUmar => CharacterClass::HUmar, + PgCharacterClass::HUnewearl => CharacterClass::HUnewearl, + PgCharacterClass::HUcast => CharacterClass::HUcast, + PgCharacterClass::HUcaseal => CharacterClass::HUcaseal, + PgCharacterClass::RAmar => CharacterClass::RAmar, + PgCharacterClass::RAmarl => CharacterClass::RAmarl, + PgCharacterClass::RAcast => CharacterClass::RAcast, + PgCharacterClass::RAcaseal => CharacterClass::RAcaseal, + PgCharacterClass::FOmar => CharacterClass::FOmar, + PgCharacterClass::FOmarl => CharacterClass::FOmarl, + PgCharacterClass::FOnewm => CharacterClass::FOnewm, + PgCharacterClass::FOnewearl => CharacterClass::FOnewearl, + } + } +} + +impl From for PgCharacterClass { + fn from(other: CharacterClass) -> PgCharacterClass { + match other { + CharacterClass::HUmar => PgCharacterClass::HUmar, + CharacterClass::HUnewearl => PgCharacterClass::HUnewearl, + CharacterClass::HUcast => PgCharacterClass::HUcast, + CharacterClass::HUcaseal => PgCharacterClass::HUcaseal, + CharacterClass::RAmar => PgCharacterClass::RAmar, + CharacterClass::RAmarl => PgCharacterClass::RAmarl, + CharacterClass::RAcast => PgCharacterClass::RAcast, + CharacterClass::RAcaseal => PgCharacterClass::RAcaseal, + CharacterClass::FOmar => PgCharacterClass::FOmar, + CharacterClass::FOmarl => PgCharacterClass::FOmarl, + CharacterClass::FOnewm => PgCharacterClass::FOnewm, + CharacterClass::FOnewearl => PgCharacterClass::FOnewearl, + } + } +} + +#[derive(sqlx::Type, Debug)] +#[sqlx(rename_all = "lowercase")] +pub enum PgSectionId { + Viridia, + Greenill, + Skyly, + Bluefull, + Purplenum, + Pinkal, + Redria, + Oran, + Yellowboze, + Whitill, +} + +impl Into for PgSectionId { + fn into(self) -> SectionID { + match self { + PgSectionId::Viridia => SectionID::Viridia, + PgSectionId::Greenill => SectionID::Greenill, + PgSectionId::Skyly => SectionID::Skyly, + PgSectionId::Bluefull => SectionID::Bluefull, + PgSectionId::Purplenum => SectionID::Purplenum, + PgSectionId::Pinkal => SectionID::Pinkal, + PgSectionId::Redria => SectionID::Redria, + PgSectionId::Oran => SectionID::Oran, + PgSectionId::Yellowboze => SectionID::Yellowboze, + PgSectionId::Whitill => SectionID::Whitill, + } + } +} + +impl From for PgSectionId { + fn from(other: SectionID) -> PgSectionId { + match other { + SectionID::Viridia => PgSectionId::Viridia, + SectionID::Greenill => PgSectionId::Greenill, + SectionID::Skyly => PgSectionId::Skyly, + SectionID::Bluefull => PgSectionId::Bluefull, + SectionID::Purplenum => PgSectionId::Purplenum, + SectionID::Pinkal => PgSectionId::Pinkal, + SectionID::Redria => PgSectionId::Redria, + SectionID::Oran => PgSectionId::Oran, + SectionID::Yellowboze => PgSectionId::Yellowboze, + SectionID::Whitill => PgSectionId::Whitill, + } + } +} + + +#[derive(Debug, sqlx::FromRow)] +pub struct PgCharacter { + pub id: i32, + user_account: i32, + pub slot: i16, + name: String, + exp: i32, + class: String, + section_id: String, + + costume: i16, + skin: i16, + face: i16, + head: i16, + hair: i16, + hair_r: i16, + hair_g: i16, + hair_b: i16, + prop_x: f32, + prop_y: f32, + + techs: Vec, + + config: Vec, + infoboard: String, + guildcard: String, + option_flags: i32, + + power: i16, + mind: i16, + def: i16, + evade: i16, + luck: i16, + hp: i16, + tp: i16, + + tech_menu: Vec, + meseta: i32, + bank_meseta: i32, +} + +impl Into for PgCharacter { + fn into(self) -> CharacterEntity { + CharacterEntity { + id: CharacterEntityId(self.id as u32), + user_id: UserAccountId(self.user_account as u32), + slot: self.slot as u32, + name: self.name, + exp: self.exp as u32, + char_class: self.class.parse().unwrap(), + section_id: self.section_id.parse().unwrap(), + appearance: CharacterAppearance { + costume: self.costume as u16, + skin: self.skin as u16, + face: self.face as u16, + head: self.head as u16, + hair: self.hair as u16, + hair_r: self.hair_r as u16, + hair_g: self.hair_g as u16, + hair_b: self.hair_b as u16, + prop_x: self.prop_x, + prop_y: self.prop_y, + }, + techs: CharacterTechniques { + techs: self.techs.iter().enumerate().take(19).map(|(i, t)| (tech::Technique::from_value(i as u8), TechLevel(*t)) ).collect() + }, + config: CharacterConfig { + raw_data: vec_to_array(self.config) + }, + info_board: CharacterInfoboard { + board: libpso::utf8_to_utf16_array!(self.infoboard, 172), + }, + guildcard: CharacterGuildCard { + description: self.guildcard, + }, + option_flags: self.option_flags as u32, + materials: CharacterMaterials { + power: self.power as u32, + mind: self.mind as u32, + def: self.def as u32, + evade: self.evade as u32, + luck: self.luck as u32, + hp: self.hp as u32, + tp: self.tp as u32, + }, + tech_menu: CharacterTechMenu { + tech_menu: vec_to_array(self.tech_menu) + }, + meseta: self.meseta as u32, + bank_meseta: self.bank_meseta as u32, + } + } +} + +#[derive(Debug, sqlx::FromRow)] +pub struct PgGuildCard { +} + + + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgWeapon { + weapon: weapon::WeaponType, + special: Option, + grind: u8, + attrs: HashMap, + tekked: bool, +} + +impl From for PgWeapon { + fn from(other: weapon::Weapon) -> PgWeapon { + PgWeapon { + weapon: other.weapon, + special: other.special, + grind: other.grind, + attrs: other.attrs.iter().flatten().map(|attr| (attr.attr, attr.value)).collect(), + tekked: other.tekked, + } + } +} + +impl Into for PgWeapon { + fn into(self) -> weapon::Weapon { + let mut attrs: [Option; 3] = [None; 3]; + for (attr, (atype, value)) in attrs.iter_mut().zip(self.attrs.iter()) { + *attr = Some(weapon::WeaponAttribute { + attr: *atype, + value: *value + }); + } + + weapon::Weapon { + weapon: self.weapon, + special: self.special, + grind: self.grind, + attrs: attrs, + tekked: self.tekked, + modifiers: Vec::new(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgArmor { + armor: armor::ArmorType, + dfp: u8, + evp: u8, + slots: u8, +} + +impl From for PgArmor { + fn from(other: armor::Armor) -> PgArmor { + PgArmor { + armor: other.armor, + dfp: other.dfp, + evp: other.evp, + slots: other.slots, + } + } +} + +impl Into for PgArmor { + fn into(self) -> armor::Armor { + armor::Armor { + armor: self.armor, + dfp: self.dfp, + evp: self.evp, + slots: self.slots, + modifiers: Vec::new(), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgShield { + shield: shield::ShieldType, + dfp: u8, + evp: u8, +} + +impl From for PgShield { + fn from(other: shield::Shield) -> PgShield { + PgShield { + shield: other.shield, + dfp: other.dfp, + evp: other.evp, + } + } +} + +impl Into for PgShield { + fn into(self) -> shield::Shield { + shield::Shield { + shield: self.shield, + dfp: self.dfp, + evp: self.evp, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgUnit { + unit: unit::UnitType, + modifier: Option, +} + +impl From for PgUnit { + fn from(other: unit::Unit) -> PgUnit { + PgUnit { + unit: other.unit, + modifier: other.modifier, + } + } +} + +impl Into for PgUnit { + fn into(self) -> unit::Unit { + unit::Unit { + unit: self.unit, + modifier: self.modifier, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgTool { + pub tool: tool::ToolType, +} + +impl From for PgTool { + fn from(other: tool::Tool) -> PgTool { + PgTool { + tool: other.tool, + } + } +} + +impl Into for PgTool { + fn into(self) -> tool::Tool { + tool::Tool { + tool: self.tool, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgTechDisk { + tech: tech::Technique, + level: u32, +} + +impl From for PgTechDisk { + fn from(other: tech::TechniqueDisk) -> PgTechDisk { + PgTechDisk { + tech: other.tech, + level: other.level, + } + } +} + +impl Into for PgTechDisk { + fn into(self) -> tech::TechniqueDisk { + tech::TechniqueDisk { + tech: self.tech, + level: self.level + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgMag { + mag: mag::MagType, + synchro: u8, + color: u8, +} + +impl From for PgMag { + fn from(other: mag::Mag) -> PgMag { + PgMag { + mag: other.mag, + synchro: other.synchro, + color: other.color, + } + } +} + +impl Into for PgMag { + fn into(self) -> mag::Mag { + /*mag::Mag { + mag: self.mag, + synchro: self.synchro, + color: self.color, + def: 500, + pow: 0, + dex: 0, + mnd: 0, + iq: 0, + photon_blast: [None; 3], + class: CharacterClass::HUmar, + id: SectionID::Viridia, + }*/ + let mut mag = mag::Mag::baby_mag(self.color as u16); + mag.mag = self.mag; + mag.synchro = self.synchro; + mag + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PgESWeapon { + esweapon: esweapon::ESWeaponType, + special: Option, + name: String, + grind: u8, +} + +impl From for PgESWeapon { + fn from(other: esweapon::ESWeapon) -> PgESWeapon { + PgESWeapon { + esweapon: other.esweapon, + special: other.special, + name: other.name, + grind: other.grind, + } + } +} + +impl Into for PgESWeapon { + fn into(self) -> esweapon::ESWeapon { + esweapon::ESWeapon { + esweapon: self.esweapon, + special: self.special, + name: self.name, + grind: self.grind, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum PgItemDetail { + Weapon(PgWeapon), + Armor(PgArmor), + Shield(PgShield), + Unit(PgUnit), + Tool(PgTool), + TechDisk(PgTechDisk), + Mag(PgMag), + ESWeapon(PgESWeapon), +} + +impl From for PgItemDetail { + fn from(other: ItemDetail) -> PgItemDetail { + match other { + ItemDetail::Weapon(weapon) => PgItemDetail::Weapon(weapon.into()), + ItemDetail::Armor(armor) => PgItemDetail::Armor(armor.into()), + ItemDetail::Shield(shield) => PgItemDetail::Shield(shield.into()), + ItemDetail::Unit(unit) => PgItemDetail::Unit(unit.into()), + ItemDetail::Tool(tool) => PgItemDetail::Tool(tool.into()), + ItemDetail::TechniqueDisk(tech_disk) => PgItemDetail::TechDisk(tech_disk.into()), + ItemDetail::Mag(mag) => PgItemDetail::Mag(mag.into()), + ItemDetail::ESWeapon(esweapon) => PgItemDetail::ESWeapon(esweapon.into()), + } + } +} + +impl Into for PgItemDetail { + fn into(self) -> ItemDetail { + match self { + PgItemDetail::Weapon(weapon) => ItemDetail::Weapon(weapon.into()), + PgItemDetail::Armor(armor) => ItemDetail::Armor(armor.into()), + PgItemDetail::Shield(shield) => ItemDetail::Shield(shield.into()), + PgItemDetail::Unit(unit) => ItemDetail::Unit(unit.into()), + PgItemDetail::Tool(tool) => ItemDetail::Tool(tool.into()), + PgItemDetail::TechDisk(tech_disk) => ItemDetail::TechniqueDisk(tech_disk.into()), + PgItemDetail::Mag(mag) => ItemDetail::Mag(mag.into()), + PgItemDetail::ESWeapon(esweapon) => ItemDetail::ESWeapon(esweapon.into()), + } + } +} + +#[derive(Debug, sqlx::FromRow)] +pub struct PgItem { + pub id: i32, + pub item: sqlx::types::Json, +} + + +#[derive(Debug, Serialize, Deserialize)] +pub enum PgItemLocationDetail { + Inventory { + character_id: u32, + #[serde(skip_serializing)] + slot: usize, + equipped: bool, + }, + Bank { + character_id: u32, + name: String, + }, + LocalFloor { + character_id: u32, + map_area: MapArea, + x: f32, + y: f32, + z: f32, + }, + SharedFloor { + map_area: MapArea, + x: f32, + y: f32, + z: f32, + }, + Consumed, + FedToMag { + mag: u32, + }, + Shop, +} + +impl From for PgItemLocationDetail { + fn from(other: ItemLocation) -> PgItemLocationDetail { + match other { + ItemLocation::Inventory{character_id, slot, equipped} => PgItemLocationDetail::Inventory{character_id: character_id.0, slot, equipped}, + ItemLocation::Bank{character_id, name} => PgItemLocationDetail::Bank{character_id: character_id.0, name: name.0}, + ItemLocation::LocalFloor{character_id, map_area, x,y,z} => PgItemLocationDetail::LocalFloor{character_id: character_id.0, map_area, x,y,z}, + ItemLocation::SharedFloor{map_area, x,y,z} => PgItemLocationDetail::SharedFloor{map_area, x,y,z}, + ItemLocation::Consumed => PgItemLocationDetail::Consumed, + ItemLocation::FedToMag{mag} => PgItemLocationDetail::FedToMag{mag: mag.0}, + ItemLocation::Shop => PgItemLocationDetail::Shop, + } + } +} + +impl Into for PgItemLocationDetail { + fn into(self) -> ItemLocation { + match self { + PgItemLocationDetail::Inventory{character_id, slot, equipped} => ItemLocation::Inventory{character_id: CharacterEntityId(character_id), slot, equipped}, + PgItemLocationDetail::Bank{character_id, name} => ItemLocation::Bank{character_id: CharacterEntityId(character_id), name: BankName(name)}, + PgItemLocationDetail::LocalFloor{character_id, map_area, x,y,z} => ItemLocation::LocalFloor{character_id: CharacterEntityId(character_id), map_area, x,y,z}, + PgItemLocationDetail::SharedFloor{map_area, x,y,z} => ItemLocation::SharedFloor{map_area, x,y,z}, + PgItemLocationDetail::Consumed => ItemLocation::Consumed, + PgItemLocationDetail::FedToMag{mag} => ItemLocation::FedToMag{mag: ItemEntityId(mag)}, + PgItemLocationDetail::Shop => ItemLocation::Shop, + } + } +} + + +#[derive(Debug, sqlx::FromRow)] +pub struct PgItemLocation { + //pub id: i32, + pub location: sqlx::types::Json, + created_at: chrono::DateTime, +} + + +#[derive(Debug, Serialize, Deserialize)] +pub enum PgMagModifierDetail { + FeedMag(i32), + BankMag, + MagCell(i32), + OwnerChange(CharacterClass, SectionID) +} + +impl From for PgMagModifierDetail { + fn from(other: mag::MagModifier) -> PgMagModifierDetail { + match other { + mag::MagModifier::FeedMag{food} => PgMagModifierDetail::FeedMag(food.0 as i32), + mag::MagModifier::BankMag => PgMagModifierDetail::BankMag, + mag::MagModifier::MagCell(cell) => PgMagModifierDetail::MagCell(cell.0 as i32), + mag::MagModifier::OwnerChange(class, section_id) => PgMagModifierDetail::OwnerChange(class, section_id), + } + } +} + +impl Into for PgMagModifierDetail { + fn into(self) -> mag::MagModifier { + match self { + PgMagModifierDetail::FeedMag(food) => mag::MagModifier::FeedMag{food: ItemEntityId(food as u32)}, + PgMagModifierDetail::BankMag => mag::MagModifier::BankMag, + PgMagModifierDetail::MagCell(cell) => mag::MagModifier::MagCell(ItemEntityId(cell as u32)), + PgMagModifierDetail::OwnerChange(class, section_id) => mag::MagModifier::OwnerChange(class, section_id), + } + } +} + +#[derive(Debug, sqlx::FromRow)] +pub struct PgMagModifier { + mag: i32, + pub modifier: sqlx::types::Json, + created_at: chrono::DateTime, +} + + +#[derive(Debug, sqlx::FromRow)] +pub struct PgItemWithLocation { + pub id: i32, + pub item: sqlx::types::Json, + pub location: sqlx::types::Json, +} + + +#[derive(Debug, sqlx::FromRow)] +pub struct PgMagModifierWithParameters { + pub mag: i32, + pub modifier: sqlx::types::Json, + pub feed: Option>, + pub cell: Option>, +} diff --git a/src/entity/gateway/postgres/postgres.rs b/src/entity/gateway/postgres/postgres.rs new file mode 100644 index 0000000..c26650f --- /dev/null +++ b/src/entity/gateway/postgres/postgres.rs @@ -0,0 +1,404 @@ +use std::convert::{From, TryFrom, Into, TryInto}; +use serde::{Serialize, Deserialize}; +use futures::future::join_all; +use futures::TryStreamExt; +//use futures::StreamExt; +use async_std::stream::StreamExt; +//use futures::StreamExt; +use libpso::character::{settings, guildcard}; +use libpso::util::vec_to_array; +use crate::entity::account::*; +use crate::entity::character::*; +use crate::entity::gateway::EntityGateway; +use crate::entity::item::*; +use super::models::*; + +use sqlx::postgres::PgPoolOptions; +use sqlx::Row; +use sqlx::Execute; +use postgres::{Client, NoTls}; + +mod embedded { + use refinery::embed_migrations; + embed_migrations!("src/entity/gateway/postgres/migrations"); +} + + +#[derive(Clone)] +pub struct PostgresGateway { + pool: sqlx::Pool, +} + +impl PostgresGateway { + pub fn new(host: &str, dbname: &str, username: &str, password: &str) -> PostgresGateway { + // the postgres dep can be removed once refinery supports sqlx + let mut conn = Client::connect(&format!("host='{}' dbname='{}' user='{}' password='{}'", host, dbname, username, password), NoTls).unwrap(); + embedded::migrations::runner().run(&mut conn).unwrap(); + + let pool = async_std::task::block_on(async move { + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(&format!("postgresql://{}:{}@{}:5432/{}", username, password, host, dbname)).await.unwrap(); + + pool + }); + + PostgresGateway { + pool: pool, + } + } + + async fn apply_item_modifications(&self, item: ItemEntity) -> ItemEntity { + let ItemEntity {id, item, location} = item; + + let item = match item { + ItemDetail::Mag(mut mag) => { + let q = r#"select mag, modifier, item.item -> 'Tool' as feed, item2.item -> 'Tool' as cell + from mag_modifier + left join item on item.id = cast (modifier ->> 'FeedMag' as integer) + left join item as item2 on item2.id = cast (modifier ->> 'MagCell' as integer) + where mag = $1 order by created_at"#; + let mag_modifiers = sqlx::query_as::<_, PgMagModifierWithParameters>(q) + .bind(id.0 as i32) + .fetch(&self.pool); + + mag_modifiers.for_each(|modifier| { + let PgMagModifierWithParameters {modifier, feed, cell, ..} = modifier.unwrap(); + let modifier: mag::MagModifier = modifier.0.into(); + match modifier { + mag::MagModifier::FeedMag{..} => { + mag.feed(feed.unwrap().tool) + }, + mag::MagModifier::BankMag => { + mag.bank() + }, + mag::MagModifier::MagCell(_) => { + mag.apply_mag_cell(mag::MagCell::try_from(Into::::into(cell.unwrap().0).tool).unwrap()) + }, + mag::MagModifier::OwnerChange(class, section_id) => { + mag.change_owner(class, section_id) + }, + } + }).await; + + ItemDetail::Mag(mag) + }, + item @ _ => item + }; + + ItemEntity { + id: id, + item: item, + location: location + } + } +} + +#[async_trait::async_trait] +impl EntityGateway for PostgresGateway { + async fn create_user(&mut self, user: NewUserAccountEntity) -> Option { + let new_user = sqlx::query_as::<_, PgUserAccount>("insert into user_accounts (email, username, password) values ($1, $2, $3) returning *;") + .bind(user.email) + .bind(user.username) + .bind(user.password) + .fetch_one(&self.pool).await.unwrap(); + Some(new_user.into()) + } + + async fn get_user_by_id(&self, id: UserAccountId) -> Option { + let user = sqlx::query_as::<_, PgUserAccount>("select * from user_accounts where id = $1") + .bind(id.0) + .fetch_one(&self.pool).await.unwrap(); + Some(user.into()) + } + + async fn get_user_by_name(&self, username: String) -> Option { + let user = sqlx::query_as::<_, PgUserAccount>("select * from user_accounts where username = $1") + .bind(username) + .fetch_one(&self.pool).await.unwrap(); + Some(user.into()) + } + + async fn save_user(&mut self, user: &UserAccountEntity) { + sqlx::query("UPDATE user_accounts set name=$1, password=$2, banned=$3, muted=$4, flags=$5 where id=$6") + .bind(&user.username) + .bind(&user.password) + .bind(&user.banned_until) + .bind(&user.muted_until) + .bind(&user.flags) + .bind(&user.id.0) + .fetch_one(&self.pool).await.unwrap(); + } + + async fn create_user_settings(&mut self, settings: NewUserSettingsEntity) -> Option { + let new_settings = sqlx::query_as::<_, PgUserSettings>("insert into user_settings (user_account, blocked_users, key_config, joystick_config, option_flags, shortcuts, symbol_chats, team_name) + values ($1, $2, $3, $4, $5, $6, $7, $8) returning *;") + .bind(settings.user_id.0) + .bind(settings.settings.blocked_users.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .bind(settings.settings.key_config.to_vec()) + .bind(settings.settings.joystick_config.to_vec()) + .bind(settings.settings.option_flags as i32) + .bind(settings.settings.shortcuts.to_vec()) + .bind(settings.settings.symbol_chats.to_vec()) + .bind(settings.settings.team_name.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .fetch_one(&self.pool).await.unwrap(); + Some(new_settings.into()) + } + + async fn get_user_settings_by_user(&self, user: &UserAccountEntity) -> Option { + let settings = sqlx::query_as::<_, PgUserSettings>("select * from user_settings where id = $1") + .bind(user.id.0) + .fetch_one(&self.pool).await.unwrap(); + Some(settings.into()) + } + + async fn save_user_settings(&mut self, settings: &UserSettingsEntity) { + sqlx::query("update user_settings set blocked_users=$1, key_config=$2, joystick_config=$3, option_flags=$4, shortcuts=$5, symbol_chats=$6, team_name=$7 where id=$8") + .bind(settings.settings.blocked_users.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .bind(&settings.settings.key_config.to_vec()) + .bind(&settings.settings.joystick_config.to_vec()) + .bind(&settings.settings.option_flags) + .bind(&settings.settings.shortcuts.to_vec()) + .bind(&settings.settings.symbol_chats.to_vec()) + .bind(settings.settings.team_name.to_vec().into_iter().map(|i| i.to_le_bytes().to_vec()).flatten().collect::>()) + .bind(&settings.id.0) + .fetch_one(&self.pool).await.unwrap(); + } + + async fn create_character(&mut self, char: NewCharacterEntity) -> Option { + let q = r#"insert into player_character + (user_account, slot, name, exp, class, section_id, costume, skin, face, head, hair, hair_r, hair_g, hair_b, prop_x, prop_y, techs, + config, infoboard, guildcard, power, mind, def, evade, luck, hp, tp, tech_menu, meseta, bank_meseta, option_flags) + values + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31) + returning *;"#; + let character = sqlx::query_as::<_, PgCharacter>(q) + //sqlx::query(q) + .bind(char.user_id.0) + .bind(char.slot as i16) + .bind(char.name) + .bind(char.exp as i32) + .bind(char.char_class.to_string()) + .bind(char.section_id.to_string()) + .bind(char.appearance.costume as i16) + .bind(char.appearance.skin as i16) + .bind(char.appearance.face as i16) + .bind(char.appearance.head as i16) + .bind(char.appearance.hair as i16) + .bind(char.appearance.hair_r as i16) + .bind(char.appearance.hair_g as i16) + .bind(char.appearance.hair_b as i16) + .bind(char.appearance.prop_x) + .bind(char.appearance.prop_y) + .bind(&char.techs.as_bytes().to_vec()) + .bind(&char.config.as_bytes().to_vec()) + .bind(String::from_utf16_lossy(&char.info_board.board).trim_matches(char::from(0))) + .bind(char.guildcard.description) + .bind(char.materials.power as i16) + .bind(char.materials.mind as i16) + .bind(char.materials.def as i16) + .bind(char.materials.evade as i16) + .bind(char.materials.luck as i16) + .bind(char.materials.hp as i16) + .bind(char.materials.tp as i16) + .bind(char.tech_menu.tech_menu.to_vec()) + .bind(char.meseta as i32) + .bind(char.bank_meseta as i32) + .bind(char.option_flags as i32) + .fetch_one(&self.pool).await.unwrap(); + + sqlx::query("insert into inventory_slots (pchar) values ($1)") + .bind(character.id) + .execute(&self.pool).await.unwrap(); + Some(character.into()) + } + + async fn get_characters_by_user(&self, user: &UserAccountEntity) -> [Option; 4] { + let mut stream = sqlx::query_as::<_, PgCharacter>("select * from player_character where user_account = $1 and slot < 4 order by slot") + .bind(user.id.0) + .fetch(&self.pool); + let mut result = [None; 4]; + while let Some(character) = stream.try_next().await.unwrap() { + let index = character.slot as usize; + result[index] = Some(character.into()) + } + + result + } + + async fn save_character(&mut self, char: &CharacterEntity) { + let q = r#"update player_character set + user_account=$1, slot=$2, name=$3, exp=$4, class=$5, section_id=$6, costume=$7, skin=$8, face=$9, head=$10, hair=$11, hair_r=$12, + hair_g=$13, hair_b=$14, prop_x=$15, prop_y=$16, techs=$17, config=$18, infoboard=$19, guildcard=$20, power=$21, mind=$22, def=$23, + evade=$24, luck=$25, hp=$26, tp=$27, tech_menu=$28, meseta=$29, bank_meseta=$30 + where id=$31;"#; + sqlx::query(q) + .bind(char.user_id.0) + .bind(char.slot as i16) + .bind(&char.name) + .bind(char.exp as i32) + .bind(char.char_class.to_string()) + .bind(char.section_id.to_string()) + .bind(char.appearance.costume as i16) + .bind(char.appearance.skin as i16) + .bind(char.appearance.face as i16) + .bind(char.appearance.head as i16) + .bind(char.appearance.hair as i16) + .bind(char.appearance.hair_r as i16) + .bind(char.appearance.hair_g as i16) + .bind(char.appearance.hair_b as i16) + .bind(char.appearance.prop_x) + .bind(char.appearance.prop_y) + .bind(&char.techs.as_bytes().to_vec()) + .bind(&char.config.as_bytes().to_vec()) + .bind(String::from_utf16_lossy(&char.info_board.board).trim_matches(char::from(0))) + .bind(&char.guildcard.description) + .bind(char.materials.power as i16) + .bind(char.materials.mind as i16) + .bind(char.materials.def as i16) + .bind(char.materials.evade as i16) + .bind(char.materials.luck as i16) + .bind(char.materials.hp as i16) + .bind(char.materials.tp as i16) + .bind(char.tech_menu.tech_menu.to_vec()) + .bind(char.meseta as i32) + .bind(char.bank_meseta as i32) + .bind(char.id.0 as i32) + .execute(&self.pool).await.unwrap(); + } + + async fn get_guild_card_data_by_user(&self, user: &UserAccountEntity) -> GuildCardDataEntity { + GuildCardDataEntity { + id: GuildCardDataId(0), + user_id: user.id, + guildcard: guildcard::GuildCardData::default(), + } + } + + async fn create_item(&mut self, item: NewItemEntity) -> Option { + let new_item = sqlx::query_as::<_, PgItem>("insert into item (item) values ($1) returning *;") + .bind(sqlx::types::Json(PgItemDetail::from(item.item))) + .fetch_one(&self.pool).await.unwrap(); + let location = if let ItemLocation::Inventory{character_id, slot, ..} = &item.location { + sqlx::query("insert into item_location (item, location) values ($1, $2)") + .bind(new_item.id) + .bind(sqlx::types::Json(PgItemLocationDetail::from(item.location.clone()))) + .execute(&self.pool).await.unwrap(); + sqlx::query("update inventory_slots set items[$2] = $1 where pchar = $3") + .bind(new_item.id) + .bind(*slot as i32) + .bind(character_id.0 as i32) + .execute(&self.pool).await.unwrap(); + sqlx::query_as::<_, PgItemLocation>(r#"select + item_location.item, + jsonb_set(item_location.location, '{Inventory,slot}', (array_position(inventory_slots.items, item.id))::text::jsonb) as location, + item_location.created_at + from item_location + join item on item.id = item_location.item + join inventory_slots on inventory_slots.pchar = cast (item_location.location -> 'Inventory' ->> 'character_id' as integer) + where item.id = $1 + order by item_location.created_at + limit 1"#) + .bind(new_item.id) + .fetch_one(&self.pool).await.unwrap() + } + else { + sqlx::query_as::<_, PgItemLocation>("insert into item_location (item, location) values ($1, $2) returning *") + .bind(new_item.id) + .bind(sqlx::types::Json(PgItemLocationDetail::from(item.location))) + .fetch_one(&self.pool).await.unwrap() + }; + Some(ItemEntity { + id: ItemEntityId(new_item.id as u32), + item: new_item.item.0.into(), + location: location.location.0.into(), + }) + } + + async fn change_item_location(&mut self, item_id: &ItemEntityId, item_location: ItemLocation) { + if let ItemLocation::Inventory{character_id, slot, ..} = &item_location { + sqlx::query("update inventory_slots set items[array_position(items, $1)] = null where pchar = $2 and items[array_position(items, $1)] is not null") + .bind(item_id.0 as i32) + .bind(character_id.0 as i32) + .execute(&self.pool).await.unwrap(); + sqlx::query("update inventory_slots set items[$2] = $1 where pchar = $3") + .bind(item_id.0 as i32) + .bind(*slot as i32) + .bind(character_id.0 as i32) + .execute(&self.pool).await.unwrap(); + sqlx::query(r#"insert into item_location (item, location) + select $1, $2 + where (select jsonb_object_keys(location) from item_location where item=$1 + order by created_at desc limit 1) != 'Inventory'"#) + .bind(item_id.0) + .bind(sqlx::types::Json(PgItemLocationDetail::from(item_location))) + .execute(&self.pool).await.unwrap(); + } + else { + sqlx::query("insert into item_location (item, location) values ($1, $2)") + .bind(item_id.0) + .bind(sqlx::types::Json(PgItemLocationDetail::from(item_location))) + .execute(&self.pool).await.unwrap(); + } + } + + async fn feed_mag(&mut self, mag_item_id: &ItemEntityId, tool_item_id: &ItemEntityId) { + sqlx::query("insert into mag_modifier (mag, modifier) values ($1, $2);") + .bind(mag_item_id.0) + .bind(sqlx::types::Json(PgMagModifierDetail::from(mag::MagModifier::FeedMag{food: *tool_item_id}))) + .execute(&self.pool).await.unwrap(); + } + + async fn change_mag_owner(&mut self, mag_item_id: &ItemEntityId, character: &CharacterEntity) { + sqlx::query("insert into mag_modifier (mag, modifier) values ($1, $2);") + .bind(mag_item_id.0) + .bind(sqlx::types::Json(PgMagModifierDetail::from(mag::MagModifier::OwnerChange(character.char_class, character.section_id)))) + .execute(&self.pool).await.unwrap(); + } + + async fn use_mag_cell(&mut self, mag_item_id: &ItemEntityId, mag_cell_id: &ItemEntityId) { + sqlx::query("insert into mag_modifier (mag, modifier) values ($1, $2);") + .bind(mag_item_id.0) + .bind(sqlx::types::Json(PgMagModifierDetail::from(mag::MagModifier::MagCell(*mag_cell_id)))) + .execute(&self.pool).await.unwrap(); + } + + async fn get_items_by_character(&self, char: &CharacterEntity) -> Vec { + let q = r#"select * from ( + select distinct on (item_location.item) + item.id, + case + when item_location.location -> 'Inventory' is not null then + jsonb_set(item_location.location, '{Inventory,slot}', (array_position(inventory_slots.items, item.id))::text::jsonb) + else + item_location.location + end, + item.item + from item_location + join item on item.id = item_location.item + join inventory_slots on inventory_slots.pchar = $1 + order by item_location.item, item_location.created_at desc + ) as i + where cast (location -> 'Inventory' ->> 'character_id' as integer) = $1 + or cast (location -> 'Bank' ->> 'character_id' as integer) = $1 + "#; + let items = sqlx::query_as::<_, PgItemWithLocation>(q) + .bind(char.id.0) + .fetch(&self.pool); + join_all(items + .filter_map(|item: Result| { + let item = item.ok()?; + Some(ItemEntity { + id: ItemEntityId(item.id as u32), + item: item.item.0.into(), + location: item.location.0.into() + }) + }) + .map(|item: ItemEntity| { + self.apply_item_modifications(item) + }) + .collect::>() + .await + ).await + } +} diff --git a/src/entity/item/esweapon.rs b/src/entity/item/esweapon.rs index 1d091ec..3251924 100644 --- a/src/entity/item/esweapon.rs +++ b/src/entity/item/esweapon.rs @@ -1,3 +1,4 @@ +use serde::{Serialize, Deserialize}; // TODO: actually use this #[derive(Debug)] pub enum ItemParseError { @@ -8,7 +9,7 @@ pub enum ItemParseError { InvalidESWeaponName, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ESWeaponType { Saber = 0, Sword, @@ -120,7 +121,7 @@ impl ESWeaponType { } } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum ESWeaponSpecial { Jellen = 1, Zalure, @@ -305,4 +306,4 @@ mod test { } -} \ No newline at end of file +} diff --git a/src/entity/item/tech.rs b/src/entity/item/tech.rs index 326aab7..0403afa 100644 --- a/src/entity/item/tech.rs +++ b/src/entity/item/tech.rs @@ -48,6 +48,31 @@ impl Technique { Technique::Megid => 18, } } + + pub fn from_value(value: u8) -> Technique { + match value { + 0 => Technique::Foie, + 1 => Technique::Gifoie, + 2 => Technique::Rafoie, + 3 => Technique::Barta, + 4 => Technique::Gibarta, + 5 => Technique::Rabarta, + 6 => Technique::Zonde, + 7 => Technique::Gizonde, + 8 => Technique::Razonde, + 9 => Technique::Grants, + 10 => Technique::Deband, + 11 => Technique::Jellen, + 12 => Technique::Zalure, + 13 => Technique::Shifta, + 14 => Technique::Ryuker, + 15 => Technique::Resta, + 16 => Technique::Anti, + 17 => Technique::Reverser, + 18 => Technique::Megid, + _ => panic!() + } + } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/src/entity/item/unit.rs b/src/entity/item/unit.rs index 5528c1d..aa83bf1 100644 --- a/src/entity/item/unit.rs +++ b/src/entity/item/unit.rs @@ -323,7 +323,7 @@ impl UnitType { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum UnitModifier { PlusPlus, Plus, diff --git a/src/entity/item/weapon.rs b/src/entity/item/weapon.rs index 48a0e4c..3953da3 100644 --- a/src/entity/item/weapon.rs +++ b/src/entity/item/weapon.rs @@ -10,7 +10,7 @@ pub enum ItemParseError { InvalidWeaponAttribute, } -#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, Ord, PartialOrd, Serialize, Deserialize)] pub enum Attribute { Native = 1, ABeast, @@ -45,7 +45,7 @@ impl WeaponAttribute { } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum WeaponSpecial { Draw = 1, Drain, diff --git a/src/login/character.rs b/src/login/character.rs index 997e46d..a06a513 100644 --- a/src/login/character.rs +++ b/src/login/character.rs @@ -677,10 +677,10 @@ mod test { username: "testuser".to_owned(), password: bcrypt::hash("mypassword", 5).unwrap(), guildcard: 0, + banned_until: None, + muted_until: None, + created_at: chrono::Utc::now(), team_id: None, - banned: false, - muted_until: SystemTime::now(), - created_at: SystemTime::now(), flags: 0, }); server.clients.insert(ClientId(5), clientstate); @@ -721,9 +721,9 @@ mod test { password: bcrypt::hash("qwer", 5).unwrap(), guildcard: 3, team_id: None, - banned: false, - muted_until: SystemTime::now(), - created_at: SystemTime::now(), + banned_until: None, + muted_until: None, + created_at: chrono::Utc::now(), flags: 0, }); diff --git a/src/login/login.rs b/src/login/login.rs index bf19f22..d105b64 100644 --- a/src/login/login.rs +++ b/src/login/login.rs @@ -63,7 +63,7 @@ pub async fn get_login_status(entity_gateway: &impl EntityGateway, pkt: &Login) let user = entity_gateway.get_user_by_name(username).await.ok_or(AccountStatus::InvalidUser)?; let verified = bcrypt::verify(password, user.password.as_str()).map_err(|_err| AccountStatus::Error)?; match verified { - true => if user.banned { + true => if user.banned_until.map(|banned| banned > chrono::Utc::now()).unwrap_or(false) { Err(AccountStatus::Banned) } else { @@ -186,9 +186,9 @@ mod test { password: bcrypt::hash("mypassword", 5).unwrap(), guildcard: 0, team_id: None, - banned: false, - muted_until: SystemTime::now(), - created_at: SystemTime::now(), + banned_until: None, + muted_until: None, + created_at: chrono::Utc::now(), flags: 0, }) } @@ -270,9 +270,9 @@ mod test { password: bcrypt::hash("notpassword", 5).unwrap(), guildcard: 0, team_id: None, - banned: false, - muted_until: SystemTime::now(), - created_at: SystemTime::now(), + banned_until: None, + muted_until: None, + created_at: chrono::Utc::now(), flags: 0, }) } @@ -315,9 +315,9 @@ mod test { password: bcrypt::hash("mypassword", 5).unwrap(), guildcard: 0, team_id: None, - banned: true, - muted_until: SystemTime::now(), - created_at: SystemTime::now(), + banned_until: Some(chrono::Utc::now() + chrono::Duration::days(1)), + muted_until: None, + created_at: chrono::Utc::now(), flags: 0, }) } diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index e1519e6..0ad1126 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -565,5 +565,9 @@ impl CharacterInventory { (item, slot) }) } + + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } } diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index ace390b..59de10f 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -45,6 +45,35 @@ pub enum ItemManagerError { ItemIdNotInInventory(ClientItemId) } + +async fn update_inventory_slots(entity_gateway: &mut EG, character: &CharacterEntity, inventory: &CharacterInventory) { + for (slot, item) in inventory.iter().enumerate() { + match item { + InventoryItem::Individual(individual_inventory_item) => { + entity_gateway.change_item_location( + &individual_inventory_item.entity_id, + ItemLocation::Inventory { + character_id: character.id, + slot: slot, + equipped: individual_inventory_item.equipped, + } + ).await + }, + InventoryItem::Stacked(stacked_inventory_item) => { + for entity_id in stacked_inventory_item.entity_ids.iter() { + entity_gateway.change_item_location( + entity_id, + ItemLocation::Inventory { + character_id: character.id, + slot: slot, + equipped: false, + }).await; + }} + } + } +} + + pub struct ItemManager { id_counter: u32, @@ -449,6 +478,7 @@ impl ItemManager { }, } + update_inventory_slots(entity_gateway, character, &inventory).await; Ok(()) } @@ -528,6 +558,7 @@ impl ItemManager { ItemLocation::Consumed).await; } + update_inventory_slots(entity_gateway, character, &inventory).await; Ok(consumed_item) } @@ -564,6 +595,7 @@ impl ItemManager { } } + update_inventory_slots(entity_gateway, character, &inventory).await; Ok(()) } @@ -640,6 +672,7 @@ impl ItemManager { }).await; } + update_inventory_slots(entity_gateway, character, &inventory).await; Ok(()) } @@ -754,6 +787,7 @@ impl ItemManager { } _ => {} } + update_inventory_slots(entity_gateway, character, &inventory).await; Ok(()) } diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs index 2afcbcd..9621109 100644 --- a/src/ship/map/area.rs +++ b/src/ship/map/area.rs @@ -1,9 +1,10 @@ // TOOD: `pub(super) for most of these?` +use serde::{Serialize, Deserialize}; use std::collections::HashMap; use thiserror::Error; use crate::ship::room::Episode; -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] pub enum MapArea { Pioneer2Ep1, Forest1, diff --git a/src/ship/packet/handler/lobby.rs b/src/ship/packet/handler/lobby.rs index 55ecc3f..f8a898d 100644 --- a/src/ship/packet/handler/lobby.rs +++ b/src/ship/packet/handler/lobby.rs @@ -91,15 +91,15 @@ pub async fn change_lobby(id: ClientId, let old_neighbors = client_location.get_client_neighbors(id).unwrap(); let mut lobby = LobbyId(requested_lobby as usize); if let Err(_) = client_location.add_client_to_lobby(id, lobby) { - match prev_area { - RoomLobby::Lobby(_lobby) => { - let dialog = SmallDialog::new(String::from("Lobby is full.")); - return Ok(vec![(id, SendShipPacket::SmallDialog(dialog))]) - } - RoomLobby::Room(_room) => { - lobby = client_location.add_client_to_next_available_lobby(id, lobby).map_err(|_| ShipError::TooManyClients)?; - } + match prev_area { + RoomLobby::Lobby(_lobby) => { + let dialog = SmallDialog::new(String::from("Lobby is full.")); + return Ok(vec![(id, SendShipPacket::SmallDialog(dialog))]) } + RoomLobby::Room(_room) => { + lobby = client_location.add_client_to_next_available_lobby(id, lobby).map_err(|_| ShipError::TooManyClients)?; + } + } } item_manager.load_character(entity_gateway, &client.character).await; let join_lobby = packet::builder::lobby::join_lobby(id, lobby, client_location, clients, item_manager, level_table)?; diff --git a/tests/common.rs b/tests/common.rs index bce80d7..1eebef3 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -15,13 +15,14 @@ use libpso::{utf8_to_array, utf8_to_utf16_array}; pub async fn new_user_character(entity_gateway: &mut EG, username: &str, password: &str) -> (UserAccountEntity, CharacterEntity) { let new_user = NewUserAccountEntity { + email: format!("{}@pso.com", username), username: username.into(), password: bcrypt::hash(password, 5).unwrap(), guildcard: 1, team_id: None, - banned: false, - muted_until: SystemTime::now(), - created_at: SystemTime::now(), + banned_until: None, + muted_until: None, + created_at: chrono::Utc::now(), flags: 0, }; diff --git a/tests/test_shops.rs b/tests/test_shops.rs index f1c292c..d254b58 100644 --- a/tests/test_shops.rs +++ b/tests/test_shops.rs @@ -506,6 +506,7 @@ async fn test_techs_disappear_from_shop_when_bought() { assert!(p1_items[0].item != p1_items[1].item); } +// TOOD: this is not deterministic and can randomly fail #[async_std::test] async fn test_units_disappear_from_shop_when_bought() { let mut entity_gateway = InMemoryGateway::new();