ECS Enhancements
This commit is contained in:
@@ -40,8 +40,16 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
errorChain(gameInit());
|
errorChain(gameInit());
|
||||||
|
|
||||||
// FOF
|
// FOF
|
||||||
entityid_t ent0 = entityManagerAdd();
|
entityid_t cam = entityManagerAdd();
|
||||||
componentid_t ent0c0 = entityAddComponent(ent0, COMPONENT_TYPE_POSITION);
|
componentid_t camPos = entityAddComponent(cam, COMPONENT_TYPE_POSITION);
|
||||||
|
entityPositionLookAt(
|
||||||
|
cam,
|
||||||
|
camPos,
|
||||||
|
(vec3){ 0.0f, 0.0f, 0.0f },
|
||||||
|
(vec3){ 0.0f, 1.0f, 0.0f },
|
||||||
|
(vec3){ 5.0f, 5.0f, 5.0f }
|
||||||
|
);
|
||||||
|
componentid_t camCam = entityAddComponent(cam, COMPONENT_TYPE_CAMERA);
|
||||||
|
|
||||||
entityid_t ent1 = entityManagerAdd();
|
entityid_t ent1 = entityManagerAdd();
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ void componentInit(
|
|||||||
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot initialize null component");
|
||||||
|
|
||||||
component_t *cmp = &ENTITY_MANAGER.entities[entityId].components[componentId];
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||||
memoryZero(cmp, sizeof(component_t));
|
memoryZero(cmp, sizeof(component_t));
|
||||||
|
|
||||||
cmp->type = type;
|
cmp->type = type;
|
||||||
@@ -40,16 +41,55 @@ void componentInit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void * componentGetData(
|
void * componentGetData(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
) {
|
||||||
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot get data of null component");
|
||||||
|
|
||||||
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||||
|
assertTrue(cmp->type == type, "Component type mismatch");
|
||||||
|
|
||||||
|
return &cmp->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentindex_t componentGetIndex(
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
) {
|
) {
|
||||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
return (entityId * ENTITY_COMPONENT_COUNT_MAX) + componentId;
|
||||||
component_t *cmp = &ENTITY_MANAGER.entities[entityId].components[componentId];
|
}
|
||||||
if(cmp->type == COMPONENT_TYPE_NULL) return NULL;
|
|
||||||
|
|
||||||
return &cmp->data;
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
|
const componenttype_t type,
|
||||||
|
entityid_t outEntities[ENTITY_COUNT_MAX]
|
||||||
|
) {
|
||||||
|
assertTrue(type < COMPONENT_TYPE_COUNT, "Component type OOB");
|
||||||
|
assertTrue(type != COMPONENT_TYPE_NULL, "Cannot check NULL type");
|
||||||
|
assertNotNull(outEntities, "Output entities array cannot be null");
|
||||||
|
|
||||||
|
entityid_t written = 0;
|
||||||
|
for(entityid_t i = 0; i < ENTITY_COUNT_MAX; i++) {
|
||||||
|
entityid_t used = ENTITY_MANAGER.entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + i
|
||||||
|
];
|
||||||
|
if(used == 0xFF) continue;
|
||||||
|
outEntities[written++] = used;
|
||||||
|
assertTrue(
|
||||||
|
used == i, "Entity ID mismatch in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
assertTrue(
|
||||||
|
(ENTITY_MANAGER.entities[used].state & ENTITY_STATE_ACTIVE) != 0,
|
||||||
|
"Inactive entity in entitiesWithComponent lookup"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return written;
|
||||||
}
|
}
|
||||||
|
|
||||||
void componentDispose(
|
void componentDispose(
|
||||||
@@ -59,7 +99,8 @@ void componentDispose(
|
|||||||
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
assertTrue(entityId < ENTITY_COUNT_MAX, "Entity ID OOB");
|
||||||
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
assertTrue(componentId < ENTITY_COMPONENT_COUNT_MAX, "Component ID OOB");
|
||||||
|
|
||||||
component_t *cmp = &ENTITY_MANAGER.entities[entityId].components[componentId];
|
componentindex_t index = componentGetIndex(entityId, componentId);
|
||||||
|
component_t *cmp = &ENTITY_MANAGER.components[index];
|
||||||
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
if(cmp->type == COMPONENT_TYPE_NULL) return;
|
||||||
|
|
||||||
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
if(COMPONENT_DEFINITIONS[cmp->type].dispose) {
|
||||||
|
|||||||
@@ -60,13 +60,40 @@ void componentInit(
|
|||||||
*
|
*
|
||||||
* @param entityId The entity ID.
|
* @param entityId The entity ID.
|
||||||
* @param componentId The component ID.
|
* @param componentId The component ID.
|
||||||
|
* @param type The type of the component to get, only used for assertion.
|
||||||
* @return A pointer to the component data.
|
* @return A pointer to the component data.
|
||||||
*/
|
*/
|
||||||
void * componentGetData(
|
void * componentGetData(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
const componenttype_t type
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of a component for the entity with component ID.
|
||||||
|
*
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @return The index of the component in the component array.
|
||||||
|
*/
|
||||||
|
componentindex_t componentGetIndex(
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entity IDs of all entities with a component of the given type.
|
||||||
|
*
|
||||||
|
* @param type The type of the component to get entities for.
|
||||||
|
* @param outEntities An array to write the entity IDs to, must be at least
|
||||||
|
* ENTITY_COUNT_MAX in size.
|
||||||
|
* @return The number of entity IDs written to outEntities.
|
||||||
|
*/
|
||||||
|
entityid_t componentGetEntitiesWithComponent(
|
||||||
|
const componenttype_t type,
|
||||||
|
entityid_t outEntities[ENTITY_COUNT_MAX]
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes of a component for the entity with component ID.
|
* Disposes of a component for the entity with component ID.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,8 +3,4 @@
|
|||||||
# This software is released under the MIT License.
|
# This software is released under the MIT License.
|
||||||
# https://opensource.org/licenses/MIT
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
# Sources
|
add_subdirectory(display)
|
||||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|
||||||
PUBLIC
|
|
||||||
entityposition.c
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
# Sources
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
entityposition.c
|
||||||
|
entitycamera.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
#include "display/framebuffer/framebuffer.h"
|
||||||
|
#include "display/screen/screen.h"
|
||||||
|
|
||||||
|
void entityCameraInit(const entityid_t ent, const componentid_t comp) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
ent, comp, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
cam->nearClip = 0.1f;
|
||||||
|
cam->farClip = 100.0f;
|
||||||
|
cam->projType = CAMERA_PROJECTION_TYPE_PERSPECTIVE;
|
||||||
|
cam->perspective.fov = 60.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityCameraGetProjection(
|
||||||
|
const entityid_t ent,
|
||||||
|
const componentid_t comp,
|
||||||
|
mat4 out
|
||||||
|
) {
|
||||||
|
entitycamera_t *cam = (entitycamera_t *)componentGetData(
|
||||||
|
ent, comp, COMPONENT_TYPE_CAMERA
|
||||||
|
);
|
||||||
|
|
||||||
|
if(
|
||||||
|
cam->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE ||
|
||||||
|
cam->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED
|
||||||
|
) {
|
||||||
|
glm_mat4_identity(out);
|
||||||
|
glm_perspective(
|
||||||
|
cam->perspective.fov,
|
||||||
|
SCREEN.aspect,
|
||||||
|
cam->nearClip,
|
||||||
|
cam->farClip,
|
||||||
|
out
|
||||||
|
);
|
||||||
|
|
||||||
|
if(cam->projType == CAMERA_PROJECTION_TYPE_PERSPECTIVE_FLIPPED) {
|
||||||
|
out[1][1] *= -1.0f;
|
||||||
|
}
|
||||||
|
} else if(cam->projType == CAMERA_PROJECTION_TYPE_ORTHOGRAPHIC) {
|
||||||
|
glm_mat4_identity(out);
|
||||||
|
glm_ortho(
|
||||||
|
cam->orthographic.left,
|
||||||
|
cam->orthographic.right,
|
||||||
|
cam->orthographic.top,
|
||||||
|
cam->orthographic.bottom,
|
||||||
|
cam->nearClip,
|
||||||
|
cam->farClip,
|
||||||
|
out
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "entity/entitybase.h"
|
||||||
|
#include "display/camera/camera.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
float_t fov;
|
||||||
|
} perspective;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
float_t left;
|
||||||
|
float_t right;
|
||||||
|
float_t top;
|
||||||
|
float_t bottom;
|
||||||
|
} orthographic;
|
||||||
|
};
|
||||||
|
|
||||||
|
float_t nearClip;
|
||||||
|
float_t farClip;
|
||||||
|
cameraprojectiontype_t projType;
|
||||||
|
} entitycamera_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes an entity camera component.
|
||||||
|
*
|
||||||
|
* @param ent The entity ID.
|
||||||
|
* @param comp The component ID.
|
||||||
|
*/
|
||||||
|
void entityCameraInit(const entityid_t ent, const componentid_t comp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders out the projection matrix for the given camera.
|
||||||
|
*
|
||||||
|
* @param ent The entity ID.
|
||||||
|
* @param comp The component ID.
|
||||||
|
* @param out The output projection matrix.
|
||||||
|
*/
|
||||||
|
void entityCameraGetProjection(
|
||||||
|
const entityid_t ent,
|
||||||
|
const componentid_t comp,
|
||||||
|
mat4 out
|
||||||
|
);
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "entity/entitymanager.h"
|
||||||
|
|
||||||
|
void entityPositionInit(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
|
||||||
|
glm_mat4_identity(pos->transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
void entityPositionLookAt(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 target,
|
||||||
|
vec3 up,
|
||||||
|
vec3 eye
|
||||||
|
) {
|
||||||
|
entityposition_t *pos = componentGetData(
|
||||||
|
entityId, componentId, COMPONENT_TYPE_POSITION
|
||||||
|
);
|
||||||
|
glm_lookat(eye, target, up, pos->transform);
|
||||||
|
}
|
||||||
+17
@@ -21,4 +21,21 @@ typedef struct {
|
|||||||
void entityPositionInit(
|
void entityPositionInit(
|
||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componentid_t componentId
|
const componentid_t componentId
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the entity's position to look at a target point.
|
||||||
|
*
|
||||||
|
* @param entityId The entity ID.
|
||||||
|
* @param componentId The component ID.
|
||||||
|
* @param target The target point to look at.
|
||||||
|
* @param up The up vector for the look at transformation.
|
||||||
|
* @param eye The position of the camera/eye for the look at transformation.
|
||||||
|
*/
|
||||||
|
void entityPositionLookAt(
|
||||||
|
const entityid_t entityId,
|
||||||
|
const componentid_t componentId,
|
||||||
|
vec3 target,
|
||||||
|
vec3 up,
|
||||||
|
vec3 eye
|
||||||
);
|
);
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 Dominic Masters
|
|
||||||
*
|
|
||||||
* This software is released under the MIT License.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "entity/entitymanager.h"
|
|
||||||
|
|
||||||
void entityPositionInit(
|
|
||||||
const entityid_t entityId,
|
|
||||||
const componentid_t componentId
|
|
||||||
) {
|
|
||||||
entityposition_t *pos = componentGetData(entityId, componentId);
|
|
||||||
|
|
||||||
glm_mat4_identity(pos->transform);
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "entity/component/entityposition.h"
|
#include "entity/component/display/entityposition.h"
|
||||||
|
#include "entity/component/display/entitycamera.h"
|
||||||
|
|
||||||
X(POSITION, entityposition_t, position, entityPositionInit, NULL)
|
X(POSITION, entityposition_t, position, entityPositionInit, NULL)
|
||||||
|
X(CAMERA, entitycamera_t, camera, entityCameraInit, NULL)
|
||||||
@@ -14,6 +14,17 @@ void entityInit(const entityid_t entityId) {
|
|||||||
|
|
||||||
memoryZero(ent, sizeof(entity_t));
|
memoryZero(ent, sizeof(entity_t));
|
||||||
|
|
||||||
|
// Mark all component types not using this entity.
|
||||||
|
for(
|
||||||
|
componenttype_t compType = 0;
|
||||||
|
compType < COMPONENT_TYPE_COUNT;
|
||||||
|
compType++
|
||||||
|
) {
|
||||||
|
ENTITY_MANAGER.entitiesWithComponent[
|
||||||
|
compType * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
ent->state |= ENTITY_STATE_ACTIVE;
|
ent->state |= ENTITY_STATE_ACTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,11 +32,16 @@ componentid_t entityAddComponent(
|
|||||||
const entityid_t entityId,
|
const entityid_t entityId,
|
||||||
const componenttype_t type
|
const componenttype_t type
|
||||||
) {
|
) {
|
||||||
|
componentindex_t compInd;
|
||||||
entity_t *ent = &ENTITY_MANAGER.entities[entityId];
|
entity_t *ent = &ENTITY_MANAGER.entities[entityId];
|
||||||
|
|
||||||
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||||
if(ent->components[i].type != COMPONENT_TYPE_NULL) continue;
|
compInd = componentGetIndex(entityId, i);
|
||||||
|
if(ENTITY_MANAGER.components[compInd].type != COMPONENT_TYPE_NULL) continue;
|
||||||
componentInit(entityId, i, type);
|
componentInit(entityId, i, type);
|
||||||
|
ENTITY_MANAGER.entitiesWithComponent[
|
||||||
|
type * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = entityId;
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,11 +50,16 @@ componentid_t entityAddComponent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void entityDispose(const entityid_t entityId) {
|
void entityDispose(const entityid_t entityId) {
|
||||||
|
componentindex_t compInd;
|
||||||
entity_t *ent = &ENTITY_MANAGER.entities[entityId];
|
entity_t *ent = &ENTITY_MANAGER.entities[entityId];
|
||||||
|
|
||||||
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
for(componentid_t i = 0; i < ENTITY_COMPONENT_COUNT_MAX; i++) {
|
||||||
if(ent->components[i].type == COMPONENT_TYPE_NULL) continue;
|
compInd = componentGetIndex(entityId, i);
|
||||||
|
if(ENTITY_MANAGER.components[compInd].type == COMPONENT_TYPE_NULL) continue;
|
||||||
componentDispose(entityId, i);
|
componentDispose(entityId, i);
|
||||||
|
ENTITY_MANAGER.entitiesWithComponent[
|
||||||
|
ENTITY_MANAGER.components[compInd].type * ENTITY_COUNT_MAX + entityId
|
||||||
|
] = 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
ent->state = 0;
|
ent->state = 0;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t state;
|
uint8_t state;
|
||||||
component_t components[ENTITY_COMPONENT_COUNT_MAX];
|
|
||||||
} entity_t;
|
} entity_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,4 +12,5 @@
|
|||||||
#define ENTITY_COMPONENT_COUNT_MAX 24
|
#define ENTITY_COMPONENT_COUNT_MAX 24
|
||||||
|
|
||||||
typedef uint8_t entityid_t;
|
typedef uint8_t entityid_t;
|
||||||
typedef uint8_t componentid_t;
|
typedef uint8_t componentid_t;
|
||||||
|
typedef uint16_t componentindex_t;
|
||||||
@@ -13,6 +13,10 @@ entitymanager_t ENTITY_MANAGER;
|
|||||||
|
|
||||||
void entityManagerInit(void) {
|
void entityManagerInit(void) {
|
||||||
memoryZero(&ENTITY_MANAGER, sizeof(entitymanager_t));
|
memoryZero(&ENTITY_MANAGER, sizeof(entitymanager_t));
|
||||||
|
memorySet(
|
||||||
|
ENTITY_MANAGER.entitiesWithComponent, 0xFF,
|
||||||
|
sizeof(entityid_t) * COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
entityid_t entityManagerAdd() {
|
entityid_t entityManagerAdd() {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
entity_t entities[ENTITY_COUNT_MAX];
|
entity_t entities[ENTITY_COUNT_MAX];
|
||||||
|
component_t components[ENTITY_COUNT_MAX * ENTITY_COMPONENT_COUNT_MAX];
|
||||||
|
entityid_t entitiesWithComponent[COMPONENT_TYPE_COUNT * ENTITY_COUNT_MAX];
|
||||||
} entitymanager_t;
|
} entitymanager_t;
|
||||||
|
|
||||||
extern entitymanager_t ENTITY_MANAGER;
|
extern entitymanager_t ENTITY_MANAGER;
|
||||||
|
|||||||
Reference in New Issue
Block a user