diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index 104a7a03a..55b43ab8d 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -16,6 +16,7 @@ import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.JsonValue; import com.github.strikerx3.jxinput.XInputDevice; +import kotlin.Pair; import net.torvald.gdx.graphics.PixmapIO2; import net.torvald.getcpuname.GetCpuName; import net.torvald.terrarum.concurrent.ThreadExecutor; @@ -31,6 +32,7 @@ import net.torvald.terrarum.modulebasegame.IngameRenderer; import net.torvald.terrarum.modulebasegame.TerrarumIngame; import net.torvald.terrarum.modulebasegame.ui.ItemSlotImageFactory; import net.torvald.terrarum.serialise.WriteConfig; +import net.torvald.terrarum.serialise.WriteMeta; import net.torvald.terrarum.tvda.VirtualDisk; import net.torvald.terrarum.utils.JsonFetcher; import net.torvald.terrarum.worlddrawer.CreateTileAtlas; @@ -194,7 +196,7 @@ public class App implements ApplicationListener { /** * Sorted by the lastplaytime, in reverse order (index 0 is the most recent game played) */ - public static ArrayList savegames = new ArrayList<>(); + public static ArrayList> savegames = new ArrayList<>(); public static void updateListOfSavegames() { AppUpdateListOfSavegames(); diff --git a/src/net/torvald/terrarum/DefaultConfig.kt b/src/net/torvald/terrarum/DefaultConfig.kt index f31738db4..e9955b090 100644 --- a/src/net/torvald/terrarum/DefaultConfig.kt +++ b/src/net/torvald/terrarum/DefaultConfig.kt @@ -17,7 +17,8 @@ object DefaultConfig { "atlastexsize" to 2048, "language" to App.getSysLang(), - "notificationshowuptime" to 4000, + "notificationshowuptime" to 4096, // 4s + "autosaveinterval" to 262144, // 4m22s "multithread" to true, "showhealthmessageonstartup" to true, diff --git a/src/net/torvald/terrarum/Terrarum.kt b/src/net/torvald/terrarum/Terrarum.kt index c3be85e52..a9e50fac1 100644 --- a/src/net/torvald/terrarum/Terrarum.kt +++ b/src/net/torvald/terrarum/Terrarum.kt @@ -26,6 +26,8 @@ import net.torvald.terrarum.gameworld.fmod import net.torvald.terrarum.itemproperties.ItemCodex import net.torvald.terrarum.itemproperties.MaterialCodex import net.torvald.terrarum.serialise.Common +import net.torvald.terrarum.serialise.ReadMeta +import net.torvald.terrarum.tvda.DiskSkimmer import net.torvald.terrarum.tvda.EntryFile import net.torvald.terrarum.tvda.VDUtil import net.torvald.terrarum.tvda.VirtualDisk @@ -685,24 +687,29 @@ fun AppUpdateListOfSavegames() { App.savegames.clear() File(App.defaultSaveDir).listFiles().filter { !it.isDirectory && !it.name.contains('.') }.map { file -> try { - VDUtil.readDiskArchive(file, Level.INFO) { - printdbgerr("Terrarum", "Possibly corrupted savefile '${file.absolutePath}':\n$it") + DiskSkimmer(file, Common.CHARSET) { it.containsKey(-1) }.requestFile(-1)?.let { + file to ReadMeta.fromDiskEntry(it) } } catch (e: Throwable) { + System.err.println("Unable to load a savefile ${file.absolutePath}") e.printStackTrace() null } - }.filter { it != null && it.entries.contains(-1) } - .sortedByDescending { (it as VirtualDisk).entries[-1]!!.modificationDate }.forEach { + }.filter { it != null }.sortedByDescending { it!!.second.lastplay_t }.forEach { App.savegames.add(it!!) } } -fun checkForSavegameDamage(disk: VirtualDisk): Boolean { +/** + * @param skimmer loaded with the savefile + */ +fun checkForSavegameDamage(skimmer: DiskSkimmer): Boolean { // # check if The Player is there - val player = disk.entries[PLAYER_REF_ID.toLong().and(0xFFFFFFFFL)] ?: return true - // # check if the world The Player is at actually exists + val player = skimmer.requestFile(PLAYER_REF_ID.toLong().and(0xFFFFFFFFL))?.contents ?: return true + // # check if: + // the world The Player is at actually exists + // all the actors for the world actually exists val currentWorld = (player as EntryFile).bytes.let { val maxsize = 1 shl 30 val worldIndexRegex = Regex("""worldIndex: ?([0-9]+)""") @@ -711,6 +718,8 @@ fun checkForSavegameDamage(disk: VirtualDisk): Boolean { // todo } +// skimmer.requestFile(367228416) ?: return true + return false } \ No newline at end of file diff --git a/src/net/torvald/terrarum/gameworld/GameWorld.kt b/src/net/torvald/terrarum/gameworld/GameWorld.kt index 197ebff58..a72c9994c 100644 --- a/src/net/torvald/terrarum/gameworld/GameWorld.kt +++ b/src/net/torvald/terrarum/gameworld/GameWorld.kt @@ -128,7 +128,7 @@ open class GameWorld() : Disposable { // preliminary spawn points this.spawnX = width / 2 - this.spawnY = 200 + this.spawnY = 150 layerTerrain = BlockLayer(width, height) layerWall = BlockLayer(width, height) diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index 4872d8f66..123028b91 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -285,8 +285,14 @@ open class TerrarumIngame(batch: SpriteBatch) : IngameInstance(batch) { /** Load rest of the game with GL context */ private fun postInitForLoadFromSave(codices: Codices) { codices.actors.forEach { - val actor = ReadActor(LoadSavegame.getFileReader(codices.disk, it.toLong())) - addNewActor(actor) + try { + val actor = ReadActor(LoadSavegame.getFileReader(codices.disk, it.toLong())) + addNewActor(actor) + } + catch (e: NullPointerException) { + System.err.println("Could not read the actor ${it} from the disk") +// throw e // TODO don't rethrow -- let players play the corrupted world if it loads, they'll be able to cope with their losses even though there will be buncha lone actorblocks lying around... + } } // by doing this, whatever the "possession" the player had will be broken by the game load diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt b/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt index 547e46701..e809d17ca 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt @@ -14,13 +14,14 @@ import net.torvald.terrarum.langpack.Lang import net.torvald.terrarum.serialise.Common import net.torvald.terrarum.serialise.LoadSavegame import net.torvald.terrarum.serialise.ReadMeta -import net.torvald.terrarum.tvda.ByteArray64InputStream -import net.torvald.terrarum.tvda.VDUtil -import net.torvald.terrarum.tvda.VirtualDisk +import net.torvald.terrarum.serialise.WriteMeta +import net.torvald.terrarum.tvda.* import net.torvald.terrarum.ui.* +import java.io.File import java.time.Instant import java.time.format.DateTimeFormatter import java.util.* +import java.util.logging.Level import java.util.zip.GZIPInputStream import kotlin.math.roundToInt @@ -74,10 +75,10 @@ class UILoadDemoSavefiles : UICanvas() { // read savegames init { - App.savegames.forEachIndexed { index, disk -> + App.savegames.forEachIndexed { index, fileMetaPair -> val x = uiX val y = titleTopGradEnd + cellInterval * index - addUIitem(UIItemDemoSaveCells(this, x, y, disk as VirtualDisk)) + addUIitem(UIItemDemoSaveCells(this, x, y, fileMetaPair.first, fileMetaPair.second)) } } @@ -275,7 +276,9 @@ class UIItemDemoSaveCells( parent: UILoadDemoSavefiles, initialX: Int, initialY: Int, - val disk: VirtualDisk) : UIItem(parent, initialX, initialY) { + val diskfile: File, val meta: WriteMeta.WorldMeta) : UIItem(parent, initialX, initialY) { + + private val skimmer = DiskSkimmer(diskfile, Common.CHARSET) companion object { const val WIDTH = 480 @@ -289,7 +292,7 @@ class UIItemDemoSaveCells( private var thumb: TextureRegion? = null private val grad = CommonResourcePool.getAsTexture("title_halfgrad") - private val meta = ReadMeta(disk) + private var saveDamaged = checkForSavegameDamage(skimmer) private fun parseDuration(seconds: Long): String { val s = seconds % 60 @@ -302,6 +305,9 @@ class UIItemDemoSaveCells( "${d}d${h.toString().padStart(2,'0')}h${m.toString().padStart(2,'0')}m${s.toString().padStart(2,'0')}s" } + private val saveName = skimmer.getDiskName(Common.CHARSET) + private val saveMode = skimmer.getSaveMode() + private val lastPlayedTimestamp = Instant.ofEpochSecond(meta.lastplay_t) .atZone(TimeZone.getDefault().toZoneId()) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + @@ -310,7 +316,7 @@ class UIItemDemoSaveCells( init { try { // load a thumbnail - val zippedTga = VDUtil.getAsNormalFile(disk, -2).getContent() + val zippedTga = (skimmer.requestFile(-2)!!.contents as EntryFile).bytes val gzin = GZIPInputStream(ByteArray64InputStream(zippedTga)) val tgaFileContents = gzin.readAllBytes(); gzin.close() @@ -328,7 +334,7 @@ class UIItemDemoSaveCells( } override var clickOnceListener: ((Int, Int, Int) -> Unit)? = { _: Int, _: Int, _: Int -> - LoadSavegame(disk) + LoadSavegame(VDUtil.readDiskArchive(diskfile, Level.INFO)) } override fun render(batch: SpriteBatch, camera: Camera) { @@ -357,10 +363,10 @@ class UIItemDemoSaveCells( val tlen = App.fontSmallNumbers.getWidth(lastPlayedTimestamp) App.fontSmallNumbers.draw(batch, lastPlayedTimestamp, x + (width - tlen) - 3f, y + height - 16f) // file size - App.fontSmallNumbers.draw(batch, "${disk.usedBytes.ushr(10)} KiB", x + 3f, y + height - 16f) + App.fontSmallNumbers.draw(batch, "${diskfile.length().ushr(10)} KiB", x + 3f, y + height - 16f) // savegame name - val diskName = disk.getDiskNameString(Common.CHARSET) - App.fontGame.draw(batch, diskName + "${if (disk.saveMode % 2 == 1) "*" else ""}", x + 3f, y + 1f) + if (saveDamaged) batch.color = Color.RED + App.fontGame.draw(batch, saveName + "${if (saveMode % 2 == 1) "*" else ""}", x + 3f, y + 1f) super.render(batch, camera) batch.color = Color.WHITE diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIProxyLoadLatestSave.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIProxyLoadLatestSave.kt index 772af9fdb..a220996bb 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIProxyLoadLatestSave.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIProxyLoadLatestSave.kt @@ -5,7 +5,9 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch import net.torvald.terrarum.App import net.torvald.terrarum.Second import net.torvald.terrarum.serialise.LoadSavegame +import net.torvald.terrarum.tvda.VDUtil import net.torvald.terrarum.ui.UICanvas +import java.util.logging.Level /** * Created by minjaesong on 2021-09-13. @@ -30,7 +32,9 @@ class UIProxyLoadLatestSave : UICanvas() { override fun endOpening(delta: Float) { if (App.savegames.size > 0) { - LoadSavegame(App.savegames[0]) + LoadSavegame(VDUtil.readDiskArchive(App.savegames[0].first, Level.INFO) { + System.err.println("Possibly damaged savefile ${App.savegames[0].first.absolutePath}:\n$it") + }) } } diff --git a/src/net/torvald/terrarum/serialise/WriteMeta.kt b/src/net/torvald/terrarum/serialise/WriteMeta.kt index 04134adfb..ad7b477a0 100644 --- a/src/net/torvald/terrarum/serialise/WriteMeta.kt +++ b/src/net/torvald/terrarum/serialise/WriteMeta.kt @@ -5,10 +5,7 @@ import net.torvald.terrarum.ModMgr import net.torvald.terrarum.gameactors.ActorID import net.torvald.terrarum.modulebasegame.TerrarumIngame import net.torvald.terrarum.modulebasegame.worldgenerator.RoguelikeRandomiser -import net.torvald.terrarum.tvda.ByteArray64 -import net.torvald.terrarum.tvda.ByteArray64Reader -import net.torvald.terrarum.tvda.EntryFile -import net.torvald.terrarum.tvda.VirtualDisk +import net.torvald.terrarum.tvda.* import net.torvald.terrarum.weather.WeatherMixer /** @@ -82,4 +79,9 @@ object ReadMeta { return Common.jsoner.fromJson(WriteMeta.WorldMeta::class.java, metaReader) } + fun fromDiskEntry(metaFile: DiskEntry): WriteMeta.WorldMeta { + val metaReader = ByteArray64Reader((metaFile.contents as EntryFile).getContent(), Common.CHARSET) + return Common.jsoner.fromJson(WriteMeta.WorldMeta::class.java, metaReader) + } + } \ No newline at end of file diff --git a/src/net/torvald/terrarum/tvda/DiskSkimmer.kt b/src/net/torvald/terrarum/tvda/DiskSkimmer.kt index 004f82e69..fb60cc809 100644 --- a/src/net/torvald/terrarum/tvda/DiskSkimmer.kt +++ b/src/net/torvald/terrarum/tvda/DiskSkimmer.kt @@ -17,7 +17,11 @@ import kotlin.experimental.and * * Created by minjaesong on 2017-11-17. */ -class DiskSkimmer(private val diskFile: File, val charset: Charset = Charset.defaultCharset()) { +class DiskSkimmer( + private val diskFile: File, + val charset: Charset = Charset.defaultCharset(), + private val skimmingStopFunction: (HashMap) -> Boolean = { false } +) { /* @@ -63,6 +67,10 @@ removefile: val fa = RandomAccessFile(diskFile, "rw") + private fun debugPrintln(s: Any) { + if (false) println(s.toString()) + } + init { val fis = FileInputStream(diskFile) @@ -141,11 +149,13 @@ removefile: if (typeFlag > 0) { entryToOffsetTable[entryID] = offset - println("[DiskSkimmer] successfully read the entry $entryID at offset $offset (name: ${diskIDtoReadableFilename(entryID)})") + debugPrintln("[DiskSkimmer] ... successfully read the entry $entryID at offset $offset (name: ${diskIDtoReadableFilename(entryID)})") } else { - println("[DiskSkimmer] discarding entry $entryID at offset $offset (name: ${diskIDtoReadableFilename(entryID)})") + debugPrintln("[DiskSkimmer] ... discarding entry $entryID at offset $offset (name: ${diskIDtoReadableFilename(entryID)})") } + + if (skimmingStopFunction(entryToOffsetTable)) break } } @@ -162,7 +172,7 @@ removefile: fun requestFile(entryID: EntryID): DiskEntry? { entryToOffsetTable[entryID].let { offset -> if (offset == null) { - println("[DiskSkimmer.requestFile] entry $entryID does not exist on the table") + debugPrintln("[DiskSkimmer.requestFile] entry $entryID does not exist on the table") return null } else { @@ -231,17 +241,17 @@ removefile: // fixme pretty much untested val path = path.split(dirDelim) - //println(path) + //debugPrintln(path) // bunch-of-io-access approach (for reading) var traversedDir = 0L // entry ID var dirFile: DiskEntry? = null path.forEachIndexed { index, dirName -> - println("[DiskSkimmer.requestFile] $index\t$dirName, traversedDir = $traversedDir") + debugPrintln("[DiskSkimmer.requestFile] $index\t$dirName, traversedDir = $traversedDir") dirFile = requestFile(traversedDir) if (dirFile == null) { - println("[DiskSkimmer.requestFile] requestFile($traversedDir) came up null") + debugPrintln("[DiskSkimmer.requestFile] requestFile($traversedDir) came up null") return null } // outright null if (dirFile!!.contents !is EntryDirectory && index < path.lastIndex) { // unexpectedly encountered non-directory @@ -257,7 +267,7 @@ removefile: // get name of the file val childDirFile = requestFile(it)!! if (childDirFile.filename.toCanonicalString(charset) == dirName) { - //println("[DiskSkimmer] found, $traversedDir -> $it") + //debugPrintln("[DiskSkimmer] found, $traversedDir -> $it") dirGotcha = true traversedDir = it } @@ -306,6 +316,20 @@ removefile: fa.writeByte(bits) } + fun getSaveMode(): Int { + fa.seek(49L) + return fa.read() + } + + fun getDiskName(charset: Charset): String { + val bytes = ByteArray(268) + fa.seek(10L) + fa.read(bytes, 0, 32) + fa.seek(60L) + fa.read(bytes, 32, 236) + return bytes.toCanonicalString(charset) + } + /////////////////////////////////////////////////////// // THESE ARE METHODS TO SUPPORT ON-LINE MODIFICATION // /////////////////////////////////////////////////////// @@ -409,7 +433,7 @@ removefile: val HEADER_SIZE = DiskEntry.HEADER_SIZE - println("[DiskSkimmer.getEntryBlockSize] offset for entry $id = $offset") + debugPrintln("[DiskSkimmer.getEntryBlockSize] offset for entry $id = $offset") val fis = FileInputStream(diskFile) fis.skip(offset + 8)