diff --git a/Cargo.toml b/Cargo.toml index 0f86474..577f89f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,19 +4,6 @@ version = "0.1.0" authors = ["Jake Probst "] edition = "2018" -[[bin]] -name = "everything" -path = "src/main.rs" - -#[[bin]] -#name = "patch" -#path = "src/patch_main.rs" - -#[[bin]] -#name = "login" -#path = "src/login_main.rs" - - [dependencies] libpso = { git = "http://git.sharnoth.com/jake/libpso" } async-std = { version = "1.5.0", features = ["unstable"] } diff --git a/src/main.rs b/src/bin/main.rs similarity index 76% rename from src/main.rs rename to src/bin/main.rs index fc66329..a55a86c 100644 --- a/src/main.rs +++ b/src/bin/main.rs @@ -1,30 +1,16 @@ -#![allow(incomplete_features)] -#![feature(const_generics)] -#![feature(maybe_uninit_extra)] -#![feature(const_in_array_repeat_expressions)] -#![feature(drain_filter)] - - - -mod common; -mod entity; -mod patch; -mod login; -mod ship; - use std::time::SystemTime; use log::{info}; -use patch::patch::{PatchServerState, generate_patch_tree, load_config, load_motd}; -use login::login::LoginServerState; -use login::character::CharacterServerState; -use ship::ship::ShipServerState; -use entity::account::{NewUserAccountEntity, NewUserSettingsEntity}; -use entity::gateway::{EntityGateway, InMemoryGateway}; -use entity::character::NewCharacterEntity; -use entity::item::{NewItemEntity, ItemDetail, ItemLocation}; +use elseware::patch::patch::{PatchServerState, generate_patch_tree, load_config, load_motd}; +use elseware::login::login::LoginServerState; +use elseware::login::character::CharacterServerState; +use elseware::ship::ship::ShipServerState; +use elseware::entity::account::{NewUserAccountEntity, NewUserSettingsEntity}; +use elseware::entity::gateway::{EntityGateway, InMemoryGateway}; +use elseware::entity::character::NewCharacterEntity; +use elseware::entity::item::{NewItemEntity, ItemDetail, ItemLocation}; -use crate::entity::item; +use elseware::entity::item; fn setup_logger() { let colors = fern::colors::ColoredLevelConfig::new() @@ -48,7 +34,7 @@ fn setup_logger() { .chain(std::io::stdout()); let fileout = fern::Dispatch::new() .level(log::LevelFilter::Trace) - .chain(fern::log_file(format!("elseware-{}.log", chrono::Local::now().format("%Y-%m-%d_%H:%M:%S"))).unwrap()); + .chain(fern::log_file(format!("log/elseware-{}.log", chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"))).unwrap()); fern::Dispatch::new() .chain(stdio) .chain(fileout) @@ -86,11 +72,11 @@ fn main() { NewItemEntity { item: ItemDetail::Weapon( item::weapon::Weapon { - weapon: item::weapon::WeaponType::Handgun, + weapon: item::weapon::WeaponType::Raygun, grind: 5, special: Some(item::weapon::WeaponSpecial::Hell), attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), - None, + Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), None,], tekked: true, } @@ -111,7 +97,7 @@ fn main() { let (patch_file_tree, patch_file_lookup) = generate_patch_tree(patch_config.path.as_str()); let patch_state = PatchServerState::new(patch_file_tree, patch_file_lookup, patch_motd); - crate::common::mainloop::mainloop_async(patch_state, patch_config.port).await; + elseware::common::mainloop::mainloop_async(patch_state, patch_config.port).await; }); let thread_entity_gateway = entity_gateway.clone(); @@ -119,7 +105,7 @@ fn main() { info!("[auth] starting server"); let auth_state = LoginServerState::new(thread_entity_gateway); - common::mainloop::mainloop_async(auth_state, login::login::LOGIN_PORT).await; + elseware::common::mainloop::mainloop_async(auth_state, elseware::login::login::LOGIN_PORT).await; }); let thread_entity_gateway = entity_gateway.clone(); @@ -127,14 +113,14 @@ fn main() { info!("[character] starting server"); let char_state = CharacterServerState::new(thread_entity_gateway); - common::mainloop::mainloop_async(char_state, login::character::CHARACTER_PORT).await; + elseware::common::mainloop::mainloop_async(char_state, elseware::login::character::CHARACTER_PORT).await; }); let thread_entity_gateway = entity_gateway.clone(); let ship = async_std::task::spawn(async { info!("[ship] starting server"); let ship_state = ShipServerState::new(thread_entity_gateway); - common::mainloop::mainloop_async(ship_state, ship::ship::SHIP_PORT).await; + elseware::common::mainloop::mainloop_async(ship_state, elseware::ship::ship::SHIP_PORT).await; }); futures::join!(patch, auth, character, ship); diff --git a/src/entity/account.rs b/src/entity/account.rs index b72cb56..4699c09 100644 --- a/src/entity/account.rs +++ b/src/entity/account.rs @@ -7,11 +7,11 @@ use libpso::character::guildcard; pub const USERFLAG_NEWCHAR: u32 = 0x00000001; pub const USERFLAG_DRESSINGROOM: u32 = 0x00000002; -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct UserAccountId(pub u32); -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct UserSettingsId(pub u32); -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct GuildCardDataId(pub u32); diff --git a/src/entity/character.rs b/src/entity/character.rs index a9d868b..8f6f2b7 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -226,7 +226,7 @@ impl CharacterTechMenu { } -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct CharacterEntityId(pub u32); #[derive(Clone)] diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index 15c9bb2..1e10dad 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; use crate::entity::account::*; use crate::entity::character::*; @@ -9,19 +9,19 @@ use std::sync::{Arc, Mutex}; #[derive(Clone)] pub struct InMemoryGateway { - users: Arc>>, - user_settings: Arc>>, - characters: Arc>>, - items: Arc>>, + users: Arc>>, + user_settings: Arc>>, + characters: Arc>>, + items: Arc>>, } impl InMemoryGateway { pub fn new() -> InMemoryGateway { InMemoryGateway { - users: Arc::new(Mutex::new(HashMap::new())), - user_settings: Arc::new(Mutex::new(HashMap::new())), - characters: Arc::new(Mutex::new(HashMap::new())), - items: Arc::new(Mutex::new(HashMap::new())), + users: Arc::new(Mutex::new(BTreeMap::new())), + user_settings: Arc::new(Mutex::new(BTreeMap::new())), + characters: Arc::new(Mutex::new(BTreeMap::new())), + items: Arc::new(Mutex::new(BTreeMap::new())), } } } diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index be29d74..b5f4db6 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -11,7 +11,7 @@ use crate::entity::character::CharacterEntityId; use crate::ship::map::MapArea; use crate::ship::drops::ItemDropType; -#[derive(PartialEq, Copy, Clone, Debug, Hash, Eq)] +#[derive(PartialEq, Copy, Clone, Debug, Hash, Eq, PartialOrd, Ord)] pub struct ItemEntityId(pub u32); #[derive(Hash, PartialEq, Eq, Debug, Clone)] pub struct ItemId(u32); diff --git a/src/entity/item/tool.rs b/src/entity/item/tool.rs index a5a6645..cd9029b 100644 --- a/src/entity/item/tool.rs +++ b/src/entity/item/tool.rs @@ -218,7 +218,7 @@ impl ToolType { } } - pub fn max_stack(&self) -> u8 { + pub fn max_stack(&self) -> usize { match self { ToolType::Monomate => 10, ToolType::Dimate => 10, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..309b324 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,13 @@ +#![allow(incomplete_features)] +#![feature(const_generics)] +#![feature(maybe_uninit_extra)] +#![feature(const_in_array_repeat_expressions)] +#![feature(drain_filter)] + + + +pub mod common; +pub mod entity; +pub mod patch; +pub mod login; +pub mod ship; diff --git a/src/ship/items.rs b/src/ship/items.rs index eb49ae1..87df145 100644 --- a/src/ship/items.rs +++ b/src/ship/items.rs @@ -21,16 +21,16 @@ struct RoomItemId(RoomId, u32); #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct ClientItemId(pub u32); -#[derive(Debug, Clone)] -enum ActiveItemEntityId { +#[derive(Debug, Clone, PartialEq)] +pub enum ActiveItemEntityId { Individual(ItemEntityId), Stacked(Vec), Meseta(Meseta), } -#[derive(Debug, Clone)] -enum HeldItemType { +#[derive(Debug, Clone, PartialEq)] +pub enum HeldItemType { Individual(ItemDetail), Stacked(Tool, usize), } @@ -56,7 +56,7 @@ impl HeldItemType { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum FloorItemType { Individual(ItemDetail), Stacked(Tool, usize), @@ -89,10 +89,10 @@ impl FloorItemType { #[derive(Debug, Clone)] pub struct InventoryItem { - entity_id: ActiveItemEntityId, - item_id: ClientItemId, - item: HeldItemType, - equipped: bool, + pub entity_id: ActiveItemEntityId, + pub item_id: ClientItemId, + pub item: HeldItemType, + pub equipped: bool, } #[derive(Debug, Clone)] @@ -113,6 +113,7 @@ pub struct BankItem { } +#[derive(Debug)] pub struct CharacterInventory<'a>(&'a Vec); impl<'a> CharacterInventory<'a> { @@ -132,6 +133,10 @@ impl<'a> CharacterInventory<'a> { }) } + pub fn slot(&self, slot: usize) -> Option<&'a InventoryItem> { + self.0.get(slot) + } + pub fn count(&self) -> usize { self.0.len() } @@ -285,6 +290,35 @@ impl ItemManager { let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; + match &floor_item.item { + FloorItemType::Individual(_item) => { + if inventory.len() >= 30 { + return Err(ItemManagerError::CouldNotAddToInventory(floor_item)); + } + }, + FloorItemType::Stacked(floor_tooltype, floor_amount) => { + let tool_overflow = inventory.iter() + .find(|item| { + if let HeldItemType::Stacked(inv_tooltype, inv_amount) = item.item { + if floor_tooltype.tool == inv_tooltype.tool { + if floor_tooltype.tool.max_stack() < (inv_amount + floor_amount) { + return true + } + } + } + false + }); + if tool_overflow.is_some() { + return Err(ItemManagerError::CouldNotAddToInventory(floor_item)); + } + }, + FloorItemType::Meseta(_meseta) => { + if character.meseta == 999999 { + return Err(ItemManagerError::CouldNotAddToInventory(floor_item)); + } + }, + } + if let Some(_) = local_floor.iter().find(|i| i.item_id == floor_item.item_id) { local_floor.retain(|item| { item.item_id != floor_item.item_id @@ -299,10 +333,6 @@ impl ItemManager { return Err(ItemManagerError::NoSuchItemId(floor_item.item_id)) } - if inventory.len() >= 30 { - return Err(ItemManagerError::CouldNotAddToInventory(floor_item)); - } - match floor_item.item { FloorItemType::Individual(item) => { let inventory_item = InventoryItem { @@ -325,13 +355,45 @@ impl ItemManager { inventory.push(inventory_item); } // else something went very wrong TODO: log it }, - FloorItemType::Stacked(tool, usize) => { - let inventory_item = InventoryItem { - entity_id: floor_item.entity_id, - item_id: floor_item.item_id, - item: HeldItemType::Stacked(tool, usize), - equipped: false, - }; + FloorItemType::Stacked(tool, amount) => { + let inventory_item = inventory.iter_mut() + .filter(|i| { + if let HeldItemType::Stacked(tooltype, _amount) = i.item { + tooltype == tool + } + else { + false + } + }) + .next() + .map(|existing_inv_item| { + if let (ActiveItemEntityId::Stacked(ref mut inv_item_id), + ActiveItemEntityId::Stacked(floor_item_id)) + = (&mut existing_inv_item.entity_id, &floor_item.entity_id) + { + inv_item_id.append(&mut floor_item_id.clone()); + } + + if let (HeldItemType::Stacked(_inv_tooltype, ref mut inv_amount), + FloorItemType::Stacked(_floor_tooltype, floor_amount)) + = (&mut existing_inv_item.item, &floor_item.item) + { + // TODO: check tools are eq? + *inv_amount += floor_amount + } + + existing_inv_item.clone() + }) + .unwrap_or_else(|| { + let picked_up_item = InventoryItem { + entity_id: floor_item.entity_id, + item_id: floor_item.item_id, + item: HeldItemType::Stacked(tool, amount), + equipped: false, + }; + inventory.push(picked_up_item.clone()); + picked_up_item + }); if let ActiveItemEntityId::Stacked(item_ids) = &inventory_item.entity_id { for item_id in item_ids { @@ -345,11 +407,10 @@ impl ItemManager { }, }); // TODO: error check }; - inventory.push(inventory_item); } // else something went very wrong TODO: log it }, FloorItemType::Meseta(meseta) => { - character.meseta += meseta.0; + character.meseta = std::cmp::min(character.meseta + meseta.0, 999999); entity_gateway.save_character(&character); } } diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index d1b2fa8..36be335 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -190,4 +190,4 @@ EG: EntityGateway .collect::>(); // TODO: can EntityGateway be Sync? Ok(Box::new(item_drop_packets.into_iter())) -} \ No newline at end of file +} diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 0c39912..9749178 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -238,12 +238,12 @@ impl ClientState { pub struct ShipServerState { entity_gateway: EG, - clients: Clients, + pub clients: Clients, client_location: ClientLocation, level_table: CharacterLevelTable, name: String, rooms: Rooms, - item_manager: items::ItemManager, + pub item_manager: items::ItemManager, quests: quests::QuestList, } @@ -261,52 +261,52 @@ impl ShipServerState { } } - fn message(&mut self, id: ClientId, msg: &Message) -> Box + Send> { + fn message(&mut self, id: ClientId, msg: &Message) -> Result + Send>, ShipError> { match &msg.msg { GameMessage::RequestExp(request_exp) => { - handler::message::request_exp(id, request_exp, &self.client_location, &self.rooms) + Ok(handler::message::request_exp(id, request_exp, &self.client_location, &self.rooms)) }, GameMessage::PlayerDropItem(player_drop_item) => { - handler::message::player_drop_item(id, player_drop_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).unwrap() + handler::message::player_drop_item(id, player_drop_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager) }, GameMessage::DropCoordinates(drop_coordinates) => { - handler::message::drop_coordinates(id, drop_coordinates, &self.client_location, &mut self.clients, &self.rooms).unwrap() + handler::message::drop_coordinates(id, drop_coordinates, &self.client_location, &mut self.clients, &self.rooms) }, GameMessage::PlayerSplitItemStack(split_item_stack) => { - handler::message::split_item_stack(id, split_item_stack, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).unwrap() + handler::message::split_item_stack(id, split_item_stack, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager) }, _ => { let cmsg = msg.clone(); - Box::new(self.client_location.get_client_neighbors(id).unwrap().into_iter() + Ok(Box::new(self.client_location.get_client_neighbors(id).unwrap().into_iter() .map(move |client| { (client.client, SendShipPacket::Message(cmsg.clone())) - })) + }))) }, } } - fn direct_message(&mut self, id: ClientId, msg: &DirectMessage) -> Box + Send> { + fn direct_message(&mut self, id: ClientId, msg: &DirectMessage) -> Result + Send>, ShipError> { let target = msg.flag; match &msg.msg { GameMessage::GuildcardSend(guildcard_send) => { - handler::direct_message::guildcard_send(id, guildcard_send, target, &self.client_location, &self.clients) + Ok(handler::direct_message::guildcard_send(id, guildcard_send, target, &self.client_location, &self.clients)) }, GameMessage::RequestItem(request_item) => { - handler::direct_message::request_item(id, request_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).unwrap() + handler::direct_message::request_item(id, request_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager) }, GameMessage::PickupItem(pickup_item) => { - handler::direct_message::pickup_item(id, pickup_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).unwrap() + handler::direct_message::pickup_item(id, pickup_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager) }, GameMessage::BoxDropRequest(box_drop_request) => { - handler::direct_message::request_box_item(id, box_drop_request, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).unwrap() // TODO: unwrap + handler::direct_message::request_box_item(id, box_drop_request, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager) } _ => { let cmsg = msg.clone(); - Box::new(self.client_location.get_all_clients_by_client(id).unwrap().into_iter() + Ok(Box::new(self.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(cmsg.clone())) - })) + }))) }, } } @@ -375,10 +375,10 @@ impl ServerState for ShipServerState { Box::new(handler::lobby::send_player_to_lobby(id, chardata, &mut self.client_location, &self.clients, &self.item_manager, &self.level_table)?.into_iter()) }, RecvShipPacket::Message(msg) => { - self.message(id, msg) + self.message(id, msg)? }, RecvShipPacket::DirectMessage(msg) => { - self.direct_message(id, msg) + self.direct_message(id, msg)? }, RecvShipPacket::PlayerChat(msg) => { Box::new(handler::communication::player_chat(id, msg, &self.client_location, &self.clients)?.into_iter()) diff --git a/tests/test_item_pickup.rs b/tests/test_item_pickup.rs new file mode 100644 index 0000000..8735007 --- /dev/null +++ b/tests/test_item_pickup.rs @@ -0,0 +1,683 @@ +use std::time::SystemTime; + +use elseware::common::serverstate::{ClientId, ServerState}; +use elseware::entity::gateway::{EntityGateway, InMemoryGateway}; +use elseware::entity::account::{UserAccountEntity, NewUserAccountEntity, NewUserSettingsEntity}; +use elseware::entity::character::{CharacterEntity, NewCharacterEntity}; +//use elseware::entity::item::{NewItemEntity, ItemDetail, ItemLocation}; +use elseware::entity::item; +use elseware::ship::ship::{ShipServerState, RecvShipPacket}; +use elseware::ship::items::{ClientItemId, ActiveItemEntityId, HeldItemType, FloorItemType}; + +use libpso::packet::ship::*; +use libpso::packet::messages::*; +use libpso::packet::login::{Login, Session}; +use libpso::{utf8_to_array, utf8_to_utf16_array}; + + +pub fn new_user_character(entity_gateway: &mut EG, username: &str, password: &str) -> (UserAccountEntity, CharacterEntity) { + let new_user = NewUserAccountEntity { + username: username.into(), + password: bcrypt::hash(password, 5).unwrap(), + guildcard: 1, + team_id: None, + banned: false, + muted_until: SystemTime::now(), + created_at: SystemTime::now(), + flags: 0, + }; + + let user = entity_gateway.create_user(new_user).unwrap(); + let new_settings = NewUserSettingsEntity::new(user.id); + let _settings = entity_gateway.create_user_settings(new_settings).unwrap(); + let new_character = NewCharacterEntity::new(user.id); + let character = entity_gateway.create_character(new_character).unwrap(); + + (user, character) +} + +pub fn log_in_char(ship: &mut ShipServerState, id: ClientId, username: &str, password: &str) { + let username = username.to_string(); + let password = password.to_string(); + ship.handle(id, &RecvShipPacket::Login(Login { + tag: 0, + guildcard: 0, + version: 0, + unknown1: [0; 6], + team: 0, + username: utf8_to_array!(username, 16), + unknown2: [0; 32], + password: utf8_to_array!(password, 16), + unknown3: [0; 40], + hwinfo: [0; 8], + session: Session::new(), + })).unwrap().for_each(drop); +} + +pub fn join_lobby(ship: &mut ShipServerState, id: ClientId) { + ship.handle(id, &RecvShipPacket::CharData(CharData { + _unknown: [0; 0x828] + })).unwrap().for_each(drop); +} + +pub fn create_room(ship: &mut ShipServerState, id: ClientId, name: &str, password: &str) { + ship.handle(id, &RecvShipPacket::CreateRoom(CreateRoom { + unknown: [0; 2], + name: utf8_to_utf16_array!(name, 16), + password: utf8_to_utf16_array!(password, 16), + difficulty: 0, + battle: 0, + challenge: 0, + episode: 1, + single_player: 0, + padding: [0; 3], + })).unwrap().for_each(drop); + ship.handle(id, &RecvShipPacket::DoneBursting(DoneBursting {})).unwrap().for_each(drop); +} + +pub fn join_room(ship: &mut ShipServerState, id: ClientId, room_id: u32) { + ship.handle(id, &RecvShipPacket::MenuSelect(MenuSelect { + menu: ROOM_MENU_ID, + item: room_id, + })).unwrap().for_each(drop); +} + + +#[test] +fn test_pick_up_item_stack_of_items_already_in_inventory() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }); + + for (slot, tool) in vec![item::tool::ToolType::Monomate, item::tool::ToolType::Monofluid].into_iter().enumerate() { + for _ in 0..5 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: tool + } + ), + location: item::ItemLocation::Inventory { + character_id: char2.id, + slot: slot, + equipped: false, + } + }); + } + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem { + client: 0, + target: 0, + unknown1: 0, + area: 0, + item_id: 0x210000, + x: 0.0, + y: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0x210000, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let p1_inventory = ship.item_manager.get_character_inventory(&char1).unwrap(); + assert!(p1_inventory.count() == 1); + let inventory_item = p1_inventory.slot(0).unwrap(); + assert!(inventory_item.entity_id == ActiveItemEntityId::Stacked(vec![item::ItemEntityId(1), item::ItemEntityId(2), item::ItemEntityId(3), item::ItemEntityId(4), item::ItemEntityId(5), item::ItemEntityId(6)])); + assert!(inventory_item.item == HeldItemType::Stacked(item::tool::Tool {tool: item::tool::ToolType::Monomate}, 6)); +} + +#[test] +fn test_pick_up_item_stack_of_items_not_already_held() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate + } + ), + location: item::ItemLocation::Inventory { + character_id: char2.id, + slot: 0, + equipped: false, + } + }); + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem { + client: 0, + target: 0, + unknown1: 0, + area: 0, + item_id: 0x210000, + x: 0.0, + y: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0x210000, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let p1_inventory = ship.item_manager.get_character_inventory(&char1).unwrap(); + assert!(p1_inventory.count() == 1); + let inventory_item = p1_inventory.slot(0).unwrap(); + assert!(inventory_item.entity_id == ActiveItemEntityId::Stacked(vec![item::ItemEntityId(1)])); + assert!(inventory_item.item == HeldItemType::Stacked(item::tool::Tool {tool: item::tool::ToolType::Monomate}, 1)); +} + +#[test] +fn test_pick_up_meseta_when_inventory_full() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + for slot in 0..30 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Saber, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: slot, + equipped: false, + } + }); + } + + char2.meseta = 300; + entity_gateway.save_character(&char2); + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::DropCoordinates(DropCoordinates { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + map_area: 0, + x: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + amount: 23, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0xF0000001, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let p1_inventory = ship.item_manager.get_character_inventory(&char1).unwrap(); + assert!(p1_inventory.count() == 30); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + let c2 = ship.clients.get(&ClientId(2)).unwrap(); + assert!(c1.character.meseta == 23); + assert!(c2.character.meseta == 277); +} + +#[test] +fn test_pick_up_partial_stacked_item_when_inventory_is_otherwise_full() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + for slot in 0..29 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Saber, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: slot, + equipped: false, + } + }); + } + + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 29, + equipped: false, + } + }); + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char2.id, + slot: 0, + equipped: false, + } + }); + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem { + client: 0, + target: 0, + unknown1: 0, + area: 0, + item_id: 0x210000, + x: 0.0, + y: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0x210000, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let p1_inventory = ship.item_manager.get_character_inventory(&char1).unwrap(); + assert!(p1_inventory.count() == 30); + + + let monomates = p1_inventory.slot(29).unwrap(); + assert!(monomates.item == HeldItemType::Stacked(item::tool::Tool {tool: item::tool::ToolType::Monomate}, 2)); +} + +#[test] +fn test_can_not_pick_up_item_when_inventory_full() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + for slot in 0..30 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Saber, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: slot, + equipped: false, + } + }); + } + + 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, + } + ), + location: item::ItemLocation::Inventory { + character_id: char2.id, + slot: 0, + equipped: false, + } + } + ); + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem { + client: 0, + target: 0, + unknown1: 0, + area: 0, + item_id: 0x210000, + x: 0.0, + y: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0x210000, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let p1_inventory = ship.item_manager.get_character_inventory(&char1).unwrap(); + assert!(p1_inventory.count() == 30); + let floor_item = ship.item_manager.get_floor_item_by_id(&char1, ClientItemId(0x210000)).unwrap(); + assert!(floor_item.item_id == ClientItemId(0x210000)); +} + +#[test] +fn test_can_not_drop_more_meseta_than_is_held() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a"); + + char1.meseta = 300; + entity_gateway.save_character(&char1); + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + + join_lobby(&mut ship, ClientId(1)); + + create_room(&mut ship, ClientId(1), "room", ""); + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::DropCoordinates(DropCoordinates { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + map_area: 0, + x: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + let split_attempt = ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + amount: 301, + })))); + assert!(split_attempt.is_err()); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + assert!(c1.character.meseta == 300); + assert!(ship.item_manager.get_floor_item_by_id(&char1, ClientItemId(0xF0000001)).is_err()) +} + +#[test] +fn test_pick_up_stack_that_would_exceed_stack_limit() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + for _ in 0..6 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }); + } + for _ in 0..6 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char2.id, + slot: 0, + equipped: false, + } + }); + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem { + client: 0, + target: 0, + unknown1: 0, + area: 0, + item_id: 0x210000, + x: 0.0, + y: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0x210000, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let p1_inventory = ship.item_manager.get_character_inventory(&char1).unwrap(); + let monomates = p1_inventory.slot(0).unwrap(); + assert!(monomates.item == HeldItemType::Stacked(item::tool::Tool {tool: item::tool::ToolType::Monomate}, 6)); + let floor_monomates = ship.item_manager.get_floor_item_by_id(&char1, ClientItemId(0x210000)).unwrap(); + assert!(floor_monomates.item == FloorItemType::Stacked(item::tool::Tool {tool: item::tool::ToolType::Monomate}, 6)); +} + +#[test] +fn test_can_not_pick_up_meseta_when_full() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + char1.meseta = 999999; + entity_gateway.save_character(&char1); + char2.meseta = 300; + entity_gateway.save_character(&char2); + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::DropCoordinates(DropCoordinates { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + map_area: 0, + x: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + amount: 23, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0xF0000001, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + let c2 = ship.clients.get(&ClientId(2)).unwrap(); + assert!(c1.character.meseta == 999999); + assert!(c2.character.meseta == 277); + + let floor_meseta = ship.item_manager.get_floor_item_by_id(&char1, ClientItemId(0xF0000001)).unwrap(); + assert!(floor_meseta.item == FloorItemType::Meseta(item::Meseta(23))); +} + +#[test] +fn test_meseta_caps_at_999999_when_trying_to_pick_up_more() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a"); + let (_user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a"); + + char1.meseta = 999998; + entity_gateway.save_character(&char1); + char2.meseta = 300; + entity_gateway.save_character(&char2); + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a"); + log_in_char(&mut ship, ClientId(2), "a2", "a"); + + join_lobby(&mut ship, ClientId(1)); + join_lobby(&mut ship, ClientId(2)); + + create_room(&mut ship, ClientId(1), "room", ""); + join_room(&mut ship, ClientId(2), 0); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::DropCoordinates(DropCoordinates { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + map_area: 0, + x: 0.0, + z: 0.0, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + amount: 23, + })))).unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0xF0000001, + area: 0, + unknown: [0; 3] + })))).unwrap().for_each(drop); + + let c1 = ship.clients.get(&ClientId(1)).unwrap(); + let c2 = ship.clients.get(&ClientId(2)).unwrap(); + assert!(c1.character.meseta == 999999); + assert!(c2.character.meseta == 277); + + let floor_meseta = ship.item_manager.get_floor_item_by_id(&char1, ClientItemId(0xF0000001)); + assert!(floor_meseta.is_err()); +}