updated observer (close #260)
This commit is contained in:
@@ -432,10 +432,10 @@ from the documentation of the library that first introduced this tool,
|
||||
>only update the 10 changed units. So efficient.
|
||||
|
||||
In `EnTT`, this means to iterating over a reduced set of entities and components
|
||||
with respect to what would otherwise be returned from a view or group.<br/>
|
||||
On these words, however, the similarities with the proposal of Entitas also end.
|
||||
The rules of language and the design of the library obviously impose and allow
|
||||
different things.
|
||||
with respect to what would otherwise be returned from a view or a group.<br/>
|
||||
On these words, however, the similarities with the proposal of `Entitas` also
|
||||
end. The rules of language and the design of the library obviously impose and
|
||||
allow different things.
|
||||
|
||||
An `observer` is initialized with an instance of a registry and a set of rules
|
||||
that describe what are the entities to intercept. As an example:
|
||||
@@ -444,23 +444,24 @@ that describe what are the entities to intercept. As an example:
|
||||
entt::observer observer{registry, entt::collector.replace<sprite>()};
|
||||
```
|
||||
|
||||
The class also default constructible if required and it can be reconfigured at
|
||||
any time by means of the `connect` member function. Moreover, instances can be
|
||||
The class is default constructible if required and it can be reconfigured at any
|
||||
time by means of the `connect` member function. Moreover, instances can be
|
||||
disconnected from the underlying registries through the `disconnect` member
|
||||
function.<br/>
|
||||
The `observer` offers also some member functions to query its internal state and
|
||||
to know if it's empty or how many entities it contains. Moreover, it can return
|
||||
a raw pointer to the list of entities it contains.<br/>
|
||||
a raw pointer to the list of entities it contains.
|
||||
|
||||
However, the most important features of this class are that:
|
||||
|
||||
* It's iterable and therefore users can easily walk through the list of entities
|
||||
by means of a range-for loop.
|
||||
by means of a range-for loop or the `each` member function.
|
||||
* It's clearable and therefore users can consume the entities and literally
|
||||
reset the observer after each iteration.
|
||||
|
||||
These aspects make the observer an incredibly powerful tool to know at any time
|
||||
what are the entities that have started to respect the given rules since the
|
||||
last time one asked:
|
||||
what are the entities that matched the given rules since the last time one
|
||||
asked:
|
||||
|
||||
```cpp
|
||||
for(const auto entity: observer) {
|
||||
@@ -470,8 +471,20 @@ for(const auto entity: observer) {
|
||||
observer.clear();
|
||||
```
|
||||
|
||||
The `collector` is an utility to use to generate a list of `matcher`s (the
|
||||
actual rules) to use with an `observer`.<br/>
|
||||
Note that the snippet above is equivalent to the following:
|
||||
|
||||
```cpp
|
||||
observer.each([](const auto entity) {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
At least as long as the `observer` isn't const. This means that the non-const
|
||||
overload of `each` does also reset the underlying data structure before to
|
||||
return to the caller, while the const overload does not for obvious reasons.
|
||||
|
||||
The `collector` is an utility aimed to generate a list of `matcher`s (the actual
|
||||
rules) to use with an `observer` instead.<br/>
|
||||
There are two types of `matcher`s:
|
||||
|
||||
* Observing matcher: an observer will return at least all the living entities
|
||||
@@ -479,7 +492,7 @@ There are two types of `matcher`s:
|
||||
and not yet destroyed.
|
||||
|
||||
```cpp
|
||||
collector.replace<sprite>();
|
||||
entt::collector.replace<sprite>();
|
||||
```
|
||||
|
||||
* Grouping matcher: an observer will return at least all the living entities
|
||||
@@ -487,7 +500,7 @@ There are two types of `matcher`s:
|
||||
not yet left it.
|
||||
|
||||
```cpp
|
||||
collector.group<position, velocity>(entt::exclude<destroyed>);
|
||||
entt::collector.group<position, velocity>(entt::exclude<destroyed>);
|
||||
```
|
||||
|
||||
A grouping matcher supports also exclusion lists as well as single components.
|
||||
@@ -499,6 +512,25 @@ last time one asked.<br/>
|
||||
Note that, for a grouping matcher, if an entity already has all the components
|
||||
except one and the missing type is assigned to it, it is intercepted.
|
||||
|
||||
In addition, a matcher can be filtered with a `when` clause:
|
||||
|
||||
```cpp
|
||||
entt::collector.replace<sprite>().when<position>(entt::exclude<velocity>);
|
||||
```
|
||||
|
||||
This clause introduces a way to intercept entities if and only if they are
|
||||
already part of a hypothetical group. If they are not, they aren't returned by
|
||||
the observer, no matter if they matched the given rule.<br/>
|
||||
In the example above, whenever the component `sprite` of an entity is replaced,
|
||||
the observer probes the entity itself to verify that it has at least `position`
|
||||
and has not `velocity` before to store it aside. If one of the two conditions of
|
||||
the filter isn't respected, the entity is discared, no matter what.
|
||||
|
||||
A `when` clause accepts a theoretically unlimited number of types as well as
|
||||
multiple elements in the exclusion list. Moreover, every matcher can have it's
|
||||
own clause and multiple clauses for the same matcher are combined in a single
|
||||
one.
|
||||
|
||||
## Runtime components
|
||||
|
||||
Defining components at runtime is useful to support plugin systems and mods in
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
#include "../config/config.h"
|
||||
#include "../core/type_traits.hpp"
|
||||
#include "registry.hpp"
|
||||
@@ -17,46 +19,19 @@
|
||||
namespace entt {
|
||||
|
||||
|
||||
/*! @brief Grouping matcher. */
|
||||
template<typename...>
|
||||
struct matcher {};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Matcher.
|
||||
* @brief Collector.
|
||||
*
|
||||
* Primary template isn't defined on purpose. All the specializations give a
|
||||
* compile-time error, but for a few reasonable cases.
|
||||
*/
|
||||
template<typename...>
|
||||
struct matcher;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Observing matcher.
|
||||
*
|
||||
* An observing matcher contains a type for which changes should be
|
||||
* detected.<br/>
|
||||
* Because of the rules of the language, not all changes can be easily detected.
|
||||
* In order to avoid nasty solutions that could affect performance to an extent,
|
||||
* the matcher listens only to the `on_replace` signals emitted by a registry
|
||||
* and is therefore triggered whenever an instance of the given component is
|
||||
* explicitly replaced.
|
||||
*
|
||||
* @tparam AnyOf Type of component for which changes should be detected.
|
||||
*/
|
||||
template<typename AnyOf>
|
||||
struct matcher<AnyOf> {};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Grouping matcher.
|
||||
*
|
||||
* A grouping matcher describes the group to track in terms of accepted and
|
||||
* excluded types.<br/>
|
||||
* This kind of matcher is triggered whenever an entity _enters_ the desired
|
||||
* group because of the components it is assigned.
|
||||
*
|
||||
* @tparam AllOf Types of components tracked by the matcher.
|
||||
* @tparam NoneOf Types of components used to filter out entities.
|
||||
*/
|
||||
template<typename... AllOf, typename... NoneOf>
|
||||
struct matcher<type_list<AllOf...>, type_list<NoneOf...>> {};
|
||||
struct basic_collector;
|
||||
|
||||
|
||||
/**
|
||||
@@ -66,12 +41,9 @@ struct matcher<type_list<AllOf...>, type_list<NoneOf...>> {};
|
||||
* entities.<br/>
|
||||
* Its main purpose is to generate a descriptor that allows an observer to know
|
||||
* how to connect to a registry.
|
||||
*
|
||||
* @tparam AnyOf Types of components for which changes should be detected.
|
||||
* @tparam Matcher Types of grouping matchers.
|
||||
*/
|
||||
template<typename... Matcher>
|
||||
struct basic_collector {
|
||||
template<>
|
||||
struct basic_collector<> {
|
||||
/**
|
||||
* @brief Adds a grouping matcher to the collector.
|
||||
* @tparam AllOf Types of components tracked by the matcher.
|
||||
@@ -80,17 +52,60 @@ struct basic_collector {
|
||||
*/
|
||||
template<typename... AllOf, typename... NoneOf>
|
||||
static constexpr auto group(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
|
||||
return basic_collector<Matcher..., matcher<type_list<AllOf...>, type_list<NoneOf...>>>{};
|
||||
return basic_collector<matcher<matcher<type_list<>, type_list<>>, type_list<NoneOf...>, type_list<AllOf...>>>{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds one or more observing matchers to the collector.
|
||||
* @tparam AnyOf Types of components for which changes should be detected.
|
||||
* @brief Adds an observing matcher to the collector.
|
||||
* @tparam AnyOf Type of component for which changes should be detected.
|
||||
* @return The updated collector.
|
||||
*/
|
||||
template<typename... AnyOf>
|
||||
template<typename AnyOf>
|
||||
static constexpr auto replace() ENTT_NOEXCEPT {
|
||||
return basic_collector<Matcher..., matcher<AnyOf>...>{};
|
||||
return basic_collector<matcher<matcher<type_list<>, type_list<>>, AnyOf>>{};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Collector.
|
||||
* @copydetails basic_collector<>
|
||||
* @tparam AnyOf Types of components for which changes should be detected.
|
||||
* @tparam Matcher Types of grouping matchers.
|
||||
*/
|
||||
template<typename... Reject, typename... Require, typename... Rule, typename... Other>
|
||||
struct basic_collector<matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>, Other...> {
|
||||
/**
|
||||
* @brief Adds a grouping matcher to the collector.
|
||||
* @tparam AllOf Types of components tracked by the matcher.
|
||||
* @tparam NoneOf Types of components used to filter out entities.
|
||||
* @return The updated collector.
|
||||
*/
|
||||
template<typename... AllOf, typename... NoneOf>
|
||||
static constexpr auto group(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
|
||||
using first = matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>;
|
||||
return basic_collector<first, Other..., matcher<matcher<type_list<>, type_list<>>, type_list<NoneOf...>, type_list<AllOf...>>>{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds an observing matcher to the collector.
|
||||
* @tparam AnyOf Type of component for which changes should be detected.
|
||||
* @return The updated collector.
|
||||
*/
|
||||
template<typename AnyOf>
|
||||
static constexpr auto replace() ENTT_NOEXCEPT {
|
||||
using first = matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>;
|
||||
return basic_collector<first, Other..., matcher<matcher<type_list<>, type_list<>>, AnyOf>>{};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Updates the filter of the last added matcher.
|
||||
* @tparam AllOf Types of components required by the matcher.
|
||||
* @tparam NoneOf Types of components used to filter out entities.
|
||||
* @return The updated collector.
|
||||
*/
|
||||
template<typename... AllOf, typename... NoneOf>
|
||||
static constexpr auto when(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
|
||||
return basic_collector<matcher<matcher<type_list<Reject..., NoneOf...>, type_list<Require..., AllOf...>>, Rule...>, Other...>{};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,6 +135,13 @@ constexpr basic_collector<> collector{};
|
||||
* If an entity respects the requirements of multiple matchers, it will be
|
||||
* returned once and only once by the observer in any case.
|
||||
*
|
||||
* Matchers support also filtering by means of a _when_ clause that accepts both
|
||||
* a list of types and an exclusion list.<br/>
|
||||
* Whenever a matcher finds that an entity matches its requirements, the
|
||||
* condition of the filter is verified before to register the entity itself.
|
||||
* Moreover, a registered entity isn't returned by the observer if the condition
|
||||
* set by the filter is broken in the meantime.
|
||||
*
|
||||
* @b Important
|
||||
*
|
||||
* Iterators aren't invalidated if:
|
||||
@@ -149,10 +171,12 @@ class basic_observer {
|
||||
template<std::size_t Index, typename>
|
||||
struct matcher_handler;
|
||||
|
||||
template<std::size_t Index, typename AnyOf>
|
||||
struct matcher_handler<Index, matcher<AnyOf>> {
|
||||
static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &, const Entity entt) {
|
||||
(obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
|
||||
template<std::size_t Index, typename... Reject, typename... Require, typename AnyOf>
|
||||
struct matcher_handler<Index, matcher<matcher<type_list<Reject...>, type_list<Require...>>, AnyOf>> {
|
||||
static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> ®, const Entity entt) {
|
||||
if(reg.template has<Require...>(entt) && !(reg.template has<Reject>(entt) || ...)) {
|
||||
(obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
|
||||
}
|
||||
}
|
||||
|
||||
static void discard_if(basic_observer *obs, const basic_registry<Entity> &, const Entity entt) {
|
||||
@@ -162,20 +186,26 @@ class basic_observer {
|
||||
}
|
||||
|
||||
static void disconnect(basic_registry<Entity> ®, const basic_observer &obs) {
|
||||
(reg.template on_replace<AnyOf>().disconnect(&obs));
|
||||
(reg.template on_destroy<AnyOf>().disconnect(&obs));
|
||||
reg.template on_replace<AnyOf>().disconnect(&obs);
|
||||
reg.template on_destroy<AnyOf>().disconnect(&obs);
|
||||
(reg.template on_destroy<Require>().disconnect(&obs), ...);
|
||||
(reg.template on_construct<Reject>().disconnect(&obs), ...);
|
||||
}
|
||||
|
||||
static void connect(basic_observer &obs, basic_registry<Entity> ®) {
|
||||
reg.template on_replace<AnyOf>().template connect<&maybe_valid_if>(&obs);
|
||||
reg.template on_destroy<AnyOf>().template connect<&discard_if>(&obs);
|
||||
(reg.template on_destroy<Require>().template connect<&discard_if>(&obs), ...);
|
||||
(reg.template on_construct<Reject>().template connect<&discard_if>(&obs), ...);
|
||||
}
|
||||
};
|
||||
|
||||
template<std::size_t Index, typename... AllOf, typename... NoneOf>
|
||||
struct matcher_handler<Index, matcher<type_list<AllOf...>, type_list<NoneOf...>>> {
|
||||
template<std::size_t Index, typename... Reject, typename... Require, typename... NoneOf, typename... AllOf>
|
||||
struct matcher_handler<Index, matcher<matcher<type_list<Reject...>, type_list<Require...>>, type_list<NoneOf...>, type_list<AllOf...>>> {
|
||||
static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> ®, const Entity entt) {
|
||||
if(reg.template has<AllOf...>(entt) && !(reg.template has<NoneOf>(entt) || ...)) {
|
||||
if(reg.template has<AllOf...>(entt) && !(reg.template has<NoneOf>(entt) || ...)
|
||||
&& reg.template has<Require...>(entt) && !(reg.template has<Reject>(entt) || ...))
|
||||
{
|
||||
(obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
|
||||
}
|
||||
}
|
||||
@@ -191,6 +221,8 @@ class basic_observer {
|
||||
((reg.template on_destroy<AllOf>().disconnect(&obs)), ...);
|
||||
((reg.template on_construct<NoneOf>().disconnect(&obs)), ...);
|
||||
((reg.template on_destroy<NoneOf>().disconnect(&obs)), ...);
|
||||
(reg.template on_destroy<Require>().disconnect(&obs), ...);
|
||||
(reg.template on_construct<Reject>().disconnect(&obs), ...);
|
||||
}
|
||||
|
||||
static void connect(basic_observer &obs, basic_registry<Entity> ®) {
|
||||
@@ -198,6 +230,8 @@ class basic_observer {
|
||||
(reg.template on_destroy<NoneOf>().template connect<&maybe_valid_if>(&obs), ...);
|
||||
(reg.template on_destroy<AllOf>().template connect<&discard_if>(&obs), ...);
|
||||
(reg.template on_construct<NoneOf>().template connect<&discard_if>(&obs), ...);
|
||||
(reg.template on_destroy<Require>().template connect<&discard_if>(&obs), ...);
|
||||
(reg.template on_construct<Reject>().template connect<&discard_if>(&obs), ...);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -341,7 +375,47 @@ public:
|
||||
|
||||
/*! @brief Resets the underlying container. */
|
||||
void clear() {
|
||||
return view.reset();
|
||||
view.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterates entities and applies the given function object to them,
|
||||
* then clears the observer.
|
||||
*
|
||||
* The function object is invoked for each entity.<br/>
|
||||
* The signature of the function must be equivalent to the following form:
|
||||
*
|
||||
* @code{.cpp}
|
||||
* void(const entity_type);
|
||||
* @endcode
|
||||
*
|
||||
* @tparam Func Type of the function object to invoke.
|
||||
* @param func A valid function object.
|
||||
*/
|
||||
template<typename Func>
|
||||
void each(Func func) const {
|
||||
static_assert(std::is_invocable_v<Func, entity_type>);
|
||||
std::for_each(begin(), end(), std::move(func));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Iterates entities and applies the given function object to them,
|
||||
* then clears the observer.
|
||||
*
|
||||
* The function object is invoked for each entity.<br/>
|
||||
* The signature of the function must be equivalent to the following form:
|
||||
*
|
||||
* @code{.cpp}
|
||||
* void(const entity_type);
|
||||
* @endcode
|
||||
*
|
||||
* @tparam Func Type of the function object to invoke.
|
||||
* @param func A valid function object.
|
||||
*/
|
||||
template<typename Func>
|
||||
void each(Func func) {
|
||||
std::as_const(*this).each(std::move(func));
|
||||
clear();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <gtest/gtest.h>
|
||||
#include <entt/entity/observer.hpp>
|
||||
#include <entt/entity/registry.hpp>
|
||||
@@ -82,9 +84,49 @@ TEST(Observer, AllOf) {
|
||||
ASSERT_TRUE(observer.empty());
|
||||
}
|
||||
|
||||
TEST(Observer, AllOfFiltered) {
|
||||
constexpr auto collector = entt::collector
|
||||
.group<int>().when<char>(entt::exclude<double>);
|
||||
|
||||
entt::registry registry;
|
||||
entt::observer observer{registry, collector};
|
||||
const auto entity = registry.create();
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
|
||||
registry.assign<int>(entity);
|
||||
|
||||
ASSERT_EQ(observer.size(), entt::observer::size_type{});
|
||||
ASSERT_TRUE(observer.empty());
|
||||
ASSERT_EQ(observer.data(), nullptr);
|
||||
|
||||
registry.remove<int>(entity);
|
||||
registry.assign<char>(entity);
|
||||
registry.assign<double>(entity);
|
||||
registry.assign<int>(entity);
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
|
||||
registry.remove<int>(entity);
|
||||
registry.remove<double>(entity);
|
||||
registry.assign<int>(entity);
|
||||
|
||||
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
|
||||
ASSERT_FALSE(observer.empty());
|
||||
ASSERT_EQ(*observer.data(), entity);
|
||||
|
||||
registry.assign<double>(entity);
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
|
||||
registry.remove<double>(entity);
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
}
|
||||
|
||||
TEST(Observer, Observe) {
|
||||
entt::registry registry;
|
||||
entt::observer observer{registry, entt::collector.replace<int, char>()};
|
||||
entt::observer observer{registry, entt::collector.replace<int>().replace<char>()};
|
||||
const auto entity = registry.create();
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
@@ -116,6 +158,45 @@ TEST(Observer, Observe) {
|
||||
ASSERT_TRUE(observer.empty());
|
||||
}
|
||||
|
||||
TEST(Observer, ObserveFiltered) {
|
||||
constexpr auto collector = entt::collector
|
||||
.replace<int>().when<char>(entt::exclude<double>);
|
||||
|
||||
entt::registry registry;
|
||||
entt::observer observer{registry, collector};
|
||||
const auto entity = registry.create();
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
|
||||
registry.assign<int>(entity);
|
||||
registry.replace<int>(entity);
|
||||
|
||||
ASSERT_EQ(observer.size(), entt::observer::size_type{});
|
||||
ASSERT_TRUE(observer.empty());
|
||||
ASSERT_EQ(observer.data(), nullptr);
|
||||
|
||||
registry.assign<char>(entity);
|
||||
registry.assign<double>(entity);
|
||||
registry.replace<int>(entity);
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
|
||||
registry.remove<double>(entity);
|
||||
registry.replace<int>(entity);
|
||||
|
||||
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
|
||||
ASSERT_FALSE(observer.empty());
|
||||
ASSERT_EQ(*observer.data(), entity);
|
||||
|
||||
registry.assign<double>(entity);
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
|
||||
registry.remove<double>(entity);
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
}
|
||||
|
||||
TEST(Observer, AllOfObserve) {
|
||||
entt::registry registry;
|
||||
entt::observer observer{};
|
||||
@@ -151,7 +232,6 @@ TEST(Observer, AllOfObserve) {
|
||||
ASSERT_TRUE(observer.empty());
|
||||
}
|
||||
|
||||
|
||||
TEST(Observer, CrossRulesCornerCase) {
|
||||
entt::registry registry;
|
||||
entt::observer observer{registry, entt::collector.group<int>().group<char>()};
|
||||
@@ -167,3 +247,26 @@ TEST(Observer, CrossRulesCornerCase) {
|
||||
|
||||
ASSERT_FALSE(observer.empty());
|
||||
}
|
||||
|
||||
TEST(Observer, Each) {
|
||||
entt::registry registry;
|
||||
entt::observer observer{registry, entt::collector.group<int>()};
|
||||
const auto entity = std::get<0>(registry.create<int>());
|
||||
|
||||
ASSERT_FALSE(observer.empty());
|
||||
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
|
||||
|
||||
std::as_const(observer).each([entity](const auto entt) {
|
||||
ASSERT_EQ(entity, entt);
|
||||
});
|
||||
|
||||
ASSERT_FALSE(observer.empty());
|
||||
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
|
||||
|
||||
observer.each([entity](const auto entt) {
|
||||
ASSERT_EQ(entity, entt);
|
||||
});
|
||||
|
||||
ASSERT_TRUE(observer.empty());
|
||||
ASSERT_EQ(observer.size(), entt::observer::size_type{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user