package net.torvald.terrarum import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input import com.badlogic.gdx.utils.Disposable import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.gameactors.Actor import net.torvald.terrarum.gameactors.ActorID import net.torvald.terrarum.gameactors.ActorWithBody import net.torvald.terrarum.gameactors.ActorWithBody.Companion.PHYS_EPSILON_DIST import net.torvald.terrarum.gameactors.BlockMarkerActor import net.torvald.terrarum.gamecontroller.KeyToggler import net.torvald.terrarum.gamecontroller.TerrarumKeyboardEvent import net.torvald.terrarum.gameitems.ItemID import net.torvald.terrarum.gameworld.GameWorld import net.torvald.terrarum.modulebasegame.IngameRenderer import net.torvald.terrarum.modulebasegame.gameactors.ActorHumanoid import net.torvald.terrarum.modulebasegame.gameactors.IngamePlayer import net.torvald.terrarum.modulebasegame.ui.Notification import net.torvald.terrarum.modulebasegame.ui.UITooltip import net.torvald.terrarum.realestate.LandUtil import net.torvald.terrarum.savegame.VirtualDisk import net.torvald.terrarum.ui.ConsoleWindow import net.torvald.terrarum.ui.Toolkit import net.torvald.util.CircularArray import net.torvald.util.SortedArrayList import org.khelekore.prtree.* import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.IOException import java.nio.file.Files import java.nio.file.StandardCopyOption import java.util.* import java.util.concurrent.locks.Lock import java.util.function.Consumer import kotlin.math.min /** * Although the game (as product) can have infinitely many stages/planets/etc., those stages must be manually managed by YOU; * this instance only stores the stage that is currently being used. */ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boolean = false) : TerrarumGamescreen { var WORLD_UPDATE_TIMER = Random().nextInt(1020) + 1; protected set open protected val actorMBRConverter = object : MBRConverter { override fun getDimensions(): Int = 2 override fun getMin(axis: Int, t: ActorWithBody): Double = when (axis) { 0 -> t.hitbox.startX 1 -> t.hitbox.startY else -> throw IllegalArgumentException("nonexistent axis $axis for ${dimensions}-dimensional object") } override fun getMax(axis: Int, t: ActorWithBody): Double = when (axis) { 0 -> t.hitbox.endX - PHYS_EPSILON_DIST 1 -> t.hitbox.endY - PHYS_EPSILON_DIST else -> throw IllegalArgumentException("nonexistent axis $axis for ${dimensions}-dimensional object") } } val disposables = HashSet() lateinit var worldDisk: VirtualDisk; internal set lateinit var playerDisk: VirtualDisk; internal set lateinit var worldSavefileName: String; internal set lateinit var playerSavefileName: String; internal set var worldName: String = "SplinesReticulated"; internal set // worldName is stored as a name of the disk var screenZoom = 1.0f val ZOOM_MAXIMUM = 4.0f val ZOOM_MINIMUM = 1.0f open var consoleHandler: ConsoleWindow = ConsoleWindow() var paused: Boolean = false; protected set val consoleOpened: Boolean get() = consoleHandler.isOpened || consoleHandler.isOpening var newWorldLoadedLatch = false /** For in-world text overlays? e.g. cursor on the ore block and tooltip will say "Malachite" or something */ open var uiTooltip: UITooltip = UITooltip() open var notifier: Notification = Notification() val deltaTeeBenchmarks = CircularArray(App.getConfigInt("debug_deltat_benchmark_sample_sizes"), true) init { consoleHandler.setPosition(0, 0) notifier.setPosition( (Toolkit.drawWidth - notifier.width) / 2, App.scr.height - notifier.height - App.scr.tvSafeGraphicsHeight ) printdbg(this, "New ingame instance ${this.hashCode()}, called from") printStackTrace(this) } open var world: GameWorld = GameWorld.makeNullWorld() set(value) { val oldWorld = field newWorldLoadedLatch = true printdbg(this, "Ingame instance ${this.hashCode()}, accepting new world ${value.layerTerrain}; called from") printStackTrace(this) field = value IngameRenderer.setRenderedWorld(value) oldWorld.dispose() } /** how many different planets/stages/etc. are thenre. Whole stages must be manually managed by YOU. */ //var gameworldIndices = ArrayList() /** The actor the game is currently allowing you to control. * * Most of the time it'd be the "player", but think about the case where you have possessed * some random actor of the game. Now that actor is now actorNowPlaying, the actual gamer's avatar * (reference ID of 0x91A7E2) (must) stay in the actorContainerActive, but it's not a actorNowPlaying. * * Nullability of this property is believed to be unavoidable (trust me!). I'm sorry for the inconvenience. */ open var actorNowPlaying: ActorHumanoid? = null /** * The actual gamer */ open lateinit var actorGamer: IngamePlayer open var gameInitialised = false internal set open var gameFullyLoaded = false internal set val ACTORCONTAINER_INITIAL_SIZE = 64 val actorContainerActive = SortedArrayList(ACTORCONTAINER_INITIAL_SIZE) val actorContainerInactive = SortedArrayList(ACTORCONTAINER_INITIAL_SIZE) val actorAdditionQueue = ArrayList>() val actorRemovalQueue = ArrayList>() /** * ## BIG NOTE: Calculated actor distance is the **Euclidean distance SQUARED** * * But when a function does not take the distance (e.g. `actorsRTree.find()`) you must not square the numbers */ var actorsRTree: PRTree = PRTree(actorMBRConverter, 24) // no lateinit! protected set val terrainChangeQueue = ArrayList() val wallChangeQueue = ArrayList() val wireChangeQueue = ArrayList() // if 'old' is set and 'new' is blank, it's a wire cutter val modifiedChunks = Array(16) { TreeSet() } var loadedTime_t = App.getTIME_T() protected set val blockMarkingActor: BlockMarkerActor get() = CommonResourcePool.get("blockmarking_actor") as BlockMarkerActor protected lateinit var gameUpdateGovernor: GameUpdateGovernor override fun hide() { } override fun inputStrobed(e: TerrarumKeyboardEvent) { } override fun show() { // the very basic show() implementation for (k in Input.Keys.F1..Input.Keys.F12) { KeyToggler.forceSet(k, false) } // add blockmarking_actor into the actorlist (CommonResourcePool.get("blockmarking_actor") as BlockMarkerActor).let { forceRemoveActor(it) forceAddActor(it) } blockMarkingActor.let { it.unsetGhost() it.setGhostColourNone() } gameInitialised = true } override fun render(updateRate: Float) { } override fun pause() { paused = true } override fun resume() { paused = false } override fun resize(width: Int, height: Int) { } /** * You ABSOLUTELY must call this in your child classes (```super.dispose()```) and the AppLoader to properly * dispose of the world, which uses unsafe memory allocation. * Failing to do this will result to a memory leak! */ override fun dispose() { printdbg(this, "Thank you for properly disposing the world!") printdbg(this, "dispose called by") printStackTrace(this) blockMarkingActor.isVisible = false actorContainerActive.forEach { it.dispose() } actorContainerInactive.forEach { it.dispose() } world.dispose() disposables.forEach(Consumer { it.tryDispose() }) } //////////// // EVENTS // //////////// /** * Event for triggering held item's `startPrimaryUse(Float)` */ open fun worldPrimaryClickStart(actor: ActorWithBody, delta: Float) { } /** * Event for triggering held item's `endPrimaryUse(Float)` */ open fun worldPrimaryClickEnd(actor: ActorWithBody, delta: Float) { } // I have decided that left and right clicks must do the same thing, so no secondary use from now on. --Torvald on 2019-05-26 // Nevermind: we need to distinguish picking up and using the fixture. --Torvald on 2022-08-26 /** * Event for triggering held item's `startSecondaryUse(Float)` */ open fun worldSecondaryClickStart(actor: ActorWithBody, delta: Float) { } // I have decided that left and right clicks must do the same thing, so no secondary use from now on. --Torvald on 2019-05-26 // Nevermind: we need to distinguish picking up and using the fixture. --Torvald on 2022-08-26 /*** * Event for triggering held item's `endSecondaryUse(Float)` */ open fun worldSecondaryClickEnd(actor: ActorWithBody, delta: Float) { } /** * Event for triggering fixture update when something is placed/removed on the world. * Normally only called by GameWorld.setTileTerrain * * Queueing schema is used to make sure things are synchronised. */ open fun queueTerrainChangedEvent(old: ItemID, new: ItemID, x: Int, y: Int) { terrainChangeQueue.add(BlockChangeQueueItem(old, new, x, y)) //printdbg(this, terrainChangeQueue) } /** * Wall version of terrainChanged() event */ open fun queueWallChangedEvent(old: ItemID, new: ItemID, x: Int, y: Int) { wallChangeQueue.add(BlockChangeQueueItem(old, new, x, y)) } /** * Wire version of terrainChanged() event * * @param old previous settings of conduits in bit set format. * @param new current settings of conduits in bit set format. */ open fun queueWireChangedEvent(wire: ItemID, isRemoval: Boolean, x: Int, y: Int) { wireChangeQueue.add(BlockChangeQueueItem(if (isRemoval) wire else "", if (isRemoval) "" else wire, x, y)) //printdbg(this, wireChangeQueue) } open fun modified(layer: Int, x: Int, y: Int) { modifiedChunks[layer].add(LandUtil.toChunkNum(world, x, y)) } open fun clearModifiedChunks() { modifiedChunks.forEach { it.clear() } } /////////////////////// // UTILITY FUNCTIONS // /////////////////////// fun getActorByID(ID: Int): Actor { if (actorContainerActive.size == 0 && actorContainerInactive.size == 0) throw NoSuchActorWithIDException(ID) var actor = actorContainerActive.searchFor(ID) { it.referenceID } if (actor == null) { actor = actorContainerInactive.searchFor(ID) { it.referenceID } if (actor == null) { /*JOptionPane.showMessageDialog( null, "Actor with ID $ID does not exist.", null, JOptionPane.ERROR_MESSAGE )*/ throw NoSuchActorWithIDException(ID) } else return actor } else return actor } //fun SortedArrayList<*>.binarySearch(actor: Actor) = this.toArrayList().binarySearch(actor.referenceID) //fun SortedArrayList<*>.binarySearch(ID: Int) = this.toArrayList().binarySearch(ID) open fun queueActorRemoval(ID: Int) = queueActorRemoval(getActorByID(ID)) /** * get index of the actor and delete by the index. * we can do this as the list is guaranteed to be sorted * and only contains unique values. * * Any values behind the index will be automatically pushed to front. * This is how remove function of [java.util.ArrayList] is defined. */ open fun queueActorRemoval(actor: Actor?) { if (actor == null) return actorRemovalQueue.add(actor to StackTraceRecorder()) } protected open fun forceRemoveActor(actor: Actor, caller: Throwable = StackTraceRecorder()) { arrayOf(actorContainerActive, actorContainerInactive).forEach { actorContainer -> val indexToDelete = actorContainer.searchForIndex(actor.referenceID) { it.referenceID } if (indexToDelete != null) { actor.dispose() actorContainer.removeAt(indexToDelete) } } } protected open fun forceAddActor(actor: Actor?, caller: Throwable = StackTraceRecorder()) { if (actor == null) return if (theGameHasActor(actor.referenceID)) { throw ReferencedActorAlreadyExistsException(actor, caller) } else { actorContainerActive.add(actor) } } /** * Queue an actor to be added into the world. The actors will be added on the next update-frame and then the queue will be cleared. * If the actor is null, this function will do nothing. */ open fun queueActorAddition(actor: Actor?) { // printdbg(this, "New actor $actor") if (actor == null) return actorAdditionQueue.add(actor to StackTraceRecorder()) } fun isActive(ID: Int): Boolean = if (actorContainerActive.size == 0) false else actorContainerActive.searchFor(ID) { it.referenceID } != null fun isInactive(ID: Int): Boolean = if (actorContainerInactive.size == 0) false else actorContainerInactive.searchFor(ID) { it.referenceID } != null /** * actorContainerActive extensions */ fun theGameHasActor(actor: Actor?) = if (actor == null) false else theGameHasActor(actor.referenceID) fun theGameHasActor(ID: Int): Boolean = isActive(ID) || isInactive(ID) data class BlockChangeQueueItem(val old: ItemID, val new: ItemID, val posX: Int, val posY: Int) open fun sendNotification(messages: Array) { notifier.sendNotification(messages.toList()) } open fun sendNotification(messages: List) { notifier.sendNotification(messages) } open fun sendNotification(singleMessage: String) = sendNotification(listOf(singleMessage)) open fun setTooltipMessage(message: String?) { if (message == null) { uiTooltip.setAsClose() } else { if (uiTooltip.isClosed || uiTooltip.isClosing) { uiTooltip.setAsOpen() } uiTooltip.message = message // printStackTrace(this) } } open fun getTooltipMessage(): String { return uiTooltip.message } open fun requestForceSave(callback: () -> Unit) { } open fun saveTheGame(onSuccessful: () -> Unit, onError: (Throwable) -> Unit) { } /** * Copies most recent `save` to `save.1`, leaving `save` for overwriting, previous `save.1` will be copied to `save.2` */ fun makeSavegameBackupCopy(file: File) { if (!file.exists()) { return } val file1 = File("${file.absolutePath}.1") val file2 = File("${file.absolutePath}.2") val file3 = File("${file.absolutePath}.3") try { // do not overwrite clean .2 with dirty .1 val flags3 = FileInputStream(file3).let { it.skip(49L); val r = it.read(); it.close(); r } val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r } if (!(flags3 == 0 && flags2 != 0) || !file3.exists()) Files.move(file2.toPath(), file3.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) } catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {} try { // do not overwrite clean .2 with dirty .1 val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r } val flags1 = FileInputStream(file1).let { it.skip(49L); val r = it.read(); it.close(); r } if (!(flags2 == 0 && flags1 != 0) || !file2.exists()) Files.move(file1.toPath(), file2.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) } catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {} try { if (file2.exists() && !file3.exists()) Files.move(file2.toPath(), file3.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) if (file1.exists() && !file2.exists()) Files.move(file1.toPath(), file2.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) file.copyTo(file1, true) } catch (e: IOException) {} } fun makeSavegameBackupCopyAuto(file0: File): File { val file1 = File("${file0.absolutePath}.a") val file2 = File("${file0.absolutePath}.b") val file3 = File("${file0.absolutePath}.c") try { // do not overwrite clean .2 with dirty .1 val flags3 = FileInputStream(file3).let { it.skip(49L); val r = it.read(); it.close(); r } val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r } if (!(flags3 == 0 && flags2 != 0) || !file3.exists()) Files.move(file2.toPath(), file3.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) } catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {} try { // do not overwrite clean .2 with dirty .1 val flags2 = FileInputStream(file2).let { it.skip(49L); val r = it.read(); it.close(); r } val flags1 = FileInputStream(file1).let { it.skip(49L); val r = it.read(); it.close(); r } if (!(flags2 == 0 && flags1 != 0) || !file2.exists()) Files.move(file1.toPath(), file2.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) } catch (e: NoSuchFileException) {} catch (e: FileNotFoundException) {} try { if (file2.exists() && !file3.exists()) Files.move(file2.toPath(), file3.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) if (file1.exists() && !file2.exists()) Files.move(file1.toPath(), file2.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) file0.copyTo(file1, true) } catch (e: IOException) {} return file1 } // simple euclidean norm, squared private val actorDistanceCalculator = DistanceCalculator { t: ActorWithBody, p: PointND -> val dist1 = (p.getOrd(0) - t.hitbox.centeredX).sqr() + (p.getOrd(1) - t.hitbox.centeredY).sqr() // ROUNDWORLD implementation val dist2 = (p.getOrd(0) - (t.hitbox.centeredX - world.width * TILE_SIZE)).sqr() + (p.getOrd(1) - t.hitbox.centeredY).sqr() val dist3 = (p.getOrd(0) - (t.hitbox.centeredX + world.width * TILE_SIZE)).sqr() + (p.getOrd(1) - t.hitbox.centeredY).sqr() min(min(dist1, dist2), dist3) } /** * @return list of actors under the bounding box given, list may be empty if no actor is under the point. */ fun getActorsAt(startPoint: Point2d, endPoint: Point2d): List { val outList = ArrayList() try { actorsRTree.find(startPoint.x, startPoint.y, endPoint.x, endPoint.y, outList) } catch (e: NullPointerException) {} return outList } fun getActorsAt(worldX: Double, worldY: Double): List { val outList = ArrayList() try { actorsRTree.find(worldX, worldY, worldX, worldY, outList) } catch (e: NullPointerException) {} return outList } /** Will use centre point of the actors * HOPEFULLY sorted by the distance...? * @return List of DistanceResult (the actor and the distance SQUARED from the actor), list may be empty */ fun findKNearestActors(from: ActorWithBody, maxHits: Int, nodeFilter: (ActorWithBody) -> Boolean): List> { return actorsRTree.nearestNeighbour(actorDistanceCalculator, nodeFilter, maxHits, object : PointND { override fun getDimensions(): Int = 2 override fun getOrd(axis: Int): Double = when(axis) { 0 -> from.hitbox.centeredX 1 -> from.hitbox.centeredY else -> throw IllegalArgumentException("nonexistent axis $axis for ${dimensions}-dimensional object") } }) } /** Will use centre point of the actors * @return Pair of: the actor, distance SQUARED from the actor; null if none found */ fun findNearestActor(from: ActorWithBody, nodeFilter: (ActorWithBody) -> Boolean): DistanceResult? { val t = findKNearestActors(from, 1, nodeFilter) return if (t.isNotEmpty()) t[0] else null } fun onConfigChange() { } open val musicGovernor: MusicGovernor = MusicGovernor() } inline fun Lock.lock(body: () -> Unit) { this.lock() try { body() } finally { this.unlock() } } class StackTraceRecorder() : Exception("(I'm here to just record the stack trace, move along)") class NoSuchActorWithIDException(id: ActorID) : Exception("Actor with ID $id does not exist.") class NoSuchActorWithRefException(actor: Actor) : Exception("No such actor in the game: $actor") class ReferencedActorAlreadyExistsException(actor: Actor, caller: Throwable) : Exception("The actor $actor already exists in the game", caller) class ProtectedActorRemovalException(whatisit: String, caller: Throwable) : Exception("Attempted to removed protected actor '$whatisit'", caller) val INGAME: IngameInstance get() = Terrarum.ingame!!