mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 15:27:50 +07:00
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:
@ -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()
|
35
android/src/com/unciv/app/AndroidGame.kt
Normal file
35
android/src/com/unciv/app/AndroidGame.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
113
android/src/com/unciv/app/AndroidSaverLoader.kt
Normal file
113
android/src/com/unciv/app/AndroidSaverLoader.kt
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
@ -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
|
|
||||||
)
|
|
@ -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 {
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
)
|
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
23
core/src/com/unciv/logic/files/PlatformSaverLoader.kt
Normal file
23
core/src/com/unciv/logic/files/PlatformSaverLoader.kt
Normal 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
|
||||||
|
)
|
||||||
|
}
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)) {
|
||||||
|
@ -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)
|
||||||
|
@ -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()) {
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
package com.unciv.ui.crashhandling
|
|
||||||
|
|
||||||
interface CrashReportSysInfo {
|
|
||||||
fun getInfo(): String
|
|
||||||
}
|
|
@ -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)}
|
||||||
|
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
20
core/src/com/unciv/utils/Debug.kt
Normal file
20
core/src/com/unciv/utils/Debug.kt
Normal 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
|
||||||
|
|
||||||
|
}
|
@ -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] */
|
||||||
|
17
core/src/com/unciv/utils/PlatformSpecific.kt
Normal file
17
core/src/com/unciv/utils/PlatformSpecific.kt
Normal 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 }
|
||||||
|
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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"]}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
52
desktop/src/com/unciv/app/desktop/DesktopGame.kt
Normal file
52
desktop/src/com/unciv/app/desktop/DesktopGame.kt
Normal 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
76
desktop/src/com/unciv/app/desktop/DiscordUpdater.kt
Normal file
76
desktop/src/com/unciv/app/desktop/DiscordUpdater.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
97
desktop/src/com/unciv/app/desktop/SystemUtils.kt
Normal file
97
desktop/src/com/unciv/app/desktop/SystemUtils.kt
Normal 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"]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user