#6914 Multiplayer Status Display (#6916)

* Handle subclassing of Events properly

Previously, you could only listen to the exact class

* Add relevant parent classes for the multiplayer events

* Refactor: use the old name as the main name in MultiplayerGameNameChanged event

* Add being able to stop listening to events in the EventBus

* Add tests for EventBus

* Refactor: Extract GameList into standalone file

* Refactor: safeUpdateIf to more generic throttle function

* Refactor: Extract multiplayer UI helper functions into separate file

* Refactor: Extract load/download multiplayer game into logic class from UI

* Make loading a multiplayer game automatically update the in-memory game in OnlineMultiplayer

* Refactor: Extract multiplayer settings into separate object

* Add multiplayer status display

* Fix error with multiplayer games not correctly being cleaned up after successful update

* Prevent loadLatestMultiplayerState() while next turn update is running

* Show "Working..." while waiting for next turn calculations instead of "Waiting for [civ]..."

* Fix race condition while updating online game state
This commit is contained in:
Timo T
2022-05-25 22:22:58 +02:00
committed by GitHub
parent 8dadab872c
commit ea03b97639
35 changed files with 1883 additions and 1147 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1009 KiB

After

Width:  |  Height:  |  Size: 1008 KiB

View File

@ -542,6 +542,7 @@ Username =
Multiplayer =
Could not download game! =
Could not upload game! =
Retry =
Join game =
Invalid game ID! =
Copy user ID =
@ -577,6 +578,7 @@ You can only resign if it's your turn =
[civName] resigned and is now controlled by AI =
Last refresh: [time] [timeUnit] ago =
Current Turn: [civName] since [time] [timeUnit] ago =
Seconds =
Minutes =
Hours =
Days =
@ -1346,7 +1348,7 @@ Choose name for [unitName] =
# Multiplayer Turn Checker Service
Enable out-of-game turn notifications =
Time between turn checks out-of-game (in minutes) =
Out-of-game, update status of all games every: =
Show persistent notification for turn notifier service =
Take user ID from clipboard =
Doing this will reset your current user ID to the clipboard contents - are you sure? =
@ -1355,6 +1357,9 @@ Invalid ID! =
# Multiplayer options menu
Enable multiplayer status button in singleplayer games =
Update status of currently played game every: =
In-game, update status of all games every: =
Server address =
Reset to Dropbox =
Check connection to server =

View File

@ -69,9 +69,10 @@ open class AndroidLauncher : AndroidApplication() {
override fun onPause() {
if (UncivGame.isCurrentInitialized()
&& UncivGame.Current.isGameInfoInitialized()
&& UncivGame.Current.settings.multiplayerTurnCheckerEnabled
&& UncivGame.Current.settings.multiplayer.turnCheckerEnabled
&& UncivGame.Current.gameSaver.getMultiplayerSaves().any()) {
MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, UncivGame.Current.gameSaver, UncivGame.Current.gameInfo, UncivGame.Current.settings)
MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, UncivGame.Current.gameSaver,
UncivGame.Current.gameInfo, UncivGame.Current.settings.multiplayer)
}
super.onPause()
}

View File

@ -24,11 +24,13 @@ import com.unciv.logic.GameSaver
import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached
import com.unciv.models.metadata.GameSettings
import com.unciv.logic.multiplayer.storage.OnlineMultiplayerGameSaver
import com.unciv.models.metadata.GameSettingsMultiplayer
import kotlinx.coroutines.runBlocking
import java.io.FileNotFoundException
import java.io.PrintWriter
import java.io.StringWriter
import java.io.Writer
import java.time.Duration
import java.util.*
import java.util.concurrent.TimeUnit
@ -59,8 +61,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
private const val PERSISTENT_NOTIFICATION_ENABLED = "PERSISTENT_NOTIFICATION_ENABLED"
private const val FILE_STORAGE = "FILE_STORAGE"
fun enqueue(appContext: Context,
delayInMinutes: Int, inputData: Data) {
fun enqueue(appContext: Context, delay: Duration, inputData: Data) {
val constraints = Constraints.Builder()
// If no internet is available, worker waits before becoming active.
@ -69,7 +70,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
val checkTurnWork = OneTimeWorkRequestBuilder<MultiplayerTurnCheckWorker>()
.setConstraints(constraints)
.setInitialDelay(delayInMinutes.toLong(), TimeUnit.MINUTES)
.setInitialDelay(delay.seconds, TimeUnit.SECONDS)
.addTag(WORK_TAG)
.setInputData(inputData)
.build()
@ -123,7 +124,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
* The persistent notification is purely for informational reasons.
* It is not technically necessary for the Worker, since it is not a Service.
*/
fun showPersistentNotification(appContext: Context, lastTimeChecked: String, checkPeriod: String) {
fun showPersistentNotification(appContext: Context, lastTimeChecked: String, checkPeriod: Duration) {
val flags = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) FLAG_IMMUTABLE else 0) or
FLAG_UPDATE_CURRENT
val pendingIntent: PendingIntent =
@ -136,7 +137,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
.setContentTitle(appContext.resources.getString(R.string.Notify_Persist_Short) + " " + lastTimeChecked)
.setStyle(NotificationCompat.BigTextStyle()
.bigText(appContext.resources.getString(R.string.Notify_Persist_Long_P1) + " " +
appContext.resources.getString(R.string.Notify_Persist_Long_P2) + " " + checkPeriod + " "
appContext.resources.getString(R.string.Notify_Persist_Long_P2) + " " + checkPeriod.seconds / 60f + " "
+ appContext.resources.getString(R.string.Notify_Persist_Long_P3)
+ " " + appContext.resources.getString(R.string.Notify_Persist_Long_P4)))
.setSmallIcon(R.drawable.uncivnotification)
@ -180,7 +181,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
}
}
fun startTurnChecker(applicationContext: Context, gameSaver: GameSaver, currentGameInfo: GameInfo, settings: GameSettings) {
fun startTurnChecker(applicationContext: Context, gameSaver: GameSaver, currentGameInfo: GameInfo, settings: GameSettingsMultiplayer) {
Log.i(LOG_TAG, "startTurnChecker")
val gameFiles = gameSaver.getMultiplayerSaves()
val gameIds = Array(gameFiles.count()) {""}
@ -211,17 +212,16 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
}
} else {
val inputData = workDataOf(Pair(FAIL_COUNT, 0), Pair(GAME_ID, gameIds), Pair(GAME_NAME, gameNames),
Pair(USER_ID, settings.userId), Pair(CONFIGURED_DELAY, settings.multiplayerTurnCheckerDelayInMinutes),
Pair(PERSISTENT_NOTIFICATION_ENABLED, settings.multiplayerTurnCheckerPersistentNotificationEnabled),
Pair(FILE_STORAGE, settings.multiplayerServer))
Pair(USER_ID, settings.userId), Pair(CONFIGURED_DELAY, settings.turnCheckerDelay.seconds),
Pair(PERSISTENT_NOTIFICATION_ENABLED, settings.turnCheckerPersistentNotificationEnabled),
Pair(FILE_STORAGE, settings.server))
if (settings.multiplayerTurnCheckerPersistentNotificationEnabled) {
showPersistentNotification(applicationContext,
"", settings.multiplayerTurnCheckerDelayInMinutes.toString())
if (settings.turnCheckerPersistentNotificationEnabled) {
showPersistentNotification(applicationContext, "", settings.turnCheckerDelay)
}
Log.d(LOG_TAG, "startTurnChecker enqueue")
// Initial check always happens after a minute, ignoring delay config. Better user experience this way.
enqueue(applicationContext, 1, inputData)
enqueue(applicationContext, Duration.ofMinutes(1), inputData)
}
}
@ -247,6 +247,11 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
}
}
}
private fun getConfiguredDelay(inputData: Data): Duration {
val delay = inputData.getLong(CONFIGURED_DELAY, Duration.ofMinutes(5).seconds)
return Duration.ofSeconds(delay)
}
}
/**
@ -270,7 +275,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
override fun doWork(): Result = runBlocking {
Log.i(LOG_TAG, "doWork")
val showPersistNotific = inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true)
val configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5)
val configuredDelay = getConfiguredDelay(inputData)
val fileStorage = inputData.getString(FILE_STORAGE)
try {
@ -350,13 +355,12 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
}
return@runBlocking Result.failure()
} else {
if (showPersistNotific) { showPersistentNotification(applicationContext,
applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) }
if (showPersistNotific) { showPersistentNotification(applicationContext, applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay) }
// If check fails, retry in one minute.
// Makes sense, since checks only happen if Internet is available in principle.
// Therefore a failure means either a problem with the GameInfo or with Dropbox.
val inputDataFailIncrease = Data.Builder().putAll(inputData).putInt(FAIL_COUNT, failCount + 1).build()
enqueue(applicationContext, 1, inputDataFailIncrease)
enqueue(applicationContext, Duration.ofMinutes(1), inputDataFailIncrease)
}
} catch (outOfMemory: OutOfMemoryError){ // no point in trying multiple times if this was an oom error
return@runBlocking Result.failure()
@ -379,8 +383,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
}
val displayTime = "$hour:$minute"
showPersistentNotification(applicationContext, displayTime,
inputData.getInt(CONFIGURED_DELAY, 5).toString())
showPersistentNotification(applicationContext, displayTime, getConfiguredDelay(inputData))
}
private fun showErrorNotification(stackTraceString: String) {