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