From 86d5011da192cbeaa7486ba3a631478b47db590a Mon Sep 17 00:00:00 2001 From: Timo T Date: Sun, 8 May 2022 12:35:41 +0200 Subject: [PATCH] In-depth serialization improvement, fixes Barbarian Camps revealed by Honor not showing immediately in multiplayer * Fix Barbarian Camp Spawned notification not revealing the camp on the map in multiplayer * Fix lastSeenImprovement not being cloned * Use HashMapVector2 in BarbarianManager * Fix value not having its class written out for proper deserializing * Refactor: various code improvements --- .../app/CustomSaveLocationHelperAndroid.kt | 2 +- core/src/com/unciv/JsonParser.kt | 22 -------- core/src/com/unciv/json/HashMapVector2.kt | 24 +++++++++ .../unciv/json/NonStringKeyMapSerializer.kt | 53 +++++++++++++++++++ core/src/com/unciv/json/UncivJson.kt | 31 +++++++++++ .../com/unciv/logic/BackwardCompatibility.kt | 12 +++++ core/src/com/unciv/logic/BarbarianManager.kt | 4 +- core/src/com/unciv/logic/GameInfo.kt | 3 ++ core/src/com/unciv/logic/GameSaver.kt | 3 +- core/src/com/unciv/logic/MapSaver.kt | 3 +- .../logic/civilization/CivilizationInfo.kt | 24 ++++----- .../com/unciv/logic/multiplayer/DropBox.kt | 8 +-- .../unciv/logic/multiplayer/Multiplayer.kt | 5 +- .../unciv/logic/multiplayer/ServerMutex.kt | 3 +- .../com/unciv/models/metadata/GameSettings.kt | 5 +- core/src/com/unciv/models/ruleset/Ruleset.kt | 44 +++++++-------- .../com/unciv/models/tilesets/TileSetCache.kt | 7 +-- .../translations/TranslationFileWriter.kt | 8 +-- .../com/unciv/ui/crashhandling/CrashScreen.kt | 3 +- core/src/com/unciv/ui/images/ImageGetter.kt | 3 +- core/src/com/unciv/ui/pickerscreens/GitHub.kt | 10 ++-- .../ui/pickerscreens/ModManagementScreen.kt | 5 +- core/src/com/unciv/ui/saves/SaveGameScreen.kt | 3 +- .../unciv/ui/tutorials/TutorialController.kt | 5 +- .../CustomSaveLocationHelperDesktop.kt | 2 +- .../com/unciv/testing/SerializationTests.kt | 3 +- .../unciv/testing/TutorialTranslationTests.kt | 5 +- 27 files changed, 206 insertions(+), 94 deletions(-) delete mode 100644 core/src/com/unciv/JsonParser.kt create mode 100644 core/src/com/unciv/json/HashMapVector2.kt create mode 100644 core/src/com/unciv/json/NonStringKeyMapSerializer.kt create mode 100644 core/src/com/unciv/json/UncivJson.kt diff --git a/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt b/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt index 4eb2d69c6c..134a4b9217 100644 --- a/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt +++ b/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt @@ -6,10 +6,10 @@ import android.net.Uri import android.os.Build import androidx.annotation.GuardedBy import androidx.annotation.RequiresApi +import com.unciv.json.json import com.unciv.logic.CustomSaveLocationHelper import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver -import com.unciv.logic.GameSaver.json // The Storage Access Framework is available from API 19 and up: // https://developer.android.com/guide/topics/providers/document-provider diff --git a/core/src/com/unciv/JsonParser.kt b/core/src/com/unciv/JsonParser.kt deleted file mode 100644 index 43da9ac8c9..0000000000 --- a/core/src/com/unciv/JsonParser.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.unciv - -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.files.FileHandle -import com.badlogic.gdx.utils.Json -import com.unciv.logic.UncivShowableException - -class JsonParser { - - private val json = Json().apply { ignoreUnknownFields = true } - - fun getFromJson(tClass: Class, filePath: String): T = getFromJson(tClass, Gdx.files.internal(filePath)) - - fun getFromJson(tClass: Class, file: FileHandle): T { - try { - val jsonText = file.readString(Charsets.UTF_8.name()) - return json.fromJson(tClass, jsonText) - } catch (exception:Exception){ - throw Exception("Could not parse json of file ${file.name()}", exception) - } - } -} \ No newline at end of file diff --git a/core/src/com/unciv/json/HashMapVector2.kt b/core/src/com/unciv/json/HashMapVector2.kt new file mode 100644 index 0000000000..956eddd6a4 --- /dev/null +++ b/core/src/com/unciv/json/HashMapVector2.kt @@ -0,0 +1,24 @@ +package com.unciv.json + +import com.badlogic.gdx.math.Vector2 +import com.badlogic.gdx.utils.Json +import java.util.HashMap + +/** + * @see NonStringKeyMapSerializer + */ +class HashMapVector2 : HashMap() { + companion object { + init { + @Suppress("UNCHECKED_CAST") // kotlin can't tell that HashMapVector2 is also a MutableMap within generics + val mapClass = HashMapVector2::class.java as Class> + val serializer = NonStringKeyMapSerializer( + mapClass, + Vector2::class.java, + { HashMapVector2() } + ) + jsonSerializers.add(Pair(mapClass, serializer)) + } + } +} + diff --git a/core/src/com/unciv/json/NonStringKeyMapSerializer.kt b/core/src/com/unciv/json/NonStringKeyMapSerializer.kt new file mode 100644 index 0000000000..4b6d33bd75 --- /dev/null +++ b/core/src/com/unciv/json/NonStringKeyMapSerializer.kt @@ -0,0 +1,53 @@ +package com.unciv.json + +import com.badlogic.gdx.utils.Json +import com.badlogic.gdx.utils.Json.Serializer +import com.badlogic.gdx.utils.JsonValue + +/** + * A [Serializer] for gdx's [Json] that serializes a map that does not have [String] as its key class. + * + * Exists solely because [Json] always serializes any map by converting its key to [String], so when you load it again, + * all your keys are [String], messing up value retrieval. + * + * To work around that, we have to use a custom serializer. A custom serializer in Json is only added for a specific class + * and only checks for direct equality, and since we can't just do `HashMap::class.java`, only `HashMap::class.java`, + * we have to create a completely new class and use that class as [mapClass] here. + * + * @param MT Must be a type that extends [MutableMap] + * @param KT Must be the key type of [MT] + */ +class NonStringKeyMapSerializer, KT>( + private val mapClass: Class, + private val keyClass: Class, + private val mutableMapFactory: () -> MT +) : Serializer { + + override fun write(json: Json, toWrite: MT, knownType: Class<*>) { + json.writeObjectStart() + json.writeType(mapClass) + json.writeArrayStart("entries") + for ((key, value) in toWrite) { + json.writeArrayStart() + json.writeValue(key) + json.writeValue(value, null) + json.writeArrayEnd() + } + json.writeArrayEnd() + json.writeObjectEnd() + } + + override fun read(json: Json, jsonData: JsonValue, type: Class<*>?): MT { + val result = mutableMapFactory() + val entries = jsonData.get("entries") + var entry = entries!!.child + while (entry != null) { + val key = json.readValue(keyClass, entry.child) + val value = json.readValue(null, entry.child.next) + result[key!!] = value!! as Any + + entry = entry.next + } + return result + } +} \ No newline at end of file diff --git a/core/src/com/unciv/json/UncivJson.kt b/core/src/com/unciv/json/UncivJson.kt new file mode 100644 index 0000000000..1e6ac5a8d3 --- /dev/null +++ b/core/src/com/unciv/json/UncivJson.kt @@ -0,0 +1,31 @@ +package com.unciv.json + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.utils.Json +import com.badlogic.gdx.utils.Json.Serializer + +internal val jsonSerializers = ArrayList, Serializer<*>>>() + +/** + * [Json] is not thread-safe. + */ +fun json() = Json().apply { + setIgnoreDeprecated(true) + ignoreUnknownFields = true + for ((clazz, serializer) in jsonSerializers) { + @Suppress("UNCHECKED_CAST") // we used * to accept all types, so kotlin can't know if the class & serializer parameters are actually the same + setSerializer(clazz as Class, serializer as Serializer) + } +} + +fun Json.fromJsonFile(tClass: Class, filePath: String): T = fromJsonFile(tClass, Gdx.files.internal(filePath)) + +fun Json.fromJsonFile(tClass: Class, file: FileHandle): T { + try { + val jsonText = file.readString(Charsets.UTF_8.name()) + return fromJson(tClass, jsonText) + } catch (exception:Exception){ + throw Exception("Could not parse json of file ${file.name()}", exception) + } +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/BackwardCompatibility.kt b/core/src/com/unciv/logic/BackwardCompatibility.kt index 37029472be..4b5fe98ead 100644 --- a/core/src/com/unciv/logic/BackwardCompatibility.kt +++ b/core/src/com/unciv/logic/BackwardCompatibility.kt @@ -1,7 +1,9 @@ package com.unciv.logic +import com.badlogic.gdx.math.Vector2 import com.unciv.logic.city.CityConstructions import com.unciv.logic.city.PerpetualConstruction +import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.TechManager import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyManager @@ -147,4 +149,14 @@ object BackwardCompatibility { maxXPfromBarbarians = 30 } } + + /** Removes the workaround previously used for storing a map that does not have a [String] key + * @see com.unciv.json.NonStringKeyMapSerializer + */ + @Suppress("DEPRECATION") + fun CivilizationInfo.migrateSeenImprovements() { + if (lastSeenImprovementSaved.isEmpty()) return; + lastSeenImprovement.putAll(lastSeenImprovementSaved.mapKeys { Vector2().fromString(it.key) }) + lastSeenImprovementSaved.clear() + } } diff --git a/core/src/com/unciv/logic/BarbarianManager.kt b/core/src/com/unciv/logic/BarbarianManager.kt index 905dcd43b0..3f8bf475a2 100644 --- a/core/src/com/unciv/logic/BarbarianManager.kt +++ b/core/src/com/unciv/logic/BarbarianManager.kt @@ -2,6 +2,8 @@ package com.unciv.logic import com.badlogic.gdx.math.Vector2 import com.unciv.Constants +import com.unciv.json.HashMapVector2 +import com.unciv.json.json import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap @@ -15,7 +17,7 @@ import kotlin.math.min import kotlin.math.pow class BarbarianManager { - val camps = HashMap() + val camps = HashMapVector2() @Transient lateinit var gameInfo: GameInfo diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index b1a4dd5a7a..cc82da0345 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -3,6 +3,7 @@ package com.unciv.logic import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.BackwardCompatibility.guaranteeUnitPromotions +import com.unciv.logic.BackwardCompatibility.migrateSeenImprovements import com.unciv.logic.BackwardCompatibility.removeMissingModReferences import com.unciv.logic.automation.NextTurnAutomation import com.unciv.logic.civilization.* @@ -395,6 +396,8 @@ class GameInfo { gameParameters.baseRuleset = baseRulesetInMods gameParameters.mods = LinkedHashSet(gameParameters.mods.filter { it != baseRulesetInMods }) } + // [TEMPORARY] Convert old saves to remove json workaround + for (civInfo in civilizations) civInfo.migrateSeenImprovements() ruleSet = RulesetCache.getComplexRuleset(gameParameters) diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index 4bf8a78090..fd260827a5 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.utils.Json import com.unciv.UncivGame +import com.unciv.json.json import com.unciv.models.metadata.GameSettings import com.unciv.ui.crashhandling.crashHandlingThread import com.unciv.ui.crashhandling.postCrashHandlingRunnable @@ -21,8 +22,6 @@ object GameSaver { * See https://developer.android.com/training/data-storage/app-specific#external-access-files */ var externalFilesDirForAndroid = "" - fun json() = Json().apply { setIgnoreDeprecated(true); ignoreUnknownFields = true } // Json() is NOT THREAD SAFE so we need to create a new one for each function - fun getSubfolder(multiplayer: Boolean = false) = if (multiplayer) multiplayerFilesFolder else saveFilesFolder fun getSave(GameName: String, multiplayer: Boolean = false): FileHandle { diff --git a/core/src/com/unciv/logic/MapSaver.kt b/core/src/com/unciv/logic/MapSaver.kt index 2b7de2ad88..de7a60f8b6 100644 --- a/core/src/com/unciv/logic/MapSaver.kt +++ b/core/src/com/unciv/logic/MapSaver.kt @@ -2,13 +2,12 @@ package com.unciv.logic import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle +import com.unciv.json.json import com.unciv.logic.map.TileMap import com.unciv.ui.saves.Gzip object MapSaver { - fun json() = GameSaver.json() - const val mapsFolder = "maps" var saveZipped = true diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 773653a1c1..d6bfb52343 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -1,8 +1,15 @@ package com.unciv.logic.civilization import com.badlogic.gdx.math.Vector2 +import com.badlogic.gdx.utils.Json +import com.badlogic.gdx.utils.Json.Serializer +import com.badlogic.gdx.utils.JsonValue import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.json.HashMapVector2 +import com.unciv.json.json +import com.unciv.logic.BarbarianManager +import com.unciv.logic.Encampment import com.unciv.logic.GameInfo import com.unciv.logic.UncivShowableException import com.unciv.logic.automation.NextTurnAutomation @@ -32,9 +39,6 @@ import com.unciv.ui.utils.toPercent import com.unciv.ui.utils.withItem import com.unciv.ui.victoryscreen.RankingType import java.util.* -import kotlin.NoSuchElementException -import kotlin.collections.ArrayList -import kotlin.collections.HashMap import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt @@ -166,13 +170,11 @@ class CivilizationInfo { var citiesCreated = 0 var exploredTiles = HashSet() - // This double construction because for some reason the game wants to load a - // map as a map causing all sorts of type problems. - // So we let the game have its map and remap it in setTransients, - // everyone's happy. Sort of. + @Deprecated("Only for backward compatibility, will have no values after GameInfo.setTransients", + ReplaceWith("lastSeenImprovement")) var lastSeenImprovementSaved = HashMap() - @Transient - var lastSeenImprovement = HashMap() + + var lastSeenImprovement = HashMapVector2() // To correctly determine "game over" condition as clarified in #4707 // Nullable type meant to be deprecated and converted to non-nullable, @@ -251,7 +253,7 @@ class CivilizationInfo { // Cloning it by-pointer is a horrific move, since the serialization would go over it ANYWAY and still lead to concurrency problems. // Cloning it by iterating on the tilemap values may seem ridiculous, but it's a perfectly thread-safe way to go about it, unlike the other solutions. toReturn.exploredTiles.addAll(gameInfo.tileMap.values.asSequence().map { it.position }.filter { it in exploredTiles }) - toReturn.lastSeenImprovementSaved.putAll(lastSeenImprovement.mapKeys { it.key.toString() }) + toReturn.lastSeenImprovement.putAll(lastSeenImprovement) toReturn.notifications.addAll(notifications) toReturn.citiesCreated = citiesCreated toReturn.popupAlerts.addAll(popupAlerts) @@ -805,8 +807,6 @@ class CivilizationInfo { } hasLongCountDisplayUnique = hasUnique(UniqueType.MayanCalendarDisplay) - - lastSeenImprovement.putAll(lastSeenImprovementSaved.mapKeys { Vector2().fromString(it.key) }) } fun updateSightAndResources() { diff --git a/core/src/com/unciv/logic/multiplayer/DropBox.kt b/core/src/com/unciv/logic/multiplayer/DropBox.kt index 78487485f8..0eee21ec58 100644 --- a/core/src/com/unciv/logic/multiplayer/DropBox.kt +++ b/core/src/com/unciv/logic/multiplayer/DropBox.kt @@ -1,6 +1,6 @@ package com.unciv.logic.multiplayer -import com.unciv.logic.GameSaver +import com.unciv.json.json import com.unciv.ui.utils.UncivDateFormat.parseDate import java.io.* import java.net.HttpURLConnection @@ -63,12 +63,12 @@ object DropBox { // instead of the path. val response = dropboxApi("https://api.dropboxapi.com/2/files/list_folder", "{\"path\":\"$folder\"}", "application/json") - var currentFolderListChunk = GameSaver.json().fromJson(FolderList::class.java, response) + var currentFolderListChunk = json().fromJson(FolderList::class.java, response) folderList.addAll(currentFolderListChunk.entries) while (currentFolderListChunk.has_more) { val continuationResponse = dropboxApi("https://api.dropboxapi.com/2/files/list_folder/continue", "{\"cursor\":\"${currentFolderListChunk.cursor}\"}", "application/json") - currentFolderListChunk = GameSaver.json().fromJson(FolderList::class.java, continuationResponse) + currentFolderListChunk = json().fromJson(FolderList::class.java, continuationResponse) folderList.addAll(currentFolderListChunk.entries) } return folderList @@ -115,7 +115,7 @@ object DropBox { val stream = dropboxApi("https://api.dropboxapi.com/2/files/get_metadata", "{\"path\":\"$fileName\"}", "application/json")!! val reader = BufferedReader(InputStreamReader(stream)) - return GameSaver.json().fromJson(DropboxMetaData::class.java, reader.readText()) + return json().fromJson(DropboxMetaData::class.java, reader.readText()) } // diff --git a/core/src/com/unciv/logic/multiplayer/Multiplayer.kt b/core/src/com/unciv/logic/multiplayer/Multiplayer.kt index dc7db3c52c..89048ee707 100644 --- a/core/src/com/unciv/logic/multiplayer/Multiplayer.kt +++ b/core/src/com/unciv/logic/multiplayer/Multiplayer.kt @@ -3,6 +3,7 @@ package com.unciv.logic.multiplayer import com.badlogic.gdx.Net import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.json.json import com.unciv.logic.GameInfo import com.unciv.logic.GameInfoPreview import com.unciv.logic.GameSaver @@ -86,7 +87,7 @@ class OnlineMultiplayer(var fileStorageIdentifier: String? = null) { tryUploadGamePreview(gameInfo.asPreview()) } - val zippedGameInfo = Gzip.zip(GameSaver.json().toJson(gameInfo)) + val zippedGameInfo = Gzip.zip(json().toJson(gameInfo)) fileStorage.saveFileData(gameInfo.gameId, zippedGameInfo) } @@ -97,7 +98,7 @@ class OnlineMultiplayer(var fileStorageIdentifier: String? = null) { * @see GameInfo.asPreview */ fun tryUploadGamePreview(gameInfo: GameInfoPreview) { - val zippedGameInfo = Gzip.zip(GameSaver.json().toJson(gameInfo)) + val zippedGameInfo = Gzip.zip(json().toJson(gameInfo)) fileStorage.saveFileData("${gameInfo.gameId}_Preview", zippedGameInfo) } diff --git a/core/src/com/unciv/logic/multiplayer/ServerMutex.kt b/core/src/com/unciv/logic/multiplayer/ServerMutex.kt index 3ed6c1a461..6685f355e0 100644 --- a/core/src/com/unciv/logic/multiplayer/ServerMutex.kt +++ b/core/src/com/unciv/logic/multiplayer/ServerMutex.kt @@ -1,5 +1,6 @@ package com.unciv.logic.multiplayer +import com.unciv.json.json import com.unciv.logic.GameInfo import com.unciv.logic.GameInfoPreview import com.unciv.logic.GameSaver @@ -67,7 +68,7 @@ class ServerMutex(val gameInfo: GameInfoPreview) { } try { - OnlineMultiplayer().fileStorage.saveFileData(fileName, Gzip.zip(GameSaver.json().toJson(LockFile()))) + OnlineMultiplayer().fileStorage.saveFileData(fileName, Gzip.zip(json().toJson(LockFile()))) } catch (ex: FileStorageConflictException) { return locked } diff --git a/core/src/com/unciv/models/metadata/GameSettings.kt b/core/src/com/unciv/models/metadata/GameSettings.kt index 2d2d853689..6905feb923 100644 --- a/core/src/com/unciv/models/metadata/GameSettings.kt +++ b/core/src/com/unciv/models/metadata/GameSettings.kt @@ -3,8 +3,9 @@ package com.unciv.models.metadata import com.badlogic.gdx.Application import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle -import com.unciv.JsonParser import com.unciv.Constants +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.logic.GameSaver import com.unciv.ui.utils.Fonts import java.io.File @@ -124,7 +125,7 @@ class GameSettings { // In fact, at this point Gdx.app or Gdx.files are null but this still works. val file = FileHandle(base + File.separator + GameSaver.settingsFileName) return if (file.exists()) - JsonParser().getFromJson( + json().fromJsonFile( GameSettings::class.java, file ) diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index 169a1082dd..75cc151be8 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -4,7 +4,8 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.graphics.Color import com.unciv.Constants -import com.unciv.JsonParser +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.logic.BackwardCompatibility.updateDeprecations import com.unciv.logic.UncivShowableException import com.unciv.logic.map.MapParameters @@ -72,7 +73,6 @@ class ModOptions : IHasUniques { class Ruleset { - private val jsonParser = JsonParser() var folderLocation:FileHandle?=null var name = "" @@ -197,7 +197,7 @@ class Ruleset { val modOptionsFile = folderHandle.child("ModOptions.json") if (modOptionsFile.exists()) { try { - modOptions = jsonParser.getFromJson(ModOptions::class.java, modOptionsFile) + modOptions = json().fromJsonFile(ModOptions::class.java, modOptionsFile) modOptions.updateDeprecations() } catch (ex: Exception) {} modOptions.uniqueObjects = modOptions.uniques.map { Unique(it, UniqueTarget.ModOptions) } @@ -206,7 +206,7 @@ class Ruleset { val techFile = folderHandle.child("Techs.json") if (techFile.exists()) { - val techColumns = jsonParser.getFromJson(Array::class.java, techFile) + val techColumns = json().fromJsonFile(Array::class.java, techFile) for (techColumn in techColumns) { for (tech in techColumn.techs) { if (tech.cost == 0) tech.cost = techColumn.techCost @@ -217,49 +217,49 @@ class Ruleset { } val buildingsFile = folderHandle.child("Buildings.json") - if (buildingsFile.exists()) buildings += createHashmap(jsonParser.getFromJson(Array::class.java, buildingsFile)) + if (buildingsFile.exists()) buildings += createHashmap(json().fromJsonFile(Array::class.java, buildingsFile)) for(building in buildings.values) if(building.requiredBuildingInAllCities != null) building.uniques.add(UniqueType.RequiresBuildingInAllCities.text.fillPlaceholders(building.requiredBuildingInAllCities!!)) val terrainsFile = folderHandle.child("Terrains.json") if (terrainsFile.exists()) { - terrains += createHashmap(jsonParser.getFromJson(Array::class.java, terrainsFile)) + terrains += createHashmap(json().fromJsonFile(Array::class.java, terrainsFile)) for (terrain in terrains.values) terrain.setTransients() } val resourcesFile = folderHandle.child("TileResources.json") - if (resourcesFile.exists()) tileResources += createHashmap(jsonParser.getFromJson(Array::class.java, resourcesFile)) + if (resourcesFile.exists()) tileResources += createHashmap(json().fromJsonFile(Array::class.java, resourcesFile)) val improvementsFile = folderHandle.child("TileImprovements.json") - if (improvementsFile.exists()) tileImprovements += createHashmap(jsonParser.getFromJson(Array::class.java, improvementsFile)) + if (improvementsFile.exists()) tileImprovements += createHashmap(json().fromJsonFile(Array::class.java, improvementsFile)) val erasFile = folderHandle.child("Eras.json") - if (erasFile.exists()) eras += createHashmap(jsonParser.getFromJson(Array::class.java, erasFile)) + if (erasFile.exists()) eras += createHashmap(json().fromJsonFile(Array::class.java, erasFile)) // While `eras.values.toList()` might seem more logical, eras.values is a MutableCollection and // therefore does not guarantee keeping the order of elements like a LinkedHashMap does. // Using map{} sidesteps this problem eras.map { it.value }.withIndex().forEach { it.value.eraNumber = it.index } val unitTypesFile = folderHandle.child("UnitTypes.json") - if (unitTypesFile.exists()) unitTypes += createHashmap(jsonParser.getFromJson(Array::class.java, unitTypesFile)) + if (unitTypesFile.exists()) unitTypes += createHashmap(json().fromJsonFile(Array::class.java, unitTypesFile)) val unitsFile = folderHandle.child("Units.json") - if (unitsFile.exists()) units += createHashmap(jsonParser.getFromJson(Array::class.java, unitsFile)) + if (unitsFile.exists()) units += createHashmap(json().fromJsonFile(Array::class.java, unitsFile)) val promotionsFile = folderHandle.child("UnitPromotions.json") - if (promotionsFile.exists()) unitPromotions += createHashmap(jsonParser.getFromJson(Array::class.java, promotionsFile)) + if (promotionsFile.exists()) unitPromotions += createHashmap(json().fromJsonFile(Array::class.java, promotionsFile)) val questsFile = folderHandle.child("Quests.json") - if (questsFile.exists()) quests += createHashmap(jsonParser.getFromJson(Array::class.java, questsFile)) + if (questsFile.exists()) quests += createHashmap(json().fromJsonFile(Array::class.java, questsFile)) val specialistsFile = folderHandle.child("Specialists.json") - if (specialistsFile.exists()) specialists += createHashmap(jsonParser.getFromJson(Array::class.java, specialistsFile)) + if (specialistsFile.exists()) specialists += createHashmap(json().fromJsonFile(Array::class.java, specialistsFile)) val policiesFile = folderHandle.child("Policies.json") if (policiesFile.exists()) { policyBranches += createHashmap( - jsonParser.getFromJson(Array::class.java, policiesFile) + json().fromJsonFile(Array::class.java, policiesFile) ) for (branch in policyBranches.values) { // Setup this branch @@ -289,34 +289,34 @@ class Ruleset { val beliefsFile = folderHandle.child("Beliefs.json") if (beliefsFile.exists()) - beliefs += createHashmap(jsonParser.getFromJson(Array::class.java, beliefsFile)) + beliefs += createHashmap(json().fromJsonFile(Array::class.java, beliefsFile)) val religionsFile = folderHandle.child("Religions.json") if (religionsFile.exists()) - religions += jsonParser.getFromJson(Array::class.java, religionsFile).toList() + religions += json().fromJsonFile(Array::class.java, religionsFile).toList() val ruinRewardsFile = folderHandle.child("Ruins.json") if (ruinRewardsFile.exists()) - ruinRewards += createHashmap(jsonParser.getFromJson(Array::class.java, ruinRewardsFile)) + ruinRewards += createHashmap(json().fromJsonFile(Array::class.java, ruinRewardsFile)) val nationsFile = folderHandle.child("Nations.json") if (nationsFile.exists()) { - nations += createHashmap(jsonParser.getFromJson(Array::class.java, nationsFile)) + nations += createHashmap(json().fromJsonFile(Array::class.java, nationsFile)) for (nation in nations.values) nation.setTransients() } val difficultiesFile = folderHandle.child("Difficulties.json") if (difficultiesFile.exists()) - difficulties += createHashmap(jsonParser.getFromJson(Array::class.java, difficultiesFile)) + difficulties += createHashmap(json().fromJsonFile(Array::class.java, difficultiesFile)) val globalUniquesFile = folderHandle.child("GlobalUniques.json") if (globalUniquesFile.exists()) { - globalUniques = jsonParser.getFromJson(GlobalUniques::class.java, globalUniquesFile) + globalUniques = json().fromJsonFile(GlobalUniques::class.java, globalUniquesFile) } val victoryTypesFiles = folderHandle.child("VictoryTypes.json") if (victoryTypesFiles.exists()) { - victories += createHashmap(jsonParser.getFromJson(Array::class.java, victoryTypesFiles)) + victories += createHashmap(json().fromJsonFile(Array::class.java, victoryTypesFiles)) } diff --git a/core/src/com/unciv/models/tilesets/TileSetCache.kt b/core/src/com/unciv/models/tilesets/TileSetCache.kt index ecfefe444e..c73f22cb12 100644 --- a/core/src/com/unciv/models/tilesets/TileSetCache.kt +++ b/core/src/com/unciv/models/tilesets/TileSetCache.kt @@ -2,8 +2,9 @@ package com.unciv.models.tilesets import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle -import com.unciv.JsonParser import com.unciv.UncivGame +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.images.ImageGetter @@ -49,7 +50,7 @@ object TileSetCache : HashMap() { try { val key = TileSetAndMod(tileSetName, "") assert(key !in allConfigs) - allConfigs[key] = JsonParser().getFromJson(TileSetConfig::class.java, configFile) + allConfigs[key] = json().fromJsonFile(TileSetConfig::class.java, configFile) if (printOutput) { println("TileSetConfig loaded successfully: ${configFile.name()}") println() @@ -78,7 +79,7 @@ object TileSetCache : HashMap() { tileSetName = configFile.nameWithoutExtension().removeSuffix("Config") val key = TileSetAndMod(tileSetName, modName) assert(key !in allConfigs) - allConfigs[key] = JsonParser().getFromJson(TileSetConfig::class.java, configFile) + allConfigs[key] = json().fromJsonFile(TileSetConfig::class.java, configFile) if (printOutput) { println("TileSetConfig loaded successfully: ${configFile.path()}") println() diff --git a/core/src/com/unciv/models/translations/TranslationFileWriter.kt b/core/src/com/unciv/models/translations/TranslationFileWriter.kt index eb750064ae..b78bd07ad8 100644 --- a/core/src/com/unciv/models/translations/TranslationFileWriter.kt +++ b/core/src/com/unciv/models/translations/TranslationFileWriter.kt @@ -3,7 +3,8 @@ package com.unciv.models.translations import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.utils.Array -import com.unciv.JsonParser +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.LocaleCode import com.unciv.models.ruleset.* @@ -219,7 +220,7 @@ object TranslationFileWriter { private fun generateTutorialsStrings(): MutableSet { val tutorialsStrings = mutableSetOf() - val tutorials = JsonParser().getFromJson(LinkedHashMap>().javaClass, "jsons/Tutorials.json") + val tutorials = json().fromJsonFile(LinkedHashMap>().javaClass, "jsons/Tutorials.json") var uniqueIndexOfNewLine = 0 for (tutorial in tutorials) { @@ -273,7 +274,6 @@ object TranslationFileWriter { val startMillis = System.currentTimeMillis() var uniqueIndexOfNewLine = 0 - val jsonParser = JsonParser() val listOfJSONFiles = jsonsFolder .list { file -> file.name.endsWith(".json", true) } .sortedBy { it.name() } // generatedStrings maintains order, so let's feed it a predictable one @@ -290,7 +290,7 @@ object TranslationFileWriter { if (javaClass == this.javaClass) continue // unknown JSON, let's skip it - val array = jsonParser.getFromJson(javaClass, jsonFile.path()) + val array = json().fromJsonFile(javaClass, jsonFile.path()) resultStrings = mutableSetOf() this[filename] = resultStrings diff --git a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt index 47c54d27be..05726fa68a 100644 --- a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt +++ b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt @@ -9,6 +9,7 @@ import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Json import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.json.json import com.unciv.models.ruleset.RulesetCache import com.unciv.ui.images.IconTextButton import com.unciv.ui.images.ImageGetter @@ -55,7 +56,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() { private fun tryGetSaveGame() = try { UncivGame.Current.gameInfo.let { gameInfo -> - Json().toJson(gameInfo).let { + json().toJson(gameInfo).let { jsonString -> Gzip.zip(jsonString) } } // Taken from old CrashController().buildReport(). diff --git a/core/src/com/unciv/ui/images/ImageGetter.kt b/core/src/com/unciv/ui/images/ImageGetter.kt index 28e6bc6583..030c585d11 100644 --- a/core/src/com/unciv/ui/images/ImageGetter.kt +++ b/core/src/com/unciv/ui/images/ImageGetter.kt @@ -18,6 +18,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.json.json import com.unciv.logic.GameSaver import com.unciv.models.ruleset.Nation import com.unciv.models.ruleset.Ruleset @@ -79,7 +80,7 @@ object ImageGetter { fun loadModAtlases(mod: String, folder: FileHandle) { // See #4993 - you can't .list() on a jar file, so the ImagePacker leaves us the list of actual atlases. val controlFile = folder.child("Atlases.json") - val fileNames = (if (controlFile.exists()) GameSaver.json().fromJson(Array::class.java, controlFile) + val fileNames = (if (controlFile.exists()) json().fromJson(Array::class.java, controlFile) else emptyArray()).toMutableList() if (mod.isNotEmpty()) fileNames += "game" for (fileName in fileNames) { diff --git a/core/src/com/unciv/ui/pickerscreens/GitHub.kt b/core/src/com/unciv/ui/pickerscreens/GitHub.kt index 9cf8cc34f3..a37d0ce5fd 100644 --- a/core/src/com/unciv/ui/pickerscreens/GitHub.kt +++ b/core/src/com/unciv/ui/pickerscreens/GitHub.kt @@ -3,9 +3,9 @@ package com.unciv.ui.pickerscreens import com.badlogic.gdx.Files import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.utils.Json -import com.unciv.JsonParser +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.logic.BackwardCompatibility.updateDeprecations -import com.unciv.logic.GameSaver import com.unciv.models.ruleset.ModOptions import java.io.* import java.net.HttpURLConnection @@ -234,7 +234,7 @@ object Github { retries++ // An extra retry so the 403 is ignored in the retry count } } ?: continue - return GameSaver.json().fromJson(RepoSearch::class.java, inputStream.bufferedReader().readText()) + return json().fromJson(RepoSearch::class.java, inputStream.bufferedReader().readText()) } return null } @@ -315,13 +315,13 @@ object Github { */ fun rewriteModOptions(repo: Repo, modFolder: FileHandle) { val modOptionsFile = modFolder.child("jsons/ModOptions.json") - val modOptions = if (modOptionsFile.exists()) JsonParser().getFromJson(ModOptions::class.java, modOptionsFile) else ModOptions() + val modOptions = if (modOptionsFile.exists()) json().fromJsonFile(ModOptions::class.java, modOptionsFile) else ModOptions() modOptions.modUrl = repo.html_url modOptions.lastUpdated = repo.pushed_at modOptions.author = repo.owner.login modOptions.modSize = repo.size modOptions.updateDeprecations() - Json().toJson(modOptions, modOptionsFile) + json().toJson(modOptions, modOptionsFile) } } diff --git a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt index 48112974e8..30b2c0ecd5 100644 --- a/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/ModManagementScreen.kt @@ -6,8 +6,9 @@ import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.* import com.badlogic.gdx.utils.Align -import com.unciv.JsonParser import com.unciv.MainMenuScreen +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.models.ruleset.ModOptions import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache @@ -581,7 +582,7 @@ class ModManagementScreen( companion object { val modsToHideAsUrl by lazy { val blockedModsFile = Gdx.files.internal("jsons/ManuallyBlockedMods.json") - JsonParser().getFromJson(Array::class.java, blockedModsFile) + json().fromJsonFile(Array::class.java, blockedModsFile) } } } diff --git a/core/src/com/unciv/ui/saves/SaveGameScreen.kt b/core/src/com/unciv/ui/saves/SaveGameScreen.kt index cdd1f1702a..dba158db84 100644 --- a/core/src/com/unciv/ui/saves/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/saves/SaveGameScreen.kt @@ -7,6 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.utils.Json import com.unciv.UncivGame +import com.unciv.json.json import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver import com.unciv.models.translations.tr @@ -45,7 +46,7 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true copyJsonButton.onClick { thread(name="Copy to clipboard") { // the Gzip rarely leads to ANRs try { - val json = Json().toJson(gameInfo) + val json = json().toJson(gameInfo) val base64Gzip = Gzip.zip(json) Gdx.app.clipboard.contents = base64Gzip } catch (OOM: OutOfMemoryError) { diff --git a/core/src/com/unciv/ui/tutorials/TutorialController.kt b/core/src/com/unciv/ui/tutorials/TutorialController.kt index 4321c8cc40..99c47ff6ed 100644 --- a/core/src/com/unciv/ui/tutorials/TutorialController.kt +++ b/core/src/com/unciv/ui/tutorials/TutorialController.kt @@ -1,8 +1,9 @@ package com.unciv.ui.tutorials import com.badlogic.gdx.utils.Array -import com.unciv.JsonParser import com.unciv.UncivGame +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.models.Tutorial import com.unciv.models.stats.INamed import com.unciv.ui.civilopedia.FormattedLine @@ -15,7 +16,7 @@ class TutorialController(screen: BaseScreen) { private var isTutorialShowing = false var allTutorialsShowedCallback: (() -> Unit)? = null private val tutorialRender = TutorialRender(screen) - private val tutorials = JsonParser().getFromJson(LinkedHashMap>().javaClass, "jsons/Tutorials.json") + private val tutorials = json().fromJsonFile(LinkedHashMap>().javaClass, "jsons/Tutorials.json") fun showTutorial(tutorial: Tutorial) { tutorialQueue.add(tutorial) diff --git a/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt b/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt index b37b3d9ff3..586f2abc8b 100644 --- a/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt +++ b/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt @@ -1,10 +1,10 @@ package com.unciv.app.desktop import com.badlogic.gdx.Gdx +import com.unciv.json.json import com.unciv.logic.CustomSaveLocationHelper import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver -import com.unciv.logic.GameSaver.json import java.awt.event.WindowEvent import java.io.File import java.util.concurrent.CancellationException diff --git a/tests/src/com/unciv/testing/SerializationTests.kt b/tests/src/com/unciv/testing/SerializationTests.kt index 1267cd5319..577f93bf77 100644 --- a/tests/src/com/unciv/testing/SerializationTests.kt +++ b/tests/src/com/unciv/testing/SerializationTests.kt @@ -1,6 +1,7 @@ package com.unciv.testing import com.unciv.UncivGame +import com.unciv.json.json import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver import com.unciv.logic.GameStarter @@ -83,7 +84,7 @@ class SerializationTests { @Test fun canSerializeGame() { val json = try { - GameSaver.json().toJson(game) + json().toJson(game) } catch (ex: Exception) { "" } diff --git a/tests/src/com/unciv/testing/TutorialTranslationTests.kt b/tests/src/com/unciv/testing/TutorialTranslationTests.kt index 5fc3e7044a..4ca4d83f70 100644 --- a/tests/src/com/unciv/testing/TutorialTranslationTests.kt +++ b/tests/src/com/unciv/testing/TutorialTranslationTests.kt @@ -1,7 +1,8 @@ package com.unciv.testing import com.badlogic.gdx.utils.Array -import com.unciv.JsonParser +import com.unciv.json.fromJsonFile +import com.unciv.json.json import com.unciv.models.Tutorial import org.junit.Assert.assertTrue import org.junit.Test @@ -16,7 +17,7 @@ class TutorialTranslationTests { @Test fun tutorialsFileIsSerializable() { - val map = JsonParser().getFromJson(LinkedHashMap>().javaClass, "jsons/Tutorials.json") + val map = json().fromJsonFile(LinkedHashMap>().javaClass, "jsons/Tutorials.json") assertTrue("The number of items from Tutorials.json must match to the enum Tutorial", map.size == tutorialCount)