diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbc3e98..dc72820 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,8 +2,16 @@ name: build on: push: + branches-ignore: + - 'beta' + paths-ignore: + - 'README.md' + - 'LICENSE' + - '.gitignore' + schedule: - - cron: '0 0 * * *' # Runs daily at midnight UTC + - cron: '0 0 * * *' + workflow_dispatch: inputs: force_nightly: diff --git a/build.gradle.kts b/build.gradle.kts index d326b18..47c0b51 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("me.modmuss50.mod-publish-plugin") version "0.8.4" } -val modVersion = "1.6.1-hotfix" +val modVersion = "1.7.2" version = "${modVersion}+${property("mod.mod_version") as String}" group = property("maven_group") as String @@ -21,9 +21,8 @@ base { repositories { mavenCentral() - maven { + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/"){ name = "sonatype-oss-snapshots1" - url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") mavenContent { snapshotsOnly() } } maven("https://maven.terraformersmc.com/") { @@ -32,6 +31,9 @@ repositories { maven("https://maven.isxander.dev/releases") { name = "Xander Maven" } + maven( "https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1") { + name = "DevAuth" + } } loom { @@ -39,7 +41,6 @@ loom { mods { - create("yoinkgui").project.sourceSets { sourceSets["main"] sourceSets["client"] @@ -71,9 +72,12 @@ dependencies { modImplementation("com.terraformersmc:modmenu:${modMenu}") modImplementation("dev.isxander:yet-another-config-lib:${yacl}") - // kotlin implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + + //devauth + modRuntimeOnly("me.djtheredstoner:DevAuth-fabric:1.2.2") + } tasks.processResources { @@ -95,7 +99,7 @@ tasks.processResources { } tasks.withType().configureEach { - options.release.set(21) + options.release.set(21) } java { @@ -126,44 +130,47 @@ val generateTemplates = tasks.register("generateTemplates") { sourceSets.main.configure { java.srcDir(generateTemplates.map { it.outputs }) } publishMods { - displayName.set("YoinkGUI $cleanVersion for MC $mcVersion") - file.set(tasks.remapJar.get().archiveFile) - changelog.set( - rootProject.file("changelogs/${cleanVersion}.md") - .takeIf { it.exists() } - ?.readText() - ?: "No changelog provided." - ) - type = STABLE - modLoaders.add("fabric") - - fun versionList(prop: String) = findProperty(prop)?.toString() - ?.split(',') - ?.map { it.trim() } - ?: emptyList() - - modrinth { - projectId.set(property("modrinthId") as String) - accessToken.set(providers.environmentVariable("MODRINTH_API_KEY")) - minecraftVersions.addAll(versionList("pub.modrinthMC")) - - requires { slug.set("fabric-api") } - requires { slug.set("fabric-language-kotlin") } + displayName.set("YoinkGUI $cleanVersion for MC $mcVersion") + file.set(tasks.remapJar.get().archiveFile) + changelog.set( + rootProject.file("src/main/resources/changelogs/${cleanVersion}.md") + .takeIf { it.exists() } + ?.readText() + ?: "No changelog provided." + ) + + type = STABLE + modLoaders.add("fabric") + + fun versionList(prop: String) = findProperty(prop)?.toString() + ?.split(',') + ?.map { it.trim() } + ?: emptyList() + + modrinth { + projectId.set(property("modrinthId") as String) + accessToken.set(providers.environmentVariable("MODRINTH_API_KEY")) + minecraftVersions.addAll(versionList("pub.modrinthMC")) + + requires { slug.set("fabric-api") } + requires { slug.set("fabric-language-kotlin") } requires { slug.set("yacl") } requires { slug.set("modmenu") } - } + } - curseforge { - projectId.set(property("curseforgeId") as String) - accessToken.set(providers.environmentVariable("CURSEFORGE_API_KEY")) - minecraftVersions.addAll(versionList("pub.curseMC")) + curseforge { + projectId.set(property("curseforgeId") as String) + accessToken.set(providers.environmentVariable("CURSEFORGE_API_KEY")) + minecraftVersions.addAll(versionList("pub.curseMC")) - requires { slug.set("fabric-api") } - requires { slug.set("fabric-language-kotlin") } + requires { slug.set("fabric-api") } + requires { slug.set("fabric-language-kotlin") } requires { slug.set("yacl") } requires { slug.set("modmenu") } - } - + } } + + + diff --git a/imageCache/configMenuDevOptions.png b/imageCache/configMenuDevOptions.png new file mode 100644 index 0000000..6419b74 Binary files /dev/null and b/imageCache/configMenuDevOptions.png differ diff --git a/imageCache/dragScreen.png b/imageCache/dragScreen.png new file mode 100644 index 0000000..a0a752d Binary files /dev/null and b/imageCache/dragScreen.png differ diff --git a/imageCache/exampleItem.png b/imageCache/exampleItem.png new file mode 100644 index 0000000..1920551 Binary files /dev/null and b/imageCache/exampleItem.png differ diff --git a/imageCache/exampleOutput.png b/imageCache/exampleOutput.png new file mode 100644 index 0000000..37c29b2 Binary files /dev/null and b/imageCache/exampleOutput.png differ diff --git a/readme.md b/readme.md index 6669b11..fc082f6 100644 --- a/readme.md +++ b/readme.md @@ -5,9 +5,9 @@ [![Download on Modrinth](https://raw.githubusercontent.com/intergrav/devins-badges/c7fd18efdadd1c3f12ae56b49afd834640d2d797/assets/cozy/available/modrinth_vector.svg)](https://modrinth.com/mod/yoinkgui) [![Download on CurseForge](https://raw.githubusercontent.com/intergrav/devins-badges/c7fd18efdadd1c3f12ae56b49afd834640d2d797/assets/cozy/available/curseforge_vector.svg)](https://www.curseforge.com/minecraft/mc-mods/yoinkgui) [![fapi-badge](https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/requires/fabric-api_vector.svg)](https://modrinth.com/mod/fabric-api) -[![sinytra-connector](https://github.com/Sinytra/.github/blob/main/badges/connector/cozy.svg)](https://modrinth.com/mod/connector) +[![sinytra-connector](https://raw.githubusercontent.com/Sinytra/.github/refs/heads/main/badges/connector/cozy.svg)](https://modrinth.com/mod/connector) -![Build Status](https://github.com/ThatOneDevil/yoinkgui/actions/workflows/build.yml/badge.svg) +[![Build Status](https://github.com/ThatOneDevil/yoinkgui/actions/workflows/build.yml/badge.svg)](https://github.com/ThatOneDevil/yoinkgui) [![Modrinth Donwloads](https://img.shields.io/modrinth/dt/yoinkgui?color=00AF5C&label=downloads&logo=modrinth)](https://modrinth.com/mod/yoinkgui) [![CurseForge Downloads](https://cf.way2muchnoise.eu/full_yoinkgui_downloads.svg)](https://www.curseforge.com/minecraft/mc-mods/yoinkgui) [![Discord](https://img.shields.io/discord/1405856687851704420?color=blue&logo=discord&label=Discord)](https://discord.gg/kcegGvZvpC) @@ -36,12 +36,19 @@ Report bugs here: ## ✨ Features +- Press **M** to open the configuration menu and customize your parsing options + - **Full Minecraft formatting code support**: - - **Color codes** - - **Bold**, *Italic*, __Underline__, ~~Strikethrough~~, and obfuscated text + - **Color codes, &c &e** + - **Bold**, *Italic*, __Underline__, ~~Strikethrough~~, and obfuscated text + - **Hex and gradient support** (MiniMessage format): - - Hex colors: `` - - Gradients: `` + - Hex colors: `` + - Gradients: `` + - Shadow: `` + +![ConfigMenu](https://github.com/ThatOneDevil/yoinkgui/blob/master/imageCache/configMenuDevOptions.png?raw=true) +![DragScreen](https://github.com/ThatOneDevil/yoinkgui/blob/master/imageCache/dragScreen.png?raw=true) --- @@ -53,10 +60,6 @@ Report bugs here: 4. The output `.txt` file will appear in your `config` folder. 5. The file path is also displayed in chat, as shown below -**Example of the path message:** - -![Example of the path message](https://cdn.modrinth.com/data/cached_images/681a4eb24635ac98ae987f4a72a741c27c4c3c87.png) - -![Example item with lore](https://cdn.modrinth.com/data/cached_images/acb2558b12d52504936e9d1e1afd8f54a93d04ef.png) +![ExampleItem](https://github.com/ThatOneDevil/yoinkgui/blob/master/imageCache/exampleItem.png?raw=true) -![Parsed Lore](https://cdn.modrinth.com/data/cached_images/c468d843708f68328354a1c17ea48f4d8b4dd297.png) +![ExampleOutput](https://github.com/ThatOneDevil/yoinkgui/blob/master/imageCache/exampleOutput.png?raw=true) diff --git a/src/client/java/me/thatonedevil/mixin/client/ScreenMixin.java b/src/client/java/me/thatonedevil/mixin/client/ScreenMixin.java index 5b5619d..2d3b782 100644 --- a/src/client/java/me/thatonedevil/mixin/client/ScreenMixin.java +++ b/src/client/java/me/thatonedevil/mixin/client/ScreenMixin.java @@ -5,6 +5,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.*; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -17,7 +18,15 @@ public class ScreenMixin { @Inject(at = @At("HEAD"), method = "render") private void render(DrawContext context, int mouseX, int mouseY, float deltaTicks, CallbackInfo ci) { MinecraftClient client = MinecraftClient.getInstance(); - if (client.player == null) { + if (client.player == null || client.world == null) { + return; + } + + if (!(client.currentScreen instanceof InventoryScreen + || client.currentScreen instanceof GenericContainerScreen + || client.currentScreen instanceof MerchantScreen + || client.currentScreen instanceof CreativeInventoryScreen + || client.currentScreen instanceof ShulkerBoxScreen)) { return; } @@ -31,18 +40,13 @@ private void render(DrawContext context, int mouseX, int mouseY, float deltaTick int baseButtonWidth = 160; int baseButtonHeight = 20; - int baseButtonX = 40; - int baseButtonY = 35; - - int parseButtonX = (int) (baseButtonX * scaleFactor); - int parseButtonY = (int) (baseButtonY * scaleFactor); + int parseButtonX = config.getButtonX().get(); + int parseButtonY = config.getButtonY().get(); int parseButtonWidth = (int) (baseButtonWidth * scaleFactor); int parseButtonHeight = (int) (baseButtonHeight * scaleFactor); String parseButtonText = "Yoink and Parse NBT into file"; - // Register HUD rendering event - // Calculate mouse position in UI space int scaledWidth = client.getWindow().getScaledWidth(); int scaledHeight = client.getWindow().getScaledHeight(); int mouseXUi = (int) (client.mouse.getX() * scaledWidth / client.getWindow().getWidth()); @@ -51,7 +55,6 @@ private void render(DrawContext context, int mouseX, int mouseY, float deltaTick YoinkGUIClient.INSTANCE.setParseButtonHovered(mouseXUi >= parseButtonX && mouseXUi <= parseButtonX + parseButtonWidth && mouseYUi >= parseButtonY && mouseYUi <= parseButtonY + parseButtonHeight); - // Draw second button (Parse NBT) with appropriate color int parseBgColor = YoinkGUIClient.INSTANCE.getParseButtonHovered() ? 0xAA444444 : 0xAA000000; context.fill(parseButtonX, parseButtonY, parseButtonX + parseButtonWidth, parseButtonY + parseButtonHeight, parseBgColor); context.drawCenteredTextWithShadow( diff --git a/src/client/kotlin/me/thatonedevil/NBTParser.kt b/src/client/kotlin/me/thatonedevil/NBTParser.kt index 04b352a..d61c2fd 100644 --- a/src/client/kotlin/me/thatonedevil/NBTParser.kt +++ b/src/client/kotlin/me/thatonedevil/NBTParser.kt @@ -4,11 +4,14 @@ import com.google.gson.Gson import com.google.gson.JsonObject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import me.thatonedevil.utils.Utils.toClickable +import me.thatonedevil.YoinkGUIClient.logger +import me.thatonedevil.config.YoinkGuiSettings +import me.thatonedevil.utils.Utils.toClickCopy import me.thatonedevil.utils.Utils.toComponent -import me.thatonedevil.YoinkGUI.logger import me.thatonedevil.utils.Utils import me.thatonedevil.nbt.ComponentValueRegistry +import me.thatonedevil.utils.LatestErrorLog +import me.thatonedevil.utils.api.UpdateChecker.serverName import java.io.File import java.io.FileWriter import java.time.Duration @@ -21,12 +24,8 @@ object NBTParser { private fun parseTextComponent(obj: JsonObject): String = buildString { val result = ComponentValueRegistry.process(obj) - if (result.text.isNotEmpty()) { - append(result.text) - if (result.stopPropagation) return@buildString - } - - append(obj.get("text")?.asString ?: "") + append(result.text) + if (result.stopPropagation) return@buildString obj.get("extra")?.asJsonArray?.forEach { if (it.isJsonObject) append(parseTextComponent(it.asJsonObject)) @@ -46,11 +45,12 @@ object NBTParser { val json = gson.fromJson(raw, JsonObject::class.java) val components = json.getAsJsonObject("components") ?: return@buildString - val hasName = components.has("minecraft:custom_name") + val hasName = components.has("minecraft:custom_name") || components.has("minecraft:item_name") val hasLore = components.has("minecraft:lore") if (!hasName && !hasLore) return@buildString - components.get("minecraft:custom_name")?.let { customNameElement -> + val nameElement = components.get("minecraft:custom_name") ?: components.get("minecraft:item_name") + nameElement?.let { customNameElement -> append("Name: ") when { customNameElement.isJsonObject -> append(parseTextComponent(customNameElement.asJsonObject)) @@ -76,15 +76,14 @@ object NBTParser { suspend fun saveFormattedNBTToFile(nbtList: List, configDir: File) = withContext(Dispatchers.IO) { + val start = LocalDateTime.now() + val formattedTime = start.format(DateTimeFormatter.ofPattern("MM-dd HH-mm-ss")) try { - val start = LocalDateTime.now() - val formattedTime = start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")) - val yoinkDir = File(configDir, "yoinkgui").apply { mkdirs() } - val fileName = "formatted_nbt_${formattedTime}.txt" + val yoinkDir = File(configDir, "assets/yoinkgui").apply { mkdirs() } + val fileName = "${serverName}-${formattedTime}.txt" val file = File(yoinkDir, fileName) FileWriter(file).use { writer -> - // keep the original index so we can reference the raw NBT correctly later val contentItems = nbtList.mapIndexedNotNull { i, raw -> val formatted = parseNewNBTFormat(raw) if (formatted.isNotBlank()) Pair(i, formatted) else null @@ -99,7 +98,8 @@ object NBTParser { contentItems.forEachIndexed { outIdx, (originalIndex, formatted) -> writer.write("=== ITEM ${originalIndex + 1} ===\n") - writer.write("Raw NBT: ${nbtList[originalIndex]}\n") + if (YoinkGuiSettings.includeRawNbt.get()) { writer.write("Raw NBT: ${nbtList[originalIndex]}\n") } + writer.write("\n$formatted\n") if (outIdx < contentItems.lastIndex) writer.write("\n${"=".repeat(50)}\n\n") } @@ -107,17 +107,15 @@ object NBTParser { val duration = Duration.between(start, LocalDateTime.now()).toMillis() - try { - Utils.sendChat( - "\nFormatted NBT data saved to:".toComponent(), - " Parse time: ${duration}ms".toComponent(), - " ${file.absolutePath} &7&o(Click to copy)\n".toClickable(file.absolutePath) - ) - } catch (inner: Exception) { - logger?.error("Error while sending save notification to chat: ${inner.message}", inner) - } + Utils.sendChat( + "\nFormatted NBT data saved to:".toComponent(), + " Parse time: ${duration}ms".toComponent(), + " ${file.absolutePath} &7&o(Click to copy)\n".toClickCopy(file.absolutePath) + ) + } catch (e: Exception) { - logger?.error("Error saving NBT file: ${e.message}", e) + LatestErrorLog.record(e, "Error saving NBT file") + logger.error("Error saving NBT file: ${e.message}", e) } } } diff --git a/src/client/kotlin/me/thatonedevil/YoinkGUIClient.kt b/src/client/kotlin/me/thatonedevil/YoinkGUIClient.kt index f008944..8724aa3 100644 --- a/src/client/kotlin/me/thatonedevil/YoinkGUIClient.kt +++ b/src/client/kotlin/me/thatonedevil/YoinkGUIClient.kt @@ -3,35 +3,71 @@ package me.thatonedevil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import me.thatonedevil.YoinkGUI.logger -import me.thatonedevil.commands.YoinkGuiCommandRegistry +import me.thatonedevil.commands.YoinkGuiClientCommandRegistry import me.thatonedevil.config.YoinkGuiSettings +import me.thatonedevil.gui.ButtonPositionScreen import me.thatonedevil.inventory.TopInventory import me.thatonedevil.inventory.YoinkInventory +import me.thatonedevil.utils.LatestErrorLog import me.thatonedevil.utils.Utils.sendChat -import me.thatonedevil.utils.Utils.toClickable +import me.thatonedevil.utils.Utils.toClickCopy import me.thatonedevil.utils.api.UpdateChecker import net.fabricmc.api.ClientModInitializer import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper import net.minecraft.client.MinecraftClient +import net.minecraft.client.option.KeyBinding +import net.minecraft.client.util.InputUtil +import net.minecraft.util.Identifier import org.lwjgl.glfw.GLFW +import org.slf4j.Logger +import org.slf4j.LoggerFactory object YoinkGUIClient : ClientModInitializer { - - private var buttonHovered = false var parseButtonHovered = false private var wasLeftClicking = false + val logger: Logger = LoggerFactory.getLogger(BuildConfig.MOD_ID) @JvmStatic val yoinkGuiSettings = YoinkGuiSettings + //? if >=1.21.9 { + private val positionButtonKeybind = KeyBindingHelper.registerKeyBinding( + KeyBinding( + "key.yoinkgui.position", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_M, + KeyBinding.Category.create(Identifier.of("keybinds")) + ) + ) + //? } else { + /*private val positionButtonKeybind = KeyBindingHelper.registerKeyBinding( + KeyBinding( + "key.yoinkgui.position", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_M, + "key.category.minecraft.keybinds" + ) + )*/ + //?} + override fun onInitializeClient() { ClientTickEvents.END_CLIENT_TICK.register { client -> + if (positionButtonKeybind.wasPressed()) { + client.setScreen(ButtonPositionScreen(client.currentScreen)) + } + val isLeftClicking = GLFW.glfwGetMouseButton(client.window.handle, GLFW.GLFW_MOUSE_BUTTON_LEFT) == GLFW.GLFW_PRESS + if (client.currentScreen == null) { + return@register + } + if (client.currentScreen is ButtonPositionScreen) { + return@register + } + if (client.player != null && isLeftClicking && !wasLeftClicking) { when { - buttonHovered -> handleYoinkButton(client) parseButtonHovered -> handleParseButton(client) } } @@ -40,22 +76,17 @@ object YoinkGUIClient : ClientModInitializer { } UpdateChecker.setupJoinListener() - YoinkGuiCommandRegistry.register() + YoinkGuiClientCommandRegistry.register() yoinkGuiSettings // Load settings on client init } - private fun handleYoinkButton(client: MinecraftClient) { - val yoinkInventory = YoinkInventory(client.player!!, TopInventory(client)) - yoinkInventory.yoinkItems() - logger?.info("Yoinked Items: ${yoinkInventory.getYoinkedItems()}") - } - - private fun handleParseButton(client: MinecraftClient) { + fun handleParseButton(client: MinecraftClient) { CoroutineScope(Dispatchers.IO).launch { try { val player = client.player ?: return@launch val configDir = client.runDirectory.resolve("config") - val yoinkedItems = YoinkInventory(player, TopInventory(client)).apply { yoinkItems() }.getYoinkedItems().map { it.toString() } + val yoinkInventory = YoinkInventory(player, TopInventory(client)) + val yoinkedItems = yoinkInventory.apply { yoinkItems() }.getYoinkedItems().map { it.toString() } if (yoinkedItems.isEmpty()) { sendChat("Inventory is empty!") @@ -65,8 +96,9 @@ object YoinkGUIClient : ClientModInitializer { NBTParser.saveFormattedNBTToFile(yoinkedItems, configDir) } catch (e: Exception) { - sendChat("Error during NBT parsing: ${e.message} &7&o(Report on github, Click to copy)".toClickable(e.message.toString())) - logger?.error("Error during NBT parsing: ${e.stackTraceToString()}") + LatestErrorLog.record(e, "Error during NBT parsing") + sendChat("Error during NBT parsing: ${e.message} &7&o(Report on github, Click to copy)".toClickCopy(e.message.toString())) + logger.error("Error during NBT parsing: ${e.stackTraceToString()}") } } diff --git a/src/client/kotlin/me/thatonedevil/commands/DebugCommand.kt b/src/client/kotlin/me/thatonedevil/commands/DebugCommand.kt new file mode 100644 index 0000000..2ee21dd --- /dev/null +++ b/src/client/kotlin/me/thatonedevil/commands/DebugCommand.kt @@ -0,0 +1,41 @@ +package me.thatonedevil.commands + +import com.mojang.brigadier.Command +import me.thatonedevil.BuildConfig.MC_VERSION +import me.thatonedevil.BuildConfig.VERSION +import me.thatonedevil.utils.LatestErrorLog +import me.thatonedevil.utils.Utils.sendChat +import me.thatonedevil.utils.Utils.toComponent +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer + +class DebugCommand { + fun execute(): Int { + val latestStacktrace = LatestErrorLog.getLatestStackTraceMessage() + val className = LatestErrorLog.getLatestErrorName() + + val errorInfo = className ?: latestStacktrace ?: "No error information available." + val message = debugMessage(errorInfo) + val plainText = PlainTextComponentSerializer.plainText().serialize(debugMessage(latestStacktrace ?: "No stacktrace available.")) + + sendChat(message.clickEvent(ClickEvent.copyToClipboard(plainText))) + return Command.SINGLE_SUCCESS + } + + private fun debugMessage(error: String): Component { + return Component.text() + .append("&f--- YoinkGUI Debug Info &f---\n".toComponent()) + .append(" Game Version: &e$MC_VERSION\n".toComponent()) + .append(" Mod Version: &e$VERSION\n".toComponent()) + .append(" Java Version: &e${System.getProperty("java.version")}\n".toComponent()) + .append(Component.newline()) + .append("&f--- YoinkGUI Error Info &f---\n".toComponent()) + .append(Component.newline()) + .append(" Latest Error: &c${LatestErrorLog.getLatestMessage()}\n".toComponent()) + .append(" Error Stacktrace: &c$error\n".toComponent()) + .append(Component.newline()) + .append("&7&o(Click to copy)".toComponent()) + .build() + } +} \ No newline at end of file diff --git a/src/client/kotlin/me/thatonedevil/commands/VersionCommand.kt b/src/client/kotlin/me/thatonedevil/commands/VersionCommand.kt deleted file mode 100644 index 529d610..0000000 --- a/src/client/kotlin/me/thatonedevil/commands/VersionCommand.kt +++ /dev/null @@ -1,14 +0,0 @@ -package me.thatonedevil.commands - -import com.mojang.brigadier.Command -import me.thatonedevil.utils.api.UpdateChecker - -class VersionCommand { - - fun execute(): Int { - UpdateChecker.checkVersion() - return Command.SINGLE_SUCCESS - } - -} - diff --git a/src/client/kotlin/me/thatonedevil/commands/YoinkGuiClientCommandRegistry.kt b/src/client/kotlin/me/thatonedevil/commands/YoinkGuiClientCommandRegistry.kt new file mode 100644 index 0000000..fd80efe --- /dev/null +++ b/src/client/kotlin/me/thatonedevil/commands/YoinkGuiClientCommandRegistry.kt @@ -0,0 +1,58 @@ +package me.thatonedevil.commands + +import com.mojang.brigadier.Command +import com.mojang.brigadier.CommandDispatcher +import me.thatonedevil.gui.ButtonPositionScreen +import me.thatonedevil.gui.ChangelogScreen +import me.thatonedevil.utils.api.UpdateChecker +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource + +object YoinkGuiClientCommandRegistry { + + private val debugCommand = DebugCommand() + + fun register() { + ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ -> + registerCommands(dispatcher) + } + } + + private fun registerCommands(dispatcher: CommandDispatcher) { + dispatcher.register( + ClientCommandManager.literal("yoinkguiclient") + .then( + ClientCommandManager.literal("version") + .executes { + UpdateChecker.checkVersion() + Command.SINGLE_SUCCESS + } + ).then( + ClientCommandManager.literal("menu") + .executes { context -> + val client = context.source.client + client.send { + client.setScreen(ButtonPositionScreen(client.currentScreen)) + } + Command.SINGLE_SUCCESS + } + ) + .then( + ClientCommandManager.literal("changelog") + .executes { context -> + val client = context.source.client + client.send { + client.setScreen(ChangelogScreen(client.currentScreen)) + } + Command.SINGLE_SUCCESS + } + ) + .then( + ClientCommandManager.literal("debug") + .executes { _ -> debugCommand.execute() } + ) + + ) + } +} \ No newline at end of file diff --git a/src/client/kotlin/me/thatonedevil/commands/YoinkGuiCommandRegistry.kt b/src/client/kotlin/me/thatonedevil/commands/YoinkGuiCommandRegistry.kt deleted file mode 100644 index bb5b6f0..0000000 --- a/src/client/kotlin/me/thatonedevil/commands/YoinkGuiCommandRegistry.kt +++ /dev/null @@ -1,30 +0,0 @@ -package me.thatonedevil.commands - -import com.mojang.brigadier.CommandDispatcher -import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager -import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource - -object YoinkGuiCommandRegistry { - - private val versionCommand = VersionCommand() - - fun register() { - ClientCommandRegistrationCallback.EVENT.register { dispatcher, _ -> - registerCommands(dispatcher) - } - } - - private fun registerCommands(dispatcher: CommandDispatcher) { - dispatcher.register( - ClientCommandManager.literal("yoinkgui") - .then( - ClientCommandManager.literal("version") - .executes { _ -> - versionCommand.execute() - } - ) - ) - } - -} \ No newline at end of file diff --git a/src/client/kotlin/me/thatonedevil/config/ModMenuIntegration.kt b/src/client/kotlin/me/thatonedevil/config/ModMenuIntegration.kt index 32385d3..a2751e4 100644 --- a/src/client/kotlin/me/thatonedevil/config/ModMenuIntegration.kt +++ b/src/client/kotlin/me/thatonedevil/config/ModMenuIntegration.kt @@ -6,8 +6,10 @@ import dev.isxander.yacl3.api.ConfigCategory import dev.isxander.yacl3.api.OptionGroup import dev.isxander.yacl3.api.YetAnotherConfigLib import me.thatonedevil.YoinkGUIClient.yoinkGuiSettings -import me.thatonedevil.config.YaclConfigHelper.floatSliderOption import me.thatonedevil.config.YaclConfigHelper.booleanOption +import me.thatonedevil.config.YaclConfigHelper.enumOptionString +import me.thatonedevil.nbt.ComponentValueRegistry.refreshHandlers +import me.thatonedevil.nbt.FormatOptions import net.minecraft.client.gui.screen.Screen import net.minecraft.text.Text @@ -18,7 +20,10 @@ class ModMenuIntegration : ModMenuApi { private fun createScreen(parentScreen: Screen?): Screen { val screen = YetAnotherConfigLib.createBuilder() - .save(YoinkGuiSettings::saveToFile) + .save { + YoinkGuiSettings.saveToFile() + refreshHandlers() + } .title(Text.of("YoinkGUI Settings")) .category(ConfigCategory.createBuilder() .name(Text.of("Button settings")) @@ -31,20 +36,66 @@ class ModMenuIntegration : ModMenuApi { field = yoinkGuiSettings.enableYoinkButton, defaultValue = true )) + .build()) + .build()) - .option(floatSliderOption( - name = "Button Scale Factor", - field = yoinkGuiSettings.buttonScaleFactor, - defaultValue = 1.0f, - range = 0.1f..2f, - step = 0.1f, - formatValue = { Text.of("${"%.2f".format(it)}x scale") } + .category(ConfigCategory.createBuilder() + .name(Text.of("Dev settings")) + .tooltip(Text.of("Developer settings")) + .group(OptionGroup.createBuilder() + .name(Text.of("Dev Options")) + .option(booleanOption( + name = "Debug Mode", + field = yoinkGuiSettings.debugMode, + defaultValue = false, + description = "Enables debug logging to help diagnose issues." + )) + .name(Text.of("Toggle formatting option")) + .option(enumOptionString( + name = "Default NBT Format", + field = yoinkGuiSettings.formatOption, + enumClass = FormatOptions::class.java, + defaultValue = FormatOptions.LEGACY + )) + .build()) + .group(OptionGroup.createBuilder() + .name(Text.of("Nbt Parser Options")) + .option(booleanOption( + name = "Include Raw NBT", + field = yoinkGuiSettings.includeRawNbt, + defaultValue = false, + description = "Includes the raw NBT data in the parsed output." + )) + .option(booleanOption( + name = "Color parser", + field = yoinkGuiSettings.toggleColorParser, + defaultValue = true, + description = "Toggles color parsing in NBT text. , , " + )) + .option(booleanOption( + name = "Style parser", + field = yoinkGuiSettings.toggleStyleParser, + defaultValue = true, + description = "Toggles style parsing in NBT text. , , " + )) + .option(booleanOption( + name = "Shadow parser", + field = yoinkGuiSettings.toggleShadowParser, + defaultValue = true, + description = "Toggles shadow parsing in NBT text. " + )) + .option(booleanOption( + name = "Gradient parser", + field = yoinkGuiSettings.toggleGradientParser, + defaultValue = true, + description = "Toggles gradient parsing in NBT text. " )) .build()) .build()) + .build() .generateScreen(parentScreen) return screen } -} \ No newline at end of file +} diff --git a/src/client/kotlin/me/thatonedevil/config/YaclConfigHelper.kt b/src/client/kotlin/me/thatonedevil/config/YaclConfigHelper.kt index 2402137..5d87c65 100644 --- a/src/client/kotlin/me/thatonedevil/config/YaclConfigHelper.kt +++ b/src/client/kotlin/me/thatonedevil/config/YaclConfigHelper.kt @@ -2,7 +2,7 @@ package me.thatonedevil.config import dev.isxander.yacl3.api.Option import dev.isxander.yacl3.api.OptionDescription -import dev.isxander.yacl3.api.controller.FloatSliderControllerBuilder +import dev.isxander.yacl3.api.controller.EnumControllerBuilder import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder import dev.isxander.yacl3.config.v3.ConfigEntry import dev.isxander.yacl3.config.v3.value @@ -16,6 +16,7 @@ object YaclConfigHelper { defaultValue: Boolean = true, description: String? = null ): Option { + return Option.createBuilder() .name(Text.of(name)) .apply { description?.let { description(OptionDescription.of(Text.of(it))) } } @@ -24,26 +25,23 @@ object YaclConfigHelper { .build() } - fun floatSliderOption( + + fun > enumOptionString( name: String, - field: ConfigEntry, - defaultValue: Float, - range: ClosedFloatingPointRange, - step: Float = 0.1f, - formatValue: ((Float) -> Text)? = null, - description: String? = null - ): Option { - return Option.createBuilder() + field: ConfigEntry, + enumClass: Class, + defaultValue: T + ): Option { + return Option.createBuilder() .name(Text.of(name)) - .apply { description?.let { description(OptionDescription.of(Text.of(it))) } } - .binding(defaultValue, { field.value }, { field.value = it }) - .controller { option -> - FloatSliderControllerBuilder.create(option) - .range(range.start, range.endInclusive) - .step(step) - .apply { formatValue?.let { formatValue(it) } } - } + .binding( + defaultValue, + { try { java.lang.Enum.valueOf(enumClass, field.value) } catch (_: Exception) { defaultValue } }, + { field.value = it.name } + ) + .controller { option -> EnumControllerBuilder.create(option).enumClass(enumClass) } .build() } + } diff --git a/src/client/kotlin/me/thatonedevil/config/YoinkGuiSettings.kt b/src/client/kotlin/me/thatonedevil/config/YoinkGuiSettings.kt index 4c090c9..535c0b5 100644 --- a/src/client/kotlin/me/thatonedevil/config/YoinkGuiSettings.kt +++ b/src/client/kotlin/me/thatonedevil/config/YoinkGuiSettings.kt @@ -3,6 +3,7 @@ package me.thatonedevil.config import dev.isxander.yacl3.config.v3.JsonFileCodecConfig import dev.isxander.yacl3.config.v3.register import dev.isxander.yacl3.config.v3.value +import me.thatonedevil.nbt.FormatOptions import net.fabricmc.loader.api.FabricLoader open class YoinkGuiSettings() : JsonFileCodecConfig( @@ -11,11 +12,25 @@ open class YoinkGuiSettings() : JsonFileCodecConfig( constructor(settings: YoinkGuiSettings) : this() { this.enableYoinkButton.value = settings.enableYoinkButton.value this.buttonScaleFactor.value = settings.buttonScaleFactor.value + this.buttonX.value = settings.buttonX.value + this.buttonY.value = settings.buttonY.value + this.debugMode.value = settings.debugMode.value this._firstLaunch.value = settings._firstLaunch.value } val enableYoinkButton by register(default = true, BOOL) val buttonScaleFactor by register(default = 1.0f, FLOAT) + val buttonX by register(default = 40, INT) + val buttonY by register(default = 35, INT) + val debugMode by register(default = false, BOOL) + + // parser options + val formatOption by register(default = FormatOptions.LEGACY.name, STRING) + val includeRawNbt by register(default = false, BOOL) + val toggleColorParser by register(default = true, BOOL) + val toggleStyleParser by register(default = true, BOOL) + val toggleShadowParser by register(default = true, BOOL) + val toggleGradientParser by register(default = true, BOOL) var firstLaunch = false val _firstLaunch by register(default = true, BOOL) @@ -25,7 +40,6 @@ open class YoinkGuiSettings() : JsonFileCodecConfig( if (!loadFromFile()) { saveToFile() } - if (_firstLaunch.value) { firstLaunch = true _firstLaunch.value = false diff --git a/src/client/kotlin/me/thatonedevil/gui/ButtonPositionScreen.kt b/src/client/kotlin/me/thatonedevil/gui/ButtonPositionScreen.kt new file mode 100644 index 0000000..945a5ec --- /dev/null +++ b/src/client/kotlin/me/thatonedevil/gui/ButtonPositionScreen.kt @@ -0,0 +1,141 @@ +package me.thatonedevil.gui + +import me.thatonedevil.YoinkGUIClient +import me.thatonedevil.config.YoinkGuiSettings +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import org.lwjgl.glfw.GLFW + +@Environment(EnvType.CLIENT) +class ButtonPositionScreen(private val parent: Screen?) : Screen(Text.literal("Position Yoink Button")) { + + override fun init() { + super.init() + } + + private val config: YoinkGuiSettings = YoinkGUIClient.yoinkGuiSettings + private var dragging = false + private var dragOffsetX = 0 + private var dragOffsetY = 0 + private var wasMousePressed = false + + private val baseButtonWidth = 160 + private val baseButtonHeight = 20 + + private var buttonX: Int + get() = config.buttonX.get() + set(value) { + config.buttonX.set(value) + } + + private var buttonY: Int + get() = config.buttonY.get() + set(value) { + config.buttonY.set(value) + } + + private var scaleFactor: Float + get() = config.buttonScaleFactor.get() + set(value) { + config.buttonScaleFactor.set(value) + } + + private val scaledButtonWidth: Int + get() = (baseButtonWidth * scaleFactor).toInt() + + private val scaledButtonHeight: Int + get() = (baseButtonHeight * scaleFactor).toInt() + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + + val window = client?.window?.handle + + if (window != null) { + val isMousePressed = GLFW.glfwGetMouseButton(window, GLFW.GLFW_MOUSE_BUTTON_LEFT) == GLFW.GLFW_PRESS + if (isMousePressed && !wasMousePressed) { + if (isMouseOverButton(mouseX, mouseY)) { + dragging = true + dragOffsetX = mouseX - buttonX + dragOffsetY = mouseY - buttonY + } + } else if (!isMousePressed && wasMousePressed) { + dragging = false + } + + wasMousePressed = isMousePressed + } + + // Handle dragging + if (dragging) { + buttonX = (mouseX - dragOffsetX).coerceIn(0, width - scaledButtonWidth) + buttonY = (mouseY - dragOffsetY).coerceIn(0, height - scaledButtonHeight) + } + + val buttonColor = if (isMouseOverButton(mouseX, mouseY)) 0xAA444444.toInt() else 0xAA000000.toInt() + context.fill( + buttonX, + buttonY, + buttonX + scaledButtonWidth, + buttonY + scaledButtonHeight, + buttonColor + ) + context.drawCenteredTextWithShadow( + textRenderer, + Text.literal("Yoink and Parse NBT into file"), + buttonX + scaledButtonWidth / 2, + buttonY + (scaledButtonHeight - 8) / 2, + 0xFFFFFFFF.toInt() + ) + + context.drawCenteredTextWithShadow( + textRenderer, + Text.literal("Drag the button to reposition it"), + width / 2, + 20, + 0xFFFFFFFF.toInt() + ) + + context.drawCenteredTextWithShadow( + textRenderer, + Text.literal("Use mouse wheel to scale (Current: ${"%.2f".format(scaleFactor)}x)"), + width / 2, + 35, + 0xFFFFFFFF.toInt() + ) + + context.drawCenteredTextWithShadow( + textRenderer, + Text.literal("Press ESC or ENTER to save and exit"), + width / 2, + 50, + 0xFFFFFFFF.toInt() + ) + + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean { + val delta = verticalAmount.toFloat() * 0.1f + scaleFactor = (scaleFactor + delta).coerceIn(0.1f, 2.0f) + + buttonX = buttonX.coerceIn(0, width - scaledButtonWidth) + buttonY = buttonY.coerceIn(0, height - scaledButtonHeight) + + return true + } + + override fun close() { + YoinkGuiSettings.saveToFile() + client?.setScreen(parent) + } + + private fun isMouseOverButton(mouseX: Int, mouseY: Int): Boolean { + return mouseX >= buttonX && mouseX <= buttonX + scaledButtonWidth && + mouseY >= buttonY && mouseY <= buttonY + scaledButtonHeight + } + +} + diff --git a/src/client/kotlin/me/thatonedevil/gui/ChangelogScreen.kt b/src/client/kotlin/me/thatonedevil/gui/ChangelogScreen.kt new file mode 100644 index 0000000..bf19819 --- /dev/null +++ b/src/client/kotlin/me/thatonedevil/gui/ChangelogScreen.kt @@ -0,0 +1,113 @@ +package me.thatonedevil.gui + + +import me.thatonedevil.BuildConfig.VERSION +import me.thatonedevil.gui.MarkdownLoader.parse +import me.thatonedevil.utils.LatestErrorLog +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import kotlin.math.max + +@Environment(EnvType.CLIENT) +class ChangelogScreen(private val parent: Screen?) : Screen(Text.literal("Changelog")) { + private lateinit var content: List + private var scrollOffset = 0.0 + private var maxScroll = 0.0 + + private val lineHeight = 12 + private val topPadding = 20 + private val bottomPadding = 30 + private val scrollSpeed = 20.0 + + override fun init() { + try { + val resourceStream = javaClass.getResourceAsStream("/changelogs/${VERSION}.md") + + if (resourceStream != null) { + content = resourceStream.bufferedReader().use { + parse(it.readLines(), width - 40) + } + updateMaxScroll() + } + } catch (e: Exception) { + LatestErrorLog.record(e, "Failed to load changelog markdown.") + content = emptyList() + } + } + + private fun updateMaxScroll() { + if (!this::content.isInitialized) return + + val contentHeight = content.size * lineHeight + val viewportHeight = height - topPadding - bottomPadding + maxScroll = max(0.0, (contentHeight - viewportHeight).toDouble()) + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean { + scrollOffset = (scrollOffset - verticalAmount * scrollSpeed).coerceIn(0.0, maxScroll) + return true + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + val centerX = width / 2 + + if (!this::content.isInitialized || content.isEmpty()) { + context.drawCenteredTextWithShadow( + textRenderer, + Text.literal("No changelog available."), + centerX, + height / 2, + 0xFFE0E0E0.toInt() + ) + return + } + + val scissorTop = topPadding + val scissorBottom = height - bottomPadding + + context.enableScissor(0, scissorTop, width, scissorBottom) + + var y = topPadding - scrollOffset.toInt() + + content.forEach { line -> + if (y + lineHeight > scissorTop && y < scissorBottom) { + context.drawCenteredTextWithShadow( + textRenderer, + line, + centerX, + y, + 0xFFE0E0E0.toInt() + ) + } + y += lineHeight + } + + context.disableScissor() + + context.drawCenteredTextWithShadow( + textRenderer, + Text.literal("Press ESC to close"), + centerX, + height - 20, + 0xFFAAAAAA.toInt() + ) + + if (maxScroll > 0) { + val scrollPercentage = (scrollOffset / maxScroll * 100).toInt() + context.drawCenteredTextWithShadow( + textRenderer, + Text.literal("↕ Scroll: $scrollPercentage%"), + centerX, + height - 10, + 0xFF888888.toInt() + ) + } + } + + override fun close() { + client?.setScreen(parent) + } +} diff --git a/src/client/kotlin/me/thatonedevil/gui/MarkdownLoader.kt b/src/client/kotlin/me/thatonedevil/gui/MarkdownLoader.kt new file mode 100644 index 0000000..19d6a00 --- /dev/null +++ b/src/client/kotlin/me/thatonedevil/gui/MarkdownLoader.kt @@ -0,0 +1,104 @@ +package me.thatonedevil.gui + +import net.minecraft.client.MinecraftClient +import net.minecraft.text.Text +import net.minecraft.text.TextColor + +object MarkdownLoader { + + private val TITLE = TextColor.fromRgb(0xF5F5F5) + private val SECTION = TextColor.fromRgb(0xB8C7FF) + private val SUBSECTION = TextColor.fromRgb(0xC7EDE6) + private val BULLET = TextColor.fromRgb(0xDADADA) + private val NORMAL = TextColor.fromRgb(0xB5B5B5) + + fun parse(lines: List, wrapWidth: Int): List { + val result = mutableListOf() + + for (rawLine in lines) { + // Remove all backticks from the line + val line = rawLine.replace("`", "") + + when { + line.startsWith("# ") -> { + result += createStyledText(line.removePrefix("# "), TITLE, bold = true) + } + + line.startsWith("## ") -> { + result += createStyledText(line.removePrefix("## "), SECTION, bold = true) + } + + line.startsWith("### ") -> { + result += createStyledText(line.removePrefix("### "), SUBSECTION, bold = true) + } + + line.startsWith("- ") || line.startsWith("* ") -> { + result += wrapAndStyle("• ${line.drop(2)}", wrapWidth, BULLET) + } + + line.isBlank() -> { + result += Text.empty() + } + + else -> { + result += wrapAndStyle(line, wrapWidth, NORMAL) + } + } + } + + return result + } + + /** + * Creates a styled text with the specified color and optional bold formatting. + */ + private fun createStyledText(text: String, color: TextColor, bold: Boolean = false): Text { + return Text.literal(text).styled { style -> + var s = style.withColor(color) + if (bold) s = s.withBold(true) + s + } + } + + /** + * Wraps text to fit within the specified width and applies styling to each line. + */ + private fun wrapAndStyle(text: String, maxWidth: Int, color: TextColor): List { + return wrapText(text, maxWidth).map { wrappedLine -> + Text.literal(wrappedLine).styled { style -> style.withColor(color) } + } + } + + /** + * Wraps text to fit within the specified pixel width, breaking on word boundaries. + * Returns a list of strings (not Text objects). + */ + private fun wrapText(text: String, maxWidth: Int): List { + if (text.isEmpty()) return listOf("") + + val renderer = MinecraftClient.getInstance().textRenderer + val words = text.split(" ") + + val lines = mutableListOf() + var current = "" + + for (word in words) { + val test = if (current.isEmpty()) word else "$current $word" + + if (renderer.getWidth(test) > maxWidth) { + if (current.isNotEmpty()) { + lines += current + current = word + } else { + lines += word + } + } else { + current = test + } + } + + if (current.isNotEmpty()) lines += current + + return lines + } +} diff --git a/src/client/kotlin/me/thatonedevil/inventory/TopInventory.kt b/src/client/kotlin/me/thatonedevil/inventory/TopInventory.kt index 3011d4b..6f659ca 100644 --- a/src/client/kotlin/me/thatonedevil/inventory/TopInventory.kt +++ b/src/client/kotlin/me/thatonedevil/inventory/TopInventory.kt @@ -1,5 +1,6 @@ package me.thatonedevil.inventory +import me.thatonedevil.utils.Utils.debug import net.minecraft.client.MinecraftClient import net.minecraft.item.ItemStack @@ -8,6 +9,7 @@ class TopInventory(client: MinecraftClient) { private val topInventory = client.player?.currentScreenHandler fun getTopInventory(): Any? { + debug("Top Inventory: $topInventory") return topInventory } @@ -16,6 +18,8 @@ class TopInventory(client: MinecraftClient) { } fun inventoryItems(): MutableList? { - return topInventory?.stacks?.stream()?.toList() + val items = topInventory?.stacks?.stream()?.toList() + debug("Items: $items") + return items } } \ No newline at end of file diff --git a/src/client/kotlin/me/thatonedevil/inventory/YoinkInventory.kt b/src/client/kotlin/me/thatonedevil/inventory/YoinkInventory.kt index 6669496..92c93d0 100644 --- a/src/client/kotlin/me/thatonedevil/inventory/YoinkInventory.kt +++ b/src/client/kotlin/me/thatonedevil/inventory/YoinkInventory.kt @@ -29,6 +29,5 @@ class YoinkInventory(private val player: ClientPlayerEntity, private val invento } - fun getYoinkedItems(): List = encodedItems } diff --git a/src/client/kotlin/me/thatonedevil/nbt/ComponentValue.kt b/src/client/kotlin/me/thatonedevil/nbt/ComponentValue.kt index e5eca4e..6fc0e94 100644 --- a/src/client/kotlin/me/thatonedevil/nbt/ComponentValue.kt +++ b/src/client/kotlin/me/thatonedevil/nbt/ComponentValue.kt @@ -2,6 +2,8 @@ package me.thatonedevil.nbt import com.google.gson.JsonElement import com.google.gson.JsonObject +import me.thatonedevil.config.YoinkGuiSettings +import me.thatonedevil.utils.Utils.debug interface ComponentValueHandler { fun handle(obj: JsonObject): HandlerResult @@ -13,17 +15,26 @@ object ComponentValueRegistry { private val handlers = mutableListOf() init { - // Registration order matters for the final string composition. Keep as close to original behaviour as possible. - register(ColorHandler) - register(StyleHandler) - register(GradientHandler) - register(TextHandler) + refreshHandlers() } fun register(handler: ComponentValueHandler) { handlers.add(handler) } + fun refreshHandlers() { + handlers.clear() + // Registration order matters for the final string composition. Keep as close to original behaviour as possible. + if (YoinkGuiSettings.toggleColorParser.get()) { register(ColorHandler) } + if (YoinkGuiSettings.toggleStyleParser.get()) { register(StyleHandler) } + if (YoinkGuiSettings.toggleShadowParser.get()) { register(ShadowHandler) } + if (YoinkGuiSettings.toggleGradientParser.get()) { register(GradientHandler) } + register(TextHandler) + + debug("ComponentValueRegistry handlers refreshed. Current handlers: ${handlers.map { it.javaClass.simpleName }}") + } + + fun process(obj: JsonObject): HandlerResult { val builder = StringBuilder() handlers.forEach { h -> @@ -63,18 +74,41 @@ object ComponentValueRegistry { } } +private fun isLegacyFormat(): Boolean = YoinkGuiSettings.formatOption.get() == "LEGACY" object ColorHandler : ComponentValueHandler { - private val colorCodes = mapOf( + private val LEGACY_COLOR_CODES = mapOf( "black" to "&0", "dark_blue" to "&1", "dark_green" to "&2", "dark_aqua" to "&3", "dark_red" to "&4", "dark_purple" to "&5", "gold" to "&6", "gray" to "&7", "dark_gray" to "&8", "blue" to "&9", "green" to "&a", "aqua" to "&b", "red" to "&c", "light_purple" to "&d", "yellow" to "&e", "white" to "&f" ) + private val MINIMESSAGE_COLOR_CODES = mapOf( + "black" to "", "dark_blue" to "", "dark_green" to "", "dark_aqua" to "", + "dark_red" to "", "dark_purple" to "", "gold" to "", "gray" to "", + "dark_gray" to "", "blue" to "", "green" to "", "aqua" to "", + "red" to "", "light_purple" to "", "yellow" to "", "white" to "" + ) + private fun formatColor(color: String?): String { - val lower = color?.lowercase() ?: return "" - return colorCodes[lower] ?: if (color.startsWith("#")) "" else "" + if (color == null) return "" + val lower = color.lowercase() + val map = if (isLegacyFormat()) LEGACY_COLOR_CODES else MINIMESSAGE_COLOR_CODES + + // Named color + map[lower]?.let { return it } + + if (color.startsWith("#")) { + return if (isLegacyFormat()) { + // Legacy can't represent hex reliably and messyly, so skip + "" + } else { + "" + } + } + + return "" } override fun handle(obj: JsonObject): HandlerResult { @@ -86,20 +120,54 @@ object ColorHandler : ComponentValueHandler { object StyleHandler : ComponentValueHandler { override fun handle(obj: JsonObject): HandlerResult { val b = StringBuilder() - if (ComponentValueRegistry.getBooleanValue(obj.get("bold"))) b.append("&l") + val legacy = isLegacyFormat() + + if (ComponentValueRegistry.getBooleanValue(obj.get("bold"))) b.append(if (legacy) "&l" else "") - // Default to italic when the `italic` field is missing. If `italic` is present and - val el = obj.get("italic") - val isItalic = el == null || ComponentValueRegistry.getBooleanValue(el) - if (isItalic) b.append("&o") + val isItalic = ComponentValueRegistry.getBooleanValue(obj.get("italic")) + if (isItalic) b.append(if (legacy) "&o" else "") + + if (ComponentValueRegistry.getBooleanValue(obj.get("underlined"))) b.append(if (legacy) "&n" else "") + if (ComponentValueRegistry.getBooleanValue(obj.get("strikethrough"))) b.append(if (legacy) "&m" else "") + if (ComponentValueRegistry.getBooleanValue(obj.get("obfuscated"))) b.append(if (legacy) "&k" else "") - if (ComponentValueRegistry.getBooleanValue(obj.get("underlined"))) b.append("&n") - if (ComponentValueRegistry.getBooleanValue(obj.get("strikethrough"))) b.append("&m") - if (ComponentValueRegistry.getBooleanValue(obj.get("obfuscated"))) b.append("&k") return HandlerResult(b.toString()) } } +object ShadowHandler : ComponentValueHandler { + private val namedColors = mapOf( + 0x000000 to "black", 0x0000AA to "dark_blue", 0x00AA00 to "dark_green", 0x00AAAA to "dark_aqua", + 0xAA0000 to "dark_red", 0xAA00AA to "dark_purple", 0xFFAA00 to "gold", 0xAAAAAA to "gray", + 0x555555 to "dark_gray", 0x5555FF to "blue", 0x55FF55 to "green", 0x55FFFF to "aqua", + 0xFF5555 to "red", 0xFF55FF to "light_purple", 0xFFFF55 to "yellow", 0xFFFFFF to "white" + ) + + override fun handle(obj: JsonObject): HandlerResult { + val shadowColorElement = obj.get("shadow_color") ?: return HandlerResult("") + + if (!shadowColorElement.isJsonPrimitive) return HandlerResult("") + + // shadow_color is a signed 32-bit int representing ARGB + val shadowColorInt = shadowColorElement.asInt + + // Extract ARGB components + val alpha = (shadowColorInt shr 24) and 0xFF + val red = (shadowColorInt shr 16) and 0xFF + val green = (shadowColorInt shr 8) and 0xFF + val blue = shadowColorInt and 0xFF + + // Convert alpha from 0-255 to 0.0-1.0 + val alphaDecimal = String.format("%.2f", alpha / 255.0).trimEnd('0').trimEnd('.') + + // Check if RGB matches a named color + val rgbInt = (red shl 16) or (green shl 8) or blue + val colorString = namedColors[rgbInt] ?: String.format("%02X%02X%02X", red, green, blue).lowercase() + + return HandlerResult("") + } +} + object GradientHandler : ComponentValueHandler { override fun handle(obj: JsonObject): HandlerResult { val extra = obj.get("extra") diff --git a/src/client/kotlin/me/thatonedevil/nbt/FormatOptions.kt b/src/client/kotlin/me/thatonedevil/nbt/FormatOptions.kt new file mode 100644 index 0000000..854455a --- /dev/null +++ b/src/client/kotlin/me/thatonedevil/nbt/FormatOptions.kt @@ -0,0 +1,17 @@ +package me.thatonedevil.nbt + +import dev.isxander.yacl3.api.NameableEnum +import net.minecraft.text.Text + +enum class FormatOptions : NameableEnum { + + MINIMESSAGE, + LEGACY; + + override fun getDisplayName(): Text? { + return when (this) { + MINIMESSAGE -> Text.of("Minimessage") + LEGACY -> Text.of("Legacy") + } + } +} \ No newline at end of file diff --git a/src/client/kotlin/me/thatonedevil/utils/LatestErrorLog.kt b/src/client/kotlin/me/thatonedevil/utils/LatestErrorLog.kt new file mode 100644 index 0000000..0339454 --- /dev/null +++ b/src/client/kotlin/me/thatonedevil/utils/LatestErrorLog.kt @@ -0,0 +1,22 @@ +package me.thatonedevil.utils + +import java.util.concurrent.atomic.AtomicReference + +object LatestErrorLog { + private val latest = AtomicReference(null) + private val latestMessage = AtomicReference(null) + + fun record(t: Throwable?, context: String? = null) { + latest.set(t) + latestMessage.set(context) + } + + fun getLatestThrowable(): Throwable? = latest.get() + fun getLatestMessage(): String? = latestMessage.get() + + fun getLatestStackTraceMessage(): String? = getLatestThrowable()?.stackTraceToString() ?: getLatestMessage() + fun getLatestErrorName(): String? = + getLatestThrowable()?.let { it::class.simpleName } + ?: getLatestMessage() + +} \ No newline at end of file diff --git a/src/client/kotlin/me/thatonedevil/utils/Utils.kt b/src/client/kotlin/me/thatonedevil/utils/Utils.kt index 415a40f..93f2128 100644 --- a/src/client/kotlin/me/thatonedevil/utils/Utils.kt +++ b/src/client/kotlin/me/thatonedevil/utils/Utils.kt @@ -1,9 +1,12 @@ package me.thatonedevil.utils +import me.thatonedevil.YoinkGUIClient.logger +import me.thatonedevil.YoinkGUIClient.yoinkGuiSettings import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.minimessage.MiniMessage import net.minecraft.client.MinecraftClient + //? if >1.21.1 { import net.kyori.adventure.platform.modcommon.MinecraftClientAudiences //?} else { @@ -39,12 +42,15 @@ object Utils { return miniMessage.deserialize(convertLegacyToMini(this)) } - fun String.toClickable(message: String): Component { + fun String.toClickCopy(message: String): Component { return this.toComponent().clickEvent(ClickEvent.copyToClipboard(message)) } fun String.toClickURL(message: String): Component { return this.toComponent().clickEvent(ClickEvent.openUrl(message)) } + fun String.toClickCommand(command: String): Component { + return this.toComponent().clickEvent(ClickEvent.runCommand(command)) + } // Ensure message sending runs on the client/render thread fun sendChat(message: String) { @@ -54,12 +60,24 @@ object Utils { } fun sendChat(vararg messages: Component) { - val mc = MinecraftClient.getInstance() - val action = Runnable { - for (component in messages) { - audience.sendMessage(component) + try { + val mc = MinecraftClient.getInstance() + val action = Runnable { + for (component in messages) { + audience.sendMessage(component) + } } + mc?.execute(action) ?: action.run() + } catch (e: Exception) { + LatestErrorLog.record(e, "Error sending chat message (MiniMessage)") + debug("Failed to send chat message: ${e.message}") } - mc?.execute(action) ?: action.run() } + + fun debug(message: String) { + if (yoinkGuiSettings.debugMode.get() == true) { + logger.info(message) + } + } + } diff --git a/src/client/kotlin/me/thatonedevil/utils/api/UpdateChecker.kt b/src/client/kotlin/me/thatonedevil/utils/api/UpdateChecker.kt index 04b4696..cda853f 100644 --- a/src/client/kotlin/me/thatonedevil/utils/api/UpdateChecker.kt +++ b/src/client/kotlin/me/thatonedevil/utils/api/UpdateChecker.kt @@ -7,11 +7,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.thatonedevil.BuildConfig -import me.thatonedevil.YoinkGUI.logger +import me.thatonedevil.YoinkGUIClient.logger +import me.thatonedevil.utils.LatestErrorLog +import me.thatonedevil.utils.Utils.debug import me.thatonedevil.utils.Utils.sendChat +import me.thatonedevil.utils.Utils.toClickCommand import me.thatonedevil.utils.Utils.toClickURL import me.thatonedevil.utils.Utils.toComponent -import net.fabricmc.fabric.api.client.networking.v1.ClientLoginConnectionEvents +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader @@ -19,6 +22,7 @@ import java.net.URI object UpdateChecker { + var serverName: String? = "Unknown" var currentUpdateVersion: ModrinthVersion? = null suspend fun getUpdateVersion(): ModrinthVersion? { @@ -40,7 +44,14 @@ object UpdateChecker { } fun setupJoinListener() { - ClientLoginConnectionEvents.INIT.register { _, _ -> + ClientPlayConnectionEvents.JOIN.register { _, _, client -> + serverName = when (client.currentServerEntry?.address){ + "0", "localhost" -> "Singleplayer" + else -> client.currentServerEntry?.address ?: "Singleplayer" + } + + debug("Server name: $serverName") + checkVersion() } } @@ -50,10 +61,10 @@ object UpdateChecker { getUpdateVersion()?.let { version -> sendChat( "\nA new update is available: &m&c${BuildConfig.VERSION}&r &a${version.cleanVersion}".toComponent(), - "${version.getUpdateLink()} &7&o(Click to open)\n".toClickURL(version.getUpdateLink()) + "${version.getUpdateLink()}\n&7&o(Click to open)\n".toClickURL(version.getUpdateLink()) ) } ?: run { - sendChat("You have the latest version of YoinkGUI!") + sendChat("You have the latest version of YoinkGUI! &7&o(Click to open changelog)".toClickCommand("/yoinkguiclient changelog")) } } } @@ -67,15 +78,16 @@ object UpdateChecker { for (element in elements) { val version = ModrinthVersion(element) if (version.supportsGameVersion(BuildConfig.MC_VERSION)) { - logger?.info("Found compatible version: ${version.cleanVersion} for MC ${BuildConfig.MC_VERSION}") + debug("Found compatible version: ${version.cleanVersion} for MC ${BuildConfig.MC_VERSION}") return version } } - logger?.error("No compatible version found for MC ${BuildConfig.MC_VERSION}") + logger.error("No compatible version found for MC ${BuildConfig.MC_VERSION}") return null - } catch (_: IOException) { - logger?.error("Checking for update failed!") + } catch (error: IOException) { + LatestErrorLog.record(error, "Update Check Failure") + logger.error("Checking for update failed!") } return null } diff --git a/src/main/kotlin/me/thatonedevil/YoinkGUI.kt b/src/main/kotlin/me/thatonedevil/YoinkGUI.kt index c8efe79..1515eea 100644 --- a/src/main/kotlin/me/thatonedevil/YoinkGUI.kt +++ b/src/main/kotlin/me/thatonedevil/YoinkGUI.kt @@ -5,9 +5,8 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory object YoinkGUI : ModInitializer { - val logger: Logger? = LoggerFactory.getLogger(BuildConfig.MOD_ID) + val logger: Logger = LoggerFactory.getLogger(BuildConfig.MOD_ID) override fun onInitialize() { - - } + } } \ No newline at end of file diff --git a/src/main/resources/assets/yoinkgui/icon.png b/src/main/resources/assets/yoinkgui/icon.png index 1ffea01..0df29be 100644 Binary files a/src/main/resources/assets/yoinkgui/icon.png and b/src/main/resources/assets/yoinkgui/icon.png differ diff --git a/src/main/resources/assets/yoinkgui/lang/en_us.json b/src/main/resources/assets/yoinkgui/lang/en_us.json new file mode 100644 index 0000000..50f3b41 --- /dev/null +++ b/src/main/resources/assets/yoinkgui/lang/en_us.json @@ -0,0 +1,5 @@ +{ + "key.category.minecraft.keybinds": "Yoinkgui Key Binds", + "key.yoinkgui.position": "Open Button Position Screen" +} + diff --git a/changelogs/1.3.0.md b/src/main/resources/changelogs/1.3.0.md similarity index 100% rename from changelogs/1.3.0.md rename to src/main/resources/changelogs/1.3.0.md diff --git a/changelogs/1.4.0.md b/src/main/resources/changelogs/1.4.0.md similarity index 100% rename from changelogs/1.4.0.md rename to src/main/resources/changelogs/1.4.0.md diff --git a/changelogs/1.5.0.md b/src/main/resources/changelogs/1.5.0.md similarity index 100% rename from changelogs/1.5.0.md rename to src/main/resources/changelogs/1.5.0.md diff --git a/changelogs/1.6.0.md b/src/main/resources/changelogs/1.6.0.md similarity index 100% rename from changelogs/1.6.0.md rename to src/main/resources/changelogs/1.6.0.md diff --git a/changelogs/1.6.1.md b/src/main/resources/changelogs/1.6.1.md similarity index 100% rename from changelogs/1.6.1.md rename to src/main/resources/changelogs/1.6.1.md diff --git a/src/main/resources/changelogs/1.6.2.md b/src/main/resources/changelogs/1.6.2.md new file mode 100644 index 0000000..520d9e7 --- /dev/null +++ b/src/main/resources/changelogs/1.6.2.md @@ -0,0 +1,16 @@ +# Update 1.6.2 (2025-12-23) + +## Summary + +Modrinth is currently unavailable; downloads and some services may be temporarily affected. + +## Fixes + +- Fixed a bug where hovering over a button, closing the inventory, and then left-clicking still registered the button press. + +## Code Changes + +- Added a `debug` option to the config to enable more detailed developer logging. +- Renamed parsed files to the format `_.txt` for easier identification. +- Switched logger context from `main` to `client`. +- Resolved build error for Minecraft `1.21.1`. \ No newline at end of file diff --git a/src/main/resources/changelogs/1.6.3.md b/src/main/resources/changelogs/1.6.3.md new file mode 100644 index 0000000..ecbdd0c --- /dev/null +++ b/src/main/resources/changelogs/1.6.3.md @@ -0,0 +1,14 @@ +# Update 1.6.3 (2025-12-30) + +## Features + +- Added shadow parsing (e.g., ``) to allow for custom shadow colors in text +- Added the ability to toggle the parsing options for all options on by default +- Added the option to include raw nbt data in the file + +## Bug Fixes + +- Fixed duplicate text rendering issue where text was being appended twice: + - The TextHandler was appending text via `ComponentValueRegistry.process(obj)` on line 28 + - The same text was being appended again via `obj.get("text")?.asString` on line 32 + - Removed the redundant append to prevent text duplication \ No newline at end of file diff --git a/src/main/resources/changelogs/1.7.0.md b/src/main/resources/changelogs/1.7.0.md new file mode 100644 index 0000000..368edc3 --- /dev/null +++ b/src/main/resources/changelogs/1.7.0.md @@ -0,0 +1,15 @@ +# Update 1.7.0 (2025-12-27) + +## Features +- Added /yoinkgui debug command where u can copy latest log along with mod info for easier bug reporting +- Added the ability to move the parse buttons around the screen by dragging them. Press M to toggle the menu +- Removed scale from config and made it a scrollable option in the menu (press M to open) + +## Fixes + +- Ensure the parse button hover state won't accidentally trigger when the inventory (or other screens) are closed. +- Prevent failures when sending formatted chat notifications (wraps chat sends and records errors instead of crashing). + +## Code changed +- Added devauth for development stuff idk + diff --git a/src/main/resources/changelogs/1.7.1.md b/src/main/resources/changelogs/1.7.1.md new file mode 100644 index 0000000..6997181 --- /dev/null +++ b/src/main/resources/changelogs/1.7.1.md @@ -0,0 +1,13 @@ +# Update 1.7.1 (2025-12-27) + +## Features +- Added a toggle feature in config to switch between Minimessage and Legacy format options + +## Fixes + +- Fixed italic issue on latest versions of Minecraft where parsed text would always contain italics +- even if it was not in the lore/name. + + + + diff --git a/src/main/resources/changelogs/1.7.2.md b/src/main/resources/changelogs/1.7.2.md new file mode 100644 index 0000000..452773d --- /dev/null +++ b/src/main/resources/changelogs/1.7.2.md @@ -0,0 +1,26 @@ +# YoinkGUI v1.7.2 +### Released December 27, 2025 + +### In-Game Changelog Viewer +- You can now view changelogs directly in-game! +- Use /yoinkguiclient changelog to view this changelog + +### GUI Repositioning Menu +- Use /yoinkguiclient menu to open the repositioning screen +- Drag the button to your preferred location + +## Bug Fixes +- Fixed the move button appearing on non-inventory screens +- The move button now only shows up when you're viewing your inventory + +## Technical Improvements +- Enhanced stability with null safety checks for changelog loading +- Improved error handling to prevent crashes when viewing changelogs +- Better resource management for markdown parsing +- Debug command now includes class name instead of stacktrace. But stacktrace is copied. +- Added `Utils#toClickCommand` method. + +## Commands Summary +- /yoinkguiclient changelog - View version history +- /yoinkguiclient menu - Reposition GUI button + diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index ba14f86..1b5bb55 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,54 +1,58 @@ { - "schemaVersion": 1, - "id": "yoinkgui", - "version": "${version}", - "name": "YoinkGUI", - "description": "A Fabric mod that allows users to easily copy the name and lore of items from any GUI!", - "authors": [ - "ThatOneDevil" - ], - "contact": { - "homepage": "https://modrinth.com/project/5j4oEPp2", - "sources": "https://github.com/ThatOneDevil/yoinkgui", - "issues": "https://github.com/ThatOneDevil/yoinkgui/issues" - }, - "license": "GPL-3.0 license", - "icon": "assets/yoinkgui/icon.png", - "environment": "*", - "entrypoints": { - "modmenu": [ - "me.thatonedevil.config.ModMenuIntegration" - ], - "main": [ - { - "value": "me.thatonedevil.YoinkGUI", - "adapter": "kotlin" - } - ], - "client": [ - { - "value": "me.thatonedevil.YoinkGUIClient", - "adapter": "kotlin" - } - ] - }, - "mixins": [ - "yoinkgui.mixins.json", - { - "config": "yoinkgui.client.mixins.json", - "environment": "client" - } - ], - "depends": { - "fabricloader": ">=0.16.0", - "minecraft": "${mc}", - "java": ">=21", - "fabric-api": "*", - "fabric-language-kotlin": "*", - "yet_another_config_lib_v3": ">=${yaclVersion}", - "modmenu": ">=${modmenuVersion}" - }, - "suggests": { - "another-mod": "*" - } + "schemaVersion": 1, + "id": "yoinkgui", + "version": "${version}", + "name": "YoinkGUI", + "description": "A Fabric mod that allows users to easily copy the name and lore of items from any GUI!", + "authors": [ + "ThatOneDevil" + ], + "contact": { + "homepage": "https://modrinth.com/project/5j4oEPp2", + "sources": "https://github.com/ThatOneDevil/yoinkgui", + "issues": "https://github.com/ThatOneDevil/yoinkgui/issues" + }, + "license": "GPL-3.0 license", + "icon": "assets/yoinkgui/icon.png", + "environment": "*", + "entrypoints": { + "modmenu": [ + "me.thatonedevil.config.ModMenuIntegration" + ], + "main": [ + { + "value": "me.thatonedevil.YoinkGUI", + "adapter": "kotlin" + } + ], + "client": [ + { + "value": "me.thatonedevil.YoinkGUIClient", + "adapter": "kotlin" + } + ] + }, + "mixins": [ + "yoinkgui.mixins.json", + { + "config": "yoinkgui.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.16.0", + "minecraft": "${mc}", + "java": ">=21", + "fabric-api": "*", + "fabric-language-kotlin": "*", + "yet_another_config_lib_v3": ">=${yaclVersion}", + "modmenu": ">=${modmenuVersion}" + }, + "custom": { + "modmenu": { + "links": { + "Discord": "https://discord.com/invite/kcegGvZvpC" + } + } + } } \ No newline at end of file diff --git a/src/templates/kotlin/me.thatonedevil/BuildConfig.kt b/src/templates/kotlin/me.thatonedevil/BuildConfig.kt index 1b620c7..c87444f 100644 --- a/src/templates/kotlin/me.thatonedevil/BuildConfig.kt +++ b/src/templates/kotlin/me.thatonedevil/BuildConfig.kt @@ -1,7 +1,7 @@ package me.thatonedevil object BuildConfig { - const val MOD_ID = "yoinkGUI" + const val MOD_ID = "yoinkgui" const val VERSION = "${version}" const val MC_VERSION = "${mcVersion}"