AnimationTreePlayer filters improved
Now the AnimationTreePlayer filters for Blend2 and OneShot nodes behave as expected, that is the main animation is not affected by the secondary animation if the track is filterd out for arbitarily complex trees.
This commit is contained in:
parent
e69e50040b
commit
cfa11777c0
|
@ -445,8 +445,44 @@ void AnimationTreePlayer::_notification(int p_what) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AnimationTreePlayer::_compute_weights(float *p_fallback_weight, HashMap<NodePath,float> *p_weights, float p_coeff, const HashMap<NodePath,bool> *p_filter, float p_filtered_coeff) {
|
||||||
|
|
||||||
float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode **r_prev_anim,float p_weight, float p_time, bool p_seek,const HashMap<NodePath,bool> *p_filter, float p_reverse_weight) {
|
if (p_filter != NULL) {
|
||||||
|
|
||||||
|
List<NodePath> key_list;
|
||||||
|
p_filter->get_key_list(&key_list);
|
||||||
|
|
||||||
|
for (List<NodePath>::Element *E = key_list.front();E; E=E->next()) {
|
||||||
|
|
||||||
|
if ((*p_filter)[E->get()]) {
|
||||||
|
|
||||||
|
if (p_weights->has(E->get())) {
|
||||||
|
(*p_weights)[E->get()] *= p_filtered_coeff;
|
||||||
|
} else {
|
||||||
|
p_weights->set(E->get(), *p_fallback_weight * p_filtered_coeff);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (p_weights->has(E->get())) {
|
||||||
|
(*p_weights)[E->get()] *= p_coeff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<NodePath> key_list;
|
||||||
|
p_weights->get_key_list(&key_list);
|
||||||
|
|
||||||
|
for (List<NodePath>::Element *E = key_list.front();E;E=E->next()) {
|
||||||
|
if (p_filter == NULL || !p_filter->has(E->get())) {
|
||||||
|
(*p_weights)[E->get()] *= p_coeff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*p_fallback_weight *= p_coeff;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode **r_prev_anim, float p_time, bool p_seek, float p_fallback_weight, HashMap<NodePath,float>* p_weights) {
|
||||||
|
|
||||||
ERR_FAIL_COND_V(!node_map.has(p_node), 0);
|
ERR_FAIL_COND_V(!node_map.has(p_node), 0);
|
||||||
NodeBase *nb=node_map[p_node];
|
NodeBase *nb=node_map[p_node];
|
||||||
|
@ -458,7 +494,16 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
case NODE_OUTPUT: {
|
case NODE_OUTPUT: {
|
||||||
|
|
||||||
NodeOut *on = static_cast<NodeOut*>(nb);
|
NodeOut *on = static_cast<NodeOut*>(nb);
|
||||||
return _process_node(on->inputs[0].node,r_prev_anim,p_weight,p_time,p_seek);
|
|
||||||
|
for(TrackMap::Element *E=track_map.front();E;E=E->next()) {
|
||||||
|
E->get().total_weight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap<NodePath, float> weights;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return _process_node(on->inputs[0].node,r_prev_anim,p_time,p_seek, p_fallback_weight, &weights);
|
||||||
|
|
||||||
} break;
|
} break;
|
||||||
case NODE_ANIMATION: {
|
case NODE_ANIMATION: {
|
||||||
|
@ -494,15 +539,21 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
}
|
}
|
||||||
|
|
||||||
an->skip=true;
|
an->skip=true;
|
||||||
|
|
||||||
for (List<AnimationNode::TrackRef>::Element *E=an->tref.front();E;E=E->next()) {
|
for (List<AnimationNode::TrackRef>::Element *E=an->tref.front();E;E=E->next()) {
|
||||||
NodePath track_path = an->animation->track_get_path(E->get().local_track);
|
NodePath track_path = an->animation->track_get_path(E->get().local_track);
|
||||||
if (p_filter && p_filter->has(track_path)) {
|
if (an->filter.has(track_path) && an->filter[track_path]) {
|
||||||
E->get().weight = MAX(0, p_reverse_weight);
|
|
||||||
} else if(an->filter.has(track_path)) {
|
|
||||||
E->get().weight = 0;
|
E->get().weight = 0;
|
||||||
E->get().track->skip = true;
|
E->get().track->total_weight += p_fallback_weight;
|
||||||
} else {
|
} else {
|
||||||
E->get().weight=p_weight;
|
if (p_weights->has(track_path)) {
|
||||||
|
float weight = (*p_weights)[track_path];
|
||||||
|
E->get().weight = weight;
|
||||||
|
E->get().track->total_weight += weight;
|
||||||
|
} else {
|
||||||
|
E->get().weight = p_fallback_weight;
|
||||||
|
E->get().track->total_weight += p_fallback_weight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (E->get().weight>CMP_EPSILON)
|
if (E->get().weight>CMP_EPSILON)
|
||||||
an->skip=false;
|
an->skip=false;
|
||||||
|
@ -531,7 +582,7 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
|
|
||||||
if (!osn->active) {
|
if (!osn->active) {
|
||||||
//make it as if this node doesn't exist, pass input 0 by.
|
//make it as if this node doesn't exist, pass input 0 by.
|
||||||
return _process_node(osn->inputs[0].node,r_prev_anim,p_weight,p_time,p_seek,p_filter,p_reverse_weight);
|
return _process_node(osn->inputs[0].node,r_prev_anim,p_time,p_seek, p_fallback_weight, p_weights);
|
||||||
}
|
}
|
||||||
|
|
||||||
float os_seek = p_seek;
|
float os_seek = p_seek;
|
||||||
|
@ -563,16 +614,14 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
|
|
||||||
float main_rem;
|
float main_rem;
|
||||||
float os_rem;
|
float os_rem;
|
||||||
float os_reverse_weight = p_reverse_weight;
|
|
||||||
|
|
||||||
if (!osn->filter.empty()) {
|
HashMap<NodePath, float> os_weights(*p_weights);
|
||||||
p_filter = &osn->filter;
|
float os_fallback_weight = p_fallback_weight;
|
||||||
p_reverse_weight = p_weight;
|
_compute_weights(&p_fallback_weight, p_weights, osn->mix?1.0 : 1.0 - blend, &osn->filter, 1.0);
|
||||||
os_reverse_weight = -1;
|
_compute_weights(&os_fallback_weight, &os_weights, blend, &osn->filter, 0.0);
|
||||||
}
|
|
||||||
|
|
||||||
main_rem = _process_node(osn->inputs[0].node,r_prev_anim,(osn->mix?p_weight:p_weight*(1.0-blend)),p_time,p_seek,p_filter,p_reverse_weight);
|
main_rem = _process_node(osn->inputs[0].node,r_prev_anim,p_time,p_seek, p_fallback_weight, p_weights);
|
||||||
os_rem = _process_node(osn->inputs[1].node,r_prev_anim,p_weight*blend,p_time,os_seek,p_filter,os_reverse_weight);
|
os_rem = _process_node(osn->inputs[1].node,r_prev_anim,p_time,os_seek, os_fallback_weight, &os_weights);
|
||||||
|
|
||||||
if (osn->start) {
|
if (osn->start) {
|
||||||
osn->remaining=os_rem;
|
osn->remaining=os_rem;
|
||||||
|
@ -591,9 +640,11 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
case NODE_MIX: {
|
case NODE_MIX: {
|
||||||
MixNode *mn = static_cast<MixNode*>(nb);
|
MixNode *mn = static_cast<MixNode*>(nb);
|
||||||
|
|
||||||
|
HashMap<NodePath, float> mn_weights(*p_weights);
|
||||||
float rem = _process_node(mn->inputs[0].node,r_prev_anim,p_weight,p_time,p_seek,p_filter,p_reverse_weight);
|
float mn_fallback_weight = p_fallback_weight;
|
||||||
_process_node(mn->inputs[1].node,r_prev_anim,p_weight*mn->amount,p_time,p_seek,p_filter,p_reverse_weight);
|
_compute_weights(&mn_fallback_weight, &mn_weights, mn->amount);
|
||||||
|
float rem = _process_node(mn->inputs[0].node,r_prev_anim, p_time,p_seek,p_fallback_weight,p_weights);
|
||||||
|
_process_node(mn->inputs[1].node,r_prev_anim,p_time,p_seek,mn_fallback_weight,&mn_weights);
|
||||||
return rem;
|
return rem;
|
||||||
|
|
||||||
} break;
|
} break;
|
||||||
|
@ -601,16 +652,12 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
|
|
||||||
Blend2Node *bn = static_cast<Blend2Node*>(nb);
|
Blend2Node *bn = static_cast<Blend2Node*>(nb);
|
||||||
|
|
||||||
float rem;
|
HashMap<NodePath, float> bn_weights(*p_weights);
|
||||||
if (!bn->filter.empty()) {
|
float bn_fallback_weight = p_fallback_weight;
|
||||||
|
_compute_weights(&p_fallback_weight,p_weights, 1.0 - bn->value, &bn->filter, 1.0);
|
||||||
rem = _process_node(bn->inputs[0].node,r_prev_anim,p_weight*(1.0-bn->value),p_time,p_seek,&bn->filter,p_weight);
|
_compute_weights(&bn_fallback_weight,&bn_weights, bn->value, &bn->filter, 0.0);
|
||||||
_process_node(bn->inputs[1].node,r_prev_anim,p_weight*bn->value,p_time,p_seek,&bn->filter,-1);
|
float rem = _process_node(bn->inputs[0].node,r_prev_anim,p_time,p_seek,p_fallback_weight,p_weights);
|
||||||
|
_process_node(bn->inputs[1].node,r_prev_anim,p_time,p_seek,bn_fallback_weight,&bn_weights);
|
||||||
} else {
|
|
||||||
rem = _process_node(bn->inputs[0].node,r_prev_anim,p_weight*(1.0-bn->value),p_time,p_seek,p_filter,p_reverse_weight*(1.0-bn->value));
|
|
||||||
_process_node(bn->inputs[1].node,r_prev_anim,p_weight*bn->value,p_time,p_seek,p_filter,p_reverse_weight*bn->value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rem;
|
return rem;
|
||||||
} break;
|
} break;
|
||||||
|
@ -629,19 +676,39 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
upper_blend = bn->value;
|
upper_blend = bn->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
rem = _process_node(bn->inputs[1].node,r_prev_anim,p_weight*blend,p_time,p_seek,p_filter,p_reverse_weight*blend);
|
HashMap<NodePath, float> upper_weights(*p_weights);
|
||||||
_process_node(bn->inputs[2].node,r_prev_anim,p_weight*upper_blend,p_time,p_seek,p_filter,p_reverse_weight*upper_blend);
|
float upper_fallback_weight = p_fallback_weight;
|
||||||
_process_node(bn->inputs[0].node,r_prev_anim,p_weight*lower_blend,p_time,p_seek,p_filter,p_reverse_weight*lower_blend);
|
HashMap<NodePath, float> lower_weights(*p_weights);
|
||||||
|
float lower_fallback_weight = p_fallback_weight;
|
||||||
|
_compute_weights(&upper_fallback_weight,&upper_weights, upper_blend);
|
||||||
|
_compute_weights(&p_fallback_weight,p_weights, blend);
|
||||||
|
_compute_weights(&lower_fallback_weight,&lower_weights, lower_blend);
|
||||||
|
|
||||||
|
rem = _process_node(bn->inputs[1].node,r_prev_anim,p_time,p_seek,p_fallback_weight,p_weights);
|
||||||
|
_process_node(bn->inputs[0].node,r_prev_anim,p_time,p_seek,lower_fallback_weight,&lower_weights);
|
||||||
|
_process_node(bn->inputs[2].node,r_prev_anim,p_time,p_seek,upper_fallback_weight,&upper_weights);
|
||||||
|
|
||||||
return rem;
|
return rem;
|
||||||
} break;
|
} break;
|
||||||
case NODE_BLEND4: {
|
case NODE_BLEND4: {
|
||||||
Blend4Node *bn = static_cast<Blend4Node*>(nb);
|
Blend4Node *bn = static_cast<Blend4Node*>(nb);
|
||||||
|
|
||||||
float rem = _process_node(bn->inputs[0].node,r_prev_anim,p_weight*(1.0-bn->value.x),p_time,p_seek,p_filter,p_reverse_weight*(1.0-bn->value.x));
|
HashMap<NodePath, float> weights1(*p_weights);
|
||||||
_process_node(bn->inputs[1].node,r_prev_anim,p_weight*bn->value.x,p_time,p_seek,p_filter,p_reverse_weight*bn->value.x);
|
float fallback_weight1 = p_fallback_weight;
|
||||||
float rem2 = _process_node(bn->inputs[2].node,r_prev_anim,p_weight*(1.0-bn->value.y),p_time,p_seek,p_filter,p_reverse_weight*(1.0-bn->value.y));
|
HashMap<NodePath, float> weights2(*p_weights);
|
||||||
_process_node(bn->inputs[3].node,r_prev_anim,p_weight*bn->value.y,p_time,p_seek,p_filter,p_reverse_weight*bn->value.y);
|
float fallback_weight2 = p_fallback_weight;
|
||||||
|
HashMap<NodePath, float> weights3(*p_weights);
|
||||||
|
float fallback_weight3 = p_fallback_weight;
|
||||||
|
|
||||||
|
_compute_weights(&p_fallback_weight,p_weights, 1.0-bn->value.x);
|
||||||
|
_compute_weights(&fallback_weight1,&weights1, bn->value.x);
|
||||||
|
_compute_weights(&fallback_weight2,&weights2, 1.0-bn->value.y);
|
||||||
|
_compute_weights(&fallback_weight3,&weights3, bn->value.y);
|
||||||
|
|
||||||
|
float rem = _process_node(bn->inputs[0].node,r_prev_anim,p_time,p_seek,p_fallback_weight,p_weights);
|
||||||
|
_process_node(bn->inputs[1].node,r_prev_anim,p_time,p_seek,fallback_weight1,&weights1);
|
||||||
|
float rem2 = _process_node(bn->inputs[2].node,r_prev_anim,p_time,p_seek,fallback_weight2,&weights2);
|
||||||
|
_process_node(bn->inputs[3].node,r_prev_anim,p_time,p_seek,fallback_weight3,&weights3);
|
||||||
|
|
||||||
return MAX(rem,rem2);
|
return MAX(rem,rem2);
|
||||||
|
|
||||||
|
@ -650,9 +717,9 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
TimeScaleNode *tsn = static_cast<TimeScaleNode*>(nb);
|
TimeScaleNode *tsn = static_cast<TimeScaleNode*>(nb);
|
||||||
float rem;
|
float rem;
|
||||||
if (p_seek)
|
if (p_seek)
|
||||||
rem = _process_node(tsn->inputs[0].node,r_prev_anim,p_weight,p_time,true,p_filter,p_reverse_weight);
|
rem = _process_node(tsn->inputs[0].node,r_prev_anim,p_time,true,p_fallback_weight,p_weights);
|
||||||
else
|
else
|
||||||
rem = _process_node(tsn->inputs[0].node,r_prev_anim,p_weight,p_time*tsn->scale,false,p_filter,p_reverse_weight);
|
rem = _process_node(tsn->inputs[0].node,r_prev_anim,p_time*tsn->scale,false,p_fallback_weight,p_weights);
|
||||||
if (tsn->scale == 0)
|
if (tsn->scale == 0)
|
||||||
return INFINITY;
|
return INFINITY;
|
||||||
else
|
else
|
||||||
|
@ -669,16 +736,18 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
}
|
}
|
||||||
tsn->seek_pos=-1;
|
tsn->seek_pos=-1;
|
||||||
|
|
||||||
return _process_node(tsn->inputs[0].node,r_prev_anim,p_weight,p_time,p_seek, p_filter, p_reverse_weight);
|
return _process_node(tsn->inputs[0].node,r_prev_anim,p_time,p_seek,p_fallback_weight,p_weights);
|
||||||
|
|
||||||
} break;
|
} break;
|
||||||
case NODE_TRANSITION: {
|
case NODE_TRANSITION: {
|
||||||
|
|
||||||
TransitionNode *tn = static_cast<TransitionNode*>(nb);
|
TransitionNode *tn = static_cast<TransitionNode*>(nb);
|
||||||
|
HashMap<NodePath, float> prev_weights(*p_weights);
|
||||||
|
float prev_fallback_weight = p_fallback_weight;
|
||||||
|
|
||||||
if (tn->prev<0) { // process current animation, check for transition
|
if (tn->prev<0) { // process current animation, check for transition
|
||||||
|
|
||||||
float rem = _process_node(tn->inputs[tn->current].node,r_prev_anim,p_weight,p_time,p_seek,p_filter,p_reverse_weight);
|
float rem = _process_node(tn->inputs[tn->current].node,r_prev_anim,p_time,p_seek,p_fallback_weight,p_weights);
|
||||||
if (p_seek)
|
if (p_seek)
|
||||||
tn->time=p_time;
|
tn->time=p_time;
|
||||||
else
|
else
|
||||||
|
@ -698,22 +767,25 @@ float AnimationTreePlayer::_process_node(const StringName& p_node,AnimationNode
|
||||||
|
|
||||||
float rem;
|
float rem;
|
||||||
|
|
||||||
|
_compute_weights(&p_fallback_weight,p_weights, 1.0-blend);
|
||||||
|
_compute_weights(&prev_fallback_weight,&prev_weights, blend);
|
||||||
|
|
||||||
if (!p_seek && tn->switched) { //just switched, seek to start of current
|
if (!p_seek && tn->switched) { //just switched, seek to start of current
|
||||||
|
|
||||||
rem = _process_node(tn->inputs[tn->current].node,r_prev_anim,p_weight*(1.0-blend),0,true,p_filter,p_reverse_weight*(1.0-blend));
|
rem = _process_node(tn->inputs[tn->current].node,r_prev_anim,0,true,p_fallback_weight,p_weights);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
rem = _process_node(tn->inputs[tn->current].node,r_prev_anim,p_weight*(1.0-blend),p_time,p_seek,p_filter,p_reverse_weight*(1.0-blend));
|
rem = _process_node(tn->inputs[tn->current].node,r_prev_anim,p_time,p_seek,p_fallback_weight,p_weights);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tn->switched=false;
|
tn->switched=false;
|
||||||
|
|
||||||
if (p_seek) { // don't seek prev animation
|
if (p_seek) { // don't seek prev animation
|
||||||
_process_node(tn->inputs[tn->prev].node,r_prev_anim,p_weight*blend,0,false,p_filter,p_reverse_weight*blend);
|
_process_node(tn->inputs[tn->prev].node,r_prev_anim,0,false,prev_fallback_weight,&prev_weights);
|
||||||
tn->time=p_time;
|
tn->time=p_time;
|
||||||
} else {
|
} else {
|
||||||
_process_node(tn->inputs[tn->prev].node,r_prev_anim,p_weight*blend,p_time,false,p_filter,p_reverse_weight*blend);
|
_process_node(tn->inputs[tn->prev].node,r_prev_anim,p_time,false,prev_fallback_weight,&prev_weights);
|
||||||
tn->time+=p_time;
|
tn->time+=p_time;
|
||||||
tn->prev_xfading-=p_time;
|
tn->prev_xfading-=p_time;
|
||||||
if (tn->prev_xfading<0) {
|
if (tn->prev_xfading<0) {
|
||||||
|
@ -750,10 +822,11 @@ void AnimationTreePlayer::_process_animation(float p_delta) {
|
||||||
AnimationNode *prev=NULL;
|
AnimationNode *prev=NULL;
|
||||||
|
|
||||||
if (reset_request) {
|
if (reset_request) {
|
||||||
_process_node(out_name,&prev, 1.0, 0, true);
|
|
||||||
|
_process_node(out_name,&prev, 0, true);
|
||||||
reset_request=false;
|
reset_request=false;
|
||||||
} else
|
} else
|
||||||
_process_node(out_name,&prev, 1.0, p_delta);
|
_process_node(out_name,&prev, p_delta);
|
||||||
|
|
||||||
if (dirty_caches) {
|
if (dirty_caches) {
|
||||||
//some animation changed.. ignore this pass
|
//some animation changed.. ignore this pass
|
||||||
|
@ -802,7 +875,7 @@ void AnimationTreePlayer::_process_animation(float p_delta) {
|
||||||
if (tr.track==NULL || tr.local_track<0 || tr.weight < CMP_EPSILON)
|
if (tr.track==NULL || tr.local_track<0 || tr.weight < CMP_EPSILON)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
float blend=tr.weight;
|
float blend=tr.weight / tr.track->total_weight;
|
||||||
|
|
||||||
switch(a->track_get_type(tr.local_track)) {
|
switch(a->track_get_type(tr.local_track)) {
|
||||||
case Animation::TYPE_TRANSFORM: { ///< Transform a node or a bone.
|
case Animation::TYPE_TRANSFORM: { ///< Transform a node or a bone.
|
||||||
|
@ -1904,6 +1977,3 @@ AnimationTreePlayer::~AnimationTreePlayer() {
|
||||||
node_map.erase( node_map.front() );
|
node_map.erase( node_map.front() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,7 @@ private:
|
||||||
Variant value;
|
Variant value;
|
||||||
|
|
||||||
bool skip;
|
bool skip;
|
||||||
|
float total_weight;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -273,7 +274,7 @@ private:
|
||||||
Map<StringName,NodeBase*> node_map;
|
Map<StringName,NodeBase*> node_map;
|
||||||
|
|
||||||
// return time left to finish animation
|
// return time left to finish animation
|
||||||
float _process_node(const StringName& p_node,AnimationNode **r_prev_anim, float p_weight,float p_step, bool p_seek=false,const HashMap<NodePath,bool> *p_filter=NULL, float p_reverse_weight=0);
|
float _process_node(const StringName& p_node,AnimationNode **r_prev_anim,float p_step, bool p_seek=false, float p_fallback_weight = 1.0, HashMap<NodePath,float>* p_weights = NULL);
|
||||||
void _process_animation(float p_delta);
|
void _process_animation(float p_delta);
|
||||||
bool reset_request;
|
bool reset_request;
|
||||||
|
|
||||||
|
@ -283,6 +284,8 @@ private:
|
||||||
void _recompute_caches();
|
void _recompute_caches();
|
||||||
void _recompute_caches(const StringName& p_node);
|
void _recompute_caches(const StringName& p_node);
|
||||||
DVector<String> _get_node_list();
|
DVector<String> _get_node_list();
|
||||||
|
|
||||||
|
void _compute_weights(float *p_fallback_weight, HashMap<NodePath,float> *p_weights, float p_coeff, const HashMap<NodePath,bool> *p_filter = NULL, float p_filtered_coeff = 0);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue