Notifications architectural update (#9605)

* Improved Notifications architecture

* Revert NotificationIcon as class hierarchy

* Improved Notifications architecture - migration roadmap first step
This commit is contained in:
SomeTroglodyte 2023-06-25 08:47:50 +02:00 committed by GitHub
parent 1a6d8d72bb
commit 0aca3c307b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 329 additions and 125 deletions

View File

@ -6,6 +6,7 @@ import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonWriter import com.badlogic.gdx.utils.JsonWriter
import com.badlogic.gdx.utils.SerializationException import com.badlogic.gdx.utils.SerializationException
import com.unciv.logic.civilization.CivRankingHistory import com.unciv.logic.civilization.CivRankingHistory
import com.unciv.logic.civilization.Notification
import com.unciv.logic.map.tile.TileHistory import com.unciv.logic.map.tile.TileHistory
import com.unciv.ui.components.input.KeyCharAndCode import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.KeyboardBindings import com.unciv.ui.components.input.KeyboardBindings
@ -29,6 +30,7 @@ fun json() = Json(JsonWriter.OutputType.json).apply {
setSerializer(KeyboardBindings::class.java, KeyboardBindings.Serializer()) setSerializer(KeyboardBindings::class.java, KeyboardBindings.Serializer())
setSerializer(TileHistory::class.java, TileHistory.Serializer()) setSerializer(TileHistory::class.java, TileHistory.Serializer())
setSerializer(CivRankingHistory::class.java, CivRankingHistory.Serializer()) setSerializer(CivRankingHistory::class.java, CivRankingHistory.Serializer())
setSerializer(Notification::class.java, Notification.Serializer())
} }
/** /**

View File

@ -529,8 +529,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
else else
"[$positionsCount] sources of [$resourceName] revealed, e.g. near [${chosenCity.name}]" "[$positionsCount] sources of [$resourceName] revealed, e.g. near [${chosenCity.name}]"
return Notification(text, arrayListOf("ResourceIcons/$resourceName"), return Notification(text, arrayOf("ResourceIcons/$resourceName"),
LocationAction(positions), NotificationCategory.General) LocationAction(positions).asIterable(), NotificationCategory.General)
} }
// All cross-game data which needs to be altered (e.g. when removing or changing a name of a building/tech) // All cross-game data which needs to be altered (e.g. when removing or changing a name of a building/tech)

View File

@ -756,18 +756,24 @@ class Civilization : IsPartOfGameInfoSerialization {
} }
} }
fun addNotification(text: String, location: Vector2, category: NotificationCategory, vararg notificationIcons: String) { // region addNotification
fun addNotification(text: String, category: NotificationCategory, vararg notificationIcons: String) =
addNotification(text, null, category, *notificationIcons)
fun addNotification(text: String, location: Vector2, category: NotificationCategory, vararg notificationIcons: String) =
addNotification(text, LocationAction(location), category, *notificationIcons) addNotification(text, LocationAction(location), category, *notificationIcons)
}
fun addNotification(text: String, category: NotificationCategory, vararg notificationIcons: String) = addNotification(text, null, category, *notificationIcons) fun addNotification(text: String, action: NotificationAction, category: NotificationCategory, vararg notificationIcons: String) =
addNotification(text, listOf(action), category, *notificationIcons)
fun addNotification(text: String, action: NotificationAction?, category: NotificationCategory, vararg notificationIcons: String) { fun addNotification(text: String, actions: Sequence<NotificationAction>, category:NotificationCategory, vararg notificationIcons: String) =
addNotification(text, actions.asIterable(), category, *notificationIcons)
fun addNotification(text: String, actions: Iterable<NotificationAction>?, category: NotificationCategory, vararg notificationIcons: String) {
if (playerType == PlayerType.AI) return // no point in lengthening the saved game info if no one will read it if (playerType == PlayerType.AI) return // no point in lengthening the saved game info if no one will read it
val arrayList = notificationIcons.toCollection(ArrayList()) notifications.add(Notification(text, notificationIcons, actions, category))
notifications.add(Notification(text, arrayList,
if (action is LocationAction && action.locations.isEmpty()) null else action, category))
} }
// endregion
fun addCity(location: Vector2) { fun addCity(location: Vector2) {
val newCity = CityFounder().foundCity(this, location) val newCity = CityFounder().foundCity(this, location)

View File

@ -3,44 +3,50 @@ package com.unciv.logic.civilization
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.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.ui.screens.cityscreen.CityScreen
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.pickerscreens.TechPickerScreen
import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen
import com.unciv.ui.components.MayaCalendar
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
object NotificationIcon {
// Remember: The typical white-on-transparency icon will not be visible on Notifications
const val Barbarians = "ImprovementIcons/Barbarian encampment" typealias NotificationCategory = Notification.NotificationCategory
const val Citadel = "ImprovementIcons/Citadel"
const val City = "ImprovementIcons/City center" open class Notification() : IsPartOfGameInfoSerialization {
const val CityState = "OtherIcons/CityState" /** Category - UI grouping, within a Category the most recent Notification will be shown on top */
const val Crosshair = "OtherIcons/CrosshairB" var category: NotificationCategory = NotificationCategory.General
const val Culture = "StatIcons/Culture" private set
const val Construction = "StatIcons/Production"
const val Death = "OtherIcons/DisbandUnit" /** The notification text, untranslated - will be translated on the fly */
const val Diplomacy = "OtherIcons/Diplomacy" var text: String = ""
const val Faith = "StatIcons/Faith" private set
const val Food = "StatIcons/Food"
const val Gold = "StatIcons/Gold" /** Icons to be shown */
const val Growth = "StatIcons/Population" var icons: ArrayList<String> = ArrayList() // Must be ArrayList and not List so it can be deserialized
const val Happiness = "StatIcons/Happiness" private set
const val Population = "StatIcons/Population"
const val Production = "StatIcons/Production" /** Actions on clicking a Notification - will be activated round-robin style */
const val Question = "OtherIcons/Question" var actions: ArrayList<NotificationAction> = ArrayList()
const val Ruins = "ImprovementIcons/Ancient ruins" private set
const val Science = "StatIcons/Science"
const val Scout = "UnitIcons/Scout" constructor(
const val Spy = "OtherIcons/Spy" text: String,
const val Trade = "StatIcons/Acquire" notificationIcons: Array<out String>,
const val War = "OtherIcons/Pillage" actions: Iterable<NotificationAction>?,
category: NotificationCategory = NotificationCategory.General
) : this() {
this.category = category
this.text = text
if (notificationIcons.isNotEmpty()) {
this.icons = notificationIcons.toCollection(ArrayList())
}
actions?.toCollection(this.actions)
} }
enum class NotificationCategory { enum class NotificationCategory {
// These names are displayed, so remember to add a translation template
// - if there's no other source for one.
General, General,
Trade, Trade,
Diplomacy, Diplomacy,
@ -51,30 +57,16 @@ enum class NotificationCategory {
Espionage, Espionage,
Cities Cities
; ;
companion object { companion object {
fun safeValueOf(name: String): NotificationCategory? = fun safeValueOf(name: String): NotificationCategory? =
values().firstOrNull { it.name == name } values().firstOrNull { it.name == name }
} }
} }
/** @Transient
* [action] is not realized as lambda, as it would be too easy to introduce references to objects /** For round-robin activation in [execute] */
* there that should not be serialized to the saved game. private var index = 0
*/
open class Notification() : IsPartOfGameInfoSerialization {
var text: String = ""
var icons: ArrayList<String> = ArrayList() // Must be ArrayList and not List so it can be deserialized
var action: NotificationAction? = null
var category: String = NotificationCategory.General.name
constructor(text: String, notificationIcons: ArrayList<String>, action: NotificationAction?, category: NotificationCategory) : this() {
this.text = text
this.icons = notificationIcons
this.action = action
this.category = category.name
}
fun addNotificationIconsTo(table: Table, ruleset: Ruleset, iconSize: Float) { fun addNotificationIconsTo(table: Table, ruleset: Ruleset, iconSize: Float) {
if (icons.isEmpty()) return if (icons.isEmpty()) return
@ -92,63 +84,120 @@ open class Notification() : IsPartOfGameInfoSerialization {
table.add(image).size(iconSize).padRight(5f) table.add(image).size(iconSize).padRight(5f)
} }
} }
fun execute(worldScreen: WorldScreen) {
if (actions.isEmpty()) return
actions[index].execute(worldScreen)
index = ++index % actions.size // cycle through tiles
} }
/** defines what to do if the user clicks on a notification */ /**
interface NotificationAction : IsPartOfGameInfoSerialization { * Custom [Gdx.Json][Json] serializer/deserializer for one [Notification].
fun execute(worldScreen: WorldScreen)
}
/** A notification action that cycles through tiles.
* *
* Constructors accept any kind of [Vector2] collection, including [Iterable], [Sequence], `vararg`. * Migration roadmap:
* `varargs` allows nulls which are ignored, a resulting empty list is allowed and equivalent to no [NotificationAction]. *
* 1.) Change internal structures but write old json format
* 2.) Wait for good distribution in multiplayer user base
* 3.) Switch to writing new format
* 4.) Wait for Versions prior to Step 3 to fade out, keep switch for quick revert
* 5.) Remove Switch, old format routines and this comment
*
* Caveats:
*
* * New format can express Notifications the old can't.
* In that case, in Phase 1, reduce to first action and throw away the rest.
*/ */
data class LocationAction(var locations: ArrayList<Vector2> = ArrayList()) : NotificationAction, IsPartOfGameInfoSerialization { class Serializer : Json.Serializer<Notification> {
constructor(locations: Iterable<Vector2>) : this(locations.toCollection(ArrayList())) companion object {
constructor(locations: Sequence<Vector2>) : this(locations.toCollection(ArrayList())) /** The switch that starts Phase III and dies with Phase V
constructor(vararg locations: Vector2?) : this(locations.asSequence().filterNotNull()) * @see Serializer */
private const val compatibilityMode = true
@Transient
private var index = 0
override fun execute(worldScreen: WorldScreen) {
if (locations.isNotEmpty()) {
worldScreen.mapHolder.setCenterPosition(locations[index], selectUnit = false)
index = ++index % locations.size // cycle through tiles
} }
override fun write(json: Json, notification: Notification, knownType: Class<*>?) {
json.writeObjectStart()
if (notification.category != NotificationCategory.General)
json.writeValue("category", notification.category)
if (notification.text.isNotEmpty())
json.writeValue("text", notification.text)
if (notification.icons.isNotEmpty())
json.writeValue("icons", notification.icons, null, String::class.java)
if (compatibilityMode) writeOldFormatAction(json, notification)
else writeNewFormatActions(json, notification)
json.writeObjectEnd()
}
private fun writeNewFormatActions(json: Json, notification: Notification) {
if (notification.actions.isEmpty()) return
json.writeArrayStart("actions")
for (action in notification.actions) {
json.writeObjectStart()
json.writeObjectStart(action::class.java.simpleName)
json.writeFields(action)
json.writeObjectEnd()
json.writeObjectEnd()
}
json.writeArrayEnd()
}
private fun writeOldFormatAction(json: Json, notification: Notification) {
if (notification.actions.isEmpty()) return
val firstAction = notification.actions.first()
if (firstAction !is LocationAction) {
json.writeValue("action", firstAction, null)
return
}
val locations = notification.actions.filterIsInstance<LocationAction>()
.map { it.location }.toTypedArray()
json.writeObjectStart("action")
json.writeValue("class", "com.unciv.logic.civilization.LocationAction")
json.writeValue("locations", locations, Array<Vector2>::class.java, Vector2::class.java)
json.writeObjectEnd()
}
override fun read(json: Json, jsonData: JsonValue, type: Class<*>?) = Notification().apply {
// Cannot be distinguished 100% certain by field names but if neither action / actions exist then both formats are compatible
json.readField(this, "category", jsonData)
json.readField(this, "text", jsonData)
readOldFormatAction(json, jsonData)
readNewFormatActions(json, jsonData)
json.readField(this, "icons", jsonData)
}
private fun Notification.readNewFormatActions(json: Json, jsonData: JsonValue) {
// New format looks like this: "notifications":[
// {"category":"Cities","text":"[Stockholm] has expanded its borders!","icons":["StatIcons/Culture"],"actions":[{"LocationAction":{"location":{"x":7,"y":1}}},{"LocationAction":{"location":{"x":9,"y":3}}}]},
// {"category":"Production","text":"[Nobel Foundation] has been built in [Stockholm]","icons":["BuildingIcons/Nobel Foundation"],"actions":[{"LocationAction":{"location":{"x":9,"y":3}}}]}
// ]
if (!jsonData.hasChild("actions")) return
var entry = jsonData.get("actions").child
while (entry != null) {
actions.addAll(NotificationActionsDeserializer().read(json, entry))
entry = entry.next
} }
} }
/** show tech screen */ private fun Notification.readOldFormatAction(json: Json, jsonData: JsonValue) {
class TechAction(val techName: String = "") : NotificationAction, IsPartOfGameInfoSerialization { // Old format looks like: "notifications":[
override fun execute(worldScreen: WorldScreen) { // {"text":"[Stockholm] has expanded its borders!","icons":["StatIcons/Culture"],"action":{"class":"com.unciv.logic.civilization.LocationAction","locations":[{"x":7,"y":1},{"x":9,"y":3}]},"category":"Cities"},
val tech = worldScreen.gameInfo.ruleset.technologies[techName] // {"text":"[Nobel Foundation] has been built in [Stockholm]","icons":["BuildingIcons/Nobel Foundation"],"action":{"class":"com.unciv.logic.civilization.LocationAction","locations":[{"x":9,"y":3}]},"category":"Production"}
worldScreen.game.pushScreen(TechPickerScreen(worldScreen.viewingCiv, tech)) // ]
val actionData = jsonData.get("action") ?: return
val actionClass = actionData.getString("class")
when (actionClass.substring(actionClass.lastIndexOf('.') + 1)) {
"LocationAction" -> actions += getOldFormatLocations(json, actionData)
"TechAction" -> actions += json.readValue(TechAction::class.java, actionData)
"CityAction" -> actions += json.readValue(CityAction::class.java, actionData)
"DiplomacyAction" -> actions += json.readValue(DiplomacyAction::class.java, actionData)
"MayaLongCountAction" -> actions += MayaLongCountAction()
} }
} }
/** enter city */ private fun getOldFormatLocations(json: Json, actionData: JsonValue): Sequence<LocationAction> {
data class CityAction(val city: Vector2 = Vector2.Zero): NotificationAction, IsPartOfGameInfoSerialization { val locations = json.readValue("locations", Array<Vector2>::class.java, actionData)
override fun execute(worldScreen: WorldScreen) { return locations.asSequence().map { LocationAction(it) }
worldScreen.mapHolder.tileMap[city].getCity()?.let {
if (it.civ == worldScreen.viewingCiv)
worldScreen.game.pushScreen(CityScreen(it))
} }
} }
} }
/** enter diplomacy screen */
data class DiplomacyAction(val otherCivName: String = ""): NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
val otherCiv = worldScreen.gameInfo.getCivilization(otherCivName)
worldScreen.game.pushScreen(DiplomacyScreen(worldScreen.viewingCiv, otherCiv))
}
}
/** enter Maya Long Count popup */
class MayaLongCountAction : NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
MayaCalendar.openPopup(worldScreen, worldScreen.selectedCiv, worldScreen.gameInfo.getYear())
}
}

View File

@ -0,0 +1,118 @@
package com.unciv.logic.civilization
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.ui.components.MayaCalendar
import com.unciv.ui.screens.cityscreen.CityScreen
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.diplomacyscreen.DiplomacyScreen
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
import com.unciv.ui.screens.pickerscreens.TechPickerScreen
import com.unciv.ui.screens.worldscreen.WorldScreen
/** defines what to do if the user clicks on a notification */
/*
* Not realized as lambda, as it would be too easy to introduce references to objects
* there that should not be serialized to the saved game.
*/
interface NotificationAction : IsPartOfGameInfoSerialization {
fun execute(worldScreen: WorldScreen)
}
/** A notification action that shows map places. */
class LocationAction(var location: Vector2) : NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
worldScreen.mapHolder.setCenterPosition(location, selectUnit = false)
}
companion object {
operator fun invoke(locations: Sequence<Vector2>): Sequence<LocationAction> =
locations.map { LocationAction(it) }
operator fun invoke(locations: Iterable<Vector2>): Sequence<LocationAction> =
locations.asSequence().map { LocationAction(it) }
operator fun invoke(vararg locations: Vector2?): Sequence<LocationAction> =
locations.asSequence().filterNotNull().map { LocationAction(it) }
}
}
/** show tech screen */
class TechAction(val techName: String = "") : NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
val tech = worldScreen.gameInfo.ruleset.technologies[techName]
worldScreen.game.pushScreen(TechPickerScreen(worldScreen.viewingCiv, tech))
}
}
/** enter city */
class CityAction(val city: Vector2 = Vector2.Zero): NotificationAction,
IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
val cityObject = worldScreen.mapHolder.tileMap[city].getCity()
?: return
if (cityObject.civ == worldScreen.viewingCiv)
worldScreen.game.pushScreen(CityScreen(cityObject))
}
}
/** enter diplomacy screen */
class DiplomacyAction(val otherCivName: String = ""): NotificationAction,
IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
val otherCiv = worldScreen.gameInfo.getCivilization(otherCivName)
worldScreen.game.pushScreen(DiplomacyScreen(worldScreen.viewingCiv, otherCiv))
}
}
/** enter Maya Long Count popup */
class MayaLongCountAction : NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
MayaCalendar.openPopup(worldScreen, worldScreen.selectedCiv, worldScreen.gameInfo.getYear())
}
}
/** A notification action that shows and selects units on the map. */
class MapUnitAction(var location: Vector2) : NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
worldScreen.mapHolder.setCenterPosition(location, selectUnit = true)
}
}
/** A notification action that shows the Civilopedia entry for a Wonder. */
class WonderAction(val wonderName: String) : NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
worldScreen.game.pushScreen(CivilopediaScreen(worldScreen.gameInfo.ruleset, CivilopediaCategories.Wonder, wonderName))
}
}
/** Show Promotion picker for a MapUnit - by name and location, as they lack a serialized unique ID */
class PromoteUnitAction(val name: String, val location: Vector2) : NotificationAction, IsPartOfGameInfoSerialization {
override fun execute(worldScreen: WorldScreen) {
val tile = worldScreen.gameInfo.tileMap[location]
val unit = tile.militaryUnit?.takeIf { it.name == name && it.civ == worldScreen.selectedCiv }
?: return
worldScreen.game.pushScreen(PromotionPickerScreen(unit))
}
}
@Suppress("PropertyName")
internal class NotificationActionsDeserializer {
// This exists as trick to leverage readFields for Json deserialization
var LocationAction: LocationAction? = null
var TechAction: TechAction? = null
var CityAction: CityAction? = null
var DiplomacyAction: DiplomacyAction? = null
var MayaLongCountAction: MayaLongCountAction? = null
var MapUnitAction: MapUnitAction? = null
var WonderAction: WonderAction? = null
var PromoteUnitAction: PromoteUnitAction? = null
fun read(json: Json, jsonData: JsonValue): List<NotificationAction> {
json.readFields(this, jsonData)
return listOfNotNull(
LocationAction, TechAction, CityAction, DiplomacyAction,
MayaLongCountAction, MapUnitAction, WonderAction, PromoteUnitAction
)
}
}

View File

@ -0,0 +1,29 @@
package com.unciv.logic.civilization
object NotificationIcon {
// Remember: The typical white-on-transparency icon will not be visible on Notifications
const val Barbarians = "ImprovementIcons/Barbarian encampment"
const val Citadel = "ImprovementIcons/Citadel"
const val City = "ImprovementIcons/City center"
const val CityState = "OtherIcons/CityState"
const val Crosshair = "OtherIcons/CrosshairB"
const val Culture = "StatIcons/Culture"
const val Construction = "StatIcons/Production"
const val Death = "OtherIcons/DisbandUnit"
const val Diplomacy = "OtherIcons/Diplomacy"
const val Faith = "StatIcons/Faith"
const val Food = "StatIcons/Food"
const val Gold = "StatIcons/Gold"
const val Growth = "StatIcons/Population"
const val Happiness = "StatIcons/Happiness"
const val Population = "StatIcons/Population"
const val Production = "StatIcons/Production"
const val Question = "OtherIcons/Question"
const val Ruins = "ImprovementIcons/Ancient ruins"
const val Science = "StatIcons/Science"
const val Scout = "UnitIcons/Scout"
const val Spy = "OtherIcons/Spy"
const val Trade = "StatIcons/Acquire"
const val War = "OtherIcons/Pillage"
}

View File

@ -382,7 +382,7 @@ class TechManager : IsPartOfGameInfoSerialization {
// Add notifications for obsolete units/constructions // Add notifications for obsolete units/constructions
for ((unit, cities) in unitUpgrades) { for ((unit, cities) in unitUpgrades) {
if (cities.isEmpty()) continue if (cities.isEmpty()) continue
val locationAction = LocationAction(cities.mapTo(ArrayList(cities.size)) { it.location }) val locationAction = LocationAction(cities.asSequence().map { it.location })
val cityText = if (cities.size == 1) "[${cities.first().name}]" val cityText = if (cities.size == 1) "[${cities.first().name}]"
else "[${cities.size}] cities" else "[${cities.size}] cities"
val newUnit = obsoleteUnits[unit]?.name val newUnit = obsoleteUnits[unit]?.name

View File

@ -36,6 +36,6 @@ abstract class EmpireOverviewTab (
val worldScreen = GUI.getWorldScreen() val worldScreen = GUI.getWorldScreen()
worldScreen.notificationsScroll.oneTimeNotification = notification worldScreen.notificationsScroll.oneTimeNotification = notification
UncivGame.Current.resetToWorldScreen() UncivGame.Current.resetToWorldScreen()
notification.action?.execute(worldScreen) notification.execute(worldScreen)
} }
} }

View File

@ -73,7 +73,7 @@ class NotificationsOverviewTable(
}).row() }).row()
for (category in NotificationCategory.values()){ for (category in NotificationCategory.values()){
val categoryNotifications = notifications.filter { it.category == category.name } val categoryNotifications = notifications.filter { it.category == category }
if (categoryNotifications.isEmpty()) continue if (categoryNotifications.isEmpty()) continue
if (category != NotificationCategory.General) if (category != NotificationCategory.General)
@ -88,7 +88,7 @@ class NotificationsOverviewTable(
notificationTable.add(label).width(stageWidth / 2 - iconSize * notification.icons.size) notificationTable.add(label).width(stageWidth / 2 - iconSize * notification.icons.size)
notificationTable.background = BaseScreen.skinStrings.getUiBackground("OverviewScreen/NotificationOverviewTable/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape) notificationTable.background = BaseScreen.skinStrings.getUiBackground("OverviewScreen/NotificationOverviewTable/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape)
notificationTable.touchable = Touchable.enabled notificationTable.touchable = Touchable.enabled
if (notification.action != null) if (notification.actions.isNotEmpty())
notificationTable.onClick { showOneTimeNotification(notification) } notificationTable.onClick { showOneTimeNotification(notification) }
notification.addNotificationIconsTo(notificationTable, gameInfo.ruleset, iconSize) notification.addNotificationIconsTo(notificationTable, gameInfo.ruleset, iconSize)

View File

@ -238,7 +238,7 @@ class NotificationsScroll(
val backgroundDrawable = BaseScreen.skinStrings.getUiBackground("WorldScreen/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape) val backgroundDrawable = BaseScreen.skinStrings.getUiBackground("WorldScreen/Notification", BaseScreen.skinStrings.roundedEdgeRectangleShape)
val orderedNotifications = (additionalNotification + notifications.asReversed()) val orderedNotifications = (additionalNotification + notifications.asReversed())
.groupBy { NotificationCategory.safeValueOf(it.category) ?: NotificationCategory.General } .groupBy { it.category }
.toSortedMap() // This sorts by Category ordinal, so far intentional - the order of the grouped lists are unaffected .toSortedMap() // This sorts by Category ordinal, so far intentional - the order of the grouped lists are unaffected
for ((category, categoryNotifications) in orderedNotifications) { for ((category, categoryNotifications) in orderedNotifications) {
if (category == NotificationCategory.General) if (category == NotificationCategory.General)
@ -351,7 +351,7 @@ class NotificationsScroll(
add(listItem).pad(topBottomPad, listItemPad, topBottomPad, rightPadToScreenEdge) add(listItem).pad(topBottomPad, listItemPad, topBottomPad, rightPadToScreenEdge)
touchable = Touchable.enabled touchable = Touchable.enabled
onClick { onClick {
notification.action?.execute(worldScreen) notification.execute(worldScreen)
clickedNotification = notification clickedNotification = notification
GUI.setUpdateWorldOnNextRender() GUI.setUpdateWorldOnNextRender()
} }