Allow EmpireOverview persistence across game launches (#11725)

* Remove blank space between header and list in ResourceOverviewTab vertical mode

* Allow EmpireOverview persistence across game launches
This commit is contained in:
SomeTroglodyte
2024-06-10 21:22:46 +02:00
committed by GitHub
parent b61961d0a5
commit 03a85fb3ad
10 changed files with 100 additions and 31 deletions

View File

@ -98,7 +98,10 @@ class GameSettings {
var autoPlay = GameSettingsAutoPlay()
// This is a string not an enum so if tabs change it won't screw up the json serialization
//TODO remove line in a future update
var lastOverviewPage = EmpireOverviewCategories.Cities.name
/** Holds EmpireOverviewScreen per-page persistable states */
val overview = OverviewPersistableData()
/** Orientation for mobile platforms */
var displayOrientation = ScreenOrientation.Landscape

View File

@ -0,0 +1,61 @@
package com.unciv.models.metadata
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.badlogic.gdx.utils.SerializationException
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories
import com.unciv.ui.screens.overviewscreen.EmpireOverviewTab
class OverviewPersistableData(
private val map: LinkedHashMap<EmpireOverviewCategories, EmpireOverviewTab.EmpireOverviewTabPersistableData> = linkedMapOf()
) : Json.Serializable,
Map<EmpireOverviewCategories, EmpireOverviewTab.EmpireOverviewTabPersistableData> by map
{
//todo so far this is saved but not used, should replace [GameSettings.lastOverviewPage] in a future update.
var last: EmpireOverviewCategories = EmpireOverviewCategories.Cities
fun update(pageObjects: Map<EmpireOverviewCategories, EmpireOverviewTab>) {
for ((category, page) in pageObjects)
map[category] = page.persistableData
}
/**
* Gdx Serialize
* - Intended format: `"overview":{"Cities":{"sortedBy":"Population","direction":"Descending"},...,"Resources":{"vertical":true}}`
* - Outer field name and Object markers are already cared for, so we can begin directly with our fields
*/
override fun write(json: Json) {
if (last != EmpireOverviewCategories.Cities)
json.writeValue("last", last.name, String::class.java)
for ((category, data) in map) {
val clazz = category.getPersistDataClass() ?: continue
if (data.isEmpty()) continue
json.writeValue(category.name, data, clazz)
}
}
/**
* Gdx Deserialize (format see [write])
* - Gdx architecture means this operates on a default instance Gdx just created using the zero-args constructor
* - Should be tolerant against Enum values that do not exist in current code
*/
override fun read(json: Json, jsonData: JsonValue) {
val lastName = jsonData.get("last")?.asString() // Nullable, benign if field missing - getString() is not.
EmpireOverviewCategories.values().firstOrNull { it.name == lastName }?.let { last = it }
if (jsonData.isObject && jsonData.notEmpty())
for (element in jsonData) readEntry(json, element)
}
private fun readEntry(json: Json, element: JsonValue) {
val name = element.name()
val category = EmpireOverviewCategories.values().firstOrNull { it.name == name }
?: return // Guards against downgrading to an Unciv missing a category, but also skips "last" here
val clazz = category.getPersistDataClass()
try {
val data = json.readValue(clazz, element)
map[category] = data
} catch (ex: SerializationException) {
// Unknown Enum values inside an EmpireOverviewTabPersistableData subclass end up here
}
}
}

View File

@ -16,12 +16,10 @@ class CityOverviewTab(
overviewScreen: EmpireOverviewScreen,
persistedData: EmpireOverviewTabPersistableData? = null
) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
class CityTabPersistableData(
override var sortedBy: CityOverviewTabColumn = CityOverviewTabColumn.CityColumn,
) : EmpireOverviewTabPersistableData(), SortableGrid.ISortState<CityOverviewTabColumn> {
override fun isEmpty() = sortedBy == CityOverviewTabColumn.CityColumn
override var direction = SortableGrid.SortDirection.None
class CityTabPersistableData : EmpireOverviewTabPersistableData(), SortableGrid.ISortState<CityOverviewTabColumn> {
override var sortedBy: CityOverviewTabColumn = CityOverviewTabColumn.CityColumn
override var direction: SortableGrid.SortDirection = SortableGrid.SortDirection.None
override fun isEmpty() = sortedBy == CityOverviewTabColumn.CityColumn && direction != SortableGrid.SortDirection.Descending
}
override val persistableData = (persistedData as? CityTabPersistableData) ?: CityTabPersistableData()

View File

@ -23,6 +23,7 @@ enum class EmpireOverviewCategories(
override fun createTab(viewingPlayer: Civilization, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?) =
CityOverviewTab(viewingPlayer, overviewScreen, persistedData)
override fun showDisabled(viewingPlayer: Civilization) = viewingPlayer.cities.isEmpty()
override fun getPersistDataClass() = CityOverviewTab.CityTabPersistableData::class.java
},
Stats("StatIcons/Gold", 'S', Align.top) {
override fun createTab(viewingPlayer: Civilization, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?) =
@ -42,16 +43,19 @@ enum class EmpireOverviewCategories(
override fun createTab(viewingPlayer: Civilization, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?) =
UnitOverviewTab(viewingPlayer, overviewScreen, persistedData)
override fun showDisabled(viewingPlayer: Civilization) = viewingPlayer.units.getCivUnits().none()
override fun getPersistDataClass() = UnitOverviewTab.UnitTabPersistableData::class.java
},
Politics("OtherIcons/Politics", 'P', Align.top) {
override fun createTab(viewingPlayer: Civilization, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?) =
GlobalPoliticsOverviewTable(viewingPlayer, overviewScreen, persistedData)
override fun showDisabled(viewingPlayer: Civilization) = viewingPlayer.diplomacy.isEmpty()
override fun getPersistDataClass() = GlobalPoliticsOverviewTable.DiplomacyTabPersistableData::class.java
},
Resources("StatIcons/Happiness", 'R', Align.topLeft) {
override fun createTab(viewingPlayer: Civilization, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?) =
ResourcesOverviewTab(viewingPlayer, overviewScreen, persistedData)
override fun showDisabled(viewingPlayer: Civilization) = viewingPlayer.detailedCivResources.none { it.resource.resourceType != ResourceType.Bonus }
override fun getPersistDataClass() = ResourcesOverviewTab.ResourcesTabPersistableData::class.java
},
Religion("StatIcons/Faith", 'F', Align.top) {
override fun createTab(viewingPlayer: Civilization, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?) =
@ -85,4 +89,9 @@ enum class EmpireOverviewCategories(
open fun testState(viewingPlayer: Civilization) =
if (showDisabled(viewingPlayer)) EmpireOverviewTabState.Disabled
else EmpireOverviewTabState.Normal
/** Get Java class of persistable data for Json serialization
* - only needed if the data should actually be saved to GameSettings.json: Leaving it at `null` means any specific state persists only through one game run
*/
open fun getPersistDataClass(): Class<out EmpireOverviewTabPersistableData>? = null
}

View File

@ -11,7 +11,6 @@ import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.RecreateOnResize
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories.EmpireOverviewTabState
import com.unciv.ui.screens.overviewscreen.EmpireOverviewTab.EmpireOverviewTabPersistableData
class EmpireOverviewScreen(
private var viewingPlayer: Civilization,
@ -25,25 +24,19 @@ class EmpireOverviewScreen(
private val tabbedPager: TabbedPager
private val pageObjects = HashMap<EmpireOverviewCategories, EmpireOverviewTab>()
companion object {
// This is what keeps per-tab states between overview invocations
var persistState: Map<EmpireOverviewCategories, EmpireOverviewTabPersistableData>? = null
private fun updatePersistState(pageObjects: HashMap<EmpireOverviewCategories, EmpireOverviewTab>) {
persistState = pageObjects.mapValues { it.value.persistableData }.filterNot { it.value.isEmpty() }
}
}
internal val persistState by game.settings::overview
override fun dispose() {
tabbedPager.selectPage(-1)
updatePersistState(pageObjects)
super.dispose()
}
override fun getCivilopediaRuleset() = viewingPlayer.gameInfo.ruleset
init {
val selectCategory = defaultCategory ?: EmpireOverviewCategories.values().firstOrNull { it.name == game.settings.lastOverviewPage }
val selectCategory = defaultCategory
//TODO replace with `?: persistState.last` in a future update
?: EmpireOverviewCategories.values().firstOrNull { it.name == game.settings.lastOverviewPage }
val iconSize = Constants.defaultFontSize.toFloat()
tabbedPager = TabbedPager(
@ -56,7 +49,7 @@ class EmpireOverviewScreen(
val tabState = category.testState(viewingPlayer)
if (tabState == EmpireOverviewTabState.Hidden) continue
val icon = if (category.iconName.isEmpty()) null else ImageGetter.getImage(category.iconName)
val pageObject = category.createTab(viewingPlayer, this, persistState?.get(category))
val pageObject = category.createTab(viewingPlayer, this, persistState[category])
pageObject.pad(10f, 0f, 10f, 0f)
pageObjects[category] = pageObject
val index = tabbedPager.addPage(
@ -72,21 +65,19 @@ class EmpireOverviewScreen(
select(pageObject, selection)
}
}
persistState.update(pageObjects)
val closeButton = getCloseButton { game.popScreen() }
tabbedPager.decorateHeader(closeButton)
tabbedPager.setFillParent(true)
stage.addActor(tabbedPager)
// closeButton.setPosition(stage.width - 10f, stage.height - 10f, Align.topRight)
// stage.addActor(closeButton)
}
override fun recreate(): BaseScreen {
tabbedPager.selectPage(-1) // trigger deselect on _old_ instance so the tabs can persist their stuff
updatePersistState(pageObjects)
return EmpireOverviewScreen(viewingPlayer,
//TODO replace with `persistState.last)` in a future update
EmpireOverviewCategories.values().firstOrNull { it.name == game.settings.lastOverviewPage })
}

View File

@ -10,14 +10,22 @@ abstract class EmpireOverviewTab (
val overviewScreen: EmpireOverviewScreen,
persistedData: EmpireOverviewTabPersistableData? = null
) : Table(BaseScreen.skin), TabbedPager.IPageExtensions {
/** Abstract container for persistable data
* - default class does nothing
* - If persistence should end when quitting the game - do not override [isEmpty] and [EmpireOverviewCategories.getPersistDataClass].
* - For persistence in GameSettings.json, override **both**.
*/
open class EmpireOverviewTabPersistableData {
/** Used by serialization to detect when a default state can be omitted */
open fun isEmpty() = true
}
open val persistableData = persistedData ?: EmpireOverviewTabPersistableData()
override fun activated(index: Int, caption: String, pager: TabbedPager) {
if (caption.isEmpty()) return
if (caption.isEmpty()) return // called from EmpireOverviewScreen.resume()
//TODO remove line in a future update
overviewScreen.game.settings.lastOverviewPage = caption
overviewScreen.persistState.last = EmpireOverviewCategories.values()[index] // Change this if categories are ever reordered or filtered
}
/** Override if the tab can _select_ something specific.

View File

@ -19,10 +19,8 @@ class NotificationsOverviewTable(
persistedData: EmpireOverviewTabPersistableData? = null
) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
class NotificationsTabPersistableData(
var scrollY: Float? = null
) : EmpireOverviewTabPersistableData() {
override fun isEmpty() = scrollY == null
}
var scrollY: Float? = null
) : EmpireOverviewTabPersistableData()
override val persistableData = (persistedData as? NotificationsTabPersistableData) ?: NotificationsTabPersistableData()
override fun activated(index: Int, caption: String, pager: TabbedPager) {
if (persistableData.scrollY != null)

View File

@ -29,9 +29,7 @@ class ReligionOverviewTab(
) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
class ReligionTabPersistableData(
var selectedReligion: String? = null
) : EmpireOverviewTabPersistableData() {
override fun isEmpty() = selectedReligion == null
}
) : EmpireOverviewTabPersistableData()
override val persistableData = (persistedData as? ReligionTabPersistableData) ?: ReligionTabPersistableData()
private val civStatsTable = Table()

View File

@ -129,6 +129,7 @@ class ResourcesOverviewTab(
init {
defaults().pad(defaultPad)
top()
fixedContent.defaults().pad(defaultPad)
turnImageH.onClick {

View File

@ -20,10 +20,12 @@ class UnitOverviewTab(
persistedData: EmpireOverviewTabPersistableData? = null
) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
class UnitTabPersistableData : EmpireOverviewTabPersistableData(), SortableGrid.ISortState<UnitOverviewTabColumn> {
@Transient
var scrollY: Float? = null
override var sortedBy: UnitOverviewTabColumn = UnitOverviewTabColumn.Name
override var direction = SortableGrid.SortDirection.None
override fun isEmpty() = scrollY == null && sortedBy == UnitOverviewTabColumn.Name && direction != SortableGrid.SortDirection.Descending
override fun isEmpty() = sortedBy == UnitOverviewTabColumn.Name && direction != SortableGrid.SortDirection.Descending
}
override val persistableData = (persistedData as? UnitTabPersistableData) ?: UnitTabPersistableData()