added emitter debug tools. More testing required before 1.0!
This commit is contained in:
@@ -53,10 +53,6 @@ import kotlin.reflect.full.declaredMemberProperties
|
|||||||
// Custom curve equation command args (parse from strings).
|
// Custom curve equation command args (parse from strings).
|
||||||
// Force field "config" option.
|
// Force field "config" option.
|
||||||
|
|
||||||
// TODO: (now)
|
|
||||||
// Better particle debug tools.
|
|
||||||
// Use more BDEs instead of particles, and show more values than origin and velocity.
|
|
||||||
|
|
||||||
|
|
||||||
var ALL_PARTICLE_EMITTERS: Array<ParticleEmitter> = emptyArray()
|
var ALL_PARTICLE_EMITTERS: Array<ParticleEmitter> = emptyArray()
|
||||||
var LIVING_PARTICLE_COUNT: Int = 0
|
var LIVING_PARTICLE_COUNT: Int = 0
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.bytedice.bde_particles.particles
|
|||||||
import com.bytedice.bde_particles.Billboard
|
import com.bytedice.bde_particles.Billboard
|
||||||
import com.bytedice.bde_particles.DisplayEntity
|
import com.bytedice.bde_particles.DisplayEntity
|
||||||
import com.bytedice.bde_particles.DisplayEntityProperties
|
import com.bytedice.bde_particles.DisplayEntityProperties
|
||||||
|
import com.bytedice.bde_particles.SESSION_UUID
|
||||||
import net.minecraft.server.world.ServerWorld
|
import net.minecraft.server.world.ServerWorld
|
||||||
import net.minecraft.util.math.Vec3d
|
import net.minecraft.util.math.Vec3d
|
||||||
import org.joml.Vector2f
|
import org.joml.Vector2f
|
||||||
@@ -14,26 +15,31 @@ class EmitterDebug(
|
|||||||
eOrigin: Vec3d,
|
eOrigin: Vec3d,
|
||||||
spawnPosOffset: Vector3f,
|
spawnPosOffset: Vector3f,
|
||||||
shape: SpawningShape,
|
shape: SpawningShape,
|
||||||
forceFields: ParamClasses.ForceFieldArray
|
|
||||||
) {
|
) {
|
||||||
private val eOrigin = EOrigin(rot, eOrigin)
|
private val eOriginDebug = EOrigin(rot, eOrigin)
|
||||||
private val shapeDebug = ShapeDebug(rot, eOrigin, spawnPosOffset, shape)
|
private val shapeDebug = ShapeDebug(eOrigin, spawnPosOffset, shape)
|
||||||
|
private val forceFieldDebug = ForceFieldDebug()
|
||||||
|
|
||||||
fun spawn(world: ServerWorld) {
|
fun spawn(world: ServerWorld, eOrigin: Vec3d, forceFields: ParamClasses.ForceFieldArray, spawnPosOffset: Vector3f) {
|
||||||
eOrigin.spawn(world)
|
eOriginDebug.spawn(world)
|
||||||
shapeDebug.spawn(world)
|
shapeDebug.spawn(world)
|
||||||
|
forceFieldDebug.makeArray(eOrigin, forceFields, spawnPosOffset)
|
||||||
|
forceFieldDebug.spawn(world)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
rot: Vector2f,
|
rot: Vector2f,
|
||||||
eOrigin: Vec3d,
|
eOrigin: Vec3d,
|
||||||
pOrigin: Vector3f,
|
spawnPosOffset: Vector3f
|
||||||
pVel: Vector3f,
|
|
||||||
pScale: Vector3f,
|
|
||||||
spawnPosOffset: Vector3f,
|
|
||||||
shape: SpawningShape,
|
|
||||||
) {
|
) {
|
||||||
|
eOriginDebug.update(rot, eOrigin)
|
||||||
|
shapeDebug.update(rot, eOrigin, spawnPosOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun kill() {
|
||||||
|
eOriginDebug.kill()
|
||||||
|
shapeDebug.kill()
|
||||||
|
forceFieldDebug.kill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +50,10 @@ class EOrigin(rot: Vector2f, eOrigin: Vec3d) {
|
|||||||
pos = eOrigin,
|
pos = eOrigin,
|
||||||
rot = rot,
|
rot = rot,
|
||||||
model = "minecraft:red_concrete",
|
model = "minecraft:red_concrete",
|
||||||
tags = arrayOf("DEBUG", "E_DEBUG", "eOrigin_DEBUG"),
|
tags = arrayOf("DEBUG", "E_DEBUG", "eOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
|
||||||
brightnessOverride = 15,
|
brightnessOverride = 15,
|
||||||
scale = Vector3f(0.25f, 0.25f, 0.25f)
|
scale = Vector3f(0.25f, 0.25f, 0.25f)
|
||||||
)
|
))
|
||||||
)
|
|
||||||
|
|
||||||
fun spawn(world: ServerWorld) {
|
fun spawn(world: ServerWorld) {
|
||||||
eOriginDE.spawn(world)
|
eOriginDE.spawn(world)
|
||||||
@@ -61,36 +66,52 @@ class EOrigin(rot: Vector2f, eOrigin: Vec3d) {
|
|||||||
|
|
||||||
eOriginDE.updateProperties(newProperties!!)
|
eOriginDE.updateProperties(newProperties!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun kill() {
|
||||||
|
eOriginDE.kill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShapeDebug(rot: Vector2f, eOrigin: Vec3d, spawnPosOffset: Vector3f, shape: SpawningShape) {
|
class ShapeDebug(eOrigin: Vec3d, spawnPosOffset: Vector3f, shape: SpawningShape) {
|
||||||
val shapeScale: Vector3f? = when (shape) {
|
val shapeScale: Vector3f? = when (shape) {
|
||||||
is SpawningShape.Circle -> Vector3f(shape.radius * 1.75f, shape.radius * 1.75f, 0.0f)
|
is SpawningShape.Circle -> Vector3f(shape.radius * 1.75f * 2, shape.radius * 1.75f * 2, 0.0001f)
|
||||||
is SpawningShape.Sphere -> Vector3f(shape.radius * 1.75f, shape.radius * 1.75f, 0.0f)
|
is SpawningShape.Sphere -> Vector3f(shape.radius * 1.75f * 2, shape.radius * 1.75f * 2, 0.0001f)
|
||||||
is SpawningShape.Rect -> Vector3f(shape.size.x, 0.0f, shape.size.y)
|
is SpawningShape.Rect -> Vector3f(shape.size.x, 0.0001f, shape.size.y)
|
||||||
is SpawningShape.Cube -> Vector3f(shape.size.x, shape.size.y, shape.size.z)
|
is SpawningShape.Cube -> Vector3f(shape.size.x, shape.size.y, shape.size.z)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
val shapeRot = if (shape is SpawningShape.Circle) { Vector4f(-0.707f, 0.0f, 0.0f, 0.707f) }
|
val shapeRot = when (shape) {
|
||||||
else { Vector4f(0.0f, 0.0f, 0.0f, 1.0f) }
|
is SpawningShape.Circle -> Vector4f(-0.707f, 0.0f, 0.0f, 0.707f)
|
||||||
|
else -> Vector4f(0.0f, 0.0f, 0.0f, 1.0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun determineModel(shape: SpawningShape): String =
|
||||||
|
when (shape) {
|
||||||
|
is SpawningShape.Circle, is SpawningShape.Sphere -> "minecraft:sunflower"
|
||||||
|
else -> "minecraft:yellow_stained_glass"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun determineBillboard(shape: SpawningShape): String =
|
||||||
|
if (shape !is SpawningShape.Sphere) Billboard.FIXED else Billboard.CENTER
|
||||||
|
|
||||||
val shapeDE: DisplayEntity? = if (shape !is SpawningShape.Point) {
|
val shapeDE: DisplayEntity? = if (shape !is SpawningShape.Point) {
|
||||||
|
val position = eOrigin.add(
|
||||||
|
spawnPosOffset.x.toDouble(),
|
||||||
|
spawnPosOffset.y.toDouble(),
|
||||||
|
spawnPosOffset.z.toDouble()
|
||||||
|
)
|
||||||
|
|
||||||
DisplayEntity(DisplayEntityProperties(
|
DisplayEntity(DisplayEntityProperties(
|
||||||
pos = eOrigin.add(spawnPosOffset.x.toDouble(), spawnPosOffset.y.toDouble(), spawnPosOffset.z.toDouble()),
|
pos = position.add(spawnPosOffset.x.toDouble(), spawnPosOffset.y.toDouble(), spawnPosOffset.z.toDouble()),
|
||||||
rot = rot,
|
model = determineModel(shape),
|
||||||
model = if (shape is SpawningShape.Circle || shape is SpawningShape.Sphere) { "minecraft:sunflower" }
|
tags = arrayOf("DEBUG", "P_DEBUG", "shape_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
|
||||||
else { "minecraft:yellow_stained_glass" },
|
|
||||||
tags = arrayOf("DEBUG", "P_DEBUG", "shape_DEBUG"),
|
|
||||||
brightnessOverride = 15,
|
brightnessOverride = 15,
|
||||||
billboard = if (shape !is SpawningShape.Sphere) { Billboard.FIXED } else { Billboard.CENTER },
|
billboard = determineBillboard(shape),
|
||||||
translation = spawnPosOffset,
|
|
||||||
leftRotation = shapeRot,
|
leftRotation = shapeRot,
|
||||||
scale = shapeScale!!
|
scale = shapeScale ?: Vector3f(1f, 1f, 1f)
|
||||||
))
|
))
|
||||||
}
|
} else null
|
||||||
else { null }
|
|
||||||
|
|
||||||
fun spawn(world: ServerWorld) {
|
fun spawn(world: ServerWorld) {
|
||||||
shapeDE?.spawn(world)
|
shapeDE?.spawn(world)
|
||||||
@@ -104,4 +125,43 @@ class ShapeDebug(rot: Vector2f, eOrigin: Vec3d, spawnPosOffset: Vector3f, shape:
|
|||||||
|
|
||||||
shapeDE?.updateProperties(newProperties!!)
|
shapeDE?.updateProperties(newProperties!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun kill() {
|
||||||
|
shapeDE?.kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ForceFieldDebug {
|
||||||
|
var FFDEArray: Array<DisplayEntity> = emptyArray()
|
||||||
|
|
||||||
|
fun makeArray(eOrigin: Vec3d, forceFieldArray: ParamClasses.ForceFieldArray, spawnPosOffset: Vector3f) {
|
||||||
|
forceFieldArray.array.forEach {
|
||||||
|
val shapeScale = when (it.shape) {
|
||||||
|
is ForceFieldShape.Sphere -> Vector3f(1.333f * it.shape.radius * 2, 1.333f * it.shape.radius * 2, 0.0001f)
|
||||||
|
is ForceFieldShape.Cube -> it.shape.size
|
||||||
|
}
|
||||||
|
|
||||||
|
val newOffset = Vector3f(spawnPosOffset).add(it.pos)
|
||||||
|
|
||||||
|
val displayEntity = DisplayEntity(
|
||||||
|
DisplayEntityProperties(
|
||||||
|
pos = eOrigin.add(newOffset.x.toDouble(), newOffset.y.toDouble(), newOffset.z.toDouble()),
|
||||||
|
model = if (it.shape is ForceFieldShape.Sphere) { "minecraft:snowball" } else { "minecraft:white_stained_glass" },
|
||||||
|
tags = arrayOf("DEBUG", "E_DEBUG", "eOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
|
||||||
|
brightnessOverride = 15,
|
||||||
|
billboard = if (it.shape is ForceFieldShape.Sphere) { Billboard.CENTER } else { Billboard.FIXED },
|
||||||
|
scale = shapeScale
|
||||||
|
))
|
||||||
|
|
||||||
|
FFDEArray += displayEntity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun spawn(world: ServerWorld) {
|
||||||
|
FFDEArray.forEach { it.spawn(world) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun kill() {
|
||||||
|
FFDEArray.forEach { it.kill() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ data class EmitterParams (
|
|||||||
ForceFieldShape.Sphere(15.0f, Pair(0.1f, 0.2f))
|
ForceFieldShape.Sphere(15.0f, Pair(0.1f, 0.2f))
|
||||||
))
|
))
|
||||||
),
|
),
|
||||||
constVel = Vector3f(0.0f, 0.001f, 0.0f),
|
constVel = Vector3f(0.0f, 0.00001f, 0.0f),
|
||||||
drag = 0.075f,
|
drag = 0.075f,
|
||||||
minVel = 0.0f,
|
minVel = 0.0f,
|
||||||
offsetCurve = ParamClasses.LerpValVec3f.Null,
|
offsetCurve = ParamClasses.LerpValVec3f.Null,
|
||||||
|
|||||||
@@ -43,14 +43,22 @@ class ParamClasses {
|
|||||||
private val min: Int,
|
private val min: Int,
|
||||||
private val max: Int,
|
private val max: Int,
|
||||||
) {
|
) {
|
||||||
fun randomize() : Int { return randomIntBetween(min, max) }
|
fun randomize() : Int {
|
||||||
|
return if (min == max) { max }
|
||||||
|
else if (min > max) { max }
|
||||||
|
else { randomIntBetween(min, max) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PairFloat(
|
class PairFloat(
|
||||||
private val min: Float,
|
private val min: Float,
|
||||||
private val max: Float,
|
private val max: Float,
|
||||||
) {
|
) {
|
||||||
fun randomize() : Float { return randomFloatBetween(min, max) }
|
fun randomize() : Float {
|
||||||
|
return if (min == max) { max }
|
||||||
|
else if (min > max) { max }
|
||||||
|
else { randomFloatBetween(min, max) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class LerpValVec3f {
|
sealed class LerpValVec3f {
|
||||||
|
|||||||
@@ -60,8 +60,10 @@ class Particle(
|
|||||||
|
|
||||||
|
|
||||||
fun init() {
|
fun init() {
|
||||||
val so = emitterParams.spawnPosOffset
|
val so = Vector3f(emitterParams.spawnPosOffset)
|
||||||
entityPos = entityPos.add(so.x.toDouble(), so.y.toDouble(), so.z.toDouble())
|
if (so.x + so.y + so.z != 0.0f) {
|
||||||
|
entityPos = entityPos.add(so.x.toDouble(), so.y.toDouble(), so.z.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
val shape = emitterParams.shape
|
val shape = emitterParams.shape
|
||||||
val newSpawnPos = shape.random(shape)
|
val newSpawnPos = shape.random(shape)
|
||||||
@@ -222,7 +224,10 @@ class Particle(
|
|||||||
|
|
||||||
private fun calcPos() : Pair<Vector3f, Vector3f> {
|
private fun calcPos() : Pair<Vector3f, Vector3f> {
|
||||||
var vel = Vector3f(vel)
|
var vel = Vector3f(vel)
|
||||||
vel = vel.add(emitterParams.constVel)
|
val constVel = emitterParams.constVel
|
||||||
|
if (constVel.x + constVel.y + constVel.z != 0.0f) {
|
||||||
|
vel = vel.add(emitterParams.constVel)
|
||||||
|
}
|
||||||
vel = vel.mul(1.0f - emitterParams.drag)
|
vel = vel.mul(1.0f - emitterParams.drag)
|
||||||
|
|
||||||
if (abs(vel.x) < emitterParams.minVel) { vel.x = 0.0f }
|
if (abs(vel.x) < emitterParams.minVel) { vel.x = 0.0f }
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class POrigin(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f) {
|
|||||||
pos = eOrigin,
|
pos = eOrigin,
|
||||||
rot = rot,
|
rot = rot,
|
||||||
model = "minecraft:lime_concrete",
|
model = "minecraft:lime_concrete",
|
||||||
tags = arrayOf("DEBUG", "P_DEBUG", "pOrigin_DEBUG"),
|
tags = arrayOf("DEBUG", "P_DEBUG", "pOrigin_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
|
||||||
brightnessOverride = 15,
|
brightnessOverride = 15,
|
||||||
translation = Vector3f(pOrigin),
|
translation = Vector3f(pOrigin),
|
||||||
scale = Vector3f(0.15f, 0.15f, 0.15f)
|
scale = Vector3f(0.15f, 0.15f, 0.15f)
|
||||||
@@ -78,7 +78,7 @@ class PVel(rot: Vector2f, eOrigin: Vec3d, pOrigin: Vector3f, pVel: Vector3f, pSc
|
|||||||
pos = eOrigin,
|
pos = eOrigin,
|
||||||
rot = rot,
|
rot = rot,
|
||||||
model = "minecraft:red_nether_brick_slab", //"minecraft:red_stained_glass",
|
model = "minecraft:red_nether_brick_slab", //"minecraft:red_stained_glass",
|
||||||
tags = arrayOf("DEBUG", "P_DEBUG", "pVel_DEBUG"),
|
tags = arrayOf("DEBUG", "P_DEBUG", "pVel_DEBUG", "BPS_UUID", SESSION_UUID.toString()),
|
||||||
brightnessOverride = 15,
|
brightnessOverride = 15,
|
||||||
translation = Vector3f(pOrigin),
|
translation = Vector3f(pOrigin),
|
||||||
scale = Vector3f(0.05f, max(max(pScale.x, pScale.y), pScale.z) + (pVel.x + pVel.y + pVel.z), 0.05f)
|
scale = Vector3f(0.05f, max(max(pScale.x, pScale.y), pScale.z) + (pVel.x + pVel.y + pVel.z), 0.05f)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ParticleEmitter(
|
|||||||
private var allParticles: Array<Particle> = emptyArray()
|
private var allParticles: Array<Particle> = emptyArray()
|
||||||
|
|
||||||
private var timeAlive = 0
|
private var timeAlive = 0
|
||||||
|
private var timeEmitted = 0
|
||||||
private var timePaused = 0
|
private var timePaused = 0
|
||||||
private var timesLooped = 0
|
private var timesLooped = 0
|
||||||
private var isPaused = false
|
private var isPaused = false
|
||||||
@@ -28,13 +29,20 @@ class ParticleEmitter(
|
|||||||
|
|
||||||
var isDead = false
|
var isDead = false
|
||||||
|
|
||||||
|
private var debugTool = EmitterDebug(rot, pos, params.spawnPosOffset, params.shape)
|
||||||
|
|
||||||
|
|
||||||
suspend fun tick() {
|
suspend fun tick() {
|
||||||
if (isTicking) { count() }
|
if (isTicking) { count() }
|
||||||
if (isTicking) { addParticle() }
|
if (isTicking) { addParticle() }
|
||||||
if (!isTicking && allParticles.isEmpty()) { isDead = true }
|
if (!isTicking && allParticles.isEmpty()) { isDead = true; kill() }
|
||||||
|
|
||||||
processParticlesInBatches(100)
|
processParticlesInBatches(100)
|
||||||
|
|
||||||
|
if (timeAlive == 0 && debug) { debugTool.spawn(world, pos, params.forceFields, params.spawnPosOffset) }
|
||||||
|
if (debug) { debugTool.update(rot, pos, params.spawnPosOffset) }
|
||||||
|
|
||||||
|
timeAlive += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -87,9 +95,9 @@ class ParticleEmitter(
|
|||||||
private fun count() {
|
private fun count() {
|
||||||
if (params.spawnDuration is ParamClasses.Duration.SingleBurst) {
|
if (params.spawnDuration is ParamClasses.Duration.SingleBurst) {
|
||||||
val loopDur = (params.spawnDuration as ParamClasses.Duration.SingleBurst).loopDur
|
val loopDur = (params.spawnDuration as ParamClasses.Duration.SingleBurst).loopDur
|
||||||
if (timeAlive >= loopDur) { isTicking = false; return }
|
if (timeEmitted >= loopDur) { isTicking = false; return }
|
||||||
|
|
||||||
timeAlive += 1
|
timeEmitted += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (params.spawnDuration is ParamClasses.Duration.MultiBurst) {
|
else if (params.spawnDuration is ParamClasses.Duration.MultiBurst) {
|
||||||
@@ -97,19 +105,19 @@ class ParticleEmitter(
|
|||||||
val loopDelay = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopDelay
|
val loopDelay = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopDelay
|
||||||
val loopCount = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopCount
|
val loopCount = (params.spawnDuration as ParamClasses.Duration.MultiBurst).loopCount
|
||||||
|
|
||||||
if (timeAlive >= loopDur && timesLooped + 1 >= loopCount) { isTicking = false; return }
|
if (timeEmitted >= loopDur && timesLooped + 1 >= loopCount) { isTicking = false; return }
|
||||||
|
|
||||||
if (timePaused >= loopDelay) {
|
if (timePaused >= loopDelay) {
|
||||||
isPaused = false
|
isPaused = false
|
||||||
timePaused = 0
|
timePaused = 0
|
||||||
timesLooped += 1
|
timesLooped += 1
|
||||||
}
|
}
|
||||||
if (timeAlive >= loopDur && loopDelay > 0) {
|
if (timeEmitted >= loopDur && loopDelay > 0) {
|
||||||
isPaused = true
|
isPaused = true
|
||||||
timeAlive = 0
|
timeEmitted = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPaused) { timeAlive += 1 }
|
if (!isPaused) { timeEmitted += 1 }
|
||||||
else { timePaused += 1 }
|
else { timePaused += 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,12 +129,12 @@ class ParticleEmitter(
|
|||||||
isPaused = false
|
isPaused = false
|
||||||
timePaused = 0
|
timePaused = 0
|
||||||
}
|
}
|
||||||
if (timeAlive >= loopDur && loopDelay > 0) {
|
if (timeEmitted >= loopDur && loopDelay > 0) {
|
||||||
isPaused = true
|
isPaused = true
|
||||||
timeAlive = 0
|
timeEmitted = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPaused) { timeAlive += 1 }
|
if (!isPaused) { timeEmitted += 1 }
|
||||||
else { timePaused += 1 }
|
else { timePaused += 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +145,7 @@ class ParticleEmitter(
|
|||||||
particle.kill()
|
particle.kill()
|
||||||
allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray()
|
allParticles = allParticles.toMutableList().apply { remove(particle) }.toTypedArray()
|
||||||
}
|
}
|
||||||
|
if (debug) { debugTool.kill() }
|
||||||
isTicking = false
|
isTicking = false
|
||||||
isDead = true
|
isDead = true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user