Browse Source

redo items to account for client ignoring the assigned item_id and using its own. also item picking up

pbs
jake 5 years ago
parent
commit
ac28354dd7
  1. 1
      src/main.rs
  2. 2
      src/ship/character.rs
  3. 348
      src/ship/items.rs
  4. 3
      src/ship/location.rs
  5. 7
      src/ship/packet/builder/lobby.rs
  6. 6
      src/ship/packet/builder/message.rs
  7. 8
      src/ship/packet/builder/mod.rs
  8. 4
      src/ship/packet/builder/room.rs
  9. 4
      src/ship/packet/handler/auth.rs
  10. 62
      src/ship/packet/handler/direct_message.rs
  11. 19
      src/ship/packet/handler/lobby.rs
  12. 36
      src/ship/packet/handler/message.rs
  13. 9
      src/ship/packet/handler/room.rs
  14. 15
      src/ship/room.rs
  15. 46
      src/ship/ship.rs

1
src/main.rs

@ -2,6 +2,7 @@
#![feature(const_generics)]
#![feature(maybe_uninit_extra)]
#![feature(const_in_array_repeat_expressions)]
#![feature(drain_filter)]

2
src/ship/character.rs

@ -80,7 +80,7 @@ pub struct FullCharacterBytesBuilder<'a> {
character: Option<&'a CharacterEntity>,
stats: Option<&'a CharacterStats>,
level: Option<u32>,
inventory: Option<&'a CharacterInventory>,
inventory: Option<&'a CharacterInventory<'a>>,
key_config: Option<&'a [u8; 0x16C]>,
joystick_config: Option<&'a [u8; 0x38]>,
symbol_chat: Option<&'a [u8; 1248]>,

348
src/ship/items.rs

@ -1,27 +1,34 @@
#![allow(dead_code)]
use std::collections::{HashMap, BTreeMap};
use libpso::character::character;//::InventoryItem;
use thiserror::Error;
use crate::entity::gateway::EntityGateway;
use crate::entity::character::CharacterEntity;
use crate::entity::character::{CharacterEntity, CharacterEntityId};
use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation};
use crate::entity::item::{Meseta, NewItemEntity};
use crate::entity::item::tool::Tool;
use crate::ship::map::MapArea;
use crate::ship::drops::{ItemDrop, ItemDropType};
use crate::ship::ship::ClientState;
use crate::ship::location::{AreaClient, RoomId};
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct ActiveItemId(pub u32);
#[derive(Debug)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
struct RoomItemId(RoomId, u32);
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct ClientItemId(pub u32);
#[derive(Debug, Clone)]
enum ActiveItemEntityId {
Individual(ItemEntityId),
Stacked(Vec<ItemEntityId>),
Meseta(Meseta),
}
#[derive(Debug)]
#[derive(Debug, Clone)]
enum HeldItemType {
Individual(ItemDetail),
Stacked(Tool, usize),
@ -48,21 +55,7 @@ impl HeldItemType {
}
}
#[derive(Debug)]
pub struct InventoryItem {
id: ActiveItemId,
item: HeldItemType,
//slot: usize,
equipped: bool,
}
#[derive(Debug)]
pub struct BankItem {
id: ActiveItemId,
item: HeldItemType,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum FloorItemType {
Individual(ItemDetail),
Stacked(Tool, usize),
@ -93,9 +86,18 @@ impl FloorItemType {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct InventoryItem {
entity_id: ActiveItemEntityId,
item_id: ClientItemId,
item: HeldItemType,
equipped: bool,
}
#[derive(Debug, Clone)]
pub struct FloorItem {
pub id: ActiveItemId,
entity_id: ActiveItemEntityId,
pub item_id: ClientItemId,
pub item: FloorItemType,
pub map_area: MapArea,
pub x: f32,
@ -104,12 +106,15 @@ pub struct FloorItem {
}
#[derive(Debug)]
pub enum InventoryError {
pub struct BankItem {
id: ActiveItemId,
item: HeldItemType,
}
pub struct CharacterInventory(Vec<InventoryItem>);
impl CharacterInventory {
pub struct CharacterInventory<'a>(&'a Vec<InventoryItem>);
impl<'a> CharacterInventory<'a> {
pub fn as_client_inventory_items(&self) -> [character::InventoryItem; 30] {
self.0.iter()
.enumerate()
@ -117,7 +122,7 @@ impl CharacterInventory {
let bytes = item.item.as_client_bytes();
inventory[slot].data1.copy_from_slice(&bytes[0..12]);
inventory[slot].data2.copy_from_slice(&bytes[12..16]);
inventory[slot].item_id = item.id.0;
inventory[slot].item_id = item.item_id.0;
// does this do anything?
inventory[slot].equipped = if item.equipped { 1 } else { 0 };
// because this actually equips the item
@ -126,42 +131,60 @@ impl CharacterInventory {
})
}
pub fn add_item(&mut self, item: InventoryItem) -> Result<usize, InventoryError> {
self.0.push(item);
Ok(self.count() - 1)
}
pub fn count(&self) -> usize {
self.0.len()
}
}
#[derive(Debug)]
#[derive(Error, Debug)]
#[error("")]
pub enum ItemManagerError {
EntityGatewayError,
NoSuchItemId(ClientItemId),
NoCharacter(CharacterEntityId),
CouldNotAddToInventory(FloorItem),
//ItemBelongsToOtherPlayer,
Idunnoman,
}
pub struct ItemManager {
id: usize,
active_to_entity: HashMap<ActiveItemId, ActiveItemEntityId>,
id_counter: u32,
character_inventory: HashMap<CharacterEntityId, Vec<InventoryItem>>,
character_floor: HashMap<CharacterEntityId, Vec<FloorItem>>,
character_item_id_counter: HashMap<CharacterEntityId, u32>,
character_room: HashMap<CharacterEntityId, RoomId>,
room_floor: HashMap<RoomId, Vec<FloorItem>>,
room_item_id_counter: HashMap<RoomId, u32>,
}
impl ItemManager {
pub fn new() -> ItemManager {
ItemManager {
id: 0,
active_to_entity: HashMap::new()
id_counter: 0,
character_inventory: HashMap::new(),
character_floor: HashMap::new(),
character_item_id_counter: HashMap::new(),
character_room: HashMap::new(),
room_floor: HashMap::new(),
room_item_id_counter: HashMap::new(),
}
}
fn next_id(&mut self) -> ActiveItemId {
self.id += 1;
ActiveItemId(self.id as u32)
pub fn next_global_item_id(&mut self) -> ClientItemId {
self.id_counter += 1;
ClientItemId(self.id_counter)
}
pub fn get_character_inventory<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) -> CharacterInventory {
pub fn next_drop_item_id(&mut self, room_id: RoomId) -> ClientItemId {
let next_id = self.room_item_id_counter.entry(room_id).or_insert(0xF0000000);
*next_id += 1;
ClientItemId(*next_id)
}
// TODO: Result
pub fn load_character<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) {
let items = entity_gateway.get_items_by_character(&character);
let inventory_items = items.into_iter()
.filter_map(|item| {
@ -190,18 +213,151 @@ impl ItemManager {
})
.into_iter()
.map(|(_slot, (held_item, entity_id, equipped))| {
let id = self.next_id();
self.active_to_entity.insert(id, entity_id);
let id = self.next_global_item_id();
InventoryItem {
id: id,
entity_id: entity_id,
item_id: id,
item: held_item,
equipped: equipped,
}
});
CharacterInventory(inventory_items.take(30).collect())
let k = inventory_items.take(30).collect();
self.character_inventory.insert(character.id, k);
}
pub fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) {
let base_id = ((area_client.local_client.id() as u32) << 21) | 0x10000;
let inventory = self.character_inventory.get_mut(&character.id).unwrap();
for (i, item) in inventory.iter_mut().enumerate() {
item.item_id = ClientItemId(base_id + i as u32);
}
self.character_room.insert(character.id, room_id);
self.character_floor.insert(character.id, Vec::new());
self.room_floor.entry(room_id).or_insert(Vec::new());
self.character_item_id_counter.insert(character.id, base_id + inventory.len() as u32);
}
pub fn get_character_inventory(&self, character: &CharacterEntity) -> Result<CharacterInventory, ItemManagerError> {
Ok(CharacterInventory(self.character_inventory.get(&character.id)
.ok_or(ItemManagerError::NoCharacter(character.id))?))
}
pub fn remove_character_from_room(&mut self, character: &CharacterEntity) {
self.character_inventory.remove(&character.id);
self.character_floor.remove(&character.id);
self.character_room.remove(&character.id)
.as_ref()
.map(|room| {
if self.character_room.iter().find(|(_, r)| *r == room).is_none() {
self.room_floor.remove(room);
}
});
}
pub fn get_inventory_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result<InventoryItem, ItemManagerError> {
let inventory = self.character_inventory.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
inventory.iter()
.filter(|item| {
item.item_id == item_id
})
.nth(0)
.ok_or(ItemManagerError::NoSuchItemId(item_id))
.map(Clone::clone)
}
pub fn get_floor_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result<FloorItem, ItemManagerError> {
let floor = self.character_floor.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
let room = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
let shared_floor = self.room_floor.get(room).ok_or(ItemManagerError::NoCharacter(character.id))?;
floor.iter()
.chain(shared_floor.iter())
.filter(|item| {
item.item_id == item_id
})
.nth(0)
.ok_or(ItemManagerError::NoSuchItemId(item_id))
.map(Clone::clone)
}
pub fn character_picks_up_item<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, floor_item: FloorItem) -> Result<(), ItemManagerError> {
let local_floor = self.character_floor.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
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))?;
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
});
}
else if let Some(_) = shared_floor.iter().find(|i| i.item_id == floor_item.item_id) {
shared_floor.retain(|item| {
item.item_id != floor_item.item_id
});
}
else {
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 {
entity_id: floor_item.entity_id,
item_id: floor_item.item_id,
item: HeldItemType::Individual(item.clone()),
equipped: false,
};
if let ActiveItemEntityId::Individual(item_id) = &inventory_item.entity_id {
entity_gateway.save_item(&ItemEntity {
id: *item_id,
item: item,
location: ItemLocation::Inventory {
character_id: character.id,
slot: inventory.len(),
equipped: false,
},
}); // TODO: error check
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,
};
if let ActiveItemEntityId::Stacked(item_ids) = &inventory_item.entity_id {
for item_id in item_ids {
entity_gateway.save_item(&ItemEntity {
id: *item_id,
item: ItemDetail::Tool(tool),
location: ItemLocation::Inventory {
character_id: character.id,
slot: inventory.len(),
equipped: false,
},
}); // TODO: error check
};
inventory.push(inventory_item);
} // else something went very wrong TODO: log it
},
FloorItemType::Meseta(meseta) => {
character.meseta += meseta.0;
entity_gateway.save_character(&character);
}
}
pub fn drop_item_on_local_floor<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<FloorItem, ItemManagerError> {
Ok(())
}
pub fn enemy_drop_item_on_local_floor<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<&FloorItem, ItemManagerError> {
let item = match item_drop.item {
ItemDropType::Weapon(w) => FloorItemType::Individual(ItemDetail::Weapon(w)),
ItemDropType::Armor(w) => FloorItemType::Individual(ItemDetail::Armor(w)),
@ -215,7 +371,7 @@ impl ItemManager {
ItemDropType::Meseta(m) => FloorItemType::Meseta(Meseta(m))
};
let active_entity_ids = match &item {
let entity_id = match &item {
FloorItemType::Individual(i) => {
let entity = entity_gateway.create_item(NewItemEntity {
item: i.clone(),
@ -250,70 +406,86 @@ impl ItemManager {
FloorItemType::Meseta(m) => ActiveItemEntityId::Meseta(m.clone()),
};
let id = self.next_id();
self.active_to_entity.insert(id, active_entity_ids);
Ok(FloorItem {
id: id,
let room_id = *self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
let item_id = self.next_drop_item_id(room_id);
let floor_item = FloorItem {
entity_id: entity_id,
item_id: item_id,
item: item,
map_area: item_drop.map_area,
x: item_drop.x,
y: item_drop.y,
z: item_drop.z,
})
};
self.character_floor.entry(character.id).or_insert(Vec::new()).push(floor_item);
self.character_floor.get(&character.id).ok_or(ItemManagerError::Idunnoman)?.last().ok_or(ItemManagerError::Idunnoman)
}
pub fn move_item_from_floor_to_inventory<EG: EntityGateway>(&mut self, entity_gateway: &mut EG, client: &mut ClientState, floor_item: FloorItem) -> Result<(), ItemManagerError> {
match floor_item.item {
FloorItemType::Individual(item) => {
let inventory_item = InventoryItem {
id: floor_item.id,
item: HeldItemType::Individual(item.clone()),
equipped: false,
pub fn player_drop_item_on_shared_floor<EG: EntityGateway>(&mut self,
entity_gateway: &mut EG,
character: &CharacterEntity,
inventory_item: InventoryItem,
item_drop_location: (MapArea, f32, f32, f32))
-> Result<(), ItemManagerError> {
let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?;
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))?;
inventory
.drain_filter(|i| i.item_id == inventory_item.item_id)
.nth(0)
.ok_or(ItemManagerError::NoSuchItemId(inventory_item.item_id))?;
let room_floor_item = FloorItem {
entity_id: inventory_item.entity_id,
item_id: inventory_item.item_id,
item: match inventory_item.item {
HeldItemType::Individual(item) => FloorItemType::Individual(item),
HeldItemType::Stacked(tool, count) => FloorItemType::Stacked(tool, count),
},
map_area: item_drop_location.0,
x: item_drop_location.1,
y: item_drop_location.2,
z: item_drop_location.3,
};
let item_entity_id = self.active_to_entity.get(&floor_item.id).unwrap(); // TODO: unwrap
if let ActiveItemEntityId::Individual(item_id) = item_entity_id {
let slot = client.inventory.add_item(inventory_item).unwrap(); // TODO: unwrap
match &room_floor_item.item {
FloorItemType::Individual(item) => {
if let ActiveItemEntityId::Individual(item_id) = &room_floor_item.entity_id {
entity_gateway.save_item(&ItemEntity {
id: *item_id,
item: item,
location: ItemLocation::Inventory {
character_id: client.character.id,
slot: slot,
equipped: false,
},
item: item.clone(),
location: ItemLocation::SharedFloor {
map_area: item_drop_location.0,
x: item_drop_location.1,
y: item_drop_location.2,
z: item_drop_location.3,
}
}); // TODO: error check
} // else something went very wrong TODO: log it
} // else something went very wrong: TODO: log it
},
FloorItemType::Stacked(tool, usize) => {
let inventory_item = InventoryItem {
id: floor_item.id,
item: HeldItemType::Stacked(tool, usize),
equipped: false,
};
let item_entity_id = self.active_to_entity.get(&floor_item.id).unwrap(); // TODO: unwrap
if let ActiveItemEntityId::Stacked(item_ids) = item_entity_id {
let slot = client.inventory.add_item(inventory_item).unwrap(); // TODO: unwrap
FloorItemType::Stacked(tool, _count) => {
if let ActiveItemEntityId::Stacked(item_ids) = &room_floor_item.entity_id {
for item_id in item_ids {
entity_gateway.save_item(&ItemEntity {
id: *item_id,
item: ItemDetail::Tool(tool),
location: ItemLocation::Inventory {
character_id: client.character.id,
slot: slot,
equipped: false,
item: ItemDetail::Tool(*tool),
location: ItemLocation::SharedFloor {
map_area: item_drop_location.0,
x: item_drop_location.1,
y: item_drop_location.2,
z: item_drop_location.3,
},
}); // TODO: error check
}
} // else something went very wrong TODO: log it
},
FloorItemType::Meseta(meseta) => {
client.character.meseta += meseta.0;
entity_gateway.save_character(&client.character);
}
_ => {}, // can meseta get here?
}
shared_floor.push(room_floor_item);
Ok(())
}
}

3
src/ship/location.rs

@ -14,7 +14,8 @@ pub enum AreaType {
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct LobbyId(pub usize);
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
pub struct RoomId(pub usize);
impl LobbyId {

7
src/ship/packet/builder/lobby.rs

@ -4,19 +4,21 @@ use crate::common::leveltable::CharacterLevelTable;
use crate::ship::ship::{ShipError, Clients};
use crate::ship::location::{ClientLocation, LobbyId, ClientLocationError};
use crate::ship::packet::builder::{player_info};
use crate::ship::items::ItemManager;
pub fn join_lobby(id: ClientId,
lobby: LobbyId,
client_location: &ClientLocation,
clients: &Clients,
item_manager: &ItemManager,
level_table: &CharacterLevelTable)
-> Result<JoinLobby, ShipError> {
let lobby_clients = client_location.get_clients_in_lobby(lobby).map_err(|err| -> ClientLocationError { err.into() })?;
let playerinfo = lobby_clients.iter()
.map(|area_client| {
let client = clients.get(&area_client.client).ok_or(ShipError::ClientNotFound(area_client.client)).unwrap();
player_info(0x100, &client, area_client, level_table)
player_info(0x100, &client, area_client, item_manager, level_table)
});
let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id)).unwrap();
@ -38,6 +40,7 @@ pub fn add_to_lobby(id: ClientId,
lobby: LobbyId,
client_location: &ClientLocation,
clients: &Clients,
item_manager: &ItemManager,
level_table: &CharacterLevelTable)
-> Result<AddToLobby, ShipError> {
let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id)).unwrap();
@ -52,7 +55,7 @@ pub fn add_to_lobby(id: ClientId,
block: client.block as u16,
event: 0,
padding: 0,
playerinfo: player_info(0x100, &client, &area_client, level_table),
playerinfo: player_info(0x100, &client, &area_client, item_manager, level_table),
})
}

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

@ -17,7 +17,7 @@ pub fn item_drop(client: u8, target: u8, item_drop: &FloorItem) -> Result<ItemDr
z: item_drop.z,
y: item_drop.y,
item_bytes: item_bytes[0..12].try_into()?,
item_id: item_drop.id.0,
item_id: item_drop.item_id.0,
item_bytes2: item_bytes[12..16].try_into()?,
unknown2: 0,
})
@ -29,7 +29,7 @@ pub fn create_item(area_client: AreaClient, item: &FloorItem) -> Result<CreateIt
client: area_client.local_client.id(),
target: 0,
item_data: bytes[0..12].try_into()?,
item_id: item.id.0,
item_id: item.item_id.0,
item_data2: bytes[12..16].try_into()?,
unknown: 0,
})
@ -43,6 +43,6 @@ pub fn remove_item_from_floor(area_client: AreaClient, item: &FloorItem) -> Resu
unknown: 0,
area: item.map_area.area_value(),
unknown2: 0,
item_id: item.id.0,
item_id: item.item_id.0,
})
}

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

@ -8,6 +8,7 @@ use crate::common::leveltable::CharacterLevelTable;
use crate::ship::character::CharacterBytesBuilder;
use crate::ship::ship::ClientState;
use crate::ship::location::AreaClient;
use crate::ship::items::ItemManager;
pub fn player_header(tag: u32, client: &ClientState, area_client: &AreaClient) -> PlayerHeader {
PlayerHeader {
@ -20,8 +21,9 @@ pub fn player_header(tag: u32, client: &ClientState, area_client: &AreaClient) -
}
}
pub fn player_info(tag: u32, client: &ClientState, area_client: &AreaClient, level_table: &CharacterLevelTable) -> PlayerInfo {
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 character = CharacterBytesBuilder::new()
.character(&client.character)
.stats(&stats)
@ -30,11 +32,11 @@ pub fn player_info(tag: u32, client: &ClientState, area_client: &AreaClient, lev
PlayerInfo {
header: player_header(tag, client, area_client),
inventory: Inventory {
item_count: client.inventory.count() as u8,
item_count: inventory.count() as u8,
hp_mats_used: 0, // TODO: materials
tp_mats_used: 0, // TODO: materials
language: 0, // TODO: account language
items: client.inventory.as_client_inventory_items(),
items: inventory.as_client_inventory_items(),
},
character: character,
}

4
src/ship/packet/builder/room.rs

@ -4,6 +4,7 @@ use crate::common::leveltable::CharacterLevelTable;
use crate::ship::ship::{ShipError, ClientState, Clients};
use crate::ship::location::{ClientLocation, RoomId, AreaClient, ClientLocationError};
use crate::ship::room::RoomState;
use crate::ship::items::ItemManager;
use crate::ship::packet::builder::{player_header, player_info};
pub fn join_room(id: ClientId,
@ -51,6 +52,7 @@ pub fn add_to_room(_id: ClientId,
client: &ClientState,
area_client: &AreaClient,
leader: &AreaClient,
item_manager: &ItemManager,
level_table: &CharacterLevelTable,
_room_id: RoomId,
)
@ -65,7 +67,7 @@ pub fn add_to_room(_id: ClientId,
block: 0,
event: 0,
padding: 0,
playerinfo: player_info(0x10000, client, &area_client, level_table),
playerinfo: player_info(0x10000, client, &area_client, item_manager, level_table),
})
}

4
src/ship/packet/handler/auth.rs

@ -26,9 +26,9 @@ pub fn validate_login<EG: EntityGateway>(id: ClientId,
.clone();
let settings = entity_gateway.get_user_settings_by_user(&user)
.ok_or(ShipError::ClientNotFound(id))?;
let inventory = item_manager.get_character_inventory(entity_gateway, &character);
clients.insert(id, ClientState::new(user, settings, character, inventory, pkt.session));
item_manager.load_character(entity_gateway, &character);
clients.insert(id, ClientState::new(user, settings, character, pkt.session));
vec![SendShipPacket::LoginResponse(response), SendShipPacket::ShipBlockList(ShipBlockList::new(&&ship_name, 3))]
},
Err(err) => {

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

@ -5,7 +5,7 @@ use crate::common::serverstate::ClientId;
use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms};
use crate::ship::location::{ClientLocation, ClientLocationError};
use crate::ship::drops::ItemDrop;
use crate::ship::items::{ItemManager, ItemManagerError, FloorItemType};
use crate::ship::items::{ItemManager, FloorItemType, ClientItemId};
use crate::entity::gateway::EntityGateway;
use libpso::utf8_to_utf16_array;
use crate::ship::packet::builder;
@ -84,9 +84,8 @@ where
item: item_drop_type,
};
let client = clients.get_mut(&area_client.client).ok_or(ShipError::ClientNotFound(area_client.client))?;
let floor_item = item_manager.drop_item_on_local_floor(entity_gateway, &client.character, item_drop).unwrap(); // TODO: unwrap
let floor_item = item_manager.enemy_drop_item_on_local_floor(entity_gateway, &client.character, item_drop).unwrap(); // TODO: unwrap
let item_drop_msg = builder::message::item_drop(request_item.client, request_item.target, &floor_item)?;
client.floor_items.push(floor_item);
Ok((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg)))))
})
.filter_map(|item_drop_pkt| {
@ -103,81 +102,40 @@ pub fn pickup_item<EG>(id: ClientId,
entity_gateway: &mut EG,
client_location: &ClientLocation,
clients: &mut Clients,
rooms: &mut Rooms,
item_manager: &mut ItemManager)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
where
EG: EntityGateway
{
#[derive(Copy, Clone)]
enum ItemFloor {
Local,
Shared
}
let mut client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?;
let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?;
let room = rooms.get_mut(room_id.0)
.ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
.as_mut()
.ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?;
let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?;
let (item_index, (_, floor)) = client.floor_items.iter()
.zip(std::iter::repeat(ItemFloor::Local))
.enumerate()
.filter(|(_, (item, _))| item.id.0 == pickup_item.item_id)
.next()
.or_else(|| {
room.floor_items.iter()
.zip(std::iter::repeat(ItemFloor::Shared))
.enumerate()
.filter(|(_, (item, _))| item.id.0 == pickup_item.item_id)
.next()
})
.ok_or(ShipError::PickUpInvalidItemId(pickup_item.item_id))?;
let item = match floor {
ItemFloor::Local => client.floor_items.remove(item_index),
ItemFloor::Shared => room.floor_items.remove(item_index),
};
let item = 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.item {
FloorItemType::Meseta(_) => None,
_ => Some(builder::message::create_item(area_client, &item)?),
};
match item_manager.move_item_from_floor_to_inventory(entity_gateway, &mut client, item) {
match item_manager.character_picks_up_item(entity_gateway, &mut client.character, item) {
Ok(_) => {
Ok(Box::new(Vec::new().into_iter()
.chain(clients_in_area.clone().into_iter()
.filter(move |c| {
match floor {
ItemFloor::Local => c.client == id,
ItemFloor::Shared => true,
}
})
.map(move |c| {
(c.client, SendShipPacket::Message(Message::new(GameMessage::RemoveItemFromFloor(remove_item.clone()))))
}))
.chain(clients_in_area.into_iter().filter_map(move |c| {
//(c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(create_item.clone()))))
.chain(clients_in_area.into_iter().
filter_map(move |c| {
create_item.clone().map(|ci| (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(ci)))))
}
)))
)
},
Err(err) => {
// inventory full, probably
if let ItemManagerError::CouldNotAddToInventory(item) = err {
match floor {
ItemFloor::Local => client.floor_items.push(item),
ItemFloor::Shared => room.floor_items.push(item),
}
}
warn!("character {:?} could not pick up item: {:?}", client.character.id, err);
Ok(Box::new(None.into_iter()))
}
},
}
}

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

@ -4,12 +4,15 @@ use crate::common::leveltable::CharacterLevelTable;
use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms};
use crate::ship::character::{FullCharacterBytesBuilder};
use crate::ship::location::{ClientLocation, LobbyId, RoomLobby, ClientLocationError};
//use crate::ship::items::;
use crate::ship::packet;
use crate::ship::items::ItemManager;
// this function needs a better home
pub fn block_selected(id: ClientId,
pkt: &MenuSelect,
clients: &mut Clients,
item_manager: &ItemManager,
level_table: &CharacterLevelTable)
-> Result<Vec<SendShipPacket>, ShipError> {
let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
@ -17,11 +20,13 @@ 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 fc = FullCharacterBytesBuilder::new()
.character(&client.character)
.stats(&stats)
.level(level)
.inventory(&client.inventory)
.inventory(&inventory)
.key_config(&client.settings.settings.key_config)
.joystick_config(&client.settings.settings.joystick_config)
.symbol_chat(&client.settings.settings.symbol_chats)
@ -41,11 +46,12 @@ pub fn send_player_to_lobby(id: ClientId,
_pkt: &CharData,
client_location: &mut ClientLocation,
clients: &Clients,
item_manager: &ItemManager,
level_table: &CharacterLevelTable)
-> Result<Vec<(ClientId, SendShipPacket)>, ShipError> {
let lobby = client_location.add_client_to_next_available_lobby(id, LobbyId(0)).map_err(|_| ShipError::TooManyClients)?;
let join_lobby = packet::builder::lobby::join_lobby(id, lobby, client_location, clients, level_table)?;
let addto = packet::builder::lobby::add_to_lobby(id, lobby, client_location, clients, level_table)?;
let join_lobby = packet::builder::lobby::join_lobby(id, lobby, client_location, clients, item_manager, level_table)?;
let addto = packet::builder::lobby::add_to_lobby(id, lobby, client_location, clients, item_manager, level_table)?;
let neighbors = client_location.get_client_neighbors(id).unwrap();
Ok(vec![(id, SendShipPacket::JoinLobby(join_lobby))]
.into_iter()
@ -57,9 +63,11 @@ pub fn change_lobby(id: ClientId,
requested_lobby: u32,
client_location: &mut ClientLocation,
clients: &Clients,
item_manager: &mut ItemManager,
level_table: &CharacterLevelTable,
ship_rooms: &mut Rooms)
-> Result<Vec<(ClientId, SendShipPacket)>, ShipError> {
let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?;
let prev_area = client_location.get_area(id).map_err(|err| -> ClientLocationError {err.into()})?;
match prev_area {
RoomLobby::Lobby(old_lobby) => {
@ -71,6 +79,7 @@ pub fn change_lobby(id: ClientId,
if client_location.get_client_neighbors(id).map_err(|err| -> ClientLocationError {err.into()})?.len() == 0 {
ship_rooms[old_room.0] = None;
}
item_manager.remove_character_from_room(&client.character);
},
}
let leave_lobby = packet::builder::lobby::remove_from_lobby(id, client_location)?;
@ -87,8 +96,8 @@ pub fn change_lobby(id: ClientId,
}
}
}
let join_lobby = packet::builder::lobby::join_lobby(id, lobby, client_location, clients, level_table)?;
let addto = packet::builder::lobby::add_to_lobby(id, lobby, client_location, clients, level_table)?;
let join_lobby = packet::builder::lobby::join_lobby(id, lobby, client_location, clients, item_manager, level_table)?;
let addto = packet::builder::lobby::add_to_lobby(id, lobby, client_location, clients, item_manager, level_table)?;
let neighbors = client_location.get_client_neighbors(id).unwrap();
Ok(vec![(id, SendShipPacket::JoinLobby(join_lobby))]
.into_iter()

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

@ -1,8 +1,12 @@
use log::warn;
use libpso::packet::ship::*;
use libpso::packet::messages::*;
use crate::entity::gateway::EntityGateway;
use crate::common::serverstate::ClientId;
use crate::ship::ship::{SendShipPacket, Rooms};
use crate::ship::location::{ClientLocation, RoomLobby};
use crate::ship::ship::{SendShipPacket, ShipError, Rooms, Clients};
use crate::ship::location::{ClientLocation, ClientLocationError, RoomLobby};
use crate::ship::map::{MapArea};
use crate::ship::items::{ItemManager, ClientItemId};
pub fn request_exp(id: ClientId,
request_exp: &RequestExp,
@ -19,3 +23,31 @@ pub fn request_exp(id: ClientId,
};
Box::new(None.into_iter())
}
pub fn player_drop_item<EG>(id: ClientId,
player_drop_item: &PlayerDropItem2,
entity_gateway: &mut EG,
client_location: &ClientLocation,
clients: &mut Clients,
rooms: &mut Rooms,
item_manager: &mut ItemManager)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError>
where
EG: EntityGateway
{
let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?;
let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?;
let room = rooms.get_mut(room_id.0)
.ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?
.as_mut()
.ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?;
let area = MapArea::from_value(&room.mode.episode(), player_drop_item.area as u32)?;
let item = item_manager.get_inventory_item_by_id(&client.character, ClientItemId(player_drop_item.item_id))?;
item_manager.player_drop_item_on_shared_floor(entity_gateway, &client.character, item, (area, player_drop_item.x, player_drop_item.y, player_drop_item.z))?;
let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?;
let pdi = player_drop_item.clone();
Ok(Box::new(clients_in_area.into_iter()
.map(move |c| {
(c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerDropItem2(pdi.clone()))))
})))
}

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

@ -5,11 +5,13 @@ use crate::ship::ship::{SendShipPacket, ShipError, Rooms, Clients};
use crate::ship::location::{ClientLocation, RoomId, RoomLobby, ClientLocationError};
use crate::ship::packet::builder;
use crate::ship::room;
use crate::ship::items::ItemManager;
pub fn create_room(id: ClientId,
create_room: &CreateRoom,
client_location: &mut ClientLocation,
clients: &mut Clients,
item_manager: &mut ItemManager,
rooms: &mut Rooms)
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError> {
let area = client_location.get_area(id).unwrap();
@ -21,6 +23,8 @@ pub fn create_room(id: ClientId,
let mut room = room::RoomState::from_create_room(create_room, client.character.section_id).unwrap();
room.bursting = true;
item_manager.add_character_to_room(room_id, &client.character, area_client);
let join_room = builder::room::join_room(id, clients, client_location, room_id, &room)?;
rooms[room_id.0] = Some(room);
@ -54,6 +58,7 @@ pub fn join_room(id: ClientId,
pkt: &MenuSelect,
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> {
@ -73,11 +78,13 @@ pub fn join_room(id: ClientId,
let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?;
let leader = client_location.get_room_leader(room_id).map_err(|err| -> ClientLocationError { err.into() })?;
let join_room = builder::room::join_room(id, clients, client_location, room_id, &room)?;
let add_to = builder::room::add_to_room(id, &client, &area_client, &leader, level_table, room_id)?;
let add_to = builder::room::add_to_room(id, &client, &area_client, &leader, item_manager, level_table, room_id)?;
let room = rooms.get_mut(room_id.0).unwrap().as_mut().unwrap();
room.bursting = true;
item_manager.add_character_to_room(room_id, &client.character, area_client);
let mut result: Box<dyn Iterator<Item=(ClientId, SendShipPacket)> + Send> = Box::new(
vec![(id, SendShipPacket::JoinRoom(join_room))]
.into_iter()

15
src/ship/room.rs

@ -1,11 +1,9 @@
#![allow(dead_code)]
use std::convert::{From, Into, TryFrom, TryInto};
use rand::Rng;
use crate::ship::map::Maps;
use crate::ship::drops::DropTable;
use crate::entity::character::SectionID;
use crate::ship::items::FloorItem;
#[derive(Debug)]
pub enum RoomCreationError {
@ -147,24 +145,16 @@ pub struct RoomState {
pub mode: RoomMode,
pub name: String,
pub password: [u16; 16],
//pub maps: [u32; 0x20],
pub maps: Maps,
pub drop_table: Box<DropTable<rand_chacha::ChaCha20Rng>>,
pub section_id: SectionID,
pub random_seed: u32,
pub bursting: bool,
pub floor_items: Vec<FloorItem>,
// items on ground
// enemy info
}
impl RoomState {
/*fn new(mode: RoomMode) -> Room {
Room {
mode: mode,
}
}*/
pub fn get_flags_for_room_list(&self) -> u8 {
let mut flags = 0u8;
@ -233,11 +223,6 @@ impl RoomState {
section_id: section_id,
drop_table: Box::new(DropTable::new(room_mode.episode(), room_mode.difficulty(), section_id)),
bursting: false,
floor_items: Vec::new(),
})
}
pub fn map_headers(&self) -> [u32; 0x20] {
self.maps.map_headers()
}
}

46
src/ship/ship.rs

@ -25,7 +25,7 @@ use crate::ship::location::{ClientLocation, RoomLobby, MAX_ROOMS, ClientLocation
use crate::ship::items;
use crate::ship::room;
use crate::ship::map::MapsError;
use crate::ship::map::{MapsError, MapAreaError};
use crate::ship::packet::handler;
pub const SHIP_PORT: u16 = 23423;
@ -41,11 +41,14 @@ pub enum ShipError {
TooManyClients,
ClientLocationError(#[from] ClientLocationError),
MapsError(#[from] MapsError),
MapAreaError(#[from] MapAreaError),
InvalidRoom(u32),
MonsterAlreadyDroppedItem(ClientId, u16),
SliceError(#[from] std::array::TryFromSliceError),
ItemError, // TODO: refine this
PickUpInvalidItemId(u32),
DropInvalidItemId(u32),
ItemManagerError(#[from] items::ItemManagerError)
}
#[derive(Debug)]
@ -158,22 +161,16 @@ pub struct ClientState {
pub character: CharacterEntity,
session: Session,
//guildcard: GuildCard,
pub inventory: items::CharacterInventory,
//bank: Bank,
pub floor_items: Vec<items::FloorItem>,
pub block: u32,
}
impl ClientState {
pub fn new(user: UserAccountEntity, settings: UserSettingsEntity, character: CharacterEntity, inventory: items::CharacterInventory, /*bank: Bank,*/ session: Session) -> ClientState {
pub fn new(user: UserAccountEntity, settings: UserSettingsEntity, character: CharacterEntity, session: Session) -> ClientState {
ClientState {
user: user,
settings: settings,
character: character,
session: session,
inventory: inventory,
//bank: bank,
floor_items: Vec::new(),
block: 1,
}
}
@ -187,7 +184,7 @@ pub struct ShipServerState<EG: EntityGateway> {
level_table: CharacterLevelTable,
name: String,
rooms: Rooms,
item_database: items::ItemManager,
item_manager: items::ItemManager,
}
impl<EG: EntityGateway> ShipServerState<EG> {
@ -199,7 +196,7 @@ impl<EG: EntityGateway> ShipServerState<EG> {
level_table: CharacterLevelTable::new(),
name: "Sona-Nyl".into(),
rooms: [None; MAX_ROOMS],
item_database: items::ItemManager::new(),
item_manager: items::ItemManager::new(),
}
}
@ -208,6 +205,9 @@ impl<EG: EntityGateway> ShipServerState<EG> {
GameMessage::RequestExp(request_exp) => {
handler::message::request_exp(id, request_exp, &self.client_location, &self.rooms)
},
GameMessage::PlayerDropItem2(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()
},
_ => {
let cmsg = msg.clone();
Box::new(self.client_location.get_client_neighbors(id).unwrap().into_iter()
@ -225,10 +225,10 @@ impl<EG: EntityGateway> ShipServerState<EG> {
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_database).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).unwrap()
},
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.rooms, &mut self.item_database).unwrap()
handler::direct_message::pickup_item(id, pickup_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).unwrap()
},
_ => {
let cmsg = msg.clone();
@ -265,12 +265,12 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, ShipError> {
Ok(match pkt {
RecvShipPacket::Login(login) => {
Box::new(handler::auth::validate_login(id, login, &mut self.entity_gateway, &mut self.clients, &mut self.item_database, &self.name)?.into_iter().map(move |pkt| (id, pkt)))
Box::new(handler::auth::validate_login(id, login, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager, &self.name)?.into_iter().map(move |pkt| (id, pkt)))
},
RecvShipPacket::MenuSelect(menuselect) => {
match menuselect.menu {
BLOCK_MENU_ID => Box::new(handler::lobby::block_selected(id, menuselect, &mut self.clients, &self.level_table)?.into_iter().map(move |pkt| (id, pkt))),
ROOM_MENU_ID => handler::room::join_room(id, menuselect, &mut self.client_location, &mut self.clients, &self.level_table, &mut self.rooms)?,
BLOCK_MENU_ID => Box::new(handler::lobby::block_selected(id, menuselect, &mut self.clients, &self.item_manager, &self.level_table)?.into_iter().map(move |pkt| (id, pkt))),
ROOM_MENU_ID => handler::room::join_room(id, menuselect, &mut self.client_location, &mut self.clients, &mut self.item_manager, &self.level_table, &mut self.rooms)?,
_ => unreachable!(),
}
},
@ -282,14 +282,14 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
menu: room_password_req.menu,
item: room_password_req.item,
};
handler::room::join_room(id, &menuselect, &mut self.client_location, &mut self.clients, &self.level_table, &mut self.rooms)?
handler::room::join_room(id, &menuselect, &mut self.client_location, &mut self.clients, &mut self.item_manager, &self.level_table, &mut self.rooms)?
}
else {
Box::new(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("Incorrect password".into())))].into_iter())
}
},
RecvShipPacket::CharData(chardata) => {
Box::new(handler::lobby::send_player_to_lobby(id, chardata, &mut self.client_location, &self.clients, &self.level_table)?.into_iter())
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)
@ -301,7 +301,7 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
Box::new(handler::communication::player_chat(id, msg, &self.client_location, &self.clients)?.into_iter())
},
RecvShipPacket::CreateRoom(create_room) => {
handler::room::create_room(id, create_room, &mut self.client_location, &mut self.clients, &mut self.rooms)?
handler::room::create_room(id, create_room, &mut self.client_location, &mut self.clients, &mut self.item_manager, &mut self.rooms)?
},
RecvShipPacket::RoomNameRequest(_req) => {
handler::room::room_name_request(id, &self.client_location, &self.rooms)
@ -329,14 +329,15 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
handler::room::done_bursting(id, &self.client_location, &mut self.rooms)
},
RecvShipPacket::LobbySelect(pkt) => {
Box::new(handler::lobby::change_lobby(id, pkt.lobby, &mut self.client_location, &self.clients, &self.level_table, &mut self.rooms)?.into_iter())
Box::new(handler::lobby::change_lobby(id, pkt.lobby, &mut self.client_location, &self.clients, &mut self.item_manager, &self.level_table, &mut self.rooms)?.into_iter())
}
})
}
fn on_disconnect(&mut self, id: ClientId) -> Vec<(ClientId, SendShipPacket)> {
// TODO: don't unwrap!
let client = self.client_location.get_local_client(id).unwrap();
let client = self.clients.get(&id).unwrap();
let area_client = self.client_location.get_local_client(id).unwrap();
let neighbors = self.client_location.get_client_neighbors(id).unwrap();
let pkt = match self.client_location.get_area(id).unwrap() {
@ -345,15 +346,16 @@ impl<EG: EntityGateway> ServerState for ShipServerState<EG> {
self.rooms[room.0] = None;
}
let leader = self.client_location.get_room_leader(room).unwrap();
SendShipPacket::LeaveRoom(LeaveRoom::new(client.local_client.id(), leader.local_client.id()))
SendShipPacket::LeaveRoom(LeaveRoom::new(area_client.local_client.id(), leader.local_client.id()))
},
RoomLobby::Lobby(lobby) => {
let leader = self.client_location.get_lobby_leader(lobby).unwrap();
SendShipPacket::LeaveLobby(LeaveLobby::new(client.local_client.id(), leader.local_client.id()))
SendShipPacket::LeaveLobby(LeaveLobby::new(area_client.local_client.id(), leader.local_client.id()))
}
};
self.client_location.remove_client_from_area(id);
self.item_manager.remove_character_from_room(&client.character);
neighbors.into_iter().map(|n| {
(n.client, pkt.clone())

Loading…
Cancel
Save