From 614352f993914cf828455b4d4cb8189beb335873 Mon Sep 17 00:00:00 2001 From: Ian Tapply Date: Sun, 23 Jun 2024 21:26:39 -0400 Subject: [PATCH] Single Balloon MEG Implementation (#44) --- pom.xml | 17 ++++ .../net/jeqo/bloons/balloon/BalloonCore.java | 17 +++- .../bloons/balloon/single/SingleBalloon.java | 81 +++++++++++++++++-- .../balloon/single/SingleBalloonType.java | 34 +++++++- .../bloons/commands/manager/CommandCore.java | 4 +- .../configuration/ConfigConfiguration.java | 47 +++++++---- .../resources/balloons/dyeable_example.yml | 12 +++ .../resources/balloons/multipart_example.yml | 2 +- 8 files changed, 183 insertions(+), 31 deletions(-) diff --git a/pom.xml b/pom.xml index 8ca02d2b..fb26de32 100644 --- a/pom.xml +++ b/pom.xml @@ -84,9 +84,15 @@ sonatype https://oss.sonatype.org/content/groups/public/ + + nexus + Lumine Public + https://mvn.lumine.io/repository/maven-public/ + + kiputyttö-releases Ilari's Project Repository @@ -95,6 +101,7 @@ + io.papermc.paper paper-api @@ -106,6 +113,8 @@ adventure-text-minimessage 4.17.0 + + org.projectlombok lombok @@ -123,5 +132,13 @@ maven-model 3.9.7 + + + + com.ticxo.modelengine + ModelEngine + R4.0.4 + provided + diff --git a/src/main/java/net/jeqo/bloons/balloon/BalloonCore.java b/src/main/java/net/jeqo/bloons/balloon/BalloonCore.java index 770c6536..b4ee9f11 100644 --- a/src/main/java/net/jeqo/bloons/balloon/BalloonCore.java +++ b/src/main/java/net/jeqo/bloons/balloon/BalloonCore.java @@ -8,6 +8,7 @@ import net.jeqo.bloons.configuration.ConfigConfiguration; import org.bukkit.plugin.java.JavaPlugin; +import java.io.File; import java.util.ArrayList; /** @@ -65,10 +66,20 @@ public void initialize() { * Copies the example balloons folder to the plugin's data folder if it doesn't exist */ public void copyExampleBalloons() { + // List of example balloon files + String[] exampleBalloons = new String[] { + "/color_pack_example.yml", + "/dyeable_example.yml", + "/multipart_example.yml" + }; + // Save all example files in the balloons folder in /resources - Bloons.getInstance().saveResource(ConfigConfiguration.BALLOON_CONFIGURATION_FOLDER + "/color_pack_example.yml", false); - Bloons.getInstance().saveResource(ConfigConfiguration.BALLOON_CONFIGURATION_FOLDER + "/dyeable_example.yml", false); - Bloons.getInstance().saveResource(ConfigConfiguration.BALLOON_CONFIGURATION_FOLDER + "/multipart_example.yml", false); + for (String example : exampleBalloons) { + File file = new File(Bloons.getInstance().getDataFolder() + File.separator + ConfigConfiguration.BALLOON_CONFIGURATION_FOLDER + example); + if (file.exists()) continue; + + Bloons.getInstance().saveResource(ConfigConfiguration.BALLOON_CONFIGURATION_FOLDER + example, false); + } } /** diff --git a/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloon.java b/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloon.java index aa53cbf3..4ea39abd 100644 --- a/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloon.java +++ b/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloon.java @@ -1,5 +1,9 @@ package net.jeqo.bloons.balloon.single; +import com.ticxo.modelengine.api.ModelEngineAPI; +import com.ticxo.modelengine.api.animation.handler.AnimationHandler; +import com.ticxo.modelengine.api.model.ActiveModel; +import com.ticxo.modelengine.api.model.ModeledEntity; import lombok.Getter; import lombok.Setter; import net.jeqo.bloons.Bloons; @@ -29,11 +33,18 @@ @Getter @Setter public class SingleBalloon extends BukkitRunnable { + private SingleBalloonType balloonType; private Player player; private ItemStack balloonVisual; private ArmorStand balloonArmorStand; public Chicken balloonChicken; + /** MEG related variables **/ + private ModeledEntity modeledEntity; + private ActiveModel activeModel; + private AnimationHandler animationHandler; + private final String defaultIdleAnimationID = "idle"; + private Location playerLocation; private Location moveLocation; @@ -49,9 +60,11 @@ public class SingleBalloon extends BukkitRunnable { */ public SingleBalloon(Player player, String balloonID) { this.setPlayer(player); + this.setBalloonType(Bloons.getBalloonCore().getSingleBalloonByID(balloonID)); - // Configure the balloon visual elements - this.setBalloonVisual(this.getConfiguredBalloonVisual(balloonID)); + if (this.getBalloonType().getMegModelID() == null) { + this.setBalloonVisual(getConfiguredBalloonVisual(balloonID)); + } } /** @@ -88,7 +101,24 @@ public void run() { this.setMoveLocation(this.getMoveLocation().add(vector)); double vectorZ = vector.getZ() * 50.0D * -1.0D; double vectorX = vector.getX() * 50.0D * -1.0D; - this.getBalloonArmorStand().setHeadPose(new EulerAngle(Math.toRadians(vectorZ), Math.toRadians(playerLocation.getYaw()), Math.toRadians(vectorX))); + // Create EulerAngle to tilt parts of the armor body + EulerAngle tiltAngle = new EulerAngle(Math.toRadians(vectorZ), Math.toRadians(playerLocation.getYaw()), Math.toRadians(vectorX)); + + // Set the pose(s) of the armor stand + ArmorStand armorStand = this.getBalloonArmorStand(); + + // Set the pose of only the head regardless of the model type + armorStand.setHeadPose(tiltAngle); + + // Only set the entire pose of the armor stand if it uses MEG, this is to reduce lag across the server + // when having 100's of models/armor stands used simultaneously + if (this.getBalloonType().getMegModelID() != null) { + armorStand.setBodyPose(tiltAngle); + armorStand.setLeftArmPose(tiltAngle); + armorStand.setRightArmPose(tiltAngle); + armorStand.setLeftLegPose(tiltAngle); + armorStand.setRightLegPose(tiltAngle); + } // Teleport the balloon to the move location and set the player location yaw this.teleport(this.getMoveLocation()); @@ -98,6 +128,15 @@ public void run() { this.teleport(playerLocation); } + // If all parts of a MEG balloon exist and the idle animation exists and it isn't playing, play it + if (this.getAnimationHandler() != null) { + if (this.getAnimationHandler().getAnimation(this.getDefaultIdleAnimationID()) != null) { + if (!this.getAnimationHandler().isPlayingAnimation(this.getDefaultIdleAnimationID())) { + this.getAnimationHandler().playAnimation(this.getDefaultIdleAnimationID(), 0.3, 0.3, 1,true); + } + } + } + this.setPlayerLocation(this.getPlayer().getLocation()); this.getPlayerLocation().setYaw(playerLocation.getYaw()); this.setTicks(this.getTicks() + 1); @@ -108,6 +147,11 @@ public void run() { * @throws IllegalStateException If the task has already been cancelled */ public synchronized void cancel() throws IllegalStateException { + if (this.getModeledEntity() != null) { + // Remove the MEG model if it exists + ModeledEntity modeledEntity = ModelEngineAPI.getModeledEntity(this.getBalloonArmorStand()); + modeledEntity.removeModel(this.getBalloonType().getMegModelID()); + } this.getBalloonArmorStand().remove(); this.getBalloonChicken().remove(); super.cancel(); @@ -137,10 +181,12 @@ private void initializeBalloon() { this.setPlayerLocation(this.getPlayer().getLocation()); this.getPlayerLocation().setYaw(0.0F); - // Create and set the balloons visual appearance/model - ItemMeta meta = this.getBalloonVisual().getItemMeta(); - meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); - this.getBalloonVisual().setItemMeta(meta); + if (this.getBalloonType().getMegModelID() == null) { + // Create and set the balloons visual appearance/model + ItemMeta meta = this.getBalloonVisual().getItemMeta(); + meta.addItemFlags(ItemFlag.HIDE_UNBREAKABLE); + this.getBalloonVisual().setItemMeta(meta); + } // Initialize the armor stand and lead to the player this.initializeBalloonArmorStand(); @@ -211,7 +257,26 @@ public void initializeBalloonArmorStand() { this.getBalloonArmorStand().setSmall(false); this.getBalloonArmorStand().setMarker(true); this.getBalloonArmorStand().setCollidable(false); - this.getBalloonArmorStand().getEquipment().setHelmet(this.getBalloonVisual()); + if (this.getBalloonType().getMegModelID() == null) { + this.getBalloonArmorStand().getEquipment().setHelmet(this.getBalloonVisual()); + } else { + try { + // Create the entity and tag it onto the armor stand + ModeledEntity modeledEntity = ModelEngineAPI.createModeledEntity(this.getBalloonArmorStand()); + ActiveModel activeModel = ModelEngineAPI.createActiveModel(this.getBalloonType().getMegModelID()); + + modeledEntity.addModel(activeModel, true); + + // Set the animation handler to the one of the active model + this.setAnimationHandler(activeModel.getAnimationHandler()); + + // If an idle animation exists, play it initially + this.getAnimationHandler().playAnimation(this.getDefaultIdleAnimationID(), 0.3, 0.3, 1,true); + } catch (Exception e) { + Logger.logError("An error occurred while creating the MEG model for the balloon " + this.getBalloonType().getId() + "! This is because a MEG model error occurred."); + e.printStackTrace(); + } + } this.getBalloonArmorStand().customName(Component.text(BalloonConfiguration.BALLOON_ARMOR_STAND_ID)); } diff --git a/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloonType.java b/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloonType.java index 39ea953c..877e00b8 100644 --- a/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloonType.java +++ b/src/main/java/net/jeqo/bloons/balloon/single/SingleBalloonType.java @@ -11,14 +11,19 @@ public class SingleBalloonType { private String key; private String id; private String permission; - private String material; - private String color; - private int customModelData; private String name; + private String[] lore; + private String material; + // Not used by MEG balloons + private String color = "#ffffff"; + private int customModelData; + + /** MEG only options **/ + private String megModelID; /** - * Creates a new single balloon type configuration + * Creates a new single balloon type configuration for a non-MEG balloon * @param key The key of the balloon type in the configuration, type java.lang.String * @param id The unique identifier for the balloon type, type java.lang.String * @param permission The permission required to use the balloon type, type java.lang.String @@ -38,4 +43,25 @@ public SingleBalloonType(String key, String id, String permission, String materi this.setName(name); this.setLore(lore); } + + /** + * Creates a new single balloon type configuration for a MEG balloon + * @param key The key of the balloon type in the configuration, type java.lang.String + * @param id The unique identifier for the balloon type, type java.lang.String + * @param permission The permission required to use the balloon type, type java.lang.String + * @param material The name of the Bukkit Material used to create the item, type java.lang.String + * @param customModelData The custom model data value stored in the item metadata, type int + * @param name The name of the balloon, type java.lang.String + * @param lore The lore of the balloon, type java.lang.String[] + */ + public SingleBalloonType(String key, String id, String permission, String material, int customModelData, String megModelID, String name, String[] lore) { + this.setKey(key); + this.setId(id); + this.setPermission(permission); + this.setMegModelID(megModelID); + this.setMaterial(material); + this.setCustomModelData(customModelData); + this.setName(name); + this.setLore(lore); + } } diff --git a/src/main/java/net/jeqo/bloons/commands/manager/CommandCore.java b/src/main/java/net/jeqo/bloons/commands/manager/CommandCore.java index de28d21f..61473382 100644 --- a/src/main/java/net/jeqo/bloons/commands/manager/CommandCore.java +++ b/src/main/java/net/jeqo/bloons/commands/manager/CommandCore.java @@ -339,7 +339,9 @@ private void setBalloonColor(ItemMeta meta, SingleBalloonType singleBalloonType) if (meta instanceof LeatherArmorMeta) { ((LeatherArmorMeta) meta).setColor(Color.hexToColor(color)); } else { - Logger.logWarning(String.format(Languages.getMessage("material-not-dyeable"), singleBalloonType.getMaterial())); + if (singleBalloonType.getMegModelID() == null) { + Logger.logWarning(String.format(Languages.getMessage("material-not-dyeable"), singleBalloonType.getMaterial())); + } } } } diff --git a/src/main/java/net/jeqo/bloons/configuration/ConfigConfiguration.java b/src/main/java/net/jeqo/bloons/configuration/ConfigConfiguration.java index 9377dec8..43f452cf 100644 --- a/src/main/java/net/jeqo/bloons/configuration/ConfigConfiguration.java +++ b/src/main/java/net/jeqo/bloons/configuration/ConfigConfiguration.java @@ -96,20 +96,39 @@ public static ArrayList getSingleBalloons() { if (!type.equals(BalloonConfiguration.SINGLE_BALLOON_TYPE_IDENTIFIER)) continue; - try { - // Add the single balloon type to the array list - singleBalloons.add(new SingleBalloonType( - key, - config.getString(key + ".id"), - config.getString(key + ".permission"), - config.getString(key + ".material"), - config.getString(key + ".color"), - config.getInt(key + ".custom-model-data"), - config.getString(key + ".name"), - config.getStringList(key + ".lore").toArray(new String[0]) - )); - } catch (Exception e) { - Logger.logWarning("Error processing multipart balloon type for section: " + key + " in file: " + fileName + " - " + e.getMessage()); + if (config.getString(key + ".meg-model-id") != null) { + // Process the MEG balloon type + try { + singleBalloons.add(new SingleBalloonType( + key, + config.getString(key + ".id"), + config.getString(key + ".permission"), + config.getString(key + ".icon.material"), + config.getInt(key + ".icon.custom-model-data"), + config.getString(key + ".meg-model-id"), + config.getString(key + ".icon.name"), + config.getStringList(key + ".icon.lore").toArray(new String[0]) + )); + } catch (Exception e) { + Logger.logWarning("Error processing MEG balloon type for section: " + key + " in file: " + fileName + " - " + e.getMessage()); + } + } else { + // Process the non-MEG balloon type + try { + // Add the single balloon type to the array list + singleBalloons.add(new SingleBalloonType( + key, + config.getString(key + ".id"), + config.getString(key + ".permission"), + config.getString(key + ".material"), + config.getString(key + ".color"), + config.getInt(key + ".custom-model-data"), + config.getString(key + ".name"), + config.getStringList(key + ".lore").toArray(new String[0]) + )); + } catch (Exception e) { + Logger.logWarning("Error processing multipart balloon type for section: " + key + " in file: " + fileName + " - " + e.getMessage()); + } } } } diff --git a/src/main/resources/balloons/dyeable_example.yml b/src/main/resources/balloons/dyeable_example.yml index 078519d8..345f45ca 100644 --- a/src/main/resources/balloons/dyeable_example.yml +++ b/src/main/resources/balloons/dyeable_example.yml @@ -13,3 +13,15 @@ dyeable_example: - '&8Bloons Example Balloon' - '' - '&eᴄʟɪᴄᴋ ᴛᴏ ᴇǫᴜɪᴘ' +single_meg_example: + id: single_meg_example + permission: balloon.single_meg_example + meg-model-id: single_meg_example + icon: + material: FLINT + custom-model-data: 7 + name: 'Single MEG Balloon' + lore: + - '&8Bloons Default Balloon' + - '' + - '&eᴄʟɪᴄᴋ ᴛᴏ ᴇǫᴜɪᴘ' diff --git a/src/main/resources/balloons/multipart_example.yml b/src/main/resources/balloons/multipart_example.yml index 7f919acb..955f5bb1 100644 --- a/src/main/resources/balloons/multipart_example.yml +++ b/src/main/resources/balloons/multipart_example.yml @@ -32,4 +32,4 @@ multipart_example: tail: # Tail must be supplied for a balloon of this kind material: LEATHER_HORSE_ARMOR # The material of the tail color: '#ffffff' # The color of the tail, this is optional - custom-model-data: 10282 # The custom model data of the tail, this is not required but is recommended for use of models \ No newline at end of file + custom-model-data: 10282 # The custom model data of the tail, this is not required but is recommended for use of models