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)