mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 17:28:57 +07:00
Flat Earth Hexagonal (#8140)
* Add new map shape: Flat Earth Hexagonal. * Make flat earth logic easier to read. * Make flat earth set temperature based on tile distance from center. * Make flat earth waste less space on the ice ring. * Add variety to the flat earth ice ring. * Use baseTerrain for snow on flat earth edge instead of addTerrainFeature. * Ensure flat earth center tiles and edge tiles are ocean not coast. * Give flat earth ice ring some random juts and dips. * Make 3rd continent smaller when flat earth. * Enable more continent positions for three continents when flat earth. * Add parens around or statement. * Refactor flat earth temperature code into functions. * Invert some flat earth if statements to reduce nesting. * Allow coast near flat earth ice walls. * Make flat earth ice wall generation more efficient. * Stop adding ice to flat earth mountains. * Move flat earth ice wall generation to its own function. * Improve flat earth water placement comments. * Move flat earth extra water generation to its own function. * Move flat earth center ice spawn to its own function. * Move flat earth edge ice spawn to its own function. * Minor efficiency tweak. * Add Flat Earth Hexagonal to template.
This commit is contained in:
@ -377,6 +377,7 @@ Time =
|
||||
|
||||
Map Shape =
|
||||
Hexagonal =
|
||||
Flat Earth Hexagonal =
|
||||
Rectangular =
|
||||
Height =
|
||||
Width =
|
||||
|
@ -124,6 +124,7 @@ class MapSizeNew : IsPartOfGameInfoSerialization {
|
||||
|
||||
object MapShape : IsPartOfGameInfoSerialization {
|
||||
const val hexagonal = "Hexagonal"
|
||||
const val flatEarth = "Flat Earth Hexagonal"
|
||||
const val rectangular = "Rectangular"
|
||||
}
|
||||
|
||||
@ -229,12 +230,12 @@ class MapParameters : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
fun getArea() = when {
|
||||
shape == MapShape.hexagonal -> getNumberOfTilesInHexagon(mapSize.radius)
|
||||
shape == MapShape.hexagonal || shape == MapShape.flatEarth -> getNumberOfTilesInHexagon(mapSize.radius)
|
||||
worldWrap && mapSize.width % 2 != 0 -> (mapSize.width - 1) * mapSize.height
|
||||
else -> mapSize.width * mapSize.height
|
||||
}
|
||||
fun displayMapDimensions() = mapSize.run {
|
||||
(if (shape == MapShape.hexagonal) "R$radius" else "${width}x$height") +
|
||||
(if (shape == MapShape.hexagonal || shape == MapShape.flatEarth) "R$radius" else "${width}x$height") +
|
||||
(if (worldWrap) "w" else "")
|
||||
}
|
||||
|
||||
@ -266,7 +267,7 @@ class MapParameters : IsPartOfGameInfoSerialization {
|
||||
}.joinToString("")
|
||||
|
||||
fun numberOfTiles() =
|
||||
if (shape == MapShape.hexagonal) {
|
||||
if (shape == MapShape.hexagonal || shape == MapShape.flatEarth) {
|
||||
1 + 3 * mapSize.radius * (mapSize.radius - 1)
|
||||
} else {
|
||||
mapSize.width * mapSize.height
|
||||
|
@ -26,6 +26,7 @@ import kotlin.math.max
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.math.ulp
|
||||
import kotlin.random.Random
|
||||
|
||||
@ -35,6 +36,14 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
private const val consoleTimings = false
|
||||
}
|
||||
|
||||
private val landTerrainName =
|
||||
MapLandmassGenerator.getInitializationTerrain(ruleset, TerrainType.Land)
|
||||
private val waterTerrainName: String = try {
|
||||
MapLandmassGenerator.getInitializationTerrain(ruleset, TerrainType.Water)
|
||||
} catch (_: Exception) {
|
||||
landTerrainName
|
||||
}
|
||||
|
||||
private var randomness = MapGenerationRandomness()
|
||||
private val firstLandTerrain = ruleset.terrains.values.first { it.type==TerrainType.Land }
|
||||
|
||||
@ -503,9 +512,19 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
val humidityRandom = randomness.getPerlinNoise(tile, humiditySeed, scale = scale, nOctaves = 1)
|
||||
val humidity = ((humidityRandom + 1.0) / 2.0 + humidityShift).coerceIn(0.0..1.0)
|
||||
|
||||
val randomTemperature = randomness.getPerlinNoise(tile, temperatureSeed, scale = scale, nOctaves = 1)
|
||||
val expectedTemperature = if (tileMap.mapParameters.shape === MapShape.flatEarth) {
|
||||
// Flat Earth uses radius because North is center of map
|
||||
val radius = getTileRadius(tile, tileMap)
|
||||
val radiusTemperature = getTemperatureAtRadius(radius)
|
||||
radiusTemperature
|
||||
} else {
|
||||
// Globe Earth uses latitude because North is top of map
|
||||
val latitudeTemperature = 1.0 - 2.0 * abs(tile.latitude) / tileMap.maxLatitude
|
||||
var temperature = (5.0 * latitudeTemperature + randomTemperature) / 6.0
|
||||
latitudeTemperature
|
||||
}
|
||||
|
||||
val randomTemperature = randomness.getPerlinNoise(tile, temperatureSeed, scale = scale, nOctaves = 1)
|
||||
var temperature = (5.0 * expectedTemperature + randomTemperature) / 6.0
|
||||
temperature = abs(temperature).pow(1.0 - temperatureExtremeness) * temperature.sign
|
||||
temperature = (temperature + temperatureShift).coerceIn(-1.0..1.0)
|
||||
|
||||
@ -538,6 +557,65 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTileRadius(tile: TileInfo, tileMap: TileMap): Float {
|
||||
val latitudeRatio = abs(tile.latitude) / tileMap.maxLatitude
|
||||
val longitudeRatio = abs(tile.longitude) / tileMap.maxLongitude
|
||||
return sqrt(latitudeRatio.pow(2) + longitudeRatio.pow(2))
|
||||
}
|
||||
|
||||
private fun getTemperatureAtRadius(radius: Float): Double {
|
||||
/*
|
||||
Radius is in the range of 0.0 to 1.0
|
||||
Temperature is in the range of -1.0 to 1.0
|
||||
|
||||
Radius of 0.0 (arctic) is -1.0 (cold)
|
||||
Radius of 0.25 (mid North) is 0.0 (temperate)
|
||||
Radius of 0.5 (equator) is 1.0 (hot)
|
||||
Radius of 0.75 (mid South) is 0.0 (temperate)
|
||||
Radius of 1.0 (antarctic) is -1.0 (cold)
|
||||
|
||||
Scale the radius range to the temperature range
|
||||
*/
|
||||
return when {
|
||||
/*
|
||||
North Zone
|
||||
Starts cold at arctic and gets hotter as it goes South to equator
|
||||
x1 is set to 0.05 instead of 0.0 to offset the ice in the center of the map
|
||||
*/
|
||||
radius < 0.5 -> scaleToRange(0.05, 0.5, -1.0, 1.0, radius)
|
||||
|
||||
/*
|
||||
South Zone
|
||||
Starts hot at equator and gets colder as it goes South to antarctic
|
||||
x2 is set to 0.95 instead of 1.0 to offset the ice on the edges of the map
|
||||
*/
|
||||
radius > 0.5 -> scaleToRange(0.5, 0.95, 1.0, -1.0, radius)
|
||||
|
||||
/*
|
||||
Equator
|
||||
Always hot
|
||||
radius == 0.5
|
||||
*/
|
||||
else -> 1.0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @x1 start of the original range
|
||||
* @x2 end of the original range
|
||||
* @y1 start of the new range
|
||||
* @y2 end of the new range
|
||||
* @value value to be scaled from the original range to the new range
|
||||
*
|
||||
* @returns value in new scale
|
||||
* special thanks to @letstalkaboutdune for the math
|
||||
*/
|
||||
private fun scaleToRange(x1: Double, x2: Double, y1: Double, y2: Double, value: Float): Double {
|
||||
val gain = (y2 - y1) / (x2 - x1)
|
||||
val offset = y2 - (gain * x2)
|
||||
return (gain * value) + offset
|
||||
}
|
||||
|
||||
/**
|
||||
* [MapParameters.vegetationRichness] is the threshold for vegetation spawn
|
||||
*/
|
||||
@ -596,6 +674,11 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
-1f, ruleset.modOptions.constants.spawnIceBelowTemperature,
|
||||
0f, 1f))
|
||||
}.toList()
|
||||
|
||||
if (tileMap.mapParameters.shape === MapShape.flatEarth) {
|
||||
spawnFlatEarthIceWalls(tileMap, iceEquivalents)
|
||||
}
|
||||
|
||||
if (iceEquivalents.isEmpty()) return
|
||||
|
||||
tileMap.setTransients(ruleset)
|
||||
@ -621,6 +704,145 @@ class MapGenerator(val ruleset: Ruleset) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawnFlatEarthIceWalls(tileMap: TileMap, iceEquivalents: List<TerrainOccursRange>) {
|
||||
val iceCandidates = iceEquivalents.filter {
|
||||
it.matches(-1.0, 1.0)
|
||||
}.map {
|
||||
it.terrain.name
|
||||
}
|
||||
val iceTerrainName =
|
||||
when (iceCandidates.size) {
|
||||
1 -> iceCandidates.first()
|
||||
!in 0..1 -> iceCandidates.random(randomness.RNG)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val snowCandidates = ruleset.terrains.values.asSequence().filter {
|
||||
it.type == TerrainType.Land
|
||||
}.flatMap { terrain ->
|
||||
val conditions = terrain.getGenerationConditions()
|
||||
if (conditions.any()) conditions
|
||||
else sequenceOf(
|
||||
TerrainOccursRange(
|
||||
terrain,
|
||||
-1f, ruleset.modOptions.constants.spawnIceBelowTemperature,
|
||||
0f, 1f
|
||||
)
|
||||
)
|
||||
}.toList().filter {
|
||||
it.matches(-1.0, 1.0)
|
||||
}.map {
|
||||
it.terrain.name
|
||||
}
|
||||
val snowTerrainName =
|
||||
when (snowCandidates.size) {
|
||||
1 -> snowCandidates.first()
|
||||
!in 0..1 -> snowCandidates.random(randomness.RNG)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val mountainTerrainName =
|
||||
ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInChains) }?.name
|
||||
|
||||
val bestArcticTileName = when {
|
||||
iceTerrainName != null -> iceTerrainName
|
||||
snowTerrainName != null -> snowTerrainName
|
||||
mountainTerrainName != null -> mountainTerrainName
|
||||
else -> null
|
||||
}
|
||||
|
||||
val arcticTileNameList =
|
||||
arrayOf(iceTerrainName, snowTerrainName, mountainTerrainName).filterNotNull()
|
||||
|
||||
// Skip the tile loop if nothing can be done in it
|
||||
if (bestArcticTileName == null && arcticTileNameList.isEmpty()) return
|
||||
|
||||
// Flat Earth needs a 1 tile wide perimeter of ice/mountain/snow and a 2 radius cluster of ice in the center.
|
||||
for (tile in tileMap.values) {
|
||||
val isCenterTile = tile.latitude == 0f && tile.longitude == 0f
|
||||
val isEdgeTile = tile.neighbors.count() < 6
|
||||
|
||||
// Make center tiles ice or snow or mountain depending on availability
|
||||
if (isCenterTile && bestArcticTileName != null) {
|
||||
spawnFlatEarthCenterIceWall(tile, bestArcticTileName, iceTerrainName)
|
||||
}
|
||||
|
||||
// Make edge tiles randomly ice or snow or mountain if available
|
||||
if (isEdgeTile && arcticTileNameList.isNotEmpty()) {
|
||||
spawnFlatEarthEdgeIceWall(tile, arcticTileNameList, iceTerrainName, mountainTerrainName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawnFlatEarthCenterIceWall(tile: TileInfo, bestArcticTileName: String, iceTerrainName: String?) {
|
||||
// Spawn ice on center tile
|
||||
if (bestArcticTileName == iceTerrainName) {
|
||||
tile.baseTerrain = waterTerrainName
|
||||
tile.addTerrainFeature(iceTerrainName)
|
||||
} else {
|
||||
tile.baseTerrain = bestArcticTileName
|
||||
}
|
||||
|
||||
// Spawn circle of ice around center tile
|
||||
for (neighbor in tile.neighbors) {
|
||||
if (bestArcticTileName == iceTerrainName) {
|
||||
neighbor.baseTerrain = waterTerrainName
|
||||
neighbor.addTerrainFeature(iceTerrainName)
|
||||
} else {
|
||||
neighbor.baseTerrain = bestArcticTileName
|
||||
}
|
||||
|
||||
// Spawn partial circle of ice around circle of ice
|
||||
for (neighbor2 in neighbor.neighbors) {
|
||||
if (randomness.RNG.nextDouble() < 0.75) {
|
||||
// Do nothing most of the time at random.
|
||||
} else if (bestArcticTileName == iceTerrainName) {
|
||||
neighbor2.baseTerrain = waterTerrainName
|
||||
neighbor2.addTerrainFeature(iceTerrainName)
|
||||
} else {
|
||||
neighbor2.baseTerrain = bestArcticTileName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawnFlatEarthEdgeIceWall(tile: TileInfo, arcticTileNameList: List<String>, iceTerrainName: String?, mountainTerrainName: String?) {
|
||||
// Select one of the arctic tiles at random
|
||||
val arcticTileName = when (arcticTileNameList.size) {
|
||||
1 -> arcticTileNameList.first()
|
||||
else -> arcticTileNameList.random(randomness.RNG)
|
||||
}
|
||||
|
||||
// Spawn arctic tiles on edge tile
|
||||
if (arcticTileName == iceTerrainName) {
|
||||
tile.baseTerrain = waterTerrainName
|
||||
tile.addTerrainFeature(iceTerrainName)
|
||||
} else if (iceTerrainName != null && arcticTileName != mountainTerrainName) {
|
||||
tile.baseTerrain = arcticTileName
|
||||
tile.addTerrainFeature(iceTerrainName)
|
||||
} else {
|
||||
tile.baseTerrain = arcticTileName
|
||||
}
|
||||
|
||||
// Spawn partial circle of arctic tiles next to the edge
|
||||
for (neighbor in tile.neighbors) {
|
||||
val neighborIsEdgeTile = neighbor.neighbors.count() < 6
|
||||
if (neighborIsEdgeTile) {
|
||||
// Do not redo edge tile. It is already done.
|
||||
} else if (randomness.RNG.nextDouble() < 0.75) {
|
||||
// Do nothing most of the time at random.
|
||||
} else if (arcticTileName == iceTerrainName) {
|
||||
neighbor.baseTerrain = waterTerrainName
|
||||
neighbor.addTerrainFeature(iceTerrainName)
|
||||
} else if (iceTerrainName != null && arcticTileName != mountainTerrainName) {
|
||||
neighbor.baseTerrain = arcticTileName
|
||||
neighbor.addTerrainFeature(iceTerrainName)
|
||||
} else {
|
||||
neighbor.baseTerrain = arcticTileName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MapGenerationRandomness {
|
||||
|
@ -48,6 +48,36 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
|
||||
MapType.archipelago -> createArchipelago(tileMap)
|
||||
MapType.default -> createPerlin(tileMap)
|
||||
}
|
||||
|
||||
if (tileMap.mapParameters.shape === MapShape.flatEarth) {
|
||||
generateFlatEarthExtraWater(tileMap)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateFlatEarthExtraWater(tileMap: TileMap) {
|
||||
for (tile in tileMap.values) {
|
||||
val isCenterTile = tile.latitude == 0f && tile.longitude == 0f
|
||||
val isEdgeTile = tile.neighbors.count() < 6
|
||||
|
||||
if (!isCenterTile && !isEdgeTile) continue
|
||||
|
||||
/*
|
||||
Flat Earth needs a 3 tile wide water perimeter and a 4 tile radius water center.
|
||||
This helps map generators to not place important things there which would be destroyed
|
||||
when the ice walls are placed there.
|
||||
*/
|
||||
tile.baseTerrain = waterTerrainName
|
||||
for (neighbor in tile.neighbors) {
|
||||
neighbor.baseTerrain = waterTerrainName
|
||||
for (neighbor2 in neighbor.neighbors) {
|
||||
neighbor2.baseTerrain = waterTerrainName
|
||||
if (!isCenterTile) continue
|
||||
for (neighbor3 in neighbor2.neighbors) {
|
||||
neighbor3.baseTerrain = waterTerrainName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun spawnLandOrWater(tile: TileInfo, elevation: Double) {
|
||||
@ -109,7 +139,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
|
||||
|
||||
private fun createTwoContinents(tileMap: TileMap) {
|
||||
val isLatitude =
|
||||
if (tileMap.mapParameters.shape === MapShape.hexagonal) randomness.RNG.nextDouble() > 0.5f
|
||||
if (tileMap.mapParameters.shape === MapShape.hexagonal || tileMap.mapParameters.shape === MapShape.flatEarth) randomness.RNG.nextDouble() > 0.5f
|
||||
else if (tileMap.mapParameters.mapSize.height > tileMap.mapParameters.mapSize.width) true
|
||||
else if (tileMap.mapParameters.mapSize.width > tileMap.mapParameters.mapSize.height) false
|
||||
else randomness.RNG.nextDouble() > 0.5f
|
||||
@ -123,12 +153,14 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
|
||||
}
|
||||
|
||||
private fun createThreeContinents(tileMap: TileMap) {
|
||||
val isNorth = randomness.RNG.nextDouble() < 0.5f
|
||||
val isNorth = randomness.RNG.nextDouble() < 0.5
|
||||
// On flat earth maps we can randomly do East or West instead of North or South
|
||||
val isEastWest = tileMap.mapParameters.shape === MapShape.flatEarth && randomness.RNG.nextDouble() > 0.5
|
||||
|
||||
val elevationSeed = randomness.RNG.nextInt().toDouble()
|
||||
for (tile in tileMap.values) {
|
||||
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
|
||||
elevation = (elevation + getThreeContinentsTransform(tile, tileMap, isNorth)) / 2.0
|
||||
elevation = (elevation + getThreeContinentsTransform(tile, tileMap, isNorth, isEastWest)) / 2.0
|
||||
spawnLandOrWater(tile, elevation)
|
||||
}
|
||||
}
|
||||
@ -180,16 +212,25 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
|
||||
return min(0.2, -1.0 + (5.0 * factor.pow(0.6f) + randomScale) / 3.0)
|
||||
}
|
||||
|
||||
private fun getThreeContinentsTransform(tileInfo: TileInfo, tileMap: TileMap, isNorth: Boolean): Double {
|
||||
private fun getThreeContinentsTransform(tileInfo: TileInfo, tileMap: TileMap, isNorth: Boolean, isEastWest: Boolean): Double {
|
||||
// The idea here is to create a water area separating the two four areas.
|
||||
// So what we do it create a line of water in the middle - where longitude is close to 0.
|
||||
val randomScale = randomness.RNG.nextDouble()
|
||||
var longitudeFactor = abs(tileInfo.longitude) / tileMap.maxLongitude
|
||||
var latitudeFactor = abs(tileInfo.latitude) / tileMap.maxLatitude
|
||||
|
||||
// 3rd continent should use only half the map width, or if flat earth, only a third
|
||||
val sizeReductionFactor = if (tileMap.mapParameters.shape === MapShape.flatEarth) 3f else 2f
|
||||
|
||||
// We then pick one side to be merged into one centered continent instead of two cornered.
|
||||
if (isEastWest) {
|
||||
// In EastWest mode North represents West
|
||||
if (isNorth && tileInfo.longitude < 0 || !isNorth && tileInfo.longitude > 0)
|
||||
latitudeFactor = max(0f, tileMap.maxLatitude - abs(tileInfo.latitude * sizeReductionFactor)) / tileMap.maxLatitude
|
||||
} else {
|
||||
if (isNorth && tileInfo.latitude < 0 || !isNorth && tileInfo.latitude > 0)
|
||||
longitudeFactor = max(0f, tileMap.maxLongitude - abs(tileInfo.longitude * 2f)) / tileMap.maxLongitude
|
||||
longitudeFactor = max(0f, tileMap.maxLongitude - abs(tileInfo.longitude * sizeReductionFactor)) / tileMap.maxLongitude
|
||||
}
|
||||
|
||||
// If this is a world wrap, we want it to be separated on both sides -
|
||||
// so we make the actual strip of water thinner, but we put it both in the middle of the map and on the edges of the map
|
||||
|
@ -76,7 +76,7 @@ class MapRegions (val ruleset: Ruleset){
|
||||
val totalLand = tileMap.continentSizes.values.sum().toFloat()
|
||||
val largestContinent = tileMap.continentSizes.values.maxOf { it }.toFloat()
|
||||
|
||||
val radius = if (tileMap.mapParameters.shape == MapShape.hexagonal)
|
||||
val radius = if (tileMap.mapParameters.shape == MapShape.hexagonal || tileMap.mapParameters.shape == MapShape.flatEarth)
|
||||
tileMap.mapParameters.mapSize.radius.toFloat()
|
||||
else
|
||||
(max(tileMap.mapParameters.mapSize.width / 2, tileMap.mapParameters.mapSize.height / 2)).toFloat()
|
||||
|
@ -230,7 +230,7 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
|
||||
ToastPopup(message, this@MapEditorScreen, 4000L )
|
||||
}
|
||||
|
||||
if (params.shape == MapShape.hexagonal) {
|
||||
if (params.shape == MapShape.hexagonal || params.shape == MapShape.flatEarth) {
|
||||
params.mapSize = MapSizeNew(HexMath.getHexagonalRadiusForArea(areaFromTiles).toInt())
|
||||
return
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ class MapParametersTable(
|
||||
private fun addMapShapeSelectBox() {
|
||||
val mapShapes = listOfNotNull(
|
||||
MapShape.hexagonal,
|
||||
MapShape.flatEarth,
|
||||
MapShape.rectangular
|
||||
)
|
||||
val mapShapeSelectBox =
|
||||
@ -176,7 +177,7 @@ class MapParametersTable(
|
||||
private fun updateWorldSizeTable() {
|
||||
customWorldSizeTable.clear()
|
||||
|
||||
if (mapParameters.shape == MapShape.hexagonal && worldSizeSelectBox.selected.value == MapSize.custom)
|
||||
if ((mapParameters.shape == MapShape.hexagonal || mapParameters.shape == MapShape.flatEarth) && worldSizeSelectBox.selected.value == MapSize.custom)
|
||||
customWorldSizeTable.add(hexagonalSizeTable).grow().row()
|
||||
else if (mapParameters.shape == MapShape.rectangular && worldSizeSelectBox.selected.value == MapSize.custom)
|
||||
customWorldSizeTable.add(rectangularSizeTable).grow().row()
|
||||
|
Reference in New Issue
Block a user