UI work for level editor
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
This commit is contained in:
parent
3e5cade90e
commit
34403c3155
@ -215,6 +215,10 @@ UI Work
|
||||
Terrain editing UI
|
||||
- Menu to select palette to generate, populated based on data
|
||||
|
||||
(04/13/2024)
|
||||
UI Work
|
||||
- Level editor ability to destroy an entity on server, have it also destroy on client, AND not persist on save
|
||||
|
||||
# TODO
|
||||
|
||||
More Debug menus
|
||||
@ -258,6 +262,8 @@ Build a lod system
|
||||
- LOD trees aggressively
|
||||
- LOD foliage cells aggressively
|
||||
|
||||
Refactor attach logic to better encapsulate semantic attachment
|
||||
|
||||
Light Manager
|
||||
- Creates and manages light entities
|
||||
- Uses priority queue mechanism like foliage manager to only draw the most important lights
|
||||
|
||||
@ -522,16 +522,6 @@ public class CollisionEngine {
|
||||
spaceLock.release();
|
||||
}
|
||||
|
||||
public void deregisterRigidBody(DBody body){
|
||||
if(bodies.contains(body)){
|
||||
bodies.remove(body);
|
||||
}
|
||||
if((body) != null){
|
||||
body.destroy();
|
||||
// world.removeRigidBody(body);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroyEntityThatHasPhysics(Entity e){
|
||||
//make uncollidable
|
||||
if(e.containsKey(EntityDataStrings.PHYSICS_COLLISION_BODY)){
|
||||
|
||||
@ -4,6 +4,7 @@ import org.joml.Vector3d;
|
||||
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.entity.types.collision.CollisionObjUtils;
|
||||
import electrosphere.net.parser.net.message.EntityMessage;
|
||||
import electrosphere.server.datacell.Realm;
|
||||
import electrosphere.server.datacell.ServerDataCell;
|
||||
import electrosphere.server.datacell.utils.DataCellSearchUtils;
|
||||
@ -74,4 +75,20 @@ public class ServerEntityUtils {
|
||||
CollisionObjUtils.serverPositionCharacter(entity, position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys an entity on the server
|
||||
* @param entity the entity to destroy
|
||||
*/
|
||||
public static void destroyEntity(Entity entity){
|
||||
ServerDataCell cell = DataCellSearchUtils.getEntityDataCell(entity);
|
||||
Realm realm = Globals.realmManager.getEntityRealm(entity);
|
||||
if(cell != null){
|
||||
cell.broadcastNetworkMessage(EntityMessage.constructDestroyMessage(entity.getId()));
|
||||
|
||||
//if the entity had physics, remove them from the world
|
||||
realm.getCollisionEngine().destroyEntityThatHasPhysics(entity);
|
||||
}
|
||||
EntityUtils.cleanUpEntity(entity);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -21,6 +21,9 @@ public class LoggerInterface {
|
||||
public static Logger loggerAudio;
|
||||
public static Logger loggerUI;
|
||||
|
||||
/**
|
||||
* Initializes all logic objects
|
||||
*/
|
||||
public static void initLoggers(){
|
||||
loggerStartup = new Logger(LogLevel.WARNING);
|
||||
loggerNetworking = new Logger(LogLevel.WARNING);
|
||||
@ -31,7 +34,7 @@ public class LoggerInterface {
|
||||
loggerAuth = new Logger(LogLevel.WARNING);
|
||||
loggerDB = new Logger(LogLevel.WARNING);
|
||||
loggerAudio = new Logger(LogLevel.WARNING);
|
||||
loggerUI = new Logger(LogLevel.DEBUG);
|
||||
loggerUI = new Logger(LogLevel.WARNING);
|
||||
loggerStartup.INFO("Initialized loggers");
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,21 @@ import org.joml.Vector3d;
|
||||
import org.lwjgl.util.yoga.Yoga;
|
||||
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.entity.Entity;
|
||||
import electrosphere.entity.ServerEntityUtils;
|
||||
import electrosphere.entity.types.camera.CameraEntityUtils;
|
||||
import electrosphere.entity.types.creature.CreatureUtils;
|
||||
import electrosphere.entity.types.foliage.FoliageUtils;
|
||||
import electrosphere.entity.types.item.ItemUtils;
|
||||
import electrosphere.entity.types.object.ObjectUtils;
|
||||
import electrosphere.game.data.creature.type.CreatureType;
|
||||
import electrosphere.game.data.foliage.type.FoliageType;
|
||||
import electrosphere.logger.LoggerInterface;
|
||||
import electrosphere.menu.WindowStrings;
|
||||
import electrosphere.menu.WindowUtils;
|
||||
import electrosphere.renderer.ui.elements.Button;
|
||||
import electrosphere.renderer.ui.elements.Div;
|
||||
import electrosphere.renderer.ui.elements.Label;
|
||||
import electrosphere.renderer.ui.elements.VirtualScrollable;
|
||||
import electrosphere.renderer.ui.elements.Window;
|
||||
import electrosphere.renderer.ui.elementtypes.ClickableElement.ClickEventCallback;
|
||||
@ -22,35 +28,38 @@ import electrosphere.renderer.ui.elementtypes.NavigableElement.NavigationEventCa
|
||||
import electrosphere.renderer.ui.events.ClickEvent;
|
||||
import electrosphere.renderer.ui.events.NavigationEvent;
|
||||
import electrosphere.server.datacell.Realm;
|
||||
import electrosphere.server.datacell.utils.EntityLookupUtils;
|
||||
|
||||
/**
|
||||
* Menu generators for level editor
|
||||
*/
|
||||
public class MenuGeneratorsLevelEditor {
|
||||
|
||||
static Window rVal;
|
||||
|
||||
//
|
||||
//side panel
|
||||
static Window mainSidePanel;
|
||||
//width of the side panel
|
||||
static final int SIDE_PANEL_WIDTH = 500;
|
||||
|
||||
|
||||
/**
|
||||
* Creates the level editor side panel top view
|
||||
* @return
|
||||
*/
|
||||
public static Window createLevelEditorSidePanel(){
|
||||
//setup window
|
||||
rVal = new Window(0,0,SIDE_PANEL_WIDTH,Globals.WINDOW_HEIGHT,true);
|
||||
rVal.setParentAlignContent(Yoga.YGAlignFlexEnd);
|
||||
rVal.setParentJustifyContent(Yoga.YGJustifyFlexEnd);
|
||||
rVal.setParentAlignItem(Yoga.YGAlignFlexEnd);
|
||||
rVal.setAlignContent(Yoga.YGAlignFlexStart);
|
||||
rVal.setAlignItems(Yoga.YGAlignFlexStart);
|
||||
rVal.setJustifyContent(Yoga.YGJustifyFlexStart);
|
||||
mainSidePanel = new Window(0,0,SIDE_PANEL_WIDTH,Globals.WINDOW_HEIGHT,true);
|
||||
mainSidePanel.setParentAlignContent(Yoga.YGAlignFlexEnd);
|
||||
mainSidePanel.setParentJustifyContent(Yoga.YGJustifyFlexEnd);
|
||||
mainSidePanel.setParentAlignItem(Yoga.YGAlignFlexEnd);
|
||||
mainSidePanel.setAlignContent(Yoga.YGAlignFlexStart);
|
||||
mainSidePanel.setAlignItems(Yoga.YGAlignFlexStart);
|
||||
mainSidePanel.setJustifyContent(Yoga.YGJustifyFlexStart);
|
||||
|
||||
//scrollable
|
||||
VirtualScrollable scrollable = new VirtualScrollable(SIDE_PANEL_WIDTH, Globals.WINDOW_HEIGHT);
|
||||
rVal.addChild(scrollable);
|
||||
rVal.setOnNavigationCallback(new NavigationEventCallback() {public boolean execute(NavigationEvent event){
|
||||
mainSidePanel.addChild(scrollable);
|
||||
mainSidePanel.setOnNavigationCallback(new NavigationEventCallback() {public boolean execute(NavigationEvent event){
|
||||
WindowUtils.closeWindow(WindowStrings.LEVEL_EDTIOR_SIDE_PANEL);
|
||||
return false;
|
||||
}});
|
||||
@ -58,9 +67,9 @@ public class MenuGeneratorsLevelEditor {
|
||||
fillInDefaultContent(scrollable);
|
||||
|
||||
|
||||
rVal.applyYoga();
|
||||
mainSidePanel.applyYoga();
|
||||
|
||||
return rVal;
|
||||
return mainSidePanel;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,7 +104,13 @@ public class MenuGeneratorsLevelEditor {
|
||||
return false;
|
||||
}}));
|
||||
|
||||
rVal.applyYoga();
|
||||
//entity tree view
|
||||
scrollable.addChild(Button.createButton("View Entity Tree", new ClickEventCallback() {public boolean execute(ClickEvent event){
|
||||
fillInEntityTreeContent(scrollable);
|
||||
return false;
|
||||
}}));
|
||||
|
||||
mainSidePanel.applyYoga();
|
||||
|
||||
}
|
||||
|
||||
@ -127,7 +142,7 @@ public class MenuGeneratorsLevelEditor {
|
||||
}}));
|
||||
}
|
||||
|
||||
rVal.applyYoga();
|
||||
mainSidePanel.applyYoga();
|
||||
}
|
||||
|
||||
private static void fillInSpawnFoliageContent(VirtualScrollable scrollable){
|
||||
@ -153,7 +168,79 @@ public class MenuGeneratorsLevelEditor {
|
||||
}}));
|
||||
}
|
||||
|
||||
rVal.applyYoga();
|
||||
mainSidePanel.applyYoga();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates tree view of entities in server
|
||||
* @param scrollable
|
||||
*/
|
||||
private static void fillInEntityTreeContent(VirtualScrollable scrollable){
|
||||
scrollable.clearChildren();
|
||||
|
||||
//back button
|
||||
scrollable.addChild(Button.createButton("Close", new ClickEventCallback() {public boolean execute(ClickEvent event){
|
||||
fillInDefaultContent(scrollable);
|
||||
return false;
|
||||
}}));
|
||||
|
||||
//elements for the entity
|
||||
for(Entity entity : EntityLookupUtils.getAllEntities()){
|
||||
if(
|
||||
CreatureUtils.isCreature(entity) ||
|
||||
ItemUtils.isItem(entity) ||
|
||||
ObjectUtils.isObject(entity) ||
|
||||
FoliageUtils.isFoliage(entity)
|
||||
){
|
||||
Div div = new Div();
|
||||
div.setFlexDirection(Yoga.YGFlexDirectionRow);
|
||||
div.setMaxHeight(30);
|
||||
div.setMarginBottom(5);
|
||||
div.setMarginLeft(5);
|
||||
div.setMarginRight(5);
|
||||
div.setMarginTop(5);
|
||||
|
||||
//delete button
|
||||
Button deleteButton = Button.createButton("X", new ClickEventCallback() {public boolean execute(ClickEvent event){
|
||||
LoggerInterface.loggerEngine.INFO("Delete " + entity.getId());
|
||||
ServerEntityUtils.destroyEntity(entity);
|
||||
return false;
|
||||
}});
|
||||
deleteButton.setMarginRight(5);
|
||||
deleteButton.setMarginLeft(5);
|
||||
div.addChild(deleteButton);
|
||||
|
||||
|
||||
Label entityName = new Label(1.0f);
|
||||
entityName.setText("(" + entity.getId() + ") " + getEntityString(entity));
|
||||
div.addChild(entityName);
|
||||
|
||||
|
||||
scrollable.addChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
mainSidePanel.applyYoga();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string to display for this entity in the entity tree view
|
||||
* @param e the entity
|
||||
* @return the string to display
|
||||
*/
|
||||
private static String getEntityString(Entity e){
|
||||
if(CreatureUtils.isCreature(e)){
|
||||
return "Object - " + CreatureUtils.getType(e);
|
||||
} else if(ItemUtils.isItem(e)){
|
||||
return "Object - " + ItemUtils.getType(e);
|
||||
} else if(FoliageUtils.isFoliage(e)){
|
||||
return "Object - " + FoliageUtils.getFoliageType(e).getName();
|
||||
} else if(ObjectUtils.isObject(e)){
|
||||
return "Object - " + ObjectUtils.getType(e);
|
||||
}
|
||||
return "Entity Unknown Type";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
package electrosphere.net.client.protocol;
|
||||
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.engine.Main;
|
||||
import electrosphere.entity.ClientEntityUtils;
|
||||
import electrosphere.entity.Entity;
|
||||
import electrosphere.entity.EntityUtils;
|
||||
@ -18,15 +16,32 @@ import electrosphere.logger.LoggerInterface;
|
||||
import electrosphere.net.parser.net.message.EntityMessage;
|
||||
import electrosphere.util.Utilities;
|
||||
|
||||
/**
|
||||
* Client entity network protocol
|
||||
*/
|
||||
public class EntityProtocol {
|
||||
|
||||
/**
|
||||
* Handles a single clientbound entity message
|
||||
* @param message The message to handle
|
||||
*/
|
||||
protected static void handleEntityMessage(EntityMessage message){
|
||||
Globals.profiler.beginCpuSample("EntityProtocol.handleEntityMessage");
|
||||
LoggerInterface.loggerNetworking.DEBUG("Parse entity message of type " + message.getMessageSubtype());
|
||||
Entity newlySpawnedEntity;
|
||||
switch(message.getMessageSubtype()){
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// SPAWNING STUFF IN
|
||||
//
|
||||
//
|
||||
case CREATE:
|
||||
LoggerInterface.loggerNetworking.DEBUG("Spawn ID " + message.getentityID() + " of type " + message.getentityCategory() + " subtype " + message.getentitySubtype());
|
||||
LoggerInterface.loggerNetworking.DEBUG(
|
||||
"Spawn ID " + message.getentityID() + " of type " + message.getentityCategory() + " subtype " + message.getentitySubtype() +
|
||||
" @ " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ()
|
||||
);
|
||||
switch(message.getentityCategory()){
|
||||
case 0:
|
||||
// newlySpawnedEntity = CreatureUtils.spawnBasicCreature(message.getentitySubtype());
|
||||
@ -42,14 +57,14 @@ public class EntityProtocol {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SPAWNCREATURE:
|
||||
case SPAWNCREATURE: {
|
||||
LoggerInterface.loggerNetworking.DEBUG("Spawn Creature " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
|
||||
CreatureTemplate template = Utilities.deserialize(message.getcreatureTemplate(), CreatureTemplate.class);
|
||||
newlySpawnedEntity = CreatureUtils.clientSpawnBasicCreature(template.getCreatureType(),template);
|
||||
ClientEntityUtils.initiallyPositionEntity(newlySpawnedEntity, new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()));
|
||||
Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
|
||||
break;
|
||||
case SPAWNITEM:
|
||||
} break;
|
||||
case SPAWNITEM: {
|
||||
if(!Globals.RUN_SERVER){
|
||||
LoggerInterface.loggerNetworking.DEBUG("Spawn Item " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
|
||||
//spawn item
|
||||
@ -59,26 +74,29 @@ public class EntityProtocol {
|
||||
ClientEntityUtils.initiallyPositionEntity(newlySpawnedEntity, new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()));
|
||||
Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
|
||||
}
|
||||
break;
|
||||
case DESTROY:
|
||||
//only obey if we're not also the server
|
||||
if(!Globals.RUN_SERVER){
|
||||
EntityUtils.cleanUpEntity(Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID()));
|
||||
} break;
|
||||
case SPAWNFOLIAGESEED: {
|
||||
LoggerInterface.loggerNetworking.DEBUG("Spawn foliage " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
|
||||
String type = message.getcreatureTemplate();
|
||||
newlySpawnedEntity = FoliageUtils.spawnBasicFoliage(type,message.getfoliageSeed());
|
||||
ClientEntityUtils.initiallyPositionEntity(newlySpawnedEntity, new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()));
|
||||
Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
|
||||
} break;
|
||||
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// UPDATING PROPERTIES
|
||||
//
|
||||
//
|
||||
case MOVE: {
|
||||
Entity target = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
|
||||
LoggerInterface.loggerNetworking.DEBUG("ID: " + message.getentityID());
|
||||
if(target != null){
|
||||
EntityUtils.getPosition(target).set(message.getpositionX(),message.getpositionY(),message.getpositionZ());
|
||||
}
|
||||
break;
|
||||
case MOVE:
|
||||
//literally just adding this to scope so I can use `` Entity target; `` again
|
||||
if(message.getentityID() != -1){
|
||||
Entity target = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
|
||||
LoggerInterface.loggerNetworking.DEBUG("ID: " + message.getentityID());
|
||||
if(target != null){
|
||||
EntityUtils.getPosition(target).set(message.getpositionX(),message.getpositionY(),message.getpositionZ());
|
||||
}
|
||||
}
|
||||
// CreatureUtils.attachEntityMessageToMovementTree(Globals.entityManager.getEntityFromId(message.getId()),message);
|
||||
break;
|
||||
case SETBEHAVIORTREE:
|
||||
break;
|
||||
} break;
|
||||
case SETPROPERTY: {
|
||||
if(Globals.clientSceneWrapper.serverToClientMapContainsId(message.getentityID())){
|
||||
if(message.getpropertyType() == 0){
|
||||
@ -95,33 +113,51 @@ public class EntityProtocol {
|
||||
//TODO: bounce message
|
||||
}
|
||||
} break;
|
||||
case ATTACHENTITYTOENTITY:
|
||||
case ATTACHENTITYTOENTITY: {
|
||||
Entity child = Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID());
|
||||
Entity parent = Globals.clientSceneWrapper.getEntityFromServerId(message.gettargetID());
|
||||
LoggerInterface.loggerNetworking.DEBUG("Attach " + message.getentityID() + " to " + message.gettargetID() + " on bone " + message.getbone());
|
||||
if(child != null && parent != null){
|
||||
AttachUtils.clientAttachEntityToEntityAtBone(parent, child, message.getbone(), new Quaterniond());
|
||||
}
|
||||
break;
|
||||
case MOVEUPDATE:
|
||||
CreatureUtils.clientAttachEntityMessageToMovementTree(Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID()),message);
|
||||
break;
|
||||
case ATTACKUPDATE:
|
||||
CreatureUtils.attachEntityMessageToAttackTree(Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID()),message);
|
||||
break;
|
||||
case KILL:
|
||||
break;
|
||||
case SETPOSITION:
|
||||
break;
|
||||
case SETFACING:
|
||||
break;
|
||||
case SPAWNFOLIAGESEED: {
|
||||
LoggerInterface.loggerNetworking.DEBUG("Spawn foliage " + message.getentityID() + " at " + message.getpositionX() + " " + message.getpositionY() + " " + message.getpositionZ());
|
||||
String type = message.getcreatureTemplate();
|
||||
newlySpawnedEntity = FoliageUtils.spawnBasicFoliage(type,message.getfoliageSeed());
|
||||
ClientEntityUtils.initiallyPositionEntity(newlySpawnedEntity, new Vector3d(message.getpositionX(),message.getpositionY(),message.getpositionZ()));
|
||||
Globals.clientSceneWrapper.mapIdToId(newlySpawnedEntity.getId(), message.getentityID());
|
||||
} break;
|
||||
case MOVEUPDATE: {
|
||||
CreatureUtils.clientAttachEntityMessageToMovementTree(Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID()),message);
|
||||
} break;
|
||||
case ATTACKUPDATE: {
|
||||
CreatureUtils.attachEntityMessageToAttackTree(Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID()),message);
|
||||
} break;
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// DESTROYING AND DESTRUCTING STUFF
|
||||
//
|
||||
//
|
||||
case DESTROY: {
|
||||
EntityUtils.cleanUpEntity(Globals.clientSceneWrapper.getEntityFromServerId(message.getentityID()));
|
||||
} break;
|
||||
|
||||
|
||||
//
|
||||
//
|
||||
// TODO
|
||||
//
|
||||
//
|
||||
case KILL:
|
||||
case SETPOSITION:
|
||||
case SETFACING:
|
||||
//to be implemented
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
case SETBEHAVIORTREE:
|
||||
case SETBTREEPROPERTYDOUBLE:
|
||||
case SETBTREEPROPERTYENUM:
|
||||
case SETBTREEPROPERTYFLOAT:
|
||||
case SETBTREEPROPERTYINT:
|
||||
case SETBTREEPROPERTYSTRING:
|
||||
//unused
|
||||
break;
|
||||
}
|
||||
Globals.profiler.endCpuSample();
|
||||
}
|
||||
|
||||
@ -101,9 +101,12 @@ public class PhysicsDataCell {
|
||||
// realm.getCollisionEngine().registerPhysicsEntity(physicsEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the physics for this data cell
|
||||
*/
|
||||
public void destroyPhysics(){
|
||||
Realm realm = Globals.realmManager.getEntityRealm(physicsEntity);
|
||||
realm.getCollisionEngine().deregisterRigidBody((DBody)physicsObject);
|
||||
realm.getCollisionEngine().destroyEntityThatHasPhysics(physicsEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package electrosphere.server.datacell.utils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ -47,5 +48,14 @@ public class EntityLookupUtils {
|
||||
public static boolean isServerEntity(Entity entity){
|
||||
return idToEntityMap.containsKey(entity.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* !!!DANGER!!! USE IN DEBUG/LEVEL EDITOR ONLY
|
||||
* Gets all entities tracked by the entity lookup utils
|
||||
* @return The collection of all server entities
|
||||
*/
|
||||
public static Collection<Entity> getAllEntities(){
|
||||
return idToEntityMap.values();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user