diff --git a/build.gradle.kts b/build.gradle.kts index 46c1c8a1d4..6cb83107a4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ buildscript { dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${com.unciv.build.BuildConfig.kotlinVersion}") classpath("de.richsource.gradle.plugins:gwt-gradle-plugin:0.6") - classpath("com.android.tools.build:gradle:7.0.4") + classpath("com.android.tools.build:gradle:7.1.3") classpath("com.mobidevelop.robovm:robovm-gradle-plugin:2.3.1") // This is for wrapping the .jar file into a standalone executable diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 9a6126cdba..55f159bd73 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -37,6 +37,7 @@ import com.unciv.utils.concurrency.launchOnGLThread import com.unciv.utils.concurrency.withGLContext import com.unciv.utils.concurrency.withThreadPoolContext import com.unciv.utils.debug +import kotlinx.coroutines.CancellationException import java.io.PrintWriter import java.util.* import kotlin.collections.ArrayDeque @@ -405,6 +406,9 @@ class UncivGame(parameters: UncivGameParameters) : Game() { /** Handles an uncaught exception or error. First attempts a platform-specific handler, and if that didn't handle the exception or error, brings the game to a [CrashScreen]. */ fun handleUncaughtThrowable(ex: Throwable) { + if (ex is CancellationException) { + return // kotlin coroutines use this for control flow... so we can just ignore them. + } Log.error("Uncaught throwable", ex) try { PrintWriter(files.fileWriter("lasterror.txt")).use { diff --git a/core/src/com/unciv/ui/UncivStage.kt b/core/src/com/unciv/ui/UncivStage.kt index 981a4556dc..5fbd063054 100644 --- a/core/src/com/unciv/ui/UncivStage.kt +++ b/core/src/com/unciv/ui/UncivStage.kt @@ -24,6 +24,7 @@ class UncivStage(viewport: Viewport) : Stage(viewport) { private set private val events = EventBus.EventReceiver() + init { lastKnownVisibleArea = Rectangle(0f, 0f, width, height) events.receive(VisibleAreaChanged::class) { @@ -35,10 +36,15 @@ class UncivStage(viewport: Viewport) : Stage(viewport) { override fun dispose() { events.stopReceiving() super.dispose() + + /** [Stage.dispose] is supposed to clear all references it holds. But it forgets the mouse over properties: + the [Stage.mouseOverActor] and [Stage.pointerOverActors]. [Stage.act] updates those properties, + and since there aren't any children left, sets all those properties to `null`. */ + super.act() } override fun draw() = - { super.draw() }.wrapCrashHandlingUnit()() + { super.draw() }.wrapCrashHandlingUnit()() /** libGDX has no built-in way to disable/enable pointer enter/exit events. It is simply being done in [Stage.act]. So to disable this, we have * to replicate the [Stage.act] method without the code for pointer enter/exit events. This is of course inherently brittle, but the only way. */ @@ -54,31 +60,31 @@ class UncivStage(viewport: Viewport) : Stage(viewport) { }.wrapCrashHandlingUnit()() override fun act(delta: Float) = - { super.act(delta) }.wrapCrashHandlingUnit()() + { super.act(delta) }.wrapCrashHandlingUnit()() override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int) = - { super.touchDown(screenX, screenY, pointer, button) }.wrapCrashHandling()() ?: true + { super.touchDown(screenX, screenY, pointer, button) }.wrapCrashHandling()() ?: true override fun touchDragged(screenX: Int, screenY: Int, pointer: Int) = - { super.touchDragged(screenX, screenY, pointer) }.wrapCrashHandling()() ?: true + { super.touchDragged(screenX, screenY, pointer) }.wrapCrashHandling()() ?: true override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int) = - { super.touchUp(screenX, screenY, pointer, button) }.wrapCrashHandling()() ?: true + { super.touchUp(screenX, screenY, pointer, button) }.wrapCrashHandling()() ?: true override fun mouseMoved(screenX: Int, screenY: Int) = - { super.mouseMoved(screenX, screenY) }.wrapCrashHandling()() ?: true + { super.mouseMoved(screenX, screenY) }.wrapCrashHandling()() ?: true override fun scrolled(amountX: Float, amountY: Float) = - { super.scrolled(amountX, amountY) }.wrapCrashHandling()() ?: true + { super.scrolled(amountX, amountY) }.wrapCrashHandling()() ?: true override fun keyDown(keyCode: Int) = - { super.keyDown(keyCode) }.wrapCrashHandling()() ?: true + { super.keyDown(keyCode) }.wrapCrashHandling()() ?: true override fun keyUp(keyCode: Int) = - { super.keyUp(keyCode) }.wrapCrashHandling()() ?: true + { super.keyUp(keyCode) }.wrapCrashHandling()() ?: true override fun keyTyped(character: Char) = - { super.keyTyped(character) }.wrapCrashHandling()() ?: true + { super.keyTyped(character) }.wrapCrashHandling()() ?: true class VisibleAreaChanged( val visibleArea: Rectangle diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index b20feaccf3..9ca8dcb678 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -212,8 +212,9 @@ class WorldScreen( } override fun dispose() { - super.dispose() events.stopReceiving() + statusButtons.dispose() + super.dispose() } private fun addKeyboardPresses() { diff --git a/core/src/com/unciv/ui/worldscreen/status/MultiplayerStatusButton.kt b/core/src/com/unciv/ui/worldscreen/status/MultiplayerStatusButton.kt index f64617b074..76bce9a109 100644 --- a/core/src/com/unciv/ui/worldscreen/status/MultiplayerStatusButton.kt +++ b/core/src/com/unciv/ui/worldscreen/status/MultiplayerStatusButton.kt @@ -11,6 +11,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Stack import com.badlogic.gdx.utils.Align +import com.badlogic.gdx.utils.Disposable import com.unciv.UncivGame import com.unciv.logic.event.EventBus import com.unciv.logic.multiplayer.HasMultiplayerGameName @@ -26,14 +27,15 @@ import com.unciv.ui.utils.extensions.onClick import com.unciv.ui.utils.extensions.setSize import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.launchOnGLThread +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import java.time.Duration import java.time.Instant class MultiplayerStatusButton( - /*val*/ screen: BaseScreen, + screen: BaseScreen, curGame: OnlineMultiplayerGame? -) : Button(BaseScreen.skin) { +) : Button(BaseScreen.skin), Disposable { private var curGameName = curGame?.name private val multiplayerImage = createMultiplayerImage() private val loadingImage = createLoadingImage() @@ -43,6 +45,7 @@ class MultiplayerStatusButton( private var loadingStarted: Instant? = null private val events = EventBus.EventReceiver() + private var loadStopJob: Job? = null init { turnIndicatorCell = add().padTop(10f).padBottom(10f) @@ -96,7 +99,7 @@ class MultiplayerStatusButton( } else { Duration.ZERO } - Concurrency.run("Hide loading indicator") { + loadStopJob = Concurrency.run("Hide loading indicator") { delay(waitFor.toMillis()) launchOnGLThread { loadingImage.clearActions() @@ -147,11 +150,18 @@ class MultiplayerStatusButton( turnIndicator.flash() } } + + override fun dispose() { + events.stopReceiving() + turnIndicator.dispose() + loadStopJob?.cancel() + } } -private class TurnIndicator : HorizontalGroup() { +private class TurnIndicator : HorizontalGroup(), Disposable { val gameAmount = Label("2", BaseScreen.skin) val image: Image + private var job: Job? = null init { image = ImageGetter.getImage("OtherIcons/ExclamationMark") image.setSize(30f) @@ -175,11 +185,15 @@ private class TurnIndicator : HorizontalGroup() { if (alternations == 0) return gameAmount.color = nextColor image.color = nextColor - Concurrency.run("StatusButton color flash") { + job = Concurrency.run("StatusButton color flash") { delay(500) launchOnGLThread { flash(alternations - 1, nextColor, curColor) } } } + + override fun dispose() { + job?.cancel() + } } diff --git a/core/src/com/unciv/ui/worldscreen/status/StatusButtons.kt b/core/src/com/unciv/ui/worldscreen/status/StatusButtons.kt index da1e0c6ecd..a6c46cb40f 100644 --- a/core/src/com/unciv/ui/worldscreen/status/StatusButtons.kt +++ b/core/src/com/unciv/ui/worldscreen/status/StatusButtons.kt @@ -1,11 +1,12 @@ package com.unciv.ui.worldscreen.status import com.badlogic.gdx.scenes.scene2d.ui.HorizontalGroup +import com.badlogic.gdx.utils.Disposable class StatusButtons( nextTurnButton: NextTurnButton, multiplayerStatusButton: MultiplayerStatusButton? = null -) : HorizontalGroup() { +) : HorizontalGroup(), Disposable { var multiplayerStatusButton: MultiplayerStatusButton? = multiplayerStatusButton set(button) { multiplayerStatusButton?.remove() @@ -23,4 +24,8 @@ class StatusButtons( } addActor(nextTurnButton) } + + override fun dispose() { + multiplayerStatusButton?.dispose() + } }