Add TransformBundle (#3054)
# Objective - Bevy currently has no simple way to make an "empty" Entity work correctly in a Hierachy. - The current Solution is to insert a Tuple instead: ```rs .insert_bundle((Transform::default(), GlobalTransform::default())) ``` ## Solution * Add a `TransformBundle` that combines the Components: ```rs .insert_bundle(TransformBundle::default()) ``` * The code is based on #2331, except for missing the more controversial usage of `TransformBundle` as a Sub-bundle in preexisting Bundles. Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
7604665880
commit
59ee512292
@ -26,7 +26,8 @@ use bevy_render::{
|
|||||||
use bevy_scene::Scene;
|
use bevy_scene::Scene;
|
||||||
use bevy_transform::{
|
use bevy_transform::{
|
||||||
hierarchy::{BuildWorldChildren, WorldChildBuilder},
|
hierarchy::{BuildWorldChildren, WorldChildBuilder},
|
||||||
prelude::{GlobalTransform, Transform},
|
prelude::Transform,
|
||||||
|
TransformBundle,
|
||||||
};
|
};
|
||||||
use bevy_utils::{HashMap, HashSet};
|
use bevy_utils::{HashMap, HashSet};
|
||||||
use gltf::{
|
use gltf::{
|
||||||
@ -289,7 +290,7 @@ async fn load_gltf<'a, 'b>(
|
|||||||
let mut world = World::default();
|
let mut world = World::default();
|
||||||
world
|
world
|
||||||
.spawn()
|
.spawn()
|
||||||
.insert_bundle((Transform::identity(), GlobalTransform::identity()))
|
.insert_bundle(TransformBundle::identity())
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
for node in scene.nodes() {
|
for node in scene.nodes() {
|
||||||
let result = load_node(&node, parent, load_context, &buffer_data);
|
let result = load_node(&node, parent, load_context, &buffer_data);
|
||||||
@ -462,10 +463,9 @@ fn load_node(
|
|||||||
) -> Result<(), GltfError> {
|
) -> Result<(), GltfError> {
|
||||||
let transform = gltf_node.transform();
|
let transform = gltf_node.transform();
|
||||||
let mut gltf_error = None;
|
let mut gltf_error = None;
|
||||||
let mut node = world_builder.spawn_bundle((
|
let mut node = world_builder.spawn_bundle(TransformBundle::from(Transform::from_matrix(
|
||||||
Transform::from_matrix(Mat4::from_cols_array_2d(&transform.matrix())),
|
Mat4::from_cols_array_2d(&transform.matrix()),
|
||||||
GlobalTransform::identity(),
|
)));
|
||||||
));
|
|
||||||
|
|
||||||
if let Some(name) = gltf_node.name() {
|
if let Some(name) = gltf_node.name() {
|
||||||
node.insert(Name::new(name.to_string()));
|
node.insert(Name::new(name.to_string()));
|
||||||
|
|||||||
@ -7,8 +7,9 @@ use std::ops::Mul;
|
|||||||
/// Describe the position of an entity relative to the reference frame.
|
/// Describe the position of an entity relative to the reference frame.
|
||||||
///
|
///
|
||||||
/// * To place or move an entity, you should set its [`Transform`].
|
/// * To place or move an entity, you should set its [`Transform`].
|
||||||
/// * To be displayed, an entity must have both a [`Transform`] and a [`GlobalTransform`].
|
|
||||||
/// * To get the global position of an entity, you should get its [`GlobalTransform`].
|
/// * To get the global position of an entity, you should get its [`GlobalTransform`].
|
||||||
|
/// * For transform hierarchies to work correctly, you must have both a [`Transform`] and a [`GlobalTransform`].
|
||||||
|
/// * You may use the [`TransformBundle`](crate::TransformBundle) to guarantee this.
|
||||||
///
|
///
|
||||||
/// ## [`Transform`] and [`GlobalTransform`]
|
/// ## [`Transform`] and [`GlobalTransform`]
|
||||||
///
|
///
|
||||||
@ -20,16 +21,6 @@ use std::ops::Mul;
|
|||||||
/// [`GlobalTransform`] is updated from [`Transform`] in the system
|
/// [`GlobalTransform`] is updated from [`Transform`] in the system
|
||||||
/// [`transform_propagate_system`](crate::transform_propagate_system::transform_propagate_system).
|
/// [`transform_propagate_system`](crate::transform_propagate_system::transform_propagate_system).
|
||||||
///
|
///
|
||||||
/// In pseudo code:
|
|
||||||
/// ```ignore
|
|
||||||
/// for entity in entities_without_parent:
|
|
||||||
/// set entity.global_transform to entity.transform
|
|
||||||
/// recursively:
|
|
||||||
/// set parent to current entity
|
|
||||||
/// for child in parent.children:
|
|
||||||
/// set child.global_transform to parent.global_transform * child.transform
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
|
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
|
||||||
/// update the[`Transform`] of an entity in this stage or after, you will notice a 1 frame lag
|
/// update the[`Transform`] of an entity in this stage or after, you will notice a 1 frame lag
|
||||||
/// before the [`GlobalTransform`] is updated.
|
/// before the [`GlobalTransform`] is updated.
|
||||||
|
|||||||
@ -8,8 +8,9 @@ use std::ops::Mul;
|
|||||||
/// to its parent position.
|
/// to its parent position.
|
||||||
///
|
///
|
||||||
/// * To place or move an entity, you should set its [`Transform`].
|
/// * To place or move an entity, you should set its [`Transform`].
|
||||||
/// * To be displayed, an entity must have both a [`Transform`] and a [`GlobalTransform`].
|
|
||||||
/// * To get the global position of an entity, you should get its [`GlobalTransform`].
|
/// * To get the global position of an entity, you should get its [`GlobalTransform`].
|
||||||
|
/// * To be displayed, an entity must have both a [`Transform`] and a [`GlobalTransform`].
|
||||||
|
/// * You may use the [`TransformBundle`](crate::TransformBundle) to guarantee this.
|
||||||
///
|
///
|
||||||
/// ## [`Transform`] and [`GlobalTransform`]
|
/// ## [`Transform`] and [`GlobalTransform`]
|
||||||
///
|
///
|
||||||
@ -21,16 +22,6 @@ use std::ops::Mul;
|
|||||||
/// [`GlobalTransform`] is updated from [`Transform`] in the system
|
/// [`GlobalTransform`] is updated from [`Transform`] in the system
|
||||||
/// [`transform_propagate_system`](crate::transform_propagate_system::transform_propagate_system).
|
/// [`transform_propagate_system`](crate::transform_propagate_system::transform_propagate_system).
|
||||||
///
|
///
|
||||||
/// In pseudo code:
|
|
||||||
/// ```ignore
|
|
||||||
/// for entity in entities_without_parent:
|
|
||||||
/// set entity.global_transform to entity.transform
|
|
||||||
/// recursively:
|
|
||||||
/// set parent to current entity
|
|
||||||
/// for child in parent.children:
|
|
||||||
/// set child.global_transform to parent.global_transform * child.transform
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
|
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
|
||||||
/// update the[`Transform`] of an entity in this stage or after, you will notice a 1 frame lag
|
/// update the[`Transform`] of an entity in this stage or after, you will notice a 1 frame lag
|
||||||
/// before the [`GlobalTransform`] is updated.
|
/// before the [`GlobalTransform`] is updated.
|
||||||
|
|||||||
@ -11,13 +11,76 @@ pub mod transform_propagate_system;
|
|||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::{components::*, hierarchy::*, TransformPlugin};
|
pub use crate::{components::*, hierarchy::*, TransformBundle, TransformPlugin};
|
||||||
}
|
}
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
|
use bevy_ecs::{
|
||||||
|
bundle::Bundle,
|
||||||
|
schedule::{ParallelSystemDescriptorCoercion, SystemLabel},
|
||||||
|
};
|
||||||
use prelude::{parent_update_system, Children, GlobalTransform, Parent, PreviousParent, Transform};
|
use prelude::{parent_update_system, Children, GlobalTransform, Parent, PreviousParent, Transform};
|
||||||
|
|
||||||
|
/// A [`Bundle`] of the [`Transform`] and [`GlobalTransform`]
|
||||||
|
/// [`Component`](bevy_ecs::component::Component)s, which describe the position of an entity.
|
||||||
|
///
|
||||||
|
/// * To place or move an entity, you should set its [`Transform`].
|
||||||
|
/// * To get the global position of an entity, you should get its [`GlobalTransform`].
|
||||||
|
/// * For transform hierarchies to work correctly, you must have both a [`Transform`] and a [`GlobalTransform`].
|
||||||
|
/// * You may use the [`TransformBundle`] to guarantee this.
|
||||||
|
///
|
||||||
|
/// ## [`Transform`] and [`GlobalTransform`]
|
||||||
|
///
|
||||||
|
/// [`Transform`] is the position of an entity relative to its parent position, or the reference
|
||||||
|
/// frame if it doesn't have a [`Parent`](Parent).
|
||||||
|
///
|
||||||
|
/// [`GlobalTransform`] is the position of an entity relative to the reference frame.
|
||||||
|
///
|
||||||
|
/// [`GlobalTransform`] is updated from [`Transform`] in the system
|
||||||
|
/// [`transform_propagate_system`](crate::transform_propagate_system::transform_propagate_system).
|
||||||
|
///
|
||||||
|
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
|
||||||
|
/// update the[`Transform`] of an entity in this stage or after, you will notice a 1 frame lag
|
||||||
|
/// before the [`GlobalTransform`] is updated.
|
||||||
|
#[derive(Bundle, Clone, Copy, Debug, Default)]
|
||||||
|
pub struct TransformBundle {
|
||||||
|
/// The transform of the entity.
|
||||||
|
pub local: Transform,
|
||||||
|
/// The global transform of the entity.
|
||||||
|
pub global: GlobalTransform,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransformBundle {
|
||||||
|
/// Creates a new [`TransformBundle`] from a [`Transform`].
|
||||||
|
///
|
||||||
|
/// This initializes [`GlobalTransform`] as identity, to be updated later by the
|
||||||
|
/// [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate) stage.
|
||||||
|
#[inline]
|
||||||
|
pub const fn from_transform(transform: Transform) -> Self {
|
||||||
|
TransformBundle {
|
||||||
|
local: transform,
|
||||||
|
// Note: `..Default::default()` cannot be used here, because it isn't const
|
||||||
|
..Self::identity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new identity [`TransformBundle`], with no translation, rotation, and a scale of 1
|
||||||
|
/// on all axes.
|
||||||
|
#[inline]
|
||||||
|
pub const fn identity() -> Self {
|
||||||
|
TransformBundle {
|
||||||
|
local: Transform::identity(),
|
||||||
|
global: GlobalTransform::identity(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Transform> for TransformBundle {
|
||||||
|
#[inline]
|
||||||
|
fn from(transform: Transform) -> Self {
|
||||||
|
Self::from_transform(transform)
|
||||||
|
}
|
||||||
|
}
|
||||||
/// The base plugin for handling [`Transform`] components
|
/// The base plugin for handling [`Transform`] components
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct TransformPlugin;
|
pub struct TransformPlugin;
|
||||||
|
|||||||
@ -82,7 +82,10 @@ mod test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::hierarchy::{parent_update_system, BuildChildren, BuildWorldChildren};
|
use crate::{
|
||||||
|
hierarchy::{parent_update_system, BuildChildren, BuildWorldChildren},
|
||||||
|
TransformBundle,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn did_propagate() {
|
fn did_propagate() {
|
||||||
@ -96,33 +99,23 @@ mod test {
|
|||||||
schedule.add_stage("update", update_stage);
|
schedule.add_stage("update", update_stage);
|
||||||
|
|
||||||
// Root entity
|
// Root entity
|
||||||
world.spawn().insert_bundle((
|
world
|
||||||
Transform::from_xyz(1.0, 0.0, 0.0),
|
.spawn()
|
||||||
GlobalTransform::identity(),
|
.insert_bundle(TransformBundle::from(Transform::from_xyz(1.0, 0.0, 0.0)));
|
||||||
));
|
|
||||||
|
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
world
|
world
|
||||||
.spawn()
|
.spawn()
|
||||||
.insert_bundle((
|
.insert_bundle(TransformBundle::from(Transform::from_xyz(1.0, 0.0, 0.0)))
|
||||||
Transform::from_xyz(1.0, 0.0, 0.0),
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
children.push(
|
children.push(
|
||||||
parent
|
parent
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, 2.0, 0.)))
|
||||||
Transform::from_xyz(0.0, 2.0, 0.),
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.id(),
|
.id(),
|
||||||
);
|
);
|
||||||
children.push(
|
children.push(
|
||||||
parent
|
parent
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, 0.0, 3.)))
|
||||||
Transform::from_xyz(0.0, 0.0, 3.),
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.id(),
|
.id(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -155,25 +148,16 @@ mod test {
|
|||||||
let mut commands = Commands::new(&mut queue, &world);
|
let mut commands = Commands::new(&mut queue, &world);
|
||||||
let mut children = Vec::new();
|
let mut children = Vec::new();
|
||||||
commands
|
commands
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(1.0, 0.0, 0.0)))
|
||||||
Transform::from_xyz(1.0, 0.0, 0.0),
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
children.push(
|
children.push(
|
||||||
parent
|
parent
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, 2.0, 0.0)))
|
||||||
Transform::from_xyz(0.0, 2.0, 0.0),
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.id(),
|
.id(),
|
||||||
);
|
);
|
||||||
children.push(
|
children.push(
|
||||||
parent
|
parent
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, 0.0, 3.0)))
|
||||||
Transform::from_xyz(0.0, 0.0, 3.0),
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.id(),
|
.id(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -54,7 +54,7 @@ fn setup(
|
|||||||
});
|
});
|
||||||
// light
|
// light
|
||||||
commands.spawn_bundle(PointLightBundle {
|
commands.spawn_bundle(PointLightBundle {
|
||||||
transform: Transform::from_translation(Vec3::new(50.0, 50.0, 50.0)),
|
transform: Transform::from_xyz(50.0, 50.0, 50.0),
|
||||||
point_light: PointLight {
|
point_light: PointLight {
|
||||||
intensity: 600000.,
|
intensity: 600000.,
|
||||||
range: 100.,
|
range: 100.,
|
||||||
@ -64,8 +64,7 @@ fn setup(
|
|||||||
});
|
});
|
||||||
// camera
|
// camera
|
||||||
commands.spawn_bundle(OrthographicCameraBundle {
|
commands.spawn_bundle(OrthographicCameraBundle {
|
||||||
transform: Transform::from_translation(Vec3::new(0.0, 0.0, 8.0))
|
transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y),
|
||||||
.looking_at(Vec3::default(), Vec3::Y),
|
|
||||||
orthographic_projection: OrthographicProjection {
|
orthographic_projection: OrthographicProjection {
|
||||||
scale: 0.01,
|
scale: 0.01,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@ -38,10 +38,7 @@ fn setup(
|
|||||||
// Spawn the scene as a child of another entity. This first scene will be translated backward
|
// Spawn the scene as a child of another entity. This first scene will be translated backward
|
||||||
// with its parent
|
// with its parent
|
||||||
commands
|
commands
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(0.0, 0.0, -1.0)))
|
||||||
Transform::from_xyz(0.0, 0.0, -1.0),
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
parent.spawn_scene(asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"));
|
parent.spawn_scene(asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -60,7 +60,7 @@ fn spawn_tasks(mut commands: Commands, thread_pool: Res<AsyncComputeTaskPool>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Such hard work, all done!
|
// Such hard work, all done!
|
||||||
Transform::from_translation(Vec3::new(x as f32, y as f32, z as f32))
|
Transform::from_xyz(x as f32, y as f32, z as f32)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Spawn new entity and add our new task as a component
|
// Spawn new entity and add our new task as a component
|
||||||
@ -107,13 +107,13 @@ fn setup_env(mut commands: Commands) {
|
|||||||
|
|
||||||
// lights
|
// lights
|
||||||
commands.spawn_bundle(PointLightBundle {
|
commands.spawn_bundle(PointLightBundle {
|
||||||
transform: Transform::from_translation(Vec3::new(4.0, 12.0, 15.0)),
|
transform: Transform::from_xyz(4.0, 12.0, 15.0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
// camera
|
// camera
|
||||||
commands.spawn_bundle(PerspectiveCameraBundle {
|
commands.spawn_bundle(PerspectiveCameraBundle {
|
||||||
transform: Transform::from_translation(Vec3::new(offset, offset, 15.0))
|
transform: Transform::from_xyz(offset, offset, 15.0)
|
||||||
.looking_at(Vec3::new(offset, offset, 0.0), Vec3::Y),
|
.looking_at(Vec3::new(offset, offset, 0.0), Vec3::Y),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|||||||
@ -117,10 +117,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMu
|
|||||||
.map(|i| {
|
.map(|i| {
|
||||||
let height = rand::thread_rng().gen_range(-0.1..0.1);
|
let height = rand::thread_rng().gen_range(-0.1..0.1);
|
||||||
commands
|
commands
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(
|
||||||
Transform::from_xyz(i as f32, height - 0.2, j as f32),
|
i as f32,
|
||||||
GlobalTransform::identity(),
|
height - 0.2,
|
||||||
))
|
j as f32,
|
||||||
|
)))
|
||||||
.with_children(|cell| {
|
.with_children(|cell| {
|
||||||
cell.spawn_scene(cell_scene.clone());
|
cell.spawn_scene(cell_scene.clone());
|
||||||
});
|
});
|
||||||
@ -133,8 +134,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMu
|
|||||||
// spawn the game character
|
// spawn the game character
|
||||||
game.player.entity = Some(
|
game.player.entity = Some(
|
||||||
commands
|
commands
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform {
|
||||||
Transform {
|
|
||||||
translation: Vec3::new(
|
translation: Vec3::new(
|
||||||
game.player.i as f32,
|
game.player.i as f32,
|
||||||
game.board[game.player.j][game.player.i].height,
|
game.board[game.player.j][game.player.i].height,
|
||||||
@ -142,9 +142,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut game: ResMu
|
|||||||
),
|
),
|
||||||
rotation: Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2),
|
rotation: Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
}))
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.with_children(|cell| {
|
.with_children(|cell| {
|
||||||
cell.spawn_scene(asset_server.load("models/AlienCake/alien.glb#Scene0"));
|
cell.spawn_scene(asset_server.load("models/AlienCake/alien.glb#Scene0"));
|
||||||
})
|
})
|
||||||
@ -324,17 +322,11 @@ fn spawn_bonus(
|
|||||||
}
|
}
|
||||||
game.bonus.entity = Some(
|
game.bonus.entity = Some(
|
||||||
commands
|
commands
|
||||||
.spawn_bundle((
|
.spawn_bundle(TransformBundle::from(Transform::from_xyz(
|
||||||
Transform {
|
|
||||||
translation: Vec3::new(
|
|
||||||
game.bonus.i as f32,
|
game.bonus.i as f32,
|
||||||
game.board[game.bonus.j][game.bonus.i].height + 0.2,
|
game.board[game.bonus.j][game.bonus.i].height + 0.2,
|
||||||
game.bonus.j as f32,
|
game.bonus.j as f32,
|
||||||
),
|
)))
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
GlobalTransform::identity(),
|
|
||||||
))
|
|
||||||
.with_children(|children| {
|
.with_children(|children| {
|
||||||
children.spawn_bundle(PointLightBundle {
|
children.spawn_bundle(PointLightBundle {
|
||||||
point_light: PointLight {
|
point_light: PointLight {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user