GameWorld.kt 17.9 KB
Newer Older
1

2
package net.torvald.terrarum.gameworld
3

4
import net.torvald.gdx.graphics.Cvec
Minjae Song's avatar
Minjae Song committed
5
import net.torvald.terrarum.AppLoader.printdbg
6
import net.torvald.terrarum.Terrarum
7
import net.torvald.terrarum.blockproperties.Block
8
import net.torvald.terrarum.blockproperties.BlockCodex
9
import net.torvald.terrarum.blockproperties.Fluid
10
import net.torvald.terrarum.modulebasegame.gameworld.WorldSimulator
11
import net.torvald.terrarum.realestate.LandUtil
Minjae Song's avatar
Minjae Song committed
12
import net.torvald.terrarum.serialise.ReadLayerDataZip
13
import net.torvald.util.SortedArrayList
14
import org.dyn4j.geometry.Vector2
15
import kotlin.math.absoluteValue
16
import kotlin.math.sign
17

18
typealias BlockAddress = Long
19

20 21
open class GameWorld {

Minjae Song's avatar
Minjae Song committed
22
    var worldName: String = "New World"
Minjae Song's avatar
Minjae Song committed
23
    /** Index start at 1 */
24
    var worldIndex: Int
Minjae Song's avatar
Minjae Song committed
25 26 27 28 29 30 31 32 33 34 35
        set(value) {
            if (value <= 0)
                throw Error("World index start at 1; you entered $value")

            printdbg(this, "Creation of new world with index $value, called by:")
            Thread.currentThread().stackTrace.forEach {
                printdbg(this, "--> $it")
            }

            field = value
        }
36 37
    val width: Int
    val height: Int
38

Minjae Song's avatar
Minjae Song committed
39 40 41
    val creationTime: Long
    var lastPlayTime: Long
        internal set // there's a case of save-and-continue-playing
42 43
    var totalPlayTime: Int
        internal set
Minjae Song's avatar
Minjae Song committed
44 45 46

    /** Used to calculate play time */
    val loadTime: Long = System.currentTimeMillis() / 1000L
47 48

    //layers
49
    @TEMzPayload("WALL", TEMzPayload.EIGHT_MSB)
50
    val layerWall: MapLayer
51
    @TEMzPayload("TERR", TEMzPayload.EIGHT_MSB)
52
    val layerTerrain: MapLayer
53
    //val layerWire: MapLayer
54

55
    @TEMzPayload("WALL", TEMzPayload.FOUR_LSB)
56
    val layerWallLowBits: PairedMapLayer
57
    @TEMzPayload("TERR", TEMzPayload.FOUR_LSB)
58 59
    val layerTerrainLowBits: PairedMapLayer

60
    //val layerThermal: MapLayerHalfFloat // in Kelvins
61
    //val layerFluidPressure: MapLayerHalfFloat // (milibar - 1000)
62

63 64 65 66
    /** Tilewise spawn point */
    var spawnX: Int
    /** Tilewise spawn point */
    var spawnY: Int
67

68
    @TEMzPayload("WdMG", TEMzPayload.INT48_FLOAT_PAIR)
69
    val wallDamages: HashMap<BlockAddress, Float>
70
    @TEMzPayload("TdMG", TEMzPayload.INT48_FLOAT_PAIR)
71
    val terrainDamages: HashMap<BlockAddress, Float>
72
    @TEMzPayload("FlTP", TEMzPayload.INT48_SHORT_PAIR)
73
    val fluidTypes: HashMap<BlockAddress, FluidType>
74
    @TEMzPayload("FlFL", TEMzPayload.INT48_FLOAT_PAIR)
75
    val fluidFills: HashMap<BlockAddress, Float>
76

77 78 79 80 81
    /**
     * Single block can have multiple conduits, different types of conduits are stored separately.
     */
    @TEMzPayload("WiNt", TEMzPayload.EXTERNAL_JSON)
    private val wirings: HashMap<BlockAddress, SortedArrayList<WiringNode>>
82

83 84 85 86
    /**
     * Used by the renderer. When wirings are updated, `wirings` and this properties must be synchronised.
     */
    private val wiringBlocks: HashMap<BlockAddress, Int>
87

88
    //public World physWorld = new World( new Vec2(0, -Terrarum.game.gravitationalAccel) );
89
    //physics
90
    /** Meter per second squared. Currently only the downward gravity is supported. No reverse gravity :p */
91
    var gravitation: Vector2 = Vector2(0.0, 9.80665)
92
    /** 0.0..1.0+ */
93
    var globalLight = Cvec(0f, 0f, 0f, 0f)
94
    var averageTemperature = 288f // 15 deg celsius; simulates global warming
95 96


97
    var generatorSeed: Long = 0
98
        internal set
99

100

101
    constructor(worldIndex: Int, width: Int, height: Int, creationTIME_T: Long, lastPlayTIME_T: Long, totalPlayTime: Int) {
102 103
        if (width <= 0 || height <= 0) throw IllegalArgumentException("Non-positive width/height: ($width, $height)")

104 105 106
        this.worldIndex = worldIndex
        this.width = width
        this.height = height
107

108 109 110 111 112
        this.spawnX = width / 2
        this.spawnY = 200

        layerTerrain = MapLayer(width, height)
        layerWall = MapLayer(width, height)
113
        //layerWire = MapLayer(width, height)
114 115
        layerTerrainLowBits = PairedMapLayer(width, height)
        layerWallLowBits = PairedMapLayer(width, height)
116

117 118 119 120
        wallDamages = HashMap()
        terrainDamages = HashMap()
        fluidTypes = HashMap()
        fluidFills = HashMap()
121

122 123
        wiringBlocks = HashMap()
        wirings = HashMap()
124

125
        // temperature layer: 2x2 is one cell
126
        //layerThermal = MapLayerHalfFloat(width, height, averageTemperature)
127

128 129
        // fluid pressure layer: 4 * 8 is one cell
        //layerFluidPressure = MapLayerHalfFloat(width, height, 13f) // 1013 mBar
Minjae Song's avatar
Minjae Song committed
130 131 132 133


        creationTime = creationTIME_T
        lastPlayTime = lastPlayTIME_T
134
        this.totalPlayTime = totalPlayTime
135 136
    }

Minjae Song's avatar
Minjae Song committed
137
    internal constructor(worldIndex: Int, layerData: ReadLayerDataZip.LayerData, creationTIME_T: Long, lastPlayTIME_T: Long, totalPlayTime: Int) {
138 139 140 141
        this.worldIndex = worldIndex

        layerTerrain = layerData.layerTerrain
        layerWall = layerData.layerWall
142
        //layerWire = layerData.layerWire
143 144 145 146 147
        layerTerrainLowBits = layerData.layerTerrainLowBits
        layerWallLowBits = layerData.layerWallLowBits

        wallDamages = layerData.wallDamages
        terrainDamages = layerData.terrainDamages
148 149
        fluidTypes = layerData.fluidTypes
        fluidFills = layerData.fluidFills
150

151 152
        wiringBlocks = HashMap()
        wirings = HashMap()
153

154 155 156 157 158
        spawnX = layerData.spawnX
        spawnY = layerData.spawnY

        width = layerTerrain.width
        height = layerTerrain.height
Minjae Song's avatar
Minjae Song committed
159 160 161 162


        creationTime = creationTIME_T
        lastPlayTime = lastPlayTIME_T
163
        this.totalPlayTime = totalPlayTime
164 165 166
    }


167 168 169 170 171
    /**
     * Get 2d array data of terrain

     * @return byte[][] terrain layer
     */
172
    val terrainArray: ByteArray
173 174 175 176 177 178 179
        get() = layerTerrain.data

    /**
     * Get 2d array data of wall

     * @return byte[][] wall layer
     */
180
    val wallArray: ByteArray
181 182 183 184 185 186 187
        get() = layerWall.data

    /**
     * Get 2d array data of wire

     * @return byte[][] wire layer
     */
188 189
    //val wireArray: ByteArray
    //    get() = layerWire.data
190

191
    private fun coerceXY(x: Int, y: Int) = (x fmod width) to (y.coerceIn(0, height - 1))
192

193
    fun getTileFromWall(x: Int, y: Int): Int? {
194 195 196
        val (x, y) = coerceXY(x, y)
        val wall: Int? = layerWall.getTile(x, y)
        val wallDamage: Int? = getWallLowBits(x, y)
197 198 199 200
        return if (wall == null || wallDamage == null)
            null
        else
            wall * PairedMapLayer.RANGE + wallDamage
201 202
    }

203
    fun getTileFromTerrain(x: Int, y: Int): Int? {
204 205 206
        val (x, y) = coerceXY(x, y)
        val terrain: Int? = layerTerrain.getTile(x, y)
        val terrainDamage: Int? = getTerrainLowBits(x, y)
207 208 209 210
        return if (terrain == null || terrainDamage == null)
            null
        else
            terrain * PairedMapLayer.RANGE + terrainDamage
211 212
    }

213
    private fun getWallLowBits(x: Int, y: Int): Int? {
214 215
        val (x, y) = coerceXY(x, y)
        return layerWallLowBits.getData(x, y)
216 217
    }

218
    private fun getTerrainLowBits(x: Int, y: Int): Int? {
219 220
        val (x, y) = coerceXY(x, y)
        return layerTerrainLowBits.getData(x, y)
221 222 223 224 225 226 227 228
    }

    /**
     * Set the tile of wall as specified, with damage value of zero.
     * @param x
     * *
     * @param y
     * *
229
     * @param tilenum Item id of the wall block. Less-than-4096-value is permitted.
230
     */
231
    fun setTileWall(x: Int, y: Int, tilenum: Int) {
232
        val (x, y) = coerceXY(x, y)
233
        val tilenum = tilenum % TILES_SUPPORTED // does work without this, but to be safe...
234

235
        val oldWall = getTileFromWall(x, y)
236 237
        layerWall.setTile(x, y, (tilenum / PairedMapLayer.RANGE).toByte())
        layerWallLowBits.setData(x, y, tilenum % PairedMapLayer.RANGE)
238
        wallDamages.remove(LandUtil.getBlockAddr(this, x, y))
239 240

        if (oldWall != null)
241
            Terrarum.ingame?.queueWallChangedEvent(oldWall, tilenum, LandUtil.getBlockAddr(this, x, y))
242 243
    }

244
    /**
245 246
     * Set the tile of wall as specified, with damage value of zero.
     *
247
     * Warning: this function alters fluid lists: be wary of call order!
248 249 250 251 252 253
     *
     * @param x
     * *
     * @param y
     * *
     * @param tilenum Item id of the terrain block, <4096
254
     */
255
    fun setTileTerrain(x: Int, y: Int, tilenum: Int) {
256
        val (x, y) = coerceXY(x, y)
257

258
        val oldTerrain = getTileFromTerrain(x, y)
259 260
        layerTerrain.setTile(x, y, (tilenum / PairedMapLayer.RANGE).toByte())
        layerTerrainLowBits.setData(x, y, tilenum % PairedMapLayer.RANGE)
261 262 263
        val blockAddr = LandUtil.getBlockAddr(this, x, y)
        terrainDamages.remove(blockAddr)

264
        if (BlockCodex[tilenum].isSolid) {
265 266 267 268
            fluidFills.remove(blockAddr)
            fluidTypes.remove(blockAddr)
        }
        // fluid tiles-item should be modified so that they will also place fluid onto their respective map
269 270

        if (oldTerrain != null)
271
            Terrarum.ingame?.queueTerrainChangedEvent(oldTerrain, tilenum, LandUtil.getBlockAddr(this, x, y))
272 273
    }

274
    /*fun setTileWire(x: Int, y: Int, tile: Byte) {
275
        val (x, y) = coerceXY(x, y)
276
        val oldWire = getTileFromWire(x, y)
277
        layerWire.setTile(x, y, tile)
278 279 280

        if (oldWire != null)
            Terrarum.ingame?.queueWireChangedEvent(oldWire, tile.toUint(), LandUtil.getBlockAddr(this, x, y))
281
    }*/
282

283 284 285 286
    fun getWiringBlocks(x: Int, y: Int): Int {
        return wiringBlocks.getOrDefault(LandUtil.getBlockAddr(this, x, y), 0)
    }

287 288
    fun getAllConduitsFrom(x: Int, y: Int): SortedArrayList<WiringNode>? {
        return wirings.get(LandUtil.getBlockAddr(this, x, y))
289 290 291
    }

    /**
292
     * @param conduitTypeBit defined in net.torvald.terrarum.blockproperties.Wire, always power-of-two
293
     */
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
    fun getConduitByTypeFrom(x: Int, y: Int, conduitTypeBit: Int): WiringNode? {
        val conduits = getAllConduitsFrom(x, y)
        return conduits?.searchFor(conduitTypeBit) { it.typeBitMask }
    }

    fun addNewConduitTo(x: Int, y: Int, node: WiringNode) {
        val blockAddr = LandUtil.getBlockAddr(this, x, y)

        // check for existing type of conduit
        // if there's no duplicate...
        if (getWiringBlocks(x, y) and node.typeBitMask == 0) {
            // store as-is
            wirings.getOrPut(blockAddr) { SortedArrayList() }.add(node)
            // synchronise wiringBlocks
            wiringBlocks[blockAddr] = (wiringBlocks[blockAddr] ?: 0) or node.typeBitMask
        }
        else {
            TODO("need overwriting policy for existing conduit node")
312 313 314
        }
    }

315
    fun getTileFrom(mode: Int, x: Int, y: Int): Int? {
316 317 318 319 320 321 322
        if (mode == TERRAIN) {
            return getTileFromTerrain(x, y)
        }
        else if (mode == WALL) {
            return getTileFromWall(x, y)
        }
        else if (mode == WIRE) {
323
            return getWiringBlocks(x, y)
324 325 326 327 328
        }
        else
            throw IllegalArgumentException("illegal mode input: " + mode.toString())
    }

Minjae Song's avatar
Minjae Song committed
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
    fun terrainIterator(): Iterator<Int> {
        return object : Iterator<Int> {

            private var iteratorCount = 0

            override fun hasNext(): Boolean {
                return iteratorCount < width * height
            }

            override fun next(): Int {
                val y = iteratorCount / width
                val x = iteratorCount % width
                // advance counter
                iteratorCount += 1

                return getTileFromTerrain(x, y)!!
            }

        }
    }

    fun wallIterator(): Iterator<Int> {
        return object : Iterator<Int> {

            private var iteratorCount = 0

355 356
            override fun hasNext(): Boolean =
                    iteratorCount < width * height
Minjae Song's avatar
Minjae Song committed
357 358 359 360 361 362 363 364 365 366 367 368 369

            override fun next(): Int {
                val y = iteratorCount / width
                val x = iteratorCount % width
                // advance counter
                iteratorCount += 1

                return getTileFromWall(x, y)!!
            }

        }
    }

370 371 372
    /**
     * @return true if block is broken
     */
373 374
    fun inflictTerrainDamage(x: Int, y: Int, damage: Double): Boolean {
        val damage = damage.toFloat()
375
        val addr = LandUtil.getBlockAddr(this, x, y)
376

377 378
        //println("[GameWorld] ($x, $y) Damage: $damage")

379
        if (terrainDamages[addr] == null) { // add new
380 381
            terrainDamages[addr] = damage
        }
382 383 384 385
        else if (terrainDamages[addr]!! + damage <= 0) { // tile is (somehow) fully healed
            terrainDamages.remove(addr)
        }
        else { // normal situation
386 387
            terrainDamages[addr] = terrainDamages[addr]!! + damage
        }
388 389 390 391

        //println("[GameWorld] accumulated damage: ${terrainDamages[addr]}")

        // remove tile from the world
392
        if (terrainDamages[addr] ?: 0f >= BlockCodex[getTileFromTerrain(x, y)].strength) {
393
            setTileTerrain(x, y, 0)
394
            terrainDamages.remove(addr)
395 396 397 398
            return true
        }

        return false
399
    }
400
    fun getTerrainDamage(x: Int, y: Int): Float =
401
            terrainDamages[LandUtil.getBlockAddr(this, x, y)] ?: 0f
402

403 404 405
    /**
     * @return true if block is broken
     */
406 407
    fun inflictWallDamage(x: Int, y: Int, damage: Double): Boolean {
        val damage = damage.toFloat()
408
        val addr = LandUtil.getBlockAddr(this, x, y)
409

410
        if (wallDamages[addr] == null) { // add new
411 412
            wallDamages[addr] = damage
        }
413 414 415 416
        else if (wallDamages[addr]!! + damage <= 0) { // tile is (somehow) fully healed
            wallDamages.remove(addr)
        }
        else { // normal situation
417 418
            wallDamages[addr] = wallDamages[addr]!! + damage
        }
419 420

        // remove tile from the world
421
        if (wallDamages[addr]!! >= BlockCodex[getTileFromWall(x, y)].strength) {
422
            setTileWall(x, y, 0)
423
            wallDamages.remove(addr)
424 425 426 427
            return true
        }

        return false
428
    }
429
    fun getWallDamage(x: Int, y: Int): Float =
430
            wallDamages[LandUtil.getBlockAddr(this, x, y)] ?: 0f
431

432
    fun setFluid(x: Int, y: Int, fluidType: FluidType, fill: Float) {
433 434 435 436 437
        /*if (x == 60 && y == 256) {
            printdbg(this, "Setting fluid $fill at ($x,$y)")
        }*/


438 439 440 441 442
        if (fluidType == Fluid.NULL && fill != 0f) {
            throw Error("Illegal fluid at ($x,$y): ${FluidInfo(fluidType, fill)}")
        }


443
        val addr = LandUtil.getBlockAddr(this, x, y)
Minjae Song's avatar
Minjae Song committed
444 445

        if (fill > WorldSimulator.FLUID_MIN_MASS) {
446
            //setTileTerrain(x, y, fluidTypeToBlock(fluidType))
Minjae Song's avatar
Minjae Song committed
447 448
            fluidFills[addr] = fill
            fluidTypes[addr] = fluidType
449 450
        }
        else {
Minjae Song's avatar
Minjae Song committed
451 452
            fluidFills.remove(addr)
            fluidTypes.remove(addr)
453

454
        }
455 456 457 458 459 460 461


        /*if (x == 60 && y == 256) {
            printdbg(this, "TileTerrain: ${getTileFromTerrain(x, y)}")
            printdbg(this, "fluidTypes[$addr] = ${fluidTypes[addr]} (should be ${fluidType.value})")
            printdbg(this, "fluidFills[$addr] = ${fluidFills[addr]} (should be $fill)")
        }*/
462 463
    }

464
    fun getFluid(x: Int, y: Int): FluidInfo {
465 466 467
        val addr = LandUtil.getBlockAddr(this, x, y)
        val fill = fluidFills[addr]
        val type = fluidTypes[addr]
468
        return if (type == null) FluidInfo(Fluid.NULL, 0f) else FluidInfo(type, fill!!)
469 470
    }

471 472 473 474 475 476
    private fun fluidTypeToBlock(type: FluidType) = when (type.abs()) {
        Fluid.NULL.value -> Block.AIR
        in Fluid.fluidRange -> GameWorld.TILES_SUPPORTED - type.abs()
        else -> throw IllegalArgumentException("Unsupported fluid type: $type")
    }

477
    data class FluidInfo(val type: FluidType, val amount: Float) {
478 479
        /** test if this fluid should be considered as one */
        fun isFluid() = type != Fluid.NULL && amount >= WorldSimulator.FLUID_MIN_MASS
Minjae Song's avatar
Minjae Song committed
480
        fun getProp() = BlockCodex[type]
481 482
        override fun toString() = "Fluid type: ${type.value}, amount: $amount"
    }
483

484 485 486 487 488 489
    /**
     * Connection rules: connect to all nearby, except:
     *
     * If the wire allows 3- or 4-way connection, make such connection.
     * If the wire does not allow them (e.g. wire bridge, thicknet), connect top-bottom and left-right nodes.
     */
490
    data class WiringNode(
491 492 493
            val position: BlockAddress,
            /** One defined in WireCodex, always power of two */
            val typeBitMask: Int,
494
            var fills: Float = 0f
495 496 497 498 499
    ) : Comparable<WiringNode> {
        override fun compareTo(other: WiringNode): Int {
            return (this.position - other.position).sign
        }
    }
500 501

    fun getTemperature(worldTileX: Int, worldTileY: Int): Float? {
502
        return null
503 504
    }

505
    fun getAirPressure(worldTileX: Int, worldTileY: Int): Float? {
506
        return null
507 508 509
    }


510 511 512 513 514
    companion object {
        @Transient val WALL = 0
        @Transient val TERRAIN = 1
        @Transient val WIRE = 2

515
        /** 4096 */
516
        @Transient val TILES_SUPPORTED = MapLayer.RANGE * PairedMapLayer.RANGE
517
        @Transient val SIZEOF: Byte = MapLayer.SIZEOF
518
        @Transient val LAYERS: Byte = 4 // terrain, wall (layerTerrainLowBits + layerWallLowBits), wire
519

Minjae Song's avatar
Minjae Song committed
520
        fun makeNullWorld() = GameWorld(1, 1, 1, 0, 0, 0)
521
    }
522 523 524
}

infix fun Int.fmod(other: Int) = Math.floorMod(this, other)
525
infix fun Long.fmod(other: Long) = Math.floorMod(this, other)
526
infix fun Float.fmod(other: Float) = if (this >= 0f) this % other else (this % other) + other
527 528 529

inline class FluidType(val value: Int) {
    infix fun sameAs(other: FluidType) = this.value.absoluteValue == other.value.absoluteValue
530
    fun abs() = this.value.absoluteValue
531 532 533 534 535 536 537 538 539 540 541
}

/**
 * @param payloadName Payload name defined in Map Data Format.txt
 * * 4 Letters: regular payload
 * * 3 Letters: only valid for arrays with 16 elements, names are auto-generated by appending '0'..'9'+'a'..'f'. E.g.: 'CfL' turns into 'CfL0', 'CfL1' ... 'CfLe', 'CfLf'
 *
 * @param arg 0 for 8 MSBs of Terrain/Wall layer, 1 for 4 LSBs of Terrain/Wall layer, 2 for Int48-Float pair, 3 for Int48-Short pair, 4 for Int48-Int pair
 */
annotation class TEMzPayload(val payloadName: String, val arg: Int) {
    companion object {
542 543 544
        const val EXTERNAL_JAVAPROPERTIES = -3
        const val EXTERNAL_CSV = -2
        const val EXTERNAL_JSON = -1
545 546 547 548 549 550 551
        const val EIGHT_MSB = 0
        const val FOUR_LSB = 1
        const val INT48_FLOAT_PAIR = 2
        const val INT48_SHORT_PAIR = 3
        const val INT48_INT_PAIR = 4
    }
}