use log::warn; use rand::Rng; use rand::seq::SliceRandom; use libpso::packet::ship::*; use libpso::packet::messages::*; use crate::common::leveltable::LEVEL_TABLE; use crate::common::serverstate::ClientId; use crate::ship::ship::{SendShipPacket, ShipError, Clients, ItemShops}; use crate::ship::location::ClientLocation; use crate::ship::drops::ItemDrop; use crate::ship::room::Rooms; use crate::ship::items::ClientItemId; use crate::entity::gateway::EntityGateway; use crate::entity::item; use libpso::utf8_to_utf16_array; use crate::ship::packet::builder; use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem}; use crate::ship::items::state::{ItemState, ItemStateError}; use crate::ship::items::floor::{FloorType, FloorItemDetail}; use crate::ship::items::actions::TriggerCreateItem; use crate::ship::items::tasks::{pick_up_item, withdraw_meseta, deposit_meseta, withdraw_item, deposit_item, buy_shop_item, enemy_drops_item, box_drops_item, take_meseta, apply_modifier}; const BANK_ACTION_DEPOSIT: u8 = 0; const BANK_ACTION_WITHDRAW: u8 = 1; const SHOP_OPTION_TOOL: u8 = 0; const SHOP_OPTION_WEAPON: u8 = 1; const SHOP_OPTION_ARMOR: u8 = 2; #[derive(thiserror::Error, Debug)] pub enum MessageError { #[error("invalid tek {0}")] InvalidTek(ClientItemId), #[error("mismatched tek {0} {1}")] MismatchedTekIds(ClientItemId, ClientItemId), } async fn send_to_client(id: ClientId, target: u8, msg: DirectMessage, client_location: &ClientLocation) -> Result, anyhow::Error> { Ok(client_location.get_all_clients_by_client(id) .await? .into_iter() .filter(move |client| client.local_client.id() == target) .map(move |client| { (client.client, SendShipPacket::DirectMessage(msg.clone())) }) .collect()) } pub async fn guildcard_send(id: ClientId, guildcard_send: GuildcardSend, target: u32, client_location: &ClientLocation, clients: &Clients) -> Result, anyhow::Error> { let msg = clients.with(id, |client| Box::pin(async move { DirectMessage{ flag: target, msg: GameMessage::GuildcardRecv(GuildcardRecv { client: guildcard_send.client, target: guildcard_send.target, guildcard: client.user.id.0, name: utf8_to_utf16_array!(client.character.name, 0x18), team: [0; 0x10], // TODO: teams not yet implemented desc: utf8_to_utf16_array!(client.character.guildcard.description, 0x58), one: 1, language: 0, // TODO: add language flag to character section_id: client.character.section_id.into(), class: client.character.char_class.into(), }), } })).await?; send_to_client(id, target as u8, msg, client_location).await } pub async fn request_item(id: ClientId, request_item: RequestItem, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &Clients, rooms: &Rooms, item_state: &mut ItemState) -> Result, anyhow::Error> where EG: EntityGateway + 'static, { let room_id = client_location.get_room(id).await?; let (room_entity_id, monster) = rooms.with(room_id, |room| Box::pin(async move { Ok::<_, anyhow::Error>((room.room_id, room.maps.enemy_by_id(request_item.enemy_id as usize)?)) })).await??; if monster.dropped_item { return Err(ShipError::MonsterAlreadyDroppedItem(id, request_item.enemy_id).into()) } let clients_in_area = client_location.get_clients_in_room(room_id).await?; let client_and_drop = rooms.with_mut(room_id, |room| Box::pin(async move { clients_in_area.into_iter() .filter_map(move |area_client| { room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| { (area_client, item_drop_type) }) }) .collect::>() })).await?; let mut item_drop_packets = Vec::new(); for (area_client, item_drop) in client_and_drop { let item_drop = ItemDrop { map_area: monster.map_area, x: request_item.x, y: request_item.y, z: request_item.z, item: item_drop, }; let character_id = clients.with(area_client.client, |client| Box::pin(async move { client.character.id })).await?; let floor_item = enemy_drops_item(item_state, entity_gateway, character_id, room_entity_id, monster.monster, item_drop).await?; let item_drop_msg = builder::message::item_drop(request_item.client, request_item.target, &floor_item)?; item_drop_packets.push((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg))))); } Ok(item_drop_packets) } pub async fn pickup_item(id: ClientId, pickup_item: PickupItem, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &Clients, item_state: &mut ItemState) -> Result, anyhow::Error> where EG: EntityGateway + Clone + 'static, { let area_client = client_location.get_local_client(id).await?; let room_id = client_location.get_room(id).await?; let clients_in_area = client_location.get_clients_in_room(room_id).await?; clients.with(id, |client| { let mut entity_gateway = entity_gateway.clone(); let mut item_state = item_state.clone(); Box::pin(async move { let (item, floor_type) = item_state.get_floor_item(&client.character.id, &ClientItemId(pickup_item.item_id)).await?; let remove_item = builder::message::remove_item_from_floor(area_client, &item)?; let create_item = match &item.item { FloorItemDetail::Individual(individual_floor_item) => Some(builder::message::create_individual_item(area_client, item.item_id, individual_floor_item)?), FloorItemDetail::Stacked(stacked_floor_item) => Some(builder::message::create_stacked_item(area_client, item.item_id, &stacked_floor_item.tool, stacked_floor_item.count())?), FloorItemDetail::Meseta(_) => None, }; match pick_up_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(pickup_item.item_id)).await { Ok(trigger_create_item) => { let remove_packets: Box + Send> = match floor_type { FloorType::Local => { Box::new(vec![(id, SendShipPacket::Message(Message::new(GameMessage::RemoveItemFromFloor(remove_item.clone()))))].into_iter()) }, FloorType::Shared => { Box::new(clients_in_area.clone().into_iter() .map(move |c| { (c.client, SendShipPacket::Message(Message::new(GameMessage::RemoveItemFromFloor(remove_item.clone())))) })) }, }; Ok(remove_packets .chain(clients_in_area.into_iter() .filter_map(move |c| { match trigger_create_item { TriggerCreateItem::Yes => create_item.clone().map(|ci| (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(ci))))), _ => None } })) .collect()) }, Err(err) => { warn!("character {:?} could not pick up item: {:?}", client.character.id, err); Ok(Vec::new()) }, } })}).await? } pub async fn request_box_item(id: ClientId, box_drop_request: BoxDropRequest, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &Clients, rooms: &Rooms, item_state: &mut ItemState) -> Result, anyhow::Error> where EG: EntityGateway + Clone + 'static { let room_id = client_location.get_room(id).await?; let (room_entity_id, box_object) = rooms.with(room_id, |room| Box::pin(async move { Ok::<_, anyhow::Error>((room.room_id, room.maps.object_by_id(box_drop_request.object_id as usize)?)) })).await??; if box_object.dropped_item { return Err(ShipError::BoxAlreadyDroppedItem(id, box_drop_request.object_id).into()) } let clients_in_area = client_location.get_clients_in_room(room_id).await?; let client_and_drop = rooms.with_mut(room_id, |room| Box::pin(async move { clients_in_area.into_iter() .filter_map(move |area_client| { room.drop_table.get_box_drop(&box_object.map, &box_object).map(|item_drop_type| { (area_client, item_drop_type) }) }) .collect::>() })).await?; let mut item_drop_packets = Vec::new(); for (area_client, item_drop) in client_and_drop { let item_drop = ItemDrop { map_area: box_object.map, x: box_drop_request.x, y: 0.0, z: box_drop_request.z, item: item_drop, }; //let client = clients.get_mut(&area_client.client).ok_or(ShipError::ClientNotFound(area_client.client))?; let character_id = clients.with(area_client.client, |client| Box::pin(async move { client.character.id })).await?; let floor_item = box_drops_item(item_state, entity_gateway, character_id, room_entity_id, item_drop).await?; //let floor_item = enemy_drops_item(item_state, &mut entity_gateway, client.character.id, item_drop).await?; let item_drop_msg = builder::message::item_drop(box_drop_request.client, box_drop_request.target, &floor_item)?; item_drop_packets.push((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg))))) } Ok(item_drop_packets) } pub async fn send_bank_list(id: ClientId, clients: &Clients, item_state: &mut ItemState) -> Result, anyhow::Error> { let bank = clients.with(id, |client| { let item_state = item_state.clone(); Box::pin(async move { item_state.get_character_bank(&client.character).await }) }).await??; let bank_items_pkt = builder::message::bank_item_list(&bank); Ok(vec![(id, SendShipPacket::BankItemList(bank_items_pkt))]) } pub async fn bank_interaction(id: ClientId, bank_interaction: BankInteraction, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &Clients, item_state: &mut ItemState) -> Result, anyhow::Error> where EG: EntityGateway + Clone + 'static, { let area_client = client_location.get_local_client(id).await?; let other_clients_in_area = client_location.get_all_clients_by_client(id).await?; let bank_action_pkts = clients.with(id, |client| { let mut entity_gateway = entity_gateway.clone(); let mut item_state = item_state.clone(); Box::pin(async move { Ok::<_, anyhow::Error>(match bank_interaction.action { BANK_ACTION_DEPOSIT => { if bank_interaction.item_id == 0xFFFFFFFF { deposit_meseta(&mut item_state, &mut entity_gateway, &client.character, bank_interaction.meseta_amount).await?; Vec::new() } else { deposit_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as u32).await?; let player_no_longer_has_item = builder::message::player_no_longer_has_item(area_client, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as u32); vec![SendShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(player_no_longer_has_item)))] } }, BANK_ACTION_WITHDRAW => { if bank_interaction.item_id == 0xFFFFFFFF { withdraw_meseta(&mut item_state, &mut entity_gateway, &client.character, bank_interaction.meseta_amount).await?; Vec::new() } else { let item_added_to_inventory = withdraw_item(&mut item_state, &mut entity_gateway, &client.character, &ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as u32).await?; let item_created = builder::message::create_withdrawn_inventory_item2(area_client, &item_added_to_inventory)?; vec![SendShipPacket::Message(Message::new(GameMessage::CreateItem(item_created)))] } }, _ => { // TODO: error? Vec::new() } }) }) }).await??; Ok(other_clients_in_area.into_iter() .flat_map(move |c| { bank_action_pkts.clone().into_iter() .map(move |pkt| { (c.client, pkt) }) }) .collect() ) } pub async fn shop_request(id: ClientId, shop_request: ShopRequest, client_location: &ClientLocation, clients: &Clients, rooms: &Rooms, shops: &ItemShops) -> Result, anyhow::Error> { //let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; let room_id = client_location.get_room(id).await?; /* let room = rooms.get(room_id.0) .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? .as_ref() .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; */ let difficulty = rooms.with(room_id, |room| Box::pin(async move { room.mode.difficulty() })).await?; let shop_list = clients.with_mut(id, |client| { let mut shops = shops.clone(); Box::pin(async move { let level = LEVEL_TABLE.get_level_from_exp(client.character.char_class, client.character.exp) as usize; match shop_request.shop_type { SHOP_OPTION_WEAPON => { client.weapon_shop = shops.weapon_shop.get_mut(&(difficulty, client.character.section_id)) .ok_or(ShipError::ShopError)? .lock() .await .generate_weapon_list(level); Ok(builder::message::shop_list(shop_request.shop_type, &client.weapon_shop)) }, SHOP_OPTION_TOOL => { client.tool_shop = shops.tool_shop .lock() .await .generate_tool_list(level); Ok(builder::message::shop_list(shop_request.shop_type, &client.tool_shop)) }, SHOP_OPTION_ARMOR => { client.armor_shop = shops.armor_shop .lock() .await .generate_armor_list(level); Ok(builder::message::shop_list(shop_request.shop_type, &client.armor_shop)) }, _ => { Err(ShipError::ShopError) } } })}).await??; /* let shop_list = match shop_request.shop_type { SHOP_OPTION_WEAPON => { client.weapon_shop = shops.weapon_shop.get_mut(&(room.mode.difficulty(), client.character.section_id)) .ok_or(ShipError::ShopError)? .generate_weapon_list(level); builder::message::shop_list(shop_request.shop_type, &client.weapon_shop) }, SHOP_OPTION_TOOL => { client.tool_shop = shops.tool_shop.generate_tool_list(level); builder::message::shop_list(shop_request.shop_type, &client.tool_shop) }, SHOP_OPTION_ARMOR => { client.armor_shop = shops.armor_shop.generate_armor_list(level); builder::message::shop_list(shop_request.shop_type, &client.armor_shop) }, _ => { return Err(ShipError::ShopError.into()) } }; */ Ok(vec![(id, SendShipPacket::Message(Message::new(GameMessage::ShopList(shop_list))))]) } pub async fn buy_item(id: ClientId, buy_item: BuyItem, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &Clients, item_state: &mut ItemState) -> Result, anyhow::Error> where EG: EntityGateway + Clone + 'static, { let area_client = client_location.get_local_client(id).await?; let create = clients.with_mut(id, |client| { let mut entity_gateway = entity_gateway.clone(); let mut item_state = item_state.clone(); Box::pin(async move { let (item, remove): (&(dyn ShopItem + Send + Sync), bool) = match buy_item.shop_type { SHOP_OPTION_WEAPON => { (client.weapon_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)?, false) }, SHOP_OPTION_TOOL => { let item = client.tool_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)?; let remove = matches!(item, ToolShopItem::Tech(_)); (item, remove) }, SHOP_OPTION_ARMOR => { let item = client.armor_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)?; let remove = matches!(item, ArmorShopItem::Unit(_)); (item, remove) }, _ => { return Err(ShipError::ShopError.into()) } }; let inventory_item = buy_shop_item(&mut item_state, &mut entity_gateway, &client.character, item, ClientItemId(buy_item.item_id), buy_item.amount as u32).await?; if remove { match buy_item.shop_type { SHOP_OPTION_TOOL => { client.tool_shop.remove(buy_item.shop_index as usize); }, SHOP_OPTION_ARMOR => { client.armor_shop.remove(buy_item.shop_index as usize); }, _ => {} } } Ok::<_, anyhow::Error>(builder::message::create_withdrawn_inventory_item(area_client, &inventory_item)?) })}).await??; let other_clients_in_area = client_location.get_client_neighbors(id).await?; Ok(other_clients_in_area.into_iter() .map(move |c| { (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(create.clone())))) }) .collect()) } const TEK_SPECIAL_MODIFIER: [item::weapon::TekSpecialModifier; 3] = [item::weapon::TekSpecialModifier::Plus, item::weapon::TekSpecialModifier::Neutral, item::weapon::TekSpecialModifier::Minus]; const TEK_PERCENT_MODIFIER: [item::weapon::TekPercentModifier; 5] = [item::weapon::TekPercentModifier::PlusPlus, item::weapon::TekPercentModifier::Plus, item::weapon::TekPercentModifier::Neutral, item::weapon::TekPercentModifier::Minus, item::weapon::TekPercentModifier::MinusMinus]; pub async fn request_tek_item(id: ClientId, tek_request: TekRequest, entity_gateway: &mut EG, clients: &Clients, item_state: &mut ItemState) -> Result, anyhow::Error> where EG: EntityGateway + Clone + 'static, { //let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; // TODO: secids have different mod rates let (grind_mod, special_mod, percent_mod) = { let mut rng = rand::thread_rng(); let grind_mod = rng.gen_range(-4, 4); let special_mod = TEK_SPECIAL_MODIFIER.choose(&mut rng).cloned().unwrap(); let percent_mod = TEK_PERCENT_MODIFIER.choose(&mut rng).cloned().unwrap(); (grind_mod, special_mod, percent_mod) }; let preview_pkt = clients.with_mut(id, |client| { let mut entity_gateway = entity_gateway.clone(); let mut item_state = item_state.clone(); Box::pin(async move { client.tek = Some((ClientItemId(tek_request.item_id), special_mod, percent_mod, grind_mod)); let inventory = item_state.get_character_inventory(&client.character).await?; let item = inventory.get_by_client_id(&ClientItemId(tek_request.item_id)) .ok_or_else(|| ItemStateError::WrongItemType(ClientItemId(tek_request.item_id)))?; let mut weapon = *item.item.as_individual() .ok_or_else(|| ItemStateError::WrongItemType(ClientItemId(tek_request.item_id)))? .as_weapon() .ok_or_else(|| ItemStateError::WrongItemType(ClientItemId(tek_request.item_id)))?; weapon.apply_modifier(&item::weapon::WeaponModifier::Tekked { special: special_mod, percent: percent_mod, grind: grind_mod, }); take_meseta(&mut item_state, &mut entity_gateway, &client.character.id, item::Meseta(100)).await?; Ok::<_, anyhow::Error>(builder::message::tek_preview(ClientItemId(tek_request.item_id), &weapon)?) })}).await??; Ok(vec![(id, SendShipPacket::Message(Message::new(GameMessage::TekPreview(preview_pkt))))]) } pub async fn accept_tek_item(id: ClientId, tek_accept: TekAccept, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &Clients, item_state: &mut ItemState) -> Result, anyhow::Error> where EG: EntityGateway + Clone + 'static, { let area_client = client_location.get_local_client(id).await?; let neighbors = client_location.get_client_neighbors(id).await?; clients.with(id, |client| { let mut entity_gateway = entity_gateway.clone(); let mut item_state = item_state.clone(); Box::pin(async move { if let Some((item_id, special_mod, percent_mod, grind_mod)) = client.tek { if item_id.0 != tek_accept.item_id { return Err(MessageError::MismatchedTekIds(item_id, ClientItemId(tek_accept.item_id)).into()); } let modifier = item::weapon::WeaponModifier::Tekked { special: special_mod, percent: percent_mod, grind: grind_mod, }; let weapon = apply_modifier(&mut item_state, &mut entity_gateway, &client.character, item_id, item::ItemModifier::WeaponModifier(modifier)).await?; let create_item_pkt = builder::message::create_individual_item(area_client, item_id, &weapon)?; Ok(neighbors.into_iter() .map(move |c| { (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(create_item_pkt.clone())))) }) .collect()) } else { Err(MessageError::InvalidTek(ClientItemId(tek_accept.item_id)).into()) } })}).await? }