Skip to content

Commit

Permalink
migrate old save games
Browse files Browse the repository at this point in the history
  • Loading branch information
meikpiep committed Feb 28, 2025
1 parent e9c5673 commit 9fb6d64
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.koin.core.module.dsl.binds
import org.koin.core.module.dsl.createdAtStart
import org.koin.core.module.dsl.withOptions
import org.koin.dsl.module
import org.piepmeyer.gauguin.game.save.SavedGamesService
import org.piepmeyer.gauguin.preferences.ApplicationPreferences
import org.piepmeyer.gauguin.preferences.ApplicationPreferencesImpl
import org.piepmeyer.gauguin.preferences.ApplicationPreferencesMigrations
Expand All @@ -28,6 +29,8 @@ class MainApplication : Application() {

logger.info { "Starting application Gauguin..." }

SavedGamesService.migrateOldSavedGameFilesBeforeKoinStartup(filesDir)

val applicationPreferences = ApplicationPreferencesImpl(this)
val preferenceMigrations = ApplicationPreferencesMigrations(applicationPreferences)
preferenceMigrations.migrateThemeToNightModeIfNecessary()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.piepmeyer.gauguin.game.save.v1.V1SavedGrid
import org.piepmeyer.gauguin.grid.Grid
import java.io.File
import java.nio.charset.StandardCharsets
Expand Down Expand Up @@ -38,18 +39,56 @@ class SaveGame private constructor(
return restore(fileData)
} catch (e: SerializationException) {
throw SerializationException(
"Error decoding grid with length " +
"Error decoding grid of file '${file.name}' with length " +
"${file.length()} and first bytes: '${fileData.substring(0, 50)}'.",
e,
)
}
}

fun migrateOldSavedGridVersion() {
if (file.length() == 0L) {
return
}

logger.info { "Checking ${file.name} if migration is needed..." }

val fileData = file.readText(StandardCharsets.UTF_8)

val gridVersion =
try {
jsonIgnoringUnknownKeys.decodeFromString<SavedGridVersion>(fileData)
} catch (e: SerializationException) {
throw SerializationException(
"Error decoding version grid info with length " +
"${file.length()} and first bytes: '${fileData.substring(0, 50)}'.",
e,
)
}

when (gridVersion.version) {
2 -> {
logger.info { "No migration needed." }
return
}
1 -> {
logger.info { "Migrating from version 1..." }
val savedGrid = Json.decodeFromString<V1SavedGrid>(fileData)

save(savedGrid.toGrid())
logger.info { "Finished migration of '${file.name}" }
}
else -> error("Unknown version '${gridVersion.version}' of file '${file.name}'")
}
}

companion object {
const val SAVEGAME_AUTO_NAME = "autosave.yml"
const val SAVEGAME_NAME_PREFIX = "game_"
const val SAVEGAME_NAME_SUFFIX = ".yml"

private val jsonIgnoringUnknownKeys: Json by lazy { Json { ignoreUnknownKeys = true } }

fun autosaveByDirectory(directory: File): SaveGame = SaveGame(getAutosave(directory))

fun createWithFile(filename: File): SaveGame = SaveGame(filename)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ data class SavedGameOptionsVariant(
var showOperators: Boolean,
var cageOperation: GridCageOperation,
var digitSetting: DigitSetting,
var difficultySetting: SavedDifficultySetting?,
var difficultiesSetting: Set<DifficultySetting> = emptySet(),
var singleCageUsage: SingleCageUsage,
var numeralSystem: NumeralSystem,
Expand All @@ -23,7 +22,7 @@ data class SavedGameOptionsVariant(
showOperators = showOperators,
cageOperation = cageOperation,
digitSetting = digitSetting,
difficultiesSetting = difficultiesSetting.ifEmpty { difficultySetting!!.toDifficultySetting() },
difficultiesSetting = difficultiesSetting,
singleCageUsage = singleCageUsage,
numeralSystem = numeralSystem,
)
Expand All @@ -34,7 +33,6 @@ data class SavedGameOptionsVariant(
showOperators = options.showOperators,
cageOperation = options.cageOperation,
digitSetting = options.digitSetting,
difficultySetting = null,
difficultiesSetting = options.difficultiesSetting,
singleCageUsage = options.singleCageUsage,
numeralSystem = options.numeralSystem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ class SavedGamesService(
) : KoinComponent {
private var listeners = mutableListOf<SavedGamesListener>()

fun savedGameFiles(): List<File> {
return filesDir.listFiles { _: File?, name: String ->
name.startsWith(SaveGame.SAVEGAME_NAME_PREFIX) &&
name.endsWith(SaveGame.SAVEGAME_NAME_SUFFIX)
}
?.toList()
fun savedGameFiles(): List<File> =
filesDir
.listFiles { _: File?, name: String ->
name.startsWith(SaveGame.SAVEGAME_NAME_PREFIX) &&
name.endsWith(SaveGame.SAVEGAME_NAME_SUFFIX)
}?.toList()
?.filterNotNull() ?: emptyList()
}

fun countOfSavedGames(): Int {
return savedGameFiles().count()
}
fun countOfSavedGames(): Int = savedGameFiles().count()

fun informSavedGamesChanged() {
listeners.forEach { it.savedGamesChanged() }
Expand All @@ -28,4 +25,15 @@ class SavedGamesService(
fun addSavedGamesListener(listener: SavedGamesListener) {
listeners += listener
}

companion object {
fun migrateOldSavedGameFilesBeforeKoinStartup(filesDir: File) {
val service = SavedGamesService(filesDir)

service.savedGameFiles().forEach {
val saveGame = SaveGame.createWithFile(it)
saveGame.migrateOldSavedGridVersion()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,98 +1,103 @@
package org.piepmeyer.gauguin.game.save

import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import org.piepmeyer.gauguin.grid.Grid
import org.piepmeyer.gauguin.grid.GridCage
import kotlin.time.Duration.Companion.milliseconds

@Serializable
data class SavedGrid(
val version: Int = 1,
val variant: SavedGameVariant,
val savedAtInMilliseconds: Long,
val playTimeInMilliseconds: Long,
val startedToBePlayed: Boolean,
val description: String? = null,
val isActive: Boolean,
val cells: List<SavedCell>,
val selectedCellNumber: Int?,
val invalidCellNumbers: List<Int>,
val cheatedCellNumbers: List<Int>,
val cages: List<SavedCage>,
val undoSteps: List<SavedUndoStep> = emptyList(),
) {
fun toGrid(): Grid {
val grid = Grid(variant.toVariant(), savedAtInMilliseconds)
data class SavedGrid
@OptIn(ExperimentalSerializationApi::class)
constructor(
@EncodeDefault
val version: Int = 2,
val variant: SavedGameVariant,
val savedAtInMilliseconds: Long,
val playTimeInMilliseconds: Long,
val startedToBePlayed: Boolean,
val description: String? = null,
val isActive: Boolean,
val cells: List<SavedCell>,
val selectedCellNumber: Int?,
val invalidCellNumbers: List<Int>,
val cheatedCellNumbers: List<Int>,
val cages: List<SavedCage>,
val undoSteps: List<SavedUndoStep> = emptyList(),
) {
fun toGrid(): Grid {
val grid = Grid(variant.toVariant(), savedAtInMilliseconds)

grid.isActive = isActive
grid.playTime = playTimeInMilliseconds.milliseconds
grid.startedToBePlayed = startedToBePlayed
grid.description = description
grid.isActive = isActive
grid.playTime = playTimeInMilliseconds.milliseconds
grid.startedToBePlayed = startedToBePlayed
grid.description = description

cells.forEach {
val cell = grid.getCell(it.cellNumber)
cells.forEach {
val cell = grid.getCell(it.cellNumber)

cell.value = it.value
cell.userValue = it.userValue
cell.possibles = it.possibles
}
cell.value = it.value
cell.userValue = it.userValue
cell.possibles = it.possibles
}

selectedCellNumber?.let {
grid.getCell(it).isSelected = true
}
selectedCellNumber?.let {
grid.getCell(it).isSelected = true
}

invalidCellNumbers.forEach {
grid.getCell(it).isInvalidHighlight = true
}
invalidCellNumbers.forEach {
grid.getCell(it).isInvalidHighlight = true
}

cheatedCellNumbers.forEach {
grid.getCell(it).isCheated = true
}
cheatedCellNumbers.forEach {
grid.getCell(it).isCheated = true
}

grid.cages =
cages.map {
val cage = GridCage(it.id, grid.options.showOperators, it.action, it.type)
grid.cages =
cages.map {
val cage = GridCage(it.id, grid.options.showOperators, it.action, it.type)

cage.result = it.result
it.cellNumbers.forEach { cellNumber -> cage.addCell(grid.getCell(cellNumber)) }
cage.result = it.result
it.cellNumbers.forEach { cellNumber -> cage.addCell(grid.getCell(cellNumber)) }

cage
}
cage
}

grid.undoSteps.addAll(undoSteps.map { it.toUndoStep(grid) })
grid.undoSteps.addAll(undoSteps.map { it.toUndoStep(grid) })

return grid
}
return grid
}

companion object {
fun fromGrid(grid: Grid): SavedGrid {
val savedCells =
grid.cells.map {
SavedCell.fromCell(it)
}
val savedCages =
grid.cages.map {
SavedCage.fromCage(it)
}
val savedUndoSteps =
grid.undoSteps.map {
SavedUndoStep.fromUndoStep(it)
}
companion object {
fun fromGrid(grid: Grid): SavedGrid {
val savedCells =
grid.cells.map {
SavedCell.fromCell(it)
}
val savedCages =
grid.cages.map {
SavedCage.fromCage(it)
}
val savedUndoSteps =
grid.undoSteps.map {
SavedUndoStep.fromUndoStep(it)
}

return SavedGrid(
variant = SavedGameVariant.fromVariant(grid.variant),
savedAtInMilliseconds = System.currentTimeMillis(),
playTimeInMilliseconds = grid.playTime.inWholeMilliseconds,
startedToBePlayed = grid.startedToBePlayed,
description = grid.description,
isActive = grid.isActive,
cells = savedCells,
selectedCellNumber = grid.selectedCell?.cellNumber,
invalidCellNumbers = grid.invalidsHighlighted().map { it.cellNumber },
cheatedCellNumbers = grid.cheatedHighlighted().map { it.cellNumber },
cages = savedCages,
undoSteps = savedUndoSteps,
)
return SavedGrid(
variant = SavedGameVariant.fromVariant(grid.variant),
savedAtInMilliseconds = System.currentTimeMillis(),
playTimeInMilliseconds = grid.playTime.inWholeMilliseconds,
startedToBePlayed = grid.startedToBePlayed,
description = grid.description,
isActive = grid.isActive,
cells = savedCells,
selectedCellNumber = grid.selectedCell?.cellNumber,
invalidCellNumbers = grid.invalidsHighlighted().map { it.cellNumber },
cheatedCellNumbers = grid.cheatedHighlighted().map { it.cellNumber },
cages = savedCages,
undoSteps = savedUndoSteps,
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.piepmeyer.gauguin.game.save

import kotlinx.serialization.Serializable

@Serializable
data class SavedGridVersion(
val version: Int = 1,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.piepmeyer.gauguin.game.save.v1

import kotlinx.serialization.Serializable
import org.piepmeyer.gauguin.game.save.SavedDifficultySetting
import org.piepmeyer.gauguin.game.save.SavedGameOptionsVariant
import org.piepmeyer.gauguin.options.DifficultySetting
import org.piepmeyer.gauguin.options.DigitSetting
import org.piepmeyer.gauguin.options.GameOptionsVariant
import org.piepmeyer.gauguin.options.GridCageOperation
import org.piepmeyer.gauguin.options.NumeralSystem
import org.piepmeyer.gauguin.options.SingleCageUsage

@Serializable
data class V1SavedGameOptionsVariant(
var showOperators: Boolean,
var cageOperation: GridCageOperation,
var digitSetting: DigitSetting,
var difficultySetting: SavedDifficultySetting?,
var difficultiesSetting: Set<DifficultySetting> = emptySet(),
var singleCageUsage: SingleCageUsage,
var numeralSystem: NumeralSystem,
) {
fun toSavedGameOptionsVariant(): SavedGameOptionsVariant =
SavedGameOptionsVariant.fromOptionsVariant(
GameOptionsVariant(
showOperators = showOperators,
cageOperation = cageOperation,
digitSetting = digitSetting,
difficultiesSetting = difficultySetting!!.toDifficultySetting(),
singleCageUsage = singleCageUsage,
numeralSystem = numeralSystem,
),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.piepmeyer.gauguin.game.save.v1

import kotlinx.serialization.Serializable
import org.piepmeyer.gauguin.game.save.SavedGameVariant
import org.piepmeyer.gauguin.grid.GridSize

@Serializable
data class V1SavedGameVariant(
val gridSize: GridSize,
val options: V1SavedGameOptionsVariant,
) {
fun toUpdatedSavedGameVariant(): SavedGameVariant = SavedGameVariant(gridSize, options.toSavedGameOptionsVariant())
}
Loading

0 comments on commit 9fb6d64

Please sign in to comment.