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/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..d3e14a0 --- /dev/null +++ b/src/entity/gateway/postgres/migrations/V0001__initial.sql @@ -0,0 +1,111 @@ +create table user_accounts ( + id serial primary key not null, + name 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 +); + +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, + + 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 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..3ebb29e --- /dev/null +++ b/src/entity/gateway/postgres/models.rs @@ -0,0 +1,688 @@ +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, + name: 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.name, + 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 { + 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, + + 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, + }, + 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, + 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..c400da4 --- /dev/null +++ b/src/entity/gateway/postgres/postgres.rs @@ -0,0 +1,343 @@ +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 (name, password) values ($1, $2) returning *;") + .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 name = $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) + 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) + 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) + .fetch_one(&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 = 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) { + sqlx::query("insert into item_location (item, location) values ($1, $2);") + .bind(item_id.0) + .bind(sqlx::types::Json(PgItemLocationDetail::from(item_location))) + .fetch_one(&self.pool).await.unwrap(); + } + + async fn feed_mag(&mut self, mag_item_id: &ItemEntityId, tool_item_id: &ItemEntityId) { + sqlx::query("insert into item_location (item, location) values ($1, $2);") + .bind(tool_item_id.0) + .bind(sqlx::types::Json(PgItemLocationDetail::from(ItemLocation::FedToMag {mag: *mag_item_id}))) + .execute(&self.pool).await.unwrap(); + 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) { + // consume cell? + 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 item.id, item_location.location, item.item from item_location + join item on item.id = item_location.item + 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 + } +}