package net.torvald.terrarum import com.badlogic.gdx.utils.JsonWriter import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.gameactors.AVKey import net.torvald.terrarum.savegame.* import net.torvald.terrarum.savegame.VDFileID.ROOT import net.torvald.terrarum.savegame.VDFileID.SAVEGAMEINFO import net.torvald.terrarum.serialise.Common import net.torvald.terrarum.utils.JsonFetcher import net.torvald.terrarum.utils.forEachSiblings import java.io.File import java.io.IOException import java.nio.file.Files import java.nio.file.StandardCopyOption import java.util.* import kotlin.io.path.Path /** * Created by minjaesong on 2023-06-24. */ class SavegameCollection(files0: List) { /** Sorted in reverse by the last modified time of the files, index zero being the most recent */ val files = files0.sortedByDescending { it.getLastModifiedTime().shl(2) or it.diskFile.extension.matches(Regex("^[abc]${'$'}")).toLong(1) or it.diskFile.extension.isBlank().toLong(0) } /** Sorted in reverse by the last modified time of the files, index zero being the most recent */ val autoSaves = files.filter { it.diskFile.extension.matches(Regex("[a-z]")) } /** Sorted in reverse by the last modified time of the files, index zero being the most recent */ val manualSaves = files.filter { !it.diskFile.extension.matches(Regex("[a-z]")) } init { files.forEach { it.rebuild() } } companion object { fun collectFromBaseFilename(basedir: File, name: String): SavegameCollection { val files = basedir.listFiles().filter { it.name.startsWith(name) } .mapNotNull { try { DiskSkimmer(it, true) } catch (e: Throwable) { null } } return SavegameCollection(files) } } /** * Returns the most recent not-corrupted file */ fun loadable(): DiskSkimmer { return files.first() } fun moveToRecycle(basedir: String) { files.forEach { try { Files.move(it.diskFile.toPath(), Path(basedir, it.diskFile.name), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) } catch (e: IOException) { e.printStackTrace() } } } fun getBaseFile(): DiskSkimmer { return files.first { it.diskFile.extension.isBlank() } } fun getUUID(): UUID { var uuid: UUID? = null loadable().getFile(SAVEGAMEINFO)!!.let { JsonFetcher.readFromJsonString(ByteArray64Reader(it.bytes, Common.CHARSET)).forEachSiblings { name, value -> if (name == "worldIndex" || name == "uuid") uuid = UUID.fromString(value.asString()) } } return uuid!! } fun renamePlayer(name: String) { files.forEach { skimmer -> skimmer.rebuild() skimmer.getFile(SAVEGAMEINFO)!!.let { file -> val json = JsonFetcher.readFromJsonString(ByteArray64Reader(file.bytes, Common.CHARSET)) json.getChild("actorValue").getChild(AVKey.NAME).set(name) val jsonBytes = json.prettyPrint(JsonWriter.OutputType.json, 0).encodeToByteArray().toByteArray64() val newEntry = DiskEntry(SAVEGAMEINFO, ROOT, skimmer.requestFile(SAVEGAMEINFO)!!.creationDate, App.getTIME_T(), EntryFile(jsonBytes)) skimmer.appendEntry(newEntry) skimmer.setDiskName(name, Common.CHARSET) } } } fun renameWorld(name: String) { files.forEach { skimmer -> skimmer.setDiskName(name, Common.CHARSET) } } } class SavegameCollectionPair(player: SavegameCollection?, world: SavegameCollection?) { private var manualPlayer: DiskSkimmer? = null private var manualWorld: DiskSkimmer? = null private var autoPlayer: DiskSkimmer? = null private var autoWorld: DiskSkimmer? = null var status = 0 // 0: none available, 1: loadable manual save is newer than loadable auto; 2: loadable autosave is newer than loadable manual private set var newerSaveIsDamaged = false // only when most recent save is corrupted private set init { printdbg(this, "init ($player, $world)") if (player != null && world != null) { printdbg(this, "player files: " + player.files.joinToString { it.diskFile.name }) printdbg(this, "world files:" + world.files.joinToString { it.diskFile.name }) // if a pair of files were saved successfully, they must have identical lastModifiedTime() var pc = 0; val pt = player.files[0].getLastModifiedTime() var wc = 0; val wt = world.files[0].getLastModifiedTime() while (pc < player.files.size && wc < world.files.size) { val pcf = player.files[pc] val pcm = pcf.getLastModifiedTime() val wcf = world.files[wc] val wcm = wcf.getLastModifiedTime() printdbg(this, "pc=$pc, wc=$wc, pcm=$pcm, wcm=$wcm") if (playerDiskNotDamaged(pcf) && worldDiskNotDamaged(wcf)) { printdbg(this, "pcf.autosaved=${pcf.isAutosaved()}, wcf.autosaved=${wcf.isAutosaved()}") when (pcf.isAutosaved().toInt(1) or wcf.isAutosaved().toInt()) { 3 -> { if (autoPlayer == null && autoWorld == null) { autoPlayer = pcf autoWorld = wcf } pc += 1 wc += 1 } 0 -> { if (manualPlayer == null && manualWorld == null) { manualPlayer = pcf manualWorld = wcf } pc += 1 wc += 1 } else -> { if (pcm > wcm) pc += 1 else if (pcm == wcm) { pc += 1 wc += 1 } else wc += 1 } } } if (manualPlayer != null && manualWorld != null && autoPlayer != null && autoWorld != null) break } if (manualPlayer != null && manualWorld != null && autoPlayer != null && autoWorld != null) { status = if (manualPlayer!!.getLastModifiedTime() > autoPlayer!!.getLastModifiedTime()) 1 else 2 } else if (manualPlayer != null && manualWorld != null || autoPlayer != null && autoWorld != null) { status = 1 } else { status = 0 } printdbg(this, "manualPlayer = ${manualPlayer?.diskFile?.path}") printdbg(this, "manualWorld = ${manualWorld?.diskFile?.path}") printdbg(this, "autoPlayer = ${autoPlayer?.diskFile?.path}") printdbg(this, "autoWorld = ${autoWorld?.diskFile?.path}") printdbg(this, "status = $status") } } private fun DiskSkimmer.isAutosaved() = this.getSaveMode().and(0b0000_0010) != 0 private fun playerDiskNotDamaged(disk: DiskSkimmer): Boolean { return true } private fun worldDiskNotDamaged(disk: DiskSkimmer): Boolean { return true } fun moreRecentAutosaveAvailable() = (status == 2) fun saveAvaliable() = (status > 0) fun getManualSave(): DiskPair? { if (status == 0) return null return DiskPair(manualPlayer!!, manualWorld!!) } fun getAutoSave(): DiskPair? { if (status != 2) return null return DiskPair(autoPlayer!!, autoWorld!!) } fun getLoadableSave(): DiskPair? { if (status == 0) return null return if (manualPlayer != null && manualWorld != null) DiskPair(manualPlayer!!, manualWorld!!) else DiskPair(autoPlayer!!, autoWorld!!) } } data class DiskPair(val player: DiskSkimmer, val world: DiskSkimmer)