mirror of
https://github.com/godotengine/godot.git
synced 2024-11-21 03:18:37 +08:00
Merge pull request #71264 from TokageItLab/improve-statemachine
Add next/reset function to `AnimationStateMachine`
This commit is contained in:
commit
a202f5104f
@ -50,11 +50,19 @@
|
||||
Returns [code]true[/code] if an animation is playing.
|
||||
</description>
|
||||
</method>
|
||||
<method name="next">
|
||||
<return type="void" />
|
||||
<description>
|
||||
If there is a next path by travel or auto advance, immediately transitions from the current state to the next state.
|
||||
</description>
|
||||
</method>
|
||||
<method name="start">
|
||||
<return type="void" />
|
||||
<param index="0" name="node" type="StringName" />
|
||||
<param index="1" name="reset" type="bool" default="true" />
|
||||
<description>
|
||||
Starts playing the given animation.
|
||||
If [param reset] is [code]true[/code], the animation is played from the beginning.
|
||||
</description>
|
||||
</method>
|
||||
<method name="stop">
|
||||
@ -66,8 +74,11 @@
|
||||
<method name="travel">
|
||||
<return type="void" />
|
||||
<param index="0" name="to_node" type="StringName" />
|
||||
<param index="1" name="reset_on_teleport" type="bool" default="true" />
|
||||
<description>
|
||||
Transitions from the current state to another one, following the shortest path.
|
||||
If the path does not connect from the current state, the animation will play after the state teleports.
|
||||
If [param reset_on_teleport] is [code]true[/code], the animation is played from the beginning when the travel cause a teleportation.
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
|
@ -28,6 +28,9 @@
|
||||
<member name="priority" type="int" setter="set_priority" getter="get_priority" default="1">
|
||||
Lower priority transitions are preferred when travelling through the tree via [method AnimationNodeStateMachinePlayback.travel] or [member advance_mode] is set to [constant ADVANCE_MODE_AUTO].
|
||||
</member>
|
||||
<member name="reset" type="bool" setter="set_reset" getter="is_reset" default="true">
|
||||
If [code]true[/code], the destination animation is played back from the beginning when switched.
|
||||
</member>
|
||||
<member name="switch_mode" type="int" setter="set_switch_mode" getter="get_switch_mode" enum="AnimationNodeStateMachineTransition.SwitchMode" default="0">
|
||||
The transition type.
|
||||
</member>
|
||||
|
@ -43,7 +43,7 @@
|
||||
<member name="enabled_inputs" type="int" setter="set_enabled_inputs" getter="get_enabled_inputs" default="0">
|
||||
The number of enabled input ports for this node.
|
||||
</member>
|
||||
<member name="from_start" type="bool" setter="set_from_start" getter="is_from_start" default="true">
|
||||
<member name="reset" type="bool" setter="set_reset" getter="is_reset" default="true">
|
||||
If [code]true[/code], the destination animation is played back from the beginning when switched.
|
||||
</member>
|
||||
<member name="xfade_curve" type="Curve" setter="set_xfade_curve" getter="get_xfade_curve">
|
||||
|
@ -718,12 +718,12 @@ Ref<Curve> AnimationNodeTransition::get_xfade_curve() const {
|
||||
return xfade_curve;
|
||||
}
|
||||
|
||||
void AnimationNodeTransition::set_from_start(bool p_from_start) {
|
||||
from_start = p_from_start;
|
||||
void AnimationNodeTransition::set_reset(bool p_reset) {
|
||||
reset = p_reset;
|
||||
}
|
||||
|
||||
bool AnimationNodeTransition::is_from_start() const {
|
||||
return from_start;
|
||||
bool AnimationNodeTransition::is_reset() const {
|
||||
return reset;
|
||||
}
|
||||
|
||||
double AnimationNodeTransition::process(double p_time, bool p_seek, bool p_is_external_seeking) {
|
||||
@ -783,7 +783,7 @@ double AnimationNodeTransition::process(double p_time, bool p_seek, bool p_is_ex
|
||||
|
||||
// Blend values must be more than CMP_EPSILON to process discrete keys in edge.
|
||||
real_t blend_inv = 1.0 - blend;
|
||||
if (from_start && !p_seek && switched) { //just switched, seek to start of current
|
||||
if (reset && !p_seek && switched) { //just switched, seek to start of current
|
||||
rem = blend_input(cur_current, 0, true, p_is_external_seeking, Math::is_zero_approx(blend_inv) ? CMP_EPSILON : blend_inv, FILTER_IGNORE, true);
|
||||
} else {
|
||||
rem = blend_input(cur_current, p_time, p_seek, p_is_external_seeking, Math::is_zero_approx(blend_inv) ? CMP_EPSILON : blend_inv, FILTER_IGNORE, true);
|
||||
@ -836,13 +836,13 @@ void AnimationNodeTransition::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("set_xfade_curve", "curve"), &AnimationNodeTransition::set_xfade_curve);
|
||||
ClassDB::bind_method(D_METHOD("get_xfade_curve"), &AnimationNodeTransition::get_xfade_curve);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_from_start", "from_start"), &AnimationNodeTransition::set_from_start);
|
||||
ClassDB::bind_method(D_METHOD("is_from_start"), &AnimationNodeTransition::is_from_start);
|
||||
ClassDB::bind_method(D_METHOD("set_reset", "reset"), &AnimationNodeTransition::set_reset);
|
||||
ClassDB::bind_method(D_METHOD("is_reset"), &AnimationNodeTransition::is_reset);
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "enabled_inputs", PROPERTY_HINT_RANGE, "0,64,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), "set_enabled_inputs", "get_enabled_inputs");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "xfade_time", PROPERTY_HINT_RANGE, "0,120,0.01,suffix:s"), "set_xfade_time", "get_xfade_time");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "xfade_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_xfade_curve", "get_xfade_curve");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "from_start"), "set_from_start", "is_from_start");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "reset"), "set_reset", "is_reset");
|
||||
|
||||
for (int i = 0; i < MAX_INPUTS; i++) {
|
||||
ADD_PROPERTYI(PropertyInfo(Variant::STRING, "input_" + itos(i) + "/name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_INTERNAL), "set_input_caption", "get_input_caption", i);
|
||||
|
@ -299,7 +299,7 @@ class AnimationNodeTransition : public AnimationNodeSync {
|
||||
|
||||
double xfade_time = 0.0;
|
||||
Ref<Curve> xfade_curve;
|
||||
bool from_start = true;
|
||||
bool reset = true;
|
||||
|
||||
void _update_inputs();
|
||||
|
||||
@ -328,8 +328,8 @@ public:
|
||||
void set_xfade_curve(const Ref<Curve> &p_curve);
|
||||
Ref<Curve> get_xfade_curve() const;
|
||||
|
||||
void set_from_start(bool p_from_start);
|
||||
bool is_from_start() const;
|
||||
void set_reset(bool p_reset);
|
||||
bool is_reset() const;
|
||||
|
||||
double process(double p_time, bool p_seek, bool p_is_external_seeking) override;
|
||||
|
||||
|
@ -107,6 +107,15 @@ Ref<Curve> AnimationNodeStateMachineTransition::get_xfade_curve() const {
|
||||
return xfade_curve;
|
||||
}
|
||||
|
||||
void AnimationNodeStateMachineTransition::set_reset(bool p_reset) {
|
||||
reset = p_reset;
|
||||
emit_changed();
|
||||
}
|
||||
|
||||
bool AnimationNodeStateMachineTransition::is_reset() const {
|
||||
return reset;
|
||||
}
|
||||
|
||||
void AnimationNodeStateMachineTransition::set_priority(int p_priority) {
|
||||
priority = p_priority;
|
||||
emit_changed();
|
||||
@ -132,6 +141,9 @@ void AnimationNodeStateMachineTransition::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("set_xfade_curve", "curve"), &AnimationNodeStateMachineTransition::set_xfade_curve);
|
||||
ClassDB::bind_method(D_METHOD("get_xfade_curve"), &AnimationNodeStateMachineTransition::get_xfade_curve);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_reset", "reset"), &AnimationNodeStateMachineTransition::set_reset);
|
||||
ClassDB::bind_method(D_METHOD("is_reset"), &AnimationNodeStateMachineTransition::is_reset);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("set_priority", "priority"), &AnimationNodeStateMachineTransition::set_priority);
|
||||
ClassDB::bind_method(D_METHOD("get_priority"), &AnimationNodeStateMachineTransition::get_priority);
|
||||
|
||||
@ -140,6 +152,9 @@ void AnimationNodeStateMachineTransition::_bind_methods() {
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "xfade_time", PROPERTY_HINT_RANGE, "0,240,0.01,suffix:s"), "set_xfade_time", "get_xfade_time");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "xfade_curve", PROPERTY_HINT_RESOURCE_TYPE, "Curve"), "set_xfade_curve", "get_xfade_curve");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "reset"), "set_reset", "is_reset");
|
||||
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "priority", PROPERTY_HINT_RANGE, "0,32,1"), "set_priority", "get_priority");
|
||||
ADD_GROUP("Switch", "");
|
||||
ADD_PROPERTY(PropertyInfo(Variant::INT, "switch_mode", PROPERTY_HINT_ENUM, "Immediate,Sync,At End"), "set_switch_mode", "get_switch_mode");
|
||||
@ -164,16 +179,25 @@ AnimationNodeStateMachineTransition::AnimationNodeStateMachineTransition() {
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
void AnimationNodeStateMachinePlayback::travel(const StringName &p_state) {
|
||||
start_request_travel = true;
|
||||
void AnimationNodeStateMachinePlayback::travel(const StringName &p_state, bool p_reset_on_teleport) {
|
||||
travel_request = p_state;
|
||||
reset_request_on_teleport = p_reset_on_teleport;
|
||||
stop_request = false;
|
||||
}
|
||||
|
||||
void AnimationNodeStateMachinePlayback::start(const StringName &p_state, bool p_reset) {
|
||||
travel_request = StringName();
|
||||
reset_request = p_reset;
|
||||
_start(p_state);
|
||||
}
|
||||
|
||||
void AnimationNodeStateMachinePlayback::_start(const StringName &p_state) {
|
||||
start_request = p_state;
|
||||
stop_request = false;
|
||||
}
|
||||
|
||||
void AnimationNodeStateMachinePlayback::start(const StringName &p_state) {
|
||||
start_request_travel = false;
|
||||
start_request = p_state;
|
||||
stop_request = false;
|
||||
void AnimationNodeStateMachinePlayback::next() {
|
||||
next_request = true;
|
||||
}
|
||||
|
||||
void AnimationNodeStateMachinePlayback::stop() {
|
||||
@ -323,6 +347,15 @@ bool AnimationNodeStateMachinePlayback::_travel(AnimationNodeStateMachine *p_sta
|
||||
}
|
||||
|
||||
double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_state_machine, double p_time, bool p_seek, bool p_is_external_seeking) {
|
||||
double rem = _process(p_state_machine, p_time, p_seek, p_is_external_seeking);
|
||||
start_request = StringName();
|
||||
next_request = false;
|
||||
stop_request = false;
|
||||
reset_request_on_teleport = false;
|
||||
return rem;
|
||||
}
|
||||
|
||||
double AnimationNodeStateMachinePlayback::_process(AnimationNodeStateMachine *p_state_machine, double p_time, bool p_seek, bool p_is_external_seeking) {
|
||||
if (p_time == -1) {
|
||||
Ref<AnimationNodeStateMachine> anodesm = p_state_machine->states[current].node;
|
||||
if (anodesm.is_valid()) {
|
||||
@ -335,14 +368,13 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
//if not playing and it can restart, then restart
|
||||
if (!playing && start_request == StringName()) {
|
||||
if (!stop_request && p_state_machine->start_node) {
|
||||
start(p_state_machine->start_node);
|
||||
_start(p_state_machine->start_node);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (playing && stop_request) {
|
||||
stop_request = false;
|
||||
playing = false;
|
||||
return 0;
|
||||
}
|
||||
@ -350,42 +382,45 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
bool play_start = false;
|
||||
|
||||
if (start_request != StringName()) {
|
||||
if (start_request_travel) {
|
||||
if (!playing) {
|
||||
if (!stop_request && p_state_machine->start_node) {
|
||||
// can restart, just postpone traveling
|
||||
path.clear();
|
||||
current = p_state_machine->start_node;
|
||||
playing = true;
|
||||
play_start = true;
|
||||
} else {
|
||||
// stopped, invalid state
|
||||
String node_name = start_request;
|
||||
start_request = StringName(); //clear start request
|
||||
ERR_FAIL_V_MSG(0, "Can't travel to '" + node_name + "' if state machine is not playing. Maybe you need to enable Autoplay on Load for one of the nodes in your state machine or call .start() first?");
|
||||
}
|
||||
} else {
|
||||
if (!_travel(p_state_machine, start_request)) {
|
||||
// can't travel, then teleport
|
||||
path.clear();
|
||||
current = start_request;
|
||||
play_start = true;
|
||||
}
|
||||
start_request = StringName(); //clear start request
|
||||
}
|
||||
// teleport to start
|
||||
if (p_state_machine->states.has(start_request)) {
|
||||
path.clear();
|
||||
current = start_request;
|
||||
playing = true;
|
||||
play_start = true;
|
||||
} else {
|
||||
// teleport to start
|
||||
if (p_state_machine->states.has(start_request)) {
|
||||
StringName node = start_request;
|
||||
ERR_FAIL_V_MSG(0, "No such node: '" + node + "'");
|
||||
}
|
||||
} else if (travel_request != StringName()) {
|
||||
if (!playing) {
|
||||
if (!stop_request && p_state_machine->start_node) {
|
||||
// can restart, just postpone traveling
|
||||
path.clear();
|
||||
current = start_request;
|
||||
current = p_state_machine->start_node;
|
||||
playing = true;
|
||||
play_start = true;
|
||||
start_request = StringName(); //clear start request
|
||||
} else {
|
||||
StringName node = start_request;
|
||||
start_request = StringName(); //clear start request
|
||||
ERR_FAIL_V_MSG(0, "No such node: '" + node + "'");
|
||||
// stopped, invalid state
|
||||
String node_name = travel_request;
|
||||
travel_request = StringName();
|
||||
ERR_FAIL_V_MSG(0, "Can't travel to '" + node_name + "' if state machine is not playing. Maybe you need to enable Autoplay on Load for one of the nodes in your state machine or call .start() first?");
|
||||
}
|
||||
} else {
|
||||
if (!_travel(p_state_machine, travel_request)) {
|
||||
// can't travel, then teleport
|
||||
if (p_state_machine->states.has(travel_request)) {
|
||||
path.clear();
|
||||
current = travel_request;
|
||||
play_start = true;
|
||||
reset_request = reset_request_on_teleport;
|
||||
} else {
|
||||
StringName node = travel_request;
|
||||
travel_request = StringName();
|
||||
ERR_FAIL_V_MSG(0, "No such node: '" + node + "'");
|
||||
}
|
||||
}
|
||||
travel_request = StringName();
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,8 +431,11 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
current = p_state_machine->start_node;
|
||||
}
|
||||
|
||||
len_current = p_state_machine->blend_node(current, p_state_machine->states[current].node, 0, true, p_is_external_seeking, 1.0, AnimationNode::FILTER_IGNORE, true);
|
||||
pos_current = 0;
|
||||
if (reset_request) {
|
||||
len_current = p_state_machine->blend_node(current, p_state_machine->states[current].node, 0, true, p_is_external_seeking, 1.0, AnimationNode::FILTER_IGNORE, true);
|
||||
pos_current = 0;
|
||||
reset_request = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!p_state_machine->states.has(current)) {
|
||||
@ -421,7 +459,8 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
if (current_curve.is_valid()) {
|
||||
fade_blend = current_curve->sample(fade_blend);
|
||||
}
|
||||
double rem = p_state_machine->blend_node(current, p_state_machine->states[current].node, p_time, p_seek, p_is_external_seeking, Math::is_zero_approx(fade_blend) ? CMP_EPSILON : fade_blend, AnimationNode::FILTER_IGNORE, true); // Blend values must be more than CMP_EPSILON to process discrete keys in edge.
|
||||
|
||||
double rem = do_start ? len_current : p_state_machine->blend_node(current, p_state_machine->states[current].node, p_time, p_seek, p_is_external_seeking, Math::is_zero_approx(fade_blend) ? CMP_EPSILON : fade_blend, AnimationNode::FILTER_IGNORE, true); // Blend values must be more than CMP_EPSILON to process discrete keys in edge.
|
||||
|
||||
if (fading_from != StringName()) {
|
||||
double fade_blend_inv = 1.0 - fade_blend;
|
||||
@ -457,6 +496,7 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
next_xfade = p_state_machine->transitions[i].transition->get_xfade_time();
|
||||
current_curve = p_state_machine->transitions[i].transition->get_xfade_curve();
|
||||
switch_mode = p_state_machine->transitions[i].transition->get_switch_mode();
|
||||
reset_request = p_state_machine->transitions[i].transition->is_reset();
|
||||
next = path[0];
|
||||
}
|
||||
}
|
||||
@ -513,6 +553,7 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
current_curve = p_state_machine->transitions[auto_advance_to].transition->get_xfade_curve();
|
||||
next_xfade = p_state_machine->transitions[auto_advance_to].transition->get_xfade_time();
|
||||
switch_mode = p_state_machine->transitions[auto_advance_to].transition->get_switch_mode();
|
||||
reset_request = p_state_machine->transitions[auto_advance_to].transition->is_reset();
|
||||
}
|
||||
}
|
||||
|
||||
@ -567,7 +608,7 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
goto_next = fading_from == StringName();
|
||||
}
|
||||
|
||||
if (goto_next) { //end_loop should be used because fade time may be too small or zero and animation may have looped
|
||||
if (next_request || goto_next) { //end_loop should be used because fade time may be too small or zero and animation may have looped
|
||||
if (next_xfade) {
|
||||
//time to fade, baby
|
||||
fading_from = current;
|
||||
@ -591,7 +632,9 @@ double AnimationNodeStateMachinePlayback::process(AnimationNodeStateMachine *p_s
|
||||
|
||||
current = next;
|
||||
|
||||
len_current = p_state_machine->blend_node(current, p_state_machine->states[current].node, 0, true, p_is_external_seeking, CMP_EPSILON, AnimationNode::FILTER_IGNORE, true); // Process next node's first key in here.
|
||||
if (reset_request) {
|
||||
len_current = p_state_machine->blend_node(current, p_state_machine->states[current].node, 0, true, p_is_external_seeking, CMP_EPSILON, AnimationNode::FILTER_IGNORE, true); // Process next node's first key in here.
|
||||
}
|
||||
if (switch_mode == AnimationNodeStateMachineTransition::SWITCH_MODE_SYNC) {
|
||||
pos_current = MIN(pos_current, len_current);
|
||||
p_state_machine->blend_node(current, p_state_machine->states[current].node, pos_current, true, p_is_external_seeking, 0, AnimationNode::FILTER_IGNORE, true);
|
||||
@ -652,8 +695,9 @@ bool AnimationNodeStateMachinePlayback::_check_advance_condition(const Ref<Anima
|
||||
}
|
||||
|
||||
void AnimationNodeStateMachinePlayback::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("travel", "to_node"), &AnimationNodeStateMachinePlayback::travel);
|
||||
ClassDB::bind_method(D_METHOD("start", "node"), &AnimationNodeStateMachinePlayback::start);
|
||||
ClassDB::bind_method(D_METHOD("travel", "to_node", "reset_on_teleport"), &AnimationNodeStateMachinePlayback::travel, DEFVAL(true));
|
||||
ClassDB::bind_method(D_METHOD("start", "node", "reset"), &AnimationNodeStateMachinePlayback::start, DEFVAL(true));
|
||||
ClassDB::bind_method(D_METHOD("next"), &AnimationNodeStateMachinePlayback::next);
|
||||
ClassDB::bind_method(D_METHOD("stop"), &AnimationNodeStateMachinePlayback::stop);
|
||||
ClassDB::bind_method(D_METHOD("is_playing"), &AnimationNodeStateMachinePlayback::is_playing);
|
||||
ClassDB::bind_method(D_METHOD("get_current_node"), &AnimationNodeStateMachinePlayback::get_current_node);
|
||||
|
@ -57,6 +57,7 @@ private:
|
||||
StringName advance_condition_name;
|
||||
float xfade_time = 0.0;
|
||||
Ref<Curve> xfade_curve;
|
||||
bool reset = true;
|
||||
int priority = 1;
|
||||
String advance_expression;
|
||||
|
||||
@ -84,6 +85,9 @@ public:
|
||||
void set_xfade_time(float p_xfade);
|
||||
float get_xfade_time() const;
|
||||
|
||||
void set_reset(bool p_reset);
|
||||
bool is_reset() const;
|
||||
|
||||
void set_xfade_curve(const Ref<Curve> &p_curve);
|
||||
Ref<Curve> get_xfade_curve() const;
|
||||
|
||||
@ -131,10 +135,15 @@ class AnimationNodeStateMachinePlayback : public Resource {
|
||||
bool playing = false;
|
||||
|
||||
StringName start_request;
|
||||
bool start_request_travel = false;
|
||||
StringName travel_request;
|
||||
bool reset_request = false;
|
||||
bool reset_request_on_teleport = false;
|
||||
bool next_request = false;
|
||||
bool stop_request = false;
|
||||
|
||||
bool _travel(AnimationNodeStateMachine *p_state_machine, const StringName &p_travel);
|
||||
void _start(const StringName &p_state);
|
||||
double _process(AnimationNodeStateMachine *p_state_machine, double p_time, bool p_seek, bool p_is_external_seeking);
|
||||
|
||||
double process(AnimationNodeStateMachine *p_state_machine, double p_time, bool p_seek, bool p_is_external_seeking);
|
||||
|
||||
@ -144,8 +153,9 @@ protected:
|
||||
static void _bind_methods();
|
||||
|
||||
public:
|
||||
void travel(const StringName &p_state);
|
||||
void start(const StringName &p_state);
|
||||
void travel(const StringName &p_state, bool p_reset_on_teleport = true);
|
||||
void start(const StringName &p_state, bool p_reset = true);
|
||||
void next();
|
||||
void stop();
|
||||
bool is_playing() const;
|
||||
StringName get_current_node() const;
|
||||
|
Loading…
Reference in New Issue
Block a user