diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bd8a38 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# CobbleSync + +

+ CobbleSync Logo +

+ +

+ Sync your Pokémon between servers! +

+ +

+ + Minecraft Version + + + Fabric + + + License + +

+ +--- + +## 📖 Description + +CobbleSync is a Fabric mod for [Cobblemon](https://cobblemon.com/) that allows players to sync their Pokémon across different servers. Upload your Pokémon from one server and download them on another! + +## ✨ Features + +- **GUI Interface** - Easy-to-use graphical interface accessible with a keybind +- **Command Support** - Full command-line control for advanced users +- **Cross-Server Sync** - Transfer Pokémon between different Cobblemon servers +- **Safe Transfers** - Loads Pokémon to empty slots without overwriting existing ones + +## 🎮 How to Use + +### GUI Method (Recommended) + +1. Press **U** (default keybind) to open the CobbleSync GUI +2. Click **"⬆ Sync Box 30"** to upload Pokémon from Box 30 +3. On another server, click **"⬇ Load Pokémon"** to download your Pokémon to empty slots + +### Command Method + +| Command | Description | +|---------|-------------| +| `/cobblesync sync` | Upload Pokémon from Box 30 to the cloud | +| `/cobblesync load` | Download Pokémon to empty PC slots | + +## ⚠️ Important Notes + +- **Sync (Upload)**: Uses **Box 30** - Maximum of 12 Pokémon +- **Load (Download)**: Adds Pokémon to **empty slots** across all PC boxes (won't overwrite existing Pokémon) +- The keybind can be changed in Minecraft's Controls settings under "CobbleSync" + +## 📦 Installation + +### Requirements + +- Minecraft **1.21.1** +- [Fabric Loader](https://fabricmc.net/) **0.16.14+** +- [Fabric API](https://modrinth.com/mod/fabric-api) +- [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin) +- [Cobblemon](https://cobblemon.com/) **1.7.2+** + +### Steps + +1. Download the latest release from the [Releases](https://git.sirblob.co/GMMC/Cobblesync/releases) page +2. Place the `.jar` file in your `mods` folder +3. Make sure all dependencies are installed +4. Launch Minecraft with Fabric + +## 🛠️ Building from Source + +```bash +# Clone the repository +git clone https://git.sirblob.co/GMMC/Cobblesync.git +cd Cobblesync + +# Build the mod +./gradlew build + +# The compiled JAR will be in build/libs/ +``` + +## 📁 Project Structure + +``` +src/ +├── client/kotlin/co/sirblob/ +│ ├── CobbleSyncClient.kt # Client entry point +│ ├── gui/ +│ │ └── CobbleSyncScreen.kt # GUI implementation +│ └── network/ +│ └── ClientPacketHandler.kt +├── main/kotlin/co/sirblob/ +│ ├── CobbleSync.kt # Main mod entry point +│ ├── Request.kt # HTTP client +│ ├── HTTPException.kt +│ └── network/ +│ ├── CobbleSyncPackets.kt # Network packet definitions +│ └── ServerPacketHandler.kt +└── main/resources/ + ├── fabric.mod.json + └── assets/cobblesync/ + ├── icon.png + └── lang/en_us.json +``` + +## 🔧 Configuration + +The keybind for opening the GUI can be configured in: +**Options → Controls → Key Binds → CobbleSync → Open CobbleSync** + +Default: `U` + +## 📝 License + +This project is licensed under the [CC0-1.0 License](LICENSE). + +## 👤 Author + +**Sir_Blob_** + +--- + +

+ Made with ❤️ for the GMMC +

diff --git a/src/client/kotlin/co/sirblob/gui/CobbleSyncScreen.kt b/src/client/kotlin/co/sirblob/gui/CobbleSyncScreen.kt index 16a242e..11143b6 100644 --- a/src/client/kotlin/co/sirblob/gui/CobbleSyncScreen.kt +++ b/src/client/kotlin/co/sirblob/gui/CobbleSyncScreen.kt @@ -30,27 +30,28 @@ class CobbleSyncScreen : Screen(Component.literal("CobbleSync")) { 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() + 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() + // Load Button - downloads saved Pokemon to empty slots + loadButton = + Button.builder(Component.literal("⬇ Load Pokémon")) { _ -> + 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() + closeButton = + Button.builder(Component.literal("Close")) { _ -> onClose() } + .bounds(centerX - buttonWidth / 2, centerY + 30, buttonWidth, buttonHeight) + .build() addRenderableWidget(syncButton) addRenderableWidget(loadButton) @@ -63,56 +64,56 @@ class CobbleSyncScreen : Screen(Component.literal("CobbleSync")) { // Draw title guiGraphics.drawCenteredString( - font, - Component.literal("§6§lCobbleSync"), - width / 2, - height / 2 - 80, - 0xFFFFFF + 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 + 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() + 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 + 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 + 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 + font, + Component.literal("§8Load: Download to empty PC slots"), + width / 2, + height / 2 + 92, + 0x888888 ) super.render(guiGraphics, mouseX, mouseY, partialTick) diff --git a/src/main/kotlin/co/sirblob/CobbleSync.kt b/src/main/kotlin/co/sirblob/CobbleSync.kt index 332e263..587b748 100644 --- a/src/main/kotlin/co/sirblob/CobbleSync.kt +++ b/src/main/kotlin/co/sirblob/CobbleSync.kt @@ -171,30 +171,12 @@ object CobbleSync : ModInitializer { val pc = player.pc() player.sendSystemMessage( Component.literal( - "Syncing box..." + "Loading Pokémon..." ) .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/" + @@ -208,7 +190,7 @@ object CobbleSync : ModInitializer { ) { player.sendSystemMessage( Component.literal( - "Failed to load box 1!" + "No saved Pokémon found!" ) .red() ) @@ -223,28 +205,94 @@ object CobbleSync : ModInitializer { ) .asJsonObject - var newBox = - box1.loadFromJSON( - obj, - player.registryAccess() - ) + // Use a + // temporary + // box to + // parse the + // JSON + val tempBox = + pc.boxes[0] + .loadFromJSON( + obj, + player.registryAccess() + ) + val pokemonToLoad = + tempBox.pc + .filterNotNull() + .toMutableList() - newBox.pc.filterNotNull() - .forEach { pokemon -> - player.sendSystemMessage( - Component - .literal( - "Received Pokémon: ${pokemon.species.name} (Level ${pokemon.level})" - ) - .blue() - ) + if (pokemonToLoad.isEmpty()) { + player.sendSystemMessage( + Component.literal( + "No Pokémon to load!" + ) + .red() + ) + return@Command 1 + } - box1.pc.add(pokemon) + // Count + // available + // empty + // slots + var emptySlots = 0 + for (box in pc.boxes) { + for (slot in 0 until 30) { + if (box[slot] == null) { + emptySlots++ } + } + } + + if (emptySlots < + pokemonToLoad + .size + ) { + player.sendSystemMessage( + Component.literal( + "Not enough empty slots! Need ${pokemonToLoad.size}, have $emptySlots" + ) + .red() + ) + return@Command 1 + } + + // Add + // Pokemon + // to empty + // slots + // across + // all boxes + var loadedCount = 0 + for (pokemon in pokemonToLoad) { + var placed = false + for (box in pc.boxes) { + if (placed) break + for (slot in + 0 until 30) { + if (box[slot] == + null + ) { + box[slot] = + pokemon + player.sendSystemMessage( + Component + .literal( + "Received Pokémon: ${pokemon.species.name} (Level ${pokemon.level})" + ) + .blue() + ) + loadedCount++ + placed = true + break + } + } + } + } player.sendSystemMessage( Component.literal( - "Box 1 loaded successfully!" + "Successfully loaded $loadedCount Pokémon to empty slots!" ) .green() ) @@ -254,7 +302,7 @@ object CobbleSync : ModInitializer { ) player.sendSystemMessage( Component.literal( - "Error loading box 1!" + "Error loading Pokémon!" ) .red() ) diff --git a/src/main/kotlin/co/sirblob/network/ServerPacketHandler.kt b/src/main/kotlin/co/sirblob/network/ServerPacketHandler.kt index beb5922..719ac95 100644 --- a/src/main/kotlin/co/sirblob/network/ServerPacketHandler.kt +++ b/src/main/kotlin/co/sirblob/network/ServerPacketHandler.kt @@ -91,16 +91,8 @@ object ServerPacketHandler { 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 - } + // Fetch synced Pokemon from the server val response = request.GET("/api/cobblesync/${player.uuid}") logger.info(response.toString()) @@ -110,16 +102,56 @@ object ServerPacketHandler { } 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++ + // Use a temporary box to parse the JSON data + val tempBox = pc.boxes[0].loadFromJSON(obj, player.registryAccess()) + val pokemonToLoad = tempBox.pc.filterNotNull().toMutableList() + + if (pokemonToLoad.isEmpty()) { + sendResponse(player, false, "No Pokémon to load!") + return } - sendResponse(player, true, "Successfully loaded $loadedCount Pokémon!") + // Count available empty slots across all boxes + var emptySlots = 0 + for (box in pc.boxes) { + for (slot in 0 until 30) { // Each box has 30 slots + if (box[slot] == null) { + emptySlots++ + } + } + } + + if (emptySlots < pokemonToLoad.size) { + sendResponse( + player, + false, + "Not enough empty slots! Need ${pokemonToLoad.size}, have $emptySlots" + ) + return + } + + // Add Pokemon to empty slots across all boxes + var loadedCount = 0 + for (pokemon in pokemonToLoad) { + var placed = false + for (box in pc.boxes) { + if (placed) break + for (slot in 0 until 30) { + if (box[slot] == null) { + box[slot] = pokemon + logger.info( + "Loaded Pokémon: ${pokemon.species.name} (Level ${pokemon.level}) to slot $slot" + ) + loadedCount++ + placed = true + break + } + } + } + } + + sendResponse(player, true, "Successfully loaded $loadedCount Pokémon to empty slots!") } catch (e: HTTPException) { logger.error("HTTP Exception: ${e.message}") sendResponse(player, false, "Server error: ${e.message}")