Behaviour tree composites
This commit is contained in:
@@ -35,6 +35,9 @@ struct BzAIBTNodeState {
|
|||||||
BzAIBTNodeState *prev;
|
BzAIBTNodeState *prev;
|
||||||
|
|
||||||
union {
|
union {
|
||||||
|
struct {
|
||||||
|
BzAIBTNode *running;
|
||||||
|
} composite;
|
||||||
struct {
|
struct {
|
||||||
i32 iter;
|
i32 iter;
|
||||||
} repeat;
|
} repeat;
|
||||||
@@ -164,32 +167,14 @@ void bzAIBTDestroyState(BzAIBTState *state) {
|
|||||||
bzMemSet(state, 0, sizeof(*state));
|
bzMemSet(state, 0, sizeof(*state));
|
||||||
}
|
}
|
||||||
|
|
||||||
void bzAIBTStatePush(BzAIBTState *state, BzAIBTNodeState *nodeState,
|
void bzAIBTStateAppend(BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
||||||
const BzAIBTNodeState *desc) {
|
nodeState->next = NULL;
|
||||||
BzAIBTNodeState *newState = bzObjectPool(state->nodeStatePool);
|
nodeState->prev = state->last;
|
||||||
BZ_ASSERT(newState && desc);
|
if (state->last)
|
||||||
*newState = *desc;
|
state->last->next = nodeState;
|
||||||
|
else
|
||||||
newState->next = NULL;
|
state->first = nodeState;
|
||||||
newState->prev = NULL;
|
state->last = nodeState;
|
||||||
|
|
||||||
if (nodeState == NULL)
|
|
||||||
nodeState = state->last;
|
|
||||||
|
|
||||||
if (nodeState) {
|
|
||||||
BzAIBTNodeState *next = nodeState->next;
|
|
||||||
nodeState->next = newState;
|
|
||||||
newState->prev = nodeState;
|
|
||||||
newState->next = next;
|
|
||||||
if (next)
|
|
||||||
next->prev = newState;
|
|
||||||
if (state->last == nodeState)
|
|
||||||
state->last = newState;
|
|
||||||
} else {
|
|
||||||
newState->prev = state->last;
|
|
||||||
state->last = newState;
|
|
||||||
if (state->first == NULL) state->first = newState;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void bzAIBTStatePop(BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
void bzAIBTStatePop(BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
||||||
if (state->first == nodeState) state->first = nodeState->next;
|
if (state->first == nodeState) state->first = nodeState->next;
|
||||||
@@ -200,74 +185,170 @@ void bzAIBTStatePop(BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
|||||||
nodeState->prev->next = next;
|
nodeState->prev->next = next;
|
||||||
if (nodeState->next)
|
if (nodeState->next)
|
||||||
nodeState->next->prev = prev;
|
nodeState->next->prev = prev;
|
||||||
|
nodeState->next = NULL;
|
||||||
|
nodeState->prev = NULL;
|
||||||
|
}
|
||||||
|
void bzAIBTStateRenew(BzAIBTState *oldState, BzAIBTState *newState, BzAIBTNodeState *nodeState) {
|
||||||
|
// Pop nodeState and transfer it to the back
|
||||||
|
bzAIBTStatePop(oldState, nodeState);
|
||||||
|
bzAIBTStateAppend(newState, nodeState);
|
||||||
|
}
|
||||||
|
BzAIBTNodeState *bzAIBTStatePool(BzAIBTState *state, const BzAIBTNode *node) {
|
||||||
|
BzAIBTNodeState *nodeState = bzObjectPool(state->nodeStatePool);
|
||||||
|
nodeState->next = NULL;
|
||||||
|
nodeState->prev = NULL;
|
||||||
|
nodeState->node = node;
|
||||||
|
return nodeState;
|
||||||
|
}
|
||||||
|
void bzAIBTStateRelease(BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
||||||
bzObjectPoolRelease(state->nodeStatePool, nodeState);
|
bzObjectPoolRelease(state->nodeStatePool, nodeState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool nodeMatchesState(const BzAIBTNode *node, const BzAIBTNodeState *state) {
|
||||||
|
return state && state->node == node;
|
||||||
|
}
|
||||||
|
BzAIBTNodeState *getNextNodeState(const BzAIBTNode *node, BzAIBTNodeState *nodeState) {
|
||||||
|
if (nodeState && nodeMatchesState(node, nodeState))
|
||||||
|
return nodeState->next;
|
||||||
|
return nodeState;
|
||||||
|
}
|
||||||
|
|
||||||
static inline BzAIBTStatus bzAIBTExecuteNode(const BzAIBTNode *node, f32 dt,
|
static inline BzAIBTStatus bzAIBTExecuteNode(const BzAIBTNode *node, f32 dt,
|
||||||
BzAIBTState *state, BzAIBTNodeState *nodeState);
|
BzAIBTNodeState *nodeState,
|
||||||
/*
|
BzAIBTState *oldState, BzAIBTState *newState);
|
||||||
static inline BzAIBTStatus bzAIBTExecuteComposite(const BzObjectPool *nodePool, const BzAIBTNode *node,
|
static inline BzAIBTStatus bzAIBTExecuteComposite(const BzAIBTNode *node, f32 dt,
|
||||||
BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
BzAIBTNodeState *nodeState,
|
||||||
|
BzAIBTState *oldState, BzAIBTState *newState) {
|
||||||
|
BzAIBTNodeState *nextState = getNextNodeState(node, nodeState);
|
||||||
|
BzAIBTNode *start = node->first;
|
||||||
|
bool isParallel = node->type == BZ_AIBT_COMP_PARALLEL_SEQUENCE ||
|
||||||
|
node->type == BZ_AIBT_COMP_PARALLEL_SELECTOR;
|
||||||
|
|
||||||
|
if (!isParallel && nodeMatchesState(node, nodeState))
|
||||||
|
start = nodeState->as.composite.running;
|
||||||
|
|
||||||
|
// Always push dummy state
|
||||||
|
if (nodeMatchesState(node, nodeState)) {
|
||||||
|
bzAIBTStateRenew(oldState, newState, nodeState);
|
||||||
|
} else {
|
||||||
|
nodeState = bzAIBTStatePool(oldState, node);
|
||||||
|
bzAIBTStateAppend(newState, nodeState);
|
||||||
|
}
|
||||||
|
i32 numRunning = 0;
|
||||||
|
i32 numSuccessful = 0;
|
||||||
|
i32 numFailed = 0;
|
||||||
|
i32 numChildren = 0;
|
||||||
|
BzAIBTStatus status = BZ_AIBT_ERROR;
|
||||||
|
BzAIBTNode *child = start;
|
||||||
|
for (;child; child = child->next) {
|
||||||
|
BzAIBTStatus childStatus = bzAIBTExecuteNode(child, dt, nextState, oldState, newState);
|
||||||
|
numChildren++;
|
||||||
|
switch (childStatus) {
|
||||||
|
case BZ_AIBT_RUNNING:
|
||||||
|
numRunning++;
|
||||||
|
break;
|
||||||
|
case BZ_AIBT_SUCCESS:
|
||||||
|
numSuccessful++;
|
||||||
|
break;
|
||||||
|
case BZ_AIBT_FAIL:
|
||||||
|
numFailed++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (node->type) {
|
||||||
|
case BZ_AIBT_COMP_SELECTOR:
|
||||||
|
case BZ_AIBT_COMP_PARALLEL_SELECTOR:
|
||||||
|
if (childStatus == BZ_AIBT_SUCCESS)
|
||||||
|
status = BZ_AIBT_SUCCESS;
|
||||||
|
break;
|
||||||
|
case BZ_AIBT_COMP_SEQUENCE:
|
||||||
|
case BZ_AIBT_COMP_PARALLEL_SEQUENCE:
|
||||||
|
if (childStatus == BZ_AIBT_FAIL)
|
||||||
|
status = BZ_AIBT_FAIL;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (status == BZ_AIBT_FAIL || status == BZ_AIBT_SUCCESS)
|
||||||
|
break;
|
||||||
|
if (numRunning > 0 && !isParallel) {
|
||||||
|
status = BZ_AIBT_RUNNING;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
switch (node->type) {
|
switch (node->type) {
|
||||||
case BZ_AIBT_COMP_SELECTOR:
|
case BZ_AIBT_COMP_SELECTOR:
|
||||||
for (BzAIBTNode *child = node->first; child; child = child->next) {
|
case BZ_AIBT_COMP_PARALLEL_SELECTOR:
|
||||||
BzAIBTStatus status = bzAIBTExecuteNode(bt, child);
|
if (numFailed == numChildren)
|
||||||
if (status == BZ_AIBT_SUCCESS) return status;
|
status = BZ_AIBT_FAIL;
|
||||||
if (status == BZ_AIBT_RUNNING) return status;
|
break;
|
||||||
}
|
|
||||||
return BZ_AIBT_FAIL;
|
|
||||||
case BZ_AIBT_COMP_SEQUENCE:
|
case BZ_AIBT_COMP_SEQUENCE:
|
||||||
for (BzAIBTNode *child = node->first; child; child = child->next) {
|
case BZ_AIBT_COMP_PARALLEL_SEQUENCE:
|
||||||
BzAIBTStatus status = bzAIBTExecuteNode(bt, child);
|
if (numSuccessful == numChildren)
|
||||||
if (status == BZ_AIBT_FAIL) return status;
|
status = BZ_AIBT_SUCCESS;
|
||||||
if (status == BZ_AIBT_RUNNING) return status;
|
break;
|
||||||
}
|
|
||||||
return BZ_AIBT_SUCCESS;
|
|
||||||
default:
|
default:
|
||||||
assert(false);
|
break;
|
||||||
return BZ_AIBT_ERROR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status == BZ_AIBT_ERROR) {
|
||||||
|
bzAIBTStatePop(newState, nodeState);
|
||||||
|
return BZ_AIBT_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool finished = status == BZ_AIBT_SUCCESS ||
|
||||||
|
status == BZ_AIBT_FAIL;
|
||||||
|
if (finished) {
|
||||||
|
// Dummy state is no longer needed
|
||||||
|
bzAIBTStatePop(newState, nodeState);
|
||||||
|
} else {
|
||||||
|
BZ_ASSERT(status == BZ_AIBT_RUNNING);
|
||||||
|
nodeState->as.composite.running = child;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
static inline BzAIBTStatus bzAIBTExecuteDecorator(const BzAIBTNode *node, f32 dt,
|
static inline BzAIBTStatus bzAIBTExecuteDecorator(const BzAIBTNode *node, f32 dt,
|
||||||
BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
BzAIBTNodeState *nodeState,
|
||||||
// Ensure decorator has only one child
|
BzAIBTState *oldState, BzAIBTState *newState) {
|
||||||
BZ_ASSERT(node->first && node->first == node->last);
|
// Ensure decorator has only one child, if any
|
||||||
BzAIBTNodeState *first = nodeState;
|
BZ_ASSERT(!node->first || node->first == node->last);
|
||||||
if (nodeState && first->node == node) {
|
BzAIBTNodeState *nextState = getNextNodeState(node, nodeState);
|
||||||
first = first->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (node->type) {
|
switch (node->type) {
|
||||||
case BZ_AIBT_DECOR_REPEAT:
|
case BZ_AIBT_DECOR_REPEAT:
|
||||||
if (!nodeState || nodeState->node != node) {
|
if (!nodeMatchesState(node, nodeState)) {
|
||||||
bzAIBTStatePush(state, nodeState, &(BzAIBTNodeState) {
|
BzAIBTNodeState *newNodeState = bzAIBTStatePool(oldState, node);
|
||||||
.node = node,
|
newNodeState->as.repeat.iter = 0;
|
||||||
.as.repeat.iter = 0
|
bzAIBTStateAppend(newState, newNodeState);
|
||||||
});
|
nodeState = newNodeState;
|
||||||
|
} else {
|
||||||
|
bzAIBTStateRenew(oldState, newState, nodeState);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BZ_AIBT_DECOR_DELAY:
|
case BZ_AIBT_DECOR_DELAY:
|
||||||
if (!nodeState || nodeState->node != node) {
|
if (!nodeMatchesState(node, nodeState)) {
|
||||||
bzAIBTStatePush(state, nodeState, &(BzAIBTNodeState) {
|
BzAIBTNodeState *newNodeState = bzAIBTStatePool(oldState, node);
|
||||||
.node = node,
|
newNodeState->as.delay.elapsed = dt;
|
||||||
.as.delay = {0.2f}
|
bzAIBTStateAppend(newState, newNodeState);
|
||||||
});
|
|
||||||
return BZ_AIBT_RUNNING;
|
return BZ_AIBT_RUNNING;
|
||||||
}
|
}
|
||||||
nodeState->as.delay.elapsed += 0.2f;
|
nodeState->as.delay.elapsed += dt;
|
||||||
if (nodeState->as.delay.elapsed < node->as.delay.ms) {
|
if (nodeState->as.delay.elapsed < node->as.delay.ms) {
|
||||||
|
bzAIBTStateRenew(oldState, newState, nodeState);
|
||||||
return BZ_AIBT_RUNNING;
|
return BZ_AIBT_RUNNING;
|
||||||
}
|
}
|
||||||
bzAIBTStatePop(state, nodeState);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
BzAIBTStatus inStatus = bzAIBTExecuteNode(node->first, dt, state, first);
|
// Implicit success, if no children are present
|
||||||
|
BzAIBTStatus inStatus = BZ_AIBT_SUCCESS;
|
||||||
|
if (node->first)
|
||||||
|
inStatus = bzAIBTExecuteNode(node->first, dt, nextState, oldState, newState);
|
||||||
|
|
||||||
// ERROR, RUNNING are propagated up
|
// Propagate ERROR, RUNNING up
|
||||||
if (inStatus == BZ_AIBT_ERROR)
|
if (inStatus == BZ_AIBT_ERROR)
|
||||||
return BZ_AIBT_ERROR;
|
return BZ_AIBT_ERROR;
|
||||||
if (inStatus == BZ_AIBT_RUNNING)
|
if (inStatus == BZ_AIBT_RUNNING)
|
||||||
@@ -286,8 +367,10 @@ static inline BzAIBTStatus bzAIBTExecuteDecorator(const BzAIBTNode *node, f32 dt
|
|||||||
status = BZ_AIBT_FAIL;
|
status = BZ_AIBT_FAIL;
|
||||||
break;
|
break;
|
||||||
case BZ_AIBT_DECOR_INVERT:
|
case BZ_AIBT_DECOR_INVERT:
|
||||||
if (inStatus == BZ_AIBT_FAIL) status = BZ_AIBT_SUCCESS;
|
if (inStatus == BZ_AIBT_FAIL)
|
||||||
if (inStatus == BZ_AIBT_SUCCESS) status = BZ_AIBT_FAIL;
|
status = BZ_AIBT_SUCCESS;
|
||||||
|
if (inStatus == BZ_AIBT_SUCCESS)
|
||||||
|
status = BZ_AIBT_FAIL;
|
||||||
break;
|
break;
|
||||||
case BZ_AIBT_DECOR_UNTIL_SUCCESS:
|
case BZ_AIBT_DECOR_UNTIL_SUCCESS:
|
||||||
if (inStatus == BZ_AIBT_SUCCESS)
|
if (inStatus == BZ_AIBT_SUCCESS)
|
||||||
@@ -305,7 +388,7 @@ static inline BzAIBTStatus bzAIBTExecuteDecorator(const BzAIBTNode *node, f32 dt
|
|||||||
BZ_ASSERT(nodeState->node == node);
|
BZ_ASSERT(nodeState->node == node);
|
||||||
nodeState->as.repeat.iter++;
|
nodeState->as.repeat.iter++;
|
||||||
if (nodeState->as.repeat.iter >= node->as.repeat.n) {
|
if (nodeState->as.repeat.iter >= node->as.repeat.n) {
|
||||||
bzAIBTStatePop(state, nodeState);
|
bzAIBTStatePop(newState, nodeState);
|
||||||
status = inStatus;
|
status = inStatus;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -317,14 +400,15 @@ static inline BzAIBTStatus bzAIBTExecuteDecorator(const BzAIBTNode *node, f32 dt
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
static inline BzAIBTStatus bzAIBTExecuteNode(const BzAIBTNode *node, f32 dt,
|
static inline BzAIBTStatus bzAIBTExecuteNode(const BzAIBTNode *node, f32 dt,
|
||||||
BzAIBTState *state, BzAIBTNodeState *nodeState) {
|
BzAIBTNodeState *nodeState,
|
||||||
|
BzAIBTState *oldState, BzAIBTState *newState) {
|
||||||
BzAIBTStatus status = BZ_AIBT_ERROR;
|
BzAIBTStatus status = BZ_AIBT_ERROR;
|
||||||
switch (node->type) {
|
switch (node->type) {
|
||||||
case BZ_AIBT_COMP_SELECTOR:
|
case BZ_AIBT_COMP_SELECTOR:
|
||||||
case BZ_AIBT_COMP_SEQUENCE:
|
case BZ_AIBT_COMP_SEQUENCE:
|
||||||
case BZ_AIBT_COMP_PARALLEL_SELECTOR:
|
case BZ_AIBT_COMP_PARALLEL_SELECTOR:
|
||||||
case BZ_AIBT_COMP_PARALLEL_SEQUENCE:
|
case BZ_AIBT_COMP_PARALLEL_SEQUENCE:
|
||||||
//status = bzAIBTExecuteComposite(bt, node, state, nodeState);
|
status = bzAIBTExecuteComposite(node, dt, nodeState, oldState, newState);
|
||||||
break;
|
break;
|
||||||
case BZ_AIBT_DECOR_DUMMY:
|
case BZ_AIBT_DECOR_DUMMY:
|
||||||
case BZ_AIBT_DECOR_SUCCESS:
|
case BZ_AIBT_DECOR_SUCCESS:
|
||||||
@@ -334,11 +418,11 @@ static inline BzAIBTStatus bzAIBTExecuteNode(const BzAIBTNode *node, f32 dt,
|
|||||||
case BZ_AIBT_DECOR_UNTIL_FAIL:
|
case BZ_AIBT_DECOR_UNTIL_FAIL:
|
||||||
case BZ_AIBT_DECOR_REPEAT:
|
case BZ_AIBT_DECOR_REPEAT:
|
||||||
case BZ_AIBT_DECOR_DELAY:
|
case BZ_AIBT_DECOR_DELAY:
|
||||||
status = bzAIBTExecuteDecorator(node, dt, state, nodeState);
|
status = bzAIBTExecuteDecorator(node, dt, nodeState, oldState, newState);
|
||||||
break;
|
break;
|
||||||
case BZ_AIBT_ACTION:
|
case BZ_AIBT_ACTION:
|
||||||
BZ_ASSERT(node->as.action.fn);
|
BZ_ASSERT(node->as.action.fn);
|
||||||
return node->as.action.fn(state->userData);
|
return node->as.action.fn(oldState->userData);
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
@@ -348,8 +432,24 @@ BzAIBTStatus bzAIBTExecute(BzAIBTState *state, f32 dt) {
|
|||||||
BZ_ASSERT(bzObjectPoolGetObjectSize(state->nodeStatePool) == bzAIBTGetNodeStateSize());
|
BZ_ASSERT(bzObjectPoolGetObjectSize(state->nodeStatePool) == bzAIBTGetNodeStateSize());
|
||||||
BZ_ASSERT(state);
|
BZ_ASSERT(state);
|
||||||
BZ_ASSERT(state->root);
|
BZ_ASSERT(state->root);
|
||||||
|
|
||||||
|
BzAIBTState newState = {
|
||||||
|
.first = NULL,
|
||||||
|
.last = NULL,
|
||||||
|
};
|
||||||
|
|
||||||
BzAIBTNodeState *first = state->first;
|
BzAIBTNodeState *first = state->first;
|
||||||
const BzAIBTNode *firstNode = first ? first->node : state->root;
|
const BzAIBTNode *firstNode = first ? first->node : state->root;
|
||||||
BzAIBTStatus status = bzAIBTExecuteNode(firstNode, dt, state, first);
|
BzAIBTStatus status = bzAIBTExecuteNode(firstNode, dt, first, state, &newState);
|
||||||
|
|
||||||
|
// Release leftover states
|
||||||
|
BzAIBTNodeState *pState = state->first;
|
||||||
|
while (pState) {
|
||||||
|
BzAIBTNodeState *next = pState->next;
|
||||||
|
bzAIBTStateRelease(state, pState);
|
||||||
|
pState = next;
|
||||||
|
}
|
||||||
|
state->first = newState.first;
|
||||||
|
state->last = newState.last;
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ BzAIBTNode *printBT = NULL;
|
|||||||
|
|
||||||
BzAIBTStatus printAction(void *data) {
|
BzAIBTStatus printAction(void *data) {
|
||||||
bzLogInfo("Hello, world!");
|
bzLogInfo("Hello, world!");
|
||||||
return BZ_AIBT_FAIL;
|
return BZ_AIBT_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init(int *game) {
|
bool init(int *game) {
|
||||||
@@ -20,12 +20,16 @@ bool init(int *game) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// for 1..5:
|
// for 1..5:
|
||||||
// delay 1s
|
// seq
|
||||||
// print "Hello, world!"
|
// delay 1s
|
||||||
|
// print "Hello, world!"
|
||||||
printBT = bzAIBTMakeRoot(nodePool);
|
printBT = bzAIBTMakeRoot(nodePool);
|
||||||
BzAIBTNode *node = bzAIBTDecorRepeat(nodePool, printBT, 5);
|
BzAIBTNode *node = bzAIBTDecorRepeat(nodePool, printBT, 5);
|
||||||
node = bzAIBTDecorDelay(nodePool, node, 1.0f);
|
|
||||||
bzAIBTAction(nodePool, node, printAction);
|
BzAIBTNode *seq = bzAIBTCompSequence(nodePool, node, false);
|
||||||
|
|
||||||
|
bzAIBTDecorDelay(nodePool, seq, 1.0f);
|
||||||
|
bzAIBTAction(nodePool, seq, printAction);
|
||||||
|
|
||||||
BzAIBTState state = bzAIBTCreateState(&(BzAIBTStateDesc) {
|
BzAIBTState state = bzAIBTCreateState(&(BzAIBTStateDesc) {
|
||||||
.root = printBT,
|
.root = printBT,
|
||||||
@@ -39,6 +43,7 @@ bool init(int *game) {
|
|||||||
while (status == BZ_AIBT_RUNNING) {
|
while (status == BZ_AIBT_RUNNING) {
|
||||||
status = bzAIBTExecute(&state, 0.2f);
|
status = bzAIBTExecute(&state, 0.2f);
|
||||||
count++;
|
count++;
|
||||||
|
assert(status != BZ_AIBT_ERROR);
|
||||||
}
|
}
|
||||||
bzLogInfo("Iter: %d", count);
|
bzLogInfo("Iter: %d", count);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user