Files
Terrarum/src/net/torvald/terrarum/IngameInstance.kt
2023-10-24 16:49:04 +09:00

573 lines
22 KiB
Kotlin

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<ActorWithBody> {
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<Disposable>()
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<Float>(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<Int>()
/** 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<Actor>(ACTORCONTAINER_INITIAL_SIZE)
val actorContainerInactive = SortedArrayList<Actor>(ACTORCONTAINER_INITIAL_SIZE)
val actorAdditionQueue = ArrayList<Pair<Actor, Throwable>>()
val actorRemovalQueue = ArrayList<Pair<Actor, Throwable>>()
/**
* ## 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<ActorWithBody> = PRTree(actorMBRConverter, 24) // no lateinit!
protected set
val terrainChangeQueue = ArrayList<BlockChangeQueueItem>()
val wallChangeQueue = ArrayList<BlockChangeQueueItem>()
val wireChangeQueue = ArrayList<BlockChangeQueueItem>() // if 'old' is set and 'new' is blank, it's a wire cutter
val modifiedChunks = Array(16) { TreeSet<Int>() }
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<String>) {
notifier.sendNotification(messages.toList())
}
open fun sendNotification(messages: List<String>) {
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<ActorWithBody> { 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<ActorWithBody> {
val outList = ArrayList<ActorWithBody>()
try {
actorsRTree.find(startPoint.x, startPoint.y, endPoint.x, endPoint.y, outList)
}
catch (e: NullPointerException) {}
return outList
}
fun getActorsAt(worldX: Double, worldY: Double): List<ActorWithBody> {
val outList = ArrayList<ActorWithBody>()
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<DistanceResult<ActorWithBody>> {
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<ActorWithBody>? {
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!!