package net.torvald.terrarum.modulebasegame

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.glutils.ShapeRenderer
import net.torvald.terrarum.*
import net.torvald.terrarum.App.*
import net.torvald.terrarum.Terrarum.getPlayerSaveFiledesc
import net.torvald.terrarum.Terrarum.getWorldSaveFiledesc
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE
import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED
import net.torvald.terrarum.blockproperties.BlockPropUtil
import net.torvald.terrarum.blockstats.MinimapComposer
import net.torvald.terrarum.blockstats.TileSurvey
import net.torvald.terrarum.concurrent.ThreadExecutor
import net.torvald.terrarum.console.AVTracker
import net.torvald.terrarum.console.ActorsList
import net.torvald.terrarum.console.Authenticator
import net.torvald.terrarum.gameactors.*
import net.torvald.terrarum.gamecontroller.IngameController
import net.torvald.terrarum.gamecontroller.KeyToggler
import net.torvald.terrarum.gamecontroller.TerrarumKeyboardEvent
import net.torvald.terrarum.gameitems.FixtureInteractionBlocked
import net.torvald.terrarum.gameitems.GameItem
import net.torvald.terrarum.gameitems.mouseInInteractableRange
import net.torvald.terrarum.gameparticles.ParticleBase
import net.torvald.terrarum.gameworld.GameWorld
import net.torvald.terrarum.gameworld.fmod
import net.torvald.terrarum.langpack.Lang
import net.torvald.terrarum.modulebasegame.gameactors.*
import net.torvald.terrarum.modulebasegame.gameactors.physicssolver.CollisionSolver
import net.torvald.terrarum.modulebasegame.gameitems.AxeCore
import net.torvald.terrarum.modulebasegame.gameitems.PickaxeCore
import net.torvald.terrarum.modulebasegame.gameworld.GameEconomy
import net.torvald.terrarum.modulebasegame.serialise.LoadSavegame
import net.torvald.terrarum.modulebasegame.serialise.ReadActor
import net.torvald.terrarum.modulebasegame.serialise.WriteSavegame
import net.torvald.terrarum.modulebasegame.ui.*
import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.gradEndCol
import net.torvald.terrarum.modulebasegame.worldgenerator.RoguelikeRandomiser
import net.torvald.terrarum.modulebasegame.worldgenerator.Worldgen
import net.torvald.terrarum.modulebasegame.worldgenerator.WorldgenParams
import net.torvald.terrarum.realestate.LandUtil
import net.torvald.terrarum.realestate.LandUtil.CHUNK_H
import net.torvald.terrarum.realestate.LandUtil.CHUNK_W
import net.torvald.terrarum.savegame.VDUtil
import net.torvald.terrarum.savegame.VirtualDisk
import net.torvald.terrarum.serialise.Common
import net.torvald.terrarum.ui.BasicDebugInfoWindow.Companion.toIntAndFrac
import net.torvald.terrarum.ui.Toolkit
import net.torvald.terrarum.ui.Toolkit.hdrawWidth
import net.torvald.terrarum.ui.UIAutosaveNotifier
import net.torvald.terrarum.ui.UICanvas
import net.torvald.terrarum.weather.WeatherMixer
import net.torvald.terrarum.worlddrawer.BlocksDrawer
import net.torvald.terrarum.worlddrawer.FeaturesDrawer
import net.torvald.terrarum.worlddrawer.WorldCamera
import net.torvald.unicode.EMDASH
import net.torvald.util.CircularArray
import org.khelekore.prtree.PRTree
import java.io.File
import java.util.*
import java.util.logging.Level
import kotlin.experimental.and
import kotlin.math.absoluteValue
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.roundToInt


/**
 * Ingame instance for the game Terrarum.
 *
 * Created by minjaesong on 2017-06-16.
 */

open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) {

    var historicalFigureIDBucket: ArrayList<Int> = ArrayList<Int>()


    /**
     * list of Actors that is sorted by Actors' referenceID
     */
    //val ACTORCONTAINER_INITIAL_SIZE = 64
    val PARTICLES_MAX = App.getConfigInt("maxparticles")
    val particlesContainer = CircularArray<ParticleBase>(PARTICLES_MAX, true)

    // these are required because actors always change their position
    private var visibleActorsRenderFarBehind: ArrayList<ActorWithBody> = ArrayList(1)
    private var visibleActorsRenderBehind: ArrayList<ActorWithBody> = ArrayList(1)
    private var visibleActorsRenderMiddle: ArrayList<ActorWithBody> = ArrayList(1)
    private var visibleActorsRenderMidTop: ArrayList<ActorWithBody> = ArrayList(1)
    private var visibleActorsRenderFront: ArrayList<ActorWithBody> = ArrayList(1)
    private var visibleActorsRenderOverlay: ArrayList<ActorWithBody> = ArrayList(1)

    //var screenZoom = 1.0f   // definition moved to IngameInstance
    //val ZOOM_MAXIMUM = 4.0f // definition moved to IngameInstance
    //val ZOOM_MINIMUM = 0.5f // definition moved to IngameInstance

    companion object {
        /** Sets camera position so that (0,0) would be top-left of the screen, (width, height) be bottom-right. */
        fun setCameraPosition(batch: SpriteBatch, camera: OrthographicCamera, newX: Float, newY: Float) {
            camera.position.set((-newX + App.scr.halfw).roundToFloat(), (-newY + App.scr.halfh).roundToFloat(), 0f)
            camera.update()
            batch.projectionMatrix = camera.combined
        }
        fun setCameraPosition(batch: SpriteBatch, shape: ShapeRenderer, camera: OrthographicCamera, newX: Float, newY: Float) {
            camera.setToOrtho(true, App.scr.wf, App.scr.hf)
            camera.update()
            camera.position.set((-newX + App.scr.halfw).roundToFloat(), (-newY + App.scr.halfh).roundToFloat(), 0f)
            camera.update()
            shape.projectionMatrix = camera.combined

            camera.setToOrtho(true, App.scr.wf, App.scr.hf)
            camera.update()
            camera.position.set((-newX + App.scr.halfw).roundToFloat(), (-newY + App.scr.halfh).roundToFloat(), 0f)
            camera.update()
            batch.projectionMatrix = camera.combined
        }
        fun getCanonicalTitle() = App.GAME_NAME +
                                  " $EMDASH F: ${Gdx.graphics.framesPerSecond}" +
                                  if (App.IS_DEVELOPMENT_BUILD)
                                      " (ΔF${Terrarum.updateRateStr})" +
                                      " $EMDASH M: H${Terrarum.memJavaHeap}M / X${Terrarum.memXmx}M / U${Terrarum.memUnsafe}M"
                                  else
                                      ""

        val ACTOR_UPDATE_RANGE = 4096

        fun distToActorSqr(world: GameWorld, a: ActorWithBody, p: ActorWithBody) =
                min(// take min of normal position and wrapped (x < 0) position
                        min((a.hitbox.centeredX - p.hitbox.centeredX).sqr() +
                        (a.hitbox.centeredY - p.hitbox.centeredY).sqr(),
                        ((a.hitbox.centeredX + world.width * TILE_SIZE) - p.hitbox.centeredX).sqr() +
                        (a.hitbox.centeredY - p.hitbox.centeredY).sqr()),
                        ((a.hitbox.centeredX - world.width * TILE_SIZE) - p.hitbox.centeredX).sqr() +
                        (a.hitbox.centeredY - p.hitbox.centeredY).sqr()
                )
        fun distToCameraSqr(world: GameWorld, a: ActorWithBody) =
                min(
                        min((a.hitbox.centeredX - WorldCamera.xCentre).sqr() +
                        (a.hitbox.centeredY - WorldCamera.yCentre).sqr(),
                        ((a.hitbox.centeredX + world.width * TILE_SIZE) - WorldCamera.xCentre).sqr() +
                        (a.hitbox.centeredY - WorldCamera.yCentre).sqr()),
                        ((a.hitbox.centeredX - world.width * TILE_SIZE) - WorldCamera.xCentre).sqr() +
                        (a.hitbox.centeredY - WorldCamera.yCentre).sqr()
                )

        /**
         * Returns the minimum squared distance from the actor to any living WorldUpdater.
         * Falls back to camera distance if no WorldUpdaters exist.
         */
        fun distToNearestUpdaterSqr(world: GameWorld, actor: ActorWithBody): Double {
            val worldUpdaters = Terrarum.ingame?.worldUpdaters
            if (worldUpdaters.isNullOrEmpty()) {
                return distToCameraSqr(world, actor)
            }
            return worldUpdaters.minOf { updater ->
                if (updater.flagDespawn || updater.despawned) Double.MAX_VALUE
                else distToActorSqr(world, actor, updater)
            }
        }

        /** whether the actor is within update range of any WorldUpdater */
        fun ActorWithBody.inUpdateRange(world: GameWorld) = distToNearestUpdaterSqr(world, this) <= ACTOR_UPDATE_RANGE.sqr()

        /** whether the actor is within screen */
        fun ActorWithBody.inScreen(world: GameWorld) =

                // y
                this.hitbox.endY >= WorldCamera.y && this.hitbox.startY <= WorldCamera.yEnd

                &&

                // x: camera is on the right side of the seam
                ((this.hitbox.endX - world.width >= WorldCamera.x && this.hitbox.startX - world.width <= WorldCamera.xEnd) ||
                 // x: camera in on the left side of the seam
                 (this.hitbox.endX + world.width >= WorldCamera.x && this.hitbox.startX + world.width <= WorldCamera.xEnd) ||
                 // x: neither
                 (this.hitbox.endX >= WorldCamera.x && this.hitbox.startX <= WorldCamera.xEnd))

        val SIZE_SMALL = Point2i(CHUNK_W*66, CHUNK_H*60)
        val SIZE_NORMAL = Point2i(CHUNK_W*100, CHUNK_H*60)
        val SIZE_LARGE = Point2i(CHUNK_W*150, CHUNK_H*60)
        val SIZE_HUGE = Point2i(CHUNK_W*250, CHUNK_H*60)
        val SIZE_TEST = Point2i(CHUNK_W*11, CHUNK_H*7)
        val NEW_WORLD_SIZE =
            if (App.IS_DEVELOPMENT_BUILD)
                arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE, SIZE_TEST)
            else
                arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE)
        val WORLDPORTAL_NEW_WORLD_SIZE =
            if (App.IS_DEVELOPMENT_BUILD)
                arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE, SIZE_TEST)
            else
                arrayOf(SIZE_SMALL, SIZE_NORMAL, SIZE_LARGE, SIZE_HUGE)

        val worldgenThreadExecutor = ThreadExecutor()
    }



    init {
        particlesContainer.overwritingPolicy = {
            it.dispose()
        }

        gameUpdateGovernor = ConsistentUpdateRate
    }


    lateinit var uiBlur: UIFakeBlurOverlay
    lateinit var uiPieMenu: UIQuickslotPie
    lateinit var uiQuickBar: UIQuickslotBar
    lateinit var uiInventoryPlayer: UIInventoryFull

    internal val uiInventoryPlayerReady: Boolean get() = ::uiInventoryPlayer.isInitialized // this is some ugly hack but I didn't want to make uiInventoryPlayer nullable so bite me

    /**
     * This is a dedicated property for the fixtures' UI.
     *
     * When it's not null, the UI will be updated and rendered;
     * when the UI is closed, it'll be replaced with a null value.
     *
     * This will not allow multiple fixture UIs from popping up (does not prevent them actually being open)
     * because UI updating and rendering is whitelist-operated
     */
    private var uiFixture: UICanvas? = null
        set(value) {
            printdbg(this, "uiFixture change: $uiFixture -> $value")
            field?.setAsClose()
            value?.let { uiFixturesHistory.add(it) }
            field = value
        }

    var wearableDeviceUI: UICanvas? = null
        set(value) {
            field?.setAsClose()
            value?.setAsOpen()
            value?.setPosition(App.scr.tvSafeGraphicsWidth/2, App.scr.tvSafeActionHeight/2)
            field = value
        }

    val getUIFixture = object : Id_UICanvasNullable { // quick workaround for the type erasure (you can't use lambda...)
        override fun get(): UICanvas? {
            return uiFixture
        }
    }
    val getWearableDeviceUI = object : Id_UICanvasNullable { // quick workaround for the type erasure (you can't use lambda...)
        override fun get(): UICanvas? {
            return wearableDeviceUI
        }
    }

    lateinit var uiVitalPrimary: UICanvas
    lateinit var uiVitalSecondary: UICanvas
    lateinit var uiVitalItem: UICanvas // itemcount/durability of held block or active ammo of held gun. As for the block, max value is 500.

    private val uiFixturesHistory = HashSet<UICanvas>()

    private lateinit var uiBasicInfo: UICanvas
    private lateinit var uiWatchTierOne: UICanvas
    lateinit var uiAutosaveNotifier: UIAutosaveNotifier
    lateinit var uiCheatDetected: UICheatDetected


    var particlesActive = 0
        private set

    var selectedWireRenderClass = ""
    private var oldSelectedWireRenderClass = ""



    private lateinit var ingameUpdateThread: ThreadIngameUpdate
    private lateinit var updateThreadWrapper: Thread
    //private val ingameDrawThread: ThreadIngameDraw // draw must be on the main thread


    override var gameInitialised = false
        internal set
    override var gameFullyLoaded = false
        internal set

    override val musicStreamer = TerrarumMusicAndAmbientStreamer()


    //////////////
    // GDX code //
    //////////////


    lateinit var gameLoadMode: GameLoadMode
    lateinit var gameLoadInfoPayload: Any


    enum class GameLoadMode {
        CREATE_NEW, LOAD_FROM
    }

    private val soundReflectiveMaterials = hashSetOf(
        ""
    )

    override fun show() {
        //initViewPort(AppLoader.terrarumAppConfig.screenW, AppLoader.terrarumAppConfig.screenH)

        // gameLoadMode and gameLoadInfoPayload must be set beforehand!!

        when (gameLoadMode) {
            GameLoadMode.CREATE_NEW -> enterCreateNewWorld(gameLoadInfoPayload as NewGameParams)
            GameLoadMode.LOAD_FROM  -> enterLoadFromSave(gameLoadInfoPayload as Codices)
        }

        IngameRenderer.setRenderedWorld(world)
        blockMarkingActor.isVisible = true

        App.audioMixer.reset()

        super.show() // this function sets gameInitialised = true

        TileSurvey.submitProposal(
            TileSurvey.SurveyProposal(
                "basegame.Ingame.audioReflection", 73, 73, 2, 4
            ) { world, x, y ->
                val tileProp = BlockCodex[world.getTileFromTerrain(x, y)]
                val wallProp = BlockCodex[world.getTileFromWall(x, y)]
                val prop = if (tileProp.isSolid && !tileProp.isActorBlock) tileProp else wallProp
                MaterialCodex[prop.material].sondrefl
            }
        )

        TileSurvey.submitProposal(
            TileSurvey.SurveyProposal(
                "basegame.Ingame.openness", 73, 73, 2, 4
            ) { world, x, y ->
                val tileProp = BlockCodex[world.getTileFromTerrain(x, y)]
                val wallProp = BlockCodex[world.getTileFromWall(x, y)]
                (!tileProp.isSolid && !wallProp.isSolid).toInt().toFloat()
            }
        )

        loadedTime_t = App.getTIME_T()

        MusicService.enterScene("ingame")
    }

    data class NewGameParams(
        val player: IngamePlayer,
        val newWorldParams: NewWorldParameters,
        val callbackAfterLoad: (TerrarumIngame) -> Unit

    )

    data class NewWorldParameters(
        val width: Int,
        val height: Int,
        val worldGenSeed: Long,
        val savegameName: String
        // other worldgen options
    ) {
        init {
            if (width % LandUtil.CHUNK_W != 0 || height % LandUtil.CHUNK_H != 0) {
                throw IllegalArgumentException("World size is not a multiple of chunk size; World size: ($width, $height), Chunk size: (${LandUtil.CHUNK_W}, ${LandUtil.CHUNK_H})")
            }
        }
    }

    data class Codices(
        val disk: VirtualDisk, // WORLD disk
        val world: GameWorld,
//        val meta: WriteMeta.WorldMeta,
//        val block: BlockCodex,
//        val item: ItemCodex,
//        val wire: WireCodex,
//        val material: MaterialCodex,
//        val faction: FactionCodex,
//        val apocryphas: Map<String, Any>,
        val actors: List<ActorID>,
        val player: IngamePlayer,
        val worldGenver: Long,
        val playerGenver: Long,
        val callbackAfterLoad: (TerrarumIngame) -> Unit
    )

    private var loadCallback: ((TerrarumIngame) -> Unit)? = null

    /**
     * Init instance by loading saved world
     */
    private fun enterLoadFromSave(codices: Codices) {
        if (gameInitialised) {
            printdbg(this, "loaded successfully.")
        }
        else {
            printdbg(this, "Ingame setting things up from the savegame")

            RoguelikeRandomiser.loadFromSave(this, codices.world.randSeeds[0], codices.world.randSeeds[1])
            WeatherMixer.loadFromSave(this, codices.world.randSeeds[2], codices.world.randSeeds[3])

//            Terrarum.itemCodex.loadFromSave(codices.item)
//            Terrarum.apocryphas = HashMap(codices.apocryphas)


            // feed info to the worldgen
            Worldgen.attachMap(world, WorldgenParams.getParamsByVersion(codices.worldGenver, world.generatorSeed))
        }

        loadCallback = codices.callbackAfterLoad
        worldGenVer = codices.worldGenver
    }

    /** Load rest of the game with GL context */
    private fun postInitForLoadFromSave(codices: Codices) {
        codices.actors.forEach {
            try {
                val actor = ReadActor(codices.disk, LoadSavegame.getFileReader(codices.disk, it.toLong()))
                if (actor !is IngamePlayer) { // actor list should not contain IngamePlayers (see WriteWorld.preWrite) but just in case...
                    forceAddActor(actor)
                }
            }
            catch (e: NullPointerException) {
                System.err.println("Could not read the actor ${it} from the disk")
                e.printStackTrace()
            }
        }

        printdbg(this, "Player localhash: ${codices.player.localHashStr}, hasSprite: ${codices.player.sprite != null}")

        // assign new random referenceID for player
        codices.player.referenceID = Terrarum.generateUniqueReferenceID()
        forceAddActor(codices.player)


        // overwrite player's props with world's for multiplayer
        // see comments on IngamePlayer.unauthorisedPlayerProps to know why this is necessary.
        codices.player.backupPlayerProps(isMultiplayer) // backup first!
        printdbg(this, "postInitForLoadFromSave")
        printdbg(this, "Player UUID: ${codices.player.uuid}")
        printdbg(this, world.playersLastStatus.keys)
        printdbg(this, world.playersLastStatus[codices.player.uuid])
        world.playersLastStatus[codices.player.uuid].let { // regardless of the null-ness, we still keep the backup, which WriteActor looks for it
            // if the world has some saved values, use them
            if (it != null) {

                printdbg(this, "Found LastStatus mapping for Player ${codices.player.uuid}")
                printdbg(this, "Changing XY Position ${codices.player.hitbox.canonVec} -> ${it.physics.position}")

                codices.player.setPosition(it.physics.position)
                if (isMultiplayer) {
                    printdbg(this, "Using world's ActorValue instead of player's")
                    codices.player.actorValue = it.actorValue!!
                    printdbg(this, "Using world's Inventory instead of player's")
                    codices.player.inventory = it.inventory!!
                }
            }
            // if not, move player to the spawn point
            else {
                printdbg(this, "No mapping found")
                printdbg(this, "Changing XY Position ${codices.player.hitbox.canonVec} -> (${world.spawnX * TILE_SIZED}, ${world.spawnY * TILE_SIZED})")

                codices.player.setPosition((world.portalPoint ?: world.spawnPoint).toVector() * TILE_SIZED)
            }
        }


        // try to unstuck the repositioned player
        codices.player.tryUnstuck()



        // by doing this, whatever the "possession" the player had will be broken by the game load
        actorNowPlaying = codices.player
        actorGamer = codices.player

        SavegameMigrator.invoke(codices.worldGenver, codices.playerGenver, actorContainerActive)

        printdbg(this, "postInitForLoadFromSave exit")
    }

    private val autosaveOnErrorAction = { e: Throwable -> uiAutosaveNotifier.setAsError() }


    private fun postInitForNewGame() {
        worldSavefileName = LoadSavegame.getWorldSavefileName(world)
        playerSavefileName = LoadSavegame.getPlayerSavefileName(actorGamer)

        worldDisk = VDUtil.createNewDisk(
                1L shl 60,
                worldName,
                Common.CHARSET
        )

        playerDisk = VDUtil.readDiskArchive(App.savegamePlayers[actorGamer.uuid]!!.loadable().diskFile, Level.INFO)

        // go to spawn position
        printdbg(this, "World Spawn position: (${world.spawnX}, ${world.spawnY})")
        actorGamer.setPosition(world.spawnPoint.toVector() * TILE_SIZED)
        actorGamer.backupPlayerProps(isMultiplayer)

        // make initial savefile
        // we're not writing multiple files at one go because:
        //  1. lighten the IO burden
        //  2. cannot sync up the "counter" to determine whether both are finished
        uiAutosaveNotifier.setAsOpen()
        val saveTime_t = App.getTIME_T()
        printdbg(this, "Immediate Save")
        WriteSavegame.immediate(saveTime_t, WriteSavegame.SaveMode.PLAYER, playerDisk, getPlayerSaveFiledesc(playerSavefileName), this, true, autosaveOnErrorAction) {
            printdbg(this, "immediate save callback from PLAYER")

            makeSavegameBackupCopy(getPlayerSaveFiledesc(playerSavefileName))

            WriteSavegame.immediate(saveTime_t, WriteSavegame.SaveMode.WORLD, worldDisk, getWorldSaveFiledesc(worldSavefileName), this, true, autosaveOnErrorAction) {
                printdbg(this, "immediate save callback from WORLD")

                makeSavegameBackupCopy(getWorldSaveFiledesc(worldSavefileName)) // don't put it on the postInit() or render(); must be called using callback
                uiAutosaveNotifier.setAsClose()

                App.savegameWorlds[world.worldIndex] = SavegameCollection.collectFromBaseFilename(File(worldsDir), worldSavefileName)
                App.savegameWorldsName[world.worldIndex] = worldName

                loadedTime_t = App.getTIME_T()
            }
        }
    }

    /**
     * Init instance by creating new world
     */
    private fun enterCreateNewWorld(newGameParams: NewGameParams) {

        val player = newGameParams.player
        val worldParams = newGameParams.newWorldParams

        printdbg(this, "Ingame called")
        printStackTrace(this)

        if (gameInitialised) {
            printdbg(this, "loaded successfully.")
        }
        else {
            App.getLoadScreen().addMessage("${App.GAME_NAME} ${App.getVERSION_STRING()}")

            App.getLoadScreen().addMessage("Creating new world")


            // init map as chosen size
            val timeNow = App.getTIME_T()
            world = GameWorld(worldParams.width, worldParams.height, timeNow, timeNow) // new game, so the creation time is right now
            world.generatorSeed = worldParams.worldGenSeed
            //gameworldIndices.add(world.worldIndex)
            world.extraFields["basegame.economy"] = GameEconomy()

            // generate terrain for the map
            //WorldGenerator.attachMap(world)
            //WorldGenerator.SEED = worldParams.worldGenSeed
            //WorldGenerator.generateMap()
            Worldgen.attachMap(world, WorldgenParams.getParamsByVersion(null, worldParams.worldGenSeed))
            Worldgen.generateMap(App.getLoadScreen())


            historicalFigureIDBucket = ArrayList<Int>()

            worldName = worldParams.savegameName


            world.worldCreator = UUID.fromString(player.uuid.toString())

            printdbg(this, "new worldIndex: ${world.worldIndex}")
            printdbg(this, "worldCurrentlyPlaying: ${player.worldCurrentlyPlaying}")

            actorNowPlaying = player
            actorGamer = player
            forceAddActor(player)

            WeatherMixer.internalReset(this)

            UILoadGovernor.worldUUID = world.worldIndex
        }

        KeyToggler.forceSet(Input.Keys.Q, false)

        loadCallback = newGameParams.callbackAfterLoad
        worldGenVer = null
    }

    val ingameController = IngameController(this)

    /** Load rest of the game with GL context */
    fun postInit() {
        actorNowPlaying!! // null check, just in case...


        MegaRainGovernor // invoke MegaRain Governor
        MinimapComposer // invoke MinimapComposer



        // make controls work
        Gdx.input.inputProcessor = ingameController
        if (App.gamepad != null) {
            ingameController.gamepad = App.gamepad
        }

        val drawWidth = Toolkit.drawWidth



        // >- queue up game UIs that should pause the world -<
        uiInventoryPlayer = UIInventoryFull()
        uiInventoryPlayer.setPosition(0, 0)

        // >- lesser UIs -<
        // quick bar
        uiQuickBar = UIQuickslotBar()
        uiQuickBar.isVisible = true
        uiQuickBar.setPosition((drawWidth - uiQuickBar.width) / 2, App.scr.tvSafeGraphicsHeight)

        // pie menu
        uiPieMenu = UIQuickslotPie()
        uiPieMenu.setPosition(hdrawWidth, App.scr.halfh)

        // vital metre
        // fill in getter functions by
        //      (uiAliases[UI_QUICK_BAR]!!.UI as UIVitalMetre).vitalGetterMax = { some_function }
        //uiVitalPrimary = UIVitalMetre(player, { 80f }, { 100f }, Color.red, 2, customPositioning = true)
        //uiVitalPrimary.setAsAlwaysVisible()
        //uiVitalSecondary = UIVitalMetre(player, { 73f }, { 100f }, Color(0x00dfff), 1) customPositioning = true)
        //uiVitalSecondary.setAsAlwaysVisible()
        //uiVitalItem = UIVitalMetre(player, { null }, { null }, Color(0xffcc00), 0, customPositioning = true)
        //uiVitalItem.setAsAlwaysVisible()

        // fake UI for blurring the background
        uiBlur = UIFakeBlurOverlay(1f, true)
        uiBlur.setPosition(0,0)

        uiWatchTierOne = UIWatchLargeAnalogue()
//        uiWatchTierOne = UIWatchLargeDigital()
        uiWatchTierOne.setAsAlwaysVisible()
        uiWatchTierOne.setPosition(
                ((drawWidth - App.scr.tvSafeActionWidth) - (uiQuickBar.posX + uiQuickBar.width) - uiWatchTierOne.width) / 2 + (uiQuickBar.posX + uiQuickBar.width),
                App.scr.tvSafeGraphicsHeight + 8
        )

        // basic watch-style notification bar (temperature, new mail)
        uiBasicInfo = UIBasicInfo()
        uiBasicInfo.setAsAlwaysVisible()
        uiBasicInfo.setPosition((uiQuickBar.posX - uiBasicInfo.width - App.scr.tvSafeActionWidth) / 2 + App.scr.tvSafeActionWidth, uiWatchTierOne.posY)


        uiCheatDetected = UICheatDetected()


        // batch-process uiAliases
        // NOTE: UIs that should pause the game (e.g. Inventory) must have relevant codes ON THEIR SIDE
        uiContainer.add(
            // drawn first
            //uiVitalPrimary,
            //uiVitalSecondary,
            //uiVitalItem,
            noticelet,

            uiBlur,

            uiPieMenu,
            uiQuickBar,
//            uiBasicInfo, // temporarily commenting out: wouldn't make sense for v 0.3 release
            uiWatchTierOne,
            getWearableDeviceUI,
            UIScreenZoom(),
            uiAutosaveNotifier,

            uiInventoryPlayer,
            getUIFixture,
            uiTooltip,
            consoleHandler,
            uiPaused,
            uiCheatDetected,
            // drawn last
        )


        ingameUpdateThread = ThreadIngameUpdate(this)
        updateThreadWrapper = Thread(ingameUpdateThread, "Terrarum UpdateThread")

        // add extra UIs from the other modules
        ModMgr.GameExtraGuiLoader.guis.forEach {
            uiContainer.add(it(this))
        }

        ModMgr.GameWatchdogLoader.watchdogs.forEach {
            registerWatchdog(it.key, it.value)
        }

        // these need to appear on top of any others
        uiContainer.add(notifier)

        App.setDebugTime("Ingame.UpdateCounter", 0)

        // some sketchy test code here

        KeyToggler.forceSet(Input.Keys.F2, false)


        if (loadCallback != null) {
            try {
                loadCallback!!(this)
            }
            catch (e: Throwable) {
                e.printStackTrace()
            }
            loadCallback = null
        }
    }// END enter



    private var worldPrimaryClickLatch = false
    private var worldSecondaryClickLatch = false

    private fun fireFixtureInteractEvent(fixture: FixtureBase, mwx: Double, mwy: Double) {
        if (fixture.mouseUp) {
            fixture.onInteract(mwx, mwy)
        }
    }

    // left click: use held item, attack, pick up fixture if i'm holding a pickaxe or hammer (aka tool), do 'bare hand action' if holding nothing
    override fun worldPrimaryClickStart(actor: ActorWithBody, delta: Float) {
        //println("[Ingame] worldPrimaryClickStart $delta")

        // prepare some variables

        val itemOnGrip = ItemCodex[(actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)]
        // bring up the UIs of the fixtures (e.g. crafting menu from a crafting table)
        var uiOpened = false
        val fixtureUnderMouse0: List<FixtureBase> = getActorsUnderMouse(Terrarum.mouseX, Terrarum.mouseY).filterIsInstance<FixtureBase>()
        if (fixtureUnderMouse0.size > 1) {
            App.printdbgerr(this, "Multiple fixtures at tile coord ${Terrarum.mouseX / TILE_SIZED}, ${Terrarum.mouseY / TILE_SIZED}: [${fixtureUnderMouse0.map { it.javaClass.simpleName }.joinToString()}]")
        }
        val fixtureUnderMouse = fixtureUnderMouse0.firstOrNull()

        ////////////////////////////////

        mouseInInteractableRange(actor) { mwx, mwy, mtx, mty ->
            // #1. interact with the fixture if NOT FixtureInteractionBlocked
            // scan for the one with non-null UI.
            // what if there's multiple of such fixtures? whatever, you are supposed to DISALLOW such situation.
            if (fixtureUnderMouse != null && itemOnGrip !is FixtureInteractionBlocked) {
                if (!worldPrimaryClickLatch) {
                    worldPrimaryClickLatch = true
                    fixtureUnderMouse.let { fixture ->
                        fixture.mainUI?.let { ui ->
                            uiOpened = true

                            // property 'uiFixture' is a dedicated property that the TerrarumIngame recognises.
                            // when it's not null, the UI will be updated and rendered
                            // when the UI is closed, it'll be replaced with a null value
                            uiFixture = ui
                            ui.setPosition(
                                (Toolkit.drawWidth - ui.width) / 4,
                                (App.scr.height - ui.height) / 4 // what the fuck?
                            )
                            ui.setAsOpen()
                        }

                        if (!uiOpened) {
                            fireFixtureInteractEvent(fixture, mwx, mwy)
                        }
                    }
                }
            }
            // #2. If no fixture under mouse or FixtureInteractionBlocked, use the item
            else if (itemOnGrip != null) {
                // click filtering (latch stuff) is handled by IngameController (see inventoryCategoryAllowClickAndDrag)
                // To disable click dragging for tool/block/etc., put `override val disallowToolDragging = true` to the item's code
                val consumptionSuccessful = itemOnGrip.startPrimaryUse(actor, delta)
                if (consumptionSuccessful > -1)
                    (actor as Pocketed).inventory.consumeItem(itemOnGrip, consumptionSuccessful)

                worldPrimaryClickLatch = true
            }
            // #3. If not holding any item and can do barehandaction (size big enough that barehandactionminheight check passes), do it
            else {
                performBarehandAction(actor, delta, mwx, mwy, mtx, mty)
            }

            0L
        }
    }

    override fun worldPrimaryClickEnd(actor: ActorWithBody, delta: Float) {
        val canPerformBarehandAction = actor.scale * actor.baseHitboxH >=
                                       (actor.actorValue.getAsDouble(AVKey.BAREHAND_MINHEIGHT) ?: 4294967296.0)

        val itemOnGrip = (actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)
        ItemCodex[itemOnGrip]?.endPrimaryUse(actor, delta)

        if (canPerformBarehandAction) {
            endPerformBarehandAction(actor)
        }

        worldPrimaryClickLatch = false
    }

    override fun worldSecondaryClickStart(actor: ActorWithBody, delta: Float) {
        val itemOnGrip = ItemCodex[(actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)]

        if (!worldSecondaryClickLatch) {
            // #1. Try to pick up the fixture or dropped item first (if in range and there's something to pick up)
            val pickupSuccessful = mouseInInteractableRange(actor) { mwx, mwy, mtx, mty ->
                val actorsUnderMouse = getActorsUnderMouse(mwx, mwy)
                val hasPickupableActor = actorsUnderMouse.any {
                    (it is FixtureBase && it.canBeDespawned && System.nanoTime() - it.spawnRequestedTime > 50000000) || // give freshly spawned fixtures 0.05 seconds of immunity
                    (it is DroppedItem && it.noAutoPickup)
                }
                if (hasPickupableActor) {
                    pickupFixtureOrDroppedItem(actor, delta, mwx, mwy, mtx, mty, assignToQuickslot = (itemOnGrip == null))
                    0L
                }
                else {
                    -1L
                }
            }

            // #2. If pickup didn't happen, try to perform item's secondaryUse
            if (pickupSuccessful < 0) {
                val consumptionSuccessful = itemOnGrip?.startSecondaryUse(actor, delta) ?: -1
                if (consumptionSuccessful > -1)
                    (actor as Pocketed).inventory.consumeItem(itemOnGrip!!, consumptionSuccessful)
            }

            worldSecondaryClickLatch = true
        }
    }

    override fun worldSecondaryClickEnd(actor: ActorWithBody, delta: Float) {
        val itemOnGrip = (actor as Pocketed).inventory.itemEquipped.get(GameItem.EquipPosition.HAND_GRIP)
        ItemCodex[itemOnGrip]?.endSecondaryUse(actor, delta)

        worldSecondaryClickLatch = false
    }



    private var firstTimeRun = true

    ///////////////
    // prod code //
    ///////////////
    private class ThreadIngameUpdate(val terrarumIngame: TerrarumIngame): Runnable {
        override fun run() {
            TODO()
        }
    }

    internal var autosaveTimer: Second = 0f

    override fun renderImpl(updateRate: Float) {
        // Q&D solution for LoadScreen and Ingame, where while LoadScreen is working, Ingame now no longer has GL Context
        // there's still things to load which needs GL context to be present
        if (!gameFullyLoaded) {

            uiAutosaveNotifier = UIAutosaveNotifier()

            if (gameLoadMode == GameLoadMode.CREATE_NEW) {
                postInitForNewGame()
            }
            else if (gameLoadMode == GameLoadMode.LOAD_FROM) {
                postInitForLoadFromSave(gameLoadInfoPayload as Codices)
            }

            postInit()

            gameUpdateGovernor.reset()

            if (UILoadGovernor.previousSaveWasLoaded) {
                sendNotification(listOf(Lang["GAME_PREV_SAVE_WAS_LOADED1"], Lang["GAME_PREV_SAVE_WAS_LOADED2"]))
            }

            gameFullyLoaded = true
        }
        else {
            IngameRenderer.setRenderedWorld(world) // this doesn't slow down the game and prevents world-changing related bugs
        }

        super.renderImpl(updateRate)

        ingameController.update()


        // ASYNCHRONOUS UPDATE AND RENDER //

        /** UPDATE CODE GOES HERE */
        val dt = Gdx.graphics.deltaTime
        if (!uiAutosaveNotifier.isVisible) {
            autosaveTimer += dt
        }

        gameUpdateGovernor.update(dt, App.UPDATE_RATE, updateGame, renderGame)


        val autosaveInterval = App.getConfigInt("autosaveinterval").coerceAtLeast(60000) / 1000f
        if (autosaveTimer >= autosaveInterval) {
            queueAutosave()
            autosaveTimer -= autosaveInterval
        }

    }

    private var worldWidth: Double = 0.0
    private var oldCamX = 0
    private var oldPlayerX = 0.0

    private var deltaTeeCleared = false

    private val terrarumWorldWatchdogs = TreeMap<String, TerrarumWorldWatchdog>()

    fun registerWatchdog(identifier: String, watchdog: TerrarumWorldWatchdog) {
        terrarumWorldWatchdogs[identifier] = watchdog
    }

    /**
     * Ingame (world) related updates; UI update must go to renderGame()
     */
    private val updateGame =  { delta: Float ->
        val world = this.world
        worldWidth = world.width.toDouble() * TILE_SIZE

        particlesActive = 0


        // synchronised Ingame Input Updater
        // will also queue up the block/wall/wire placed events
        ingameController.update()

        if ((!paused && !App.isScreenshotRequested()) || newWorldLoadedLatch) {

            //hypothetical_input_capturing_function_if_you_finally_decided_to_forgo_gdx_input_processor_and_implement_your_own_to_synchronise_everything()


            ////////////////////////////
            // camera-related updates //
            ////////////////////////////
            FeaturesDrawer.update(delta)

            ///////////////////////////
            // actor-related updates //
            ///////////////////////////
            repossessActor()

            // process actor addition requests
            val addCueCpy = actorAdditionQueue.toList()
            addCueCpy.forEach {
                forceAddActor(it.first, it.second)
                it.third(it.first)
            }
            actorAdditionQueue.removeAll(addCueCpy)
            // determine whether the inactive actor should be activated
            wakeDormantActors()
            // update NOW; allow one last update for the actors flagged to despawn
            updateActors(delta)
            // determine whether the actor should keep being activated or be dormant
            killOrKnockdownActors()
            // process actor removal requests
            val remCueCpy = actorRemovalQueue.toList()
            remCueCpy.forEach {
                forceRemoveActor(it.first, it.second)
                it.third(it.first)
            }
            actorRemovalQueue.removeAll(remCueCpy)
            // update particles
            particlesContainer.toList().forEach { if (!it.flagDespawn) particlesActive++; it.update(delta) }
            // TODO thread pool(?)
            CollisionSolver.process()


            ///////////////////////////
            // world-related updates //
            ///////////////////////////
            actorsRTree = PRTree(actorMBRConverter, 24)
            actorsRTree.load(actorContainerActive.filterIsInstance<ActorWithBody>())

            BlockPropUtil.dynamicLumFuncTickClock()
            world.updateWorldTime(delta)
            measureDebugTime("WorldSimulator.update") {
                WorldSimulator.invoke(actorNowPlaying, delta)
            }
            measureDebugTime("WeatherMixer.update") {
                WeatherMixer.update(delta, actorNowPlaying, world)
            }
            measureDebugTime("BlockStats.update") {
                TileSurvey.update()
            }
            // fill up visibleActorsRenderFront for wires but not on every update
            measureDebugTime("Ingame.FillUpWiresBuffer*") {
                if (WORLD_UPDATE_TIMER % 2 == 1L) {
                    fillUpWiresBuffer()
                }
            }
            measureDebugTime("Ingame.FillUpWirePortsView*") {
                if (WORLD_UPDATE_TIMER % 2 == 0L) {
                    val fixtures = INGAME.actorContainerActive.filterIsInstance<Electric>()
                    fillUpWirePortsView(fixtures)
                }
            }
            terrarumWorldWatchdogs.entries.forEach {
                measureDebugTime("Ingame.Watchdog.${it.key}*") {
                    if (WORLD_UPDATE_TIMER % it.value.runIntervalByTick.toLong() == 0L) {
                        it.value(world)
                    }
                }
            }
            oldCamX = WorldCamera.x
            oldPlayerX = actorNowPlaying?.hitbox?.canonicalX ?: 0.0

            // update audio mixer
            val ratio = (TileSurvey.getRatio("basegame.Ingame.audioReflection") ?: 0.0)
            if (ratio >= 0.0) {
                val ratio1 = ratio.coerceIn(0.0, 1.0)
                App.audioMixer.convolveBusCave.volume = ratio1
                App.audioMixer.convolveBusOpen.volume = 1.0 - ratio1
            }
            else {
                val ratio1 = (ratio / MaterialCodex["AIIR"].sondrefl).absoluteValue.coerceIn(0.0, 1.0)
                App.audioMixer.convolveBusOpen.volume = (1.0 - ratio1).pow(0.75)
                App.audioMixer.convolveBusCave.volume = 0.0
            }
            val openness = (TileSurvey.getRatio("basegame.Ingame.openness") ?: 0.0).times(1.74).coerceIn(0.0, 1.0)
            App.audioMixer.amb1plus2.volume = openness.pow(2.0 / 3.0)



            actorNowPlaying?.let { if (WORLD_UPDATE_TIMER % 4 == 1L) updateWorldGenerator(actorNowPlaying!!) }



            WORLD_UPDATE_TIMER += 1

            // run benchmark if F2 is on
            if (KeyToggler.isOn(Input.Keys.F2)) {
                deltaTeeBenchmarks.appendHead(1f / Gdx.graphics.deltaTime)

                if (deltaTeeCleared) deltaTeeCleared = false
            }
            else if (!deltaTeeCleared) {
                deltaTeeCleared = true
                deltaTeeBenchmarks.clear()
            }
        }


        if ((!paused && !App.isScreenshotRequested()) || newWorldLoadedLatch) {
            // completely consume block change queues because why not
            terrainChangeQueue.clear()
            wallChangeQueue.clear()
            wireChangeQueue.clear()

            oldSelectedWireRenderClass = selectedWireRenderClass
        }

        musicStreamer.update(this, delta)
        MusicService.update(delta)

        ////////////////////////
        // ui-related updates //
        ////////////////////////
        // open/close fake blur UI according to what's opened
        if (uiInventoryPlayer.isVisible ||
            getUIFixture.get()?.isVisible == true || worldTransitionOngoing) {
            uiBlur.setAsOpen()
        }
        else {
            uiBlur.setAsClose()
        }

        // update debuggers using javax.swing //
        if (Authenticator.b()) {
            AVTracker.update()
            ActorsList.update()
        }

        //println("paused = $paused")

        if ((!paused && !App.isScreenshotRequested()) && newWorldLoadedLatch) newWorldLoadedLatch = false


        if (doThingsAfterSave) {
            saveRequested2 = false
            doThingsAfterSave = false
            saveCallback!!()
        }

        if (saveRequested2) {
            saveRequested2 = false
            doForceSave()
        }

        if (worldTransitionPauseRequested > 0) { // let a frame to update before locking (=pausing) entirely
            worldTransitionPauseRequested -= 1
        }
        else if (worldTransitionPauseRequested == 0) {
            paused = true
        }


        if (KeyToggler.isOn(Input.Keys.F10)) {
            batch.inUse {
                batch.color = Color.WHITE
                App.fontSmallNumbers.draw(batch, "$ccY\u00DCupd $ccG${delta.toIntAndFrac(1)}", 2f, App.scr.height - 16f)
                _dbgDeltaUpd = delta
            }
        }
    }

    private var _dbgDeltaUpd = 0f


    private val renderGame = { frameDelta: Float ->
        Gdx.graphics.setTitle(getCanonicalTitle())

        WorldCamera.update(world, actorNowPlaying)

        measureDebugTime("Ingame.FilterVisibleActors") {
            filterVisibleActors()
        }

        if (uiInventoryPlayer == uiFixture) throw IllegalStateException("Do NOT use InventoryPlayer as a UIFixture, the engine cannot handle this situation XwX")

        uiContainer.forEach {

            // suppress inventory opening if fixture inventory is opened
            if (uiFixture?.isClosed == false)
                uiInventoryPlayer.handler.lockToggle()
            else
                uiInventoryPlayer.handler.unlockToggle()


            when (it) {
                is Id_UICanvasNullable -> it.get()?.update(Gdx.graphics.deltaTime)
                is UICanvas -> it.update(Gdx.graphics.deltaTime)
            }
        }
        //uiFixture?.update(Gdx.graphics.deltaTime)
        // deal with the uiFixture being closed
        if (uiFixture?.isClosed == true) { uiFixture = null }

        IngameRenderer.invoke(
            frameDelta,
            paused,
            screenZoom,
            visibleActorsRenderFarBehind,
            visibleActorsRenderBehind,
            visibleActorsRenderMiddle,
            visibleActorsRenderMidTop,
            visibleActorsRenderFront,
            visibleActorsRenderOverlay,
            particlesContainer,
            actorNowPlaying,
            uiContainer// + uiFixture
        )

        // quick and dirty way to show
        if (worldTransitionOngoing) {
            batch.inUse {
                batch.color = gradEndCol
                Toolkit.fillArea(batch, 0, 0, App.scr.width, App.scr.height)


                batch.color = Color.WHITE
                val t = Lang["MENU_IO_SAVING"]
                val circleSheet = CommonResourcePool.getAsTextureRegionPack("loading_circle_64")
                Toolkit.drawTextCentered(batch, App.fontGame, t, Toolkit.drawWidth, 0, ((App.scr.height - circleSheet.tileH) / 2) - 40)

                // -1..63
                val index =
                    ((WriteSavegame.saveProgress / WriteSavegame.saveProgressMax) * circleSheet.horizontalCount * circleSheet.verticalCount).roundToInt() - 1
                if (index >= 0) {
                    val sx = index % circleSheet.horizontalCount
                    val sy = index / circleSheet.horizontalCount
                    // q&d fix for ArrayIndexOutOfBoundsException caused when saving huge world... wut?
                    if (sx in 0 until circleSheet.horizontalCount && sy in 0 until circleSheet.horizontalCount) {
                        batch.draw(
                            circleSheet.get(sx, sy),
                            ((Toolkit.drawWidth - circleSheet.tileW) / 2).toFloat(),
                            ((App.scr.height - circleSheet.tileH) / 2).toFloat()
                        )
                    }
                }
            }
        }


        if (KeyToggler.isOn(Input.Keys.F10)) {
            batch.inUse {
                batch.color = Color.WHITE
                App.fontSmallNumbers.draw(batch, "$ccY\u00DCupd $ccG${_dbgDeltaUpd.toIntAndFrac(1)}", 2f, App.scr.height - 16f)
                App.fontSmallNumbers.draw(batch, "$ccY\u00DCren $ccG${frameDelta.toIntAndFrac(1)}", 2f + 7*12, App.scr.height - 16f)
                App.fontSmallNumbers.draw(batch, "$ccY\u00DCgdx $ccG${Gdx.graphics.deltaTime.toIntAndFrac(1)}", 2f + 7*24, App.scr.height - 16f)
            }
        }
    }

    private fun Point2iMod(x: Int, y: Int) = Point2i(x fmod (world.width / CHUNK_W), y)

    private fun updateWorldGenerator(actor: ActorWithBody) {
        val pcx = (actor.intTilewiseHitbox.canonicalX.toInt() fmod world.width) / CHUNK_W
        val pcy = (actor.intTilewiseHitbox.canonicalY.toInt() fmod world.width) / CHUNK_H
        listOf(
            Point2iMod(pcx - 1, pcy - 2), Point2iMod(pcx, pcy - 2), Point2iMod(pcx + 1, pcy - 2),
            Point2iMod(pcx - 2, pcy - 1), Point2iMod(pcx - 1, pcy - 1), Point2iMod(pcx, pcy - 1), Point2iMod(pcx + 1, pcy - 1),  Point2iMod(pcx + 2, pcy - 1),
            Point2iMod(pcx - 2, pcy), Point2iMod(pcx - 1, pcy), Point2iMod(pcx + 1, pcy),  Point2iMod(pcx + 2, pcy),
            Point2iMod(pcx - 2, pcy + 1), Point2iMod(pcx - 1, pcy + 1), Point2iMod(pcx, pcy + 1), Point2iMod(pcx + 1, pcy + 1),  Point2iMod(pcx + 2, pcy + 1),
            Point2iMod(pcx - 1, pcy + 2), Point2iMod(pcx, pcy + 2), Point2iMod(pcx + 1, pcy + 2),
        ).filter { it.y in 0 until world.height }.filter {  (cx, cy) ->
            if (cy !in 0 until world.height / CHUNK_H) false
            else (world.chunkFlags[cy][cx].and(0x7F) == 0.toByte())
        }.forEach { (cx, cy) ->
            Worldgen.generateChunkIngame(cx, cy) { cx, cy ->
                listOf(0,1,2).forEach { layer ->
                    modified(layer, cx, cy)
                }
            }
        }
    }

    private var worldTransitionOngoing = false
    private var worldTransitionPauseRequested = -1
    private var saveRequested2 = false
    private var saveCallback: (() -> Unit)? = null
    private var doThingsAfterSave = false

    override fun requestForceSave(callback: () -> Unit) {
        saveCallback = callback
        worldTransitionOngoing = true
        saveRequested2 = true
        worldTransitionPauseRequested = 1
        blockMarkingActor.isVisible = false
    }

    internal fun doForceSave() {
        // TODO show appropriate UI
//        uiBlur.setAsOpen()

        saveTheGame({ // onSuccessful
            System.gc()
            autosaveTimer = 0f

            // TODO hide appropriate UI
            uiBlur.setAsClose()
            doThingsAfterSave = true
        }, { // onError
            // TODO show failure message

            // TODO hide appropriate UI
            uiBlur.setAsClose()
        })
    }

    override fun saveTheGame(onSuccessful: () -> Unit, onError: (Throwable) -> Unit) {
        val saveTime_t = App.getTIME_T()
        val playerSavefile = getPlayerSaveFiledesc(INGAME.playerSavefileName)
        val worldSavefile = getWorldSaveFiledesc(INGAME.worldSavefileName)


        INGAME.makeSavegameBackupCopy(playerSavefile)
        WriteSavegame(saveTime_t, WriteSavegame.SaveMode.PLAYER, INGAME.playerDisk, playerSavefile, INGAME as TerrarumIngame, false, onError) {

            INGAME.makeSavegameBackupCopy(worldSavefile)
            WriteSavegame(saveTime_t, WriteSavegame.SaveMode.WORLD, INGAME.worldDisk, worldSavefile, INGAME as TerrarumIngame, false, onError) {
                // callback:
                // rebuild the disk skimmers
                INGAME.actorContainerActive.filterIsInstance<IngamePlayer>().forEach {
                    printdbg(this, "Game Save callback -- rebuilding the disk skimmer for IngamePlayer ${it.actorValue.getAsString(AVKey.NAME)}")
//                                it.rebuildingDiskSkimmer?.rebuild()
                }

                // return to normal state
                onSuccessful()

                loadedTime_t = App.getTIME_T()
            }
        }
    }

    private val maxRenderableWires = ReferencingRanges.ACTORS_WIRES.last - ReferencingRanges.ACTORS_WIRES.first + 1
    private val maxRenderablePorts = ReferencingRanges.ACTORS_WIRE_PORTS.last - ReferencingRanges.ACTORS_WIRE_PORTS.first + 1
    private val wireActorsContainer = Array(maxRenderableWires) { WireActor(ReferencingRanges.ACTORS_WIRES.first + it).let {
        forceAddActor(it)
        /*^let*/ it
    } }
    private val wirePortActorsContainer = Array(maxRenderablePorts) { WirePortActor(ReferencingRanges.ACTORS_WIRE_PORTS.first + it).let {
        forceAddActor(it)
        /*^let*/ it
    } }

    private fun fillUpWiresBuffer() {
        val for_y_start = (WorldCamera.y.toFloat() / TILE_SIZE).floorToInt() - 1
        val for_y_end = for_y_start + BlocksDrawer.tilesInVertical + 2*1

        val for_x_start = (WorldCamera.x.toFloat() / TILE_SIZE).floorToInt() - 1
        val for_x_end = for_x_start + BlocksDrawer.tilesInHorizontal + 2*1

        var wiringCounter = 0
        for (y in for_y_start..for_y_end) {
            for (x in for_x_start..for_x_end) {
                if (wiringCounter >= maxRenderableWires) break

                val (wires, nodes) = world.getAllWiresFrom(x, y)

                wires?.forEach {
                    val wireActor = wireActorsContainer[wiringCounter]

                    wireActor.setWire(it, x, y, nodes!![it]!!.cnx)

                    if (WireCodex[it].renderClass == selectedWireRenderClass || selectedWireRenderClass == "wire_render_all") {
                        wireActor.renderOrder = Actor.RenderOrder.OVERLAY
                    }
                    else {
                        wireActor.renderOrder = Actor.RenderOrder.FAR_BEHIND
                    }

                    wireActor.isUpdate = true
                    wireActor.isVisible = true
                    wireActor.forceDormant = false

                    wiringCounter += 1
                }

            }
        }

        for (i in wiringCounter until maxRenderableWires) {
            val wireActor = wireActorsContainer[i]
            wireActor.isUpdate = false
            wireActor.isVisible = false
            wireActor.forceDormant = true
        }
    }

    private fun fillUpWirePortsView(fixtures: List<Electric>) {
        var portsCounter = 0

        fixtures.forEach {
            (it.wireSinkTypes.toList() + it.wireEmitterTypes.toList()).forEach { (boxIndex, type) ->
                if (portsCounter < wirePortActorsContainer.size) {
                    val wireActor = wirePortActorsContainer[portsCounter]
                    val (wx, wy) = it.worldBlockPos!! + it.blockBoxIndexToPoint2i(boxIndex)

                    wireActor.setPort(type, wx, wy)
                    wireActor.isUpdate = true
                    wireActor.isVisible = true
                    wireActor.forceDormant = false

                    portsCounter += 1
                }
            }
        }



        for (i in portsCounter until maxRenderablePorts) {
            val wireActor = wirePortActorsContainer[i]
            wireActor.isUpdate = false
            wireActor.isVisible = false
            wireActor.forceDormant = true
        }
    }

    private fun filterVisibleActors() {
        visibleActorsRenderFarBehind.clear()
        visibleActorsRenderBehind.clear()
        visibleActorsRenderMiddle.clear()
        visibleActorsRenderMidTop.clear()
        visibleActorsRenderFront.clear()
        visibleActorsRenderOverlay.clear()

        actorContainerActive.forEach {
            if (it is ActorWithBody)
                actorToRenderQueue(it).add(it)
        }
    }

    private fun repossessActor() {
        // check if currently pocessed actor is removed from game
        if (!theGameHasActor(actorNowPlaying)) {
            // re-possess canonical player
            if (theGameHasActor(actorGamer))
                changePossession(actorGamer)
            else
                actorNowPlaying = null
        }
    }

    internal fun changePossession(newActor: ActorHumanoid) {
        if (!theGameHasActor(actorNowPlaying)) { throw NoSuchActorWithRefException(newActor) }

        actorNowPlaying = newActor
    }

    internal fun changePossession(refid: Int) {
        val actorToChange = getActorByID(refid)

        if (actorToChange !is ActorHumanoid) {
            throw Error("Unpossessable actor $refid: type expected ActorHumanoid, got ${actorToChange.javaClass.canonicalName}")
        }

        changePossession(getActorByID(refid) as ActorHumanoid)
    }

    fun wakeDormantActors() {
        var actorContainerSize = actorContainerInactive.size
        var i = 0
        while (i < actorContainerSize) { // loop through actorContainerInactive
            val actor = actorContainerInactive[i]
            if (actor is ActorWithBody && (actor.chunkAnchoring || actor.inUpdateRange(world)) && !actor.forceDormant) {
                activateDormantActor(actor) // duplicates are checked here
                actorContainerSize -= 1
                i-- // array removed 1 elem, so we also decrement counter by 1
            }
            i++
        }
    }

    /**
     * determine whether the actor should be active or dormant by its distance from the player.
     * If the actor must be dormant, the target actor will be put to the list specifically for them.
     * if the actor is not to be dormant, it will be just ignored.
     */
    fun killOrKnockdownActors() {
        var actorContainerSize = actorContainerActive.size
        var i = 0
        while (i < actorContainerSize) { // loop through actorContainerActive
            val actor = actorContainerActive[i]
            val actorIndex = i
            // kill actors flagged to despawn
            if (actor.despawned) {
                queueActorRemoval(actor)
                actorContainerSize -= 1
                i-- // array removed 1 elem, so we also decrement counter by 1
            }
            // inactivate distant actors
            else if (actor is ActorWithBody && (!actor.chunkAnchoring && !actor.inUpdateRange(world) || actor.forceDormant)) {
                if (actor !is Projectile) { // if it's a projectile, don't inactivate it; just kill it.
                    actorContainerInactive.add(actor) // naïve add; duplicates are checked when the actor is re-activated
                }
                actorContainerActive.removeAt(actorIndex)
                actorContainerSize -= 1
                i-- // array removed 1 elem, so we also decrement counter by 1
            }
            i++
        }
    }

    /**
     * Update actors concurrently.
     *
     * NOTE: concurrency for actor updating is currently disabled because of it's poor performance
     */
    fun updateActors(delta: Float) {
        if (false) { // don't multithread this for now, it's SLOWER //if (Terrarum.MULTITHREAD && actorContainerActive.size > Terrarum.THREADS) {
            /*ThreadExecutor.renew()

            val actors = actorContainerActive.size.toFloat()
            // set up indices
            for (i in 0..App.THREAD_COUNT - 1) {
                ThreadExecutor.submit(
                        ThreadActorUpdate(
                                actors.div(App.THREAD_COUNT).times(i).roundToInt(),
                                actors.div(App.THREAD_COUNT).times(i + 1).roundToInt() - 1
                        )
                )
            }

            ThreadExecutor.join()

            actorNowPlaying?.update(delta)*/
        }
        else {
            // Pass 1: update moving platforms first so riders get displaced before their own update
            actorContainerActive.forEach {
                if (it is ActorMovingPlatform && it != actorNowPlaying) {
                    it.update(delta)
                }
            }

            // Pass 2: update all non-platform actors with existing callbacks
            actorContainerActive.forEach {
                if (it !is ActorMovingPlatform && it != actorNowPlaying) {
                    it.update(delta)

                    if (it is Pocketed) {
                        it.inventory.forEach { inventoryEntry ->
                            ItemCodex[inventoryEntry.itm]!!.effectWhileInPocket(it as ActorWithBody, delta) // kind of an error checking because all Pocketed must be ActorWithBody
                            if (it.equipped(inventoryEntry.itm)) {
                                ItemCodex[inventoryEntry.itm]!!.effectWhileEquipped(it as ActorWithBody, delta)
                            }
                        }
                    }

                    if (it is CuedByTerrainChange) {
                        terrainChangeQueue.toList().forEach { cue ->
//                            printdbg(this, "Ingame actors terrainChangeCue: ${cue}")
                            if (cue != null) it.updateForTerrainChange(cue)
                        }
                    }

                    if (it is CuedByWallChange) {
                        wallChangeQueue.toList().forEach { cue ->
//                            printdbg(this, "Ingame actors wallChangeCue: ${cue}")
                            if (cue != null) it.updateForWallChange(cue)
                        }
                    }

                    if (it is CuedByWireChange) {
                        wireChangeQueue.toList().forEach { cue ->
//                            printdbg(this, "Ingame actors wireChangeCue: ${cue}")
                            if (cue != null) it.updateForWireChange(cue)
                        }
                    }
                }
            }

            // Pass 3: update player
            actorNowPlaying?.update(delta)
            //AmmoMeterProxy(player, uiVitalItem.UI as UIVitalMetre)
        }
    }

    fun queueAutosave() {
        val start = System.nanoTime()

        uiAutosaveNotifier.setAsOpen()

        val saveTime_t = App.getTIME_T()
        val playerSavefile0 = getPlayerSaveFiledesc(INGAME.playerSavefileName)
        val worldSavefile0 = getWorldSaveFiledesc(INGAME.worldSavefileName)

        val playerSavefile = INGAME.makeSavegameBackupCopyAuto(playerSavefile0)
        WriteSavegame(saveTime_t, WriteSavegame.SaveMode.PLAYER, INGAME.playerDisk, playerSavefile, INGAME as TerrarumIngame, true, autosaveOnErrorAction) {

            val worldSavefile = INGAME.makeSavegameBackupCopyAuto(worldSavefile0)
            WriteSavegame(saveTime_t, WriteSavegame.SaveMode.QUICK_WORLD, INGAME.worldDisk, worldSavefile, INGAME as TerrarumIngame, true, autosaveOnErrorAction) {
                // callback:
                // rebuild the disk skimmers
                INGAME.actorContainerActive.filterIsInstance<IngamePlayer>().forEach {
                    printdbg(this, "Game Save callback -- rebuilding the disk skimmer for IngamePlayer ${it.actorValue.getAsString(AVKey.NAME)}")
//                    it.rebuildingDiskSkimmer?.rebuild()
                }

                // return to normal state
                uiAutosaveNotifier.setAsClose()
                autosaveTimer = 0f

                val timeDiff = System.nanoTime() - start
                debugTimers.put("Last Autosave Duration", timeDiff)
                printdbg(this, "Last Autosave Duration: ${(timeDiff) / 1000000000} s")


                loadedTime_t = App.getTIME_T()
            }
        }

    }


    fun Double.sqr() = this * this
    fun Int.sqr() = this * this
    fun min(vararg d: Double): Double {
        var ret = Double.MAX_VALUE
        d.forEach { if (it < ret) ret = it }
        return ret
    }


    private val cameraWindowX = WorldCamera.x.toDouble()..WorldCamera.xEnd.toDouble()
    private val cameraWindowY = WorldCamera.y.toDouble()..WorldCamera.yEnd.toDouble()

    private fun actorToRenderQueue(actor: ActorWithBody): ArrayList<ActorWithBody> {
        return when (actor.renderOrder) {
            Actor.RenderOrder.FAR_BEHIND -> visibleActorsRenderFarBehind
            Actor.RenderOrder.BEHIND -> visibleActorsRenderBehind
            Actor.RenderOrder.MIDDLE -> visibleActorsRenderMiddle
            Actor.RenderOrder.MIDTOP -> visibleActorsRenderMidTop
            Actor.RenderOrder.FRONT  -> visibleActorsRenderFront
            Actor.RenderOrder.OVERLAY-> visibleActorsRenderOverlay
        }
    }


    override fun forceRemoveActor(actor: Actor, caller: Throwable) {
        arrayOf(actorContainerActive, actorContainerInactive).forEach { actorContainer ->
            val indexToDelete = actorContainer.searchForIndex(actor.referenceID) { it.referenceID }
            if (indexToDelete != null) {
//                printdbg(this, "Removing actor $actor")
//                printStackTrace(this)

                actor.dispose()
                actorContainer.removeAt(indexToDelete)

                // indexToDelete >= 0 means that the actor certainly exists in the game
                // which means we don't need to check if i >= 0 again
                if (actor is ActorWithBody) {
                    actorToRenderQueue(actor).remove(actor)
                }
            }
        }
    }

    private fun ArrayList<*>.binarySearch(actor: Actor) = this.binarySearch(actor.referenceID)

    private fun ArrayList<*>.binarySearch(ID: Int): Int {
        // code from collections/Collections.kt
        var low = 0
        var high = this.size - 1

        while (low <= high) {
            val mid = (low + high).ushr(1) // safe from overflows

            val midVal = get(mid)!!

            if (ID > midVal.hashCode())
                low = mid + 1
            else if (ID < midVal.hashCode())
                high = mid - 1
            else
                return mid // key found
        }
        return -(low + 1)  // key not found
    }

    /**
     * Check for duplicates, append actor and sort the list
     */
    override fun forceAddActor(actor: Actor?, caller: Throwable) {
        if (actor == null) return

        if (theGameHasActor(actor.referenceID)) {
            throw ReferencedActorAlreadyExistsException(actor, caller)
        }
        else {
            actorContainerActive.add(actor)
            if (actor is ActorWithBody) actorToRenderQueue(actor).add(actor)
        }
    }

    override fun inputStrobed(e: TerrarumKeyboardEvent) {
        uiContainer.forEach { it?.inputStrobed(e) }
    }

    fun activateDormantActor(actor: Actor) {
        if (!isInactive(actor.referenceID)) {
            /*if (isActive(actor.referenceID))
                throw Error("The actor $actor is already activated")
            else
                throw Error("The actor $actor already exists in the game")*/
            return
        }
        else {
            actorContainerInactive.remove(actor)
            actorContainerActive.add(actor)

            if (actor is ActorWithBody) {
                actorToRenderQueue(actor).add(actor)
            }
        }
    }

    fun addParticle(particle: ParticleBase) {
        particlesContainer.appendHead(particle)
    }

    private var barehandAxeInUse = false
    private var barehandPickInUse = false

    fun endPerformBarehandAction(actor: ActorWithBody) {
        if (barehandAxeInUse) {
            barehandAxeInUse = false
            AxeCore.endPrimaryUse(actor, null)
        }
        if (barehandPickInUse) {
            barehandPickInUse = false
            PickaxeCore.endPrimaryUse(actor, null)
        }
    }

    private fun getActorsAtVicinity(worldX: Double, worldY: Double, radius: Double): List<ActorWithBody> {
        val outList = java.util.ArrayList<ActorWithBody>()
        try {
            actorsRTree.find(worldX - radius, worldY - radius, worldX + radius, worldY + radius, outList)
        }
        catch (e: NullPointerException) {
        }
        return outList
    }

    private fun getPunchSize(actor: ActorWithBody) = actor.scale * actor.actorValue.getAsDouble(AVKey.BAREHAND_BASE_DIGSIZE)!!


    fun performBarehandAction(actor: ActorWithBody, delta: Float, mwx: Double, mwy: Double, mtx: Int, mty: Int) {
        // for giant actors punching every structure pickaxe can dig out
        val canAttackOrDig =
            actor.scale * actor.baseHitboxH >= (actor.actorValue.getAsDouble(AVKey.BAREHAND_MINHEIGHT) ?: 4294967296.0)

        // for players punching dirts or weaker blocks
        val canDigSoftTileOnly =
            actor is ActorHumanoid && (actor.baseHitboxH * actor.scale) >= 32f


        val punchSize = getPunchSize(actor)
        val punchBlockSize = punchSize.div(TILE_SIZED).floorToInt()

        val mouseUnderPunchableTree = BlockCodex[world.getTileFromTerrain(mtx, mty)].hasAnyTagsOf("LEAVES", "TREESMALL")


        // punch a small tree/shrub
        if (mouseUnderPunchableTree) {
            barehandAxeInUse = true
            AxeCore.startPrimaryUse(actor, delta, null, mtx, mty, punchBlockSize.coerceAtLeast(1), punchBlockSize.coerceAtLeast(1), listOf("TREESMALL"))
        }
        // TODO attack a mob
//        else if (mobsUnderHand.size > 0 && canAttackOrDig) {
//        }
        // else, punch a block
        else if (canAttackOrDig) {
            if (punchBlockSize > 0) {
                barehandPickInUse = true
                PickaxeCore.startPrimaryUse(actor, delta, null, mtx, mty, 1.0 / punchBlockSize, punchBlockSize, punchBlockSize)
            }
        }
        else if (canDigSoftTileOnly) {
            barehandPickInUse = true
            val tileUnderCursor = INGAME.world.getTileFromTerrain(mtx, mty)
            val tileprop = BlockCodex[tileUnderCursor]
            if (tileprop.strength <= 12)
                PickaxeCore.startPrimaryUse(actor, delta, null, mtx, mty, 1.0, 1, 1)
        }
    }

    fun getActorsUnderMouse(mwx: Double, mwy: Double): List<ActorWithBody> {
        val actorsUnderMouse: List<ActorWithBody> = getActorsAt(mwx, mwy).filter { it !is InternalActor }.sortedBy {
            (mwx - it.hitbox.centeredX).sqr() + (mwy - it.hitbox.centeredY).sqr()
        } // sorted by the distance from the mouse
        return actorsUnderMouse
    }

    fun pickupFixtureOrDroppedItem(actor: ActorWithBody, delta: Float, mwx: Double, mwy: Double, mtx: Int, mty: Int, assignToQuickslot: Boolean = true) {
        val actorsUnderMouse = getActorsUnderMouse(mwx, mwy)

        val fixture = actorsUnderMouse.firstOrNull {
            it is FixtureBase && it.canBeDespawned &&
                    System.nanoTime() - it.spawnRequestedTime > 500000000 // don't pick up the fixture if it was recently placed (0.5 seconds)
        } as? FixtureBase
        val droppedItemNoAutoPickup = actorsUnderMouse.firstOrNull {
            it is DroppedItem && it.noAutoPickup
        } as? DroppedItem
        val mob = actorsUnderMouse.firstOrNull {
            it !is FixtureBase && it.physProp.usePhysics && !it.physProp.immobileBody
        } as? ActorWithBody

        // pickup a fixture
        if (fixture != null) {
            val fixtureItem = fixture.itemise()
            printdbg(this, "Fixture pickup at F${WORLD_UPDATE_TIMER}: ${fixture.javaClass.canonicalName} -> $fixtureItem")
            // 0. hide tooltips
            TooltipManager.tooltipShowing.clear()
            setTooltipMessage(null)
            if (!fixture.flagDespawn) {
                // 1. put the fixture to the inventory
                fixture.flagDespawn()
                // 2. register this item(fixture) to the quickslot so that the player sprite would be actually lifting the fixture
                if (actor is Pocketed) {
                    actor.inventory.add(fixtureItem)

                    if (assignToQuickslot) {
                        actor.equipItem(fixtureItem)
                        actor.inventory.setQuickslotItemAtSelected(fixtureItem)
                        // 2-1. unregister if other slot has the same item
                        for (k in 0..9) {
                            if (actor.inventory.getQuickslotItem(k)?.itm == fixtureItem && k != actor.actorValue.getAsInt(
                                    AVKey.__PLAYER_QUICKSLOTSEL
                                )
                            ) {
                                actor.inventory.setQuickslotItem(k, null)
                            }
                        }
                    }
                }
            }
        }
        // pickup a dropped item (no auto-pickup variants only)
        else if (droppedItemNoAutoPickup != null) {
            val droppedItemID = droppedItemNoAutoPickup.itemID
            val droppedItemCount = droppedItemNoAutoPickup.itemCount
            // 0. hide tooltips
            TooltipManager.tooltipShowing.clear()
            setTooltipMessage(null)
            if (!droppedItemNoAutoPickup.flagDespawn) {
                // 1. put the fixture to the inventory
                droppedItemNoAutoPickup.flagDespawn()
                // 2. register this item(fixture) to the quickslot so that the player sprite would be actually lifting the fixture
                // BUUUUUUUT, only when the slot is already empty this time
                if (actor is Pocketed) {
                    actor.inventory.add(droppedItemID, droppedItemCount)

                    if (assignToQuickslot && actor.inventory.getQuickslotItem(actor.actorValue.getAsInt(AVKey.__PLAYER_QUICKSLOTSEL)) == null) {
                        actor.equipItem(droppedItemID)
                        actor.inventory.setQuickslotItemAtSelected(droppedItemID)
                        // 2-1. unregister if other slot has the same item
                        for (k in 0..9) {
                            if (actor.inventory.getQuickslotItem(k)?.itm == droppedItemID && k != actor.actorValue.getAsInt(
                                    AVKey.__PLAYER_QUICKSLOTSEL
                                )
                            ) {
                                actor.inventory.setQuickslotItem(k, null)
                            }
                        }
                    }
                }
            }
        }
        else if (mob != null) {
            // TODO pickup a mob
        }
    }

    override fun hide() {
        uiContainer.forEach { it?.handler?.dispose() }
    }

    /**
     * @param width same as AppLoader.terrarumAppConfig.screenW
     * @param height same as AppLoader.terrarumAppConfig.screenH
     * @see net.torvald.terrarum.Terrarum
     */
    override fun resize(width: Int, height: Int) {

        // FIXME debugger is pointing at this thing, not sure it actually caused memleak
        //MegaRainGovernor.resize()


        IngameRenderer.resize(App.scr.width, App.scr.height)
        val drawWidth = Toolkit.drawWidth


        if (gameFullyLoaded) {
            // resize UIs

            notifier.setPosition(
                (Toolkit.drawWidth - notifier.width) / 2,
                App.scr.height - notifier.height - App.scr.tvSafeGraphicsHeight
            )
            noticelet.setPosition(0, 0)
            uiQuickBar.setPosition((drawWidth - uiQuickBar.width) / 2, App.scr.tvSafeGraphicsHeight)

            // inventory
            /*uiInventoryPlayer =
                    UIInventory(player,
                            width = 840,
                            height = AppLoader.terrarumAppConfig.screenH - 160,
                            categoryWidth = 210
                    )*/


            // basic watch-style notification bar (temperature, new mail)
            uiBasicInfo.setPosition(drawWidth - uiBasicInfo.width, 0)
            uiWatchTierOne.setPosition(
                    ((drawWidth - App.scr.tvSafeGraphicsWidth) - (uiQuickBar.posX + uiQuickBar.width) - uiWatchTierOne.width) / 2 + (uiQuickBar.posX + uiQuickBar.width),
                    App.scr.tvSafeGraphicsHeight + 8
            )
        }


        printdbg(this, "Resize event")
    }

    override fun dispose() {
        visibleActorsRenderFarBehind.forEach { it.dispose() }
        visibleActorsRenderBehind.forEach { it.dispose() }
        visibleActorsRenderMiddle.forEach { it.dispose() }
        visibleActorsRenderMidTop.forEach { it.dispose() }
        visibleActorsRenderFront.forEach { it.dispose() }
        visibleActorsRenderOverlay.forEach { it.dispose() }

        uiContainer.forEach {
            it?.handler?.dispose()
            it?.dispose()
        }
        uiFixturesHistory.forEach {
            try {
                it.handler.dispose()
                it.dispose()
            }
            catch (e: IllegalArgumentException) {}
        }

        musicStreamer.dispose()
        super.dispose()
    }
}
