Browse Source

Merge branch 'master' into rare_monsters

pull/60/head
jake 2 years ago
parent
commit
27fcfcc1cc
  1. 6
      Cargo.toml
  2. 87
      src/bin/main.rs
  3. 6
      src/common/mainloop/client.rs
  4. 2
      src/common/serverstate.rs
  5. 22
      src/entity/account.rs
  6. 61
      src/entity/character.rs
  7. 18
      src/entity/gateway/entitygateway.rs
  8. 45
      src/entity/gateway/inmemory.rs
  9. 7
      src/entity/gateway/postgres/migrations/V0003__item_notes.sql
  10. 15
      src/entity/gateway/postgres/migrations/V0004__meseta.sql
  11. 121
      src/entity/gateway/postgres/models.rs
  12. 111
      src/entity/gateway/postgres/postgres.rs
  13. 1
      src/entity/item/mag.rs
  14. 55
      src/entity/item/mod.rs
  15. 3
      src/lib.rs
  16. 85
      src/login/character.rs
  17. 54
      src/ship/character.rs
  18. 26
      src/ship/items/bank.rs
  19. 14
      src/ship/items/floor.rs
  20. 208
      src/ship/items/inventory.rs
  21. 764
      src/ship/items/manager.rs
  22. 3
      src/ship/items/mod.rs
  23. 337
      src/ship/items/transaction.rs
  24. 26
      src/ship/location.rs
  25. 9
      src/ship/map/area.rs
  26. 46
      src/ship/map/enemy.rs
  27. 60
      src/ship/map/object.rs
  28. 1
      src/ship/mod.rs
  29. 37
      src/ship/packet/builder/message.rs
  30. 2
      src/ship/packet/builder/mod.rs
  31. 0
      src/ship/packet/builder/trade.rs
  32. 48
      src/ship/packet/handler/direct_message.rs
  33. 2
      src/ship/packet/handler/lobby.rs
  34. 46
      src/ship/packet/handler/message.rs
  35. 1
      src/ship/packet/handler/mod.rs
  36. 53
      src/ship/packet/handler/room.rs
  37. 544
      src/ship/packet/handler/trade.rs
  38. 6
      src/ship/room.rs
  39. 46
      src/ship/ship.rs
  40. 10
      src/ship/shops/weapon.rs
  41. 133
      src/ship/trade.rs
  42. 3
      tests/common.rs
  43. 212
      tests/test_bank.rs
  44. 27
      tests/test_item_actions.rs
  45. 190
      tests/test_item_pickup.rs
  46. 15
      tests/test_item_use.rs
  47. 19
      tests/test_mags.rs
  48. 6
      tests/test_rooms.rs
  49. 57
      tests/test_shops.rs
  50. 4382
      tests/test_trade.rs

6
Cargo.toml

@ -11,7 +11,7 @@ futures = "0.3.5"
rand = "0.7.3"
rand_chacha = "0.2.2"
crc = "^1.0.0"
bcrypt = "0.4"
bcrypt = "0.10"
chrono = "0.4.11"
serde = "*"
serde_json = "*"
@ -24,12 +24,12 @@ enum-utils = "0.1.2"
derive_more = { version = "0.99.3", features = ["display"]}
thiserror = "1.0.15"
ages-prs = "0.1"
async-trait = "0.1.41"
async-trait = "0.1.51"
lazy_static = "1.4.0"
barrel = { version = "0.6.5", features = ["pg"] }
refinery = { version = "0.5.0", features = ["postgres"] }
sqlx = { version = "0.4.0", features = ["runtime-async-std-native-tls", "postgres", "json", "chrono"] }
strum = "0.19.5"
strum_macros = "0.19"
anyhow = "1.0.33"
anyhow = { version = "1.0.47", features = ["backtrace"] }

87
src/bin/main.rs

@ -9,7 +9,7 @@ use elseware::entity::account::{NewUserAccountEntity, NewUserSettingsEntity};
#[allow(unused_imports)]
use elseware::entity::gateway::{EntityGateway, InMemoryGateway, PostgresGateway};
use elseware::entity::character::NewCharacterEntity;
use elseware::entity::item::{NewItemEntity, ItemDetail, ItemLocation};
use elseware::entity::item::{NewItemEntity, ItemDetail, InventoryItemEntity};
use elseware::common::interserver::AuthToken;
use elseware::entity::item;
@ -67,13 +67,16 @@ fn main() {
entity_gateway.create_user_settings(NewUserSettingsEntity::new(fake_user.id)).await.unwrap();
let mut character = NewCharacterEntity::new(fake_user.id);
character.name = format!("Test Char {}", i*2);
entity_gateway.create_character(character).await.unwrap();
let character = entity_gateway.create_character(character).await.unwrap();
entity_gateway.set_character_meseta(&character.id, item::Meseta(999999)).await.unwrap();
entity_gateway.set_bank_meseta(&character.id, item::BankName("".into()), item::Meseta(999999)).await.unwrap();
let mut character = NewCharacterEntity::new(fake_user.id);
character.slot = 2;
character.name = "ItemRefactor".into();
character.exp = 80000000;
character.meseta = 999999;
let character = entity_gateway.create_character(character).await.unwrap();
entity_gateway.set_character_meseta(&character.id, item::Meseta(999999)).await.unwrap();
entity_gateway.set_bank_meseta(&character.id, item::BankName("".into()), item::Meseta(999999)).await.unwrap();
for _ in 0..3 {
entity_gateway.create_item(
@ -87,10 +90,6 @@ fn main() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: character.id,
name: item::BankName("".to_string())
}
}).await.unwrap();
}
@ -102,10 +101,6 @@ fn main() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: character.id,
name: item::BankName("".to_string())
}
}).await.unwrap();
}
@ -122,9 +117,6 @@ fn main() {
tekked: false,
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}).await.unwrap();
let item1 = entity_gateway.create_item(
NewItemEntity {
@ -139,9 +131,6 @@ fn main() {
tekked: true,
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}).await.unwrap();
let item2_w = entity_gateway.create_item(
NewItemEntity {
@ -156,9 +145,6 @@ fn main() {
tekked: true,
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}).await.unwrap();
let item3 = entity_gateway.create_item(
NewItemEntity {
@ -173,34 +159,25 @@ fn main() {
tekked: true,
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}).await.unwrap();
let item4 = entity_gateway.create_item(
NewItemEntity {
item: ItemDetail::Weapon(
item::weapon::Weapon {
weapon: item::weapon::WeaponType::DarkFlow,
grind: 5,
special: Some(item::weapon::WeaponSpecial::Charge),
grind: 0,
special: None,
attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}),
Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}),
Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Native, value: 100}),],
tekked: true,
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}).await.unwrap();
let item5_m = entity_gateway.create_item(
item::NewItemEntity {
item: item::ItemDetail::Mag(item::mag::Mag::baby_mag(0)),
location: item::ItemLocation::Inventory {
character_id: character.id,
}
}).await.unwrap();
for _ in 0..10usize {
@ -211,9 +188,6 @@ fn main() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::FedToMag {
mag: item5_m.id,
}
}).await.unwrap();
entity_gateway.feed_mag(&item5_m.id, &fed_tool.id).await.unwrap();
}
@ -226,9 +200,6 @@ fn main() {
tool: item::tool::ToolType::CellOfMag502,
}
),
location: item::ItemLocation::Inventory {
character_id: character.id,
}
}).await.unwrap();
let cell = entity_gateway.create_item(
item::NewItemEntity {
@ -237,7 +208,6 @@ fn main() {
tool: item::tool::ToolType::CellOfMag502,
}
),
location: item::ItemLocation::Consumed,
}).await.unwrap();
entity_gateway.use_mag_cell(&item5_m.id, &cell.id).await.unwrap();
@ -254,10 +224,6 @@ fn main() {
tekked: false,
}
),
location: ItemLocation::Bank {
character_id: character.id,
name: item::BankName("".to_string()),
}
}).await.unwrap();
let item7_a = entity_gateway.create_item(
NewItemEntity {
@ -269,9 +235,6 @@ fn main() {
slots: 4,
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}
).await.unwrap();
let item8_s = entity_gateway.create_item(
@ -283,9 +246,6 @@ fn main() {
evp: 0,
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}
).await.unwrap();
let item9_u0 = entity_gateway.create_item(
@ -296,9 +256,6 @@ fn main() {
modifier: Some(item::unit::UnitModifier::Minus),
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}
).await.unwrap();
let item10_u1 = entity_gateway.create_item(
@ -309,9 +266,6 @@ fn main() {
modifier: Some(item::unit::UnitModifier::Minus),
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}
).await.unwrap();
let item11_u2 = entity_gateway.create_item(
@ -322,9 +276,6 @@ fn main() {
modifier: Some(item::unit::UnitModifier::Minus),
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}
).await.unwrap();
let item12_u3 = entity_gateway.create_item(
@ -335,9 +286,6 @@ fn main() {
modifier: Some(item::unit::UnitModifier::Minus),
}
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}
).await.unwrap();
let item13 = entity_gateway.create_item(
@ -345,12 +293,23 @@ fn main() {
item: ItemDetail::Mag(
item::mag::Mag::baby_mag(5)
),
location: ItemLocation::Inventory {
character_id: character.id,
}
}
).await.unwrap();
let monomates = futures::future::join_all((0..6).map(|_| {
let mut entity_gateway = entity_gateway.clone();
async move {
entity_gateway.create_item(
NewItemEntity {
item: ItemDetail::Tool (
item::tool::Tool {
tool: item::tool::ToolType::Monomate,
}
),
}).await.unwrap()
}
})).await;
let equipped = item::EquippedEntity {
weapon: Some(item2_w.id),
armor: Some(item7_a.id),
@ -360,7 +319,7 @@ fn main() {
};
entity_gateway.set_character_equips(&character.id, &equipped).await.unwrap();
let inventory = item::InventoryEntity::new(vec![item0, item1, item2_w, item3, item4, item5_m, item6, item7_a, item8_s, item9_u0, item10_u1, item11_u2, item12_u3, item13]);
let inventory = item::InventoryEntity::new(vec![InventoryItemEntity::from(item0), item1.into(), item2_w.into(), item3.into(), item4.into(), item5_m.into(), item6.into(), item7_a.into(), item8_s.into(), item9_u0.into(), item10_u1.into(), item11_u2.into(), item12_u3.into(), item13.into(), monomates.into()]);
entity_gateway.set_character_inventory(&character.id, &inventory).await.unwrap();
entity_gateway.set_character_bank(&character.id, &item::BankEntity::default(), item::BankName("".into())).await.unwrap();
}

6
src/common/mainloop/client.rs

@ -119,7 +119,7 @@ async fn send_pkt<S: SendServerPacket + Send + std::fmt::Debug>(socket: Arc<asyn
-> Result<(), NetworkError>
{
let buf = pkt.as_bytes();
//println!("sndbuf: {:?}", buf);
trace!("[send buf] {:?}", buf);
let cbuf = cipher.lock().await.encrypt(&buf)?;
let mut ssock = &*socket;
ssock.write_all(&cbuf).await?;
@ -156,7 +156,7 @@ where
match pkt_receiver.recv_pkts().await {
Ok(pkts) => {
for pkt in pkts {
trace!("[recv from {:?}] {:?}", client_id, pkt);
info!("[recv from {:?}] {:?}", client_id, pkt);
server_sender.send(ClientAction::Packet(client_id, pkt)).await.unwrap();
}
},
@ -194,7 +194,7 @@ where
*cipher_out.lock().await = outc;
}
ServerStateAction::Packet(pkt) => {
trace!("[send to {:?}] {:?}", client_id, pkt);
info!("[send to {:?}] {:?}", client_id, pkt);
if let Err(err) = send_pkt(socket.clone(), cipher_out.clone(), pkt).await {
warn!("[client {:?} send error ] {:?}", client_id, err);
}

2
src/common/serverstate.rs

@ -1,7 +1,7 @@
use libpso::PacketParseError;
use libpso::crypto::PSOCipher;
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, derive_more::Display)]
pub struct ClientId(pub usize);
pub enum OnConnect<S: SendServerPacket> {

22
src/entity/account.rs

@ -5,7 +5,7 @@ use libpso::character::guildcard;
pub const USERFLAG_NEWCHAR: u32 = 0x00000001;
pub const USERFLAG_DRESSINGROOM: u32 = 0x00000002;
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
pub struct UserAccountId(pub u32);
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct UserSettingsId(pub u32);
@ -59,6 +59,26 @@ pub struct UserAccountEntity {
pub at_ship: bool,
}
impl Default for UserAccountEntity {
fn default() -> UserAccountEntity {
UserAccountEntity {
id: UserAccountId(0),
username: "".into(),
password: "".into(),
guildcard: 0xFFFFFFFF,
team_id: None,
banned_until: None,
muted_until: None,
created_at: chrono::Utc::now(),
flags: 0,
activated: false,
at_login: false,
at_character: false,
at_ship: false,
}
}
}
impl UserAccountEntity {
pub fn is_currently_online(&self) -> bool {
self.at_login | self.at_character | self.at_ship

61
src/entity/character.rs

@ -7,8 +7,9 @@ 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, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize, Default)]
pub enum CharacterClass {
#[default]
HUmar,
HUnewearl,
HUcast,
@ -90,8 +91,9 @@ impl CharacterClass {
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize, Default)]
pub enum SectionID {
#[default]
Viridia,
Greenill,
Skyly,
@ -158,18 +160,12 @@ pub struct CharacterAppearance {
#[derive(Clone, Debug)]
pub struct TechLevel(pub u8);
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct CharacterTechniques {
pub techs: HashMap<Technique, TechLevel>
}
impl CharacterTechniques {
fn new() -> CharacterTechniques {
CharacterTechniques {
techs: HashMap::new(),
}
}
pub fn set_tech(&mut self, tech: Technique, level: TechLevel) {
self.techs.insert(tech, TechLevel(level.0 - 1));
}
@ -187,18 +183,20 @@ impl CharacterTechniques {
}
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct CharacterConfig {
pub raw_data: [u8; 0xE8],
}
impl CharacterConfig {
fn new() -> CharacterConfig {
impl Default for CharacterConfig {
fn default() -> CharacterConfig {
CharacterConfig {
raw_data: DEFAULT_PALETTE_CONFIG,
}
}
}
impl CharacterConfig {
pub fn update(&mut self, new_config: &UpdateConfig) {
self.raw_data = new_config.config;
}
@ -208,18 +206,20 @@ impl CharacterConfig {
}
}
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct CharacterInfoboard {
pub board: [u16; 172],
}
impl CharacterInfoboard {
fn new() -> CharacterInfoboard {
impl Default for CharacterInfoboard {
fn default() -> CharacterInfoboard {
CharacterInfoboard {
board: [0; 172]
}
}
}
impl CharacterInfoboard {
pub fn as_bytes(&self) -> [u16; 172] {
self.board
}
@ -229,29 +229,31 @@ impl CharacterInfoboard {
}
}
#[derive(Clone, Default)]
#[derive(Debug, Clone, Default)]
pub struct CharacterGuildCard {
pub description: String,
}
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct CharacterTechMenu {
pub tech_menu: [u8; 40],
}
impl CharacterTechMenu {
fn new() -> CharacterTechMenu {
impl Default for CharacterTechMenu {
fn default() -> CharacterTechMenu {
CharacterTechMenu {
tech_menu: DEFAULT_TECH_MENU,
}
}
}
impl CharacterTechMenu {
pub fn as_bytes(&self) -> [u8; 40] {
self.tech_menu
}
}
#[derive(Clone, Default)]
#[derive(Clone, Default, Debug)]
pub struct CharacterMaterials {
pub power: u32,
pub mind: u32,
@ -262,7 +264,7 @@ pub struct CharacterMaterials {
pub tp: u32,
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default, derive_more::Display)]
pub struct CharacterEntityId(pub u32);
#[derive(Clone)]
@ -284,8 +286,6 @@ pub struct NewCharacterEntity {
pub materials: CharacterMaterials,
pub tech_menu: CharacterTechMenu,
pub meseta: u32,
pub bank_meseta: u32,
pub option_flags: u32,
}
@ -299,20 +299,18 @@ impl NewCharacterEntity {
char_class: CharacterClass::HUmar,
section_id: SectionID::Viridia,
appearance: CharacterAppearance::default(),
techs: CharacterTechniques::new(),
config: CharacterConfig::new(),
info_board: CharacterInfoboard::new(),
techs: CharacterTechniques::default(),
config: CharacterConfig::default(),
info_board: CharacterInfoboard::default(),
guildcard: CharacterGuildCard::default(),
materials: CharacterMaterials::default(),
tech_menu: CharacterTechMenu::new(),
meseta: 0,
bank_meseta: 0,
tech_menu: CharacterTechMenu::default(),
option_flags: 0,
}
}
}
#[derive(Clone)]
#[derive(Clone, Default, Debug)]
pub struct CharacterEntity {
pub id: CharacterEntityId,
pub user_id: UserAccountId,
@ -332,8 +330,5 @@ pub struct CharacterEntity {
pub materials: CharacterMaterials,
pub tech_menu: CharacterTechMenu,
pub meseta: u32,
// TODO: this should not be tied to the character
pub bank_meseta: u32,
pub option_flags: u32,
}

18
src/entity/gateway/entitygateway.rs

@ -65,7 +65,7 @@ pub trait EntityGateway: Send + Sync + Clone {
unimplemented!();
}
async fn change_item_location(&mut self, _item_id: &ItemEntityId, _item_location: ItemLocation) -> Result<(), GatewayError> {
async fn add_item_note(&mut self, _item_id: &ItemEntityId, _item_note: ItemNote) -> Result<(), GatewayError> {
unimplemented!();
}
@ -115,4 +115,20 @@ pub trait EntityGateway: Send + Sync + Clone {
async fn set_character_equips(&mut self, _char_id: &CharacterEntityId, _equips: &EquippedEntity) -> Result<(), GatewayError> {
unimplemented!();
}
async fn get_character_meseta(&mut self, _char_id: &CharacterEntityId) -> Result<Meseta, GatewayError> {
unimplemented!();
}
async fn set_character_meseta(&mut self, _char_id: &CharacterEntityId, _amount: Meseta) -> Result<(), GatewayError> {
unimplemented!();
}
async fn get_bank_meseta(&mut self, _char_id: &CharacterEntityId, _bank: BankName) -> Result<Meseta, GatewayError> {
unimplemented!();
}
async fn set_bank_meseta(&mut self, _char_id: &CharacterEntityId, _bank: BankName, _amount: Meseta) -> Result<(), GatewayError> {
unimplemented!();
}
}

45
src/entity/gateway/inmemory.rs

@ -13,6 +13,8 @@ pub struct InMemoryGateway {
users: Arc<Mutex<BTreeMap<UserAccountId, UserAccountEntity>>>,
user_settings: Arc<Mutex<BTreeMap<UserSettingsId, UserSettingsEntity>>>,
characters: Arc<Mutex<BTreeMap<CharacterEntityId, CharacterEntity>>>,
character_meseta: Arc<Mutex<BTreeMap<CharacterEntityId, Meseta>>>,
bank_meseta: Arc<Mutex<BTreeMap<(CharacterEntityId, BankName), Meseta>>>,
items: Arc<Mutex<BTreeMap<ItemEntityId, ItemEntity>>>,
inventories: Arc<Mutex<BTreeMap<CharacterEntityId, InventoryEntity>>>,
banks: Arc<Mutex<BTreeMap<CharacterEntityId, BankEntity>>>,
@ -27,6 +29,8 @@ impl Default for InMemoryGateway {
users: Arc::new(Mutex::new(BTreeMap::new())),
user_settings: Arc::new(Mutex::new(BTreeMap::new())),
characters: Arc::new(Mutex::new(BTreeMap::new())),
character_meseta: Arc::new(Mutex::new(BTreeMap::new())),
bank_meseta: Arc::new(Mutex::new(BTreeMap::new())),
items: Arc::new(Mutex::new(BTreeMap::new())),
inventories: Arc::new(Mutex::new(BTreeMap::new())),
banks: Arc::new(Mutex::new(BTreeMap::new())),
@ -197,8 +201,6 @@ impl EntityGateway for InMemoryGateway {
guildcard: character.guildcard,
materials: character.materials,
tech_menu: character.tech_menu,
meseta: character.meseta,
bank_meseta: character.bank_meseta,
option_flags: character.option_flags,
};
characters.insert(new_character.id, new_character.clone());
@ -223,17 +225,13 @@ impl EntityGateway for InMemoryGateway {
+ 1;
let new_item = ItemEntity {
id: ItemEntityId(id),
location: item.location,
item: item.item,
};
items.insert(ItemEntityId(id), new_item.clone());
Ok(new_item)
}
async fn change_item_location(&mut self, item_id: &ItemEntityId, item_location: ItemLocation) -> Result<(), GatewayError> {
if let Some(item_entity) = self.items.lock().unwrap().get_mut(item_id) {
item_entity.location = item_location
}
async fn add_item_note(&mut self, _item_id: &ItemEntityId, _item_note: ItemNote) -> Result<(), GatewayError> {
Ok(())
}
@ -272,7 +270,6 @@ impl EntityGateway for InMemoryGateway {
}
async fn get_character_inventory(&mut self, char_id: &CharacterEntityId) -> Result<InventoryEntity, GatewayError> {
println!("getting inv");
let inventories = self.inventories.lock().unwrap();
Ok(inventories
.iter()
@ -318,4 +315,36 @@ impl EntityGateway for InMemoryGateway {
equips.insert(*char_id, equipped.clone());
Ok(())
}
async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, meseta: Meseta) -> Result<(), GatewayError> {
let mut character_meseta = self.character_meseta.lock().unwrap();
character_meseta.insert(*char_id, meseta);
Ok(())
}
async fn get_character_meseta(&mut self, char_id: &CharacterEntityId) -> Result<Meseta, GatewayError> {
let mut character_meseta = self.character_meseta.lock().unwrap();
if let Some(meseta) = character_meseta.get_mut(char_id) {
Ok(*meseta)
}
else {
Err(GatewayError::Error)
}
}
async fn set_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName, meseta: Meseta) -> Result<(), GatewayError> {
let mut bank_meseta = self.bank_meseta.lock().unwrap();
bank_meseta.insert((*char_id, bank), meseta);
Ok(())
}
async fn get_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName) -> Result<Meseta, GatewayError> {
let mut bank_meseta = self.bank_meseta.lock().unwrap();
if let Some(meseta) = bank_meseta.get_mut(&(*char_id, bank)) {
Ok(*meseta)
}
else {
Err(GatewayError::Error)
}
}
}

7
src/entity/gateway/postgres/migrations/V0003__item_notes.sql

@ -0,0 +1,7 @@
drop table item_location;
create table item_note (
item integer references item (id) not null,
note jsonb not null,
created_at timestamptz default current_timestamp not null
);

15
src/entity/gateway/postgres/migrations/V0004__meseta.sql

@ -0,0 +1,15 @@
create table character_meseta (
pchar integer references character (id) not null unique,
meseta integer not null,
);
create table bank_meseta (
pchar integer references character (id) not null,
bank varchar(128) not null,
meseta integer not null,
unique (pchar, bank)
);
alter table player_character
drop column meseta, bank_meseta;

121
src/entity/gateway/postgres/models.rs

@ -1,3 +1,4 @@
#![allow(dead_code)]
use std::collections::HashMap;
use std::convert::Into;
use serde::{Serialize, Deserialize};
@ -216,8 +217,6 @@ pub struct PgCharacter {
tp: i16,
tech_menu: Vec<u8>,
meseta: i32,
bank_meseta: i32,
}
impl From<PgCharacter> for CharacterEntity {
@ -267,8 +266,6 @@ impl From<PgCharacter> for CharacterEntity {
tech_menu: CharacterTechMenu {
tech_menu: vec_to_array(other.tech_menu)
},
meseta: other.meseta as u32,
bank_meseta: other.bank_meseta as u32,
}
}
}
@ -571,22 +568,21 @@ pub struct PgItem {
#[derive(Debug, Serialize, Deserialize)]
pub enum PgItemLocationDetail {
Inventory {
pub enum PgItemNoteDetail {
CharacterCreation {
character_id: u32,
},
Bank {
character_id: u32,
name: String,
},
LocalFloor {
EnemyDrop {
character_id: u32,
map_area: MapArea,
x: f32,
y: f32,
z: f32,
},
SharedFloor {
Pickup {
character_id: u32,
},
PlayerDrop {
map_area: MapArea,
x: f32,
y: f32,
@ -596,42 +592,92 @@ pub enum PgItemLocationDetail {
FedToMag {
mag: u32,
},
Shop,
BoughtAtShop {
character_id: u32,
},
SoldToShop,
Trade {
id: u32,
character_to: u32,
character_from: u32,
},
}
impl From<ItemLocation> for PgItemLocationDetail {
fn from(other: ItemLocation) -> PgItemLocationDetail {
impl From<ItemNote> for PgItemNoteDetail {
fn from(other: ItemNote) -> PgItemNoteDetail {
match other {
ItemLocation::Inventory{character_id} => PgItemLocationDetail::Inventory{character_id: character_id.0},
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,
ItemNote::CharacterCreation{character_id} => PgItemNoteDetail::CharacterCreation {
character_id: character_id.0,
},
ItemNote::EnemyDrop{character_id, map_area, x, y, z} => PgItemNoteDetail::EnemyDrop {
character_id: character_id.0,
map_area,
x,y,z,
},
ItemNote::Pickup{character_id} => PgItemNoteDetail::Pickup {
character_id: character_id.0,
},
ItemNote::PlayerDrop{map_area, x, y, z} => PgItemNoteDetail::PlayerDrop {
map_area,
x,y,z,
},
ItemNote::Consumed => PgItemNoteDetail::Consumed,
ItemNote::FedToMag{mag} => PgItemNoteDetail::FedToMag{
mag: mag.0
},
ItemNote::BoughtAtShop{character_id} => PgItemNoteDetail::BoughtAtShop {
character_id: character_id.0,
},
ItemNote::SoldToShop => PgItemNoteDetail::SoldToShop,
ItemNote::Trade{id, character_to, character_from} => PgItemNoteDetail::Trade {
id: id.0,
character_to: character_to.0,
character_from: character_from.0,
}
}
}
}
impl From<PgItemLocationDetail> for ItemLocation {
fn from(other: PgItemLocationDetail) -> ItemLocation {
match other{
PgItemLocationDetail::Inventory{character_id} => ItemLocation::Inventory{character_id: CharacterEntityId(character_id)},
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,
impl From<PgItemNoteDetail> for ItemNote {
fn from(other: PgItemNoteDetail) -> ItemNote {
match other {
PgItemNoteDetail::CharacterCreation{character_id} => ItemNote::CharacterCreation {
character_id: CharacterEntityId(character_id as u32),
},
PgItemNoteDetail::EnemyDrop{character_id, map_area, x, y, z} => ItemNote::EnemyDrop {
character_id: CharacterEntityId(character_id as u32),
map_area,
x,y,z,
},
PgItemNoteDetail::Pickup{character_id} => ItemNote::Pickup {
character_id: CharacterEntityId(character_id as u32),
},
PgItemNoteDetail::PlayerDrop{map_area, x, y, z} => ItemNote::PlayerDrop {
map_area,
x,y,z,
},
PgItemNoteDetail::Consumed => ItemNote::Consumed,
PgItemNoteDetail::FedToMag{mag} => ItemNote::FedToMag{
mag: ItemEntityId(mag)
},
PgItemNoteDetail::BoughtAtShop{character_id} => ItemNote::BoughtAtShop {
character_id: CharacterEntityId(character_id),
},
PgItemNoteDetail::SoldToShop => ItemNote::SoldToShop,
PgItemNoteDetail::Trade {id, character_to, character_from} => ItemNote::Trade {
id: TradeId(id as u32),
character_to: CharacterEntityId(character_to as u32),
character_from: CharacterEntityId(character_from as u32),
}
}
}
}
#[derive(Debug, sqlx::FromRow)]
pub struct PgItemLocation {
pub struct PgItemNote {
//pub id: i32,
pub location: sqlx::types::Json<PgItemLocationDetail>,
pub note: sqlx::types::Json<PgItemNoteDetail>,
created_at: chrono::DateTime<chrono::Utc>,
}
@ -680,19 +726,20 @@ pub struct PgItemEntity {
pub item: sqlx::types::Json<PgItemDetail>,
}
/*
#[derive(Debug, sqlx::FromRow)]
pub struct PgItemWithLocation {
pub id: i32,
pub item: sqlx::types::Json<PgItemDetail>,
pub location: sqlx::types::Json<PgItemLocationDetail>,
}
*/
impl From<PgItemWithLocation> for ItemEntity {
fn from(other: PgItemWithLocation) -> ItemEntity {
impl From<PgItemEntity> for ItemEntity {
fn from(other: PgItemEntity) -> ItemEntity {
ItemEntity {
id: ItemEntityId(other.id as u32),
item: other.item.0.into(),
location: other.location.0.into(),
}
}
}

111
src/entity/gateway/postgres/postgres.rs

@ -44,7 +44,7 @@ impl PostgresGateway {
}
async fn apply_item_modifications(&self, item: ItemEntity) -> ItemEntity {
let ItemEntity {id, item, location} = item;
let ItemEntity {id, item} = item;
let item = match item {
ItemDetail::Weapon(mut weapon) => {
@ -101,7 +101,6 @@ impl PostgresGateway {
ItemEntity {
id,
item,
location
}
}
}
@ -183,7 +182,7 @@ impl EntityGateway for PostgresGateway {
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
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)
config, infoboard, guildcard, power, mind, def, evade, luck, hp, tp, tech_menu, 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 *;"#;
@ -216,8 +215,6 @@ impl EntityGateway for PostgresGateway {
.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?;
@ -242,7 +239,7 @@ impl EntityGateway for PostgresGateway {
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, option_flags=$31
evade=$24, luck=$25, hp=$26, tp=$27, tech_menu=$28, option_flags=$29
where id=$32;"#;
sqlx::query(q)
.bind(char.user_id.0)
@ -273,8 +270,6 @@ impl EntityGateway for PostgresGateway {
.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)
.bind(char.id.0 as i32)
.execute(&self.pool).await?;
@ -294,64 +289,18 @@ impl EntityGateway for PostgresGateway {
let new_item = sqlx::query_as::<_, PgItem>("insert into item (item) values ($1) returning *;")
.bind(sqlx::types::Json(PgItemDetail::from(item.item)))
.fetch_one(&mut tx).await?;
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(&mut tx).await?;
tx.commit().await?;
Ok(ItemEntity {
id: ItemEntityId(new_item.id as u32),
item: new_item.item.0.into(),
location: location.location.0.into(),
})
/*
let mut tx = self.pool.begin().await?;
let new_item = sqlx::query_as::<_, PgItem>("insert into item (item) values ($1) returning *;")
.bind(sqlx::types::Json(PgItemDetail::from(item.item)))
.fetch_one(&mut tx).await?;
let location = if let ItemLocation::Inventory{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(&mut tx).await?;
sqlx::query("insert into inventory_slot (item, slot) values ($1, $2)")
.bind(new_item.id)
.bind(*slot as i32)
.execute(&mut tx).await?;
sqlx::query_as::<_, PgItemLocation>(r#"select
item_location.item,
jsonb_set(item_location.location, '{Inventory,slot}', inventory_slot.slot::text::jsonb) as location,
item_location.created_at
from item_location
join item on item.id = item_location.item
join inventory_slot on inventory_slot.item = item.id
where item.id = $1
order by item_location.created_at
limit 1"#)
.bind(new_item.id)
.fetch_one(&mut tx).await?
}
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(&mut tx).await?
};
tx.commit().await?;
Ok(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) -> Result<(), GatewayError> {
sqlx::query("insert into item_location (item, location) values ($1, $2)")
async fn add_item_note(&mut self, item_id: &ItemEntityId, item_note: ItemNote) -> Result<(), GatewayError> {
sqlx::query("insert into item_note(item, note) values ($1, $2)")
.bind(item_id.0)
.bind(sqlx::types::Json(PgItemLocationDetail::from(item_location)))
.bind(sqlx::types::Json(PgItemNoteDetail::from(item_note)))
.execute(&self.pool).await?;
Ok(())
@ -465,7 +414,7 @@ impl EntityGateway for PostgresGateway {
for inv_item in inventory.items.0.into_iter() {
match inv_item {
PgInventoryItemEntity::Individual(item) => {
let entity = sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1")
let entity = sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1")
.bind(item)
.fetch_one(&self.pool).await
.map(|item| item.into())
@ -476,7 +425,7 @@ impl EntityGateway for PostgresGateway {
PgInventoryItemEntity::Stacked(items) => {
let mut stacked_item = Vec::new();
for s_item in items {
stacked_item.push(sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1")
stacked_item.push(sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1")
.bind(s_item)
.fetch_one(&self.pool).await
.map(|item| item.into())
@ -501,7 +450,7 @@ impl EntityGateway for PostgresGateway {
for bank_item in bank.items.0.into_iter() {
match bank_item {
PgInventoryItemEntity::Individual(item) => {
let entity = sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1")
let entity = sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1")
.bind(item)
.fetch_one(&self.pool).await
.map(|item| item.into())
@ -512,7 +461,7 @@ impl EntityGateway for PostgresGateway {
PgInventoryItemEntity::Stacked(items) => {
let mut stacked_item = Vec::new();
for s_item in items {
stacked_item.push(sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1")
stacked_item.push(sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1")
.bind(s_item)
.fetch_one(&self.pool).await
.map(|item| item.into())
@ -597,4 +546,44 @@ impl EntityGateway for PostgresGateway {
.await?;
Ok(())
}
async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, meseta: Meseta) -> Result<(), GatewayError> {
sqlx::query("insert into character_meseta values ($1, $2) on conflict (pchar) do update set items = $2")
.bind(char_id.0)
.bind(meseta.0 as i32)
.execute(&self.pool)
.await?;
Ok(())
}
async fn get_character_meseta(&mut self, char_id: &CharacterEntityId) -> Result<Meseta, GatewayError> {
#[derive(sqlx::FromRow)]
struct PgMeseta(i32);
let meseta = sqlx::query_as::<_, PgMeseta>(r#"select meseta from character_meseta where id = $1"#)
.bind(char_id.0)
.fetch_one(&self.pool)
.await?;
Ok(Meseta(meseta.0 as u32))
}
async fn set_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName, meseta: Meseta) -> Result<(), GatewayError> {
sqlx::query("insert into bank_meseta values ($1, $2, $3) on conflict (pchar, bank) do update set items = $2")
.bind(char_id.0)
.bind(meseta.0 as i32)
.bind(bank.0)
.execute(&self.pool)
.await?;
Ok(())
}
async fn get_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName) -> Result<Meseta, GatewayError> {
#[derive(sqlx::FromRow)]
struct PgMeseta(i32);
let meseta = sqlx::query_as::<_, PgMeseta>(r#"select meseta from character_meseta where id = $1 and bank = $2"#)
.bind(char_id.0)
.bind(bank.0)
.fetch_one(&self.pool)
.await?;
Ok(Meseta(meseta.0 as u32))
}
}

1
src/entity/item/mag.rs

@ -567,6 +567,7 @@ impl Mag {
pub fn as_bytes(&self) -> [u8; 16] {
let mut result = [0; 16];
result[0..3].copy_from_slice(&self.mag.value());
result[2] = self.level() as u8;
result[3] = self.photon_blast_value();
result[4..6].copy_from_slice(&self.def.to_le_bytes());
result[6..8].copy_from_slice(&self.pow.to_le_bytes());

55
src/entity/item/mod.rs

@ -17,26 +17,29 @@ use crate::ship::drops::ItemDropType;
pub struct ItemEntityId(pub u32);
#[derive(Hash, PartialEq, Eq, Debug, Clone)]
pub struct ItemId(u32);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, derive_more::Display)]
pub struct BankName(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TradeId(pub u32);
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ItemLocation {
Inventory {
pub enum ItemNote {
CharacterCreation {
character_id: CharacterEntityId,
},
Bank {
character_id: CharacterEntityId,
name: BankName,
},
LocalFloor {
EnemyDrop {
character_id: CharacterEntityId,
//monster_type: MonsterType,
//droprate: f32,
map_area: MapArea,
x: f32,
y: f32,
z: f32,
},
SharedFloor {
Pickup {
character_id: CharacterEntityId,
},
PlayerDrop {
map_area: MapArea,
x: f32,
y: f32,
@ -46,18 +49,18 @@ pub enum ItemLocation {
FedToMag {
mag: ItemEntityId,
},
Shop,
/*Destroyed {
// marks an item that has been consumed in some way
BoughtAtShop {
character_id: CharacterEntityId,
},
SoldToShop,
Trade {
id: TradeId,
character_to: CharacterEntityId,
character_from: CharacterEntityId,
},
Transformed {
item_id,
change_event
}
*/
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Meseta(pub u32);
impl Meseta {
@ -159,18 +162,23 @@ impl ItemDetail {
_ => None,
}
}
pub fn tool(&self) -> Option<&tool::Tool> {
match self {
ItemDetail::Tool(tool) => Some(tool),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct NewItemEntity {
pub location: ItemLocation,
pub item: ItemDetail,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ItemEntity {
pub id: ItemEntityId,
pub location: ItemLocation,
pub item: ItemDetail,
}
@ -215,6 +223,13 @@ impl InventoryItemEntity {
_ => None,
}
}
pub fn individual(&self) -> Option<&ItemEntity> {
match self {
InventoryItemEntity::Individual(i) => Some(i),
_ => None,
}
}
}
#[derive(Clone, Debug, Default)]

3
src/lib.rs

@ -2,7 +2,8 @@
#![feature(maybe_uninit_extra)]
#![feature(inline_const)]
#![feature(drain_filter)]
#![feature(derive_default_enum)]
#![feature(try_blocks)]

85
src/login/character.rs

@ -20,7 +20,7 @@ use libpso::{utf8_to_array, utf8_to_utf16_array};
use crate::entity::gateway::{EntityGateway, GatewayError};
use crate::entity::account::{UserAccountId, UserAccountEntity, NewUserSettingsEntity, USERFLAG_NEWCHAR, USERFLAG_DRESSINGROOM};
use crate::entity::item::{NewItemEntity, ItemDetail, ItemLocation, InventoryItemEntity, InventoryEntity, BankEntity, BankName, EquippedEntity};
use crate::entity::item::{NewItemEntity, ItemDetail, ItemNote, InventoryItemEntity, InventoryEntity, BankEntity, BankName, EquippedEntity, Meseta};
use crate::entity::item::weapon::Weapon;
use crate::entity::item::armor::Armor;
use crate::entity::item::tech::Technique;
@ -201,8 +201,8 @@ async fn new_character<EG: EntityGateway>(entity_gateway: &mut EG, user: &UserAc
_ => {}
}
character.meseta = 300;
let character = entity_gateway.create_character(character).await?;
entity_gateway.set_character_meseta(&character.id, Meseta(300)).await?;
let new_weapon = match character.char_class {
CharacterClass::HUmar | CharacterClass::HUnewearl | CharacterClass::HUcast | CharacterClass::HUcaseal => item::weapon::WeaponType::Saber,
@ -220,10 +220,11 @@ async fn new_character<EG: EntityGateway>(entity_gateway: &mut EG, user: &UserAc
special: None,
attrs: [None; 3],
tekked: true,
}),
location: ItemLocation::Inventory {
character_id: character.id,
}}).await?;
})}).await?;
entity_gateway.add_item_note(&weapon.id, ItemNote::CharacterCreation {
character_id: character.id,
}).await?;
let armor = entity_gateway.create_item(
NewItemEntity {
@ -233,10 +234,11 @@ async fn new_character<EG: EntityGateway>(entity_gateway: &mut EG, user: &UserAc
dfp: 0,
evp: 0,
slots: 0,
}),
location: ItemLocation::Inventory {
character_id: character.id,
}}).await?;
})}).await?;
entity_gateway.add_item_note(&armor.id, ItemNote::CharacterCreation {
character_id: character.id,
}).await?;
let mut mag = {
if character.char_class.is_android() {
@ -249,35 +251,40 @@ async fn new_character<EG: EntityGateway>(entity_gateway: &mut EG, user: &UserAc
let mag = entity_gateway.create_item(
NewItemEntity {
item: ItemDetail::Mag(mag),
location: ItemLocation::Inventory {
character_id: character.id,
}}).await?;
let mut monomates = Vec::new();
for _ in 0..4usize {
monomates.push(entity_gateway.create_item(
NewItemEntity {
item: ItemDetail::Tool (
Tool {
tool: item::tool::ToolType::Monomate,
}),
location: ItemLocation::Inventory {
character_id: character.id,
}}).await?)
}
let mut monofluids = Vec::new();
for _ in 0..4usize {
monofluids.push(entity_gateway.create_item(
NewItemEntity {
item: ItemDetail::Tool (
Tool {
tool: item::tool::ToolType::Monofluid,
}),
location: ItemLocation::Inventory {
character_id: character.id,
}}).await?)
}
}).await?;
entity_gateway.add_item_note(&mag.id, ItemNote::CharacterCreation {
character_id: character.id,
}).await?;
let (monomates, monofluids) = futures::future::join_all((0..4usize).map(|_| {
let mut eg = entity_gateway.clone();
let character_id = character.id;
async move {
let monomate = eg.create_item(
NewItemEntity {
item: ItemDetail::Tool (
Tool {
tool: item::tool::ToolType::Monomate,
})}).await?;
eg.add_item_note(&monomate.id, ItemNote::CharacterCreation {
character_id
}).await?;
let monofluid = eg.create_item(
NewItemEntity {
item: ItemDetail::Tool (
Tool {
tool: item::tool::ToolType::Monofluid,
})}).await?;
eg.add_item_note(&monofluid.id, ItemNote::CharacterCreation {
character_id
}).await?;
Ok((monomate, monofluid))
}})).await.into_iter().collect::<Result<Vec<_>, GatewayError>>()?.into_iter().unzip();
let inventory = InventoryEntity {
items: vec![InventoryItemEntity::Individual(weapon.clone()), InventoryItemEntity::Individual(armor.clone()), InventoryItemEntity::Individual(mag.clone()),

54
src/ship/character.rs

@ -2,24 +2,17 @@ use libpso::character::character;
use crate::common::leveltable::CharacterStats;
use crate::entity::character::CharacterEntity;
use crate::ship::items::{CharacterInventory, CharacterBank};
use crate::entity::item::Meseta;
#[derive(Default)]
pub struct CharacterBytesBuilder<'a> {
character: Option<&'a CharacterEntity>,
stats: Option<&'a CharacterStats>,
level: Option<u32>,
meseta: Option<Meseta>,
}
impl<'a> Default for CharacterBytesBuilder<'a> {
fn default() -> CharacterBytesBuilder<'a> {
CharacterBytesBuilder {
character: None,
stats: None,
level: None,
}
}
}
impl<'a> CharacterBytesBuilder<'a> {
pub fn character(self, character: &'a CharacterEntity) -> CharacterBytesBuilder<'a> {
CharacterBytesBuilder {
@ -42,10 +35,18 @@ impl<'a> CharacterBytesBuilder<'a> {
}
}
pub fn meseta(self, meseta: Meseta) -> CharacterBytesBuilder<'a> {
CharacterBytesBuilder {
meseta: Some(meseta),
..self
}
}
pub fn build(self) -> character::Character {
let character = self.character.unwrap();
let stats = self.stats.unwrap();
let level = self.level.unwrap();
let meseta = self.meseta.unwrap();
character::Character {
name: libpso::utf8_to_utf16_array!(character.name, 16),
hp: stats.hp,
@ -70,7 +71,7 @@ impl<'a> CharacterBytesBuilder<'a> {
prop_y: character.appearance.prop_y,
config: character.config.as_bytes(),
techniques: character.techs.as_bytes(),
meseta: character.meseta,
meseta: meseta.0 as u32,
exp: character.exp,
..character::Character::default()
}
@ -78,10 +79,12 @@ impl<'a> CharacterBytesBuilder<'a> {
}
#[derive(Default)]
pub struct FullCharacterBytesBuilder<'a> {
character: Option<&'a CharacterEntity>,
stats: Option<&'a CharacterStats>,
level: Option<u32>,
meseta: Option<Meseta>,
inventory: Option<&'a CharacterInventory>,
bank: Option<&'a CharacterBank>,
key_config: Option<&'a [u8; 0x16C]>,
@ -91,24 +94,6 @@ pub struct FullCharacterBytesBuilder<'a> {
option_flags: Option<u32>,
}
impl<'a> Default for FullCharacterBytesBuilder<'a> {
fn default() -> FullCharacterBytesBuilder<'a> {
FullCharacterBytesBuilder {
character: None,
stats: None,
level: None,
inventory: None,
bank: None,
key_config: None,
joystick_config: None,
symbol_chat: None,
tech_menu: None,
option_flags: None,
}
}
}
impl<'a> FullCharacterBytesBuilder<'a> {
pub fn character(self, character: &'a CharacterEntity) -> FullCharacterBytesBuilder<'a> {
FullCharacterBytesBuilder {
@ -131,6 +116,13 @@ impl<'a> FullCharacterBytesBuilder<'a> {
}
}
pub fn meseta(self, meseta: Meseta) -> FullCharacterBytesBuilder<'a> {
FullCharacterBytesBuilder {
meseta: Some(meseta),
..self
}
}
pub fn inventory(self, inventory: &'a CharacterInventory) -> FullCharacterBytesBuilder<'a> {
FullCharacterBytesBuilder {
inventory: Some(inventory),
@ -184,6 +176,7 @@ impl<'a> FullCharacterBytesBuilder<'a> {
let character = self.character.unwrap();
let stats = self.stats.unwrap();
let level = self.level.unwrap();
let meseta = self.meseta.unwrap();
let inventory = self.inventory.unwrap();
let bank = self.bank.unwrap();
let key_config = self.key_config.unwrap();
@ -204,6 +197,7 @@ impl<'a> FullCharacterBytesBuilder<'a> {
.character(character)
.stats(stats)
.level(level - 1)
.meseta(meseta)
.build(),
inventory: character::Inventory {
item_count: inventory.count() as u8,

26
src/ship/items/bank.rs

@ -1,6 +1,6 @@
use crate::ship::items::ClientItemId;
use libpso::character::character;//::InventoryItem;
use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation, BankEntity, BankItemEntity, BankName};
use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, BankEntity, BankItemEntity, BankName};
use crate::entity::character::CharacterEntityId;
use crate::entity::item::tool::Tool;
use crate::ship::items::inventory::{InventoryItemHandle, InventoryItem};
@ -293,7 +293,7 @@ impl CharacterBank {
self.items.last()
}
pub fn as_bank_entity(&self, character_id: &CharacterEntityId, bank_name: &BankName) -> BankEntity {
pub fn as_bank_entity(&self, _character_id: &CharacterEntityId, _bank_name: &BankName) -> BankEntity {
BankEntity {
items: self.items.iter()
.map(|item| {
@ -301,26 +301,18 @@ impl CharacterBank {
BankItem::Individual(item) => {
BankItemEntity::Individual(ItemEntity {
id: item.entity_id,
location: ItemLocation::Bank {
character_id: *character_id,
name: bank_name.clone(),
},
item: item.item.clone(),
})
},
BankItem::Stacked(items) => {
BankItemEntity::Stacked(items.entity_ids.iter()
.map(|id| {
ItemEntity {
id: *id,
location: ItemLocation::Bank {
character_id: *character_id,
name: bank_name.clone(),
},
item: ItemDetail::Tool(items.tool)
}
})
.collect())
.map(|id| {
ItemEntity {
id: *id,
item: ItemDetail::Tool(items.tool)
}
})
.collect())
},
}
})

14
src/ship/items/floor.rs

@ -159,24 +159,24 @@ impl<'a> FloorItemHandle<'a> {
}
// TODO: floors should keep track of their own item_ids
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct RoomFloorItems(Vec<FloorItem>);
impl Default for RoomFloorItems {
fn default() -> RoomFloorItems {
RoomFloorItems(Vec::new())
}
}
impl RoomFloorItems {
pub fn add_item(&mut self, item: FloorItem) {
self.0.push(item);
}
pub fn remove_item(&mut self, item_id: &ClientItemId) {
self.0.retain(|item| item.item_id() != *item_id);
}
// TODO: &ClientItemId
pub fn get_item_by_id(&self, item_id: ClientItemId) -> Option<&FloorItem> {
self.0.iter().find(|item| item.item_id() == item_id)
}
// TODO: &ClientItemId
pub fn get_item_handle_by_id(&mut self, item_id: ClientItemId) -> Option<FloorItemHandle> {
let index = self.0.iter().position(|item| item.item_id() == item_id)?;
Some(FloorItemHandle {

208
src/ship/items/inventory.rs

@ -2,7 +2,7 @@ use std::cmp::Ordering;
use thiserror::Error;
use libpso::character::character;//::InventoryItem;
use crate::entity::character::CharacterEntityId;
use crate::entity::item::{ItemEntityId, ItemDetail, ItemEntity, ItemType, ItemLocation, InventoryEntity, InventoryItemEntity, EquippedEntity};
use crate::entity::item::{ItemEntityId, ItemDetail, ItemEntity, ItemType, InventoryEntity, InventoryItemEntity, EquippedEntity};
use crate::entity::item::tool::Tool;
use crate::entity::item::mag::Mag;
use crate::entity::item::weapon::Weapon;
@ -87,6 +87,24 @@ pub enum InventoryItemAddToError {
pub enum InventoryAddError {
}
#[derive(Debug, Clone)]
pub enum YesThereIsSpace {
NewStack,
ExistingStack,
}
#[derive(Debug, Clone)]
pub enum NoThereIsNotSpace {
FullStack,
FullInventory,
}
#[derive(Debug, Clone)]
pub enum SpaceForStack {
Yes(YesThereIsSpace),
No(NoThereIsNotSpace),
}
impl InventoryItem {
pub fn entity_ids(&self) -> Vec<ItemEntityId> {
match self {
@ -220,6 +238,27 @@ impl InventoryItem {
_ => None
}
}
pub fn stacked(&self) -> Option<&StackedInventoryItem> {
match self {
InventoryItem::Stacked(ref stacked_inventory_item) => Some(stacked_inventory_item),
_ => None
}
}
pub fn stacked_mut(&mut self) -> Option<&mut StackedInventoryItem> {
match self {
InventoryItem::Stacked(ref mut stacked_inventory_item) => Some(stacked_inventory_item),
_ => None
}
}
pub fn mag(&self) -> Option<&Mag> {
match self {
InventoryItem::Individual(individual_inventory_item) => individual_inventory_item.mag(),
_ => None
}
}
}
@ -413,6 +452,62 @@ impl CharacterInventory {
self.items.len()
}
pub fn space_for_individual_item(&self) -> bool {
self.count() < INVENTORY_CAPACITY
}
pub fn space_for_stacked_item(&self, tool: &Tool, amount: usize) -> SpaceForStack {
let existing_item = self.items.iter()
.filter_map(|item| {
match item {
InventoryItem::Stacked(s_item) => {
Some(s_item)
},
_ => None
}
})
.find(|s_item| {
s_item.tool == *tool
});
match existing_item {
Some(item) => {
if item.count() + amount <= tool.tool.max_stack() {
SpaceForStack::Yes(YesThereIsSpace::ExistingStack)
}
else {
SpaceForStack::No(NoThereIsNotSpace::FullStack)
}
}
None => {
if self.count() < INVENTORY_CAPACITY {
SpaceForStack::Yes(YesThereIsSpace::NewStack)
}
else {
SpaceForStack::No(NoThereIsNotSpace::FullInventory)
}
}
}
}
pub fn stack_item_id(&self, tool: &Tool) -> Option<ClientItemId> {
self.items.iter()
.filter_map(|item| {
match item {
InventoryItem::Stacked(s_item) => {
Some(s_item)
},
_ => None
}
})
.find(|s_item| {
s_item.tool == *tool
})
.map(|item| {
item.item_id
})
}
pub fn get_item_handle_by_id(&mut self, item_id: ClientItemId) -> Option<InventoryItemHandle> {
let (slot, _) = self.items.iter()
.enumerate()
@ -506,10 +601,80 @@ impl CharacterInventory {
.next()
}
pub fn add_item(&mut self, item: InventoryItem) -> Result<(), InventoryAddError> { // TODO: errors
// TODO: check slot conflict?
pub fn take_stacked_item_by_id(&mut self, item_id: ClientItemId, amount: usize) -> Option<StackedInventoryItem> {
let idx = self.items
.iter_mut()
.position(|i| i.item_id() == item_id)?;
let item: &mut StackedInventoryItem = self.items.get_mut(idx)?.stacked_mut()?;
match item.entity_ids.len().cmp(&amount) {
Ordering::Equal => {
let item = self.items.remove(idx);
item.stacked().cloned()
},
Ordering::Greater => {
let entity_ids = item.entity_ids.drain(..amount).collect();
Some(StackedInventoryItem {
entity_ids,
tool: item.tool,
item_id: item.item_id,
})
},
Ordering::Less => {
None
}
}
}
pub fn add_item(&mut self, item: InventoryItem) {
self.items.push(item);
Ok(())
}
pub fn add_stacked_item(&mut self, mut item: StackedInventoryItem) {
let existing_item = self.items
.iter_mut()
.filter_map(|i| {
match i {
InventoryItem::Stacked(stacked) => {
Some(stacked)
},
_ => None
}
})
.find(|i| {
i.tool == item.tool
});
match existing_item {
Some(existing_item) => {
existing_item.entity_ids.append(&mut item.entity_ids)
},
None => {
self.items.push(InventoryItem::Stacked(item))
}
}
}
pub fn add_item_with_new_item_id(&mut self, item: InventoryItem, item_id: ClientItemId) {
match item {
InventoryItem::Individual(mut individual_inventory_item) => {
individual_inventory_item.item_id = item_id;
self.add_item(InventoryItem::Individual(individual_inventory_item));
},
InventoryItem::Stacked(mut stacked_inventory_item) => {
stacked_inventory_item.item_id = item_id;
self.add_stacked_item(stacked_inventory_item)
}
}
}
pub fn add_individual_floor_item(&mut self, floor_item: &IndividualFloorItem) -> &InventoryItem {
self.items.push(InventoryItem::Individual(IndividualInventoryItem {
entity_id: floor_item.entity_id,
item_id: floor_item.item_id,
item: floor_item.item.clone(),
}));
self.items.last().unwrap()
}
// TODO: should these pick up functions take floor_item as mut and remove the ids?
@ -532,6 +697,33 @@ impl CharacterInventory {
}
}
pub fn add_stacked_floor_item(&mut self, floor_item: &StackedFloorItem) {
let existing_item = self.items.iter_mut()
.filter_map(|item| {
match item {
InventoryItem::Stacked(s_item) => Some(s_item),
_ => None,
}
})
.find(|item| {
item.tool == floor_item.tool
});
match existing_item {
Some(item) => {
item.entity_ids.append(&mut floor_item.entity_ids.clone())
},
None => {
self.items.push(InventoryItem::Stacked(StackedInventoryItem {
entity_ids: floor_item.entity_ids.clone(),
item_id: floor_item.item_id,
tool: floor_item.tool,
}));
}
}
}
// TODO: can be simplified using find instead of position
pub fn pick_up_stacked_floor_item(&mut self, floor_item: &StackedFloorItem) -> Option<(&StackedInventoryItem, InventorySlot)> {
let existing_stack_position = self.items.iter()
@ -705,7 +897,7 @@ impl CharacterInventory {
}
}
pub fn as_inventory_entity(&self, character_id: &CharacterEntityId) -> InventoryEntity {
pub fn as_inventory_entity(&self, _character_id: &CharacterEntityId) -> InventoryEntity {
InventoryEntity {
items: self.items.iter()
.map(|item| {
@ -713,9 +905,6 @@ impl CharacterInventory {
InventoryItem::Individual(item) => {
InventoryItemEntity::Individual(ItemEntity {
id: item.entity_id,
location: ItemLocation::Inventory {
character_id: *character_id,
},
item: item.item.clone(),
})
},
@ -724,9 +913,6 @@ impl CharacterInventory {
.map(|id| {
ItemEntity {
id: *id,
location: ItemLocation::Inventory {
character_id: *character_id,
},
item: ItemDetail::Tool(items.tool)
}
})

764
src/ship/items/manager.rs
File diff suppressed because it is too large
View File

3
src/ship/items/mod.rs

@ -2,10 +2,11 @@ mod bank;
mod floor;
pub mod inventory;
mod manager;
pub mod transaction;
pub mod use_tool;
use serde::{Serialize, Deserialize};
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, derive_more::Display)]
pub struct ClientItemId(pub u32);
// TODO: remove these and fix use statements in the rest of the codebase

337
src/ship/items/transaction.rs

@ -0,0 +1,337 @@
use crate::entity::gateway::EntityGateway;
use thiserror::Error;
use crate::ship::items::manager::{ItemManager, ItemManagerError};
use crate::entity::gateway::GatewayError;
#[derive(Error, Debug)]
pub enum TransactionCommitError {
#[error("transaction commit gateway error {0}")]
Gateway(#[from] GatewayError),
#[error("transaction commit itemmanager error {0}")]
ItemManager(#[from] ItemManagerError),
}
#[async_trait::async_trait]
pub trait ItemAction<EG: EntityGateway>: std::marker::Send + std::marker::Sync + std::fmt::Debug {
async fn commit(&self, manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError>;
}
pub struct ItemTransactionActions<'a, EG: EntityGateway> {
action_queue: Vec<Box<dyn ItemAction<EG>>>,
pub manager: &'a ItemManager,
}
impl<'a, EG: EntityGateway> ItemTransactionActions<'a, EG> {
fn new(manager: &'a ItemManager) -> ItemTransactionActions<'a, EG> {
ItemTransactionActions {
action_queue: Vec::new(),
manager
}
}
pub fn action(&mut self, action: Box<dyn ItemAction<EG>>) {
self.action_queue.push(action)
}
}
pub struct ItemTransaction<'a, T, EG: EntityGateway> {
data: T,
actions: ItemTransactionActions<'a, EG>,
}
impl<'a, T, EG: EntityGateway> ItemTransaction<'a, T, EG> {
pub fn new(manager: &'a ItemManager, arg: T) -> ItemTransaction<'a, T, EG> {
ItemTransaction {
data: arg,
actions: ItemTransactionActions::new(manager),
}
}
pub fn act<E: std::fmt::Debug, U>(mut self, action: fn(&mut ItemTransactionActions<EG>, &T) -> Result<U, E>) -> FinalizedItemTransaction<U, E, EG> {
match action(&mut self.actions, &self.data) {
Ok(k) => {
FinalizedItemTransaction {
value: Ok(k),
action_queue: self.actions.action_queue,
}
},
Err(err) => {
FinalizedItemTransaction {
value: Err(err),
action_queue: Vec::new(),
}
}
}
}
}
#[derive(Error, Debug)]
pub enum TransactionError<E: std::fmt::Debug> {
#[error("transaction action error {0:?}")]
Action(E),
#[error("transaction commit error {0}")]
Commit(#[from] TransactionCommitError),
}
// this only exists to drop the ItemManager borrow of ItemTransaction so a mutable ItemTransaction can be passed in later
pub struct FinalizedItemTransaction<T, E: std::fmt::Debug, EG: EntityGateway> {
value: Result<T, E>,
action_queue: Vec<Box<dyn ItemAction<EG>>>,
}
impl<T, E: std::fmt::Debug, EG: EntityGateway> FinalizedItemTransaction<T, E, EG> {
pub async fn commit(self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<T, TransactionError<E>> {
match self.value {
Ok(value) => {
for action in self.action_queue.into_iter() {
// TODO: better handle rolling back if this ever errors out
action.commit(item_manager, entity_gateway).await.map_err(|err| TransactionError::Commit(err))?;
}
Ok(value)
},
Err(err) => Err(TransactionError::Action(err)),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::entity::account::{UserAccountId, NewUserAccountEntity, UserAccountEntity};
use crate::entity::character::{NewCharacterEntity, CharacterEntity};
use crate::entity::gateway::GatewayError;
use thiserror::Error;
#[async_std::test]
async fn test_item_transaction() {
#[derive(Debug)]
struct DummyAction1 {
name: String,
}
#[derive(Debug)]
struct DummyAction2 {
value: u32,
}
#[derive(Error, Debug)]
#[error("")]
enum DummyError {
Error
}
#[derive(Default, Clone)]
struct DummyGateway {
d1_set: String,
d2_inc: u32,
}
#[async_trait::async_trait]
impl EntityGateway for DummyGateway {
async fn create_user(&mut self, user: NewUserAccountEntity) -> Result<UserAccountEntity, GatewayError> {
self.d1_set = user.username;
Ok(UserAccountEntity::default())
}
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
self.d2_inc += char.slot;
Ok(CharacterEntity::default())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> for DummyAction1 {
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
item_manager.id_counter = 55555;
entity_gateway.create_user(NewUserAccountEntity {
username: self.name.clone(),
..NewUserAccountEntity::default()
})
.await?;
Ok(())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> for DummyAction2 {
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
item_manager.id_counter += self.value;
entity_gateway.create_character(NewCharacterEntity {
slot: self.value,
..NewCharacterEntity::new(UserAccountId(0))
})
.await?;
Ok(())
}
}
let mut item_manager = ItemManager::default();
let mut entity_gateway = DummyGateway::default();
let result = ItemTransaction::new(&item_manager, 12)
.act(|it, k| {
it.action(Box::new(DummyAction1 {name: "asdf".into()}));
it.action(Box::new(DummyAction2 {value: 11}));
it.action(Box::new(DummyAction2 {value: *k}));
if *k == 99 {
return Err(DummyError::Error)
}
Ok(String::from("hello"))
})
.commit(&mut item_manager, &mut entity_gateway)
.await;
assert!(entity_gateway.d1_set == "asdf");
assert!(entity_gateway.d2_inc == 23);
assert!(item_manager.id_counter == 55578);
assert!(result.unwrap() == "hello");
}
#[async_std::test]
async fn test_item_transaction_with_action_error() {
#[derive(Debug)]
struct DummyAction1 {
}
#[derive(Debug)]
struct DummyAction2 {
}
#[derive(Error, Debug, PartialEq, Eq)]
#[error("")]
enum DummyError {
Error
}
#[derive(Default, Clone)]
struct DummyGateway {
d1_set: String,
d2_inc: u32,
}
#[async_trait::async_trait]
impl EntityGateway for DummyGateway {
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
self.d2_inc += char.slot;
Ok(CharacterEntity::default())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> for DummyAction1 {
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
entity_gateway.create_character(NewCharacterEntity {
slot: 1,
..NewCharacterEntity::new(UserAccountId(0))
})
.await?;
Ok(())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> for DummyAction2 {
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
entity_gateway.create_character(NewCharacterEntity {
slot: 1,
..NewCharacterEntity::new(UserAccountId(0))
})
.await?;
Ok(())
}
}
let mut item_manager = ItemManager::default();
let mut entity_gateway = DummyGateway::default();
let result = ItemTransaction::new(&item_manager, 12)
.act(|it, _| -> Result<(), _> {
it.action(Box::new(DummyAction1 {}));
it.action(Box::new(DummyAction2 {}));
it.action(Box::new(DummyAction2 {}));
Err(DummyError::Error)
})
.commit(&mut item_manager, &mut entity_gateway)
.await;
assert!(entity_gateway.d2_inc == 0);
assert!(matches!(result, Err(TransactionError::Action(DummyError::Error))));
}
#[async_std::test]
async fn test_item_transaction_with_commit_error() {
#[derive(Debug)]
struct DummyAction1 {
}
#[derive(Debug)]
struct DummyAction2 {
}
#[derive(Error, Debug, PartialEq, Eq)]
#[error("")]
enum DummyError {
}
#[derive(Default, Clone)]
struct DummyGateway {
d1_set: String,
d2_inc: u32,
}
#[async_trait::async_trait]
impl EntityGateway for DummyGateway {
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
self.d2_inc += char.slot;
Ok(CharacterEntity::default())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> for DummyAction1 {
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
entity_gateway.create_character(NewCharacterEntity {
slot: 1,
..NewCharacterEntity::new(UserAccountId(0))
})
.await?;
Err(GatewayError::Error.into())
}
}
#[async_trait::async_trait]
impl<EG: EntityGateway> ItemAction<EG> for DummyAction2 {
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
entity_gateway.create_character(NewCharacterEntity {
slot: 1,
..NewCharacterEntity::new(UserAccountId(0))
})
.await?;
Ok(())
}
}
let mut item_manager = ItemManager::default();
let mut entity_gateway = DummyGateway::default();
let result = ItemTransaction::new(&item_manager, 12)
.act(|it, _| -> Result<_, DummyError> {
it.action(Box::new(DummyAction1 {}));
it.action(Box::new(DummyAction2 {}));
it.action(Box::new(DummyAction2 {}));
Ok(())
})
.commit(&mut item_manager, &mut entity_gateway)
.await;
// in an ideal world this would be 0 as rollbacks would occur
assert!(entity_gateway.d2_inc == 1);
assert!(matches!(result, Err(TransactionError::Commit(TransactionCommitError::Gateway(GatewayError::Error)))));
}
}

26
src/ship/location.rs

@ -15,7 +15,7 @@ pub enum AreaType {
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LobbyId(pub usize);
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, derive_more::Display)]
pub struct RoomId(pub usize);
impl LobbyId {
@ -26,7 +26,7 @@ impl LobbyId {
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("create room")]
pub enum CreateRoomError {
NoOpenSlots,
ClientInAreaAlready,
@ -34,7 +34,7 @@ pub enum CreateRoomError {
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("join room")]
pub enum JoinRoomError {
RoomDoesNotExist,
RoomFull,
@ -42,7 +42,7 @@ pub enum JoinRoomError {
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("join lobby")]
pub enum JoinLobbyError {
LobbyDoesNotExist,
LobbyFull,
@ -50,7 +50,7 @@ pub enum JoinLobbyError {
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("get area")]
pub enum GetAreaError {
NotInRoom,
NotInLobby,
@ -58,28 +58,28 @@ pub enum GetAreaError {
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("client removal")]
pub enum ClientRemovalError {
ClientNotInArea,
InvalidArea,
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("get clients")]
pub enum GetClientsError {
InvalidClient,
InvalidArea,
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("get neighbor")]
pub enum GetNeighborError {
InvalidClient,
InvalidArea,
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("get leader")]
pub enum GetLeaderError {
InvalidClient,
InvalidArea,
@ -87,7 +87,7 @@ pub enum GetLeaderError {
}
#[derive(Error, Debug, PartialEq)]
#[error("")]
#[error("clientlocation")]
pub enum ClientLocationError {
CreateRoomError(#[from] CreateRoomError),
JoinRoomError(#[from] JoinRoomError),
@ -109,6 +109,12 @@ impl LocalClientId {
}
}
impl PartialEq<u8> for LocalClientId {
fn eq(&self, other: &u8) -> bool {
self.0 == *other as usize
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct AreaClient {
pub client: ClientId,

9
src/ship/map/area.rs

@ -335,18 +335,11 @@ impl MapAreaLookup {
}
#[derive(Default)]
pub struct MapAreaLookupBuilder {
map_areas: HashMap<u16, MapArea>,
}
impl Default for MapAreaLookupBuilder {
fn default() -> MapAreaLookupBuilder {
MapAreaLookupBuilder {
map_areas: HashMap::new()
}
}
}
impl MapAreaLookupBuilder {
pub fn add(mut self, value: u16, map_area: MapArea) -> MapAreaLookupBuilder {
self.map_areas.insert(value, map_area);

46
src/ship/map/enemy.rs

@ -19,17 +19,17 @@ pub struct RawMapEnemy {
id: u32,
_unknown1: u16,
pub children: u16,
map_area: u16,
_map_area: u16,
_unknown4: u16,
section: u16,
wave_idd: u16,
wave_id: u32,
x: f32,
y: f32,
z: f32,
xrot: u32,
yrot: u32,
zrot: u32,
_section: u16,
_wave_idd: u16,
_wave_id: u32,
_x: f32,
_y: f32,
_z: f32,
_xrot: u32,
_yrot: u32,
_zrot: u32,
_field1: u32,
field2: u32,
_field3: u32,
@ -45,17 +45,17 @@ impl RawMapEnemy {
id: cursor.read_u32::<LittleEndian>()?, // TODO: is this really u32? shiny monsters are referred to by u16 in the client
_unknown1: cursor.read_u16::<LittleEndian>()?,
children: cursor.read_u16::<LittleEndian>()?,
map_area: cursor.read_u16::<LittleEndian>()?,
_map_area: cursor.read_u16::<LittleEndian>()?,
_unknown4: cursor.read_u16::<LittleEndian>()?,
section: cursor.read_u16::<LittleEndian>()?,
wave_idd: cursor.read_u16::<LittleEndian>()?,
wave_id: cursor.read_u32::<LittleEndian>()?,
x: cursor.read_f32::<LittleEndian>()?,
y: cursor.read_f32::<LittleEndian>()?,
z: cursor.read_f32::<LittleEndian>()?,
xrot: cursor.read_u32::<LittleEndian>()?,
yrot: cursor.read_u32::<LittleEndian>()?,
zrot: cursor.read_u32::<LittleEndian>()?,
_section: cursor.read_u16::<LittleEndian>()?,
_wave_idd: cursor.read_u16::<LittleEndian>()?,
_wave_id: cursor.read_u32::<LittleEndian>()?,
_x: cursor.read_f32::<LittleEndian>()?,
_y: cursor.read_f32::<LittleEndian>()?,
_z: cursor.read_f32::<LittleEndian>()?,
_xrot: cursor.read_u32::<LittleEndian>()?,
_yrot: cursor.read_u32::<LittleEndian>()?,
_zrot: cursor.read_u32::<LittleEndian>()?,
_field1: cursor.read_u32::<LittleEndian>()?,
field2: cursor.read_u32::<LittleEndian>()?,
_field3: cursor.read_u32::<LittleEndian>()?,
@ -112,7 +112,7 @@ impl RareMonsterAppearTable {
pub struct MapEnemy {
pub monster: MonsterType,
pub map_area: MapArea,
hp: u32,
_hp: u32,
// TODO: other stats from battleparam
pub player_hit: [bool; 4],
pub dropped_item: bool,
@ -296,7 +296,7 @@ impl MapEnemy {
Ok(MapEnemy {
monster,
map_area: *map_area,
hp: 0,
_hp: 0,
dropped_item: false,
gave_exp: false,
player_hit: [false; 4],
@ -308,7 +308,7 @@ impl MapEnemy {
MapEnemy {
monster,
map_area,
hp: 0,
_hp: 0,
dropped_item: false,
gave_exp: false,
player_hit: [false; 4],

60
src/ship/map/object.rs

@ -13,50 +13,50 @@ use crate::ship::map::*;
#[derive(Debug, Copy, Clone)]
pub struct RawMapObject {
otype: u16,
unknown1: u16,
unknown2: u32,
id: u16,
group: u16,
section: u16,
unknown3: u16,
x: f32,
y: f32,
z: f32,
xrot: u32,
yrot: u32,
zrot: u32,
_unknown1: u16,
_unknown2: u32,
_id: u16,
_group: u16,
_section: u16,
_unknown3: u16,
_x: f32,
_y: f32,
_z: f32,
_xrot: u32,
_yrot: u32,
_zrot: u32,
field1: f32,
field2: f32,
field3: f32,
field4: u32,
field5: u32,
field6: u32,
field7: u32,
_field5: u32,
_field6: u32,
_field7: u32,
}
impl RawMapObject {
pub fn from_byte_stream<R: Read>(cursor: &mut R) -> Result<RawMapObject, std::io::Error> {
Ok(RawMapObject {
otype: cursor.read_u16::<LittleEndian>()?,
unknown1: cursor.read_u16::<LittleEndian>()?,
unknown2: cursor.read_u32::<LittleEndian>()?,
id: cursor.read_u16::<LittleEndian>()?,
group: cursor.read_u16::<LittleEndian>()?,
section: cursor.read_u16::<LittleEndian>()?,
unknown3: cursor.read_u16::<LittleEndian>()?,
x: cursor.read_f32::<LittleEndian>()?,
y: cursor.read_f32::<LittleEndian>()?,
z: cursor.read_f32::<LittleEndian>()?,
xrot: cursor.read_u32::<LittleEndian>()?,
yrot: cursor.read_u32::<LittleEndian>()?,
zrot: cursor.read_u32::<LittleEndian>()?,
_unknown1: cursor.read_u16::<LittleEndian>()?,
_unknown2: cursor.read_u32::<LittleEndian>()?,
_id: cursor.read_u16::<LittleEndian>()?,
_group: cursor.read_u16::<LittleEndian>()?,
_section: cursor.read_u16::<LittleEndian>()?,
_unknown3: cursor.read_u16::<LittleEndian>()?,
_x: cursor.read_f32::<LittleEndian>()?,
_y: cursor.read_f32::<LittleEndian>()?,
_z: cursor.read_f32::<LittleEndian>()?,
_xrot: cursor.read_u32::<LittleEndian>()?,
_yrot: cursor.read_u32::<LittleEndian>()?,
_zrot: cursor.read_u32::<LittleEndian>()?,
field1: cursor.read_f32::<LittleEndian>()?,
field2: cursor.read_f32::<LittleEndian>()?,
field3: cursor.read_f32::<LittleEndian>()?,
field4: cursor.read_u32::<LittleEndian>()?,
field5: cursor.read_u32::<LittleEndian>()?,
field6: cursor.read_u32::<LittleEndian>()?,
field7: cursor.read_u32::<LittleEndian>()?,
_field5: cursor.read_u32::<LittleEndian>()?,
_field6: cursor.read_u32::<LittleEndian>()?,
_field7: cursor.read_u32::<LittleEndian>()?,
})
}
}

1
src/ship/mod.rs

@ -11,3 +11,4 @@ pub mod drops;
pub mod packet;
pub mod quests;
pub mod shops;
pub mod trade;

37
src/ship/packet/builder/message.rs

@ -27,7 +27,8 @@ pub fn item_drop(client: u8, target: u8, item_drop: &FloorItem) -> Result<ItemDr
})
}
pub fn create_item(area_client: AreaClient, item_id: ClientItemId, item: &item::ItemDetail) -> Result<CreateItem, ShipError> {
// TODO: this doesn't need to be a Result, just unwrap try_intos they are guaranteed to succeed
pub fn create_individual_item(area_client: AreaClient, item_id: ClientItemId, item: &item::ItemDetail) -> Result<CreateItem, ShipError> {
let bytes = item.as_client_bytes();
Ok(CreateItem {
client: area_client.local_client.id(),
@ -39,6 +40,31 @@ pub fn create_item(area_client: AreaClient, item_id: ClientItemId, item: &item::
})
}
// TODO: this doesn't need to be a Result, just unwrap try_intos they are guaranteed to succeed
pub fn create_stacked_item(area_client: AreaClient, item_id: ClientItemId, tool: &item::tool::Tool, amount: usize) -> Result<CreateItem, ShipError> {
let bytes = tool.as_stacked_bytes(amount);
Ok(CreateItem {
client: area_client.local_client.id(),
target: 0,
item_data: bytes[0..12].try_into()?,
item_id: item_id.0,
item_data2: bytes[12..16].try_into()?,
unknown: 0,
})
}
pub fn create_meseta(area_client: AreaClient, amount: usize) -> CreateItem {
let bytes: [u8; 12] = [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
CreateItem {
client: area_client.local_client.id(),
target: 0,
item_data: bytes,
item_id: 0xFFFFFFFF,
item_data2: u32::to_le_bytes(amount as u32),
unknown: 0,
}
}
pub fn create_withdrawn_inventory_item(area_client: AreaClient, item: &InventoryItem) -> Result<CreateItem, ShipError> {
let bytes = item.as_client_bytes();
Ok(CreateItem {
@ -142,6 +168,15 @@ pub fn player_no_longer_has_item(area_client: AreaClient, item_id: ClientItemId,
}
}
pub fn player_no_longer_has_meseta(area_client: AreaClient, amount: u32) -> PlayerNoLongerHasItem {
PlayerNoLongerHasItem {
client: area_client.local_client.id(),
target: 0,
item_id: 0xFFFFFFFF,
amount,
}
}
pub fn shop_list<I: ShopItem>(shop_type: u8, items: &[I]) -> ShopList {
let items = items.iter()
.enumerate()

2
src/ship/packet/builder/mod.rs

@ -26,10 +26,12 @@ pub fn player_header(tag: u32, client: &ClientState, area_client: &AreaClient) -
pub fn player_info(tag: u32, client: &ClientState, area_client: &AreaClient, item_manager: &ItemManager, level_table: &CharacterLevelTable) -> PlayerInfo {
let (level, stats) = level_table.get_stats_from_exp(client.character.char_class, client.character.exp);
let inventory = item_manager.get_character_inventory(&client.character).unwrap();
let meseta = item_manager.get_character_meseta(&client.character.id).unwrap();
let character = CharacterBytesBuilder::default()
.character(&client.character)
.stats(&stats)
.level(level - 1)
.meseta(*meseta)
.build();
PlayerInfo {
header: player_header(tag, client, area_client),

0
src/ship/packet/builder/trade.rs

48
src/ship/packet/handler/direct_message.rs

@ -137,8 +137,8 @@ where
let (item, floor_type) = item_manager.get_floor_item_by_id(&client.character, ClientItemId(pickup_item.item_id))?;
let remove_item = builder::message::remove_item_from_floor(area_client, item)?;
let create_item = match item {
FloorItem::Individual(individual_floor_item) => Some(builder::message::create_item(area_client, item.item_id(), &individual_floor_item.item)?),
FloorItem::Stacked(stacked_floor_item) => Some(builder::message::create_item(area_client, item.item_id(), &item::ItemDetail::Tool(stacked_floor_item.tool))?),
FloorItem::Individual(individual_floor_item) => Some(builder::message::create_individual_item(area_client, item.item_id(), &individual_floor_item.item)?),
FloorItem::Stacked(stacked_floor_item) => Some(builder::message::create_stacked_item(area_client, item.item_id(), &stacked_floor_item.tool, stacked_floor_item.count())?),
FloorItem::Meseta(_) => None,
//_ => Some(builder::message::create_item(area_client, &item)?),
};
@ -232,8 +232,8 @@ pub async fn send_bank_list(id: ClientId,
{
let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?;
let bank_items = item_manager.get_character_bank(&client.character)?;
let bank_items_pkt = builder::message::bank_item_list(bank_items, client.character.bank_meseta);
let bank_meseta = item_manager.get_bank_meseta(&client.character.id)?;
let bank_items_pkt = builder::message::bank_item_list(bank_items, bank_meseta.0);
Ok(Box::new(vec![(id, SendShipPacket::BankItemList(bank_items_pkt))].into_iter()))
}
@ -252,11 +252,16 @@ where
let other_clients_in_area = client_location.get_all_clients_by_client(id).map_err(|err| -> ClientLocationError { err.into() })?;
let bank_action_pkts = match bank_interaction.action {
BANK_ACTION_DEPOSIT => {
let character_meseta = item_manager.get_character_meseta(&client.character.id)?;
let bank_meseta = item_manager.get_bank_meseta(&client.character.id)?;
if bank_interaction.item_id == 0xFFFFFFFF {
if client.character.meseta >= bank_interaction.meseta_amount && (bank_interaction.meseta_amount + client.character.bank_meseta) <= BANK_MESETA_CAPACITY {
client.character.meseta -= bank_interaction.meseta_amount;
client.character.bank_meseta += bank_interaction.meseta_amount;
entity_gateway.save_character(&client.character).await?;
if character_meseta.0 >= bank_interaction.meseta_amount && (bank_interaction.meseta_amount + bank_meseta.0) <= BANK_MESETA_CAPACITY {
let (character_meseta, bank_meseta) = item_manager.get_character_and_bank_meseta_mut(&client.character.id)?;
character_meseta.0 -= bank_interaction.meseta_amount;
bank_meseta.0 += bank_interaction.meseta_amount;
entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?;
// TODO: BankName
entity_gateway.set_bank_meseta(&client.character.id, item::BankName("".into()), *bank_meseta).await?;
}
Vec::new()
}
@ -267,11 +272,16 @@ where
}
},
BANK_ACTION_WITHDRAW => {
let character_meseta = item_manager.get_character_meseta(&client.character.id)?;
let bank_meseta = item_manager.get_bank_meseta(&client.character.id)?;
if bank_interaction.item_id == 0xFFFFFFFF {
if client.character.meseta + bank_interaction.meseta_amount <= INVENTORY_MESETA_CAPACITY {
client.character.meseta += bank_interaction.meseta_amount;
client.character.bank_meseta -= bank_interaction.meseta_amount;
entity_gateway.save_character(&client.character).await?;
if (bank_meseta.0 >= bank_interaction.meseta_amount) && (character_meseta.0 + bank_interaction.meseta_amount <= INVENTORY_MESETA_CAPACITY) {
let (character_meseta, bank_meseta) = item_manager.get_character_and_bank_meseta_mut(&client.character.id)?;
character_meseta.0 += bank_interaction.meseta_amount;
bank_meseta.0 -= bank_interaction.meseta_amount;
entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?;
// TODO: BankName
entity_gateway.set_bank_meseta(&client.character.id, item::BankName("".into()), *bank_meseta).await?;
}
Vec::new()
}
@ -370,12 +380,13 @@ where
}
};
if client.character.meseta < item.price() as u32 {
let character_meseta = item_manager.get_character_meseta_mut(&client.character.id)?;
if character_meseta.0 < item.price() as u32 {
return Err(ShipError::ShopError.into())
}
client.character.meseta -= item.price() as u32;
entity_gateway.save_character(&client.character).await?;
character_meseta.0 -= item.price() as u32;
entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?;
let inventory_item = item_manager.player_buys_item(entity_gateway, &client.character, item, ClientItemId(buy_item.item_id), buy_item.amount as usize).await?;
let create = builder::message::create_withdrawn_inventory_item(area_client, inventory_item)?;
@ -447,8 +458,9 @@ where
grind: grind_mod,
});
client.character.meseta -= 100;
entity_gateway.save_character(&client.character).await?;
let character_meseta = item_manager.get_character_meseta_mut(&client.character.id)?;
character_meseta.0 -= 100;
entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?;
let preview_pkt = builder::message::tek_preview(ClientItemId(tek_request.item_id), &weapon)?;
@ -480,7 +492,7 @@ where
};
let weapon = item_manager.replace_item_with_tekked(entity_gateway, &client.character, item_id, modifier).await?;
let create_item_pkt = builder::message::create_item(area_client, item_id, &item::ItemDetail::Weapon(weapon))?;
let create_item_pkt = builder::message::create_individual_item(area_client, item_id, &item::ItemDetail::Weapon(weapon))?;
let neighbors = client_location.get_client_neighbors(id).map_err(|err| -> ClientLocationError { err.into() })?;
Ok(Box::new(neighbors.into_iter()

2
src/ship/packet/handler/lobby.rs

@ -22,12 +22,14 @@ pub fn block_selected(id: ClientId,
let (level, stats) = level_table.get_stats_from_exp(client.character.char_class, client.character.exp);
let inventory = item_manager.get_character_inventory(&client.character).unwrap();
let meseta = item_manager.get_character_meseta(&client.character.id).unwrap();
let bank = item_manager.get_character_bank(&client.character).unwrap();
let fc = FullCharacterBytesBuilder::default()
.character(&client.character)
.stats(&stats)
.level(level)
.meseta(*meseta)
.inventory(inventory)
.bank(bank)
.key_config(&client.settings.settings.key_config)

46
src/ship/packet/handler/message.rs

@ -135,13 +135,26 @@ where
let dropped_meseta = item_manager.player_drops_meseta_on_shared_floor(entity_gateway, &mut client.character, drop_location, no_longer_has_item.amount as u32).await?;
let dropped_meseta_pkt = builder::message::drop_split_meseta_stack(area_client, &dropped_meseta)?;
let no_longer_has_meseta_pkt = builder::message::player_no_longer_has_meseta(area_client, no_longer_has_item.amount as u32);
client.item_drop_location = None;
let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?;
Ok(Box::new(clients_in_area.into_iter()
.map(move |c| {
(c.client, SendShipPacket::Message(Message::new(GameMessage::DropSplitStack(dropped_meseta_pkt.clone()))))
})))
std::iter::once((c.client, SendShipPacket::Message(Message::new(GameMessage::DropSplitStack(dropped_meseta_pkt.clone())))))
.chain(
if c.client != id {
Box::new(std::iter::once(
(c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(no_longer_has_meseta_pkt.clone()))))
)) as Box<dyn Iterator<Item = _> + Send>
}
else {
Box::new(std::iter::empty()) as Box<dyn Iterator<Item = _> + Send>
}
)
})
.flatten()
))
}
else {
let dropped_item = item_manager.player_drops_partial_stack_on_shared_floor(entity_gateway, &client.character, drop_location.item_id, drop_location, no_longer_has_item.amount as usize).await?;
@ -181,7 +194,7 @@ pub fn update_player_position(id: ClientId,
.ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
.as_ref()
.ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?;
match &message.msg {
GameMessage::PlayerChangedMap(p) => {
client.x = p.x;
@ -245,18 +258,22 @@ pub fn update_player_position(id: ClientId,
pub async fn charge_attack<EG>(id: ClientId,
charge: &ChargeAttack,
clients: &mut Clients,
entity_gateway: &mut EG)
entity_gateway: &mut EG,
item_manager: &mut ItemManager)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error>
where
EG: EntityGateway
{
let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
if client.character.meseta >= charge.meseta {
client.character.meseta -= charge.meseta;
entity_gateway.save_character(&client.character).await?;
let meseta = item_manager.get_character_meseta_mut(&client.character.id)?;
if meseta.0 >= charge.meseta {
meseta.0 -= charge.meseta;
entity_gateway.set_character_meseta(&client.character.id, *meseta).await?;
// TODO: this should probably echo the packet
Ok(Box::new(None.into_iter()))
} else {
Err(ShipError::NotEnoughMeseta(id, client.character.meseta).into())
Err(ShipError::NotEnoughMeseta(id, meseta.0).into())
}
}
@ -280,18 +297,21 @@ where
pub async fn player_used_medical_center<EG>(id: ClientId,
_pumc: &PlayerUsedMedicalCenter, // not needed?
entity_gateway: &mut EG,
clients: &mut Clients)
clients: &mut Clients,
item_manager: &mut ItemManager)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error>
where
EG: EntityGateway
{
let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
if client.character.meseta >= 10 {
client.character.meseta -= 10;
entity_gateway.save_character(&client.character).await?;
let meseta = item_manager.get_character_meseta_mut(&client.character.id)?;
if meseta.0 >= 10 {
meseta.0 -= 10;
entity_gateway.set_character_meseta(&client.character.id, *meseta).await?;
// TODO: this should probably echo the packet
Ok(Box::new(None.into_iter()))
} else {
Err(ShipError::NotEnoughMeseta(id, client.character.meseta).into())
Err(ShipError::NotEnoughMeseta(id, meseta.0).into())
}
}

1
src/ship/packet/handler/mod.rs

@ -7,3 +7,4 @@ pub mod room;
pub mod settings;
pub mod quest;
pub mod ship;
pub mod trade;

53
src/ship/packet/handler/room.rs

@ -7,20 +7,41 @@ use crate::ship::location::{ClientLocation, RoomId, RoomLobby, ClientLocationErr
use crate::ship::packet::builder;
use crate::ship::room;
use crate::ship::items::ItemManager;
use std::convert::{TryFrom};
pub fn create_room(id: ClientId,
create_room: &CreateRoom,
client_location: &mut ClientLocation,
clients: &mut Clients,
item_manager: &mut ItemManager,
level_table: &CharacterLevelTable,
rooms: &mut Rooms)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError> {
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error> {
let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?;
let level = level_table.get_level_from_exp(client.character.char_class, client.character.exp);
match room::Difficulty::try_from(create_room.difficulty)? {
room::Difficulty::Ultimate => {
if level < 80 {
return Ok(Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("You must be at least level 80 \nto create Ultimate rooms.".into())))].into_iter()))
}
},
room::Difficulty::VeryHard => {
if level < 40 {
return Ok(Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("You must be at least level 40 \nto create Very Hard rooms.".into())))].into_iter()))
}
},
room::Difficulty::Hard => {
if level < 20 {
return Ok(Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("You must be at least level 20 \nto create Hard rooms.".into())))].into_iter()))
}
},
room::Difficulty::Normal => {},
};
let area = client_location.get_area(id).unwrap();
let area_client = client_location.get_local_client(id).unwrap();
let lobby_neighbors = client_location.get_client_neighbors(id).unwrap();
let room_id = client_location.create_new_room(id).unwrap();
let client = clients.get_mut(&id).unwrap();//.ok_or(ShipError::ClientNotFound(id)).unwrap();
let mut room = room::RoomState::from_create_room(create_room, client.character.section_id).unwrap();
room.bursting = true;
@ -63,11 +84,31 @@ pub fn join_room(id: ClientId,
level_table: &CharacterLevelTable,
rooms: &mut Rooms)
-> Result<Box<dyn Iterator<Item=(ClientId, SendShipPacket)> + Send>, ShipError> {
let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?;
let level = level_table.get_level_from_exp(client.character.char_class, client.character.exp);
let room = rooms.get(pkt.item as usize).ok_or(ShipError::InvalidRoom(pkt.item))?.as_ref().unwrap(); // clippy look what you made me do
match room.mode.difficulty() {
room::Difficulty::Ultimate => {
if level < 80 {
return Ok(Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("You must be at least level 80 \nto join Ultimate rooms.".into())))].into_iter()))
}
},
room::Difficulty::VeryHard => {
if level < 40 {
return Ok(Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("You must be at least level 40 \nto join Very Hard rooms.".into())))].into_iter()))
}
},
room::Difficulty::Hard => {
if level < 20 {
return Ok(Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("You must be at least level 20 \nto join Hard rooms.".into())))].into_iter()))
}
},
_ => {},
};
let original_area = client_location.get_area(id).unwrap();
let original_neighbors = client_location.get_client_neighbors(id).unwrap();
let room = rooms.get(pkt.item as usize)
.ok_or(ShipError::InvalidRoom(pkt.item))?.as_ref()
.ok_or(ShipError::InvalidRoom(pkt.item))?;
if room.bursting {
return Ok(Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("player is bursting\nplease wait".into())))].into_iter()))
}

544
src/ship/packet/handler/trade.rs

@ -0,0 +1,544 @@
use std::convert::TryInto;
use libpso::packet::ship::*;
use libpso::packet::messages::*;
use crate::common::serverstate::ClientId;
use crate::ship::ship::{SendShipPacket, ShipError, Clients};
use crate::ship::location::{ClientLocation, ClientLocationError};
use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, ItemToTradeDetail};
use crate::ship::items::inventory::InventoryItem;
use crate::ship::trade::{TradeItem, TradeState, TradeStatus};
use crate::entity::gateway::EntityGateway;
use crate::ship::packet::builder;
pub const MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFF01);
pub const OTHER_MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFFFF);
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum TradeError {
#[error("no partner")]
CouldNotFindTradePartner,
#[error("invalid item id")]
InvalidItemId(ClientItemId),
#[error("item does not match id")]
ClientItemIdDidNotMatchItem(ClientItemId, [u8; 16]),
#[error("invalid stack {1}")]
InvalidStackAmount(ClientItemId, usize),
#[error("not in trade menu")]
NotInTradeMenu,
#[error("trade menu at an invalid point")]
MismatchedStatus,
#[error("no space in inventory")]
NoInventorySpace,
#[error("no space in stack")]
NoStackSpace,
#[error("invalid meseta amount")]
InvalidMeseta,
#[error("tried starting a trade while in one already")]
ClientAlreadyInTrade,
#[error("tried starting a trade while with player already in a trade")]
OtherAlreadyInTrade,
#[error("tried to trade item not specified in trade request")]
SketchyTrade,
#[error("items in trade window and items attempted to trade do not match")]
MismatchedTradeItems,
}
// TODO: remove target
pub async fn trade_request(id: ClientId,
trade_request: &TradeRequest,
target: u32,
client_location: &ClientLocation,
clients: &mut Clients,
item_manager: &mut ItemManager,
trades: &mut TradeState)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error>
{
let trade_request = trade_request.clone(); // TODO: make this function take ownership of packet
match trade_request.trade {
TradeRequestCommand::Initialize(ref act, _meseta) => {
match act {
TradeRequestInitializeCommand::Initialize => {
if trades.in_trade(&id) {
return Err(TradeError::ClientAlreadyInTrade.into())
}
let trade_partner = client_location.get_client_neighbors(id)?
.into_iter()
.find(|ac| {
ac.local_client.id() == target as u8 //trade_request.client
})
.ok_or(TradeError::CouldNotFindTradePartner)?;
if trades.in_trade(&trade_partner.client) {
return Err(TradeError::OtherAlreadyInTrade.into())
}
trades.new_trade(&id, &trade_partner.client);
Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone()))))
})))
},
TradeRequestInitializeCommand::Respond => {
Ok(trades
.with(&id, |this, other| -> Option<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>> {
if this.status == TradeStatus::ReceivedRequest && other.status == TradeStatus::SentRequest {
this.status = TradeStatus::Trading;
other.status = TradeStatus::Trading;
let trade_request = trade_request.clone();
Some(Box::new(client_location.get_all_clients_by_client(id).ok()?.into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone()))))
})))
}
else {
None
}
})?
.unwrap_or_else(|| -> Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send> {
trades.remove_trade(&id);
Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))
}))
}
}
},
TradeRequestCommand::AddItem(item_id, amount) => {
Ok(trades
.with(&id, |this, other| -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error> {
if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading {
let client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?;
let inventory = item_manager.get_character_inventory(&client.character)?;
if ClientItemId(item_id) == MESETA_ITEM_ID {
this.meseta += amount as usize;
}
else {
let item = inventory.get_item_by_id(ClientItemId(item_id)).ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item_id)))?;
match item {
InventoryItem::Individual(_) => {
this.items.push(TradeItem::Individual(ClientItemId(item_id)));
},
InventoryItem::Stacked(stacked_item) => {
if stacked_item.count() < amount as usize {
return Err(TradeError::InvalidStackAmount(ClientItemId(item_id), amount as usize).into());
}
this.items.push(TradeItem::Stacked(ClientItemId(item_id), amount as usize));
},
}
}
let trade_request = trade_request.clone();
Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone()))))
})))
}
else {
Err(TradeError::MismatchedStatus.into())
}
})?
.unwrap_or_else(|_err| {
trades.remove_trade(&id);
Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))
}))
},
TradeRequestCommand::RemoveItem(item_id, amount) => {
Ok(trades
.with(&id, |this, other| -> Option<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>> {
if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading {
let client = clients.get(&this.client())?; //.ok_or(ShipError::ClientNotFound(id)).ok()?;
let inventory = item_manager.get_character_inventory(&client.character).ok()?;
if ClientItemId(item_id) == MESETA_ITEM_ID {
this.meseta -= amount as usize;
}
else {
let item = inventory.get_item_by_id(ClientItemId(item_id))?;
match item {
InventoryItem::Individual(_) => {
this.items.retain(|item| {
item.item_id() != ClientItemId(item_id)
})
},
InventoryItem::Stacked(_stacked_item) => {
let trade_item_index = this.items.iter()
.position(|item| {
item.item_id() == ClientItemId(item_id)
})?;
match this.items[trade_item_index].stacked()?.1.cmp(&(amount as usize)) {
std::cmp::Ordering::Greater => {
*this.items[trade_item_index].stacked_mut()?.1 -= amount as usize;
},
std::cmp::Ordering::Equal => {
this.items.remove(trade_item_index);
},
std::cmp::Ordering::Less => {
return None
}
}
},
}
}
let trade_request = trade_request.clone();
Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone()))))
})))
}
else {
None
}
})?
.unwrap_or_else(|| {
trades.remove_trade(&id);
Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))
}))
},
TradeRequestCommand::Confirm => {
Ok(trades
.with(&id, |this, other| -> Option<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>> {
if status_is(&this.status, &[TradeStatus::Trading]) && status_is(&other.status, &[TradeStatus::Trading, TradeStatus::Confirmed]) {
this.status = TradeStatus::Confirmed;
let trade_request = trade_request.clone();
Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone()))))
})))
}
else {
None
}
})?
.unwrap_or_else(|| {
trades.remove_trade(&id);
Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))
}))
},
TradeRequestCommand::FinalConfirm => {
Ok(trades
.with(&id, |this, other| -> Option<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>> {
if this.status == TradeStatus::Confirmed && (other.status == TradeStatus::Confirmed || other.status == TradeStatus::FinalConfirm) {
this.status = TradeStatus::FinalConfirm;
let trade_request = trade_request.clone();
Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone()))))
})))
}
else {
None
}
})?
.unwrap_or_else(|| {
trades.remove_trade(&id);
Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))
}))
},
TradeRequestCommand::Cancel => {
trades.remove_trade(&id);
Ok(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))))
}
}
}
fn status_is<const N: usize>(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool {
statuses.iter().any(|s| s == status)
}
fn status_is_not<const N: usize>(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool {
!status_is(status, statuses)
}
pub async fn inner_items_to_trade(id: ClientId,
items_to_trade: &ItemsToTrade,
client_location: &ClientLocation,
clients: &mut Clients,
item_manager: &mut ItemManager,
trades: &mut TradeState)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error>
{
Ok(trades
.with(&id, |this, other| -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error> {
if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) {
return Err(TradeError::MismatchedStatus.into())
}
let client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?;
let other_client = clients.get(&other.client()).ok_or_else(|| ShipError::ClientNotFound(other.client()))?;
let inventory = item_manager.get_character_inventory(&client.character)?;
if items_to_trade.count as usize != (this.items.len() + (if this.meseta != 0 { 1 } else { 0 })) {
return Err(TradeError::MismatchedTradeItems.into())
}
items_to_trade.items
.iter()
.take(items_to_trade.count as usize)
.map(|item| {
if ClientItemId(item.item_id) == OTHER_MESETA_ITEM_ID {
if item.item_data[0] != 4 {
return Err(TradeError::InvalidItemId(ClientItemId(item.item_id)).into())
}
let amount = u32::from_le_bytes(item.item_data2);
let character_meseta = item_manager.get_character_meseta(&client.character.id).map_err(|_| TradeError::InvalidMeseta)?;
let other_character_meseta = item_manager.get_character_meseta(&other_client.character.id).map_err(|_| TradeError::InvalidMeseta)?;
if amount > character_meseta.0 {
return Err(TradeError::InvalidMeseta.into())
}
if (amount + other_character_meseta.0) > 999999 {
return Err(TradeError::InvalidMeseta.into())
}
if amount != this.meseta as u32{
return Err(TradeError::InvalidMeseta.into())
}
Ok(())
}
else {
let real_item = inventory.get_item_by_id(ClientItemId(item.item_id))
.ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?;
let real_trade_item = this.items
.iter()
.find(|i| i.item_id() == ClientItemId(item.item_id))
.ok_or(TradeError::SketchyTrade)?;
let trade_item_bytes: [u8; 16] = item.item_data.iter()
.chain(item.item_data2.iter())
.cloned().collect::<Vec<u8>>()
.try_into()
.unwrap();
match real_item {
InventoryItem::Individual(_individual_inventory_item) => {
if real_item.as_client_bytes() == trade_item_bytes {
Ok(())
}
else {
Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into())
}
},
InventoryItem::Stacked(stacked_inventory_item) => {
if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] {
let amount = trade_item_bytes[5] as usize;
if amount <= stacked_inventory_item.entity_ids.len() {
if real_trade_item.stacked().ok_or(TradeError::SketchyTrade)?.1 == amount {
Ok(())
}
else {
Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into())
}
}
else {
Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into())
}
}
else {
Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into())
}
}
}
}
})
.collect::<Result<Vec<_>, anyhow::Error>>()?;
this.status = TradeStatus::ItemsChecked;
if this.status == TradeStatus::ItemsChecked && other.status == TradeStatus::ItemsChecked {
Ok(Box::new(vec![
(this.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})),
(other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})),
].into_iter()))
}
else {
Ok(Box::new(None.into_iter()))
}
})?
.unwrap_or_else(|err| {
log::warn!("trade error: {:?}", err);
let (_this, other) = trades.remove_trade(&id);
Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| other.as_ref().map(|other| client.client == other.client() ).unwrap_or_else(|| false))
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))
}))
}
pub async fn items_to_trade(id: ClientId,
items_to_trade_pkt: &ItemsToTrade,
client_location: &ClientLocation,
clients: &mut Clients,
item_manager: &mut ItemManager,
trades: &mut TradeState)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error>
{
let t = inner_items_to_trade(id, items_to_trade_pkt, client_location, clients, item_manager, trades).await;
match t {
Ok(p) => Ok(p),
Err(err) => {
log::warn!("atrade error: {:?}", err);
let (_this, other) = trades.remove_trade(&id);
Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter()
.filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false))
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))))
}
}
}
pub async fn trade_confirmed<EG>(id: ClientId,
entity_gateway: &mut EG,
client_location: &ClientLocation,
clients: &mut Clients,
item_manager: &mut ItemManager,
trades: &mut TradeState)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error>
where
EG: EntityGateway
{
enum TradeReady<'a> {
OnePlayer,
BothPlayers(crate::ship::location::RoomId,
(crate::ship::location::AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState),
(crate::ship::location::AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState)),
}
let trade_instructions = trades
.with(&id, |this, other| -> Result<_, anyhow::Error> {
if status_is_not(&this.status, &[TradeStatus::ItemsChecked]) || status_is_not(&other.status, &[TradeStatus::ItemsChecked, TradeStatus::TradeComplete]) {
return Err(TradeError::MismatchedStatus.into())
}
this.status = TradeStatus::TradeComplete;
if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete {
let this_client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?;
let other_client = clients.get(&other.client()).ok_or_else(|| ShipError::ClientNotFound(other.client()))?;
let this_local_client = client_location.get_local_client(this.client())?;
let other_local_client = client_location.get_local_client(other.client())?;
let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?;
Ok(TradeReady::BothPlayers(room_id,
(this_local_client, this_client, this.clone()),
(other_local_client, other_client, other.clone())))
}
else {
Ok(TradeReady::OnePlayer)
}
});
// TODO: this match needs to handle errors better
match trade_instructions {
Ok(Ok(trade)) => {
match trade {
TradeReady::OnePlayer => {
Ok(Box::new(None.into_iter()) as Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>)
},
TradeReady::BothPlayers(room_id, (this_local_client, this_client, this), (other_local_client, other_client, other)) => {
let traded_items = item_manager.trade_items(entity_gateway,
room_id,
(&this_local_client, &this_client.character, &this.items, this.meseta),
(&other_local_client, &other_client.character, &other.items, other.meseta)).await?;
let clients_in_room = client_location.get_all_clients_by_client(id)?;
let traded_item_packets = traded_items
.into_iter()
.map(|item| {
match item.item_detail {
ItemToTradeDetail::Individual(item_detail) => {
[
GameMessage::CreateItem(builder::message::create_individual_item(item.add_to, item.new_item_id, &item_detail).unwrap()),
GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, 1)) // TODO: amount = ?
]
},
ItemToTradeDetail::Stacked(tool, amount) => {
[
GameMessage::CreateItem(builder::message::create_stacked_item(item.add_to, item.new_item_id, &tool, amount).unwrap()),
GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, amount as u32))
]
},
ItemToTradeDetail::Meseta(amount) => {
[
GameMessage::CreateItem(builder::message::create_meseta(item.add_to, amount)),
GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, amount as u32))
]
},
}
})
.flatten()
.map(move |packet| {
clients_in_room
.clone()
.into_iter()
.filter_map(move |client| {
match packet {
GameMessage::PlayerNoLongerHasItem(ref no_longer) => {
if client.local_client == no_longer.client {
None
}
else {
Some((client.client, SendShipPacket::Message(Message::new(packet.clone()))))
}
}
_ => Some((client.client, SendShipPacket::Message(Message::new(packet.clone()))))
}
})
})
.flatten();
let close_trade = vec![
(this.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())),
(other.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default()))
].into_iter();
Ok(Box::new(traded_item_packets.chain(close_trade)))
}
}
},
_ => {
let (_this, other) = trades.remove_trade(&id);
Ok(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter()
.filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false))
.map(move |client| {
(client.client, SendShipPacket::CancelTrade(CancelTrade {}))
})
.chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))))
}
}
}

6
src/ship/room.rs

@ -1,6 +1,8 @@
use std::collections::HashMap;
use std::convert::{From, Into, TryFrom, TryInto};
use thiserror::Error;
use rand::Rng;
use crate::ship::map::Maps;
use crate::ship::drops::DropTable;
@ -9,13 +11,13 @@ use crate::ship::monster::{load_monster_stats_table, MonsterType, MonsterStats};
use crate::ship::map::area::MapAreaLookup;
use crate::ship::map::enemy::RareMonsterAppearTable;
#[derive(Debug)]
#[derive(Debug, Error)]
#[error("")]
pub enum RoomCreationError {
InvalidMode,
InvalidEpisode(u8),
InvalidDifficulty(u8),
CouldNotLoadMonsterStats(RoomMode),
}
#[derive(Debug, Copy, Clone, derive_more::Display)]

46
src/ship/ship.rs

@ -33,6 +33,7 @@ use crate::ship::quests;
use crate::ship::map::{MapsError, MapAreaError, MapArea};
use crate::ship::packet::handler;
use crate::ship::shops::{WeaponShop, ToolShop, ArmorShop, WeaponShopItem, ToolShopItem, ArmorShopItem};
use crate::ship::trade::TradeState;
pub const SHIP_PORT: u16 = 23423;
pub const QUEST_CATEGORY_MENU_ID: u32 = 0xA2;
@ -41,11 +42,12 @@ pub type Rooms = [Option<room::RoomState>; MAX_ROOMS];
pub type Clients = HashMap<ClientId, ClientState>;
#[derive(Error, Debug)]
#[error("")]
#[error("shiperror {0:?}")]
pub enum ShipError {
ClientNotFound(ClientId),
NoCharacterInSlot(ClientId, u32),
InvalidSlot(ClientId, u32),
#[error("")]
TooManyClients,
ClientLocationError(#[from] ClientLocationError),
GetNeighborError(#[from] GetNeighborError),
@ -56,10 +58,12 @@ pub enum ShipError {
InvalidRoom(u32),
MonsterAlreadyDroppedItem(ClientId, u16),
SliceError(#[from] std::array::TryFromSliceError),
#[error("")]
ItemError, // TODO: refine this
PickUpInvalidItemId(u32),
DropInvalidItemId(u32),
ItemManagerError(#[from] items::ItemManagerError),
#[error("")]
ItemDropLocationNotSet,
BoxAlreadyDroppedItem(ClientId, u16),
InvalidQuestCategory(u32),
@ -67,12 +71,15 @@ pub enum ShipError {
InvalidQuestFilename(String),
IoError(#[from] std::io::Error),
NotEnoughMeseta(ClientId, u32),
#[error("")]
ShopError,
GatewayError(#[from] GatewayError),
UnknownMonster(crate::ship::monster::MonsterType),
InvalidShip(usize),
InvalidBlock(usize),
InvalidItem(items::ClientItemId),
#[error("tradeerror {0}")]
TradeError(#[from] crate::ship::packet::handler::trade::TradeError),
}
#[derive(Debug)]
@ -106,6 +113,8 @@ pub enum RecvShipPacket {
SaveOptions(SaveOptions),
RequestShipList(RequestShipList),
RequestShipBlockList(RequestShipBlockList),
ItemsToTrade(ItemsToTrade),
TradeConfirmed(TradeConfirmed),
}
impl RecvServerPacket for RecvShipPacket {
@ -143,6 +152,8 @@ impl RecvServerPacket for RecvShipPacket {
0xA1 => Ok(RecvShipPacket::RequestShipBlockList(RequestShipBlockList::from_bytes(data)?)),
0xA2 => Ok(RecvShipPacket::RequestQuestList(RequestQuestList::from_bytes(data)?)),
0xAC => Ok(RecvShipPacket::DoneLoadingQuest(DoneLoadingQuest::from_bytes(data)?)),
0xD0 => Ok(RecvShipPacket::ItemsToTrade(ItemsToTrade::from_bytes(data)?)),
0xD2 => Ok(RecvShipPacket::TradeConfirmed(TradeConfirmed::from_bytes(data)?)),
0xE7 => Ok(RecvShipPacket::FullCharacterData(Box::new(FullCharacterData::from_bytes(data)?))),
0x1ED => Ok(RecvShipPacket::SaveOptions(SaveOptions::from_bytes(data)?)),
_ => Err(PacketParseError::WrongPacketForServerType(u16::from_le_bytes([data[2], data[3]]), data.to_vec()))
@ -185,6 +196,9 @@ pub enum SendShipPacket {
BankItemList(BankItemList),
RedirectClient(RedirectClient),
RareMonsterList(RareMonsterList),
AcknowledgeTrade(AcknowledgeTrade),
CancelTrade(CancelTrade),
TradeSuccessful(TradeSuccessful),
}
impl SendServerPacket for SendShipPacket {
@ -223,6 +237,9 @@ impl SendServerPacket for SendShipPacket {
SendShipPacket::BankItemList(pkt) => pkt.as_bytes(),
SendShipPacket::RedirectClient(pkt) => pkt.as_bytes(),
SendShipPacket::RareMonsterList(pkt) => pkt.as_bytes(),
SendShipPacket::AcknowledgeTrade(pkt) => pkt.as_bytes(),
SendShipPacket::CancelTrade(pkt) => pkt.as_bytes(),
SendShipPacket::TradeSuccessful(pkt) => pkt.as_bytes(),
}
}
}
@ -241,6 +258,12 @@ pub struct LoadingQuest {
//pub quest_chunk_bin: Option<Box<dyn Iterator<Item = >>>,
}
pub struct ClientState {
pub user: UserAccountEntity,
pub settings: UserSettingsEntity,
@ -381,6 +404,7 @@ impl<EG: EntityGateway> ShipServerStateBuilder<EG> {
auth_token: self.auth_token.unwrap_or_else(|| AuthToken("".into())),
ship_list: Vec::new(),
shipgate_sender: None,
trades: Default::default(),
}
}
}
@ -426,6 +450,7 @@ pub struct ShipServerState<EG: EntityGateway> {
auth_token: AuthToken,
ship_list: Vec<Ship>,
shipgate_sender: Option<Box<dyn Fn(ShipMessage) + Send + Sync>>,
trades: TradeState,
}
impl<EG: EntityGateway> ShipServerState<EG> {
@ -463,14 +488,14 @@ impl<EG: EntityGateway> ShipServerState<EG> {
handler::message::update_player_position(id, msg, &mut self.clients, &block.client_location, &block.rooms)?
},
GameMessage::ChargeAttack(charge_attack) => {
handler::message::charge_attack(id, charge_attack, &mut self.clients, &mut self.entity_gateway).await?
handler::message::charge_attack(id, charge_attack, &mut self.clients, &mut self.entity_gateway, &mut self.item_manager).await?
},
GameMessage::PlayerUseItem(player_use_item) => {
let block = self.blocks.with_client(id, &self.clients)?;
handler::message::use_item(id, player_use_item, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await?
},
GameMessage::PlayerUsedMedicalCenter(player_used_medical_center) => {
handler::message::player_used_medical_center(id, player_used_medical_center, &mut self.entity_gateway, &mut self.clients).await?
handler::message::player_used_medical_center(id, player_used_medical_center, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await?
},
GameMessage::PlayerFeedMag(player_feed_mag) => {
let block = self.blocks.with_client(id, &self.clients)?;
@ -530,6 +555,9 @@ impl<EG: EntityGateway> ShipServerState<EG> {
GameMessage::TekAccept(tek_accept) => {
handler::direct_message::accept_tek_item(id, tek_accept, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await?
},
GameMessage::TradeRequest(trade_request) => {
handler::trade::trade_request(id, trade_request, target, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await?
},
_ => {
let cmsg = msg.clone();
Box::new(block.client_location.get_all_clients_by_client(id).unwrap().into_iter()
@ -632,7 +660,7 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
},
RecvShipPacket::CreateRoom(create_room) => {
let block = self.blocks.with_client(id, &self.clients)?;
handler::room::create_room(id, create_room, &mut block.client_location, &mut self.clients, &mut self.item_manager, &mut block.rooms)?
handler::room::create_room(id, create_room, &mut block.client_location, &mut self.clients, &mut self.item_manager, &self.level_table, &mut block.rooms)?
},
RecvShipPacket::RoomNameRequest(_req) => {
let block = self.blocks.with_client(id, &self.clients)?;
@ -696,7 +724,15 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
},
RecvShipPacket::RequestShipBlockList(_) => {
handler::ship::block_list(id, &self.name, self.blocks.0.len())
}
},
RecvShipPacket::ItemsToTrade(items_to_trade) => {
let block = self.blocks.with_client(id, &self.clients)?;
handler::trade::items_to_trade(id, items_to_trade, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await?
},
RecvShipPacket::TradeConfirmed(_) => {
let block = self.blocks.with_client(id, &self.clients)?;
handler::trade::trade_confirmed(id, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await?
},
})
}

10
src/ship/shops/weapon.rs

@ -315,7 +315,7 @@ fn number_of_weapons_to_generate(character_level: usize) -> usize {
#[derive(Debug)]
pub struct WeaponShop<R: Rng + SeedableRng> {
difficulty: Difficulty,
_difficulty: Difficulty,
section_id: SectionID,
weapon: WeaponTable,
special: SpecialTable,
@ -329,7 +329,7 @@ pub struct WeaponShop<R: Rng + SeedableRng> {
impl<R: Rng + SeedableRng> WeaponShop<R> {
pub fn new(difficulty: Difficulty, section_id: SectionID) -> WeaponShop<R> {
WeaponShop {
difficulty,
_difficulty: difficulty,
section_id,
weapon: load_weapon_table(difficulty, section_id),
special: load_special_table(),
@ -358,8 +358,10 @@ impl<R: Rng + SeedableRng> WeaponShop<R> {
.last()
.unwrap();
let special_tier = WeightedIndex::new(tier.special.iter().map(|t| t.probability)).unwrap();
match special_tier.sample(&mut self.rng) {
let special_tier_choice = WeightedIndex::new(tier.special.iter().map(|t| t.probability)).unwrap();
let special_tier_index = special_tier_choice.sample(&mut self.rng);
let special_tier = tier.special.get(special_tier_index)?;
match special_tier.tier {
1 => TIER1_SPECIAL.choose(&mut self.rng).cloned(),
2 => TIER2_SPECIAL.choose(&mut self.rng).cloned(),
_ => None

133
src/ship/trade.rs

@ -0,0 +1,133 @@
use std::collections::HashMap;
use std::cell::RefCell;
use crate::common::serverstate::ClientId;
use crate::ship::items;
#[derive(Debug, Clone)]
pub enum TradeItem {
Individual(items::ClientItemId),
Stacked(items::ClientItemId, usize),
}
impl TradeItem {
pub fn stacked(&self) -> Option<(items::ClientItemId, usize)> {
match self {
TradeItem::Stacked(item_id, amount) => Some((*item_id, *amount)),
_ => None
}
}
pub fn stacked_mut(&mut self) -> Option<(items::ClientItemId, &mut usize)> {
match self {
TradeItem::Stacked(item_id, ref mut amount) => Some((*item_id, amount)),
_ => None
}
}
pub fn item_id(&self) -> items::ClientItemId {
match self {
TradeItem::Individual(item_id) => *item_id,
TradeItem::Stacked(item_id, _) => *item_id,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TradeStatus {
SentRequest,
ReceivedRequest,
Trading,
Confirmed,
FinalConfirm,
ItemsChecked,
TradeComplete,
}
#[derive(Debug, Clone)]
pub struct ClientTradeState {
client: ClientId,
other_client: ClientId,
pub items: Vec<TradeItem>,
pub meseta: usize,
pub status: TradeStatus,
}
impl ClientTradeState {
pub fn client(&self) -> ClientId {
self.client
}
pub fn other_client(&self) -> ClientId {
self.other_client
}
}
#[derive(thiserror::Error, Debug)]
#[error("")]
pub enum TradeStateError {
ClientNotInTrade(ClientId),
MismatchedTrade(ClientId, ClientId),
}
#[derive(Default, Debug)]
pub struct TradeState {
trades: HashMap<ClientId, RefCell<ClientTradeState>>,
}
impl TradeState {
pub fn new_trade(&mut self, sender: &ClientId, receiver: &ClientId) {
let state = ClientTradeState {
client: *sender,
other_client: *receiver,
items: Default::default(),
meseta: 0,
status: TradeStatus::SentRequest,
};
self.trades.insert(*sender, RefCell::new(state));
let state = ClientTradeState {
client: *receiver,
other_client: *sender,
items: Default::default(),
meseta: 0,
status: TradeStatus::ReceivedRequest,
};
self.trades.insert(*receiver, RefCell::new(state));
}
pub fn in_trade(&self, client: &ClientId) -> bool {
self.trades.contains_key(client)
}
pub fn with<T, F> (&self, client: &ClientId, func: F) -> Result<T, TradeStateError>
where
F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> T
{
let mut c1 = self.trades.get(client).ok_or_else(|| TradeStateError::ClientNotInTrade(*client))?.borrow_mut();
let mut c2 = self.trades.get(&c1.other_client).ok_or(TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut();
// sanity check
if c1.client != c2.other_client {
return Err(TradeStateError::MismatchedTrade(c1.client, c2.client));
}
Ok(func(&mut *c1, &mut *c2))
}
// TODO: is it possible for this to not return Options?
pub fn remove_trade(&mut self, client: &ClientId) -> (Option<ClientTradeState>, Option<ClientTradeState>) {
let c1 = self.trades.remove(client).map(|c| c.into_inner());
let c2 = if let Some(ref state) = c1 {
self.trades.remove(&state.other_client).map(|c| c.into_inner())
}
else {
None
};
(c1, c2)
}
}

3
tests/common.rs

@ -4,6 +4,7 @@ use elseware::common::serverstate::{ClientId, ServerState};
use elseware::entity::gateway::EntityGateway;
use elseware::entity::account::{UserAccountEntity, NewUserAccountEntity, NewUserSettingsEntity};
use elseware::entity::character::{CharacterEntity, NewCharacterEntity};
use elseware::entity::item::{Meseta, BankName};
use elseware::ship::ship::{ShipServerState, RecvShipPacket};
use elseware::ship::room::Difficulty;
@ -27,6 +28,8 @@ pub async fn new_user_character<EG: EntityGateway>(entity_gateway: &mut EG, user
let _settings = entity_gateway.create_user_settings(new_settings).await.unwrap();
let new_character = NewCharacterEntity::new(user.id);
let character = entity_gateway.create_character(new_character).await.unwrap();
entity_gateway.set_character_meseta(&character.id, Meseta(0)).await.unwrap();
entity_gateway.set_bank_meseta(&character.id, BankName("".into()), Meseta(0)).await.unwrap();
(user, character)
}

212
tests/test_bank.rs

@ -29,10 +29,6 @@ async fn test_bank_items_sent_in_character_login() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap();
entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(vec![item]), item::BankName("".into())).await.unwrap();
@ -70,10 +66,6 @@ async fn test_request_bank_items() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
@ -118,10 +110,6 @@ async fn test_request_stacked_bank_items() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
@ -167,10 +155,6 @@ async fn test_request_bank_items_sorted() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap();
let monomate = entity_gateway.create_item(
item::NewItemEntity {
@ -179,10 +163,6 @@ async fn test_request_bank_items_sorted() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap();
let item2 = entity_gateway.create_item(
item::NewItemEntity {
@ -195,10 +175,6 @@ async fn test_request_bank_items_sorted() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap();
let bank = vec![item::BankItemEntity::Individual(item1), vec![monomate].into(), item2.into()];
@ -245,9 +221,6 @@ async fn test_deposit_individual_item() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap();
let item1 = entity_gateway.create_item(
item::NewItemEntity {
@ -260,9 +233,6 @@ async fn test_deposit_individual_item() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap();
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![item0, item1])).await.unwrap();
@ -322,9 +292,6 @@ async fn test_deposit_stacked_item() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -386,9 +353,6 @@ async fn test_deposit_partial_stacked_item() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -460,9 +424,6 @@ async fn test_deposit_stacked_item_with_stack_already_in_bank() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
bank_monomates.push(entity_gateway.create_item(
@ -472,10 +433,6 @@ async fn test_deposit_stacked_item_with_stack_already_in_bank() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".into()),
}
}).await.unwrap());
}
@ -537,9 +494,6 @@ async fn test_deposit_stacked_item_with_full_stack_in_bank() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -552,10 +506,6 @@ async fn test_deposit_stacked_item_with_full_stack_in_bank() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".into()),
}
}).await.unwrap());
}
@ -619,9 +569,6 @@ async fn test_deposit_individual_item_in_full_bank() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
let mut bank = Vec::new();
@ -637,10 +584,6 @@ async fn test_deposit_individual_item_in_full_bank() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
@ -697,9 +640,6 @@ async fn test_deposit_stacked_item_in_full_bank() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -716,10 +656,6 @@ async fn test_deposit_stacked_item_in_full_bank() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
@ -777,9 +713,6 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -792,10 +725,6 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
@ -812,10 +741,6 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap().into());
}
almost_full_bank.push(bank_monomates.into());
@ -860,9 +785,8 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() {
async fn test_deposit_meseta() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.meseta = 300;
entity_gateway.save_character(&char1).await.unwrap();
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -887,20 +811,19 @@ async fn test_deposit_meseta() {
unknown: 0,
})))).await.unwrap().for_each(drop);
let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let char = characters[0].as_ref().unwrap();
assert!(char.meseta == 277);
assert!(char.bank_meseta == 23);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap();
assert!(c1_meseta.0 == 277);
assert!(c1_bank_meseta.0 == 23);
}
#[async_std::test]
async fn test_deposit_too_much_meseta() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.meseta = 300;
char1.bank_meseta = 999980;
entity_gateway.save_character(&char1).await.unwrap();
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap();
entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(999980)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -925,21 +848,19 @@ async fn test_deposit_too_much_meseta() {
unknown: 0,
})))).await.unwrap().for_each(drop);
let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let char = characters[0].as_ref().unwrap();
assert!(char.meseta == 300);
assert!(char.bank_meseta == 999980);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap();
assert!(c1_meseta.0 == 300);
assert!(c1_bank_meseta.0 == 999980);
}
#[async_std::test]
async fn test_deposit_meseta_when_bank_is_maxed() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.meseta = 300;
char1.bank_meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap();
entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -964,10 +885,10 @@ async fn test_deposit_meseta_when_bank_is_maxed() {
unknown: 0,
})))).await.unwrap().for_each(drop);
let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let char = characters[0].as_ref().unwrap();
assert!(char.meseta == 300);
assert!(char.bank_meseta == 999999);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap();
assert!(c1_meseta.0 == 300);
assert!(c1_bank_meseta.0 == 999999);
}
@ -990,10 +911,6 @@ async fn test_withdraw_individual_item() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(bank), item::BankName("".into())).await.unwrap();
@ -1053,10 +970,6 @@ async fn test_withdraw_stacked_item() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
@ -1117,10 +1030,6 @@ async fn test_withdraw_partial_stacked_item() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".into())
}
}).await.unwrap());
}
entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(vec![monomates]), item::BankName("".into())).await.unwrap();
@ -1188,9 +1097,6 @@ async fn test_withdraw_stacked_item_with_stack_already_in_inventory() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
bank_monomates.push(entity_gateway.create_item(
@ -1200,10 +1106,6 @@ async fn test_withdraw_stacked_item_with_stack_already_in_inventory() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".into()),
}
}).await.unwrap());
}
@ -1267,10 +1169,6 @@ async fn test_withdraw_stacked_item_with_full_stack_in_inventory() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".into()),
}
}).await.unwrap());
}
@ -1283,9 +1181,6 @@ async fn test_withdraw_stacked_item_with_full_stack_in_inventory() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -1349,10 +1244,6 @@ async fn test_withdraw_individual_item_in_full_inventory() {
tekked: true,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
let mut inventory = Vec::new();
@ -1368,9 +1259,6 @@ async fn test_withdraw_individual_item_in_full_inventory() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -1423,10 +1311,6 @@ async fn test_withdraw_stacked_item_in_full_inventory() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
@ -1443,9 +1327,6 @@ async fn test_withdraw_stacked_item_in_full_inventory() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -1504,10 +1385,6 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Bank {
character_id: char1.id,
name: item::BankName("".to_string())
}
}).await.unwrap());
}
entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(vec![bank_item]), item::BankName("".into())).await.unwrap();
@ -1525,9 +1402,6 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap().into());
}
@ -1540,9 +1414,6 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
items.push(item::InventoryItemEntity::Stacked(item29));
@ -1589,9 +1460,8 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() {
async fn test_withdraw_meseta() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.bank_meseta = 300;
entity_gateway.save_character(&char1).await.unwrap();
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -1616,20 +1486,19 @@ async fn test_withdraw_meseta() {
unknown: 0,
})))).await.unwrap().for_each(drop);
let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let char = characters[0].as_ref().unwrap();
assert!(char.meseta == 23);
assert!(char.bank_meseta == 277);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap();
assert!(c1_meseta.0 == 23);
assert!(c1_bank_meseta.0 == 277);
}
#[async_std::test]
async fn test_withdraw_too_much_meseta() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.meseta = 999980;
char1.bank_meseta = 300;
entity_gateway.save_character(&char1).await.unwrap();
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999980)).await.unwrap();
entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -1654,20 +1523,19 @@ async fn test_withdraw_too_much_meseta() {
unknown: 0,
})))).await.unwrap().for_each(drop);
let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let char = characters[0].as_ref().unwrap();
assert!(char.meseta == 999980);
assert!(char.bank_meseta == 300);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap();
assert!(c1_meseta.0 == 999980);
assert!(c1_bank_meseta.0 == 300);
}
#[async_std::test]
async fn test_withdraw_meseta_inventory_is_maxed() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.meseta = 999999;
char1.bank_meseta = 300;
entity_gateway.save_character(&char1).await.unwrap();
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -1692,8 +1560,8 @@ async fn test_withdraw_meseta_inventory_is_maxed() {
unknown: 0,
})))).await.unwrap().for_each(drop);
let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let char = characters[0].as_ref().unwrap();
assert!(char.meseta == 999999);
assert!(char.bank_meseta == 300);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap();
assert!(c1_meseta.0 == 999999);
assert!(c1_bank_meseta.0 == 300);
}

27
tests/test_item_actions.rs

@ -26,9 +26,6 @@ async fn test_equip_unit_from_equip_menu() {
evp: 0,
slots: 4,
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
p1_inv.push(entity_gateway.create_item(
@ -38,9 +35,6 @@ async fn test_equip_unit_from_equip_menu() {
unit: item::unit::UnitType::KnightPower,
modifier: None,
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
p1_inv.push(entity_gateway.create_item(
@ -50,9 +44,6 @@ async fn test_equip_unit_from_equip_menu() {
unit: item::unit::UnitType::KnightPower,
modifier: Some(item::unit::UnitModifier::Plus),
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
let equipped = item::EquippedEntity {
@ -112,9 +103,6 @@ async fn test_unequip_armor_with_units() {
evp: 0,
slots: 4,
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
p1_inv.push(entity_gateway.create_item(
@ -124,9 +112,6 @@ async fn test_unequip_armor_with_units() {
unit: item::unit::UnitType::KnightPower,
modifier: None,
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
p1_inv.push(entity_gateway.create_item(
@ -136,9 +121,6 @@ async fn test_unequip_armor_with_units() {
unit: item::unit::UnitType::KnightPower,
modifier: Some(item::unit::UnitModifier::Plus),
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
let equipped = item::EquippedEntity {
@ -189,9 +171,6 @@ async fn test_sort_items() {
evp: 0,
slots: 4,
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
p1_inv.push(entity_gateway.create_item(
@ -201,9 +180,6 @@ async fn test_sort_items() {
unit: item::unit::UnitType::KnightPower,
modifier: None,
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
p1_inv.push(entity_gateway.create_item(
@ -213,9 +189,6 @@ async fn test_sort_items() {
unit: item::unit::UnitType::KnightPower,
modifier: Some(item::unit::UnitModifier::Plus),
}),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap();

190
tests/test_item_pickup.rs

@ -10,6 +10,77 @@ use libpso::packet::messages::*;
mod common;
use common::*;
#[async_std::test]
async fn test_pick_up_individual_item() {
let mut entity_gateway = InMemoryGateway::default();
let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
let mut p1_inv = Vec::new();
p1_inv.push(entity_gateway.create_item(
item::NewItemEntity {
item: item::ItemDetail::Weapon(
item::weapon::Weapon {
weapon: item::weapon::WeaponType::Handgun,
grind: 0,
special: None,
attrs: [None, None, None],
tekked: true,
}
),
}).await.unwrap());
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap();
entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::<item::InventoryItemEntity>::new())).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
.build());
log_in_char(&mut ship, ClientId(1), "a1", "a").await;
log_in_char(&mut ship, ClientId(2), "a2", "a").await;
join_lobby(&mut ship, ClientId(1)).await;
join_lobby(&mut ship, ClientId(2)).await;
create_room(&mut ship, ClientId(1), "room", "").await;
join_room(&mut ship, ClientId(2), 0).await;
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 1);
let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap();
assert_eq!(p2_items.items.len(), 0);
ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem {
client: 0,
target: 0,
unknown1: 0,
map_area: 0,
item_id: 0x10000,
x: 0.0,
y: 0.0,
z: 0.0,
})))).await.unwrap().for_each(drop);
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 0);
let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap();
assert_eq!(p2_items.items.len(), 0);
ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem {
client: 0,
target: 0,
item_id: 0x10000,
map_area: 0,
unknown: [0; 3]
})))).await.unwrap().for_each(drop);
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 0);
let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap();
assert_eq!(p2_items.items.len(), 1);
}
#[async_std::test]
async fn test_pick_up_item_stack_of_items_already_in_inventory() {
let mut entity_gateway = InMemoryGateway::default();
@ -25,9 +96,6 @@ async fn test_pick_up_item_stack_of_items_already_in_inventory() {
tool: item::tool::ToolType::Monomate
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
let mut p2_items = Vec::new();
@ -41,9 +109,6 @@ async fn test_pick_up_item_stack_of_items_already_in_inventory() {
tool: tool
}
),
location: item::ItemLocation::Inventory {
character_id: char2.id,
}
}).await.unwrap());
}
p2_items.push(item);
@ -107,9 +172,6 @@ async fn test_pick_up_item_stack_of_items_not_already_held() {
tool: item::tool::ToolType::Monomate
}
),
location: item::ItemLocation::Inventory {
character_id: char2.id,
}
}).await.unwrap());
entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_monomate])).await.unwrap();
@ -159,7 +221,7 @@ async fn test_pick_up_meseta_when_inventory_full() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
let (user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
let mut p1_items = Vec::new();
for _ in 0..30usize {
@ -174,16 +236,11 @@ async fn test_pick_up_meseta_when_inventory_full() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_items)).await.unwrap();
char2.meseta = 300;
entity_gateway.save_character(&char2).await.unwrap();
entity_gateway.set_character_meseta(&char2.id, item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -225,12 +282,10 @@ async fn test_pick_up_meseta_when_inventory_full() {
let inventory_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(inventory_items.items.len(), 30);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
let characters2 = entity_gateway.get_characters_by_user(&user2).await.unwrap();
let c2 = characters2.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta == 23);
assert!(c2.meseta == 277);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap();
assert!(c1_meseta.0 == 23);
assert!(c2_meseta.0 == 277);
}
#[async_std::test]
@ -253,9 +308,6 @@ async fn test_pick_up_partial_stacked_item_when_inventory_is_otherwise_full() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap().into());
}
@ -266,9 +318,6 @@ async fn test_pick_up_partial_stacked_item_when_inventory_is_otherwise_full() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap()]));
let mut p2_monomates = Vec::new();
@ -279,9 +328,6 @@ async fn test_pick_up_partial_stacked_item_when_inventory_is_otherwise_full() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char2.id,
}
}).await.unwrap());
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap();
@ -345,9 +391,6 @@ async fn test_can_not_pick_up_item_when_inventory_full() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -363,9 +406,6 @@ async fn test_can_not_pick_up_item_when_inventory_full() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char2.id,
}
}).await.unwrap());
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap();
@ -425,10 +465,9 @@ async fn test_can_not_pick_up_item_when_inventory_full() {
async fn test_can_not_drop_more_meseta_than_is_held() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.meseta = 300;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -457,9 +496,8 @@ async fn test_can_not_drop_more_meseta_than_is_held() {
})))).await;
assert!(split_attempt.is_err());
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta == 300);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
assert!(c1_meseta.0 == 300);
}
#[async_std::test]
@ -478,9 +516,6 @@ async fn test_pick_up_stack_that_would_exceed_stack_limit() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -493,9 +528,6 @@ async fn test_pick_up_stack_that_would_exceed_stack_limit() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char2.id,
}
}).await.unwrap());
}
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_monomates])).await.unwrap();
@ -546,13 +578,11 @@ async fn test_pick_up_stack_that_would_exceed_stack_limit() {
async fn test_can_not_pick_up_meseta_when_full() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
char2.meseta = 300;
entity_gateway.save_character(&char2).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
entity_gateway.set_character_meseta(&char2.id, item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -592,25 +622,21 @@ async fn test_can_not_pick_up_meseta_when_full() {
})))).await.unwrap().collect::<Vec<_>>();
assert!(packets.len() == 0);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
let characters2 = entity_gateway.get_characters_by_user(&user2).await.unwrap();
let c2 = characters2.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta == 999999);
assert!(c2.meseta == 277);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap();
assert!(c1_meseta.0 == 999999);
assert!(c2_meseta.0 == 277);
}
#[async_std::test]
async fn test_meseta_caps_at_999999_when_trying_to_pick_up_more() {
let mut entity_gateway = InMemoryGateway::default();
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
char1.meseta = 999998;
entity_gateway.save_character(&char1).await.unwrap();
char2.meseta = 300;
entity_gateway.save_character(&char2).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999998)).await.unwrap();
entity_gateway.set_character_meseta(&char2.id, item::Meseta(300)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -649,12 +675,10 @@ async fn test_meseta_caps_at_999999_when_trying_to_pick_up_more() {
unknown: [0; 3]
})))).await.unwrap().for_each(drop);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
let characters2 = entity_gateway.get_characters_by_user(&user2).await.unwrap();
let c2 = characters2.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta == 999999);
assert!(c2.meseta == 277);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap();
assert!(c1_meseta.0 == 999999);
assert!(c2_meseta.0 == 277);
}
#[async_std::test]
@ -673,9 +697,6 @@ async fn test_player_drops_partial_stack_and_other_player_picks_it_up() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -732,3 +753,20 @@ async fn test_player_drops_partial_stack_and_other_player_picks_it_up() {
vec![item::ItemEntityId(1), item::ItemEntityId(2)]);
}).unwrap();
}
/*
#[async_std::test]
async fn test_try_and_pick_up_individual_item_twice() {
panic!()
}
#[async_std::test]
async fn test_try_and_pick_up_stacked_item_twice() {
panic!()
}
#[async_std::test]
async fn test_try_and_pick_up_meseta_twice() {
panic!()
}
*/

15
tests/test_item_use.rs

@ -29,9 +29,6 @@ async fn test_use_monomate() {
tool: tool
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
p1_items.push(item::InventoryItemEntity::Stacked(item));
@ -79,9 +76,6 @@ async fn test_use_monomate_twice() {
tool: tool
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
p1_items.push(item::InventoryItemEntity::Stacked(item));
@ -132,9 +126,6 @@ async fn test_use_last_monomate() {
tool: tool
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap()]));
}
@ -176,9 +167,6 @@ async fn test_use_nonstackable_tool() {
tool: item::tool::ToolType::MagicStoneIritista,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_items)).await.unwrap();
@ -217,9 +205,6 @@ async fn test_use_materials() {
tool: tool
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
p1_inv.push(item::InventoryItemEntity::Stacked(item));

19
tests/test_mags.rs

@ -22,10 +22,6 @@ async fn test_mag_feed() {
item: item::ItemDetail::Mag(
item::mag::Mag::baby_mag(0)
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
//equipped: true,
}
}).await.unwrap();
let mut monomates = Vec::new();
@ -37,9 +33,6 @@ async fn test_mag_feed() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -108,9 +101,6 @@ async fn test_mag_change_owner() {
item: item::ItemDetail::Mag(
item::mag::Mag::baby_mag(0)
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap();
entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![mag])).await.unwrap();
@ -169,9 +159,6 @@ async fn test_mag_cell() {
item: item::ItemDetail::Mag(
item::mag::Mag::baby_mag(0)
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap();
for _ in 0..1000usize {
@ -182,9 +169,6 @@ async fn test_mag_cell() {
tool: item::tool::ToolType::Monomate,
}
),
location: item::ItemLocation::FedToMag {
mag: mag.id,
}
}).await.unwrap();
entity_gateway.feed_mag(&mag.id, &fed_tool.id).await.unwrap();
}
@ -195,9 +179,6 @@ async fn test_mag_cell() {
tool: item::tool::ToolType::CellOfMag502,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap();
let equipped = item::EquippedEntity {

6
tests/test_rooms.rs

@ -31,9 +31,6 @@ async fn test_item_ids_reset_when_rejoining_rooms() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap());
}
@ -50,9 +47,6 @@ async fn test_item_ids_reset_when_rejoining_rooms() {
tekked: true,
}
),
location: item::ItemLocation::Inventory {
character_id: char2.id,
}
}).await.unwrap());
}

57
tests/test_shops.rs

@ -107,8 +107,8 @@ async fn test_player_buys_from_weapon_shop() {
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -132,9 +132,8 @@ async fn test_player_buys_from_weapon_shop() {
unknown1: 0,
})))).await.unwrap().for_each(drop);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta < 999999);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
assert!(c1_meseta.0 < 999999);
//let p1_items = entity_gateway.get_items_by_character(&char1.id).await.unwrap();
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 1);
@ -146,8 +145,8 @@ async fn test_player_buys_from_tool_shop() {
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -171,9 +170,8 @@ async fn test_player_buys_from_tool_shop() {
unknown1: 0,
})))).await.unwrap().for_each(drop);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta < 999999);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
assert!(c1_meseta.0 < 999999);
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 1);
}
@ -184,8 +182,8 @@ async fn test_player_buys_multiple_from_tool_shop() {
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -209,9 +207,8 @@ async fn test_player_buys_multiple_from_tool_shop() {
unknown1: 0,
})))).await.unwrap().for_each(drop);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta < 999999);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
assert!(c1_meseta.0 < 999999);
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 1);
p1_items.items[0].with_stacked(|item| {
@ -226,8 +223,8 @@ async fn test_player_buys_from_armor_shop() {
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -251,9 +248,8 @@ async fn test_player_buys_from_armor_shop() {
unknown1: 0,
})))).await.unwrap().for_each(drop);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta < 999999);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
assert!(c1_meseta.0 < 999999);
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 1);
}
@ -269,7 +265,7 @@ async fn test_other_clients_see_purchase() {
let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
entity_gateway.save_character(&char1).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
@ -279,7 +275,7 @@ async fn test_other_clients_see_purchase() {
log_in_char(&mut ship, ClientId(2), "a2", "a").await;
join_lobby(&mut ship, ClientId(1)).await;
join_lobby(&mut ship, ClientId(2)).await;
create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await;
create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Normal).await;
join_room(&mut ship, ClientId(2), 0).await;
ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest {
@ -312,8 +308,8 @@ async fn test_other_clients_see_stacked_purchase() {
let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
entity_gateway.create_item(
item::NewItemEntity {
item: item::ItemDetail::Tool(
@ -321,9 +317,6 @@ async fn test_other_clients_see_stacked_purchase() {
tool: item::tool::ToolType::Monomate
}
),
location: item::ItemLocation::Inventory {
character_id: char1.id,
}
}).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
@ -333,7 +326,7 @@ async fn test_other_clients_see_stacked_purchase() {
log_in_char(&mut ship, ClientId(2), "a2", "a").await;
join_lobby(&mut ship, ClientId(1)).await;
join_lobby(&mut ship, ClientId(2)).await;
create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await;
create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Normal).await;
join_room(&mut ship, ClientId(2), 0).await;
ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest {
@ -370,7 +363,7 @@ async fn test_buying_item_without_enough_mseseta() {
.build());
log_in_char(&mut ship, ClientId(1), "a1", "a").await;
join_lobby(&mut ship, ClientId(1)).await;
create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await;
create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Normal).await;
ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest {
client: 255,
@ -388,9 +381,8 @@ async fn test_buying_item_without_enough_mseseta() {
})))).await;
assert!(packets.is_err());
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
assert_eq!(c1.meseta, 0);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
assert_eq!(c1_meseta.0, 0);
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 0);
}
@ -401,8 +393,8 @@ async fn test_player_double_buys_from_tool_shop() {
let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -444,9 +436,8 @@ async fn test_player_double_buys_from_tool_shop() {
unknown1: 0,
})))).await.unwrap().for_each(drop);
let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap();
let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap();
assert!(c1.meseta < 999999);
let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap();
assert!(c1_meseta.0 < 999999);
let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap();
assert_eq!(p1_items.items.len(), 2);
p1_items.items[0].with_stacked(|item| {
@ -467,8 +458,8 @@ async fn test_techs_disappear_from_shop_when_bought() {
let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())
@ -529,8 +520,8 @@ async fn test_units_disappear_from_shop_when_bought() {
let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
char1.exp = 80000000;
char1.meseta = 999999;
entity_gateway.save_character(&char1).await.unwrap();
entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap();
let mut ship = Box::new(ShipServerState::builder()
.gateway(entity_gateway.clone())

4382
tests/test_trade.rs
File diff suppressed because it is too large
View File

Loading…
Cancel
Save