killcounters, redbox mode, failing unseal test, and random debug strings
This commit is contained in:
parent
ca44dcf689
commit
117a2daa31
@ -186,6 +186,10 @@ impl<R: Rng + SeedableRng> DropTable<R> {
|
|||||||
pub fn get_box_drop(&mut self, map_area: &MapArea, object: &MapObject) -> Option<ItemDropType> {
|
pub fn get_box_drop(&mut self, map_area: &MapArea, object: &MapObject) -> Option<ItemDropType> {
|
||||||
self.box_table.get_drop(map_area, object, &mut self.rng)
|
self.box_table.get_drop(map_area, object, &mut self.rng)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_rare_drop(&mut self, map_area: &MapArea, monster: &MonsterType) -> Option<ItemDropType> {
|
||||||
|
self.rare_table.get_rare_drop(map_area, monster, &mut self.rng)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -205,4 +209,11 @@ mod test {
|
|||||||
.into_iter().choose(&mut rng).unwrap();
|
.into_iter().choose(&mut rng).unwrap();
|
||||||
DropTable::<rand_chacha::ChaCha20Rng>::new(episode, difficulty, section_id);
|
DropTable::<rand_chacha::ChaCha20Rng>::new(episode, difficulty, section_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sjs_drop() {
|
||||||
|
let mut drop_table = DropTable::<rand_chacha::ChaCha20Rng>::new(Episode::Two, Difficulty::Ultimate, SectionID::Skyly);
|
||||||
|
let drop = drop_table.get_rare_drop(&MapArea::Seaside, &MonsterType::GiGue);
|
||||||
|
println!("drop: {:?}", drop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,4 +167,14 @@ impl RareDropTable {
|
|||||||
}).next()
|
}).next()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_rare_drop<R: Rng>(&self, map_area: &MapArea, monster: &MonsterType, rng: &mut R) -> Option<ItemDropType> {
|
||||||
|
self.rates.get(monster)
|
||||||
|
.and_then(|drop_rates| {
|
||||||
|
drop_rates.iter()
|
||||||
|
.filter_map(|drop_rate| {
|
||||||
|
Some(self.apply_item_stats(map_area, drop_rate.item, rng))
|
||||||
|
}).next()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,6 +320,16 @@ impl Maps {
|
|||||||
self.object_data = objects;
|
self.object_data = objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_enemy_id_by_monster_type(&self, monster: MonsterType) -> Option<u16> {
|
||||||
|
let (id, _) = self.enemy_data
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_i, &m)| m.is_some())
|
||||||
|
.find(|(_i, &m)| m.unwrap().monster == monster)?;
|
||||||
|
|
||||||
|
Some(id as u16)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_rare_monster_list(&self) -> Vec<u16> {
|
pub fn get_rare_monster_list(&self) -> Vec<u16> {
|
||||||
let mut rare_monsters = vec![0xFFFF; 16];
|
let mut rare_monsters = vec![0xFFFF; 16];
|
||||||
let shiny: Vec<(usize, &Option<MapEnemy>)> = self.enemy_data.iter()
|
let shiny: Vec<(usize, &Option<MapEnemy>)> = self.enemy_data.iter()
|
||||||
|
@ -79,6 +79,7 @@ pub async fn request_item<EG>(id: ClientId,
|
|||||||
where
|
where
|
||||||
EG: EntityGateway
|
EG: EntityGateway
|
||||||
{
|
{
|
||||||
|
println!("src::ship::packet::handler::request_item() - requesting an item!");
|
||||||
let room_id = client_location.get_room(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)
|
let room = rooms.get_mut(room_id.0)
|
||||||
.ok_or(ShipError::InvalidRoom(room_id.0 as u32))?
|
.ok_or(ShipError::InvalidRoom(room_id.0 as u32))?
|
||||||
@ -86,7 +87,9 @@ where
|
|||||||
.ok_or(ShipError::InvalidRoom(room_id.0 as u32))?;
|
.ok_or(ShipError::InvalidRoom(room_id.0 as u32))?;
|
||||||
|
|
||||||
let monster = room.maps.enemy_by_id(request_item.enemy_id as usize)?;
|
let monster = room.maps.enemy_by_id(request_item.enemy_id as usize)?;
|
||||||
|
println!("room id: {:?}, monster: {:?}", room_id, monster);
|
||||||
if monster.dropped_item {
|
if monster.dropped_item {
|
||||||
|
println!("monster {:?} already dropped an item!", monster);
|
||||||
return Err(ShipError::MonsterAlreadyDroppedItem(id, request_item.enemy_id).into())
|
return Err(ShipError::MonsterAlreadyDroppedItem(id, request_item.enemy_id).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,9 +97,16 @@ where
|
|||||||
|
|
||||||
let client_and_drop = clients_in_area.into_iter()
|
let client_and_drop = clients_in_area.into_iter()
|
||||||
.filter_map(|area_client| {
|
.filter_map(|area_client| {
|
||||||
room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| {
|
if room.redbox {
|
||||||
(area_client, item_drop_type)
|
println!("red box mode is currently enabled. dropping red box!");
|
||||||
})
|
room.drop_table.get_rare_drop(&monster.map_area, &monster.monster).map(|item_drop_type| {
|
||||||
|
(area_client, item_drop_type)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| {
|
||||||
|
(area_client, item_drop_type)
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut item_drop_packets = Vec::new();
|
let mut item_drop_packets = Vec::new();
|
||||||
|
@ -203,6 +203,7 @@ pub struct RoomState {
|
|||||||
pub rare_monster_table: Box<RareMonsterAppearTable>,
|
pub rare_monster_table: Box<RareMonsterAppearTable>,
|
||||||
pub quest_group: QuestCategoryType,
|
pub quest_group: QuestCategoryType,
|
||||||
pub quests: Vec<quests::QuestList>,
|
pub quests: Vec<quests::QuestList>,
|
||||||
|
pub redbox: bool,
|
||||||
// items on ground
|
// items on ground
|
||||||
// enemy info
|
// enemy info
|
||||||
}
|
}
|
||||||
@ -316,6 +317,11 @@ impl RoomState {
|
|||||||
map_areas: MapAreaLookup::new(&room_mode.episode()),
|
map_areas: MapAreaLookup::new(&room_mode.episode()),
|
||||||
quest_group: QuestCategoryType::Standard,
|
quest_group: QuestCategoryType::Standard,
|
||||||
quests: room_quests,
|
quests: room_quests,
|
||||||
|
redbox: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_redbox_mode(&mut self) {
|
||||||
|
self.redbox = !self.redbox;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -542,6 +542,7 @@ impl<EG: EntityGateway> ShipServerState<EG> {
|
|||||||
handler::direct_message::guildcard_send(id, guildcard_send, target, &block.client_location, &self.clients)
|
handler::direct_message::guildcard_send(id, guildcard_send, target, &block.client_location, &self.clients)
|
||||||
},
|
},
|
||||||
GameMessage::RequestItem(request_item) => {
|
GameMessage::RequestItem(request_item) => {
|
||||||
|
println!("someone requested an item from the ship!");
|
||||||
handler::direct_message::request_item(id, request_item, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut block.rooms, &mut self.item_manager).await?
|
handler::direct_message::request_item(id, request_item, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut block.rooms, &mut self.item_manager).await?
|
||||||
},
|
},
|
||||||
GameMessage::PickupItem(pickup_item) => {
|
GameMessage::PickupItem(pickup_item) => {
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
use elseware::common::serverstate::{ClientId, ServerState};
|
use elseware::common::serverstate::{ClientId, ServerState};
|
||||||
use elseware::entity::gateway::EntityGateway;
|
use elseware::entity::gateway::EntityGateway;
|
||||||
use elseware::entity::account::{UserAccountEntity, NewUserAccountEntity, NewUserSettingsEntity};
|
use elseware::entity::account::{UserAccountEntity, NewUserAccountEntity, NewUserSettingsEntity};
|
||||||
use elseware::entity::character::{CharacterEntity, NewCharacterEntity};
|
|
||||||
use elseware::entity::item::{Meseta, BankName};
|
use elseware::entity::item::{Meseta, BankName};
|
||||||
|
use elseware::entity::character::{CharacterEntity, NewCharacterEntity, SectionID};
|
||||||
use elseware::ship::ship::{ShipServerState, RecvShipPacket};
|
use elseware::ship::ship::{ShipServerState, RecvShipPacket};
|
||||||
use elseware::ship::room::Difficulty;
|
use elseware::ship::room::Difficulty;
|
||||||
|
|
||||||
@ -34,6 +34,26 @@ pub async fn new_user_character<EG: EntityGateway>(entity_gateway: &mut EG, user
|
|||||||
(user, character)
|
(user, character)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn new_user_character_with_sid<EG: EntityGateway>(entity_gateway: &mut EG, username: &str, password: &str, sid: SectionID) -> (UserAccountEntity, CharacterEntity) {
|
||||||
|
let new_user = NewUserAccountEntity {
|
||||||
|
email: format!("{}@pso.com", username),
|
||||||
|
username: username.into(),
|
||||||
|
password: bcrypt::hash(password, 5).unwrap(),
|
||||||
|
guildcard: 1,
|
||||||
|
activated: true,
|
||||||
|
..NewUserAccountEntity::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let user = entity_gateway.create_user(new_user).await.unwrap();
|
||||||
|
let new_settings = NewUserSettingsEntity::new(user.id);
|
||||||
|
let _settings = entity_gateway.create_user_settings(new_settings).await.unwrap();
|
||||||
|
let mut new_character = NewCharacterEntity::new(user.id);
|
||||||
|
new_character.section_id = sid;
|
||||||
|
let character = entity_gateway.create_character(new_character).await.unwrap();
|
||||||
|
|
||||||
|
(user, character)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn log_in_char<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId, username: &str, password: &str) {
|
pub async fn log_in_char<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId, username: &str, password: &str) {
|
||||||
let username = username.to_string();
|
let username = username.to_string();
|
||||||
let password = password.to_string();
|
let password = password.to_string();
|
||||||
@ -84,6 +104,21 @@ pub async fn create_room_with_difficulty<EG: EntityGateway>(ship: &mut ShipServe
|
|||||||
ship.handle(id, &RecvShipPacket::DoneBursting(DoneBursting {})).await.unwrap().for_each(drop);
|
ship.handle(id, &RecvShipPacket::DoneBursting(DoneBursting {})).await.unwrap().for_each(drop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_ep2_room_with_difficulty<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId, name: &str, password: &str, difficulty: Difficulty) {
|
||||||
|
ship.handle(id, &RecvShipPacket::CreateRoom(CreateRoom {
|
||||||
|
unknown: [0; 2],
|
||||||
|
name: utf8_to_utf16_array!(name, 16),
|
||||||
|
password: utf8_to_utf16_array!(password, 16),
|
||||||
|
difficulty: difficulty.into(),
|
||||||
|
battle: 0,
|
||||||
|
challenge: 0,
|
||||||
|
episode: 2,
|
||||||
|
single_player: 0,
|
||||||
|
padding: [0; 3],
|
||||||
|
})).await.unwrap().for_each(drop);
|
||||||
|
ship.handle(id, &RecvShipPacket::DoneBursting(DoneBursting {})).await.unwrap().for_each(drop);
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn join_room<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId, room_id: u32) {
|
pub async fn join_room<EG: EntityGateway>(ship: &mut ShipServerState<EG>, id: ClientId, room_id: u32) {
|
||||||
ship.handle(id, &RecvShipPacket::MenuSelect(MenuSelect {
|
ship.handle(id, &RecvShipPacket::MenuSelect(MenuSelect {
|
||||||
menu: ROOM_MENU_ID,
|
menu: ROOM_MENU_ID,
|
||||||
|
@ -34,6 +34,7 @@ async fn test_equip_unit_from_equip_menu() {
|
|||||||
item::unit::Unit{
|
item::unit::Unit{
|
||||||
unit: item::unit::UnitType::KnightPower,
|
unit: item::unit::UnitType::KnightPower,
|
||||||
modifier: None,
|
modifier: None,
|
||||||
|
kills: None,
|
||||||
}),
|
}),
|
||||||
}).await.unwrap());
|
}).await.unwrap());
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ async fn test_equip_unit_from_equip_menu() {
|
|||||||
item::unit::Unit{
|
item::unit::Unit{
|
||||||
unit: item::unit::UnitType::KnightPower,
|
unit: item::unit::UnitType::KnightPower,
|
||||||
modifier: Some(item::unit::UnitModifier::Plus),
|
modifier: Some(item::unit::UnitModifier::Plus),
|
||||||
|
kills: None,
|
||||||
}),
|
}),
|
||||||
}).await.unwrap());
|
}).await.unwrap());
|
||||||
|
|
||||||
@ -111,6 +113,7 @@ async fn test_unequip_armor_with_units() {
|
|||||||
item::unit::Unit{
|
item::unit::Unit{
|
||||||
unit: item::unit::UnitType::KnightPower,
|
unit: item::unit::UnitType::KnightPower,
|
||||||
modifier: None,
|
modifier: None,
|
||||||
|
kills: None,
|
||||||
}),
|
}),
|
||||||
}).await.unwrap());
|
}).await.unwrap());
|
||||||
|
|
||||||
@ -120,6 +123,7 @@ async fn test_unequip_armor_with_units() {
|
|||||||
item::unit::Unit{
|
item::unit::Unit{
|
||||||
unit: item::unit::UnitType::KnightPower,
|
unit: item::unit::UnitType::KnightPower,
|
||||||
modifier: Some(item::unit::UnitModifier::Plus),
|
modifier: Some(item::unit::UnitModifier::Plus),
|
||||||
|
kills: None,
|
||||||
}),
|
}),
|
||||||
}).await.unwrap());
|
}).await.unwrap());
|
||||||
|
|
||||||
@ -179,6 +183,7 @@ async fn test_sort_items() {
|
|||||||
item::unit::Unit{
|
item::unit::Unit{
|
||||||
unit: item::unit::UnitType::KnightPower,
|
unit: item::unit::UnitType::KnightPower,
|
||||||
modifier: None,
|
modifier: None,
|
||||||
|
kills: None,
|
||||||
}),
|
}),
|
||||||
}).await.unwrap());
|
}).await.unwrap());
|
||||||
|
|
||||||
@ -188,6 +193,7 @@ async fn test_sort_items() {
|
|||||||
item::unit::Unit{
|
item::unit::Unit{
|
||||||
unit: item::unit::UnitType::KnightPower,
|
unit: item::unit::UnitType::KnightPower,
|
||||||
modifier: Some(item::unit::UnitModifier::Plus),
|
modifier: Some(item::unit::UnitModifier::Plus),
|
||||||
|
kills: None,
|
||||||
}),
|
}),
|
||||||
}).await.unwrap());
|
}).await.unwrap());
|
||||||
|
|
||||||
|
131
tests/test_unseal_items.rs
Normal file
131
tests/test_unseal_items.rs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/* TODO:
|
||||||
|
1. test to check if sjs/lame/limiter drop with Some() kill counter enabled
|
||||||
|
2. test to make sure other items drop with None kill counter
|
||||||
|
3. test kill counters get incremented per kill
|
||||||
|
4. test unsealing item:
|
||||||
|
- client item id does not change
|
||||||
|
- unsealed item no longer has kill counter
|
||||||
|
5. test reject unsealing item if not enough kills (can this even happen?)
|
||||||
|
*/
|
||||||
|
|
||||||
|
use elseware::common::serverstate::{ClientId, ServerState};
|
||||||
|
use elseware::entity::gateway::{EntityGateway, InMemoryGateway};
|
||||||
|
use elseware::entity::item;
|
||||||
|
use elseware::ship::ship::{ShipServerState, RecvShipPacket};
|
||||||
|
use elseware::entity::character::{SectionID};
|
||||||
|
use elseware::ship::room::{Difficulty};
|
||||||
|
use elseware::ship::map::area::{MapArea};
|
||||||
|
use elseware::ship::monster::{MonsterType};
|
||||||
|
|
||||||
|
use libpso::packet::ship::*;
|
||||||
|
use libpso::packet::messages::*;
|
||||||
|
|
||||||
|
#[path = "common.rs"]
|
||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn test_item_drops_with_kill_counter() {
|
||||||
|
let mut entity_gateway = InMemoryGateway::default();
|
||||||
|
let (_user1, _char1) = new_user_character_with_sid(&mut entity_gateway, "a1", "a", SectionID::Skyly).await;
|
||||||
|
let mut ship = Box::new(ShipServerState::builder()
|
||||||
|
.gateway(entity_gateway.clone())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
log_in_char(&mut ship, ClientId(1), "a1", "a").await;
|
||||||
|
join_lobby(&mut ship, ClientId(1)).await;
|
||||||
|
create_ep2_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await;
|
||||||
|
|
||||||
|
// ship.handle(ClientId(1), &RecvShipPacket()).await.unwrap().for_each(drop);
|
||||||
|
|
||||||
|
// ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerWarpingToFloor(PlayerWarpingToFloor{
|
||||||
|
// client: 0,
|
||||||
|
// target: 0,
|
||||||
|
// area: 9, // seaside
|
||||||
|
// data: 0,
|
||||||
|
// })))).await.unwrap().for_each(drop);
|
||||||
|
|
||||||
|
|
||||||
|
// ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerWarping(PlayerWarping{
|
||||||
|
// client: 0,
|
||||||
|
// target: 0,
|
||||||
|
// })))).await.unwrap().for_each(drop);
|
||||||
|
|
||||||
|
// ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerLoadedIn(PlayerLoadedIn{
|
||||||
|
// client: 0,
|
||||||
|
// target: 0,
|
||||||
|
// unknown1: [0,0],
|
||||||
|
// rotation: 0,
|
||||||
|
// area: 9,
|
||||||
|
// room: 1,
|
||||||
|
// x: 100.0,
|
||||||
|
// y: 10.0,
|
||||||
|
// z: -20.0,
|
||||||
|
// })))).await.unwrap().for_each(drop);
|
||||||
|
|
||||||
|
// ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerChangedMap2(PlayerChangedMap2{
|
||||||
|
// client: 0,
|
||||||
|
// target: 0,
|
||||||
|
// map_area: 9,
|
||||||
|
// _unknown1: 0,
|
||||||
|
// })))).await.unwrap().for_each(drop);
|
||||||
|
|
||||||
|
// ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSpawnedIntoArea(PlayerSpawnedIntoArea{
|
||||||
|
// client: 0,
|
||||||
|
// target: 0,
|
||||||
|
// })))).await.unwrap().for_each(drop);
|
||||||
|
|
||||||
|
// ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDoneChangingMap(PlayerDoneChangingMap{
|
||||||
|
// client: 0,
|
||||||
|
// target: 0,
|
||||||
|
// })))).await.unwrap().for_each(drop);
|
||||||
|
|
||||||
|
let room = ship.blocks.0[0].rooms[0].as_mut().unwrap();
|
||||||
|
room.toggle_redbox_mode(); // enable redbox mode for sjs
|
||||||
|
|
||||||
|
println!("room redbox mode: {:?}", room.redbox);
|
||||||
|
println!("room.mode: {:?}", room.mode);
|
||||||
|
println!("killing gigue for sjs!");
|
||||||
|
let gigue_id = room.maps.get_enemy_id_by_monster_type(MonsterType::GiGue).unwrap();
|
||||||
|
println!("found gigue id: {:?}!", gigue_id);
|
||||||
|
let pkts = ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::RequestItem(RequestItem {
|
||||||
|
client: 0,
|
||||||
|
target: 0,
|
||||||
|
map_area: 9, // seaside
|
||||||
|
pt_index: 55, // gigue ?
|
||||||
|
enemy_id: gigue_id,
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 0.0,
|
||||||
|
}))))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<_>>(); // this should return 1 packet (ItemDrop)?
|
||||||
|
|
||||||
|
println!("packets returned: {:?}", pkts);
|
||||||
|
assert!(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[async_std::test]
|
||||||
|
// async fn test_all_equipped_kill_counters_increase_per_kill() {
|
||||||
|
// let mut entity_gateway = InMemoryGateway::default();
|
||||||
|
|
||||||
|
// let (_user1, _char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[async_std::test]
|
||||||
|
// async fn test_non_equipped_kill_counter_does_not_increase() {
|
||||||
|
// let mut entity_gateway = InMemoryGateway::default();
|
||||||
|
|
||||||
|
// let (_user1, _char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #[async_std::test]
|
||||||
|
// async fn test_kill_counter_increase_only_for_final_hit() { // don't share kills among players
|
||||||
|
// let mut entity_gateway = InMemoryGateway::default();
|
||||||
|
|
||||||
|
// let (_user1, _char1) = new_user_character(&mut entity_gateway, "a1", "a").await;
|
||||||
|
|
||||||
|
// }
|
Loading…
x
Reference in New Issue
Block a user