mirror of
https://github.com/kaosat-dev/Blender_bevy_components_workflow.git
synced 2024-12-25 00:54:11 +00:00
feat(Blenvy:Bevy):
* bumped up Bevy version to v0.14 ! * fixed (albeit in a clunky way) issues with sub blueprint detection * improved error message for missing material library * added HideUntilReady component & logic, to hide 'in-spawning' blueprint instances until they are ready * "add-to-world" is now only trigerred for blueprint instances that have no parents (avoid footgun) * minor cleanups & tweaks * added test component with Vec3 to double check for issues * updated test blend file to include the component above + added a light to the test spawnable blueprint to check for "light flashes" (aka indirectly testing "HideUntilReady")
This commit is contained in:
parent
478be88a55
commit
bef709a0ed
@ -14,7 +14,7 @@ license = "MIT OR Apache-2.0"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.14.0-rc.3", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "file_watcher"] }
|
||||
bevy = { version = "0.14", default-features = false, features = ["bevy_asset", "bevy_scene", "bevy_gltf", "file_watcher"] }
|
||||
serde = "1.0.188"
|
||||
ron = "0.8.1"
|
||||
serde_json = "1.0.108"
|
||||
@ -34,4 +34,4 @@ gltf = { version = "1.4.0", default-features = false, features = [
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
bevy = { version = "0.14.0-rc.3", default-features = false, features = ["dynamic_linking"] }
|
||||
bevy = { version = "0.14", default-features = false, features = ["dynamic_linking"] }
|
@ -57,7 +57,7 @@ pub(crate) fn inject_materials(
|
||||
let model_handle: Handle<Gltf> = asset_server.load(material_info.path.clone()); // FIXME: kinda weird now
|
||||
let mat_gltf = assets_gltf
|
||||
.get(model_handle.id())
|
||||
.expect("material should have been preloaded");
|
||||
.expect(&format!("materials file {} should have been preloaded", material_info.path));
|
||||
if mat_gltf.named_materials.contains_key(&material_info.name as &str) {
|
||||
let material = mat_gltf
|
||||
.named_materials
|
||||
|
@ -60,6 +60,12 @@ pub struct AddToGameWorld;
|
||||
pub(crate) struct OriginalChildren(pub Vec<Entity>);
|
||||
|
||||
|
||||
#[derive(Component)]
|
||||
/// You can add this component to a blueprint instance, and the instance will be hidden until it is ready
|
||||
/// You usually want to use this for worlds/level spawning , or dynamic spawning at runtime, but not when you are adding blueprint instances to an existing entity
|
||||
/// as it would first become invisible before re-appearing again
|
||||
pub struct HideUntilReady;
|
||||
|
||||
#[derive(Event, Debug)]
|
||||
pub enum BlueprintEvent {
|
||||
|
||||
@ -261,6 +267,7 @@ pub(crate) fn blueprints_assets_ready(spawn_placeholders: Query<
|
||||
Option<&Parent>,
|
||||
Option<&AddToGameWorld>,
|
||||
Option<&Name>,
|
||||
Option<&HideUntilReady>
|
||||
),
|
||||
(
|
||||
With<BlueprintAssetsLoaded>,
|
||||
@ -283,6 +290,7 @@ pub(crate) fn blueprints_assets_ready(spawn_placeholders: Query<
|
||||
original_parent,
|
||||
add_to_world,
|
||||
name,
|
||||
hide_until_ready,
|
||||
) in spawn_placeholders.iter()
|
||||
{
|
||||
/*info!(
|
||||
@ -336,8 +344,6 @@ pub(crate) fn blueprints_assets_ready(spawn_placeholders: Query<
|
||||
SceneBundle {
|
||||
scene: scene.clone(),
|
||||
transform: transforms,
|
||||
visibility: Visibility::Hidden,
|
||||
|
||||
..Default::default()
|
||||
},
|
||||
OriginalChildren(original_children),
|
||||
@ -345,15 +351,19 @@ pub(crate) fn blueprints_assets_ready(spawn_placeholders: Query<
|
||||
// these are animations specific to the inside of the blueprint
|
||||
named_animations: named_animations//gltf.named_animations.clone(),
|
||||
},
|
||||
|
||||
));
|
||||
|
||||
/* if add_to_world.is_some() {
|
||||
if hide_until_ready.is_some() {
|
||||
commands.entity(entity).insert(Visibility::Hidden); // visibility:
|
||||
}
|
||||
|
||||
// only allow automatically adding a newly spawned blueprint instance to the "world", if the entity does not have a parent
|
||||
if add_to_world.is_some() && original_parent.is_some() {
|
||||
let world = game_world
|
||||
.get_single_mut()
|
||||
.expect("there should be a game world present");
|
||||
commands.entity(world).add_child(entity);
|
||||
} */
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -382,8 +392,6 @@ pub struct BlueprintChildrenReady;
|
||||
#[reflect(Component)]
|
||||
pub struct BlueprintReadyForPostProcess;
|
||||
|
||||
|
||||
// TODO: disregard blueprints that have been spawned WAIT , we already have BlueprintSpawning
|
||||
pub(crate) fn blueprints_scenes_spawned(
|
||||
spawned_blueprint_scene_instances: Query<(Entity, Option<&Name>, Option<&Children>, Option<&SpawnTrackRoot>), (With<BlueprintSpawning>, Added<SceneInstance>)>,
|
||||
with_blueprint_infos : Query<(Entity, Option<&Name>), With<BlueprintInfo>>,
|
||||
@ -396,7 +404,6 @@ pub(crate) fn blueprints_scenes_spawned(
|
||||
mut commands: Commands,
|
||||
|
||||
all_names: Query<&Name>
|
||||
|
||||
) {
|
||||
for (entity, name, children, track_root) in spawned_blueprint_scene_instances.iter(){
|
||||
info!("Done spawning blueprint scene for entity named {:?} (track root: {:?})", name, track_root);
|
||||
@ -405,34 +412,59 @@ pub(crate) fn blueprints_scenes_spawned(
|
||||
|
||||
let mut tracker_data: HashMap<Entity, bool> = HashMap::new();
|
||||
|
||||
for parent in all_parents.iter_ancestors(entity) {
|
||||
if with_blueprint_infos.get(parent).is_ok() {
|
||||
if track_root.is_none() {
|
||||
for parent in all_parents.iter_ancestors(entity) {
|
||||
if with_blueprint_infos.get(parent).is_ok() {
|
||||
|
||||
println!("found a parent with blueprint_info {:?} for {:?}", all_names.get(parent), all_names.get(entity));
|
||||
commands.entity(entity).insert(SpawnTrackRoot(parent));// Injecting to know which entity is the root
|
||||
|
||||
println!("found a parent with blueprint_info {:?} for {:?}", all_names.get(parent), all_names.get(entity));
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if children.is_some() {
|
||||
for child in all_children.iter_descendants(entity) {
|
||||
if with_blueprint_infos.get(child).is_ok() {
|
||||
sub_blueprint_instances.push(child);
|
||||
if let Ok(nname) = all_names.get(child) {
|
||||
sub_blueprint_instance_names.push(nname.clone());
|
||||
}
|
||||
// println!("Parent blueprint instance of {:?} is {:?}", all_names.get(child), all_names.get(entity));
|
||||
|
||||
tracker_data.insert(child, false);
|
||||
|
||||
if track_root.is_some() {
|
||||
let prev_root = track_root.unwrap().0;
|
||||
// if we already had a track root, and it is different from the current entity , change the previous track root's list of children
|
||||
if prev_root != entity {
|
||||
let mut tracker = sub_blueprint_trackers.get_mut(prev_root).expect("should have a tracker");
|
||||
tracker.1.sub_blueprint_instances.remove(&child);
|
||||
|
||||
|
||||
for parent in all_parents.iter_ancestors(child) {
|
||||
if with_blueprint_infos.get(parent).is_ok() {
|
||||
|
||||
if parent == entity {
|
||||
//println!("yohoho");
|
||||
println!("Parent blueprint instance of {:?} is {:?}", all_names.get(child), all_names.get(parent));
|
||||
|
||||
commands.entity(child).insert(SpawnTrackRoot(entity));// Injecting to know which entity is the root
|
||||
|
||||
tracker_data.insert(child, false);
|
||||
|
||||
sub_blueprint_instances.push(child);
|
||||
if let Ok(nname) = all_names.get(child) {
|
||||
sub_blueprint_instance_names.push(nname.clone());
|
||||
}
|
||||
|
||||
/*if track_root.is_some() {
|
||||
let prev_root = track_root.unwrap().0;
|
||||
// if we already had a track root, and it is different from the current entity , change the previous track root's list of children
|
||||
if prev_root != entity {
|
||||
let mut tracker = sub_blueprint_trackers.get_mut(prev_root).expect("should have a tracker");
|
||||
tracker.1.sub_blueprint_instances.remove(&child);
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
commands.entity(child).insert(SpawnTrackRoot(entity));// Injecting to know which entity is the root
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -481,7 +513,7 @@ pub(crate) fn blueprints_transfer_components(
|
||||
) {
|
||||
|
||||
for (original, children, original_children, name, track_root) in foo.iter() {
|
||||
println!("YOOO ready !! {:?}", name);
|
||||
info!("YOOO ready !! removing empty nodes {:?}", name);
|
||||
|
||||
if children.len() == 0 {
|
||||
warn!("timing issue ! no children found, please restart your bevy app (bug being investigated)");
|
||||
@ -515,7 +547,7 @@ pub(crate) fn blueprints_transfer_components(
|
||||
}
|
||||
|
||||
commands.entity(original)
|
||||
.insert(BlueprintReadyForPostProcess); // Tag the entity so any systems dealing with post processing can now it is now their "turn"
|
||||
.insert(BlueprintReadyForPostProcess); // Tag the entity so any systems dealing with post processing can know it is now their "turn"
|
||||
// commands.entity(original).remove::<Handle<Scene>>(); // FIXME: if we delete the handle to the scene, things get despawned ! not what we want
|
||||
//commands.entity(original).remove::<BlueprintAssetsLoadState>(); // also clear the sub assets tracker to free up handles, perhaps just freeing up the handles and leave the rest would be better ?
|
||||
//commands.entity(original).remove::<BlueprintAssetsLoaded>();
|
||||
@ -524,6 +556,7 @@ pub(crate) fn blueprints_transfer_components(
|
||||
|
||||
// now check if the current entity is a child blueprint instance of another entity
|
||||
// this should always be done last, as children should be finished before the parent can be processed correctly
|
||||
// TODO: perhaps use observers for these
|
||||
if let Some(track_root) = track_root {
|
||||
let root_name = all_names.get(track_root.0);
|
||||
println!("got some root {:?}", root_name);
|
||||
@ -555,19 +588,23 @@ pub(crate) fn blueprints_transfer_components(
|
||||
pub struct BlueprintReadyForFinalizing;
|
||||
|
||||
pub(crate) fn blueprints_finalize_instances(
|
||||
blueprint_instances: Query<(Entity, Option<&Name>, &BlueprintInfo), (With<BlueprintSpawning>, With<BlueprintReadyForFinalizing>)>,
|
||||
blueprint_instances: Query<(Entity, Option<&Name>, &BlueprintInfo, Option<&HideUntilReady>), (With<BlueprintSpawning>, With<BlueprintReadyForFinalizing>)>,
|
||||
mut blueprint_events: EventWriter<BlueprintEvent>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (entity, name, blueprint_info) in blueprint_instances.iter() {
|
||||
for (entity, name, blueprint_info, hide_until_ready) in blueprint_instances.iter() {
|
||||
info!("Finalizing blueprint instance {:?}", name);
|
||||
commands.entity(entity)
|
||||
.remove::<SpawnHere>()
|
||||
.remove::<BlueprintSpawning>()
|
||||
.remove::<BlueprintReadyForPostProcess>()
|
||||
.insert(BlueprintInstanceReady)
|
||||
.insert(Visibility::Visible)
|
||||
;
|
||||
if hide_until_ready.is_some() {
|
||||
println!("REVEAAAL");
|
||||
commands.entity(entity).insert(Visibility::Visible);
|
||||
}
|
||||
|
||||
|
||||
blueprint_events.send(BlueprintEvent::InstanceReady {entity: entity, blueprint_name: blueprint_info.name.clone(), blueprint_path: blueprint_info.path.clone()});
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.14.0-rc.3", features = ["dynamic_linking"] }
|
||||
bevy = { version = "0.14", features = ["dynamic_linking"] }
|
||||
blenvy = { path = "../../crates/blenvy" }
|
||||
# bevy_gltf_blueprints = { path = "../../crates/bevy_gltf_blueprints" }
|
||||
# bevy_registry_export = { path = "../../crates/bevy_registry_export" }
|
||||
|
Binary file not shown.
@ -4182,6 +4182,30 @@
|
||||
"type": "object",
|
||||
"typeInfo": "Struct"
|
||||
},
|
||||
"bevy_example::test_components::RedirectPropHitImpulse": {
|
||||
"isComponent": true,
|
||||
"isResource": false,
|
||||
"long_name": "bevy_example::test_components::RedirectPropHitImpulse",
|
||||
"oneOf": [
|
||||
{
|
||||
"items": false,
|
||||
"long_name": "Local",
|
||||
"prefixItems": [
|
||||
{
|
||||
"type": {
|
||||
"$ref": "#/$defs/glam::Vec3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"short_name": "Local",
|
||||
"type": "array",
|
||||
"typeInfo": "Tuple"
|
||||
}
|
||||
],
|
||||
"short_name": "RedirectPropHitImpulse",
|
||||
"type": "object",
|
||||
"typeInfo": "Enum"
|
||||
},
|
||||
"bevy_example::test_components::TupleTest2": {
|
||||
"isComponent": true,
|
||||
"isResource": false,
|
||||
@ -8145,11 +8169,6 @@
|
||||
"$ref": "#/$defs/bevy_render::alpha::AlphaMode"
|
||||
}
|
||||
},
|
||||
"anisotropy_channel": {
|
||||
"type": {
|
||||
"$ref": "#/$defs/bevy_pbr::pbr_material::UvChannel"
|
||||
}
|
||||
},
|
||||
"anisotropy_rotation": {
|
||||
"type": {
|
||||
"$ref": "#/$defs/f32"
|
||||
@ -8160,11 +8179,6 @@
|
||||
"$ref": "#/$defs/f32"
|
||||
}
|
||||
},
|
||||
"anisotropy_texture": {
|
||||
"type": {
|
||||
"$ref": "#/$defs/core::option::Option<bevy_asset::handle::Handle<bevy_render::texture::image::Image>>"
|
||||
}
|
||||
},
|
||||
"attenuation_color": {
|
||||
"type": {
|
||||
"$ref": "#/$defs/bevy_color::color::Color"
|
||||
@ -8374,7 +8388,6 @@
|
||||
"clearcoat_perceptual_roughness",
|
||||
"anisotropy_strength",
|
||||
"anisotropy_rotation",
|
||||
"anisotropy_channel",
|
||||
"double_sided",
|
||||
"unlit",
|
||||
"fog_enabled",
|
||||
|
@ -1,5 +1,5 @@
|
||||
use bevy::prelude::*;
|
||||
use blenvy::{BluePrintBundle, BlueprintInfo, DynamicBlueprintInstance, GameWorldTag, SpawnHere};
|
||||
use blenvy::{BluePrintBundle, BlueprintInfo, DynamicBlueprintInstance, GameWorldTag, HideUntilReady, SpawnHere};
|
||||
use crate::{GameState, InAppRunning};
|
||||
|
||||
//use bevy_rapier3d::prelude::Velocity;
|
||||
@ -23,6 +23,7 @@ pub fn setup_game(
|
||||
|
||||
commands.spawn((
|
||||
BlueprintInfo{name: "World".into(), path: "levels/World.glb".into()},
|
||||
HideUntilReady,
|
||||
bevy::prelude::Name::from("world"), //FIXME: not really needed ? could be infered from blueprint's name/ path
|
||||
SpawnHere,
|
||||
GameWorldTag,
|
||||
@ -67,6 +68,7 @@ pub fn spawn_test(
|
||||
},
|
||||
DynamicBlueprintInstance,
|
||||
bevy::prelude::Name::from(format!("test{}", name_index)),
|
||||
HideUntilReady,
|
||||
// SpawnHere,
|
||||
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),
|
||||
/*Velocity {
|
||||
|
@ -129,10 +129,19 @@ fn exit_game(mut app_exit_events: ResMut<Events<bevy::app::AppExit>>) {
|
||||
|
||||
fn check_for_gltf_events(
|
||||
mut blueprint_events: EventReader<BlueprintEvent>,
|
||||
all_names: Query<&Name>,
|
||||
)
|
||||
{
|
||||
for event in blueprint_events.read() {
|
||||
info!("BLUEPRINT EVENT: {:?}", event);
|
||||
match event {
|
||||
BlueprintEvent::InstanceReady{entity, blueprint_name, blueprint_path} => {
|
||||
info!("BLUEPRINT EVENT: {:?} for {:?}", event, all_names.get(*entity));
|
||||
|
||||
}
|
||||
_=> {
|
||||
info!("BLUEPRINT EVENT: {:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use bevy::{gltf::{GltfMaterialExtras, GltfMeshExtras, GltfSceneExtras}, prelude::*};
|
||||
use blenvy::{BlueprintAssets, BlueprintInstanceReady};
|
||||
|
||||
use crate::{BasicTest, EnumComplex};
|
||||
use crate::{BasicTest, EnumComplex, RedirectPropHitImpulse};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct HiearchyDebugTag;
|
||||
@ -12,7 +12,7 @@ pub fn setup_hierarchy_debug(mut commands: Commands, asset_server: Res<AssetServ
|
||||
TextBundle::from_section(
|
||||
"",
|
||||
TextStyle {
|
||||
color: LinearRgba { red: 1.0, green:0.0, blue: 0.0, alpha: 1.0}.into(),
|
||||
color: LinearRgba { red: 1.0, green:1.0, blue: 1.0, alpha: 1.0}.into(),
|
||||
font_size: 15.,
|
||||
..default()
|
||||
},
|
||||
@ -30,7 +30,10 @@ pub fn setup_hierarchy_debug(mut commands: Commands, asset_server: Res<AssetServ
|
||||
|
||||
pub fn get_descendants(
|
||||
all_children: &Query<&Children>,
|
||||
all_names:&Query<&Name>, root: &Entity,
|
||||
all_names:&Query<&Name>,
|
||||
root: &Entity,
|
||||
all_transforms: &Query<&Transform>,
|
||||
all_global_transforms: &Query<&GlobalTransform>,
|
||||
nesting: usize,
|
||||
to_check: &Query<&BasicTest>//&Query<(&BlueprintInstanceReady, &BlueprintAssets)>,
|
||||
)
|
||||
@ -48,14 +51,14 @@ pub fn get_descendants(
|
||||
|
||||
let components_to_check = to_check.get(*root);
|
||||
|
||||
hierarchy_display.push( format!("{}{} ({:?})", " ".repeat(nesting), name, components_to_check) ); //
|
||||
hierarchy_display.push( format!("{}{} ({:?}) ({:?})", " ".repeat(nesting), name, all_transforms.get(*root), all_global_transforms.get(*root)) ); //components_to_check ({:?})
|
||||
|
||||
|
||||
if let Ok(children) = all_children.get(*root) {
|
||||
|
||||
for child in children.iter() {
|
||||
|
||||
let child_descendants_display = get_descendants(&all_children, &all_names, &child, nesting + 4, &to_check);
|
||||
let child_descendants_display = get_descendants(&all_children, &all_names, &child, &all_transforms, &all_global_transforms, nesting + 4, &to_check);
|
||||
hierarchy_display.push(child_descendants_display);
|
||||
}
|
||||
}
|
||||
@ -66,6 +69,8 @@ pub fn draw_hierarchy_debug(
|
||||
root: Query<(Entity, Option<&Name>, &Children), (Without<Parent>)>,
|
||||
all_children: Query<&Children>,
|
||||
all_names:Query<&Name>,
|
||||
all_transforms: Query<&Transform>,
|
||||
all_global_transforms: Query<&GlobalTransform>,
|
||||
|
||||
to_check: Query<&BasicTest>,//Query<(&BlueprintInstanceReady, &BlueprintAssets)>,
|
||||
mut display: Query<&mut Text, With<HiearchyDebugTag>>,
|
||||
@ -75,7 +80,7 @@ pub fn draw_hierarchy_debug(
|
||||
for (root_entity, name, children) in root.iter() {
|
||||
// hierarchy_display.push( format!("Hierarchy root{:?}", name) );
|
||||
|
||||
hierarchy_display.push(get_descendants(&all_children, &all_names, &root_entity, 0, &to_check));
|
||||
hierarchy_display.push(get_descendants(&all_children, &all_names, &root_entity, &all_transforms, &all_global_transforms, 0, &to_check));
|
||||
// let mut children = all_children.get(root_entity);
|
||||
/*for child in children.iter() {
|
||||
// hierarchy_display
|
||||
@ -134,13 +139,13 @@ for (id, name, scene_extras, extras, mesh_extras, material_extras) in
|
||||
}
|
||||
|
||||
fn check_for_component(
|
||||
foo: Query<(Entity, Option<&Name>, &EnumComplex)>,
|
||||
foo: Query<(Entity, Option<&Name>, &RedirectPropHitImpulse)>,
|
||||
mut display: Query<&mut Text, With<HiearchyDebugTag>>,
|
||||
|
||||
){
|
||||
let mut info_lines: Vec<String> = vec![];
|
||||
for (entiity, name , enum_complex) in foo.iter(){
|
||||
let data = format!(" We have a 'EnumComplex' component: {:?} (on {:?})", enum_complex, name);
|
||||
let data = format!(" We have a matching component: {:?} (on {:?})", enum_complex, name);
|
||||
info_lines.push(data);
|
||||
println!("yoho component");
|
||||
|
||||
@ -156,8 +161,8 @@ impl Plugin for HiearchyDebugPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app
|
||||
.add_systems(Startup, setup_hierarchy_debug)
|
||||
// .add_systems(Update, check_for_component)
|
||||
.add_systems(Update, draw_hierarchy_debug)
|
||||
//.add_systems(Update, check_for_component)
|
||||
//.add_systems(Update, draw_hierarchy_debug)
|
||||
//.add_systems(Update, check_for_gltf_extras)
|
||||
|
||||
;
|
||||
|
@ -224,6 +224,12 @@ pub struct ComponentWithFieldsOfIdenticalType{
|
||||
#[reflect(Component)]
|
||||
pub struct ComponentWithFieldsOfIdenticalType2(f32, f32, f32);
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Reflect, Component)]
|
||||
#[reflect(Component, )]
|
||||
pub enum RedirectPropHitImpulse {
|
||||
Local(Vec3),
|
||||
}
|
||||
pub struct ComponentsTestPlugin;
|
||||
impl Plugin for ComponentsTestPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
@ -279,6 +285,8 @@ impl Plugin for ComponentsTestPlugin {
|
||||
.register_type::<ComponentWithFieldsOfIdenticalType>()
|
||||
.register_type::<ComponentWithFieldsOfIdenticalType2>()
|
||||
|
||||
.register_type::<RedirectPropHitImpulse>()
|
||||
|
||||
.add_plugins(MaterialPlugin::<
|
||||
ExtendedMaterial<StandardMaterial, MyExtension>,
|
||||
>::default());
|
||||
|
Loading…
Reference in New Issue
Block a user