Cleaning: platform specifics and UncivGame (#8773)

* Cleanup: platform specifics + UncivGame

* Fix tests

* Fix requests not clearing

---------

Co-authored-by: vegeta1k95 <vfylfhby>
This commit is contained in:
vegeta1k95
2023-02-28 17:56:57 +01:00
committed by GitHub
parent 81c4057488
commit 494fde53cf
53 changed files with 823 additions and 873 deletions

View File

@ -18,7 +18,7 @@ import com.unciv.ui.components.Fonts
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
class FontAndroid : FontImplementation { class AndroidFont : FontImplementation {
private val fontList by lazy { private val fontList by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptySet() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptySet()

View File

@ -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)
}
}

View File

@ -1,8 +1,8 @@
package com.unciv.app package com.unciv.app
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Rect import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.net.Uri import android.net.Uri
import android.opengl.GLSurfaceView import android.opengl.GLSurfaceView
import android.os.Build import android.os.Build
@ -11,7 +11,7 @@ import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import androidx.annotation.RequiresApi import android.view.WindowManager
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.work.WorkManager import androidx.work.WorkManager
import com.badlogic.gdx.Gdx 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.backends.android.AndroidGraphics
import com.badlogic.gdx.math.Rectangle import com.badlogic.gdx.math.Rectangle
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.UncivGameParameters
import com.unciv.logic.files.UncivFiles import com.unciv.logic.files.UncivFiles
import com.unciv.logic.event.EventBus 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.UncivStage
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.utils.Log import com.unciv.utils.Log
@ -30,37 +30,36 @@ import com.unciv.utils.concurrency.Concurrency
import java.io.File import java.io.File
open class AndroidLauncher : AndroidApplication() { open class AndroidLauncher : AndroidApplication() {
private var customFileLocationHelper: CustomFileLocationHelperAndroid? = null
private var game: UncivGame? = null private var game: UncivGame? = null
private var deepLinkedMultiplayerGame: String? = null private var deepLinkedMultiplayerGame: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Setup Android logging
Log.backend = AndroidLogBackend() 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) MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext)
copyMods() copyMods()
val config = AndroidApplicationConfiguration().apply { val config = AndroidApplicationConfiguration().apply { useImmersiveMode = true }
useImmersiveMode = true
}
val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path) val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path)
// Manage orientation lock and display cutout // Setup orientation lock and display cutout
val platformSpecificHelper = PlatformSpecificHelpersAndroid(this) allowPortrait(settings.allowAndroidPortrait)
platformSpecificHelper.allowPortrait(settings.allowAndroidPortrait) setDisplayCutout(settings.androidCutout)
platformSpecificHelper.toggleDisplayCutout(settings.androidCutout) game = AndroidGame(this)
val androidParameters = UncivGameParameters(
crashReportSysInfo = CrashReportSysInfoAndroid,
fontImplementation = FontAndroid(),
customFileLocationHelper = customFileLocationHelper,
platformSpecificHelper = platformSpecificHelper
)
game = UncivGame(androidParameters)
initialize(game, config) initialize(game, config)
setDeepLinkedGame(intent) setDeepLinkedGame(intent)
@ -73,6 +72,23 @@ open class AndroidLauncher : AndroidApplication() {
addScreenRefreshRateListener(glView) 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 /** Request the best available device frame rate for
* the game, as soon as OpenGL surface is created */ * the game, as soon as OpenGL surface is created */
private fun addScreenRefreshRateListener(surfaceView: GLSurfaceView) { private fun addScreenRefreshRateListener(surfaceView: GLSurfaceView) {
@ -196,7 +212,8 @@ open class AndroidLauncher : AndroidApplication() {
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 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) super.onActivityResult(requestCode, resultCode, data)
} }
} }

View File

@ -20,6 +20,13 @@ class AndroidLogBackend : LogBackend {
override fun isRelease(): Boolean { override fun isRelease(): Boolean {
return !BuildConfig.DEBUG 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 { private fun toAndroidTag(tag: Tag): String {

View File

@ -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<Int, Request>()
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
}
}
}

View File

@ -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()
}

View File

@ -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<ActivityCallback>()
@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 <T> 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
)

View File

@ -269,7 +269,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
val gdxFiles = DefaultAndroidFiles(applicationContext.assets, ContextWrapper(applicationContext), true) 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's AndroidFileHandle uses Gdx.files internally, so we need to set that to our new instance
Gdx.files = gdxFiles Gdx.files = gdxFiles
files = UncivFiles(gdxFiles, null, true) files = UncivFiles(gdxFiles)
} }
override fun doWork(): Result = runBlocking { override fun doWork(): Result = runBlocking {

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -25,7 +25,7 @@ import com.unciv.ui.audio.MusicController
import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.audio.SoundPlayer 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.components.extensions.center
import com.unciv.ui.crashhandling.CrashScreen import com.unciv.ui.crashhandling.CrashScreen
import com.unciv.ui.crashhandling.wrapCrashHandlingUnit 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.WorldMapHolder
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.ui.screens.worldscreen.unit.UnitTable import com.unciv.ui.screens.worldscreen.unit.UnitTable
import com.unciv.utils.DebugUtils
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.PlatformSpecific
import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.Concurrency
import com.unciv.utils.concurrency.launchOnGLThread import com.unciv.utils.concurrency.launchOnGLThread
import com.unciv.utils.concurrency.withGLContext import com.unciv.utils.concurrency.withGLContext
@ -51,13 +53,10 @@ import kotlinx.coroutines.CancellationException
import java.io.PrintWriter import java.io.PrintWriter
import java.util.* import java.util.*
import kotlin.collections.ArrayDeque import kotlin.collections.ArrayDeque
import kotlin.system.exitProcess
object GUI { object GUI {
fun isDebugMapVisible(): Boolean {
return UncivGame.Current.viewEntireMapForDebug
}
fun setUpdateWorldOnNextRender() { fun setUpdateWorldOnNextRender() {
UncivGame.Current.worldScreen?.shouldUpdate = true UncivGame.Current.worldScreen?.shouldUpdate = true
} }
@ -74,10 +73,6 @@ object GUI {
return UncivGame.Current.settings return UncivGame.Current.settings
} }
fun getFontImpl(): FontImplementation {
return UncivGame.Current.fontImplementation!!
}
fun isWorldLoaded(): Boolean { fun isWorldLoaded(): Boolean {
return UncivGame.Current.worldScreen != null return UncivGame.Current.worldScreen != null
} }
@ -116,19 +111,11 @@ object GUI {
} }
class UncivGame(parameters: UncivGameParameters) : Game() { open class UncivGame(val isConsoleMode: Boolean = false) : Game(), PlatformSpecific {
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
var deepLinkedMultiplayerGame: String? = null var deepLinkedMultiplayerGame: String? = null
var gameInfo: GameInfo? = null var gameInfo: GameInfo? = null
lateinit var settings: GameSettings lateinit var settings: GameSettings
lateinit var musicController: MusicController lateinit var musicController: MusicController
lateinit var onlineMultiplayer: OnlineMultiplayer lateinit var onlineMultiplayer: OnlineMultiplayer
@ -136,20 +123,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
var isTutorialTaskCollapsed = false 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 var worldScreen: WorldScreen? = null
private set 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 isInitialized = false // this could be on reload, therefore we need to keep setting this to false
Gdx.input.setCatchKey(Input.Keys.BACK, true) Gdx.input.setCatchKey(Input.Keys.BACK, true)
if (Gdx.app.type != Application.ApplicationType.Desktop) { if (Gdx.app.type != Application.ApplicationType.Desktop) {
viewEntireMapForDebug = false DebugUtils.VISIBLE_MAP = false
} }
Current = this 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. // If this takes too long players, especially with older phones, get ANR problems.
// Whatever needs graphics needs to be done on the main thread, // Whatever needs graphics needs to be done on the main thread,
@ -190,10 +163,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
GameSounds.init() GameSounds.init()
musicController = MusicController() // early, but at this point does only copy volume from settings musicController = MusicController() // early, but at this point does only copy volume from settings
audioExceptionHelper?.installHooks( installAudioHooks()
musicController.getAudioLoopCallback(),
musicController.getAudioExceptionHandler()
)
onlineMultiplayer = OnlineMultiplayer() onlineMultiplayer = OnlineMultiplayer()
@ -230,7 +200,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
// Loading available fonts can take a long time on Android phones. // 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 // 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 // This stuff needs to run on the main thread because it needs the GL context
launchOnGLThread { launchOnGLThread {
@ -474,8 +444,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
override fun dispose() { override fun dispose() {
Gdx.input.inputProcessor = null // don't allow ANRs when shutting down, that's silly Gdx.input.inputProcessor = null // don't allow ANRs when shutting down, that's silly
cancelDiscordEvent?.invoke()
SoundPlayer.clearCache() SoundPlayer.clearCache()
if (::musicController.isInitialized) musicController.gracefulShutdown() // Do allow fade-out 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" // On desktop this should only be this one and "DestroyJavaVM"
logRunningThreads() logRunningThreads()
System.exit(0) exitProcess(0)
} }
private fun logRunningThreads() { private fun logRunningThreads() {
@ -523,7 +491,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
} catch (ex: Exception) { } catch (ex: Exception) {
// ignore // ignore
} }
if (platformSpecificHelper?.handleUncaughtThrowable(ex) == true) return
Gdx.app.postRunnable { Gdx.app.postRunnable {
setAsRootScreen(CrashScreen(ex)) setAsRootScreen(CrashScreen(ex))
} }

View File

@ -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
)

View File

@ -32,6 +32,7 @@ import com.unciv.models.ruleset.nation.Difficulty
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.utils.DebugUtils
import com.unciv.utils.debug import com.unciv.utils.debug
import java.util.* import java.util.*
@ -253,7 +254,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
//region State changing functions //region State changing functions
// Do we automatically simulate until N turn? // 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 || turns < simulateMaxTurns && simulateUntilWin
fun nextTurn() { fun nextTurn() {
@ -266,7 +267,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
playerIndex = (playerIndex + 1) % civilizations.size playerIndex = (playerIndex + 1) % civilizations.size
if (playerIndex == 0) { if (playerIndex == 0) {
turns++ turns++
if (UncivGame.Current.simulateUntilTurnForDebug != 0) if (DebugUtils.SIMULATE_UNTIL_TURN != 0)
debug("Starting simulation of turn %s", turns) debug("Starting simulation of turn %s", turns)
} }
player = civilizations[playerIndex] player = civilizations[playerIndex]
@ -311,8 +312,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
setNextPlayer() setNextPlayer()
} }
if (turns == UncivGame.Current.simulateUntilTurnForDebug) if (turns == DebugUtils.SIMULATE_UNTIL_TURN)
UncivGame.Current.simulateUntilTurnForDebug = 0 DebugUtils.SIMULATE_UNTIL_TURN = 0
// We found human player, so we are making him current // We found human player, so we are making him current
currentTurnStartTime = System.currentTimeMillis() currentTurnStartTime = System.currentTimeMillis()

View File

@ -17,6 +17,7 @@ import com.unciv.models.stats.Stat
import com.unciv.models.stats.StatMap import com.unciv.models.stats.StatMap
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import com.unciv.utils.DebugUtils
import kotlin.math.min import kotlin.math.min
@ -464,7 +465,7 @@ class CityStats(val city: City) {
newStatsBonusTree.add(getStatsPercentBonusesFromUniquesBySource(currentConstruction)) newStatsBonusTree.add(getStatsPercentBonusesFromUniquesBySource(currentConstruction))
if (UncivGame.Current.superchargedForDebug) { if (DebugUtils.SUPERCHARGED) {
val stats = Stats() val stats = Stats()
for (stat in Stat.values()) stats[stat] = 10000f for (stat in Stat.values()) stats[stat] = 10000f
newStatsBonusTree.addStats(stats, "Supercharged") newStatsBonusTree.addStats(stats, "Supercharged")

View File

@ -2,7 +2,6 @@ package com.unciv.logic.civilization.transients
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.City import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory 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.UniqueTriggerActivation
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.utils.DebugUtils
/** CivInfo class was getting too crowded */ /** CivInfo class was getting too crowded */
class CivInfoTransientCache(val civInfo: Civilization) { class CivInfoTransientCache(val civInfo: Civilization) {
@ -142,7 +142,7 @@ class CivInfoTransientCache(val civInfo: Civilization) {
val newViewableTiles = HashSet<Tile>() val newViewableTiles = HashSet<Tile>()
// while spectating all map is visible // 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() val allTiles = civInfo.gameInfo.tileMap.values.toSet()
civInfo.viewableTiles = allTiles civInfo.viewableTiles = allTiles
civInfo.viewableInvisibleUnitsTiles = allTiles civInfo.viewableInvisibleUnitsTiles = allTiles

View File

@ -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<String>) -> 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<String>) -> 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))
}
}

View File

@ -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
)
}

View File

@ -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], * 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. * which is normally responsible for keeping the [Gdx] static variables from being garbage collected.
*/ */
private val files: Files, private val files: Files
private val customFileLocationHelper: CustomFileLocationHelper? = null,
private val preferExternalStorage: Boolean = false
) { ) {
init { init {
debug("Creating UncivFiles, localStoragePath: %s, externalStoragePath: %s, preferExternalStorage: %s", debug("Creating UncivFiles, localStoragePath: %s, externalStoragePath: %s",
files.localStoragePath, files.externalStoragePath, preferExternalStorage) files.localStoragePath, files.externalStoragePath)
} }
//region Data //region Data
@ -112,8 +110,6 @@ class UncivFiles(
return localFiles + externalFiles return localFiles + externalFiles
} }
fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null
/** /**
* @return `true` if successful. * @return `true` if successful.
* @throws SecurityException when delete access was denied * @throws SecurityException when delete access was denied
@ -141,15 +137,6 @@ class UncivFiles(
return file.delete() 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 //endregion
//region Saving //region Saving
@ -165,7 +152,8 @@ class UncivFiles(
fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) { fun saveGame(game: GameInfo, file: FileHandle, saveCompletionCallback: (Exception?) -> Unit = { if (it != null) throw it }) {
try { try {
debug("Saving GameInfo %s to %s", game.gameId, file.path()) 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) saveCompletionCallback(null)
} catch (ex: Exception) { } catch (ex: Exception) {
saveCompletionCallback(ex) 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, * [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. * 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 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) { } catch (ex: Exception) {
Concurrency.runOnGLThread { saveCompletionCallback(CustomSaveResult(exception = ex)) } Concurrency.runOnGLThread { onError(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)
} }
} }
@ -240,11 +232,7 @@ class UncivFiles(
loadGamePreviewFromFile(getMultiplayerSave(gameName)) loadGamePreviewFromFile(getMultiplayerSave(gameName))
fun loadGamePreviewFromFile(gameFile: FileHandle): GameInfoPreview { fun loadGamePreviewFromFile(gameFile: FileHandle): GameInfoPreview {
val preview = json().fromJson(GameInfoPreview::class.java, gameFile) return json().fromJson(GameInfoPreview::class.java, gameFile) ?: throw emptyFile(gameFile)
if (preview == null) {
throw emptyFile(gameFile)
}
return preview
} }
/** /**
@ -256,36 +244,29 @@ class UncivFiles(
return SerializationException("The file for the game ${gameFile.name()} is empty") return SerializationException("The file for the game ${gameFile.name()} is empty")
} }
class CustomLoadResult<T>(
private val locationAndGameData: Pair<String, T>? = 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. * 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 [gameData] was created by a version of this game that is incompatible with the current one. * 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<GameInfo>) -> Unit) { fun loadGameFromCustomLocation(
customFileLocationHelper!!.loadGame { result -> onLoaded: (GameInfo) -> Unit,
val location = result.location onError: (Exception) -> Unit
val gameData = result.gameData ) {
if (location == null || gameData == null) { saverLoader.loadGame(
loadCompletionCallback(CustomLoadResult(exception = result.exception)) { data, location ->
return@loadGame 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 //region Settings
private fun getGeneralSettingsFile(): FileHandle { 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) else files.local(SETTINGS_FILE_NAME)
} }
@ -325,6 +306,17 @@ class UncivFiles(
var saveZipped = false 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. /** 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) * @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 // 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. // 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) val file = FileHandle(base + File.separator + SETTINGS_FILE_NAME)
return if (file.exists()) return if (file.exists()) json().fromJsonFile(GameSettings::class.java, file)
json().fromJsonFile(
GameSettings::class.java,
file
)
else GameSettings().apply { isFreshlyCreated = true } else GameSettings().apply { isFreshlyCreated = true }
} }

View File

@ -3,7 +3,6 @@
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.Constants import com.unciv.Constants
import com.unciv.GUI import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.city.City import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization 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.Unique
import com.unciv.models.ruleset.unique.UniqueMap import com.unciv.models.ruleset.unique.UniqueMap
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.utils.DebugUtils
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
import kotlin.random.Random import kotlin.random.Random
@ -235,13 +235,13 @@ open class Tile : IsPartOfGameInfoSerialization {
else ruleset.terrains[naturalWonder!!]!! else ruleset.terrains[naturalWonder!!]!!
fun isVisible(player: Civilization): Boolean { fun isVisible(player: Civilization): Boolean {
if (UncivGame.Current.viewEntireMapForDebug) if (DebugUtils.VISIBLE_MAP)
return true return true
return player.viewableTiles.contains(this) return player.viewableTiles.contains(this)
} }
fun isExplored(player: Civilization): Boolean { fun isExplored(player: Civilization): Boolean {
if (UncivGame.Current.viewEntireMapForDebug || player.isSpectator()) if (DebugUtils.VISIBLE_MAP || player.isSpectator())
return true return true
return exploredBy.contains(player.civName) return exploredBy.contains(player.civName)
} }

View File

@ -1,19 +1,19 @@
package com.unciv.logic.map.tile package com.unciv.logic.map.tile
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.components.Fonts import com.unciv.ui.components.Fonts
import com.unciv.utils.DebugUtils
object TileDescription { object TileDescription {
/** Get info on a selected tile, used on WorldScreen (right side above minimap), CityScreen or MapEditorViewTab. */ /** Get info on a selected tile, used on WorldScreen (right side above minimap), CityScreen or MapEditorViewTab. */
fun toMarkup(tile: Tile, viewingCiv: Civilization?): ArrayList<FormattedLine> { fun toMarkup(tile: Tile, viewingCiv: Civilization?): ArrayList<FormattedLine> {
val lineList = ArrayList<FormattedLine>() val lineList = ArrayList<FormattedLine>()
val isViewableToPlayer = viewingCiv == null || UncivGame.Current.viewEntireMapForDebug val isViewableToPlayer = viewingCiv == null || DebugUtils.VISIBLE_MAP
|| viewingCiv.viewableTiles.contains(tile) || viewingCiv.viewableTiles.contains(tile)
if (tile.isCityCenter()) { if (tile.isCityCenter()) {
@ -21,7 +21,7 @@ object TileDescription {
var cityString = city.name.tr() var cityString = city.name.tr()
if (isViewableToPlayer) cityString += " (${city.health})" if (isViewableToPlayer) cityString += " (${city.health})"
lineList += FormattedLine(cityString) lineList += FormattedLine(cityString)
if (UncivGame.Current.viewEntireMapForDebug || city.civ == viewingCiv) if (DebugUtils.VISIBLE_MAP || city.civ == viewingCiv)
lineList += city.cityConstructions.getProductionMarkup(tile.ruleset) lineList += city.cityConstructions.getProductionMarkup(tile.ruleset)
} }

View File

@ -177,6 +177,7 @@ object Fonts {
const val ORIGINAL_FONT_SIZE = 50f const val ORIGINAL_FONT_SIZE = 50f
const val DEFAULT_FONT_FAMILY = "" const val DEFAULT_FONT_FAMILY = ""
lateinit var fontImplementation: FontImplementation
lateinit var font: BitmapFont lateinit var font: BitmapFont
/** This resets all cached font data in object Fonts. /** This resets all cached font data in object Fonts.
@ -184,15 +185,12 @@ object Fonts {
*/ */
fun resetFont() { fun resetFont() {
val settings = GUI.getSettings() val settings = GUI.getSettings()
val fontImpl = GUI.getFontImpl() fontImplementation.setFontFamily(settings.fontFamilyData, settings.getFontSize())
fontImpl.setFontFamily(settings.fontFamilyData, settings.getFontSize()) font = fontImplementation.getBitmapFont()
font = fontImpl.getBitmapFont()
} }
/** Reduce the font list returned by platform-specific code to font families (plain variant if possible) */ /** Reduce the font list returned by platform-specific code to font families (plain variant if possible) */
fun getSystemFonts(): Sequence<FontFamilyData> { fun getSystemFonts(): Sequence<FontFamilyData> {
val fontImplementation = UncivGame.Current.fontImplementation
?: return emptySequence()
return fontImplementation.getSystemFonts() return fontImplementation.getSystemFonts()
.sortedWith(compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.localName }) .sortedWith(compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.localName })
} }

View File

@ -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
}

View File

@ -1,11 +1,16 @@
package com.unciv.ui.components 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.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor 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.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.scenes.scene2d.utils.FocusListener import com.badlogic.gdx.scenes.scene2d.utils.FocusListener
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.event.EventBus
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.screens.basescreen.UncivStage import com.unciv.ui.screens.basescreen.UncivStage
import com.unciv.ui.components.extensions.getAscendant 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.right
import com.unciv.ui.components.extensions.stageBoundingBox import com.unciv.ui.components.extensions.stageBoundingBox
import com.unciv.ui.components.extensions.top import com.unciv.ui.components.extensions.top
import com.unciv.ui.popups.Popup
import com.unciv.ui.screens.basescreen.BaseScreen 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 { 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 return textField
} }
} }
@ -96,3 +107,97 @@ fun TextField.scrollAscendantToTextField(): Boolean {
return true 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
}
}

View File

@ -33,6 +33,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.cityscreen.CityReligionInfoTable import com.unciv.ui.screens.cityscreen.CityReligionInfoTable
import com.unciv.ui.screens.cityscreen.CityScreen import com.unciv.ui.screens.cityscreen.CityScreen
import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen
import com.unciv.utils.DebugUtils
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -228,7 +229,7 @@ private class CityTable(city: City, forPopup: Boolean = false) : BorderedTable(
pad(0f) pad(0f)
defaults().pad(0f) defaults().pad(0f)
val isShowDetailedInfo = UncivGame.Current.viewEntireMapForDebug val isShowDetailedInfo = DebugUtils.VISIBLE_MAP
|| city.civ == viewingCiv || city.civ == viewingCiv
|| viewingCiv.isSpectator() || viewingCiv.isSpectator()
@ -532,7 +533,7 @@ class CityButton(val city: City, private val tileGroup: TileGroup): Table(BaseSc
if (isButtonMoved) { if (isButtonMoved) {
// second tap on the button will go to the city screen // 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 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))) { || (belongsToViewingCiv() && !tileGroup.tile.airUnits.contains(unitTable.selectedUnit))) {
GUI.pushScreen(CityScreen(city)) GUI.pushScreen(CityScreen(city))
} else if (viewingPlayer.knows(city.civ)) { } else if (viewingPlayer.knows(city.civ)) {

View File

@ -2,7 +2,6 @@ package com.unciv.ui.components.tilegroups
import com.badlogic.gdx.graphics.g2d.Batch import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.Group
import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.ui.components.tilegroups.layers.TileLayerBorders 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.TileLayerTerrain
import com.unciv.ui.components.tilegroups.layers.TileLayerUnitArt import com.unciv.ui.components.tilegroups.layers.TileLayerUnitArt
import com.unciv.ui.components.tilegroups.layers.TileLayerUnitFlag import com.unciv.ui.components.tilegroups.layers.TileLayerUnitFlag
import com.unciv.utils.DebugUtils
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.sqrt 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 hexagonImageOrigin = Pair(hexagonImageWidth / 2f, sqrt((hexagonImageWidth / 2f).pow(2) - (hexagonImageWidth / 4f).pow(2)))
val hexagonImagePosition = Pair(-hexagonImageOrigin.first / 3f, -hexagonImageOrigin.second / 4f) val hexagonImagePosition = Pair(-hexagonImageOrigin.first / 3f, -hexagonImageOrigin.second / 4f)
var isForceVisible = UncivGame.Current.viewEntireMapForDebug var isForceVisible = DebugUtils.VISIBLE_MAP
var isForMapEditorIcon = false var isForMapEditorIcon = false
@Suppress("LeakingThis") val layerTerrain = TileLayerTerrain(this, groupSize) @Suppress("LeakingThis") val layerTerrain = TileLayerTerrain(this, groupSize)

View File

@ -4,11 +4,11 @@ import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.ui.components.tilegroups.CityButton import com.unciv.ui.components.tilegroups.CityButton
import com.unciv.ui.components.tilegroups.TileGroup import com.unciv.ui.components.tilegroups.TileGroup
import com.unciv.ui.components.tilegroups.WorldTileGroup import com.unciv.ui.components.tilegroups.WorldTileGroup
import com.unciv.utils.DebugUtils
class TileLayerCityButton(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, size) { class TileLayerCityButton(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, size) {
@ -60,7 +60,7 @@ class TileLayerCityButton(tileGroup: TileGroup, size: Float) : TileLayer(tileGro
return return
val tileIsViewable = isViewable(viewingCiv) val tileIsViewable = isViewable(viewingCiv)
val shouldShow = UncivGame.Current.viewEntireMapForDebug val shouldShow = DebugUtils.VISIBLE_MAP
// Create (if not yet) and update city button // Create (if not yet) and update city button
if (city != null && tileGroup.tile.isCityCenter()) { if (city != null && tileGroup.tile.isCityCenter()) {

View File

@ -1,5 +0,0 @@
package com.unciv.ui.crashhandling
interface CrashReportSysInfo {
fun getInfo(): String
}

View File

@ -19,6 +19,7 @@ import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.ToastPopup import com.unciv.ui.popups.ToastPopup
import com.unciv.utils.Log
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter 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)}
-------------------------------- --------------------------------

View File

@ -54,17 +54,18 @@ fun advancedTab(
addAutosaveTurnsSelectBox(this, settings) addAutosaveTurnsSelectBox(this, settings)
if (UncivGame.Current.platformSpecificHelper?.hasDisplayCutout() == true) if (UncivGame.Current.hasDisplayCutout())
optionsPopup.addCheckbox(this, "Enable display cutout (requires restart)", settings.androidCutout, false) { settings.androidCutout = it } optionsPopup.addCheckbox(this, "Enable display cutout (requires restart)", settings.androidCutout) {
settings.androidCutout = it
}
addMaxZoomSlider(this, settings) addMaxZoomSlider(this, settings)
val helper = UncivGame.Current.platformSpecificHelper if (Gdx.app.type == Application.ApplicationType.Android) {
if (helper != null && Gdx.app.type == Application.ApplicationType.Android) {
optionsPopup.addCheckbox(this, "Enable portrait orientation", settings.allowAndroidPortrait) { optionsPopup.addCheckbox(this, "Enable portrait orientation", settings.allowAndroidPortrait) {
settings.allowAndroidPortrait = it settings.allowAndroidPortrait = it
// Note the following might close the options screen indirectly and delayed // 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") { Concurrency.run("GenerateScreenshot") {
val extraImagesLocation = "../../extraImages" 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 // 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(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(1280, 640, ScreenSize.Medium, "$extraImagesLocation/GithubPreviewImage.png", Vector2(-2f, 4f)),
ScreenshotConfig(1024, 500, ScreenSize.Medium, "$extraImagesLocation/Feature graphic - Google Play.png",Vector2(-2f, 6f)), 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) 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<ScreenshotConfig>) { private fun CoroutineScope.generateScreenshots(settings: GameSettings, configs:ArrayList<ScreenshotConfig>) {
val currentConfig = configs.first() val currentConfig = configs.first()
launchOnGLThread { launchOnGLThread {
val screenshotGame = val screenshotGame =
UncivGame.Current.files.loadGameByName("ScreenshotGenerationGame") UncivGame.Current.files.loadGameByName("ScreenshotGenerationGame")
UncivGame.Current.settings.screenSize = currentConfig.screenSize settings.screenSize = currentConfig.screenSize
val newScreen = UncivGame.Current.loadGame(screenshotGame) val newScreen = UncivGame.Current.loadGame(screenshotGame)
newScreen.stage.viewport.update(currentConfig.width, currentConfig.height, true) newScreen.stage.viewport.update(currentConfig.width, currentConfig.height, true)
@ -290,7 +291,7 @@ private fun CoroutineScope.generateScreenshots(configs:ArrayList<ScreenshotConfi
) )
pixmap.dispose() pixmap.dispose()
val newConfigs = configs.withoutItem(currentConfig) val newConfigs = configs.withoutItem(currentConfig)
if (newConfigs.isNotEmpty()) generateScreenshots(newConfigs) if (newConfigs.isNotEmpty()) generateScreenshots(settings, newConfigs)
} }
} }
} }

View File

@ -14,6 +14,7 @@ import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.toCheckBox import com.unciv.ui.components.extensions.toCheckBox
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.components.extensions.toTextButton
import com.unciv.utils.DebugUtils
fun debugTab() = Table(BaseScreen.skin).apply { fun debugTab() = Table(BaseScreen.skin).apply {
pad(10f) pad(10f)
@ -22,7 +23,7 @@ fun debugTab() = Table(BaseScreen.skin).apply {
if (GUI.isWorldLoaded()) { if (GUI.isWorldLoaded()) {
val simulateButton = "Simulate until turn:".toTextButton() val simulateButton = "Simulate until turn:".toTextButton()
val simulateTextField = UncivTextField.create("Turn", game.simulateUntilTurnForDebug.toString()) val simulateTextField = UncivTextField.create("Turn", DebugUtils.SIMULATE_UNTIL_TURN.toString())
val invalidInputLabel = "This is not a valid integer!".toLabel().also { it.isVisible = false } val invalidInputLabel = "This is not a valid integer!".toLabel().also { it.isVisible = false }
simulateButton.onClick { simulateButton.onClick {
val simulateUntilTurns = simulateTextField.text.toIntOrNull() val simulateUntilTurns = simulateTextField.text.toIntOrNull()
@ -30,7 +31,7 @@ fun debugTab() = Table(BaseScreen.skin).apply {
invalidInputLabel.isVisible = true invalidInputLabel.isVisible = true
return@onClick return@onClick
} }
game.simulateUntilTurnForDebug = simulateUntilTurns DebugUtils.SIMULATE_UNTIL_TURN = simulateUntilTurns
invalidInputLabel.isVisible = false invalidInputLabel.isVisible = false
GUI.getWorldScreen().nextTurn() GUI.getWorldScreen().nextTurn()
} }
@ -39,11 +40,11 @@ fun debugTab() = Table(BaseScreen.skin).apply {
add(invalidInputLabel).colspan(2).row() add(invalidInputLabel).colspan(2).row()
} }
add("Supercharged".toCheckBox(game.superchargedForDebug) { add("Supercharged".toCheckBox(DebugUtils.SUPERCHARGED) {
game.superchargedForDebug = it DebugUtils.SUPERCHARGED = it
}).colspan(2).row() }).colspan(2).row()
add("View entire map".toCheckBox(game.viewEntireMapForDebug) { add("View entire map".toCheckBox(DebugUtils.VISIBLE_MAP) {
game.viewEntireMapForDebug = it DebugUtils.VISIBLE_MAP = it
}).colspan(2).row() }).colspan(2).row()
val curGameInfo = game.gameInfo val curGameInfo = game.gameInfo
if (curGameInfo != null) { if (curGameInfo != null) {

View File

@ -44,6 +44,8 @@ class OptionsPopup(
private val selectPage: Int = defaultPage, private val selectPage: Int = defaultPage,
private val onClose: () -> Unit = {} private val onClose: () -> Unit = {}
) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = false ) { ) : Popup(screen.stage, /** [TabbedPager] handles scrolling */ scrollable = false ) {
val game = screen.game
val settings = screen.game.settings val settings = screen.game.settings
val tabs: TabbedPager val tabs: TabbedPager
val selectBoxMinWidth: Float val selectBoxMinWidth: Float
@ -120,7 +122,7 @@ class OptionsPopup(
addCloseButton { addCloseButton {
screen.game.musicController.onChange(null) screen.game.musicController.onChange(null)
screen.game.platformSpecificHelper?.allowPortrait(settings.allowAndroidPortrait) screen.game.allowPortrait(settings.allowAndroidPortrait)
onClose() onClose()
}.padBottom(10f) }.padBottom(10f)
@ -205,7 +207,7 @@ open class SettingsSelect<T : Any>(
) { ) {
private val settingsProperty: KMutableProperty0<T> = setting.getProperty(settings) private val settingsProperty: KMutableProperty0<T> = setting.getProperty(settings)
private val label = createLabel(labelText) private val label = createLabel(labelText)
protected val refreshSelectBox = createSelectBox(items.toGdxArray(), settings) private val refreshSelectBox = createSelectBox(items.toGdxArray(), settings)
val items by refreshSelectBox::items val items by refreshSelectBox::items
private fun createLabel(labelText: String): Label { private fun createLabel(labelText: String): Label {

View File

@ -17,6 +17,7 @@ import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.utils.DebugUtils
class WonderOverviewTab( class WonderOverviewTab(
viewingPlayer: Civilization, viewingPlayer: Civilization,
@ -115,7 +116,7 @@ class WonderInfo {
val city: City?, val city: City?,
val location: Tile? val location: Tile?
) { ) {
val viewEntireMapForDebug = UncivGame.Current.viewEntireMapForDebug val viewEntireMapForDebug = DebugUtils.VISIBLE_MAP
fun getImage() = if (status == WonderStatus.Unknown && !viewEntireMapForDebug) null fun getImage() = if (status == WonderStatus.Unknown && !viewEntireMapForDebug) null
else category.getImage?.invoke(name, if (category == CivilopediaCategories.Terrain) 50f else 45f) else category.getImage?.invoke(name, if (category == CivilopediaCategories.Terrain) 50f else 45f)

View File

@ -159,22 +159,22 @@ class LoadGameScreen : LoadOrSaveScreen() {
} }
private fun Table.addLoadFromCustomLocationButton() { private fun Table.addLoadFromCustomLocationButton() {
if (!game.files.canLoadFromCustomSaveLocation()) return
val loadFromCustomLocation = loadFromCustomLocation.toTextButton() val loadFromCustomLocation = loadFromCustomLocation.toTextButton()
loadFromCustomLocation.onClick { loadFromCustomLocation.onClick {
errorLabel.isVisible = false errorLabel.isVisible = false
loadFromCustomLocation.setText(Constants.loading.tr()) loadFromCustomLocation.setText(Constants.loading.tr())
loadFromCustomLocation.disable() loadFromCustomLocation.disable()
Concurrency.run(Companion.loadFromCustomLocation) { Concurrency.run(Companion.loadFromCustomLocation) {
game.files.loadGameFromCustomLocation { result -> game.files.loadGameFromCustomLocation(
if (result.isError()) { {
handleLoadGameException(result.exception!!, "Could not load game from custom location!") Concurrency.run { game.loadGame(it, true) }
} else if (result.isSuccessful()) { loadFromCustomLocation.enable()
Concurrency.run { },
game.loadGame(result.gameData!!, true) {
} handleLoadGameException(it, "Could not load game from custom location!")
loadFromCustomLocation.enable()
} }
} )
} }
} }
add(loadFromCustomLocation).row() add(loadFromCustomLocation).row()

View File

@ -82,7 +82,6 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves")
} }
private fun Table.addSaveToCustomLocation() { private fun Table.addSaveToCustomLocation() {
if (!game.files.canLoadFromCustomSaveLocation()) return
val saveToCustomLocation = "Save to custom location".toTextButton() val saveToCustomLocation = "Save to custom location".toTextButton()
val errorLabel = "".toLabel(Color.RED) val errorLabel = "".toLabel(Color.RED)
saveToCustomLocation.onClick { saveToCustomLocation.onClick {
@ -90,15 +89,18 @@ class SaveGameScreen(val gameInfo: GameInfo) : LoadOrSaveScreen("Current saves")
saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.setText("Saving...".tr())
saveToCustomLocation.disable() saveToCustomLocation.disable()
Concurrency.runOnNonDaemonThreadPool("Save to custom location") { Concurrency.runOnNonDaemonThreadPool("Save to custom location") {
game.files.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result ->
if (result.isError()) { game.files.saveGameToCustomLocation(gameInfo, gameNameTextField.text,
errorLabel.setText("Could not save game to custom location!".tr()) {
result.exception?.printStackTrace()
} else if (result.isSuccessful()) {
game.popScreen() game.popScreen()
saveToCustomLocation.enable()
},
{
errorLabel.setText("Could not save game to custom location!".tr())
it.printStackTrace()
saveToCustomLocation.enable()
} }
saveToCustomLocation.enable() )
}
} }
} }
add(saveToCustomLocation).row() add(saveToCustomLocation).row()

View File

@ -350,7 +350,7 @@ class WorldScreen(
debug("loadLatestMultiplayerState downloaded game: gameId: %s, turn: %s, curCiv: %s", debug("loadLatestMultiplayerState downloaded game: gameId: %s, turn: %s, curCiv: %s",
latestGame.gameId, latestGame.turns, latestGame.currentPlayer) latestGame.gameId, latestGame.turns, latestGame.currentPlayer)
if (viewingCiv.civName == latestGame.currentPlayer || viewingCiv.civName == Constants.spectator) { if (viewingCiv.civName == latestGame.currentPlayer || viewingCiv.civName == Constants.spectator) {
game.platformSpecificHelper?.notifyTurnStarted() game.notifyTurnStarted()
} }
launchOnGLThread { launchOnGLThread {
loadingGamePopup.close() loadingGamePopup.close()

View File

@ -31,6 +31,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.flashWoundedCombatants import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.flashWoundedCombatants
import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.getHealthBar import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.getHealthBar
import com.unciv.utils.DebugUtils
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -102,15 +103,12 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
if (defender == null || (!includeFriendly && defender.getCivInfo() == attackerCiv)) if (defender == null || (!includeFriendly && defender.getCivInfo() == attackerCiv))
return null // no enemy combatant in tile return null // no enemy combatant in tile
val canSeeDefender = val canSeeDefender = when {
if (UncivGame.Current.viewEntireMapForDebug) true DebugUtils.VISIBLE_MAP -> true
else { defender.isInvisible(attackerCiv) -> attackerCiv.viewableInvisibleUnitsTiles.contains(selectedTile)
when { defender.isCity() -> attackerCiv.hasExplored(selectedTile)
defender.isInvisible(attackerCiv) -> attackerCiv.viewableInvisibleUnitsTiles.contains(selectedTile) else -> attackerCiv.viewableTiles.contains(selectedTile)
defender.isCity() -> attackerCiv.hasExplored(selectedTile) }
else -> attackerCiv.viewableTiles.contains(selectedTile)
}
}
if (!canSeeDefender) return null if (!canSeeDefender) return null

View File

@ -15,6 +15,7 @@ import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.extensions.addBorderAllowOpacity import com.unciv.ui.components.extensions.addBorderAllowOpacity
import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.utils.DebugUtils
class TileInfoTable(private val viewingCiv :Civilization) : Table(BaseScreen.skin) { class TileInfoTable(private val viewingCiv :Civilization) : Table(BaseScreen.skin) {
init { init {
@ -27,12 +28,12 @@ class TileInfoTable(private val viewingCiv :Civilization) : Table(BaseScreen.ski
internal fun updateTileTable(tile: Tile?) { internal fun updateTileTable(tile: Tile?) {
clearChildren() clearChildren()
if (tile != null && (UncivGame.Current.viewEntireMapForDebug || viewingCiv.hasExplored(tile)) ) { if (tile != null && (DebugUtils.VISIBLE_MAP || viewingCiv.hasExplored(tile)) ) {
add(getStatsTable(tile)) add(getStatsTable(tile))
add(MarkupRenderer.render(TileDescription.toMarkup(tile, viewingCiv), padding = 0f, iconDisplay = IconDisplay.None) { add(MarkupRenderer.render(TileDescription.toMarkup(tile, viewingCiv), padding = 0f, iconDisplay = IconDisplay.None) {
UncivGame.Current.pushScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleset, link = it)) UncivGame.Current.pushScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleset, link = it))
} ).pad(5f).row() } ).pad(5f).row()
if (UncivGame.Current.viewEntireMapForDebug) if (DebugUtils.VISIBLE_MAP)
add(tile.position.run { "(${x.toInt()},${y.toInt()})" }.toLabel()).colspan(2).pad(5f) add(tile.position.run { "(${x.toInt()},${y.toInt()})" }.toLabel()).colspan(2).pad(5f)
} }

View File

@ -5,7 +5,6 @@ import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.map.HexMath import com.unciv.logic.map.HexMath
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile 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.images.ImageGetter
import com.unciv.ui.components.extensions.onClick import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.utils.DebugUtils
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.atan import kotlin.math.atan
@ -36,7 +36,7 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U
} }
fun updateColor(isTileUnrevealed: Boolean) { fun updateColor(isTileUnrevealed: Boolean) {
image.isVisible = UncivGame.Current.viewEntireMapForDebug || !isTileUnrevealed image.isVisible = DebugUtils.VISIBLE_MAP || !isTileUnrevealed
if (!image.isVisible) return if (!image.isVisible) return
image.color = when { image.color = when {
tile.isCityCenter() && !tile.isWater -> tile.getOwner()!!.nation.getInnerColor() tile.isCityCenter() && !tile.isWater -> tile.getOwner()!!.nation.getInnerColor()

View File

@ -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
}

View File

@ -143,6 +143,13 @@ object Log {
fun error(tag: Tag, msg: String, throwable: Throwable) { fun error(tag: Tag, msg: String, throwable: Throwable) {
doLog(backend::error, tag, buildThrowableMessage(msg, 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) class Tag(val name: String)
@ -153,6 +160,9 @@ interface LogBackend {
/** Do not log on release builds for performance reasons. */ /** Do not log on release builds for performance reasons. */
fun isRelease(): Boolean fun isRelease(): Boolean
/** Get string information about operation system */
fun getSystemInfo(): String
} }
/** Only for tests, or temporary main() functions */ /** Only for tests, or temporary main() functions */
@ -168,6 +178,10 @@ open class DefaultLogBackend : LogBackend {
override fun isRelease(): Boolean { override fun isRelease(): Boolean {
return false return false
} }
override fun getSystemInfo(): String {
return ""
}
} }
/** Shortcut for [Log.debug] */ /** Shortcut for [Log.debug] */

View File

@ -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 }
}

View File

@ -2,7 +2,6 @@ package com.unciv.app.desktop
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.UncivGameParameters
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.logic.GameStarter import com.unciv.logic.GameStarter
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
@ -26,8 +25,7 @@ internal object ConsoleLauncher {
fun main(arg: Array<String>) { fun main(arg: Array<String>) {
Log.backend = DesktopLogBackend() Log.backend = DesktopLogBackend()
val consoleParameters = UncivGameParameters(consoleMode = true) val game = UncivGame(true)
val game = UncivGame(consoleParameters)
UncivGame.Current = game UncivGame.Current = game
UncivGame.Current.settings = GameSettings().apply { UncivGame.Current.settings = GameSettings().apply {

View File

@ -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<String,String> = 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<String,String> = 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"]}"
}
}
}

View File

@ -10,7 +10,7 @@ import java.awt.image.BufferedImage
import java.util.* import java.util.*
class FontDesktop : FontImplementation { class DesktopFont : FontImplementation {
private lateinit var font: Font private lateinit var font: Font
private lateinit var metric: FontMetrics private lateinit var metric: FontMetrics

View File

@ -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()
}
}

View File

@ -1,33 +1,33 @@
package com.unciv.app.desktop 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.Lwjgl3Application
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.graphics.glutils.HdpiMode 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.json.json
import com.unciv.logic.files.SETTINGS_FILE_NAME import com.unciv.logic.files.SETTINGS_FILE_NAME
import com.unciv.logic.files.UncivFiles import com.unciv.logic.files.UncivFiles
import com.unciv.models.metadata.ScreenSize import com.unciv.models.metadata.ScreenSize
import com.unciv.models.metadata.WindowState import com.unciv.models.metadata.WindowState
import com.unciv.ui.components.Fonts
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.debug
import java.awt.GraphicsEnvironment import java.awt.GraphicsEnvironment
import java.util.*
import kotlin.concurrent.timer
internal object DesktopLauncher { internal object DesktopLauncher {
private var discordTimer: Timer? = null
@JvmStatic @JvmStatic
fun main(arg: Array<String>) { fun main(arg: Array<String>) {
// Setup Desktop logging
Log.backend = DesktopLogBackend() 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. // 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 // 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") System.setProperty("org.lwjgl.opengl.Display.allowSoftwareOpenGL", "true")
@ -69,60 +69,7 @@ internal object DesktopLauncher {
UiElementDocsWriter().write() UiElementDocsWriter().write()
} }
val platformSpecificHelper = PlatformSpecificHelpersDesktop(config) val game = DesktopGame(config)
val desktopParameters = UncivGameParameters(
cancelDiscordEvent = { discordTimer?.cancel() },
fontImplementation = FontDesktop(),
customFileLocationHelper = CustomFileLocationHelperDesktop(),
crashReportSysInfo = CrashReportSysInfoDesktop(),
platformSpecificHelper = platformSpecificHelper,
audioExceptionHelper = HardenGdxAudio()
)
val game = UncivGame(desktopParameters)
tryActivateDiscord(game)
Lwjgl3Application(game, 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)
}
} }

View File

@ -7,10 +7,14 @@ class DesktopLogBackend : DefaultLogBackend() {
// -ea (enable assertions) or kotlin debugging property as marker for a debug run. // -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. // 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 && System.getProperty("kotlinx.coroutines.debug") == null
override fun isRelease(): Boolean { override fun isRelease(): Boolean {
return release return release
} }
override fun getSystemInfo(): String {
return SystemUtils.getSystemInfo()
}
} }

View File

@ -1,7 +1,8 @@
package com.unciv.app.desktop package com.unciv.app.desktop
import com.badlogic.gdx.Gdx 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.Component
import java.awt.EventQueue import java.awt.EventQueue
import java.awt.event.WindowEvent import java.awt.event.WindowEvent
@ -11,17 +12,45 @@ import java.io.OutputStream
import javax.swing.JFileChooser import javax.swing.JFileChooser
import javax.swing.JFrame 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) { override fun loadGame(
pickFile(callback, JFileChooser::showOpenDialog, File::inputStream) 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 <T> pickFile(callback: (String?, T?, Exception?) -> Unit, private fun <T> pickFile(onSuccess: (T, String) -> Unit,
onError: (Exception) -> Unit,
chooseAction: (JFileChooser, Component) -> Int, chooseAction: (JFileChooser, Component) -> Int,
createValue: (File) -> T, createValue: (File) -> T,
suggestedLocation: String? = null) { suggestedLocation: String? = null) {
@ -47,13 +76,13 @@ class CustomFileLocationHelperDesktop : CustomFileLocationHelper() {
frame.dispose() frame.dispose()
if (result == JFileChooser.CANCEL_OPTION) { if (result == JFileChooser.CANCEL_OPTION) {
callback(null, null, null) return@invokeLater
} else { } else {
val value = createValue(fileChooser.selectedFile) val value = createValue(fileChooser.selectedFile)
callback(fileChooser.selectedFile.absolutePath, value, null) onSuccess(value, fileChooser.selectedFile.absolutePath)
} }
} catch (ex: Exception) { } catch (ex: Exception) {
callback(null, null, ex) onError(ex)
} }
} }
} }

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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<String,String> = 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<String,String> = 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"]}"
}
}

View File

@ -11,7 +11,6 @@ import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener import com.badlogic.gdx.scenes.scene2d.InputListener
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.UncivGameParameters
import com.unciv.logic.files.UncivFiles import com.unciv.logic.files.UncivFiles
import com.unciv.logic.multiplayer.throttle import com.unciv.logic.multiplayer.throttle
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
@ -61,10 +60,11 @@ object FasterUIDevelopment {
} }
class UIDevGame : Game() { class UIDevGame : Game() {
val game = UncivGame(UncivGameParameters(
fontImplementation = FontDesktop() private val game = UncivGame()
))
override fun create() { override fun create() {
Fonts.fontImplementation = FontDesktop()
UncivGame.Current = game UncivGame.Current = game
UncivGame.Current.files = UncivFiles(Gdx.files) UncivGame.Current.files = UncivFiles(Gdx.files)
game.settings = UncivGame.Current.files.getGeneralSettings() game.settings = UncivGame.Current.files.getGeneralSettings()

View File

@ -17,6 +17,7 @@ import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText import com.unciv.models.translations.getPlaceholderText
import com.unciv.utils.DebugUtils
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.debug import com.unciv.utils.debug
import org.junit.Assert import org.junit.Assert
@ -53,10 +54,10 @@ class BasicTests {
fun gameIsNotRunWithDebugModes() { fun gameIsNotRunWithDebugModes() {
val game = UncivGame() val game = UncivGame()
Assert.assertTrue("This test will only pass if the game is not run with debug modes", Assert.assertTrue("This test will only pass if the game is not run with debug modes",
!game.superchargedForDebug !DebugUtils.SUPERCHARGED
&& !game.viewEntireMapForDebug && !DebugUtils.VISIBLE_MAP
&& game.simulateUntilTurnForDebug <= 0 && DebugUtils.SIMULATE_UNTIL_TURN <= 0
&& !game.consoleMode && !game.isConsoleMode
) )
} }