Fixed looping animations
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
#include <map>
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <scene/resources/animation.h>
|
#include <scene/resources/animation.h>
|
||||||
#include <servers/audio/audio_stream.h>
|
#include <servers/audio/audio_stream.h>
|
||||||
@@ -31,6 +33,7 @@ static void _animation_process(flecs::entity_t entity, flecs::world &ecs,
|
|||||||
static void _stop_playing_caches(struct AnimationPlayerData *player);
|
static void _stop_playing_caches(struct AnimationPlayerData *player);
|
||||||
static void _set_process(struct AnimationPlayerData *player, bool p_process,
|
static void _set_process(struct AnimationPlayerData *player, bool p_process,
|
||||||
bool p_force = false);
|
bool p_force = false);
|
||||||
|
Map<StringName, AnimationData> AnimationPlayerData::animation_set[2];
|
||||||
} //namespace ECS
|
} //namespace ECS
|
||||||
void ECS::play(struct ECS::AnimationPlayerData *player,
|
void ECS::play(struct ECS::AnimationPlayerData *player,
|
||||||
const StringName &p_name, float p_custom_blend, float p_custom_scale,
|
const StringName &p_name, float p_custom_blend, float p_custom_scale,
|
||||||
@@ -41,8 +44,9 @@ void ECS::play(struct ECS::AnimationPlayerData *player,
|
|||||||
if (String(name) == "") {
|
if (String(name) == "") {
|
||||||
name = player->playback.assigned;
|
name = player->playback.assigned;
|
||||||
}
|
}
|
||||||
|
int type_id = player->type_id;
|
||||||
|
|
||||||
ERR_FAIL_COND_MSG(!player->animation_set.has(name),
|
ERR_FAIL_COND_MSG(!player->animation_set[type_id].has(name),
|
||||||
"Animation not found: " + name + ".");
|
"Animation not found: " + name + ".");
|
||||||
|
|
||||||
Playback &c = player->playback;
|
Playback &c = player->playback;
|
||||||
@@ -51,6 +55,7 @@ void ECS::play(struct ECS::AnimationPlayerData *player,
|
|||||||
float blend_time = 0;
|
float blend_time = 0;
|
||||||
// find if it can blend
|
// find if it can blend
|
||||||
ECS::BlendKey bk;
|
ECS::BlendKey bk;
|
||||||
|
flecs::log::trace("%s: %p", __func__, c.current.from);
|
||||||
bk.from = c.current.from->name;
|
bk.from = c.current.from->name;
|
||||||
bk.to = name;
|
bk.to = name;
|
||||||
|
|
||||||
@@ -88,7 +93,7 @@ void ECS::play(struct ECS::AnimationPlayerData *player,
|
|||||||
_stop_playing_caches(player);
|
_stop_playing_caches(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
c.current.from = &player->animation_set[name];
|
c.current.from = &player->animation_set[type_id][name];
|
||||||
flecs::log::trace("setting animation from %p", c.current.from);
|
flecs::log::trace("setting animation from %p", c.current.from);
|
||||||
|
|
||||||
if (c.assigned != name) { // reset
|
if (c.assigned != name) { // reset
|
||||||
@@ -132,24 +137,24 @@ void ECS::play(struct ECS::AnimationPlayerData *player,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
StringName next = animation_get_next(player, p_name);
|
StringName next = animation_get_next(player, p_name);
|
||||||
if (next != StringName() && player->animation_set.has(next)) {
|
if (next != StringName() && player->animation_set[type_id].has(next)) {
|
||||||
queue(player, next);
|
queue(player, next);
|
||||||
}
|
}
|
||||||
flecs::log::trace("%s(): %d animation %p", __func__, __LINE__,
|
flecs::log::trace("%s(): %d animation %p", __func__, __LINE__,
|
||||||
c.current.from->get_animation().ptr());
|
c.current.from->get_animation());
|
||||||
}
|
}
|
||||||
static void ECS::_ensure_node_caches(struct ECS::AnimationPlayerData *player,
|
static void ECS::_ensure_node_caches(struct ECS::AnimationPlayerData *player,
|
||||||
flecs::entity_t entity, flecs::world &ecs, AnimationData *p_anim,
|
flecs::entity_t entity, flecs::world &ecs, AnimationData *p_anim,
|
||||||
Node *p_root_override)
|
Node *p_root_override)
|
||||||
{
|
{
|
||||||
// Already cached?
|
// Already cached?
|
||||||
assert(p_anim->get_animation().is_valid());
|
assert(p_anim->get_animation() != nullptr);
|
||||||
if (p_anim->node_cache_size ==
|
if (p_anim->node_cache_size ==
|
||||||
p_anim->get_animation()->get_track_count()) {
|
p_anim->get_animation()->get_track_count()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Animation *a = p_anim->get_animation().operator->();
|
const Animation *a = p_anim->get_animation();
|
||||||
|
|
||||||
p_anim->node_cache_size = a->get_track_count();
|
p_anim->node_cache_size = a->get_track_count();
|
||||||
|
|
||||||
@@ -178,10 +183,22 @@ static void ECS::_ensure_node_caches(struct ECS::AnimationPlayerData *player,
|
|||||||
key.bone_idx = bone_id;
|
key.bone_idx = bone_id;
|
||||||
String path = np.get_subname(0);
|
String path = np.get_subname(0);
|
||||||
|
|
||||||
if (!player->node_cache_map.has(key))
|
AnimationCache *cache = e.get_mut<AnimationCache>();
|
||||||
player->node_cache_map[key] = ECS::TrackNodeCache();
|
|
||||||
|
|
||||||
p_anim->node_cache[i] = &player->node_cache_map[key];
|
if (!cache->node_cache_map.has(key)) {
|
||||||
|
int j, idx = -1;
|
||||||
|
for (j = 0; j < NODE_CACHE_UPDATE_MAX; j++)
|
||||||
|
if (!cache->node_cache_data[j].used) {
|
||||||
|
idx = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert(idx >= 0);
|
||||||
|
cache->node_cache_data[idx] = ECS::TrackNodeCache();
|
||||||
|
cache->node_cache_data[idx].used = true;
|
||||||
|
cache->node_cache_map[key] = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
p_anim->node_cache[i] = &cache->node_cache_data[cache->node_cache_map[key]];
|
||||||
p_anim->node_cache[i]->path = a->track_get_path(i);
|
p_anim->node_cache[i]->path = a->track_get_path(i);
|
||||||
p_anim->node_cache[i]->resource = resource;
|
p_anim->node_cache[i]->resource = resource;
|
||||||
if (a->track_get_type(i) == Animation::TYPE_TRANSFORM) {
|
if (a->track_get_type(i) == Animation::TYPE_TRANSFORM) {
|
||||||
@@ -245,7 +262,7 @@ static void ECS::_animation_process_animation(flecs::entity_t entity,
|
|||||||
ERR_FAIL_COND(p_anim->node_cache_size !=
|
ERR_FAIL_COND(p_anim->node_cache_size !=
|
||||||
p_anim->get_animation()->get_track_count());
|
p_anim->get_animation()->get_track_count());
|
||||||
|
|
||||||
const Animation *a = p_anim->get_animation().ptr();
|
const Animation *a = p_anim->get_animation();
|
||||||
|
|
||||||
for (int i = 0; i < a->get_track_count(); i++) {
|
for (int i = 0; i < a->get_track_count(); i++) {
|
||||||
// If an animation changes this animation (or it
|
// If an animation changes this animation (or it
|
||||||
@@ -876,13 +893,11 @@ static void ECS::_animation_process_data(flecs::entity_t entity,
|
|||||||
{
|
{
|
||||||
float delta = p_delta * player->speed_scale * cd.speed_scale;
|
float delta = p_delta * player->speed_scale * cd.speed_scale;
|
||||||
float next_pos = cd.pos + delta;
|
float next_pos = cd.pos + delta;
|
||||||
if (cd.from->get_animation().is_null()) {
|
if (cd.from->get_animation() == nullptr) {
|
||||||
ERR_PRINT("bad animation");
|
ERR_PRINT("bad animation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
assert(cd.from->get_animation().is_valid());
|
assert(cd.from->get_animation() != nullptr);
|
||||||
flecs::log::trace("%s(): %d animation %p", __func__, __LINE__,
|
|
||||||
cd.from->get_animation().ptr());
|
|
||||||
float len = cd.from->get_animation()->get_length();
|
float len = cd.from->get_animation()->get_length();
|
||||||
bool loop = cd.from->get_animation()->has_loop();
|
bool loop = cd.from->get_animation()->has_loop();
|
||||||
if (!loop) {
|
if (!loop) {
|
||||||
@@ -890,26 +905,27 @@ static void ECS::_animation_process_data(flecs::entity_t entity,
|
|||||||
next_pos = 0;
|
next_pos = 0;
|
||||||
else if (next_pos > len)
|
else if (next_pos > len)
|
||||||
next_pos = len;
|
next_pos = len;
|
||||||
}
|
bool backwards =
|
||||||
bool backwards =
|
signbit(delta); // Negative zero means playing backwards too
|
||||||
signbit(delta); // Negative zero means playing backwards too
|
delta = next_pos -
|
||||||
delta = next_pos -
|
cd.pos; // Fix delta (after determination of backwards
|
||||||
cd.pos; // Fix delta (after determination of backwards
|
// because negative zero is lost here)
|
||||||
// because negative zero is lost here)
|
if (&cd == &player->playback.current) {
|
||||||
if (&cd == &player->playback.current) {
|
if (!backwards && cd.pos <= len && next_pos == len) {
|
||||||
if (!backwards && cd.pos <= len && next_pos == len) {
|
//playback finished
|
||||||
//playback finished
|
player->end_reached = true;
|
||||||
player->end_reached = true;
|
flecs::log::trace("AT END!!!");
|
||||||
player->end_notify =
|
player->end_notify =
|
||||||
cd.pos < len; // Notify only if not
|
cd.pos < len; // Notify only if not
|
||||||
// already at the end
|
// already at the end
|
||||||
}
|
}
|
||||||
if (backwards && cd.pos >= 0 && next_pos == 0) {
|
if (backwards && cd.pos >= 0 && next_pos <= 0) {
|
||||||
//playback finished
|
//playback finished
|
||||||
player->end_reached = true;
|
player->end_reached = true;
|
||||||
player->end_notify =
|
player->end_notify =
|
||||||
cd.pos > 0; // Notify only if not
|
cd.pos > 0; // Notify only if not
|
||||||
// already at the beginning
|
// already at the beginning
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
float looped_next_pos = Math::fposmod(next_pos, len);
|
float looped_next_pos = Math::fposmod(next_pos, len);
|
||||||
@@ -930,14 +946,12 @@ static void ECS::_animation_process2(flecs::entity_t entity, flecs::world &ecs,
|
|||||||
struct ECS::AnimationPlayerData *player, float p_delta, bool p_started)
|
struct ECS::AnimationPlayerData *player, float p_delta, bool p_started)
|
||||||
{
|
{
|
||||||
ECS::Playback &c = player->playback;
|
ECS::Playback &c = player->playback;
|
||||||
player->accum_pass++;
|
|
||||||
|
|
||||||
if (c.current.from->get_animation().is_null()) {
|
if (c.current.from->get_animation() == nullptr) {
|
||||||
ERR_PRINT("bad animation2");
|
ERR_PRINT("bad animation2");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
flecs::log::trace("%s(): %d animation %p", __func__, __LINE__,
|
player->accum_pass++;
|
||||||
c.current.from->get_animation().ptr());
|
|
||||||
_animation_process_data(entity, ecs, player, c.current, p_delta, 1.0f,
|
_animation_process_data(entity, ecs, player, c.current, p_delta, 1.0f,
|
||||||
c.seeked && p_delta != 0, p_started);
|
c.seeked && p_delta != 0, p_started);
|
||||||
if (p_delta != 0)
|
if (p_delta != 0)
|
||||||
@@ -946,7 +960,7 @@ static void ECS::_animation_process2(flecs::entity_t entity, flecs::world &ecs,
|
|||||||
for (List<Blend>::Element *E = c.blend.back(); E; E = prev) {
|
for (List<Blend>::Element *E = c.blend.back(); E; E = prev) {
|
||||||
Blend &b = E->get();
|
Blend &b = E->get();
|
||||||
float blend = b.blend_left / b.blend_time;
|
float blend = b.blend_left / b.blend_time;
|
||||||
if (b.data.from->get_animation().is_null()) {
|
if (b.data.from->get_animation() == nullptr) {
|
||||||
ERR_PRINT("bad animation3");
|
ERR_PRINT("bad animation3");
|
||||||
b.blend_left -=
|
b.blend_left -=
|
||||||
Math::absf(player->speed_scale * p_delta);
|
Math::absf(player->speed_scale * p_delta);
|
||||||
@@ -962,8 +976,6 @@ static void ECS::_animation_process2(flecs::entity_t entity, flecs::world &ecs,
|
|||||||
if (b.blend_left < 0)
|
if (b.blend_left < 0)
|
||||||
c.blend.erase(E);
|
c.blend.erase(E);
|
||||||
}
|
}
|
||||||
flecs::log::trace("%s(): %d animation %p", __func__, __LINE__,
|
|
||||||
c.current.from->get_animation().ptr());
|
|
||||||
}
|
}
|
||||||
/* transforms are set here */
|
/* transforms are set here */
|
||||||
static void ECS::_animation_update_transforms(flecs::entity_t entity,
|
static void ECS::_animation_update_transforms(flecs::entity_t entity,
|
||||||
@@ -977,15 +989,13 @@ static void ECS::_animation_update_transforms(flecs::entity_t entity,
|
|||||||
t.origin = nc->loc_accum;
|
t.origin = nc->loc_accum;
|
||||||
t.basis.set_quat_scale(nc->rot_accum, nc->scale_accum);
|
t.basis.set_quat_scale(nc->rot_accum, nc->scale_accum);
|
||||||
flecs::entity bone_entity = ecs.entity(nc->bone_idx);
|
flecs::entity bone_entity = ecs.entity(nc->bone_idx);
|
||||||
if (bone_entity) {
|
if (bone_entity.is_alive()) {
|
||||||
ECS::BonePose *bone =
|
ECS::BonePose *bone =
|
||||||
bone_entity.get_mut<ECS::BonePose>();
|
bone_entity.get_mut<ECS::BonePose>();
|
||||||
bone->pose = t;
|
bone->pose = t;
|
||||||
bone_entity.modified<ECS::BonePose>();
|
bone_entity.modified<ECS::BonePose>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flecs::log::trace("update bones %f %f %f\n", t.origin.x,
|
|
||||||
t.origin.y, t.origin.z);
|
|
||||||
}
|
}
|
||||||
player->cache_update_size = 0;
|
player->cache_update_size = 0;
|
||||||
#ifdef VALUE_TRACK_SUPPORT
|
#ifdef VALUE_TRACK_SUPPORT
|
||||||
@@ -1046,14 +1056,7 @@ static void ECS::_set_process(
|
|||||||
static void ECS::_animation_process(flecs::entity_t entity, flecs::world &ecs,
|
static void ECS::_animation_process(flecs::entity_t entity, flecs::world &ecs,
|
||||||
struct ECS::AnimationPlayerData *player, float p_time)
|
struct ECS::AnimationPlayerData *player, float p_time)
|
||||||
{
|
{
|
||||||
flecs::log::trace("animation_pocess: %f", p_time);
|
|
||||||
if (player->playback.current.from) {
|
if (player->playback.current.from) {
|
||||||
flecs::log::trace("animation at start: %p %p",
|
|
||||||
player->playback.current.from,
|
|
||||||
player->playback.current.from->get_animation().ptr());
|
|
||||||
flecs::log::trace("length: %f",
|
|
||||||
player->playback.current.from->get_animation()
|
|
||||||
->get_length());
|
|
||||||
player->end_reached = false;
|
player->end_reached = false;
|
||||||
player->end_notify = false;
|
player->end_notify = false;
|
||||||
_animation_process2(
|
_animation_process2(
|
||||||
@@ -1079,8 +1082,6 @@ static void ECS::_animation_process(flecs::entity_t entity, flecs::world &ecs,
|
|||||||
}
|
}
|
||||||
player->end_reached = false;
|
player->end_reached = false;
|
||||||
}
|
}
|
||||||
flecs::log::trace("animation at end: %p",
|
|
||||||
player->playback.current.from->get_animation().ptr());
|
|
||||||
} else
|
} else
|
||||||
_set_process(player, false);
|
_set_process(player, false);
|
||||||
}
|
}
|
||||||
@@ -1101,52 +1102,61 @@ void ECS::queue(
|
|||||||
StringName ECS::animation_get_next(
|
StringName ECS::animation_get_next(
|
||||||
struct ECS::AnimationPlayerData *player, const StringName &p_animation)
|
struct ECS::AnimationPlayerData *player, const StringName &p_animation)
|
||||||
{
|
{
|
||||||
if (!player->animation_set.has(p_animation)) {
|
int type_id = player->type_id;
|
||||||
|
if (!player->animation_set[type_id].has(p_animation)) {
|
||||||
return StringName();
|
return StringName();
|
||||||
}
|
}
|
||||||
return player->animation_set[p_animation].next;
|
return player->animation_set[type_id][p_animation].next;
|
||||||
}
|
}
|
||||||
|
|
||||||
String ECS::get_current_animation(struct ECS::AnimationPlayerData *player)
|
String ECS::get_current_animation(struct ECS::AnimationPlayerData *player)
|
||||||
{
|
{
|
||||||
return player->playing ? player->playback.assigned : "";
|
return player->playing ? player->playback.assigned : "";
|
||||||
}
|
}
|
||||||
Error ECS::add_animation(struct ECS::AnimationPlayerData *player,
|
Error ECS::add_animation(flecs::entity_t entity, flecs::world &ecs, struct ECS::AnimationPlayerData *player,
|
||||||
const StringName &p_name, const Ref<Animation> &p_animation)
|
const StringName &p_name, const Animation *p_animation)
|
||||||
{
|
{
|
||||||
ERR_FAIL_COND_V(p_animation.is_null(), ERR_INVALID_PARAMETER);
|
ERR_FAIL_COND_V(p_animation == nullptr, ERR_INVALID_PARAMETER);
|
||||||
|
int type_id = player->type_id;
|
||||||
|
|
||||||
if (player->animation_set.has(p_name)) {
|
if (player->animation_set[type_id].has(p_name)) {
|
||||||
AnimationData ad(p_animation);
|
AnimationData ad(p_animation);
|
||||||
ad.name = p_name;
|
ad.name = p_name;
|
||||||
player->animation_set[p_name] = ad;
|
player->animation_set[type_id][p_name] = ad;
|
||||||
// player->animation_set[p_name].animation =
|
// player->animation_set[p_name].animation =
|
||||||
//p_animation;
|
//p_animation;
|
||||||
clear_caches(player);
|
clear_caches(entity, ecs, player);
|
||||||
} else {
|
} else {
|
||||||
AnimationData ad(p_animation);
|
AnimationData ad(p_animation);
|
||||||
ad.name = p_name;
|
ad.name = p_name;
|
||||||
player->animation_set[p_name] = ad;
|
player->animation_set[type_id][p_name] = ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
void ECS::remove_animation(
|
void ECS::remove_animation(flecs::entity_t entity, flecs::world &ecs,
|
||||||
struct ECS::AnimationPlayerData *player, const StringName &p_name)
|
struct ECS::AnimationPlayerData *player, const StringName &p_name)
|
||||||
{
|
{
|
||||||
ERR_FAIL_COND(!player->animation_set.has(p_name));
|
int type_id = player->type_id;
|
||||||
|
ERR_FAIL_COND(!player->animation_set[type_id].has(p_name));
|
||||||
|
|
||||||
stop(player);
|
stop(player);
|
||||||
player->animation_set.erase(p_name);
|
player->animation_set[type_id].erase(p_name);
|
||||||
|
|
||||||
clear_caches(player);
|
clear_caches(entity, ecs, player);
|
||||||
}
|
}
|
||||||
void ECS::clear_caches(struct ECS::AnimationPlayerData *player)
|
void ECS::clear_caches(flecs::entity_t entity, flecs::world &ecs, struct ECS::AnimationPlayerData *player)
|
||||||
{
|
{
|
||||||
|
int type_id = player->type_id, i;
|
||||||
|
flecs::entity e = ecs.entity(entity);
|
||||||
|
ECS::AnimationCache *cache = e.get_mut<ECS::AnimationCache>();
|
||||||
_stop_playing_caches(player);
|
_stop_playing_caches(player);
|
||||||
player->node_cache_map.clear();
|
cache->node_cache_map.clear();
|
||||||
|
for (i = 0; i < NODE_CACHE_UPDATE_MAX; i++)
|
||||||
|
cache->node_cache_data[i].used = false;
|
||||||
|
|
||||||
for (Map<StringName, AnimationData>::Element *E =
|
for (Map<StringName, AnimationData>::Element *E =
|
||||||
player->animation_set.front();
|
player->animation_set[type_id].front();
|
||||||
E; E = E->next())
|
E; E = E->next())
|
||||||
E->get().node_cache_size = 0;
|
E->get().node_cache_size = 0;
|
||||||
player->cache_update_size = 0;
|
player->cache_update_size = 0;
|
||||||
@@ -1154,23 +1164,13 @@ void ECS::clear_caches(struct ECS::AnimationPlayerData *player)
|
|||||||
player->cache_update_prop_size = 0;
|
player->cache_update_prop_size = 0;
|
||||||
player->cache_update_bezier_size = 0;
|
player->cache_update_bezier_size = 0;
|
||||||
#endif
|
#endif
|
||||||
|
e.modified<ECS::AnimationCache>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ECS::advance(flecs::entity_t entity, flecs::world &ecs,
|
void ECS::advance(flecs::entity_t entity, flecs::world &ecs,
|
||||||
ECS::AnimationPlayerData *player, float p_time)
|
ECS::AnimationPlayerData *player, float p_time)
|
||||||
{
|
{
|
||||||
flecs::log::trace(
|
|
||||||
"start current.from %p", player->playback.current.from);
|
|
||||||
flecs::log::trace("advance animation at start: %p %p",
|
|
||||||
player->playback.current.from,
|
|
||||||
player->playback.current.from->get_animation().ptr());
|
|
||||||
ECS::_animation_process(entity, ecs, player, p_time);
|
ECS::_animation_process(entity, ecs, player, p_time);
|
||||||
// player->playback.current.from = nullptr;
|
|
||||||
flecs::log::trace("advance animation at end: %p %p",
|
|
||||||
player->playback.current.from,
|
|
||||||
player->playback.current.from->get_animation().ptr());
|
|
||||||
flecs::log::trace(
|
|
||||||
"end current.from %p", player->playback.current.from);
|
|
||||||
}
|
}
|
||||||
void ECS::stop(struct ECS::AnimationPlayerData *player, bool p_reset)
|
void ECS::stop(struct ECS::AnimationPlayerData *player, bool p_reset)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ enum SpecialProperty {
|
|||||||
SP_NODE2D_SCALE,
|
SP_NODE2D_SCALE,
|
||||||
};
|
};
|
||||||
struct TrackNodeCache {
|
struct TrackNodeCache {
|
||||||
|
bool used;
|
||||||
NodePath path;
|
NodePath path;
|
||||||
uint32_t id;
|
flecs::entity_t id;
|
||||||
Ref<Resource> resource;
|
Ref<Resource> resource;
|
||||||
#if 0
|
#if 0
|
||||||
Node *node;
|
Node *node;
|
||||||
@@ -38,6 +39,7 @@ struct TrackNodeCache {
|
|||||||
float audio_start;
|
float audio_start;
|
||||||
float audio_len;
|
float audio_len;
|
||||||
bool animation_playing;
|
bool animation_playing;
|
||||||
|
#if 0
|
||||||
struct PropertyAnim {
|
struct PropertyAnim {
|
||||||
TrackNodeCache *owner;
|
TrackNodeCache *owner;
|
||||||
SpecialProperty special;
|
SpecialProperty special;
|
||||||
@@ -55,6 +57,8 @@ struct TrackNodeCache {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Map<StringName, PropertyAnim> property_anim;
|
Map<StringName, PropertyAnim> property_anim;
|
||||||
|
#endif
|
||||||
|
#if 0
|
||||||
struct BezierAnim {
|
struct BezierAnim {
|
||||||
Vector<StringName> bezier_property;
|
Vector<StringName> bezier_property;
|
||||||
TrackNodeCache *owner;
|
TrackNodeCache *owner;
|
||||||
@@ -71,8 +75,9 @@ struct TrackNodeCache {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Map<StringName, BezierAnim> bezier_anim;
|
Map<StringName, BezierAnim> bezier_anim;
|
||||||
|
#endif
|
||||||
TrackNodeCache() :
|
TrackNodeCache() :
|
||||||
id(0),
|
used(false), id(-1),
|
||||||
#if 0
|
#if 0
|
||||||
node(nullptr),
|
node(nullptr),
|
||||||
spatial(nullptr),
|
spatial(nullptr),
|
||||||
@@ -188,13 +193,14 @@ struct AnimationData {
|
|||||||
StringName next;
|
StringName next;
|
||||||
int node_cache_size;
|
int node_cache_size;
|
||||||
struct TrackNodeCache *node_cache[128];
|
struct TrackNodeCache *node_cache[128];
|
||||||
AnimationData(Ref<Animation> anim) :
|
AnimationData(const Animation *anim) :
|
||||||
animation(anim), node_cache_size(0)
|
animation(anim), node_cache_size(0)
|
||||||
{
|
{
|
||||||
assert(animation.is_valid());
|
assert(animation);
|
||||||
|
animation = anim;
|
||||||
}
|
}
|
||||||
AnimationData() :
|
AnimationData() :
|
||||||
animation(Ref<Animation>(nullptr)), node_cache_size(0)
|
animation(nullptr), node_cache_size(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
AnimationData(const AnimationData &d)
|
AnimationData(const AnimationData &d)
|
||||||
@@ -202,19 +208,19 @@ struct AnimationData {
|
|||||||
int i;
|
int i;
|
||||||
name = d.name;
|
name = d.name;
|
||||||
next = d.next;
|
next = d.next;
|
||||||
|
assert(d.animation);
|
||||||
node_cache_size = d.node_cache_size;
|
node_cache_size = d.node_cache_size;
|
||||||
for (i = 0; i < 128; i++)
|
for (i = 0; i < 128; i++)
|
||||||
node_cache[i] = d.node_cache[i];
|
node_cache[i] = d.node_cache[i];
|
||||||
animation = d.animation;
|
animation = d.animation;
|
||||||
assert(animation.is_valid());
|
assert(animation);
|
||||||
flecs::log::trace("%s copy-constructed normally %p: %p",
|
|
||||||
__func__, this, animation.ptr());
|
|
||||||
}
|
}
|
||||||
AnimationData(AnimationData &&d)
|
AnimationData(AnimationData &&d)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
name = d.name;
|
name = d.name;
|
||||||
next = d.next;
|
next = d.next;
|
||||||
|
assert(d.animation);
|
||||||
node_cache_size = d.node_cache_size;
|
node_cache_size = d.node_cache_size;
|
||||||
for (i = 0; i < 128; i++)
|
for (i = 0; i < 128; i++)
|
||||||
node_cache[i] = d.node_cache[i];
|
node_cache[i] = d.node_cache[i];
|
||||||
@@ -223,97 +229,32 @@ struct AnimationData {
|
|||||||
d.node_cache_size = 0;
|
d.node_cache_size = 0;
|
||||||
d.name = "";
|
d.name = "";
|
||||||
d.next = "";
|
d.next = "";
|
||||||
d.animation = Ref<Animation>(nullptr);
|
d.animation = nullptr;
|
||||||
flecs::log::trace("%s move-constructed normally %p: %p",
|
|
||||||
__func__, this, animation.ptr());
|
|
||||||
}
|
}
|
||||||
AnimationData &operator=(const AnimationData &d)
|
AnimationData &operator=(const AnimationData &d)
|
||||||
{
|
{
|
||||||
memnew_placement(this, AnimationData(d));
|
memnew_placement(this, AnimationData(d));
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
const Ref<Animation> &get_animation() const
|
const Animation *get_animation() const
|
||||||
{
|
{
|
||||||
assert(animation.is_valid());
|
|
||||||
flecs::log::trace("animation ptr %p", animation.ptr());
|
|
||||||
return animation;
|
return animation;
|
||||||
}
|
}
|
||||||
|
|
||||||
~AnimationData() { animation = Ref<Animation>(nullptr); }
|
~AnimationData() { animation = nullptr; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Ref<Animation> animation;
|
const Animation *animation;
|
||||||
};
|
};
|
||||||
struct PlaybackData {
|
struct PlaybackData {
|
||||||
AnimationData *from;
|
AnimationData *from;
|
||||||
float pos;
|
float pos;
|
||||||
float speed_scale;
|
float speed_scale;
|
||||||
|
|
||||||
PlaybackData()
|
|
||||||
{
|
|
||||||
pos = 0;
|
|
||||||
speed_scale = 1.0;
|
|
||||||
from = nullptr;
|
|
||||||
}
|
|
||||||
PlaybackData(const PlaybackData &d)
|
|
||||||
{
|
|
||||||
pos = d.pos;
|
|
||||||
speed_scale = d.speed_scale;
|
|
||||||
from = d.from;
|
|
||||||
flecs::log::trace(
|
|
||||||
"%s copy-constructed normally %p", __func__, this);
|
|
||||||
}
|
|
||||||
PlaybackData(PlaybackData &&d)
|
|
||||||
{
|
|
||||||
pos = d.pos;
|
|
||||||
speed_scale = d.speed_scale;
|
|
||||||
from = d.from;
|
|
||||||
d.pos = 0;
|
|
||||||
d.speed_scale = 1.0f;
|
|
||||||
d.from = nullptr;
|
|
||||||
flecs::log::trace(
|
|
||||||
"%s move-constructed normally %p", __func__, this);
|
|
||||||
}
|
|
||||||
PlaybackData &operator=(const PlaybackData &d)
|
|
||||||
{
|
|
||||||
from = d.from;
|
|
||||||
pos = d.pos;
|
|
||||||
speed_scale = d.speed_scale;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
struct Blend {
|
struct Blend {
|
||||||
PlaybackData data;
|
PlaybackData data;
|
||||||
float blend_time;
|
float blend_time;
|
||||||
float blend_left;
|
float blend_left;
|
||||||
Blend()
|
|
||||||
{
|
|
||||||
blend_left = 0.0f;
|
|
||||||
blend_time = 0.0f;
|
|
||||||
}
|
|
||||||
Blend(const Blend &d)
|
|
||||||
{
|
|
||||||
data = PlaybackData(d.data);
|
|
||||||
blend_time = d.blend_time;
|
|
||||||
blend_left = d.blend_left;
|
|
||||||
flecs::log::trace(
|
|
||||||
"%s copy-constructed normally %p", __func__, this);
|
|
||||||
}
|
|
||||||
Blend(Blend &&d)
|
|
||||||
{
|
|
||||||
data = std::move(d.data);
|
|
||||||
blend_time = d.blend_time;
|
|
||||||
blend_left = d.blend_left;
|
|
||||||
d.blend_left = 0.0f;
|
|
||||||
d.blend_time = 0.0f;
|
|
||||||
flecs::log::trace(
|
|
||||||
"%s move-constructed normally %p", __func__, this);
|
|
||||||
}
|
|
||||||
Blend &operator=(const Blend &d)
|
|
||||||
{
|
|
||||||
memnew_placement(this, Blend(d));
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
struct Playback {
|
struct Playback {
|
||||||
List<Blend> blend;
|
List<Blend> blend;
|
||||||
@@ -326,39 +267,6 @@ struct Playback {
|
|||||||
blend.clear();
|
blend.clear();
|
||||||
seeked = false;
|
seeked = false;
|
||||||
started = false;
|
started = false;
|
||||||
flecs::log::trace(
|
|
||||||
"%s constructed normally %p", __func__, this);
|
|
||||||
}
|
|
||||||
Playback(const Playback &d)
|
|
||||||
{
|
|
||||||
blend = List<Blend>();
|
|
||||||
const List<Blend>::Element *e = d.blend.front();
|
|
||||||
/* copy construct blends */
|
|
||||||
while (e) {
|
|
||||||
blend.push_back(Blend(e->get()));
|
|
||||||
e = e->next();
|
|
||||||
}
|
|
||||||
current = PlaybackData(d.current);
|
|
||||||
StringName assigned = d.assigned;
|
|
||||||
seeked = d.seeked;
|
|
||||||
started = d.started;
|
|
||||||
flecs::log::trace(
|
|
||||||
"%s copy-constructed normally %p", __func__, this);
|
|
||||||
}
|
|
||||||
Playback(Playback &&d)
|
|
||||||
{
|
|
||||||
blend = std::move(d.blend);
|
|
||||||
current = std::move(d.current);
|
|
||||||
StringName assigned = d.assigned;
|
|
||||||
seeked = d.seeked;
|
|
||||||
started = d.started;
|
|
||||||
flecs::log::trace(
|
|
||||||
"%s move-constructed normally %p", __func__, this);
|
|
||||||
}
|
|
||||||
Playback &operator=(const Playback &d)
|
|
||||||
{
|
|
||||||
memnew_placement(this, Playback(d));
|
|
||||||
return *this;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
struct BlendKey {
|
struct BlendKey {
|
||||||
@@ -385,8 +293,15 @@ struct TrackNodeCacheKey {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
enum { NODE_CACHE_UPDATE_MAX = 1024, BLEND_FROM_MAX = 3 };
|
enum { NODE_CACHE_UPDATE_MAX = 1024, BLEND_FROM_MAX = 3 };
|
||||||
|
|
||||||
|
struct AnimationCache {
|
||||||
|
Map<TrackNodeCacheKey, int> node_cache_map;
|
||||||
|
TrackNodeCache node_cache_data[NODE_CACHE_UPDATE_MAX];
|
||||||
|
};
|
||||||
|
|
||||||
struct AnimationPlayerData {
|
struct AnimationPlayerData {
|
||||||
Map<TrackNodeCacheKey, TrackNodeCache> node_cache_map;
|
int type_id;
|
||||||
|
|
||||||
TrackNodeCache *cache_update[NODE_CACHE_UPDATE_MAX];
|
TrackNodeCache *cache_update[NODE_CACHE_UPDATE_MAX];
|
||||||
int cache_update_size;
|
int cache_update_size;
|
||||||
|
|
||||||
@@ -400,7 +315,7 @@ struct AnimationPlayerData {
|
|||||||
uint64_t accum_pass;
|
uint64_t accum_pass;
|
||||||
float speed_scale;
|
float speed_scale;
|
||||||
float default_blend_time;
|
float default_blend_time;
|
||||||
Map<StringName, AnimationData> animation_set;
|
static Map<StringName, AnimationData> animation_set[2];
|
||||||
|
|
||||||
Map<BlendKey, float> blend_times;
|
Map<BlendKey, float> blend_times;
|
||||||
Playback playback;
|
Playback playback;
|
||||||
@@ -416,18 +331,18 @@ struct AnimationPlayerData {
|
|||||||
NodePath root;
|
NodePath root;
|
||||||
#endif
|
#endif
|
||||||
bool playing;
|
bool playing;
|
||||||
AnimationPlayerData()
|
AnimationPlayerData(int id)
|
||||||
{
|
{
|
||||||
|
type_id = id;
|
||||||
processing = false;
|
processing = false;
|
||||||
playing = false;
|
playing = false;
|
||||||
active = false;
|
active = false;
|
||||||
node_cache_map.clear();
|
|
||||||
cache_update_size = 0;
|
cache_update_size = 0;
|
||||||
playing_caches.clear();
|
playing_caches.clear();
|
||||||
accum_pass = 0;
|
accum_pass = 0;
|
||||||
speed_scale = 1.0f;
|
speed_scale = 1.0f;
|
||||||
default_blend_time = 0;
|
default_blend_time = 0;
|
||||||
animation_set.clear();
|
animation_set[id].clear();
|
||||||
blend_times.clear();
|
blend_times.clear();
|
||||||
queued.clear();
|
queued.clear();
|
||||||
end_reached = false;
|
end_reached = false;
|
||||||
@@ -438,13 +353,11 @@ struct AnimationPlayerData {
|
|||||||
AnimationProcessMode::ANIMATION_PROCESS_MANUAL;
|
AnimationProcessMode::ANIMATION_PROCESS_MANUAL;
|
||||||
processing = false;
|
processing = false;
|
||||||
active = false;
|
active = false;
|
||||||
flecs::log::trace(
|
|
||||||
"%s constructed normally %p", __func__, this);
|
|
||||||
}
|
}
|
||||||
AnimationPlayerData(const AnimationPlayerData &d)
|
AnimationPlayerData(const AnimationPlayerData &d)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
node_cache_map = d.node_cache_map;
|
type_id = d.type_id;
|
||||||
for (i = 0; i < NODE_CACHE_UPDATE_MAX; i++)
|
for (i = 0; i < NODE_CACHE_UPDATE_MAX; i++)
|
||||||
cache_update[i] = d.cache_update[i];
|
cache_update[i] = d.cache_update[i];
|
||||||
cache_update_size = d.cache_update_size;
|
cache_update_size = d.cache_update_size;
|
||||||
@@ -452,7 +365,6 @@ struct AnimationPlayerData {
|
|||||||
accum_pass = d.accum_pass;
|
accum_pass = d.accum_pass;
|
||||||
speed_scale = d.speed_scale;
|
speed_scale = d.speed_scale;
|
||||||
default_blend_time = d.default_blend_time;
|
default_blend_time = d.default_blend_time;
|
||||||
animation_set = d.animation_set;
|
|
||||||
blend_times = d.blend_times;
|
blend_times = d.blend_times;
|
||||||
playback = d.playback;
|
playback = d.playback;
|
||||||
queued = d.queued;
|
queued = d.queued;
|
||||||
@@ -464,8 +376,6 @@ struct AnimationPlayerData {
|
|||||||
processing = d.processing;
|
processing = d.processing;
|
||||||
active = d.active;
|
active = d.active;
|
||||||
playing = d.playing;
|
playing = d.playing;
|
||||||
flecs::log::trace(
|
|
||||||
"%s copy-constructed normally %p", __func__, this);
|
|
||||||
}
|
}
|
||||||
AnimationPlayerData(AnimationPlayerData &&d)
|
AnimationPlayerData(AnimationPlayerData &&d)
|
||||||
{
|
{
|
||||||
@@ -496,8 +406,6 @@ struct AnimationPlayerData {
|
|||||||
d.playing = playing;
|
d.playing = playing;
|
||||||
d.cache_update_size = 0;
|
d.cache_update_size = 0;
|
||||||
d.active = false;
|
d.active = false;
|
||||||
flecs::log::trace(
|
|
||||||
"%s move-constructed normally %p", __func__, this);
|
|
||||||
}
|
}
|
||||||
AnimationPlayerData &operator=(const AnimationPlayerData &d)
|
AnimationPlayerData &operator=(const AnimationPlayerData &d)
|
||||||
{
|
{
|
||||||
@@ -525,15 +433,12 @@ struct AnimationPlayerData {
|
|||||||
active = d.active;
|
active = d.active;
|
||||||
playing = d.playing;
|
playing = d.playing;
|
||||||
#endif
|
#endif
|
||||||
flecs::log::trace(
|
|
||||||
"%s copy-assigned normally %p", __func__, this);
|
|
||||||
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
AnimationPlayerData &operator=(AnimationPlayerData &&d)
|
AnimationPlayerData &operator=(AnimationPlayerData &&d)
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
node_cache_map = d.node_cache_map;
|
type_id = d.type_id;
|
||||||
for (i = 0; i < NODE_CACHE_UPDATE_MAX; i++)
|
for (i = 0; i < NODE_CACHE_UPDATE_MAX; i++)
|
||||||
cache_update[i] = d.cache_update[i];
|
cache_update[i] = d.cache_update[i];
|
||||||
cache_update_size = d.cache_update_size;
|
cache_update_size = d.cache_update_size;
|
||||||
@@ -541,7 +446,6 @@ struct AnimationPlayerData {
|
|||||||
accum_pass = d.accum_pass;
|
accum_pass = d.accum_pass;
|
||||||
speed_scale = d.speed_scale;
|
speed_scale = d.speed_scale;
|
||||||
default_blend_time = d.default_blend_time;
|
default_blend_time = d.default_blend_time;
|
||||||
animation_set = d.animation_set;
|
|
||||||
blend_times = d.blend_times;
|
blend_times = d.blend_times;
|
||||||
playback = d.playback;
|
playback = d.playback;
|
||||||
queued = d.queued;
|
queued = d.queued;
|
||||||
@@ -556,9 +460,6 @@ struct AnimationPlayerData {
|
|||||||
d.playing = playing;
|
d.playing = playing;
|
||||||
d.cache_update_size = 0;
|
d.cache_update_size = 0;
|
||||||
d.active = false;
|
d.active = false;
|
||||||
flecs::log::trace(
|
|
||||||
"%s move-assigned normally %p", __func__, this);
|
|
||||||
|
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
~AnimationPlayerData() { playing = false; }
|
~AnimationPlayerData() { playing = false; }
|
||||||
@@ -566,9 +467,9 @@ struct AnimationPlayerData {
|
|||||||
/* public functions */
|
/* public functions */
|
||||||
StringName find_animation(
|
StringName find_animation(
|
||||||
struct AnimationPlayerData *player, const Ref<Animation> &p_animation);
|
struct AnimationPlayerData *player, const Ref<Animation> &p_animation);
|
||||||
Error add_animation(struct AnimationPlayerData *player,
|
Error add_animation(flecs::entity_t entity, flecs::world &ecs, struct AnimationPlayerData *player,
|
||||||
const StringName &p_name, const Ref<Animation> &p_animation);
|
const StringName &p_name, const Animation *p_animation);
|
||||||
void remove_animation(
|
void remove_animation(flecs::entity_t entity, flecs::world &ecs,
|
||||||
struct AnimationPlayerData *player, const StringName &p_name);
|
struct AnimationPlayerData *player, const StringName &p_name);
|
||||||
void rename_animation(struct AnimationPlayerData *player,
|
void rename_animation(struct AnimationPlayerData *player,
|
||||||
const StringName &p_name, const StringName &p_new_name);
|
const StringName &p_name, const StringName &p_new_name);
|
||||||
@@ -636,7 +537,7 @@ void advance(flecs::entity_t entity, flecs::world &ecs,
|
|||||||
AnimationPlayerData *player, float p_time);
|
AnimationPlayerData *player, float p_time);
|
||||||
void set_root(struct AnimationPlayerData *player, const NodePath &p_root);
|
void set_root(struct AnimationPlayerData *player, const NodePath &p_root);
|
||||||
NodePath get_root(struct AnimationPlayerData *player);
|
NodePath get_root(struct AnimationPlayerData *player);
|
||||||
void clear_caches(struct AnimationPlayerData *player);
|
void clear_caches(flecs::entity_t entity, flecs::world &ecs, struct AnimationPlayerData *player);
|
||||||
void get_argument_options(struct AnimationPlayerData *player,
|
void get_argument_options(struct AnimationPlayerData *player,
|
||||||
const StringName &p_function, int p_idx, List<String> *r_options);
|
const StringName &p_function, int p_idx, List<String> *r_options);
|
||||||
} //namespace ECS
|
} //namespace ECS
|
||||||
|
|||||||
@@ -88,15 +88,17 @@ int Character::create_character(int id)
|
|||||||
root.emplace<Transform>();
|
root.emplace<Transform>();
|
||||||
root.emplace<ECS::RigidBody>(body, ECS::RigidBody::TYPE_KINEMATIC);
|
root.emplace<ECS::RigidBody>(body, ECS::RigidBody::TYPE_KINEMATIC);
|
||||||
root.emplace<ECS::Skeleton>();
|
root.emplace<ECS::Skeleton>();
|
||||||
root.emplace<ECS::AnimationPlayerData>();
|
root.emplace<ECS::AnimationCache>();
|
||||||
|
root.emplace<ECS::AnimationPlayerData>(id);
|
||||||
List<StringName> anim_list;
|
List<StringName> anim_list;
|
||||||
scene_data[id].animations.get_key_list(&anim_list);
|
scene_data[id].animations.get_key_list(&anim_list);
|
||||||
List<StringName>::Element *e = anim_list.front();
|
List<StringName>::Element *e = anim_list.front();
|
||||||
ECS::AnimationPlayerData *player =
|
ECS::AnimationPlayerData *player =
|
||||||
root.get_mut<ECS::AnimationPlayerData>();
|
root.get_mut<ECS::AnimationPlayerData>();
|
||||||
while (e) {
|
while (e) {
|
||||||
ECS::add_animation(
|
assert(scene_data[id].animations[e->get()].is_valid());
|
||||||
player, e->get(), scene_data[id].animations[e->get()]);
|
ECS::add_animation(root, ecs,
|
||||||
|
player, e->get(), scene_data[id].animations[e->get()].ptr());
|
||||||
flecs::log::trace("added animation %s",
|
flecs::log::trace("added animation %s",
|
||||||
String(e->get()).ascii().get_data());
|
String(e->get()).ascii().get_data());
|
||||||
e = e->next();
|
e = e->next();
|
||||||
@@ -360,27 +362,17 @@ void Character::animation_system_init()
|
|||||||
ecs.system<ECS::AnimationPlayerData>("UpdateAnimation")
|
ecs.system<ECS::AnimationPlayerData>("UpdateAnimation")
|
||||||
.kind(flecs::OnUpdate)
|
.kind(flecs::OnUpdate)
|
||||||
.each([](flecs::entity e, ECS::AnimationPlayerData &player) {
|
.each([](flecs::entity e, ECS::AnimationPlayerData &player) {
|
||||||
flecs::log::trace("== start of animation frame %p",
|
|
||||||
player.playback.current.from);
|
|
||||||
if (player.playback.current.from)
|
|
||||||
flecs::log::trace("start animation %p",
|
|
||||||
player.playback.current.from
|
|
||||||
->get_animation()
|
|
||||||
.ptr());
|
|
||||||
if (!player.playing)
|
|
||||||
flecs::log::trace("not playing any animation");
|
|
||||||
if (!player.playing)
|
if (!player.playing)
|
||||||
ECS::play(&player, "male-mx-walk-loop");
|
ECS::play(&player, "male-mx-walk-loop");
|
||||||
|
if (player.playing && player.end_reached) {
|
||||||
|
ECS::stop(&player, true);
|
||||||
|
if (!player.playing)
|
||||||
|
flecs::log::trace("stopped: not playing any animation");
|
||||||
|
}
|
||||||
flecs::world w = e.world();
|
flecs::world w = e.world();
|
||||||
ECS::advance(e.id(), w, &player,
|
ECS::advance(e.id(), w, &player,
|
||||||
SceneTree::get_singleton()
|
SceneTree::get_singleton()
|
||||||
->get_physics_process_time());
|
->get_physics_process_time());
|
||||||
if (player.playback.current.from)
|
|
||||||
flecs::log::trace("end animation %p",
|
|
||||||
player.playback.current.from
|
|
||||||
->get_animation()
|
|
||||||
.ptr());
|
|
||||||
flecs::log::trace("== end of animation frame");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
void Character::initialize()
|
void Character::initialize()
|
||||||
|
|||||||
Reference in New Issue
Block a user