diff --git a/.github/workflows/incrementVersionAndChangelog.js b/.github/workflows/incrementVersionAndChangelog.js index 2afa104301..ebd5322c98 100644 --- a/.github/workflows/incrementVersionAndChangelog.js +++ b/.github/workflows/incrementVersionAndChangelog.js @@ -1,100 +1,127 @@ - -const { Octokit } = require("@octokit/rest"); -const { version } = require("os"); -const internal = require("stream"); -const fs = require("fs") +const {Octokit} = require("@octokit/rest"); +const fs = require("fs"); // To be run from the main Unciv repo directory // Summarizes and adds the summary to the changelog.md file // Meant to be run from a Github action as part of the preparation for version rollout -async function main(){ +//region Executed Code +(async () => { + const nextVersion = await createChangeLog(); + const nextIncrementalVersion = updateBuildConfig(nextVersion); + updateGameVersion(nextVersion, nextIncrementalVersion); +})(); +//endregion +//region Function Definitions +async function createChangeLog() { // no need to add auth: token since we're only reading from the commit list, which is public anyway const octokit = new Octokit({}); - var result = await octokit.repos.listCommits({ owner: "yairm210", repo: "Unciv", - per_page: 50 }) - + per_page: 50 + }); var commitSummary = ""; - var ownerToCommits = {} - var reachedPreviousVersion = false - var nextVersionString = "" + var ownerToCommits = {}; + var reachedPreviousVersion = false; + var nextVersionString = ""; result.data.forEach(commit => { - if (reachedPreviousVersion) return - var author = commit.author.login - if (author=="uncivbot[bot]") return - var commitMessage = commit.commit.message.split("\n")[0]; + if (reachedPreviousVersion) return; + var author = commit.author.login; + if (author === "uncivbot[bot]") return; + var commitMessage = commit.commit.message.split("\n")[0]; - var versionMatches = commitMessage.match(/^\d+\.\d+\.(\d+)$/) - if (versionMatches){ // match EXACT version, like 3.4.55 ^ is for start-of-line, $ for end-of-line - reachedPreviousVersion=true - var minorVersion = Number(versionMatches[1]) - console.log("Previous version: "+commitMessage) - nextVersionString = commitMessage.replace(RegExp(minorVersion+"$"), minorVersion+1 ) - console.log("Next version: " + nextVersionString) - return - } - if (commitMessage.startsWith("Merge ") || commitMessage.startsWith("Update ")) return - commitMessage = commitMessage.replace(/\(\#\d+\)/,"").replace(/\#\d+/,"") // match PR auto-text, like (#2345) or just #2345 - if (author != "yairm210"){ - if (ownerToCommits[author] == undefined) ownerToCommits[author]=[] - ownerToCommits[author].push(commitMessage) + var versionMatches = commitMessage.match(/^\d+\.\d+\.(\d+)$/); + if (versionMatches) { // match EXACT version, like 3.4.55 ^ is for start-of-line, $ for end-of-line + reachedPreviousVersion = true; + var minorVersion = Number(versionMatches[1]); + console.log("Previous version: " + commitMessage); + nextVersionString = commitMessage.replace(RegExp(minorVersion + "$"), minorVersion + 1); + console.log("Next version: " + nextVersionString); + return; + } + if (commitMessage.startsWith("Merge ") || commitMessage.startsWith("Update ")) return; + commitMessage = commitMessage.replace(/\(\#\d+\)/, "").replace(/\#\d+/, ""); // match PR auto-text, like (#2345) or just #2345 + if (author !== "yairm210") { + if (typeof ownerToCommits[author] === "undefined") ownerToCommits[author] = []; + ownerToCommits[author].push(commitMessage); + } else { + commitSummary += "\n\n" + commitMessage; } - else commitSummary += "\n\n" + commitMessage } ); - + Object.entries(ownerToCommits).forEach(entry => { const [author, commits] = entry; - if (commits.length==1) commitSummary += "\n\n" + commits[0] + " - By "+author - else { - commitSummary += "\n\nBy "+author+":" - commits.forEach(commitMessage => { commitSummary += "\n- "+commitMessage }) + if (commits.length === 1) { + commitSummary += "\n\n" + commits[0] + " - By " + author; + } else { + commitSummary += "\n\nBy " + author + ":"; + commits.forEach(commitMessage => { commitSummary += "\n- " + commitMessage }); } }) - console.log(commitSummary) + console.log(commitSummary); - var textToAddToChangelog = "## "+ nextVersionString + commitSummary + "\n\n" + var textToAddToChangelog = "## " + nextVersionString + commitSummary + "\n\n"; - var changelogPath = 'changelog.md' - var currentChangelog = fs.readFileSync(changelogPath).toString() - if (!currentChangelog.startsWith(textToAddToChangelog)){ // minor idempotency - don't add twice - var newChangelog = textToAddToChangelog + currentChangelog - fs.writeFileSync(changelogPath, newChangelog) + var changelogPath = 'changelog.md'; + var currentChangelog = fs.readFileSync(changelogPath).toString(); + if (!currentChangelog.startsWith(textToAddToChangelog)) { // minor idempotency - don't add twice + var newChangelog = textToAddToChangelog + currentChangelog; + fs.writeFileSync(changelogPath, newChangelog); } - - var buildConfigPath = "buildSrc/src/main/kotlin/BuildConfig.kt" - var buildConfigString = fs.readFileSync(buildConfigPath).toString() - - console.log("Original: "+buildConfigString) - - // node.js string.match returns a regex string array, where array[0] is the entirety of the captured string, - // and array[1] is the first group, array[2] is the second group etc. - - var appVersion = buildConfigString.match(/appVersion = "(.*)"/) - if (appVersion != nextVersionString){ - buildConfigString = buildConfigString.replace(appVersion[0], appVersion[0].replace(appVersion[1], nextVersionString)) - var androidVersion = buildConfigString.match(/appCodeNumber = (\d*)/) - console.log("Android version: "+androidVersion) - var nextAndroidVersion = Number(androidVersion[1]) + 1 - console.log("Next Android version: "+ nextAndroidVersion) - buildConfigString = buildConfigString.replace(androidVersion[0], - androidVersion[0].replace(androidVersion[1], nextAndroidVersion)) - - console.log("Final: "+buildConfigString) - fs.writeFileSync(buildConfigPath, buildConfigString) - - // A new, discrete changelog file for fastlane (F-Droid support): - var fastlaneChangelogPath = "fastlane/metadata/android/en-US/changelogs/" + nextAndroidVersion + ".txt" - fs.writeFileSync(fastlaneChangelogPath, textToAddToChangelog) - } - + return nextVersionString; } -main() \ No newline at end of file +function updateBuildConfig(nextVersionString) { + var buildConfigPath = "buildSrc/src/main/kotlin/BuildConfig.kt"; + var buildConfigString = fs.readFileSync(buildConfigPath).toString(); + + console.log("Original: " + buildConfigString); + + // Javascript string.match returns a regex string array, where array[0] is the entirety of the captured string, + // and array[1] is the first group, array[2] is the second group etc. + + var appVersionMatch = buildConfigString.match(/appVersion = "(.*)"/); + const curVersion = appVersionMatch[1]; + if (curVersion !== nextVersionString) { + buildConfigString = buildConfigString.replace(appVersionMatch[0], appVersionMatch[0].replace(curVersion, nextVersionString)); + var incrementalVersionMatch = buildConfigString.match(/appCodeNumber = (\d*)/); + let curIncrementalVersion = incrementalVersionMatch[1]; + console.log("Current incremental version: " + curIncrementalVersion); + const nextIncrementalVersion = Number(curIncrementalVersion) + 1; + console.log("Next incremental version: " + nextIncrementalVersion); + buildConfigString = buildConfigString.replace(incrementalVersionMatch[0], + incrementalVersionMatch[0].replace(curIncrementalVersion, nextIncrementalVersion)); + + console.log("Final: " + buildConfigString); + fs.writeFileSync(buildConfigPath, buildConfigString); + + // A new, discrete changelog file for fastlane (F-Droid support): + var fastlaneChangelogPath = "fastlane/metadata/android/en-US/changelogs/" + nextIncrementalVersion + ".txt"; + fs.writeFileSync(fastlaneChangelogPath, textToAddToChangelog); + return nextIncrementalVersion; + } + return appVersionMatch; +} + +function updateGameVersion(nextVersion, nextIncrementalVersion) { + const gameInfoPath = "core/src/com/unciv/UncivGame.kt"; + const gameInfoSource = fs.readFileSync(gameInfoPath).toString(); + const regexp = /(\/\/region AUTOMATICALLY GENERATED VERSION DATA - DO NOT CHANGE THIS REGION, INCLUDING THIS COMMENT)[\s\S]*(\/\/endregion)/; + const withNewVersion = gameInfoSource.replace(regexp, function(match, grp1, grp2) { + const versionClassStr = createVersionClassString(nextVersion, nextIncrementalVersion); + return `${grp1}\n val VERSION = ${versionClassStr}\n ${grp2}`; + }) + fs.writeFileSync(gameInfoPath, withNewVersion); +} + +function createVersionClassString(nextVersion, nextIncrementalVersion) { + return `Version("${nextVersion}", ${nextIncrementalVersion})`; +} + +//endregion diff --git a/.gitignore b/.gitignore index 0def93ff83..e933f2bf2e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,9 @@ www-test/ /android/libs/x86/ /android/libs/x86_64/ /android/gen/ -.idea/* -!/.idea/inspectionProfiles -/.idea/inspectionProfiles/* +.idea/* +!/.idea/inspectionProfiles +/.idea/inspectionProfiles/* !/.idea/inspectionProfiles/Project_Default.xml *.ipr *.iws @@ -154,4 +154,8 @@ android/assets/music/ # Visual Studio Code .vscode/ /.github/workflows/node_modules/* - + +# node.js (we only need these temporarily for the release build) +node_modules/ +package-lock.json +package.json diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index b4267d6dd6..3458915725 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -331,7 +331,6 @@ National ability = Uniques = Promotions = Load copied data = -Could not load game from clipboard! = Reset to defaults = Are you sure you want to reset all game options to defaults? = Start game! = @@ -630,7 +629,10 @@ Saved game name = [player] - [turns] turns = Copy to clipboard = Copy saved game to clipboard = -Could not load game = +Could not load game! = +Could not load game from clipboard! = +Could not load game from custom location! = +The save was created with an incompatible version of Unciv: [version]. Please update Unciv to at least [version] and try again. = Load [saveFileName] = Are you sure you want to delete this save? = Delete save = @@ -647,7 +649,6 @@ If you could copy your game data ("Copy saved game to clipboard" - = I could maybe help you figure out what went wrong, since this isn't supposed to happen! = Missing mods: [mods] = Load from custom location = -Could not load game from custom location! = Save to custom location = Could not save game to custom location! = Download missing mods = diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index 052bcd287d..0a2adc3101 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -50,7 +50,6 @@ open class AndroidLauncher : AndroidApplication() { platformSpecificHelper.toggleDisplayCutout(settings.androidCutout) val androidParameters = UncivGameParameters( - version = BuildConfig.VERSION_NAME, crashReportSysInfo = CrashReportSysInfoAndroid, fontImplementation = NativeFontAndroid((Fonts.ORIGINAL_FONT_SIZE * settings.fontSizeMultiplier).toInt(), fontFamily), customFileLocationHelper = customFileLocationHelper, diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index 55f159bd73..8a88160cbe 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -8,6 +8,7 @@ import com.badlogic.gdx.Screen import com.badlogic.gdx.scenes.scene2d.actions.Actions import com.badlogic.gdx.utils.Align import com.unciv.logic.GameInfo +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.UncivFiles import com.unciv.logic.civilization.PlayerType import com.unciv.logic.multiplayer.OnlineMultiplayer @@ -24,9 +25,9 @@ import com.unciv.ui.audio.SoundPlayer import com.unciv.ui.crashhandling.CrashScreen import com.unciv.ui.crashhandling.wrapCrashHandlingUnit import com.unciv.ui.images.ImageGetter -import com.unciv.ui.multiplayer.MultiplayerHelpers import com.unciv.ui.popup.ConfirmPopup import com.unciv.ui.popup.Popup +import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.extensions.center import com.unciv.ui.worldscreen.PlayerReadyScreen @@ -43,10 +44,8 @@ import java.util.* import kotlin.collections.ArrayDeque class UncivGame(parameters: UncivGameParameters) : Game() { - // we need this secondary constructor because Java code for iOS can't handle Kotlin lambda parameters - constructor(version: String) : this(UncivGameParameters(version, null)) + constructor() : this(UncivGameParameters()) - val version = parameters.version val crashReportSysInfo = parameters.crashReportSysInfo val cancelDiscordEvent = parameters.cancelDiscordEvent var fontImplementation = parameters.fontImplementation @@ -333,7 +332,8 @@ class UncivGame(parameters: UncivGameParameters) : Game() { val mainMenu = MainMenuScreen() replaceCurrentScreen(mainMenu) val popup = Popup(mainMenu) - popup.addGoodSizedLabel(MultiplayerHelpers.getLoadExceptionMessage(ex)) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex) + popup.addGoodSizedLabel(message) popup.row() popup.addCloseButton() popup.open() @@ -444,11 +444,23 @@ class UncivGame(parameters: UncivGameParameters) : Game() { } companion object { + //region AUTOMATICALLY GENERATED VERSION DATA - DO NOT CHANGE THIS REGION, INCLUDING THIS COMMENT + val VERSION = Version("4.1.14", 731) + //endregion + lateinit var Current: UncivGame fun isCurrentInitialized() = this::Current.isInitialized fun isCurrentGame(gameId: String): Boolean = isCurrentInitialized() && Current.gameInfo != null && Current.gameInfo!!.gameId == gameId fun isDeepLinkedGameLoading() = isCurrentInitialized() && Current.deepLinkedMultiplayerGame != null } + + data class Version( + val text: String, + val number: Int + ) : IsPartOfGameInfoSerialization { + @Suppress("unused") // used by json serialization + constructor() : this("", -1) + } } private class GameStartScreen : BaseScreen() { diff --git a/core/src/com/unciv/UncivGameParameters.kt b/core/src/com/unciv/UncivGameParameters.kt index 1ac4ee1976..703ff37cdc 100644 --- a/core/src/com/unciv/UncivGameParameters.kt +++ b/core/src/com/unciv/UncivGameParameters.kt @@ -6,8 +6,7 @@ import com.unciv.ui.utils.AudioExceptionHelper import com.unciv.ui.utils.GeneralPlatformSpecificHelpers import com.unciv.ui.utils.NativeFontImplementation -class UncivGameParameters(val version: String, - val crashReportSysInfo: CrashReportSysInfo? = null, +class UncivGameParameters(val crashReportSysInfo: CrashReportSysInfo? = null, val cancelDiscordEvent: (() -> Unit)? = null, val fontImplementation: NativeFontImplementation? = null, val consoleMode: Boolean = false, diff --git a/core/src/com/unciv/logic/BarbarianManager.kt b/core/src/com/unciv/logic/BarbarianManager.kt index e2b57a5605..48148ec614 100644 --- a/core/src/com/unciv/logic/BarbarianManager.kt +++ b/core/src/com/unciv/logic/BarbarianManager.kt @@ -13,7 +13,7 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.pow -class BarbarianManager { +class BarbarianManager : IsPartOfGameInfoSerialization { val camps = HashMapVector2() @Transient @@ -165,7 +165,7 @@ class BarbarianManager { } } -class Encampment() { +class Encampment() : IsPartOfGameInfoSerialization { val position = Vector2() var countdown = 0 var spawnedUnits = -1 diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index a4ac3a3cde..a3b631cc13 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -2,31 +2,83 @@ package com.unciv.logic import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.UncivGame.Version import com.unciv.logic.BackwardCompatibility.convertFortify import com.unciv.logic.BackwardCompatibility.convertOldGameSpeed -import com.unciv.utils.debug import com.unciv.logic.BackwardCompatibility.guaranteeUnitPromotions import com.unciv.logic.BackwardCompatibility.migrateBarbarianCamps import com.unciv.logic.BackwardCompatibility.migrateSeenImprovements import com.unciv.logic.BackwardCompatibility.removeMissingModReferences import com.unciv.logic.BackwardCompatibility.updateGreatGeneralUniques +import com.unciv.logic.GameInfo.Companion.CURRENT_COMPATIBILITY_NUMBER +import com.unciv.logic.GameInfo.Companion.FIRST_WITHOUT import com.unciv.logic.automation.NextTurnAutomation -import com.unciv.logic.civilization.* import com.unciv.logic.city.CityInfo +import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.CivilizationInfoPreview +import com.unciv.logic.civilization.LocationAction +import com.unciv.logic.civilization.NotificationIcon +import com.unciv.logic.civilization.PlayerType +import com.unciv.logic.civilization.TechManager import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap import com.unciv.models.Religion import com.unciv.models.metadata.GameParameters -import com.unciv.models.ruleset.* +import com.unciv.models.ruleset.Difficulty +import com.unciv.models.ruleset.ModOptionsConstants +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.ruleset.Speed import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicTrackChooserFlags +import com.unciv.utils.debug import java.util.* -import kotlin.NoSuchElementException -class GameInfo { +/** + * A class that implements this interface is part of [GameInfo] serialization, i.e. save files. + * + * Take care with `lateinit` and `by lazy` fields - both are **never** serialized. + * + * When you change the structure of any class with this interface in a way which makes it impossible + * to load the new saves from an older game version, increment [CURRENT_COMPATIBILITY_NUMBER]! And don't forget + * to add backwards compatibility for the previous format. + */ +interface IsPartOfGameInfoSerialization + +interface HasGameInfoSerializationVersion { + val version: CompatibilityVersion +} + +data class CompatibilityVersion( + /** Contains the current serialization version of [GameInfo], i.e. when this number is not equal to [CURRENT_COMPATIBILITY_NUMBER], it means + * this instance has been loaded from a save file json that was made with another version of the game. */ + val number: Int, + val createdWith: Version +) : IsPartOfGameInfoSerialization { + @Suppress("unused") // used by json serialization + constructor() : this(-1, Version()) + + operator fun compareTo(other: CompatibilityVersion) = number.compareTo(other.number) + +} + +class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion { + companion object { + /** The current compatibility version of [GameInfo]. This number is incremented whenever changes are made to the save file structure that guarantee that + * previous versions of the game will not be able to load or play a game normally. */ + const val CURRENT_COMPATIBILITY_NUMBER = 1 + + val CURRENT_COMPATIBILITY_VERSION = CompatibilityVersion(CURRENT_COMPATIBILITY_NUMBER, UncivGame.VERSION) + + /** This is the version just before this field was introduced, i.e. all saves without any version will be from this version */ + val FIRST_WITHOUT = CompatibilityVersion(1, Version("4.1.14", 731)) + } //region Fields - Serialized + + override var version = FIRST_WITHOUT + var civilizations = mutableListOf() var barbarians = BarbarianManager() var religions: HashMap = hashMapOf() @@ -117,8 +169,10 @@ class GameInfo { if (civToCheck.playerId == userId) return civToCheck civIndex++ } + } else { + // you aren't anyone. How did you even get this game? Can you spectate? + return getSpectator(userId) } - else return getSpectator(userId)// you aren't anyone. How did you even get this game? Can you spectate? } /** Get a civ by name @@ -135,24 +189,27 @@ class GameInfo { fun getAliveMajorCivs() = civilizations.filter { it.isAlive() && it.isMajorCiv() } /** Returns the first spectator for a [playerId] or creates one if none found */ - fun getSpectator(playerId: String) = - civilizations.firstOrNull { + fun getSpectator(playerId: String): CivilizationInfo { + val gameSpectatorCiv = civilizations.firstOrNull { it.isSpectator() && it.playerId == playerId - } ?: - CivilizationInfo(Constants.spectator).also { - it.playerType = PlayerType.Human - it.playerId = playerId - civilizations.add(it) - it.gameInfo = this - it.setNationTransient() - it.setTransients() } + return gameSpectatorCiv ?: createTemporarySpectatorCiv(playerId) + + } + + private fun createTemporarySpectatorCiv(playerId: String) = CivilizationInfo(Constants.spectator).also { + it.playerType = PlayerType.Human + it.playerId = playerId + civilizations.add(it) + it.gameInfo = this + it.setNationTransient() + it.setTransients() + } fun isReligionEnabled(): Boolean { - if (ruleSet.eras[gameParameters.startingEra]!!.hasUnique(UniqueType.DisablesReligion) - || ruleSet.modOptions.uniques.contains(ModOptionsConstants.disableReligion) - ) return false - return gameParameters.religionEnabled + val religionDisabledByRuleset = (ruleSet.eras[gameParameters.startingEra]!!.hasUnique(UniqueType.DisablesReligion) + || ruleSet.modOptions.uniques.contains(ModOptionsConstants.disableReligion)) + return !religionDisabledByRuleset && gameParameters.religionEnabled } private fun getEquivalentTurn(): Int { @@ -237,8 +294,10 @@ class GameInfo { if (currentPlayerCiv.isSpectator()) currentPlayerCiv.popupAlerts.clear() // no popups for spectators if (turns % 10 == 0) //todo measuring actual play time might be nicer - UncivGame.Current.musicController.chooseTrack(currentPlayerCiv.civName, - MusicMood.peaceOrWar(currentPlayerCiv.isAtWar()), MusicTrackChooserFlags.setNextTurn) + UncivGame.Current.musicController.chooseTrack( + currentPlayerCiv.civName, + MusicMood.peaceOrWar(currentPlayerCiv.isAtWar()), MusicTrackChooserFlags.setNextTurn + ) // Start our turn immediately before the player can make decisions - affects // whether our units can commit automated actions and then be attacked immediately etc. @@ -248,28 +307,32 @@ class GameInfo { private fun notifyOfCloseEnemyUnits(thisPlayer: CivilizationInfo) { val viewableInvisibleTiles = thisPlayer.viewableInvisibleUnitsTiles.map { it.position } val enemyUnitsCloseToTerritory = thisPlayer.viewableTiles - .filter { - it.militaryUnit != null && it.militaryUnit!!.civInfo != thisPlayer - && thisPlayer.isAtWarWith(it.militaryUnit!!.civInfo) - && (it.getOwner() == thisPlayer || it.neighbors.any { neighbor -> neighbor.getOwner() == thisPlayer } - && (!it.militaryUnit!!.isInvisible(thisPlayer) || viewableInvisibleTiles.contains(it.position))) - } + .filter { + it.militaryUnit != null && it.militaryUnit!!.civInfo != thisPlayer + && thisPlayer.isAtWarWith(it.militaryUnit!!.civInfo) + && (it.getOwner() == thisPlayer || it.neighbors.any { neighbor -> neighbor.getOwner() == thisPlayer } + && (!it.militaryUnit!!.isInvisible(thisPlayer) || viewableInvisibleTiles.contains(it.position))) + } // enemy units IN our territory - addEnemyUnitNotification(thisPlayer, - enemyUnitsCloseToTerritory.filter { it.getOwner() == thisPlayer }, - "in" + addEnemyUnitNotification( + thisPlayer, + enemyUnitsCloseToTerritory.filter { it.getOwner() == thisPlayer }, + "in" ) // enemy units NEAR our territory - addEnemyUnitNotification(thisPlayer, - enemyUnitsCloseToTerritory.filter { it.getOwner() != thisPlayer }, - "near" + addEnemyUnitNotification( + thisPlayer, + enemyUnitsCloseToTerritory.filter { it.getOwner() != thisPlayer }, + "near" ) - addBombardNotification(thisPlayer, - thisPlayer.cities.filter { city -> city.canBombard() && - enemyUnitsCloseToTerritory.any { tile -> tile.aerialDistanceTo(city.getCenterTile()) <= city.range } - } + addBombardNotification( + thisPlayer, + thisPlayer.cities.filter { city -> + city.canBombard() && + enemyUnitsCloseToTerritory.any { tile -> tile.aerialDistanceTo(city.getCenterTile()) <= city.range } + } ) } @@ -323,40 +386,42 @@ class GameInfo { data class CityTileAndDistance(val city: CityInfo, val tile: TileInfo, val distance: Int) val exploredRevealTiles: Sequence = - if (ruleSet.tileResources[resourceName]!!.hasUnique(UniqueType.CityStateOnlyResource)) { - // Look for matching mercantile CS centers - getAliveCityStates() - .asSequence() - .filter { it.cityStateResource == resourceName } - .map { it.getCapital()!!.getCenterTile() } - } else { - tileMap.values - .asSequence() - .filter { it.resource == resourceName } - } + if (ruleSet.tileResources[resourceName]!!.hasUnique(UniqueType.CityStateOnlyResource)) { + // Look for matching mercantile CS centers + getAliveCityStates() + .asSequence() + .filter { it.cityStateResource == resourceName } + .map { it.getCapital()!!.getCenterTile() } + } else { + tileMap.values + .asSequence() + .filter { it.resource == resourceName } + } val exploredRevealInfo = exploredRevealTiles .filter { it.position in civInfo.exploredTiles } - .flatMap { tile -> civInfo.cities.asSequence() - .map { - // build a full cross join all revealed tiles * civ's cities (should rarely surpass a few hundred) - // cache distance for each pair as sort will call it ~ 2n log n times - // should still be cheaper than looking up 'the' closest city per reveal tile before sorting - city -> CityTileAndDistance(city, tile, tile.aerialDistanceTo(city.getCenterTile())) - } + .flatMap { tile -> + civInfo.cities.asSequence() + .map { + // build a full cross join all revealed tiles * civ's cities (should rarely surpass a few hundred) + // cache distance for each pair as sort will call it ~ 2n log n times + // should still be cheaper than looking up 'the' closest city per reveal tile before sorting + city -> + CityTileAndDistance(city, tile, tile.aerialDistanceTo(city.getCenterTile())) + } } .filter { (maxDistance == 0 || it.distance <= maxDistance) && (showForeign || it.tile.getOwner() == null || it.tile.getOwner() == civInfo) } - .sortedWith ( compareBy { it.distance } ) + .sortedWith(compareBy { it.distance }) .distinctBy { it.tile } val chosenCity = exploredRevealInfo.firstOrNull()?.city ?: return false val positions = exploredRevealInfo // re-sort to a more pleasant display order - .sortedWith(compareBy{ it.tile.aerialDistanceTo(chosenCity.getCenterTile()) }) + .sortedWith(compareBy { it.tile.aerialDistanceTo(chosenCity.getCenterTile()) }) .map { it.tile.position } val positionsCount = positions.count() - val text = if (positionsCount == 1) + val text = if (positionsCount == 1) "[$resourceName] revealed near [${chosenCity.name}]" else "[$positionsCount] sources of [$resourceName] revealed, e.g. near [${chosenCity.name}]" @@ -375,7 +440,7 @@ class GameInfo { tileMap.gameInfo = this // [TEMPORARY] Convert old saves to newer ones by moving base rulesets from the mod list to the base ruleset field - val baseRulesetInMods = gameParameters.mods.firstOrNull { RulesetCache[it]?.modOptions?.isBaseRuleset==true } + val baseRulesetInMods = gameParameters.mods.firstOrNull { RulesetCache[it]?.modOptions?.isBaseRuleset == true } if (baseRulesetInMods != null) { gameParameters.baseRuleset = baseRulesetInMods gameParameters.mods = LinkedHashSet(gameParameters.mods.filter { it != baseRulesetInMods }) @@ -463,7 +528,7 @@ class GameInfo { spaceResources.clear() spaceResources.addAll(ruleSet.buildings.values.filter { it.hasUnique(UniqueType.SpaceshipPart) } - .flatMap { it.getResourceRequirements().keys } ) + .flatMap { it.getResourceRequirements().keys }) spaceResources.addAll(ruleSet.victories.values.flatMap { it.requiredSpaceshipParts }) barbarians.setTransients(this) @@ -505,3 +570,8 @@ class GameInfoPreview() { fun getCivilization(civName: String) = civilizations.first { it.civName == civName } } + +/** Class to use when parsing jsons if you only want the serialization [version]. */ +class GameInfoSerializationVersion : HasGameInfoSerializationVersion { + override var version = FIRST_WITHOUT +} diff --git a/core/src/com/unciv/logic/UncivExceptions.kt b/core/src/com/unciv/logic/UncivExceptions.kt index f6638ae909..cb4883e8c1 100644 --- a/core/src/com/unciv/logic/UncivExceptions.kt +++ b/core/src/com/unciv/logic/UncivExceptions.kt @@ -11,8 +11,8 @@ import com.unciv.models.translations.tr */ open class UncivShowableException( errorText: String, - override val cause: Throwable? = null -) : Exception(errorText) { + cause: Throwable? = null +) : Exception(errorText, cause) { // override because we _definitely_ have a non-null message from [errorText] override val message: String get() = super.message!! diff --git a/core/src/com/unciv/logic/UncivFiles.kt b/core/src/com/unciv/logic/UncivFiles.kt index 7190467d48..50a479886d 100644 --- a/core/src/com/unciv/logic/UncivFiles.kt +++ b/core/src/com/unciv/logic/UncivFiles.kt @@ -11,6 +11,7 @@ import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.doMigrations import com.unciv.models.metadata.isMigrationNecessary import com.unciv.ui.saves.Gzip +import com.unciv.ui.utils.extensions.toNiceString import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.Log import com.unciv.utils.debug @@ -242,6 +243,8 @@ class UncivFiles( /** * Calls the [loadCompleteCallback] on the main thread with the [GameInfo] on success or the [Exception] on error or null in both on cancel. + * + * The exception may be [IncompatibleGameInfoVersionException] if the [gameData] was created by a version of this game that is incompatible with the current one. */ fun loadGameFromCustomLocation(loadCompletionCallback: (CustomLoadResult) -> Unit) { customFileLocationHelper!!.loadGame { result -> @@ -316,10 +319,27 @@ class UncivFiles( else GameSettings().apply { isFreshlyCreated = true } } + /** @throws IncompatibleGameInfoVersionException if the [gameData] was created by a version of this game that is incompatible with the current one. */ fun gameInfoFromString(gameData: String): GameInfo { - return gameInfoFromStringWithoutTransients(gameData).apply { - setTransients() + val unzippedJson = try { + Gzip.unzip(gameData) + } catch (ex: Exception) { + gameData } + val gameInfo = try { + json().fromJson(GameInfo::class.java, unzippedJson) + } catch (ex: Exception) { + Log.error("Exception while deserializing GameInfo JSON", ex) + val onlyVersion = json().fromJson(GameInfoSerializationVersion::class.java, unzippedJson) + throw IncompatibleGameInfoVersionException(onlyVersion.version, ex) + } + if (gameInfo.version > GameInfo.CURRENT_COMPATIBILITY_VERSION) { + // this means there wasn't an immediate error while serializing, but this version will cause other errors later down the line + throw IncompatibleGameInfoVersionException(gameInfo.version) + } + gameInfo.version = GameInfo.CURRENT_COMPATIBILITY_VERSION + gameInfo.setTransients() + return gameInfo } /** @@ -330,22 +350,6 @@ class UncivFiles( return json().fromJson(GameInfoPreview::class.java, Gzip.unzip(gameData)) } - /** - * WARNING! transitive GameInfo data not initialized - * The returned GameInfo can not be used for most circumstances because its not initialized! - * It is therefore stateless and save to call for Multiplayer Turn Notifier, unlike gameInfoFromString(). - * - * @throws SerializationException - */ - private fun gameInfoFromStringWithoutTransients(gameData: String): GameInfo { - val unzippedJson = try { - Gzip.unzip(gameData) - } catch (ex: Exception) { - gameData - } - return json().fromJson(GameInfo::class.java, unzippedJson) - } - /** Returns gzipped serialization of [game], optionally gzipped ([forceZip] overrides [saveZipped]) */ fun gameInfoToString(game: GameInfo, forceZip: Boolean? = null): String { val plainJson = json().toJson(game) @@ -420,3 +424,12 @@ class UncivFiles( // endregion } + +class IncompatibleGameInfoVersionException( + override val version: CompatibilityVersion, + cause: Throwable? = null +) : UncivShowableException( + "The save was created with an incompatible version of Unciv: [${version.createdWith.toNiceString()}]. " + + "Please update Unciv to at least [${version.createdWith.toNiceString()}] and try again.", + cause +), HasGameInfoSerializationVersion diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index 9f88c92ced..808e92d09d 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -1,6 +1,7 @@ package com.unciv.logic.city import com.unciv.UncivGame +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.automation.Automation import com.unciv.logic.automation.ConstructionAutomation import com.unciv.logic.civilization.AlertType @@ -36,7 +37,7 @@ import kotlin.math.roundToInt * @property currentConstructionIsUserSet a flag indicating if the [currentConstructionFromQueue] has been set by the user or by the AI * @property constructionQueue a list of constructions names enqueued */ -class CityConstructions { +class CityConstructions : IsPartOfGameInfoSerialization { //region Non-Serialized Properties @Transient lateinit var cityInfo: CityInfo diff --git a/core/src/com/unciv/logic/city/CityExpansionManager.kt b/core/src/com/unciv/logic/city/CityExpansionManager.kt index aadab64af7..03e0f8b676 100644 --- a/core/src/com/unciv/logic/city/CityExpansionManager.kt +++ b/core/src/com/unciv/logic/city/CityExpansionManager.kt @@ -1,6 +1,7 @@ package com.unciv.logic.city import com.badlogic.gdx.math.Vector2 +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.automation.Automation import com.unciv.logic.civilization.LocationAction import com.unciv.logic.civilization.NotificationIcon @@ -14,7 +15,7 @@ import kotlin.math.max import kotlin.math.pow import kotlin.math.roundToInt -class CityExpansionManager { +class CityExpansionManager : IsPartOfGameInfoSerialization { @Transient lateinit var cityInfo: CityInfo var cultureStored: Int = 0 diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index df7a8ab558..42c3cdc81c 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -1,6 +1,7 @@ package com.unciv.logic.city import com.badlogic.gdx.math.Vector2 +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.battle.CityCombatant import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.NotificationIcon @@ -36,7 +37,7 @@ enum class CityFlags { } // if tableEnabled == true, then Stat != null -enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Stat? = null) { +enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Stat? = null) : IsPartOfGameInfoSerialization { NoFocus("Default Focus", true, null) { override fun getStatMultiplier(stat: Stat) = 1f // actually redundant, but that's two steps to see }, @@ -78,7 +79,7 @@ enum class CityFocus(val label: String, val tableEnabled: Boolean, val stat: Sta } -class CityInfo { +class CityInfo : IsPartOfGameInfoSerialization { @Suppress("JoinDeclarationAndAssignment") @Transient lateinit var civInfo: CivilizationInfo diff --git a/core/src/com/unciv/logic/city/CityReligion.kt b/core/src/com/unciv/logic/city/CityReligion.kt index 4728b40778..d084c0cee3 100644 --- a/core/src/com/unciv/logic/city/CityReligion.kt +++ b/core/src/com/unciv/logic/city/CityReligion.kt @@ -1,6 +1,7 @@ package com.unciv.logic.city import com.unciv.Constants +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.NotificationIcon import com.unciv.models.Counter import com.unciv.models.Religion @@ -8,7 +9,7 @@ import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.utils.extensions.toPercent -class CityInfoReligionManager { +class CityInfoReligionManager : IsPartOfGameInfoSerialization { @Transient lateinit var cityInfo: CityInfo diff --git a/core/src/com/unciv/logic/city/PopulationManager.kt b/core/src/com/unciv/logic/city/PopulationManager.kt index adeca0f174..f707ff6131 100644 --- a/core/src/com/unciv/logic/city/PopulationManager.kt +++ b/core/src/com/unciv/logic/city/PopulationManager.kt @@ -1,5 +1,6 @@ package com.unciv.logic.city +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.automation.Automation import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.map.TileInfo @@ -12,7 +13,7 @@ import com.unciv.ui.utils.extensions.withoutItem import kotlin.math.floor import kotlin.math.pow -class PopulationManager { +class PopulationManager : IsPartOfGameInfoSerialization { @Transient lateinit var cityInfo: CityInfo diff --git a/core/src/com/unciv/logic/civilization/CivConstructions.kt b/core/src/com/unciv/logic/civilization/CivConstructions.kt index ef5ca3a9da..7d27e31fcd 100644 --- a/core/src/com/unciv/logic/civilization/CivConstructions.kt +++ b/core/src/com/unciv/logic/civilization/CivConstructions.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.city.INonPerpetualConstruction import com.unciv.models.Counter import com.unciv.models.ruleset.Building @@ -9,7 +10,7 @@ import com.unciv.models.stats.Stat import java.util.* import kotlin.collections.HashMap -class CivConstructions { +class CivConstructions : IsPartOfGameInfoSerialization { @Transient lateinit var civInfo: CivilizationInfo @@ -122,12 +123,12 @@ class CivConstructions { addFreeBuilding(city.id, building) } } - + fun countConstructedObjects(objectToCount: INonPerpetualConstruction): Int { val amountInSpaceShip = civInfo.victoryManager.currentsSpaceshipParts[objectToCount.name] ?: 0 - + return amountInSpaceShip + when (objectToCount) { - is Building -> civInfo.cities.count { + is Building -> civInfo.cities.count { it.cityConstructions.containsBuildingOrEquivalent(objectToCount.name) || it.cityConstructions.isBeingConstructedOrEnqueued(objectToCount.name) } diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 220ccd757e..f8a889edcb 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -5,6 +5,7 @@ import com.unciv.Constants import com.unciv.UncivGame import com.unciv.json.HashMapVector2 import com.unciv.logic.GameInfo +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.UncivShowableException import com.unciv.logic.automation.NextTurnAutomation import com.unciv.logic.automation.WorkerAutomation @@ -47,7 +48,7 @@ import kotlin.math.min import kotlin.math.roundToInt import kotlin.math.sqrt -enum class Proximity { +enum class Proximity : IsPartOfGameInfoSerialization { None, // ie no cities Neighbors, Close, @@ -55,7 +56,7 @@ enum class Proximity { Distant } -class CivilizationInfo { +class CivilizationInfo : IsPartOfGameInfoSerialization { @Transient private var workerAutomationCache: WorkerAutomation? = null @@ -216,7 +217,7 @@ class CivilizationInfo { * @property target Position of the tile targeted by the attack. * @see [MapUnit.UnitMovementMemory], [attacksSinceTurnStart] */ - class HistoricalAttackMemory() { + class HistoricalAttackMemory() : IsPartOfGameInfoSerialization { constructor(attackingUnit: String?, source: Vector2, target: Vector2): this() { this.attackingUnit = attackingUnit this.source = source diff --git a/core/src/com/unciv/logic/civilization/GoldenAgeManager.kt b/core/src/com/unciv/logic/civilization/GoldenAgeManager.kt index c8d1d5c83e..651b91caa8 100644 --- a/core/src/com/unciv/logic/civilization/GoldenAgeManager.kt +++ b/core/src/com/unciv/logic/civilization/GoldenAgeManager.kt @@ -1,9 +1,10 @@ package com.unciv.logic.civilization +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.utils.extensions.toPercent -class GoldenAgeManager { +class GoldenAgeManager : IsPartOfGameInfoSerialization { @Transient lateinit var civInfo: CivilizationInfo diff --git a/core/src/com/unciv/logic/civilization/GreatPersonManager.kt b/core/src/com/unciv/logic/civilization/GreatPersonManager.kt index f537f18950..ad5edb2d44 100644 --- a/core/src/com/unciv/logic/civilization/GreatPersonManager.kt +++ b/core/src/com/unciv/logic/civilization/GreatPersonManager.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.Counter import java.util.HashSet @@ -7,7 +8,7 @@ import java.util.HashSet // todo: Free GP from policies and wonders should increase threshold according to the wiki // todo: GP from Maya long count should increase threshold as well - implement together -class GreatPersonManager { +class GreatPersonManager : IsPartOfGameInfoSerialization { var pointsForNextGreatPerson = 100 var pointsForNextGreatGeneral = 200 @@ -53,4 +54,4 @@ class GreatPersonManager { } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/civilization/Notification.kt b/core/src/com/unciv/logic/civilization/Notification.kt index 0954647c32..51c2962d8e 100644 --- a/core/src/com/unciv/logic/civilization/Notification.kt +++ b/core/src/com/unciv/logic/civilization/Notification.kt @@ -3,6 +3,7 @@ package com.unciv.logic.civilization import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.Ruleset import com.unciv.ui.cityscreen.CityScreen import com.unciv.ui.images.ImageGetter @@ -40,7 +41,7 @@ object NotificationIcon { * [action] is 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. */ -open class Notification() { +open class Notification() : IsPartOfGameInfoSerialization { var text: String = "" @@ -71,7 +72,7 @@ open class Notification() { } /** defines what to do if the user clicks on a notification */ -interface NotificationAction { +interface NotificationAction : IsPartOfGameInfoSerialization { fun execute(worldScreen: WorldScreen) } @@ -80,7 +81,7 @@ interface NotificationAction { * Constructors accept any kind of [Vector2] collection, including [Iterable], [Sequence], `vararg`. * `varargs` allows nulls which are ignored, a resulting empty list is allowed and equivalent to no [NotificationAction]. */ -data class LocationAction(var locations: ArrayList = ArrayList()) : NotificationAction { +data class LocationAction(var locations: ArrayList = ArrayList()) : NotificationAction, IsPartOfGameInfoSerialization { constructor(locations: Iterable) : this(locations.toCollection(ArrayList())) constructor(locations: Sequence) : this(locations.toCollection(ArrayList())) constructor(vararg locations: Vector2?) : this(locations.asSequence().filterNotNull()) @@ -95,7 +96,7 @@ data class LocationAction(var locations: ArrayList = ArrayList()) : Not } /** show tech screen */ -class TechAction(val techName: String = "") : NotificationAction { +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)) @@ -103,7 +104,7 @@ class TechAction(val techName: String = "") : NotificationAction { } /** enter city */ -data class CityAction(val city: Vector2 = Vector2.Zero): NotificationAction { +data class CityAction(val city: Vector2 = Vector2.Zero): NotificationAction, IsPartOfGameInfoSerialization { override fun execute(worldScreen: WorldScreen) { worldScreen.mapHolder.tileMap[city].getCity()?.let { if (it.civInfo == worldScreen.viewingCiv) @@ -113,7 +114,7 @@ data class CityAction(val city: Vector2 = Vector2.Zero): NotificationAction { } /** enter diplomacy screen */ -data class DiplomacyAction(val otherCivName: String = ""): NotificationAction { +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)) @@ -121,7 +122,7 @@ data class DiplomacyAction(val otherCivName: String = ""): NotificationAction { } /** enter Maya Long Count popup */ -class MayaLongCountAction : NotificationAction { +class MayaLongCountAction : NotificationAction, IsPartOfGameInfoSerialization { override fun execute(worldScreen: WorldScreen) { MayaCalendar.openPopup(worldScreen, worldScreen.selectedCiv, worldScreen.gameInfo.getYear()) } diff --git a/core/src/com/unciv/logic/civilization/PlayerType.kt b/core/src/com/unciv/logic/civilization/PlayerType.kt index 5d5a4790a9..7a034fb896 100644 --- a/core/src/com/unciv/logic/civilization/PlayerType.kt +++ b/core/src/com/unciv/logic/civilization/PlayerType.kt @@ -1,6 +1,8 @@ package com.unciv.logic.civilization -enum class PlayerType{ +import com.unciv.logic.IsPartOfGameInfoSerialization + +enum class PlayerType : IsPartOfGameInfoSerialization { AI, Human -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/civilization/PolicyManager.kt b/core/src/com/unciv/logic/civilization/PolicyManager.kt index 3cb39579d4..c0924c9eb3 100644 --- a/core/src/com/unciv/logic/civilization/PolicyManager.kt +++ b/core/src/com/unciv/logic/civilization/PolicyManager.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.map.MapSize import com.unciv.models.ruleset.Policy import com.unciv.models.ruleset.Policy.PolicyBranchType @@ -14,7 +15,7 @@ import kotlin.math.pow import kotlin.math.roundToInt -class PolicyManager { +class PolicyManager : IsPartOfGameInfoSerialization { @Transient lateinit var civInfo: CivilizationInfo diff --git a/core/src/com/unciv/logic/civilization/PopupAlert.kt b/core/src/com/unciv/logic/civilization/PopupAlert.kt index 17a949267f..7ef57694cd 100644 --- a/core/src/com/unciv/logic/civilization/PopupAlert.kt +++ b/core/src/com/unciv/logic/civilization/PopupAlert.kt @@ -1,6 +1,8 @@ package com.unciv.logic.civilization -enum class AlertType { +import com.unciv.logic.IsPartOfGameInfoSerialization + +enum class AlertType : IsPartOfGameInfoSerialization { Defeated, WonderBuilt, TechResearched, @@ -20,7 +22,7 @@ enum class AlertType { RecapturedCivilian, } -class PopupAlert { +class PopupAlert : IsPartOfGameInfoSerialization { lateinit var type: AlertType lateinit var value: String @@ -30,4 +32,4 @@ class PopupAlert { } constructor() // for json serialization -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/civilization/QuestManager.kt b/core/src/com/unciv/logic/civilization/QuestManager.kt index ac3203190f..17d9edcfaf 100644 --- a/core/src/com/unciv/logic/civilization/QuestManager.kt +++ b/core/src/com/unciv/logic/civilization/QuestManager.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.GameInfo +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.map.TileInfo @@ -23,7 +24,7 @@ import kotlin.math.max import kotlin.random.Random @Suppress("NON_EXHAUSTIVE_WHEN") // Many when uses in here are much clearer this way -class QuestManager { +class QuestManager : IsPartOfGameInfoSerialization { companion object { const val UNSET = -1 @@ -894,7 +895,7 @@ class AssignedQuest(val questName: String = "", val assignee: String = "", val assignedOnTurn: Int = 0, val data1: String = "", - val data2: String = "") { + val data2: String = "") : IsPartOfGameInfoSerialization { @Transient lateinit var gameInfo: GameInfo diff --git a/core/src/com/unciv/logic/civilization/ReligionManager.kt b/core/src/com/unciv/logic/civilization/ReligionManager.kt index c947c2d6a7..0992bcf1e2 100644 --- a/core/src/com/unciv/logic/civilization/ReligionManager.kt +++ b/core/src/com/unciv/logic/civilization/ReligionManager.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.map.MapUnit import com.unciv.models.Counter import com.unciv.models.Religion @@ -10,7 +11,7 @@ import com.unciv.ui.utils.extensions.toPercent import java.lang.Integer.min import kotlin.random.Random -class ReligionManager { +class ReligionManager : IsPartOfGameInfoSerialization { @Transient lateinit var civInfo: CivilizationInfo @@ -359,7 +360,7 @@ class ReligionManager { } } -enum class ReligionState { +enum class ReligionState : IsPartOfGameInfoSerialization { None, Pantheon, FoundingReligion, // Great prophet used, but religion has not yet been founded diff --git a/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt b/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt index 846ea82910..1b9ce92d95 100644 --- a/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt +++ b/core/src/com/unciv/logic/civilization/RuinsManager/RuinsManager.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization.RuinsManager +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.ReligionState import com.unciv.logic.map.MapUnit @@ -8,38 +9,38 @@ import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType import kotlin.random.Random -class RuinsManager { +class RuinsManager : IsPartOfGameInfoSerialization { var lastChosenRewards: MutableList = mutableListOf("", "") private fun rememberReward(reward: String) { lastChosenRewards[0] = lastChosenRewards[1] lastChosenRewards[1] = reward } - + @Transient lateinit var civInfo: CivilizationInfo @Transient - lateinit var validRewards: List - + lateinit var validRewards: List + fun clone(): RuinsManager { val toReturn = RuinsManager() toReturn.lastChosenRewards = lastChosenRewards return toReturn } - + fun setTransients(civInfo: CivilizationInfo) { this.civInfo = civInfo validRewards = civInfo.gameInfo.ruleSet.ruinRewards.values.toList() } - + fun selectNextRuinsReward(triggeringUnit: MapUnit) { val tileBasedRandom = Random(triggeringUnit.getTile().position.toString().hashCode()) val availableRewards = validRewards.filter { it.name !in lastChosenRewards } - + // This might be a dirty way to do this, but it works. // For each possible reward, this creates a list with reward.weight amount of copies of this reward // These lists are then combined into a single list, and the result is shuffled. val possibleRewards = availableRewards.flatMap { reward -> List(reward.weight) { reward } }.shuffled(tileBasedRandom) - + for (possibleReward in possibleRewards) { if (civInfo.gameInfo.difficulty in possibleReward.excludedDifficulties) continue if (possibleReward.hasUnique(UniqueType.HiddenWithoutReligion) && !civInfo.gameInfo.isReligionEnabled()) continue @@ -52,11 +53,11 @@ class RuinsManager { continue if (possibleReward.getMatchingUniques(UniqueType.AvailableAfterCertainTurns).any { it.params[0].toInt() < civInfo.gameInfo.turns }) continue - + var atLeastOneUniqueHadEffect = false for (unique in possibleReward.uniqueObjects) { - atLeastOneUniqueHadEffect = - atLeastOneUniqueHadEffect + atLeastOneUniqueHadEffect = + atLeastOneUniqueHadEffect || UniqueTriggerActivation.triggerCivwideUnique(unique, civInfo, tile = triggeringUnit.getTile(), notification = possibleReward.notification) || UniqueTriggerActivation.triggerUnitwideUnique(unique, triggeringUnit, notification = possibleReward.notification) } diff --git a/core/src/com/unciv/logic/civilization/TechManager.kt b/core/src/com/unciv/logic/civilization/TechManager.kt index 11e79b0e20..9bbf615f26 100644 --- a/core/src/com/unciv/logic/civilization/TechManager.kt +++ b/core/src/com/unciv/logic/civilization/TechManager.kt @@ -1,5 +1,6 @@ package com.unciv.logic.civilization +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.city.CityInfo import com.unciv.logic.map.MapSize import com.unciv.logic.map.RoadStatus @@ -17,7 +18,7 @@ import kotlin.math.ceil import kotlin.math.max import kotlin.math.min -class TechManager { +class TechManager : IsPartOfGameInfoSerialization { @Transient var era: Era = Era() diff --git a/core/src/com/unciv/logic/civilization/VictoryManager.kt b/core/src/com/unciv/logic/civilization/VictoryManager.kt index f6dec75268..bcb7ae6d73 100644 --- a/core/src/com/unciv/logic/civilization/VictoryManager.kt +++ b/core/src/com/unciv/logic/civilization/VictoryManager.kt @@ -1,11 +1,12 @@ package com.unciv.logic.civilization import com.unciv.Constants +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.Counter import com.unciv.models.ruleset.Milestone import com.unciv.models.ruleset.unique.UniqueType -class VictoryManager { +class VictoryManager : IsPartOfGameInfoSerialization { @Transient lateinit var civInfo: CivilizationInfo diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index 93f4d756c0..5aeba811a3 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -2,6 +2,7 @@ package com.unciv.logic.civilization.diplomacy import com.badlogic.gdx.graphics.Color import com.unciv.Constants +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.CityStatePersonality import com.unciv.logic.civilization.CityStateType @@ -91,7 +92,7 @@ enum class DiplomaticModifiers { ReturnedCapturedUnits, } -class DiplomacyManager() { +class DiplomacyManager() : IsPartOfGameInfoSerialization { companion object { /** The value city-state influence can't go below */ diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomaticStatus.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomaticStatus.kt index 2adc79db31..b4f78739fc 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomaticStatus.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomaticStatus.kt @@ -1,7 +1,9 @@ package com.unciv.logic.civilization.diplomacy -enum class DiplomaticStatus{ +import com.unciv.logic.IsPartOfGameInfoSerialization + +enum class DiplomaticStatus : IsPartOfGameInfoSerialization { Peace, Protector, //city state's diplomacy for major civ can be marked as Protector, not vice versa. War -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index dc06ae3a21..eb378baf30 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -3,6 +3,7 @@ package com.unciv.logic.map import com.unciv.logic.HexMath.getEquivalentHexagonalRadius import com.unciv.logic.HexMath.getEquivalentRectangularSize import com.unciv.logic.HexMath.getNumberOfTilesInHexagon +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.metadata.BaseRuleset @@ -29,7 +30,7 @@ enum class MapSize(val radius: Int, val width: Int, val height: Int) { } } -class MapSizeNew { +class MapSizeNew : IsPartOfGameInfoSerialization { var radius = 0 var width = 0 var height = 0 @@ -121,12 +122,12 @@ class MapSizeNew { override fun toString() = if (name == MapSize.custom) "${width}x${height}" else name } -object MapShape { +object MapShape : IsPartOfGameInfoSerialization { const val hexagonal = "Hexagonal" const val rectangular = "Rectangular" } -object MapType { +object MapType : IsPartOfGameInfoSerialization { const val pangaea = "Pangaea" const val continents = "Continents" const val fourCorners = "Four Corners" @@ -152,7 +153,7 @@ object MapResources { const val legendaryStart = "Legendary Start" } -class MapParameters { +class MapParameters : IsPartOfGameInfoSerialization { var name = "" var type = MapType.pangaea var shape = MapShape.hexagonal diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 3219c9250a..4143aaa7a2 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -3,6 +3,7 @@ package com.unciv.logic.map import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.automation.UnitAutomation import com.unciv.logic.automation.WorkerAutomation import com.unciv.logic.battle.Battle @@ -33,7 +34,7 @@ import kotlin.math.pow /** * The immutable properties and mutable game state of an individual unit present on the map */ -class MapUnit { +class MapUnit : IsPartOfGameInfoSerialization { @Transient lateinit var civInfo: CivilizationInfo @@ -189,7 +190,7 @@ class MapUnit { * @property type Category of the last change in position that brought the unit to this position. * @see [movementMemories] * */ - class UnitMovementMemory(position: Vector2, val type: UnitMovementMemoryType) { + class UnitMovementMemory(position: Vector2, val type: UnitMovementMemoryType) : IsPartOfGameInfoSerialization { @Suppress("unused") // needed because this is part of a save and gets deserialized constructor(): this(Vector2.Zero, UnitMovementMemoryType.UnitMoved) val position = Vector2(position) diff --git a/core/src/com/unciv/logic/map/RoadStatus.kt b/core/src/com/unciv/logic/map/RoadStatus.kt index cddb0493ca..474008a10a 100644 --- a/core/src/com/unciv/logic/map/RoadStatus.kt +++ b/core/src/com/unciv/logic/map/RoadStatus.kt @@ -1,11 +1,12 @@ package com.unciv.logic.map +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.Ruleset /** * You can use RoadStatus.name to identify [Road] and [Railroad] * in string-based identification, as done in [improvement]. - * + * * Note: Order is important, [ordinal] _is_ compared - please interpret as "roadLevel". */ enum class RoadStatus( @@ -13,7 +14,7 @@ enum class RoadStatus( val movement: Float = 1f, val movementImproved: Float = 1f, val removeAction: String? = null -) { +) : IsPartOfGameInfoSerialization { None, Road (1, 0.5f, 1/3f, "Remove Road"), diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 9d5b61125f..240711f051 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.HexMath +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.PlayerType @@ -27,7 +28,7 @@ import kotlin.math.abs import kotlin.math.min import kotlin.random.Random -open class TileInfo { +open class TileInfo : IsPartOfGameInfoSerialization { @Transient lateinit var tileMap: TileMap diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 689582dad0..6bf50d8329 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.math.Rectangle import com.badlogic.gdx.math.Vector2 import com.unciv.logic.GameInfo import com.unciv.logic.HexMath +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.mapgenerator.MapLandmassGenerator import com.unciv.models.metadata.Player @@ -14,11 +15,11 @@ import com.unciv.models.ruleset.unique.UniqueType import kotlin.math.abs /** An Unciv map with all properties as produced by the [map editor][com.unciv.ui.mapeditor.MapEditorScreen] - * or [MapGenerator][com.unciv.logic.map.mapgenerator.MapGenerator]; or as part of a running [game][GameInfo]. - * - * Note: Will be Serialized -> Take special care with lateinit and lazy! + * or [MapGenerator][com.unciv.logic.map.mapgenerator.MapGenerator]; or as part of a running [game][GameInfo]. + * + * Note: Will be Serialized -> Take special care with lateinit and lazy! */ -class TileMap { +class TileMap : IsPartOfGameInfoSerialization { companion object { /** Legacy way to store starting locations - now this is used only in [translateStartingLocationsFromMap] */ const val startingLocationPrefix = "StartingLocation " @@ -41,11 +42,11 @@ class TileMap { * @param position [Vector2] of the location * @param nation Name of the nation */ - private data class StartingLocation(val position: Vector2 = Vector2.Zero, val nation: String = "") + private data class StartingLocation(val position: Vector2 = Vector2.Zero, val nation: String = "") : IsPartOfGameInfoSerialization private val startingLocations = arrayListOf(StartingLocation(Vector2.Zero, legacyMarker)) //endregion - //region Fields, Transient + //region Fields, Transient /** Attention: lateinit will _stay uninitialized_ while in MapEditorScreen! */ @Transient @@ -354,7 +355,7 @@ class TileMap { val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any { bNeighbor: TileInfo -> val bNeighborHeight = bNeighbor.height - viewableTiles.contains(bNeighbor) + viewableTiles.contains(bNeighbor) && ( currentTileHeight > bNeighborHeight // a>b || cTileHeight > bNeighborHeight // c>b @@ -380,7 +381,7 @@ class TileMap { } /** Build a list of incompatibilities of a map with a ruleset for the new game loader - * + * * Is run before setTransients, so make do without startingLocationsByNation */ fun getRulesetIncompatibility(ruleset: Ruleset): HashSet { @@ -428,7 +429,7 @@ class TileMap { leftX = tileList.asSequence().map { it.position.x.toInt() }.minOrNull()!! // Initialize arrays with enough capacity to avoid re-allocations (+Arrays.copyOf). - // We have just calculated the dimensions above, so we know the final size. + // We have just calculated the dimensions above, so we know the final size. tileMatrix.ensureCapacity(rightX - leftX + 1) for (x in leftX..rightX) { val row = ArrayList(topY - bottomY + 1) @@ -545,9 +546,9 @@ class TileMap { /** Strips all units and starting locations from [TileMap] for specified [Player] * Operation in place - * + * * Currently unreachable code - * + * * @param player units of this player will be removed */ fun stripPlayer(player: Player) { @@ -560,9 +561,9 @@ class TileMap { /** Finds all units and starting location of [Player] and changes their [Nation] * Operation in place - * + * * Currently unreachable code - * + * * @param player player whose all units will be changed * @param newNation new nation to be set up */ @@ -611,7 +612,7 @@ class TileMap { setStartingLocationsTransients() } - /** Adds a starting position, maintaining the transients + /** Adds a starting position, maintaining the transients * @return true if the starting position was not already stored as per [Collection]'s add */ fun addStartingLocation(nationName: String, tile: TileInfo): Boolean { if (startingLocationsByNation[nationName]?.contains(tile) == true) return false @@ -658,7 +659,7 @@ class TileMap { * @param mode As follows: * [Assign][AssignContinentsMode.Assign] = initial assign, throw if tiles have continents. * [Reassign][AssignContinentsMode.Reassign] = clear continent data and redo for map editor. - * [Ensure][AssignContinentsMode.Ensure] = regenerate continent sizes from tile data, and if that is empty, Assign. + * [Ensure][AssignContinentsMode.Ensure] = regenerate continent sizes from tile data, and if that is empty, Assign. * @throws Exception when `mode==Assign` and any land tile already has a continent ID * @return A map of continent sizes (continent ID to tile count) */ diff --git a/core/src/com/unciv/logic/map/UnitPromotions.kt b/core/src/com/unciv/logic/map/UnitPromotions.kt index 56c5cbe678..1b77546d3d 100644 --- a/core/src/com/unciv/logic/map/UnitPromotions.kt +++ b/core/src/com/unciv/logic/map/UnitPromotions.kt @@ -1,16 +1,17 @@ package com.unciv.logic.map +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.Promotion -class UnitPromotions { +class UnitPromotions : IsPartOfGameInfoSerialization { // Having this as mandatory constructor parameter would be safer, but this class is part of a // saved game and as usual the json deserializer needs a default constructor. // Initialization occurs in setTransients() - called as part of MapUnit.setTransients, // or copied in clone() as part of the UnitAction `Upgrade`. - @Transient + @Transient private lateinit var unit: MapUnit /** Experience this unit has accumulated on top of the last promotion */ diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 64a8757c86..06ac192ce1 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -20,6 +20,7 @@ import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.mapeditor.MapGeneratorSteps +import com.unciv.ui.utils.extensions.toNiceString import com.unciv.utils.Log import com.unciv.utils.debug import kotlin.math.abs @@ -87,7 +88,7 @@ class MapGenerator(val ruleset: Ruleset) { else TileMap(mapSize.radius, ruleset, mapParameters.worldWrap) - mapParameters.createdWithVersion = UncivGame.Current.version + mapParameters.createdWithVersion = UncivGame.VERSION.toNiceString() map.mapParameters = mapParameters if (mapType == MapType.empty) { diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt index cd1871b245..c86209b5c5 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayer.kt @@ -8,6 +8,7 @@ import com.unciv.logic.GameInfoPreview import com.unciv.logic.civilization.PlayerType import com.unciv.logic.event.EventBus import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached +import com.unciv.logic.multiplayer.storage.MultiplayerFileNotFoundException import com.unciv.logic.multiplayer.storage.OnlineMultiplayerFiles import com.unciv.ui.utils.extensions.isLargerThan import com.unciv.utils.concurrency.Concurrency @@ -130,14 +131,14 @@ class OnlineMultiplayer { * @param gameName if this is null or blank, will use the gameId as the game name * @return the final name the game was added under * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time - * @throws FileNotFoundException if the file can't be found + * @throws MultiplayerFileNotFoundException if the file can't be found */ suspend fun addGame(gameId: String, gameName: String? = null) { val saveFileName = if (gameName.isNullOrBlank()) gameId else gameName var gamePreview: GameInfoPreview try { gamePreview = multiplayerFiles.tryDownloadGamePreview(gameId) - } catch (ex: FileNotFoundException) { + } catch (ex: MultiplayerFileNotFoundException) { // Game is so old that a preview could not be found on dropbox lets try the real gameInfo instead gamePreview = multiplayerFiles.tryDownloadGame(gameId).asPreview() } @@ -178,7 +179,7 @@ class OnlineMultiplayer { * Fires [MultiplayerGameUpdated] * * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time - * @throws FileNotFoundException if the file can't be found + * @throws MultiplayerFileNotFoundException if the file can't be found * @return false if it's not the user's turn and thus resigning did not happen */ suspend fun resign(game: OnlineMultiplayerGame): Boolean { @@ -213,7 +214,7 @@ class OnlineMultiplayer { /** * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time - * @throws FileNotFoundException if the file can't be found + * @throws MultiplayerFileNotFoundException if the file can't be found */ suspend fun loadGame(game: OnlineMultiplayerGame) { val preview = game.preview ?: throw game.error!! @@ -222,7 +223,7 @@ class OnlineMultiplayer { /** * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time - * @throws FileNotFoundException if the file can't be found + * @throws MultiplayerFileNotFoundException if the file can't be found */ suspend fun loadGame(gameId: String) = coroutineScope { val gameInfo = downloadGame(gameId) @@ -253,7 +254,7 @@ class OnlineMultiplayer { /** * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time - * @throws FileNotFoundException if the file can't be found + * @throws MultiplayerFileNotFoundException if the file can't be found */ suspend fun downloadGame(gameId: String): GameInfo { val latestGame = multiplayerFiles.tryDownloadGame(gameId) @@ -301,7 +302,7 @@ class OnlineMultiplayer { /** * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time - * @throws FileNotFoundException if the file can't be found + * @throws MultiplayerFileNotFoundException if the file can't be found */ suspend fun updateGame(gameInfo: GameInfo) { debug("Updating remote game %s", gameInfo.gameId) diff --git a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt index cb8c9503fa..19042278ff 100644 --- a/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt +++ b/core/src/com/unciv/logic/multiplayer/OnlineMultiplayerGame.kt @@ -63,7 +63,7 @@ class OnlineMultiplayerGame( * Fires: [MultiplayerGameUpdateStarted], [MultiplayerGameUpdated], [MultiplayerGameUpdateUnchanged], [MultiplayerGameUpdateFailed] * * @throws FileStorageRateLimitReached if the file storage backend can't handle any additional actions for a time - * @throws FileNotFoundException if the file can't be found + * @throws MultiplayerFileNotFoundException if the file can't be found */ suspend fun requestUpdate(forceUpdate: Boolean = false) = coroutineScope { val onUnchanged = { GameUpdateResult.UNCHANGED } diff --git a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt b/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt index 285f2a665a..7f64202438 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/DropBox.kt @@ -56,7 +56,7 @@ object DropBox: FileStorage { // Throw Exceptions based on the HTTP response from dropbox when { error.error_summary.startsWith("too_many_requests/") -> triggerRateLimit(error) - error.error_summary.startsWith("path/not_found/") -> throw FileNotFoundException() + error.error_summary.startsWith("path/not_found/") -> throw MultiplayerFileNotFoundException(ex) error.error_summary.startsWith("path/conflict/file") -> throw FileStorageConflictException() } @@ -132,7 +132,7 @@ object DropBox: FileStorage { dropboxApi("https://api.dropboxapi.com/2/files/get_metadata", "{\"path\":\"$fileName\"}", "application/json") true - } catch (ex: FileNotFoundException) { + } catch (ex: MultiplayerFileNotFoundException) { false } diff --git a/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt b/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt index e95ed7ecfe..4816fa042e 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/FileStorage.kt @@ -1,9 +1,11 @@ package com.unciv.logic.multiplayer.storage +import com.unciv.logic.UncivShowableException import java.util.* class FileStorageConflictException : Exception() -class FileStorageRateLimitReached(val limitRemainingSeconds: Int) : Exception() +class FileStorageRateLimitReached(val limitRemainingSeconds: Int) : UncivShowableException("Server limit reached! Please wait for [${limitRemainingSeconds}] seconds") +class MultiplayerFileNotFoundException(cause: Throwable?) : UncivShowableException("File could not be found on the multiplayer server", cause) interface FileMetaData { fun getLastModified(): Date? diff --git a/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt b/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt index 030d7ae03b..9eb9bcbf77 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/SimpleHttp.kt @@ -2,6 +2,7 @@ package com.unciv.logic.multiplayer.storage import com.badlogic.gdx.Net import com.unciv.UncivGame +import com.unciv.ui.utils.extensions.toNiceString import com.unciv.utils.debug import java.io.BufferedReader import java.io.DataOutputStream @@ -33,7 +34,7 @@ object SimpleHttp { connectTimeout = timeout instanceFollowRedirects = true if (UncivGame.isCurrentInitialized()) - setRequestProperty("User-Agent", "Unciv/${UncivGame.Current.version}-GNU-Terry-Pratchett") + setRequestProperty("User-Agent", "Unciv/${UncivGame.VERSION.toNiceString()}-GNU-Terry-Pratchett") else setRequestProperty("User-Agent", "Unciv/Turn-Checker-GNU-Terry-Pratchett") diff --git a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt b/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt index 8dfbea19aa..beb5216638 100644 --- a/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt +++ b/core/src/com/unciv/logic/multiplayer/storage/UncivServerFileStorage.kt @@ -2,8 +2,7 @@ package com.unciv.logic.multiplayer.storage import com.badlogic.gdx.Net import com.unciv.utils.debug -import java.io.FileNotFoundException -import java.lang.Exception +import kotlin.Exception class UncivServerFileStorage(val serverUrl: String, val timeout: Int = 30000) : FileStorage { override fun saveFileData(fileName: String, data: String, overwrite: Boolean) { @@ -23,7 +22,7 @@ class UncivServerFileStorage(val serverUrl: String, val timeout: Int = 30000) : if (!success) { debug("Error from UncivServer during load: %s", result) when (code) { - 404 -> throw FileNotFoundException(result) + 404 -> throw MultiplayerFileNotFoundException(Exception(result)) else -> throw Exception(result) } @@ -42,7 +41,7 @@ class UncivServerFileStorage(val serverUrl: String, val timeout: Int = 30000) : success, result, code -> if (!success) { when (code) { - 404 -> throw FileNotFoundException(result) + 404 -> throw MultiplayerFileNotFoundException(Exception(result)) else -> throw Exception(result) } } diff --git a/core/src/com/unciv/logic/trade/Trade.kt b/core/src/com/unciv/logic/trade/Trade.kt index 9c3098448f..d1b827cccf 100644 --- a/core/src/com/unciv/logic/trade/Trade.kt +++ b/core/src/com/unciv/logic/trade/Trade.kt @@ -1,11 +1,12 @@ package com.unciv.logic.trade import com.unciv.Constants +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.diplomacy.DiplomacyFlags -class Trade{ +class Trade : IsPartOfGameInfoSerialization { val theirOffers = TradeOffersList() val ourOffers = TradeOffersList() @@ -48,7 +49,7 @@ class Trade{ } -class TradeRequest { +class TradeRequest : IsPartOfGameInfoSerialization { fun decline(decliningCiv:CivilizationInfo) { val requestingCivInfo = decliningCiv.gameInfo.getCivilization(requestingCiv) val diplomacyManager = requestingCivInfo.getDiplomacyManager(decliningCiv) diff --git a/core/src/com/unciv/logic/trade/TradeOffer.kt b/core/src/com/unciv/logic/trade/TradeOffer.kt index 45748aa1a0..0931227205 100644 --- a/core/src/com/unciv/logic/trade/TradeOffer.kt +++ b/core/src/com/unciv/logic/trade/TradeOffer.kt @@ -2,12 +2,13 @@ package com.unciv.logic.trade import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.Speed import com.unciv.models.translations.tr import com.unciv.ui.utils.Fonts import com.unciv.logic.trade.TradeType.TradeTypeNumberType -data class TradeOffer(val name: String, val type: TradeType, var amount: Int = 1, var duration: Int) { +data class TradeOffer(val name: String, val type: TradeType, var amount: Int = 1, var duration: Int) : IsPartOfGameInfoSerialization { constructor( name: String, diff --git a/core/src/com/unciv/logic/trade/TradeOffersList.kt b/core/src/com/unciv/logic/trade/TradeOffersList.kt index 448fbd84ed..70fcb89114 100644 --- a/core/src/com/unciv/logic/trade/TradeOffersList.kt +++ b/core/src/com/unciv/logic/trade/TradeOffersList.kt @@ -1,8 +1,9 @@ package com.unciv.logic.trade +import com.unciv.logic.IsPartOfGameInfoSerialization import java.util.* -class TradeOffersList: ArrayList() { +class TradeOffersList: ArrayList(), IsPartOfGameInfoSerialization { override fun add(element: TradeOffer): Boolean { val equivalentOffer = firstOrNull { it.name == element.name && it.type == element.type } if (equivalentOffer == null) { @@ -20,4 +21,4 @@ class TradeOffersList: ArrayList() { for (offer in otherTradeOffersList) tradeOffersListCopy.add(offer.copy(amount = -offer.amount)) return tradeOffersListCopy } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/models/Counter.kt b/core/src/com/unciv/models/Counter.kt index 18dc56eec5..80ed8d0f09 100644 --- a/core/src/com/unciv/models/Counter.kt +++ b/core/src/com/unciv/models/Counter.kt @@ -1,6 +1,8 @@ package com.unciv.models -open class Counter : LinkedHashMap() { +import com.unciv.logic.IsPartOfGameInfoSerialization + +open class Counter : LinkedHashMap(), IsPartOfGameInfoSerialization { override operator fun get(key: K): Int? { // don't return null if empty return if (containsKey(key)) @@ -30,7 +32,7 @@ open class Counter : LinkedHashMap() { for (key in keys) newCounter[key] = this[key]!! * amount return newCounter } - + fun sumValues(): Int { return this.map { it.value }.sum() } diff --git a/core/src/com/unciv/models/Religion.kt b/core/src/com/unciv/models/Religion.kt index f0447b31e7..d0f6ee9e4a 100644 --- a/core/src/com/unciv/models/Religion.kt +++ b/core/src/com/unciv/models/Religion.kt @@ -1,12 +1,13 @@ package com.unciv.models import com.unciv.logic.GameInfo +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.Belief import com.unciv.models.ruleset.BeliefType import com.unciv.models.stats.INamed /** Data object for Religions */ -class Religion() : INamed { +class Religion() : INamed, IsPartOfGameInfoSerialization { override lateinit var name: String var displayName: String? = null @@ -14,7 +15,7 @@ class Religion() : INamed { var founderBeliefs: HashSet = hashSetOf() var followerBeliefs: HashSet = hashSetOf() - + @Transient lateinit var gameInfo: GameInfo @@ -35,15 +36,15 @@ class Religion() : INamed { fun setTransients(gameInfo: GameInfo) { this.gameInfo = gameInfo } - - fun getIconName() = + + fun getIconName() = if (isPantheon()) "Pantheon" else name - + fun getReligionDisplayName() = if (displayName != null) displayName!! else name - + private fun mapToExistingBeliefs(beliefs: HashSet): List { val rulesetBeliefs = gameInfo.ruleSet.beliefs return beliefs.mapNotNull { @@ -51,12 +52,12 @@ class Religion() : INamed { else rulesetBeliefs[it]!! } } - + fun getBeliefs(beliefType: BeliefType): Sequence { if (beliefType == BeliefType.Any) return mapToExistingBeliefs((founderBeliefs + followerBeliefs).toHashSet()).asSequence() - - val beliefs = + + val beliefs = when (beliefType) { BeliefType.Pantheon -> followerBeliefs BeliefType.Follower -> followerBeliefs @@ -64,20 +65,20 @@ class Religion() : INamed { BeliefType.Enhancer -> founderBeliefs else -> null!! // This is fine... } - + return mapToExistingBeliefs(beliefs) .asSequence() .filter { it.type == beliefType } } - + fun getAllBeliefsOrdered(): Sequence { return mapToExistingBeliefs(followerBeliefs).asSequence().filter { it.type == BeliefType.Pantheon } + mapToExistingBeliefs(founderBeliefs).asSequence().filter { it.type == BeliefType.Founder } + mapToExistingBeliefs(followerBeliefs).asSequence().filter { it.type == BeliefType.Follower } + mapToExistingBeliefs(founderBeliefs).asSequence().filter { it.type == BeliefType.Enhancer } } - - private fun getUniquesOfBeliefs(beliefs: HashSet) = + + private fun getUniquesOfBeliefs(beliefs: HashSet) = mapToExistingBeliefs(beliefs).asSequence().flatMap { it.uniqueObjects } fun getFollowerUniques() = getUniquesOfBeliefs(followerBeliefs) @@ -89,8 +90,8 @@ class Religion() : INamed { fun isPantheon() = getBeliefs(BeliefType.Pantheon).any() && !isMajorReligion() fun isMajorReligion() = getBeliefs(BeliefType.Founder).any() - + fun isEnhancedReligion() = getBeliefs(BeliefType.Enhancer).any() - + fun getFounder() = gameInfo.civilizations.first { it.civName == foundingCivName } } diff --git a/core/src/com/unciv/models/helpers/MapArrowType.kt b/core/src/com/unciv/models/helpers/MapArrowType.kt index 71876315bf..3fe06934af 100644 --- a/core/src/com/unciv/models/helpers/MapArrowType.kt +++ b/core/src/com/unciv/models/helpers/MapArrowType.kt @@ -1,12 +1,13 @@ package com.unciv.models.helpers import com.badlogic.gdx.graphics.Color +import com.unciv.logic.IsPartOfGameInfoSerialization /** Base interface for classes the instances of which signify a distinctive type of look and feel with which to draw arrows on the map. */ interface MapArrowType /** Enum constants describing how/why a unit changed position. Each is also associated with an arrow type to draw on the map overlay. */ -enum class UnitMovementMemoryType: MapArrowType { +enum class UnitMovementMemoryType: MapArrowType, IsPartOfGameInfoSerialization { UnitMoved, UnitAttacked, // For when attacked, killed, and moved into tile. UnitWithdrew, // Caravel, destroyer, etc. diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index dba752331a..36d362e46b 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -1,5 +1,6 @@ package com.unciv.models.metadata +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.PlayerType import com.unciv.models.ruleset.Speed @@ -8,7 +9,7 @@ enum class BaseRuleset(val fullName:String){ Civ_V_GnK("Civ V - Gods & Kings"), } -class GameParameters { // Default values are the default new game +class GameParameters : IsPartOfGameInfoSerialization { // Default values are the default new game var difficulty = "Prince" var speed = Speed.DEFAULT diff --git a/core/src/com/unciv/models/metadata/Player.kt b/core/src/com/unciv/models/metadata/Player.kt index cbb8a3eb1d..0fddbd7abf 100644 --- a/core/src/com/unciv/models/metadata/Player.kt +++ b/core/src/com/unciv/models/metadata/Player.kt @@ -1,9 +1,10 @@ package com.unciv.models.metadata import com.unciv.Constants +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.civilization.PlayerType -class Player(var chosenCiv: String = Constants.random) { +class Player(var chosenCiv: String = Constants.random) : IsPartOfGameInfoSerialization { var playerType: PlayerType = PlayerType.AI var playerId="" -} \ No newline at end of file +} diff --git a/core/src/com/unciv/models/ruleset/Speed.kt b/core/src/com/unciv/models/ruleset/Speed.kt index 6000a10942..2341830fcb 100644 --- a/core/src/com/unciv/models/ruleset/Speed.kt +++ b/core/src/com/unciv/models/ruleset/Speed.kt @@ -1,5 +1,6 @@ package com.unciv.models.ruleset +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.stats.Stat import com.unciv.models.translations.tr @@ -7,7 +8,7 @@ import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.utils.Fonts import kotlin.math.abs -class Speed : RulesetObject() { +class Speed : RulesetObject(), IsPartOfGameInfoSerialization { var modifier: Float = 1f var goldCostModifier: Float = modifier var productionCostModifier: Float = modifier diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 4f6df03060..76dcc50353 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -1,6 +1,7 @@ package com.unciv.models.ruleset.unique import com.unciv.Constants +import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.battle.CombatAction import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.city.CityInfo @@ -312,7 +313,7 @@ class UniqueMap: HashMap>() { } -class TemporaryUnique() { +class TemporaryUnique() : IsPartOfGameInfoSerialization { constructor(uniqueObject: Unique, turns: Int) : this() { unique = uniqueObject.text diff --git a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt index 2bfd0e2c41..e13c277f8d 100644 --- a/core/src/com/unciv/ui/crashhandling/CrashScreen.kt +++ b/core/src/com/unciv/ui/crashhandling/CrashScreen.kt @@ -19,6 +19,7 @@ import com.unciv.ui.utils.extensions.addBorder import com.unciv.ui.utils.extensions.onClick import com.unciv.ui.utils.extensions.setFontSize import com.unciv.ui.utils.extensions.toLabel +import com.unciv.ui.utils.extensions.toNiceString import java.io.PrintWriter import java.io.StringWriter @@ -82,7 +83,7 @@ class CrashScreen(val exception: Throwable): BaseScreen() { /// The $lastScreenType substitution is the only one completely under the control of this class— Everything else can, in theory, have new lines in it due to containing strings or custom .toString behaviour with new lines (which… I think Table.toString or something actually does). So normalize indentation for basically everything. return """ **Platform:** ${Gdx.app.type.toString().prependIndentToOnlyNewLines(subIndent)} - **Version:** ${UncivGame.Current.version.prependIndentToOnlyNewLines(subIndent)} + **Version:** ${UncivGame.VERSION.toNiceString().prependIndentToOnlyNewLines(subIndent)} **Rulesets:** ${RulesetCache.keys.toString().prependIndentToOnlyNewLines(subIndent)} **Last Screen:** `$lastScreenType` diff --git a/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt b/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt index 24a475716b..e5e1142032 100644 --- a/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/AddMultiplayerGameScreen.kt @@ -7,6 +7,7 @@ import com.unciv.models.translations.tr import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.popup.Popup import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.utils.UncivTextField import com.unciv.ui.utils.extensions.enable import com.unciv.ui.utils.extensions.onClick @@ -63,7 +64,7 @@ class AddMultiplayerGameScreen : PickerScreen() { game.popScreen() } } catch (ex: Exception) { - val message = MultiplayerHelpers.getLoadExceptionMessage(ex) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex) launchOnGLThread { popup.reuseWith(message, true) } diff --git a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt index 1c08eec30f..7feb17adb5 100644 --- a/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt +++ b/core/src/com/unciv/ui/multiplayer/EditMultiplayerGameInfoScreen.kt @@ -8,6 +8,7 @@ import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.popup.ConfirmPopup import com.unciv.ui.popup.Popup import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.utils.UncivTextField import com.unciv.ui.utils.extensions.disable import com.unciv.ui.utils.extensions.enable @@ -110,7 +111,7 @@ class EditMultiplayerGameInfoScreen(val multiplayerGame: OnlineMultiplayerGame) } } } catch (ex: Exception) { - val message = MultiplayerHelpers.getLoadExceptionMessage(ex) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex) launchOnGLThread { popup.reuseWith(message, true) } diff --git a/core/src/com/unciv/ui/multiplayer/MultiplayerHelpers.kt b/core/src/com/unciv/ui/multiplayer/MultiplayerHelpers.kt index ebcc25f7b8..d2f808aaf8 100644 --- a/core/src/com/unciv/ui/multiplayer/MultiplayerHelpers.kt +++ b/core/src/com/unciv/ui/multiplayer/MultiplayerHelpers.kt @@ -2,28 +2,20 @@ package com.unciv.ui.multiplayer import com.badlogic.gdx.Gdx import com.unciv.UncivGame -import com.unciv.logic.UncivShowableException import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.logic.multiplayer.OnlineMultiplayerGame -import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached import com.unciv.models.translations.tr import com.unciv.ui.popup.Popup +import com.unciv.ui.saves.LoadGameScreen import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.extensions.formatShort import com.unciv.ui.utils.extensions.toCheckBox import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.launchOnGLThread -import java.io.FileNotFoundException import java.time.Duration import java.time.Instant object MultiplayerHelpers { - fun getLoadExceptionMessage(ex: Throwable) = when (ex) { - is FileStorageRateLimitReached -> "Server limit reached! Please wait for [${ex.limitRemainingSeconds}] seconds" - is FileNotFoundException -> "File could not be found on the multiplayer server" - is UncivShowableException -> ex.message // If this is already translated, it's an error on the throwing side! - else -> "Unhandled problem, [${ex::class.simpleName} ${ex.localizedMessage}]" - } fun loadMultiplayerGame(screen: BaseScreen, selectedGame: OnlineMultiplayerGame) { val loadingGamePopup = Popup(screen) @@ -34,7 +26,7 @@ object MultiplayerHelpers { try { UncivGame.Current.onlineMultiplayer.loadGame(selectedGame) } catch (ex: Exception) { - val message = getLoadExceptionMessage(ex) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex) launchOnGLThread { loadingGamePopup.reuseWith(message, true) } @@ -46,9 +38,8 @@ object MultiplayerHelpers { val descriptionText = StringBuilder() val ex = multiplayerGame.error if (ex != null) { - descriptionText.append("Error while refreshing:".tr()).append(' ') - val message = getLoadExceptionMessage(ex) - descriptionText.appendLine(message.tr()) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex, "Error while refreshing:") + descriptionText.appendLine(message) } val lastUpdate = multiplayerGame.lastUpdate descriptionText.appendLine("Last refresh: [${Duration.between(lastUpdate, Instant.now()).formatShort()}] ago".tr()) diff --git a/core/src/com/unciv/ui/options/AboutTab.kt b/core/src/com/unciv/ui/options/AboutTab.kt index 9a1bd18cc4..5ffffe59f0 100644 --- a/core/src/com/unciv/ui/options/AboutTab.kt +++ b/core/src/com/unciv/ui/options/AboutTab.kt @@ -1,19 +1,20 @@ package com.unciv.ui.options import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.UncivGame import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.civilopedia.MarkupRenderer import com.unciv.ui.utils.BaseScreen +import com.unciv.ui.utils.extensions.toNiceString fun aboutTab(screen: BaseScreen): Table { - val version = screen.game.version - val versionAnchor = version.replace(".", "") + val versionAnchor = UncivGame.VERSION.text.replace(".", "") val lines = sequence { yield(FormattedLine(extraImage = "banner", imageSize = 240f, centered = true)) yield(FormattedLine()) - yield(FormattedLine("{Version}: $version", link = "https://github.com/yairm210/Unciv/blob/master/changelog.md#$versionAnchor")) + yield(FormattedLine("{Version}: ${UncivGame.VERSION.toNiceString()}", link = "https://github.com/yairm210/Unciv/blob/master/changelog.md#$versionAnchor")) yield(FormattedLine("See online Readme", link = "https://github.com/yairm210/Unciv/blob/master/README.md#unciv---foss-civ-v-for-androiddesktop")) yield(FormattedLine("Visit repository", link = "https://github.com/yairm210/Unciv")) } return MarkupRenderer.render(lines.toList()).pad(20f) -} \ No newline at end of file +} diff --git a/core/src/com/unciv/ui/saves/LoadGameScreen.kt b/core/src/com/unciv/ui/saves/LoadGameScreen.kt index e1149f8216..eec2cd8cb6 100644 --- a/core/src/com/unciv/ui/saves/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadGameScreen.kt @@ -26,6 +26,7 @@ import com.unciv.ui.utils.extensions.onActivation import com.unciv.ui.utils.extensions.onClick import com.unciv.ui.utils.extensions.toLabel import com.unciv.ui.utils.extensions.toTextButton +import com.unciv.utils.Log import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.launchOnGLThread import java.io.FileNotFoundException @@ -42,6 +43,38 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { private const val loadFromClipboard = "Load copied data" private const val copyExistingSaveToClipboard = "Copy saved game to clipboard" private const val downloadMissingMods = "Download missing mods" + + /** Gets a translated exception message to show to the user. + * @return The first returned value is the message, the second is signifying if the user can likely fix this problem. */ + fun getLoadExceptionMessage(ex: Throwable, primaryText: String = "Could not load game!"): Pair { + val errorText = StringBuilder(primaryText.tr()) + + val isUserFixable: Boolean + errorText.appendLine() + when (ex) { + is UncivShowableException -> { + errorText.append("${ex.localizedMessage}") + isUserFixable = true + } + is SerializationException -> { + errorText.append("The file data seems to be corrupted.".tr()) + isUserFixable = false + } + is FileNotFoundException -> { + if (ex.cause?.message?.contains("Permission denied") == true) { + errorText.append("You do not have sufficient permissions to access the file.".tr()) + isUserFixable = true + } else { + isUserFixable = false + } + } + else -> { + errorText.append("Unhandled problem, [${ex::class.simpleName} ${ex.localizedMessage}]".tr()) + isUserFixable = false + } + } + return Pair(errorText.toString(), isUserFixable) + } } init { @@ -87,27 +120,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { } catch (ex: Exception) { launchOnGLThread { loadingPopup.close() - if (ex is MissingModsException) { - handleLoadGameException("Could not load game", ex) - return@launchOnGLThread - } - val cantLoadGamePopup = Popup(this@LoadGameScreen) - cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row() - if (ex is SerializationException) - cantLoadGamePopup.addGoodSizedLabel("The file data seems to be corrupted.").row() - if (ex.cause is FileNotFoundException && (ex.cause as FileNotFoundException).message?.contains("Permission denied") == true) { - cantLoadGamePopup.addGoodSizedLabel("You do not have sufficient permissions to access the file.").row() - } else if (ex is UncivShowableException) { - // thrown exceptions are our own tests and can be shown to the user - cantLoadGamePopup.addGoodSizedLabel(ex.message).row() - } else { - cantLoadGamePopup.addGoodSizedLabel("If you could copy your game data (\"Copy saved game to clipboard\" - ").row() - cantLoadGamePopup.addGoodSizedLabel(" paste into an email to yairm210@hotmail.com)").row() - cantLoadGamePopup.addGoodSizedLabel("I could maybe help you figure out what went wrong, since this isn't supposed to happen!").row() - ex.printStackTrace() - } - cantLoadGamePopup.addCloseButton() - cantLoadGamePopup.open() + handleLoadGameException(ex) } } } @@ -122,7 +135,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { val loadedGame = UncivFiles.gameInfoFromString(clipboardContentsString) game.loadGame(loadedGame) } catch (ex: Exception) { - launchOnGLThread { handleLoadGameException("Could not load game from clipboard!", ex) } + launchOnGLThread { handleLoadGameException(ex, "Could not load game from clipboard!") } } } } @@ -142,7 +155,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { Concurrency.run(Companion.loadFromCustomLocation) { game.files.loadGameFromCustomLocation { result -> if (result.isError()) { - handleLoadGameException("Could not load game from custom location!", result.exception) + handleLoadGameException(result.exception!!, "Could not load game from custom location!") } else if (result.isSuccessful()) { Concurrency.run { game.loadGame(result.gameData!!) @@ -183,10 +196,20 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { return button } - private fun handleLoadGameException(primaryText: String, ex: Exception?) { - var errorText = primaryText.tr() - if (ex is UncivShowableException) errorText += "\n${ex.localizedMessage}" - ex?.printStackTrace() + private fun handleLoadGameException(ex: Exception, primaryText: String = "Could not load game!") { + Log.error("Error while loading game", ex) + val (errorText, isUserFixable) = getLoadExceptionMessage(ex, primaryText) + + if (!isUserFixable) { + val cantLoadGamePopup = Popup(this@LoadGameScreen) + cantLoadGamePopup.addGoodSizedLabel("It looks like your saved game can't be loaded!").row() + cantLoadGamePopup.addGoodSizedLabel("If you could copy your game data (\"Copy saved game to clipboard\" - ").row() + cantLoadGamePopup.addGoodSizedLabel(" paste into an email to yairm210@hotmail.com)").row() + cantLoadGamePopup.addGoodSizedLabel("I could maybe help you figure out what went wrong, since this isn't supposed to happen!").row() + cantLoadGamePopup.addCloseButton() + cantLoadGamePopup.open() + } + Concurrency.runOnGLThread { errorLabel.setText(errorText) errorLabel.isVisible = true @@ -228,7 +251,7 @@ class LoadGameScreen(previousScreen:BaseScreen) : LoadOrSaveScreen() { ToastPopup("Missing mods are downloaded successfully.", this@LoadGameScreen) } } catch (ex: Exception) { - handleLoadGameException("Could not load the missing mods!", ex) + handleLoadGameException(ex, "Could not load the missing mods!") } finally { loadMissingModsButton.isEnabled = true descriptionLabel.setText("") diff --git a/core/src/com/unciv/ui/saves/QuickSave.kt b/core/src/com/unciv/ui/saves/QuickSave.kt index 6e549b7777..b9ce4b61ce 100644 --- a/core/src/com/unciv/ui/saves/QuickSave.kt +++ b/core/src/com/unciv/ui/saves/QuickSave.kt @@ -44,9 +44,11 @@ object QuickSave { ToastPopup("Quickload successful.", screen) } } catch (ex: Exception) { + Log.error("Exception while quickloading", ex) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex) launchOnGLThread { toast.close() - ToastPopup("Could not load game!", screen) + ToastPopup(message, screen) } } } @@ -75,7 +77,8 @@ object QuickSave { Log.error("Could not autoload game", ex) launchOnGLThread { loadingPopup.close() - ToastPopup("Cannot resume game!", screen) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex, "Cannot resume game!") + ToastPopup(message, screen) } return@run } @@ -86,8 +89,8 @@ object QuickSave { } catch (oom: OutOfMemoryError) { outOfMemory() } catch (ex: Exception) { - val message = MultiplayerHelpers.getLoadExceptionMessage(ex) Log.error("Could not autoload game", ex) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex) launchOnGLThread { loadingPopup.close() ToastPopup(message, screen) diff --git a/core/src/com/unciv/ui/utils/extensions/FormattingExtensions.kt b/core/src/com/unciv/ui/utils/extensions/FormattingExtensions.kt index dc7e3fdfc5..f743c7da22 100644 --- a/core/src/com/unciv/ui/utils/extensions/FormattingExtensions.kt +++ b/core/src/com/unciv/ui/utils/extensions/FormattingExtensions.kt @@ -1,5 +1,6 @@ package com.unciv.ui.utils.extensions +import com.unciv.UncivGame import com.unciv.models.translations.tr import java.text.SimpleDateFormat import java.time.Duration @@ -91,3 +92,5 @@ fun String.filterCompositeLogic(predicate: (String) -> T?, operation: (T, T) * otherwise return `null` for Elvis chaining of the individual filter. */ fun String.filterAndLogic(predicate: (String) -> Boolean): Boolean? = if (contains('{')) filterCompositeLogic(predicate) { a, b -> a && b } else null + +fun UncivGame.Version.toNiceString() = "$text (Build $number)" diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 9ca8dcb678..7897d494f4 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -332,9 +332,8 @@ class WorldScreen( startNewScreenJob(latestGame) } catch (ex: Throwable) { launchOnGLThread { - val message = MultiplayerHelpers.getLoadExceptionMessage(ex) + val (message) = LoadGameScreen.getLoadExceptionMessage(ex, "Couldn't download the latest game state!") loadingGamePopup.innerTable.clear() - loadingGamePopup.addGoodSizedLabel("Couldn't download the latest game state!").colspan(2).row() loadingGamePopup.addGoodSizedLabel(message).colspan(2).row() loadingGamePopup.addButton("Retry") { launchOnThreadPool("Load latest multiplayer state after error") { diff --git a/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt b/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt index 4afaa50c34..8677f6741c 100644 --- a/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/ConsoleLauncher.kt @@ -25,14 +25,7 @@ internal object ConsoleLauncher { fun main(arg: Array) { Log.backend = DesktopLogBackend() - val version = "0.1" - val consoleParameters = UncivGameParameters( - version, - null, - null, - null, - true - ) + val consoleParameters = UncivGameParameters(consoleMode = true) val game = UncivGame(consoleParameters) UncivGame.Current = game diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index 10d8c39f6d..ac519a1699 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -47,15 +47,13 @@ internal object DesktopLauncher { config.setWindowedMode(settings.windowState.width.coerceAtLeast(120), settings.windowState.height.coerceAtLeast(80)) } - val versionFromJar = DesktopLauncher.javaClass.`package`.specificationVersion ?: "Desktop" - - if (versionFromJar == "Desktop") { + val isRunFromIDE = DesktopLauncher.javaClass.`package`.specificationVersion == null + if (isRunFromIDE) { UniqueDocsWriter().write() } val platformSpecificHelper = PlatformSpecificHelpersDesktop(config) val desktopParameters = UncivGameParameters( - versionFromJar, cancelDiscordEvent = { discordTimer?.cancel() }, fontImplementation = NativeFontDesktop((Fonts.ORIGINAL_FONT_SIZE * settings.fontSizeMultiplier).toInt(), settings.fontFamily), customFileLocationHelper = CustomFileLocationHelperDesktop(), diff --git a/ios/src/com/unciv/app/IOSLauncher.java b/ios/src/com/unciv/app/IOSLauncher.java index de09d695e6..f56b442e4f 100644 --- a/ios/src/com/unciv/app/IOSLauncher.java +++ b/ios/src/com/unciv/app/IOSLauncher.java @@ -10,7 +10,7 @@ class IOSLauncher extends IOSApplication.Delegate { @Override protected IOSApplication createApplication() { IOSApplicationConfiguration config = new IOSApplicationConfiguration(); - return new IOSApplication(new com.unciv.UncivGame("IOS"), config); + return new IOSApplication(new com.unciv.UncivGame(), config); } public static void main(String[] argv) { @@ -18,4 +18,4 @@ class IOSLauncher extends IOSApplication.Delegate { UIApplication.main(argv, null, IOSLauncher.class); pool.close(); } -} \ No newline at end of file +} diff --git a/server/src/com/unciv/app/server/UncivServer.kt b/server/src/com/unciv/app/server/UncivServer.kt index 71d15260d5..c9f7a4dc00 100644 --- a/server/src/com/unciv/app/server/UncivServer.kt +++ b/server/src/com/unciv/app/server/UncivServer.kt @@ -13,12 +13,8 @@ import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.utils.io.jvm.javaio.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File -import java.io.FileNotFoundException -import java.time.Instant internal object UncivServer { diff --git a/tests/src/com/unciv/testing/BasicTests.kt b/tests/src/com/unciv/testing/BasicTests.kt index f8ba2cbcba..2fcf1cf026 100644 --- a/tests/src/com/unciv/testing/BasicTests.kt +++ b/tests/src/com/unciv/testing/BasicTests.kt @@ -4,7 +4,6 @@ package com.unciv.testing import com.badlogic.gdx.Gdx import com.unciv.Constants import com.unciv.UncivGame -import com.unciv.UncivGameParameters import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.GameSettings import com.unciv.models.ruleset.Ruleset @@ -50,8 +49,7 @@ class BasicTests { @Test fun gameIsNotRunWithDebugModes() { - val params = UncivGameParameters("", null) - val game = UncivGame(params) + val game = UncivGame() Assert.assertTrue("This test will only pass if the game is not run with debug modes", !game.superchargedForDebug && !game.viewEntireMapForDebug @@ -84,7 +82,7 @@ class BasicTests { val statsThatShouldBe = Stats(gold = 1f, production = 2f) Assert.assertTrue(Stats.parse("+1 Gold, +2 Production").equals(statsThatShouldBe)) - UncivGame.Current = UncivGame("") + UncivGame.Current = UncivGame() UncivGame.Current.settings = GameSettings().apply { language = "Italian" } } diff --git a/tests/src/com/unciv/testing/SerializationTests.kt b/tests/src/com/unciv/testing/SerializationTests.kt index 27e3564705..8889f5964a 100644 --- a/tests/src/com/unciv/testing/SerializationTests.kt +++ b/tests/src/com/unciv/testing/SerializationTests.kt @@ -60,7 +60,7 @@ class SerializationTests { seed = 42L } val setup = GameSetupInfo(param, mapParameters) - UncivGame.Current = UncivGame("") + UncivGame.Current = UncivGame() UncivGame.Current.files = UncivFiles(Gdx.files) // Both startNewGame and makeCivilizationsMeet will cause a save to storage of our empty settings diff --git a/tests/src/com/unciv/testing/TranslationTests.kt b/tests/src/com/unciv/testing/TranslationTests.kt index 960a4822a8..86fb167c8c 100644 --- a/tests/src/com/unciv/testing/TranslationTests.kt +++ b/tests/src/com/unciv/testing/TranslationTests.kt @@ -163,7 +163,7 @@ class TranslationTests { @Test fun allStringsTranslate() { // Needed for .tr() to work - UncivGame.Current = UncivGame("") + UncivGame.Current = UncivGame() UncivGame.Current.settings = GameSettings() for ((key, value) in translations) @@ -231,7 +231,7 @@ class TranslationTests { superNestedString.getPlaceholderParametersIgnoringLowerLevelBraces()[0] .getPlaceholderParametersIgnoringLowerLevelBraces()) - UncivGame.Current = UncivGame("") + UncivGame.Current = UncivGame() UncivGame.Current.settings = GameSettings() fun addTranslation(original:String, result:String){ diff --git a/tests/src/com/unciv/uniques/TestGame.kt b/tests/src/com/unciv/uniques/TestGame.kt index ee65013f13..94d78b430d 100644 --- a/tests/src/com/unciv/uniques/TestGame.kt +++ b/tests/src/com/unciv/uniques/TestGame.kt @@ -30,7 +30,7 @@ class TestGame { init { // Set UncivGame.Current so that debug variables are initialized - UncivGame.Current = UncivGame("Test") + UncivGame.Current = UncivGame() // And the settings can be reached for the locale used in .tr() UncivGame.Current.settings = GameSettings()