diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 1383a3e9cd..cbe1819596 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -60,8 +60,8 @@
-
+
diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt
index 6f944b8e33..7ebaa3faee 100644
--- a/android/src/com/unciv/app/AndroidLauncher.kt
+++ b/android/src/com/unciv/app/AndroidLauncher.kt
@@ -20,7 +20,7 @@ open class AndroidLauncher : AndroidApplication() {
super.onCreate(savedInstanceState)
// Setup Android logging
- Log.backend = AndroidLogBackend()
+ Log.backend = AndroidLogBackend(this)
// Setup Android display
Display.platform = AndroidDisplay(this)
@@ -109,5 +109,3 @@ open class AndroidLauncher : AndroidApplication() {
super.onActivityResult(requestCode, resultCode, data)
}
}
-
-class AndroidTvLauncher:AndroidLauncher()
diff --git a/android/src/com/unciv/app/AndroidLogBackend.kt b/android/src/com/unciv/app/AndroidLogBackend.kt
index ed32784cc8..5cf7465d02 100644
--- a/android/src/com/unciv/app/AndroidLogBackend.kt
+++ b/android/src/com/unciv/app/AndroidLogBackend.kt
@@ -1,5 +1,8 @@
package com.unciv.app
+import android.app.Activity
+import android.app.ActivityManager
+import android.content.Context
import android.os.Build
import android.util.Log
import com.unciv.utils.LogBackend
@@ -7,7 +10,14 @@ import com.unciv.utils.Tag
private const val TAG_MAX_LENGTH = 23
-class AndroidLogBackend : LogBackend {
+/**
+ * Unciv's logger implementation for Android
+ *
+ * * Note: Gets and keeps a reference to [AndroidLauncher] as [activity] only to get memory info for [CrashScreen][com.unciv.ui.crashhandling.CrashScreen].
+ *
+ * @see com.unciv.utils.Log
+ */
+class AndroidLogBackend(private val activity: Activity) : LogBackend {
override fun debug(tag: Tag, curThreadName: String, msg: String) {
Log.d(toAndroidTag(tag), "[$curThreadName] $msg")
@@ -21,12 +31,29 @@ class AndroidLogBackend : LogBackend {
return !BuildConfig.DEBUG
}
+ /**
+ * @see com.unciv.app.desktop.SystemUtils.getSystemInfo
+ */
override fun getSystemInfo(): String {
+ val memoryInfo = getMemoryInfo()
+ val javaRuntime = Runtime.getRuntime()
return """
Device Model: ${Build.MODEL}
API Level: ${Build.VERSION.SDK_INT}
+ System Memory: ${memoryInfo.totalMem.formatMB()}
+ Available (used by Kernel): ${memoryInfo.availMem.formatMB()}
+ System Low Memory state: ${memoryInfo.lowMemory}
+ Java heap limit: ${javaRuntime.maxMemory().formatMB()}
+ Java heap free: ${javaRuntime.freeMemory().formatMB()}
""".trimIndent()
}
+
+ private fun getMemoryInfo() = ActivityManager.MemoryInfo().apply {
+ val activityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ activityManager.getMemoryInfo(this) // API writes into a structure we must supply
+ }
+
+ private fun Long.formatMB() = "${(this + 524288L) / 1048576L} MB"
}
private fun toAndroidTag(tag: Tag): String {
diff --git a/android/src/com/unciv/app/AndroidTvLauncher.kt b/android/src/com/unciv/app/AndroidTvLauncher.kt
new file mode 100644
index 0000000000..7a0a8797c5
--- /dev/null
+++ b/android/src/com/unciv/app/AndroidTvLauncher.kt
@@ -0,0 +1,6 @@
+package com.unciv.app
+
+/**
+ * Proxy without functionality, referenced in AndroidManifest.xml for intent.category.LEANBACK_LAUNCHER
+ */
+class AndroidTvLauncher : AndroidLauncher()
diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt
index 251f9536ef..237c14d6b2 100644
--- a/core/src/com/unciv/models/metadata/GameParameters.kt
+++ b/core/src/com/unciv/models/metadata/GameParameters.kt
@@ -105,7 +105,14 @@ class GameParameters : IsPartOfGameInfoSerialization { // Default values are the
yield(if (mods.isEmpty()) "no mods" else mods.joinToString(",", "mods=(", ")", 6) )
}.joinToString(prefix = "(", postfix = ")")
- fun getModsAndBaseRuleset(): HashSet {
- return mods.toHashSet().apply { add(baseRuleset) }
- }
+ /** Get all mods including base
+ *
+ * The returned Set is ordered base first, then in the order they are stored in a save.
+ * This creates a fresh instance, and the caller is allowed to mutate it.
+ */
+ fun getModsAndBaseRuleset() =
+ LinkedHashSet(mods.size + 1).apply {
+ add(baseRuleset)
+ addAll(mods)
+ }
}
diff --git a/core/src/com/unciv/ui/components/input/ActivationExtensions.kt b/core/src/com/unciv/ui/components/input/ActivationExtensions.kt
index a464661b9d..c60d9d005a 100644
--- a/core/src/com/unciv/ui/components/input/ActivationExtensions.kt
+++ b/core/src/com/unciv/ui/components/input/ActivationExtensions.kt
@@ -98,7 +98,6 @@ fun Actor.onRightClick(sound: UncivSound = UncivSound.Click, action: ActivationA
* A [sound] will be played (concurrently) on activation unless you specify [UncivSound.Silent].
* @return `this` to allow chaining
*/
-@Suppress("unused") // Just in case - for now, the Longpress in WorldMapHolder is using onActivation directly
fun Actor.onLongPress(sound: UncivSound = UncivSound.Click, action: ActivationAction): Actor =
onActivation(ActivationTypes.Longpress, sound, noEquivalence = true, action)
diff --git a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt
index 6825d04779..886b483396 100644
--- a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt
+++ b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt
@@ -10,11 +10,11 @@ import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.files.UncivFiles
import com.unciv.models.ruleset.RulesetCache
-import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.extensions.addBorder
-import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.extensions.setFontSize
import com.unciv.ui.components.extensions.toLabel
+import com.unciv.ui.components.input.onClick
+import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.ToastPopup
@@ -24,6 +24,8 @@ import java.io.PrintWriter
import java.io.StringWriter
/** Screen to crash to when an otherwise unhandled exception or error is thrown. */
+//todo We may be in a critical low-memory situation. Using a lot ot String concatenation and trimIndent
+// could make the display fail when a more efficient StringBuilder approach might still succeed.
class CrashScreen(val exception: Throwable) : BaseScreen() {
private companion object {
@@ -59,14 +61,23 @@ class CrashScreen(val exception: Throwable) : BaseScreen() {
/** @return Mods from the last active save game if any, or an informational note otherwise. */
private fun tryGetSaveMods(): String {
- if (!UncivGame.isCurrentInitialized() || UncivGame.Current.gameInfo == null)
- return ""
- return "\n**Save Mods:**\n```\n" +
- try { // Also from old CrashController().buildReport(), also could still error at .toString().
- LinkedHashSet(UncivGame.Current.gameInfo!!.gameParameters.getModsAndBaseRuleset()).toString()
- } catch (e: Throwable) {
- "No mod data: $e"
- } + "\n```\n"
+ if (!UncivGame.isCurrentInitialized()) return ""
+ val game = UncivGame.Current.gameInfo ?: return ""
+ val sb = StringBuilder(160) // capacity: Just some guess
+ sb.append("\n**Save Mods:**\n```\n")
+ try { // Also from old CrashController().buildReport(), also could still error at .toString().
+ sb.append(game.gameParameters.getModsAndBaseRuleset().toString())
+ } catch (e: Throwable) {
+ sb.append("No mod data: $e")
+ }
+ sb.append("\n```\n")
+ val visualMods = UncivGame.Current.settings.visualMods
+ if (visualMods.isEmpty())
+ return sb.toString()
+ sb.append("**Permanent audiovisual Mods**:\n```\n")
+ sb.append(visualMods.toString())
+ sb.append("\n```\n")
+ return sb.toString()
}
diff --git a/core/src/com/unciv/ui/popups/options/DebugTab.kt b/core/src/com/unciv/ui/popups/options/DebugTab.kt
index f36de37232..3b4957a032 100644
--- a/core/src/com/unciv/ui/popups/options/DebugTab.kt
+++ b/core/src/com/unciv/ui/popups/options/DebugTab.kt
@@ -1,18 +1,21 @@
package com.unciv.ui.popups.options
import com.badlogic.gdx.scenes.scene2d.ui.Table
+import com.badlogic.gdx.scenes.scene2d.ui.TextButton.TextButtonStyle
import com.unciv.GUI
import com.unciv.UncivGame
+import com.unciv.logic.UncivShowableException
import com.unciv.logic.files.MapSaver
import com.unciv.logic.files.UncivFiles
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.tile.ResourceType
-import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.components.UncivTextField
+import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.onClick
+import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.utils.DebugUtils
@@ -121,4 +124,10 @@ fun debugTab(
GUI.setUpdateWorldOnNextRender()
}
add(giveResourcesButton).colspan(2).row()
+
+ addSeparator()
+ add("* Crash Unciv! *".toTextButton(skin.get("negative", TextButtonStyle::class.java)).onClick {
+ throw UncivShowableException("Intentional crash")
+ }).colspan(2).row()
+ addSeparator()
}
diff --git a/core/src/com/unciv/ui/popups/options/OptionsPopup.kt b/core/src/com/unciv/ui/popups/options/OptionsPopup.kt
index dee7fac6ea..bdc196ed45 100644
--- a/core/src/com/unciv/ui/popups/options/OptionsPopup.kt
+++ b/core/src/com/unciv/ui/popups/options/OptionsPopup.kt
@@ -7,10 +7,10 @@ import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.RulesetCache
-import com.unciv.ui.components.widgets.TabbedPager
import com.unciv.ui.components.extensions.areSecretKeysPressed
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.toCheckBox
+import com.unciv.ui.components.widgets.TabbedPager
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
@@ -28,6 +28,7 @@ import kotlin.reflect.KMutableProperty0
class OptionsPopup(
screen: BaseScreen,
private val selectPage: Int = defaultPage,
+ withDebug: Boolean = false,
private val onClose: () -> Unit = {}
) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = Scrollability.None) {
@@ -110,7 +111,7 @@ class OptionsPopup(
val content = ModCheckTab(screen)
tabs.addPage("Locate mod errors", content, ImageGetter.getImage("OtherIcons/Mods"), 24f)
}
- if (Gdx.input.areSecretKeysPressed()) {
+ if (withDebug || Gdx.input.areSecretKeysPressed()) {
tabs.addPage("Debug", debugTab(this), ImageGetter.getImage("OtherIcons/SecretOptions"), 24f, secret = true)
}
diff --git a/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt b/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt
index 9517d897eb..ec97b7da25 100644
--- a/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt
+++ b/core/src/com/unciv/ui/screens/basescreen/BaseScreen.kt
@@ -174,8 +174,8 @@ abstract class BaseScreen : Screen {
/** @return `true` if the screen is narrower than 4:3 landscape */
fun isNarrowerThan4to3() = stage.isNarrowerThan4to3()
- open fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage, onClose: () -> Unit = {}) {
- OptionsPopup(this, startingPage, onClose).open(force = true)
+ open fun openOptionsPopup(startingPage: Int = OptionsPopup.defaultPage, withDebug: Boolean = false, onClose: () -> Unit = {}) {
+ OptionsPopup(this, startingPage, withDebug, onClose).open(force = true)
}
}
diff --git a/core/src/com/unciv/ui/screens/mainmenuscreen/MainMenuScreen.kt b/core/src/com/unciv/ui/screens/mainmenuscreen/MainMenuScreen.kt
index ab82f81070..f714037f3c 100644
--- a/core/src/com/unciv/ui/screens/mainmenuscreen/MainMenuScreen.kt
+++ b/core/src/com/unciv/ui/screens/mainmenuscreen/MainMenuScreen.kt
@@ -22,7 +22,6 @@ import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.tilesets.TileSetCache
-import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.surroundWithCircle
@@ -32,7 +31,9 @@ import com.unciv.ui.components.input.KeyShortcutDispatcherVeto
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
+import com.unciv.ui.components.input.onLongPress
import com.unciv.ui.components.tilegroups.TileGroupMap
+import com.unciv.ui.components.widgets.AutoScrollPane
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
import com.unciv.ui.popups.ToastPopup
@@ -45,9 +46,9 @@ import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.mainmenuscreen.EasterEggRulesets.modifyForEasterEgg
import com.unciv.ui.screens.mapeditorscreen.EditorMapHolder
import com.unciv.ui.screens.mapeditorscreen.MapEditorScreen
+import com.unciv.ui.screens.modmanager.ModManagementScreen
import com.unciv.ui.screens.multiplayerscreens.MultiplayerScreen
import com.unciv.ui.screens.newgamescreen.NewGameScreen
-import com.unciv.ui.screens.modmanager.ModManagementScreen
import com.unciv.ui.screens.savescreens.LoadGameScreen
import com.unciv.ui.screens.savescreens.QuickSave
import com.unciv.ui.screens.worldscreen.BackgroundActor
@@ -164,7 +165,8 @@ class MainMenuScreen: BaseScreen(), RecreateOnResize {
column2.add(modsTable).row()
val optionsTable = getMenuButton("Options", "OtherIcons/Options", KeyboardBinding.MainMenuOptions)
- { this.openOptionsPopup() }
+ { openOptionsPopup() }
+ optionsTable.onLongPress { openOptionsPopup(withDebug = true) }
column2.add(optionsTable).row()
diff --git a/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt
index 384891dce5..71a71ec4cb 100644
--- a/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt
+++ b/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt
@@ -17,10 +17,6 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
-import com.unciv.ui.components.widgets.AutoScrollPane
-import com.unciv.ui.components.widgets.ExpanderTab
-import com.unciv.ui.components.widgets.TranslatedSelectBox
-import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toImageButton
@@ -31,6 +27,10 @@ import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onChange
import com.unciv.ui.components.input.onClick
+import com.unciv.ui.components.widgets.AutoScrollPane
+import com.unciv.ui.components.widgets.ExpanderTab
+import com.unciv.ui.components.widgets.TranslatedSelectBox
+import com.unciv.ui.components.widgets.UncivSlider
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen
@@ -458,7 +458,7 @@ class GameOptionsTable(
}
private fun onChooseMod(mod: String) {
- val activeMods: LinkedHashSet = LinkedHashSet(gameParameters.getModsAndBaseRuleset())
+ val activeMods = gameParameters.getModsAndBaseRuleset()
UncivGame.Current.translations.translationActiveMods = activeMods
reloadRuleset()
update()
diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt
index 9c010067ba..9610da95b0 100644
--- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt
+++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt
@@ -276,10 +276,10 @@ class WorldScreen(
}
// Handle disabling and re-enabling WASD listener while Options are open
- override fun openOptionsPopup(startingPage: Int, onClose: () -> Unit) {
+ override fun openOptionsPopup(startingPage: Int, withDebug: Boolean, onClose: () -> Unit) {
val oldListener = stage.root.listeners.filterIsInstance().firstOrNull()
if (oldListener != null) stage.removeListener(oldListener)
- super.openOptionsPopup(startingPage) {
+ super.openOptionsPopup(startingPage, withDebug) {
addKeyboardListener()
onClose()
}
diff --git a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt
index a2ee1bf037..672f7475bf 100644
--- a/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt
+++ b/core/src/com/unciv/ui/screens/worldscreen/mainmenu/WorldScreenMenuPopup.kt
@@ -1,6 +1,7 @@
package com.unciv.ui.screens.worldscreen.mainmenu
import com.unciv.ui.components.input.KeyboardBinding
+import com.unciv.ui.components.input.onLongPress
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.savescreens.LoadGameScreen
@@ -35,10 +36,15 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen, sc
close()
worldScreen.game.pushScreen(VictoryScreen(worldScreen))
}.row()
- addButton("Options", KeyboardBinding.Options) {
+ val optionsCell = addButton("Options", KeyboardBinding.Options) {
close()
worldScreen.openOptionsPopup()
- }.row()
+ }
+ optionsCell.actor.onLongPress {
+ close()
+ worldScreen.openOptionsPopup(withDebug = true)
+ }
+ optionsCell.row()
addButton("Community") {
close()
WorldScreenCommunityPopup(worldScreen).open(force = true)