diff --git a/assets/locales/en/terrarum.json b/assets/locales/en/terrarum.json index 7d11f3696..a36258460 100644 --- a/assets/locales/en/terrarum.json +++ b/assets/locales/en/terrarum.json @@ -14,7 +14,7 @@ "GAME_32BIT_WARNING2": "Please download and install the latest 64-Bit Java at:", "GAME_32BIT_WARNING3": "https://www.java.com/en/download/", "GAME_APPLE_ROSETTA_WARNING1": "It seems you are using a Mac with Apple Silicon but running the x86 build of the game.", - "GAME_APPLE_ROSETTA_WARNING2": "Please use the native build for improved performance and the gameplay experiences.", + "GAME_APPLE_ROSETTA_WARNING2": "Please use the native build for improved performance and gameplay experiences.", "MENU_OPTION_STREAMERS_LAYOUT": "Chat Overlay", "MENU_LABEL_RESTART_REQUIRED": "Restart Required", "MENU_LABEL_KEYBOARD_LAYOUT": "Keyboard Layout", @@ -33,5 +33,8 @@ "MENU_OPTIONS_JVM_HEAP_MAX": "Max JVM Heap Memory", "MENU_OPTIONS_AUTOSAVE": "Autosave", "CONTEXT_TIME_MINUTE_PLURAL": "Minutes", - "MENU_LABEL_SYSTEM_INFO": "System Info" + "MENU_LABEL_SYSTEM_INFO": "System Info", + "GAME_PREV_SAVE_WAS_LOADED": "The most recently saved game was corrupted.\nThe previously saved game was loaded.", + "GAME_MORE_RECENT_AUTOSAVE1": "The Autosave is more recent than the manual save.", + "GAME_MORE_RECENT_AUTOSAVE2": "Please select the saved game you want to load:" } \ No newline at end of file diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index ed53accca..fbebc56d6 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -207,11 +207,11 @@ public class App implements ApplicationListener { * Sorted by the lastplaytime, in reverse order (index 0 is the most recent game played) */ public static ArrayList sortedSavegameWorlds = new ArrayList(); - public static HashMap savegameWorlds = new HashMap<>(); // UNSORTED even with the TreeMap + public static HashMap savegameWorlds = new HashMap<>(); // UNSORTED even with the TreeMap public static HashMap savegameWorldsName = new HashMap<>(); public static ArrayList sortedPlayers = new ArrayList(); - public static HashMap savegamePlayers = new HashMap<>(); + public static HashMap savegamePlayers = new HashMap<>(); public static HashMap savegamePlayersName = new HashMap<>(); public static void updateListOfSavegames() { diff --git a/src/net/torvald/terrarum/SavegameCollection.kt b/src/net/torvald/terrarum/SavegameCollection.kt new file mode 100644 index 000000000..c32b378e8 --- /dev/null +++ b/src/net/torvald/terrarum/SavegameCollection.kt @@ -0,0 +1,37 @@ +package net.torvald.terrarum + +import net.torvald.terrarum.savegame.DiskSkimmer +import java.io.File + +/** + * 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() } + /** 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() + } + +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/Terrarum.kt b/src/net/torvald/terrarum/Terrarum.kt index 2af728364..44cf84940 100644 --- a/src/net/torvald/terrarum/Terrarum.kt +++ b/src/net/torvald/terrarum/Terrarum.kt @@ -31,6 +31,8 @@ import net.torvald.terrarum.savegame.DiskSkimmer import net.torvald.terrarum.savegame.VDFileID.SAVEGAMEINFO import net.torvald.terrarum.serialise.Common import net.torvald.terrarum.ui.UICanvas +import net.torvald.terrarum.utils.JsonFetcher +import net.torvald.terrarum.utils.forEachSiblings import net.torvald.terrarum.worlddrawer.WorldCamera import net.torvald.terrarumsansbitmap.gdx.TerrarumSansBitmap import net.torvald.unsafe.UnsafeHelper @@ -797,15 +799,17 @@ fun AppUpdateListOfSavegames() { } }.sortedByDescending { it.getLastModifiedTime() }.forEachIndexed { index, it -> println("${index+1}.\t${it.diskFile.absolutePath}") - it.rebuild() // disk skimmer was created without initialisation, so do it now + it.rebuild() val jsonFile = it.getFile(SAVEGAMEINFO)!! - val json = JsonReader().parse(ByteArray64Reader(jsonFile.bytes, Common.CHARSET).readText()) - val worldUUID = UUID.fromString(json.getString("worldIndex")) + var worldUUID: UUID? = null + JsonFetcher.readFromJsonString(ByteArray64Reader(jsonFile.bytes, Common.CHARSET)).forEachSiblings { name, value -> + if (name == "worldIndex") worldUUID = UUID.fromString(value.asString()) + } // if multiple valid savegames with same UUID exist, only the most recent one is retained if (!App.savegameWorlds.contains(worldUUID)) { - App.savegameWorlds[worldUUID] = it + App.savegameWorlds[worldUUID] = SavegameCollection.collectFromBaseFilename(File(worldsDir), it.diskFile.name) App.savegameWorldsName[worldUUID] = it.getDiskName(Common.CHARSET) App.sortedSavegameWorlds.add(worldUUID) } @@ -827,15 +831,17 @@ fun AppUpdateListOfSavegames() { } }.sortedByDescending { it.getLastModifiedTime() }.forEachIndexed { index, it -> println("${index+1}.\t${it.diskFile.absolutePath}") - it.rebuild() // disk skimmer was created without initialisation, so do it now + it.rebuild() val jsonFile = it.getFile(SAVEGAMEINFO)!! - val json = JsonReader().parse(ByteArray64Reader(jsonFile.bytes, Common.CHARSET).readText()) - val playerUUID = UUID.fromString(json.getString("uuid")) + var playerUUID: UUID? = null + JsonFetcher.readFromJsonString(ByteArray64Reader(jsonFile.bytes, Common.CHARSET)).forEachSiblings { name, value -> + if (name == "uuid") playerUUID = UUID.fromString(value.asString()) + } // if multiple valid savegames with same UUID exist, only the most recent one is retained if (!App.savegamePlayers.contains(playerUUID)) { - App.savegamePlayers[playerUUID] = it + App.savegamePlayers[playerUUID] = SavegameCollection.collectFromBaseFilename(File(playersDir), it.diskFile.name) App.savegamePlayersName[playerUUID] = it.getDiskName(Common.CHARSET) App.sortedPlayers.add(playerUUID) } diff --git a/src/net/torvald/terrarum/modulebasegame/serialise/WriteSavegame.kt b/src/net/torvald/terrarum/modulebasegame/serialise/WriteSavegame.kt index 59ec444c8..98783b0fe 100644 --- a/src/net/torvald/terrarum/modulebasegame/serialise/WriteSavegame.kt +++ b/src/net/torvald/terrarum/modulebasegame/serialise/WriteSavegame.kt @@ -126,7 +126,7 @@ object LoadSavegame { printdbg(this, "Player localhash: ${player.localHashStr}, hasSprite: ${player.sprite != null}") val currentWorldId = player.worldCurrentlyPlaying - val worldDisk = worldDisk0 ?: App.savegameWorlds[currentWorldId]!! + val worldDisk = worldDisk0 ?: App.savegameWorlds[currentWorldId]!!.loadable() val world = ReadWorld(ByteArray64Reader(worldDisk.getFile(SAVEGAMEINFO)!!.bytes, Common.CHARSET), worldDisk.diskFile) world.layerTerrain = BlockLayer(world.width, world.height) diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt b/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt index c329fd0fe..237793d1c 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UILoadDemoSavefiles.kt @@ -36,6 +36,8 @@ import net.torvald.terrarum.ui.Movement import net.torvald.terrarum.ui.Toolkit import net.torvald.terrarum.ui.UICanvas import net.torvald.terrarum.ui.UIItem +import net.torvald.terrarum.utils.JsonFetcher +import net.torvald.terrarum.utils.forEachSiblings import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack import java.time.Instant import java.time.format.DateTimeFormatter @@ -74,12 +76,16 @@ object UILoadGovernor { } } +abstract class Advanceable : UICanvas() { + abstract fun advanceMode() +} + /** * Only works if current screen set by the App is [TitleScreen] * * Created by minjaesong on 2021-09-09. */ -class UILoadDemoSavefiles(val remoCon: UIRemoCon) : UICanvas() { +class UILoadDemoSavefiles(val remoCon: UIRemoCon) : Advanceable() { // private val hash = RandomWordsName(3) @@ -155,7 +161,7 @@ class UILoadDemoSavefiles(val remoCon: UIRemoCon) : UICanvas() { this.mode = mode } - fun advanceMode() { + override fun advanceMode() { mode += 1 uiScroll = 0f scrollFrom = 0 @@ -175,7 +181,7 @@ class UILoadDemoSavefiles(val remoCon: UIRemoCon) : UICanvas() { // read savegames var savegamesCount = 0 App.sortedSavegameWorlds.forEach { uuid -> - val skimmer = App.savegameWorlds[uuid]!! + val skimmer = App.savegameWorlds[uuid]!!.loadable() val x = uiX val y = titleTopGradEnd + cellInterval * savegamesCount try { @@ -190,7 +196,7 @@ class UILoadDemoSavefiles(val remoCon: UIRemoCon) : UICanvas() { savegamesCount = 0 App.sortedPlayers.forEach { uuid -> - val skimmer = App.savegamePlayers[uuid]!! + val skimmer = App.savegamePlayers[uuid]!!.loadable() val x = uiX val y = titleTopGradEnd + cellInterval * savegamesCount try { @@ -470,7 +476,7 @@ class UILoadDemoSavefiles(val remoCon: UIRemoCon) : UICanvas() { class UIItemPlayerCells( - parent: UILoadDemoSavefiles, + parent: Advanceable, initialX: Int, initialY: Int, val skimmer: DiskSkimmer) : UIItem(parent, initialX, initialY) { @@ -492,24 +498,22 @@ class UIItemPlayerCells( init { skimmer.getFile(SAVEGAMEINFO)?.bytes?.let { - val json = JsonReader().parse(ByteArray64Reader(it, Common.CHARSET)) + var playerUUID: UUID? = null + var worldUUID: UUID? = null + var lastPlayTime0 = 0L - playerUUID = UUID.fromString(json["uuid"]?.asString()) - val worldUUID = UUID.fromString(json["worldCurrentlyPlaying"]?.asString()) + JsonFetcher.readFromJsonString(ByteArray64Reader(it, Common.CHARSET)).forEachSiblings { name, value -> + if (name == "uuid") playerUUID = UUID.fromString(value.asString()) + if (name == "worldCurrentlyPlaying") worldUUID = UUID.fromString(value.asString()) + if (name == "totalPlayTime") totalPlayTime = parseDuration(value.asLong()) + if (name == "lastPlayTime") lastPlayTime0 = value.asLong() + } App.savegamePlayersName[playerUUID]?.let { if (it.isNotBlank()) playerName = it else "(name)" } App.savegameWorldsName[worldUUID]?.let { if (it.isNotBlank()) worldName = it } - /*json["lastPlayTime"]?.asString()?.let { - lastPlayTime = Instant.ofEpochSecond(it.toLong()) - .atZone(TimeZone.getDefault().toZoneId()) - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - }*/ - lastPlayTime = Instant.ofEpochSecond(skimmer.getLastModifiedTime()) + lastPlayTime = Instant.ofEpochSecond(lastPlayTime0) .atZone(TimeZone.getDefault().toZoneId()) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) - json["totalPlayTime"]?.asString()?.let { - totalPlayTime = parseDuration(it.toLong()) - } } } @@ -670,7 +674,7 @@ class UIItemPlayerCells( class UIItemWorldCells( - parent: UILoadDemoSavefiles, + parent: Advanceable, initialX: Int, initialY: Int, val skimmer: DiskSkimmer) : UIItem(parent, initialX, initialY) { @@ -685,9 +689,6 @@ class UIItemWorldCells( private val lastPlayedTimestamp: String init { - printdbg(this, "Rebuilding skimmer for savefile ${skimmer.diskFile.absolutePath}") - skimmer.rebuild() - metaFile = skimmer.getFile(-1) if (metaFile == null) saveDamaged = true @@ -699,14 +700,18 @@ class UIItemWorldCells( saveDamaged = saveDamaged or checkForSavegameDamage(skimmer) if (metaFile != null) { - val worldJson = JsonReader().parse(ByteArray64Reader(metaFile.bytes, Common.CHARSET)) - val lastplay_t = skimmer.getLastModifiedTime()//worldJson["lastPlayTime"].asLong() - val playtime_t = worldJson["totalPlayTime"].asLong() +// val lastplay_t = skimmer.getLastModifiedTime()//worldJson["lastPlayTime"].asLong() + var playtime_t = "" + var lastplay_t = 0L + JsonFetcher.readFromJsonString(ByteArray64Reader(metaFile.bytes, Common.CHARSET)).forEachSiblings { name, value -> + if (name == "lastPlayTime") lastplay_t = value.asLong() + if (name == "totalPlayTime") playtime_t = parseDuration(value.asLong()) + } lastPlayedTimestamp = Instant.ofEpochSecond(lastplay_t) .atZone(TimeZone.getDefault().toZoneId()) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + - "/${parseDuration(playtime_t)}" + "/$playtime_t" } else { lastPlayedTimestamp = "--:--:--/--h--m--s" diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UILoadSavegame.kt b/src/net/torvald/terrarum/modulebasegame/ui/UILoadSavegame.kt new file mode 100644 index 000000000..ead2772d5 --- /dev/null +++ b/src/net/torvald/terrarum/modulebasegame/ui/UILoadSavegame.kt @@ -0,0 +1,433 @@ +package net.torvald.terrarum.modulebasegame.ui + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input +import com.badlogic.gdx.graphics.* +import com.badlogic.gdx.graphics.g2d.SpriteBatch +import com.badlogic.gdx.graphics.g2d.TextureRegion +import com.badlogic.gdx.graphics.glutils.FrameBuffer +import com.badlogic.gdx.graphics.glutils.ShapeRenderer +import com.badlogic.gdx.utils.GdxRuntimeException +import com.badlogic.gdx.utils.JsonReader +import com.jme3.math.FastMath +import net.torvald.unicode.EMDASH +import net.torvald.unicode.getKeycapConsole +import net.torvald.unicode.getKeycapPC +import net.torvald.terrarum.* +import net.torvald.terrarum.App.printdbg +import net.torvald.terrarum.langpack.Lang +import net.torvald.terrarum.modulebasegame.ui.UIInventoryFull.Companion.CELL_COL +import net.torvald.terrarum.savegame.ByteArray64InputStream +import net.torvald.terrarum.savegame.ByteArray64Reader +import net.torvald.terrarum.savegame.DiskSkimmer +import net.torvald.terrarum.savegame.EntryFile +import net.torvald.terrarum.serialise.Common +import net.torvald.terrarum.serialise.SaveLoadError +import net.torvald.terrarum.modulebasegame.serialise.LoadSavegame +import net.torvald.terrarum.savegame.VDFileID.BODYPART_TO_ENTRY_MAP +import net.torvald.terrarum.savegame.VDFileID.SAVEGAMEINFO +import net.torvald.terrarum.savegame.VDFileID.SPRITEDEF +import net.torvald.terrarum.spriteassembler.ADProperties +import net.torvald.terrarum.spriteassembler.ADProperties.Companion.EXTRA_HEADROOM_X +import net.torvald.terrarum.spriteassembler.ADProperties.Companion.EXTRA_HEADROOM_Y +import net.torvald.terrarum.spriteassembler.AssembleFrameBase +import net.torvald.terrarum.spriteassembler.AssembleSheetPixmap +import net.torvald.terrarum.ui.Movement +import net.torvald.terrarum.ui.Toolkit +import net.torvald.terrarum.ui.UICanvas +import net.torvald.terrarum.ui.UIItem +import net.torvald.terrarum.utils.JsonFetcher +import net.torvald.terrarum.utils.forEachSiblings +import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack +import java.time.Instant +import java.time.format.DateTimeFormatter +import java.util.* +import java.util.zip.GZIPInputStream +import kotlin.math.roundToInt + + +/** + * Only works if current screen set by the App is [TitleScreen] + * + * Created by minjaesong on 2023-06-24. + */ +class UILoadSavegame(val remoCon: UIRemoCon) : Advanceable() { + +// private val hash = RandomWordsName(3) + + init { + CommonResourcePool.addToLoadingList("terrarum-defaultsavegamethumb") { + TextureRegion(Texture(Gdx.files.internal("assets/graphics/gui/savegame_thumb_placeholder.png"))) + } + CommonResourcePool.addToLoadingList("savegame_status_icon") { + TextureRegionPack("assets/graphics/gui/savegame_status_icon.tga", 24, 24) + } + CommonResourcePool.loadAll() + } + + override var width: Int + get() = Toolkit.drawWidth + set(value) {} + override var height: Int + get() = App.scr.height + set(value) {} + override var openCloseTime: Second = OPENCLOSE_GENERIC + + + private val shapeRenderer = App.makeShapeRenderer() + + + internal val uiWidth = SAVE_CELL_WIDTH + internal val uiX: Int + get() = (Toolkit.drawWidth - uiWidth) / 2 + internal val uiXdiffChatOverlay = App.scr.chatWidth / 2 + + internal val textH = App.fontGame.lineHeight.toInt() + + internal val cellGap = 20 + internal val cellInterval = cellGap + SAVE_CELL_HEIGHT + internal val gradAreaHeight = 32 + + internal val titleTextPosY: Int = App.scr.tvSafeGraphicsHeight + 10 + internal val titleTopGradStart: Int = titleTextPosY + textH + internal val titleTopGradEnd: Int = titleTopGradStart + gradAreaHeight + internal val titleBottomGradStart: Int = height - App.scr.tvSafeGraphicsHeight - gradAreaHeight + internal val titleBottomGradEnd: Int = titleBottomGradStart + gradAreaHeight + internal val controlHelperY: Int = titleBottomGradStart + gradAreaHeight - textH + + + private val controlHelp: String + get() = if (App.environment == RunningEnvironment.PC) + "${getKeycapPC(App.getConfigInt("control_key_up"))}${getKeycapPC(App.getConfigInt("control_key_down"))}" + + " ${Lang["MENU_CONTROLS_SCROLL"]}" + else + "${getKeycapConsole('R')} ${Lang["MENU_CONTROLS_SCROLL"]}" + + + private var scrollAreaHeight = height - 2 * App.scr.tvSafeGraphicsHeight - 64 + private var listScroll = 0 // only update when animation is finished + private var savesVisible = (scrollAreaHeight + cellGap) / cellInterval + + private var uiScroll = 0f + private var scrollFrom = 0 + private var scrollTarget = 0 + private var scrollAnimCounter = 0f + private val scrollAnimLen = 0.1f + + private var sliderFBO = FrameBuffer(Pixmap.Format.RGBA8888, uiWidth + 10, height, false) + + private var showSpinner = false + + private val playerCells = ArrayList() + + var mode = 0; private set// 0: show players, 1: show worlds + + override fun advanceMode() { + mode += 1 + uiScroll = 0f + scrollFrom = 0 + scrollTarget = 0 + scrollAnimCounter = 0f + loadFired = 0 + + printdbg(this, "savelist mode: $mode") + + // look for recently played world + if (mode == 1) { + UILoadGovernor.playerDisk!!.getFile(SAVEGAMEINFO)?.bytes?.let { + var worldUUID: UUID? = null + JsonFetcher.readFromJsonString(ByteArray64Reader(it, Common.CHARSET)).forEachSiblings { name, value -> + if (name == "worldCurrentlyPlaying") worldUUID = UUID.fromString(value.asString()) + } + + // TODO select the most recent loadable save by comparing manual and autosaves, NOT JUST going with loadable() + UILoadGovernor.worldDisk = App.savegameWorlds[worldUUID!!]!!.loadable() + + mode += 1 + } + } + } + + override fun show() { + try { + remoCon.handler.lockToggle() + showSpinner = true + + Thread { + // read savegames + var savegamesCount = 0 + App.sortedPlayers.forEach { uuid -> + val skimmer = App.savegamePlayers[uuid]!!.loadable() + val x = uiX + val y = titleTopGradEnd + cellInterval * savegamesCount + try { + playerCells.add(UIItemPlayerCells(this, x, y, skimmer)) + savegamesCount += 1 + } + catch (e: Throwable) { + System.err.println("[UILoadDemoSavefiles] Error while loading Player '${skimmer.diskFile.absolutePath}'") + e.printStackTrace() + } + } + + + remoCon.handler.unlockToggle() + showSpinner = false + }.start() + + } + catch (e: UninitializedPropertyAccessException) {} + } + + override fun hide() { + playerCells.forEach { it.dispose() } + playerCells.clear() + } + + private fun getCells() = playerCells + private var loadFired = 0 + private var oldMode = -1 + + private val mode1Node = Yaml(UITitleRemoConYaml.injectedMenuSingleCharSel).parse() + private val mode2Node = Yaml(UITitleRemoConYaml.injectedMenuSingleWorldSel).parse() + + private val menus = listOf(mode1Node, mode2Node) + private val titles = listOf("CONTEXT_CHARACTER", "MENU_LABEL_WORLD") + + init { + // this UI will NOT persist; the parent of the mode1Node must be set using an absolute value (e.g. treeRoot, not remoCon.currentRemoConContents) + + //printdbg(this, "UILoadDemoSaveFiles called, from:") + //printStackTrace(this) + + mode1Node.parent = remoCon.treeRoot + mode2Node.parent = mode1Node + + mode1Node.data = "MENU_MODE_SINGLEPLAYER : net.torvald.terrarum.modulebasegame.ui.UILoadDemoSavefiles" + mode2Node.data = "MENU_MODE_SINGLEPLAYER : net.torvald.terrarum.modulebasegame.ui.UILoadDemoSavefiles" + +// printdbg(this, "mode1Node parent: ${mode1Node.parent?.data}") // will be 'null' because the parent is the root node +// printdbg(this, "mode1Node data: ${mode1Node.data}") +// printdbg(this, "mode2Node data: ${mode2Node.data}") + } + + private fun modeChangedHandler(mode: Int) { + remoCon.setNewRemoConContents(menus[mode]) + } + + override fun updateUI(delta: Float) { + + if (mode < 2) { + + if (oldMode != mode) { + modeChangedHandler(mode) + oldMode = mode + } + + if (scrollTarget != listScroll) { + if (scrollAnimCounter < scrollAnimLen) { + scrollAnimCounter += delta + uiScroll = Movement.fastPullOut( + scrollAnimCounter / scrollAnimLen, + listScroll * cellInterval.toFloat(), + scrollTarget * cellInterval.toFloat() + ) + } + else { + scrollAnimCounter = 0f + listScroll = scrollTarget + uiScroll = cellInterval.toFloat() * scrollTarget + } + } + + val cells = getCells() + + for (index in 0 until cells.size) { + + + val it = cells[index] + if (index in listScroll - 2 until listScroll + savesVisible + 2) { + // re-position + it.posY = (it.initialY - uiScroll).roundToInt() + it.update(delta) + } + } + } + } + + override fun renderUI(batch: SpriteBatch, camera: Camera) { + + if (mode == 2) { + loadFired += 1 + // to hide the "flipped skybox" artefact + batch.end() + + gdxClearAndEnableBlend(.094f, .094f, .094f, 0f) + + batch.begin() + + batch.color = Color.WHITE + val txt = Lang["MENU_IO_LOADING"] + App.fontGame.draw(batch, txt, (App.scr.width - App.fontGame.getWidth(txt)) / 2f, (App.scr.height - App.fontGame.lineHeight) / 2f) + + if (loadFired == 2) { + LoadSavegame(UILoadGovernor.playerDisk!!, UILoadGovernor.worldDisk) + } + } + else { + batch.end() + + val cells = getCells() + + lateinit var savePixmap: Pixmap + sliderFBO.inAction(camera as OrthographicCamera, batch) { + gdxClearAndEnableBlend(0f, 0f, 0f, 0f) + + setCameraPosition(batch, camera, 0f, 0f) + batch.color = Color.WHITE + batch.inUse { + for (index in 0 until cells.size) { + val it = cells[index] + + if (App.getConfigBoolean("fx_streamerslayout")) + it.posX += uiXdiffChatOverlay + + if (index in listScroll - 2 until listScroll + savesVisible + 2) + it.render(batch, camera) + + if (App.getConfigBoolean("fx_streamerslayout")) + it.posX -= uiXdiffChatOverlay + } + } + savePixmap = Pixmap.createFromFrameBuffer(0, 0, sliderFBO.width, sliderFBO.height) + savePixmap.blending = Pixmap.Blending.None + } + + + // implement "wipe-out" by CPU-rendering (*deep exhale*) + //savePixmap.setColor(1f,1f,1f,0f) + savePixmap.setColor(0f, 0f, 0f, 0f) + savePixmap.fillRectangle(0, savePixmap.height - titleTopGradStart, savePixmap.width, titleTopGradStart) + // top grad + for (y in titleTopGradStart until titleTopGradEnd) { + val alpha = (y - titleTopGradStart).toFloat() / gradAreaHeight + for (x in 0 until savePixmap.width) { + val col = savePixmap.getPixel(x, savePixmap.height - y) + val blendAlpha = (col.and(0xFF) * alpha).roundToInt() + savePixmap.drawPixel(x, savePixmap.height - y, col.and(0xFFFFFF00.toInt()) or blendAlpha) + } + } + // bottom grad + for (y in titleBottomGradStart until titleBottomGradEnd) { + val alpha = 1f - ((y - titleBottomGradStart).toFloat() / gradAreaHeight) + for (x in 0 until savePixmap.width) { + val col = savePixmap.getPixel(x, savePixmap.height - y) + val blendAlpha = (col.and(0xFF) * alpha).roundToInt() + savePixmap.drawPixel(x, savePixmap.height - y, col.and(0xFFFFFF00.toInt()) or blendAlpha) + } + } + savePixmap.setColor(0f, 0f, 0f, 0f) + savePixmap.fillRectangle(0, 0, savePixmap.width, height - titleBottomGradEnd + 1) + + + + setCameraPosition(batch, camera, 0f, 0f) + val saveTex = TextureRegion(Texture(savePixmap)); saveTex.flip(false, true) + batch.inUse { + batch.draw(saveTex, (width - uiWidth - 10) / 2f, 0f) + + // draw texts + val loadGameTitleStr = Lang[titles[mode]]// + "$EMDASH$hash" + // "Game Load" + App.fontUITitle.draw(batch, loadGameTitleStr, (width - App.fontUITitle.getWidth(loadGameTitleStr)).div(2).toFloat(), titleTextPosY.toFloat()) + // Control help + App.fontGame.draw(batch, controlHelp, uiX.toFloat(), controlHelperY.toFloat()) + } + + saveTex.texture.dispose() + savePixmap.dispose() + + batch.begin() + } + } + + + override fun keyDown(keycode: Int): Boolean { + if (this.isVisible) { + val cells = getCells() + + if ((keycode == Input.Keys.UP || keycode == App.getConfigInt("control_key_up")) && scrollTarget > 0) { + scrollFrom = listScroll + scrollTarget -= 1 + scrollAnimCounter = 0f + } + else if ((keycode == Input.Keys.DOWN || keycode == App.getConfigInt("control_key_down")) && scrollTarget < cells.size - savesVisible) { + scrollFrom = listScroll + scrollTarget += 1 + scrollAnimCounter = 0f + } + } + return true + } + + override fun touchDown(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean { + getCells().forEach { it.touchDown(screenX, screenY, pointer, button) } + return true + } + + override fun touchUp(screenX: Int, screenY: Int, pointer: Int, button: Int): Boolean { + getCells().forEach { it.touchUp(screenX, screenY, pointer, button) } + return true + } + + override fun scrolled(amountX: Float, amountY: Float): Boolean { + if (this.isVisible) { + val cells = getCells() + + if (amountY <= -1f && scrollTarget > 0) { + scrollFrom = listScroll + scrollTarget -= 1 + scrollAnimCounter = 0f + } + else if (amountY >= 1f && scrollTarget < cells.size - savesVisible) { + scrollFrom = listScroll + scrollTarget += 1 + scrollAnimCounter = 0f + } + } + return true + } + + override fun endClosing(delta: Float) { + super.endClosing(delta) + listScroll = 0 + scrollTarget = 0 + uiScroll = 0f + } + + override fun dispose() { + try { shapeRenderer.dispose() } catch (e: IllegalArgumentException) {} + try { sliderFBO.dispose() } catch (e: IllegalArgumentException) {} + } + + override fun resize(width: Int, height: Int) { + super.resize(width, height) + scrollAreaHeight = height - 2 * App.scr.tvSafeGraphicsHeight - 64 + savesVisible = (scrollAreaHeight + cellInterval) / (cellInterval + SAVE_CELL_HEIGHT) + + listScroll = 0 + scrollTarget = 0 + uiScroll = 0f + + sliderFBO.dispose() + sliderFBO = FrameBuffer(Pixmap.Format.RGBA8888, uiWidth + 10, height, false) + } + + private fun setCameraPosition(batch: SpriteBatch, camera: Camera, newX: Float, newY: Float) { + camera.position.set((-newX + App.scr.halfw).round(), (-newY + App.scr.halfh).round(), 0f) + camera.update() + batch.projectionMatrix = camera.combined + } + +} + diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UITitleRemoConYaml.kt b/src/net/torvald/terrarum/modulebasegame/ui/UITitleRemoConYaml.kt index e5b3791d9..a71b40508 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UITitleRemoConYaml.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UITitleRemoConYaml.kt @@ -12,7 +12,7 @@ object UITitleRemoConYaml { * The class must be the UICanvas */ val menuBase = """ -- MENU_MODE_SINGLEPLAYER : net.torvald.terrarum.modulebasegame.ui.UILoadDemoSavefiles +- MENU_MODE_SINGLEPLAYER : net.torvald.terrarum.modulebasegame.ui.UILoadSavegame - MENU_OPTIONS - MENU_LABEL_GRAPHICS : net.torvald.terrarum.modulebasegame.ui.UIGraphicsControlPanel - MENU_OPTIONS_CONTROLS : net.torvald.terrarum.modulebasegame.ui.UIKeyboardControlPanel diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt index fdba385af..b7b8c245a 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalListing.kt @@ -174,8 +174,9 @@ class UIWorldPortalListing(val full: UIWorldPortal) : UICanvas() { worldList.clear() (INGAME.actorGamer.actorValue.getAsString(AVKey.WORLD_PORTAL_DICT) ?: "").split(",").filter { it.isNotBlank() }.map { it.ascii85toUUID().let { it to App.savegameWorlds[it] } - }.filter { it.second != null }.mapIndexed { index, (uuid, disk) -> + }.filter { it.second != null }.mapIndexed { index, (uuid, disk0) -> + val disk = disk0!!.loadable() var chunksCount = 0 var seed = 0L var lastPlayed = 0L diff --git a/src/net/torvald/terrarum/savegame/DiskSkimmer.kt b/src/net/torvald/terrarum/savegame/DiskSkimmer.kt index 34b38e4b2..674e802c8 100644 --- a/src/net/torvald/terrarum/savegame/DiskSkimmer.kt +++ b/src/net/torvald/terrarum/savegame/DiskSkimmer.kt @@ -50,13 +50,13 @@ removefile: fun checkFileSanity() { if (!diskFile.exists()) throw NoSuchFileException(diskFile.absoluteFile) - if (diskFile.length() < 310L) throw RuntimeException("Invalid Virtual Disk file!") + if (diskFile.length() < 310L) throw RuntimeException("Invalid Virtual Disk file: ${diskFile.path}") // check magic val fis = FileInputStream(diskFile) val magic = ByteArray(4).let { fis.read(it); it } - if (!magic.contentEquals(VirtualDisk.MAGIC)) throw RuntimeException("Invalid Virtual Disk file!") + if (!magic.contentEquals(VirtualDisk.MAGIC)) throw RuntimeException("Invalid Virtual Disk file: ${diskFile.path}") fis.close() }