diff --git a/assets/mods/basegame/locales/en/sentences.json b/assets/mods/basegame/locales/en/sentences.json index 812c07ce4..de775ae98 100644 --- a/assets/mods/basegame/locales/en/sentences.json +++ b/assets/mods/basegame/locales/en/sentences.json @@ -1,5 +1,6 @@ { "CONTEXT_THIS_IS_A_WORLD_CURRENTLY_PLAYING": "This is a world currently playing.", - "CONTEXT_IMPORT_AVATAR_INSTRUCTION_1": "Copy the Avatar Code into the clipboard, then hit the Paste button below.", - "CONTEXT_IMPORT_AVATAR_INSTRUCTION_2": "" + "CONTEXT_IMPORT_AVATAR_INSTRUCTION_1": "1. Place the Avatar file into the following directory:", + "CONTEXT_IMPORT_AVATAR_INSTRUCTION_2": "", + "CONTEXT_IMPORT_AVATAR_INSTRUCTION_3": "2. Enter the name of the file below, then press Import" } \ No newline at end of file diff --git a/assets/mods/basegame/locales/koKR/sentences.json b/assets/mods/basegame/locales/koKR/sentences.json index f6129f9aa..5b11d6022 100644 --- a/assets/mods/basegame/locales/koKR/sentences.json +++ b/assets/mods/basegame/locales/koKR/sentences.json @@ -1,5 +1,5 @@ { "CONTEXT_THIS_IS_A_WORLD_CURRENTLY_PLAYING": "현재 플레이 중인 월드입니다.", - "CONTEXT_IMPORT_AVATAR_INSTRUCTION_1": "아바타 코드를 클립보드에 복사한 다음, 아래의 붙여넣기 버튼을 눌러주세요.", - "CONTEXT_IMPORT_AVATAR_INSTRUCTION_2": "" + "CONTEXT_IMPORT_AVATAR_INSTRUCTION_1": "1. 아바타 파일을 다음 폴더에 넣어주세요", + "CONTEXT_IMPORT_AVATAR_INSTRUCTION_3": "2. 아바타 파일 이름을 아래에 입력하고 가져오기를 눌러주세요" } \ No newline at end of file diff --git a/src/net/torvald/terrarum/App.java b/src/net/torvald/terrarum/App.java index 94df16497..e25db0774 100644 --- a/src/net/torvald/terrarum/App.java +++ b/src/net/torvald/terrarum/App.java @@ -1185,6 +1185,8 @@ public class App implements ApplicationListener { public static String configDir; /** defaultDir + "/LoadOrder.txt" */ public static String loadOrderDir; + /** defaultDir + "/Imported" */ + public static String importDir; public static RunningEnvironment environment; @@ -1224,6 +1226,7 @@ public class App implements ApplicationListener { loadOrderDir = defaultDir + "/LoadOrder.txt"; recycledPlayersDir = defaultDir + "/Recycled/Players"; recycledWorldsDir = defaultDir + "/Recycled/Worlds"; + importDir = defaultDir + "/Imports"; System.out.println(String.format("os.name = %s (with identifier %s)", OSName, operationSystem)); System.out.println(String.format("os.version = %s", OSVersion)); @@ -1239,6 +1242,7 @@ public class App implements ApplicationListener { new File(worldsDir), new File(recycledPlayersDir), new File(recycledWorldsDir), + new File(importDir) }; for (File it : dirs) { diff --git a/src/net/torvald/terrarum/NoModuleDefaultTitlescreen.kt b/src/net/torvald/terrarum/NoModuleDefaultTitlescreen.kt index d2fc1e193..1225d5ca6 100644 --- a/src/net/torvald/terrarum/NoModuleDefaultTitlescreen.kt +++ b/src/net/torvald/terrarum/NoModuleDefaultTitlescreen.kt @@ -8,6 +8,7 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.glutils.FrameBuffer import net.torvald.terrarum.langpack.Lang import net.torvald.terrarum.ui.Toolkit +import net.torvald.terrarum.utils.OpenFile import java.awt.Desktop import java.io.File @@ -90,7 +91,7 @@ class NoModuleDefaultTitlescreen(batch: FlippingSpriteBatch) : IngameInstance(ba App.scr.hf - Gdx.input.y in pathButtonY - 12..pathButtonY + pathButtonH + 12) if (mouseOnLink && Gdx.input.isButtonJustPressed(Input.Buttons.LEFT)) { - Desktop.getDesktop().open(pathFile) + OpenFile(pathFile) } fbatch.inUse { diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt index 8cca62ba6..b1bca8749 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt @@ -6,15 +6,22 @@ import com.badlogic.gdx.graphics.Camera import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.g2d.SpriteBatch import net.torvald.terrarum.App +import net.torvald.terrarum.App.printdbg +import net.torvald.terrarum.AppUpdateListOfSavegames import net.torvald.terrarum.Second import net.torvald.terrarum.ceilToInt import net.torvald.terrarum.gamecontroller.* import net.torvald.terrarum.langpack.Lang -import net.torvald.terrarum.serialise.Ascii85Codec -import net.torvald.terrarum.ui.Toolkit -import net.torvald.terrarum.ui.UIItem -import net.torvald.terrarum.ui.UIItemTextButton +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.ui.* import net.torvald.terrarum.utils.Clipboard +import net.torvald.terrarum.utils.JsonFetcher +import net.torvald.terrarum.utils.OpenFile +import java.awt.Desktop +import java.io.File /** * Created by minjaesong on 2023-08-24. @@ -31,14 +38,23 @@ class UIImportAvatar(val remoCon: UIRemoCon) : Advanceable() { private val rows = 30 private val goButtonWidth = 180 + private val descStartY = 24 * 4 + private val lh = App.fontGame.lineHeight.toInt() - private val codeBox = UIItemCodeBox(this, (Toolkit.drawWidth - App.fontSmallNumbers.W * cols) / 2, drawY, cols, rows) +// private val codeBox = UIItemCodeBox(this, (Toolkit.drawWidth - App.fontSmallNumbers.W * cols) / 2, drawY, cols, rows) + private val inputWidth = 340 + private val filenameInput = UIItemTextLineInput(this, + (Toolkit.drawWidth - inputWidth) / 2, (App.scr.height - height) / 2 + descStartY + (5) * lh, inputWidth, + maxLen = InputLenCap(256, InputLenCap.CharLenUnit.UTF8_BYTES) + ) + + /* private val clearButton = UIItemTextButton(this, { Lang["MENU_IO_CLEAR"] }, drawX + (width/2 - goButtonWidth) / 2, drawY + height - 24 - 34, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true) private val pasteButton = UIItemTextButton(this, { Lang["MENU_LABEL_PASTE"] }, drawX + width/2 + (width/2 - goButtonWidth) / 2, drawY + height - 24 - 34, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true) - + */ private val backButton = UIItemTextButton(this, { Lang["MENU_LABEL_BACK"] }, drawX + (width/2 - goButtonWidth) / 2, drawY + height - 24, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true) @@ -46,32 +62,63 @@ class UIImportAvatar(val remoCon: UIRemoCon) : Advanceable() { { Lang["MENU_IO_IMPORT"] }, drawX + width/2 + (width/2 - goButtonWidth) / 2, drawY + height - 24, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true) init { - addUIitem(codeBox) - addUIitem(clearButton) - addUIitem(pasteButton) +// addUIitem(codeBox) +// addUIitem(clearButton) +// addUIitem(pasteButton) + addUIitem(filenameInput) addUIitem(backButton) addUIitem(goButton) - clearButton.clickOnceListener = { _,_ -> + /*clearButton.clickOnceListener = { _,_ -> codeBox.clearTextBuffer() } pasteButton.clickOnceListener = { _,_ -> codeBox.pasteFromClipboard() - } + }*/ backButton.clickOnceListener = { _,_ -> remoCon.openUI(UILoadSavegame(remoCon)) } goButton.clickOnceListener = { _,_ -> - doImport() + val returnCode = doImport() + if (returnCode == 0) remoCon.openUI(UILoadSavegame(remoCon)) } } +// private var textX = 0 + private var textY = 0 + private var mouseOnLink = false + private var pathW = 0 override fun updateUI(delta: Float) { uiItems.forEach { it.update(delta) } + + + pathW = App.fontGame.getWidth(App.importDir) + val textX = (Toolkit.drawWidth - pathW) / 2 + textY = (App.scr.height - height) / 2 + descStartY + (1) * lh + mouseOnLink = (Gdx.input.x in textX - 48..textX + 48 + pathW && + Gdx.input.y in textY - 12..textY + lh + 12) + + if (mouseOnLink && Gdx.input.isButtonJustPressed(Input.Buttons.LEFT)) { + OpenFile(File(App.importDir)) + } } + private val textboxIndices = (1..3) + override fun renderUI(batch: SpriteBatch, camera: Camera) { + batch.color = Color.WHITE + val textboxWidth = textboxIndices.maxOf { App.fontGame.getWidth(Lang["CONTEXT_IMPORT_AVATAR_INSTRUCTION_$it"]) } + val textX = (Toolkit.drawWidth - textboxWidth) / 2 + // draw texts + for (i in textboxIndices) { + App.fontGame.draw(batch, Lang["CONTEXT_IMPORT_AVATAR_INSTRUCTION_$i"], textX, (App.scr.height - height) / 2 + descStartY + (i - 1) * lh) + } + // draw path + batch.color = if (mouseOnLink) Toolkit.Theme.COL_SELECTED else Toolkit.Theme.COL_MOUSE_UP + App.fontGame.draw(batch, App.importDir, (Toolkit.drawWidth - pathW) / 2, textY) + + uiItems.forEach { it.render(batch, camera) } } @@ -81,13 +128,68 @@ class UIImportAvatar(val remoCon: UIRemoCon) : Advanceable() { override fun advanceMode(button: UIItem) { } - private fun doImport() { - val rawStr = codeBox.textBuffer.toString() - // sanity check + private fun doImport(): Int { + val file = File("${App.importDir}/${filenameInput.getText()}") + + // check file's existence + if (!file.exists()) { + return 1 + } + + // try to mount the TEVd + try { + val dom = VDUtil.readDiskArchive(file) + val timeNow = App.getTIME_T() + + // get the uuid + val oldPlayerInfoFile = dom.getEntry(SAVEGAMEINFO)!! + val playerInfo = JsonFetcher.readFromJsonString(ByteArray64Reader(VDUtil.getAsNormalFile(dom, SAVEGAMEINFO).bytes, Common.CHARSET)) + val uuid = playerInfo.getString("uuid") + val newFile = File("${App.playersDir}/$uuid") + + printdbg(this, "Avatar uuid: $uuid") + + if (newFile.exists()) return 2 + + // update playerinfo so that: + // totalPlayTime to zero + // lastPlayedTime to now + // playerinfofile's lastModifiedTime to now + // root's lastModifiedTime to now + printdbg(this, "avatar old lastPlayTime: ${playerInfo.getLong("lastPlayTime")}") + printdbg(this, "avatar old totalPlayTime: ${playerInfo.getLong("totalPlayTime")}") + playerInfo.get("lastPlayTime").set(timeNow, null) + playerInfo.get("totalPlayTime").set(0, null) + printdbg(this, "avatar new lastPlayTime: ${playerInfo.getLong("lastPlayTime")}") + printdbg(this, "avatar new totalPlayTime: ${playerInfo.getLong("totalPlayTime")}") + + val newJsonBytes = ByteArray64Writer(Common.CHARSET).let { +// println(playerInfo.toString()) + it.write(playerInfo.toString()) + it.close() + it.toByteArray64() + } + val newPlayerInfo = DiskEntry(SAVEGAMEINFO, ROOT, oldPlayerInfoFile.creationDate, timeNow, EntryFile(newJsonBytes)) + VDUtil.addFile(dom, newPlayerInfo) + + dom.getEntry(ROOT)!!.modificationDate = timeNow + + // mark the file as Imported + dom.saveOrigin = VDSaveOrigin.IMPORTED + + // write modified file to the Players dir + VDUtil.dumpToRealMachine(dom, newFile) - val ascii85codec = Ascii85Codec((33..117).map { it.toChar() }.joinToString("")) - val ascii85str = rawStr.substring(2 until rawStr.length - 2).replace("z", "!!!!!") + AppUpdateListOfSavegames() + } + catch (e: Throwable) { + // format error + e.printStackTrace() + return -1 + } + + return 0 } } diff --git a/src/net/torvald/terrarum/savegame/DiskSkimmer.kt b/src/net/torvald/terrarum/savegame/DiskSkimmer.kt index 73a94a591..ec630f232 100644 --- a/src/net/torvald/terrarum/savegame/DiskSkimmer.kt +++ b/src/net/torvald/terrarum/savegame/DiskSkimmer.kt @@ -447,6 +447,13 @@ removefile: fa.close() } + fun setSaveOrigin(bits: Int) { + val fa = RandomAccessFile(diskFile, "rwd") + fa.seek(51L) + fa.writeByte(bits) + fa.close() + } + /** * @return Save type (0b 0000 00ab) * b: unset - full save; set - quicksave (only applicable to worlds -- quicksave just means the disk is in dirty state) @@ -467,6 +474,15 @@ removefile: return fa.read().also { fa.close() } } + /** + * @return 16 if the savegame was imported, 0 if the savegame was generated in-game + */ + fun getSaveOrigin(): Int { + val fa = RandomAccessFile(diskFile, "rwd") + fa.seek(51L) + return fa.read().also { fa.close() } + } + override fun getDiskName(charset: Charset): String { diff --git a/src/net/torvald/terrarum/savegame/VirtualDisk.kt b/src/net/torvald/terrarum/savegame/VirtualDisk.kt index 450bb07fd..fdb77218e 100644 --- a/src/net/torvald/terrarum/savegame/VirtualDisk.kt +++ b/src/net/torvald/terrarum/savegame/VirtualDisk.kt @@ -76,8 +76,11 @@ Version 254 is a customised version of TEVD tailored to be used as a savegame fo 0: Undefined (or very old version of the game) 1: Player Data 2: World Data - Int8[13] Extra info bytes reserved for future usage - /* END extraInfoBytes */ + Int8 Savefile Origin Flags + 0: Created in-game + 16: Imported + Int8[12] Extra info bytes reserved for future usage + -- END extraInfoBytes -- UInt8[236] Rest of the long disk name (268 bytes total) (Header size: 300 bytes) @@ -150,6 +153,9 @@ class VirtualDisk( var saveKind: Int set(value) { extraInfoBytes[2] = value.toByte() } get() = extraInfoBytes[2].toUint() + var saveOrigin: Int + set(value) { extraInfoBytes[3] = value.toByte() } + get() = extraInfoBytes[3].toUint() override fun getDiskName(charset: Charset) = diskName.toCanonicalString(charset) val root: DiskEntry get() = entries[0]!! @@ -249,6 +255,11 @@ object VDSaveKind { const val WORLD_DATA = 2 } +object VDSaveOrigin { + const val INGAME = 0 + const val IMPORTED = 16 +} + object VDFileID { const val ROOT = 0L const val SAVEGAMEINFO = -1L diff --git a/src/net/torvald/terrarum/utils/Clipboard.kt b/src/net/torvald/terrarum/utils/Clipboard.kt index be03ec9b9..ecbe71662 100644 --- a/src/net/torvald/terrarum/utils/Clipboard.kt +++ b/src/net/torvald/terrarum/utils/Clipboard.kt @@ -1,24 +1,43 @@ package net.torvald.terrarum.utils +import net.torvald.terrarum.App +import java.awt.Desktop import java.awt.Toolkit import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.StringSelection import java.awt.datatransfer.UnsupportedFlavorException +import java.io.File /** * Created by minjaesong on 2016-07-31. */ object Clipboard { - fun fetch(): String = try { - Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as String - } - catch (e: UnsupportedFlavorException) { - "" - } + private val IS_MACOS = App.operationSystem == "OSX" + + fun fetch(): String = + if (IS_MACOS) "Clipboard is disabled on macOS" else + try { + Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as String + } + catch (e: UnsupportedFlavorException) { + "" + } fun copy(s: String) { + if (IS_MACOS) return val selection = StringSelection(s) val clipboard = Toolkit.getDefaultToolkit().systemClipboard clipboard.setContents(selection, selection) } +} + +/** + * Created by minjaesong on 2023-08-25. + */ +object OpenFile { + private val IS_MACOS = App.operationSystem == "OSX" + operator fun invoke(file: File) { + if (IS_MACOS) return // at this point macOS might as well be a bane of existence for "some" devs Apple fanboys think they are not worthy of existence + Desktop.getDesktop().open(file) + } } \ No newline at end of file