From c45cab388e8bef5709ba705befbb8ab0a9805bfd Mon Sep 17 00:00:00 2001 From: minjaesong Date: Mon, 4 Sep 2023 21:48:28 +0900 Subject: [PATCH] working invitation code via portal --- assets/graphics/fonts/code.tga | 2 +- .../mods/basegame/locales/en/sentences.json | 3 +- .../mods/basegame/locales/koKR/sentences.json | 3 +- .../gameactors/FixtureWorldPortal.kt | 6 +- .../modulebasegame/ui/UIImportAvatar.kt | 58 ++--- .../terrarum/modulebasegame/ui/UINewWorld.kt | 204 ++++++++++++++---- .../terrarum/modulebasegame/ui/UIShare.kt | 5 +- .../modulebasegame/ui/UIWorldPortalShare.kt | 4 +- .../ui/UIWorldPortalUseInvitation.kt | 56 ++++- src/net/torvald/terrarum/tests/Base32Test.kt | 15 +- .../torvald/terrarum/ui/UIItemTextButton.kt | 2 +- .../torvald/terrarum/utils/PasswordBase32.kt | 134 ++---------- 12 files changed, 276 insertions(+), 216 deletions(-) diff --git a/assets/graphics/fonts/code.tga b/assets/graphics/fonts/code.tga index c4ea0987f..50115e006 100644 --- a/assets/graphics/fonts/code.tga +++ b/assets/graphics/fonts/code.tga @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1d534251036d4554e1211b0f1037a11445a41022d5f5bc27e4b5f64e1c5f4a8 +oid sha256:3ad87a26920e3f185e29d5ced2bed743bac04c7eac0432753c213ca2a2558751 size 36882 diff --git a/assets/mods/basegame/locales/en/sentences.json b/assets/mods/basegame/locales/en/sentences.json index 289891052..228a77b90 100644 --- a/assets/mods/basegame/locales/en/sentences.json +++ b/assets/mods/basegame/locales/en/sentences.json @@ -7,5 +7,6 @@ "CONTEXT_WORLD_CODE_SHARE_2": "world when you are away.", "CONTEXT_WORLD_CODE_SHARE_3": "", "CONTEXT_WORLD_CODE_SHARE_4": "Share the code below so the other people can join!", - "ERROR_AVATAR_ALREADY_EXISTS": "The Avatar already exists." + "ERROR_AVATAR_ALREADY_EXISTS": "The Avatar already exists.", + "ERROR_WORLD_NOT_FOUND": "World not found." } \ 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 c83b44d09..24b6a91b5 100644 --- a/assets/mods/basegame/locales/koKR/sentences.json +++ b/assets/mods/basegame/locales/koKR/sentences.json @@ -6,5 +6,6 @@ "CONTEXT_WORLD_CODE_SHARE_2": "플레이할 수 있도록 합니다.", "CONTEXT_WORLD_CODE_SHARE_3": "", "CONTEXT_WORLD_CODE_SHARE_4": "아래의 코드를 공유해 다른 사람을 초대하세요!", - "ERROR_AVATAR_ALREADY_EXISTS": "이미 존재하는 아바타입니다." + "ERROR_AVATAR_ALREADY_EXISTS": "이미 존재하는 아바타입니다.", + "ERROR_WORLD_NOT_FOUND": "월드를 찾을 수 없습니다." } \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureWorldPortal.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureWorldPortal.kt index 4250c7742..c7a2c0e84 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureWorldPortal.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureWorldPortal.kt @@ -120,5 +120,9 @@ class FixtureWorldPortal : Electric { internal data class TeleportRequest( val worldDiskToLoad: DiskSkimmer?, // for loading existing worlds val worldLoadParam: TerrarumIngame.NewWorldParameters? // for creating new world - ) + ) { + override fun toString(): String { + return "TeleportRequest(disk: ${worldDiskToLoad?.diskFile?.name}, param: $worldLoadParam)" + } + } } \ No newline at end of file diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt index 76b67de77..1869b3686 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIImportAvatar.kt @@ -32,63 +32,47 @@ class UIImportAvatar(val remoCon: UIRemoCon) : Advanceable() { private val drawX = (Toolkit.drawWidth - width) / 2 private val drawY = (App.scr.height - height) / 2 - private val cols = 80 - 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 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) - */ + ).also { + // reset importReturnCode if the text input has changed + it.onKeyDown = { _ -> + importReturnCode = 0 + } + } 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) - private val goButton = UIItemTextButton(this, - { Lang["MENU_IO_IMPORT"] }, drawX + width/2 + (width/2 - goButtonWidth) / 2, drawY + height - 24, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true) + { Lang["MENU_LABEL_BACK"] }, drawX + (width/2 - goButtonWidth) / 2, drawY + height - 24, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true).also { - private var importReturnCode = 0 - - init { -// addUIitem(codeBox) -// addUIitem(clearButton) -// addUIitem(pasteButton) - addUIitem(filenameInput) - addUIitem(backButton) - addUIitem(goButton) - - /*clearButton.clickOnceListener = { _,_ -> - codeBox.clearTextBuffer() - } - pasteButton.clickOnceListener = { _,_ -> - codeBox.pasteFromClipboard() - }*/ - backButton.clickOnceListener = { _,_ -> + it.clickOnceListener = { _,_ -> remoCon.openUI(UILoadSavegame(remoCon)) } - goButton.clickOnceListener = { _,_ -> + } + private val goButton = UIItemTextButton(this, + { Lang["MENU_IO_IMPORT"] }, drawX + width/2 + (width/2 - goButtonWidth) / 2, drawY + height - 24, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true).also { + + it.clickOnceListener = { _,_ -> if (filenameInput.getText().isNotBlank()) { importReturnCode = doImport() if (importReturnCode == 0) remoCon.openUI(UILoadSavegame(remoCon)) } } + } + + private var importReturnCode = 0 + + init { + addUIitem(filenameInput) + addUIitem(backButton) + addUIitem(goButton) - // reset importReturnCode if the text input has changed - filenameInput.onKeyDown = { _ -> - importReturnCode = 0 - } } // private var textX = 0 diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt b/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt index db7715ecc..7d728e085 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UINewWorld.kt @@ -1,6 +1,5 @@ package net.torvald.terrarum.modulebasegame.ui -import com.badlogic.gdx.graphics.Camera import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.OrthographicCamera import com.badlogic.gdx.graphics.Texture @@ -21,8 +20,11 @@ import net.torvald.terrarum.serialise.Common import net.torvald.terrarum.modulebasegame.serialise.ReadActor import net.torvald.terrarum.savegame.DiskSkimmer import net.torvald.terrarum.savegame.VDFileID.SAVEGAMEINFO +import net.torvald.terrarum.serialise.toBigInt64 import net.torvald.terrarum.ui.* +import net.torvald.terrarum.utils.PasswordBase32 import net.torvald.terrarum.utils.RandomWordsName +import java.util.UUID /** * Created by minjaesong on 2021-10-25. @@ -56,7 +58,7 @@ class UINewWorld(val remoCon: UIRemoCon) : UICanvas() { private val radioCellWidth = 120 private val inputWidth = 350 private val radioX = (width - (radioCellWidth * NEW_WORLD_SIZE.size + 9)) / 2 - private val inputX = width - inputWidth + private val inputX = drawX + width - inputWidth + 5 private val sizeSelY = 186 + 40 @@ -85,74 +87,166 @@ class UINewWorld(val remoCon: UIRemoCon) : UICanvas() { private val inputLineY1 = 90 private val inputLineY2 = 130 + private val goButtonWidth = 180 + private val gridGap = 10 + private val buttonBaseX = (Toolkit.drawWidth - 3 * goButtonWidth - 2 * gridGap) / 2 + private val buttonY = drawY + height - 24 + + private var mode = 0 // 0: new world, 1: use invitation + + private var uiItemsChangeRequest: (() -> Unit)? = null private val nameInput = UIItemTextLineInput(this, - drawX + width - inputWidth + 5, drawY + sizeSelY + inputLineY1, inputWidth, + inputX, drawY + sizeSelY + inputLineY1, inputWidth, { RandomWordsName(4) }, InputLenCap(VirtualDisk.NAME_LENGTH, InputLenCap.CharLenUnit.UTF8_BYTES)) private val seedInput = UIItemTextLineInput(this, - drawX + width - inputWidth + 5, drawY + sizeSelY + inputLineY2, inputWidth, + inputX, drawY + sizeSelY + inputLineY2, inputWidth, { rng.nextLong().toString() }, InputLenCap(256, InputLenCap.CharLenUnit.CODEPOINTS)) - private val goButtonWidth = 180 + private val codeInput = UIItemTextLineInput(this, + inputX, drawY + sizeSelY, inputWidth, + { "AAAA BB CCCCC DDDDD EEEEE FFFFF" }, InputLenCap(31, InputLenCap.CharLenUnit.CODEPOINTS)).also { + + // reset importReturnCode if the text input has changed + it.onKeyDown = { _ -> + importReturnCode = 0 + } + } + 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).also { + { Lang["MENU_LABEL_BACK"] }, buttonBaseX, buttonY, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true).also { it.clickOnceListener = { _, _ -> remoCon.openUI(UILoadSavegame(remoCon)) } } + private val useInvitationButton = UIItemTextButton(this, + { Lang["MENU_LABEL_USE_CODE"] }, buttonBaseX + goButtonWidth + gridGap, buttonY, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true).also { + + it.clickOnceListener = { _, _ -> + if (mode == 0) { + it.textfun = { Lang["CONTEXT_WORLD_NEW"] } + uiItemsChangeRequest = { + uiItems.clear() + addUIitem(codeInput) + addUIitem(goButton) + addUIitem(it) + addUIitem(backButton) + } + mode = 1 + } + else if (mode == 1) { + it.textfun = { Lang["MENU_LABEL_USE_CODE"] } + uiItemsChangeRequest = { + uiItems.clear() + addUIitem(sizeSelector) + addUIitem(seedInput) // order is important + addUIitem(nameInput) // because of the IME candidates overlay + addUIitem(goButton) + addUIitem(it) + addUIitem(backButton) + } + mode = 0 + } + } + } private val goButton = UIItemTextButton(this, - { Lang["MENU_LABEL_CONFIRM_BUTTON"] }, drawX + width/2 + (width/2 - goButtonWidth) / 2, drawY + height - 24, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true).also { + { Lang["MENU_LABEL_CONFIRM_BUTTON"] }, buttonBaseX + (goButtonWidth + gridGap) * 2, buttonY, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true).also { it.clickOnceListener = { _, _ -> - // after the save is complete, proceed to new world generation - if (existingPlayer == null) { - newPlayerCreationThread.start() - newPlayerCreationThread.join() - } + + if (mode == 0) { + // after the save is complete, proceed to new world generation + if (existingPlayer == null) { + newPlayerCreationThread.start() + newPlayerCreationThread.join() + } - printdbg(this, "generate! Size=${sizeSelector.selection}, Name=${nameInput.getTextOrPlaceholder()}, Seed=${seedInput.getTextOrPlaceholder()}") + printdbg(this, "generate! Size=${sizeSelector.selection}, Name=${nameInput.getTextOrPlaceholder()}, Seed=${seedInput.getTextOrPlaceholder()}") - val ingame = TerrarumIngame(App.batch) - val playerDisk = existingPlayer ?: App.savegamePlayers[UILoadGovernor.playerUUID]!!.loadable() - val player = ReadActor.invoke(playerDisk, ByteArray64Reader(playerDisk.getFile(SAVEGAMEINFO)!!.bytes, Common.CHARSET)) as IngamePlayer - val seed = try { - seedInput.getTextOrPlaceholder().toLong() - } - catch (e: NumberFormatException) { - XXHash64.hash(seedInput.getTextOrPlaceholder().toByteArray(Charsets.UTF_8), 10000) - } - val (wx, wy) = TerrarumIngame.NEW_WORLD_SIZE[sizeSelector.selection] - val worldParam = TerrarumIngame.NewGameParams( + val ingame = TerrarumIngame(App.batch) + val playerDisk = existingPlayer ?: App.savegamePlayers[UILoadGovernor.playerUUID]!!.loadable() + val player = ReadActor.invoke( + playerDisk, + ByteArray64Reader(playerDisk.getFile(SAVEGAMEINFO)!!.bytes, Common.CHARSET) + ) as IngamePlayer + val seed = try { + seedInput.getTextOrPlaceholder().toLong() + } + catch (e: NumberFormatException) { + XXHash64.hash(seedInput.getTextOrPlaceholder().toByteArray(Charsets.UTF_8), 10000) + } + val (wx, wy) = TerrarumIngame.NEW_WORLD_SIZE[sizeSelector.selection] + val worldParam = TerrarumIngame.NewGameParams( player, TerrarumIngame.NewWorldParameters( wx, wy, seed, nameInput.getTextOrPlaceholder() ) - ) - ingame.gameLoadInfoPayload = worldParam - ingame.gameLoadMode = TerrarumIngame.GameLoadMode.CREATE_NEW + ) + ingame.gameLoadInfoPayload = worldParam + ingame.gameLoadMode = TerrarumIngame.GameLoadMode.CREATE_NEW - Terrarum.setCurrentIngameInstance(ingame) - val loadScreen = WorldgenLoadScreen(ingame, wx, wy) - App.setLoadScreen(loadScreen) + Terrarum.setCurrentIngameInstance(ingame) + val loadScreen = WorldgenLoadScreen(ingame, wx, wy) + App.setLoadScreen(loadScreen) + } + else { + val code = codeInput.getText().replace(" ", "") + val uuid = PasswordBase32.decode(code, 16).let { + UUID(it.toBigInt64(0), it.toBigInt64(8)) + } + val world = App.savegameWorlds[uuid] + printdbg(this, "Decoded UUID=$uuid") + + // world exists? + if (world == null) { + importReturnCode = 1 + } + else { + TODO() + + // after the save is complete, proceed to importing + if (existingPlayer == null) { + newPlayerCreationThread.start() + newPlayerCreationThread.join() + } + } + } } - } + private var importReturnCode = 0 + private val errorMessages = listOf( + "", // 0 + Lang["ERROR_WORLD_NOT_FOUND"], // 1 + ) init { addUIitem(sizeSelector) addUIitem(seedInput) // order is important addUIitem(nameInput) // because of the IME candidates overlay addUIitem(goButton) + addUIitem(useInvitationButton) addUIitem(backButton) } + override fun show() { + super.show() + seedInput.clearText() + nameInput.clearText() + codeInput.clearText() + importReturnCode = 0 + } override fun updateUI(delta: Float) { + if (uiItemsChangeRequest != null) { + uiItemsChangeRequest!!() + uiItemsChangeRequest = null + } + uiItems.forEach { it.update(delta) } } @@ -162,23 +256,41 @@ class UINewWorld(val remoCon: UIRemoCon) : UICanvas() { // val titlestr = Lang["CONTEXT_WORLD_NEW"] // App.fontUITitle.draw(batch, titlestr, drawX + (width - App.fontUITitle.getWidth(titlestr)).div(2).toFloat(), titleTextPosY.toFloat()) - // draw size previews - val texture = tex[sizeSelector.selection.coerceAtMost(tex.lastIndex)] - val tx = drawX + (width - texture.regionWidth) / 2 - val ty = drawY + (160 - texture.regionHeight) / 2 - batch.draw(texture, tx.toFloat(), ty.toFloat()) - // border - batch.color = Toolkit.Theme.COL_INACTIVE - Toolkit.drawBoxBorder(batch, tx - 1, ty - 1, texture.regionWidth + 2, texture.regionHeight + 2) + if (mode == 0) { + // draw size previews + val texture = tex[sizeSelector.selection.coerceAtMost(tex.lastIndex)] + val tx = drawX + (width - texture.regionWidth) / 2 + val ty = drawY + (160 - texture.regionHeight) / 2 + batch.draw(texture, tx.toFloat(), ty.toFloat()) + // border + batch.color = Toolkit.Theme.COL_INACTIVE + Toolkit.drawBoxBorder(batch, tx - 1, ty - 1, texture.regionWidth + 2, texture.regionHeight + 2) - batch.color = Color.WHITE - // size selector title - val sizestr = Lang["MENU_OPTIONS_SIZE"] - App.fontGame.draw(batch, sizestr, drawX + (width - App.fontGame.getWidth(sizestr)).div(2).toFloat(), drawY + sizeSelY - 40f) + batch.color = Color.WHITE + // size selector title + val sizestr = Lang["MENU_OPTIONS_SIZE"] + App.fontGame.draw( + batch, + sizestr, + drawX + (width - App.fontGame.getWidth(sizestr)).div(2).toFloat(), + drawY + sizeSelY - 40f + ) - // name/seed input labels - App.fontGame.draw(batch, Lang["MENU_NAME"], drawX - 4, drawY + sizeSelY + inputLineY1) - App.fontGame.draw(batch, Lang["CONTEXT_GENERATOR_SEED"], drawX - 4, drawY + sizeSelY + inputLineY2) + // name/seed input labels + App.fontGame.draw(batch, Lang["MENU_NAME"], drawX - 4, drawY + sizeSelY + inputLineY1) + App.fontGame.draw(batch, Lang["CONTEXT_GENERATOR_SEED"], drawX - 4, drawY + sizeSelY + inputLineY2) + } + else if (mode == 1) { + // code input labels + App.fontGame.draw(batch, Lang["CREDITS_CODE"], drawX - 4, drawY + sizeSelY) + + if (importReturnCode != 0) { + batch.color = Toolkit.Theme.COL_RED + val tby = codeInput.posY + val btny = backButton.posY + Toolkit.drawTextCentered(batch, App.fontGame, errorMessages[importReturnCode], Toolkit.drawWidth, 0, (tby + btny) / 2) + } + } uiItems.forEach { it.render(batch, camera) } } diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIShare.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIShare.kt index 39ea76f8f..28a5a458c 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIShare.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIShare.kt @@ -30,12 +30,13 @@ class UIShare : UICanvas() { override fun show() { shareCode = PasswordBase32.encode( INGAME.world.worldIndex.mostSignificantBits.toBig64() + - INGAME.world.worldIndex.mostSignificantBits.toBig64() - ).let { it.substring(0, it.indexOf('=')) }.let { + INGAME.world.worldIndex.leastSignificantBits.toBig64() + ).let { "${it.substring(0..3)}$dash${it.substring(4..5)}$dash${it.substring(6..10)}$dash${it.substring(11..15)}$dash${it.substring(16..20)}$dash${it.substring(21)}" } App.printdbg(this, shareCode) + App.printdbg(this, INGAME.world.worldIndex) wotKeys = (1..4).map { Lang["CONTEXT_WORLD_CODE_SHARE_$it", false] } } diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalShare.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalShare.kt index 5b6bae691..84b205821 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalShare.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalShare.kt @@ -49,8 +49,8 @@ class UIWorldPortalShare(private val full: UIWorldPortal) : UICanvas() { override fun show() { shareCode = PasswordBase32.encode( INGAME.world.worldIndex.mostSignificantBits.toBig64() + - INGAME.world.worldIndex.mostSignificantBits.toBig64() - ).let { it.substring(0, it.indexOf('=')) }.let { + INGAME.world.worldIndex.leastSignificantBits.toBig64() + ).let { "${it.substring(0..3)}$dash${it.substring(4..5)}$dash${it.substring(6..10)}$dash${it.substring(11..15)}$dash${it.substring(16..20)}$dash${it.substring(21)}" } diff --git a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalUseInvitation.kt b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalUseInvitation.kt index e101dd725..0563843a9 100644 --- a/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalUseInvitation.kt +++ b/src/net/torvald/terrarum/modulebasegame/ui/UIWorldPortalUseInvitation.kt @@ -4,12 +4,16 @@ import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.OrthographicCamera import com.badlogic.gdx.graphics.g2d.SpriteBatch import net.torvald.terrarum.App +import net.torvald.terrarum.INGAME import net.torvald.terrarum.gamecontroller.TerrarumKeyboardEvent import net.torvald.terrarum.langpack.Lang import net.torvald.terrarum.printStackTrace import net.torvald.terrarum.savegame.VirtualDisk +import net.torvald.terrarum.serialise.toBigInt64 import net.torvald.terrarum.ui.* +import net.torvald.terrarum.utils.PasswordBase32 import net.torvald.terrarum.utils.RandomWordsName +import java.util.* /** * Created by minjaesong on 2023-09-03. @@ -34,9 +38,14 @@ class UIWorldPortalUseInvitation(val full: UIWorldPortal) : UICanvas() { private val codeInput = UIItemTextLineInput(this, - drawX + width - inputWidth + 5, drawY + sizeSelY + inputLineY1, inputWidth, - { "AAAA BB CCCCC DDDDD EEEEE FFFFF" }, InputLenCap(VirtualDisk.NAME_LENGTH, InputLenCap.CharLenUnit.UTF8_BYTES) - ) + drawX + width - inputWidth + 5, drawY + sizeSelY, inputWidth, + { "AAAA BB CCCCC DDDDD EEEEE FFFFF" }, InputLenCap(31, InputLenCap.CharLenUnit.CODEPOINTS) + ).also { + // reset importReturnCode if the text input has changed + it.onKeyDown = { _ -> + importReturnCode = 0 + } + } @@ -59,10 +68,35 @@ class UIWorldPortalUseInvitation(val full: UIWorldPortal) : UICanvas() { { Lang["MENU_LABEL_CONFIRM_BUTTON"] }, buttonBaseX + (goButtonWidth + gridGap) * 2, buttonY, goButtonWidth, alignment = UIItemTextButton.Companion.Alignment.CENTRE, hasBorder = true).also { it.clickOnceListener = { _, _ -> + val code = codeInput.getText().replace(" ", "") + val uuid = PasswordBase32.decode(code, 16).let { + UUID(it.toBigInt64(0), it.toBigInt64(8)) + } + val world = App.savegameWorlds[uuid] + App.printdbg(this, "Decoded UUID=$uuid") + + // world exists? + if (world == null) { + importReturnCode = 1 + } + else { + // add the world to the player's worldbook + // TODO memory cap check? or disable check against shared worlds? + full.addWorldToPlayersDict(uuid) + full.cleanUpWorldDict() + + full.requestTransition(0) + } } } + private var importReturnCode = 0 + private val errorMessages = listOf( + "", // 0 + Lang["ERROR_WORLD_NOT_FOUND"], // 1 + ) + init { addUIitem(backButton) addUIitem(searchWorldButton) @@ -70,20 +104,32 @@ class UIWorldPortalUseInvitation(val full: UIWorldPortal) : UICanvas() { addUIitem(codeInput) } + override fun show() { + super.show() + codeInput.clearText() + importReturnCode = 0 + } + override fun updateUI(delta: Float) { uiItems.forEach { it.update(delta) } } override fun renderUI(batch: SpriteBatch, camera: OrthographicCamera) { + // error messages + if (importReturnCode != 0) { + batch.color = Toolkit.Theme.COL_RED + val tby = codeInput.posY + val btny = backButton.posY + Toolkit.drawTextCentered(batch, App.fontGame, errorMessages[importReturnCode], Toolkit.drawWidth, 0, (tby + btny) / 2) + } // input labels batch.color = Color.WHITE - App.fontGame.draw(batch, Lang["CREDITS_CODE"], drawX - 4, drawY + sizeSelY + inputLineY1) + App.fontGame.draw(batch, Lang["CREDITS_CODE"], drawX - 4, drawY + sizeSelY) // control hints App.fontGame.draw(batch, full.portalListingControlHelp, 2 + (Toolkit.drawWidth - 560)/2 + 2, (full.yEnd - 20).toInt()) - uiItems.forEach { it.render(batch, camera) } } diff --git a/src/net/torvald/terrarum/tests/Base32Test.kt b/src/net/torvald/terrarum/tests/Base32Test.kt index 07fbbb289..9689e3e6c 100644 --- a/src/net/torvald/terrarum/tests/Base32Test.kt +++ b/src/net/torvald/terrarum/tests/Base32Test.kt @@ -1,19 +1,24 @@ package net.torvald.terrarum.tests +import net.torvald.terrarum.serialise.toBig64 import net.torvald.terrarum.utils.PasswordBase32 import java.nio.charset.Charset +import java.util.* object Base32Test { operator fun invoke() { - val testStr = "정 참판 양반댁 규수 혼례 치른 날. 123456709".toByteArray() - val pwd = "béchamel".toByteArray() + val testStr = UUID.fromString("145efab2-d465-4e1e-abae-db6c809817a9").let { + it.mostSignificantBits.toBig64() + it.leastSignificantBits.toBig64() + } +// val pwd = "béchamel".toByteArray() - val enc = PasswordBase32.encode(testStr, pwd).let { it.substring(0, it.indexOf('=')) } - val dec = PasswordBase32.decode(enc, testStr.size, pwd) + val enc = PasswordBase32.encode(testStr) + val dec = PasswordBase32.decode(enc, testStr.size) println("Encoded text: $enc") - println("Decoded text: ${dec.toString(Charset.defaultCharset())}") + println("Encoded bytes: ${testStr.joinToString(" ") { it.toInt().and(255).toString(16).padStart(2, '0') }}") + println("Decoded bytes: ${dec.joinToString(" ") { it.toInt().and(255).toString(16).padStart(2, '0') }}") } } diff --git a/src/net/torvald/terrarum/ui/UIItemTextButton.kt b/src/net/torvald/terrarum/ui/UIItemTextButton.kt index f7642236e..0d8d8ee06 100644 --- a/src/net/torvald/terrarum/ui/UIItemTextButton.kt +++ b/src/net/torvald/terrarum/ui/UIItemTextButton.kt @@ -17,7 +17,7 @@ import net.torvald.terrarum.langpack.Lang open class UIItemTextButton( parentUI: UICanvas, /** Stored text (independent to the Langpack) */ - val textfun: () -> String, + var textfun: () -> String, initialX: Int, initialY: Int, override val width: Int, diff --git a/src/net/torvald/terrarum/utils/PasswordBase32.kt b/src/net/torvald/terrarum/utils/PasswordBase32.kt index c80f38c80..23fd4620c 100644 --- a/src/net/torvald/terrarum/utils/PasswordBase32.kt +++ b/src/net/torvald/terrarum/utils/PasswordBase32.kt @@ -1,5 +1,6 @@ package net.torvald.terrarum.utils +import org.apache.commons.codec.binary.Base32 import kotlin.experimental.xor @@ -10,91 +11,25 @@ import kotlin.experimental.xor */ object PasswordBase32 { - private val stringSet = "YBNDRFG8EJKMCP2XOTLVUIS2A345H769=" + private val si = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=" + private val so = "YBNDRFG8EJKMCPQXOTLVUIS2A345H769 " - private val substituteSet = hashMapOf( - Pair('0', 'O'), - Pair('1', 'I'), - Pair('Z', '2') - ) + private val standardToModified = HashMap(32) + private val modifiedToStandard = HashMap(32) - /* - 0 x - 1 6 - 2 4 - 3 3 - 4 1 - */ - val padLen = arrayOf(0, 6, 4, 3, 1) - private val nullPw = byteArrayOf(0.toByte()) - - - private fun encodeToLetters(byteArray: ByteArray, password: ByteArray): IntArray { - val out = ArrayList() - - fun get(i: Int) = byteArray[i] xor (password[i % password.size]) - - - /* - 5 Bytes -> 8 Letters - - 0000 0000 | 1111 1111 | 2222 2222 | 3333 3333 | 4444 4444 - AAAA ABBB | BBCC CCCD | DDDD EEEE | EFFF FFGG | GGGH HHHH - */ - - // non-pads - (0..byteArray.lastIndex - 5 step 5).forEach { - /* A */ out.add(get(it).toInt().and(0xF8).ushr(3)) - /* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6)) - /* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1)) - /* D */ out.add(get(it+1).toInt().and(1).shl(4) or get(it+2).toInt().and(0xF0).ushr(4)) - /* E */ out.add(get(it+2).toInt().and(0xF).shl(1) or get(it+3).toInt().and(0x80).ushr(7)) - /* F */ out.add(get(it+3).toInt().and(0x7C).ushr(2)) - /* G */ out.add(get(it+3).toInt().and(3).shl(3) or get(it+4).toInt().and(0xE0).ushr(5)) - /* H */ out.add(get(it+4).toInt().and(0x1F)) - } - // pads - val residue = byteArray.size % 5 - if (residue != 0){ - - val it = (byteArray.size / 5) * 5 // dark magic of integer division, let's hope the compiler won't "optimise" this... - - when (residue) { - 1 -> { - /* A */ out.add(get(it).toInt().and(0xF8).ushr(3)) - /* B */ out.add(get(it).toInt().and(7).shl(2)) - } - 2 -> { - /* A */ out.add(get(it).toInt().and(0xF8).ushr(3)) - /* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6)) - /* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1)) - /* D */ out.add(get(it+1).toInt().and(1).shl(4)) - } - 3 -> { - /* A */ out.add(get(it).toInt().and(0xF8).ushr(3)) - /* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6)) - /* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1)) - /* D */ out.add(get(it+1).toInt().and(1).shl(4) or get(it+2).toInt().and(0xF0).ushr(4)) - /* E */ out.add(get(it+2).toInt().and(0xF).shl(1)) - } - 4 -> { - /* A */ out.add(get(it).toInt().and(0xF8).ushr(3)) - /* B */ out.add(get(it).toInt().and(7).shl(2) or get(it+1).toInt().and(0xC0).ushr(6)) - /* C */ out.add(get(it+1).toInt().and(0x3E).ushr(1)) - /* D */ out.add(get(it+1).toInt().and(1).shl(4) or get(it+2).toInt().and(0xF0).ushr(4)) - /* E */ out.add(get(it+2).toInt().and(0xF).shl(1) or get(it+3).toInt().and(0x80).ushr(7)) - /* F */ out.add(get(it+3).toInt().and(0x7C).ushr(2)) - /* G */ out.add(get(it+3).toInt().and(3).shl(3)) - } - } - - // append padding - kotlin.repeat(padLen[residue], { out.add(32) }) + init { + (0 until si.length).forEach { + standardToModified[si[it]] = so[it] + modifiedToStandard[so[it]] = si[it] } - return out.toIntArray() + modifiedToStandard['0'] = modifiedToStandard['O']!! + modifiedToStandard['1'] = modifiedToStandard['I']!! + modifiedToStandard['Z'] = modifiedToStandard['2']!! } + private val nullPw = byteArrayOf(0.toByte()) + /** * * @param bytes size of multiple of five (5, 10, 15, 20, ...) is highly recommended to prevent @@ -103,11 +38,8 @@ object PasswordBase32 { * from doing naughty things. Longer, the better. */ fun encode(bytes: ByteArray, password: ByteArray = nullPw): String { - val plaintext = encodeToLetters(bytes, password) - val sb = StringBuilder() - plaintext.forEach { sb.append(stringSet[it]) } - - return sb.toString() + val rawstr = Base32().encode(ByteArray(bytes.size) { bytes[it] xor password[it % password.size] }).toString(Charsets.US_ASCII) + return rawstr.map { standardToModified[it] }.joinToString("").trim() } /** @@ -117,36 +49,10 @@ object PasswordBase32 { * suspect user inputs and sanitise them. */ fun decode(input: String, outByteLength: Int, password: ByteArray = nullPw): ByteArray { - val buffer = ByteArray(outByteLength) - var appendCount = 0 - var input = input.toUpperCase() - substituteSet.forEach { from, to -> - input = input.replace(from, to) + val decInput = input.trim().map { modifiedToStandard[it] }.joinToString("") + return Base32().decode(decInput).let { ba -> + ByteArray(outByteLength) { ba.getOrElse(it) { 0.toByte() } xor password[it % password.size] } } - - fun append(byte: Int) { - buffer[appendCount] = byte.toByte() xor (password[appendCount % password.size]) - appendCount++ - } - fun sbyteOf(i: Int) = stringSet.indexOf(input.getOrElse(i) { '=' }).and(0x1F) - - try { - /* - 8 Letters -> 5 Bytes - - 0000 0000 | 1111 1111 | 2222 2222 | 3333 3333 | 4444 4444 - AAAA ABBB | BBCC CCCD | DDDD EEEE | EFFF FFGG | GGGH HHHH - */ - (0..input.lastIndex.plus(8) step 8).forEach { - /* 0 */ append(sbyteOf(it+0).shl(3) or sbyteOf(it+1).ushr(2)) - /* 1 */ append(sbyteOf(it+1).shl(6) or sbyteOf(it+2).shl(1) or sbyteOf(it+3).ushr(4)) - /* 2 */ append(sbyteOf(it+3).shl(4) or sbyteOf(it+4).ushr(1)) - /* 3 */ append(sbyteOf(it+4).shl(7) or sbyteOf(it+5).shl(2) or sbyteOf(it+6).ushr(3)) - /* 4 */ append(sbyteOf(it+6).shl(5) or sbyteOf(it+7)) - } - } - catch (endOfStream: ArrayIndexOutOfBoundsException) { } - - return buffer } + }