summaryrefslogtreecommitdiff
path: root/src/main/kotlin/io/dico/parcels2/defaultimpl/DefaultParcelGenerator.kt
blob: 73b6b4d7ee4298b079e620283aaebac148716b6b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
package io.dico.parcels2.defaultimpl

import io.dico.parcels2.*
import io.dico.parcels2.blockvisitor.RegionTraverser
import io.dico.parcels2.options.DefaultGeneratorOptions
import io.dico.parcels2.util.math.*
import kotlinx.coroutines.CoroutineScope
import org.bukkit.*
import org.bukkit.block.Biome
import org.bukkit.block.BlockFace
import org.bukkit.block.Skull
import org.bukkit.block.data.type.Slab
import org.bukkit.block.data.type.WallSign
import org.bukkit.entity.Player
import java.util.Random

private val airType = Bukkit.createBlockData(Material.AIR)

private const val chunkSize = 16

class DefaultParcelGenerator(
    override val worldName: String,
    private val o: DefaultGeneratorOptions
) : ParcelGenerator() {
    private var _world: World? = null
    override val world: World
        get() {
            if (_world == null) {
                val world = Bukkit.getWorld(worldName)
                maxHeight = world.maxHeight
                _world = world
                return world
            }
            return _world!!
        }

    private var maxHeight = 0
    val sectionSize = o.parcelSize + o.pathSize
    val pathOffset = (if (o.pathSize % 2 == 0) o.pathSize + 2 else o.pathSize + 1) / 2
    val makePathMain = o.pathSize > 2
    val makePathAlt = o.pathSize > 4

    private inline fun <T> generate(
        chunkX: Int,
        chunkZ: Int,
        floor: T, wall:
        T, pathMain: T,
        pathAlt: T,
        fill: T,
        setter: (Int, Int, Int, T) -> Unit
    ) {

        val floorHeight = o.floorHeight
        val parcelSize = o.parcelSize
        val sectionSize = sectionSize
        val pathOffset = pathOffset
        val makePathMain = makePathMain
        val makePathAlt = makePathAlt

        // parcel bottom x and z
        // umod is unsigned %: the result is always >= 0
        val pbx = ((chunkX shl 4) - o.offsetX) umod sectionSize
        val pbz = ((chunkZ shl 4) - o.offsetZ) umod sectionSize

        var curHeight: Int
        var x: Int
        var z: Int
        for (cx in 0..15) {
            for (cz in 0..15) {
                x = (pbx + cx) % sectionSize - pathOffset
                z = (pbz + cz) % sectionSize - pathOffset
                curHeight = floorHeight

                val type = when {
                    (x in 0 until parcelSize && z in 0 until parcelSize) -> floor
                    (x in -1..parcelSize && z in -1..parcelSize) -> {
                        curHeight++
                        wall
                    }
                    (makePathAlt && x in -2 until parcelSize + 2 && z in -2 until parcelSize + 2) -> pathAlt
                    (makePathMain) -> pathMain
                    else -> {
                        curHeight++
                        wall
                    }
                }

                for (y in 0 until curHeight) {
                    setter(cx, y, cz, fill)
                }
                setter(cx, curHeight, cz, type)
            }
        }
    }

    override fun generateChunkData(world: World?, random: Random?, chunkX: Int, chunkZ: Int, biome: BiomeGrid?): ChunkData {
        val out = Bukkit.createChunkData(world)
        generate(chunkX, chunkZ, o.floorType, o.wallType, o.pathMainType, o.pathAltType, o.fillType) { x, y, z, type ->
            out.setBlock(x, y, z, type)
        }
        return out
    }

    override fun populate(world: World?, random: Random?, chunk: Chunk?) {
        // do nothing
    }

    override fun getFixedSpawnLocation(world: World?, random: Random?): Location {
        val fix = if (o.parcelSize.even) 0.5 else 0.0
        return Location(world, o.offsetX + fix, o.floorHeight + 1.0, o.offsetZ + fix)
    }

    override fun makeParcelLocatorAndBlockManager(
        parcelProvider: ParcelProvider,
        container: ParcelContainer,
        coroutineScope: CoroutineScope,
        jobDispatcher: JobDispatcher
    ): Pair<ParcelLocator, ParcelBlockManager> {
        val impl = ParcelLocatorAndBlockManagerImpl(parcelProvider, container, coroutineScope, jobDispatcher)
        return impl to impl
    }

    private inline fun <T> convertBlockLocationToId(x: Int, z: Int, mapper: (Int, Int) -> T): T? {
        val sectionSize = sectionSize
        val parcelSize = o.parcelSize
        val absX = x - o.offsetX - pathOffset
        val absZ = z - o.offsetZ - pathOffset
        val modX = absX umod sectionSize
        val modZ = absZ umod sectionSize
        if (modX in 0 until parcelSize && modZ in 0 until parcelSize) {
            return mapper((absX - modX) / sectionSize + 1, (absZ - modZ) / sectionSize + 1)
        }
        return null
    }

    @Suppress("DEPRECATION")
    private inner class ParcelLocatorAndBlockManagerImpl(
        val parcelProvider: ParcelProvider,
        val container: ParcelContainer,
        coroutineScope: CoroutineScope,
        override val jobDispatcher: JobDispatcher
    ) : ParcelBlockManagerBase(), ParcelLocator, CoroutineScope by coroutineScope {

        override val world: World get() = this@DefaultParcelGenerator.world
        val worldId = parcelProvider.getWorld(world)?.id ?: ParcelWorldId(world)
        override val parcelTraverser: RegionTraverser = RegionTraverser.convergingTo(o.floorHeight)

        private val cornerWallType = when {
            o.wallType is Slab -> (o.wallType.clone() as Slab).apply { type = Slab.Type.DOUBLE }
            o.wallType.material.name.endsWith("CARPET") -> {
                Bukkit.createBlockData(Material.getMaterial(o.wallType.material.name.substringBefore("CARPET") + "WOOL"))
            }
            else -> null
        }

        override fun getParcelAt(x: Int, z: Int): Parcel? {
            return convertBlockLocationToId(x, z, container::getParcelById)
        }

        override fun getParcelIdAt(x: Int, z: Int): ParcelId? {
            return convertBlockLocationToId(x, z) { idx, idz -> ParcelId(worldId, idx, idz) }
        }


        private fun checkParcelId(parcel: ParcelId): ParcelId {
            if (!parcel.worldId.equals(worldId)) {
                throw IllegalArgumentException()
            }
            return parcel
        }

        override fun getRegionOrigin(parcel: ParcelId): Vec2i {
            checkParcelId(parcel)
            return Vec2i(
                sectionSize * (parcel.x - 1) + pathOffset + o.offsetX,
                sectionSize * (parcel.z - 1) + pathOffset + o.offsetZ
            )
        }

        override fun getRegion(parcel: ParcelId): Region {
            val origin = getRegionOrigin(parcel)
            return Region(
                Vec3i(origin.x, 0, origin.z),
                Vec3i(o.parcelSize, maxHeight, o.parcelSize)
            )
        }

        override fun getHomeLocation(parcel: ParcelId): Location {
            val origin = getRegionOrigin(parcel)
            val x = origin.x + (o.parcelSize - 1) / 2.0
            val z = origin.z - 2
            return Location(world, x + 0.5, o.floorHeight + 1.0, z + 0.5, 0F, 0F)
        }

        override fun getParcelForInfoBlockInteraction(block: Vec3i, type: Material, face: BlockFace): Parcel? {
            if (block.y != o.floorHeight + 1) return null

            val expectedParcelOrigin = when (type) {
                Material.WALL_SIGN -> Vec2i(block.x + 1, block.z + 2)
                o.wallType.material, cornerWallType?.material -> {
                    if (face != BlockFace.NORTH || world[block + Vec3i.convert(BlockFace.NORTH)].type == Material.WALL_SIGN) {
                        return null
                    }

                    Vec2i(block.x + 1, block.z + 1)
                }
                else -> return null
            }

            return getParcelAt(expectedParcelOrigin.x, expectedParcelOrigin.z)
                ?.takeIf { expectedParcelOrigin == getRegionOrigin(it.id) }
                ?.also { parcel ->
                    if (type != Material.WALL_SIGN && parcel.owner != null) {
                        updateParcelInfo(parcel.id, parcel.owner)
                        parcel.isOwnerSignOutdated = false
                    }
                }
        }

        override fun isParcelInfoSectionLoaded(parcel: ParcelId): Boolean {
            val wallBlockChunk = getRegionOrigin(parcel).add(-1, -1).toChunk()
            return world.isChunkLoaded(wallBlockChunk.x, wallBlockChunk.z)
        }

        override fun updateParcelInfo(parcel: ParcelId, owner: PlayerProfile?) {
            val b = getRegionOrigin(parcel)

            val wallBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 1)
            val signBlock = world.getBlockAt(b.x - 1, o.floorHeight + 1, b.z - 2)
            val skullBlock = world.getBlockAt(b.x - 1, o.floorHeight + 2, b.z - 1)

            if (owner == null) {
                wallBlock.blockData = o.wallType
                signBlock.type = Material.AIR
                skullBlock.type = Material.AIR

            } else {
                cornerWallType?.let { wallBlock.blockData = it }
                signBlock.blockData = (Bukkit.createBlockData(Material.WALL_SIGN) as WallSign).apply { facing = BlockFace.NORTH }

                val sign = signBlock.state as org.bukkit.block.Sign
                sign.setLine(0, "${parcel.x},${parcel.z}")
                sign.setLine(2, owner.name ?: "")
                sign.update()

                skullBlock.type = Material.AIR
                skullBlock.type = Material.PLAYER_HEAD
                val skull = skullBlock.state as Skull
                if (owner is PlayerProfile.Real) {
                    skull.owningPlayer = Bukkit.getOfflinePlayer(owner.uuid)

                } else if (!skull.setOwner(owner.name)) {
                    skullBlock.type = Material.AIR
                    return
                }

                skull.rotation = BlockFace.SOUTH
                skull.update()
            }
        }

        private fun trySubmitBlockVisitor(vararg parcels: ParcelId, function: JobFunction): Job? {
            parcels.forEach { checkParcelId(it) }
            return parcelProvider.trySubmitBlockVisitor(Permit(), parcels, function)
        }

        override fun setBiome(parcel: ParcelId, biome: Biome) = trySubmitBlockVisitor(checkParcelId(parcel)) {
            val world = world
            val b = getRegionOrigin(parcel)
            val parcelSize = o.parcelSize
            for (x in b.x until b.x + parcelSize) {
                for (z in b.z until b.z + parcelSize) {
                    markSuspensionPoint()
                    world.setBiome(x, z, biome)
                }
            }
        }

        override fun clearParcel(parcel: ParcelId) = trySubmitBlockVisitor(checkParcelId(parcel)) {
            val region = getRegion(parcel)
            val blocks = parcelTraverser.traverseRegion(region)
            val blockCount = region.blockCount.toDouble()
            val world = world
            val floorHeight = o.floorHeight
            val airType = airType
            val floorType = o.floorType
            val fillType = o.fillType

            delegateWork(0.95) {
                for ((index, vec) in blocks.withIndex()) {
                    markSuspensionPoint()
                    val y = vec.y
                    val blockType = when {
                        y > floorHeight -> airType
                        y == floorHeight -> floorType
                        else -> fillType
                    }
                    world[vec].blockData = blockType
                    setProgress((index + 1) / blockCount)
                }
            }

            delegateWork {
                val entities = getEntities(region)
                for ((index, entity) in entities.withIndex()) {
                    if (entity is Player) continue
                    entity.remove()
                    setProgress((index + 1) / entities.size.toDouble())
                }
            }

        }

        override fun getParcelsWithOwnerBlockIn(chunk: Chunk): Collection<Vec2i> {
            /*
             * Get the offsets for the world out of the way
             * to simplify the calculation that follows.
             */

            val x = chunk.x.shl(4) - (o.offsetX + pathOffset)
            val z = chunk.z.shl(4) - (o.offsetZ + pathOffset)

            /* Locations of wall corners (where owner blocks are placed) are defined as:
             *
             * x umod sectionSize == sectionSize-1
             *
             * This check needs to be made for all 16 slices of the chunk in 2 dimensions
             * How to optimize this?
             * Let's take the expression
             *
             * x umod sectionSize
             *
             * And call it modX
             * x can be shifted (chunkSize -1) times to attempt to get a modX of 0.
             * This means that if the modX is 1, and sectionSize == (chunkSize-1), there would be a match at the last shift.
             * To check that there are any matches, we can see if the following holds:
             *
             * modX >= ((sectionSize-1) - (chunkSize-1))
             *
             * Which can be simplified to:
             * modX >= sectionSize - chunkSize
             *
             * if sectionSize == chunkSize, this expression can be simplified to
             * modX >= 0
             * which is always true. This is expected.
             * To get the total number of matches on a dimension, we can evaluate the following:
             *
             * (modX - (sectionSize - chunkSize) + sectionSize) / sectionSize
             *
             * We add sectionSize to the lhs because, if the other part of the lhs is 0, we need at least 1.
             * This can be simplified to:
             *
             * (modX + chunkSize) / sectionSize
             */

            val sectionSize = sectionSize

            val modX = x umod sectionSize
            val matchesOnDimensionX = (modX + chunkSize) / sectionSize
            if (matchesOnDimensionX <= 0) return emptyList()

            val modZ = z umod sectionSize
            val matchesOnDimensionZ = (modZ + chunkSize) / sectionSize
            if (matchesOnDimensionZ <= 0) return emptyList()

            /*
             * Now we need to find the first id within the matches,
             * and then return the subsequent matches in a rectangle following it.
             *
             * On each dimension, get the distance to the first match, which is equal to (sectionSize-1 - modX)
             * and add it to the coordinate value
             */
            val firstX = x + (sectionSize - 1 - modX)
            val firstZ = z + (sectionSize - 1 - modZ)

            val firstIdX = (firstX + 1) / sectionSize + 1
            val firstIdZ = (firstZ + 1) / sectionSize + 1

            if (matchesOnDimensionX == 1 && matchesOnDimensionZ == 1) {
                // fast-path optimization
                return listOf(Vec2i(firstIdX, firstIdZ))
            }

            return (0 until matchesOnDimensionX).flatMap { idOffsetX ->
                (0 until matchesOnDimensionZ).map { idOffsetZ -> Vec2i(firstIdX + idOffsetX, firstIdZ + idOffsetZ) }
            }
        }

    }

}