Notifications remove backward compatibility (#10228)

* Remove backwards compatibility code from Notification serializer

* Notification constructor simplified

* One measly typo
This commit is contained in:
SomeTroglodyte
2023-10-05 09:24:27 +02:00
committed by GitHub
parent 8c6e046fc8
commit ed97c5b0ea
2 changed files with 22 additions and 70 deletions

View File

@ -1,6 +1,5 @@
package com.unciv.logic.civilization package com.unciv.logic.civilization
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.Json
@ -32,15 +31,13 @@ open class Notification() : IsPartOfGameInfoSerialization {
constructor( constructor(
text: String, text: String,
notificationIcons: Array<out String>, notificationIcons: Array<out String>, // `out` needed so we can pass a vararg directly
actions: Iterable<NotificationAction>?, actions: Iterable<NotificationAction>?,
category: NotificationCategory = NotificationCategory.General category: NotificationCategory = NotificationCategory.General
) : this() { ) : this() {
this.category = category this.category = category
this.text = text this.text = text
if (notificationIcons.isNotEmpty()) { notificationIcons.toCollection(this.icons)
this.icons = notificationIcons.toCollection(ArrayList())
}
actions?.toCollection(this.actions) actions?.toCollection(this.actions)
} }
@ -98,25 +95,23 @@ open class Notification() : IsPartOfGameInfoSerialization {
/** /**
* Custom [Gdx.Json][Json] serializer/deserializer for one [Notification]. * Custom [Gdx.Json][Json] serializer/deserializer for one [Notification].
* *
* Migration roadmap: * Example of the serialized format:
* * ```json
* 1.) Change internal structures but write old json format * "notifications":[
* 2.) Wait for good distribution in multiplayer user base * {
* 3.) Switch to writing new format * "category":"Production",
* 4.) Wait for Versions prior to Step 3 to fade out, keep switch for quick revert * "text":"[Nobel Foundation] has been built in [Stockholm]",
* 5.) Remove Switch, old format routines and this comment * "icons":["BuildingIcons/Nobel Foundation"],
* * "actions":[
* Caveats: * {"LocationAction":{"location":{"x":9,"y":3}}},
* * {"CivilopediaAction":{"link":"Wonder/Nobel Foundation"}},
* * New format can express Notifications the old can't. * {"CityAction":{"city":{"x":9,"y":3}}}
* In that case, in Phase 1, reduce to first action and throw away the rest. * ]
* }
* ]
* ```
*/ */
class Serializer : Json.Serializer<Notification> { class Serializer : Json.Serializer<Notification> {
companion object {
/** The switch that starts Phase III and dies with Phase V
* @see Serializer */
private const val compatibilityMode = false
}
override fun write(json: Json, notification: Notification, knownType: Class<*>?) { override fun write(json: Json, notification: Notification, knownType: Class<*>?) {
json.writeObjectStart() json.writeObjectStart()
@ -127,13 +122,12 @@ open class Notification() : IsPartOfGameInfoSerialization {
if (notification.icons.isNotEmpty()) if (notification.icons.isNotEmpty())
json.writeValue("icons", notification.icons, null, String::class.java) json.writeValue("icons", notification.icons, null, String::class.java)
if (compatibilityMode) writeOldFormatAction(json, notification) writeActions(json, notification)
else writeNewFormatActions(json, notification)
json.writeObjectEnd() json.writeObjectEnd()
} }
private fun writeNewFormatActions(json: Json, notification: Notification) { private fun writeActions(json: Json, notification: Notification) {
if (notification.actions.isEmpty()) return if (notification.actions.isEmpty()) return
json.writeArrayStart("actions") json.writeArrayStart("actions")
for (action in notification.actions) { for (action in notification.actions) {
@ -146,35 +140,14 @@ open class Notification() : IsPartOfGameInfoSerialization {
json.writeArrayEnd() 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 { 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, "category", jsonData)
json.readField(this, "text", jsonData) json.readField(this, "text", jsonData)
readOldFormatAction(json, jsonData) readActions(json, jsonData)
readNewFormatActions(json, jsonData)
json.readField(this, "icons", jsonData) json.readField(this, "icons", jsonData)
} }
private fun Notification.readNewFormatActions(json: Json, jsonData: JsonValue) { private fun Notification.readActions(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 if (!jsonData.hasChild("actions")) return
var entry = jsonData.get("actions").child var entry = jsonData.get("actions").child
while (entry != null) { while (entry != null) {
@ -182,26 +155,5 @@ open class Notification() : IsPartOfGameInfoSerialization {
entry = entry.next entry = entry.next
} }
} }
private fun Notification.readOldFormatAction(json: Json, jsonData: JsonValue) {
// Old format looks like: "notifications":[
// {"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"},
// {"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"}
// ]
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()
}
}
private fun getOldFormatLocations(json: Json, actionData: JsonValue): Sequence<LocationAction> {
val locations = json.readValue("locations", Array<Vector2>::class.java, actionData)
return locations.asSequence().map { LocationAction(it) }
}
} }
} }

View File

@ -133,7 +133,7 @@ class OverviewAction(
internal class NotificationActionsDeserializer { internal class NotificationActionsDeserializer {
/* This exists as trick to leverage readFields for Json deserialization. /* This exists as trick to leverage readFields for Json deserialization.
// The serializer writes each NotificationAction as json object (within the actions array), // The serializer writes each NotificationAction as json object (within the actions array),
// containing the class simpleName as subfield name, which carries any (on none) subclass- // containing the class simpleName as subfield name, which carries any (or none) subclass-
// specific data as its object value. So, reading this from json data will fill just one of the // specific data as its object value. So, reading this from json data will fill just one of the
// fields, and the listOfNotNull will output that field only. // fields, and the listOfNotNull will output that field only.
// Even though we know there's only one result, no need to first() since it's no advantage to the caller. // Even though we know there's only one result, no need to first() since it's no advantage to the caller.