├── ReadMe.md ├── pom.xml └── src └── main ├── java └── com │ └── magmaguy │ └── freeminecraftmodels │ ├── FreeMinecraftModels.java │ ├── MetadataHandler.java │ ├── animation │ ├── Animation.java │ ├── AnimationManager.java │ ├── AnimationStateConfig.java │ ├── AnimationStateType.java │ ├── Animations.java │ ├── AttackState.java │ ├── CustomAnimationState.java │ ├── DeathState.java │ ├── IAnimState.java │ ├── IdleState.java │ ├── SpawnState.java │ └── WalkState.java │ ├── api │ ├── DynamicEntityHitboxContactEvent.java │ ├── DynamicEntityLeftClickEvent.java │ ├── DynamicEntityRightClickEvent.java │ ├── ModeledEntityHitboxContactEvent.java │ ├── ModeledEntityLeftClickEvent.java │ ├── ModeledEntityManager.java │ ├── ModeledEntityRightClickEvent.java │ ├── PropEntityHitboxContactEvent.java │ ├── PropEntityLeftClickEvent.java │ ├── PropEntityRightClickEvent.java │ ├── StaticEntityHitboxContactEvent.java │ ├── StaticEntityLeftClickEvent.java │ └── StaticEntityRightClickEvent.java │ ├── commands │ ├── DeleteAllCommand.java │ ├── HitboxDebugCommand.java │ ├── MountCommand.java │ ├── ReloadCommand.java │ ├── SpawnCommand.java │ └── VersionCommand.java │ ├── config │ ├── DefaultConfig.java │ ├── ModelsFolder.java │ ├── OutputFolder.java │ └── props │ │ ├── PropsConfig.java │ │ └── PropsConfigFields.java │ ├── customentity │ ├── DynamicEntity.java │ ├── ModeledEntitiesClock.java │ ├── ModeledEntity.java │ ├── ModeledEntityEvents.java │ ├── PropEntity.java │ ├── StaticEntity.java │ └── core │ │ ├── Bone.java │ │ ├── BoneTransforms.java │ │ ├── ModeledEntityInterface.java │ │ ├── OBBHitDetection.java │ │ ├── OrientedBoundingBox.java │ │ ├── RegisterModelEntity.java │ │ ├── Skeleton.java │ │ └── SkeletonWatchers.java │ ├── dataconverter │ ├── AnimationBlueprint.java │ ├── AnimationFrame.java │ ├── AnimationsBlueprint.java │ ├── BoneBlueprint.java │ ├── CubeBlueprint.java │ ├── FileModelConverter.java │ ├── HitboxBlueprint.java │ ├── ImportsProcessor.java │ ├── Keyframe.java │ └── SkeletonBlueprint.java │ ├── entities │ └── ModelArmorStand.java │ ├── events │ └── ResourcePackGenerationEvent.java │ ├── listeners │ └── EntityTeleportEvent.java │ ├── packets │ ├── PacketArmorStand.java │ └── PushArmorStandState.java │ ├── thirdparty │ ├── BedrockChecker.java │ ├── Floodgate.java │ └── Geyser.java │ └── utils │ ├── ChunkHasher.java │ ├── ConfigurationLocation.java │ ├── CoordinateSystemConverter.java │ ├── InterpolationType.java │ ├── LoopType.java │ ├── StringToResourcePackFilename.java │ ├── TransformationMatrix.java │ └── TransformationType.java └── resources ├── blocks.json ├── pack.mcmeta ├── pack.png └── plugin.yml /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | com.magmaguy 7 | FreeMinecraftModels 8 | 2.1.1-SNAPSHOT-1 9 | 10 | 11 | 17 12 | 17 13 | UTF-8 14 | 15 | 16 | 17 | 18 | 19 | spigot-repo 20 | https://hub.spigotmc.org/nexus/content/repositories/snapshots/ 21 | 22 | 23 | 24 | opencollab-snapshot 25 | https://repo.opencollab.dev/main/ 26 | 27 | 28 | magmaguy-repo-releases 29 | MagmaGuy's Repository 30 | https://repo.magmaguy.com/releases 31 | 32 | 33 | magmaguy-repo-snapshots 34 | MagmaGuy's Snapshot Repository 35 | https://repo.magmaguy.com/snapshots 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.spigotmc 43 | spigot-api 44 | 1.21.4-R0.1-SNAPSHOT 45 | provided 46 | 47 | 48 | 49 | 50 | com.google.code.gson 51 | gson 52 | 2.9.0 53 | 54 | 55 | 56 | 57 | org.projectlombok 58 | lombok 59 | 1.18.24 60 | provided 61 | 62 | 63 | 64 | 65 | org.geysermc.floodgate 66 | api 67 | 2.2.0-SNAPSHOT 68 | provided 69 | 70 | 71 | 72 | org.geysermc.geyser 73 | api 74 | 2.7.0-SNAPSHOT 75 | provided 76 | 77 | 78 | 79 | 80 | commons-io 81 | commons-io 82 | 2.13.0 83 | 84 | 85 | 86 | 87 | org.bstats 88 | bstats-bukkit 89 | 3.0.2 90 | compile 91 | 92 | 93 | 94 | com.magmaguy 95 | EasyMinecraftGoals-dist 96 | 1.19.5-SNAPSHOT 97 | 98 | 99 | 100 | com.magmaguy 101 | EliteMobs 102 | 9.3.0-SNAPSHOT 103 | provided 104 | 105 | 106 | 107 | com.magmaguy 108 | MagmaCore 109 | 1.13-SNAPSHOT 110 | compile 111 | 112 | 113 | 114 | com.magmaguy 115 | ResourcePackManager 116 | 1.3.0-SNAPSHOT 117 | provided 118 | 119 | 120 | 121 | 122 | 123 | FreeMinecraftModels 124 | 125 | 126 | 127 | maven-compiler-plugin 128 | 129 | 17 130 | 17 131 | 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-shade-plugin 138 | 3.6.0 139 | 140 | 141 | 142 | org.bstats 143 | com.magmaguy.freeminecraftmodels.bstats 144 | 145 | 146 | com.magmaguy.magmacore 147 | com.magmaguy.freeminecraftmodels.magmacore 148 | 149 | 150 | com.magmaguy.easyminecraftgoals 151 | com.magmaguy.freeminecraftmodels.easyminecraftgoals 152 | 153 | 154 | 155 | 156 | 157 | package 158 | 159 | shade 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | magmaguy-repo-snapshots 170 | https://repo.magmaguy.com/snapshots 171 | 172 | 173 | magmaguy-repo 174 | MagmaGuy's Repository 175 | https://repo.magmaguy.com/releases 176 | 177 | 178 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/FreeMinecraftModels.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels; 2 | 3 | import com.magmaguy.easyminecraftgoals.NMSManager; 4 | import com.magmaguy.freeminecraftmodels.commands.*; 5 | import com.magmaguy.freeminecraftmodels.config.DefaultConfig; 6 | import com.magmaguy.freeminecraftmodels.config.ModelsFolder; 7 | import com.magmaguy.freeminecraftmodels.config.OutputFolder; 8 | import com.magmaguy.freeminecraftmodels.config.props.PropsConfig; 9 | import com.magmaguy.freeminecraftmodels.customentity.*; 10 | import com.magmaguy.freeminecraftmodels.customentity.core.OBBHitDetection; 11 | import com.magmaguy.freeminecraftmodels.dataconverter.FileModelConverter; 12 | import com.magmaguy.freeminecraftmodels.listeners.EntityTeleportEvent; 13 | import com.magmaguy.magmacore.MagmaCore; 14 | import com.magmaguy.magmacore.command.CommandManager; 15 | import org.bstats.bukkit.Metrics; 16 | import org.bukkit.Bukkit; 17 | import org.bukkit.entity.LivingEntity; 18 | import org.bukkit.event.EventHandler; 19 | import org.bukkit.event.EventPriority; 20 | import org.bukkit.event.HandlerList; 21 | import org.bukkit.event.Listener; 22 | import org.bukkit.event.entity.EntityDamageByEntityEvent; 23 | import org.bukkit.plugin.java.JavaPlugin; 24 | 25 | public final class FreeMinecraftModels extends JavaPlugin implements Listener { 26 | 27 | @Override 28 | public void onEnable() { 29 | Bukkit.getLogger().info(" _______ __ ___ __ _______ __ __ "); 30 | Bukkit.getLogger().info("| | |__|.-----.-----.----.----.---.-.' _| |_| | |.-----.--| |.-----.| |.-----."); 31 | Bukkit.getLogger().info("| | || | -__| __| _| _ | _| _| || _ | _ || -__|| ||__ --|"); 32 | Bukkit.getLogger().info("|__|_|__|__||__|__|_____|____|__| |___._|__| |____|__|_|__||_____|_____||_____||__||_____|"); 33 | Bukkit.getLogger().info("Version " + this.getDescription().getVersion()); 34 | MetadataHandler.PLUGIN = this; 35 | MagmaCore.onEnable(); 36 | MagmaCore.checkVersionUpdate("111660"); 37 | //Initialize plugin configuration files 38 | new DefaultConfig(); 39 | MagmaCore.initializeImporter(); 40 | OutputFolder.initializeConfig(); 41 | ModelsFolder.initializeConfig(); 42 | Metrics metrics = new Metrics(this, 19337); 43 | Bukkit.getPluginManager().registerEvents(new ModeledEntityEvents(), this); 44 | Bukkit.getPluginManager().registerEvents(new OBBHitDetection(), this); 45 | Bukkit.getPluginManager().registerEvents(new PropEntity.PropEntityEvents(), this); 46 | Bukkit.getPluginManager().registerEvents(new EntityTeleportEvent(), this); 47 | Bukkit.getPluginManager().registerEvents(new DynamicEntity.ModeledEntityEvents(), this); 48 | NMSManager.initializeAdapter(this); 49 | CommandManager manager = new CommandManager(this, "freeminecraftmodels"); 50 | manager.registerCommand(new MountCommand()); 51 | manager.registerCommand(new HitboxDebugCommand()); 52 | manager.registerCommand(new DeleteAllCommand()); 53 | manager.registerCommand(new ReloadCommand()); 54 | manager.registerCommand(new SpawnCommand()); 55 | manager.registerCommand(new VersionCommand()); 56 | new PropsConfig(); 57 | Bukkit.getPluginManager().registerEvents(this, this); 58 | OutputFolder.zipResourcePack(); 59 | 60 | ModeledEntitiesClock.start(); 61 | 62 | PropEntity.onStartup(); 63 | OBBHitDetection.startProjectileDetection(); 64 | } 65 | 66 | @Override 67 | public void onLoad() { 68 | MagmaCore.createInstance(this); 69 | } 70 | 71 | @Override 72 | public void onDisable() { 73 | // Plugin shutdown logic 74 | MagmaCore.shutdown(); 75 | FileModelConverter.shutdown(); 76 | ModeledEntity.shutdown(); 77 | ModeledEntitiesClock.shutdown(); 78 | OBBHitDetection.shutdown(); 79 | Bukkit.getServer().getScheduler().cancelTasks(MetadataHandler.PLUGIN); 80 | HandlerList.unregisterAll(MetadataHandler.PLUGIN); 81 | } 82 | 83 | @EventHandler(priority = EventPriority.LOWEST) 84 | public void onEntityDamagedByEntityEvent(EntityDamageByEntityEvent event) { 85 | if (!event.isCancelled()) return; 86 | if (!(event.getEntity() instanceof LivingEntity livingEntity)) return; 87 | if (DynamicEntity.isDynamicEntity(livingEntity)) 88 | event.setCancelled(false); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/MetadataHandler.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels; 2 | 3 | import org.bukkit.plugin.Plugin; 4 | 5 | public class MetadataHandler { 6 | public static Plugin PLUGIN; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/Animation.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | import com.magmaguy.freeminecraftmodels.customentity.core.Bone; 5 | import com.magmaguy.freeminecraftmodels.dataconverter.AnimationBlueprint; 6 | import com.magmaguy.freeminecraftmodels.dataconverter.AnimationFrame; 7 | import lombok.Getter; 8 | 9 | import java.util.HashMap; 10 | 11 | public class Animation { 12 | @Getter 13 | private final AnimationBlueprint animationBlueprint; 14 | @Getter 15 | private final HashMap animationFrames = new HashMap<>(); 16 | @Getter 17 | private int counter = 0; 18 | 19 | public void incrementCounter() { 20 | counter++; 21 | } 22 | 23 | public Animation(AnimationBlueprint animationBlueprint, ModeledEntity modeledEntity) { 24 | this.animationBlueprint = animationBlueprint; 25 | animationBlueprint.getAnimationFrames().forEach((key, value) -> { 26 | for (Bone bone : modeledEntity.getSkeleton().getBones()) 27 | if (bone.getBoneBlueprint().equals(key)) { 28 | animationFrames.put(bone, value); 29 | break; 30 | } 31 | }); 32 | modeledEntity.getSkeleton().getBones().forEach(bone -> { 33 | if (!animationFrames.containsKey(bone)) { 34 | animationFrames.put(bone, null); 35 | } 36 | }); 37 | } 38 | 39 | public void resetCounter() { 40 | counter = 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/AnimationManager.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | import com.magmaguy.freeminecraftmodels.dataconverter.AnimationFrame; 5 | import com.magmaguy.freeminecraftmodels.dataconverter.AnimationsBlueprint; 6 | 7 | import java.util.EnumMap; 8 | import java.util.Map; 9 | 10 | public class AnimationManager { 11 | private final Map states = new EnumMap<>(AnimationStateType.class); 12 | private final ModeledEntity modeledEntity; 13 | private final Animations animations; 14 | private IAnimState current; 15 | private IAnimState nextQueued; 16 | private AnimationStateType lastCommitted; 17 | 18 | public AnimationManager(ModeledEntity modeledEntity, AnimationsBlueprint bp) { 19 | this.modeledEntity = modeledEntity; 20 | this.animations = new Animations(bp, modeledEntity); 21 | 22 | // register states 23 | if (animations.getAnimations().get("idle") != null) 24 | states.put(AnimationStateType.IDLE, new IdleState(modeledEntity, new AnimationStateConfig(animations.getAnimations().get("idle"), true))); 25 | if (animations.getAnimations().get("walk") != null) 26 | states.put(AnimationStateType.WALK, new WalkState(modeledEntity, new AnimationStateConfig(animations.getAnimations().get("walk"), true))); 27 | if (animations.getAnimations().get("attack") != null) 28 | states.put(AnimationStateType.ATTACK, new AttackState(new AnimationStateConfig(animations.getAnimations().get("attack"), false))); 29 | if (animations.getAnimations().get("death") != null) 30 | states.put(AnimationStateType.DEATH, new DeathState(modeledEntity, new AnimationStateConfig(animations.getAnimations().get("death"), false))); 31 | if (animations.getAnimations().get("spawn") != null) 32 | states.put(AnimationStateType.SPAWN, new SpawnState(new AnimationStateConfig(animations.getAnimations().get("spawn"), false))); 33 | lastCommitted = null; 34 | 35 | // start with spawn or idle 36 | current = states.get(AnimationStateType.SPAWN) != null 37 | ? states.get(AnimationStateType.SPAWN) 38 | : states.get(AnimationStateType.IDLE); 39 | current.enter(); 40 | } 41 | 42 | private void transitionTo(IAnimState target) { 43 | if (target == null || target == current) return; 44 | current.exit(); 45 | if (!(current instanceof CustomAnimationState)) { 46 | lastCommitted = current.getType(); 47 | } 48 | current = target; 49 | current.enter(); 50 | } 51 | 52 | /** 53 | * Play either a built-in state (idle, walk, attack, death, spawn) 54 | * or a data-driven animation by name. 55 | * 56 | * @param name name of the animation/state (case-insensitive) 57 | * @param blendAnimation if true, queue it behind the current; if false, interrupt immediately 58 | * @param loop only applies for custom animations 59 | * @return true if the animation exists and was scheduled 60 | */ 61 | public boolean play(String name, boolean blendAnimation, boolean loop) { 62 | // 1) try built-in 63 | AnimationStateType st = null; 64 | try { 65 | st = AnimationStateType.valueOf(name.toUpperCase()); 66 | } catch (IllegalArgumentException ignored) { 67 | } 68 | 69 | if (st != null && states.containsKey(st)) { 70 | IAnimState builtIn = states.get(st); 71 | if (blendAnimation) nextQueued = builtIn; 72 | else transitionTo(builtIn); 73 | return true; 74 | } 75 | 76 | // 2) fallback to custom 77 | Animation anim = animations.getAnimations().get(name); 78 | if (anim == null) return false; 79 | 80 | CustomAnimationState custom = new CustomAnimationState( 81 | modeledEntity, 82 | anim, 83 | loop, 84 | lastCommitted != null ? lastCommitted : AnimationStateType.IDLE 85 | ); 86 | 87 | if (blendAnimation) nextQueued = custom; 88 | else transitionTo(custom); 89 | 90 | return true; 91 | } 92 | 93 | 94 | public void stop() { 95 | current.exit(); 96 | transitionTo(states.get(AnimationStateType.IDLE)); 97 | } 98 | 99 | public void tick() { 100 | // 1) let the state update its own “finished” logic 101 | current.update(); 102 | 103 | // 2) render one frame of whatever the current state holds 104 | renderCurrentFrame(); 105 | 106 | // 3) handle transitions (including blends) 107 | if (nextQueued != null) { 108 | transitionTo(nextQueued); 109 | nextQueued = null; 110 | } else { 111 | current.nextState().ifPresent(stateType -> { 112 | IAnimState next = states.get(stateType); 113 | transitionTo(next); 114 | }); 115 | } 116 | } 117 | 118 | private void renderCurrentFrame() { 119 | Animation anim = current.getAnimation(); 120 | boolean loop = current.isLoop(); 121 | int duration = anim.getAnimationBlueprint().getDuration(); 122 | long counter = anim.getCounter(); 123 | 124 | // if non-looping and we’re past the end, just don’t render further 125 | if (!loop && counter >= duration) { 126 | return; 127 | } 128 | 129 | // compute frame index: either modulo (loop) or clamp to last frame 130 | int frame; 131 | if (loop) { 132 | frame = (int) (counter % duration); 133 | } else { 134 | frame = (int) Math.min(counter, duration - 1); 135 | } 136 | 137 | // apply rotations/translations/scales in one pass 138 | anim.getAnimationFrames().forEach((part, frames) -> { 139 | if (frames == null || frames.length <= frame) { 140 | // reset to default 141 | part.updateAnimationRotation(0, 0, 0); 142 | part.updateAnimationTranslation(0, 0, 0); 143 | part.updateAnimationScale(1f); 144 | } else { 145 | AnimationFrame f = frames[frame]; 146 | part.updateAnimationRotation(f.xRotation, f.yRotation, f.zRotation); 147 | part.updateAnimationTranslation(f.xPosition, f.yPosition, f.zPosition); 148 | part.updateAnimationScale(f.scale != null ? f.scale : 1f); 149 | } 150 | }); 151 | 152 | // advance the counter for next tick 153 | anim.incrementCounter(); 154 | } 155 | 156 | public boolean hasAnimation(String animationName) { 157 | return animations.getAnimations().containsKey(animationName); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/AnimationStateConfig.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | public class AnimationStateConfig { 6 | private final Animation animation; 7 | private final boolean loop; 8 | 9 | public AnimationStateConfig(Animation animation, boolean loop) { 10 | this.animation = animation; 11 | this.loop = loop; 12 | } 13 | 14 | @Nullable 15 | public Animation getAnimation() { 16 | return animation; 17 | } 18 | 19 | public boolean isLoop() { 20 | return loop; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/AnimationStateType.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | public enum AnimationStateType { 4 | SPAWN, IDLE, WALK, JUMP, ATTACK, DEATH, CUSTOM; 5 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/Animations.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | import com.magmaguy.freeminecraftmodels.dataconverter.AnimationsBlueprint; 5 | import lombok.Getter; 6 | 7 | import java.util.HashMap; 8 | 9 | public class Animations { 10 | @Getter 11 | private final HashMap animations = new HashMap<>(); 12 | private final AnimationsBlueprint animationBlueprint; 13 | public Animations(AnimationsBlueprint animationsBlueprint, ModeledEntity modeledEntity){ 14 | this.animationBlueprint = animationsBlueprint; 15 | animationsBlueprint.getAnimations().forEach((key, value) -> animations.put(key, new Animation(value, modeledEntity))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/AttackState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import java.util.Optional; 4 | 5 | public class AttackState implements IAnimState { 6 | private final AnimationStateConfig cfg; 7 | private boolean finished; 8 | 9 | public AttackState(AnimationStateConfig cfg) { 10 | this.cfg = cfg; 11 | } 12 | 13 | @Override 14 | public void enter() { 15 | cfg.getAnimation().resetCounter(); 16 | finished = false; 17 | } 18 | 19 | @Override 20 | public void update() { 21 | if (cfg.getAnimation().getCounter() >= cfg.getAnimation().getAnimationBlueprint().getDuration()) { 22 | finished = true; 23 | } 24 | } 25 | 26 | @Override 27 | public void exit() { 28 | } 29 | 30 | @Override 31 | public AnimationStateType getType() { 32 | return AnimationStateType.ATTACK; 33 | } 34 | 35 | @Override 36 | public Optional nextState() { 37 | return finished ? Optional.of(AnimationStateType.IDLE) : Optional.empty(); 38 | } 39 | 40 | @Override 41 | public Animation getAnimation() { 42 | return cfg.getAnimation(); 43 | } 44 | 45 | @Override 46 | public boolean isLoop() { 47 | return cfg.isLoop(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/CustomAnimationState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | 5 | import java.util.Optional; 6 | 7 | public class CustomAnimationState implements IAnimState { 8 | private final ModeledEntity entity; 9 | private final Animation animation; 10 | private final boolean loop; 11 | private final AnimationStateType returnTo; 12 | private boolean finished; 13 | 14 | public CustomAnimationState(ModeledEntity entity, 15 | Animation animation, 16 | boolean loop, 17 | AnimationStateType returnTo) { 18 | this.entity = entity; 19 | this.animation = animation; 20 | this.loop = loop; 21 | this.returnTo = returnTo; 22 | } 23 | 24 | @Override 25 | public void enter() { 26 | animation.resetCounter(); 27 | finished = false; 28 | } 29 | 30 | @Override 31 | public void update() { 32 | if (!loop && animation.getCounter() >= animation.getAnimationBlueprint().getDuration()) { 33 | finished = true; 34 | } 35 | } 36 | 37 | @Override 38 | public void exit() { 39 | } 40 | 41 | @Override 42 | public AnimationStateType getType() { 43 | return AnimationStateType.CUSTOM; 44 | } 45 | 46 | @Override 47 | public Optional nextState() { 48 | return finished ? Optional.of(returnTo) : Optional.empty(); 49 | } 50 | 51 | @Override 52 | public Animation getAnimation() { 53 | return animation; 54 | } 55 | 56 | @Override 57 | public boolean isLoop() { 58 | return loop; 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/DeathState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | 5 | import java.util.Optional; 6 | 7 | public class DeathState implements IAnimState { 8 | private final ModeledEntity modeledEntity; 9 | private final AnimationStateConfig cfg; 10 | 11 | public DeathState(ModeledEntity entity, AnimationStateConfig cfg) { 12 | this.modeledEntity = entity; 13 | this.cfg = cfg; 14 | } 15 | 16 | @Override 17 | public void enter() { 18 | cfg.getAnimation().resetCounter(); 19 | } 20 | 21 | @Override 22 | public void update() { 23 | // no-op: we’ll check for “done” in nextState() 24 | } 25 | 26 | @Override 27 | public void exit() { 28 | } 29 | 30 | @Override 31 | public AnimationStateType getType() { 32 | return AnimationStateType.DEATH; 33 | } 34 | 35 | @Override 36 | public Optional nextState() { 37 | Animation anim = cfg.getAnimation(); 38 | long counter = anim.getCounter(); 39 | int dur = anim.getAnimationBlueprint().getDuration(); 40 | 41 | if (counter >= dur) { 42 | // only remove after the last frame has been drawn (counter was incremented 43 | // at end of renderCurrentFrame) 44 | modeledEntity.removeWithMinimizedAnimation(); 45 | } 46 | return Optional.empty(); 47 | } 48 | 49 | @Override 50 | public Animation getAnimation() { 51 | return cfg.getAnimation(); 52 | } 53 | 54 | @Override 55 | public boolean isLoop() { 56 | return cfg.isLoop(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/IAnimState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import java.util.Optional; 4 | 5 | public interface IAnimState { 6 | void enter(); 7 | 8 | void update(); 9 | 10 | void exit(); 11 | 12 | AnimationStateType getType(); 13 | 14 | Optional nextState(); 15 | 16 | Animation getAnimation(); 17 | 18 | boolean isLoop(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/IdleState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | 5 | import java.util.Optional; 6 | 7 | public class IdleState implements IAnimState { 8 | private final ModeledEntity entity; 9 | private final AnimationStateConfig cfg; 10 | private AnimationStateType requestedNext; 11 | 12 | public IdleState(ModeledEntity entity, AnimationStateConfig cfg) { 13 | this.entity = entity; 14 | this.cfg = cfg; 15 | } 16 | 17 | @Override 18 | public void enter() { 19 | cfg.getAnimation().resetCounter(); 20 | } 21 | 22 | @Override 23 | public void update() { 24 | requestedNext = null; 25 | if (entity.getLivingEntity() != null && entity.getLivingEntity().getVelocity().length() > .08) { 26 | requestedNext = AnimationStateType.WALK; 27 | } 28 | } 29 | 30 | @Override 31 | public void exit() { 32 | } 33 | 34 | @Override 35 | public AnimationStateType getType() { 36 | return AnimationStateType.IDLE; 37 | } 38 | 39 | @Override 40 | public Optional nextState() { 41 | return Optional.ofNullable(requestedNext); 42 | } 43 | 44 | @Override 45 | public Animation getAnimation() { 46 | return cfg.getAnimation(); 47 | } 48 | 49 | @Override 50 | public boolean isLoop() { 51 | return cfg.isLoop(); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/SpawnState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import java.util.Optional; 4 | 5 | public class SpawnState implements IAnimState { 6 | private final AnimationStateConfig cfg; 7 | private boolean finished; 8 | 9 | public SpawnState(AnimationStateConfig cfg) { 10 | this.cfg = cfg; 11 | } 12 | 13 | @Override 14 | public void enter() { 15 | cfg.getAnimation().resetCounter(); 16 | finished = false; 17 | } 18 | 19 | @Override 20 | public void update() { 21 | if (cfg.getAnimation().getCounter() >= cfg.getAnimation().getAnimationBlueprint().getDuration()) { 22 | finished = true; 23 | } 24 | } 25 | 26 | @Override 27 | public void exit() { 28 | } 29 | 30 | @Override 31 | public AnimationStateType getType() { 32 | return AnimationStateType.SPAWN; 33 | } 34 | 35 | @Override 36 | public Optional nextState() { 37 | return finished ? Optional.of(AnimationStateType.IDLE) : Optional.empty(); 38 | } 39 | 40 | @Override 41 | public Animation getAnimation() { 42 | return cfg.getAnimation(); 43 | } 44 | 45 | @Override 46 | public boolean isLoop() { 47 | return cfg.isLoop(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/animation/WalkState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.animation; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | 5 | import java.util.Optional; 6 | 7 | public class WalkState implements IAnimState { 8 | private final ModeledEntity entity; 9 | private final AnimationStateConfig cfg; 10 | private AnimationStateType requestedNext; 11 | 12 | public WalkState(ModeledEntity entity, AnimationStateConfig cfg) { 13 | this.entity = entity; 14 | this.cfg = cfg; 15 | } 16 | 17 | @Override 18 | public void enter() { 19 | cfg.getAnimation().resetCounter(); 20 | } 21 | 22 | @Override 23 | public void update() { 24 | requestedNext = null; 25 | if (!entity.getLivingEntity().isOnGround()) { 26 | requestedNext = AnimationStateType.JUMP; 27 | } else if (entity.getLivingEntity().getVelocity().length() <= .08) { 28 | requestedNext = AnimationStateType.IDLE; 29 | } 30 | } 31 | 32 | @Override 33 | public void exit() { 34 | } 35 | 36 | @Override 37 | public AnimationStateType getType() { 38 | return AnimationStateType.WALK; 39 | } 40 | 41 | @Override 42 | public Optional nextState() { 43 | return Optional.ofNullable(requestedNext); 44 | } 45 | 46 | @Override 47 | public Animation getAnimation() { 48 | return cfg.getAnimation(); 49 | } 50 | 51 | @Override 52 | public boolean isLoop() { 53 | return cfg.isLoop(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/DynamicEntityHitboxContactEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class DynamicEntityHitboxContactEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final DynamicEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public DynamicEntityHitboxContactEvent(Player player, DynamicEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/DynamicEntityLeftClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class DynamicEntityLeftClickEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final DynamicEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public DynamicEntityLeftClickEvent(Player player, DynamicEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/DynamicEntityRightClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class DynamicEntityRightClickEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final DynamicEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public DynamicEntityRightClickEvent(Player player, DynamicEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/ModeledEntityHitboxContactEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class ModeledEntityHitboxContactEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final ModeledEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public ModeledEntityHitboxContactEvent(Player player, ModeledEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/ModeledEntityLeftClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | @Getter 11 | public class ModeledEntityLeftClickEvent extends Event implements Cancellable { 12 | private static final HandlerList handlers = new HandlerList(); 13 | 14 | private final ModeledEntity entity; 15 | private final Player player; 16 | private boolean cancelled = false; 17 | 18 | public ModeledEntityLeftClickEvent(Player player, ModeledEntity entity) { 19 | this.entity = entity; 20 | this.player = player; 21 | } 22 | 23 | public static HandlerList getHandlerList() { 24 | return handlers; 25 | } 26 | 27 | @Override 28 | public HandlerList getHandlers() { 29 | return handlers; 30 | } 31 | 32 | @Override 33 | public boolean isCancelled() { 34 | return cancelled; 35 | } 36 | 37 | @Override 38 | public void setCancelled(boolean cancel) { 39 | this.cancelled = cancel; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/ModeledEntityManager.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.commands.ReloadCommand; 4 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 5 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 6 | import com.magmaguy.freeminecraftmodels.dataconverter.FileModelConverter; 7 | import org.bukkit.Bukkit; 8 | 9 | import java.util.HashSet; 10 | import java.util.List; 11 | 12 | public class ModeledEntityManager { 13 | private ModeledEntityManager() { 14 | } 15 | 16 | /** 17 | * Returns combined lists of all ModeledEntities (dynamic and static). 18 | * 19 | * @return Returns all ModeledEntities currently instanced by the plugin 20 | */ 21 | public static HashSet getAllEntities() { 22 | return (HashSet) ModeledEntity.getLoadedModeledEntities().clone(); 23 | } 24 | 25 | /** 26 | * Returns whether a model exists by a given name 27 | * 28 | * @param modelName Name to check 29 | * @return Whether the model exists 30 | */ 31 | public static boolean modelExists(String modelName) { 32 | return FileModelConverter.getConvertedFileModels().containsKey(modelName); 33 | } 34 | 35 | /** 36 | * Returns the list of dynamic entities currently instanced by the plugin 37 | * 38 | * @return The list of currently instanced dynamic entities 39 | */ 40 | public static List getDynamicEntities() { 41 | return DynamicEntity.getDynamicEntities(); 42 | } 43 | 44 | /** 45 | * Safely handles reloading the plugin, importing new data and reinitializing models 46 | */ 47 | public static void reload() { 48 | ReloadCommand.reloadPlugin(Bukkit.getConsoleSender()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/ModeledEntityRightClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | @Getter 11 | public class ModeledEntityRightClickEvent extends Event implements Cancellable { 12 | private static final HandlerList handlers = new HandlerList(); 13 | 14 | private final ModeledEntity entity; 15 | private final Player player; 16 | private boolean cancelled = false; 17 | 18 | public ModeledEntityRightClickEvent(Player player, ModeledEntity entity) { 19 | this.entity = entity; 20 | this.player = player; 21 | } 22 | 23 | public static HandlerList getHandlerList() { 24 | return handlers; 25 | } 26 | 27 | @Override 28 | public HandlerList getHandlers() { 29 | return handlers; 30 | } 31 | 32 | @Override 33 | public boolean isCancelled() { 34 | return cancelled; 35 | } 36 | 37 | @Override 38 | public void setCancelled(boolean cancel) { 39 | this.cancelled = cancel; 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/PropEntityHitboxContactEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.PropEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class PropEntityHitboxContactEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final PropEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public PropEntityHitboxContactEvent(Player player, PropEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/PropEntityLeftClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.PropEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class PropEntityLeftClickEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final PropEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public PropEntityLeftClickEvent(Player player, PropEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/PropEntityRightClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.PropEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class PropEntityRightClickEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final PropEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public PropEntityRightClickEvent(Player player, PropEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/StaticEntityHitboxContactEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.StaticEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class StaticEntityHitboxContactEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final StaticEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public StaticEntityHitboxContactEvent(Player player, StaticEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/StaticEntityLeftClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.StaticEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class StaticEntityLeftClickEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final StaticEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public StaticEntityLeftClickEvent(Player player, StaticEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/api/StaticEntityRightClickEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.api; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.StaticEntity; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | 10 | public class StaticEntityRightClickEvent extends Event implements Cancellable { 11 | private static final HandlerList handlers = new HandlerList(); 12 | 13 | @Getter 14 | private final StaticEntity entity; 15 | @Getter 16 | private final Player player; 17 | private boolean cancelled = false; 18 | 19 | public StaticEntityRightClickEvent(Player player, StaticEntity entity) { 20 | this.entity = entity; 21 | this.player = player; 22 | } 23 | 24 | public static HandlerList getHandlerList() { 25 | return handlers; 26 | } 27 | 28 | @Override 29 | public HandlerList getHandlers() { 30 | return handlers; 31 | } 32 | 33 | @Override 34 | public boolean isCancelled() { 35 | return cancelled; 36 | } 37 | 38 | @Override 39 | public void setCancelled(boolean cancel) { 40 | this.cancelled = cancel; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/commands/DeleteAllCommand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.commands; 2 | 3 | import com.magmaguy.freeminecraftmodels.api.ModeledEntityManager; 4 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 5 | import com.magmaguy.magmacore.command.AdvancedCommand; 6 | import com.magmaguy.magmacore.command.CommandData; 7 | import com.magmaguy.magmacore.command.SenderType; 8 | import com.magmaguy.magmacore.util.Logger; 9 | import org.bukkit.ChatColor; 10 | import org.bukkit.entity.Player; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * Command to delete all modeled entities in the current world. 16 | */ 17 | public class DeleteAllCommand extends AdvancedCommand { 18 | 19 | public DeleteAllCommand() { 20 | super(List.of("deleteall")); 21 | setDescription("Delete all loaded modeled entities in your world"); 22 | setPermission("freeminecraftmodels.deleteall"); 23 | setUsage("/fmm deleteall"); 24 | setSenderType(SenderType.PLAYER); 25 | } 26 | 27 | @Override 28 | public void execute(CommandData commandData) { 29 | Player player = commandData.getPlayerSender(); 30 | 31 | int removedCount = 0; 32 | 33 | // Remove all entities in the player's world 34 | for (ModeledEntity allEntity : ModeledEntityManager.getAllEntities()) { 35 | allEntity.remove(); 36 | removedCount++; 37 | } 38 | 39 | // Notify player of the result 40 | if (removedCount > 0) { 41 | Logger.sendMessage(player, ChatColor.GREEN + "Successfully deleted " 42 | + removedCount + " modeled entities."); 43 | } else { 44 | Logger.sendMessage(player, ChatColor.YELLOW + "No modeled entities found to delete."); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/commands/HitboxDebugCommand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.commands; 2 | 3 | import com.magmaguy.freeminecraftmodels.api.ModeledEntityManager; 4 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 5 | import com.magmaguy.freeminecraftmodels.customentity.core.OrientedBoundingBox; 6 | import com.magmaguy.magmacore.command.AdvancedCommand; 7 | import com.magmaguy.magmacore.command.CommandData; 8 | import com.magmaguy.magmacore.command.SenderType; 9 | import com.magmaguy.magmacore.command.arguments.ListStringCommandArgument; 10 | import com.magmaguy.magmacore.util.Logger; 11 | import org.bukkit.ChatColor; 12 | import org.bukkit.entity.Player; 13 | 14 | import java.util.List; 15 | import java.util.stream.Collectors; 16 | 17 | /** 18 | * Debug command for the OBB hitbox system 19 | */ 20 | public class HitboxDebugCommand extends AdvancedCommand { 21 | 22 | public HitboxDebugCommand() { 23 | super(List.of("hitbox")); 24 | addArgument("action", new ListStringCommandArgument( 25 | List.of("visualize"), 26 | "")); 27 | // Optional duration argument for visualize 28 | addArgument("duration", new ListStringCommandArgument( 29 | List.of("60", "100", "200", "600"), 30 | "[duration]")); 31 | 32 | setDescription("Debug commands for OBB hitbox system"); 33 | setPermission("freeminecraftmodels.*"); 34 | setUsage("/fmm hitbox visualize [duration]"); 35 | setSenderType(SenderType.PLAYER); 36 | } 37 | 38 | @Override 39 | public void execute(CommandData commandData) { 40 | Player player = commandData.getPlayerSender(); 41 | int duration = 100; // Default duration in ticks (5 seconds) 42 | 43 | String durationArg = commandData.getStringArgument("duration"); 44 | if (durationArg != null && !durationArg.isEmpty()) { 45 | try { 46 | duration = Integer.parseInt(durationArg); 47 | if (duration <= 0) { 48 | duration = 100; 49 | } else if (duration > 1200) { // Cap at 60 seconds 50 | duration = 1200; 51 | } 52 | } catch (NumberFormatException e) { 53 | Logger.sendMessage(player, "Invalid duration. Using default of 5 seconds."); 54 | } 55 | } 56 | 57 | // Get all nearby modeled entities 58 | List nearbyEntities = ModeledEntityManager.getAllEntities().stream() 59 | .filter(entity -> entity.getWorld() != null && 60 | entity.getWorld().equals(player.getWorld()) && 61 | entity.getLocation().distanceSquared(player.getLocation()) < 100) // Within 10 blocks 62 | .collect(Collectors.toList()); 63 | 64 | if (nearbyEntities.isEmpty()) { 65 | Logger.sendMessage(player, ChatColor.RED + "No modeled entities found nearby."); 66 | return; 67 | } 68 | 69 | int finalDuration = duration; 70 | nearbyEntities.forEach(entity -> { 71 | OrientedBoundingBox.visualizeOBB(entity, finalDuration, player); 72 | entity.showUnderlyingEntity(player); 73 | }); 74 | 75 | Logger.sendMessage(player, ChatColor.GREEN + "Visualizing " + nearbyEntities.size() + 76 | " modeled entities for " + (finalDuration / 20.0) + " seconds."); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/commands/MountCommand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.commands; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 5 | import com.magmaguy.freeminecraftmodels.dataconverter.FileModelConverter; 6 | import com.magmaguy.magmacore.command.AdvancedCommand; 7 | import com.magmaguy.magmacore.command.CommandData; 8 | import com.magmaguy.magmacore.command.SenderType; 9 | import com.magmaguy.magmacore.command.arguments.ListStringCommandArgument; 10 | import com.magmaguy.magmacore.util.Logger; 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.Material; 13 | import org.bukkit.entity.EntityType; 14 | import org.bukkit.entity.Horse; 15 | import org.bukkit.entity.LivingEntity; 16 | import org.bukkit.inventory.ItemStack; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | public class MountCommand extends AdvancedCommand { 22 | 23 | List entityIDs = new ArrayList<>(); 24 | 25 | public MountCommand() { 26 | super(List.of("mount")); 27 | setDescription("Mounts a model (experimental!)"); 28 | setPermission("freeminecraftmodels.*"); 29 | setDescription("/fmm mount "); 30 | setSenderType(SenderType.PLAYER); 31 | entityIDs = new ArrayList<>(); 32 | FileModelConverter.getConvertedFileModels().values().forEach(fileModelConverter -> entityIDs.add(fileModelConverter.getID())); 33 | addArgument("models", new ListStringCommandArgument(entityIDs, "")); 34 | } 35 | 36 | @Override 37 | public void execute(CommandData commandData) { 38 | if (!entityIDs.contains(commandData.getStringArgument("models"))) { 39 | Logger.sendMessage(commandData.getCommandSender(), "Invalid entity ID!"); 40 | return; 41 | } 42 | 43 | DynamicEntity dynamicEntity = DynamicEntity.create( 44 | commandData.getStringArgument("models"), 45 | (LivingEntity) commandData.getPlayerSender().getWorld().spawnEntity((commandData.getPlayerSender()).getLocation(), EntityType.HORSE)); 46 | 47 | Bukkit.getScheduler().scheduleSyncDelayedTask(MetadataHandler.PLUGIN, () -> { 48 | ((Horse) dynamicEntity.getLivingEntity()).setTamed(true); 49 | ((Horse) dynamicEntity.getLivingEntity()).setOwner(commandData.getPlayerSender()); 50 | ((Horse) dynamicEntity.getLivingEntity()).getInventory().setSaddle(new ItemStack(Material.SADDLE)); 51 | dynamicEntity.getLivingEntity().addPassenger(commandData.getPlayerSender()); 52 | }, 5); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/commands/ReloadCommand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.commands; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import com.magmaguy.freeminecraftmodels.events.ResourcePackGenerationEvent; 5 | import com.magmaguy.magmacore.command.AdvancedCommand; 6 | import com.magmaguy.magmacore.command.CommandData; 7 | import com.magmaguy.magmacore.util.Logger; 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.command.CommandSender; 10 | 11 | import java.util.List; 12 | 13 | public class ReloadCommand extends AdvancedCommand { 14 | public ReloadCommand() { 15 | super(List.of("reload")); 16 | setDescription("Reloads the plugin"); 17 | setPermission("freeminecraftmodels.*"); 18 | setUsage("/fmm reload"); 19 | } 20 | 21 | public static void reloadPlugin(CommandSender sender) { 22 | MetadataHandler.PLUGIN.onDisable(); 23 | MetadataHandler.PLUGIN.onEnable(); 24 | if (Bukkit.getPluginManager().isPluginEnabled("ResourcePackManager")) 25 | Bukkit.getPluginManager().callEvent(new ResourcePackGenerationEvent()); 26 | Logger.sendMessage(sender, "Reloaded!"); 27 | } 28 | 29 | @Override 30 | public void execute(CommandData commandData) { 31 | reloadPlugin(commandData.getCommandSender()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/commands/SpawnCommand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.commands; 2 | 3 | import com.magmaguy.freeminecraftmodels.config.props.PropsConfig; 4 | import com.magmaguy.freeminecraftmodels.config.props.PropsConfigFields; 5 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 6 | import com.magmaguy.freeminecraftmodels.customentity.StaticEntity; 7 | import com.magmaguy.freeminecraftmodels.dataconverter.FileModelConverter; 8 | import com.magmaguy.magmacore.command.AdvancedCommand; 9 | import com.magmaguy.magmacore.command.CommandData; 10 | import com.magmaguy.magmacore.command.SenderType; 11 | import com.magmaguy.magmacore.command.arguments.ListStringCommandArgument; 12 | import com.magmaguy.magmacore.util.Logger; 13 | import org.bukkit.Location; 14 | import org.bukkit.entity.EntityType; 15 | import org.bukkit.entity.LivingEntity; 16 | import org.bukkit.entity.Player; 17 | import org.bukkit.util.RayTraceResult; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | public class SpawnCommand extends AdvancedCommand { 23 | 24 | List spawnTypes = List.of("static", "dynamic", "prop"); 25 | List entityIDs = new ArrayList<>(); 26 | 27 | public SpawnCommand() { 28 | super(List.of("spawn")); 29 | addArgument("type", new ListStringCommandArgument(List.of("STATIC", "DYNAMIC", "PROP"), "")); 30 | entityIDs = new ArrayList<>(); 31 | FileModelConverter.getConvertedFileModels().values().forEach(fileModelConverter -> entityIDs.add(fileModelConverter.getID())); 32 | addArgument("model", new ListStringCommandArgument(entityIDs, "")); 33 | setDescription("Spawns custom models or creates props"); 34 | setPermission("freeminecraftmodels.*"); 35 | setUsage("/fmm spawn "); 36 | setSenderType(SenderType.PLAYER); 37 | } 38 | 39 | @Override 40 | public void execute(CommandData commandData) { 41 | Player player = commandData.getPlayerSender(); 42 | String modelID = commandData.getStringArgument("model"); 43 | 44 | if (!entityIDs.contains(modelID)) { 45 | Logger.sendMessage(commandData.getCommandSender(), "Invalid entity ID!"); 46 | return; 47 | } 48 | 49 | String type = commandData.getStringArgument("type").toLowerCase(); 50 | 51 | if (type.equals("prop")) { 52 | createProp(player, modelID); 53 | return; 54 | } 55 | 56 | // Handle static and dynamic types (existing functionality) 57 | RayTraceResult rayTraceResult = player.rayTraceBlocks(300); 58 | if (rayTraceResult == null) { 59 | Logger.sendMessage(commandData.getCommandSender(), "You need to be looking at the ground to spawn a mob!"); 60 | return; 61 | } 62 | 63 | Location location = rayTraceResult.getHitBlock().getLocation().add(0.5, 1, 0.5); 64 | location.setPitch(0); 65 | location.setYaw(180); 66 | 67 | if (type.equals("static")) { 68 | StaticEntity.create(modelID, location); 69 | } else if (type.equals("dynamic")) { 70 | DynamicEntity.create(modelID, (LivingEntity) location.getWorld().spawnEntity(location, EntityType.PIG)); 71 | } 72 | } 73 | 74 | private void createProp(Player player, String propFilename) { 75 | // Get or create the prop configuration, messaging is now handled in PropsConfig 76 | PropsConfigFields prop = PropsConfig.addPropConfigurationFile(propFilename, player); 77 | prop.permanentlyAddLocation(player.getLocation()); 78 | Logger.sendMessage(player, "Successfully added prop!"); 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/commands/VersionCommand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.commands; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import com.magmaguy.magmacore.command.AdvancedCommand; 5 | import com.magmaguy.magmacore.command.CommandData; 6 | import com.magmaguy.magmacore.util.Logger; 7 | 8 | import java.util.List; 9 | 10 | public class VersionCommand extends AdvancedCommand { 11 | public VersionCommand() { 12 | super(List.of("version")); 13 | setDescription("Reports the FreeMinecraftModels version"); 14 | setUsage("/fmm version"); 15 | } 16 | 17 | @Override 18 | public void execute(CommandData commandData) { 19 | Logger.sendMessage(commandData.getCommandSender(), "This server is running FreeMinecraftModels version " + MetadataHandler.PLUGIN.getDescription().getVersion()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/config/DefaultConfig.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.config; 2 | 3 | import com.magmaguy.magmacore.config.ConfigurationEngine; 4 | import com.magmaguy.magmacore.config.ConfigurationFile; 5 | 6 | import java.util.List; 7 | 8 | public class DefaultConfig extends ConfigurationFile { 9 | 10 | public static boolean useDisplayEntitiesWhenPossible; 11 | public static int maxModelViewDistance; 12 | public static int maxInteractionAndAttackDistance; 13 | public static boolean sendCustomModelsToBedrockClients; 14 | 15 | public DefaultConfig() { 16 | super("config.yml"); 17 | } 18 | 19 | @Override 20 | public void initializeValues() { 21 | useDisplayEntitiesWhenPossible = ConfigurationEngine.setBoolean( 22 | List.of("Sets whether display entities will be used over armor stands.", 23 | "It is not always possible to use display entities as they do not exist for bedrock, nor do they exist for servers older than 1.19.4.", 24 | "Free Minecraft Models automatically falls back to armor stand displays when it's not possible to use display entities!"), 25 | fileConfiguration, "useDisplayEntitiesWhenPossible", true); 26 | maxModelViewDistance = ConfigurationEngine.setInt( 27 | List.of("Sets the maximum distance in blocks that a modeled entity can be seen from.", 28 | "This is to prevent the server and clients from lagging when a modeled entity is far away.", 29 | "The default value is 60, which is similar to vanilla defaults."), 30 | fileConfiguration, "maxModelViewDistance", 60); 31 | maxInteractionAndAttackDistance = ConfigurationEngine.setInt( 32 | List.of("Sets the maximum distance in blocks that a modeled entity can be interacted with or attacked from.", 33 | "The default value is 3, which is similar to vanilla defaults."), 34 | fileConfiguration, "maxInteractionAndAttackDistance", 3); 35 | sendCustomModelsToBedrockClients = ConfigurationEngine.setBoolean( 36 | List.of("Sets whether custom models should be sent to bedrock clients.", 37 | "If you can't convert the resource pack, you will not be able to send disguises to the players", 38 | "If false, players will not see the custom models, but for dynamic models (bosses and such) they will see the minecraft creature the are based on."), 39 | fileConfiguration, "doNotSendCustomModelsToBedrockClients", false); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/config/ModelsFolder.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.config; 2 | 3 | import com.google.gson.Gson; 4 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 5 | import com.magmaguy.freeminecraftmodels.dataconverter.BoneBlueprint; 6 | import com.magmaguy.freeminecraftmodels.dataconverter.FileModelConverter; 7 | import com.magmaguy.magmacore.util.Logger; 8 | import com.magmaguy.magmacore.util.VersionChecker; 9 | import org.apache.commons.io.FileUtils; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.*; 15 | 16 | public class ModelsFolder { 17 | private static int counter; 18 | private static int folderCounter; 19 | 20 | public static void initializeConfig() { 21 | counter = 1; 22 | folderCounter = 50; 23 | 24 | File file = new File(MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + File.separatorChar + "models"); 25 | 26 | if (!file.exists()) { 27 | file.mkdirs(); 28 | file.mkdir(); 29 | } 30 | 31 | if (!file.exists()) { 32 | Logger.warn("Failed to create models directory!"); 33 | return; 34 | } 35 | 36 | if (!file.isDirectory()) { 37 | Logger.warn("Directory models was not a directory!"); 38 | return; 39 | } 40 | 41 | if (VersionChecker.serverVersionOlderThan(21, 4)) 42 | legacyHorseArmorGeneration(file); 43 | else 44 | newModelGeneration(file); 45 | } 46 | 47 | /** 48 | * In the old file generation, a horse armor file just had a series of numbers reserved for the different IDs of the different models 49 | */ 50 | private static void legacyHorseArmorGeneration(File file) { 51 | Gson gson = new Gson(); 52 | List bbModelConverterList = new ArrayList<>(); 53 | HashMap leatherHorseArmor = new HashMap<>(); 54 | leatherHorseArmor.put("parent", "item/generated"); 55 | leatherHorseArmor.put("textures", Collections.singletonMap("layer0", "minecraft:item/leather_horse_armor")); 56 | 57 | processFolders(file, bbModelConverterList, leatherHorseArmor, true); 58 | leatherHorseArmor.put("data", counter - 1 + folderCounter * 1000); 59 | 60 | try { 61 | FileUtils.writeStringToFile( 62 | new File(MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + File.separatorChar + "output" 63 | + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "assets" + File.separatorChar + 64 | "minecraft" + File.separatorChar + "models" + File.separatorChar + "item" + File.separatorChar 65 | + "leather_horse_armor.json"), 66 | gson.toJson(leatherHorseArmor), StandardCharsets.UTF_8); 67 | } catch (IOException e) { 68 | Logger.warn("Failed to generate the iron horse armor file!"); 69 | throw new RuntimeException(e); 70 | } 71 | } 72 | 73 | /** 74 | * In the new file generation, each model can be its own file, and referenced by namespace and name 75 | * 76 | * @param file 77 | */ 78 | private static void newModelGeneration(File file) { 79 | //Items holds the item model definition, which will be used as the reference for what the namespaces and names are 80 | //and then point to where the actual json models are 81 | File itemModelsFolder = new File(MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + 82 | File.separatorChar + "output" + 83 | File.separatorChar + "FreeMinecraftModels" + 84 | File.separatorChar + "assets" + 85 | File.separatorChar + "freeminecraftmodels" + 86 | File.separatorChar + "items"); 87 | if (!itemModelsFolder.exists()) itemModelsFolder.mkdir(); 88 | 89 | List bbModelConverterList = new ArrayList<>(); 90 | HashMap jsonConfig = new HashMap<>(); 91 | processFolders(file, bbModelConverterList, jsonConfig, true); 92 | 93 | HashMap> mappedModels = new HashMap<>(); 94 | for (FileModelConverter model : bbModelConverterList) { 95 | String modelName = model.getID(); 96 | mappedModels.computeIfAbsent(modelName, k -> new ArrayList<>()).add(model); 97 | } 98 | 99 | for (Map.Entry> entry : mappedModels.entrySet()) { 100 | String modelName = entry.getKey(); 101 | List models = entry.getValue(); 102 | File modelFolder = new File(MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + 103 | File.separatorChar + "output" + 104 | File.separatorChar + "FreeMinecraftModels" + 105 | File.separatorChar + "assets" + 106 | File.separatorChar + "freeminecraftmodels" + 107 | File.separatorChar + "items" + 108 | File.separatorChar + modelName); 109 | modelFolder.mkdir(); 110 | for (FileModelConverter fileModelConverter : models) { 111 | for (BoneBlueprint boneBlueprint : fileModelConverter.getSkeletonBlueprint().getBoneMap().values()) { 112 | if (boneBlueprint.getBoneName().contains("freeminecraftmodels_autogenerated_root")) continue; 113 | HashMap modelJson = new HashMap<>(); 114 | 115 | HashMap modelContentsJson = new HashMap<>(); 116 | modelContentsJson.put("type", "minecraft:model"); 117 | modelContentsJson.put("model", "freeminecraftmodels:" + boneBlueprint.getBoneName().split(":")[1]); 118 | 119 | modelJson.put("model", modelContentsJson); 120 | 121 | Gson gson = new Gson(); 122 | try { 123 | FileUtils.writeStringToFile( 124 | new File(MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + 125 | File.separatorChar + "output" + 126 | File.separatorChar + "FreeMinecraftModels" + 127 | File.separatorChar + "assets" + 128 | File.separatorChar + "freeminecraftmodels" + 129 | File.separatorChar + "items" + 130 | File.separatorChar + boneBlueprint.getBoneName().split(":")[1] + ".json"), 131 | gson.toJson(modelJson), StandardCharsets.UTF_8); 132 | } catch (Exception e) { 133 | e.printStackTrace(); 134 | } 135 | } 136 | } 137 | } 138 | 139 | } 140 | 141 | private static void processFiles(File childFile, 142 | List bbModelConverterList, 143 | HashMap leatherHorseArmor) { 144 | try { 145 | FileModelConverter bbModelConverter = new FileModelConverter(childFile); 146 | bbModelConverterList.add(bbModelConverter); 147 | for (BoneBlueprint boneBlueprint : bbModelConverter.getSkeletonBlueprint().getMainModel()) 148 | if (!boneBlueprint.getBoneName().equals("hitbox") && 149 | !boneBlueprint.getBoneName().equals("tag_name") && 150 | !boneBlueprint.getBoneName().equals("freeminecraftmodels_autogenerated_root")) 151 | assignBoneModelID(leatherHorseArmor, boneBlueprint); 152 | } catch (Exception e) { 153 | Logger.warn("Failed to parse model " + childFile.getName() + "! Warn the developer about this"); 154 | e.printStackTrace(); 155 | } 156 | } 157 | 158 | private static void processFolders(File file, 159 | List bbModelConverterList, 160 | HashMap leatherHorseArmor, 161 | boolean firstLevel) { 162 | if (!firstLevel) folderCounter++; 163 | File[] modelFiles = file.listFiles(); 164 | Arrays.sort(modelFiles, new Comparator() { 165 | @Override 166 | public int compare(File o1, File o2) { 167 | return o1.getName().compareTo(o2.getName()); 168 | } 169 | }); 170 | 171 | for (File childFile : modelFiles) { 172 | if (childFile.isFile()) processFiles(childFile, bbModelConverterList, leatherHorseArmor); 173 | else processFolders(childFile, bbModelConverterList, leatherHorseArmor, false); 174 | } 175 | } 176 | 177 | private static void assignBoneModelID(HashMap ironHorseArmorFile, BoneBlueprint boneBlueprint) { 178 | Map entryMap = new HashMap<>(); 179 | entryMap.put("predicate", Collections.singletonMap("custom_model_data", counter + folderCounter * 1000)); 180 | if (!boneBlueprint.getCubeBlueprintChildren().isEmpty()) { 181 | if (VersionChecker.serverVersionOlderThan(21, 4)) 182 | boneBlueprint.setModelID(counter + folderCounter * 1000 + ""); 183 | else 184 | boneBlueprint.setModelID(boneBlueprint.getBoneName()); 185 | counter++; 186 | } 187 | entryMap.put("model", boneBlueprint.getBoneName()); 188 | ironHorseArmorFile.computeIfAbsent("overrides", k -> new ArrayList>()); 189 | List> existingList = ((List>) ironHorseArmorFile.get("overrides")); 190 | existingList.add(entryMap); 191 | ironHorseArmorFile.put("overrides", existingList); 192 | if (!boneBlueprint.getBoneBlueprintChildren().isEmpty()) 193 | for (BoneBlueprint childBoneBlueprint : boneBlueprint.getBoneBlueprintChildren()) 194 | assignBoneModelID(ironHorseArmorFile, childBoneBlueprint); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/config/OutputFolder.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.config; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import com.magmaguy.magmacore.util.Logger; 5 | import com.magmaguy.magmacore.util.ZipFile; 6 | import org.apache.commons.io.FileUtils; 7 | 8 | import java.io.File; 9 | import java.io.InputStream; 10 | import java.nio.file.Files; 11 | import java.nio.file.StandardCopyOption; 12 | 13 | public class OutputFolder { 14 | private OutputFolder() { 15 | } 16 | 17 | public static void initializeConfig() { 18 | String path = MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath(); 19 | String baseDirectory = path + File.separatorChar + "output"; 20 | File mainFolder = new File(baseDirectory); 21 | try { 22 | if (mainFolder.exists()) FileUtils.deleteDirectory(mainFolder); 23 | } catch (Exception e) { 24 | Logger.warn("Failed to delete folder " + mainFolder.getAbsolutePath()); 25 | } 26 | mainFolder.mkdir(); 27 | generateDirectory(baseDirectory + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "assets" + File.separatorChar + "freeminecraftmodels" + File.separatorChar + "textures"); 28 | generateDirectory(baseDirectory + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "assets" + File.separatorChar + "freeminecraftmodels" + File.separatorChar + "models"); 29 | generateDirectory(baseDirectory + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "assets" + File.separatorChar + "minecraft" + File.separatorChar + "atlases"); 30 | generateFileFromResources("pack.mcmeta", baseDirectory + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "pack.mcmeta"); 31 | generateFileFromResources("pack.png", baseDirectory + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "pack.png"); 32 | generateFileFromResources("blocks.json", baseDirectory + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "assets" + File.separatorChar + "minecraft" + File.separatorChar + "atlases" + File.separatorChar + "blocks.json"); 33 | } 34 | 35 | public static void zipResourcePack() { 36 | ZipFile.zip( 37 | new File(MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + File.separatorChar + "output" + File.separatorChar + "FreeMinecraftModels"), 38 | MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + File.separatorChar + "output" + File.separatorChar + "FreeMinecraftModels.zip"); 39 | } 40 | 41 | private static void generateFileFromResources(String filename, String destination) { 42 | try { 43 | InputStream inputStream = MetadataHandler.PLUGIN.getResource(filename); 44 | File newFile = new File(destination); 45 | newFile.mkdirs(); 46 | if (!newFile.exists()) newFile.createNewFile(); 47 | // Copy the InputStream to the file 48 | Files.copy(inputStream, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING); 49 | } catch (Exception e) { 50 | Logger.warn("Failed to generate default resource pack elements"); 51 | e.printStackTrace(); 52 | } 53 | } 54 | 55 | private static void generateDirectory(String path) { 56 | File file = new File(path); 57 | file.mkdirs(); 58 | file.mkdir(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/config/props/PropsConfig.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.config.props; 2 | 3 | import com.magmaguy.magmacore.config.ConfigurationEngine; 4 | import com.magmaguy.magmacore.config.CustomConfig; 5 | import com.magmaguy.magmacore.config.CustomConfigFields; 6 | import com.magmaguy.magmacore.util.Logger; 7 | import lombok.Getter; 8 | import org.bukkit.configuration.file.FileConfiguration; 9 | import org.bukkit.entity.Player; 10 | 11 | import java.io.File; 12 | import java.util.HashMap; 13 | 14 | public class PropsConfig extends CustomConfig { 15 | public static PropsConfig INSTANCE; 16 | @Getter 17 | private static HashMap propsConfigs = new HashMap<>(); 18 | 19 | public PropsConfig() { 20 | super("props", "com.magmaguy.freeminecraftmodels.config.props.premade", PropsConfigFields.class); 21 | INSTANCE = this; 22 | propsConfigs = new HashMap<>(); 23 | for (String key : super.getCustomConfigFieldsHashMap().keySet()) 24 | if (super.getCustomConfigFieldsHashMap().get(key).isEnabled()) 25 | propsConfigs.put(key, (PropsConfigFields) super.getCustomConfigFieldsHashMap().get(key)); 26 | } 27 | 28 | public static PropsConfigFields addPropConfigurationFile(String propFilename, Player player) { 29 | if (!propsConfigs.containsKey(propFilename)) { 30 | PropsConfigFields newProp = new PropsConfigFields(propFilename, true); 31 | propsConfigs.put(propFilename, newProp); 32 | INSTANCE.initialize(newProp); 33 | 34 | // Only show this message if a new config was created 35 | Logger.sendMessage(player, "Created new prop config file at ~/plugins/FreeMinecraftModels/props/" + propFilename + ".yml"); 36 | return newProp; 37 | } else { 38 | // If the config already exists, inform the user where they can edit the values 39 | Logger.sendMessage(player, "Using existing prop config. You can edit properties at ~/plugins/FreeMinecraftModels/props/" + propFilename + ".yml"); 40 | return propsConfigs.get(propFilename); 41 | } 42 | } 43 | 44 | private void initialize(CustomConfigFields customConfigFields) { 45 | //Create configuration file from defaults if it does not exist 46 | File file = ConfigurationEngine.fileCreator("props", customConfigFields.getFilename()); 47 | //Get config file 48 | FileConfiguration fileConfiguration = ConfigurationEngine.fileConfigurationCreator(file); 49 | 50 | //Associate config 51 | customConfigFields.setFile(file); 52 | customConfigFields.setFileConfiguration(fileConfiguration); 53 | 54 | //Parse actual fields and load into RAM to be used 55 | customConfigFields.processConfigFields(); 56 | 57 | //Save all configuration values as they exist 58 | ConfigurationEngine.fileSaverCustomValues(fileConfiguration, file); 59 | 60 | //if (customConfigFields.isEnabled) 61 | //Store for use by the plugin 62 | addCustomConfigFields(file.getName(), customConfigFields); 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/config/props/PropsConfigFields.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.config.props; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.PropEntity; 4 | import com.magmaguy.magmacore.config.CustomConfigFields; 5 | import lombok.Getter; 6 | import org.bukkit.Location; 7 | 8 | import java.util.Objects; 9 | 10 | public class PropsConfigFields extends CustomConfigFields { 11 | 12 | @Getter 13 | private boolean onlyRemovableByOPs = true; 14 | 15 | /** 16 | * Used by plugin-generated files (defaults) 17 | * 18 | * @param filename 19 | * @param isEnabled 20 | */ 21 | public PropsConfigFields(String filename, boolean isEnabled) { 22 | super(filename, isEnabled); 23 | } 24 | 25 | @Override 26 | public void processConfigFields() { 27 | onlyRemovableByOPs = processBoolean("onlyRemovableByOPs", onlyRemovableByOPs, onlyRemovableByOPs, false); 28 | } 29 | 30 | public void permanentlyAddLocation(Location location) { 31 | spawnPropEntity(location); 32 | } 33 | 34 | public void spawnPropEntity(Location location) { 35 | // Pass 'this' as the configuration to the PropEntity 36 | PropEntity.spawnPropEntity(filename.replace(".yml", ""), Objects.requireNonNull(location), this); 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/DynamicEntity.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity; 2 | 3 | import com.magmaguy.easyminecraftgoals.NMSManager; 4 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 5 | import com.magmaguy.freeminecraftmodels.api.DynamicEntityHitboxContactEvent; 6 | import com.magmaguy.freeminecraftmodels.api.DynamicEntityLeftClickEvent; 7 | import com.magmaguy.freeminecraftmodels.api.DynamicEntityRightClickEvent; 8 | import com.magmaguy.freeminecraftmodels.customentity.core.ModeledEntityInterface; 9 | import com.magmaguy.freeminecraftmodels.customentity.core.OBBHitDetection; 10 | import com.magmaguy.freeminecraftmodels.customentity.core.RegisterModelEntity; 11 | import com.magmaguy.freeminecraftmodels.dataconverter.FileModelConverter; 12 | import com.magmaguy.magmacore.util.AttributeManager; 13 | import com.magmaguy.magmacore.util.Logger; 14 | import lombok.Getter; 15 | import lombok.Setter; 16 | import org.bukkit.Bukkit; 17 | import org.bukkit.Location; 18 | import org.bukkit.NamespacedKey; 19 | import org.bukkit.World; 20 | import org.bukkit.entity.LivingEntity; 21 | import org.bukkit.entity.Player; 22 | import org.bukkit.event.EventHandler; 23 | import org.bukkit.event.Listener; 24 | import org.bukkit.event.entity.EntityDeathEvent; 25 | import org.bukkit.persistence.PersistentDataType; 26 | import org.checkerframework.checker.nullness.qual.Nullable; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | public class DynamicEntity extends ModeledEntity implements ModeledEntityInterface { 32 | @Getter 33 | private static final List dynamicEntities = new ArrayList<>(); 34 | private static final NamespacedKey namespacedKey = new NamespacedKey(MetadataHandler.PLUGIN, "DynamicEntity"); 35 | @Getter 36 | private final String name = "default"; 37 | int counter = 0; 38 | boolean oneTimeDamageWarning = false; 39 | // Contact damage detection is integrated into the entity's internal clock 40 | // Contact damage properties 41 | @Getter 42 | @Setter 43 | private boolean damagesOnContact = false; 44 | @Getter 45 | private int customDamage = 1; 46 | 47 | public DynamicEntity(String entityID, Location targetLocation) { 48 | super(entityID, targetLocation); 49 | setCollisionDetectionEnabled(true); 50 | dynamicEntities.add(this); 51 | super.getSkeleton().setDynamicEntity(this); 52 | } 53 | 54 | public static boolean isDynamicEntity(LivingEntity livingEntity) { 55 | if (livingEntity == null) return false; 56 | return livingEntity.getPersistentDataContainer().has(namespacedKey, PersistentDataType.BYTE); 57 | } 58 | 59 | public static DynamicEntity getDynamicEntity(LivingEntity livingEntity) { 60 | for (DynamicEntity dynamicEntity : dynamicEntities) 61 | if (dynamicEntity.getLivingEntity().equals(livingEntity)) 62 | return dynamicEntity; 63 | return null; 64 | } 65 | 66 | //safer since it can return null 67 | @Nullable 68 | public static DynamicEntity create(String entityID, LivingEntity livingEntity) { 69 | FileModelConverter fileModelConverter = FileModelConverter.getConvertedFileModels().get(entityID); 70 | if (fileModelConverter == null) return null; 71 | DynamicEntity dynamicEntity = new DynamicEntity(entityID, livingEntity.getLocation()); 72 | dynamicEntity.spawn(livingEntity); 73 | livingEntity.setVisibleByDefault(false); 74 | Bukkit.getOnlinePlayers().forEach(player -> { 75 | if (player.getLocation().getWorld().equals(dynamicEntity.getLocation().getWorld())) { 76 | player.hideEntity(MetadataHandler.PLUGIN, livingEntity); 77 | } 78 | }); 79 | 80 | livingEntity.getPersistentDataContainer().set(namespacedKey, PersistentDataType.BYTE, (byte) 0); 81 | return dynamicEntity; 82 | } 83 | 84 | @Override 85 | protected void shutdownRemove() { 86 | remove(); 87 | } 88 | 89 | /** 90 | * This value only gets used if damagesOnContact is set to true and the entity doesn ot have an attack damage attribute 91 | * 92 | * @param customDamage 93 | */ 94 | public void setCustomDamage(int customDamage) { 95 | this.customDamage = customDamage; 96 | } 97 | 98 | public void spawn(LivingEntity entity) { 99 | super.livingEntity = entity; 100 | RegisterModelEntity.registerModelEntity(entity, getSkeletonBlueprint().getModelName()); 101 | super.spawn(); 102 | syncSkeletonWithEntity(); 103 | setHitbox(); 104 | getObbHitbox().setAssociatedEntity(this); 105 | } 106 | 107 | @Override 108 | public void tick() { 109 | syncSkeletonWithEntity(); 110 | super.tick(); 111 | } 112 | 113 | private void syncSkeletonWithEntity() { 114 | if (isDying()) return; 115 | 116 | if ((livingEntity == null || !livingEntity.isValid())) { 117 | remove(); 118 | return; 119 | } 120 | 121 | // Update skeleton position and rotation 122 | getSkeleton().setCurrentLocation(getBodyLocation()); 123 | getSkeleton().setCurrentHeadPitch(livingEntity.getEyeLocation().getPitch()); 124 | getSkeleton().setCurrentHeadYaw(livingEntity.getEyeLocation().getYaw()); 125 | 126 | counter++; 127 | } 128 | 129 | @Override 130 | public void remove() { 131 | super.remove(); 132 | if (livingEntity != null) 133 | livingEntity.remove(); 134 | dynamicEntities.remove(this); 135 | } 136 | 137 | private void setHitbox() { 138 | if (getSkeletonBlueprint().getHitbox() == null) return; 139 | NMSManager.getAdapter().setCustomHitbox(super.livingEntity, getSkeletonBlueprint().getHitbox().getWidthX() < getSkeletonBlueprint().getHitbox().getWidthZ() ? (float) getSkeletonBlueprint().getHitbox().getWidthX() : (float) getSkeletonBlueprint().getHitbox().getWidthZ(), (float) getSkeletonBlueprint().getHitbox().getHeight(), true); 140 | } 141 | 142 | @Override 143 | public void damageByLivingEntity(LivingEntity damagerLivingEntity, double damage) { 144 | if (this.livingEntity == null) return; 145 | OBBHitDetection.applyDamage = true; 146 | livingEntity.damage(damage, damagerLivingEntity); 147 | OBBHitDetection.applyDamage = false; 148 | getSkeleton().tint(); 149 | if (!this.livingEntity.isValid()) removeWithDeathAnimation(); 150 | } 151 | 152 | @Override 153 | public void damageByLivingEntity(LivingEntity damagerLivingEntity) { 154 | if (damagerLivingEntity == null) return; 155 | OBBHitDetection.applyDamage = true; 156 | if (AttributeManager.getAttribute("generic_attack_damage") != null) 157 | damagerLivingEntity.attack(livingEntity); 158 | else 159 | //this should not be happening and hopefully if it does some other plugin will override it 160 | livingEntity.damage(2, damagerLivingEntity); 161 | OBBHitDetection.applyDamage = false; 162 | getSkeleton().tint(); 163 | if (!damagerLivingEntity.isValid()) removeWithDeathAnimation(); 164 | } 165 | 166 | @Override 167 | public World getWorld() { 168 | if (livingEntity == null || !livingEntity.isValid()) return null; 169 | return livingEntity.getWorld(); 170 | } 171 | 172 | @Override 173 | public Location getLocation() { 174 | if (livingEntity == null) return null; 175 | return livingEntity.getLocation(); 176 | } 177 | 178 | /** 179 | * Gets the location of the model's body, clamped so its yaw never 180 | * lags the head yaw by more than ±45°. 181 | *

182 | * Note: this is done because the body rotation is handled by the client after the caching of the body rotation one the living entity stops moving around. 183 | * This is not a 1:1 recreation of the body rotation, as Minecraft entities have a further behavior that lerps the body rotation to the position they are looking at if they stare at it for about 1 second. 184 | * That lerping behavior would be unnecessarily demanding, and the is considered to be close enough for 99.99% of cases. 185 | */ 186 | public Location getBodyLocation() { 187 | Location bodyLoc = livingEntity.getLocation().clone(); 188 | 189 | // current body yaw (what Minecraft thinks the body is doing) 190 | float bodyYaw = NMSManager.getAdapter().getBodyRotation(livingEntity); 191 | // actual head yaw 192 | float headYaw = livingEntity.getEyeLocation().getYaw(); 193 | 194 | // compute signed difference in range –180…+180 195 | float delta = wrapDegrees(headYaw - bodyYaw); 196 | 197 | // clamp delta to –45…+45 198 | if (delta > 45) delta = 45; 199 | if (delta < -45) delta = -45; 200 | 201 | // new body yaw is headYaw minus that clamped delta 202 | float newBodyYaw = headYaw - delta; 203 | 204 | bodyLoc.setYaw(newBodyYaw); 205 | bodyLoc.setPitch(0); // assume no body pitch 206 | return bodyLoc; 207 | } 208 | 209 | /** 210 | * Wrap an angle in degrees to the range –180…+180. 211 | */ 212 | private float wrapDegrees(float angle) { 213 | angle %= 360; 214 | if (angle >= 180) angle -= 360; 215 | if (angle < -180) angle += 360; 216 | return angle; 217 | } 218 | 219 | @Override 220 | protected void updateHitbox() { 221 | getObbHitbox().update(getBodyLocation()); 222 | } 223 | 224 | @Override 225 | public void triggerLeftClickEvent(Player player) { 226 | super.triggerLeftClickEvent(player); 227 | DynamicEntityLeftClickEvent event = new DynamicEntityLeftClickEvent(player, this); 228 | Bukkit.getPluginManager().callEvent(event); 229 | } 230 | 231 | @Override 232 | public void triggerRightClickEvent(Player player) { 233 | super.triggerRightClickEvent(player); 234 | DynamicEntityRightClickEvent event = new DynamicEntityRightClickEvent(player, this); 235 | Bukkit.getPluginManager().callEvent(event); 236 | } 237 | 238 | @Override 239 | protected void handlePlayerCollision(Player player) { 240 | if (livingEntity.getAttribute(AttributeManager.getAttribute("generic_attack_damage")) != null) 241 | livingEntity.attack(player); 242 | else { 243 | //This is not ideal, the underlying entity should have a damage attribute 244 | player.damage(customDamage, livingEntity); 245 | if (!oneTimeDamageWarning) { 246 | Logger.info("Damaged player " + player.getName() + " for " + customDamage + " damage using custom damage value!"); 247 | oneTimeDamageWarning = true; 248 | } 249 | } 250 | } 251 | 252 | @Override 253 | protected void triggerHitboxContactEvent(Player player) { 254 | DynamicEntityHitboxContactEvent event = new DynamicEntityHitboxContactEvent(player, this); 255 | Bukkit.getPluginManager().callEvent(event); 256 | } 257 | 258 | public static class ModeledEntityEvents implements Listener { 259 | @EventHandler 260 | public void onEntityDeath(EntityDeathEvent event) { 261 | DynamicEntity dynamicEntity = DynamicEntity.getDynamicEntity(event.getEntity()); 262 | if (dynamicEntity == null) return; 263 | dynamicEntity.removeWithDeathAnimation(); 264 | } 265 | } 266 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/ModeledEntitiesClock.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import org.bukkit.scheduler.BukkitRunnable; 5 | import org.bukkit.scheduler.BukkitTask; 6 | 7 | import java.util.ArrayList; 8 | 9 | public class ModeledEntitiesClock { 10 | private static BukkitTask clock = null; 11 | 12 | private ModeledEntitiesClock() { 13 | } 14 | 15 | public static void start() { 16 | clock = new BukkitRunnable() { 17 | @Override 18 | public void run() { 19 | tick(); 20 | } 21 | }.runTaskTimer(MetadataHandler.PLUGIN, 0, 1); 22 | } 23 | 24 | public static void shutdown() { 25 | clock.cancel(); 26 | } 27 | 28 | public static void tick() { 29 | // Create a copy of the collection before iterating 30 | new ArrayList<>(ModeledEntity.getLoadedModeledEntities()).forEach(ModeledEntity::tick); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/ModeledEntityEvents.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity; 2 | 3 | import org.bukkit.event.Listener; 4 | 5 | public class ModeledEntityEvents implements Listener { 6 | //todo: this solution was actually just planned for static models, and isn't functional yet, dynamic models can wander 7 | // private static final ArrayListMultimap loadedModeledEntities = ArrayListMultimap.create(); 8 | // private static final ArrayListMultimap unloadedModeledEntities = ArrayListMultimap.create(); 9 | // 10 | // public static void addLoadedModeledEntity(ModeledEntity modeledEntity) { 11 | // loadedModeledEntities.put(modeledEntity.getChunkHash(), modeledEntity); 12 | // } 13 | // 14 | // public static void addUnloadedModeledEntity(ModeledEntity modeledEntity) { 15 | // unloadedModeledEntities.put(modeledEntity.getChunkHash(), modeledEntity); 16 | // } 17 | // 18 | // public static void removeLoadedModeledEntity(ModeledEntity modeledEntity) { 19 | // loadedModeledEntities.remove(modeledEntity.getChunkHash(), modeledEntity); 20 | // } 21 | // public static void removeUnloadedModeledEntity(ModeledEntity modeledEntity){ 22 | // unloadedModeledEntities.remove(modeledEntity.getChunkHash(), modeledEntity); 23 | // } 24 | // 25 | // @EventHandler (ignoreCancelled = true, priority = EventPriority.HIGHEST) 26 | // public void ChunkLoadEvent(ChunkLoadEvent event){ 27 | // int chunkHash = ChunkHasher.hash(event.getChunk()); 28 | // List modeledEntities = unloadedModeledEntities.get(chunkHash); 29 | // if (modeledEntities == null) return; 30 | // unloadedModeledEntities.removeAll(chunkHash); 31 | // modeledEntities.forEach(ModeledEntity::loadChunk); 32 | // loadedModeledEntities.putAll(chunkHash, modeledEntities); 33 | // } 34 | // 35 | // @EventHandler (ignoreCancelled = true, priority = EventPriority.HIGHEST) 36 | // public void ChunkUnloadEvent (ChunkUnloadEvent event) { 37 | // int chunkHash = ChunkHasher.hash(event.getChunk()); 38 | // loadedModeledEntities.values().forEach(modeledEntity->{ 39 | // if (modeledEntity.chunkHash != null && chunkHash == modeledEntity.chunkHash) { 40 | // modeledEntity.unloadChunk(); 41 | // loadedModeledEntities.remove(chunkHash, modeledEntity); 42 | // unloadedModeledEntities.put(chunkHash, modeledEntity); 43 | // } 44 | // }); 45 | // } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/PropEntity.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import com.magmaguy.freeminecraftmodels.api.PropEntityHitboxContactEvent; 5 | import com.magmaguy.freeminecraftmodels.api.PropEntityLeftClickEvent; 6 | import com.magmaguy.freeminecraftmodels.api.PropEntityRightClickEvent; 7 | import com.magmaguy.freeminecraftmodels.config.props.PropsConfig; 8 | import com.magmaguy.freeminecraftmodels.config.props.PropsConfigFields; 9 | import com.magmaguy.magmacore.util.Logger; 10 | import org.bukkit.*; 11 | import org.bukkit.entity.*; 12 | import org.bukkit.event.EventHandler; 13 | import org.bukkit.event.Listener; 14 | import org.bukkit.event.player.PlayerInteractEntityEvent; 15 | import org.bukkit.event.world.ChunkLoadEvent; 16 | import org.bukkit.event.world.ChunkUnloadEvent; 17 | import org.bukkit.persistence.PersistentDataType; 18 | 19 | import java.util.HashMap; 20 | 21 | public class PropEntity extends StaticEntity { 22 | public static final NamespacedKey propNamespacedKey = new NamespacedKey(MetadataHandler.PLUGIN, "prop"); 23 | public static HashMap propEntities = new HashMap<>(); 24 | private ArmorStand armorStand; 25 | private PropsConfigFields propsConfigFields; 26 | private double health = 3; 27 | 28 | public PropEntity(String entityID, Location spawnLocation) { 29 | super(entityID, spawnLocation); 30 | propsConfigFields = PropsConfig.getPropsConfigs().get(entityID + ".yml"); 31 | if (propsConfigFields == null) { 32 | Logger.warn("Failed to initialize PropEntity: PropsConfigFields not found for entityID: " + entityID); 33 | return; 34 | } 35 | armorStand = (ArmorStand) spawnLocation.getWorld().spawn(spawnLocation, EntityType.ARMOR_STAND.getEntityClass(), entity -> { 36 | entity.setVisibleByDefault(false); 37 | entity.setGravity(false); 38 | entity.setInvulnerable(true); 39 | entity.setPersistent(true); 40 | entity.getPersistentDataContainer().set(propNamespacedKey, PersistentDataType.STRING, entityID); 41 | }); 42 | } 43 | 44 | public PropEntity(String entityID, ArmorStand armorStand) { 45 | super(entityID, armorStand.getLocation()); 46 | propsConfigFields = PropsConfig.getPropsConfigs().get(entityID + ".yml"); 47 | if (propsConfigFields == null) { 48 | Logger.warn("Failed to initialize PropEntity: PropsConfigFields not found for entityID: " + entityID); 49 | return; 50 | } 51 | this.armorStand = armorStand; 52 | } 53 | 54 | public static void onStartup() { 55 | for (World world : Bukkit.getWorlds()) { 56 | for (Chunk loadedChunk : world.getLoadedChunks()) { 57 | for (Entity entity : loadedChunk.getEntities()) { 58 | if (entity instanceof ArmorStand armorStand) { 59 | String propEntityID = getPropEntityID(armorStand); 60 | if (propEntityID == null) continue; 61 | spawnPropEntity(propEntityID, armorStand); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | public static PropEntity spawnPropEntity(String entityID, Location location, PropsConfigFields config) { 69 | PropEntity propEntity = new PropEntity(entityID, location); 70 | propEntity.propsConfigFields = config; 71 | propEntity.spawn(); 72 | return propEntity; 73 | } 74 | 75 | public static void spawnPropEntity(String entityID, Location spawnLocation) { 76 | PropEntity propEntity = new PropEntity(entityID, spawnLocation); 77 | propEntity.spawn(); 78 | } 79 | 80 | public static void spawnPropEntity(String entityID, ArmorStand armorStand) { 81 | PropEntity propEntity = new PropEntity(entityID, armorStand); 82 | propEntity.spawn(); 83 | } 84 | 85 | public static boolean isPropEntity(ArmorStand armorStand) { 86 | return armorStand.getPersistentDataContainer().has(propNamespacedKey, PersistentDataType.STRING); 87 | } 88 | 89 | public static String getPropEntityID(ArmorStand armorStand) { 90 | return armorStand.getPersistentDataContainer().get(propNamespacedKey, PersistentDataType.STRING); 91 | } 92 | 93 | @Override 94 | public void damageByLivingEntity(LivingEntity livingEntity) { 95 | if (propsConfigFields.isOnlyRemovableByOPs() && !livingEntity.isOp()) return; 96 | if (armorStand == null) { 97 | permanentlyRemove(); 98 | Logger.warn("Failed to damage PropEntity: ArmorStand is null!"); 99 | return; 100 | } 101 | health -= 1; 102 | getSkeleton().tint(); 103 | if (!armorStand.isValid() || health <= 0) permanentlyRemove(); 104 | } 105 | 106 | @Override 107 | public void damageByLivingEntity(LivingEntity livingEntity, double damage) { 108 | if (propsConfigFields.isOnlyRemovableByOPs() && !livingEntity.isOp()) return; 109 | if (armorStand == null) { 110 | permanentlyRemove(); 111 | Logger.warn("Failed to damage PropEntity: ArmorStand is null!"); 112 | return; 113 | } 114 | health -= 1; 115 | getSkeleton().tint(); 116 | if (!armorStand.isValid() || health <= 0) permanentlyRemove(); 117 | } 118 | 119 | @Override 120 | public void remove() { 121 | super.remove(); 122 | propEntities.remove(armorStand); 123 | } 124 | 125 | @Override 126 | protected void shutdownRemove() { 127 | remove(); 128 | } 129 | 130 | public void permanentlyRemove() { 131 | remove(); 132 | if (armorStand != null) armorStand.remove(); 133 | } 134 | 135 | @Override 136 | public void triggerLeftClickEvent(Player player) { 137 | super.triggerLeftClickEvent(player); 138 | PropEntityLeftClickEvent event = new PropEntityLeftClickEvent(player, this); 139 | Bukkit.getPluginManager().callEvent(event); 140 | } 141 | 142 | @Override 143 | public void triggerRightClickEvent(Player player) { 144 | super.triggerRightClickEvent(player); 145 | PropEntityRightClickEvent event = new PropEntityRightClickEvent(player, this); 146 | Bukkit.getPluginManager().callEvent(event); 147 | } 148 | 149 | @Override 150 | protected void triggerHitboxContactEvent(Player player) { 151 | PropEntityHitboxContactEvent event = new PropEntityHitboxContactEvent(player, this); 152 | Bukkit.getPluginManager().callEvent(event); 153 | } 154 | 155 | public static class PropEntityEvents implements Listener { 156 | @EventHandler 157 | public void onArmorStandInteract(PlayerInteractEntityEvent event) { 158 | if (event.getRightClicked() instanceof ArmorStand armorStand && isPropEntity(armorStand)) 159 | event.setCancelled(true); 160 | } 161 | 162 | @EventHandler 163 | public void onChunkLoadEvent(ChunkLoadEvent event) { 164 | for (Entity entity : event.getChunk().getEntities()) { 165 | if (entity instanceof ArmorStand armorStand) { 166 | String propEntityID = getPropEntityID(armorStand); 167 | if (propEntityID == null) continue; 168 | spawnPropEntity(propEntityID, armorStand); 169 | } 170 | } 171 | } 172 | 173 | @EventHandler 174 | private void onChunkUnloadEvent(ChunkUnloadEvent event) { 175 | for (Entity entity : event.getChunk().getEntities()) { 176 | if (entity instanceof ArmorStand armorStand) { 177 | PropEntity propEntity = propEntities.get(armorStand); 178 | if (propEntity == null) continue; 179 | propEntity.remove(); 180 | } 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/StaticEntity.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity; 2 | 3 | import com.magmaguy.freeminecraftmodels.api.StaticEntityHitboxContactEvent; 4 | import com.magmaguy.freeminecraftmodels.api.StaticEntityLeftClickEvent; 5 | import com.magmaguy.freeminecraftmodels.api.StaticEntityRightClickEvent; 6 | import com.magmaguy.freeminecraftmodels.customentity.core.ModeledEntityInterface; 7 | import com.magmaguy.freeminecraftmodels.dataconverter.FileModelConverter; 8 | import lombok.Getter; 9 | import org.bukkit.Bukkit; 10 | import org.bukkit.Location; 11 | import org.bukkit.World; 12 | import org.bukkit.entity.LivingEntity; 13 | import org.bukkit.entity.Player; 14 | import org.bukkit.util.BoundingBox; 15 | import org.bukkit.util.Vector; 16 | import org.checkerframework.checker.nullness.qual.Nullable; 17 | 18 | public class StaticEntity extends ModeledEntity implements ModeledEntityInterface { 19 | @Getter 20 | private final String name = "default"; 21 | @Getter 22 | private double health = 3; 23 | @Getter 24 | private BoundingBox hitbox = null; 25 | 26 | protected StaticEntity(String entityID, Location targetLocation) { 27 | super(entityID, targetLocation); 28 | } 29 | 30 | @Override 31 | protected void shutdownRemove() { 32 | remove(); 33 | } 34 | 35 | //safer since it can return null 36 | @Nullable 37 | public static StaticEntity create(String entityID, Location targetLocation) { 38 | FileModelConverter fileModelConverter = FileModelConverter.getConvertedFileModels().get(entityID); 39 | if (fileModelConverter == null) return null; 40 | StaticEntity staticEntity = new StaticEntity(entityID, targetLocation); 41 | staticEntity.spawn(); 42 | return staticEntity; 43 | } 44 | 45 | @Override 46 | public void spawn() { 47 | super.spawn(); 48 | if (getSkeletonBlueprint().getHitbox() != null) { 49 | double halfWidthX = getSkeletonBlueprint().getHitbox().getWidthX() / 2D; 50 | double halfWidthZ = getSkeletonBlueprint().getHitbox().getWidthZ() / 2D; 51 | double height = getSkeletonBlueprint().getHitbox().getHeight(); 52 | Vector modelOffset = getSkeletonBlueprint().getHitbox().getModelOffset(); 53 | Location hitboxLocation = getSpawnLocation().add(modelOffset); 54 | hitbox = new BoundingBox(hitboxLocation.getX() - halfWidthX, hitboxLocation.getY(), hitboxLocation.getZ() - halfWidthZ, 55 | hitboxLocation.getX() + halfWidthX, hitboxLocation.getY() + height, hitboxLocation.getZ() + halfWidthZ); 56 | } 57 | } 58 | 59 | @Override 60 | public void damageByLivingEntity(LivingEntity livingEntity, double damage) { 61 | //If the health is -1, then the entity is not meant to be damageable. 62 | health -= 1; 63 | if (health <= 0) remove(); 64 | else remove(); 65 | } 66 | 67 | @Override 68 | public void damageByLivingEntity(LivingEntity livingEntity) { 69 | health -= 1; 70 | if (health <= 0) remove(); 71 | else remove(); 72 | } 73 | 74 | @Override 75 | public World getWorld() { 76 | Location spawnLocation = getSpawnLocation(); 77 | if (spawnLocation == null) return null; 78 | return spawnLocation.getWorld(); 79 | } 80 | 81 | @Override 82 | public void tick() { 83 | super.tick(); 84 | } 85 | 86 | @Override 87 | public void triggerLeftClickEvent(Player player) { 88 | super.triggerLeftClickEvent(player); 89 | StaticEntityLeftClickEvent event = new StaticEntityLeftClickEvent(player, this); 90 | Bukkit.getPluginManager().callEvent(event); 91 | } 92 | 93 | @Override 94 | public void triggerRightClickEvent(Player player) { 95 | super.triggerRightClickEvent(player); 96 | StaticEntityRightClickEvent event = new StaticEntityRightClickEvent(player, this); 97 | Bukkit.getPluginManager().callEvent(event); 98 | } 99 | 100 | @Override 101 | protected void triggerHitboxContactEvent(Player player) { 102 | StaticEntityHitboxContactEvent event = new StaticEntityHitboxContactEvent(player, this); 103 | Bukkit.getPluginManager().callEvent(event); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/core/Bone.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity.core; 2 | 3 | import com.magmaguy.freeminecraftmodels.config.DefaultConfig; 4 | import com.magmaguy.freeminecraftmodels.dataconverter.BoneBlueprint; 5 | import com.magmaguy.freeminecraftmodels.thirdparty.BedrockChecker; 6 | import com.magmaguy.magmacore.util.VersionChecker; 7 | import lombok.Getter; 8 | import org.bukkit.Color; 9 | import org.bukkit.Location; 10 | import org.bukkit.Particle; 11 | import org.bukkit.entity.ArmorStand; 12 | import org.bukkit.entity.Player; 13 | import org.joml.Vector3f; 14 | 15 | import java.util.ArrayList; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.UUID; 19 | 20 | public class Bone { 21 | @Getter 22 | private final BoneBlueprint boneBlueprint; 23 | @Getter 24 | private final List boneChildren = new ArrayList<>(); 25 | @Getter 26 | private final Bone parent; 27 | @Getter 28 | private final Skeleton skeleton; 29 | @Getter 30 | private final BoneTransforms boneTransforms; 31 | @Getter 32 | private Vector3f animationTranslation = new Vector3f(); 33 | @Getter 34 | private Vector3f animationRotation = new Vector3f(); 35 | @Getter 36 | private float animationScale = -1; 37 | 38 | public Bone(BoneBlueprint boneBlueprint, Bone parent, Skeleton skeleton) { 39 | this.boneBlueprint = boneBlueprint; 40 | this.parent = parent; 41 | this.skeleton = skeleton; 42 | this.boneTransforms = new BoneTransforms(this, parent); 43 | for (BoneBlueprint child : boneBlueprint.getBoneBlueprintChildren()) 44 | boneChildren.add(new Bone(child, this, skeleton)); 45 | } 46 | 47 | public void updateAnimationTranslation(float x, float y, float z) { 48 | animationTranslation = new Vector3f(x, y, z); 49 | } 50 | 51 | public void updateAnimationRotation(double x, double y, double z) { 52 | animationRotation = new Vector3f((float) Math.toRadians(x), (float) Math.toRadians(y), (float) Math.toRadians(z)); 53 | } 54 | 55 | public void updateAnimationScale(float animationScale) { 56 | this.animationScale = animationScale; 57 | } 58 | 59 | //Note that several optimizations might be possible here, but that syncing with a base entity is necessary. 60 | public void transform() { 61 | boneTransforms.transform(); 62 | boneChildren.forEach(Bone::transform); 63 | skeleton.getSkeletonWatchers().sendPackets(this); 64 | } 65 | 66 | public void generateDisplay() { 67 | boneTransforms.generateDisplay(); 68 | boneChildren.forEach(Bone::generateDisplay); 69 | } 70 | 71 | public void setName(String name) { 72 | boneChildren.forEach(child -> child.setName(name)); 73 | } 74 | 75 | public void setNameVisible(boolean visible) { 76 | boneChildren.forEach(child -> child.setNameVisible(visible)); 77 | } 78 | 79 | public void getNametags(List nametags) { 80 | boneChildren.forEach(child -> child.getNametags(nametags)); 81 | } 82 | 83 | public void remove() { 84 | if (boneTransforms.getPacketArmorStandEntity() != null) boneTransforms.getPacketArmorStandEntity().remove(); 85 | if (boneTransforms.getPacketDisplayEntity() != null) boneTransforms.getPacketDisplayEntity().remove(); 86 | boneChildren.forEach(Bone::remove); 87 | } 88 | 89 | protected void getAllChildren(HashMap children) { 90 | boneChildren.forEach(child -> { 91 | children.put(child.getBoneBlueprint().getBoneName(), child); 92 | child.getAllChildren(children); 93 | }); 94 | } 95 | 96 | public void sendUpdatePacket() { 97 | boneTransforms.sendUpdatePacket(); 98 | } 99 | 100 | public void displayTo(Player player) { 101 | boolean isBedrock = BedrockChecker.isBedrock(player); 102 | if (isBedrock && DefaultConfig.sendCustomModelsToBedrockClients) return; 103 | if (boneTransforms.getPacketArmorStandEntity() != null && 104 | (!DefaultConfig.useDisplayEntitiesWhenPossible || 105 | isBedrock || 106 | VersionChecker.serverVersionOlderThan(19, 4))) 107 | boneTransforms.getPacketArmorStandEntity().displayTo(player.getUniqueId()); 108 | else if (boneTransforms.getPacketDisplayEntity() != null) 109 | boneTransforms.getPacketDisplayEntity().displayTo(player.getUniqueId()); 110 | } 111 | 112 | public void hideFrom(UUID playerUUID) { 113 | if (boneTransforms.getPacketArmorStandEntity() != null) 114 | boneTransforms.getPacketArmorStandEntity().hideFrom(playerUUID); 115 | if (boneTransforms.getPacketDisplayEntity() != null) 116 | boneTransforms.getPacketDisplayEntity().hideFrom(playerUUID); 117 | } 118 | 119 | public void setHorseLeatherArmorColor(Color color) { 120 | if (boneTransforms.getPacketArmorStandEntity() != null) 121 | boneTransforms.getPacketArmorStandEntity().setHorseLeatherArmorColor(color); 122 | if (boneTransforms.getPacketDisplayEntity() != null) 123 | boneTransforms.getPacketDisplayEntity().setHorseLeatherArmorColor(color); 124 | } 125 | 126 | public void spawnParticles(Particle particle, double speed) { 127 | Location boneLocation; 128 | if (boneTransforms.getPacketDisplayEntity() != null) { 129 | boneLocation = boneTransforms.getDisplayEntityTargetLocation(); 130 | if (boneLocation.getWorld() == null) return; 131 | boneLocation.getWorld().spawnParticle(particle, boneLocation, 1, 1, 1, 1, speed); 132 | } else if (boneTransforms.getPacketArmorStandEntity() != null) { 133 | boneLocation = boneTransforms.getArmorStandTargetLocation(); 134 | if (boneLocation.getWorld() == null) return; 135 | boneLocation.getWorld().spawnParticle(particle, boneLocation, 1, 1, 1, 1, speed); 136 | } 137 | } 138 | 139 | public void teleport() { 140 | sendTeleportPacket(); 141 | boneChildren.forEach(Bone::teleport); 142 | } 143 | 144 | private void sendTeleportPacket() { 145 | if (boneTransforms.getPacketArmorStandEntity() != null) { 146 | boneTransforms.getPacketArmorStandEntity().teleport(boneTransforms.getArmorStandTargetLocation()); 147 | } 148 | if (boneTransforms.getPacketDisplayEntity() != null) { 149 | boneTransforms.getPacketDisplayEntity().teleport(boneTransforms.getDisplayEntityTargetLocation()); 150 | } 151 | skeleton.getSkeletonWatchers().resync(true); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/core/BoneTransforms.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity.core; 2 | 3 | import com.magmaguy.easyminecraftgoals.NMSManager; 4 | import com.magmaguy.easyminecraftgoals.internal.PacketModelEntity; 5 | import com.magmaguy.freeminecraftmodels.config.DefaultConfig; 6 | import com.magmaguy.freeminecraftmodels.dataconverter.BoneBlueprint; 7 | import com.magmaguy.freeminecraftmodels.utils.TransformationMatrix; 8 | import com.magmaguy.magmacore.util.AttributeManager; 9 | import com.magmaguy.magmacore.util.VersionChecker; 10 | import lombok.Getter; 11 | import org.bukkit.Location; 12 | import org.bukkit.util.EulerAngle; 13 | import org.bukkit.util.Vector; 14 | import org.joml.Vector3f; 15 | 16 | public class BoneTransforms { 17 | 18 | private final Bone parent; 19 | private final Bone bone; 20 | private final TransformationMatrix localMatrix = new TransformationMatrix(); 21 | private TransformationMatrix globalMatrix = new TransformationMatrix(); 22 | @Getter 23 | private PacketModelEntity packetArmorStandEntity = null; 24 | @Getter 25 | private PacketModelEntity packetDisplayEntity = null; 26 | 27 | public BoneTransforms(Bone bone, Bone parent) { 28 | this.bone = bone; 29 | this.parent = parent; 30 | } 31 | 32 | public void transform() { 33 | updateLocalTransform(); 34 | updateGlobalTransform(); 35 | } 36 | 37 | public void updateGlobalTransform() { 38 | if (parent != null) { 39 | TransformationMatrix.multiplyMatrices(parent.getBoneTransforms().globalMatrix, localMatrix, globalMatrix); 40 | if (bone.getBoneBlueprint().isHead()) { 41 | // Store the inherited scale before resetting 42 | double[] inheritedScale = globalMatrix.getScale(); // or however you access scale in your matrix 43 | 44 | globalMatrix.resetRotation(); 45 | float yaw = -bone.getSkeleton().getCurrentHeadYaw() + 180; 46 | globalMatrix.rotateY((float) Math.toRadians(yaw)); 47 | globalMatrix.rotateX(-(float) Math.toRadians(bone.getSkeleton().getCurrentHeadPitch())); 48 | 49 | // Reapply the inherited scale 50 | globalMatrix.scale(inheritedScale[0], inheritedScale[1], inheritedScale[2]); 51 | } 52 | } else { 53 | globalMatrix = localMatrix; 54 | } 55 | } 56 | 57 | public void updateLocalTransform() { 58 | localMatrix.resetToIdentityMatrix(); 59 | shiftPivotPoint(); 60 | translateModelCenter(); 61 | translateAnimation(); 62 | rotateAnimation(); 63 | rotateDefaultBoneRotation(); 64 | scaleAnimation(); 65 | shiftPivotPointBack(); 66 | rotateByEntityYaw(); 67 | } 68 | 69 | private void scaleAnimation() { 70 | localMatrix.scale(getDisplayEntityScale() / 2.5f, getDisplayEntityScale() / 2.5f, getDisplayEntityScale() / 2.5f); 71 | } 72 | 73 | //Shift to model center 74 | private void translateModelCenter() { 75 | localMatrix.translateLocal(bone.getBoneBlueprint().getModelCenter()); 76 | 77 | //The bone is relative to its parent, so remove the offset of the parent 78 | if (parent != null) { 79 | Vector3f modelCenter = parent.getBoneBlueprint().getModelCenter(); 80 | modelCenter.mul(-1); 81 | localMatrix.translateLocal(modelCenter); 82 | } 83 | } 84 | 85 | private void shiftPivotPoint() { 86 | localMatrix.translateLocal(bone.getBoneBlueprint().getBlueprintModelPivot().mul(-1)); 87 | } 88 | 89 | private void translateAnimation() { 90 | localMatrix.translateLocal( 91 | -bone.getAnimationTranslation().get(0), 92 | bone.getAnimationTranslation().get(1), 93 | bone.getAnimationTranslation().get(2)); 94 | } 95 | 96 | private void rotateAnimation() { 97 | Vector test = new Vector(bone.getAnimationRotation().get(0), -bone.getAnimationRotation().get(1), -bone.getAnimationRotation().get(2)); 98 | test.rotateAroundY(Math.PI); 99 | localMatrix.rotateAnimation( 100 | (float) test.getX(), 101 | (float) test.getY(), 102 | (float) test.getZ()); 103 | } 104 | 105 | private void rotateDefaultBoneRotation() { 106 | localMatrix.rotateLocal( 107 | bone.getBoneBlueprint().getBlueprintOriginalBoneRotation().get(0), 108 | bone.getBoneBlueprint().getBlueprintOriginalBoneRotation().get(1), 109 | bone.getBoneBlueprint().getBlueprintOriginalBoneRotation().get(2)); 110 | } 111 | 112 | private void shiftPivotPointBack() { 113 | //Remove the pivot point, go back to the model center 114 | localMatrix.translateLocal(bone.getBoneBlueprint().getBlueprintModelPivot()); 115 | } 116 | 117 | public void generateDisplay() { 118 | transform(); 119 | if (bone.getBoneBlueprint().isDisplayModel()) { 120 | initializeDisplayEntityBone(); 121 | initializeArmorStandBone(); 122 | } 123 | } 124 | 125 | private void initializeDisplayEntityBone() { 126 | if (!DefaultConfig.useDisplayEntitiesWhenPossible) return; 127 | packetDisplayEntity = NMSManager.getAdapter().createPacketDisplayEntity(getDisplayEntityTargetLocation()); 128 | if (VersionChecker.serverVersionOlderThan(21, 4)) 129 | packetDisplayEntity.initializeModel(getDisplayEntityTargetLocation(), Integer.parseInt(bone.getBoneBlueprint().getModelID())); 130 | else { 131 | packetDisplayEntity.initializeModel(getDisplayEntityTargetLocation(), bone.getBoneBlueprint().getModelID()); 132 | } 133 | packetDisplayEntity.sendLocationAndRotationPacket(getDisplayEntityTargetLocation(), getDisplayEntityRotation()); 134 | } 135 | 136 | private void initializeArmorStandBone() { 137 | //todo: add way to disable armor stands later via config 138 | packetArmorStandEntity = NMSManager.getAdapter().createPacketArmorStandEntity(getArmorStandTargetLocation()); 139 | if (VersionChecker.serverVersionOlderThan(21, 4)) 140 | packetArmorStandEntity.initializeModel(getArmorStandTargetLocation(), Integer.parseInt(bone.getBoneBlueprint().getModelID())); 141 | else 142 | packetArmorStandEntity.initializeModel(getArmorStandTargetLocation(), bone.getBoneBlueprint().getModelID()); 143 | 144 | packetArmorStandEntity.sendLocationAndRotationPacket(getArmorStandTargetLocation(), getArmorStandEntityRotation()); 145 | } 146 | 147 | private void rotateByEntityYaw() { 148 | //rotate by yaw amount 149 | if (parent == null) { 150 | localMatrix.rotateLocal(0, (float) -Math.toRadians(bone.getSkeleton().getCurrentLocation().getYaw() + 180), 0); 151 | } 152 | } 153 | 154 | protected Location getArmorStandTargetLocation() { 155 | double[] translatedGlobalMatrix = globalMatrix.getTranslation(); 156 | Location armorStandLocation = new Location(bone.getSkeleton().getCurrentLocation().getWorld(), 157 | translatedGlobalMatrix[0], 158 | translatedGlobalMatrix[1], 159 | translatedGlobalMatrix[2]) 160 | .add(bone.getSkeleton().getCurrentLocation()); 161 | armorStandLocation.setYaw(180); 162 | armorStandLocation.subtract(new Vector(0, BoneBlueprint.getARMOR_STAND_PIVOT_POINT_HEIGHT(), 0)); 163 | return armorStandLocation; 164 | } 165 | 166 | protected Location getDisplayEntityTargetLocation() { 167 | double[] translatedGlobalMatrix = globalMatrix.getTranslation(); 168 | Location armorStandLocation; 169 | if (!VersionChecker.serverVersionOlderThan(20, 0)) { 170 | armorStandLocation = new Location(bone.getSkeleton().getCurrentLocation().getWorld(), 171 | translatedGlobalMatrix[0], 172 | translatedGlobalMatrix[1], 173 | translatedGlobalMatrix[2]) 174 | .add(bone.getSkeleton().getCurrentLocation()); 175 | armorStandLocation.setYaw(180); 176 | } else 177 | armorStandLocation = new Location(bone.getSkeleton().getCurrentLocation().getWorld(), 178 | translatedGlobalMatrix[0], 179 | translatedGlobalMatrix[1], 180 | translatedGlobalMatrix[2]) 181 | .add(bone.getSkeleton().getCurrentLocation()); 182 | return armorStandLocation; 183 | } 184 | 185 | protected EulerAngle getDisplayEntityRotation() { 186 | double[] rotation = globalMatrix.getRotation(); 187 | if (VersionChecker.serverVersionOlderThan(20, 0)) 188 | return new EulerAngle(rotation[0], rotation[1], rotation[2]); 189 | else { 190 | return new EulerAngle(-rotation[0], rotation[1], -rotation[2]); 191 | } 192 | } 193 | 194 | protected EulerAngle getArmorStandEntityRotation() { 195 | double[] rotation = globalMatrix.getRotation(); 196 | return new EulerAngle(-rotation[0], -rotation[1], rotation[2]); 197 | } 198 | 199 | public void sendUpdatePacket() { 200 | if (packetArmorStandEntity != null && packetArmorStandEntity.hasViewers()) 201 | sendArmorStandUpdatePacket(); 202 | if (packetDisplayEntity != null && packetDisplayEntity.hasViewers()) 203 | sendDisplayEntityUpdatePacket(); 204 | } 205 | 206 | private void sendArmorStandUpdatePacket() { 207 | if (packetArmorStandEntity != null) { 208 | packetArmorStandEntity.sendLocationAndRotationAndScalePacket( 209 | getArmorStandTargetLocation(), 210 | getArmorStandEntityRotation(), 211 | 1f); 212 | } 213 | } 214 | 215 | protected float getDisplayEntityScale() { 216 | float scale = bone.getAnimationScale() == -1 ? 2.5f : bone.getAnimationScale() * 2.5f; 217 | //Only the root bone/head should be scaling up globally like this, otherwise the scale will be inherited by each bone and then become progressively larger or smaller 218 | if (bone.getParent() == null) { 219 | double scaleModifier = bone.getSkeleton().getModeledEntity().getScaleModifier(); 220 | if (bone.getSkeleton().getModeledEntity().getLivingEntity() != null && bone.getSkeleton().getModeledEntity().getLivingEntity().getAttribute(AttributeManager.getAttribute("generic_scale")) != null) 221 | scaleModifier *= bone.getSkeleton().getModeledEntity().getLivingEntity().getAttribute(AttributeManager.getAttribute("generic_scale")).getValue(); 222 | scale *= (float) scaleModifier; 223 | } 224 | return scale; 225 | } 226 | 227 | 228 | private void sendDisplayEntityUpdatePacket() { 229 | if (packetDisplayEntity != null) { 230 | packetDisplayEntity.sendLocationAndRotationAndScalePacket(getDisplayEntityTargetLocation(), getDisplayEntityRotation(), (float) globalMatrix.getScale()[0] * 2.5f); 231 | } 232 | } 233 | 234 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/core/ModeledEntityInterface.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity.core; 2 | 3 | import org.bukkit.World; 4 | 5 | public interface ModeledEntityInterface { 6 | World getWorld(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/core/OBBHitDetection.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity.core; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import com.magmaguy.freeminecraftmodels.api.ModeledEntityManager; 5 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 6 | import org.bukkit.entity.Projectile; 7 | import org.bukkit.event.EventHandler; 8 | import org.bukkit.event.EventPriority; 9 | import org.bukkit.event.Listener; 10 | import org.bukkit.event.block.Action; 11 | import org.bukkit.event.block.BlockBreakEvent; 12 | import org.bukkit.event.entity.EntityDamageByEntityEvent; 13 | import org.bukkit.event.entity.ProjectileLaunchEvent; 14 | import org.bukkit.event.player.PlayerAnimationEvent; 15 | import org.bukkit.event.player.PlayerAnimationType; 16 | import org.bukkit.event.player.PlayerInteractEvent; 17 | import org.bukkit.scheduler.BukkitRunnable; 18 | import org.bukkit.scheduler.BukkitTask; 19 | 20 | import java.util.HashSet; 21 | import java.util.Iterator; 22 | import java.util.Optional; 23 | 24 | /** 25 | * Handles hit detection for modeled entities using Oriented Bounding Boxes. 26 | * This system can detect hits even when the model is rotated. 27 | */ 28 | public class OBBHitDetection implements Listener { 29 | 30 | public static boolean applyDamage = false; 31 | 32 | private static HashSet activeProjectiles = new HashSet<>(); 33 | private static BukkitTask projectileDetectionTask = null; 34 | 35 | @EventHandler(priority = EventPriority.LOWEST) 36 | public void EntityDamageByEntityEvent(EntityDamageByEntityEvent event) { 37 | if (!RegisterModelEntity.isModelEntity(event.getEntity()) && 38 | !RegisterModelEntity.isModelArmorStand(event.getEntity()) || 39 | !RegisterModelEntity.isModelEntity(event.getDamager()) && 40 | !RegisterModelEntity.isModelArmorStand(event.getDamager())) return; 41 | if (applyDamage) { 42 | applyDamage = false; 43 | return; 44 | } 45 | event.setCancelled(true); 46 | } 47 | 48 | @EventHandler(priority = EventPriority.HIGHEST) 49 | public void blockBreakEvent(BlockBreakEvent event) { 50 | // Get the block location and calculate distance to player 51 | double blockDistance = event.getPlayer().getEyeLocation().distance( 52 | event.getBlock().getLocation().add(0.5, 0.5, 0.5)); // Center of block 53 | 54 | // Check for hit entity 55 | Optional hitEntityOpt = OrientedBoundingBox.raytraceFromPlayer(event.getPlayer()); 56 | 57 | // If no entity was hit, allow the block break 58 | if (hitEntityOpt.isEmpty()) return; 59 | 60 | // Get the hit entity and calculate its distance 61 | ModeledEntity hitEntity = hitEntityOpt.get(); 62 | double entityDistance = event.getPlayer().getEyeLocation().distance(hitEntity.getLocation()); 63 | 64 | // Only cancel if the entity is closer than or at the same distance as the block 65 | if (entityDistance <= blockDistance) { 66 | event.setCancelled(true); 67 | } 68 | } 69 | 70 | public static void startProjectileDetection() { 71 | projectileDetectionTask = new BukkitRunnable() { 72 | @Override 73 | public void run() { 74 | Iterator iter = activeProjectiles.iterator(); 75 | while (iter.hasNext()) { 76 | Projectile proj = iter.next(); 77 | 78 | // 1) drop invalid projectiles 79 | if (!proj.isValid()) { 80 | iter.remove(); 81 | continue; 82 | } 83 | 84 | // 2) scan against every modeled entity in the same world 85 | for (ModeledEntity entity : ModeledEntityManager.getAllEntities()) { 86 | if (entity.getWorld() == null || 87 | !entity.getWorld().equals(proj.getWorld())) { 88 | continue; 89 | } 90 | 91 | // update the OBB to the entity's current position/orientation 92 | OrientedBoundingBox obb = entity.getObbHitbox().update(entity.getLocation()); 93 | if (obb.containsPoint(proj.getLocation())) { 94 | // hit! deal damage and stop scanning this projectile 95 | if (!entity.damageByProjectile(proj)) break; 96 | 97 | // remove it from our set so we don't double‐hit 98 | iter.remove(); 99 | proj.remove(); 100 | break; 101 | } 102 | } 103 | } 104 | } 105 | }.runTaskTimer(MetadataHandler.PLUGIN, 0L, 1L); 106 | } 107 | 108 | public static void shutdown() { 109 | activeProjectiles.clear(); 110 | projectileDetectionTask.cancel(); 111 | } 112 | 113 | /** 114 | * Handles player arm swing animations to detect attacks 115 | */ 116 | @EventHandler(priority = EventPriority.HIGHEST) 117 | public void playerAnimation(PlayerAnimationEvent event) { 118 | // Only process arm swings (attacks) 119 | if (!event.getAnimationType().equals(PlayerAnimationType.ARM_SWING)) { 120 | return; 121 | } 122 | 123 | // Check for hit entity 124 | Optional hitEntity = OrientedBoundingBox.raytraceFromPlayer(event.getPlayer()); 125 | 126 | // If no entity was hit, allow normal processing 127 | if (hitEntity.isEmpty()) { 128 | return; 129 | } 130 | 131 | // Process the hit 132 | event.setCancelled(true); 133 | hitEntity.get().triggerLeftClickEvent(event.getPlayer()); 134 | hitEntity.get().damageByLivingEntity(event.getPlayer()); 135 | } 136 | 137 | @EventHandler 138 | public void EntityInteractEvent(PlayerInteractEvent event) { 139 | // Only process right-click actions (both air and block) 140 | if (event.getAction() != Action.RIGHT_CLICK_AIR && 141 | event.getAction() != Action.RIGHT_CLICK_BLOCK) { 142 | return; 143 | } 144 | 145 | // Check for hit entity using raytrace 146 | Optional hitEntity = OrientedBoundingBox.raytraceFromPlayer(event.getPlayer()); 147 | 148 | // If no entity was hit, allow normal processing 149 | if (hitEntity.isEmpty()) { 150 | return; 151 | } 152 | 153 | // Cancel the event to prevent interaction with blocks behind the entity 154 | event.setCancelled(true); 155 | 156 | // Trigger the right-click event on the modeled entity 157 | hitEntity.get().triggerRightClickEvent(event.getPlayer()); 158 | } 159 | 160 | @EventHandler 161 | public void onProjectileCreate(ProjectileLaunchEvent event) { 162 | activeProjectiles.add(event.getEntity()); 163 | } 164 | 165 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/core/RegisterModelEntity.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity.core; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import org.bukkit.NamespacedKey; 5 | import org.bukkit.entity.ArmorStand; 6 | import org.bukkit.entity.Entity; 7 | import org.bukkit.persistence.PersistentDataType; 8 | 9 | public class RegisterModelEntity { 10 | public static final NamespacedKey ARMOR_STAND_KEY = new NamespacedKey(MetadataHandler.PLUGIN, "armor_stand"); 11 | public static final NamespacedKey ENTITY_KEY = new NamespacedKey(MetadataHandler.PLUGIN, "entity"); 12 | 13 | private RegisterModelEntity() { 14 | } 15 | 16 | public static void registerModelArmorStand(ArmorStand armorStand, String name) { 17 | armorStand.getPersistentDataContainer().set(ENTITY_KEY, PersistentDataType.STRING, name); 18 | } 19 | 20 | public static void registerModelEntity(Entity entity, String name) { 21 | entity.getPersistentDataContainer().set(ENTITY_KEY, PersistentDataType.STRING, name); 22 | } 23 | 24 | public static boolean isModelArmorStand(Entity entity) { 25 | return entity.getPersistentDataContainer().getKeys().contains(ARMOR_STAND_KEY); 26 | } 27 | 28 | public static boolean isModelEntity(Entity entity) { 29 | return entity.getPersistentDataContainer().getKeys().contains(ENTITY_KEY); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/core/Skeleton.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity.core; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 4 | import com.magmaguy.freeminecraftmodels.customentity.ModeledEntity; 5 | import com.magmaguy.freeminecraftmodels.dataconverter.BoneBlueprint; 6 | import com.magmaguy.freeminecraftmodels.dataconverter.SkeletonBlueprint; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import org.bukkit.Color; 10 | import org.bukkit.Location; 11 | import org.bukkit.Particle; 12 | import org.bukkit.entity.ArmorStand; 13 | import org.bukkit.scheduler.BukkitTask; 14 | 15 | import javax.annotation.Nullable; 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | 21 | public class Skeleton { 22 | 23 | @Getter 24 | private final List mainModel = new ArrayList<>(); 25 | //In BlockBench models are referred to by name for animations, and names are unique 26 | @Getter 27 | private final HashMap boneMap = new HashMap<>(); 28 | @Getter 29 | private final SkeletonBlueprint skeletonBlueprint; 30 | @Getter 31 | private final SkeletonWatchers skeletonWatchers; 32 | private final List nametags = new ArrayList<>(); 33 | @Setter 34 | private Location currentLocation = null; 35 | @Getter 36 | @Setter 37 | private float currentHeadPitch = 0; 38 | @Getter 39 | @Setter 40 | private float currentHeadYaw = 0; 41 | private BukkitTask damageTintTask = null; 42 | @Getter 43 | @Setter 44 | private DynamicEntity dynamicEntity = null; 45 | @Getter 46 | @Setter 47 | private ModeledEntity modeledEntity = null; 48 | private Bone rootBone = null; 49 | 50 | public Skeleton(SkeletonBlueprint skeletonBlueprint, ModeledEntity modeledEntity) { 51 | this.skeletonBlueprint = skeletonBlueprint; 52 | this.modeledEntity = modeledEntity; 53 | skeletonBlueprint.getBoneMap().forEach((key, value) -> { 54 | if (value.getParent() == null) { 55 | Bone bone = new Bone(value, null, this); 56 | boneMap.put(key, bone); 57 | bone.getAllChildren(boneMap); 58 | rootBone = bone; 59 | } 60 | }); 61 | skeletonWatchers = new SkeletonWatchers(this); 62 | } 63 | 64 | @Nullable 65 | public Location getCurrentLocation() { 66 | if (currentLocation == null) return null; 67 | return currentLocation.clone(); 68 | } 69 | 70 | public void generateDisplays(Location location) { 71 | currentLocation = location; 72 | rootBone.generateDisplay(); 73 | boneMap.values().forEach(bone -> { 74 | if (bone.getBoneBlueprint().isNameTag()) nametags.add(bone); 75 | }); 76 | } 77 | 78 | public void remove() { 79 | boneMap.values().forEach(Bone::remove); 80 | } 81 | 82 | /** 83 | * Used to set the name over nameable bones 84 | * 85 | * @param name The name to set over the bone 86 | */ 87 | public void setName(String name) { 88 | boneMap.values().forEach(bone -> bone.setName(name)); 89 | } 90 | 91 | /** 92 | * Used to make names over nameable bones visible 93 | * 94 | * @param visible Whether the name should be visible 95 | */ 96 | public void setNameVisible(boolean visible) { 97 | boneMap.values().forEach(bone -> bone.setNameVisible(visible)); 98 | } 99 | 100 | public List getNametags() { 101 | List nametags = new ArrayList<>(); 102 | boneMap.values().forEach(bone -> bone.getNametags(nametags)); 103 | return nametags; 104 | } 105 | 106 | /** 107 | * Returns the map of bones the Skeleton has 108 | * 109 | * @return 110 | */ 111 | public Collection getBones() { 112 | return boneMap.values(); 113 | } 114 | 115 | private boolean tinting = false; 116 | private int tintCounter = 0; 117 | 118 | /** 119 | * This updates animations. The plugin runs this automatically, don't use it unless you know what you're doing! 120 | */ 121 | public void transform() { 122 | skeletonWatchers.tick(); 123 | 124 | // handle tint animation 125 | if (tinting) { 126 | tintCounter++; 127 | 128 | if (tintCounter <= 10) { 129 | // ramp from red back toward white 130 | int gAndB = (int) (255 / (double) tintCounter); 131 | Color tint = Color.fromRGB(255, gAndB, gAndB); 132 | boneMap.values().forEach(b -> b.setHorseLeatherArmorColor(tint)); 133 | } else { 134 | // after frame 10, either keep poofing (if dying) or finish 135 | if (!modeledEntity.isDying()) { 136 | // done 137 | tinting = false; 138 | boneMap.values().forEach(b -> b.setHorseLeatherArmorColor(Color.WHITE)); 139 | } else if (modeledEntity.isRemoved()) { 140 | // entity gone, cancel 141 | tinting = false; 142 | } else { 143 | // still dying: emit poofs every 5 ticks 144 | if (tintCounter % 5 == 0) { 145 | boneMap.values().forEach(b -> b.spawnParticles(Particle.POOF, .1)); 146 | } 147 | } 148 | } 149 | } 150 | 151 | if (getSkeletonWatchers().hasObservers()) { 152 | rootBone.transform(); 153 | } 154 | } 155 | 156 | public void tint() { 157 | // start (or restart) the tint animation 158 | tinting = true; 159 | tintCounter = 0; 160 | } 161 | 162 | public void teleport(Location location) { 163 | currentLocation = location; 164 | rootBone.teleport(); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/customentity/core/SkeletonWatchers.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.customentity.core; 2 | 3 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 4 | import com.magmaguy.freeminecraftmodels.config.DefaultConfig; 5 | import com.magmaguy.freeminecraftmodels.thirdparty.BedrockChecker; 6 | import org.bukkit.Bukkit; 7 | import org.bukkit.FluidCollisionMode; 8 | import org.bukkit.Location; 9 | import org.bukkit.entity.Player; 10 | import org.bukkit.event.Listener; 11 | import org.bukkit.scheduler.BukkitRunnable; 12 | import org.bukkit.util.Vector; 13 | import org.joml.Vector3d; 14 | 15 | import java.util.*; 16 | import java.util.concurrent.CopyOnWriteArraySet; 17 | import java.util.concurrent.ThreadLocalRandom; 18 | 19 | public class SkeletonWatchers implements Listener { 20 | private final Skeleton skeleton; 21 | private final Set viewers = new CopyOnWriteArraySet<>(); 22 | 23 | // Reused collections to avoid constant reallocation 24 | private final List newPlayers = new ArrayList<>(); 25 | private final List toRemove = new ArrayList<>(); 26 | private final int resetTimer = 20 * 60; 27 | private int counter = ThreadLocalRandom.current().nextInt(20 * 60); 28 | 29 | public SkeletonWatchers(Skeleton skeleton) { 30 | this.skeleton = skeleton; 31 | tick(); 32 | } 33 | private boolean updateWatchers = true; 34 | 35 | public boolean hasObservers() { 36 | return !viewers.isEmpty(); 37 | } 38 | 39 | public void tick() { 40 | if (updateWatchers) { 41 | updateWatcherList(); 42 | updateWatchers = false; 43 | new BukkitRunnable() { 44 | @Override 45 | public void run() { 46 | updateWatchers = true; 47 | } 48 | }.runTaskLater(MetadataHandler.PLUGIN, 4); 49 | } 50 | resync(false); 51 | } 52 | 53 | private volatile long lastResyncTime = 0L; 54 | 55 | // Clients gets a bit of drift due to some inaccuracies, this resyncs the skeleton 56 | public void resync(boolean force) { 57 | long now = System.currentTimeMillis(); 58 | 59 | // throttle: if not forced and we ran <1s ago, skip entirely 60 | if (!force && now - lastResyncTime < 1_000) { 61 | return; 62 | } 63 | 64 | counter++; 65 | // your existing random / timer logic 66 | if (force || (counter > resetTimer && ThreadLocalRandom.current().nextBoolean())) { 67 | // update timestamp and reset counter 68 | lastResyncTime = now; 69 | counter = 0; 70 | 71 | // do the actual hide/display 72 | Set tempViewers = Collections.synchronizedSet(new HashSet<>(viewers)); 73 | tempViewers.forEach(viewer -> { 74 | hideFrom(viewer); 75 | Player p = Bukkit.getPlayer(viewer); 76 | if (p != null) { 77 | displayTo(p); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | private void updateWatcherList() { 84 | if (skeleton.getCurrentLocation() == null) return; 85 | 86 | // Clear reused collections instead of creating new ones 87 | newPlayers.clear(); 88 | toRemove.clear(); 89 | 90 | double sightCheckDistanceMin = Math.pow(20, 2); 91 | double maxViewDistanceSquared = Math.pow(DefaultConfig.maxModelViewDistance, 2); 92 | 93 | for (Player player : skeleton.getCurrentLocation().getWorld().getPlayers()) { 94 | double distance = player.getLocation().distanceSquared(skeleton.getCurrentLocation()); 95 | 96 | if (distance < sightCheckDistanceMin || 97 | distance < maxViewDistanceSquared && isModelInSight(player)) { 98 | newPlayers.add(player.getUniqueId()); 99 | if (!viewers.contains(player.getUniqueId())) displayTo(player); 100 | } 101 | } 102 | 103 | for (UUID viewer : viewers) { 104 | if (!newPlayers.contains(viewer)) { 105 | toRemove.add(viewer); 106 | } 107 | } 108 | 109 | toRemove.forEach(viewers::remove); 110 | toRemove.forEach(this::hideFrom); 111 | } 112 | 113 | /** 114 | * Checks if any part of the skeleton model is in the player's line of sight. 115 | * Tests the center and strategic corners of the bounding box, going from top to bottom. 116 | * 117 | * @param player the player to check for 118 | * @return true if any part of the entity is visible 119 | */ 120 | private boolean isModelInSight(Player player) { 121 | // Quick sanity checks 122 | if (skeleton.getModeledEntity() == null) return true; 123 | 124 | // Get the entity's hitbox 125 | OrientedBoundingBox hitbox = skeleton.getModeledEntity().getObbHitbox(); 126 | if (hitbox == null) return true; 127 | 128 | // First try the center point (most efficient check) 129 | Vector centerPoint = skeleton.getCurrentLocation().toVector(); 130 | if (isPointVisible(player, centerPoint)) { 131 | return true; 132 | } 133 | 134 | // If center isn't visible, check key points of the bounding box 135 | Vector3d[] corners = hitbox.getCorners(); 136 | 137 | // Check every other corner, prioritizing top to bottom 138 | // OBB corner layout: 139 | // 0: top front right, 1: top back right (top corners) 140 | // 4: top front left, 5: top back left (top corners) 141 | // 2: bottom front right, 3: bottom back right (bottom corners) 142 | // 6: bottom front left, 7: bottom back left (bottom corners) 143 | 144 | // Check top corners first (0, 4) - one from each side, skipping every other one 145 | if (isPointVisible(player, new Vector(corners[0].x, corners[0].y, corners[0].z))) { 146 | return true; 147 | } 148 | if (isPointVisible(player, new Vector(corners[4].x, corners[4].y, corners[4].z))) { 149 | return true; 150 | } 151 | 152 | // Then check bottom corners (2, 6) - one from each side, maintaining top-to-bottom order 153 | if (isPointVisible(player, new Vector(corners[2].x, corners[2].y, corners[2].z))) { 154 | return true; 155 | } 156 | return isPointVisible(player, new Vector(corners[6].x, corners[6].y, corners[6].z)); 157 | 158 | // No points were visible 159 | } 160 | 161 | /** 162 | * Helper method to check if a specific point is visible to the player, 163 | * with recursive handling of non-occluding blocks 164 | */ 165 | private boolean isPointVisible(Player player, Vector point) { 166 | return isPointVisibleRecursive(player.getEyeLocation(), point, 5); // Max 5 passes through non-occluding blocks 167 | } 168 | 169 | private boolean isPointVisibleRecursive(Location eyeLocation, Vector targetPoint, int remainingPasses) { 170 | if (remainingPasses <= 0) { 171 | return false; // Prevent infinite recursion 172 | } 173 | 174 | Vector toPoint = targetPoint.clone().subtract(eyeLocation.toVector()); 175 | double distance = toPoint.length(); 176 | toPoint.normalize(); 177 | 178 | var result = eyeLocation.getWorld().rayTraceBlocks( 179 | eyeLocation, 180 | toPoint, 181 | distance, 182 | FluidCollisionMode.NEVER, 183 | true 184 | ); 185 | 186 | // No block was hit, clear line of sight 187 | if (result == null) { 188 | return true; 189 | } 190 | 191 | // A block was hit, check if it's occluding or non-occluding 192 | if (result.getHitBlock() != null) { 193 | if (result.getHitBlock().getType().isOccluding()) { 194 | // Occluding block (like stone) blocks vision 195 | return false; 196 | } else { 197 | // Non-occluding block (like glass), continue tracing through it 198 | // Create a new starting point past this block 199 | 200 | // Get hit position and normalize our direction vector 201 | Location hitPos = result.getHitPosition().toLocation(eyeLocation.getWorld()); 202 | Vector normalizedDirection = toPoint.clone().normalize(); 203 | 204 | // Use a larger offset to ensure we move past the block 205 | // 0.1 blocks (1/10th of a block) should be sufficient 206 | Location nextLocation = hitPos.clone().add( 207 | normalizedDirection.clone().multiply(2) 208 | ); 209 | 210 | // Continue the ray trace 211 | return isPointVisibleRecursive(nextLocation, targetPoint, remainingPasses - 1); 212 | } 213 | } 214 | 215 | // Something else was hit (should rarely happen) 216 | return false; 217 | } 218 | 219 | private void displayTo(Player player) { 220 | boolean isBedrock = BedrockChecker.isBedrock(player); 221 | if (isBedrock && !DefaultConfig.sendCustomModelsToBedrockClients && skeleton.getModeledEntity().getLivingEntity() != null) 222 | player.showEntity(MetadataHandler.PLUGIN, skeleton.getModeledEntity().getLivingEntity()); 223 | viewers.add(player.getUniqueId()); 224 | skeleton.getBones().forEach(bone -> bone.displayTo(player)); 225 | } 226 | 227 | private void hideFrom(UUID uuid) { 228 | boolean isBedrock = BedrockChecker.isBedrock(Bukkit.getPlayer(uuid)); 229 | if (isBedrock && !DefaultConfig.sendCustomModelsToBedrockClients && skeleton.getModeledEntity().getLivingEntity() != null) 230 | Bukkit.getPlayer(uuid).hideEntity(MetadataHandler.PLUGIN, skeleton.getModeledEntity().getLivingEntity()); 231 | viewers.remove(uuid); 232 | skeleton.getBones().forEach(bone -> bone.hideFrom(uuid)); 233 | } 234 | 235 | public void sendPackets(Bone bone) { 236 | if (viewers.isEmpty()) return; 237 | bone.sendUpdatePacket(); 238 | } 239 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/AnimationBlueprint.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import com.magmaguy.freeminecraftmodels.utils.LoopType; 4 | import com.magmaguy.magmacore.util.Logger; 5 | import lombok.Getter; 6 | 7 | import java.util.*; 8 | 9 | public class AnimationBlueprint { 10 | @Getter 11 | private final HashMap> boneKeyframes = new HashMap<>(); 12 | @Getter 13 | private final HashMap animationFrames = new HashMap<>(); 14 | @Getter 15 | private LoopType loopType; 16 | @Getter 17 | private String animationName; 18 | private SkeletonBlueprint skeletonBlueprint; 19 | @Getter 20 | private int duration; 21 | 22 | public AnimationBlueprint(Object data, String modelName, SkeletonBlueprint skeletonBlueprint) { 23 | Map animationData; 24 | try { 25 | animationData = (Map) data; 26 | } catch (Exception e) { 27 | Logger.warn("Failed to get animation data! Model format is not as expected, this version of BlockBench is not compatible with FreeMinecraftModels!"); 28 | e.printStackTrace(); 29 | return; 30 | } 31 | 32 | this.skeletonBlueprint = skeletonBlueprint; 33 | initializeGlobalValues(animationData); 34 | 35 | if (animationData.get("animators") == null) return; 36 | //In BBModel files, each bone holds the data for their transformations, so data is stored from the bone's perspective 37 | ((Map) animationData.get("animators")).entrySet().forEach(pair -> initializeBones((Map) pair.getValue(), modelName, animationName)); 38 | 39 | //Process the keyframes 40 | try { 41 | interpolateKeyframes(); 42 | } catch (Exception e) { 43 | Logger.warn("Failed to interpolate animations for model " + modelName + "! Animation name: " + animationName); 44 | e.printStackTrace(); 45 | } 46 | } 47 | 48 | public static float lerp(float start, float end, float t) { 49 | return (1 - t) * start + t * end; 50 | } 51 | 52 | private void initializeGlobalValues(Map animationData) { 53 | //Parse global data for animation 54 | animationName = (String) animationData.get("name"); 55 | loopType = LoopType.valueOf(((String) animationData.get("loop")).toUpperCase()); 56 | duration = (int) (20 * (Double) animationData.get("length")); 57 | } 58 | 59 | private void initializeBones(Map animationData, String modelName, String animationName) { 60 | String boneName = (String) animationData.get("name"); 61 | BoneBlueprint boneBlueprint = skeletonBlueprint.getBoneMap().get(boneName); 62 | //hitboxes do not get animated! 63 | if (boneName.equalsIgnoreCase("hitbox")) return; 64 | if (boneBlueprint == null) { 65 | Logger.warn("Failed to get bone " + boneName + " from model " + modelName + "!"); 66 | return; 67 | } 68 | List keyframes = new ArrayList<>(); 69 | for (Object keyframeData : (List) animationData.get("keyframes")) { 70 | keyframes.add(new Keyframe(keyframeData, modelName, animationName)); 71 | } 72 | keyframes.sort(Comparator.comparingInt(Keyframe::getTimeInTicks)); 73 | boneKeyframes.put(boneBlueprint, keyframes); 74 | } 75 | 76 | private void interpolateKeyframes() { 77 | boneKeyframes.forEach(this::interpolateBoneKeyframes); 78 | } 79 | 80 | private void interpolateBoneKeyframes(BoneBlueprint boneBlueprint, List keyframes) { 81 | List rotationKeyframes = new ArrayList<>(); 82 | List positionKeyframes = new ArrayList<>(); 83 | List scaleKeyframes = new ArrayList<>(); 84 | for (Keyframe keyframe : keyframes) { 85 | switch (keyframe.getTransformationType()) { 86 | case ROTATION -> rotationKeyframes.add(keyframe); 87 | case POSITION -> positionKeyframes.add(keyframe); 88 | case SCALE -> scaleKeyframes.add(keyframe); 89 | } 90 | } 91 | 92 | AnimationFrame[] animationFramesArray = new AnimationFrame[duration]; 93 | for (int i = 0; i < animationFramesArray.length; i++) 94 | animationFramesArray[i] = new AnimationFrame(); 95 | 96 | //Interpolation time 97 | interpolateRotations(animationFramesArray, rotationKeyframes); 98 | interpolateTranslations(animationFramesArray, positionKeyframes); 99 | interpolateScales(animationFramesArray, scaleKeyframes); 100 | 101 | this.animationFrames.put(boneBlueprint, animationFramesArray); 102 | } 103 | 104 | private void interpolateRotations(AnimationFrame[] animationFramesArray, List rotationKeyframes) { 105 | Keyframe firstFrame = null; 106 | Keyframe previousFrame = null; 107 | Keyframe lastFrame = null; 108 | for (int i = 0; i < rotationKeyframes.size(); i++) { 109 | Keyframe animationFrame = rotationKeyframes.get(i); 110 | if (i == 0) { 111 | firstFrame = animationFrame; 112 | previousFrame = animationFrame; 113 | lastFrame = animationFrame; 114 | continue; 115 | } 116 | //It is possible for frames to go beyond the animation's duration, so we need to clamp that 117 | if (previousFrame.getTimeInTicks() >= duration) return; 118 | int durationBetweenKeyframes = Math.min(animationFrame.getTimeInTicks(), duration) - previousFrame.getTimeInTicks(); 119 | for (int j = 0; j < durationBetweenKeyframes; j++) { 120 | int currentFrame = j + previousFrame.getTimeInTicks(); 121 | animationFramesArray[currentFrame].xRotation = lerp(previousFrame.getDataX(), animationFrame.getDataX(), j / (float) durationBetweenKeyframes); 122 | animationFramesArray[currentFrame].yRotation = lerp(previousFrame.getDataY(), animationFrame.getDataY(), j / (float) durationBetweenKeyframes); 123 | animationFramesArray[currentFrame].zRotation = lerp(previousFrame.getDataZ(), animationFrame.getDataZ(), j / (float) durationBetweenKeyframes); 124 | } 125 | previousFrame = animationFrame; 126 | if (animationFrame.getTimeInTicks() > lastFrame.getTimeInTicks()) lastFrame = animationFrame; 127 | if (animationFrame.getTimeInTicks() < firstFrame.getTimeInTicks()) firstFrame = animationFrame; 128 | } 129 | if (lastFrame != null && lastFrame.getTimeInTicks() < duration - 1) { 130 | int durationBetweenKeyframes = duration - lastFrame.getTimeInTicks(); 131 | for (int j = 0; j < durationBetweenKeyframes; j++) { 132 | int currentFrame = j + previousFrame.getTimeInTicks(); 133 | animationFramesArray[currentFrame].xRotation = lastFrame.getDataX(); 134 | animationFramesArray[currentFrame].yRotation = lastFrame.getDataY(); 135 | animationFramesArray[currentFrame].zRotation = lastFrame.getDataZ(); 136 | } 137 | } 138 | if (firstFrame != null && firstFrame.getTimeInTicks() > 0) { 139 | int durationBetweenKeyframes = firstFrame.getTimeInTicks(); 140 | durationBetweenKeyframes = Math.min(durationBetweenKeyframes, duration - 1); 141 | for (int j = 0; j < durationBetweenKeyframes; j++) { 142 | animationFramesArray[j].xRotation = firstFrame.getDataX(); 143 | animationFramesArray[j].yRotation = firstFrame.getDataY(); 144 | animationFramesArray[j].zRotation = firstFrame.getDataZ(); 145 | } 146 | } 147 | } 148 | 149 | private void interpolateTranslations(AnimationFrame[] animationFramesArray, List positionKeyframes) { 150 | Keyframe firstFrame = null; 151 | Keyframe previousFrame = null; 152 | Keyframe lastFrame = null; 153 | for (int i = 0; i < positionKeyframes.size(); i++) { 154 | Keyframe animationFrame = positionKeyframes.get(i); 155 | if (i == 0) { 156 | firstFrame = animationFrame; 157 | previousFrame = animationFrame; 158 | lastFrame = animationFrame; 159 | continue; 160 | } 161 | int durationBetweenKeyframes = animationFrame.getTimeInTicks() - previousFrame.getTimeInTicks(); 162 | for (int j = 0; j < durationBetweenKeyframes; j++) { 163 | int currentFrame = j + previousFrame.getTimeInTicks(); 164 | animationFramesArray[currentFrame].xPosition = lerp(previousFrame.getDataX(), animationFrame.getDataX(), j / (float) durationBetweenKeyframes) / 16f; 165 | animationFramesArray[currentFrame].yPosition = lerp(previousFrame.getDataY(), animationFrame.getDataY(), j / (float) durationBetweenKeyframes) / 16f; 166 | animationFramesArray[currentFrame].zPosition = lerp(previousFrame.getDataZ(), animationFrame.getDataZ(), j / (float) durationBetweenKeyframes) / 16f; 167 | } 168 | previousFrame = animationFrame; 169 | if (animationFrame.getTimeInTicks() > lastFrame.getTimeInTicks()) lastFrame = animationFrame; 170 | if (animationFrame.getTimeInTicks() < firstFrame.getTimeInTicks()) firstFrame = animationFrame; 171 | } 172 | if (lastFrame != null && lastFrame.getTimeInTicks() < duration - 1) { 173 | int durationBetweenKeyframes = duration - lastFrame.getTimeInTicks(); 174 | for (int j = 0; j < durationBetweenKeyframes; j++) { 175 | int currentFrame = j + previousFrame.getTimeInTicks(); 176 | animationFramesArray[currentFrame].xPosition = lastFrame.getDataX() / 16f; 177 | animationFramesArray[currentFrame].yPosition = lastFrame.getDataY() / 16f; 178 | animationFramesArray[currentFrame].zPosition = lastFrame.getDataZ() / 16f; 179 | } 180 | } 181 | if (firstFrame != null && firstFrame.getTimeInTicks() > 0) { 182 | int durationBetweenKeyframes = firstFrame.getTimeInTicks(); 183 | durationBetweenKeyframes = Math.min(durationBetweenKeyframes, duration - 1); 184 | for (int j = 0; j < durationBetweenKeyframes; j++) { 185 | animationFramesArray[j].xPosition = firstFrame.getDataX() / 16f; 186 | animationFramesArray[j].yPosition = firstFrame.getDataY() / 16f; 187 | animationFramesArray[j].zPosition = firstFrame.getDataZ() / 16f; 188 | } 189 | } 190 | } 191 | 192 | //todo: Scale currently does nothing, will change soon 193 | private void interpolateScales(AnimationFrame[] animationFramesArray, List scaleKeyframes) { 194 | Keyframe previousFrame = null; 195 | for (int i = 0; i < scaleKeyframes.size(); i++) { 196 | Keyframe animationFrame = scaleKeyframes.get(i); 197 | if (i == 0) { 198 | previousFrame = animationFrame; 199 | continue; 200 | } 201 | int durationBetweenKeyframes = animationFrame.getTimeInTicks() - previousFrame.getTimeInTicks(); 202 | for (int j = 0; j < durationBetweenKeyframes; j++) { 203 | int currentFrame = j + previousFrame.getTimeInTicks(); 204 | animationFramesArray[currentFrame].scale = lerp(previousFrame.getDataX(), animationFrame.getDataX(), j / (float) durationBetweenKeyframes); //note: probably needs a multiplier here depending on implementation 205 | } 206 | previousFrame = animationFrame; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/AnimationFrame.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | public class AnimationFrame { 4 | public float xRotation; 5 | public float yRotation; 6 | public float zRotation; 7 | public float xPosition; 8 | public float yPosition; 9 | public float zPosition; 10 | public Float scale = null; 11 | 12 | public AnimationFrame(){ 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/AnimationsBlueprint.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.HashMap; 6 | import java.util.List; 7 | 8 | public class AnimationsBlueprint { 9 | @Getter 10 | private final HashMap animations = new HashMap<>(); 11 | 12 | public AnimationsBlueprint(List rawAnimationData, String modelName, SkeletonBlueprint skeletonBlueprint) { 13 | for (Object animation : rawAnimationData) { 14 | AnimationBlueprint animationBlueprintObject =new AnimationBlueprint(animation, modelName, skeletonBlueprint); 15 | animations.put(animationBlueprintObject.getAnimationName(), animationBlueprintObject); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/CubeBlueprint.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import com.magmaguy.magmacore.util.Logger; 4 | import com.magmaguy.magmacore.util.Round; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import org.joml.Vector3f; 8 | 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | public class CubeBlueprint { 15 | @Getter 16 | private final Map cubeJSON; 17 | @Getter 18 | private Vector3f to; 19 | @Getter 20 | private Vector3f from; 21 | @Getter 22 | private boolean validatedData = false; 23 | @Getter 24 | @Setter 25 | private Vector3f boneOffset = new Vector3f(); 26 | //Fun bug, if a single face does not have a texture but the rest of the cube does it breaks Minecraft. 27 | //Null means uninitialized. False means initialized with no texture. True means initialized with a texture. 28 | private Boolean textureDataExists = null; 29 | 30 | public CubeBlueprint(double projectResolution, Map cubeJSON, String modelName) { 31 | this.cubeJSON = cubeJSON; 32 | //Sanitize data from ModelEngine which is not used by Minecraft resource packs 33 | cubeJSON.remove("rescale"); 34 | cubeJSON.remove("locked"); 35 | cubeJSON.remove("type"); 36 | cubeJSON.remove("uuid"); 37 | cubeJSON.remove("color"); 38 | cubeJSON.remove("autouv"); 39 | cubeJSON.remove("name"); 40 | cubeJSON.remove("box_uv"); 41 | cubeJSON.remove("render_order"); 42 | cubeJSON.remove("allow_mirror_modeling"); 43 | //process face textures 44 | processFace(projectResolution, (Map) cubeJSON.get("faces"), "north", modelName); 45 | processFace(projectResolution, (Map) cubeJSON.get("faces"), "east", modelName); 46 | processFace(projectResolution, (Map) cubeJSON.get("faces"), "south", modelName); 47 | processFace(projectResolution, (Map) cubeJSON.get("faces"), "west", modelName); 48 | processFace(projectResolution, (Map) cubeJSON.get("faces"), "up", modelName); 49 | processFace(projectResolution, (Map) cubeJSON.get("faces"), "down", modelName); 50 | 51 | //The model is scaled up 4x to reach the maximum theoretical size for large models, thus needs to be scaled correctly here 52 | //Note that how much it is scaled relies on the scaling of the head slot, it's somewhat arbitrary and just 53 | //works out that this is the right amount to get the right final size. 54 | ArrayList fromList = (ArrayList) cubeJSON.get("from"); 55 | if (fromList == null) return; 56 | from = new Vector3f( 57 | Round.fourDecimalPlaces(fromList.get(0).floatValue() * BoneBlueprint.ARMOR_STAND_HEAD_SIZE_MULTIPLIER), 58 | Round.fourDecimalPlaces(fromList.get(1).floatValue() * BoneBlueprint.ARMOR_STAND_HEAD_SIZE_MULTIPLIER), 59 | Round.fourDecimalPlaces(fromList.get(2).floatValue() * BoneBlueprint.ARMOR_STAND_HEAD_SIZE_MULTIPLIER)); 60 | ArrayList toList = (ArrayList) cubeJSON.get("to"); 61 | if (toList == null) return; 62 | to = new Vector3f( 63 | Round.fourDecimalPlaces(toList.get(0).floatValue() * BoneBlueprint.ARMOR_STAND_HEAD_SIZE_MULTIPLIER), 64 | Round.fourDecimalPlaces(toList.get(1).floatValue() * BoneBlueprint.ARMOR_STAND_HEAD_SIZE_MULTIPLIER), 65 | Round.fourDecimalPlaces(toList.get(2).floatValue() * BoneBlueprint.ARMOR_STAND_HEAD_SIZE_MULTIPLIER)); 66 | validatedData = true; 67 | } 68 | 69 | private void processFace(double projectResolution, Map map, String faceName, String modelName) { 70 | setTextureData(projectResolution, (Map) map.get(faceName), modelName); 71 | } 72 | 73 | private void setTextureData(double projectResolution, Map map, String modelName) { 74 | if (map == null || map.get("texture") == null) { 75 | if (textureDataExists != null && textureDataExists) 76 | Logger.warn("A cube in the model " + modelName + " has a face which does not have a texture while the rest of the cube has a texture. Minecraft does not allow this. Go through every cube in that model and make sure they all either have or do not have textures on all faces, but don't mix having and not having textures for the same cube. The model will appear with the debug black and purple cube texture until fixed."); 77 | textureDataExists = false; 78 | return; 79 | } 80 | if (textureDataExists != null && !textureDataExists) 81 | Logger.warn("A cube in the model " + modelName + " has a face which does not have a texture while the rest of the cube has a texture. Minecraft does not allow this. Go through every cube in that model and make sure they all either have or do not have textures on all faces, but don't mix having and not having textures for the same cube. The model will appear with the debug black and purple cube texture until fixed."); 82 | textureDataExists = true; 83 | Double textureDouble = (Double) map.get("texture"); 84 | int textureValue = (int) Math.round(textureDouble); 85 | map.put("texture", "#" + textureValue); 86 | map.put("tintindex", 0); 87 | map.put("rotation", 0); 88 | ArrayList originalUV = (ArrayList) map.get("uv"); 89 | //For some reason Minecraft really wants images to be 16x16 so here we scale the UV to fit that 90 | double uvMultiplier = 16 / projectResolution; 91 | map.put("uv", List.of( 92 | Round.fourDecimalPlaces(originalUV.get(0) * uvMultiplier), 93 | Round.fourDecimalPlaces(originalUV.get(1) * uvMultiplier), 94 | Round.fourDecimalPlaces(originalUV.get(2) * uvMultiplier), 95 | Round.fourDecimalPlaces(originalUV.get(3) * uvMultiplier))); 96 | } 97 | 98 | public void shiftPosition() { 99 | from.sub(boneOffset); 100 | to.sub(boneOffset); 101 | cubeJSON.put("from", List.of(from.get(0), from.get(1), from.get(2))); 102 | cubeJSON.put("to", List.of(to.get(0), to.get(1), to.get(2))); 103 | } 104 | 105 | public void shiftRotation() { 106 | if (cubeJSON.get("origin") == null) return; 107 | Map newRotationData = new HashMap<>(); 108 | 109 | double scaleFactor = 0.4; 110 | 111 | //Adjust the origin 112 | double xOrigin, yOrigin, zOrigin; 113 | List originData = (ArrayList) cubeJSON.get("origin"); 114 | xOrigin = originData.get(0) * scaleFactor - boneOffset.get(0); 115 | yOrigin = originData.get(1) * scaleFactor - boneOffset.get(1); 116 | zOrigin = originData.get(2) * scaleFactor - boneOffset.get(2); 117 | newRotationData.put("origin", List.of(xOrigin, yOrigin, zOrigin)); 118 | 119 | double angle = 0; 120 | String axis = "x"; 121 | if (cubeJSON.get("rotation") != null) { 122 | List rotations = (List) cubeJSON.get("rotation"); 123 | for (int i = rotations.size() - 1; i >= 0; i--) { 124 | if (rotations.get(i) != 0) { 125 | angle = Round.fourDecimalPlaces(rotations.get(i)); 126 | switch (i) { 127 | case 0 -> axis = "x"; 128 | case 1 -> axis = "y"; 129 | case 2 -> axis = "z"; 130 | default -> Logger.warn("Unexpected amount of rotation axes!"); 131 | } 132 | } 133 | } 134 | } 135 | 136 | newRotationData.put("angle", angle); 137 | newRotationData.put("axis", axis); 138 | cubeJSON.put("rotation", newRotationData); 139 | cubeJSON.remove("origin"); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/FileModelConverter.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import com.google.gson.Gson; 4 | import com.magmaguy.freeminecraftmodels.MetadataHandler; 5 | import com.magmaguy.freeminecraftmodels.utils.StringToResourcePackFilename; 6 | import com.magmaguy.magmacore.util.Logger; 7 | import lombok.Getter; 8 | import org.apache.commons.io.FileUtils; 9 | import org.bukkit.Bukkit; 10 | import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; 11 | 12 | import javax.imageio.ImageIO; 13 | import java.awt.image.BufferedImage; 14 | import java.io.ByteArrayInputStream; 15 | import java.io.File; 16 | import java.io.Reader; 17 | import java.nio.file.Files; 18 | import java.nio.file.Paths; 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | public class FileModelConverter { 25 | 26 | @Getter 27 | private static final HashMap convertedFileModels = new HashMap<>(); 28 | @Getter 29 | private static final HashMap imageSize = new HashMap<>(); 30 | private final HashMap values = new HashMap<>(); 31 | private final HashMap outliner = new HashMap<>(); 32 | //Store the texture with the identifier and the name of the texture file 33 | private final HashMap textures = new HashMap<>(); 34 | private String modelName; 35 | @Getter 36 | private SkeletonBlueprint skeletonBlueprint; 37 | @Getter 38 | private AnimationsBlueprint animationsBlueprint = null; 39 | @Getter 40 | private String ID; 41 | 42 | /** 43 | * In this instance, the file is the raw bbmodel file which is actually in a JSON format 44 | * 45 | * @param file bbmodel file to parse 46 | */ 47 | public FileModelConverter(File file) { 48 | if (file.getName().contains(".bbmodel")) modelName = file.getName().replace(".bbmodel", ""); 49 | else if (file.getName().contains(".fmmodel")) modelName = file.getName().replace(".fmmodel", ""); 50 | else { 51 | Bukkit.getLogger().warning("File " + file.getName() + " should not be in the models folder!"); 52 | return; 53 | } 54 | 55 | modelName = StringToResourcePackFilename.convert(modelName); 56 | 57 | Gson gson = new Gson(); 58 | 59 | Reader reader; 60 | // create a reader 61 | try { 62 | reader = Files.newBufferedReader(Paths.get(file.getPath())); 63 | } catch (Exception ex) { 64 | Logger.warn("Failed to read file " + file.getAbsolutePath()); 65 | return; 66 | } 67 | 68 | // convert JSON file to map 69 | Map map = gson.fromJson(reader, Map.class); 70 | 71 | /* Just for debugging, this is very spammy 72 | // print map entries 73 | for (Map.Entry entry : map.entrySet()) { 74 | System.out.println(entry.getKey() + "=" + entry.getValue()); 75 | } 76 | */ 77 | 78 | // close reader 79 | try { 80 | reader.close(); 81 | } catch (Exception exception) { 82 | Logger.warn("Failed to close reader for file!"); 83 | return; 84 | } 85 | 86 | double projectResolution = (double) ((Map) map.get("resolution")).get("height"); 87 | 88 | //This parses the textures, extracts them to the correct directory and stores their values for the bone texture references 89 | List> texturesValues = (ArrayList>) map.get("textures"); 90 | for (int i = 0; i < texturesValues.size(); i++) { 91 | Map element = texturesValues.get(i); 92 | String imageName = StringToResourcePackFilename.convert((String) element.get("name")); 93 | if (!imageName.contains(".png")) { 94 | if (!imageName.contains(".")) imageName += ".png"; 95 | else imageName.split("\\.")[0] += ".png"; 96 | } 97 | String base64Image = (String) element.get("source"); 98 | //So while there is an ID in blockbench it is not what it uses internally, what it uses internally is the ordered list of textures. Don't ask why. 99 | Integer id = i; 100 | textures.put(id, imageName.replace(".png", "")); 101 | base64Image = base64Image.split(",")[base64Image.split(",").length - 1]; 102 | if (!imageSize.containsKey(modelName + "/" + imageName)) try { 103 | ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(base64Image)); 104 | File imageFile = new File(MetadataHandler.PLUGIN.getDataFolder().getAbsolutePath() + File.separatorChar + "output" + File.separatorChar + "FreeMinecraftModels" + File.separatorChar + "assets" + File.separatorChar + "freeminecraftmodels" + File.separatorChar + "textures" + File.separatorChar + "entity" + File.separatorChar + modelName + File.separatorChar + imageName); 105 | FileUtils.writeByteArrayToFile(imageFile, inputStream.readAllBytes()); 106 | BufferedImage bufferedImage = ImageIO.read(imageFile); 107 | imageSize.put(modelName + "/" + imageName, bufferedImage.getWidth()); 108 | } catch (Exception ex) { 109 | Logger.warn("Failed to convert image " + imageName + " to its corresponding image file!"); 110 | } 111 | } 112 | 113 | //This parses the blocks 114 | List elementValues = (ArrayList) map.get("elements"); 115 | for (Map element : elementValues) { 116 | values.put((String) element.get("uuid"), element); 117 | } 118 | 119 | //This creates the bones and skeleton 120 | List outlinerValues = (ArrayList) map.get("outliner"); 121 | for (int i = 0; i < outlinerValues.size(); i++) { 122 | if (!(outlinerValues.get(i) instanceof Map)) { 123 | //Bukkit.getLogger().warning("WTF format for model name " + modelName + ": " + outlinerValues.get(i)); 124 | //I don't really know why Blockbench does this 125 | continue; 126 | } else { 127 | Map element = (Map) outlinerValues.get(i); 128 | outliner.put((String) element.get("uuid"), element); 129 | } 130 | } 131 | 132 | ID = modelName; 133 | skeletonBlueprint = new SkeletonBlueprint(projectResolution, outlinerValues, values, generateFileTextures(), modelName, null);//todo: pass path 134 | 135 | List animationList = (ArrayList) map.get("animations"); 136 | if (animationList != null) 137 | animationsBlueprint = new AnimationsBlueprint(animationList, modelName, skeletonBlueprint); 138 | convertedFileModels.put(modelName, this);//todo: id needs to be more unique, add folder directory into it 139 | } 140 | 141 | public static void shutdown() { 142 | convertedFileModels.clear(); 143 | imageSize.clear(); 144 | } 145 | 146 | private Map> generateFileTextures() { 147 | Map> texturesMap = new HashMap<>(); 148 | Map textureContents = new HashMap<>(); 149 | for (Integer key : textures.keySet()) 150 | textureContents.put("" + key, "freeminecraftmodels:entity/" + modelName + "/" + textures.get(key)); 151 | texturesMap.put("textures", textureContents); 152 | return texturesMap; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/HitboxBlueprint.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import com.magmaguy.magmacore.util.Logger; 4 | import com.magmaguy.magmacore.util.Round; 5 | import lombok.Getter; 6 | import org.bukkit.util.Vector; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public class HitboxBlueprint { 13 | private final String modelName; 14 | @Getter 15 | private Vector modelOffset = new Vector(); 16 | private BoneBlueprint parent = null; 17 | @Getter 18 | private double widthX; 19 | @Getter 20 | private double widthZ; 21 | @Getter 22 | private double height; 23 | 24 | public HitboxBlueprint(Map boneJSON, HashMap values, String modelName, BoneBlueprint parent) { 25 | this.parent = parent; 26 | this.modelName = modelName; 27 | ArrayList childrenValues = (ArrayList) boneJSON.get("children"); 28 | if (childrenValues.size() > 1) { 29 | Logger.warn("Model " + modelName + " has more than one value defining a hitbox! Only the first cube will be used to define the hitbox."); 30 | } 31 | if (childrenValues.isEmpty()) { 32 | Logger.warn("Model " + modelName + " has a hitbox bone but no hitbox cube! This means the hitbox won't be able to generate correctly!"); 33 | return; 34 | } 35 | if (childrenValues.get(0) instanceof String) { 36 | parseCube((Map) values.get(childrenValues.get(0))); 37 | } else { 38 | Logger.warn("Model " + modelName + " has an invalid hitbox! The hitbox bone should only have one cube in it defining its boundaries."); 39 | } 40 | 41 | } 42 | 43 | public Vector getModelOffset() { 44 | return modelOffset.clone(); 45 | } 46 | 47 | private void parseCube(Map cubeJSON) { 48 | double scaleFactor = .16D / 2.5; 49 | ArrayList fromList = (ArrayList) cubeJSON.get("from"); 50 | Vector from = new Vector(Round.fourDecimalPlaces(fromList.get(0) * scaleFactor), Round.fourDecimalPlaces(fromList.get(1) * scaleFactor), Round.fourDecimalPlaces(fromList.get(2) * scaleFactor)); 51 | ArrayList toList = (ArrayList) cubeJSON.get("to"); 52 | Vector to = new Vector(Round.fourDecimalPlaces(toList.get(0) * scaleFactor), Round.fourDecimalPlaces(toList.get(1) * scaleFactor), Round.fourDecimalPlaces(toList.get(2) * scaleFactor)); 53 | widthX = Math.abs(to.getX() - from.getX()); 54 | widthZ = Math.abs(to.getZ() - from.getZ()); 55 | height = Math.abs(from.getY() - to.getY()); 56 | modelOffset = new Vector(0, 0, 0); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/ImportsProcessor.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * At times model makers may want to distribute models without allowing the people receiving the models to edit these models. 7 | * This class allows model makers to put a bbmodel file into the imports folder, where it will be stripped of all non-relevant 8 | * JSON formatting vis-a-vis FreeMinecraftModels, providing an end result that can be used to generate resource packs and 9 | * can be used to read the necessary data for skeleton hierarchy, all without giving the source editable files used in 10 | * the BlockBench software. 11 | */ 12 | public class ImportsProcessor { 13 | public ImportsProcessor(File file) { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/Keyframe.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import com.magmaguy.freeminecraftmodels.utils.InterpolationType; 4 | import com.magmaguy.freeminecraftmodels.utils.TransformationType; 5 | import com.magmaguy.magmacore.util.Logger; 6 | import lombok.Getter; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public class Keyframe { 12 | @Getter 13 | private final TransformationType transformationType; 14 | @Getter 15 | private final int timeInTicks; 16 | @Getter 17 | private final InterpolationType interpolationType; 18 | @Getter 19 | private final float dataX; 20 | @Getter 21 | private final float dataY; 22 | @Getter 23 | private final float dataZ; 24 | 25 | public Keyframe(Object object, String modelName, String animationName) { 26 | Map data = (Map) object; 27 | transformationType = TransformationType.valueOf(((String) data.get("channel")).toUpperCase()); 28 | interpolationType = InterpolationType.valueOf(((String) data.get("interpolation")).toUpperCase()); 29 | timeInTicks = (int) (20 * (double) data.get("time")); 30 | Map dataPoints = ((List>) data.get("data_points")).get(0); 31 | 32 | dataX = tryParseFloat(dataPoints.get("x"), modelName, animationName); 33 | dataY = tryParseFloat(dataPoints.get("y"), modelName, animationName); 34 | dataZ = tryParseFloat(dataPoints.get("z"), modelName, animationName); 35 | } 36 | 37 | private float tryParseFloat(Object rawObject, String modelName, String animationName) { 38 | if (!(rawObject instanceof String rawValue)) return ((Double) rawObject).floatValue(); 39 | rawValue = rawValue.replaceAll("\\n", ""); 40 | if (rawValue.isEmpty()) return transformationType == TransformationType.SCALE ? 1f : 0f; 41 | try { 42 | return (float) Double.parseDouble(rawValue); 43 | } catch (Exception e) { 44 | Logger.warn("Failed to parse supposed number value " + rawValue + " in animation " + animationName + " for model " + modelName + "!"); 45 | return 0; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/dataconverter/SkeletonBlueprint.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.dataconverter; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class SkeletonBlueprint { 11 | //In BlockBench models are referred to by name for animations, and names are unique 12 | @Getter 13 | private final HashMap boneMap = new HashMap<>(); 14 | @Getter 15 | private final List mainModel = new ArrayList<>(); 16 | @Getter 17 | private String modelName; 18 | @Getter 19 | private HitboxBlueprint hitbox; 20 | 21 | public SkeletonBlueprint(double projectResolution, 22 | List outlinerJSON, 23 | HashMap values, 24 | Map> textureReferences, 25 | String modelName, 26 | String pathName) { 27 | this.modelName = modelName; 28 | 29 | //Create a root bone for everything 30 | BoneBlueprint rootBone = new BoneBlueprint(modelName, null, this); 31 | List rootChildren = new ArrayList<>(); 32 | 33 | for (int i = 0; i < outlinerJSON.size(); i++) { 34 | if (!(outlinerJSON.get(i) instanceof Map)) continue; 35 | Map bone = (Map) outlinerJSON.get(i); 36 | if (((String) bone.get("name")).equalsIgnoreCase("hitbox")) 37 | hitbox = new HitboxBlueprint(bone, values, modelName, null); 38 | else { 39 | rootChildren.add(new BoneBlueprint(projectResolution, bone, values, textureReferences, modelName, rootBone, this)); 40 | } 41 | } 42 | 43 | rootBone.setBoneBlueprintChildren(rootChildren); 44 | mainModel.add(rootBone); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/entities/ModelArmorStand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.entities; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.core.Bone; 4 | import com.magmaguy.freeminecraftmodels.customentity.core.RegisterModelEntity; 5 | import com.magmaguy.magmacore.util.VersionChecker; 6 | import org.bukkit.Color; 7 | import org.bukkit.Location; 8 | import org.bukkit.Material; 9 | import org.bukkit.NamespacedKey; 10 | import org.bukkit.entity.ArmorStand; 11 | import org.bukkit.entity.EntityType; 12 | import org.bukkit.inventory.ItemStack; 13 | import org.bukkit.inventory.meta.LeatherArmorMeta; 14 | 15 | public class ModelArmorStand { 16 | private ModelArmorStand() { 17 | } 18 | 19 | public static ArmorStand generate(Location location, Bone bone) { 20 | return (ArmorStand) location.getWorld().spawn(location, EntityType.ARMOR_STAND.getEntityClass(), 21 | entity -> applyFeatures((ArmorStand) entity, bone)); 22 | } 23 | 24 | private static void applyFeatures(ArmorStand armorStand, Bone bone) { 25 | armorStand.setGravity(false); 26 | armorStand.setMarker(true); 27 | armorStand.setPersistent(false); 28 | armorStand.setVisible(false); 29 | //This should only really be true for name tags and maybe other utility bones later on 30 | if (bone.getBoneBlueprint().getCubeBlueprintChildren().isEmpty() || 31 | !bone.getBoneBlueprint().isDisplayModel()) { 32 | RegisterModelEntity.registerModelArmorStand(armorStand, bone.getBoneBlueprint().getBoneName()); 33 | return; 34 | } 35 | ItemStack leatherHorseArmor = new ItemStack(Material.LEATHER_HORSE_ARMOR); 36 | LeatherArmorMeta itemMeta = (LeatherArmorMeta) leatherHorseArmor.getItemMeta(); 37 | itemMeta.setColor(Color.WHITE); 38 | if (bone.getBoneBlueprint().getModelID() != null) 39 | if (VersionChecker.serverVersionOlderThan(21, 4)) { 40 | itemMeta.setCustomModelData(Integer.valueOf(bone.getBoneBlueprint().getModelID())); 41 | } else { 42 | itemMeta.setItemModel(NamespacedKey.fromString(bone.getBoneBlueprint().getModelID())); 43 | } 44 | 45 | leatherHorseArmor.setItemMeta(itemMeta); 46 | armorStand.setHelmet(leatherHorseArmor); 47 | RegisterModelEntity.registerModelArmorStand(armorStand, bone.getBoneBlueprint().getBoneName()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/events/ResourcePackGenerationEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.events; 2 | 3 | import org.bukkit.event.Event; 4 | import org.bukkit.event.HandlerList; 5 | 6 | public class ResourcePackGenerationEvent extends Event { 7 | private static final HandlerList handlers = new HandlerList(); 8 | 9 | public ResourcePackGenerationEvent() { 10 | } 11 | 12 | public static HandlerList getHandlerList() { 13 | return handlers; 14 | } 15 | 16 | @Override 17 | public HandlerList getHandlers() { 18 | return handlers; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/listeners/EntityTeleportEvent.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.listeners; 2 | 3 | import com.magmaguy.freeminecraftmodels.customentity.DynamicEntity; 4 | import org.bukkit.entity.LivingEntity; 5 | import org.bukkit.event.EventHandler; 6 | import org.bukkit.event.EventPriority; 7 | import org.bukkit.event.Listener; 8 | 9 | public class EntityTeleportEvent implements Listener { 10 | @EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR) 11 | public void onEntityTeleport(org.bukkit.event.entity.EntityTeleportEvent event) { 12 | if (event.getEntity() instanceof LivingEntity livingEntity && DynamicEntity.isDynamicEntity(livingEntity)) { 13 | DynamicEntity dynamicEntity = DynamicEntity.getDynamicEntity(livingEntity); 14 | dynamicEntity.teleport(event.getTo()); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/packets/PacketArmorStand.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.packets; 2 | 3 | public class PacketArmorStand { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/packets/PushArmorStandState.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.packets; 2 | 3 | import com.magmaguy.easyminecraftgoals.NMSManager; 4 | import com.magmaguy.easyminecraftgoals.internal.PacketModelEntity; 5 | import org.bukkit.Location; 6 | import org.bukkit.entity.Player; 7 | 8 | public class PushArmorStandState { 9 | private PushArmorStandState() { 10 | } 11 | 12 | public static void push() { 13 | 14 | } 15 | 16 | public static void test(Player player, Location location) { 17 | PacketModelEntity packetArmorStandEntityInterface = NMSManager.getAdapter().createPacketDisplayEntity(player.getLocation()); 18 | packetArmorStandEntityInterface.initializeModel(location, 1); 19 | packetArmorStandEntityInterface.displayTo(player.getUniqueId()); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/thirdparty/BedrockChecker.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.thirdparty; 2 | 3 | import org.bukkit.Bukkit; 4 | import org.bukkit.entity.Player; 5 | 6 | public class BedrockChecker { 7 | private BedrockChecker() { 8 | } 9 | 10 | public static boolean isBedrock(Player player) { 11 | if (Bukkit.getPluginManager().isPluginEnabled("Floodgate")) 12 | return Floodgate.isBedrock(player); 13 | else if (Bukkit.getPluginManager().isPluginEnabled("Geyser-Spigot")) 14 | return Geyser.isBedrock(player); 15 | else return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/thirdparty/Floodgate.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.thirdparty; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.geysermc.floodgate.api.FloodgateApi; 5 | 6 | public class Floodgate { 7 | private Floodgate() { 8 | } 9 | 10 | public static boolean isBedrock(Player player) { 11 | return FloodgateApi.getInstance().isFloodgatePlayer(player.getUniqueId()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/thirdparty/Geyser.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.thirdparty; 2 | 3 | import org.bukkit.entity.Player; 4 | import org.geysermc.geyser.api.GeyserApi; 5 | import org.geysermc.geyser.api.connection.GeyserConnection; 6 | 7 | public class Geyser { 8 | 9 | public static boolean isBedrock(Player player) { 10 | GeyserConnection geyserConnection = GeyserApi.api().connectionByUuid(player.getUniqueId()); 11 | return geyserConnection != null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/ChunkHasher.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | import org.bukkit.Chunk; 4 | import org.bukkit.Location; 5 | 6 | import java.util.Objects; 7 | import java.util.UUID; 8 | import java.util.Vector; 9 | 10 | public class ChunkHasher { 11 | public static int hash(Chunk chunk) { 12 | return Objects.hash(chunk.getX(), chunk.getZ(), chunk.getWorld().getUID()); 13 | } 14 | 15 | //pseudo-chunks - prevent it form having to load the chunk 16 | public static int hash(int x, int z, UUID worldUUID) { 17 | return Objects.hash(x, z, worldUUID); 18 | } 19 | 20 | public static int hash(Location location) { 21 | return Objects.hash(location.getBlockX() >> 4, location.getBlockZ() >> 4, location.getWorld().getUID()); 22 | } 23 | 24 | public static Vector hash(double x, double z) { 25 | Vector vector = new Vector(2); 26 | vector.addElement(x); 27 | vector.addElement(z); 28 | return vector; 29 | } 30 | 31 | public static boolean isSameChunk(Chunk chunk, int hashedChunk) { 32 | return hash(chunk) == hashedChunk; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/ConfigurationLocation.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | import com.magmaguy.magmacore.util.Logger; 4 | import org.bukkit.Bukkit; 5 | import org.bukkit.Location; 6 | import org.bukkit.World; 7 | 8 | import java.util.HashSet; 9 | import java.util.Objects; 10 | import java.util.Set; 11 | 12 | public class ConfigurationLocation { 13 | private static final Set notLoadedWorldNames = new HashSet<>(); 14 | 15 | private ConfigurationLocation() { 16 | } 17 | 18 | /* 19 | Location format: worldname,x,y,z,pitch,yaw 20 | */ 21 | public static String deserialize(String worldName, double x, double y, double z, float pitch, float yaw) { 22 | return worldName + "," + x + "," + y + "," + z + "," + pitch + "," + yaw; 23 | } 24 | 25 | public static String deserialize(Location location) { 26 | return deserialize(Objects.requireNonNull(location.getWorld()).getName(), location.getX(), location.getY(), location.getZ(), location.getPitch(), location.getYaw()); 27 | } 28 | 29 | 30 | public static Location serialize(String locationString) { 31 | return serialize(locationString, false); 32 | } 33 | 34 | public static Location serialize(String locationString, boolean silent) { 35 | 36 | if (locationString == null) 37 | return null; 38 | 39 | World world = null; 40 | double x = 0; 41 | double y = 0; 42 | double z = 0; 43 | float yaw = 0; 44 | float pitch = 0; 45 | 46 | try { 47 | String locationOnlyString = locationString.split(":")[0]; 48 | String[] slicedString = locationOnlyString.split(","); 49 | 50 | if (slicedString.length == 6 || slicedString.length == 4) { 51 | 52 | world = Bukkit.getWorld(slicedString[0]); 53 | if (world == null && 54 | !slicedString[0].equalsIgnoreCase("same_as_boss") && 55 | !notLoadedWorldNames.contains(slicedString[0]) && !silent) { 56 | // if (!notLoadedWorldNames.isEmpty()) 57 | // Logger.warn(("Some NPCs/bosses don't have their world installed! If you need help setting things up, you can go to " + DiscordLinks.mainLink + " !"); 58 | // new InfoMessage("World " + slicedString[0] + " is not yet loaded! Entities that should spawn there have been queued."); 59 | notLoadedWorldNames.add(slicedString[0]); 60 | } 61 | x = Double.parseDouble(slicedString[1]); 62 | y = Double.parseDouble(slicedString[2]); 63 | z = Double.parseDouble(slicedString[3]); 64 | if (slicedString.length > 4) { 65 | yaw = Float.parseFloat(slicedString[4]); 66 | pitch = Float.parseFloat(slicedString[5]); 67 | } else { 68 | yaw = 0; 69 | pitch = 0; 70 | } 71 | } else if (slicedString.length == 5) { 72 | x = Double.parseDouble(slicedString[0]); 73 | y = Double.parseDouble(slicedString[1]); 74 | z = Double.parseDouble(slicedString[2]); 75 | yaw = Float.parseFloat(slicedString[3]); 76 | pitch = Float.parseFloat(slicedString[4]); 77 | } else throw new Exception(); 78 | } catch (Exception ex) { 79 | if (locationString.equals("null")) 80 | return null; 81 | Logger.warn("Attempted to deserialize an invalid location!"); 82 | Logger.warn("Expected location format: worldname,x,y,z,pitch,yaw"); 83 | Logger.warn("Actual location format: " + locationString); 84 | return null; 85 | } 86 | return new Location(world, x, y, z, yaw, pitch); 87 | } 88 | 89 | public static String worldName(String locationString) { 90 | String locationOnlyString = locationString.split(":")[0]; 91 | String[] slicedString = locationOnlyString.split(","); 92 | return slicedString[0]; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/CoordinateSystemConverter.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | import org.joml.Vector3f; 4 | 5 | /** 6 | * Handles transformations between Blockbench and Minecraft coordinate systems. 7 | *

8 | * This class provides a comprehensive solution for the coordinate system mismatch 9 | * between Blockbench models and Minecraft display entities. 10 | */ 11 | public class CoordinateSystemConverter { 12 | 13 | public static Vector3f convertBlockbenchAnimationToMinecraftRotation(Vector3f rotVec) { 14 | return new Vector3f( 15 | rotVec.x, // Invert X rotation 16 | rotVec.y, // Invert Y rotation 17 | rotVec.z // Keep Z as is 18 | ); 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/InterpolationType.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | public enum InterpolationType { 4 | LINEAR, 5 | CATMULLROM, 6 | SPHERICAL 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/LoopType.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | public enum LoopType { 4 | LOOP, 5 | ONCE, 6 | HOLD 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/StringToResourcePackFilename.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | import java.util.Locale; 4 | 5 | public class StringToResourcePackFilename { 6 | private StringToResourcePackFilename() { 7 | } 8 | 9 | public static String convert(String original) { 10 | return original.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]", "_"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/TransformationMatrix.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | import org.joml.Matrix4d; 4 | import org.joml.Vector3d; 5 | import org.joml.Vector3f; 6 | 7 | public class TransformationMatrix { 8 | Matrix4d replacementMatrix = new Matrix4d(); 9 | private double[][] matrix = new double[4][4]; 10 | 11 | public TransformationMatrix() { 12 | // Initialize with identity matrix 13 | resetToIdentityMatrix(); 14 | } 15 | 16 | public static void multiplyMatrices(TransformationMatrix firstMatrix, TransformationMatrix secondMatrix, TransformationMatrix resultMatrix) { 17 | resultMatrix.replacementMatrix = new Matrix4d(firstMatrix.replacementMatrix).mul(secondMatrix.replacementMatrix); 18 | // Assume resultMatrix is already initialized to the correct dimensions (4x4) 19 | for (int row = 0; row < 4; row++) { 20 | for (int col = 0; col < 4; col++) { 21 | resultMatrix.matrix[row][col] = 0; // Reset result matrix cell 22 | for (int i = 0; i < 4; i++) { 23 | resultMatrix.matrix[row][col] += firstMatrix.matrix[row][i] * secondMatrix.matrix[i][col]; 24 | } 25 | } 26 | } 27 | } 28 | 29 | public void resetToIdentityMatrix() { 30 | replacementMatrix.identity(); 31 | for (int i = 0; i < 4; i++) { 32 | for (int j = 0; j < 4; j++) { 33 | matrix[i][j] = (i == j) ? 1 : 0; 34 | } 35 | } 36 | } 37 | 38 | public void translateLocal(Vector3f vector) { 39 | translateLocal(vector.get(0), vector.get(1), vector.get(2)); 40 | } 41 | 42 | public void translateLocal(float x, float y, float z) { 43 | TransformationMatrix translationMatrix = new TransformationMatrix(); 44 | translationMatrix.matrix[0][3] = x; 45 | translationMatrix.matrix[1][3] = y; 46 | translationMatrix.matrix[2][3] = z; 47 | multiplyWith(translationMatrix); 48 | replacementMatrix.translateLocal(new Vector3d(x, y, z)); 49 | } 50 | 51 | public void scale(double x, double y, double z) { 52 | TransformationMatrix scaleMatrix = new TransformationMatrix(); 53 | scaleMatrix.matrix[0][0] = x; 54 | scaleMatrix.matrix[1][1] = y; 55 | scaleMatrix.matrix[2][2] = z; 56 | multiplyWith(scaleMatrix); 57 | } 58 | 59 | /** 60 | * Rotates the matrix by x y z coordinates. Must be in radian! 61 | */ 62 | public void rotateLocal(double x, double y, double z) { 63 | rotateZ(z); 64 | rotateY(y); 65 | rotateX(x); 66 | replacementMatrix.rotateLocalZ(z); 67 | replacementMatrix.rotateLocalY(y); 68 | replacementMatrix.rotateLocalX(x); 69 | } 70 | 71 | public void rotateAnimation(double x, double y, double z) { 72 | rotateZ(z); 73 | rotateY(y); 74 | rotateX(x); 75 | replacementMatrix.rotateLocalZ(z); 76 | replacementMatrix.rotateLocalY(y); 77 | replacementMatrix.rotateLocalX(x); 78 | } 79 | 80 | /** 81 | * Extracts the scale factors from the transformation matrix 82 | * 83 | * @return [scaleX, scaleY, scaleZ] 84 | */ 85 | public double[] getScale() { 86 | double[] scale = new double[3]; 87 | 88 | // Extract scale by calculating the magnitude of the basis vectors 89 | scale[0] = Math.sqrt( 90 | matrix[0][0] * matrix[0][0] + 91 | matrix[1][0] * matrix[1][0] + 92 | matrix[2][0] * matrix[2][0] 93 | ); 94 | 95 | scale[1] = Math.sqrt( 96 | matrix[0][1] * matrix[0][1] + 97 | matrix[1][1] * matrix[1][1] + 98 | matrix[2][1] * matrix[2][1] 99 | ); 100 | 101 | scale[2] = Math.sqrt( 102 | matrix[0][2] * matrix[0][2] + 103 | matrix[1][2] * matrix[1][2] + 104 | matrix[2][2] * matrix[2][2] 105 | ); 106 | 107 | Vector3d jomlScale = new Vector3d(); 108 | replacementMatrix.getScale(jomlScale); 109 | return scale; 110 | } 111 | 112 | public void rotateX(double angleRadians) { 113 | TransformationMatrix rotationMatrix = new TransformationMatrix(); 114 | rotationMatrix.matrix[1][1] = Math.cos(angleRadians); 115 | rotationMatrix.matrix[1][2] = -Math.sin(angleRadians); 116 | rotationMatrix.matrix[2][1] = Math.sin(angleRadians); 117 | rotationMatrix.matrix[2][2] = Math.cos(angleRadians); 118 | multiplyWith(rotationMatrix); 119 | } 120 | 121 | public void rotateY(double angleRadians) { 122 | TransformationMatrix rotationMatrix = new TransformationMatrix(); 123 | rotationMatrix.matrix[0][0] = Math.cos(angleRadians); 124 | rotationMatrix.matrix[0][2] = Math.sin(angleRadians); 125 | rotationMatrix.matrix[2][0] = -Math.sin(angleRadians); 126 | rotationMatrix.matrix[2][2] = Math.cos(angleRadians); 127 | multiplyWith(rotationMatrix); 128 | } 129 | 130 | public void rotateZ(double angleRadians) { 131 | TransformationMatrix rotationMatrix = new TransformationMatrix(); 132 | rotationMatrix.matrix[0][0] = Math.cos(angleRadians); 133 | rotationMatrix.matrix[0][1] = -Math.sin(angleRadians); 134 | rotationMatrix.matrix[1][0] = Math.sin(angleRadians); 135 | rotationMatrix.matrix[1][1] = Math.cos(angleRadians); 136 | multiplyWith(rotationMatrix); 137 | } 138 | 139 | private void multiplyWith(TransformationMatrix other) { 140 | double[][] result = new double[4][4]; 141 | for (int i = 0; i < 4; i++) { 142 | for (int j = 0; j < 4; j++) { 143 | for (int k = 0; k < 4; k++) { 144 | result[i][j] += this.matrix[i][k] * other.matrix[k][j]; 145 | } 146 | } 147 | } 148 | this.matrix = result; 149 | } 150 | 151 | /** 152 | * Extracts a xyz position 153 | * 154 | * @return [x, y, z] 155 | */ 156 | public double[] getTranslation() { 157 | // Extract translation components directly from the matrix 158 | return new double[]{matrix[0][3], matrix[1][3], matrix[2][3]}; 159 | } 160 | 161 | /** 162 | * Extracts a rotation in radians 163 | * 164 | * @return [x, y, z] 165 | */ 166 | public double[] getRotation() { 167 | // Assuming the rotation matrix is "pure" (no scaling) and follows XYZ order 168 | double[] rotation = new double[3]; 169 | 170 | // Yaw (rotation around Y axis) 171 | rotation[1] = Math.atan2(-matrix[2][0], Math.sqrt(matrix[0][0] * matrix[0][0] + matrix[1][0] * matrix[1][0])); 172 | 173 | // As a special case, if cos(yaw) is close to 0, use an alternative calculation 174 | if (Math.abs(matrix[2][0]) < 1e-6 && Math.abs(matrix[2][2]) < 1e-6) { 175 | // Pitch (rotation around X axis) 176 | rotation[0] = Math.atan2(matrix[1][2], matrix[1][1]); 177 | // Roll (rotation around Z axis) is indeterminate: set to 0 or use previous value 178 | rotation[2] = 0; 179 | } else { 180 | // Pitch (rotation around X axis) 181 | rotation[0] = Math.atan2(matrix[2][1], matrix[2][2]); 182 | // Roll (rotation around Z axis) 183 | rotation[2] = Math.atan2(matrix[1][0], matrix[0][0]); 184 | } 185 | 186 | return rotation; // Returns rotations in radians 187 | } 188 | 189 | public void resetRotation() { 190 | // Create a new identity matrix to reset rotation 191 | double[][] identityRotation = { 192 | {1, 0, 0, 0}, 193 | {0, 1, 0, 0}, 194 | {0, 0, 1, 0}, 195 | {0, 0, 0, 1} 196 | }; 197 | 198 | // Keep the translation values intact 199 | identityRotation[0][3] = matrix[0][3]; 200 | identityRotation[1][3] = matrix[1][3]; 201 | identityRotation[2][3] = matrix[2][3]; 202 | 203 | // Replace the current matrix's rotation part with the identity matrix 204 | for (int i = 0; i < 3; i++) { 205 | System.arraycopy(identityRotation[i], 0, matrix[i], 0, 3); 206 | } 207 | 208 | // Reset the rotation part of replacementMatrix using the JOML library 209 | replacementMatrix.m00(1).m01(0).m02(0); 210 | replacementMatrix.m10(0).m11(1).m12(0); 211 | replacementMatrix.m20(0).m21(0).m22(1); 212 | 213 | // The translation part remains unchanged in replacementMatrix 214 | } 215 | } -------------------------------------------------------------------------------- /src/main/java/com/magmaguy/freeminecraftmodels/utils/TransformationType.java: -------------------------------------------------------------------------------- 1 | package com.magmaguy.freeminecraftmodels.utils; 2 | 3 | public enum TransformationType { 4 | ROTATION, 5 | POSITION, 6 | SCALE 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/blocks.json: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | { 4 | "type": "directory", 5 | "source": "entity", 6 | "prefix": "entity/" 7 | } 8 | ] 9 | } -------------------------------------------------------------------------------- /src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "pack_format": 34, 4 | "description": "- FreeMinecraftModels -" 5 | } 6 | } -------------------------------------------------------------------------------- /src/main/resources/pack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagmaGuy/FreeMinecraftModels/2d43168dc963296306c9746d0a472d410f28bb89/src/main/resources/pack.png -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: FreeMinecraftModels 2 | version: '2.1.1-SNAPSHOT-1' 3 | main: com.magmaguy.freeminecraftmodels.FreeMinecraftModels 4 | api-version: 1.18 5 | prefix: FreeMinecraftModels 6 | load: STARTUP 7 | authors: [ MagmaGuy ] 8 | description: Welcome to the new age of custom Minecraft models. 9 | website: magmaguy.com 10 | 11 | commands: 12 | freeminecraftmodels: 13 | aliases: 14 | - fmm 15 | description: "Main command" 16 | logify: 17 | description: Posts current latest.log in mclo.gs, to make reporting bugs easier for admins. 18 | permissions: 19 | logify.*: 20 | description: Lets admins run the /logify command, which sends the current latest server log to mclo.gs. 21 | default: op --------------------------------------------------------------------------------