diff --git a/build.gradle b/build.gradle index e36830e..b8d7767 100644 --- a/build.gradle +++ b/build.gradle @@ -47,14 +47,14 @@ dependencies { modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" // Other Mod Dependencies (e.g., Cobblemon) - modImplementation "com.cobblemon:fabric:1.6.0+1.21.1-SNAPSHOT" + modImplementation "com.cobblemon:fabric:1.7.2+1.21.1-SNAPSHOT" // JSON and GSON dependencies - add implementation first, then include - implementation "org.json:json:20231013" - include "org.json:json:20231013" + implementation "org.json:json:20250517" + include "org.json:json:20250517" - implementation "com.google.code.gson:gson:2.10.1" - include "com.google.code.gson:gson:2.10.1" + implementation "com.google.code.gson:gson:2.13.2" + include "com.google.code.gson:gson:2.13.2" } processResources { diff --git a/gradle.properties b/gradle.properties index b551732..f301262 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,19 +1,19 @@ # Done to increase the memory available to gradle. -org.gradle.jvmargs=-Xmx1G +org.gradle.jvmargs=-Xmx2G org.gradle.parallel=true # Fabric Properties # check these on https://fabricmc.net/develop minecraft_version=1.21.1 -loader_version=0.16.14 -loom_version=1.10-SNAPSHOT -fabric_kotlin_version=1.13.2+kotlin.2.1.20 +loader_version=0.17.2 +loom_version=1.11-SNAPSHOT +fabric_kotlin_version=1.13.6+kotlin.2.2.20 # Mod Properties -mod_version=1.0.0 +mod_version=1.5.0 maven_group=co.sirblob archives_base_name=cobblesync # Dependencies -fabric_version=0.116.0+1.21.1 +fabric_version=0.116.6+1.21.1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..f8e1ee3 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e18bc25..ca025c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3..adff685 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -205,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/client/kotlin/co/sirblob/CobbleSyncClient.kt b/src/client/kotlin/co/sirblob/CobbleSyncClient.kt index a16921a..d590d95 100644 --- a/src/client/kotlin/co/sirblob/CobbleSyncClient.kt +++ b/src/client/kotlin/co/sirblob/CobbleSyncClient.kt @@ -1,42 +1,49 @@ package co.sirblob +import co.sirblob.gui.CobbleSyncScreen +import co.sirblob.network.ClientPacketHandler import com.mojang.blaze3d.platform.InputConstants 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.KeyMapping import org.lwjgl.glfw.GLFW +import org.slf4j.LoggerFactory object CobbleSyncClient : ClientModInitializer { - override fun onInitializeClient() { + private val logger = LoggerFactory.getLogger("cobblesync-client") - val binding1 = KeyBindingHelper.registerKeyBinding( - KeyMapping( - "key.fabric-key-binding-api-v1-testmod.test_keybinding_1", - InputConstants.Type.KEYSYM, - GLFW.GLFW_KEY_U, - "key.category.first.test" - ) - ) + override fun onInitializeClient() { + logger.info("CobbleSync Client initializing...") - ClientTickEvents.END_CLIENT_TICK.register { client -> - while (binding1.consumeClick()) { + // Register client-side network handlers + ClientPacketHandler.register() - var player = client.player - - // client.player?.sendSystemMessage( - // Component.literal("Key 1 was pressed!") - // .withStyle { style -> - // style.withColor(0x00FF00) - // } - // ) + // Register keybind for opening the CobbleSync GUI + val openGuiKeybind = + KeyBindingHelper.registerKeyBinding( + KeyMapping( + "key.cobblesync.open_gui", + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_U, + "key.categories.cobblesync" + ) + ) - if(player != null) { + // Handle keybind press + ClientTickEvents.END_CLIENT_TICK.register { client -> + while (openGuiKeybind.consumeClick()) { + val player = client.player + if (player != null && client.screen == null) { + // Open the CobbleSync GUI + client.setScreen(CobbleSyncScreen()) + logger.info("Opened CobbleSync GUI") } - } - } - } -} \ No newline at end of file + } + + logger.info("CobbleSync Client initialized!") + } +} diff --git a/src/client/kotlin/co/sirblob/gui/CobbleSyncScreen.kt b/src/client/kotlin/co/sirblob/gui/CobbleSyncScreen.kt new file mode 100644 index 0000000..16a242e --- /dev/null +++ b/src/client/kotlin/co/sirblob/gui/CobbleSyncScreen.kt @@ -0,0 +1,151 @@ +package co.sirblob.gui + +import co.sirblob.network.CobbleSyncPackets +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component + +@Environment(EnvType.CLIENT) +class CobbleSyncScreen : Screen(Component.literal("CobbleSync")) { + + // Status message to display feedback + private var statusMessage: Component = Component.literal("") + private var statusColor: Int = 0xFFFFFF + + // Buttons + private lateinit var syncButton: Button + private lateinit var loadButton: Button + private lateinit var closeButton: Button + + override fun init() { + super.init() + + val buttonWidth = 200 + val buttonHeight = 20 + val centerX = width / 2 + val centerY = height / 2 + + // Sync Button - uploads Box 30 to the server + syncButton = Button.builder(Component.literal("⬆ Sync Box 30")) { _ -> + setStatus(Component.literal("Syncing..."), 0xFFFF55) + ClientPlayNetworking.send(CobbleSyncPackets.SyncRequestPayload()) + } + .bounds(centerX - buttonWidth / 2, centerY - 40, buttonWidth, buttonHeight) + .build() + + // Load Button - downloads saved Pokemon to Box 1 + loadButton = Button.builder(Component.literal("⬇ Load to Box 1")) { _ -> + setStatus(Component.literal("Loading..."), 0xFFFF55) + ClientPlayNetworking.send(CobbleSyncPackets.LoadRequestPayload()) + } + .bounds(centerX - buttonWidth / 2, centerY - 10, buttonWidth, buttonHeight) + .build() + + // Close Button + closeButton = Button.builder(Component.literal("Close")) { _ -> + onClose() + } + .bounds(centerX - buttonWidth / 2, centerY + 30, buttonWidth, buttonHeight) + .build() + + addRenderableWidget(syncButton) + addRenderableWidget(loadButton) + addRenderableWidget(closeButton) + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + // Render background + renderBackground(guiGraphics, mouseX, mouseY, partialTick) + + // Draw title + guiGraphics.drawCenteredString( + font, + Component.literal("§6§lCobbleSync"), + width / 2, + height / 2 - 80, + 0xFFFFFF + ) + + // Draw subtitle/description + guiGraphics.drawCenteredString( + font, + Component.literal("§7Sync your Pokémon across servers"), + width / 2, + height / 2 - 65, + 0xAAAAAA + ) + + // Draw separator line + guiGraphics.fill( + width / 2 - 100, + height / 2 - 55, + width / 2 + 100, + height / 2 - 54, + 0xFF444444.toInt() + ) + + // Draw status message + if (statusMessage.string.isNotEmpty()) { + guiGraphics.drawCenteredString( + font, + statusMessage, + width / 2, + height / 2 + 60, + statusColor + ) + } + + // Draw info text + guiGraphics.drawCenteredString( + font, + Component.literal("§8Sync: Upload Box 30 (max 12 Pokémon)"), + width / 2, + height / 2 + 80, + 0x888888 + ) + guiGraphics.drawCenteredString( + font, + Component.literal("§8Load: Download to Box 1 (must be empty)"), + width / 2, + height / 2 + 92, + 0x888888 + ) + + super.render(guiGraphics, mouseX, mouseY, partialTick) + } + + override fun isPauseScreen(): Boolean = false + + fun setStatus(message: Component, color: Int) { + this.statusMessage = message + this.statusColor = color + } + + companion object { + // Singleton instance for updating status from network handlers + var currentInstance: CobbleSyncScreen? = null + private set + + fun open(screen: CobbleSyncScreen) { + currentInstance = screen + } + + fun close() { + currentInstance = null + } + } + + override fun onClose() { + CobbleSyncScreen.close() + super.onClose() + } + + override fun added() { + super.added() + CobbleSyncScreen.open(this) + } +} diff --git a/src/client/kotlin/co/sirblob/network/ClientPacketHandler.kt b/src/client/kotlin/co/sirblob/network/ClientPacketHandler.kt new file mode 100644 index 0000000..bff5762 --- /dev/null +++ b/src/client/kotlin/co/sirblob/network/ClientPacketHandler.kt @@ -0,0 +1,27 @@ +package co.sirblob.network + +import co.sirblob.gui.CobbleSyncScreen +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking +import net.minecraft.network.chat.Component + +/** + * Client-side packet handlers for CobbleSync + */ +@Environment(EnvType.CLIENT) +object ClientPacketHandler { + + fun register() { + // Register handler for sync response + ClientPlayNetworking.registerGlobalReceiver(CobbleSyncPackets.SYNC_RESPONSE_ID) { payload, context -> + context.client().execute { + val screen = CobbleSyncScreen.currentInstance + if (screen != null) { + val color = if (payload.success) 0x55FF55 else 0xFF5555 + screen.setStatus(Component.literal(payload.message), color) + } + } + } + } +} diff --git a/src/main/kotlin/co/sirblob/CobbleSync.kt b/src/main/kotlin/co/sirblob/CobbleSync.kt index 030f719..332e263 100644 --- a/src/main/kotlin/co/sirblob/CobbleSync.kt +++ b/src/main/kotlin/co/sirblob/CobbleSync.kt @@ -1,158 +1,283 @@ package co.sirblob -import com.cobblemon.mod.common.api.text.red -import com.cobblemon.mod.common.api.text.green +import co.sirblob.network.ServerPacketHandler import com.cobblemon.mod.common.api.text.blue -import com.cobblemon.mod.common.api.pokemon.PokemonSpecies -import com.cobblemon.mod.common.api.pokemon.stats.Stat -import com.cobblemon.mod.common.api.pokemon.stats.Stats -import com.cobblemon.mod.common.api.storage.pc.PCBox +import com.cobblemon.mod.common.api.text.green +import com.cobblemon.mod.common.api.text.red import com.cobblemon.mod.common.util.pc -import com.cobblemon.mod.common.pokemon.Pokemon -import com.cobblemon.mod.common.pokemon.Species -import com.cobblemon.mod.common.Cobblemon -import com.cobblemon.mod.relocations.oracle.truffle.regex.tregex.util.json.Json -import org.json.JSONObject -import com.mojang.brigadier.Command -import com.mojang.brigadier.arguments.IntegerArgumentType -import com.mojang.brigadier.builder.LiteralArgumentBuilder -import com.mojang.brigadier.context.CommandContext import com.google.gson.JsonObject import com.google.gson.JsonParser +import com.mojang.brigadier.Command +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.context.CommandContext import net.fabricmc.api.ModInitializer import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.minecraft.commands.CommandSourceStack import net.minecraft.network.chat.Component -import net.minecraft.core.RegistryAccess -import net.minecraft.resources.ResourceLocation +import org.json.JSONObject import org.slf4j.LoggerFactory - object CobbleSync : ModInitializer { - private val logger = LoggerFactory.getLogger("cobblesync") + private val logger = LoggerFactory.getLogger("cobblesync") private val request = Request("https://authserver.sirblob.co") override fun onInitialize() { + logger.info("CobbleSync initializing...") - CommandRegistrationCallback.EVENT.register(CommandRegistrationCallback { dispatcher, _, _ -> - dispatcher.register( - LiteralArgumentBuilder.literal("cobblesync") - .then( - LiteralArgumentBuilder.literal("sync") - .executes(Command { context: CommandContext -> - - val player = context.source.playerOrException - player.sendSystemMessage(Component.literal("Syncing box...").red()) - - val box30 = player.pc().boxes.get(29) - var pokemonCount = 0 + // Register network packet handlers for GUI communication + ServerPacketHandler.register() + logger.info("Registered CobbleSync network handlers") - box30.filterNotNull().forEach { pokemon -> - player.sendSystemMessage( - Component.literal("Syncing Pokémon: ${pokemon.species.name} (Level ${pokemon.level})") - .blue() + CommandRegistrationCallback.EVENT.register( + CommandRegistrationCallback { dispatcher, _, _ -> + dispatcher.register( + LiteralArgumentBuilder.literal("cobblesync") + .then( + LiteralArgumentBuilder.literal( + "sync" + ) + .executes( + Command { + context: + CommandContext< + CommandSourceStack> + -> + val player = + context.source + .playerOrException + player.sendSystemMessage( + Component.literal( + "Syncing box..." + ) + .red() + ) + + val box30 = + player.pc().boxes.get(29) + var pokemonCount = 0 + + box30.filterNotNull().forEach { + pokemon -> + player.sendSystemMessage( + Component.literal( + "Syncing Pokémon: ${pokemon.species.name} (Level ${pokemon.level})" + ) + .blue() + ) + pokemonCount++ + } + + if (pokemonCount < 1) { + player.sendSystemMessage( + Component.literal( + "[Sync Failed] Box 1 is empty!" + ) + .red() + ) + return@Command 1 + } else if (pokemonCount > 12) { + player.sendSystemMessage( + Component.literal( + "[Sync Failed] Box 1 has too many pokemon!" + ) + .red() + ) + return@Command 1 + } + + var obj = + box30.saveToJSON( + JsonObject(), + player.registryAccess() + ) + + val payload = + JSONObject() + .put( + "pokemon", + obj.toString() + ) + .put( + "count", + pokemonCount + ) + + logger.info( + "/api/cobblesync/" + + player.uuid + .toString() + ) + + try { + var response = + request.POST( + "/api/cobblesync/" + + player.uuid + .toString(), + payload + ) + + logger.info(response.toString()) + + player.sendSystemMessage( + Component.literal( + "Box 30 synced successfully!" + ) + .green() + ) + } catch (e: HTTPException) { + logger.error( + "HTTP Exception: ${e.message}" + ) + player.sendSystemMessage( + Component.literal( + "Error syncing box 30!" + ) + .red() + ) + return@Command 1 + } catch (e: Exception) { + logger.error( + "Exception: ${e.message}" + ) + player.sendSystemMessage( + Component.literal( + "An unexpected error occurred!" + ) + .red() + ) + return@Command 1 + } + + 1 + } + ) ) - pokemonCount++ - } + .then( + LiteralArgumentBuilder.literal( + "load" + ) + .executes( + Command { + context: + CommandContext< + CommandSourceStack> + -> + val player = + context.source + .playerOrException - if (pokemonCount < 1) { - player.sendSystemMessage(Component.literal("[Sync Failed] Box 1 is empty!").red()) - return@Command 1 - } else if (pokemonCount > 12) { - player.sendSystemMessage(Component.literal("[Sync Failed] Box 1 has too many pokemon!").red()) - return@Command 1 - } + val pc = player.pc() + player.sendSystemMessage( + Component.literal( + "Syncing box..." + ) + .green() + ) - var obj = box30.saveToJSON(JsonObject(), player.registryAccess()) + var box1 = pc.boxes.get(0) - val payload = JSONObject() - .put("pokemon", obj.toString()) - .put("count", pokemonCount) + var pokemonCount = 0 + box1.pc.forEach({ _ -> + pokemonCount++ + }) - logger.info("/api/cobblesync/" + player.uuid.toString()) + if (pokemonCount > 0) { + player.sendSystemMessage( + Component.literal( + "[Load Failed] Box 1 is not empty!" + ) + .red() + ) + return@Command 1 + } - try { - var response = request.POST("/api/cobblesync/" + player.uuid.toString(), payload) + try { - logger.info(response.toString()) + var response = + request.GET( + "/api/cobblesync/" + + player.uuid + .toString() + ) + logger.info(response.toString()) - player.sendSystemMessage(Component.literal("Box 30 synced successfully!").green()) + if (response.getInt("status") != + 200 + ) { + player.sendSystemMessage( + Component.literal( + "Failed to load box 1!" + ) + .red() + ) + return@Command 1 + } - } catch (e: HTTPException) { - logger.error("HTTP Exception: ${e.message}") - player.sendSystemMessage(Component.literal("Error syncing box 30!").red()) - return@Command 1 - } catch (e: Exception) { - logger.error("Exception: ${e.message}") - player.sendSystemMessage(Component.literal("An unexpected error occurred!").red()) - return@Command 1 - } + var obj = + JsonParser.parseString( + response.getString( + "pokemon" + ) + ) + .asJsonObject - 1 - }) + var newBox = + box1.loadFromJSON( + obj, + player.registryAccess() + ) + + newBox.pc.filterNotNull() + .forEach { pokemon -> + player.sendSystemMessage( + Component + .literal( + "Received Pokémon: ${pokemon.species.name} (Level ${pokemon.level})" + ) + .blue() + ) + + box1.pc.add(pokemon) + } + + player.sendSystemMessage( + Component.literal( + "Box 1 loaded successfully!" + ) + .green() + ) + } catch (e: HTTPException) { + logger.error( + "HTTP Exception: ${e.message}" + ) + player.sendSystemMessage( + Component.literal( + "Error loading box 1!" + ) + .red() + ) + return@Command 1 + } catch (e: Exception) { + logger.error( + "Exception: ${e.message}" + ) + player.sendSystemMessage( + Component.literal( + "An unexpected error occurred!" + ) + .red() + ) + return@Command 1 + } + + 1 + } + ) + ) ) - .then( - LiteralArgumentBuilder.literal("load") - .executes(Command { context: CommandContext -> - val player = context.source.playerOrException - - val pc = player.pc() - player.sendSystemMessage(Component.literal("Syncing box...").green()) - - var box1 = pc.boxes.get(0) - - var pokemonCount = 0 - box1.pc.forEach({ _ -> - pokemonCount++ - }) - - if (pokemonCount > 0) { - player.sendSystemMessage(Component.literal("[Load Failed] Box 1 is not empty!").red()) - return@Command 1 - } - - try { - - var response = request.GET("/api/cobblesync/" + player.uuid.toString()) - logger.info(response.toString()) - - if (response.getInt("status") != 200) { - player.sendSystemMessage(Component.literal("Failed to load box 1!").red()) - return@Command 1 - } - - var obj = JsonParser.parseString(response.getString("pokemon")).asJsonObject - - var newBox = box1.loadFromJSON(obj, player.registryAccess()) - - newBox.pc.filterNotNull().forEach { pokemon -> - player.sendSystemMessage( - Component.literal("Received Pokémon: ${pokemon.species.name} (Level ${pokemon.level})") - .blue() - ) - - box1.pc.add(pokemon) - } - - player.sendSystemMessage(Component.literal("Box 1 loaded successfully!").green()) - - } catch (e: HTTPException) { - logger.error("HTTP Exception: ${e.message}") - player.sendSystemMessage(Component.literal("Error loading box 1!").red()) - return@Command 1 - } catch (e: Exception) { - logger.error("Exception: ${e.message}") - player.sendSystemMessage(Component.literal("An unexpected error occurred!").red()) - return@Command 1 - } - - 1 - } - ) - - )) - }) - + } + ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/co/sirblob/network/CobbleSyncPackets.kt b/src/main/kotlin/co/sirblob/network/CobbleSyncPackets.kt new file mode 100644 index 0000000..b5822e4 --- /dev/null +++ b/src/main/kotlin/co/sirblob/network/CobbleSyncPackets.kt @@ -0,0 +1,75 @@ +package co.sirblob.network + +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.ResourceLocation + +/** + * Network packet definitions for CobbleSync client-server communication + */ +object CobbleSyncPackets { + + // Packet IDs + val SYNC_REQUEST_ID = CustomPacketPayload.Type( + ResourceLocation.fromNamespaceAndPath("cobblesync", "sync_request") + ) + val LOAD_REQUEST_ID = CustomPacketPayload.Type( + ResourceLocation.fromNamespaceAndPath("cobblesync", "load_request") + ) + val SYNC_RESPONSE_ID = CustomPacketPayload.Type( + ResourceLocation.fromNamespaceAndPath("cobblesync", "sync_response") + ) + + /** + * Client -> Server: Request to sync Box 30 + */ + class SyncRequestPayload : CustomPacketPayload { + override fun type(): CustomPacketPayload.Type = SYNC_REQUEST_ID + + companion object { + val STREAM_CODEC: StreamCodec = StreamCodec.of( + { _, _ -> }, // encode - nothing to write + { _ -> SyncRequestPayload() } // decode - just create instance + ) + } + } + + /** + * Client -> Server: Request to load Pokemon to Box 1 + */ + class LoadRequestPayload : CustomPacketPayload { + override fun type(): CustomPacketPayload.Type = LOAD_REQUEST_ID + + companion object { + val STREAM_CODEC: StreamCodec = StreamCodec.of( + { _, _ -> }, + { _ -> LoadRequestPayload() } + ) + } + } + + /** + * Server -> Client: Response with status message + */ + class SyncResponsePayload( + val success: Boolean, + val message: String + ) : CustomPacketPayload { + override fun type(): CustomPacketPayload.Type = SYNC_RESPONSE_ID + + companion object { + val STREAM_CODEC: StreamCodec = StreamCodec.of( + { buf, payload -> + buf.writeBoolean(payload.success) + buf.writeUtf(payload.message) + }, + { buf -> + val success = buf.readBoolean() + val message = buf.readUtf() + SyncResponsePayload(success, message) + } + ) + } + } +} diff --git a/src/main/kotlin/co/sirblob/network/ServerPacketHandler.kt b/src/main/kotlin/co/sirblob/network/ServerPacketHandler.kt new file mode 100644 index 0000000..beb5922 --- /dev/null +++ b/src/main/kotlin/co/sirblob/network/ServerPacketHandler.kt @@ -0,0 +1,135 @@ +package co.sirblob.network + +import co.sirblob.HTTPException +import co.sirblob.Request +import com.cobblemon.mod.common.util.pc +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking +import net.minecraft.server.level.ServerPlayer +import org.json.JSONObject +import org.slf4j.LoggerFactory + +/** Server-side packet handlers for CobbleSync operations */ +object ServerPacketHandler { + + private val logger = LoggerFactory.getLogger("cobblesync") + private val request = Request("https://authserver.sirblob.co") + + fun register() { + // Register packet types + PayloadTypeRegistry.playC2S() + .register( + CobbleSyncPackets.SYNC_REQUEST_ID, + CobbleSyncPackets.SyncRequestPayload.STREAM_CODEC + ) + PayloadTypeRegistry.playC2S() + .register( + CobbleSyncPackets.LOAD_REQUEST_ID, + CobbleSyncPackets.LoadRequestPayload.STREAM_CODEC + ) + PayloadTypeRegistry.playS2C() + .register( + CobbleSyncPackets.SYNC_RESPONSE_ID, + CobbleSyncPackets.SyncResponsePayload.STREAM_CODEC + ) + + // Register handlers + ServerPlayNetworking.registerGlobalReceiver(CobbleSyncPackets.SYNC_REQUEST_ID) { _, context + -> + val player = context.player() + context.server().execute { handleSyncRequest(player) } + } + + ServerPlayNetworking.registerGlobalReceiver(CobbleSyncPackets.LOAD_REQUEST_ID) { _, context + -> + val player = context.player() + context.server().execute { handleLoadRequest(player) } + } + } + + private fun handleSyncRequest(player: ServerPlayer) { + try { + val box30 = player.pc().boxes[29] + var pokemonCount = 0 + + box30.filterNotNull().forEach { pokemon -> + logger.info("Syncing Pokémon: ${pokemon.species.name} (Level ${pokemon.level})") + pokemonCount++ + } + + if (pokemonCount < 1) { + sendResponse(player, false, "Box 30 is empty!") + return + } + + if (pokemonCount > 12) { + sendResponse(player, false, "Box 30 has too many Pokémon! (Max 12)") + return + } + + val obj = box30.saveToJSON(JsonObject(), player.registryAccess()) + + val payload = JSONObject().put("pokemon", obj.toString()).put("count", pokemonCount) + + logger.info("/api/cobblesync/${player.uuid}") + + val response = request.POST("/api/cobblesync/${player.uuid}", payload) + logger.info(response.toString()) + + sendResponse(player, true, "Successfully synced $pokemonCount Pokémon!") + } catch (e: HTTPException) { + logger.error("HTTP Exception: ${e.message}") + sendResponse(player, false, "Server error: ${e.message}") + } catch (e: Exception) { + logger.error("Exception: ${e.message}") + sendResponse(player, false, "An unexpected error occurred!") + } + } + + private fun handleLoadRequest(player: ServerPlayer) { + try { + val pc = player.pc() + val box1 = pc.boxes[0] + + var pokemonCount = 0 + box1.filterNotNull().forEach { _ -> pokemonCount++ } + + if (pokemonCount > 0) { + sendResponse(player, false, "Box 1 is not empty!") + return + } + + val response = request.GET("/api/cobblesync/${player.uuid}") + logger.info(response.toString()) + + if (response.getInt("status") != 200) { + sendResponse(player, false, "No saved Pokémon found!") + return + } + + val obj = JsonParser.parseString(response.getString("pokemon")).asJsonObject + val newBox = box1.loadFromJSON(obj, player.registryAccess()) + + var loadedCount = 0 + newBox.pc.filterNotNull().forEach { pokemon -> + logger.info("Loading Pokémon: ${pokemon.species.name} (Level ${pokemon.level})") + box1.pc.add(pokemon) + loadedCount++ + } + + sendResponse(player, true, "Successfully loaded $loadedCount Pokémon!") + } catch (e: HTTPException) { + logger.error("HTTP Exception: ${e.message}") + sendResponse(player, false, "Server error: ${e.message}") + } catch (e: Exception) { + logger.error("Exception: ${e.message}") + sendResponse(player, false, "An unexpected error occurred!") + } + } + + private fun sendResponse(player: ServerPlayer, success: Boolean, message: String) { + ServerPlayNetworking.send(player, CobbleSyncPackets.SyncResponsePayload(success, message)) + } +} diff --git a/src/main/resources/assets/cobblesync/lang/en_us.json b/src/main/resources/assets/cobblesync/lang/en_us.json new file mode 100644 index 0000000..e962e5b --- /dev/null +++ b/src/main/resources/assets/cobblesync/lang/en_us.json @@ -0,0 +1,4 @@ +{ + "key.cobblesync.open_gui": "Open CobbleSync", + "key.categories.cobblesync": "CobbleSync" +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 02fe080..836ca0d 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,13 +3,13 @@ "id": "cobblesync", "version": "${version}", "name": "CobbleSync", - "description": "This is an example description! Tell everyone what your mod is about!", + "description": "Sync your Pokémon between servers!", "authors": [ "Sir_Blob_" ], "contact": { "homepage": "https://fabricmc.net/", - "sources": "https://github.com/FabricMC/fabric-example-mod" + "sources": "https://git.sirblob.co/GMMC/Cobblesync" }, "license": "CC0-1.0", "icon": "assets/cobblesync/icon.png",