Skip to content

Commit

Permalink
Save compatibility handling (#7255)
Browse files Browse the repository at this point in the history
* Add serialization version to GameInfo

* Add handling of incompatible saves due to a dfiferent save version

* Fix compilation?

* Fix ios compilation

* Refactor: Make it clearer that GameInfo serialization version is only supposed to be incremented when it's guaranteed to cause issues & rename to compatibility version

* Update initial version

* Update initial version

* Fix merge mistake
  • Loading branch information
Azzurite authored Jul 1, 2022
1 parent 08cede4 commit 57ed61a
Show file tree
Hide file tree
Showing 70 changed files with 537 additions and 360 deletions.
153 changes: 90 additions & 63 deletions .github/workflows/incrementVersionAndChangelog.js
Original file line number Diff line number Diff line change
@@ -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];

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)
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 (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);
}
return nextVersionString;
}

var buildConfigPath = "buildSrc/src/main/kotlin/BuildConfig.kt"
var buildConfigString = fs.readFileSync(buildConfigPath).toString()
function updateBuildConfig(nextVersionString) {
var buildConfigPath = "buildSrc/src/main/kotlin/BuildConfig.kt";
var buildConfigString = fs.readFileSync(buildConfigPath).toString();

console.log("Original: "+buildConfigString)
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.
// 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 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))
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)
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)
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})`;
}

main()
//endregion
12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
7 changes: 4 additions & 3 deletions android/assets/jsons/translations/template.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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! =
Expand Down Expand Up @@ -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 =
Expand All @@ -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 =
Expand Down
1 change: 0 additions & 1 deletion android/src/com/unciv/app/AndroidLauncher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 17 additions & 5 deletions core/src/com/unciv/UncivGame.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down
3 changes: 1 addition & 2 deletions core/src/com/unciv/UncivGameParameters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions core/src/com/unciv/logic/BarbarianManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow

class BarbarianManager {
class BarbarianManager : IsPartOfGameInfoSerialization {
val camps = HashMapVector2<Encampment>()

@Transient
Expand Down Expand Up @@ -165,7 +165,7 @@ class BarbarianManager {
}
}

class Encampment() {
class Encampment() : IsPartOfGameInfoSerialization {
val position = Vector2()
var countdown = 0
var spawnedUnits = -1
Expand Down
Loading

0 comments on commit 57ed61a

Please sign in to comment.