From 494fde53cfe5e5388cc0762446087f3a0def744d Mon Sep 17 00:00:00 2001 From: vegeta1k95 <32207817+vegeta1k95@users.noreply.github.com> Date: Tue, 28 Feb 2023 17:56:57 +0100 Subject: [PATCH] Cleaning: platform specifics and UncivGame (#8773) * Cleanup: platform specifics + UncivGame * Fix tests * Fix requests not clearing --------- Co-authored-by: vegeta1k95 --- .../app/{FontAndroid.kt => AndroidFont.kt} | 2 +- android/src/com/unciv/app/AndroidGame.kt | 35 +++++ android/src/com/unciv/app/AndroidLauncher.kt | 63 +++++--- .../src/com/unciv/app/AndroidLogBackend.kt | 7 + .../src/com/unciv/app/AndroidSaverLoader.kt | 113 +++++++++++++++ .../unciv/app/CrashReportSysInfoAndroid.kt | 13 -- .../app/CustomFileLocationHelperAndroid.kt | 98 ------------- .../unciv/app/MultiplayerTurnCheckWorker.kt | 2 +- .../app/PlatformSpecificHelpersAndroid.kt | 68 --------- .../com/unciv/app/TextfieldImprovements.kt | 113 --------------- core/src/com/unciv/UncivGame.kt | 55 ++----- core/src/com/unciv/UncivGameParameters.kt | 16 --- core/src/com/unciv/logic/GameInfo.kt | 9 +- core/src/com/unciv/logic/city/CityStats.kt | 3 +- .../transients/CivInfoTransientCache.kt | 4 +- .../logic/files/CustomFileLocationHelper.kt | 95 ------------- .../unciv/logic/files/PlatformSaverLoader.kt | 23 +++ core/src/com/unciv/logic/files/UncivFiles.kt | 134 ++++++++---------- core/src/com/unciv/logic/map/tile/Tile.kt | 6 +- .../unciv/logic/map/tile/TileDescription.kt | 6 +- core/src/com/unciv/ui/components/Fonts.kt | 8 +- .../GeneralPlatformSpecificHelpers.kt | 42 ------ .../com/unciv/ui/components/UncivTextField.kt | 107 +++++++++++++- .../ui/components/tilegroups/CityButton.kt | 5 +- .../ui/components/tilegroups/TileGroup.kt | 4 +- .../tilegroups/layers/TileLayerCityButton.kt | 4 +- .../ui/crashhandling/CrashReportSysInfo.kt | 5 - .../com/unciv/ui/crashhandling/CrashScreen.kt | 3 +- .../unciv/ui/popups/options/AdvancedTab.kt | 19 +-- .../com/unciv/ui/popups/options/DebugTab.kt | 13 +- .../unciv/ui/popups/options/OptionsPopup.kt | 6 +- .../overviewscreen/WonderOverviewTable.kt | 3 +- .../ui/screens/savescreens/LoadGameScreen.kt | 18 +-- .../ui/screens/savescreens/SaveGameScreen.kt | 18 +-- .../ui/screens/worldscreen/WorldScreen.kt | 2 +- .../worldscreen/bottombar/BattleTable.kt | 16 +-- .../worldscreen/bottombar/TileInfoTable.kt | 5 +- .../worldscreen/minimap/MinimapTile.kt | 4 +- core/src/com/unciv/utils/Debug.kt | 20 +++ core/src/com/unciv/utils/Log.kt | 14 ++ core/src/com/unciv/utils/PlatformSpecific.kt | 17 +++ .../com/unciv/app/desktop/ConsoleLauncher.kt | 4 +- .../app/desktop/CrashReportSysInfoDesktop.kt | 100 ------------- .../{FontDesktop.kt => DesktopFont.kt} | 2 +- .../src/com/unciv/app/desktop/DesktopGame.kt | 52 +++++++ .../com/unciv/app/desktop/DesktopLauncher.kt | 77 ++-------- .../unciv/app/desktop/DesktopLogBackend.kt | 6 +- ...HelperDesktop.kt => DesktopSaverLoader.kt} | 49 +++++-- .../com/unciv/app/desktop/DiscordUpdater.kt | 76 ++++++++++ .../desktop/PlatformSpecificHelpersDesktop.kt | 18 --- .../src/com/unciv/app/desktop/SystemUtils.kt | 97 +++++++++++++ .../src/com/unciv/dev/FasterUIDevelopment.kt | 8 +- tests/src/com/unciv/testing/BasicTests.kt | 9 +- 53 files changed, 823 insertions(+), 873 deletions(-) rename android/src/com/unciv/app/{FontAndroid.kt => AndroidFont.kt} (99%) create mode 100644 android/src/com/unciv/app/AndroidGame.kt create mode 100644 android/src/com/unciv/app/AndroidSaverLoader.kt delete mode 100644 android/src/com/unciv/app/CrashReportSysInfoAndroid.kt delete mode 100644 android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt delete mode 100644 android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt delete mode 100644 android/src/com/unciv/app/TextfieldImprovements.kt delete mode 100644 core/src/com/unciv/UncivGameParameters.kt delete mode 100644 core/src/com/unciv/logic/files/CustomFileLocationHelper.kt create mode 100644 core/src/com/unciv/logic/files/PlatformSaverLoader.kt delete mode 100644 core/src/com/unciv/ui/components/GeneralPlatformSpecificHelpers.kt delete mode 100644 core/src/com/unciv/ui/crashhandling/CrashReportSysInfo.kt create mode 100644 core/src/com/unciv/utils/Debug.kt create mode 100644 core/src/com/unciv/utils/PlatformSpecific.kt delete mode 100644 desktop/src/com/unciv/app/desktop/CrashReportSysInfoDesktop.kt rename desktop/src/com/unciv/app/desktop/{FontDesktop.kt => DesktopFont.kt} (98%) create mode 100644 desktop/src/com/unciv/app/desktop/DesktopGame.kt rename desktop/src/com/unciv/app/desktop/{CustomFileLocationHelperDesktop.kt => DesktopSaverLoader.kt} (50%) create mode 100644 desktop/src/com/unciv/app/desktop/DiscordUpdater.kt delete mode 100644 desktop/src/com/unciv/app/desktop/PlatformSpecificHelpersDesktop.kt create mode 100644 desktop/src/com/unciv/app/desktop/SystemUtils.kt diff --git a/android/src/com/unciv/app/FontAndroid.kt b/android/src/com/unciv/app/AndroidFont.kt similarity index 99% rename from android/src/com/unciv/app/FontAndroid.kt rename to android/src/com/unciv/app/AndroidFont.kt index 7eeb47150e..6b6a30deb9 100644 --- a/android/src/com/unciv/app/FontAndroid.kt +++ b/android/src/com/unciv/app/AndroidFont.kt @@ -18,7 +18,7 @@ import com.unciv.ui.components.Fonts import java.util.* import kotlin.math.abs -class FontAndroid : FontImplementation { +class AndroidFont : FontImplementation { private val fontList by lazy { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptySet() diff --git a/android/src/com/unciv/app/AndroidGame.kt b/android/src/com/unciv/app/AndroidGame.kt new file mode 100644 index 0000000000..2b44ed89b2 --- /dev/null +++ b/android/src/com/unciv/app/AndroidGame.kt @@ -0,0 +1,35 @@ +package com.unciv.app + +import android.os.Build +import com.unciv.UncivGame + +class AndroidGame(private val activity: AndroidLauncher) : UncivGame() { + + /* + Sources for Info about current orientation in case need: + val windowManager = (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager) + val displayRotation = windowManager.defaultDisplay.rotation + val currentOrientation = activity.resources.configuration.orientation + const val ORIENTATION_UNDEFINED = 0 + const val ORIENTATION_PORTRAIT = 1 + const val ORIENTATION_LANDSCAPE = 2 + */ + + override fun hasDisplayCutout(): Boolean { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> + activity.display?.cutout != null + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> + @Suppress("DEPRECATION") + activity.windowManager.defaultDisplay.cutout != null + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> + activity.window.decorView.rootWindowInsets.displayCutout != null + else -> false + } + } + + override fun allowPortrait(allow: Boolean) { + activity.allowPortrait(allow) + } + +} diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index fbda5c5f0e..1593f915c0 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -1,8 +1,8 @@ package com.unciv.app import android.content.Intent +import android.content.pm.ActivityInfo import android.graphics.Rect -import android.hardware.display.DisplayManager import android.net.Uri import android.opengl.GLSurfaceView import android.os.Build @@ -11,7 +11,7 @@ import android.view.Surface import android.view.SurfaceHolder import android.view.View import android.view.ViewTreeObserver -import androidx.annotation.RequiresApi +import android.view.WindowManager import androidx.core.app.NotificationManagerCompat import androidx.work.WorkManager import com.badlogic.gdx.Gdx @@ -20,9 +20,9 @@ import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration import com.badlogic.gdx.backends.android.AndroidGraphics import com.badlogic.gdx.math.Rectangle import com.unciv.UncivGame -import com.unciv.UncivGameParameters import com.unciv.logic.files.UncivFiles import com.unciv.logic.event.EventBus +import com.unciv.ui.components.Fonts import com.unciv.ui.screens.basescreen.UncivStage import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.utils.Log @@ -30,37 +30,36 @@ import com.unciv.utils.concurrency.Concurrency import java.io.File open class AndroidLauncher : AndroidApplication() { - private var customFileLocationHelper: CustomFileLocationHelperAndroid? = null + private var game: UncivGame? = null private var deepLinkedMultiplayerGame: String? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // Setup Android logging Log.backend = AndroidLogBackend() - customFileLocationHelper = CustomFileLocationHelperAndroid(this) + + // Setup Android fonts + Fonts.fontImplementation = AndroidFont() + + // Setup Android custom saver-loader + UncivFiles.saverLoader = AndroidSaverLoader(this) + UncivFiles.preferExternalStorage = true + + // Create notification channels for Multiplayer notificator MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext) copyMods() - val config = AndroidApplicationConfiguration().apply { - useImmersiveMode = true - } - + val config = AndroidApplicationConfiguration().apply { useImmersiveMode = true } val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path) - // Manage orientation lock and display cutout - val platformSpecificHelper = PlatformSpecificHelpersAndroid(this) - platformSpecificHelper.allowPortrait(settings.allowAndroidPortrait) + // Setup orientation lock and display cutout + allowPortrait(settings.allowAndroidPortrait) + setDisplayCutout(settings.androidCutout) - platformSpecificHelper.toggleDisplayCutout(settings.androidCutout) - - val androidParameters = UncivGameParameters( - crashReportSysInfo = CrashReportSysInfoAndroid, - fontImplementation = FontAndroid(), - customFileLocationHelper = customFileLocationHelper, - platformSpecificHelper = platformSpecificHelper - ) - - game = UncivGame(androidParameters) + game = AndroidGame(this) initialize(game, config) setDeepLinkedGame(intent) @@ -73,6 +72,23 @@ open class AndroidLauncher : AndroidApplication() { addScreenRefreshRateListener(glView) } + fun allowPortrait(allow: Boolean) { + val orientation = when { + allow -> ActivityInfo.SCREEN_ORIENTATION_USER + else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE + } + // Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary + if (requestedOrientation != orientation) requestedOrientation = orientation + } + + private fun setDisplayCutout(cutout: Boolean) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return + window.attributes.layoutInDisplayCutoutMode = when { + cutout -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES + else -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + } + /** Request the best available device frame rate for * the game, as soon as OpenGL surface is created */ private fun addScreenRefreshRateListener(surfaceView: GLSurfaceView) { @@ -196,7 +212,8 @@ open class AndroidLauncher : AndroidApplication() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - customFileLocationHelper?.onActivityResult(requestCode, data) + val saverLoader = UncivFiles.saverLoader as AndroidSaverLoader + saverLoader.onActivityResult(requestCode, data) super.onActivityResult(requestCode, resultCode, data) } } diff --git a/android/src/com/unciv/app/AndroidLogBackend.kt b/android/src/com/unciv/app/AndroidLogBackend.kt index 5ff48517cf..ed32784cc8 100644 --- a/android/src/com/unciv/app/AndroidLogBackend.kt +++ b/android/src/com/unciv/app/AndroidLogBackend.kt @@ -20,6 +20,13 @@ class AndroidLogBackend : LogBackend { override fun isRelease(): Boolean { return !BuildConfig.DEBUG } + + override fun getSystemInfo(): String { + return """ + Device Model: ${Build.MODEL} + API Level: ${Build.VERSION.SDK_INT} + """.trimIndent() + } } private fun toAndroidTag(tag: Tag): String { diff --git a/android/src/com/unciv/app/AndroidSaverLoader.kt b/android/src/com/unciv/app/AndroidSaverLoader.kt new file mode 100644 index 0000000000..d9f0e26a3d --- /dev/null +++ b/android/src/com/unciv/app/AndroidSaverLoader.kt @@ -0,0 +1,113 @@ +package com.unciv.app + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.DocumentsContract +import android.provider.OpenableColumns +import com.unciv.logic.files.PlatformSaverLoader +import java.io.InputStream +import java.io.OutputStream + +private class Request( + val onFileChosen: (Uri) -> Unit +) + +class AndroidSaverLoader(private val activity: Activity) : PlatformSaverLoader { + + private val contentResolver = activity.contentResolver + private val requests = HashMap() + private var requestCode = 100 + + override fun saveGame( + data: String, + suggestedLocation: String, + onSaved: (location: String) -> Unit, + onError: (ex: Exception) -> Unit + ) { + + // When we loaded, we returned a "content://" URI as file location. + val suggestedUri = Uri.parse(suggestedLocation) + val fileName = getFilename(suggestedUri, suggestedLocation) + + val onFileChosen = {uri: Uri -> + var stream: OutputStream? = null + try { + stream = contentResolver.openOutputStream(uri, "rwt") + stream!!.writer().use { it.write(data) } + onSaved(uri.toString()) + } catch (ex: Exception) { + onError(ex) + } finally { + stream?.close() + } + } + + requests[requestCode] = Request(onFileChosen) + openSaveFileChooser(fileName, suggestedUri, requestCode) + requestCode += 1 + } + + override fun loadGame( + onLoaded: (data: String, location: String) -> Unit, + onError: (ex: Exception) -> Unit) { + + val onFileChosen = {uri: Uri -> + var stream: InputStream? = null + try { + stream = contentResolver.openInputStream(uri) + val text = stream!!.reader().use { it.readText() } + onLoaded(text, uri.toString()) + } catch (ex: Exception) { + onError(ex) + } finally { + stream?.close() + } + } + + requests[requestCode] = Request(onFileChosen) + openLoadFileChooser(requestCode) + requestCode += 1 + } + + fun onActivityResult(requestCode: Int, data: Intent?) { + val uri: Uri = data?.data ?: return + val request = requests.remove(requestCode) ?: return + request.onFileChosen(uri) + } + + private fun openSaveFileChooser(fileName: String, uri: Uri, requestCode: Int) { + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.type = "application/json" + intent.putExtra(Intent.EXTRA_TITLE, fileName) + if (uri.scheme == "content") + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri) + activity.startActivityForResult(intent, requestCode) + } + + private fun openLoadFileChooser(requestCode: Int) { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.type = "*/*" + /* It is theoretically possible to use an initial URI here, + however, the only Android URIs we have are obtained from here, so, no dice */ + activity.startActivityForResult(intent, requestCode) + } + + private fun getFilename(uri: Uri, suggestedLocation: String): String { + + if (uri.scheme != "content") + return suggestedLocation + + try { + contentResolver.query(uri, null, null, null, null).use { + if (it?.moveToFirst() == true) + return it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) + else + return "" + } + } catch(ex: Exception) { + return suggestedLocation.split("2F").last() // I have no idea why but the content path ends with this before the filename + } + + } +} diff --git a/android/src/com/unciv/app/CrashReportSysInfoAndroid.kt b/android/src/com/unciv/app/CrashReportSysInfoAndroid.kt deleted file mode 100644 index 7b478fe918..0000000000 --- a/android/src/com/unciv/app/CrashReportSysInfoAndroid.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.unciv.app - -import android.os.Build -import com.unciv.ui.crashhandling.CrashReportSysInfo - -object CrashReportSysInfoAndroid: CrashReportSysInfo { - - override fun getInfo(): String = - """ - Device Model: ${Build.MODEL} - API Level: ${Build.VERSION.SDK_INT} - """.trimIndent() -} diff --git a/android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt b/android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt deleted file mode 100644 index 567f5ffef3..0000000000 --- a/android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.unciv.app - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.provider.DocumentsContract -import android.provider.OpenableColumns -import androidx.annotation.GuardedBy -import com.unciv.logic.files.CustomFileLocationHelper -import java.io.InputStream -import java.io.OutputStream - -class CustomFileLocationHelperAndroid(private val activity: Activity) : CustomFileLocationHelper() { - - @GuardedBy("this") - private val callbacks = mutableListOf() - @GuardedBy("this") - private var curActivityRequestCode = 100 - - override fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) { - val requestCode = createActivityCallback(callback) { activity.contentResolver.openOutputStream(it, "rwt") } - - // When we loaded, we returned a "content://" URI as file location. - val uri = Uri.parse(suggestedLocation) - val fileName = if (uri.scheme == "content") { - try { - val cursor = activity.contentResolver.query(uri, null, null, null, null) - cursor.use { - // we should have a direct URI to a file, so first is enough - if (it?.moveToFirst() == true) { - it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) - } else "" - } - } - catch(ex:Exception) { - suggestedLocation.split("2F").last() // I have no idea why but the content path ends with this before the filename - } - } else { - // if we didn't load, this is some file name entered by the user - suggestedLocation - } - - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - type = "application/json" - putExtra(Intent.EXTRA_TITLE, fileName) - if (uri.scheme == "content") { - putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri) - } - activity.startActivityForResult(this, requestCode) - } - } - - override fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) { - val callbackIndex = createActivityCallback(callback, activity.contentResolver::openInputStream) - - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - type = "*/*" - // It is theoretically possible to use an initial URI here, however, the only Android URIs we have are obtained from here, so, no dice - activity.startActivityForResult(this, callbackIndex) - } - } - - private fun createActivityCallback(callback: (String?, T?, Exception?) -> Unit, - createValue: (Uri) -> T): Int { - synchronized(this) { - val requestCode = curActivityRequestCode++ - val activityCallback = ActivityCallback(requestCode) { uri -> - if (uri == null) { - callback(null, null, null) - return@ActivityCallback - } - - try { - val outputStream = createValue(uri) - callback(uri.toString(), outputStream, null) - } catch (ex: Exception) { - callback(null, null, ex) - } - } - callbacks.add(activityCallback) - return requestCode - } - } - - fun onActivityResult(requestCode: Int, data: Intent?) { - val activityCallback = synchronized(this) { - val index = callbacks.indexOfFirst { it.requestCode == requestCode } - if (index == -1) return - callbacks.removeAt(index) - } - activityCallback.callback(data?.data) - } -} - -private class ActivityCallback( - val requestCode: Int, - val callback: (Uri?) -> Unit -) diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index 5019046b8d..43f10ab52f 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -269,7 +269,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame val gdxFiles = DefaultAndroidFiles(applicationContext.assets, ContextWrapper(applicationContext), true) // GDX's AndroidFileHandle uses Gdx.files internally, so we need to set that to our new instance Gdx.files = gdxFiles - files = UncivFiles(gdxFiles, null, true) + files = UncivFiles(gdxFiles) } override fun doWork(): Result = runBlocking { diff --git a/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt b/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt deleted file mode 100644 index 617777c42c..0000000000 --- a/android/src/com/unciv/app/PlatformSpecificHelpersAndroid.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.unciv.app - -import android.app.Activity -import android.content.pm.ActivityInfo -import android.os.Build -import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER -import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES -import com.badlogic.gdx.scenes.scene2d.ui.TextField -import com.unciv.ui.components.GeneralPlatformSpecificHelpers -import kotlin.concurrent.thread - -/** See also interface [GeneralPlatformSpecificHelpers]. - * - * The Android implementation (currently the only one) effectively ends up doing - * [Activity.setRequestedOrientation] - */ -class PlatformSpecificHelpersAndroid(private val activity: Activity) : GeneralPlatformSpecificHelpers { - -/* -Sources for Info about current orientation in case need: - val windowManager = (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager) - val displayRotation = windowManager.defaultDisplay.rotation - val currentOrientation = activity.resources.configuration.orientation - const val ORIENTATION_UNDEFINED = 0 - const val ORIENTATION_PORTRAIT = 1 - const val ORIENTATION_LANDSCAPE = 2 -*/ - - override fun allowPortrait(allow: Boolean) { - val orientation = when { - allow -> ActivityInfo.SCREEN_ORIENTATION_USER - else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE - } - // Comparison ensures ActivityTaskManager.getService().setRequestedOrientation isn't called unless necessary - if (activity.requestedOrientation != orientation) activity.requestedOrientation = orientation - } - - override fun hasDisplayCutout() = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> - activity.display?.cutout != null - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> - @Suppress("DEPRECATION") - activity.windowManager.defaultDisplay.cutout != null - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> - activity.window.decorView.rootWindowInsets.displayCutout != null - else -> false - } - - override fun toggleDisplayCutout(androidCutout: Boolean) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return - val layoutParams = activity.window.attributes - if (androidCutout) { - layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - } else { - layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - } - } - - /** - * On Android, local is some android-internal data directory which may or may not be accessible by the user. - * External is probably on an SD-card or similar which is always accessible by the user. - */ - override fun shouldPreferExternalStorage(): Boolean = true - - override fun addImprovements(textField: TextField): TextField { - return TextfieldImprovements.add(textField) - } -} diff --git a/android/src/com/unciv/app/TextfieldImprovements.kt b/android/src/com/unciv/app/TextfieldImprovements.kt deleted file mode 100644 index 34f316224e..0000000000 --- a/android/src/com/unciv/app/TextfieldImprovements.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.unciv.app - -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.scenes.scene2d.Actor -import com.badlogic.gdx.scenes.scene2d.InputEvent -import com.badlogic.gdx.scenes.scene2d.InputListener -import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane -import com.badlogic.gdx.scenes.scene2d.ui.TextField -import com.badlogic.gdx.scenes.scene2d.utils.FocusListener -import com.unciv.logic.event.EventBus -import com.unciv.models.translations.tr -import com.unciv.ui.screens.basescreen.UncivStage -import com.unciv.ui.popups.Popup -import com.unciv.ui.components.KeyCharAndCode -import com.unciv.ui.components.extensions.getAscendant -import com.unciv.ui.components.scrollAscendantToTextField -import com.unciv.utils.concurrency.Concurrency -import com.unciv.utils.concurrency.withGLContext -import kotlinx.coroutines.delay - -object TextfieldImprovements { - private val hideKeyboard = { Gdx.input.setOnscreenKeyboardVisible(false) } - fun add(textField: TextField): TextField { - textField.addListener(object : InputListener() { - private val events = EventBus.EventReceiver() - init { - events.receive(UncivStage.VisibleAreaChanged::class) { - if (textField.stage == null || !textField.hasKeyboardFocus()) return@receive - Concurrency.run { - // If anything resizes, it also does so with this event. So we need to wait for that to finish to update the scroll position. - delay(100) - withGLContext { - if (textField.stage == null) return@withGLContext - - if (textField.scrollAscendantToTextField()) { - val scrollPane = textField.getAscendant { it is ScrollPane } as ScrollPane? - // when screen dimensions change, we don't want an animation for scrolling, just show, just show the textfield immediately - scrollPane?.updateVisualScroll() - } else { - // We can't scroll the text field into view, so we need to show a popup - TextfieldPopup(textField).open() - } - } - } - } - } - override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: Int): Boolean { - addPopupCloseListener(textField) - return false - } - }) - textField.addListener(object : FocusListener() { - override fun keyboardFocusChanged(event: FocusEvent?, actor: Actor?, focused: Boolean) { - if (focused) { - addPopupCloseListener(textField) - Gdx.input.setOnscreenKeyboardVisible(true) - } - } - }) - - return textField - } - - private fun addPopupCloseListener(textField: TextField) { - val popup = textField.getAscendant { it is Popup } as Popup? - if (popup != null && !popup.closeListeners.contains(hideKeyboard)) { - popup.closeListeners.add(hideKeyboard) - } - } -} - -class TextfieldPopup( - textField: TextField -) : Popup(textField.stage) { - val popupTextfield = clone(textField) - init { - addGoodSizedLabel(popupTextfield.messageText) - .colspan(2) - .row() - - add(popupTextfield) - .width(stageToShowOn.width / 2) - .colspan(2) - .row() - - addCloseButton("Cancel") - .left() - addOKButton { textField.text = popupTextfield.text } - .right() - .row() - - showListeners.add { - stageToShowOn.keyboardFocus = popupTextfield - } - closeListeners.add { - stageToShowOn.keyboardFocus = null - Gdx.input.setOnscreenKeyboardVisible(false) - } - } - - private fun clone(textField: TextField): TextField { - @Suppress("UNCIV_RAW_TEXTFIELD") // we are copying the existing text field - val copy = TextField(textField.text, textField.style) - copy.textFieldFilter = textField.textFieldFilter - copy.messageText = textField.messageText - copy.setSelection(textField.selectionStart, textField.selection.length) - copy.cursorPosition = textField.cursorPosition - copy.alignment = textField.alignment - copy.isPasswordMode = textField.isPasswordMode - copy.onscreenKeyboard = textField.onscreenKeyboard - return copy - } -} diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 7ebec6c66f..a293e89745 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -25,7 +25,7 @@ import com.unciv.ui.audio.MusicController import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicTrackChooserFlags import com.unciv.ui.audio.SoundPlayer -import com.unciv.ui.components.FontImplementation +import com.unciv.ui.components.Fonts import com.unciv.ui.components.extensions.center import com.unciv.ui.crashhandling.CrashScreen import com.unciv.ui.crashhandling.wrapCrashHandlingUnit @@ -41,7 +41,9 @@ import com.unciv.ui.screens.worldscreen.PlayerReadyScreen import com.unciv.ui.screens.worldscreen.WorldMapHolder import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.unit.UnitTable +import com.unciv.utils.DebugUtils import com.unciv.utils.Log +import com.unciv.utils.PlatformSpecific import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.launchOnGLThread import com.unciv.utils.concurrency.withGLContext @@ -51,13 +53,10 @@ import kotlinx.coroutines.CancellationException import java.io.PrintWriter import java.util.* import kotlin.collections.ArrayDeque +import kotlin.system.exitProcess object GUI { - fun isDebugMapVisible(): Boolean { - return UncivGame.Current.viewEntireMapForDebug - } - fun setUpdateWorldOnNextRender() { UncivGame.Current.worldScreen?.shouldUpdate = true } @@ -74,10 +73,6 @@ object GUI { return UncivGame.Current.settings } - fun getFontImpl(): FontImplementation { - return UncivGame.Current.fontImplementation!! - } - fun isWorldLoaded(): Boolean { return UncivGame.Current.worldScreen != null } @@ -116,19 +111,11 @@ object GUI { } -class UncivGame(parameters: UncivGameParameters) : Game() { - constructor() : this(UncivGameParameters()) - - val crashReportSysInfo = parameters.crashReportSysInfo - val cancelDiscordEvent = parameters.cancelDiscordEvent - var fontImplementation = parameters.fontImplementation - val consoleMode = parameters.consoleMode - private val customSaveLocationHelper = parameters.customFileLocationHelper - val platformSpecificHelper = parameters.platformSpecificHelper - private val audioExceptionHelper = parameters.audioExceptionHelper +open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpecific { var deepLinkedMultiplayerGame: String? = null var gameInfo: GameInfo? = null + lateinit var settings: GameSettings lateinit var musicController: MusicController lateinit var onlineMultiplayer: OnlineMultiplayer @@ -136,20 +123,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() { var isTutorialTaskCollapsed = false - /** - * This exists so that when debugging we can see the entire map. - * Remember to turn this to false before commit and upload! - */ - var viewEntireMapForDebug = false - /** For when you need to test something in an advanced game and don't have time to faff around */ - var superchargedForDebug = false - - /** Simulate until this turn on the first "Next turn" button press. - * Does not update World View changes until finished. - * Set to 0 to disable. - */ - var simulateUntilTurnForDebug: Int = 0 - var worldScreen: WorldScreen? = null private set @@ -167,10 +140,10 @@ class UncivGame(parameters: UncivGameParameters) : Game() { isInitialized = false // this could be on reload, therefore we need to keep setting this to false Gdx.input.setCatchKey(Input.Keys.BACK, true) if (Gdx.app.type != Application.ApplicationType.Desktop) { - viewEntireMapForDebug = false + DebugUtils.VISIBLE_MAP = false } Current = this - files = UncivFiles(Gdx.files, customSaveLocationHelper, platformSpecificHelper?.shouldPreferExternalStorage() == true) + files = UncivFiles(Gdx.files) // If this takes too long players, especially with older phones, get ANR problems. // Whatever needs graphics needs to be done on the main thread, @@ -190,10 +163,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { GameSounds.init() musicController = MusicController() // early, but at this point does only copy volume from settings - audioExceptionHelper?.installHooks( - musicController.getAudioLoopCallback(), - musicController.getAudioExceptionHandler() - ) + installAudioHooks() onlineMultiplayer = OnlineMultiplayer() @@ -230,7 +200,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { // Loading available fonts can take a long time on Android phones. // Therefore we initialize the lazy parameters in the font implementation, while we're in another thread, to avoid ANRs on main thread - fontImplementation?.setFontFamily(settings.fontFamilyData, settings.getFontSize()) + Fonts.fontImplementation.setFontFamily(settings.fontFamilyData, settings.getFontSize()) // This stuff needs to run on the main thread because it needs the GL context launchOnGLThread { @@ -474,8 +444,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() { override fun dispose() { Gdx.input.inputProcessor = null // don't allow ANRs when shutting down, that's silly - - cancelDiscordEvent?.invoke() SoundPlayer.clearCache() if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out @@ -498,7 +466,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { // On desktop this should only be this one and "DestroyJavaVM" logRunningThreads() - System.exit(0) + exitProcess(0) } private fun logRunningThreads() { @@ -523,7 +491,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() { } catch (ex: Exception) { // ignore } - if (platformSpecificHelper?.handleUncaughtThrowable(ex) == true) return Gdx.app.postRunnable { setAsRootScreen(CrashScreen(ex)) } diff --git a/core/src/com/unciv/UncivGameParameters.kt b/core/src/com/unciv/UncivGameParameters.kt deleted file mode 100644 index 7bd1a67c36..0000000000 --- a/core/src/com/unciv/UncivGameParameters.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.unciv - -import com.unciv.logic.files.CustomFileLocationHelper -import com.unciv.ui.crashhandling.CrashReportSysInfo -import com.unciv.ui.components.AudioExceptionHelper -import com.unciv.ui.components.GeneralPlatformSpecificHelpers -import com.unciv.ui.components.FontImplementation - -class UncivGameParameters(val crashReportSysInfo: CrashReportSysInfo? = null, - val cancelDiscordEvent: (() -> Unit)? = null, - val fontImplementation: FontImplementation? = null, - val consoleMode: Boolean = false, - val customFileLocationHelper: CustomFileLocationHelper? = null, - val platformSpecificHelper: GeneralPlatformSpecificHelpers? = null, - val audioExceptionHelper: AudioExceptionHelper? = null -) diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 6341e73f28..8a9ffc5b7b 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -32,6 +32,7 @@ import com.unciv.models.ruleset.nation.Difficulty import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicTrackChooserFlags +import com.unciv.utils.DebugUtils import com.unciv.utils.debug import java.util.* @@ -253,7 +254,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion //region State changing functions // Do we automatically simulate until N turn? - fun isSimulation(): Boolean = turns < UncivGame.Current.simulateUntilTurnForDebug + fun isSimulation(): Boolean = turns < DebugUtils.SIMULATE_UNTIL_TURN || turns < simulateMaxTurns && simulateUntilWin fun nextTurn() { @@ -266,7 +267,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion playerIndex = (playerIndex + 1) % civilizations.size if (playerIndex == 0) { turns++ - if (UncivGame.Current.simulateUntilTurnForDebug != 0) + if (DebugUtils.SIMULATE_UNTIL_TURN != 0) debug("Starting simulation of turn %s", turns) } player = civilizations[playerIndex] @@ -311,8 +312,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion setNextPlayer() } - if (turns == UncivGame.Current.simulateUntilTurnForDebug) - UncivGame.Current.simulateUntilTurnForDebug = 0 + if (turns == DebugUtils.SIMULATE_UNTIL_TURN) + DebugUtils.SIMULATE_UNTIL_TURN = 0 // We found human player, so we are making him current currentTurnStartTime = System.currentTimeMillis() diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index 5975bf9686..26ff970941 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -17,6 +17,7 @@ import com.unciv.models.stats.Stat import com.unciv.models.stats.StatMap import com.unciv.models.stats.Stats import com.unciv.ui.components.extensions.toPercent +import com.unciv.utils.DebugUtils import kotlin.math.min @@ -464,7 +465,7 @@ class CityStats(val city: City) { newStatsBonusTree.add(getStatsPercentBonusesFromUniquesBySource(currentConstruction)) - if (UncivGame.Current.superchargedForDebug) { + if (DebugUtils.SUPERCHARGED) { val stats = Stats() for (stat in Stat.values()) stats[stat] = 10000f newStatsBonusTree.addStats(stats, "Supercharged") diff --git a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt index d9c462b6c0..40d8279147 100644 --- a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt +++ b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt @@ -2,7 +2,6 @@ package com.unciv.logic.civilization.transients import com.badlogic.gdx.math.Vector2 import com.unciv.Constants -import com.unciv.UncivGame import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.NotificationCategory @@ -19,6 +18,7 @@ import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit +import com.unciv.utils.DebugUtils /** CivInfo class was getting too crowded */ class CivInfoTransientCache(val civInfo: Civilization) { @@ -142,7 +142,7 @@ class CivInfoTransientCache(val civInfo: Civilization) { val newViewableTiles = HashSet() // while spectating all map is visible - if (civInfo.isSpectator() || UncivGame.Current.viewEntireMapForDebug) { + if (civInfo.isSpectator() || DebugUtils.VISIBLE_MAP) { val allTiles = civInfo.gameInfo.tileMap.values.toSet() civInfo.viewableTiles = allTiles civInfo.viewableInvisibleUnitsTiles = allTiles diff --git a/core/src/com/unciv/logic/files/CustomFileLocationHelper.kt b/core/src/com/unciv/logic/files/CustomFileLocationHelper.kt deleted file mode 100644 index 327de292a2..0000000000 --- a/core/src/com/unciv/logic/files/CustomFileLocationHelper.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.unciv.logic.files - -import com.unciv.logic.files.UncivFiles.CustomLoadResult -import com.unciv.logic.files.UncivFiles.CustomSaveResult -import com.unciv.utils.concurrency.Concurrency -import java.io.InputStream -import java.io.OutputStream - -/** - * Contract for platform-specific helper classes to handle saving and loading games to and from - * arbitrary external locations. - * - * Implementation note: If a game is loaded with [loadGame] and the same game is saved with [saveGame], - * the suggestedLocation in [saveGame] will be the location returned by [loadGame]. - */ -abstract class CustomFileLocationHelper { - /** - * Saves a game asynchronously to a location selected by the user. - * - * Prefills their UI with a [suggestedLocation]. - * - * Calls the [saveCompleteCallback] on the main thread with the save location on success or the [Exception] on error or null in both on cancel. - */ - fun saveGame( - gameData: String, - suggestedLocation: String, - saveCompleteCallback: (CustomSaveResult) -> Unit = {} - ) { - createOutputStream(suggestedLocation) { location, outputStream, exception -> - if (outputStream == null) { - callSaveCallback(saveCompleteCallback, exception = exception) - return@createOutputStream - } - - try { - outputStream.writer().use { it.write(gameData) } - callSaveCallback(saveCompleteCallback, location) - } catch (ex: Exception) { - callSaveCallback(saveCompleteCallback, exception = ex) - } - } - } - - /** - * Loads a game asynchronously from a location selected by the user. - * - * Calls the [loadCompleteCallback] on the main thread. - */ - fun loadGame(loadCompleteCallback: (CustomLoadResult) -> Unit) { - createInputStream { location, inputStream, exception -> - if (inputStream == null) { - callLoadCallback(loadCompleteCallback, exception = exception) - return@createInputStream - } - - try { - val gameData = inputStream.reader().use { it.readText() } - callLoadCallback(loadCompleteCallback, location, gameData) - } catch (ex: Exception) { - callLoadCallback(loadCompleteCallback, exception = ex) - } - } - } - - /** - * [callback] should be called with the actual selected location and an OutputStream to the location, or an exception if something failed. - */ - protected abstract fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) - - /** - * [callback] should be called with the actual selected location and an InputStream to read the location, or an exception if something failed. - */ - protected abstract fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) -} - -private fun callLoadCallback(loadCompleteCallback: (CustomLoadResult) -> Unit, - location: String? = null, - gameData: String? = null, - exception: Exception? = null) { - val result = if (location != null && gameData != null && exception == null) { - CustomLoadResult(location to gameData) - } else { - CustomLoadResult(null, exception) - } - Concurrency.runOnGLThread { - loadCompleteCallback(result) - } -} -private fun callSaveCallback(saveCompleteCallback: (CustomSaveResult) -> Unit, - location: String? = null, - exception: Exception? = null) { - Concurrency.runOnGLThread { - saveCompleteCallback(CustomSaveResult(location, exception)) - } -} diff --git a/core/src/com/unciv/logic/files/PlatformSaverLoader.kt b/core/src/com/unciv/logic/files/PlatformSaverLoader.kt new file mode 100644 index 0000000000..0e2157b6a5 --- /dev/null +++ b/core/src/com/unciv/logic/files/PlatformSaverLoader.kt @@ -0,0 +1,23 @@ +package com.unciv.logic.files + +/** + * Contract for platform-specific helper classes to handle saving and loading games to and from + * arbitrary external locations. + * + * Implementation note: If a game is loaded with [loadGame] and the same game is saved with [saveGame], + * the suggestedLocation in [saveGame] will be the location returned by [loadGame]. + */ +interface PlatformSaverLoader { + + fun saveGame( + data: String, // Data to save + suggestedLocation: String, // Proposed location + onSaved: (location: String) -> Unit = {}, // On-save-complete callback + onError: (ex: Exception) -> Unit = {} // On-save-error callback + ) + + fun loadGame( + onLoaded: (data: String, location: String) -> Unit, // On-load-complete callback + onError: (Exception) -> Unit = {} // On-load-error callback + ) +} diff --git a/core/src/com/unciv/logic/files/UncivFiles.kt b/core/src/com/unciv/logic/files/UncivFiles.kt index 62fc8cd3d1..e7a8e14c73 100644 --- a/core/src/com/unciv/logic/files/UncivFiles.kt +++ b/core/src/com/unciv/logic/files/UncivFiles.kt @@ -30,13 +30,11 @@ class UncivFiles( * This is necessary because the Android turn check background worker does not hold any reference to the actual [com.badlogic.gdx.Application], * which is normally responsible for keeping the [Gdx] static variables from being garbage collected. */ - private val files: Files, - private val customFileLocationHelper: CustomFileLocationHelper? = null, - private val preferExternalStorage: Boolean = false + private val files: Files ) { init { - debug("Creating UncivFiles, localStoragePath: %s, externalStoragePath: %s, preferExternalStorage: %s", - files.localStoragePath, files.externalStoragePath, preferExternalStorage) + debug("Creating UncivFiles, localStoragePath: %s, externalStoragePath: %s", + files.localStoragePath, files.externalStoragePath) } //region Data @@ -112,8 +110,6 @@ class UncivFiles( return localFiles + externalFiles } - fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null - /** * @return `true` if successful. * @throws SecurityException when delete access was denied @@ -141,15 +137,6 @@ class UncivFiles( return file.delete() } - interface ChooseLocationResult { - val location: String? - val exception: Exception? - - fun isCanceled(): Boolean = location == null && exception == null - fun isError(): Boolean = exception != null - fun isSuccessful(): Boolean = location != null - } - //endregion //region Saving @@ -165,7 +152,8 @@ class UncivFiles( fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) { try { debug("Saving GameInfo %s to %s", game.gameId, file.path()) - file.writeString(gameInfoToString(game), false) + val string = gameInfoToString(game) + file.writeString(string, false) saveCompletionCallback(null) } catch (ex: Exception) { saveCompletionCallback(ex) @@ -194,31 +182,35 @@ class UncivFiles( } } - class CustomSaveResult( - override val location: String? = null, - override val exception: Exception? = null - ) : ChooseLocationResult - /** * [gameName] is a suggested name for the file. If the file has already been saved to or loaded from a custom location, * this previous custom location will be used. * - * Calls the [saveCompleteCallback] on the main thread with the save location on success, an [Exception] on error, or both null on cancel. + * Calls the [onSaved] on the main thread on success. + * Calls the [onError] on the main thread with an [Exception] on error. */ - fun saveGameToCustomLocation(game: GameInfo, gameName: String, saveCompletionCallback: (CustomSaveResult) -> Unit) { + fun saveGameToCustomLocation( + game: GameInfo, + gameName: String, + onSaved: () -> Unit, + onError: (Exception) -> Unit) { val saveLocation = game.customSaveLocation ?: Gdx.files.local(gameName).path() - val gameData = try { - gameInfoToString(game) + + try { + val data = gameInfoToString(game) + debug("Saving GameInfo %s to custom location %s", game.gameId, saveLocation) + saverLoader.saveGame(data, saveLocation, + { location -> + game.customSaveLocation = location + Concurrency.runOnGLThread { onSaved() } + }, + { + Concurrency.runOnGLThread { onError(it) } + } + ) + } catch (ex: Exception) { - Concurrency.runOnGLThread { saveCompletionCallback(CustomSaveResult(exception = ex)) } - return - } - debug("Saving GameInfo %s to custom location %s", game.gameId, saveLocation) - customFileLocationHelper!!.saveGame(gameData, saveLocation) { - if (it.isSuccessful()) { - game.customSaveLocation = it.location - } - saveCompletionCallback(it) + Concurrency.runOnGLThread { onError(ex) } } } @@ -240,11 +232,7 @@ class UncivFiles( loadGamePreviewFromFile(getMultiplayerSave(gameName)) fun loadGamePreviewFromFile(gameFile: FileHandle): GameInfoPreview { - val preview = json().fromJson(GameInfoPreview::class.java, gameFile) - if (preview == null) { - throw emptyFile(gameFile) - } - return preview + return json().fromJson(GameInfoPreview::class.java, gameFile) ?: throw emptyFile(gameFile) } /** @@ -256,36 +244,29 @@ class UncivFiles( return SerializationException("The file for the game ${gameFile.name()} is empty") } - class CustomLoadResult( - private val locationAndGameData: Pair? = null, - override val exception: Exception? = null - ) : ChooseLocationResult { - override val location: String? get() = locationAndGameData?.first - val gameData: T? get() = locationAndGameData?.second - } - /** - * Calls the [loadCompleteCallback] on the main thread with the [GameInfo] on success or the [Exception] on error or null in both on cancel. - * - * The exception may be [IncompatibleGameInfoVersionException] if the [gameData] was created by a version of this game that is incompatible with the current one. + * Calls the [onLoaded] on the main thread with the [GameInfo] on success. + * Calls the [onError] on the main thread with the [Exception] on error + * The exception may be [IncompatibleGameInfoVersionException] if the [GameInfo] was created by a version of this game that is incompatible with the current one. */ - fun loadGameFromCustomLocation(loadCompletionCallback: (CustomLoadResult) -> Unit) { - customFileLocationHelper!!.loadGame { result -> - val location = result.location - val gameData = result.gameData - if (location == null || gameData == null) { - loadCompletionCallback(CustomLoadResult(exception = result.exception)) - return@loadGame + fun loadGameFromCustomLocation( + onLoaded: (GameInfo) -> Unit, + onError: (Exception) -> Unit + ) { + saverLoader.loadGame( + { data, location -> + try { + val game = gameInfoFromString(data) + game.customSaveLocation = location + Concurrency.runOnGLThread { onLoaded(game) } + } catch (ex: Exception) { + Concurrency.runOnGLThread { onError(ex) } + } + }, + { + Concurrency.runOnGLThread { onError(it) } } - - try { - val gameInfo = gameInfoFromString(gameData) - gameInfo.customSaveLocation = location - loadCompletionCallback(CustomLoadResult(location to gameInfo)) - } catch (ex: Exception) { - loadCompletionCallback(CustomLoadResult(exception = ex)) - } - } + ) } @@ -293,7 +274,7 @@ class UncivFiles( //region Settings private fun getGeneralSettingsFile(): FileHandle { - return if (UncivGame.Current.consoleMode) FileHandle(SETTINGS_FILE_NAME) + return if (UncivGame.Current.isConsoleMode) FileHandle(SETTINGS_FILE_NAME) else files.local(SETTINGS_FILE_NAME) } @@ -325,6 +306,17 @@ class UncivFiles( var saveZipped = false + /** + * If the GDX [com.badlogic.gdx.Files.getExternalStoragePath] should be preferred for this platform, + * otherwise uses [com.badlogic.gdx.Files.getLocalStoragePath] + */ + var preferExternalStorage = false + + /** + * Platform dependent saver-loader to custom system locations + */ + lateinit var saverLoader: PlatformSaverLoader + /** Specialized function to access settings before Gdx is initialized. * * @param base Path to the directory where the file should be - if not set, the OS current directory is used (which is "/" on Android) @@ -333,11 +325,7 @@ class UncivFiles( // FileHandle is Gdx, but the class and JsonParser are not dependent on app initialization // In fact, at this point Gdx.app or Gdx.files are null but this still works. val file = FileHandle(base + File.separator + SETTINGS_FILE_NAME) - return if (file.exists()) - json().fromJsonFile( - GameSettings::class.java, - file - ) + return if (file.exists()) json().fromJsonFile(GameSettings::class.java, file) else GameSettings().apply { isFreshlyCreated = true } } diff --git a/core/src/com/unciv/logic/map/tile/Tile.kt b/core/src/com/unciv/logic/map/tile/Tile.kt index a7ca64b77c..c3452be786 100644 --- a/core/src/com/unciv/logic/map/tile/Tile.kt +++ b/core/src/com/unciv/logic/map/tile/Tile.kt @@ -3,7 +3,6 @@ import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.GUI -import com.unciv.UncivGame import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization @@ -22,6 +21,7 @@ import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueMap import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.utils.DebugUtils import kotlin.math.abs import kotlin.math.min import kotlin.random.Random @@ -235,13 +235,13 @@ open class Tile : IsPartOfGameInfoSerialization { else ruleset.terrains[naturalWonder!!]!! fun isVisible(player: Civilization): Boolean { - if (UncivGame.Current.viewEntireMapForDebug) + if (DebugUtils.VISIBLE_MAP) return true return player.viewableTiles.contains(this) } fun isExplored(player: Civilization): Boolean { - if (UncivGame.Current.viewEntireMapForDebug || player.isSpectator()) + if (DebugUtils.VISIBLE_MAP || player.isSpectator()) return true return exploredBy.contains(player.civName) } diff --git a/core/src/com/unciv/logic/map/tile/TileDescription.kt b/core/src/com/unciv/logic/map/tile/TileDescription.kt index 7ec694d5c7..e902e04a17 100644 --- a/core/src/com/unciv/logic/map/tile/TileDescription.kt +++ b/core/src/com/unciv/logic/map/tile/TileDescription.kt @@ -1,19 +1,19 @@ package com.unciv.logic.map.tile import com.unciv.Constants -import com.unciv.UncivGame import com.unciv.logic.civilization.Civilization import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.translations.tr import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.components.Fonts +import com.unciv.utils.DebugUtils object TileDescription { /** Get info on a selected tile, used on WorldScreen (right side above minimap), CityScreen or MapEditorViewTab. */ fun toMarkup(tile: Tile, viewingCiv: Civilization?): ArrayList { val lineList = ArrayList() - val isViewableToPlayer = viewingCiv == null || UncivGame.Current.viewEntireMapForDebug + val isViewableToPlayer = viewingCiv == null || DebugUtils.VISIBLE_MAP || viewingCiv.viewableTiles.contains(tile) if (tile.isCityCenter()) { @@ -21,7 +21,7 @@ object TileDescription { var cityString = city.name.tr() if (isViewableToPlayer) cityString += " (${city.health})" lineList += FormattedLine(cityString) - if (UncivGame.Current.viewEntireMapForDebug || city.civ == viewingCiv) + if (DebugUtils.VISIBLE_MAP || city.civ == viewingCiv) lineList += city.cityConstructions.getProductionMarkup(tile.ruleset) } diff --git a/core/src/com/unciv/ui/components/Fonts.kt b/core/src/com/unciv/ui/components/Fonts.kt index 30579e1980..2090ce6b04 100644 --- a/core/src/com/unciv/ui/components/Fonts.kt +++ b/core/src/com/unciv/ui/components/Fonts.kt @@ -177,6 +177,7 @@ object Fonts { const val ORIGINAL_FONT_SIZE = 50f const val DEFAULT_FONT_FAMILY = "" + lateinit var fontImplementation: FontImplementation lateinit var font: BitmapFont /** This resets all cached font data in object Fonts. @@ -184,15 +185,12 @@ object Fonts { */ fun resetFont() { val settings = GUI.getSettings() - val fontImpl = GUI.getFontImpl() - fontImpl.setFontFamily(settings.fontFamilyData, settings.getFontSize()) - font = fontImpl.getBitmapFont() + fontImplementation.setFontFamily(settings.fontFamilyData, settings.getFontSize()) + font = fontImplementation.getBitmapFont() } /** Reduce the font list returned by platform-specific code to font families (plain variant if possible) */ fun getSystemFonts(): Sequence { - val fontImplementation = UncivGame.Current.fontImplementation - ?: return emptySequence() return fontImplementation.getSystemFonts() .sortedWith(compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.localName }) } diff --git a/core/src/com/unciv/ui/components/GeneralPlatformSpecificHelpers.kt b/core/src/com/unciv/ui/components/GeneralPlatformSpecificHelpers.kt deleted file mode 100644 index a4270eab1e..0000000000 --- a/core/src/com/unciv/ui/components/GeneralPlatformSpecificHelpers.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.unciv.ui.components - -import com.badlogic.gdx.scenes.scene2d.ui.TextField -import com.unciv.models.metadata.GameSettings - -/** Interface to support various platform-specific tools */ -interface GeneralPlatformSpecificHelpers { - /** Pass a Boolean setting as used in [allowAndroidPortrait][GameSettings.allowAndroidPortrait] to the OS. - * - * You can turn a mobile device on its side or upside down, and a mobile OS may or may not allow the - * position changes to automatically result in App orientation changes. This is about limiting that feature. - * @param allow `true`: allow all orientations (follows sensor as limited by OS settings) - * `false`: allow only landscape orientations (both if supported, otherwise default landscape only) - */ - fun allowPortrait(allow: Boolean) {} - - fun hasDisplayCutout(): Boolean { return false } - fun toggleDisplayCutout(androidCutout: Boolean) {} - - /** - * Notifies the user that it's their turn while the game is running - */ - fun notifyTurnStarted() {} - - /** - * If the GDX [com.badlogic.gdx.Files.getExternalStoragePath] should be preferred for this platform, - * otherwise uses [com.badlogic.gdx.Files.getLocalStoragePath] - */ - fun shouldPreferExternalStorage(): Boolean - - /** - * Handle an uncaught throwable. - * @return true if the throwable was handled. - */ - fun handleUncaughtThrowable(ex: Throwable): Boolean = false - - /** - * Adds platform-specific improvements to the given text field, making it nicer to interact with on this platform. - */ - fun addImprovements(textField: TextField): TextField = textField - -} diff --git a/core/src/com/unciv/ui/components/UncivTextField.kt b/core/src/com/unciv/ui/components/UncivTextField.kt index 72ab8cf4c8..eebc499003 100644 --- a/core/src/com/unciv/ui/components/UncivTextField.kt +++ b/core/src/com/unciv/ui/components/UncivTextField.kt @@ -1,11 +1,16 @@ package com.unciv.ui.components +import com.badlogic.gdx.Application +import com.badlogic.gdx.Gdx import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.InputEvent +import com.badlogic.gdx.scenes.scene2d.InputListener import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.scenes.scene2d.utils.FocusListener import com.unciv.UncivGame +import com.unciv.logic.event.EventBus import com.unciv.models.translations.tr import com.unciv.ui.screens.basescreen.UncivStage import com.unciv.ui.components.extensions.getAscendant @@ -13,7 +18,11 @@ import com.unciv.ui.components.extensions.getOverlap import com.unciv.ui.components.extensions.right import com.unciv.ui.components.extensions.stageBoundingBox import com.unciv.ui.components.extensions.top +import com.unciv.ui.popups.Popup import com.unciv.ui.screens.basescreen.BaseScreen +import com.unciv.utils.concurrency.Concurrency +import com.unciv.utils.concurrency.withGLContext +import kotlinx.coroutines.delay object UncivTextField { /** @@ -33,7 +42,9 @@ object UncivTextField { } } }) - UncivGame.Current.platformSpecificHelper?.addImprovements(textField) + + if (Gdx.app.type == Application.ApplicationType.Android) + TextfieldImprovements.add(textField) return textField } } @@ -96,3 +107,97 @@ fun TextField.scrollAscendantToTextField(): Boolean { return true } + +object TextfieldImprovements { + private val hideKeyboard = { Gdx.input.setOnscreenKeyboardVisible(false) } + fun add(textField: TextField): TextField { + textField.addListener(object : InputListener() { + private val events = EventBus.EventReceiver() + init { + events.receive(UncivStage.VisibleAreaChanged::class) { + if (textField.stage == null || !textField.hasKeyboardFocus()) return@receive + Concurrency.run { + // If anything resizes, it also does so with this event. So we need to wait for that to finish to update the scroll position. + delay(100) + withGLContext { + if (textField.stage == null) return@withGLContext + + if (textField.scrollAscendantToTextField()) { + val scrollPane = textField.getAscendant { it is ScrollPane } as ScrollPane? + // when screen dimensions change, we don't want an animation for scrolling, just show, just show the textfield immediately + scrollPane?.updateVisualScroll() + } else { + // We can't scroll the text field into view, so we need to show a popup + TextfieldPopup(textField).open() + } + } + } + } + } + override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: Int): Boolean { + addPopupCloseListener(textField) + return false + } + }) + textField.addListener(object : FocusListener() { + override fun keyboardFocusChanged(event: FocusEvent?, actor: Actor?, focused: Boolean) { + if (focused) { + addPopupCloseListener(textField) + Gdx.input.setOnscreenKeyboardVisible(true) + } + } + }) + + return textField + } + + private fun addPopupCloseListener(textField: TextField) { + val popup = textField.getAscendant { it is Popup } as Popup? + if (popup != null && !popup.closeListeners.contains(hideKeyboard)) { + popup.closeListeners.add(hideKeyboard) + } + } +} + +class TextfieldPopup( + textField: TextField +) : Popup(textField.stage) { + val popupTextfield = clone(textField) + init { + addGoodSizedLabel(popupTextfield.messageText) + .colspan(2) + .row() + + add(popupTextfield) + .width(stageToShowOn.width / 2) + .colspan(2) + .row() + + addCloseButton("Cancel") + .left() + addOKButton { textField.text = popupTextfield.text } + .right() + .row() + + showListeners.add { + stageToShowOn.keyboardFocus = popupTextfield + } + closeListeners.add { + stageToShowOn.keyboardFocus = null + Gdx.input.setOnscreenKeyboardVisible(false) + } + } + + private fun clone(textField: TextField): TextField { + @Suppress("UNCIV_RAW_TEXTFIELD") // we are copying the existing text field + val copy = TextField(textField.text, textField.style) + copy.textFieldFilter = textField.textFieldFilter + copy.messageText = textField.messageText + copy.setSelection(textField.selectionStart, textField.selection.length) + copy.cursorPosition = textField.cursorPosition + copy.alignment = textField.alignment + copy.isPasswordMode = textField.isPasswordMode + copy.onscreenKeyboard = textField.onscreenKeyboard + return copy + } +} diff --git a/core/src/com/unciv/ui/components/tilegroups/CityButton.kt b/core/src/com/unciv/ui/components/tilegroups/CityButton.kt index 5daa77a99a..f8791ba027 100644 --- a/core/src/com/unciv/ui/components/tilegroups/CityButton.kt +++ b/core/src/com/unciv/ui/components/tilegroups/CityButton.kt @@ -33,6 +33,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.cityscreen.CityReligionInfoTable import com.unciv.ui.screens.cityscreen.CityScreen import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen +import com.unciv.utils.DebugUtils import kotlin.math.max import kotlin.math.min @@ -228,7 +229,7 @@ private class CityTable(city: City, forPopup: Boolean = false) : BorderedTable( pad(0f) defaults().pad(0f) - val isShowDetailedInfo = UncivGame.Current.viewEntireMapForDebug + val isShowDetailedInfo = DebugUtils.VISIBLE_MAP || city.civ == viewingCiv || viewingCiv.isSpectator() @@ -532,7 +533,7 @@ class CityButton(val city: City, private val tileGroup: TileGroup): Table(BaseSc if (isButtonMoved) { // second tap on the button will go to the city screen // if this city belongs to you and you are not iterating though the air units - if (GUI.isDebugMapVisible() || viewingPlayer.isSpectator() + if (DebugUtils.VISIBLE_MAP || viewingPlayer.isSpectator() || (belongsToViewingCiv() && !tileGroup.tile.airUnits.contains(unitTable.selectedUnit))) { GUI.pushScreen(CityScreen(city)) } else if (viewingPlayer.knows(city.civ)) { diff --git a/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt b/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt index e3c49a72ac..71eac94357 100644 --- a/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt +++ b/core/src/com/unciv/ui/components/tilegroups/TileGroup.kt @@ -2,7 +2,6 @@ package com.unciv.ui.components.tilegroups import com.badlogic.gdx.graphics.g2d.Batch import com.badlogic.gdx.scenes.scene2d.Group -import com.unciv.UncivGame import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.tile.Tile import com.unciv.ui.components.tilegroups.layers.TileLayerBorders @@ -13,6 +12,7 @@ import com.unciv.ui.components.tilegroups.layers.TileLayerOverlay import com.unciv.ui.components.tilegroups.layers.TileLayerTerrain import com.unciv.ui.components.tilegroups.layers.TileLayerUnitArt import com.unciv.ui.components.tilegroups.layers.TileLayerUnitFlag +import com.unciv.utils.DebugUtils import kotlin.math.pow import kotlin.math.sqrt @@ -40,7 +40,7 @@ open class TileGroup( val hexagonImageOrigin = Pair(hexagonImageWidth / 2f, sqrt((hexagonImageWidth / 2f).pow(2) - (hexagonImageWidth / 4f).pow(2))) val hexagonImagePosition = Pair(-hexagonImageOrigin.first / 3f, -hexagonImageOrigin.second / 4f) - var isForceVisible = UncivGame.Current.viewEntireMapForDebug + var isForceVisible = DebugUtils.VISIBLE_MAP var isForMapEditorIcon = false @Suppress("LeakingThis") val layerTerrain = TileLayerTerrain(this, groupSize) diff --git a/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerCityButton.kt b/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerCityButton.kt index 992e2c71ef..7532ccb6a8 100644 --- a/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerCityButton.kt +++ b/core/src/com/unciv/ui/components/tilegroups/layers/TileLayerCityButton.kt @@ -4,11 +4,11 @@ import com.badlogic.gdx.graphics.g2d.Batch import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.utils.Align -import com.unciv.UncivGame import com.unciv.logic.civilization.Civilization import com.unciv.ui.components.tilegroups.CityButton import com.unciv.ui.components.tilegroups.TileGroup import com.unciv.ui.components.tilegroups.WorldTileGroup +import com.unciv.utils.DebugUtils class TileLayerCityButton(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, size) { @@ -60,7 +60,7 @@ class TileLayerCityButton(tileGroup: TileGroup, size: Float) : TileLayer(tileGro return val tileIsViewable = isViewable(viewingCiv) - val shouldShow = UncivGame.Current.viewEntireMapForDebug + val shouldShow = DebugUtils.VISIBLE_MAP // Create (if not yet) and update city button if (city != null && tileGroup.tile.isCityCenter()) { diff --git a/core/src/com/unciv/ui/crashhandling/CrashReportSysInfo.kt b/core/src/com/unciv/ui/crashhandling/CrashReportSysInfo.kt deleted file mode 100644 index febae70a24..0000000000 --- a/core/src/com/unciv/ui/crashhandling/CrashReportSysInfo.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.unciv.ui.crashhandling - -interface CrashReportSysInfo { - fun getInfo(): String -} diff --git a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt index 86ac71f204..eb54a9c31c 100644 --- a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt +++ b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt @@ -19,6 +19,7 @@ import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.ToastPopup +import com.unciv.utils.Log import java.io.PrintWriter import java.io.StringWriter @@ -88,7 +89,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() { -------------------------------- - ${UncivGame.Current.crashReportSysInfo?.getInfo().toString().prependIndentToOnlyNewLines(baseIndent)} + ${Log.getSystemInfo().prependIndentToOnlyNewLines(baseIndent)} -------------------------------- diff --git a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt index 7aeb319a47..4b5af2e3cb 100644 --- a/core/src/com/unciv/ui/popups/options/AdvancedTab.kt +++ b/core/src/com/unciv/ui/popups/options/AdvancedTab.kt @@ -54,17 +54,18 @@ fun advancedTab( addAutosaveTurnsSelectBox(this, settings) - if (UncivGame.Current.platformSpecificHelper?.hasDisplayCutout() == true) - optionsPopup.addCheckbox(this, "Enable display cutout (requires restart)", settings.androidCutout, false) { settings.androidCutout = it } + if (UncivGame.Current.hasDisplayCutout()) + optionsPopup.addCheckbox(this, "Enable display cutout (requires restart)", settings.androidCutout) { + settings.androidCutout = it + } addMaxZoomSlider(this, settings) - val helper = UncivGame.Current.platformSpecificHelper - if (helper != null && Gdx.app.type == Application.ApplicationType.Android) { + if (Gdx.app.type == Application.ApplicationType.Android) { optionsPopup.addCheckbox(this, "Enable portrait orientation", settings.allowAndroidPortrait) { settings.allowAndroidPortrait = it // Note the following might close the options screen indirectly and delayed - helper.allowPortrait(it) + UncivGame.Current.allowPortrait(it) } } @@ -237,7 +238,7 @@ private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) { Concurrency.run("GenerateScreenshot") { val extraImagesLocation = "../../extraImages" // I'm not sure why we need to advance the y by 2 for every screenshot... but that's the only way it remains centered - generateScreenshots(arrayListOf( + generateScreenshots(optionsPopup.settings, arrayListOf( ScreenshotConfig(630, 500, ScreenSize.Medium, "$extraImagesLocation/itch.io image.png", Vector2(-2f, 2f),false), ScreenshotConfig(1280, 640, ScreenSize.Medium, "$extraImagesLocation/GithubPreviewImage.png", Vector2(-2f, 4f)), ScreenshotConfig(1024, 500, ScreenSize.Medium, "$extraImagesLocation/Feature graphic - Google Play.png",Vector2(-2f, 6f)), @@ -251,12 +252,12 @@ private fun addTranslationGeneration(table: Table, optionsPopup: OptionsPopup) { data class ScreenshotConfig(val width: Int, val height: Int, val screenSize: ScreenSize, var fileLocation:String, var centerTile:Vector2, var attackCity:Boolean=true) -private fun CoroutineScope.generateScreenshots(configs:ArrayList) { +private fun CoroutineScope.generateScreenshots(settings: GameSettings, configs:ArrayList) { val currentConfig = configs.first() launchOnGLThread { val screenshotGame = UncivGame.Current.files.loadGameByName("ScreenshotGenerationGame") - UncivGame.Current.settings.screenSize = currentConfig.screenSize + settings.screenSize = currentConfig.screenSize val newScreen = UncivGame.Current.loadGame(screenshotGame) newScreen.stage.viewport.update(currentConfig.width, currentConfig.height, true) @@ -290,7 +291,7 @@ private fun CoroutineScope.generateScreenshots(configs:ArrayList Unit = {} ) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = false ) { + + val game = screen.game val settings = screen.game.settings val tabs: TabbedPager val selectBoxMinWidth: Float @@ -120,7 +122,7 @@ class OptionsPopup( addCloseButton { screen.game.musicController.onChange(null) - screen.game.platformSpecificHelper?.allowPortrait(settings.allowAndroidPortrait) + screen.game.allowPortrait(settings.allowAndroidPortrait) onClose() }.padBottom(10f) @@ -205,7 +207,7 @@ open class SettingsSelect( ) { private val settingsProperty: KMutableProperty0 = setting.getProperty(settings) private val label = createLabel(labelText) - protected val refreshSelectBox = createSelectBox(items.toGdxArray(), settings) + private val refreshSelectBox = createSelectBox(items.toGdxArray(), settings) val items by refreshSelectBox::items private fun createLabel(labelText: String): Label { diff --git a/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTable.kt b/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTable.kt index a5161f8bf8..87b3c65cb5 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTable.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/WonderOverviewTable.kt @@ -17,6 +17,7 @@ import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.images.ImageGetter import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen +import com.unciv.utils.DebugUtils class WonderOverviewTab( viewingPlayer: Civilization, @@ -115,7 +116,7 @@ class WonderInfo { val city: City?, val location: Tile? ) { - val viewEntireMapForDebug = UncivGame.Current.viewEntireMapForDebug + val viewEntireMapForDebug = DebugUtils.VISIBLE_MAP fun getImage() = if (status == WonderStatus.Unknown && !viewEntireMapForDebug) null else category.getImage?.invoke(name, if (category == CivilopediaCategories.Terrain) 50f else 45f) diff --git a/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt b/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt index 080c3fd5bd..78af80dfd4 100644 --- a/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/screens/savescreens/LoadGameScreen.kt @@ -159,22 +159,22 @@ class LoadGameScreen : LoadOrSaveScreen() { } private fun Table.addLoadFromCustomLocationButton() { - if (!game.files.canLoadFromCustomSaveLocation()) return val loadFromCustomLocation = loadFromCustomLocation.toTextButton() loadFromCustomLocation.onClick { errorLabel.isVisible = false loadFromCustomLocation.setText(Constants.loading.tr()) loadFromCustomLocation.disable() Concurrency.run(Companion.loadFromCustomLocation) { - game.files.loadGameFromCustomLocation { result -> - if (result.isError()) { - handleLoadGameException(result.exception!!, "Could not load game from custom location!") - } else if (result.isSuccessful()) { - Concurrency.run { - game.loadGame(result.gameData!!, true) - } + game.files.loadGameFromCustomLocation( + { + Concurrency.run { game.loadGame(it, true) } + loadFromCustomLocation.enable() + }, + { + handleLoadGameException(it, "Could not load game from custom location!") + loadFromCustomLocation.enable() } - } + ) } } add(loadFromCustomLocation).row() diff --git a/core/src/com/unciv/ui/screens/savescreens/SaveGameScreen.kt b/core/src/com/unciv/ui/screens/savescreens/SaveGameScreen.kt index 9e0573f781..3c907e426c 100644 --- a/core/src/com/unciv/ui/screens/savescreens/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/screens/savescreens/SaveGameScreen.kt @@ -82,7 +82,6 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") } private fun Table.addSaveToCustomLocation() { - if (!game.files.canLoadFromCustomSaveLocation()) return val saveToCustomLocation = "Save to custom location".toTextButton() val errorLabel = "".toLabel(Color.RED) saveToCustomLocation.onClick { @@ -90,15 +89,18 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves") saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.disable() Concurrency.runOnNonDaemonThreadPool("Save to custom location") { - game.files.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result -> - if (result.isError()) { - errorLabel.setText("Could not save game to custom location!".tr()) - result.exception?.printStackTrace() - } else if (result.isSuccessful()) { + + game.files.saveGameToCustomLocation(gameInfo, gameNameTextField.text, + { game.popScreen() + saveToCustomLocation.enable() + }, + { + errorLabel.setText("Could not save game to custom location!".tr()) + it.printStackTrace() + saveToCustomLocation.enable() } - saveToCustomLocation.enable() - } + ) } } add(saveToCustomLocation).row() diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index b694352189..b2326fe384 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -350,7 +350,7 @@ class WorldScreen( debug("loadLatestMultiplayerState downloaded game: gameId: %s, turn: %s, curCiv: %s", latestGame.gameId, latestGame.turns, latestGame.currentPlayer) if (viewingCiv.civName == latestGame.currentPlayer || viewingCiv.civName == Constants.spectator) { - game.platformSpecificHelper?.notifyTurnStarted() + game.notifyTurnStarted() } launchOnGLThread { loadingGamePopup.close() diff --git a/core/src/com/unciv/ui/screens/worldscreen/bottombar/BattleTable.kt b/core/src/com/unciv/ui/screens/worldscreen/bottombar/BattleTable.kt index 4dd7b064b6..df442b1352 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/bottombar/BattleTable.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/bottombar/BattleTable.kt @@ -31,6 +31,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.flashWoundedCombatants import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.getHealthBar +import com.unciv.utils.DebugUtils import kotlin.math.max import kotlin.math.roundToInt @@ -102,15 +103,12 @@ class BattleTable(val worldScreen: WorldScreen): Table() { if (defender == null || (!includeFriendly && defender.getCivInfo() == attackerCiv)) return null // no enemy combatant in tile - val canSeeDefender = - if (UncivGame.Current.viewEntireMapForDebug) true - else { - when { - defender.isInvisible(attackerCiv) -> attackerCiv.viewableInvisibleUnitsTiles.contains(selectedTile) - defender.isCity() -> attackerCiv.hasExplored(selectedTile) - else -> attackerCiv.viewableTiles.contains(selectedTile) - } - } + val canSeeDefender = when { + DebugUtils.VISIBLE_MAP -> true + defender.isInvisible(attackerCiv) -> attackerCiv.viewableInvisibleUnitsTiles.contains(selectedTile) + defender.isCity() -> attackerCiv.hasExplored(selectedTile) + else -> attackerCiv.viewableTiles.contains(selectedTile) + } if (!canSeeDefender) return null diff --git a/core/src/com/unciv/ui/screens/worldscreen/bottombar/TileInfoTable.kt b/core/src/com/unciv/ui/screens/worldscreen/bottombar/TileInfoTable.kt index a9b85b2e42..87c74d8338 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/bottombar/TileInfoTable.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/bottombar/TileInfoTable.kt @@ -15,6 +15,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.components.extensions.addBorderAllowOpacity import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.toLabel +import com.unciv.utils.DebugUtils class TileInfoTable(private val viewingCiv :Civilization) : Table(BaseScreen.skin) { init { @@ -27,12 +28,12 @@ class TileInfoTable(private val viewingCiv :Civilization) : Table(BaseScreen.ski internal fun updateTileTable(tile: Tile?) { clearChildren() - if (tile != null && (UncivGame.Current.viewEntireMapForDebug || viewingCiv.hasExplored(tile)) ) { + if (tile != null && (DebugUtils.VISIBLE_MAP || viewingCiv.hasExplored(tile)) ) { add(getStatsTable(tile)) add(MarkupRenderer.render(TileDescription.toMarkup(tile, viewingCiv), padding = 0f, iconDisplay = IconDisplay.None) { UncivGame.Current.pushScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleset, link = it)) } ).pad(5f).row() - if (UncivGame.Current.viewEntireMapForDebug) + if (DebugUtils.VISIBLE_MAP) add(tile.position.run { "(${x.toInt()},${y.toInt()})" }.toLabel()).colspan(2).pad(5f) } diff --git a/core/src/com/unciv/ui/screens/worldscreen/minimap/MinimapTile.kt b/core/src/com/unciv/ui/screens/worldscreen/minimap/MinimapTile.kt index 5ccecdf71a..45219997e3 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/minimap/MinimapTile.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/minimap/MinimapTile.kt @@ -5,7 +5,6 @@ import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.utils.Align -import com.unciv.UncivGame import com.unciv.logic.map.HexMath import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.tile.Tile @@ -13,6 +12,7 @@ import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.images.ImageGetter import com.unciv.ui.components.extensions.onClick import com.unciv.ui.components.extensions.surroundWithCircle +import com.unciv.utils.DebugUtils import kotlin.math.PI import kotlin.math.atan @@ -36,7 +36,7 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U } fun updateColor(isTileUnrevealed: Boolean) { - image.isVisible = UncivGame.Current.viewEntireMapForDebug || !isTileUnrevealed + image.isVisible = DebugUtils.VISIBLE_MAP || !isTileUnrevealed if (!image.isVisible) return image.color = when { tile.isCityCenter() && !tile.isWater -> tile.getOwner()!!.nation.getInnerColor() diff --git a/core/src/com/unciv/utils/Debug.kt b/core/src/com/unciv/utils/Debug.kt new file mode 100644 index 0000000000..db95b76a13 --- /dev/null +++ b/core/src/com/unciv/utils/Debug.kt @@ -0,0 +1,20 @@ +package com.unciv.utils + +object DebugUtils { + + /** + * This exists so that when debugging we can see the entire map. + * Remember to turn this to false before commit and upload! + */ + var VISIBLE_MAP: Boolean = false + + /** For when you need to test something in an advanced game and don't have time to faff around */ + var SUPERCHARGED: Boolean = false + + /** Simulate until this turn on the first "Next turn" button press. + * Does not update World View changes until finished. + * Set to 0 to disable. + */ + var SIMULATE_UNTIL_TURN: Int = 0 + +} diff --git a/core/src/com/unciv/utils/Log.kt b/core/src/com/unciv/utils/Log.kt index 88d7598b72..54d9790284 100644 --- a/core/src/com/unciv/utils/Log.kt +++ b/core/src/com/unciv/utils/Log.kt @@ -143,6 +143,13 @@ object Log { fun error(tag: Tag, msg: String, throwable: Throwable) { doLog(backend::error, tag, buildThrowableMessage(msg, throwable)) } + + /** + * Get string information about operation system + */ + fun getSystemInfo(): String { + return backend.getSystemInfo() + } } class Tag(val name: String) @@ -153,6 +160,9 @@ interface LogBackend { /** Do not log on release builds for performance reasons. */ fun isRelease(): Boolean + + /** Get string information about operation system */ + fun getSystemInfo(): String } /** Only for tests, or temporary main() functions */ @@ -168,6 +178,10 @@ open class DefaultLogBackend : LogBackend { override fun isRelease(): Boolean { return false } + + override fun getSystemInfo(): String { + return "" + } } /** Shortcut for [Log.debug] */ diff --git a/core/src/com/unciv/utils/PlatformSpecific.kt b/core/src/com/unciv/utils/PlatformSpecific.kt new file mode 100644 index 0000000000..6c9a86ba9e --- /dev/null +++ b/core/src/com/unciv/utils/PlatformSpecific.kt @@ -0,0 +1,17 @@ +package com.unciv.utils + +interface PlatformSpecific { + + /** Notifies player that his multiplayer turn started */ + fun notifyTurnStarted() {} + + /** Install system audio hooks */ + fun installAudioHooks() {} + + /** (Android) allow screen orientation switch */ + fun allowPortrait(allow: Boolean) {} + + /** (Android) returns whether display has cutout */ + fun hasDisplayCutout(): Boolean { return false } + +} diff --git a/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt b/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt index 5db13c818b..fe6c1fa725 100644 --- a/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt @@ -2,7 +2,6 @@ package com.unciv.app.desktop import com.unciv.Constants import com.unciv.UncivGame -import com.unciv.UncivGameParameters import com.unciv.utils.Log import com.unciv.logic.GameStarter import com.unciv.logic.civilization.PlayerType @@ -26,8 +25,7 @@ internal object ConsoleLauncher { fun main(arg: Array) { Log.backend = DesktopLogBackend() - val consoleParameters = UncivGameParameters(consoleMode = true) - val game = UncivGame(consoleParameters) + val game = UncivGame(true) UncivGame.Current = game UncivGame.Current.settings = GameSettings().apply { diff --git a/desktop/src/com/unciv/app/desktop/CrashReportSysInfoDesktop.kt b/desktop/src/com/unciv/app/desktop/CrashReportSysInfoDesktop.kt deleted file mode 100644 index 415b7e22d8..0000000000 --- a/desktop/src/com/unciv/app/desktop/CrashReportSysInfoDesktop.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.unciv.app.desktop - -import com.badlogic.gdx.files.FileHandle -import com.unciv.ui.crashhandling.CrashReportSysInfo -import java.nio.charset.Charset - -class CrashReportSysInfoDesktop : CrashReportSysInfo { - - override fun getInfo(): String { - val builder = StringBuilder() - - // Operating system - val osName = System.getProperty("os.name") ?: "Unknown" - val isWindows = osName.startsWith("Windows", ignoreCase = true) - builder.append("OS: $osName") - if (!isWindows) { - val osInfo = listOfNotNull(System.getProperty("os.arch"), System.getProperty("os.version")).joinToString() - if (osInfo.isNotEmpty()) builder.append(" ($osInfo)") - } - builder.appendLine() - - // Specific release info - val osRelease = if (isWindows) getWinVer() else getLinuxDistro() - if (osRelease.isNotEmpty()) - builder.appendLine("\t$osRelease") - - // Java runtime version - val javaVendor: String? = System.getProperty("java.vendor") - if (javaVendor != null) { - val javaVersion: String = System.getProperty("java.vendor.version") ?: System.getProperty("java.vm.version") ?: "" - builder.appendLine("Java: $javaVendor $javaVersion") - } - - // Java VM memory limit as set by -Xmx - val maxMemory = try { - Runtime.getRuntime().maxMemory() / 1024 / 1024 - } catch (ex: Throwable) { -1L } - if (maxMemory > 0) { - builder.append('\t') - builder.appendLine("Max Memory: $maxMemory MB") - } - - return builder.toString() - } - - companion object { - @Suppress("SpellCheckingInspection") - private val winVerCommand = """ - cmd /c - reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ProductName && - reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ReleaseId && - reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v CurrentBuild && - reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v DisplayVersion - """.trimIndent().replace('\n', ' ') - - /** Kludge to get the important Windows version info (no easier way than the registry AFAIK) - * using a subprocess running reg query. Other methods would involve nasty reflection - * to break java.util.prefs.Preferences out of its Sandbox, or JNA requiring new bindings. - */ - fun getWinVer(): String { - val entries: Map = try { - val process = Runtime.getRuntime().exec(winVerCommand) - process.waitFor() - val output = process.inputStream.readAllBytes().toString(Charset.defaultCharset()) - - val goodLines = output.split('\n').mapNotNull { - it.removeSuffix("\r").run { - if (startsWith(" ") || startsWith("\t")) trim() else null - } - } - - goodLines.map { it.split("REG_SZ") } - .filter { it.size == 2 } - .associate { it[0].trim() to it[1].trim() } - } catch (ex: Throwable) { mapOf() } - - if ("ProductName" !in entries) return "" - - return entries["ProductName"]!! + - ((entries["DisplayVersion"] ?: entries["ReleaseId"])?.run { " Version $this" } ?: "") + - (entries["CurrentBuild"]?.run { " (Build $this)" } ?: "") - } - - /** Get linux Distribution out of the /etc/os-release file (ini-style) - * Should be safely silent on systems not supporting that file. - */ - fun getLinuxDistro(): String { - val osRelease: Map = try { - FileHandle("/etc/os-release") - .readString() - .split('\n') - .map { it.split('=') } - .filter { it.size == 2 } - .associate { it[0] to it[1].removeSuffix("\"").removePrefix("\"") } - } catch (ex: Throwable) { mapOf() } - if ("NAME" !in osRelease) return "" - return osRelease["PRETTY_NAME"] ?: "${osRelease["NAME"]} ${osRelease["VERSION"]}" - } - } -} diff --git a/desktop/src/com/unciv/app/desktop/FontDesktop.kt b/desktop/src/com/unciv/app/desktop/DesktopFont.kt similarity index 98% rename from desktop/src/com/unciv/app/desktop/FontDesktop.kt rename to desktop/src/com/unciv/app/desktop/DesktopFont.kt index 8f1c58269f..672026239d 100644 --- a/desktop/src/com/unciv/app/desktop/FontDesktop.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopFont.kt @@ -10,7 +10,7 @@ import java.awt.image.BufferedImage import java.util.* -class FontDesktop : FontImplementation { +class DesktopFont : FontImplementation { private lateinit var font: Font private lateinit var metric: FontMetrics diff --git a/desktop/src/com/unciv/app/desktop/DesktopGame.kt b/desktop/src/com/unciv/app/desktop/DesktopGame.kt new file mode 100644 index 0000000000..1a107a14f5 --- /dev/null +++ b/desktop/src/com/unciv/app/desktop/DesktopGame.kt @@ -0,0 +1,52 @@ +package com.unciv.app.desktop + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration +import com.unciv.UncivGame + +class DesktopGame(config: Lwjgl3ApplicationConfiguration) : UncivGame() { + + private val audio = HardenGdxAudio() + private var discordUpdater = DiscordUpdater() + private val turnNotifier = MultiplayerTurnNotifierDesktop() + + init { + config.setWindowListener(turnNotifier) + + discordUpdater.setOnUpdate { + + if (!isInitialized) + return@setOnUpdate null + + val info = DiscordGameInfo() + val game = gameInfo + + if (game != null) { + info.gameTurn = game.turns + info.gameLeader = game.getCurrentPlayerCivilization().nation.leaderName + info.gameNation = game.getCurrentPlayerCivilization().nation.name + } + + return@setOnUpdate info + + } + + discordUpdater.startUpdates() + } + + override fun installAudioHooks() { + audio.installHooks( + musicController.getAudioLoopCallback(), + musicController.getAudioExceptionHandler() + ) + } + + override fun notifyTurnStarted() { + turnNotifier.turnStarted() + } + + override fun dispose() { + discordUpdater.stopUpdates() + super.dispose() + } + +} diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index 17e2a8018d..f71e1e4786 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -1,33 +1,33 @@ package com.unciv.app.desktop -import club.minnced.discord.rpc.DiscordEventHandlers -import club.minnced.discord.rpc.DiscordRPC -import club.minnced.discord.rpc.DiscordRichPresence import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.graphics.glutils.HdpiMode -import com.sun.jna.Native -import com.unciv.UncivGame -import com.unciv.UncivGameParameters import com.unciv.json.json import com.unciv.logic.files.SETTINGS_FILE_NAME import com.unciv.logic.files.UncivFiles import com.unciv.models.metadata.ScreenSize import com.unciv.models.metadata.WindowState +import com.unciv.ui.components.Fonts import com.unciv.utils.Log -import com.unciv.utils.debug import java.awt.GraphicsEnvironment -import java.util.* -import kotlin.concurrent.timer - internal object DesktopLauncher { - private var discordTimer: Timer? = null @JvmStatic fun main(arg: Array) { + + // Setup Desktop logging Log.backend = DesktopLogBackend() + + // Setup Desktop font + Fonts.fontImplementation = DesktopFont() + + // Setup Desktop saver-loader + UncivFiles.saverLoader = DesktopSaverLoader() + UncivFiles.preferExternalStorage = false + // Solves a rendering problem in specific GPUs and drivers. // For more info see https://github.com/yairm210/Unciv/pull/3202 and https://github.com/LWJGL/lwjgl/issues/119 System.setProperty("org.lwjgl.opengl.Display.allowSoftwareOpenGL", "true") @@ -69,60 +69,7 @@ internal object DesktopLauncher { UiElementDocsWriter().write() } - val platformSpecificHelper = PlatformSpecificHelpersDesktop(config) - val desktopParameters = UncivGameParameters( - cancelDiscordEvent = { discordTimer?.cancel() }, - fontImplementation = FontDesktop(), - customFileLocationHelper = CustomFileLocationHelperDesktop(), - crashReportSysInfo = CrashReportSysInfoDesktop(), - platformSpecificHelper = platformSpecificHelper, - audioExceptionHelper = HardenGdxAudio() - ) - - val game = UncivGame(desktopParameters) - - tryActivateDiscord(game) + val game = DesktopGame(config) Lwjgl3Application(game, config) } - - private fun tryActivateDiscord(game: UncivGame) { - try { - /* - We try to load the Discord library manually before the instance initializes. - This is because if there's a crash when the instance initializes on a similar line, - it's not within the bounds of the try/catch and thus the app will crash. - */ - Native.load("discord-rpc", DiscordRPC::class.java) - val handlers = DiscordEventHandlers() - DiscordRPC.INSTANCE.Discord_Initialize("647066573147996161", handlers, true, null) - - Runtime.getRuntime().addShutdownHook(Thread { DiscordRPC.INSTANCE.Discord_Shutdown() }) - - discordTimer = timer(name = "Discord", daemon = true, period = 1000) { - try { - updateRpc(game) - } catch (ex: Exception) { - debug("Exception while updating Discord Rich Presence", ex) - } - } - } catch (ex: Throwable) { - // This needs to be a Throwable because if we can't find the discord_rpc library, we'll get a UnsatisfiedLinkError, which is NOT an exception. - debug("Could not initialize Discord") - } - } - - private fun updateRpc(game: UncivGame) { - if (!game.isInitialized) return - val presence = DiscordRichPresence() - presence.largeImageKey = "logo" // The actual image is uploaded to the discord app / applications webpage - - val gameInfo = game.gameInfo - if (gameInfo != null) { - val currentPlayerCiv = gameInfo.getCurrentPlayerCivilization() - presence.details = "${currentPlayerCiv.nation.leaderName} of ${currentPlayerCiv.nation.name}" - presence.largeImageText = "Turn" + " " + currentPlayerCiv.gameInfo.turns - } - - DiscordRPC.INSTANCE.Discord_UpdatePresence(presence) - } } diff --git a/desktop/src/com/unciv/app/desktop/DesktopLogBackend.kt b/desktop/src/com/unciv/app/desktop/DesktopLogBackend.kt index 0f9c10db7c..122c167ef8 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLogBackend.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLogBackend.kt @@ -7,10 +7,14 @@ class DesktopLogBackend : DefaultLogBackend() { // -ea (enable assertions) or kotlin debugging property as marker for a debug run. // Can easily be added to IntelliJ/Android Studio launch configuration template for all launches. - private val release = !ManagementFactory.getRuntimeMXBean().getInputArguments().contains("-ea") + private val release = !ManagementFactory.getRuntimeMXBean().inputArguments.contains("-ea") && System.getProperty("kotlinx.coroutines.debug") == null override fun isRelease(): Boolean { return release } + + override fun getSystemInfo(): String { + return SystemUtils.getSystemInfo() + } } diff --git a/desktop/src/com/unciv/app/desktop/CustomFileLocationHelperDesktop.kt b/desktop/src/com/unciv/app/desktop/DesktopSaverLoader.kt similarity index 50% rename from desktop/src/com/unciv/app/desktop/CustomFileLocationHelperDesktop.kt rename to desktop/src/com/unciv/app/desktop/DesktopSaverLoader.kt index 8f529eb363..cc49608de8 100644 --- a/desktop/src/com/unciv/app/desktop/CustomFileLocationHelperDesktop.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopSaverLoader.kt @@ -1,7 +1,8 @@ package com.unciv.app.desktop import com.badlogic.gdx.Gdx -import com.unciv.logic.files.CustomFileLocationHelper +import com.unciv.logic.files.PlatformSaverLoader +import com.unciv.utils.Log import java.awt.Component import java.awt.EventQueue import java.awt.event.WindowEvent @@ -11,17 +12,45 @@ import java.io.OutputStream import javax.swing.JFileChooser import javax.swing.JFrame -class CustomFileLocationHelperDesktop : CustomFileLocationHelper() { +class DesktopSaverLoader : PlatformSaverLoader { + + override fun saveGame( + data: String, + suggestedLocation: String, + onSaved: (location: String) -> Unit, + onError: (ex: Exception) -> Unit + ) { + val onFileChosen = { stream: OutputStream, location: String -> + try { + stream.writer().use { it.write(data) } + onSaved(location) + } catch (ex: Exception) { + onError(ex) + } + } + + pickFile(onFileChosen, onError, JFileChooser::showSaveDialog, File::outputStream, suggestedLocation) - override fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) { - pickFile(callback, JFileChooser::showSaveDialog, File::outputStream, suggestedLocation) } - override fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) { - pickFile(callback, JFileChooser::showOpenDialog, File::inputStream) + override fun loadGame( + onLoaded: (data: String, location: String) -> Unit, + onError: (ex: Exception) -> Unit + ) { + val onFileChosen = { stream: InputStream, location: String -> + try { + val data = stream.reader().use { it.readText() } + onLoaded(data, location) + } catch (ex: Exception) { + onError(ex) + } + } + + pickFile(onFileChosen, onError, JFileChooser::showOpenDialog, File::inputStream) } - private fun pickFile(callback: (String?, T?, Exception?) -> Unit, + private fun pickFile(onSuccess: (T, String) -> Unit, + onError: (Exception) -> Unit, chooseAction: (JFileChooser, Component) -> Int, createValue: (File) -> T, suggestedLocation: String? = null) { @@ -47,13 +76,13 @@ class CustomFileLocationHelperDesktop : CustomFileLocationHelper() { frame.dispose() if (result == JFileChooser.CANCEL_OPTION) { - callback(null, null, null) + return@invokeLater } else { val value = createValue(fileChooser.selectedFile) - callback(fileChooser.selectedFile.absolutePath, value, null) + onSuccess(value, fileChooser.selectedFile.absolutePath) } } catch (ex: Exception) { - callback(null, null, ex) + onError(ex) } } } diff --git a/desktop/src/com/unciv/app/desktop/DiscordUpdater.kt b/desktop/src/com/unciv/app/desktop/DiscordUpdater.kt new file mode 100644 index 0000000000..6e739e3567 --- /dev/null +++ b/desktop/src/com/unciv/app/desktop/DiscordUpdater.kt @@ -0,0 +1,76 @@ +package com.unciv.app.desktop + +import club.minnced.discord.rpc.DiscordEventHandlers +import club.minnced.discord.rpc.DiscordRPC +import club.minnced.discord.rpc.DiscordRichPresence +import com.sun.jna.Native +import com.unciv.utils.debug +import java.util.* +import kotlin.concurrent.timer + +class DiscordGameInfo( + var gameNation: String = "", + var gameLeader: String = "", + var gameTurn: Int = 0 +) + +class DiscordUpdater { + + private var onUpdate: (() -> DiscordGameInfo?)? = null + private var updateTimer: Timer? = null + + fun setOnUpdate(callback: () -> DiscordGameInfo?) { + onUpdate = callback + } + + fun startUpdates() { + try { + /* + We try to load the Discord library manually before the instance initializes. + This is because if there's a crash when the instance initializes on a similar line, + it's not within the bounds of the try/catch and thus the app will crash. + */ + Native.load("discord-rpc", DiscordRPC::class.java) + val handlers = DiscordEventHandlers() + DiscordRPC.INSTANCE.Discord_Initialize("647066573147996161", handlers, true, null) + + Runtime.getRuntime().addShutdownHook(Thread { DiscordRPC.INSTANCE.Discord_Shutdown() }) + + updateTimer = timer(name = "Discord", daemon = true, period = 1000) { + try { + updateRpc() + } catch (ex: Exception) { + debug("Exception while updating Discord Rich Presence", ex) + } + } + } catch (ex: Throwable) { + // This needs to be a Throwable because if we can't find the discord_rpc library, we'll get a UnsatisfiedLinkError, which is NOT an exception. + debug("Could not initialize Discord") + } + } + + fun stopUpdates() { + updateTimer?.cancel() + } + + private fun updateRpc() { + + if (onUpdate == null) + return + + val info = onUpdate!!.invoke() ?: return + + val presence = DiscordRichPresence() + presence.largeImageKey = "logo" // The actual image is uploaded to the discord app / applications webpage + + if (info.gameLeader.isNotEmpty() && info.gameNation.isNotEmpty()) { + presence.details = "${info.gameLeader} of ${info.gameNation}" + presence.details = "Turn ${info.gameTurn}" + } + + DiscordRPC.INSTANCE.Discord_UpdatePresence(presence) + } + + + +} diff --git a/desktop/src/com/unciv/app/desktop/PlatformSpecificHelpersDesktop.kt b/desktop/src/com/unciv/app/desktop/PlatformSpecificHelpersDesktop.kt deleted file mode 100644 index 9c1fe2cb12..0000000000 --- a/desktop/src/com/unciv/app/desktop/PlatformSpecificHelpersDesktop.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.unciv.app.desktop - -import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration -import com.unciv.ui.components.GeneralPlatformSpecificHelpers - -class PlatformSpecificHelpersDesktop(config: Lwjgl3ApplicationConfiguration) : GeneralPlatformSpecificHelpers { - val turnNotifier = MultiplayerTurnNotifierDesktop() - init { - config.setWindowListener(turnNotifier) - } - - override fun notifyTurnStarted() { - turnNotifier.turnStarted() - } - - /** On desktop, external is likely some document folder, while local is the game directory. We'd like to keep everything in the game directory */ - override fun shouldPreferExternalStorage(): Boolean = false -} diff --git a/desktop/src/com/unciv/app/desktop/SystemUtils.kt b/desktop/src/com/unciv/app/desktop/SystemUtils.kt new file mode 100644 index 0000000000..5da201f6c4 --- /dev/null +++ b/desktop/src/com/unciv/app/desktop/SystemUtils.kt @@ -0,0 +1,97 @@ +package com.unciv.app.desktop + +import com.badlogic.gdx.files.FileHandle +import java.nio.charset.Charset + +object SystemUtils { + + fun getSystemInfo(): String { + val builder = StringBuilder() + + // Operating system + val osName = System.getProperty("os.name") ?: "Unknown" + val isWindows = osName.startsWith("Windows", ignoreCase = true) + builder.append("OS: $osName") + if (!isWindows) { + val osInfo = listOfNotNull(System.getProperty("os.arch"), System.getProperty("os.version")).joinToString() + if (osInfo.isNotEmpty()) builder.append(" ($osInfo)") + } + builder.appendLine() + + // Specific release info + val osRelease = if (isWindows) getWinVer() else getLinuxDistro() + if (osRelease.isNotEmpty()) + builder.appendLine("\t$osRelease") + + // Java runtime version + val javaVendor: String? = System.getProperty("java.vendor") + if (javaVendor != null) { + val javaVersion: String = System.getProperty("java.vendor.version") ?: System.getProperty("java.vm.version") ?: "" + builder.appendLine("Java: $javaVendor $javaVersion") + } + + // Java VM memory limit as set by -Xmx + val maxMemory = try { + Runtime.getRuntime().maxMemory() / 1024 / 1024 + } catch (ex: Throwable) { -1L } + if (maxMemory > 0) { + builder.append('\t') + builder.appendLine("Max Memory: $maxMemory MB") + } + + return builder.toString() + } + + /** Kludge to get the important Windows version info (no easier way than the registry AFAIK) + * using a subprocess running reg query. Other methods would involve nasty reflection + * to break java.util.prefs.Preferences out of its Sandbox, or JNA requiring new bindings. + */ + private fun getWinVer(): String { + val winVerCommand = """ + cmd /c + reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ProductName && + reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ReleaseId && + reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v CurrentBuild && + reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v DisplayVersion + """.trimIndent().replace('\n', ' ') + + val entries: Map = try { + val process = Runtime.getRuntime().exec(winVerCommand) + process.waitFor() + val output = process.inputStream.readAllBytes().toString(Charset.defaultCharset()) + + val goodLines = output.split('\n').mapNotNull { + it.removeSuffix("\r").run { + if (startsWith(" ") || startsWith("\t")) trim() else null + } + } + + goodLines.map { it.split("REG_SZ") } + .filter { it.size == 2 } + .associate { it[0].trim() to it[1].trim() } + } catch (ex: Throwable) { mapOf() } + + if ("ProductName" !in entries) return "" + + return entries["ProductName"]!! + + ((entries["DisplayVersion"] ?: entries["ReleaseId"])?.run { " Version $this" } ?: "") + + (entries["CurrentBuild"]?.run { " (Build $this)" } ?: "") + } + + /** Get linux Distribution out of the /etc/os-release file (ini-style) + * Should be safely silent on systems not supporting that file. + */ + private fun getLinuxDistro(): String { + val osRelease: Map = try { + FileHandle("/etc/os-release") + .readString() + .split('\n') + .map { it.split('=') } + .filter { it.size == 2 } + .associate { it[0] to it[1].removeSuffix("\"").removePrefix("\"") } + } catch (ex: Throwable) { mapOf() } + if ("NAME" !in osRelease) return "" + return osRelease["PRETTY_NAME"] ?: "${osRelease["NAME"]} ${osRelease["VERSION"]}" + } + +} diff --git a/tests/src/com/unciv/dev/FasterUIDevelopment.kt b/tests/src/com/unciv/dev/FasterUIDevelopment.kt index dced6d7056..2a2e4f6b76 100644 --- a/tests/src/com/unciv/dev/FasterUIDevelopment.kt +++ b/tests/src/com/unciv/dev/FasterUIDevelopment.kt @@ -11,7 +11,6 @@ import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.InputListener import com.unciv.UncivGame -import com.unciv.UncivGameParameters import com.unciv.logic.files.UncivFiles import com.unciv.logic.multiplayer.throttle import com.unciv.ui.images.ImageGetter @@ -61,10 +60,11 @@ object FasterUIDevelopment { } class UIDevGame : Game() { - val game = UncivGame(UncivGameParameters( - fontImplementation = FontDesktop() - )) + + private val game = UncivGame() + override fun create() { + Fonts.fontImplementation = FontDesktop() UncivGame.Current = game UncivGame.Current.files = UncivFiles(Gdx.files) game.settings = UncivGame.Current.files.getGeneralSettings() diff --git a/tests/src/com/unciv/testing/BasicTests.kt b/tests/src/com/unciv/testing/BasicTests.kt index db2ca23940..954c929f32 100644 --- a/tests/src/com/unciv/testing/BasicTests.kt +++ b/tests/src/com/unciv/testing/BasicTests.kt @@ -17,6 +17,7 @@ import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText +import com.unciv.utils.DebugUtils import com.unciv.utils.Log import com.unciv.utils.debug import org.junit.Assert @@ -53,10 +54,10 @@ class BasicTests { fun gameIsNotRunWithDebugModes() { val game = UncivGame() Assert.assertTrue("This test will only pass if the game is not run with debug modes", - !game.superchargedForDebug - && !game.viewEntireMapForDebug - && game.simulateUntilTurnForDebug <= 0 - && !game.consoleMode + !DebugUtils.SUPERCHARGED + && !DebugUtils.VISIBLE_MAP + && DebugUtils.SIMULATE_UNTIL_TURN <= 0 + && !game.isConsoleMode ) }