delete deprecated classes
This commit is contained in:
parent
329559bfd7
commit
222a3ebe9f
@ -1800,6 +1800,7 @@ Safe executor service creation in non final static contexts
|
|||||||
Fix reloading client world repeatedly
|
Fix reloading client world repeatedly
|
||||||
AI manager thread safeing
|
AI manager thread safeing
|
||||||
AI manager better error handling
|
AI manager better error handling
|
||||||
|
Delete deprecated classes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,706 +0,0 @@
|
|||||||
package electrosphere.client.terrain.cells;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.joml.Vector3d;
|
|
||||||
import org.joml.Vector3i;
|
|
||||||
|
|
||||||
import electrosphere.client.terrain.cache.ChunkData;
|
|
||||||
import electrosphere.client.terrain.cells.DrawCell.DrawCellFace;
|
|
||||||
import electrosphere.client.terrain.manager.ClientTerrainManager;
|
|
||||||
import electrosphere.collision.PhysicsEntityUtils;
|
|
||||||
import electrosphere.engine.Globals;
|
|
||||||
import electrosphere.entity.EntityUtils;
|
|
||||||
import electrosphere.net.parser.net.message.TerrainMessage;
|
|
||||||
import electrosphere.renderer.shader.VisualShader;
|
|
||||||
import electrosphere.server.physics.terrain.manager.ServerTerrainChunk;
|
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
/**
|
|
||||||
* Manages the graphical entities for the terrain chunks
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
Notes for integrating with transvoxel algo:
|
|
||||||
Different problems to tackle
|
|
||||||
|
|
||||||
For all chunks within minimum radius, check if they can be updated and update accordingly <--- we do this currently
|
|
||||||
|
|
||||||
|
|
||||||
For all chunks between minimum radius and first LOD radius, check if we can make a LOD chunk
|
|
||||||
If we can, make a lod chunk or see if it can be updated
|
|
||||||
This check will be:
|
|
||||||
For every position, check if all four positions required for LOD chunk are within lod radius
|
|
||||||
if yes, make lod chunk
|
|
||||||
|
|
||||||
If we cannot, create a fullres chunk
|
|
||||||
This check will be:
|
|
||||||
For every position, check if all four positions required for LOD chunk are within lod radius
|
|
||||||
if yes, make lod chunk
|
|
||||||
if they are outside the far bound, create lod chunk
|
|
||||||
if they are within the near bound, create a fullres chunk
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
public class DrawCellManager {
|
|
||||||
|
|
||||||
|
|
||||||
//the center of this cell manager's array in cell space
|
|
||||||
int cellX;
|
|
||||||
int cellY;
|
|
||||||
int cellZ;
|
|
||||||
|
|
||||||
//all currently displaying mini cells
|
|
||||||
Set<DrawCell> cells = new HashSet<DrawCell>();
|
|
||||||
Map<String,DrawCell> keyCellMap = new HashMap<String,DrawCell>();
|
|
||||||
|
|
||||||
//status of all position keys
|
|
||||||
Set<String> hasNotRequested = new HashSet<String>();
|
|
||||||
Set<String> requested = new HashSet<String>();
|
|
||||||
Set<String> drawable = new HashSet<String>();
|
|
||||||
Set<String> undrawable = new HashSet<String>();
|
|
||||||
Set<String> updateable = new HashSet<String>();
|
|
||||||
|
|
||||||
//LOD level of all position keys
|
|
||||||
Map<String,Integer> positionLODLevel = new HashMap<String,Integer>();
|
|
||||||
|
|
||||||
//voxel atlas
|
|
||||||
VoxelTextureAtlas atlas;
|
|
||||||
|
|
||||||
//shader program for drawable cells
|
|
||||||
VisualShader program;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//the real-space radius for which we will construct draw cells inside of
|
|
||||||
//ie, we check if the draw cell's entity would be inside this radius. If it would, create the draw cell, otherwise don't
|
|
||||||
double drawFullModelRadius = 70;
|
|
||||||
|
|
||||||
//the radius we'll draw LODed chunks for
|
|
||||||
double drawLODRadius = drawFullModelRadius + ServerTerrainChunk.CHUNK_DIMENSION * (2*2 + 4*4 + 8*8 + 16*16);
|
|
||||||
|
|
||||||
|
|
||||||
//the number of possible LOD levels
|
|
||||||
//1,2,4,8,16
|
|
||||||
static final int NUMBER_OF_LOD_LEVELS = 5;
|
|
||||||
|
|
||||||
//the table of lod leve -> radius at which we will look for chunks within this log
|
|
||||||
double[] lodLevelRadiusTable = new double[5];
|
|
||||||
|
|
||||||
//the radius for which physics meshes are created when draw cells are created
|
|
||||||
int physicsRadius = 3;
|
|
||||||
|
|
||||||
//ready to start updating?
|
|
||||||
boolean update = false;
|
|
||||||
|
|
||||||
//controls whether we try to generate the drawable entities
|
|
||||||
//we want this to be false when in server-only mode
|
|
||||||
boolean generateDrawables = false;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DrawCellManager constructor
|
|
||||||
* @param commonWorldData The common world data
|
|
||||||
* @param clientTerrainManager The client terrain manager
|
|
||||||
* @param discreteX The initial discrete position X coordinate
|
|
||||||
* @param discreteY The initial discrete position Y coordinate
|
|
||||||
*/
|
|
||||||
public DrawCellManager(ClientTerrainManager clientTerrainManager, int discreteX, int discreteY, int discreteZ){
|
|
||||||
cellX = discreteX;
|
|
||||||
cellY = discreteY;
|
|
||||||
cellZ = discreteZ;
|
|
||||||
|
|
||||||
program = Globals.terrainShaderProgram;
|
|
||||||
|
|
||||||
//the first lod level is set by user
|
|
||||||
lodLevelRadiusTable[0] = drawFullModelRadius;
|
|
||||||
//generate LOD radius table
|
|
||||||
for(int i = 1; i < NUMBER_OF_LOD_LEVELS; i++){
|
|
||||||
double sizeOfSingleModel = Math.pow(2,i) * ServerTerrainChunk.CHUNK_DIMENSION;
|
|
||||||
//size of the radius for this lod level should be three times the size of a model + the previous radius
|
|
||||||
//this guarantees we get at least one adapter chunk, one proper chunk, and also that the radius accounts for the previous lod level chunks
|
|
||||||
lodLevelRadiusTable[i] = lodLevelRadiusTable[i-1] + sizeOfSingleModel * 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
physicsRadius = Globals.userSettings.getGameplayPhysicsCellRadius();
|
|
||||||
|
|
||||||
invalidateAllCells();
|
|
||||||
|
|
||||||
update = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private constructor
|
|
||||||
*/
|
|
||||||
DrawCellManager(){
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the player's current position in cell-space
|
|
||||||
* @param cellPos The cell's position
|
|
||||||
*/
|
|
||||||
public void setPlayerCell(Vector3i cellPos){
|
|
||||||
cellX = cellPos.x;
|
|
||||||
cellY = cellPos.y;
|
|
||||||
cellZ = cellPos.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update function that is called if a cell has not been requested
|
|
||||||
*/
|
|
||||||
void updateUnrequestedCell(){
|
|
||||||
if(hasNotRequested.size() > 0){
|
|
||||||
String targetKey = hasNotRequested.iterator().next();
|
|
||||||
hasNotRequested.remove(targetKey);
|
|
||||||
Vector3i worldPos = getVectorFromKey(targetKey);
|
|
||||||
|
|
||||||
//
|
|
||||||
//Because of the way marching cubes works, we need to request the adjacent chunks so we know how to properly blend between one chunk and the next
|
|
||||||
//The following loop-hell does this
|
|
||||||
//
|
|
||||||
for(int i = 0; i < 2; i++){
|
|
||||||
for(int j = 0; j < 2; j++){
|
|
||||||
for(int k = 0; k < 2; k++){
|
|
||||||
Vector3i posToCheck = new Vector3i(worldPos).add(i,j,k);
|
|
||||||
String requestKey = getCellKey(posToCheck.x,posToCheck.y,posToCheck.z);
|
|
||||||
if(
|
|
||||||
posToCheck.x >= 0 &&
|
|
||||||
posToCheck.x < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
posToCheck.y >= 0 &&
|
|
||||||
posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
posToCheck.z >= 0 &&
|
|
||||||
posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
!Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z, ChunkData.NO_STRIDE)
|
|
||||||
){
|
|
||||||
if(!requested.contains(requestKey)){
|
|
||||||
//client should request chunk data from server for each chunk necessary to create the model
|
|
||||||
Globals.clientConnection.queueOutgoingMessage(TerrainMessage.constructRequestChunkDataMessage(
|
|
||||||
posToCheck.x,
|
|
||||||
posToCheck.y,
|
|
||||||
posToCheck.z
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
undrawable.add(targetKey);
|
|
||||||
requested.add(targetKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes one of the undrawable cells drawable
|
|
||||||
*/
|
|
||||||
void makeCellDrawable(){
|
|
||||||
if(undrawable.size() > 0){
|
|
||||||
String targetKey = undrawable.iterator().next();
|
|
||||||
Vector3i worldPos = getVectorFromKey(targetKey);
|
|
||||||
|
|
||||||
//
|
|
||||||
//Checks if all chunk data necessary to generate a mesh is present
|
|
||||||
boolean containsNecessaryChunks = true;
|
|
||||||
for(int i = 0; i < 2; i++){
|
|
||||||
for(int j = 0; j < 2; j++){
|
|
||||||
for(int k = 0; k < 2; k++){
|
|
||||||
Vector3i posToCheck = new Vector3i(worldPos).add(i,j,k);
|
|
||||||
if(worldPos.x >= 0 &&
|
|
||||||
posToCheck.x < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
posToCheck.y >= 0 &&
|
|
||||||
posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
posToCheck.z >= 0 &&
|
|
||||||
posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
!containsChunkDataAtWorldPoint(posToCheck.x,posToCheck.y,posToCheck.z)
|
|
||||||
){
|
|
||||||
containsNecessaryChunks = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
//if contains data for all chunks necessary to generate visuals
|
|
||||||
if(containsNecessaryChunks){
|
|
||||||
|
|
||||||
//update the status of the terrain key
|
|
||||||
undrawable.remove(targetKey);
|
|
||||||
drawable.add(targetKey);
|
|
||||||
|
|
||||||
//build the cell
|
|
||||||
DrawCell cell = DrawCell.generateTerrainCell(
|
|
||||||
worldPos,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
cells.add(cell);
|
|
||||||
keyCellMap.put(targetKey,cell);
|
|
||||||
List<DrawCellFace> higherLODFace = null;
|
|
||||||
keyCellMap.get(targetKey).generateDrawableEntity(atlas,0,higherLODFace);
|
|
||||||
|
|
||||||
// //evaluate for foliage
|
|
||||||
// Globals.clientFoliageManager.evaluateChunk(worldPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a cell that can be updated
|
|
||||||
*/
|
|
||||||
void updateCellModel(){
|
|
||||||
if(updateable.size() > 0){
|
|
||||||
String targetKey = updateable.iterator().next();
|
|
||||||
updateable.remove(targetKey);
|
|
||||||
Vector3i worldPos = getVectorFromKey(targetKey);
|
|
||||||
if(
|
|
||||||
worldPos.x >= 0 &&
|
|
||||||
worldPos.x < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
worldPos.y >= 0 &&
|
|
||||||
worldPos.y < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
worldPos.z >= 0 &&
|
|
||||||
worldPos.z < Globals.clientWorldData.getWorldDiscreteSize()
|
|
||||||
){
|
|
||||||
keyCellMap.get(targetKey).destroy();
|
|
||||||
List<DrawCellFace> higherLODFace = null;
|
|
||||||
keyCellMap.get(targetKey).generateDrawableEntity(atlas,0,higherLODFace);
|
|
||||||
}
|
|
||||||
drawable.add(targetKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the manager contains a cell position that hasn't had its chunk data requested from the server yet
|
|
||||||
* @return true if there is an unrequested cell, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean containsUnrequestedCell(){
|
|
||||||
return hasNotRequested.size() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the manager contains a cell who hasn't been made drawable yet
|
|
||||||
* @return true if there is an undrawable cell, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean containsUndrawableCell(){
|
|
||||||
return undrawable.size() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the manager contains a cell who needs to be updated
|
|
||||||
* @return true if there is an updateable cell, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean containsUpdateableCell(){
|
|
||||||
return updateable.size() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms a real coordinate into a cell-space coordinate
|
|
||||||
* @param input the real coordinate
|
|
||||||
* @return the cell coordinate
|
|
||||||
*/
|
|
||||||
public int transformRealSpaceToCellSpace(double input){
|
|
||||||
return (int)(input / ServerTerrainChunk.CHUNK_DIMENSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the valid set and adds all keys to invalid set
|
|
||||||
*/
|
|
||||||
public void invalidateAllCells(){
|
|
||||||
drawable.clear();
|
|
||||||
hasNotRequested.clear();
|
|
||||||
clearOutOfBoundsCells();
|
|
||||||
queueNewCells();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates whether the position of the player has changed and if so, invalidates and cleans up cells accordingly
|
|
||||||
*/
|
|
||||||
private void calculateDeltas(){
|
|
||||||
//check if any not requested cells no longer need to be requested
|
|
||||||
clearOutOfBoundsCells();
|
|
||||||
//check if any cells should be added
|
|
||||||
queueNewCells();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all cells outside of draw radius
|
|
||||||
*/
|
|
||||||
private void clearOutOfBoundsCells(){
|
|
||||||
Set<DrawCell> cellsToRemove = new HashSet<DrawCell>();
|
|
||||||
for(DrawCell cell : cells){
|
|
||||||
Vector3d realPos = cell.getRealPos();
|
|
||||||
if(Globals.playerEntity != null && EntityUtils.getPosition(Globals.playerEntity).distance(realPos) > drawFullModelRadius){
|
|
||||||
cellsToRemove.add(cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for(DrawCell cell : cellsToRemove){
|
|
||||||
cells.remove(cell);
|
|
||||||
String key = getCellKey(cell.worldPos.x, cell.worldPos.y, cell.worldPos.z);
|
|
||||||
hasNotRequested.remove(key);
|
|
||||||
drawable.remove(key);
|
|
||||||
undrawable.remove(key);
|
|
||||||
updateable.remove(key);
|
|
||||||
keyCellMap.remove(key);
|
|
||||||
requested.remove(key);
|
|
||||||
cell.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queues new cells that are in bounds but not currently accounted for
|
|
||||||
*/
|
|
||||||
private void queueNewCells(){
|
|
||||||
if(Globals.playerEntity != null && Globals.clientWorldData != null){
|
|
||||||
Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity);
|
|
||||||
for(int x = -(int)drawFullModelRadius; x < drawFullModelRadius; x = x + ChunkData.CHUNK_DATA_SIZE){
|
|
||||||
for(int y = -(int)drawFullModelRadius; y < drawFullModelRadius; y = y + ChunkData.CHUNK_DATA_SIZE){
|
|
||||||
for(int z = -(int)drawFullModelRadius; z < drawFullModelRadius; z = z + ChunkData.CHUNK_DATA_SIZE){
|
|
||||||
Vector3d newPos = new Vector3d(playerPos.x + x, playerPos.y + y, playerPos.z + z);
|
|
||||||
Vector3i worldPos = new Vector3i(
|
|
||||||
Globals.clientWorldData.convertRealToChunkSpace(newPos.x),
|
|
||||||
Globals.clientWorldData.convertRealToChunkSpace(newPos.y),
|
|
||||||
Globals.clientWorldData.convertRealToChunkSpace(newPos.z)
|
|
||||||
);
|
|
||||||
Vector3d chunkRealSpace = new Vector3d(
|
|
||||||
Globals.clientWorldData.convertChunkToRealSpace(worldPos.x),
|
|
||||||
Globals.clientWorldData.convertChunkToRealSpace(worldPos.y),
|
|
||||||
Globals.clientWorldData.convertChunkToRealSpace(worldPos.z)
|
|
||||||
);
|
|
||||||
if(
|
|
||||||
playerPos.distance(chunkRealSpace) < drawFullModelRadius &&
|
|
||||||
worldPos.x >= 0 &&
|
|
||||||
worldPos.x < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
worldPos.y >= 0 &&
|
|
||||||
worldPos.y < Globals.clientWorldData.getWorldDiscreteSize() &&
|
|
||||||
worldPos.z >= 0 &&
|
|
||||||
worldPos.z < Globals.clientWorldData.getWorldDiscreteSize()
|
|
||||||
){
|
|
||||||
String key = getCellKey(
|
|
||||||
Globals.clientWorldData.convertRealToChunkSpace(chunkRealSpace.x),
|
|
||||||
Globals.clientWorldData.convertRealToChunkSpace(chunkRealSpace.y),
|
|
||||||
Globals.clientWorldData.convertRealToChunkSpace(chunkRealSpace.z)
|
|
||||||
);
|
|
||||||
if(!keyCellMap.containsKey(key) && !hasNotRequested.contains(key) && !undrawable.contains(key) && !drawable.contains(key) &&
|
|
||||||
!requested.contains(key)){
|
|
||||||
hasNotRequested.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates cells that need updating in this manager
|
|
||||||
*/
|
|
||||||
public void update(){
|
|
||||||
calculateDeltas();
|
|
||||||
if(containsUnrequestedCell() && !containsUndrawableCell()){
|
|
||||||
updateUnrequestedCell();
|
|
||||||
} else if(containsUndrawableCell()){
|
|
||||||
makeCellDrawable();
|
|
||||||
} else if(containsUpdateableCell()){
|
|
||||||
updateCellModel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls whether the client generates drawable chunks or just physics chunks (ie if running a headless client)
|
|
||||||
* @param generate true to generate graphics, false otherwise
|
|
||||||
*/
|
|
||||||
public void setGenerateDrawables(boolean generate){
|
|
||||||
this.generateDrawables = generate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the terrain cache has a chunk at a given world point
|
|
||||||
* @param worldX the x coordinate
|
|
||||||
* @param worldY the y coordinate
|
|
||||||
* @param worldZ the z coordinate
|
|
||||||
* @return true if the chunk data exists, false otherwise
|
|
||||||
*/
|
|
||||||
boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){
|
|
||||||
if(Globals.clientTerrainManager != null){
|
|
||||||
return Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldX,worldY,worldZ,ChunkData.NO_STRIDE);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the chunk data at a given point
|
|
||||||
* @param worldX The world position x component of the cell
|
|
||||||
* @param worldY The world position y component of the cell
|
|
||||||
* @param worldZ The world position z component of the cell
|
|
||||||
* @return The chunk data at the specified points
|
|
||||||
*/
|
|
||||||
ChunkData getChunkDataAtPoint(int worldX, int worldY, int worldZ){
|
|
||||||
return Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldX,worldY,worldZ,ChunkData.NO_STRIDE);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a unique key for the cell
|
|
||||||
* @param worldX The world position x component of the cell
|
|
||||||
* @param worldY The world position y component of the cell
|
|
||||||
* @param worldZ The world position z component of the cell
|
|
||||||
* @return The key
|
|
||||||
*/
|
|
||||||
private String getCellKey(int worldX, int worldY, int worldZ){
|
|
||||||
return worldX + "_" + worldY + "_" + worldZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a vector3i from the cell key
|
|
||||||
* @param key The cell key
|
|
||||||
* @return The vector3i containing the components of the cell key
|
|
||||||
*/
|
|
||||||
private Vector3i getVectorFromKey(String key){
|
|
||||||
String[] keyComponents = key.split("_");
|
|
||||||
return new Vector3i(Integer.parseInt(keyComponents[0]),Integer.parseInt(keyComponents[1]),Integer.parseInt(keyComponents[2]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks a data cell as updateable (can be regenerated with a new model because the underlying data has changed)
|
|
||||||
* @param chunkX The chunk x coordinate
|
|
||||||
* @param chunkY The chunk y coordinate
|
|
||||||
* @param chunkZ The chunk z coordinate
|
|
||||||
*/
|
|
||||||
public void markUpdateable(int chunkX, int chunkY, int chunkZ){
|
|
||||||
updateable.add(getCellKey(chunkX, chunkY, chunkZ));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evicts all chunks
|
|
||||||
*/
|
|
||||||
public void evictAll(){
|
|
||||||
//destroy models
|
|
||||||
for(String key : drawable){
|
|
||||||
keyCellMap.get(key).destroy();
|
|
||||||
}
|
|
||||||
keyCellMap.clear();
|
|
||||||
hasNotRequested.clear();
|
|
||||||
requested.clear();
|
|
||||||
drawable.clear();
|
|
||||||
undrawable.clear();
|
|
||||||
updateable.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the radius within which full-detail models are drawn
|
|
||||||
* @return the radius
|
|
||||||
*/
|
|
||||||
public double getDrawFullModelRadius(){
|
|
||||||
return drawFullModelRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the voxel texture atlas
|
|
||||||
*/
|
|
||||||
public void attachTextureAtlas(VoxelTextureAtlas voxelTextureAtlas){
|
|
||||||
atlas = voxelTextureAtlas;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the draw cell at the given world position has generated physics
|
|
||||||
* @param worldPos The world position
|
|
||||||
* @return true if has generated physics, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean generatedPhysics(Vector3i worldPos){
|
|
||||||
String key = this.getCellKey(worldPos.x, worldPos.y, worldPos.z);
|
|
||||||
if(!keyCellMap.containsKey(key)){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
DrawCell cell = this.keyCellMap.get(key);
|
|
||||||
return cell.modelEntity != null && PhysicsEntityUtils.getDBody(cell.modelEntity) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
transvoxel algorithm spacing management
|
|
||||||
|
|
||||||
want to specify a radius and have it be a circle so we're not wasting resolution on far off corners
|
|
||||||
To benefit from the LOD, should have a series of for loops
|
|
||||||
First loop iterates over (-largest radius) -> (largest radius) at an interval of the width of the lowest level of detail chunk
|
|
||||||
We check all 8 corners of the chunk to see if they all fall within the largest lod concentric circle
|
|
||||||
There are a few cases to consider
|
|
||||||
- All fall outside the largest LOD radius (ignore)
|
|
||||||
- All but one fall outside thel argest LOD radius (ignore)
|
|
||||||
- All fall within the largest LOD circle (make LOD chunk)
|
|
||||||
- One is within the next LOD radius, others are within the current LOD radius (ignore for this pass)
|
|
||||||
- All are within a lower LOD radius (ignore)
|
|
||||||
|
|
||||||
Once we're done with the largest LOD radius, we go to a higher resolution radius and iterate for smaller bounds
|
|
||||||
|
|
||||||
In a middle LOD level, if all corners are outside, but the lower resolution didn't
|
|
||||||
already pick it up, still need to generate a chunk at the current resolution
|
|
||||||
|
|
||||||
Detection of this case is tricky
|
|
||||||
We could snap the currently considered position to the nearest low-resolution chunk and then bounds check that low resolution chunk for every
|
|
||||||
higher resolution position we're considering
|
|
||||||
This is probably a bad idea
|
|
||||||
|
|
||||||
We could recurse every time a chunk doesn't work for the current level of detail, then evaluate it for a higher level of detail at this closer value
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
//the number of corners to consider for a valid chunk
|
|
||||||
static final int NUM_CORNERS_TO_CONSIDER = 8;
|
|
||||||
|
|
||||||
//offsets to get from current position to the actual corner to consider
|
|
||||||
int[] cornerOffsetsX = new int[]{0,1,0,1,0,1,0,1,};
|
|
||||||
int[] cornerOffsetsY = new int[]{0,0,0,0,1,1,1,1,};
|
|
||||||
int[] cornerOffsetsZ = new int[]{0,0,1,1,0,0,1,1,};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursive function that does chunk validity checking
|
|
||||||
* @param playerChunkPos
|
|
||||||
* @param minPoint
|
|
||||||
* @param maxPoint
|
|
||||||
* @param currentLOD
|
|
||||||
*/
|
|
||||||
public void assesChunkPositionsAtLOD(
|
|
||||||
Vector3i playerChunkPos,
|
|
||||||
Vector3i minPoint,
|
|
||||||
Vector3i maxPoint,
|
|
||||||
int currentLOD
|
|
||||||
){
|
|
||||||
Vector3i currentChunkPositionConsidered = new Vector3i(playerChunkPos); // The chunk position we're currently considering
|
|
||||||
double currentRadius = lodLevelRadiusTable[currentLOD];
|
|
||||||
double nextRadius = 0;
|
|
||||||
int increment = 1;
|
|
||||||
if(currentLOD > 0){
|
|
||||||
//only works when the LOD is greater than 0. When it's 0, there is no lower bound on chunk positions to generate
|
|
||||||
nextRadius = lodLevelRadiusTable[currentLOD-1];
|
|
||||||
increment = (int)Math.pow(2,currentLOD);
|
|
||||||
}
|
|
||||||
|
|
||||||
//iterate
|
|
||||||
for(int x = minPoint.x; x < maxPoint.x; x = x + increment){
|
|
||||||
for(int y = minPoint.y; y < maxPoint.x; y = y + increment){
|
|
||||||
for(int z = minPoint.z; z < maxPoint.x; z = z + increment){
|
|
||||||
//we have 8 corners to consider
|
|
||||||
//need to identify which case the current position is
|
|
||||||
int positionCase = 0;
|
|
||||||
for(int j = 0; j < NUM_CORNERS_TO_CONSIDER; j++){
|
|
||||||
currentChunkPositionConsidered.set(
|
|
||||||
x + cornerOffsetsX[j] * increment,
|
|
||||||
y + cornerOffsetsY[j] * increment,
|
|
||||||
z + cornerOffsetsZ[j] * increment
|
|
||||||
);
|
|
||||||
//figure out the case of this corner
|
|
||||||
double distance = currentChunkPositionConsidered.distance(playerChunkPos);
|
|
||||||
if(distance > currentRadius){
|
|
||||||
positionCase = 1;
|
|
||||||
break;
|
|
||||||
} else if(distance <= nextRadius){
|
|
||||||
positionCase = 2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch(positionCase){
|
|
||||||
case 0: {
|
|
||||||
//fully within current band, generate chunk
|
|
||||||
} break;
|
|
||||||
case 1: {
|
|
||||||
//partially outside bound, ignore
|
|
||||||
} break;
|
|
||||||
case 2: {
|
|
||||||
//partially inside higher resolution bound, recurse
|
|
||||||
assesChunkPositionsAtLOD(
|
|
||||||
playerChunkPos,
|
|
||||||
new Vector3i(x,y,z),
|
|
||||||
new Vector3i(x+increment,y+increment,z+increment),
|
|
||||||
currentLOD
|
|
||||||
);
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assess all LOD chunk levels
|
|
||||||
*/
|
|
||||||
public void assesLODChunks(){
|
|
||||||
|
|
||||||
//pre-existing values
|
|
||||||
Vector3d playerPosition = EntityUtils.getPosition(Globals.playerEntity);
|
|
||||||
Vector3i playerChunkPosition = Globals.clientWorldData.convertRealToWorldSpace(playerPosition);
|
|
||||||
|
|
||||||
//variables used while iterating across chunk positions
|
|
||||||
double currentRadius = 0; //the current radius is in units of chunks, even though it's a double (it's discrete, not real)
|
|
||||||
double nextRadius = 0; //the next radius is in units of chunks, even though it's a double (it's discrete, not real)
|
|
||||||
int increment = 1; //the increment is in units of chunks, not real
|
|
||||||
//the offsets from the player's position to
|
|
||||||
//the nearest possible spot we could place a chunk of the current LOD at (in units of chunks)
|
|
||||||
Vector3i lowerOffsets = new Vector3i(0,0,0);
|
|
||||||
Vector3i currentChunkPositionConsidered = new Vector3i(playerChunkPosition); // The chunk position we're currently considering
|
|
||||||
|
|
||||||
//actual logic to search for valid chunks
|
|
||||||
for(int i = NUMBER_OF_LOD_LEVELS - 1; i >= 0; i--){
|
|
||||||
currentRadius = lodLevelRadiusTable[i];
|
|
||||||
nextRadius = 0;
|
|
||||||
increment = 1;
|
|
||||||
if(i > 0){
|
|
||||||
//only works when the LOD is greater than 0. When it's 0, there is no lower bound on chunk positions to generate
|
|
||||||
nextRadius = lodLevelRadiusTable[i-1];
|
|
||||||
increment = (int)Math.pow(2,i);
|
|
||||||
}
|
|
||||||
//calculate offsets to get from player's current chunk position to the lower resolution grid for the current LOD
|
|
||||||
lowerOffsets.x = playerChunkPosition.x % ((int)increment);
|
|
||||||
lowerOffsets.y = playerChunkPosition.y % ((int)increment);
|
|
||||||
lowerOffsets.z = playerChunkPosition.z % ((int)increment);
|
|
||||||
|
|
||||||
//iterate
|
|
||||||
for(int x = -(int)currentRadius - lowerOffsets.x; x < currentRadius; x = x + increment){
|
|
||||||
for(int y = -(int)currentRadius - lowerOffsets.y; y < currentRadius; y = y + increment){
|
|
||||||
for(int z = -(int)currentRadius - lowerOffsets.z; z < currentRadius; z = z + increment){
|
|
||||||
//we have 8 corners to consider
|
|
||||||
//need to identify which case the current position is
|
|
||||||
int positionCase = 0;
|
|
||||||
for(int j = 0; j < NUM_CORNERS_TO_CONSIDER; j++){
|
|
||||||
currentChunkPositionConsidered.set(
|
|
||||||
x + cornerOffsetsX[j] * increment,
|
|
||||||
y + cornerOffsetsY[j] * increment,
|
|
||||||
z + cornerOffsetsZ[j] * increment
|
|
||||||
);
|
|
||||||
//figure out the case of this corner
|
|
||||||
double distance = currentChunkPositionConsidered.distance(playerChunkPosition);
|
|
||||||
if(distance > currentRadius){
|
|
||||||
positionCase = 1;
|
|
||||||
break;
|
|
||||||
} else if(distance <= nextRadius){
|
|
||||||
positionCase = 2;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch(positionCase){
|
|
||||||
case 0: {
|
|
||||||
//fully within current band, generate chunk
|
|
||||||
} break;
|
|
||||||
case 1: {
|
|
||||||
//partially outside bound, ignore
|
|
||||||
} break;
|
|
||||||
case 2: {
|
|
||||||
//partially inside higher resolution bound, recurse
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,245 +0,0 @@
|
|||||||
package electrosphere.renderer.meshgen;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utilities for generating and managing skyboxes
|
|
||||||
*/
|
|
||||||
public class SkyboxMeshgen {
|
|
||||||
|
|
||||||
|
|
||||||
// public static Model createSkyboxModel(Material optionalMaterial){
|
|
||||||
// Model skyboxModel = new Model();
|
|
||||||
// skyboxModel.meshes = new ArrayList<Mesh>();
|
|
||||||
|
|
||||||
// skyboxModel.modelMatrix = new Matrix4d();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// // boolean apply_lighting = false;
|
|
||||||
// // boolean has_bones = false;
|
|
||||||
|
|
||||||
// Mesh skyboxmesh = new Mesh("skybox"){
|
|
||||||
// @Override
|
|
||||||
// public void complexDraw(RenderPipelineState renderPipelineState){
|
|
||||||
// if(renderPipelineState.getUseMeshShader()){
|
|
||||||
// GL11.glDepthFunc(GL_LEQUAL);
|
|
||||||
// glUseProgram(shader.getShaderId());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if(renderPipelineState.getUseMaterial()){
|
|
||||||
// if(this.getMaterial() == null){
|
|
||||||
// Globals.materialDefault.apply_material(0,1);
|
|
||||||
// Iterator<Vector3f> colorIterator = Globals.skyboxColors.iterator();
|
|
||||||
// int counter = 0;
|
|
||||||
// float[] temp = new float[3];
|
|
||||||
// while(colorIterator.hasNext()){
|
|
||||||
// Vector3f colorCurrent = colorIterator.next();
|
|
||||||
// temp[0] = colorCurrent.x / 255.0f;
|
|
||||||
// temp[1] = colorCurrent.y / 255.0f;
|
|
||||||
// temp[2] = colorCurrent.z / 255.0f;
|
|
||||||
// // System.out.println("colors[" + counter + "] " + temp[0] + " " + temp[1] + " " + temp[2]);
|
|
||||||
// glUniform3fv(glGetUniformLocation(shader.getShaderId(), "colors[" + counter + "]"), temp);
|
|
||||||
// counter++;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// glBindVertexArray(vertexArrayObject);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if(renderPipelineState.getBufferStandardUniforms()){
|
|
||||||
// //buffer model/view/proj matrices
|
|
||||||
// glUniformMatrix4fv(shader.shaderVertexModelLoc, false, parent.modelMatrix.get(new float[16]));
|
|
||||||
// glUniformMatrix4fv(shader.shaderVertexViewLoc, false, new Matrix4f(Globals.viewMatrix).scale(100).get(new float[16]));
|
|
||||||
// glUniformMatrix4fv(shader.shaderVertexProjectionLoc, false, Globals.projectionMatrix.get(new float[16]));
|
|
||||||
// glUniform3fv(shader.shaderVertexViewPosLoc, CameraEntityUtils.getCameraEye(Globals.playerCamera).get(BufferUtils.createFloatBuffer(3)));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// GL11.glDrawElements(GL_TRIANGLES, elementCount, GL_UNSIGNED_INT, 0);
|
|
||||||
// glBindVertexArray(0);
|
|
||||||
|
|
||||||
// if(renderPipelineState.getUseMeshShader()){
|
|
||||||
// GL11.glDepthFunc(GL_LESS);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// //
|
|
||||||
// // VAO
|
|
||||||
// //
|
|
||||||
// skyboxmesh.generateVAO();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// float[] vertexcoords = {
|
|
||||||
// 100.0f, 100.0f, 100.0f,
|
|
||||||
// 100.0f, 100.0f,-100.0f,
|
|
||||||
// 100.0f,-100.0f, 100.0f,
|
|
||||||
// 100.0f,-100.0f,-100.0f,
|
|
||||||
// -100.0f, 100.0f, 100.0f,
|
|
||||||
// -100.0f, 100.0f,-100.0f,
|
|
||||||
// -100.0f,-100.0f, 100.0f,
|
|
||||||
// -100.0f,-100.0f,-100.0f,
|
|
||||||
|
|
||||||
// };
|
|
||||||
|
|
||||||
// //
|
|
||||||
// //Buffer data to GPU
|
|
||||||
// //
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// skyboxmesh.vertexCount = vertexcoords.length / 3;
|
|
||||||
// FloatBuffer VertexArrayBufferData = BufferUtils.createFloatBuffer(skyboxmesh.vertexCount * 3);
|
|
||||||
// float[] temp = new float[3];
|
|
||||||
// for (int i = 0; i < skyboxmesh.vertexCount; i++) {
|
|
||||||
// temp[0] = vertexcoords[i * 3 + 0];
|
|
||||||
// temp[1] = vertexcoords[i * 3 + 1];
|
|
||||||
// temp[2] = vertexcoords[i * 3 + 2];
|
|
||||||
// VertexArrayBufferData.put(temp);
|
|
||||||
// }
|
|
||||||
// VertexArrayBufferData.flip();
|
|
||||||
// skyboxmesh.buffer_vertices(VertexArrayBufferData, 3);
|
|
||||||
// } catch (NullPointerException ex){
|
|
||||||
// ex.printStackTrace();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// int[] facedata = {
|
|
||||||
// 0,1,4,
|
|
||||||
// 1,4,5,
|
|
||||||
// 1,3,5,
|
|
||||||
// 3,5,7,
|
|
||||||
// 4,5,7,
|
|
||||||
// 4,6,7,
|
|
||||||
|
|
||||||
// 0,2,4,
|
|
||||||
// 2,4,6,
|
|
||||||
// 0,1,2,
|
|
||||||
// 1,2,3,
|
|
||||||
|
|
||||||
// 2,3,6,
|
|
||||||
// 3,6,7,
|
|
||||||
|
|
||||||
// };
|
|
||||||
|
|
||||||
// //
|
|
||||||
// // FACES
|
|
||||||
// //
|
|
||||||
// skyboxmesh.faceCount = facedata.length / 3;
|
|
||||||
// skyboxmesh.elementCount = facedata.length;
|
|
||||||
// IntBuffer elementArrayBufferData = BufferUtils.createIntBuffer(skyboxmesh.elementCount);
|
|
||||||
// for(int i = 0; i < skyboxmesh.faceCount; i++){
|
|
||||||
// int[] temp = new int[3];
|
|
||||||
// temp[0] = facedata[i * 3 + 0];
|
|
||||||
// temp[1] = facedata[i * 3 + 1];
|
|
||||||
// temp[2] = facedata[i * 3 + 2];
|
|
||||||
// elementArrayBufferData.put(temp);
|
|
||||||
// }
|
|
||||||
// elementArrayBufferData.flip();
|
|
||||||
// skyboxmesh.buffer_faces(elementArrayBufferData);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// if(optionalMaterial != null){
|
|
||||||
// //
|
|
||||||
// // NORMALS
|
|
||||||
// //
|
|
||||||
// try {
|
|
||||||
// skyboxmesh.normalCount = vertexcoords.length / 3;
|
|
||||||
// FloatBuffer NormalArrayBufferData;
|
|
||||||
// if(skyboxmesh.normalCount > 0){
|
|
||||||
// NormalArrayBufferData = BufferUtils.createFloatBuffer(skyboxmesh.normalCount * 3);
|
|
||||||
// float[] temp = new float[3];
|
|
||||||
// for (int i = 0; i < skyboxmesh.normalCount; i++) {
|
|
||||||
// temp[0] = vertexcoords[i * 3 + 0];
|
|
||||||
// temp[1] = vertexcoords[i * 3 + 1];
|
|
||||||
// temp[2] = vertexcoords[i * 3 + 2];
|
|
||||||
// NormalArrayBufferData.put(temp);
|
|
||||||
// }
|
|
||||||
// NormalArrayBufferData.flip();
|
|
||||||
// skyboxmesh.buffer_normals(NormalArrayBufferData, 3);
|
|
||||||
// }
|
|
||||||
// } catch (NullPointerException ex){
|
|
||||||
// ex.printStackTrace();
|
|
||||||
// }
|
|
||||||
// //
|
|
||||||
// // TEXTURE COORDINATES
|
|
||||||
// //
|
|
||||||
// /*try {
|
|
||||||
// skyboxmesh.textureCoordCount = mesh.mTextureCoords(0).capacity();
|
|
||||||
// FloatBuffer TextureArrayBufferData;
|
|
||||||
// if(skyboxmesh.textureCoordCount > 0){
|
|
||||||
// TextureArrayBufferData = BufferUtils.createFloatBuffer(skyboxmesh.textureCoordCount * 2);
|
|
||||||
// float[] temp = new float[2];
|
|
||||||
// for (int i = 0; i < skyboxmesh.textureCoordCount; i++) {
|
|
||||||
// AIVector3D normal = texturecoords.get(i);
|
|
||||||
// temp[0] = normal.x();
|
|
||||||
// temp[1] = normal.y();
|
|
||||||
// // temp[2] = normal.z();
|
|
||||||
// TextureArrayBufferData.put(temp);
|
|
||||||
// }
|
|
||||||
// TextureArrayBufferData.flip();
|
|
||||||
// skyboxmesh.buffer_texture_coords(TextureArrayBufferData);
|
|
||||||
// }
|
|
||||||
// } catch (NullPointerException ex){
|
|
||||||
// ex.printStackTrace();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// skyboxmesh.shader = ShaderProgram.smart_assemble_shader(has_bones, apply_lighting);
|
|
||||||
|
|
||||||
// skybox_model.materials.add(optionalMaterial);
|
|
||||||
// */
|
|
||||||
// } else {
|
|
||||||
// skyboxmesh.shader = ShaderProgram.loadSpecificShader("/Shaders/skybox/VertexShaderNoTexture.vs", "/Shaders/skybox/FragmentShaderNoTexture.fs");
|
|
||||||
// try {
|
|
||||||
// FloatBuffer ColorArrayBufferData = BufferUtils.createFloatBuffer(skyboxmesh.vertexCount);
|
|
||||||
// for (int i = 0; i < skyboxmesh.vertexCount; i++) {
|
|
||||||
// ColorArrayBufferData.put(i);
|
|
||||||
// }
|
|
||||||
// ColorArrayBufferData.flip();
|
|
||||||
// int idBuffer = glGenBuffers();
|
|
||||||
// glBindBuffer(GL_ARRAY_BUFFER, idBuffer);
|
|
||||||
// GL15.glBufferData(GL_ARRAY_BUFFER, ColorArrayBufferData, GL_STATIC_DRAW);
|
|
||||||
// glVertexAttribPointer(1, 1, GL11.GL_FLOAT, false, 0, 0);
|
|
||||||
// glEnableVertexAttribArray(1);
|
|
||||||
// } catch (NullPointerException ex){
|
|
||||||
// ex.printStackTrace();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// skyboxmesh.setHasBones(false);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// glBindVertexArray(0);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// skyboxmesh.setParent(skyboxModel);
|
|
||||||
|
|
||||||
|
|
||||||
// skyboxModel.meshes.add(skyboxmesh);
|
|
||||||
|
|
||||||
|
|
||||||
// return skyboxModel;
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user